UNPKG

@coinbase/agentkit

Version:

Coinbase AgentKit core primitives

523 lines (522 loc) 26.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const cdpEvmWalletActionProvider_1 = require("./cdpEvmWalletActionProvider"); const schemas_1 = require("./schemas"); const spendPermissionUtils = __importStar(require("./spendPermissionUtils")); const swapUtils = __importStar(require("./swapUtils")); const utils = __importStar(require("../../utils")); // Mock the CDP SDK and utility functions jest.mock("@coinbase/cdp-sdk"); jest.mock("./spendPermissionUtils"); jest.mock("./swapUtils"); jest.mock("../../utils"); describe("CDP EVM Wallet Action Provider", () => { let actionProvider; let mockWalletProvider; let mockCdpClient; let mockAccount; const mockGetTokenDetails = swapUtils.getTokenDetails; const mockRetryWithExponentialBackoff = utils.retryWithExponentialBackoff; beforeEach(() => { jest.clearAllMocks(); mockAccount = { useSpendPermission: jest.fn(), swap: jest.fn(), address: "0x1234567890123456789012345678901234567890", }; mockCdpClient = { evm: { listSpendPermissions: jest.fn(), getAccount: jest.fn(), getSwapPrice: jest.fn(), }, }; mockWalletProvider = { getNetwork: jest.fn(), getAddress: jest.fn(), getClient: jest.fn(), sendTransaction: jest.fn(), waitForTransactionReceipt: jest.fn(), getCdpSdkNetwork: jest.fn(), }; // Default setup for utility functions mockRetryWithExponentialBackoff.mockImplementation(async (fn) => { return await fn(); }); actionProvider = new cdpEvmWalletActionProvider_1.CdpEvmWalletActionProvider(); }); describe("listSpendPermissions", () => { const mockArgs = { smartAccountAddress: "0xabcd1234567890123456789012345678901234567890", }; beforeEach(() => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: "base-sepolia", }); mockWalletProvider.getAddress.mockReturnValue("0x1234567890123456789012345678901234567890"); mockWalletProvider.getClient.mockReturnValue(mockCdpClient); mockWalletProvider.getCdpSdkNetwork.mockReturnValue("base-sepolia"); }); it("should successfully list spend permissions for EVM wallets", async () => { const expectedResult = "Found 2 spend permission(s):\n1. Token: USDC, Allowance: 500, Period: 1800 seconds, Start: 111111, End: 222222\n2. Token: ETH, Allowance: 1000, Period: 3600 seconds, Start: 123456, End: 234567"; spendPermissionUtils.listSpendPermissionsForSpender.mockResolvedValue(expectedResult); const result = await actionProvider.listSpendPermissions(mockWalletProvider, mockArgs); expect(spendPermissionUtils.listSpendPermissionsForSpender).toHaveBeenCalledWith(mockCdpClient, mockArgs.smartAccountAddress, "0x1234567890123456789012345678901234567890"); expect(result).toBe(expectedResult); }); it("should return error message for non-EVM networks", async () => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "svm", networkId: "solana-devnet", }); const result = await actionProvider.listSpendPermissions(mockWalletProvider, mockArgs); expect(result).toBe("Spend permissions are currently only supported on EVM networks."); expect(spendPermissionUtils.listSpendPermissionsForSpender).not.toHaveBeenCalled(); }); it("should handle utility function errors gracefully", async () => { spendPermissionUtils.listSpendPermissionsForSpender.mockResolvedValue("Failed to list spend permissions: Network error"); const result = await actionProvider.listSpendPermissions(mockWalletProvider, mockArgs); expect(result).toBe("Failed to list spend permissions: Network error"); }); it("should validate input schema", () => { const validInput = { smartAccountAddress: "0xabcd1234567890123456789012345678901234567890" }; const invalidInput = { wrongField: "0xabcd1234567890123456789012345678901234567890" }; expect(() => schemas_1.ListSpendPermissionsSchema.parse(validInput)).not.toThrow(); expect(() => schemas_1.ListSpendPermissionsSchema.parse(invalidInput)).toThrow(); }); }); describe("useSpendPermission", () => { const mockArgs = { smartAccountAddress: "0xabcd1234567890123456789012345678901234567890", value: "2500", }; beforeEach(() => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: "base-sepolia", }); mockWalletProvider.getAddress.mockReturnValue("0x1234567890123456789012345678901234567890"); mockWalletProvider.getClient.mockReturnValue(mockCdpClient); mockWalletProvider.getCdpSdkNetwork.mockReturnValue("base-sepolia"); mockCdpClient.evm.getAccount.mockResolvedValue(mockAccount); }); it("should successfully use spend permission for EVM wallets", async () => { const mockPermission = { spender: "0x1234567890123456789012345678901234567890", token: "USDC", allowance: "5000", period: 7200, start: 111111, end: 333333, }; const mockSpendResult = { status: "completed", transactionHash: "0xdef456789", }; spendPermissionUtils.findLatestSpendPermission.mockResolvedValue(mockPermission); mockAccount.useSpendPermission.mockResolvedValue(mockSpendResult); const result = await actionProvider.useSpendPermission(mockWalletProvider, mockArgs); expect(spendPermissionUtils.findLatestSpendPermission).toHaveBeenCalledWith(mockCdpClient, mockArgs.smartAccountAddress, "0x1234567890123456789012345678901234567890"); expect(mockCdpClient.evm.getAccount).toHaveBeenCalledWith({ address: "0x1234567890123456789012345678901234567890", }); expect(mockAccount.useSpendPermission).toHaveBeenCalledWith({ spendPermission: mockPermission, value: BigInt(2500), network: "base-sepolia", }); expect(result).toBe("Successfully spent 2500 tokens using spend permission. Transaction hash: 0xdef456789"); }); it("should handle different network conversions", async () => { const testCases = [ { networkId: "base-sepolia", expected: "base-sepolia" }, { networkId: "base-mainnet", expected: "base" }, { networkId: "ethereum-sepolia", expected: "ethereum-sepolia" }, { networkId: "ethereum-mainnet", expected: "ethereum" }, ]; const mockPermission = { spender: "0x1234", token: "ETH" }; const mockSpendResult = { status: "completed" }; spendPermissionUtils.findLatestSpendPermission.mockResolvedValue(mockPermission); mockAccount.useSpendPermission.mockResolvedValue(mockSpendResult); for (const testCase of testCases) { jest.clearAllMocks(); mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: testCase.networkId, }); mockWalletProvider.getClient.mockReturnValue(mockCdpClient); mockWalletProvider.getCdpSdkNetwork.mockReturnValue(testCase.expected); mockCdpClient.evm.getAccount.mockResolvedValue(mockAccount); await actionProvider.useSpendPermission(mockWalletProvider, mockArgs); expect(mockAccount.useSpendPermission).toHaveBeenCalledWith({ spendPermission: mockPermission, value: BigInt(2500), network: testCase.expected, }); } }); it("should handle unknown networks by passing them as-is", async () => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: "polygon-mainnet", }); mockWalletProvider.getCdpSdkNetwork.mockReturnValue("polygon-mainnet"); const mockPermission = { spender: "0x1234", token: "MATIC" }; const mockSpendResult = { status: "completed" }; spendPermissionUtils.findLatestSpendPermission.mockResolvedValue(mockPermission); mockAccount.useSpendPermission.mockResolvedValue(mockSpendResult); await actionProvider.useSpendPermission(mockWalletProvider, mockArgs); expect(mockAccount.useSpendPermission).toHaveBeenCalledWith({ spendPermission: mockPermission, value: BigInt(2500), network: "polygon-mainnet", }); }); it("should return error message for non-EVM networks", async () => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "svm", networkId: "solana-devnet", }); const result = await actionProvider.useSpendPermission(mockWalletProvider, mockArgs); expect(result).toBe("Spend permissions are currently only supported on EVM networks."); }); it("should handle spend permission not found error", async () => { spendPermissionUtils.findLatestSpendPermission.mockRejectedValue(new Error("No spend permissions found for spender")); const result = await actionProvider.useSpendPermission(mockWalletProvider, mockArgs); expect(result).toBe("Failed to use spend permission: Error: No spend permissions found for spender"); }); it("should handle account creation failure", async () => { spendPermissionUtils.findLatestSpendPermission.mockResolvedValue({ spender: "0x1234", token: "ETH", }); mockCdpClient.evm.getAccount.mockRejectedValue(new Error("Account not found")); const result = await actionProvider.useSpendPermission(mockWalletProvider, mockArgs); expect(result).toBe("Failed to use spend permission: Error: Account not found"); }); it("should handle account use permission failure", async () => { const mockPermission = { spender: "0x1234", token: "ETH" }; spendPermissionUtils.findLatestSpendPermission.mockResolvedValue(mockPermission); mockAccount.useSpendPermission.mockRejectedValue(new Error("Insufficient allowance")); const result = await actionProvider.useSpendPermission(mockWalletProvider, mockArgs); expect(result).toBe("Failed to use spend permission: Error: Insufficient allowance"); }); it("should validate input schema", () => { const validInput = { smartAccountAddress: "0xabcd1234567890123456789012345678901234567890", value: "1000", }; const invalidInput = { smartAccountAddress: "not-an-address", value: -100, }; expect(() => schemas_1.UseSpendPermissionSchema.parse(validInput)).not.toThrow(); expect(() => schemas_1.UseSpendPermissionSchema.parse(invalidInput)).toThrow(); }); }); describe("getSwapPrice", () => { const mockArgs = { fromToken: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // ETH toToken: "0xA0b86991c6218b36c1d19D4a2e9EB0cE3606eB48", // USDC fromAmount: "0.1", slippageBps: 100, }; beforeEach(() => { mockWalletProvider.getClient.mockReturnValue(mockCdpClient); mockWalletProvider.getAddress.mockReturnValue("0x1234567890123456789012345678901234567890"); mockWalletProvider.getCdpSdkNetwork.mockReturnValue("base"); mockGetTokenDetails.mockResolvedValue({ fromTokenDecimals: 18, toTokenDecimals: 6, fromTokenName: "ETH", toTokenName: "USDC", }); }); it("should get swap price quote on base-mainnet", async () => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet", }); mockCdpClient.evm.getSwapPrice.mockResolvedValue({ toAmount: "990000", // 0.99 USDC minToAmount: "980000", // 0.98 USDC liquidityAvailable: true, issues: {}, }); const result = await actionProvider.getSwapPrice(mockWalletProvider, mockArgs); const parsedResult = JSON.parse(result); expect(mockCdpClient.evm.getSwapPrice).toHaveBeenCalledWith({ fromToken: mockArgs.fromToken, toToken: mockArgs.toToken, fromAmount: 100000000000000000n, // 0.1 ETH in wei network: "base", taker: "0x1234567890123456789012345678901234567890", }); expect(parsedResult.success).toBe(true); expect(parsedResult.fromAmount).toBe("0.1"); expect(parsedResult.toAmount).toBe("0.99"); expect(parsedResult.liquidityAvailable).toBe(true); }); it("should get swap price quote on ethereum-mainnet", async () => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: "ethereum-mainnet", }); mockWalletProvider.getCdpSdkNetwork.mockReturnValue("ethereum"); mockCdpClient.evm.getSwapPrice.mockResolvedValue({ toAmount: "990000", minToAmount: "980000", liquidityAvailable: true, issues: {}, }); const result = await actionProvider.getSwapPrice(mockWalletProvider, mockArgs); const parsedResult = JSON.parse(result); expect(mockCdpClient.evm.getSwapPrice).toHaveBeenCalledWith({ fromToken: mockArgs.fromToken, toToken: mockArgs.toToken, fromAmount: 100000000000000000n, network: "ethereum", taker: "0x1234567890123456789012345678901234567890", }); expect(parsedResult.success).toBe(true); }); it("should return error for unsupported networks", async () => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: "base-sepolia", }); const result = await actionProvider.getSwapPrice(mockWalletProvider, mockArgs); const parsedResult = JSON.parse(result); expect(parsedResult.success).toBe(false); expect(parsedResult.error).toContain("CDP Swap API is currently only supported on 'base-mainnet' or 'ethereum-mainnet'"); }); it("should handle swap price API errors", async () => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet", }); mockCdpClient.evm.getSwapPrice.mockRejectedValue(new Error("API Error")); const result = await actionProvider.getSwapPrice(mockWalletProvider, mockArgs); const parsedResult = JSON.parse(result); expect(parsedResult.success).toBe(false); expect(parsedResult.error).toContain("Error fetching swap price: Error: API Error"); }); }); describe("swap", () => { const mockArgs = { fromToken: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // ETH toToken: "0xA0b86991c6218b36c1d19D4a2e9EB0cE3606eB48", // USDC fromAmount: "0.1", slippageBps: 100, }; beforeEach(() => { mockWalletProvider.getClient.mockReturnValue(mockCdpClient); mockWalletProvider.getAddress.mockReturnValue("0x1234567890123456789012345678901234567890"); mockWalletProvider.getCdpSdkNetwork.mockReturnValue("base"); mockCdpClient.evm.getAccount.mockResolvedValue(mockAccount); mockWalletProvider.waitForTransactionReceipt.mockResolvedValue({ status: "success" }); mockGetTokenDetails.mockResolvedValue({ fromTokenDecimals: 18, toTokenDecimals: 6, fromTokenName: "ETH", toTokenName: "USDC", }); }); it("should execute swap on base-mainnet", async () => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet", }); mockCdpClient.evm.getSwapPrice.mockResolvedValue({ liquidityAvailable: true, issues: {}, toAmount: "990000", // 0.99 USDC minToAmount: "980000", // 0.98 USDC }); mockAccount.swap.mockResolvedValue({ transactionHash: "0xswap789" }); const result = await actionProvider.swap(mockWalletProvider, mockArgs); const parsedResult = JSON.parse(result); expect(mockCdpClient.evm.getAccount).toHaveBeenCalledWith({ address: "0x1234567890123456789012345678901234567890", }); expect(mockAccount.swap).toHaveBeenCalledWith({ network: "base", fromToken: mockArgs.fromToken, toToken: mockArgs.toToken, fromAmount: 100000000000000000n, // 0.1 ETH in wei slippageBps: 100, signerAddress: "0x1234567890123456789012345678901234567890", }); expect(parsedResult.success).toBe(true); expect(parsedResult.transactionHash).toBe("0xswap789"); expect(parsedResult.fromAmount).toBe("0.1"); expect(parsedResult.toAmount).toBe("0.99"); }); it("should return error for unsupported networks", async () => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: "base-sepolia", }); const result = await actionProvider.swap(mockWalletProvider, mockArgs); const parsedResult = JSON.parse(result); expect(parsedResult.success).toBe(false); expect(parsedResult.error).toContain("CDP Swap API is currently only supported on 'base-mainnet' or 'ethereum-mainnet'"); }); it("should return error when liquidity is not available", async () => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet", }); mockCdpClient.evm.getSwapPrice.mockResolvedValue({ liquidityAvailable: false, issues: {}, toAmount: "0", minToAmount: "0", }); const result = await actionProvider.swap(mockWalletProvider, mockArgs); const parsedResult = JSON.parse(result); expect(parsedResult.success).toBe(false); expect(parsedResult.error).toContain("No liquidity available to swap"); }); it("should return error when balance is insufficient", async () => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet", }); mockCdpClient.evm.getSwapPrice.mockResolvedValue({ liquidityAvailable: true, issues: { balance: { currentBalance: "50000000000000000", // 0.05 ETH }, }, toAmount: "990000", minToAmount: "980000", }); const result = await actionProvider.swap(mockWalletProvider, mockArgs); const parsedResult = JSON.parse(result); expect(parsedResult.success).toBe(false); expect(parsedResult.error).toContain("Balance is not enough to perform swap"); expect(parsedResult.error).toContain("but only have 0.05 ETH"); }); it("should handle approval transaction when allowance is insufficient", async () => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet", }); mockCdpClient.evm.getSwapPrice.mockResolvedValue({ liquidityAvailable: true, issues: { allowance: { requiredAllowance: "100000000000000000", currentAllowance: "0", }, }, toAmount: "990000", minToAmount: "980000", }); mockWalletProvider.sendTransaction.mockResolvedValue("0xapproval123"); mockWalletProvider.waitForTransactionReceipt.mockResolvedValueOnce({ status: "success" }); // For approval mockWalletProvider.waitForTransactionReceipt.mockResolvedValueOnce({ status: "success" }); // For swap mockAccount.swap.mockResolvedValue({ transactionHash: "0xswap789" }); const result = await actionProvider.swap(mockWalletProvider, mockArgs); const parsedResult = JSON.parse(result); expect(mockWalletProvider.sendTransaction).toHaveBeenCalled(); expect(parsedResult.success).toBe(true); expect(parsedResult.approvalTxHash).toBe("0xapproval123"); expect(parsedResult.transactionHash).toBe("0xswap789"); }); it("should handle swap execution errors", async () => { mockWalletProvider.getNetwork.mockReturnValue({ protocolFamily: "evm", networkId: "base-mainnet", }); mockCdpClient.evm.getSwapPrice.mockResolvedValue({ liquidityAvailable: true, issues: {}, toAmount: "990000", minToAmount: "980000", }); mockAccount.swap.mockRejectedValue(new Error("Swap execution failed")); const result = await actionProvider.swap(mockWalletProvider, mockArgs); const parsedResult = JSON.parse(result); expect(parsedResult.success).toBe(false); expect(parsedResult.error).toContain("Swap failed: Error: Swap execution failed"); }); }); describe("supportsNetwork", () => { it("should return true for EVM networks", () => { const evmNetwork = { protocolFamily: "evm", networkId: "base-sepolia" }; expect(actionProvider.supportsNetwork(evmNetwork)).toBe(true); }); it("should return false for non-EVM networks", () => { const svmNetwork = { protocolFamily: "svm", networkId: "solana-devnet" }; expect(actionProvider.supportsNetwork(svmNetwork)).toBe(false); }); }); describe("SwapSchema", () => { it("should validate correct swap input", () => { const validInput = { fromToken: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", toToken: "0xA0b86991c6218b36c1d19D4a2e9EB0cE3606eB48", fromAmount: "0.1", }; const result = schemas_1.SwapSchema.safeParse(validInput); expect(result.success).toBe(true); expect(result.data).toEqual({ ...validInput, slippageBps: 100 }); }); it("should validate swap input with optional slippageBps", () => { const validInput = { fromToken: "0xA0b86991c6218b36c1d19D4a2e9EB0cE3606eB48", toToken: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", fromAmount: "100", slippageBps: 50, }; const result = schemas_1.SwapSchema.safeParse(validInput); expect(result.success).toBe(true); expect(result.data).toEqual(validInput); }); it("should fail validation when missing required fields", () => { const invalidInput = { fromToken: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // missing toToken and fromAmount }; const result = schemas_1.SwapSchema.safeParse(invalidInput); expect(result.success).toBe(false); }); }); });