From 7bcd5b84d2155778ecffc2cc23f09c6d6dd4b01d Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Thu, 5 Dec 2024 17:19:10 +0000 Subject: [PATCH 01/18] Add plugin-evm wallet provider tests and refactor the provider --- packages/plugin-evm/src/providers/wallet.ts | 166 ++++++++++--------- packages/plugin-evm/src/tests/wallet.test.ts | 125 ++++++++++++++ packages/plugin-evm/src/types/index.ts | 29 +--- 3 files changed, 220 insertions(+), 100 deletions(-) create mode 100644 packages/plugin-evm/src/tests/wallet.test.ts diff --git a/packages/plugin-evm/src/providers/wallet.ts b/packages/plugin-evm/src/providers/wallet.ts index a317a861879..8cc471efd72 100644 --- a/packages/plugin-evm/src/providers/wallet.ts +++ b/packages/plugin-evm/src/providers/wallet.ts @@ -9,38 +9,50 @@ import type { HttpTransport, Account, } from "viem"; -import type { SupportedChain, ChainConfig } from "../types"; -import { getChainConfigs } from "./chainConfigs"; -import { initializeChainConfigs } from "./chainUtils"; +import * as viemChains from "viem/chains"; +import { privateKeyToAccount } from "viem/accounts"; -export class WalletProvider { - private chainConfigs: Record; - private currentChain: SupportedChain = "ethereum"; - private address: Address; - runtime: IAgentRuntime; +import type { SupportedChain } from "../types"; - constructor(runtime: IAgentRuntime) { - const privateKey = runtime.getSetting("EVM_PRIVATE_KEY"); - if (!privateKey) throw new Error("EVM_PRIVATE_KEY not configured"); +export class WalletProvider { + private currentChain: SupportedChain = "mainnet"; + chains: Record = { mainnet: viemChains.mainnet }; + account: Account; - this.runtime = runtime; - const account = privateKeyToAccount(privateKey as `0x${string}`); - this.address = account.address; + constructor(privateKey: `0x${string}`, chainNames: SupportedChain[]) { + this.setAccount(privateKey); + this.setChains(chainNames); - // Initialize all chain configs at once - this.chainConfigs = initializeChainConfigs(runtime, account); + if (chainNames.length > 0) { + this.setCurrentChain(chainNames[0]); + } } getAddress(): Address { - return this.address; + return this.account.address; + } + + getCurrentChain(): Chain { + return this.chains[this.currentChain]; + } + + getPublicClient( + chainName: SupportedChain + ): PublicClient { + const { publicClient } = this.createClients(chainName); + return publicClient; + } + + getWalletClient(chainName: SupportedChain): WalletClient { + const { walletClient } = this.createClients(chainName); + return walletClient; } async getWalletBalance(): Promise { try { const client = this.getPublicClient(this.currentChain); - const walletClient = this.getWalletClient(); const balance = await client.getBalance({ - address: walletClient.account.address, + address: this.account.address, }); return formatUnits(balance, 18); } catch (error) { @@ -49,69 +61,68 @@ export class WalletProvider { } } - async connect(): Promise<`0x${string}`> { - return this.runtime.getSetting("EVM_PRIVATE_KEY") as `0x${string}`; - } - - async switchChain( - runtime: IAgentRuntime, - chain: SupportedChain - ): Promise { - const walletClient = this.chainConfigs[this.currentChain].walletClient; - if (!walletClient) throw new Error("Wallet not connected"); - + async getWalletBalanceForChain( + chainName: SupportedChain + ): Promise { try { - await walletClient.switchChain({ - id: getChainConfigs(runtime)[chain].chainId, + const client = this.getPublicClient(chainName); + const balance = await client.getBalance({ + address: this.account.address, }); - } catch (error: any) { - if (error.code === 4902) { - console.log( - "[WalletProvider] Chain not added to wallet (error 4902) - attempting to add chain first" - ); - await walletClient.addChain({ - chain: { - ...getChainConfigs(runtime)[chain].chain, - rpcUrls: { - default: { - http: [getChainConfigs(runtime)[chain].rpcUrl], - }, - public: { - http: [getChainConfigs(runtime)[chain].rpcUrl], - }, - }, - }, - }); - await walletClient.switchChain({ - id: getChainConfigs(runtime)[chain].chainId, - }); - } else { - throw error; - } + return formatUnits(balance, 18); + } catch (error) { + console.error("Error getting wallet balance:", error); + return null; } - - this.currentChain = chain; } - getPublicClient( - chain: SupportedChain - ): PublicClient { - return this.chainConfigs[chain].publicClient; + addChain(chain: SupportedChain) { + this.setChains([chain]); } - getWalletClient(): WalletClient { - const walletClient = this.chainConfigs[this.currentChain].walletClient; - if (!walletClient) throw new Error("Wallet not connected"); - return walletClient; + switchChain(chain: SupportedChain) { + if (!this.chains[chain]) { + this.addChain(chain); + } + this.setCurrentChain(chain); } - getCurrentChain(): SupportedChain { - return this.currentChain; - } + private setAccount = (pk: `0x${string}`) => { + this.account = privateKeyToAccount(pk); + }; + private setChains = (chainNames: SupportedChain[]) => { + chainNames.forEach((name) => { + const chain = viemChains[name]; - getChainConfig(chain: SupportedChain) { - return getChainConfigs(this.runtime)[chain]; - } + if (!chain?.id) { + throw new Error("Invalid chain name"); + } + + this.chains[name] = chain; + }); + }; + private setCurrentChain = (chain: SupportedChain) => { + this.currentChain = chain; + }; + private createHttpTransport = (chain: SupportedChain) => { + return http(this.chains[chain].rpcUrls.default.http[0]); + }; + private createClients = (chain: SupportedChain) => { + const transport = this.createHttpTransport(chain); + + return { + chain: this.chains[chain], + publicClient: createPublicClient({ + chain: this.chains[chain], + transport, + }), + walletClient: createWalletClient({ + chain: this.chains[chain], + transport, + account: this.account, + }), + }; + }; } export const evmWalletProvider: Provider = { @@ -120,12 +131,19 @@ export const evmWalletProvider: Provider = { message: Memory, state?: State ): Promise { - if (!runtime.getSetting("EVM_PRIVATE_KEY")) { + const privateKey = runtime.getSetting("EVM_PRIVATE_KEY"); + const chainNames = + (runtime.character.settings.chains?.evm as SupportedChain[]) || []; + + if (!privateKey) { return null; } try { - const walletProvider = new WalletProvider(runtime); + const walletProvider = new WalletProvider( + privateKey as `0x${string}`, + chainNames + ); const address = walletProvider.getAddress(); const balance = await walletProvider.getWalletBalance(); return `EVM Wallet Address: ${address}\nBalance: ${balance} ETH`; diff --git a/packages/plugin-evm/src/tests/wallet.test.ts b/packages/plugin-evm/src/tests/wallet.test.ts new file mode 100644 index 00000000000..e3b26c0e2d4 --- /dev/null +++ b/packages/plugin-evm/src/tests/wallet.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect, beforeAll, beforeEach } from "vitest"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { mainnet, iotex, arbitrum } from "viem/chains"; + +import { WalletProvider } from "../providers/wallet"; + +describe("Wallet provider", () => { + let walletProvider: WalletProvider; + let pk: `0x${string}`; + + beforeAll(() => { + pk = generatePrivateKey(); + }); + + describe("Constructor", () => { + it("sets address", () => { + const account = privateKeyToAccount(pk); + const expectedAddress = account.address; + + walletProvider = new WalletProvider(pk, ["iotexTestnet"]); + + expect(walletProvider.getAddress()).to.be.eq(expectedAddress); + }); + it("sets default chain to ethereum mainnet", () => { + walletProvider = new WalletProvider(pk, []); + + expect(walletProvider.chains.mainnet.id).to.be.eq(mainnet.id); + expect(walletProvider.getCurrentChain().id).to.be.eq(mainnet.id); + }); + it("sets custom chains", () => { + walletProvider = new WalletProvider(pk, ["iotex", "arbitrum"]); + + expect(walletProvider.chains.iotex.id).to.be.eq(iotex.id); + expect(walletProvider.chains.arbitrum.id).to.be.eq(arbitrum.id); + }); + it("sets the first provided custom chain as current chain", () => { + walletProvider = new WalletProvider(pk, ["iotex", "arbitrum"]); + + expect(walletProvider.getCurrentChain().id).to.be.eq(iotex.id); + }); + it("throws if invalid chain name", () => { + // @ts-ignore + expect(() => new WalletProvider(pk, ["eth"])).to.throw(); + }); + it("throws if unsupported chain name", () => { + // @ts-ignore + expect(() => new WalletProvider(pk, ["ethereum"])).to.throw(); + }); + }); + describe("Clients", () => { + beforeEach(() => { + walletProvider = new WalletProvider(pk, []); + }); + it("generates public client", () => { + const client = walletProvider.getPublicClient("mainnet"); + expect(client.chain.id).to.be.equal(mainnet.id); + }); + it("generates wallet client", () => { + const account = privateKeyToAccount(pk); + const expectedAddress = account.address; + + const client = walletProvider.getWalletClient("mainnet"); + + expect(client.account.address).to.be.equal(expectedAddress); + }); + }); + describe("Balance", () => { + beforeEach(() => { + walletProvider = new WalletProvider(pk, ["iotex"]); + }); + it("should fetch balance", async () => { + const bal = await walletProvider.getWalletBalance(); + + expect(bal).to.be.eq("0"); + }); + it("should fetch balance for a specific added chain", async () => { + const bal = await walletProvider.getWalletBalanceForChain("iotex"); + + expect(bal).to.be.eq("0"); + }); + it("should return null if chain is not added", async () => { + const bal = + await walletProvider.getWalletBalanceForChain("arbitrum"); + expect(bal).to.be.null; + }); + }); + describe("Chain", () => { + beforeEach(() => { + walletProvider = new WalletProvider(pk, ["iotex"]); + }); + it("switches chain", () => { + const initialChain = walletProvider.getCurrentChain().id; + expect(initialChain).to.be.eq(iotex.id); + + walletProvider.switchChain("mainnet"); + + const newChain = walletProvider.getCurrentChain().id; + expect(newChain).to.be.eq(mainnet.id); + }); + it("switches chain (by adding new chain)", () => { + const initialChain = walletProvider.getCurrentChain().id; + expect(initialChain).to.be.eq(iotex.id); + + walletProvider.switchChain("arbitrum"); + + const newChain = walletProvider.getCurrentChain().id; + expect(newChain).to.be.eq(arbitrum.id); + }); + it("adds chain", () => { + const initialChains = walletProvider.chains; + expect(initialChains.arbitrum).to.be.undefined; + + walletProvider.addChain("arbitrum"); + const newChains = walletProvider.chains; + expect(newChains.arbitrum.id).to.be.eq(arbitrum.id); + }); + it("throws if tries to switch to an invalid chain", () => { + const initialChain = walletProvider.getCurrentChain().id; + expect(initialChain).to.be.eq(iotex.id); + + // @ts-ignore + expect(() => walletProvider.switchChain("eth")).to.throw(); + }); + }); +}); diff --git a/packages/plugin-evm/src/types/index.ts b/packages/plugin-evm/src/types/index.ts index 885f3994fce..8fa8247dcdb 100644 --- a/packages/plugin-evm/src/types/index.ts +++ b/packages/plugin-evm/src/types/index.ts @@ -8,33 +8,10 @@ import type { PublicClient, WalletClient, } from "viem"; +import * as viemChains from "viem/chains"; -export type SupportedChain = - | "ethereum" - | "base" - | "sepolia" - | "bsc" - | "arbitrum" - | "avalanche" - | "polygon" - | "optimism" - | "cronos" - | "gnosis" - | "fantom" - | "klaytn" - | "celo" - | "moonbeam" - | "aurora" - | "harmonyOne" - | "moonriver" - | "arbitrumNova" - | "mantle" - | "linea" - | "scroll" - | "filecoin" - | "taiko" - | "zksync" - | "canto"; +const SupportedChainList = Object.keys(viemChains) as Array; +export type SupportedChain = (typeof SupportedChainList)[number]; // Transaction types export interface Transaction { From c42f9fb30212ba9ceca08445d91ab912d0d0f6ea Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Thu, 5 Dec 2024 17:19:37 +0000 Subject: [PATCH 02/18] Add test script to package.json --- packages/plugin-evm/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/plugin-evm/package.json b/packages/plugin-evm/package.json index c041a142159..b24d1045c71 100644 --- a/packages/plugin-evm/package.json +++ b/packages/plugin-evm/package.json @@ -15,7 +15,8 @@ }, "scripts": { "build": "tsup --format esm --dts", - "dev": "tsup --format esm --dts --watch" + "dev": "tsup --format esm --dts --watch", + "test": "vitest run" }, "peerDependencies": { "whatwg-url": "7.1.0" From fc9d68beb7356129d5cd673de6b3b1b352099f57 Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Thu, 5 Dec 2024 17:24:16 +0000 Subject: [PATCH 03/18] Make chainNames optional for WalletProvider constructor --- packages/plugin-evm/src/providers/wallet.ts | 9 ++++++--- packages/plugin-evm/src/tests/wallet.test.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/plugin-evm/src/providers/wallet.ts b/packages/plugin-evm/src/providers/wallet.ts index 8cc471efd72..616d0a64540 100644 --- a/packages/plugin-evm/src/providers/wallet.ts +++ b/packages/plugin-evm/src/providers/wallet.ts @@ -19,11 +19,11 @@ export class WalletProvider { chains: Record = { mainnet: viemChains.mainnet }; account: Account; - constructor(privateKey: `0x${string}`, chainNames: SupportedChain[]) { + constructor(privateKey: `0x${string}`, chainNames?: SupportedChain[]) { this.setAccount(privateKey); this.setChains(chainNames); - if (chainNames.length > 0) { + if (chainNames?.length > 0) { this.setCurrentChain(chainNames[0]); } } @@ -90,7 +90,10 @@ export class WalletProvider { private setAccount = (pk: `0x${string}`) => { this.account = privateKeyToAccount(pk); }; - private setChains = (chainNames: SupportedChain[]) => { + private setChains = (chainNames?: SupportedChain[]) => { + if (!chainNames) { + return; + } chainNames.forEach((name) => { const chain = viemChains[name]; diff --git a/packages/plugin-evm/src/tests/wallet.test.ts b/packages/plugin-evm/src/tests/wallet.test.ts index e3b26c0e2d4..24ec4f77e6e 100644 --- a/packages/plugin-evm/src/tests/wallet.test.ts +++ b/packages/plugin-evm/src/tests/wallet.test.ts @@ -22,7 +22,7 @@ describe("Wallet provider", () => { expect(walletProvider.getAddress()).to.be.eq(expectedAddress); }); it("sets default chain to ethereum mainnet", () => { - walletProvider = new WalletProvider(pk, []); + walletProvider = new WalletProvider(pk); expect(walletProvider.chains.mainnet.id).to.be.eq(mainnet.id); expect(walletProvider.getCurrentChain().id).to.be.eq(mainnet.id); From 4515d48171d026ddeeedd6d1721bfd0e166229e9 Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Thu, 5 Dec 2024 17:38:51 +0000 Subject: [PATCH 04/18] Add chain config getter --- packages/plugin-evm/src/providers/wallet.ts | 10 ++++++++++ packages/plugin-evm/src/tests/wallet.test.ts | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/packages/plugin-evm/src/providers/wallet.ts b/packages/plugin-evm/src/providers/wallet.ts index 616d0a64540..6016271a02c 100644 --- a/packages/plugin-evm/src/providers/wallet.ts +++ b/packages/plugin-evm/src/providers/wallet.ts @@ -87,6 +87,16 @@ export class WalletProvider { this.setCurrentChain(chain); } + getChainConfigs(chainName: SupportedChain): Chain { + const chain = viemChains[chainName]; + + if (!chain?.id) { + throw new Error("Invalid chain name"); + } + + return chain; + } + private setAccount = (pk: `0x${string}`) => { this.account = privateKeyToAccount(pk); }; diff --git a/packages/plugin-evm/src/tests/wallet.test.ts b/packages/plugin-evm/src/tests/wallet.test.ts index 24ec4f77e6e..6289e5fdcd0 100644 --- a/packages/plugin-evm/src/tests/wallet.test.ts +++ b/packages/plugin-evm/src/tests/wallet.test.ts @@ -114,6 +114,11 @@ describe("Wallet provider", () => { const newChains = walletProvider.chains; expect(newChains.arbitrum.id).to.be.eq(arbitrum.id); }); + it("gets chain configs", () => { + const chain = walletProvider.getChainConfigs("iotex"); + + expect(chain.id).to.eq(iotex.id); + }) it("throws if tries to switch to an invalid chain", () => { const initialChain = walletProvider.getCurrentChain().id; expect(initialChain).to.be.eq(iotex.id); From 36f594f6da03bb0e5782e1deb5f2ba6cb5c39374 Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Thu, 5 Dec 2024 17:39:28 +0000 Subject: [PATCH 05/18] Refactor bridge action --- packages/plugin-evm/src/actions/bridge.ts | 38 ++++++++++------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/plugin-evm/src/actions/bridge.ts b/packages/plugin-evm/src/actions/bridge.ts index 4d92018e96a..eeb888486c7 100644 --- a/packages/plugin-evm/src/actions/bridge.ts +++ b/packages/plugin-evm/src/actions/bridge.ts @@ -1,13 +1,11 @@ import type { IAgentRuntime, Memory, State } from "@ai16z/eliza"; import { - ChainId, createConfig, executeRoute, ExtendedChain, getRoutes, } from "@lifi/sdk"; import { WalletProvider } from "../providers/wallet"; -import { getChainConfigs } from "../providers/chainConfigs"; import { bridgeTemplate } from "../templates"; import type { BridgeParams, Transaction } from "../types"; @@ -19,25 +17,23 @@ export class BridgeAction { constructor(private walletProvider: WalletProvider) { this.config = createConfig({ integrator: "eliza", - chains: Object.values( - getChainConfigs(this.walletProvider.runtime) - ).map((config) => ({ - id: config.chainId, + chains: Object.values(this.walletProvider.chains).map((config) => ({ + id: config.id, name: config.name, key: config.name.toLowerCase(), chainType: "EVM", nativeToken: { ...config.nativeCurrency, - chainId: config.chainId, + chainId: config.id, address: "0x0000000000000000000000000000000000000000", coinKey: config.nativeCurrency.symbol, }, metamask: { - chainId: `0x${config.chainId.toString(16)}`, + chainId: `0x${config.id.toString(16)}`, chainName: config.name, nativeCurrency: config.nativeCurrency, - rpcUrls: [config.rpcUrl], - blockExplorerUrls: [config.blockExplorerUrl], + rpcUrls: [config.rpcUrls.default.http[0]], + blockExplorerUrls: [config.blockExplorers.default.url], }, diamondAddress: "0x0000000000000000000000000000000000000000", coin: config.nativeCurrency.symbol, @@ -47,16 +43,15 @@ export class BridgeAction { } async bridge(params: BridgeParams): Promise { - const walletClient = this.walletProvider.getWalletClient(); + const walletClient = this.walletProvider.getWalletClient( + params.fromChain + ); const [fromAddress] = await walletClient.getAddresses(); const routes = await getRoutes({ - fromChainId: getChainConfigs(this.walletProvider.runtime)[ - params.fromChain - ].chainId as ChainId, - toChainId: getChainConfigs(this.walletProvider.runtime)[ - params.toChain - ].chainId as ChainId, + fromChainId: this.walletProvider.getChainConfigs(params.fromChain) + .id, + toChainId: this.walletProvider.getChainConfigs(params.toChain).id, fromTokenAddress: params.fromToken, toTokenAddress: params.toToken, fromAmount: params.amount, @@ -79,9 +74,7 @@ export class BridgeAction { to: routes.routes[0].steps[0].estimate .approvalAddress as `0x${string}`, value: BigInt(params.amount), - chainId: getChainConfigs(this.walletProvider.runtime)[ - params.fromChain - ].chainId, + chainId: this.walletProvider.getChainConfigs(params.fromChain).id, }; } } @@ -95,7 +88,10 @@ export const bridgeAction = { state: State, options: any ) => { - const walletProvider = new WalletProvider(runtime); + const privateKey = runtime.getSetting( + "EVM_PRIVATE_KEY" + ) as `0x${string}`; + const walletProvider = new WalletProvider(privateKey); const action = new BridgeAction(walletProvider); return action.bridge(options); }, From 5c39893fc325ceae431424fd7b88d65f38c0dc87 Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Thu, 5 Dec 2024 17:43:19 +0000 Subject: [PATCH 06/18] Refactor plugin-evm swap action --- packages/plugin-evm/src/actions/swap.ts | 37 +++++++++++-------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/packages/plugin-evm/src/actions/swap.ts b/packages/plugin-evm/src/actions/swap.ts index 3b22916cb35..1c66f43f14b 100644 --- a/packages/plugin-evm/src/actions/swap.ts +++ b/packages/plugin-evm/src/actions/swap.ts @@ -7,7 +7,6 @@ import { getRoutes, } from "@lifi/sdk"; import { WalletProvider } from "../providers/wallet"; -import { getChainConfigs } from "../providers/chainConfigs"; import { swapTemplate } from "../templates"; import type { SwapParams, Transaction } from "../types"; @@ -19,16 +18,14 @@ export class SwapAction { constructor(private walletProvider: WalletProvider) { this.config = createConfig({ integrator: "eliza", - chains: Object.values( - getChainConfigs(this.walletProvider.runtime) - ).map((config) => ({ - id: config.chainId, + chains: Object.values(this.walletProvider.chains).map((config) => ({ + id: config.id, name: config.name, key: config.name.toLowerCase(), chainType: "EVM" as const, nativeToken: { ...config.nativeCurrency, - chainId: config.chainId, + chainId: config.id, address: "0x0000000000000000000000000000000000000000", coinKey: config.nativeCurrency.symbol, priceUSD: "0", @@ -38,15 +35,15 @@ export class SwapAction { name: config.nativeCurrency.name, }, rpcUrls: { - public: { http: [config.rpcUrl] }, + public: { http: [config.rpcUrls.default.http[0]] }, }, - blockExplorerUrls: [config.blockExplorerUrl], + blockExplorerUrls: [config.blockExplorers.default.url], metamask: { - chainId: `0x${config.chainId.toString(16)}`, + chainId: `0x${config.id.toString(16)}`, chainName: config.name, nativeCurrency: config.nativeCurrency, - rpcUrls: [config.rpcUrl], - blockExplorerUrls: [config.blockExplorerUrl], + rpcUrls: [config.rpcUrls.default.http[0]], + blockExplorerUrls: [config.blockExplorers.default.url], }, coin: config.nativeCurrency.symbol, mainnet: true, @@ -56,16 +53,12 @@ export class SwapAction { } async swap(params: SwapParams): Promise { - const walletClient = this.walletProvider.getWalletClient(); + const walletClient = this.walletProvider.getWalletClient(params.chain); const [fromAddress] = await walletClient.getAddresses(); const routes = await getRoutes({ - fromChainId: getChainConfigs(this.walletProvider.runtime)[ - params.chain - ].chainId as ChainId, - toChainId: getChainConfigs(this.walletProvider.runtime)[ - params.chain - ].chainId as ChainId, + fromChainId: this.walletProvider.getChainConfigs(params.chain).id, + toChainId: this.walletProvider.getChainConfigs(params.chain).id, fromTokenAddress: params.fromToken, toTokenAddress: params.toToken, fromAmount: params.amount, @@ -92,8 +85,7 @@ export class SwapAction { .approvalAddress as `0x${string}`, value: BigInt(params.amount), data: process.data as `0x${string}`, - chainId: getChainConfigs(this.walletProvider.runtime)[params.chain] - .chainId, + chainId: this.walletProvider.getChainConfigs(params.chain).id, }; } } @@ -109,7 +101,10 @@ export const swapAction = { callback?: any ) => { try { - const walletProvider = new WalletProvider(runtime); + const privateKey = runtime.getSetting( + "EVM_PRIVATE_KEY" + ) as `0x${string}`; + const walletProvider = new WalletProvider(privateKey); const action = new SwapAction(walletProvider); return await action.swap(options); } catch (error) { From bd268143e0e97fd78a18c33c34835ec03fecd8f8 Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Thu, 5 Dec 2024 17:44:42 +0000 Subject: [PATCH 07/18] Refactor plugin-evm transfer action --- packages/plugin-evm/src/actions/transfer.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/plugin-evm/src/actions/transfer.ts b/packages/plugin-evm/src/actions/transfer.ts index 18321097fe9..b72dcbed2a5 100644 --- a/packages/plugin-evm/src/actions/transfer.ts +++ b/packages/plugin-evm/src/actions/transfer.ts @@ -1,8 +1,9 @@ import { ByteArray, parseEther, type Hex } from "viem"; +import type { IAgentRuntime, Memory, State } from "@ai16z/eliza"; + import { WalletProvider } from "../providers/wallet"; import type { Transaction, TransferParams } from "../types"; import { transferTemplate } from "../templates"; -import type { IAgentRuntime, Memory, State } from "@ai16z/eliza"; export { transferTemplate }; export class TransferAction { @@ -12,10 +13,12 @@ export class TransferAction { runtime: IAgentRuntime, params: TransferParams ): Promise { - const walletClient = this.walletProvider.getWalletClient(); + const walletClient = this.walletProvider.getWalletClient( + params.fromChain + ); const [fromAddress] = await walletClient.getAddresses(); - await this.walletProvider.switchChain(runtime, params.fromChain); + this.walletProvider.switchChain(params.fromChain); try { const hash = await walletClient.sendTransaction({ @@ -59,7 +62,10 @@ export const transferAction = { state: State, options: any ) => { - const walletProvider = new WalletProvider(runtime); + const privateKey = runtime.getSetting( + "EVM_PRIVATE_KEY" + ) as `0x${string}`; + const walletProvider = new WalletProvider(privateKey); const action = new TransferAction(walletProvider); return action.transfer(runtime, options); }, From c6a3531fc8cc918214f15985c5223ea1bc138179 Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Sat, 7 Dec 2024 20:38:15 +0000 Subject: [PATCH 08/18] feat: add custom rpc url support --- packages/plugin-evm/src/providers/wallet.ts | 130 ++++++++++++++----- packages/plugin-evm/src/tests/wallet.test.ts | 120 ++++++++++++++--- 2 files changed, 194 insertions(+), 56 deletions(-) diff --git a/packages/plugin-evm/src/providers/wallet.ts b/packages/plugin-evm/src/providers/wallet.ts index 6016271a02c..b4c3257f52b 100644 --- a/packages/plugin-evm/src/providers/wallet.ts +++ b/packages/plugin-evm/src/providers/wallet.ts @@ -19,12 +19,12 @@ export class WalletProvider { chains: Record = { mainnet: viemChains.mainnet }; account: Account; - constructor(privateKey: `0x${string}`, chainNames?: SupportedChain[]) { + constructor(privateKey: `0x${string}`, chains?: Record) { this.setAccount(privateKey); - this.setChains(chainNames); + this.setChains(chains); - if (chainNames?.length > 0) { - this.setCurrentChain(chainNames[0]); + if (chains && Object.keys(chains).length > 0) { + this.setCurrentChain(Object.keys(chains)[0] as SupportedChain); } } @@ -48,6 +48,16 @@ export class WalletProvider { return walletClient; } + getChainConfigs(chainName: SupportedChain): Chain { + const chain = viemChains[chainName]; + + if (!chain?.id) { + throw new Error("Invalid chain name"); + } + + return chain; + } + async getWalletBalance(): Promise { try { const client = this.getPublicClient(this.currentChain); @@ -76,50 +86,47 @@ export class WalletProvider { } } - addChain(chain: SupportedChain) { - this.setChains([chain]); + addChain(chain: Record) { + this.setChains(chain); } - switchChain(chain: SupportedChain) { - if (!this.chains[chain]) { - this.addChain(chain); - } - this.setCurrentChain(chain); - } - - getChainConfigs(chainName: SupportedChain): Chain { - const chain = viemChains[chainName]; - - if (!chain?.id) { - throw new Error("Invalid chain name"); + switchChain(chainName: SupportedChain, customRpcUrl?: string) { + if (!this.chains[chainName]) { + const chain = WalletProvider.genChainFromName( + chainName, + customRpcUrl + ); + this.addChain({ [chainName]: chain }); } - - return chain; + this.setCurrentChain(chainName); } private setAccount = (pk: `0x${string}`) => { this.account = privateKeyToAccount(pk); }; - private setChains = (chainNames?: SupportedChain[]) => { - if (!chainNames) { + + private setChains = (chains?: Record) => { + if (!chains) { return; } - chainNames.forEach((name) => { - const chain = viemChains[name]; - - if (!chain?.id) { - throw new Error("Invalid chain name"); - } - - this.chains[name] = chain; + Object.keys(chains).forEach((chain: string) => { + this.chains[chain] = chains[chain]; }); }; + private setCurrentChain = (chain: SupportedChain) => { this.currentChain = chain; }; - private createHttpTransport = (chain: SupportedChain) => { - return http(this.chains[chain].rpcUrls.default.http[0]); + + private createHttpTransport = (chainName: SupportedChain) => { + const chain = this.chains[chainName]; + + if (chain.rpcUrls.custom) { + return http(chain.rpcUrls.custom.http[0]); + } + return http(chain.rpcUrls.default.http[0]); }; + private createClients = (chain: SupportedChain) => { const transport = this.createHttpTransport(chain); @@ -136,8 +143,60 @@ export class WalletProvider { }), }; }; + + static genChainFromName( + chainName: string, + customRpcUrl?: string | null + ): Chain { + const baseChain = viemChains[chainName]; + + if (!baseChain?.id) { + throw new Error("Invalid chain name"); + } + + const viemChain: Chain = customRpcUrl + ? { + ...baseChain, + rpcUrls: { + ...baseChain.rpcUrls, + custom: { + http: [customRpcUrl], + }, + }, + } + : baseChain; + + return viemChain; + } } +const genChainsFromRuntime = ( + runtime: IAgentRuntime +): Record => { + const chainNames = + (runtime.character.settings.chains?.evm as SupportedChain[]) || []; + const chains = {}; + + chainNames.forEach((chainName) => { + const rpcUrl = runtime.getSetting( + "ETHEREUM_PROVIDER_" + chainName.toUpperCase() + ); + const chain = WalletProvider.genChainFromName(chainName, rpcUrl); + chains[chainName] = chain; + }); + + const mainnet_rpcurl = runtime.getSetting("EVM_PROVIDER_URL"); + if (mainnet_rpcurl) { + const chain = WalletProvider.genChainFromName( + "mainnet", + mainnet_rpcurl + ); + chains["mainnet"] = chain; + } + + return chains; +}; + export const evmWalletProvider: Provider = { async get( runtime: IAgentRuntime, @@ -145,17 +204,16 @@ export const evmWalletProvider: Provider = { state?: State ): Promise { const privateKey = runtime.getSetting("EVM_PRIVATE_KEY"); - const chainNames = - (runtime.character.settings.chains?.evm as SupportedChain[]) || []; - if (!privateKey) { return null; } + const chains = genChainsFromRuntime(runtime); + try { const walletProvider = new WalletProvider( privateKey as `0x${string}`, - chainNames + chains ); const address = walletProvider.getAddress(); const balance = await walletProvider.getWalletBalance(); diff --git a/packages/plugin-evm/src/tests/wallet.test.ts b/packages/plugin-evm/src/tests/wallet.test.ts index 6289e5fdcd0..a8e5a3ee872 100644 --- a/packages/plugin-evm/src/tests/wallet.test.ts +++ b/packages/plugin-evm/src/tests/wallet.test.ts @@ -1,15 +1,28 @@ import { describe, it, expect, beforeAll, beforeEach } from "vitest"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; -import { mainnet, iotex, arbitrum } from "viem/chains"; +import { mainnet, iotex, arbitrum, Chain } from "viem/chains"; import { WalletProvider } from "../providers/wallet"; +const customRpcUrls = { + mainnet: "custom-rpc.mainnet.io", + arbitrum: "custom-rpc.base.io", + iotex: "custom-rpc.iotex.io", +}; + describe("Wallet provider", () => { let walletProvider: WalletProvider; let pk: `0x${string}`; + let customChains: Record = {}; beforeAll(() => { pk = generatePrivateKey(); + + const chainNames = ["iotex", "arbitrum"]; + chainNames.forEach( + (chain) => + (customChains[chain] = WalletProvider.genChainFromName(chain)) + ); }); describe("Constructor", () => { @@ -17,7 +30,7 @@ describe("Wallet provider", () => { const account = privateKeyToAccount(pk); const expectedAddress = account.address; - walletProvider = new WalletProvider(pk, ["iotexTestnet"]); + walletProvider = new WalletProvider(pk); expect(walletProvider.getAddress()).to.be.eq(expectedAddress); }); @@ -28,32 +41,44 @@ describe("Wallet provider", () => { expect(walletProvider.getCurrentChain().id).to.be.eq(mainnet.id); }); it("sets custom chains", () => { - walletProvider = new WalletProvider(pk, ["iotex", "arbitrum"]); + walletProvider = new WalletProvider(pk, customChains); expect(walletProvider.chains.iotex.id).to.be.eq(iotex.id); expect(walletProvider.chains.arbitrum.id).to.be.eq(arbitrum.id); }); it("sets the first provided custom chain as current chain", () => { - walletProvider = new WalletProvider(pk, ["iotex", "arbitrum"]); + walletProvider = new WalletProvider(pk, customChains); expect(walletProvider.getCurrentChain().id).to.be.eq(iotex.id); }); - it("throws if invalid chain name", () => { - // @ts-ignore - expect(() => new WalletProvider(pk, ["eth"])).to.throw(); - }); - it("throws if unsupported chain name", () => { - // @ts-ignore - expect(() => new WalletProvider(pk, ["ethereum"])).to.throw(); - }); }); describe("Clients", () => { beforeEach(() => { - walletProvider = new WalletProvider(pk, []); + walletProvider = new WalletProvider(pk); }); it("generates public client", () => { const client = walletProvider.getPublicClient("mainnet"); expect(client.chain.id).to.be.equal(mainnet.id); + expect(client.transport.url).toEqual( + mainnet.rpcUrls.default.http[0] + ); + }); + it("generates public client with custom rpcurl", () => { + const chain = WalletProvider.genChainFromName( + "mainnet", + customRpcUrls.mainnet + ); + const wp = new WalletProvider(pk, { ["mainnet"]: chain }); + + const client = wp.getPublicClient("mainnet"); + expect(client.chain.id).to.be.equal(mainnet.id); + expect(client.chain.rpcUrls.default.http[0]).to.eq( + mainnet.rpcUrls.default.http[0] + ); + expect(client.chain.rpcUrls.custom.http[0]).to.eq( + customRpcUrls.mainnet + ); + expect(client.transport.url).toEqual(customRpcUrls.mainnet); }); it("generates wallet client", () => { const account = privateKeyToAccount(pk); @@ -62,11 +87,35 @@ describe("Wallet provider", () => { const client = walletProvider.getWalletClient("mainnet"); expect(client.account.address).to.be.equal(expectedAddress); + expect(client.transport.url).toEqual( + mainnet.rpcUrls.default.http[0] + ); + }); + it("generates wallet client with custom rpcurl", () => { + const account = privateKeyToAccount(pk); + const expectedAddress = account.address; + const chain = WalletProvider.genChainFromName( + "mainnet", + customRpcUrls.mainnet + ); + const wp = new WalletProvider(pk, { ["mainnet"]: chain }); + + const client = wp.getWalletClient("mainnet"); + + expect(client.account.address).to.be.equal(expectedAddress); + expect(client.chain.id).to.be.equal(mainnet.id); + expect(client.chain.rpcUrls.default.http[0]).to.eq( + mainnet.rpcUrls.default.http[0] + ); + expect(client.chain.rpcUrls.custom.http[0]).to.eq( + customRpcUrls.mainnet + ); + expect(client.transport.url).toEqual(customRpcUrls.mainnet); }); }); describe("Balance", () => { beforeEach(() => { - walletProvider = new WalletProvider(pk, ["iotex"]); + walletProvider = new WalletProvider(pk, customChains); }); it("should fetch balance", async () => { const bal = await walletProvider.getWalletBalance(); @@ -79,14 +128,34 @@ describe("Wallet provider", () => { expect(bal).to.be.eq("0"); }); it("should return null if chain is not added", async () => { - const bal = - await walletProvider.getWalletBalanceForChain("arbitrum"); + const bal = await walletProvider.getWalletBalanceForChain("base"); expect(bal).to.be.null; }); }); describe("Chain", () => { beforeEach(() => { - walletProvider = new WalletProvider(pk, ["iotex"]); + walletProvider = new WalletProvider(pk, customChains); + }); + it("generates chains from chain name", () => { + const chainName = "iotex"; + const chain: Chain = WalletProvider.genChainFromName(chainName); + + expect(chain.rpcUrls.default.http[0]).to.eq( + iotex.rpcUrls.default.http[0] + ); + }); + it("generates chains from chain name with custom rpc url", () => { + const chainName = "iotex"; + const customRpcUrl = "custom.url.io"; + const chain: Chain = WalletProvider.genChainFromName( + chainName, + customRpcUrl + ); + + expect(chain.rpcUrls.default.http[0]).to.eq( + iotex.rpcUrls.default.http[0] + ); + expect(chain.rpcUrls.custom.http[0]).to.eq(customRpcUrl); }); it("switches chain", () => { const initialChain = walletProvider.getCurrentChain().id; @@ -108,9 +177,10 @@ describe("Wallet provider", () => { }); it("adds chain", () => { const initialChains = walletProvider.chains; - expect(initialChains.arbitrum).to.be.undefined; + expect(initialChains.base).to.be.undefined; - walletProvider.addChain("arbitrum"); + const base = WalletProvider.genChainFromName("base"); + walletProvider.addChain({ base }); const newChains = walletProvider.chains; expect(newChains.arbitrum.id).to.be.eq(arbitrum.id); }); @@ -118,7 +188,7 @@ describe("Wallet provider", () => { const chain = walletProvider.getChainConfigs("iotex"); expect(chain.id).to.eq(iotex.id); - }) + }); it("throws if tries to switch to an invalid chain", () => { const initialChain = walletProvider.getCurrentChain().id; expect(initialChain).to.be.eq(iotex.id); @@ -126,5 +196,15 @@ describe("Wallet provider", () => { // @ts-ignore expect(() => walletProvider.switchChain("eth")).to.throw(); }); + it("throws if unsupported chain name", () => { + // @ts-ignore + expect(() => + WalletProvider.genChainFromName("ethereum") + ).to.throw(); + }); + it("throws if invalid chain name", () => { + // @ts-ignore + expect(() => WalletProvider.genChainFromName("eth")).to.throw(); + }); }); }); From a420a966ae3b9b9b064ee525164b5379dc1b8bc3 Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Sat, 7 Dec 2024 20:41:54 +0000 Subject: [PATCH 09/18] feat: add better feedback from wallet provider --- packages/plugin-evm/src/providers/wallet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-evm/src/providers/wallet.ts b/packages/plugin-evm/src/providers/wallet.ts index b4c3257f52b..60e5a1a9a14 100644 --- a/packages/plugin-evm/src/providers/wallet.ts +++ b/packages/plugin-evm/src/providers/wallet.ts @@ -217,7 +217,7 @@ export const evmWalletProvider: Provider = { ); const address = walletProvider.getAddress(); const balance = await walletProvider.getWalletBalance(); - return `EVM Wallet Address: ${address}\nBalance: ${balance} ETH`; + return `EVM Wallet Address: ${address}\nBalance: ${balance} ETH\nChain ID: ${chain.id}, Name: ${chain.name}, Native Currency: ${chain.nativeCurrency}`; } catch (error) { console.error("Error in EVM wallet provider:", error); return null; From ee0a3aadc359c87bede143c08506abac51e809b0 Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Mon, 9 Dec 2024 15:00:55 +0000 Subject: [PATCH 10/18] fix: fix undefined chain --- packages/plugin-evm/src/providers/wallet.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/plugin-evm/src/providers/wallet.ts b/packages/plugin-evm/src/providers/wallet.ts index 60e5a1a9a14..c092b27d77e 100644 --- a/packages/plugin-evm/src/providers/wallet.ts +++ b/packages/plugin-evm/src/providers/wallet.ts @@ -217,6 +217,7 @@ export const evmWalletProvider: Provider = { ); const address = walletProvider.getAddress(); const balance = await walletProvider.getWalletBalance(); + const chain = walletProvider.getCurrentChain(); return `EVM Wallet Address: ${address}\nBalance: ${balance} ETH\nChain ID: ${chain.id}, Name: ${chain.name}, Native Currency: ${chain.nativeCurrency}`; } catch (error) { console.error("Error in EVM wallet provider:", error); From b95bf03d3b6b2ace41c0975a21d8d086c1ce45af Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Tue, 10 Dec 2024 17:51:21 +0000 Subject: [PATCH 11/18] fix: transfer action fix #735 --- packages/plugin-evm/src/actions/transfer.ts | 14 ++--- packages/plugin-evm/src/providers/wallet.ts | 42 ++++++-------- .../plugin-evm/src/tests/transfer.test.ts | 55 +++++++++++++++++++ 3 files changed, 77 insertions(+), 34 deletions(-) create mode 100644 packages/plugin-evm/src/tests/transfer.test.ts diff --git a/packages/plugin-evm/src/actions/transfer.ts b/packages/plugin-evm/src/actions/transfer.ts index b72dcbed2a5..da9e82e5a14 100644 --- a/packages/plugin-evm/src/actions/transfer.ts +++ b/packages/plugin-evm/src/actions/transfer.ts @@ -9,20 +9,14 @@ export { transferTemplate }; export class TransferAction { constructor(private walletProvider: WalletProvider) {} - async transfer( - runtime: IAgentRuntime, - params: TransferParams - ): Promise { + async transfer(params: TransferParams): Promise { const walletClient = this.walletProvider.getWalletClient( params.fromChain ); - const [fromAddress] = await walletClient.getAddresses(); - - this.walletProvider.switchChain(params.fromChain); try { const hash = await walletClient.sendTransaction({ - account: fromAddress, + account: walletClient.account, to: params.toAddress, value: parseEther(params.amount), data: params.data as Hex, @@ -42,7 +36,7 @@ export class TransferAction { return { hash, - from: fromAddress, + from: walletClient.account.address, to: params.toAddress, value: parseEther(params.amount), data: params.data as Hex, @@ -67,7 +61,7 @@ export const transferAction = { ) as `0x${string}`; const walletProvider = new WalletProvider(privateKey); const action = new TransferAction(walletProvider); - return action.transfer(runtime, options); + return action.transfer(options); }, template: transferTemplate, validate: async (runtime: IAgentRuntime) => { diff --git a/packages/plugin-evm/src/providers/wallet.ts b/packages/plugin-evm/src/providers/wallet.ts index c092b27d77e..0d62d51e340 100644 --- a/packages/plugin-evm/src/providers/wallet.ts +++ b/packages/plugin-evm/src/providers/wallet.ts @@ -8,6 +8,7 @@ import type { Chain, HttpTransport, Account, + PrivateKeyAccount, } from "viem"; import * as viemChains from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; @@ -17,7 +18,7 @@ import type { SupportedChain } from "../types"; export class WalletProvider { private currentChain: SupportedChain = "mainnet"; chains: Record = { mainnet: viemChains.mainnet }; - account: Account; + account: PrivateKeyAccount; constructor(privateKey: `0x${string}`, chains?: Record) { this.setAccount(privateKey); @@ -36,15 +37,25 @@ export class WalletProvider { return this.chains[this.currentChain]; } - getPublicClient( - chainName: SupportedChain - ): PublicClient { - const { publicClient } = this.createClients(chainName); + getPublicClient(chainName: SupportedChain): PublicClient { + const transport = this.createHttpTransport(chainName); + + const publicClient = createPublicClient({ + chain: this.chains[chainName], + transport, + }) return publicClient; } - getWalletClient(chainName: SupportedChain): WalletClient { - const { walletClient } = this.createClients(chainName); + getWalletClient(chainName: SupportedChain):WalletClient { + const transport = this.createHttpTransport(chainName); + + const walletClient = createWalletClient({ + chain: this.chains[chainName], + transport, + account: this.account, + }) + return walletClient; } @@ -127,23 +138,6 @@ export class WalletProvider { return http(chain.rpcUrls.default.http[0]); }; - private createClients = (chain: SupportedChain) => { - const transport = this.createHttpTransport(chain); - - return { - chain: this.chains[chain], - publicClient: createPublicClient({ - chain: this.chains[chain], - transport, - }), - walletClient: createWalletClient({ - chain: this.chains[chain], - transport, - account: this.account, - }), - }; - }; - static genChainFromName( chainName: string, customRpcUrl?: string | null diff --git a/packages/plugin-evm/src/tests/transfer.test.ts b/packages/plugin-evm/src/tests/transfer.test.ts new file mode 100644 index 00000000000..d637a6a3d9d --- /dev/null +++ b/packages/plugin-evm/src/tests/transfer.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { Account, Chain } from "viem"; + +import { TransferAction } from "../actions/transfer"; +import { WalletProvider } from "../providers/wallet"; + +describe("Transfer Action", () => { + let wp: WalletProvider; + + beforeEach(async () => { + const pk = generatePrivateKey(); + const customChains = prepareChains(); + wp = new WalletProvider(pk, customChains); + }); + describe("Constructor", () => { + it("should initialize with wallet provider", () => { + const ta = new TransferAction(wp); + + expect(ta).to.toBeDefined(); + }); + }); + describe("Transfer", () => { + let ta: TransferAction; + let receiver: Account; + + beforeEach(() => { + ta = new TransferAction(wp); + receiver = privateKeyToAccount(generatePrivateKey()); + }); + + it("throws if not enough gas", async () => { + await expect( + ta.transfer({ + fromChain: "iotexTestnet", + toAddress: receiver.address, + amount: "1", + }) + ).rejects.toThrow( + "Transfer failed: The total cost (gas * gas fee + value) of executing this transaction exceeds the balance of the account." + ); + }); + }); +}); + +const prepareChains = () => { + let customChains: Record = {}; + const chainNames = ["iotexTestnet"]; + chainNames.forEach( + (chain) => + (customChains[chain] = WalletProvider.genChainFromName(chain)) + ); + + return customChains; +}; From ee43b4560fb907298173b096c8a815874a5f6d51 Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Tue, 10 Dec 2024 19:36:12 +0000 Subject: [PATCH 12/18] fix: transfer action fix for EVM Plugin can't run any action #735 --- packages/plugin-evm/src/actions/transfer.ts | 63 +++++++++++++++++---- packages/plugin-evm/src/providers/wallet.ts | 35 ++++++------ packages/plugin-evm/src/templates/index.ts | 12 ++-- 3 files changed, 77 insertions(+), 33 deletions(-) diff --git a/packages/plugin-evm/src/actions/transfer.ts b/packages/plugin-evm/src/actions/transfer.ts index da9e82e5a14..64058e5ad05 100644 --- a/packages/plugin-evm/src/actions/transfer.ts +++ b/packages/plugin-evm/src/actions/transfer.ts @@ -1,7 +1,15 @@ -import { ByteArray, parseEther, type Hex } from "viem"; -import type { IAgentRuntime, Memory, State } from "@ai16z/eliza"; +import { ByteArray, formatEther, parseEther, type Hex } from "viem"; +import { + composeContext, + generateObjectDEPRECATED, + HandlerCallback, + ModelClass, + type IAgentRuntime, + type Memory, + type State, +} from "@ai16z/eliza"; -import { WalletProvider } from "../providers/wallet"; +import { initWalletProvider, WalletProvider } from "../providers/wallet"; import type { Transaction, TransferParams } from "../types"; import { transferTemplate } from "../templates"; @@ -54,14 +62,49 @@ export const transferAction = { runtime: IAgentRuntime, message: Memory, state: State, - options: any + options: any, + callback?: HandlerCallback ) => { - const privateKey = runtime.getSetting( - "EVM_PRIVATE_KEY" - ) as `0x${string}`; - const walletProvider = new WalletProvider(privateKey); - const action = new TransferAction(walletProvider); - return action.transfer(options); + try { + const walletProvider = initWalletProvider(runtime); + const action = new TransferAction(walletProvider); + + const context = composeContext({ + state, + template: transferTemplate, + }); + + const transferDetails = await generateObjectDEPRECATED({ + runtime, + context, + modelClass: ModelClass.SMALL, + }); + + const tx = await action.transfer(transferDetails); + + if (callback) { + callback({ + text: `Successfully transferred ${formatEther(tx.value)} tokens to ${tx.to}\nTransaction hash: ${tx.hash}`, + content: { + success: true, + hash: tx.hash, + amount: formatEther(tx.value), + recipient: tx.to, + }, + }); + } + + return true; + } catch (error) { + console.error("Error during token transfer:", error); + if (callback) { + callback({ + text: `Error transferring tokens: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } }, template: transferTemplate, validate: async (runtime: IAgentRuntime) => { diff --git a/packages/plugin-evm/src/providers/wallet.ts b/packages/plugin-evm/src/providers/wallet.ts index 0d62d51e340..91ea99716fd 100644 --- a/packages/plugin-evm/src/providers/wallet.ts +++ b/packages/plugin-evm/src/providers/wallet.ts @@ -37,24 +37,26 @@ export class WalletProvider { return this.chains[this.currentChain]; } - getPublicClient(chainName: SupportedChain): PublicClient { + getPublicClient( + chainName: SupportedChain + ): PublicClient { const transport = this.createHttpTransport(chainName); const publicClient = createPublicClient({ chain: this.chains[chainName], transport, - }) + }); return publicClient; } - getWalletClient(chainName: SupportedChain):WalletClient { + getWalletClient(chainName: SupportedChain): WalletClient { const transport = this.createHttpTransport(chainName); const walletClient = createWalletClient({ chain: this.chains[chainName], transport, account: this.account, - }) + }); return walletClient; } @@ -191,28 +193,29 @@ const genChainsFromRuntime = ( return chains; }; +export const initWalletProvider = (runtime: IAgentRuntime) => { + const privateKey = runtime.getSetting("EVM_PRIVATE_KEY"); + if (!privateKey) { + return null; + } + + const chains = genChainsFromRuntime(runtime); + + return new WalletProvider(privateKey as `0x${string}`, chains); +}; + export const evmWalletProvider: Provider = { async get( runtime: IAgentRuntime, message: Memory, state?: State ): Promise { - const privateKey = runtime.getSetting("EVM_PRIVATE_KEY"); - if (!privateKey) { - return null; - } - - const chains = genChainsFromRuntime(runtime); - try { - const walletProvider = new WalletProvider( - privateKey as `0x${string}`, - chains - ); + const walletProvider = initWalletProvider(runtime); const address = walletProvider.getAddress(); const balance = await walletProvider.getWalletBalance(); const chain = walletProvider.getCurrentChain(); - return `EVM Wallet Address: ${address}\nBalance: ${balance} ETH\nChain ID: ${chain.id}, Name: ${chain.name}, Native Currency: ${chain.nativeCurrency}`; + return `EVM Wallet Address: ${address}\nBalance: ${balance} ${chain.nativeCurrency.symbol}\nChain ID: ${chain.id}, Name: ${chain.name}`; } catch (error) { console.error("Error in EVM wallet provider:", error); return null; diff --git a/packages/plugin-evm/src/templates/index.ts b/packages/plugin-evm/src/templates/index.ts index a8c7f1fcc3e..c32dafe2ff5 100644 --- a/packages/plugin-evm/src/templates/index.ts +++ b/packages/plugin-evm/src/templates/index.ts @@ -5,19 +5,17 @@ export const transferTemplate = `Given the recent messages and wallet informatio {{walletInfo}} Extract the following information about the requested transfer: -- Chain to execute on -- Amount to transfer +- Chain to execute on (like in viem/chains) +- Amount to transfer (only number without coin symbol) - Recipient address -- Token symbol or address (if not native token) Respond with a JSON markdown block containing only the extracted values: \`\`\`json { - "chain": "ethereum" | "base" | "sepolia" | "bsc" | "arbitrum" | "avalanche" | "polygon" | "optimism" | "cronos" | "gnosis" | "fantom" | "klaytn" | "celo" | "moonbeam" | "aurora" | "harmonyOne" | "moonriver" | "arbitrumNova" | "mantle" | "linea" | "scroll" | "filecoin" | "taiko" | "zksync" | "canto" | null, - "amount": string | null, - "toAddress": string | null, - "token": string | null + "fromChain": "ethereum" | "base" | ..., + "amount": string, + "toAddress": string } \`\`\` `; From 9fc4b0ea7df91541b6c5853daf9bfe9c73a170db Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Wed, 11 Dec 2024 13:00:15 +0000 Subject: [PATCH 13/18] fix: throw if evm private key is missing --- packages/plugin-evm/src/providers/wallet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-evm/src/providers/wallet.ts b/packages/plugin-evm/src/providers/wallet.ts index 91ea99716fd..d731d46dd3e 100644 --- a/packages/plugin-evm/src/providers/wallet.ts +++ b/packages/plugin-evm/src/providers/wallet.ts @@ -196,7 +196,7 @@ const genChainsFromRuntime = ( export const initWalletProvider = (runtime: IAgentRuntime) => { const privateKey = runtime.getSetting("EVM_PRIVATE_KEY"); if (!privateKey) { - return null; + throw new Error("EVM_PRIVATE_KEY is missing") } const chains = genChainsFromRuntime(runtime); From 47e4c2f6e9c54d3e56a621ee10735f2b5d0db3ea Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Wed, 11 Dec 2024 16:15:17 +0000 Subject: [PATCH 14/18] chore: add console log for transfer action --- packages/plugin-evm/src/actions/transfer.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/plugin-evm/src/actions/transfer.ts b/packages/plugin-evm/src/actions/transfer.ts index 64058e5ad05..ffbfa76d371 100644 --- a/packages/plugin-evm/src/actions/transfer.ts +++ b/packages/plugin-evm/src/actions/transfer.ts @@ -18,6 +18,10 @@ export class TransferAction { constructor(private walletProvider: WalletProvider) {} async transfer(params: TransferParams): Promise { + console.log( + `Transferring: ${params.amount} tokens to (${params.toAddress} on ${params.fromChain})` + ); + const walletClient = this.walletProvider.getWalletClient( params.fromChain ); From 5e7efb0d9cbc31a1aa8526730362ff4020be05b2 Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Wed, 11 Dec 2024 16:15:52 +0000 Subject: [PATCH 15/18] fix: fix rpcError for ethereum mainnet --- packages/plugin-evm/src/templates/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-evm/src/templates/index.ts b/packages/plugin-evm/src/templates/index.ts index c32dafe2ff5..25fd11d1f0a 100644 --- a/packages/plugin-evm/src/templates/index.ts +++ b/packages/plugin-evm/src/templates/index.ts @@ -13,7 +13,7 @@ Respond with a JSON markdown block containing only the extracted values: \`\`\`json { - "fromChain": "ethereum" | "base" | ..., + "fromChain": "mainnet" | "base" | ..., "amount": string, "toAddress": string } From 8c15447d88ac37d1a9bc15d1053250bf797dd761 Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Wed, 11 Dec 2024 18:07:55 +0000 Subject: [PATCH 16/18] fix: remove duplicated import --- packages/plugin-evm/src/providers/wallet.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/plugin-evm/src/providers/wallet.ts b/packages/plugin-evm/src/providers/wallet.ts index d731d46dd3e..2b816c461bb 100644 --- a/packages/plugin-evm/src/providers/wallet.ts +++ b/packages/plugin-evm/src/providers/wallet.ts @@ -1,4 +1,9 @@ -import { formatUnits } from "viem"; +import { + createPublicClient, + createWalletClient, + formatUnits, + http, +} from "viem"; import { privateKeyToAccount } from "viem/accounts"; import type { IAgentRuntime, Provider, Memory, State } from "@ai16z/eliza"; import type { @@ -11,7 +16,6 @@ import type { PrivateKeyAccount, } from "viem"; import * as viemChains from "viem/chains"; -import { privateKeyToAccount } from "viem/accounts"; import type { SupportedChain } from "../types"; @@ -196,7 +200,7 @@ const genChainsFromRuntime = ( export const initWalletProvider = (runtime: IAgentRuntime) => { const privateKey = runtime.getSetting("EVM_PRIVATE_KEY"); if (!privateKey) { - throw new Error("EVM_PRIVATE_KEY is missing") + throw new Error("EVM_PRIVATE_KEY is missing"); } const chains = genChainsFromRuntime(runtime); From 9a33703e61d84bca962830cab15b442f55e680a3 Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Thu, 12 Dec 2024 09:39:28 +0000 Subject: [PATCH 17/18] fix: improve chain selection --- packages/plugin-evm/src/actions/transfer.ts | 55 ++++++++++++++++----- packages/plugin-evm/src/templates/index.ts | 2 +- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/packages/plugin-evm/src/actions/transfer.ts b/packages/plugin-evm/src/actions/transfer.ts index ffbfa76d371..5c3cb71957b 100644 --- a/packages/plugin-evm/src/actions/transfer.ts +++ b/packages/plugin-evm/src/actions/transfer.ts @@ -19,7 +19,7 @@ export class TransferAction { async transfer(params: TransferParams): Promise { console.log( - `Transferring: ${params.amount} tokens to (${params.toAddress} on ${params.fromChain})` + `Transferring: ${params.amount} tokens to (${params.toAddress} on ${params.fromChain})` ); const walletClient = this.walletProvider.getWalletClient( @@ -59,6 +59,43 @@ export class TransferAction { } } +const buildTransferDetails = async ( + state: State, + runtime: IAgentRuntime, + wp: WalletProvider +): Promise => { + const context = composeContext({ + state, + template: transferTemplate, + }); + + const chains = Object.keys(wp.chains); + + const contextWithChains = context.replace( + "SUPPORTED_CHAINS", + chains.toString() + ); + + const transferDetails = (await generateObjectDEPRECATED({ + runtime, + context: contextWithChains, + modelClass: ModelClass.SMALL, + })) as TransferParams; + + const existingChain = wp.chains[transferDetails.fromChain]; + + if (!existingChain) { + throw new Error( + "The chain " + + transferDetails.fromChain + + " not configured yet. Add the chain or choose one from configured: " + + chains.toString() + ); + } + + return transferDetails; +}; + export const transferAction = { name: "transfer", description: "Transfer tokens between addresses on the same chain", @@ -72,28 +109,22 @@ export const transferAction = { try { const walletProvider = initWalletProvider(runtime); const action = new TransferAction(walletProvider); - - const context = composeContext({ + const transferDetails = await buildTransferDetails( state, - template: transferTemplate, - }); - - const transferDetails = await generateObjectDEPRECATED({ runtime, - context, - modelClass: ModelClass.SMALL, - }); - + walletProvider + ); const tx = await action.transfer(transferDetails); if (callback) { callback({ - text: `Successfully transferred ${formatEther(tx.value)} tokens to ${tx.to}\nTransaction hash: ${tx.hash}`, + text: `Successfully transferred ${formatEther(tx.value)} tokens to ${tx.to}\nTransaction hash: ${tx.hash}\nChain: ${transferDetails.fromChain}`, content: { success: true, hash: tx.hash, amount: formatEther(tx.value), recipient: tx.to, + chain: transferDetails.fromChain, }, }); } diff --git a/packages/plugin-evm/src/templates/index.ts b/packages/plugin-evm/src/templates/index.ts index 25fd11d1f0a..20d6ef19af8 100644 --- a/packages/plugin-evm/src/templates/index.ts +++ b/packages/plugin-evm/src/templates/index.ts @@ -13,7 +13,7 @@ Respond with a JSON markdown block containing only the extracted values: \`\`\`json { - "fromChain": "mainnet" | "base" | ..., + "fromChain": SUPPORTED_CHAINS, "amount": string, "toAddress": string } From 5b76da674c850f8d60428d1fceea0a3b184c62cf Mon Sep 17 00:00:00 2001 From: Nicky Ru Date: Thu, 12 Dec 2024 09:40:15 +0000 Subject: [PATCH 18/18] refactor: remove unused code --- .../plugin-evm/src/providers/chainConfigs.ts | 339 ------------------ .../plugin-evm/src/providers/chainUtils.ts | 51 --- 2 files changed, 390 deletions(-) delete mode 100644 packages/plugin-evm/src/providers/chainConfigs.ts delete mode 100644 packages/plugin-evm/src/providers/chainUtils.ts diff --git a/packages/plugin-evm/src/providers/chainConfigs.ts b/packages/plugin-evm/src/providers/chainConfigs.ts deleted file mode 100644 index a410c56b962..00000000000 --- a/packages/plugin-evm/src/providers/chainConfigs.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { - mainnet, - base, - sepolia, - bsc, - arbitrum, - avalanche, - polygon, - optimism, - cronos, - gnosis, - fantom, - klaytn, - celo, - moonbeam, - aurora, - harmonyOne, - moonriver, - arbitrumNova, - mantle, - linea, - scroll, - filecoin, - taiko, - zksync, - canto, -} from "viem/chains"; -import type { ChainMetadata, SupportedChain } from "../types"; -import type { IAgentRuntime } from "@ai16z/eliza"; - -export const DEFAULT_CHAIN_CONFIGS: Record = { - ethereum: { - chainId: 1, - name: "Ethereum", - chain: mainnet, - rpcUrl: "https://eth.llamarpc.com", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18, - }, - blockExplorerUrl: "https://etherscan.io", - }, - base: { - chainId: 8453, - name: "Base", - chain: base, - rpcUrl: "https://base.llamarpc.com", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18, - }, - blockExplorerUrl: "https://basescan.org", - }, - sepolia: { - chainId: 11155111, - name: "Sepolia", - chain: sepolia, - rpcUrl: "https://rpc.sepolia.org", - nativeCurrency: { - name: "Sepolia Ether", - symbol: "ETH", - decimals: 18, - }, - blockExplorerUrl: "https://sepolia.etherscan.io", - }, - bsc: { - chainId: 56, - name: "BNB Smart Chain", - chain: bsc, - rpcUrl: "https://bsc-dataseed1.binance.org/", - nativeCurrency: { - name: "Binance Coin", - symbol: "BNB", - decimals: 18, - }, - blockExplorerUrl: "https://bscscan.com", - }, - arbitrum: { - chainId: 42161, - name: "Arbitrum One", - chain: arbitrum, - rpcUrl: "https://arb1.arbitrum.io/rpc", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18, - }, - blockExplorerUrl: "https://arbiscan.io", - }, - avalanche: { - chainId: 43114, - name: "Avalanche C-Chain", - chain: avalanche, - rpcUrl: "https://api.avax.network/ext/bc/C/rpc", - nativeCurrency: { - name: "Avalanche", - symbol: "AVAX", - decimals: 18, - }, - blockExplorerUrl: "https://snowtrace.io", - }, - polygon: { - chainId: 137, - name: "Polygon", - chain: polygon, - rpcUrl: "https://polygon-rpc.com", - nativeCurrency: { - name: "MATIC", - symbol: "MATIC", - decimals: 18, - }, - blockExplorerUrl: "https://polygonscan.com", - }, - optimism: { - chainId: 10, - name: "Optimism", - chain: optimism, - rpcUrl: "https://mainnet.optimism.io", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18, - }, - blockExplorerUrl: "https://optimistic.etherscan.io", - }, - cronos: { - chainId: 25, - name: "Cronos", - chain: cronos, - rpcUrl: "https://evm.cronos.org", - nativeCurrency: { - name: "Cronos", - symbol: "CRO", - decimals: 18, - }, - blockExplorerUrl: "https://cronoscan.com", - }, - gnosis: { - chainId: 100, - name: "Gnosis", - chain: gnosis, - rpcUrl: "https://rpc.gnosischain.com", - nativeCurrency: { - name: "xDAI", - symbol: "XDAI", - decimals: 18, - }, - blockExplorerUrl: "https://gnosisscan.io", - }, - fantom: { - chainId: 250, - name: "Fantom", - chain: fantom, - rpcUrl: "https://rpc.ftm.tools", - nativeCurrency: { - name: "Fantom", - symbol: "FTM", - decimals: 18, - }, - blockExplorerUrl: "https://ftmscan.com", - }, - klaytn: { - chainId: 8217, - name: "Klaytn", - chain: klaytn, - rpcUrl: "https://public-node-api.klaytnapi.com/v1/cypress", - nativeCurrency: { - name: "KLAY", - symbol: "KLAY", - decimals: 18, - }, - blockExplorerUrl: "https://scope.klaytn.com", - }, - celo: { - chainId: 42220, - name: "Celo", - chain: celo, - rpcUrl: "https://forno.celo.org", - nativeCurrency: { - name: "Celo", - symbol: "CELO", - decimals: 18, - }, - blockExplorerUrl: "https://celoscan.io", - }, - moonbeam: { - chainId: 1284, - name: "Moonbeam", - chain: moonbeam, - rpcUrl: "https://rpc.api.moonbeam.network", - nativeCurrency: { - name: "Glimmer", - symbol: "GLMR", - decimals: 18, - }, - blockExplorerUrl: "https://moonscan.io", - }, - aurora: { - chainId: 1313161554, - name: "Aurora", - chain: aurora, - rpcUrl: "https://mainnet.aurora.dev", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18, - }, - blockExplorerUrl: "https://aurorascan.dev", - }, - harmonyOne: { - chainId: 1666600000, - name: "harmonyOne", - chain: harmonyOne, - rpcUrl: "https://api.harmonyOne.one", - nativeCurrency: { - name: "ONE", - symbol: "ONE", - decimals: 18, - }, - blockExplorerUrl: "https://explorer.harmonyOne.one", - }, - moonriver: { - chainId: 1285, - name: "Moonriver", - chain: moonriver, - rpcUrl: "https://rpc.api.moonriver.moonbeam.network", - nativeCurrency: { - name: "Moonriver", - symbol: "MOVR", - decimals: 18, - }, - blockExplorerUrl: "https://moonriver.moonscan.io", - }, - arbitrumNova: { - chainId: 42170, - name: "Arbitrum Nova", - chain: arbitrumNova, - rpcUrl: "https://nova.arbitrum.io/rpc", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18, - }, - blockExplorerUrl: "https://nova-explorer.arbitrum.io", - }, - mantle: { - chainId: 5000, - name: "Mantle", - chain: mantle, - rpcUrl: "https://rpc.mantle.xyz", - nativeCurrency: { - name: "Mantle", - symbol: "MNT", - decimals: 18, - }, - blockExplorerUrl: "https://explorer.mantle.xyz", - }, - linea: { - chainId: 59144, - name: "Linea", - chain: linea, - rpcUrl: "https://linea-mainnet.rpc.build", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18, - }, - blockExplorerUrl: "https://lineascan.build", - }, - scroll: { - chainId: 534353, - name: "Scroll Alpha Testnet", - chain: scroll, - rpcUrl: "https://alpha-rpc.scroll.io/l2", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18, - }, - blockExplorerUrl: "https://blockscout.scroll.io", - }, - filecoin: { - chainId: 314, - name: "Filecoin", - chain: filecoin, - rpcUrl: "https://api.node.glif.io/rpc/v1", - nativeCurrency: { - name: "Filecoin", - symbol: "FIL", - decimals: 18, - }, - blockExplorerUrl: "https://filfox.info/en", - }, - taiko: { - chainId: 167005, - name: "Taiko (Alpha-3) Testnet", - chain: taiko, - rpcUrl: "https://rpc.a3.taiko.xyz", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18, - }, - blockExplorerUrl: "https://explorer.a3.taiko.xyz", - }, - zksync: { - chainId: 324, - name: "zksync Era", - chain: zksync, - rpcUrl: "https://mainnet.era.zksync.io", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18, - }, - blockExplorerUrl: "https://explorer.zksync.io", - }, - canto: { - chainId: 7700, - name: "Canto", - chain: canto, - rpcUrl: "https://canto.slingshot.finance", - nativeCurrency: { - name: "CANTO", - symbol: "CANTO", - decimals: 18, - }, - blockExplorerUrl: "https://tuber.build", - }, -} as const; - -export const getChainConfigs = (runtime: IAgentRuntime) => { - return ( - (runtime.character.settings.chains?.evm as ChainMetadata[]) || - DEFAULT_CHAIN_CONFIGS - ); -}; diff --git a/packages/plugin-evm/src/providers/chainUtils.ts b/packages/plugin-evm/src/providers/chainUtils.ts deleted file mode 100644 index 377aa3f3631..00000000000 --- a/packages/plugin-evm/src/providers/chainUtils.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { createPublicClient, createWalletClient, http } from "viem"; -import type { IAgentRuntime } from "@ai16z/eliza"; -import type { - Account, - Chain, - HttpTransport, - PublicClient, - WalletClient, -} from "viem"; -import type { SupportedChain, ChainConfig } from "../types"; -import { DEFAULT_CHAIN_CONFIGS } from "./chainConfigs"; - -export const createChainClients = ( - chain: SupportedChain, - runtime: IAgentRuntime, - account: Account -): ChainConfig => { - const chainConfig = DEFAULT_CHAIN_CONFIGS[chain]; - const transport = http(chainConfig.rpcUrl); - - return { - chain: chainConfig.chain, - publicClient: createPublicClient({ - chain: chainConfig.chain, - transport, - }) as PublicClient, - walletClient: createWalletClient({ - chain: chainConfig.chain, - transport, - account, - }), - }; -}; - -export const initializeChainConfigs = ( - runtime: IAgentRuntime, - account: Account -): Record => { - return Object.keys(DEFAULT_CHAIN_CONFIGS).reduce( - (configs, chain) => { - const supportedChain = chain as SupportedChain; - configs[supportedChain] = createChainClients( - supportedChain, - runtime, - account - ); - return configs; - }, - {} as Record - ); -};