From 068aca41365f7a635c5be6eb7196e3a6df4b17de Mon Sep 17 00:00:00 2001 From: Christopher Gerber Date: Fri, 31 Jan 2025 00:41:26 -0800 Subject: [PATCH 1/4] first pass adding deploy contract cdp action --- .../action_providers/cdp/cdpActionProvider.ts | 66 +++++++++++++++++-- .../src/action_providers/cdp/schemas.ts | 30 +++++++-- .../wallet_providers/cdp_wallet_provider.ts | 29 +++++++- 3 files changed, 113 insertions(+), 12 deletions(-) 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 1b6681368..dec56c9bd 100644 --- a/cdp-agentkit-core/typescript/src/action_providers/cdp/cdpActionProvider.ts +++ b/cdp-agentkit-core/typescript/src/action_providers/cdp/cdpActionProvider.ts @@ -1,10 +1,18 @@ +import { ExternalAddress } from "@coinbase/coinbase-sdk"; import { z } from "zod"; -import { ActionProvider } from "../action_provider"; -import { CdpWalletProvider } from "../../wallet_providers"; + import { CreateAction } from "../action_decorator"; -import { ExternalAddress } from "@coinbase/coinbase-sdk"; -import { AddressReputationSchema, DeployTokenSchema, RequestFaucetFundsSchema } from "./schemas"; +import { ActionProvider } from "../action_provider"; import { Network } from "../../network"; +import { CdpWalletProvider } from "../../wallet_providers"; + +import { SolidityVersions } from "./constants"; +import { + AddressReputationSchema, + DeployContractSchema, + DeployTokenSchema, + RequestFaucetFundsSchema, +} from "./schemas"; /** * CdpActionProvider is an action provider for Cdp. @@ -43,6 +51,56 @@ This tool checks the reputation of an address on a given network. It takes: } } + /** + * Deploys a contract. + * + * @param walletProvider - The wallet provider to deploy the contract from + * @param args - The input arguments for the action + * @returns A message containing the deployed contract address and details + */ + @CreateAction({ + name: "deploy_contract", + description: ` +Deploys smart contract with required args: solidity version (string), solidity input json (string), contract name (string), and optional constructor args (Dict[str, Any]) + +Input json structure: +{"language":"Solidity","settings":{"remappings":[],"outputSelection":{"*":{"*":["abi","evm.bytecode"]}}},"sources":{}} + +You must set the outputSelection to {"*":{"*":["abi","evm.bytecode"]}} in the settings. The solidity version must be >= 0.8.0 and <= 0.8.28. + +Sources should contain one or more contracts with the following structure: +{"contract_name.sol":{"content":"contract code"}} + +The contract code should be escaped. Contracts cannot import from external contracts but can import from one another. + +Constructor args are required if the contract has a constructor. They are a key-value +map where the key is the arg name and the value is the arg value. Encode uint/int/bytes/string/address values as strings, boolean values as true/false. For arrays/tuples, encode based on contained type.`, + schema: DeployContractSchema, + }) + async deployContract( + walletProvider: CdpWalletProvider, + args: z.infer, + ): Promise { + try { + const solidityVersion = SolidityVersions[args.solidityVersion]; + + const contract = await walletProvider.deployContract({ + solidityVersion: solidityVersion, + solidityInputJson: args.solidityInputJson, + contractName: args.contractName, + constructorArgs: args.constructorArgs ?? {}, + }); + + const result = await contract.wait(); + + return `Deployed contract ${args.contractName} at address ${result.getContractAddress()}. Transaction link: ${result + .getTransaction()! + .getTransactionLink()}`; + } catch (error) { + return `Error deploying contract: ${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 e1a56470e..b6eec1a5e 100644 --- a/cdp-agentkit-core/typescript/src/action_providers/cdp/schemas.ts +++ b/cdp-agentkit-core/typescript/src/action_providers/cdp/schemas.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { SolidityVersions } from "./constants"; /** * Input schema for address reputation check. @@ -13,16 +14,24 @@ export const AddressReputationSchema = z }) .strip() .describe("Input schema for address reputation check"); - + /** - * Input schema for request faucet funds action. + * Input schema for deploy contract action. */ -export const RequestFaucetFundsSchema = z +export const DeployContractSchema = z .object({ - assetId: z.string().optional().describe("The optional asset ID to request from faucet"), + solidityVersion: z + .enum(Object.keys(SolidityVersions) as [string, ...string[]]) + .describe("The solidity compiler version"), + solidityInputJson: z.string().describe("The input json for the solidity compiler"), + contractName: z.string().describe("The name of the contract class to be deployed"), + constructorArgs: z + .record(z.string(), z.any()) + .describe("The constructor arguments for the contract") + .optional(), }) .strip() - .describe("Instructions for requesting faucet funds"); + .describe("Instructions for deploying an arbitrary contract"); /** * Input schema for deploy token action. @@ -35,3 +44,14 @@ export const DeployTokenSchema = z }) .strip() .describe("Instructions for deploying a token"); + + +/** + * Input schema for request faucet funds action. + */ +export const RequestFaucetFundsSchema = z + .object({ + assetId: z.string().optional().describe("The optional asset ID to request from faucet"), + }) + .strip() + .describe("Instructions for requesting faucet funds"); \ No newline at end of file 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 5a7cea90b..1e2056d0f 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 @@ -184,11 +184,15 @@ export class CdpWalletProvider extends EvmWalletProvider { /** * Gets the balance of the wallet. * - * @returns The balance of the wallet. + * @returns The balance of the wallet in wei */ async getBalance(): Promise { - // TODO: Implement - throw Error("Unimplemented"); + if (!this.#cdpWallet) { + throw new Error("Wallet not initialized"); + } + + const balance = await this.#cdpWallet.getBalance('eth'); + return BigInt(balance.mul(10 ** 18).toString()); } /** @@ -227,4 +231,23 @@ export class CdpWalletProvider extends EvmWalletProvider { return this.#cdpWallet.deployToken(options); } + + /** + * Deploys a contract. + * + * @param options - The options for contract deployment + * @returns The deployed contract + */ + async deployContract(options: { + solidityVersion: string; + solidityInputJson: string; + contractName: string; + constructorArgs: Record; + }): Promise { + if (!this.#cdpWallet) { + throw new Error("Wallet not initialized"); + } + + return this.#cdpWallet.deployContract(options); + } } From e4ae894be3f0bbc6dd7d961fe7bb6bb7cd645a18 Mon Sep 17 00:00:00 2001 From: Christopher Gerber Date: Fri, 31 Jan 2025 00:49:32 -0800 Subject: [PATCH 2/4] linting --- .../typescript/src/action_providers/cdp/schemas.ts | 5 ++--- .../src/wallet_providers/cdp_wallet_provider.ts | 12 +++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) 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 b6eec1a5e..eaa02b5f3 100644 --- a/cdp-agentkit-core/typescript/src/action_providers/cdp/schemas.ts +++ b/cdp-agentkit-core/typescript/src/action_providers/cdp/schemas.ts @@ -14,7 +14,7 @@ export const AddressReputationSchema = z }) .strip() .describe("Input schema for address reputation check"); - + /** * Input schema for deploy contract action. */ @@ -45,7 +45,6 @@ export const DeployTokenSchema = z .strip() .describe("Instructions for deploying a token"); - /** * Input schema for request faucet funds action. */ @@ -54,4 +53,4 @@ export const RequestFaucetFundsSchema = z assetId: z.string().optional().describe("The optional asset ID to request from faucet"), }) .strip() - .describe("Instructions for requesting faucet funds"); \ No newline at end of file + .describe("Instructions for requesting faucet funds"); 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 1e2056d0f..1eb05693e 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 @@ -191,7 +191,7 @@ export class CdpWalletProvider extends EvmWalletProvider { throw new Error("Wallet not initialized"); } - const balance = await this.#cdpWallet.getBalance('eth'); + const balance = await this.#cdpWallet.getBalance("eth"); return BigInt(balance.mul(10 ** 18).toString()); } @@ -234,9 +234,15 @@ export class CdpWalletProvider extends EvmWalletProvider { /** * Deploys a contract. - * + * * @param options - The options for contract deployment - * @returns The deployed contract + * @param options.solidityVersion - The version of the Solidity compiler to use (e.g. "0.8.0+commit.c7dfd78e") + * @param options.solidityInputJson - The JSON input for the Solidity compiler containing contract source and settings + * @param options.contractName - The name of the contract to deploy + * @param options.constructorArgs - Key-value map of constructor args + * + * @returns A Promise that resolves to the deployed contract instance + * @throws Error if wallet is not initialized */ async deployContract(options: { solidityVersion: string; From 78edda0d8ee38fac20c0577545f93559ae8b5d6a Mon Sep 17 00:00:00 2001 From: Christopher Gerber Date: Fri, 31 Jan 2025 00:50:44 -0800 Subject: [PATCH 3/4] the lost file --- .../src/action_providers/cdp/constants.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 cdp-agentkit-core/typescript/src/action_providers/cdp/constants.ts diff --git a/cdp-agentkit-core/typescript/src/action_providers/cdp/constants.ts b/cdp-agentkit-core/typescript/src/action_providers/cdp/constants.ts new file mode 100644 index 000000000..68e49edfd --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/cdp/constants.ts @@ -0,0 +1,31 @@ +export const SolidityVersions = { + "0.8.28": "0.8.28+commit.7893614a", + "0.8.27": "0.8.27+commit.40a35a09", + "0.8.26": "0.8.26+commit.8a97fa7a", + "0.8.25": "0.8.25+commit.b61c2a91", + "0.8.24": "0.8.24+commit.e11b9ed9", + "0.8.23": "0.8.23+commit.f704f362", + "0.8.22": "0.8.22+commit.4fc1097e", + "0.8.21": "0.8.21+commit.d9974bed", + "0.8.20": "0.8.20+commit.a1b79de6", + "0.8.19": "0.8.19+commit.7dd6d404", + "0.8.18": "0.8.18+commit.87f61d96", + "0.8.17": "0.8.17+commit.8df45f5f", + "0.8.16": "0.8.16+commit.07a7930e", + "0.8.15": "0.8.15+commit.e14f2714", + "0.8.14": "0.8.14+commit.80d49f37", + "0.8.13": "0.8.13+commit.abaa5c0e", + "0.8.12": "0.8.12+commit.f00d7308", + "0.8.11": "0.8.11+commit.d7f03943", + "0.8.10": "0.8.10+commit.fc410830", + "0.8.9": "0.8.9+commit.e5eed63a", + "0.8.8": "0.8.8+commit.dddeac2f", + "0.8.7": "0.8.7+commit.e28d00a7", + "0.8.6": "0.8.6+commit.11564f7e", + "0.8.5": "0.8.5+commit.a4f2e591", + "0.8.4": "0.8.4+commit.c7e474f2", + "0.8.3": "0.8.3+commit.8d00100c", + "0.8.2": "0.8.2+commit.661d1103", + "0.8.1": "0.8.1+commit.df193b15", + "0.8.0": "0.8.0+commit.c7dfd78e", +} as const; From cba9e2affbdf7faaf0003d1de8e48f1818e230f7 Mon Sep 17 00:00:00 2001 From: Christopher Gerber Date: Fri, 31 Jan 2025 01:52:42 -0800 Subject: [PATCH 4/4] tests --- .../cdp/cdpActionProvider.test.ts | 82 +++++++++++++++++-- 1 file changed, 73 insertions(+), 9 deletions(-) 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 ce9d847f9..661b97df1 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,7 @@ import { CdpWalletProvider } from "../../wallet_providers"; import { CdpActionProvider } from "./cdpActionProvider"; import { AddressReputationSchema, RequestFaucetFundsSchema } from "./schemas"; +import { SmartContract } from "@coinbase/coinbase-sdk"; // Mock the entire module jest.mock("@coinbase/coinbase-sdk"); @@ -59,6 +60,7 @@ describe("CDP Action Provider", () => { let actionProvider: CdpActionProvider; // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockExternalAddressInstance: jest.Mocked; + let mockWallet: jest.Mocked; beforeEach(() => { // Reset all mocks before each test @@ -72,6 +74,13 @@ describe("CDP Action Provider", () => { // 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", () => { @@ -112,8 +121,6 @@ describe("CDP Action Provider", () => { }); describe("deployToken", () => { - let mockWallet: jest.Mocked; - beforeEach(() => { mockWallet = { deployToken: jest.fn().mockResolvedValue({ @@ -160,14 +167,7 @@ describe("CDP Action Provider", () => { }); describe("faucet", () => { - let mockWallet: jest.Mocked; - beforeEach(() => { - mockWallet = { - getAddress: jest.fn().mockReturnValue("0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83"), - getNetwork: jest.fn().mockReturnValue({ networkId: "base-sepolia" }), - } as unknown as jest.Mocked; - mockExternalAddressInstance.faucet.mockResolvedValue({ wait: jest.fn().mockResolvedValue({ getTransactionLink: jest.fn().mockReturnValue("tx-link"), @@ -212,4 +212,68 @@ describe("CDP Action Provider", () => { expect(result).toBe(`Error requesting faucet funds: ${error}`); }); }); + + describe("deployContract", () => { + const CONTRACT_ADDRESS = "0x123456789abcdef"; + const TRANSACTION_LINK = "https://etherscan.io/tx/0xghijkl987654321"; + const MOCK_CONTRACT_NAME = "Test Contract"; + const MOCK_SOLIDITY_VERSION = "0.8.0"; + const MOCK_SOLIDITY_INPUT_JSON = "{}"; + const MOCK_CONSTRUCTOR_ARGS = { arg1: "value1", arg2: "value2" }; + + beforeEach(() => { + mockWallet.deployContract.mockResolvedValue({ + wait: jest.fn().mockResolvedValue({ + getContractAddress: jest.fn().mockReturnValue(CONTRACT_ADDRESS), + getTransaction: jest.fn().mockReturnValue({ + getTransactionLink: jest.fn().mockReturnValue(TRANSACTION_LINK), + }), + }), + } as unknown as SmartContract); + }); + + it("should successfully deploy a contract", async () => { + const args = { + solidityVersion: MOCK_SOLIDITY_VERSION, + solidityInputJson: MOCK_SOLIDITY_INPUT_JSON, + contractName: MOCK_CONTRACT_NAME, + constructorArgs: MOCK_CONSTRUCTOR_ARGS, + }; + + const response = await actionProvider.deployContract(mockWallet, args); + + expect(mockWallet.deployContract).toHaveBeenCalledWith({ + solidityVersion: "0.8.0+commit.c7dfd78e", + solidityInputJson: MOCK_SOLIDITY_INPUT_JSON, + contractName: MOCK_CONTRACT_NAME, + constructorArgs: MOCK_CONSTRUCTOR_ARGS, + }); + expect(response).toContain( + `Deployed contract ${MOCK_CONTRACT_NAME} at address ${CONTRACT_ADDRESS}`, + ); + expect(response).toContain(`Transaction link: ${TRANSACTION_LINK}`); + }); + + it("should handle deployment errors", async () => { + const args = { + solidityVersion: MOCK_SOLIDITY_VERSION, + solidityInputJson: MOCK_SOLIDITY_INPUT_JSON, + contractName: MOCK_CONTRACT_NAME, + constructorArgs: MOCK_CONSTRUCTOR_ARGS, + }; + + const error = new Error("An error has occurred"); + mockWallet.deployContract.mockRejectedValue(error); + + const response = await actionProvider.deployContract(mockWallet, args); + + expect(mockWallet.deployContract).toHaveBeenCalledWith({ + solidityVersion: "0.8.0+commit.c7dfd78e", + solidityInputJson: MOCK_SOLIDITY_INPUT_JSON, + contractName: MOCK_CONTRACT_NAME, + constructorArgs: MOCK_CONSTRUCTOR_ARGS, + }); + expect(response).toBe(`Error deploying contract: ${error}`); + }); + }); });