Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0.1.0(ts): deploy contract #201

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -59,6 +60,7 @@ describe("CDP Action Provider", () => {
let actionProvider: CdpActionProvider;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let mockExternalAddressInstance: jest.Mocked<any>;
let mockWallet: jest.Mocked<CdpWalletProvider>;

beforeEach(() => {
// Reset all mocks before each test
Expand All @@ -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<CdpWalletProvider>;
});

describe("addressReputation", () => {
Expand Down Expand Up @@ -112,8 +121,6 @@ describe("CDP Action Provider", () => {
});

describe("deployToken", () => {
let mockWallet: jest.Mocked<CdpWalletProvider>;

beforeEach(() => {
mockWallet = {
deployToken: jest.fn().mockResolvedValue({
Expand Down Expand Up @@ -160,14 +167,7 @@ describe("CDP Action Provider", () => {
});

describe("faucet", () => {
let mockWallet: jest.Mocked<CdpWalletProvider>;

beforeEach(() => {
mockWallet = {
getAddress: jest.fn().mockReturnValue("0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83"),
getNetwork: jest.fn().mockReturnValue({ networkId: "base-sepolia" }),
} as unknown as jest.Mocked<CdpWalletProvider>;

mockExternalAddressInstance.faucet.mockResolvedValue({
wait: jest.fn().mockResolvedValue({
getTransactionLink: jest.fn().mockReturnValue("tx-link"),
Expand Down Expand Up @@ -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}`);
});
});
});
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<typeof DeployContractSchema>,
): Promise<string> {
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.
*
Expand Down
31 changes: 31 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/cdp/constants.ts
Original file line number Diff line number Diff line change
@@ -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;
27 changes: 23 additions & 4 deletions cdp-agentkit-core/typescript/src/action_providers/cdp/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { SolidityVersions } from "./constants";

/**
* Input schema for address reputation check.
Expand All @@ -15,14 +16,22 @@ export const AddressReputationSchema = z
.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.
Expand All @@ -35,3 +44,13 @@ 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");
Original file line number Diff line number Diff line change
Expand Up @@ -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<bigint> {
// 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());
}

/**
Expand Down Expand Up @@ -227,4 +231,29 @@ export class CdpWalletProvider extends EvmWalletProvider {

return this.#cdpWallet.deployToken(options);
}

/**
* Deploys a contract.
*
* @param options - The options for contract deployment
* @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;
solidityInputJson: string;
contractName: string;
constructorArgs: Record<string, unknown>;
}): Promise<SmartContract> {
if (!this.#cdpWallet) {
throw new Error("Wallet not initialized");
}

return this.#cdpWallet.deployContract(options);
}
}