diff --git a/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpApiActionProvider.test.ts b/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpApiActionProvider.test.ts new file mode 100644 index 000000000..90d364f50 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpApiActionProvider.test.ts @@ -0,0 +1,168 @@ +import { EvmWalletProvider } from "../../wallet-providers"; +import { CdpApiActionProvider } from "./cdpApiActionProvider"; +import { AddressReputationSchema, RequestFaucetFundsSchema } from "./schemas"; + +// Mock the entire module +jest.mock("@coinbase/coinbase-sdk"); + +// Get the mocked constructor +const { ExternalAddress } = jest.requireMock("@coinbase/coinbase-sdk"); + +describe("CDP API Action Provider Input Schemas", () => { + describe("Address Reputation Schema", () => { + it("should successfully parse valid input", () => { + const validInput = { + address: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83", + network: "base-mainnet", + }; + + const result = AddressReputationSchema.safeParse(validInput); + + expect(result.success).toBe(true); + expect(result.data).toEqual(validInput); + }); + + it("should fail parsing invalid address", () => { + const invalidInput = { + address: "invalid-address", + network: "base-mainnet", + }; + const result = AddressReputationSchema.safeParse(invalidInput); + + expect(result.success).toBe(false); + }); + }); + + describe("Request Faucet Funds Schema", () => { + it("should successfully parse with optional assetId", () => { + const validInput = { + assetId: "eth", + }; + + const result = RequestFaucetFundsSchema.safeParse(validInput); + + expect(result.success).toBe(true); + expect(result.data).toEqual(validInput); + }); + + it("should successfully parse without assetId", () => { + const validInput = {}; + const result = RequestFaucetFundsSchema.safeParse(validInput); + + expect(result.success).toBe(true); + expect(result.data).toEqual(validInput); + }); + }); +}); + +describe("CDP API Action Provider", () => { + let actionProvider: CdpApiActionProvider; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockExternalAddressInstance: jest.Mocked; + let mockWallet: jest.Mocked; + + beforeEach(() => { + // Reset all mocks before each test + jest.clearAllMocks(); + + actionProvider = new CdpApiActionProvider(); + mockExternalAddressInstance = { + reputation: jest.fn(), + faucet: jest.fn(), + }; + + // Mock the constructor to return our mock instance + (ExternalAddress as jest.Mock).mockImplementation(() => mockExternalAddressInstance); + + mockWallet = { + deployToken: jest.fn(), + deployContract: jest.fn(), + getAddress: jest.fn().mockReturnValue("0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83"), + getNetwork: jest.fn().mockReturnValue({ networkId: "base-sepolia" }), + } as unknown as jest.Mocked; + }); + + describe("addressReputation", () => { + it("should successfully check address reputation", async () => { + const args = { + address: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83", + network: "base-mainnet", + }; + + mockExternalAddressInstance.reputation.mockResolvedValue("Good reputation"); + + const result = await actionProvider.addressReputation(args); + + expect(ExternalAddress).toHaveBeenCalledWith(args.network, args.address); + expect(ExternalAddress).toHaveBeenCalledTimes(1); + expect(mockExternalAddressInstance.reputation).toHaveBeenCalled(); + expect(mockExternalAddressInstance.reputation).toHaveBeenCalledTimes(1); + expect(result).toBe("Good reputation"); + }); + + it("should handle errors when checking reputation", async () => { + const args = { + address: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83", + network: "base-mainnet", + }; + + const error = new Error("Reputation check failed"); + mockExternalAddressInstance.reputation.mockRejectedValue(error); + + const result = await actionProvider.addressReputation(args); + + expect(ExternalAddress).toHaveBeenCalledWith(args.network, args.address); + expect(ExternalAddress).toHaveBeenCalledTimes(1); + expect(mockExternalAddressInstance.reputation).toHaveBeenCalled(); + expect(mockExternalAddressInstance.reputation).toHaveBeenCalledTimes(1); + expect(result).toBe(`Error checking address reputation: ${error}`); + }); + }); + + describe("faucet", () => { + beforeEach(() => { + mockExternalAddressInstance.faucet.mockResolvedValue({ + wait: jest.fn().mockResolvedValue({ + getTransactionLink: jest.fn().mockReturnValue("tx-link"), + }), + }); + }); + + it("should successfully request faucet funds with assetId", async () => { + const args = { + assetId: "eth", + }; + + const result = await actionProvider.faucet(mockWallet, args); + + expect(ExternalAddress).toHaveBeenCalledWith("base-sepolia", mockWallet.getAddress()); + expect(ExternalAddress).toHaveBeenCalledTimes(1); + expect(mockExternalAddressInstance.faucet).toHaveBeenCalledWith("eth"); + expect(mockExternalAddressInstance.faucet).toHaveBeenCalledTimes(1); + expect(result).toContain("Received eth from the faucet"); + expect(result).toContain("tx-link"); + }); + + it("should successfully request faucet funds without assetId", async () => { + const args = {}; + + const result = await actionProvider.faucet(mockWallet, args); + + expect(ExternalAddress).toHaveBeenCalledWith("base-sepolia", mockWallet.getAddress()); + expect(ExternalAddress).toHaveBeenCalledTimes(1); + expect(mockExternalAddressInstance.faucet).toHaveBeenCalledWith(undefined); + expect(mockExternalAddressInstance.faucet).toHaveBeenCalledTimes(1); + expect(result).toContain("Received ETH from the faucet"); + }); + + it("should handle faucet errors", async () => { + const args = {}; + const error = new Error("Faucet request failed"); + mockExternalAddressInstance.faucet.mockRejectedValue(error); + + const result = await actionProvider.faucet(mockWallet, args); + + expect(result).toBe(`Error requesting faucet funds: ${error}`); + }); + }); +}); diff --git a/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpApiActionProvider.ts b/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpApiActionProvider.ts new file mode 100644 index 000000000..a45206ad3 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpApiActionProvider.ts @@ -0,0 +1,97 @@ +import { ExternalAddress } from "@coinbase/coinbase-sdk"; +import { z } from "zod"; + +import { CreateAction } from "../actionDecorator"; +import { ActionProvider } from "../actionProvider"; +import { Network } from "../../network"; +import { EvmWalletProvider } from "../../wallet-providers"; + +import { AddressReputationSchema, RequestFaucetFundsSchema } from "./schemas"; + +/** + * CdpApiActionProvider is an action provider for CDP API. + * + * This provider is used for any action that uses the CDP API, but does not require a CDP Wallet. + */ +export class CdpApiActionProvider extends ActionProvider { + /** + * Constructor for the CdpApiActionProvider class. + */ + constructor() { + super("cdp", []); + } + + /** + * Check the reputation of an address. + * + * @param args - The input arguments for the action + * @returns A string containing reputation data or error message + */ + @CreateAction({ + name: "address_reputation", + description: ` +This tool checks the reputation of an address on a given network. It takes: + +- network: The network to check the address on (e.g. "base-mainnet") +- address: The Ethereum address to check +`, + schema: AddressReputationSchema, + }) + async addressReputation(args: z.infer): Promise { + try { + const address = new ExternalAddress(args.network, args.address); + const reputation = await address.reputation(); + return reputation.toString(); + } catch (error) { + return `Error checking address reputation: ${error}`; + } + } + + /** + * Requests test tokens from the faucet for the default address in the wallet. + * + * @param walletProvider - The wallet provider to request funds from. + * @param args - The input arguments for the action. + * @returns A confirmation message with transaction details. + */ + @CreateAction({ + name: "request_faucet_funds", + description: `This tool will request test tokens from the faucet for the default address in the wallet. It takes the wallet and asset ID as input. +If no asset ID is provided the faucet defaults to ETH. Faucet is only allowed on 'base-sepolia' and can only provide asset ID 'eth' or 'usdc'. +You are not allowed to faucet with any other network or asset ID. If you are on another network, suggest that the user sends you some ETH +from another wallet and provide the user with your wallet details.`, + schema: RequestFaucetFundsSchema, + }) + async faucet( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const address = new ExternalAddress( + walletProvider.getNetwork().networkId!, + walletProvider.getAddress(), + ); + + const faucetTx = await address.faucet(args.assetId || undefined); + + const result = await faucetTx.wait(); + + return `Received ${ + args.assetId || "ETH" + } from the faucet. Transaction: ${result.getTransactionLink()}`; + } catch (error) { + return `Error requesting faucet funds: ${error}`; + } + } + + /** + * Checks if the Cdp action provider supports the given network. + * + * @param _ - The network to check. + * @returns True if the Cdp action provider supports the network, false otherwise. + * TODO: Split out into sub providers so network support can be tighter scoped. + */ + supportsNetwork = (_: Network) => true; +} + +export const cdpApiActionProvider = () => new CdpApiActionProvider(); diff --git a/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpActionProvider.test.ts b/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpWalletActionProvider.test.ts similarity index 64% rename from cdp-agentkit-core/typescript/src/action-providers/cdp/cdpActionProvider.test.ts rename to cdp-agentkit-core/typescript/src/action-providers/cdp/cdpWalletActionProvider.test.ts index e023c83c2..5a6ff892d 100644 --- a/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpActionProvider.test.ts +++ b/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpWalletActionProvider.test.ts @@ -1,6 +1,6 @@ import { CdpWalletProvider } from "../../wallet-providers"; -import { CdpActionProvider } from "./cdpActionProvider"; -import { AddressReputationSchema, DeployNftSchema, RequestFaucetFundsSchema } from "./schemas"; +import { CdpWalletActionProvider } from "./cdpWalletActionProvider"; +import { DeployNftSchema, DeployTokenSchema, DeployContractSchema } from "./schemas"; import { SmartContract } from "@coinbase/coinbase-sdk"; // Mock the entire module @@ -9,77 +9,70 @@ jest.mock("@coinbase/coinbase-sdk"); // Get the mocked constructor const { ExternalAddress } = jest.requireMock("@coinbase/coinbase-sdk"); -describe("CDP Action Provider Input Schemas", () => { - describe("Address Reputation Schema", () => { +describe("CDP Wallet Action Provider Input Schemas", () => { + describe("Deploy NFT Schema", () => { it("should successfully parse valid input", () => { const validInput = { - address: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83", - network: "base-mainnet", + baseURI: "https://www.test.xyz/metadata/", + name: "Test Token", + symbol: "TEST", }; - const result = AddressReputationSchema.safeParse(validInput); + const result = DeployNftSchema.safeParse(validInput); expect(result.success).toBe(true); expect(result.data).toEqual(validInput); }); - it("should fail parsing invalid address", () => { - const invalidInput = { - address: "invalid-address", - network: "base-mainnet", - }; - const result = AddressReputationSchema.safeParse(invalidInput); + it("should fail parsing empty input", () => { + const emptyInput = {}; + const result = DeployNftSchema.safeParse(emptyInput); expect(result.success).toBe(false); }); }); - describe("Deploy NFT Schema", () => { + describe("Deploy Token Schema", () => { it("should successfully parse valid input", () => { const validInput = { - baseURI: "https://www.test.xyz/metadata/", name: "Test Token", symbol: "TEST", + totalSupply: 1000000000000000000n, }; - const result = DeployNftSchema.safeParse(validInput); + const result = DeployTokenSchema.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", () => { + describe("Deploy Contract Schema", () => { + it("should successfully parse valid input", () => { const validInput = { - assetId: "eth", + solidityVersion: "0.8.0", + solidityInputJson: "{}", + contractName: "Test Contract", + constructorArgs: {}, }; - const result = RequestFaucetFundsSchema.safeParse(validInput); + const result = DeployContractSchema.safeParse(validInput); expect(result.success).toBe(true); expect(result.data).toEqual(validInput); }); - it("should successfully parse without assetId", () => { - const validInput = {}; - const result = RequestFaucetFundsSchema.safeParse(validInput); + it("should fail parsing empty input", () => { + const emptyInput = {}; + const result = DeployContractSchema.safeParse(emptyInput); - expect(result.success).toBe(true); - expect(result.data).toEqual(validInput); + expect(result.success).toBe(false); }); }); }); -describe("CDP Action Provider", () => { - let actionProvider: CdpActionProvider; +describe("CDP Wallet Action Provider", () => { + let actionProvider: CdpWalletActionProvider; // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockExternalAddressInstance: jest.Mocked; let mockWallet: jest.Mocked; @@ -88,7 +81,7 @@ describe("CDP Action Provider", () => { // Reset all mocks before each test jest.clearAllMocks(); - actionProvider = new CdpActionProvider(); + actionProvider = new CdpWalletActionProvider(); mockExternalAddressInstance = { reputation: jest.fn(), faucet: jest.fn(), @@ -105,43 +98,6 @@ describe("CDP Action Provider", () => { } as unknown as jest.Mocked; }); - describe("addressReputation", () => { - it("should successfully check address reputation", async () => { - const args = { - address: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83", - network: "base-mainnet", - }; - - mockExternalAddressInstance.reputation.mockResolvedValue("Good reputation"); - - const result = await actionProvider.addressReputation(args); - - expect(ExternalAddress).toHaveBeenCalledWith(args.network, args.address); - expect(ExternalAddress).toHaveBeenCalledTimes(1); - expect(mockExternalAddressInstance.reputation).toHaveBeenCalled(); - expect(mockExternalAddressInstance.reputation).toHaveBeenCalledTimes(1); - expect(result).toBe("Good reputation"); - }); - - it("should handle errors when checking reputation", async () => { - const args = { - address: "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83", - network: "base-mainnet", - }; - - const error = new Error("Reputation check failed"); - mockExternalAddressInstance.reputation.mockRejectedValue(error); - - const result = await actionProvider.addressReputation(args); - - expect(ExternalAddress).toHaveBeenCalledWith(args.network, args.address); - expect(ExternalAddress).toHaveBeenCalledTimes(1); - expect(mockExternalAddressInstance.reputation).toHaveBeenCalled(); - expect(mockExternalAddressInstance.reputation).toHaveBeenCalledTimes(1); - expect(result).toBe(`Error checking address reputation: ${error}`); - }); - }); - describe("deployNft", () => { let mockWallet: jest.Mocked; const MOCK_NFT_BASE_URI = "https://www.test.xyz/metadata/"; @@ -247,53 +203,6 @@ describe("CDP Action Provider", () => { }); }); - describe("faucet", () => { - beforeEach(() => { - mockExternalAddressInstance.faucet.mockResolvedValue({ - wait: jest.fn().mockResolvedValue({ - getTransactionLink: jest.fn().mockReturnValue("tx-link"), - }), - }); - }); - - it("should successfully request faucet funds with assetId", async () => { - const args = { - assetId: "eth", - }; - - const result = await actionProvider.faucet(mockWallet, args); - - expect(ExternalAddress).toHaveBeenCalledWith("base-sepolia", mockWallet.getAddress()); - expect(ExternalAddress).toHaveBeenCalledTimes(1); - expect(mockExternalAddressInstance.faucet).toHaveBeenCalledWith("eth"); - expect(mockExternalAddressInstance.faucet).toHaveBeenCalledTimes(1); - expect(result).toContain("Received eth from the faucet"); - expect(result).toContain("tx-link"); - }); - - it("should successfully request faucet funds without assetId", async () => { - const args = {}; - - const result = await actionProvider.faucet(mockWallet, args); - - expect(ExternalAddress).toHaveBeenCalledWith("base-sepolia", mockWallet.getAddress()); - expect(ExternalAddress).toHaveBeenCalledTimes(1); - expect(mockExternalAddressInstance.faucet).toHaveBeenCalledWith(undefined); - expect(mockExternalAddressInstance.faucet).toHaveBeenCalledTimes(1); - expect(result).toContain("Received ETH from the faucet"); - }); - - it("should handle faucet errors", async () => { - const args = {}; - const error = new Error("Faucet request failed"); - mockExternalAddressInstance.faucet.mockRejectedValue(error); - - const result = await actionProvider.faucet(mockWallet, args); - - expect(result).toBe(`Error requesting faucet funds: ${error}`); - }); - }); - describe("deployContract", () => { const CONTRACT_ADDRESS = "0x123456789abcdef"; const TRANSACTION_LINK = "https://etherscan.io/tx/0xghijkl987654321"; diff --git a/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpActionProvider.ts b/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpWalletActionProvider.ts similarity index 66% rename from cdp-agentkit-core/typescript/src/action-providers/cdp/cdpActionProvider.ts rename to cdp-agentkit-core/typescript/src/action-providers/cdp/cdpWalletActionProvider.ts index d4b1053e0..3ce55b4f0 100644 --- a/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpActionProvider.ts +++ b/cdp-agentkit-core/typescript/src/action-providers/cdp/cdpWalletActionProvider.ts @@ -1,4 +1,3 @@ -import { ExternalAddress } from "@coinbase/coinbase-sdk"; import { z } from "zod"; import { CreateAction } from "../actionDecorator"; @@ -7,51 +6,21 @@ import { Network } from "../../network"; import { CdpWalletProvider } from "../../wallet-providers"; import { SolidityVersions } from "./constants"; -import { - AddressReputationSchema, - DeployContractSchema, - DeployNftSchema, - DeployTokenSchema, - RequestFaucetFundsSchema, -} from "./schemas"; +import { DeployContractSchema, DeployNftSchema, DeployTokenSchema } from "./schemas"; /** - * CdpActionProvider is an action provider for Cdp. + * CdpWalletActionProvider is an action provider for Cdp. + * + * This provider is used for any action that requires a CDP Wallet. */ -export class CdpActionProvider extends ActionProvider { +export class CdpWalletActionProvider extends ActionProvider { /** - * Constructor for the CdpActionProvider class. + * Constructor for the CdpWalletActionProvider class. */ constructor() { super("cdp", []); } - /** - * Check the reputation of an address. - * - * @param args - The input arguments for the action - * @returns A string containing reputation data or error message - */ - @CreateAction({ - name: "address_reputation", - description: ` -This tool checks the reputation of an address on a given network. It takes: - -- network: The network to check the address on (e.g. "base-mainnet") -- address: The Ethereum address to check -`, - schema: AddressReputationSchema, - }) - async addressReputation(args: z.infer): Promise { - try { - const address = new ExternalAddress(args.network, args.address); - const reputation = await address.reputation(); - return reputation.toString(); - } catch (error) { - return `Error checking address reputation: ${error}`; - } - } - /** * Deploys a contract. * @@ -177,43 +146,6 @@ The token will be deployed using the wallet's default address as the owner and i } } - /** - * Requests test tokens from the faucet for the default address in the wallet. - * - * @param walletProvider - The wallet provider to request funds from. - * @param args - The input arguments for the action. - * @returns A confirmation message with transaction details. - */ - @CreateAction({ - name: "request_faucet_funds", - description: `This tool will request test tokens from the faucet for the default address in the wallet. It takes the wallet and asset ID as input. -If no asset ID is provided the faucet defaults to ETH. Faucet is only allowed on 'base-sepolia' and can only provide asset ID 'eth' or 'usdc'. -You are not allowed to faucet with any other network or asset ID. If you are on another network, suggest that the user sends you some ETH -from another wallet and provide the user with your wallet details.`, - schema: RequestFaucetFundsSchema, - }) - async faucet( - walletProvider: CdpWalletProvider, - args: z.infer, - ): Promise { - try { - const address = new ExternalAddress( - walletProvider.getNetwork().networkId!, - walletProvider.getAddress(), - ); - - const faucetTx = await address.faucet(args.assetId || undefined); - - const result = await faucetTx.wait(); - - return `Received ${ - args.assetId || "ETH" - } from the faucet. Transaction: ${result.getTransactionLink()}`; - } catch (error) { - return `Error requesting faucet funds: ${error}`; - } - } - /** * Checks if the Cdp action provider supports the given network. * @@ -224,4 +156,4 @@ from another wallet and provide the user with your wallet details.`, supportsNetwork = (_: Network) => true; } -export const cdpActionProvider = () => new CdpActionProvider(); +export const cdpWalletActionProvider = () => new CdpWalletActionProvider(); diff --git a/cdp-agentkit-core/typescript/src/action-providers/cdp/index.ts b/cdp-agentkit-core/typescript/src/action-providers/cdp/index.ts index 2f0f5fb8e..377149961 100644 --- a/cdp-agentkit-core/typescript/src/action-providers/cdp/index.ts +++ b/cdp-agentkit-core/typescript/src/action-providers/cdp/index.ts @@ -1,2 +1,3 @@ export * from "./schemas"; -export * from "./cdpActionProvider"; +export * from "./cdpApiActionProvider"; +export * from "./cdpWalletActionProvider";