diff --git a/cdp-agentkit-core/typescript/src/action_providers/cdp/cdpActionProvider.test.ts b/cdp-agentkit-core/typescript/src/action_providers/cdp/cdpActionProvider.test.ts index 661b97df1..177b0e0ba 100644 --- a/cdp-agentkit-core/typescript/src/action_providers/cdp/cdpActionProvider.test.ts +++ b/cdp-agentkit-core/typescript/src/action_providers/cdp/cdpActionProvider.test.ts @@ -1,6 +1,6 @@ import { CdpWalletProvider } from "../../wallet_providers"; import { CdpActionProvider } from "./cdpActionProvider"; -import { AddressReputationSchema, RequestFaucetFundsSchema } from "./schemas"; +import { AddressReputationSchema, DeployNftSchema, RequestFaucetFundsSchema } from "./schemas"; import { SmartContract } from "@coinbase/coinbase-sdk"; // Mock the entire module @@ -34,6 +34,28 @@ describe("CDP Action Provider Input Schemas", () => { }); }); + describe("Deploy NFT Schema", () => { + it("should successfully parse valid input", () => { + const validInput = { + baseURI: "https://www.test.xyz/metadata/", + name: "Test Token", + symbol: "TEST", + }; + + const result = DeployNftSchema.safeParse(validInput); + + expect(result.success).toBe(true); + expect(result.data).toEqual(validInput); + }); + + it("should fail parsing empty input", () => { + const emptyInput = {}; + const result = DeployNftSchema.safeParse(emptyInput); + + expect(result.success).toBe(false); + }); + }); + describe("Request Faucet Funds Schema", () => { it("should successfully parse with optional assetId", () => { const validInput = { @@ -120,6 +142,65 @@ describe("CDP Action Provider", () => { }); }); + describe("deployNft", () => { + let mockWallet: jest.Mocked; + const MOCK_NFT_BASE_URI = "https://www.test.xyz/metadata/"; + const MOCK_NFT_NAME = "Test Token"; + const MOCK_NFT_SYMBOL = "TEST"; + const CONTRACT_ADDRESS = "0x123456789abcdef"; + const NETWORK_ID = "base-sepolia"; + const TRANSACTION_HASH = "0xghijkl987654321"; + const TRANSACTION_LINK = `https://etherscan.io/tx/${TRANSACTION_HASH}`; + + beforeEach(() => { + mockWallet = { + deployNFT: jest.fn().mockResolvedValue({ + wait: jest.fn().mockResolvedValue({ + getContractAddress: jest.fn().mockReturnValue(CONTRACT_ADDRESS), + getTransaction: jest.fn().mockReturnValue({ + getTransactionHash: jest.fn().mockReturnValue(TRANSACTION_HASH), + getTransactionLink: jest.fn().mockReturnValue(TRANSACTION_LINK), + }), + }), + }), + getNetwork: jest.fn().mockReturnValue({ networkId: NETWORK_ID }), + } as unknown as jest.Mocked; + }); + + it("should successfully deploy an NFT", async () => { + const args = { + name: MOCK_NFT_NAME, + symbol: MOCK_NFT_SYMBOL, + baseURI: MOCK_NFT_BASE_URI, + }; + + const result = await actionProvider.deployNFT(mockWallet, args); + + expect(mockWallet.deployNFT).toHaveBeenCalledWith(args); + expect(result).toContain(`Deployed NFT Collection ${MOCK_NFT_NAME}:`); + expect(result).toContain(`- to address ${CONTRACT_ADDRESS}`); + expect(result).toContain(`- on network ${NETWORK_ID}`); + expect(result).toContain(`Transaction hash: ${TRANSACTION_HASH}`); + expect(result).toContain(`Transaction link: ${TRANSACTION_LINK}`); + }); + + it("should handle deployment errors", async () => { + const args = { + name: MOCK_NFT_NAME, + symbol: MOCK_NFT_SYMBOL, + baseURI: MOCK_NFT_BASE_URI, + }; + + const error = new Error("An error has occurred"); + mockWallet.deployNFT.mockRejectedValue(error); + + const result = await actionProvider.deployNFT(mockWallet, args); + + expect(mockWallet.deployNFT).toHaveBeenCalledWith(args); + expect(result).toBe(`Error deploying NFT: ${error}`); + }); + }); + describe("deployToken", () => { beforeEach(() => { mockWallet = { diff --git a/cdp-agentkit-core/typescript/src/action_providers/cdp/cdpActionProvider.ts b/cdp-agentkit-core/typescript/src/action_providers/cdp/cdpActionProvider.ts index dec56c9bd..8ceed6d30 100644 --- a/cdp-agentkit-core/typescript/src/action_providers/cdp/cdpActionProvider.ts +++ b/cdp-agentkit-core/typescript/src/action_providers/cdp/cdpActionProvider.ts @@ -10,6 +10,7 @@ import { SolidityVersions } from "./constants"; import { AddressReputationSchema, DeployContractSchema, + DeployNftSchema, DeployTokenSchema, RequestFaucetFundsSchema, } from "./schemas"; @@ -101,6 +102,48 @@ map where the key is the arg name and the value is the arg value. Encode uint/in } } + /** + * Deploys an NFT (ERC-721) token collection onchain from the wallet. + * + * @param walletProvider - The wallet provider to deploy the NFT from. + * @param args - The input arguments for the action. + * @returns A message containing the NFT token deployment details. + */ + @CreateAction({ + name: "deploy_nft", + description: `This tool will deploy an NFT (ERC-721) contract onchain from the wallet. + It takes the name of the NFT collection, the symbol of the NFT collection, and the base URI for the token metadata as inputs.`, + schema: DeployNftSchema, + }) + async deployNFT( + walletProvider: CdpWalletProvider, + args: z.infer, + ): Promise { + try { + const nftContract = await walletProvider.deployNFT({ + name: args.name, + symbol: args.symbol, + baseURI: args.baseURI, + }); + + const result = await nftContract.wait(); + + const transaction = result.getTransaction()!; + const networkId = walletProvider.getNetwork().networkId; + const contractAddress = result.getContractAddress(); + + return [ + `Deployed NFT Collection ${args.name}:`, + `- to address ${contractAddress}`, + `- on network ${networkId}.`, + `Transaction hash: ${transaction.getTransactionHash()}`, + `Transaction link: ${transaction.getTransactionLink()}`, + ].join("\n"); + } catch (error) { + return `Error deploying NFT: ${error}`; + } + } + /** * Deploys a token. * diff --git a/cdp-agentkit-core/typescript/src/action_providers/cdp/schemas.ts b/cdp-agentkit-core/typescript/src/action_providers/cdp/schemas.ts index eaa02b5f3..de312aa39 100644 --- a/cdp-agentkit-core/typescript/src/action_providers/cdp/schemas.ts +++ b/cdp-agentkit-core/typescript/src/action_providers/cdp/schemas.ts @@ -33,6 +33,18 @@ export const DeployContractSchema = z .strip() .describe("Instructions for deploying an arbitrary contract"); +/** + * Input schema for deploy NFT action + */ +export const DeployNftSchema = z + .object({ + name: z.string().describe("The name of the NFT collection"), + symbol: z.string().describe("The symbol of the NFT collection"), + baseURI: z.string().describe("The base URI for the token metadata"), + }) + .strip() + .describe("Instructions for deploying an NFT collection"); + /** * Input schema for deploy token action. */ diff --git a/cdp-agentkit-core/typescript/src/wallet_providers/cdp_wallet_provider.ts b/cdp-agentkit-core/typescript/src/wallet_providers/cdp_wallet_provider.ts index 1eb05693e..8f67c1b37 100644 --- a/cdp-agentkit-core/typescript/src/wallet_providers/cdp_wallet_provider.ts +++ b/cdp-agentkit-core/typescript/src/wallet_providers/cdp_wallet_provider.ts @@ -256,4 +256,28 @@ export class CdpWalletProvider extends EvmWalletProvider { return this.#cdpWallet.deployContract(options); } + + /** + * Deploys a new NFT (ERC-721) smart contract. + * + * @param options - Configuration options for the NFT contract deployment + * @param options.name - The name of the collection + * @param options.symbol - The token symbol for the collection + * @param options.baseURI - The base URI for token metadata. + * + * @returns A Promise that resolves to the deployed SmartContract instance + * @throws Error if the wallet is not properly initialized + * @throws Error if the deployment fails for any reason (network issues, insufficient funds, etc.) + */ + async deployNFT(options: { + name: string; + symbol: string; + baseURI: string; + }): Promise { + if (!this.#cdpWallet) { + throw new Error("Wallet not initialized"); + } + + return this.#cdpWallet.deployNFT(options); + } }