@coinbase/agentkit
Version:
Coinbase AgentKit core primitives
466 lines (465 loc) • 23.3 kB
JavaScript
"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 cdpSmartWalletActionProvider_1 = require("./cdpSmartWalletActionProvider");
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 Smart Wallet Action Provider", () => {
let actionProvider;
let mockWalletProvider;
let mockCdpClient;
let mockSmartAccount;
const mockGetTokenDetails = swapUtils.getTokenDetails;
const mockRetryWithExponentialBackoff = utils.retryWithExponentialBackoff;
beforeEach(() => {
jest.clearAllMocks();
mockSmartAccount = {
useSpendPermission: jest.fn(),
swap: jest.fn(),
address: "0x1234567890123456789012345678901234567890",
};
mockCdpClient = {
evm: {
listSpendPermissions: jest.fn(),
getSwapPrice: jest.fn(),
},
};
mockWalletProvider = {
getNetwork: jest.fn(),
getAddress: jest.fn(),
getClient: jest.fn(),
smartAccount: mockSmartAccount,
ownerAccount: {
type: "server",
address: "0xowner123",
},
sendTransaction: jest.fn(),
waitForTransactionReceipt: jest.fn(),
getPaymasterUrl: jest.fn(),
getCdpSdkNetwork: jest.fn(),
};
// Default setup for utility functions
mockRetryWithExponentialBackoff.mockImplementation(async (fn) => {
return await fn();
});
actionProvider = new cdpSmartWalletActionProvider_1.CdpSmartWalletActionProvider();
});
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 networks", async () => {
const expectedResult = "Found 1 spend permission(s):\n1. 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 validate input schema", () => {
const validInput = { smartAccountAddress: "0xabcd1234567890123456789012345678901234567890" };
const invalidInput = { invalidField: "invalid" };
expect(() => schemas_1.ListSpendPermissionsSchema.parse(validInput)).not.toThrow();
expect(() => schemas_1.ListSpendPermissionsSchema.parse(invalidInput)).toThrow();
});
});
describe("useSpendPermission", () => {
const mockArgs = {
smartAccountAddress: "0xabcd1234567890123456789012345678901234567890",
value: "1000",
};
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 use spend permission for EVM networks", async () => {
const mockPermission = {
spender: "0x1234567890123456789012345678901234567890",
token: "ETH",
allowance: "1000",
period: 3600,
start: 123456,
end: 234567,
};
const mockSpendResult = {
status: "completed",
transactionHash: "0xabcd1234",
};
spendPermissionUtils.findLatestSpendPermission.mockResolvedValue(mockPermission);
mockSmartAccount.useSpendPermission.mockResolvedValue(mockSpendResult);
const result = await actionProvider.useSpendPermission(mockWalletProvider, mockArgs);
expect(spendPermissionUtils.findLatestSpendPermission).toHaveBeenCalledWith(mockCdpClient, mockArgs.smartAccountAddress, "0x1234567890123456789012345678901234567890");
expect(mockSmartAccount.useSpendPermission).toHaveBeenCalledWith({
spendPermission: mockPermission,
value: BigInt(1000),
network: "base-sepolia",
});
expect(result).toBe("Successfully spent 1000 tokens using spend permission. Status: completed");
});
it("should handle base-mainnet network conversion", async () => {
mockWalletProvider.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "base-mainnet",
});
mockWalletProvider.getCdpSdkNetwork.mockReturnValue("base");
const mockPermission = { spender: "0x1234", token: "ETH" };
const mockSpendResult = { status: "completed" };
spendPermissionUtils.findLatestSpendPermission.mockResolvedValue(mockPermission);
mockSmartAccount.useSpendPermission.mockResolvedValue(mockSpendResult);
await actionProvider.useSpendPermission(mockWalletProvider, mockArgs);
expect(mockSmartAccount.useSpendPermission).toHaveBeenCalledWith({
spendPermission: mockPermission,
value: BigInt(1000),
network: "base",
});
});
it("should throw error for unsupported networks", async () => {
mockWalletProvider.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "ethereum-mainnet",
});
mockWalletProvider.getCdpSdkNetwork.mockImplementation(() => {
throw new Error("Unsupported network for smart wallets: ethereum-mainnet");
});
await expect(actionProvider.useSpendPermission(mockWalletProvider, mockArgs)).rejects.toThrow("Unsupported network for smart wallets: ethereum-mainnet");
});
it("should return error message for non-EVM networks", async () => {
mockWalletProvider.getNetwork.mockReturnValue({
protocolFamily: "svm",
networkId: "solana-devnet",
});
await expect(actionProvider.useSpendPermission(mockWalletProvider, mockArgs)).rejects.toThrow("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"));
await expect(actionProvider.useSpendPermission(mockWalletProvider, mockArgs)).rejects.toThrow("Failed to use spend permission: Error: No spend permissions found");
});
it("should handle smart account use permission failure", async () => {
const mockPermission = { spender: "0x1234", token: "ETH" };
spendPermissionUtils.findLatestSpendPermission.mockResolvedValue(mockPermission);
mockSmartAccount.useSpendPermission.mockRejectedValue(new Error("Transaction failed"));
await expect(actionProvider.useSpendPermission(mockWalletProvider, mockArgs)).rejects.toThrow("Failed to use spend permission: Error: Transaction failed");
});
it("should validate input schema", () => {
const validInput = {
smartAccountAddress: "0xabcd1234567890123456789012345678901234567890",
value: "1000",
};
const invalidInput = {
wrongField: "0xabcd1234567890123456789012345678901234567890",
// Missing required fields
};
expect(() => schemas_1.UseSpendPermissionSchema.parse(validInput)).not.toThrow();
expect(() => schemas_1.UseSpendPermissionSchema.parse(invalidInput)).toThrow();
});
});
describe("getSwapPrice", () => {
const mockArgs = {
fromToken: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // ETH
toToken: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", // USDC on Base
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 base-sepolia", async () => {
mockWalletProvider.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "base-sepolia",
});
mockWalletProvider.getCdpSdkNetwork.mockReturnValue("base-sepolia");
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: "base-sepolia",
taker: "0x1234567890123456789012345678901234567890",
});
expect(parsedResult.success).toBe(true);
});
it("should return error for unsupported networks", async () => {
mockWalletProvider.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "ethereum-mainnet",
});
const result = await actionProvider.getSwapPrice(mockWalletProvider, mockArgs);
const parsedResult = JSON.parse(result);
expect(parsedResult.success).toBe(false);
expect(parsedResult.error).toContain("CDP Swap API for smart wallets is currently only supported on Base networks");
});
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: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", // USDC on Base
fromAmount: "0.1",
slippageBps: 100,
};
beforeEach(() => {
mockWalletProvider.getClient.mockReturnValue(mockCdpClient);
mockWalletProvider.getAddress.mockReturnValue("0x1234567890123456789012345678901234567890");
mockWalletProvider.getCdpSdkNetwork.mockReturnValue("base");
mockWalletProvider.waitForTransactionReceipt.mockResolvedValue({ status: "complete" });
mockWalletProvider.getPaymasterUrl.mockReturnValue("https://paymaster.example");
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
});
mockSmartAccount.swap.mockResolvedValue({ userOpHash: "0xswap789" });
const result = await actionProvider.swap(mockWalletProvider, mockArgs);
const parsedResult = JSON.parse(result);
expect(mockSmartAccount.swap).toHaveBeenCalledWith({
network: "base",
fromToken: mockArgs.fromToken,
toToken: mockArgs.toToken,
fromAmount: 100000000000000000n, // 0.1 ETH in wei
slippageBps: 100,
paymasterUrl: "https://paymaster.example",
signerAddress: "0xowner123",
});
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: "ethereum-mainnet",
});
const result = await actionProvider.swap(mockWalletProvider, mockArgs);
const parsedResult = JSON.parse(result);
expect(parsedResult.success).toBe(false);
expect(parsedResult.error).toContain("CDP Swap API for smart wallets is currently only supported on Base networks");
});
it("should throw error for local owner account", async () => {
mockWalletProvider.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "base-mainnet",
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mockWalletProvider.ownerAccount = {
type: "local",
address: "0xlocal123",
};
await expect(actionProvider.swap(mockWalletProvider, mockArgs)).rejects.toThrow("Smart wallet owner account is not a CDP server account");
});
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: "complete" }); // For approval
mockWalletProvider.waitForTransactionReceipt.mockResolvedValueOnce({ status: "complete" }); // For swap
mockSmartAccount.swap.mockResolvedValue({ userOpHash: "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",
});
mockSmartAccount.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 any network", () => {
const evmNetwork = { protocolFamily: "evm", networkId: "base-sepolia" };
const svmNetwork = { protocolFamily: "svm", networkId: "solana-devnet" };
expect(actionProvider.supportsNetwork(evmNetwork)).toBe(true);
expect(actionProvider.supportsNetwork(svmNetwork)).toBe(true);
});
});
});