diff --git a/cdp-agentkit-core/typescript/src/action_providers/weth/constants.ts b/cdp-agentkit-core/typescript/src/action_providers/weth/constants.ts new file mode 100644 index 000000000..d4d7fcd14 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/weth/constants.ts @@ -0,0 +1,27 @@ +export const WETH_ADDRESS = "0x4200000000000000000000000000000000000006"; + +export const WETH_ABI = [ + { + inputs: [], + name: "deposit", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + name: "account", + type: "address", + }, + ], + name: "balanceOf", + outputs: [ + { + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; diff --git a/cdp-agentkit-core/typescript/src/action_providers/weth/index.ts b/cdp-agentkit-core/typescript/src/action_providers/weth/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/cdp-agentkit-core/typescript/src/action_providers/weth/schemas.ts b/cdp-agentkit-core/typescript/src/action_providers/weth/schemas.ts new file mode 100644 index 000000000..33a96a3d9 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/weth/schemas.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const WrapEthSchema = z + .object({ + amountToWrap: z.string().describe("Amount of ETH to wrap in wei"), + }) + .strip() + .describe("Instructions for wrapping ETH to WETH"); diff --git a/cdp-agentkit-core/typescript/src/action_providers/weth/wethActionProvider.test.ts b/cdp-agentkit-core/typescript/src/action_providers/weth/wethActionProvider.test.ts new file mode 100644 index 000000000..0286ee05b --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/weth/wethActionProvider.test.ts @@ -0,0 +1,112 @@ +import { WrapEthSchema } from "./schemas"; +import { EvmWalletProvider } from "../../wallet_providers"; +import { encodeFunctionData } from "viem"; +import { WETH_ABI, WETH_ADDRESS } from "./constants"; +import { wethActionProvider } from "./wethActionProvider"; + +const MOCK_AMOUNT = "15"; +const MOCK_ADDRESS = "0x1234567890123456789012345678901234543210"; + +describe("Wrap Eth Schema", () => { + it("should successfully parse valid input", () => { + const validInput = { + amountToWrap: MOCK_AMOUNT, + }; + + const result = WrapEthSchema.safeParse(validInput); + + expect(result.success).toBe(true); + expect(result.data).toEqual(validInput); + }); + + it("should fail parsing empty input", () => { + const emptyInput = {}; + const result = WrapEthSchema.safeParse(emptyInput); + + expect(result.success).toBe(false); + }); +}); + +describe("Wrap Eth Action", () => { + let mockWallet: jest.Mocked; + const actionProvider = wethActionProvider(); + + beforeEach(async () => { + mockWallet = { + getAddress: jest.fn().mockReturnValue(MOCK_ADDRESS), + sendTransaction: jest.fn(), + waitForTransactionReceipt: jest.fn(), + } as unknown as jest.Mocked; + }); + + it("should successfully respond", async () => { + const args = { + amountToWrap: MOCK_AMOUNT, + }; + + const hash = "0x1234567890123456789012345678901234567890"; + mockWallet.sendTransaction.mockResolvedValue(hash); + + const response = await actionProvider.wrapEth(mockWallet, args); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith({ + to: WETH_ADDRESS, + data: encodeFunctionData({ + abi: WETH_ABI, + functionName: "deposit", + }), + value: BigInt(MOCK_AMOUNT), + }); + expect(response).toContain(`Wrapped ETH with transaction hash: ${hash}`); + }); + + it("should fail with an error", async () => { + const args = { + amountToWrap: MOCK_AMOUNT, + }; + + const error = new Error("Failed to wrap ETH"); + mockWallet.sendTransaction.mockRejectedValue(error); + + const response = await actionProvider.wrapEth(mockWallet, args); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith({ + to: WETH_ADDRESS, + data: encodeFunctionData({ + abi: WETH_ABI, + functionName: "deposit", + }), + value: BigInt(MOCK_AMOUNT), + }); + + expect(response).toContain(`Error wrapping ETH: ${error}`); + }); +}); + +describe("supportsNetwork", () => { + const actionProvider = wethActionProvider(); + + it("should return true for base-mainnet", () => { + const result = actionProvider.supportsNetwork({ + protocolFamily: "evm", + networkId: "base-mainnet", + }); + expect(result).toBe(true); + }); + + it("should return true for base-sepolia", () => { + const result = actionProvider.supportsNetwork({ + protocolFamily: "evm", + networkId: "base-sepolia", + }); + expect(result).toBe(true); + }); + + it("should return false for non-base networks", () => { + const result = actionProvider.supportsNetwork({ + protocolFamily: "evm", + networkId: "ethereum-mainnet", + }); + expect(result).toBe(false); + }); +}); diff --git a/cdp-agentkit-core/typescript/src/action_providers/weth/wethActionProvider.ts b/cdp-agentkit-core/typescript/src/action_providers/weth/wethActionProvider.ts new file mode 100644 index 000000000..cf2b651ed --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/weth/wethActionProvider.ts @@ -0,0 +1,80 @@ +import { z } from "zod"; +import { ActionProvider } from "../action_provider"; +import { Network } from "../../wallet_providers/wallet_provider"; +import { CreateAction } from "../action_decorator"; +import { WrapEthSchema } from "./schemas"; +import { WETH_ABI, WETH_ADDRESS } from "./constants"; +import { encodeFunctionData, Hex } from "viem"; +import { EvmWalletProvider } from "../../wallet_providers"; + +/** + * WethActionProvider is an action provider for WETH. + */ +export class WethActionProvider extends ActionProvider { + /** + * Constructor for the WethActionProvider. + */ + constructor() { + super("weth", []); + } + + /** + * Wraps ETH to WETH. + * + * @param walletProvider - The wallet provider to use for the action. + * @param args - The input arguments for the action. + * @returns A message containing the transaction hash. + */ + @CreateAction({ + name: "wrap_eth", + description: ` + This tool can only be used to wrap ETH to WETH. +Do not use this tool for any other purpose, or trading other assets. + +Inputs: +- Amount of ETH to wrap. + +Important notes: +- The amount is a string and cannot have any decimal points, since the unit of measurement is wei. +- Make sure to use the exact amount provided, and if there's any doubt, check by getting more information before continuing with the action. +- 1 wei = 0.000000000000000001 WETH +- Minimum purchase amount is 100000000000000 wei (0.0000001 WETH) +- Only supported on the following networks: + - Base Sepolia (ie, 'base-sepolia') + - Base Mainnet (ie, 'base', 'base-mainnet') +`, + schema: WrapEthSchema, + }) + async wrapEth( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const hash = await walletProvider.sendTransaction({ + to: WETH_ADDRESS as Hex, + data: encodeFunctionData({ + abi: WETH_ABI, + functionName: "deposit", + }), + value: BigInt(args.amountToWrap), + }); + + await walletProvider.waitForTransactionReceipt(hash); + + return `Wrapped ETH with transaction hash: ${hash}`; + } catch (error) { + return `Error wrapping ETH: ${error}`; + } + } + + /** + * Checks if the Weth action provider supports the given network. + * + * @param network - The network to check. + * @returns True if the Weth action provider supports the network, false otherwise. + */ + supportsNetwork = (network: Network) => + network.networkId === "base-mainnet" || network.networkId === "base-sepolia"; +} + +export const wethActionProvider = () => new WethActionProvider();