diff --git a/typescript/agentkit/README.md b/typescript/agentkit/README.md index e80216763..2ecda0e20 100644 --- a/typescript/agentkit/README.md +++ b/typescript/agentkit/README.md @@ -330,6 +330,47 @@ const agent = createReactAgent({
+Magic Eden + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
createLaunchpadCreates a new NFT launchpad on Magic Eden with customizable parameters like name, symbol, royalties, and mint stages.
publishLaunchpadPublishes a previously created Solana launchpad, making it visible to the public on Magic Eden.
updateLaunchpadUpdates an existing NFT launchpad's parameters such as name, description, royalties, or mint stages.
listNftLists an NFT for sale on the Magic Eden marketplace with specified price and optional expiration.
cancelListingCancels an existing NFT listing on the Magic Eden marketplace.
makeItemOfferMakes an offer on an NFT listed on Magic Eden with specified price and optional expiration.
takeItemOfferAccepts an existing offer on an NFT listed on Magic Eden.
cancelItemOfferCancels an existing offer on an NFT on the Magic Eden marketplace.
buyBuys one or more NFTs directly from the Magic Eden marketplace at listed prices.
+
+
Pyth diff --git a/typescript/agentkit/package.json b/typescript/agentkit/package.json index cdbc663cf..34f2e1a46 100644 --- a/typescript/agentkit/package.json +++ b/typescript/agentkit/package.json @@ -43,6 +43,7 @@ "@alloralabs/allora-sdk": "^0.1.0", "@coinbase/coinbase-sdk": "^0.20.0", "@jup-ag/api": "^6.0.39", + "@magiceden/magiceden-sdk": "1.0.0-beta.3", "@privy-io/public-api": "^2.18.5", "@privy-io/server-auth": "^1.18.4", "@solana/spl-token": "^0.4.12", diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 6f2aeab75..19736788e 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -26,3 +26,4 @@ export * from "./wow"; export * from "./allora"; export * from "./flaunch"; export * from "./onramp"; +export * from "./magiceden"; diff --git a/typescript/agentkit/src/action-providers/magiceden/README b/typescript/agentkit/src/action-providers/magiceden/README new file mode 100644 index 000000000..91a546398 --- /dev/null +++ b/typescript/agentkit/src/action-providers/magiceden/README @@ -0,0 +1,104 @@ +# Magic Eden Action Provider + +This directory contains the **MagicEdenActionProvider** implementation, which provides actions to interact with the **Magic Eden NFT marketplace** across multiple blockchains (Solana and EVM chains). + +## Directory Structure + +``` +magiceden/ +├── magicEdenActionProvider.ts # Main provider implementation +├── magicEdenActionProvider.test.ts # Test file for provider +├── schemas.ts # Action parameter schemas +├── utils.ts # Utility functions and network mappings +├── index.ts # Main exports +└── README.md # This file +``` + +## Actions + +### NFT Trading +- `buy`: Buy one or more NFTs at listed prices +- `listNft`: List an NFT for sale with specified price +- `cancelListing`: Cancel an existing NFT listing +- `makeItemOffer`: Make an offer on a listed NFT +- `takeItemOffer`: Accept an existing offer on your NFT +- `cancelItemOffer`: Cancel an existing offer you made + +### Launchpad Management +- `createLaunchpad`: Create a new NFT launchpad with customizable parameters +- `publishLaunchpad`: Publish a Solana launchpad (required after creation) +- `updateLaunchpad`: Update an existing launchpad's parameters + +## Network Support + +The Magic Eden provider supports the following networks: + +### EVM Networks +- Ethereum +- Base +- Polygon +- Sei +- Arbitrum +- ApeChain +- BeraChain +- Monad Testnet +- Abstract + +### Solana Networks +- Solana Mainnet + +## Configuration + +The provider requires the following configuration: + +```typescript +interface MagicEdenActionProviderConfig { + apiKey?: string; // Magic Eden API key + networkId?: string; // Network identifier + privateKey?: string; // Wallet private key + rpcUrl?: string; // RPC URL (required for Solana) +} +``` + +Configuration can be provided via: +- Environment variable: `MAGICEDEN_API_KEY` +- Provider configuration: `apiKey` parameter + +## Usage Examples + +### Listing an NFT +```typescript +const result = await provider.listNft({ + token: "0x1234...5678:1", // EVM: contract:tokenId + price: "1000000000" // Price in wei/lamports +}); +``` + +### Making an Offer +```typescript +const result = await provider.makeItemOffer({ + token: "0x1234...5678:1", + price: "900000000" +}); +``` + +### Creating a Launchpad +```typescript +const result = await provider.createLaunchpad({ + chain: "solana", + name: "My Collection", + symbol: "MYCOL", + // ... other parameters +}); +``` + +For complete examples, see the [magicEdenActionProvider.test.ts](magicEdenActionProvider.test.ts) file or the README file in the [Magic Eden SDK GitHub](https://github.com/magiceden/magiceden-sdk). + +## Notes + +- For Solana operations, a valid RPC URL must be provided +- EVM operations support batch transactions for multiple NFTs +- Launchpad creation on Solana requires a separate publish step +- All prices should be in the chain's smallest unit (wei/lamports) + +For more information on the Magic Eden API, visit [Magic Eden Developer Documentation](https://docs.magiceden.io//) or visit the [Magic Eden SDK GitHub](https://github.com/magiceden/magiceden-sdk). \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/magiceden/index.ts b/typescript/agentkit/src/action-providers/magiceden/index.ts new file mode 100644 index 000000000..43b64051b --- /dev/null +++ b/typescript/agentkit/src/action-providers/magiceden/index.ts @@ -0,0 +1 @@ +export * from "./magicEdenActionProvider"; \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/magiceden/magicEdenActionProvider.test.ts b/typescript/agentkit/src/action-providers/magiceden/magicEdenActionProvider.test.ts new file mode 100644 index 000000000..00531ecf3 --- /dev/null +++ b/typescript/agentkit/src/action-providers/magiceden/magicEdenActionProvider.test.ts @@ -0,0 +1,748 @@ +import { + Blockchain, + EvmBuyParams, + EvmCreateLaunchpadParams, + MagicEdenSDK, + SolanaCreateLaunchpadParams, + EvmProtocolType, + SolProtocolType, + MintStageKind, + EvmUpdateLaunchpadParams, + SolanaUpdateLaunchpadParams, + EvmListParams, + SolanaListParams, + EvmCancelListingParams, + SolanaCancelListingParams, + EvmMakeItemOfferParams, + SolanaMakeItemOfferParams, + EvmTakeItemOfferParams, + SolanaTakeItemOfferParams, + EvmCancelItemOfferParams, + SolanaCancelItemOfferParams, +} from "@magiceden/magiceden-sdk"; +import { magicEdenActionProvider } from "./magicEdenActionProvider"; +import { Network } from "../../network"; + +jest.mock("@magiceden/magiceden-sdk", () => ({ + ...jest.requireActual("@magiceden/magiceden-sdk"), + MagicEdenSDK: { + v1: { + createSolanaKeypairClient: jest.fn(), + createViemEvmClient: jest.fn(), + }, + }, +})); + +describe("MagicEden Action Provider", () => { + const MOCK_API_KEY = "test-api-key"; + const MOCK_SOLANA_PRIVATE_KEY = + "3CCF7x1YckEPTx8QnwQdtUYcABtmQCDkd26UpJBNNfnSnsko6b4uEKTn44FvdL9yKPHkGLjco6yPgaFL79szmV7c"; + const MOCK_EVM_PRIVATE_KEY = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + const MOCK_CONTRACT = "0x1234567890123456789012345678901234567890"; + const MOCK_TOKEN_ID = "1"; + const MOCK_RPC_URL = "https://api.mainnet-beta.solana.com"; + + let actionProvider: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock default client implementations + (MagicEdenSDK.v1.createSolanaKeypairClient as jest.Mock).mockImplementation(() => ({ + nft: { + buy: jest.fn(), + }, + })); + + (MagicEdenSDK.v1.createViemEvmClient as jest.Mock).mockImplementation(() => ({ + nft: { + buy: jest.fn(), + }, + })); + }); + + describe("createLaunchpad", () => { + it("should successfully create a launchpad on Solana", async () => { + const mockResponse = [{ status: "success", txId: "solana-tx-id" }]; + const mockCreateLaunchpad = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createSolanaKeypairClient as jest.Mock).mockImplementation(() => ({ + nft: { + createLaunchpad: mockCreateLaunchpad, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_SOLANA_PRIVATE_KEY, + networkId: "solana-mainnet", + rpcUrl: MOCK_RPC_URL, + }); + + const args: SolanaCreateLaunchpadParams = { + chain: Blockchain.SOLANA, + protocol: SolProtocolType.METAPLEX_CORE, + creator: "DRnGhQzbhxB8FsKdkTZRNqkJhGzPFhxGtxVXkqgXVGZv", + name: "TestCollection", + symbol: "TEST", + description: "This is a test collection created with the Magic Eden self-serve API", + nftMetadataUrl: + "https://bafybeic3rs6wmnnhqachwxsiizlblignek6aitc5b3ooenhhtkez3onmwu.ipfs.w3s.link", + royaltyBps: 500, + royaltyRecipients: [ + { + address: "DRnGhQzbhxB8FsKdkTZRNqkJhGzPFhxGtxVXkqgXVGZv", + share: 100, + }, + ], + payoutRecipient: "DRnGhQzbhxB8FsKdkTZRNqkJhGzPFhxGtxVXkqgXVGZv", + social: { + discordUrl: "https://discord.gg/magiceden", + twitterUsername: "magiceden", + externalUrl: "https://magiceden.io", + }, + mintStages: { + maxSupply: 10000, + stages: [ + { + kind: MintStageKind.Public, + price: { + currency: { + chain: Blockchain.SOLANA, + assetId: "So11111111111111111111111111111111111111112", + }, + raw: "1", + }, + startTime: "2025-03-28T00:00:00.000Z", + endTime: "2030-03-30T00:00:00.000Z", + walletLimit: 10, + }, + ], + }, + isOpenEdition: false, + }; + + const response = await actionProvider.createLaunchpad(args); + + expect(mockCreateLaunchpad).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'createLaunchpad' action.\nTransactions: [solana-tx-id]"); + }); + + it("should successfully create a launchpad on EVM", async () => { + const mockResponse = [{ status: "success", txId: "evm-tx-id" }]; + const mockCreateLaunchpad = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createViemEvmClient as jest.Mock).mockImplementation(() => ({ + nft: { + createLaunchpad: mockCreateLaunchpad, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_EVM_PRIVATE_KEY, + networkId: "base-mainnet", + }); + + const args: EvmCreateLaunchpadParams = { + chain: Blockchain.BASE, + protocol: EvmProtocolType.ERC1155, + creator: "0x1234567890123456789012345678901234567890", + name: "TestCollection", + symbol: "TEST", + description: "This is a test collection created with the Magic Eden self-serve API", + nftMetadataUrl: + "https://bafybeic3rs6wmnnhqachwxsiizlblignek6aitc5b3ooenhhtkez3onmwu.ipfs.w3s.link", + royaltyBps: 500, + royaltyRecipients: [ + { + address: "0x1234567890123456789012345678901234567890", + share: 100, + }, + ], + payoutRecipient: "0x1234567890123456789012345678901234567890", + mintStages: { + tokenId: 0, + maxSupply: 10000, + walletLimit: 10, + stages: [ + { + kind: MintStageKind.Public, + price: { + currency: { + chain: Blockchain.BASE, + assetId: "0x0000000000000000000000000000000000000000", + }, + raw: "1", + }, + startTime: "2025-03-28T00:00:00.000Z", + endTime: "2030-03-30T00:00:00.000Z", + walletLimit: 10, + }, + ], + }, + }; + + const response = await actionProvider.createLaunchpad(args); + + expect(mockCreateLaunchpad).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'createLaunchpad' action.\nTransactions: [evm-tx-id]"); + }); + }); + + describe("updateLaunchpad", () => { + it("should successfully update a launchpad on Solana", async () => { + const mockResponse = [{ status: "success", txId: "solana-tx-id" }]; + const mockUpdateLaunchpad = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createSolanaKeypairClient as jest.Mock).mockImplementation(() => ({ + nft: { + updateLaunchpad: mockUpdateLaunchpad, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_SOLANA_PRIVATE_KEY, + networkId: "solana-mainnet", + rpcUrl: MOCK_RPC_URL, + }); + + const args: SolanaUpdateLaunchpadParams = { + chain: Blockchain.SOLANA, + protocol: SolProtocolType.METAPLEX_CORE, + collectionId: "collection123", + owner: "DRnGhQzbhxB8FsKdkTZRNqkJhGzPFhxGtxVXkqgXVGZv", + payer: "DRnGhQzbhxB8FsKdkTZRNqkJhGzPFhxGtxVXkqgXVGZv", + symbol: "TEST2", + newSymbol: "TEST", + candyMachineId: "candy123", + name: "TestCollection", + payoutRecipient: "DRnGhQzbhxB8FsKdkTZRNqkJhGzPFhxGtxVXkqgXVGZv", + royaltyRecipients: [ + { + address: "DRnGhQzbhxB8FsKdkTZRNqkJhGzPFhxGtxVXkqgXVGZv", + share: 100, + }, + ], + }; + + const response = await actionProvider.updateLaunchpad(args); + + expect(mockUpdateLaunchpad).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'updateLaunchpad' action.\nTransactions: [solana-tx-id]"); + }); + + it("should successfully update a launchpad on EVM", async () => { + const mockResponse = [{ status: "success", txId: "evm-tx-id" }]; + const mockUpdateLaunchpad = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createViemEvmClient as jest.Mock).mockImplementation(() => ({ + nft: { + updateLaunchpad: mockUpdateLaunchpad, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_EVM_PRIVATE_KEY, + networkId: "base-mainnet", + }); + + const args: EvmUpdateLaunchpadParams = { + chain: Blockchain.BASE, + protocol: EvmProtocolType.ERC1155, + tokenId: 0, + collectionId: "0x949de1b4d4cc4a8e63b7565b6dc525d8eb5dd15a", + owner: "0x1234567890123456789012345678901234567890", + name: "TestCollection2", + payoutRecipient: "0x1234567890123456789012345678901234567890", + }; + + const response = await actionProvider.updateLaunchpad(args); + + expect(mockUpdateLaunchpad).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'updateLaunchpad' action.\nTransactions: [evm-tx-id]"); + }); + }); + + describe("listNft", () => { + it("should successfully list an NFT on Solana", async () => { + const mockResponse = [{ status: "success", txId: "solana-tx-id" }]; + const mockList = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createSolanaKeypairClient as jest.Mock).mockImplementation(() => ({ + nft: { + list: mockList, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_SOLANA_PRIVATE_KEY, + networkId: "solana-mainnet", + rpcUrl: MOCK_RPC_URL, + }); + + const args: SolanaListParams = { + token: "7um9nU7CDhss1fepFMRpjHhB3qm7exfQf47cdbRSUGuS", + price: "1000000000", // 1 SOL in lamports + }; + + const response = await actionProvider.listNft(args); + + expect(mockList).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'listNft' action.\nTransactions: [solana-tx-id]"); + }); + + it("should successfully list an NFT on EVM", async () => { + const mockResponse = [{ status: "success", txId: "evm-tx-id" }]; + const mockList = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createViemEvmClient as jest.Mock).mockImplementation(() => ({ + nft: { + list: mockList, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_EVM_PRIVATE_KEY, + networkId: "base-mainnet", + }); + + const args: EvmListParams = { + chain: Blockchain.BASE, + params: [ + { + token: "0x949de1b4d4cc4a8e63b7565b6dc525d8eb5dd15a:0", + price: "10000000012", + }, + ], + }; + + const response = await actionProvider.listNft(args); + + expect(mockList).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'listNft' action.\nTransactions: [evm-tx-id]"); + }); + }); + + describe("cancelListing", () => { + it("should successfully cancel a listing on Solana", async () => { + const mockResponse = [{ status: "success", txId: "solana-tx-id" }]; + const mockCancelListing = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createSolanaKeypairClient as jest.Mock).mockImplementation(() => ({ + nft: { + cancelListing: mockCancelListing, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_SOLANA_PRIVATE_KEY, + networkId: "solana-mainnet", + rpcUrl: MOCK_RPC_URL, + }); + + const args: SolanaCancelListingParams = { + token: "7um9nU7CDhss1fepFMRpjHhB3qm7exfQf47cdbRSUGuS", + price: "1000000000", // 1 SOL in lamports + }; + + const response = await actionProvider.cancelListing(args); + + expect(mockCancelListing).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'cancelListing' action.\nTransactions: [solana-tx-id]"); + }); + + it("should successfully cancel a listing on EVM", async () => { + const mockResponse = [{ status: "success", txId: "evm-tx-id" }]; + const mockCancelListing = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createViemEvmClient as jest.Mock).mockImplementation(() => ({ + nft: { + cancelListing: mockCancelListing, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_EVM_PRIVATE_KEY, + networkId: "base-mainnet", + }); + + const args: EvmCancelListingParams = { + chain: Blockchain.BASE, + orderIds: ["0xc34124b0276f92ca985c2b7e25e9a5c3164c5aa45a2fe1ff1ac6c33b4665649c"], + }; + + const response = await actionProvider.cancelListing(args); + + expect(mockCancelListing).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'cancelListing' action.\nTransactions: [evm-tx-id]"); + }); + }); + + describe("makeItemOffer", () => { + it("should successfully make an offer on Solana", async () => { + const mockResponse = [{ status: "success", txId: "solana-tx-id" }]; + const mockMakeOffer = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createSolanaKeypairClient as jest.Mock).mockImplementation(() => ({ + nft: { + makeItemOffer: mockMakeOffer, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_SOLANA_PRIVATE_KEY, + networkId: "solana-mainnet", + rpcUrl: MOCK_RPC_URL, + }); + + const args: SolanaMakeItemOfferParams = { + token: "7YCrxt8Ux9dym832BKLDQQWJYZ2uziXgbF6cYfZaChdP", + price: "900000", // 0.0009 SOL in lamports + }; + + const response = await actionProvider.makeItemOffer(args); + + expect(mockMakeOffer).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'makeItemOffer' action.\nTransactions: [solana-tx-id]"); + }); + + it("should successfully make an offer on EVM", async () => { + const mockResponse = [{ status: "success", txId: "evm-tx-id" }]; + const mockMakeOffer = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createViemEvmClient as jest.Mock).mockImplementation(() => ({ + nft: { + makeItemOffer: mockMakeOffer, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_EVM_PRIVATE_KEY, + networkId: "base-mainnet", + }); + + const args: EvmMakeItemOfferParams = { + chain: Blockchain.BASE, + params: [ + { + token: "0x1195cf65f83b3a5768f3c496d3a05ad6412c64b7:304163", + price: "9000", + }, + ], + }; + + const response = await actionProvider.makeItemOffer(args); + + expect(mockMakeOffer).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'makeItemOffer' action.\nTransactions: [evm-tx-id]"); + }); + }); + + describe("takeItemOffer", () => { + it("should successfully take an offer on Solana", async () => { + const mockResponse = [{ status: "success", txId: "solana-tx-id" }]; + const mockTakeOffer = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createSolanaKeypairClient as jest.Mock).mockImplementation(() => ({ + nft: { + takeItemOffer: mockTakeOffer, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_SOLANA_PRIVATE_KEY, + networkId: "solana-mainnet", + rpcUrl: MOCK_RPC_URL, + }); + + const args: SolanaTakeItemOfferParams = { + token: "7um9nU7CDhss1fepFMRpjHhB3qm7exfQf47cdbRSUGuS", + buyer: "4H2bigFBsMoTAwkn7THDnThiRQuLCrFDGUWHf4YDpf14", + price: "1500000", // Original offer price + newPrice: "1000000", // Accepted price + }; + + const response = await actionProvider.takeItemOffer(args); + + expect(mockTakeOffer).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'takeItemOffer' action.\nTransactions: [solana-tx-id]"); + }); + + it("should successfully take an offer on EVM", async () => { + const mockResponse = [{ status: "success", txId: "evm-tx-id" }]; + const mockTakeOffer = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createViemEvmClient as jest.Mock).mockImplementation(() => ({ + nft: { + takeItemOffer: mockTakeOffer, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_EVM_PRIVATE_KEY, + networkId: "base-mainnet", + }); + + const args: EvmTakeItemOfferParams = { + chain: Blockchain.BASE, + items: [ + { + token: "0x949de1b4d4cc4a8e63b7565b6dc525d8eb5dd15a:0", + quantity: 1, + orderId: "0x18fc51e19bc96bc07b9bdd804eb055a691e46e3cd2c37a5d7e53daedebae70c4", + }, + ], + }; + + const response = await actionProvider.takeItemOffer(args); + + expect(mockTakeOffer).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'takeItemOffer' action.\nTransactions: [evm-tx-id]"); + }); + }); + + describe("cancelItemOffer", () => { + it("should successfully cancel an offer on Solana", async () => { + const mockResponse = [{ status: "success", txId: "solana-tx-id" }]; + const mockCancelOffer = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createSolanaKeypairClient as jest.Mock).mockImplementation(() => ({ + nft: { + cancelItemOffer: mockCancelOffer, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_SOLANA_PRIVATE_KEY, + networkId: "solana-mainnet", + rpcUrl: MOCK_RPC_URL, + }); + + const args: SolanaCancelItemOfferParams = { + token: "7YCrxt8Ux9dym832BKLDQQWJYZ2uziXgbF6cYfZaChdP", + price: "900000", // 0.0009 SOL in lamports + }; + + const response = await actionProvider.cancelItemOffer(args); + + expect(mockCancelOffer).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'cancelItemOffer' action.\nTransactions: [solana-tx-id]"); + }); + + it("should successfully cancel an offer on EVM", async () => { + const mockResponse = [{ status: "success", txId: "evm-tx-id" }]; + const mockCancelOffer = jest.fn().mockResolvedValue(mockResponse); + + (MagicEdenSDK.v1.createViemEvmClient as jest.Mock).mockImplementation(() => ({ + nft: { + cancelItemOffer: mockCancelOffer, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_EVM_PRIVATE_KEY, + networkId: "base-mainnet", + }); + + const args: EvmCancelItemOfferParams = { + chain: Blockchain.BASE, + orderIds: ["0x18fc51e19bc96bc07b9bdd804eb055a691e46e3cd2c37a5d7e53daedebae70c4"], + }; + + const response = await actionProvider.cancelItemOffer(args); + + expect(mockCancelOffer).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'cancelItemOffer' action.\nTransactions: [evm-tx-id]"); + }); + }); + + describe("buy", () => { + it("should successfully buy an NFT on Solana", async () => { + const mockBuyResponse = [{ status: "success", txId: "solana-tx-id" }]; + const mockBuy = jest.fn().mockResolvedValue(mockBuyResponse); + + (MagicEdenSDK.v1.createSolanaKeypairClient as jest.Mock).mockImplementation(() => ({ + nft: { + buy: mockBuy, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_SOLANA_PRIVATE_KEY, + networkId: "solana-mainnet", + rpcUrl: MOCK_RPC_URL, + }); + + const args = { + token: "solana-mint-address", + seller: "seller-wallet-address", + price: "0.5", + }; + + const response = await actionProvider.buy(args); + + expect(mockBuy).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'buy' action.\nTransactions: [solana-tx-id]"); + }); + + it("should successfully buy an NFT on EVM", async () => { + const mockBuyResponse = [{ status: "success", txId: "evm-tx-id" }]; + const mockBuy = jest.fn().mockResolvedValue(mockBuyResponse); + + (MagicEdenSDK.v1.createViemEvmClient as jest.Mock).mockImplementation(() => ({ + nft: { + buy: mockBuy, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_EVM_PRIVATE_KEY, + networkId: "base-mainnet", + }); + + const args: EvmBuyParams = { + chain: Blockchain.BASE, + items: [ + { + token: `${MOCK_CONTRACT}:${MOCK_TOKEN_ID}`, + quantity: 1, + }, + ], + }; + + const response = await actionProvider.buy(args); + + expect(mockBuy).toHaveBeenCalledWith(args); + expect(response).toBe("Successfully executed MagicEden 'buy' action.\nTransactions: [evm-tx-id]"); + }); + + it("should handle failed transactions", async () => { + const mockBuyResponse = [{ status: "failed", error: "Insufficient funds" }]; + const mockBuy = jest.fn().mockResolvedValue(mockBuyResponse); + + (MagicEdenSDK.v1.createViemEvmClient as jest.Mock).mockImplementation(() => ({ + nft: { + buy: mockBuy, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_EVM_PRIVATE_KEY, + networkId: "base-mainnet", + }); + + const args: EvmBuyParams = { + chain: Blockchain.BASE, + items: [ + { + token: `${MOCK_CONTRACT}:${MOCK_TOKEN_ID}`, + quantity: 1, + }, + ], + }; + + const response = await actionProvider.buy(args); + + expect(mockBuy).toHaveBeenCalledWith(args); + expect(response).toBe("Failed to execute MagicEden 'buy' action: Insufficient funds"); + }); + + it("should handle errors during buy operation", async () => { + const error = new Error("API error"); + const mockBuy = jest.fn().mockRejectedValue(error); + + (MagicEdenSDK.v1.createViemEvmClient as jest.Mock).mockImplementation(() => ({ + nft: { + buy: mockBuy, + }, + })); + + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_EVM_PRIVATE_KEY, + networkId: "base-mainnet", + }); + + const args: EvmBuyParams = { + chain: Blockchain.BASE, + items: [ + { + token: `${MOCK_CONTRACT}:${MOCK_TOKEN_ID}`, + quantity: 1, + }, + ], + }; + + const response = await actionProvider.buy(args); + + expect(mockBuy).toHaveBeenCalledWith(args); + expect(response).toBe("Error executing MagicEden 'buy' action: Error: API error"); + }); + }); + + describe("supportsNetwork", () => { + it("should return true for supported EVM networks", () => { + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_EVM_PRIVATE_KEY, + networkId: "base-mainnet", + }); + + const baseNetwork: Network = { + protocolFamily: "evm", + networkId: "base-mainnet", + chainId: "8453", + }; + + expect(actionProvider.supportsNetwork(baseNetwork)).toBe(true); + }); + + it("should return true for supported Solana networks", () => { + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_SOLANA_PRIVATE_KEY, + networkId: "solana-mainnet", + rpcUrl: MOCK_RPC_URL, + }); + + const solanaNetwork: Network = { + protocolFamily: "svm", + networkId: "solana-mainnet", + }; + + expect(actionProvider.supportsNetwork(solanaNetwork)).toBe(true); + }); + + it("should return false for unsupported networks", () => { + actionProvider = magicEdenActionProvider({ + apiKey: MOCK_API_KEY, + privateKey: MOCK_EVM_PRIVATE_KEY, + networkId: "base-mainnet", + }); + + const unsupportedNetwork: Network = { + protocolFamily: "evm", + networkId: "base_sepolia", + chainId: "84532", + }; + + expect(actionProvider.supportsNetwork(unsupportedNetwork)).toBe(false); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/magiceden/magicEdenActionProvider.ts b/typescript/agentkit/src/action-providers/magiceden/magicEdenActionProvider.ts new file mode 100644 index 000000000..aca93d234 --- /dev/null +++ b/typescript/agentkit/src/action-providers/magiceden/magicEdenActionProvider.ts @@ -0,0 +1,682 @@ +import { ActionProvider } from "../actionProvider"; +import { CreateAction } from "../actionDecorator"; +import { Network } from "../../network"; +import { + Blockchain, + MagicEdenClient, + MagicEdenSDK, + BuyParams, + SolanaNftService, + EvmNftService, + EvmBuyParams, + SolanaBuyParams, + TransactionResponse, + SolanaCreateLaunchpadParams, + CreateLaunchpadParams, + ListParams, + CancelListingParams, + MakeItemOfferParams, + TakeItemOfferParams, + CancelItemOfferParams, + EvmCancelItemOfferParams, + SolanaCancelItemOfferParams, + SolanaTakeItemOfferParams, + EvmTakeItemOfferParams, + SolanaMakeItemOfferParams, + EvmMakeItemOfferParams, + SolanaCancelListingParams, + EvmCancelListingParams, + SolanaListParams, + EvmListParams, + SolanaUpdateLaunchpadParams, + EvmUpdateLaunchpadParams, + UpdateLaunchpadParams, + EvmCreateLaunchpadParams, + PublishLaunchpadParams, + SolanaPublishLaunchpadParams, + SOL_MAX_NAME_LENGTH, + MIN_ROYALTY_BPS, + MAX_ROYALTY_BPS, + MAX_SYMBOL_LENGTH, + MAX_NAME_LENGTH, + OperationResponse, +} from "@magiceden/magiceden-sdk"; +import { getMagicEdenChainFromNetworkId, isSupportedNetwork } from "./utils"; +import { Keypair } from "@solana/web3.js"; +import bs58 from "bs58"; + +/** + * Configuration options for the MagicEdenActionProvider. + */ +export interface MagicEdenActionProviderConfig { + /** + * The Magic Eden API key. + */ + apiKey?: string; + + /** + * The network ID to use for the MagicEdenActionProvider. + */ + networkId?: string; + + /** + * The private key to use for the MagicEdenActionProvider. + */ + privateKey?: string; + + /** + * The RPC URL to use for the MagicEdenActionProvider (if Solana network). + */ + rpcUrl?: string; +} + +/** + * MagicEdenActionProvider provides functionality to interact with Magic Eden's marketplace. + */ +export class MagicEdenActionProvider extends ActionProvider { + private readonly solClient?: MagicEdenClient; + private readonly evmClient?: MagicEdenClient; + + /** + * Constructor for the MagicEdenActionProvider class. + */ + constructor(config: MagicEdenActionProviderConfig) { + super("magicEden", []); + + const apiKey = config.apiKey || process.env.MAGICEDEN_API_KEY; + if (!apiKey) { + throw new Error("MAGICEDEN_API_KEY is not configured."); + } + + const chain = getMagicEdenChainFromNetworkId(config.networkId || "base-mainnet"); + switch (chain) { + case Blockchain.SOLANA: + this.solClient = MagicEdenSDK.v1.createSolanaKeypairClient( + apiKey, + Keypair.fromSecretKey(bs58.decode(config.privateKey!)), + { + rpcUrl: config.rpcUrl, + }, + ); + break; + case Blockchain.BITCOIN: + throw new Error("Bitcoin is not a supported chain for MagicEdenActionProvider"); + // If not Bitcoin or Solana, default to viem EVM client + default: + this.evmClient = MagicEdenSDK.v1.createViemEvmClient( + apiKey, + config.privateKey! as `0x${string}`, + chain, + ); + break; + } + } + + /** + * Creates a new launchpad. + * + * @param args - Input parameters conforming to the CreateLaunchpadSchema. + * @returns A success message or error string. + */ + @CreateAction({ + name: "createLaunchpad", + description: ` + This tool will create a new NFT launchpad on Magic Eden. Both Solana and EVM chains are supported. + + It takes the following inputs: + - chain: The blockchain to deploy on, one of ["solana", "ethereum", "base", "polygon", "sei", "arbitrum", "apechain", "berachain", "monad_testnet", "bsc", "abstract"] + - protocol: The NFT standard to use (Solana: "METAPLEX_CORE", EVM: "ERC721" or "ERC1155") + - creator: The wallet address that will be the creator of the collection + - name: The name of the collection (max ${MAX_NAME_LENGTH} characters, ${SOL_MAX_NAME_LENGTH} for Solana) + - symbol: The symbol for the collection (max ${MAX_SYMBOL_LENGTH} characters) + - imageUrl: (Optional) URL pointing to the collection's image + - description: (Optional) Description of the collection + - royaltyBps: Royalty basis points (between ${MIN_ROYALTY_BPS} and ${MAX_ROYALTY_BPS}) + - royaltyRecipients: Array of {address, share} (shares must sum to 100%) (maximum of 4 royalty recipients for Solana) + - payoutRecipient: Wallet address to receive mint proceeds + - nftMetadataUrl: (Optional) URL to metadata JSON files + - tokenImageUrl: (Optional) URL for token image in open editions + - mintStages: Configuration for the minting phases, containing: + • stages: (Optional) Array of mint stages, minimum 1 stage. Each stage has: + - kind: Type of mint stage ("public" or "allowlist") + - price: Object with {currency: string, raw: string} for mint price + - startTime: Start time in ISO format (YYYY-MM-DDTHH:MM:SS.MSZ) + - endTime: End time in ISO format (YYYY-MM-DDTHH:MM:SS.MSZ) + - walletLimit: (Optional) Max mints per wallet (0-10000) + - maxSupply: (Optional) Max supply for this stage (1-uint256) + - allowlist: (Optional, for allowlist kind) Array of allowed wallet addresses (2-2500 addresses) + • tokenId: (Optional) Token ID for ERC1155 collections + • walletLimit: (Optional) Default wallet limit if no stages defined (0-10000) + • maxSupply: (Optional) Total items available for minting (1-uint256) + + Solana-specific parameters: + - isOpenEdition: Whether the collection is an open edition + - social: (Optional) Social media links (Discord, Twitter, etc.) + + Important notes: + - Creating a launchpad requires approval transactions and will incur gas fees + - For Solana, a separate 'publishLaunchpad' action is required after creation + - Royalty recipients' shares must sum to exactly 100% + - All URLs should be publicly accessible + - For non-open editions, metadata JSON files should follow the 0.json, 1.json naming pattern + - Mint stages must not overlap in time + - For Solana, if no stages are defined but walletLimit is set, it becomes the default public mint stage limit + `, + schema: CreateLaunchpadParams, + }) + public async createLaunchpad(args: CreateLaunchpadParams): Promise { + try { + const response = this.solClient + ? await this.solClient?.nft.createLaunchpad(args as SolanaCreateLaunchpadParams) + : await this.evmClient?.nft.createLaunchpad(args as EvmCreateLaunchpadParams); + + return this.handleTransactionResponse(response, "createLaunchpad"); + } catch (error) { + return `Error executing MagicEden 'createLaunchpad' action: ${error}`; + } + } + + /** + * Publishes a launchpad (only for Solana) + * + * @param args - Input parameters conforming to the PublishLaunchpadParams. + * @returns A success message or error string. + */ + @CreateAction({ + name: "publishLaunchpad", + description: ` + This tool will publish a previously created launchpad, making it visible to the public on Magic Eden. Currently, this action is only required and supported for Solana launchpads. + + It takes the following inputs: + - chain: The blockchain to publish on (must be "solana") + - candyMachineId: The Solana address of the candy machine + - symbol: The symbol of the collection/launchpad + + Important notes: + - This action is only required for Solana launchpads + - Must be called after successfully creating a launchpad + - The candyMachineId is provided in the response of the createLaunchpad action + - The symbol must match the one used in createLaunchpad + - Publishing a launchpad requires an on-chain transaction and will incur gas fees + - Once published, the launchpad cannot be unpublished + - The launchpad must be published before minting can begin + - EVM launchpads are automatically published during creation and do not need this step + `, + schema: PublishLaunchpadParams, + }) + public async publishLaunchpad(args: PublishLaunchpadParams): Promise { + try { + if (!this.solClient) { + return `Solana client not initialized. Publish launchpad is only supported on Solana.`; + } + + const response = await this.solClient?.nft.publishLaunchpad( + args as SolanaPublishLaunchpadParams, + ); + + if (!response) { + return `Failed to execute MagicEden 'publishLaunchpad' action`; + } + + return `Successfully executed MagicEden 'publishLaunchpad' action.`; + } catch (error) { + return `Error executing MagicEden 'publishLaunchpad' action: ${error}`; + } + } + + /** + * Updates an existing launchpad. + * + * @param args - Input parameters for updating the launchpad. + * @returns A success message or error string. + */ + @CreateAction({ + name: "updateLaunchpad", + description: ` + This tool will update an existing NFT launchpad on Magic Eden. Both Solana and EVM chains are supported. + + All chains take the following inputs: + - chain: The blockchain to update on, one of ["solana", "ethereum", "base", "polygon", "sei", "arbitrum", "apechain", "berachain", "monad_testnet", "bsc", "abstract"] + - protocol: The NFT standard (e.g., "METAPLEX_CORE" for Solana, "ERC721" or "ERC1155" for EVM) + - collectionId: The collection address/ID to update + - owner: The owner wallet address + - name: (Optional) Collection name (max ${MAX_NAME_LENGTH} chars, ${SOL_MAX_NAME_LENGTH} for Solana) + - imageUrl: (Optional) URL pointing to collection image + - description: (Optional) Collection description + - royaltyBps: (Optional) Royalty basis points (${MIN_ROYALTY_BPS}-${MAX_ROYALTY_BPS}) + - royaltyRecipients: (Optional) Array of {address, share} (shares must sum to 100%) + - payoutRecipient: (Optional) Wallet to receive mint proceeds + - nftMetadataUrl: (Optional) URL to metadata JSON files + - tokenImageUrl: (Optional) URL for token image (open editions) + - mintStages: (Optional) Configuration for minting phases, containing: + • stages: (Optional) Array of mint stages, minimum 1 stage. Each stage has: + - kind: Type of mint stage ("public" or "allowlist") + - price: Object with {currency: string, raw: string} for mint price + - startTime: Start time in ISO format (YYYY-MM-DDTHH:MM:SS.MSZ) + - endTime: End time in ISO format (YYYY-MM-DDTHH:MM:SS.MSZ) + - walletLimit: (Optional) Max mints per wallet (0-10000) + - maxSupply: (Optional) Max supply for this stage (1-uint256) + - allowlist: (Optional, for allowlist kind) Array of allowed wallet addresses (2-2500 addresses) + • tokenId: (Optional) Token ID for ERC1155 collections + • walletLimit: (Optional) Default wallet limit if no stages defined (0-10000) + • maxSupply: (Optional) Total items available for minting (1-uint256) + + EVM-specific parameters: + - tokenId: (Optional) Token ID for ERC1155 collections, required if protocol is "ERC1155" + - collectionId must be a valid EVM address + - Uses contract:tokenId format for tokens + + Solana-specific parameters: + - payer: Address paying for transaction fees + - candyMachineId: The Candy Machine address + - symbol: Current collection symbol + - name: Current collection name (required on Solana) + - royaltyRecipients: Array of {address, share} (shares must sum to 100%) (required on Solana) (maximum of 4 royalty recipients) + - payoutRecipient: Wallet to receive mint proceeds (required on Solana) + - newSymbol: (Optional) New symbol to update to + - social: (Optional) Social media links (Discord, Twitter, etc.) + • discordUrl: (Optional) Discord URL + • twitterUsername: (Optional) Twitter username + • telegramUrl: (Optional) Telegram URL + • externalUrl: (Optional) External URL + + Important notes: + - Updates require approval transactions and will incur gas fees + - All URLs must be publicly accessible + - For non-open editions, metadata JSONs should follow 0.json, 1.json pattern + - Some parameters may be immutable depending on the chain and protocol + - All addresses must be valid for the specified chain + - Royalty recipient shares must sum to exactly 100% + `, + schema: UpdateLaunchpadParams, + }) + public async updateLaunchpad(args: UpdateLaunchpadParams): Promise { + try { + const response = this.solClient + ? await this.solClient?.nft.updateLaunchpad(args as SolanaUpdateLaunchpadParams) + : await this.evmClient?.nft.updateLaunchpad(args as EvmUpdateLaunchpadParams); + + return this.handleTransactionResponse(response, "updateLaunchpad"); + } catch (error) { + return `Error executing MagicEden 'updateLaunchpad' action: ${error}`; + } + } + + /** + * Lists an NFT for sale. + * + * @param args - Input parameters for listing the NFT. + * @returns A success message or error string. + */ + @CreateAction({ + name: "listNft", + description: ` + This tool will list an NFT for sale on the Magic Eden marketplace. Both Solana and EVM chains are supported. + + EVM-specific parameters: + - chain: The blockchain to list on, one of ["ethereum", "base", "polygon", "sei", "arbitrum", "apechain", "berachain", "monad_testnet", "bsc", "abstract"] + - params: Array of listings, each containing: + • token: Contract and token ID in format "contractAddress:tokenId" + • price: Amount in wei + • expiry: (Optional) Expiration Unix timestamp in seconds for when the listing should expire + + Solana-specific parameters: + - token: The NFT's mint address + - price: The listing price in lamports + - expiry: (Optional) Expiration Unix timestamp in seconds for when the listing should expire + - tokenAccount: (Optional) Required only for legacy NFTs + - splPrice: (Optional) Details for SPL token pricing + - sellerReferral: (Optional) Referral address + - prioFeeMicroLamports: (Optional) Priority fee in micro lamports + - maxPrioFeeLamports: (Optional) Maximum priority fee + - exactPrioFeeLamports: (Optional) Exact priority fee + - txFeePayer: (Optional) Address paying for transaction fees + + Important notes: + - The wallet must own the NFT being listed + - First-time listings require an approval transaction + • This will incur a one-time gas fee + • Subsequent listings will be gasless + - Prices must be in the chain's smallest unit (wei/lamports) + - For EVM chains, multiple NFTs can be listed in one transaction + - For Solana, priority fees can be adjusted to speed up transactions + - Legacy Solana NFTs require additional token account information + `, + schema: ListParams, + }) + public async listNft(args: ListParams): Promise { + try { + const response = this.solClient + ? await this.solClient?.nft.list(args as SolanaListParams) + : await this.evmClient?.nft.list(args as EvmListParams); + + return this.handleTransactionResponse(response, "listNft"); + } catch (error) { + return `Error executing MagicEden 'listNft' action: ${error}`; + } + } + + /** + * Cancels an existing NFT listing. + * + * @param args - Input parameters for canceling the listing. + * @returns A success message or error string. + */ + @CreateAction({ + name: "cancelListing", + description: ` + This tool will cancel an existing NFT listing on the Magic Eden marketplace. Both Solana and EVM chains are supported. + + Required inputs differ by chain: + + EVM-specific parameters: + - chain: The blockchain where the listing exists (e.g., "base", "ethereum") + - orderIds: Array of order IDs to cancel + • Each ID represents a specific listing to cancel + • Multiple listings can be canceled in one transaction + + Solana-specific parameters: + - token: The NFT's mint address + - price: The listing price that was used (in lamports) + - tokenAccount: (Optional) Required only for legacy NFTs + - sellerReferral: (Optional) Referral address + - expiry: (Optional) Original listing expiration Unix timestamp in seconds + - prioFeeMicroLamports: (Optional) Priority fee in micro lamports + - maxPrioFeeLamports: (Optional) Maximum priority fee + - exactPrioFeeLamports: (Optional) Exact priority fee + + Important notes: + - Only the wallet that created the listing can cancel it + - Canceling a listing requires an on-chain transaction + • This will incur gas fees + • Gas fees are typically lower than listing fees + - For Solana: + • Legacy NFTs require the tokenAccount parameter + • Priority fees can be adjusted to speed up transactions + • The price must match the original listing exactly + - For EVM: + • Multiple listings can be canceled in one transaction + • Order IDs can be found in the original listing response + `, + schema: CancelListingParams, + }) + public async cancelListing(args: CancelListingParams): Promise { + try { + const response = this.solClient + ? await this.solClient?.nft.cancelListing(args as SolanaCancelListingParams) + : await this.evmClient?.nft.cancelListing(args as EvmCancelListingParams); + + return this.handleTransactionResponse(response, "cancelListing"); + } catch (error) { + return `Error executing MagicEden 'cancelListing' action: ${error}`; + } + } + + /** + * Makes an offer on an NFT. + * + * @param args - Input parameters for making the offer. + * @returns A success message or error string. + */ + @CreateAction({ + name: "makeItemOffer", + description: ` + This tool will make an offer on an NFT listed on the Magic Eden marketplace. Both Solana and EVM chains are supported. + + EVM-specific parameters: + - chain: The blockchain to make the offer on, one of ["ethereum", "base", "polygon", "sei", "arbitrum", "apechain", "berachain", "monad_testnet", "bsc", "abstract"] + - params: Array of offers, each containing: + • token: Contract and token ID in format "contractAddress:tokenId" + • price: The offer amount in wei (smallest unit) + • expiry: (Optional) Offer expiration Unix timestamp in seconds + • quantity: (Optional) Number of NFTs to bid on + • automatedRoyalties: (Optional) Auto-set royalty amounts and recipients + • royaltyBps: (Optional) Maximum royalty basis points to pay (1 BPS = 0.01%) + • currency: (Optional) Token address for payment (defaults to wrapped native token) + + Solana-specific parameters: + - token: The NFT's mint address + - price: The offer amount in lamports + - expiry: (Optional) Offer expiration Unix timestamp in seconds + - buyerReferral: (Optional) Referral address + - useBuyV2: (Optional) Whether to use buy v2 protocol + - buyerCreatorRoyaltyPercent: (Optional) Buyer's share of creator royalties + - prioFeeMicroLamports: (Optional) Priority fee in micro lamports + - maxPrioFeeLamports: (Optional) Maximum priority fee + - exactPrioFeeLamports: (Optional) Exact priority fee + + Important notes: + - The wallet must have sufficient funds to cover the offer amount + - Making an offer requires an approval transaction for the currency + • First-time approval will incur a gas fee + • Subsequent offers using the same currency will be gasless + - For EVM: + • Multiple offers can be made in one transaction + • Custom currencies (tokens) can be used instead of native currency + • Royalties can be configured or automated + - For Solana: + • Priority fees can be adjusted to speed up transactions + • Creator royalties can be split between buyer and seller + `, + schema: MakeItemOfferParams, + }) + public async makeItemOffer(args: MakeItemOfferParams): Promise { + try { + const response = this.solClient + ? await this.solClient?.nft.makeItemOffer(args as SolanaMakeItemOfferParams) + : await this.evmClient?.nft.makeItemOffer(args as EvmMakeItemOfferParams); + + return this.handleTransactionResponse(response, "makeItemOffer"); + } catch (error) { + return `Error executing MagicEden 'makeItemOffer' action: ${error}`; + } + } + + /** + * Accepts an existing offer on an NFT. + * + * @param args - Input parameters for accepting the offer. + * @returns A success message or error string. + */ + @CreateAction({ + name: "takeItemOffer", + description: ` + This tool will accept an existing offer on an NFT listed on the Magic Eden marketplace. Both Solana and EVM chains are supported. + + EVM-specific parameters: + - chain: The blockchain where the offer exists, one of ["ethereum", "base", "polygon", "sei", "arbitrum", "apechain", "berachain", "monad_testnet", "bsc", "abstract"] + - items: Array of offers to accept, each containing: + • token: Contract and token ID in format "contractAddress:tokenId" + • quantity: (Optional) Number of tokens to sell + • orderId: (Optional) Specific order ID to accept + + Solana-specific parameters: + - token: The NFT's mint address + - buyer: The wallet address of the offer maker + - price: The original offer price in lamports + - newPrice: The price at which to accept the offer + - buyerReferral: (Optional) Buyer's referral address + - sellerReferral: (Optional) Seller's referral address + - buyerExpiry: (Optional) Buyer's offer expiration Unix timestamp in seconds + - sellerExpiry: (Optional) Seller's acceptance expiration Unix timestamp in seconds + - prioFeeMicroLamports: (Optional) Priority fee in micro lamports + - maxPrioFeeLamports: (Optional) Maximum priority fee + - exactPrioFeeLamports: (Optional) Exact priority fee + + Important notes: + - Only the NFT owner can accept offers + - Accepting an offer requires an on-chain transaction + • This will incur gas fees + • First-time approvals may require additional gas + - For EVM: + • Multiple offers can be accepted in one transaction + • Order IDs can be used to accept specific offers + • Quantity can be specified for ERC1155 tokens + - For Solana: + • Priority fees can be adjusted to speed up transactions + • Both original and new prices must be specified + • Expiration times can be set for both parties + `, + schema: TakeItemOfferParams, + }) + public async takeItemOffer(args: TakeItemOfferParams): Promise { + try { + const response = this.solClient + ? await this.solClient?.nft.takeItemOffer(args as SolanaTakeItemOfferParams) + : await this.evmClient?.nft.takeItemOffer(args as EvmTakeItemOfferParams); + + return this.handleTransactionResponse(response, "takeItemOffer"); + } catch (error) { + return `Error executing MagicEden 'takeItemOffer' action: ${error}`; + } + } + + /** + * Cancels an existing offer on an NFT. + * + * @param args - Input parameters for canceling the offer. + * @returns A success message or error string. + */ + @CreateAction({ + name: "cancelItemOffer", + description: ` + This tool will cancel an existing offer on an NFT on the Magic Eden marketplace. Both Solana and EVM chains are supported. + + Required inputs differ by chain: + + EVM-specific parameters: + - chain: The blockchain where the offer exists, one of ["ethereum", "base", "polygon", "sei", "arbitrum", "apechain", "berachain", "monad_testnet", "bsc", "abstract"] + - orderIds: Array of order IDs to cancel + • Each ID represents a specific offer to cancel + • Multiple offers can be canceled in one transaction + + Solana-specific parameters: + - token: The NFT's mint address + - price: The original offer price in lamports + - expiry: (Optional) Original offer expiration Unix timestamp in seconds + - buyerReferral: (Optional) Referral address + - prioFeeMicroLamports: (Optional) Priority fee in micro lamports + - maxPrioFeeLamports: (Optional) Maximum priority fee + - exactPrioFeeLamports: (Optional) Exact priority fee + + Important notes: + - Only the wallet that made the offer can cancel it + - Canceling an offer requires an on-chain transaction + • This will incur gas fees + • Gas fees are typically lower than making offers + - For Solana: + • Priority fees can be adjusted to speed up transactions + • The price must match the original offer exactly + • Expiration time must match if it was set + - For EVM: + • Multiple offers can be canceled in one transaction + • Order IDs can be found in the original offer response + `, + schema: CancelItemOfferParams, + }) + public async cancelItemOffer(args: CancelItemOfferParams): Promise { + try { + const response = this.solClient + ? await this.solClient?.nft.cancelItemOffer(args as SolanaCancelItemOfferParams) + : await this.evmClient?.nft.cancelItemOffer(args as EvmCancelItemOfferParams); + + return this.handleTransactionResponse(response, "cancelItemOffer"); + } catch (error) { + return `Error executing MagicEden 'cancelItemOffer' action: ${error}`; + } + } + + /** + * Buys one or more NFTs from the Magic Eden marketplace. + * + * @param args - Input parameters conforming to the BuySchema. + * @returns A success message or error string. + */ + @CreateAction({ + name: "buy", + description: ` + This tool will buy one or more NFTs from the Magic Eden marketplace. Both Solana and EVM chains are supported. + + EVM-specific parameters: + - chain: The blockchain to buy on, one of ["ethereum", "base", "polygon", "sei", "arbitrum", "apechain", "berachain", "monad_testnet", "bsc", "abstract"] + - currency: (Optional) Token address to use for payment + - currencyChainId: (Optional) Chain ID of the payment token + - items: Array of NFTs to buy, each containing: + • token: Contract and token ID in format "contractAddress:tokenId" + • collection: (Optional) Collection address + • quantity: (Optional) Number of NFTs to buy + • orderId: (Optional) Specific listing to buy from + + Solana-specific parameters: + - token: The NFT's mint address + - seller: The wallet address of the NFT seller + - price: The purchase price in lamports + - buyerReferral: (Optional) Buyer's referral address + - sellerReferral: (Optional) Seller's referral address + - buyerExpiry: (Optional) Buyer's purchase expiration Unix timestamp in seconds + - sellerExpiry: (Optional) Seller's listing expiration Unix timestamp in seconds + - buyerCreatorRoyaltyPercent: (Optional) Buyer's share of creator royalties + - splPrice: (Optional) Details for SPL token purchases + + Important notes: + - The wallet must have sufficient funds to cover the purchase + - Buying an NFT requires an on-chain transaction + • This will incur gas fees + • First-time token approvals may require additional gas + - For EVM: + • Multiple NFTs can be purchased in one transaction + • Custom currencies can be used for payment + • Order IDs can be used to buy specific listings + • Quantity can be specified for ERC1155 tokens + - For Solana: + • Each purchase requires a separate transaction + • Creator royalties can be split between buyer and seller + • SPL tokens can be used for payment + `, + schema: BuyParams, + }) + public async buy(args: BuyParams): Promise { + try { + const response = this.solClient + ? await this.solClient?.nft.buy(args as SolanaBuyParams) + : await this.evmClient?.nft.buy(args as EvmBuyParams); + + return this.handleTransactionResponse(response, "buy"); + } catch (error) { + return `Error executing MagicEden 'buy' action: ${error}`; + } + } + + /** + * Determines if the provider supports the given network. + * + * @param network - The network to check. + * @returns True if supported, false otherwise. + */ + public supportsNetwork = (network: Network): boolean => isSupportedNetwork(network); + + private handleTransactionResponse( + response: OperationResponse[] | undefined, + action: string + ): string { + if (!response) { + return `Failed to ${action}`; + } + + const failures = response.filter( + r => r.status === "failed" || r.status === undefined || r.error + ); + if (failures.length) { + return `Failed to execute MagicEden '${action}' action: ${failures.map(f => f.error).join(", ")}`; + } + + const transactionResponse = response + .filter(r => r !== undefined) + .map(r => r as TransactionResponse); + + return `Successfully executed MagicEden '${action}' action.\nTransactions: [${transactionResponse.map(r => r.txId).join(", ")}]`; + } +} + +export const magicEdenActionProvider = (config: MagicEdenActionProviderConfig) => + new MagicEdenActionProvider(config); diff --git a/typescript/agentkit/src/action-providers/magiceden/utils.ts b/typescript/agentkit/src/action-providers/magiceden/utils.ts new file mode 100644 index 000000000..c694bee20 --- /dev/null +++ b/typescript/agentkit/src/action-providers/magiceden/utils.ts @@ -0,0 +1,88 @@ +import { Network, NETWORK_ID_TO_CHAIN_ID } from "../../network"; +import { Blockchain } from "@magiceden/magiceden-sdk"; + +/** + * Maps a network ID to the corresponding MagicEden blockchain. + * + * @param networkId - The network ID to map. + * @returns The corresponding MagicEden blockchain. + */ +export const NETWORK_ID_TO_MAGICEDEN_CHAIN: Record = { + "solana-mainnet": Blockchain.SOLANA, + "ethereum-mainnet": Blockchain.ETHEREUM, + "polygon-mainnet": Blockchain.POLYGON, + "base-mainnet": Blockchain.BASE, + "arbitrum-mainnet": Blockchain.ARBITRUM, +}; + +/** + * Maps a chain ID to the corresponding MagicEden blockchain. + * + * @param chainId - The chain ID to map. + * @returns The corresponding MagicEden blockchain. + */ +export const CHAIN_ID_TO_MAGICEDEN_CHAIN: Record = { + "1": Blockchain.ETHEREUM, + "137": Blockchain.POLYGON, + "8453": Blockchain.BASE, + "42161": Blockchain.ARBITRUM, + "1329": Blockchain.SEI, + "33139": Blockchain.APECHAIN, + "80094": Blockchain.BERACHAIN, + "10143": Blockchain.MONAD_TESTNET, + "56": Blockchain.BSC, + "2741": Blockchain.ABSTRACT, +}; + +/** + * Checks if the given network is supported by MagicEden. + * + * @param network - The network to check. + * @returns True if the network is supported, false otherwise. + */ +export const isSupportedNetwork = (network: Network): boolean => { + // Check if the network is supported by MagicEden + // Currently only supports EVM and Solana + const isSupportedProtocol = + network.protocolFamily === "evm" || network.protocolFamily === "svm"; + + // Check if the network is supported by MagicEden + const isSupportedNetwork = + network.networkId !== undefined && NETWORK_ID_TO_MAGICEDEN_CHAIN[network.networkId] !== undefined; + + // Check if the chain ID is supported by MagicEden + const isSupportedChain = + network.chainId !== undefined && CHAIN_ID_TO_MAGICEDEN_CHAIN[network.chainId] !== undefined; + + return isSupportedProtocol && (isSupportedNetwork || isSupportedChain); +}; + +/** + * Gets the MagicEden blockchain from a network ID. + * + * @param networkId - The network ID to get the blockchain from. + * @returns The corresponding MagicEden blockchain. + */ +export const getMagicEdenChainFromNetworkId = (networkId: string): Blockchain => { + // First we check the known network IDs, and if they map to a chain, we return that chain + const chainFromNetworkId = NETWORK_ID_TO_MAGICEDEN_CHAIN[networkId]; + if (chainFromNetworkId) { + return chainFromNetworkId; + } + + // If the chain is not found from the network ID, try to get the chain from the chain ID + // Chain IDs always stay the same + // If Coinbase Agentkit supports a new EVM chain, it will be supported by MagicEden through the chain ID + // (currently there are some EVM chains MagicEden supports which Coinbase Agentkit does not) + const chainId = NETWORK_ID_TO_CHAIN_ID[networkId]; + if (!chainId) { + throw new Error(`Could not find chain ID for network ID: ${networkId}`); + } + + const chainFromChainId = CHAIN_ID_TO_MAGICEDEN_CHAIN[chainId]; + if (chainFromChainId) { + return chainFromChainId; + } + + throw new Error(`Unsupported network ID on MagicEden: ${networkId}`); +};