Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plugin: solana magic eden #41

Merged
merged 6 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions typescript/packages/plugins/solana-magiceden/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@goat-sdk/plugin-solana-magiceden",
"version": "0.0.1",
"files": ["dist/**/*", "README.md", "package.json"],
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "vitest run --passWithNoTests"
},
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"dependencies": {
"@goat-sdk/core": "workspace:*",
"@solana/web3.js": "catalog:",
"zod": "catalog:"
},
"peerDependencies": {
"@goat-sdk/core": "workspace:*"
},
"homepage": "https://ohmygoat.dev",
"repository": {
"type": "git",
"url": "git+https://github.com/goat-sdk/goat.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/goat-sdk/goat/issues"
},
"keywords": ["ai", "agents", "web3"]
}
1 change: 1 addition & 0 deletions typescript/packages/plugins/solana-magiceden/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./plugin";
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { SolanaWalletClient } from "@goat-sdk/core";
import { type Connection, VersionedTransaction } from "@solana/web3.js";
import type { z } from "zod";
import type { getBuyListingTransactionResponseSchema, getNftInfoParametersSchema } from "../parameters";
import { decompileVersionedTransactionToInstructions } from "../utils/decompileVersionedTransactionToInstructions";
import { getNftListings } from "./getNftListings";

export async function buyListing(
apiKey: string,
connection: Connection,
walletClient: SolanaWalletClient,
parameters: z.infer<typeof getNftInfoParametersSchema>,
) {
const nftInfo = await getNftListings(apiKey, parameters);

const queryParams = new URLSearchParams({
buyer: walletClient.getAddress(),
seller: nftInfo.seller,
tokenMint: parameters.mintHash,
tokenATA: nftInfo.tokenAddress,
price: nftInfo.price.toString(),
...(nftInfo.auctionHouse ? { auctionHouseAddress: nftInfo.auctionHouse } : {}),
});

let data: z.infer<typeof getBuyListingTransactionResponseSchema>;
try {
const response = await fetch(
`https://api-mainnet.magiceden.dev/v2/instructions/buy_now?${queryParams.toString()}`,
{
headers: {
"Content-Type": "application/json",
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
},
},
);

data = (await response.json()) as z.infer<typeof getBuyListingTransactionResponseSchema>;
} catch (error) {
throw new Error(`Failed to get buy listing transaction: ${error}`);
}

const versionedTransaction = VersionedTransaction.deserialize(Buffer.from(data.v0.tx.data));
const instructions = await decompileVersionedTransactionToInstructions(connection, versionedTransaction);
const lookupTableAddresses = versionedTransaction.message.addressTableLookups.map((lookup) => lookup.accountKey);

return { versionedTransaction, instructions, lookupTableAddresses };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { z } from "zod";
import type { getNftInfoParametersSchema, getNftInfoResponseSchema } from "../parameters";

export async function getNftListings(
apiKey: string | undefined,
parameters: z.infer<typeof getNftInfoParametersSchema>,
) {
let nftInfo: z.infer<typeof getNftInfoResponseSchema>;
try {
const response = await fetch(
`https://api-mainnet.magiceden.dev/v2/tokens/${parameters.mintHash}/listings
`,
{
headers: {
"Content-Type": "application/json",
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
},
},
);

nftInfo = (await response.json()) as z.infer<typeof getNftInfoResponseSchema>;
} catch (error) {
throw new Error(`Failed to get NFT listings: ${error}`);
}

return nftInfo[0];
}
45 changes: 45 additions & 0 deletions typescript/packages/plugins/solana-magiceden/src/parameters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { z } from "zod";

export const getNftInfoParametersSchema = z.object({
mintHash: z.string(),
});

export const getNftInfoResponseSchema = z.array(
z.object({
pdaAddress: z.string(),
auctionHouse: z.string().optional(),
tokenAddress: z.string(),
tokenMint: z.string().optional(),
seller: z.string(),
sellerReferral: z.string().optional(),
tokenSize: z.number().optional(),
price: z.number(),
priceInfo: z
.object({
solPrice: z.object({
rawAmount: z.string(),
address: z.string(),
decimals: z.number(),
}),
})
.optional(),
rarity: z.any().optional(),
extra: z.any().optional(),
expiry: z.number().optional(),
token: z.any().optional(),
listingSource: z.string().optional(),
}),
);

export const getBuyListingTransactionResponseSchema = z.object({
v0: z.object({
tx: z.object({
type: z.string(),
data: z.array(z.number()),
}),
txSigned: z.object({
type: z.string(),
data: z.array(z.number()),
}),
}),
});
12 changes: 12 additions & 0 deletions typescript/packages/plugins/solana-magiceden/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Plugin, SolanaWalletClient } from "@goat-sdk/core";
import type { Connection } from "@solana/web3.js";
import { getTools } from "./tools";

export function solanaMagicEden(params: { connection: Connection; apiKey: string }): Plugin<SolanaWalletClient> {
return {
name: "solana-magiceden",
supportsSmartWallets: () => false,
supportsChain: (chain) => chain.type === "solana",
getTools: async () => getTools(params),
};
}
28 changes: 28 additions & 0 deletions typescript/packages/plugins/solana-magiceden/src/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { SolanaWalletClient } from "@goat-sdk/core";

import type { DeferredTool } from "@goat-sdk/core";
import type { Connection } from "@solana/web3.js";
import { buyListing } from "./methods/buyListing";
import { getNftListings } from "./methods/getNftListings";
import { getNftInfoParametersSchema } from "./parameters";

export function getTools({
apiKey,
connection,
}: { apiKey: string; connection: Connection }): DeferredTool<SolanaWalletClient>[] {
const getNftListingsTool: DeferredTool<SolanaWalletClient> = {
name: "get_nft_listings",
description: "Gets information about a Solana NFT, from the Magic Eden API",
parameters: getNftInfoParametersSchema,
method: async (walletClient, parameters) => getNftListings(apiKey, parameters),
};

const buyListingTool: DeferredTool<SolanaWalletClient> = {
name: "get_buy_listing_transaction",
description: "Gets a transaction to buy a Solana NFT from a listing from the Magic Eden API",
parameters: getNftInfoParametersSchema,
method: async (walletClient, parameters) => buyListing(apiKey, connection, walletClient, parameters),
};

return [getNftListingsTool, buyListingTool];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { type DecompileArgs, TransactionMessage } from "@solana/web3.js";

import type { Connection, VersionedTransaction } from "@solana/web3.js";

export async function decompileVersionedTransactionToInstructions(
connection: Connection,
versionedTransaction: VersionedTransaction,
) {
const lookupTableAddresses = versionedTransaction.message.addressTableLookups.map((lookup) => lookup.accountKey);
const addressLookupTableAccounts = await Promise.all(
lookupTableAddresses.map((address) =>
connection.getAddressLookupTable(address).then((lookupTable) => lookupTable.value),
),
);
const nonNullAddressLookupTableAccounts = addressLookupTableAccounts.filter((lookupTable) => lookupTable != null);
const decompileArgs: DecompileArgs = {
addressLookupTableAccounts: nonNullAddressLookupTableAccounts,
};
return TransactionMessage.decompile(versionedTransaction.message, decompileArgs).instructions;
}
6 changes: 6 additions & 0 deletions typescript/packages/plugins/solana-magiceden/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../tsconfig.base.json",
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
6 changes: 6 additions & 0 deletions typescript/packages/plugins/solana-magiceden/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from "tsup";
import { treeShakableConfig } from "../../../tsup.config.base";

export default defineConfig({
...treeShakableConfig,
});
24 changes: 16 additions & 8 deletions typescript/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading