Skip to content

Commit 0b2950f

Browse files
basic working evm-plugin functionality
1 parent 5eaa57d commit 0b2950f

15 files changed

+1461
-210
lines changed

packages/plugin-evm/src/abis/erc20.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export const erc20Abi = [
2+
{
3+
constant: true,
4+
inputs: [{ name: "_owner", type: "address" }],
5+
name: "balanceOf",
6+
outputs: [{ name: "balance", type: "uint256" }],
7+
type: "function"
8+
},
9+
{
10+
constant: true,
11+
inputs: [],
12+
name: "decimals",
13+
outputs: [{ name: "", type: "uint8" }],
14+
type: "function"
15+
},
16+
{
17+
constant: true,
18+
inputs: [],
19+
name: "symbol",
20+
outputs: [{ name: "", type: "string" }],
21+
type: "function"
22+
}
23+
] as const;
+232-63
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,262 @@
1-
import type { IAgentRuntime, Memory, State } from "@ai16z/eliza";
1+
import {
2+
IAgentRuntime,
3+
Memory,
4+
State,
5+
ModelClass,
6+
composeContext,
7+
generateObject,
8+
HandlerCallback
9+
} from "@ai16z/eliza";
210
import {
311
ChainId,
412
createConfig,
513
executeRoute,
614
ExtendedChain,
715
getRoutes,
16+
EVM,
17+
EVMProviderOptions,
818
} from "@lifi/sdk";
9-
import { getChainConfigs, WalletProvider } from "../providers/wallet";
19+
import { WalletProvider, evmWalletProvider, getChainConfigs } from "../providers/wallet";
1020
import { bridgeTemplate } from "../templates";
11-
import type { BridgeParams, Transaction } from "../types";
21+
import type { BridgeParams, Transaction, SupportedChain } from "../types";
22+
import { parseEther, formatEther, Client } from "viem";
23+
1224

1325
export { bridgeTemplate };
1426

27+
// Validate the generated content structure
28+
function isBridgeContent(content: any): content is BridgeParams {
29+
return (
30+
typeof content === "object" &&
31+
content !== null &&
32+
typeof content.fromChain === "string" &&
33+
typeof content.toChain === "string" &&
34+
["ethereum", "base", "sepolia"].includes(content.fromChain) &&
35+
["ethereum", "base", "sepolia"].includes(content.toChain) &&
36+
typeof content.amount === "string" &&
37+
!isNaN(Number(content.amount)) &&
38+
(content.toAddress === null ||
39+
(typeof content.toAddress === "string" &&
40+
content.toAddress.startsWith("0x") &&
41+
content.toAddress.length === 42))
42+
);
43+
}
44+
1545
export class BridgeAction {
1646
private config;
1747

1848
constructor(private walletProvider: WalletProvider) {
49+
// Configure EVM provider for LI.FI SDK
50+
const evmProviderConfig: EVMProviderOptions = {
51+
getWalletClient: async () => {
52+
const client = this.walletProvider.getWalletClient();
53+
return client as unknown as Client;
54+
},
55+
switchChain: async (chainId: number) => {
56+
const chainName = Object.entries(getChainConfigs(this.walletProvider.runtime))
57+
.find(([_, config]) => config.chainId === chainId)?.[0] as SupportedChain;
58+
59+
if (!chainName) {
60+
throw new Error(`Chain ID ${chainId} not supported`);
61+
}
62+
63+
await this.walletProvider.switchChain(
64+
this.walletProvider.runtime,
65+
chainName
66+
);
67+
const client = this.walletProvider.getWalletClient();
68+
return client as unknown as Client;
69+
}
70+
};
71+
1972
this.config = createConfig({
2073
integrator: "eliza",
21-
chains: Object.values(
22-
getChainConfigs(this.walletProvider.runtime)
23-
).map((config) => ({
24-
id: config.chainId,
25-
name: config.name,
26-
key: config.name.toLowerCase(),
27-
chainType: "EVM",
28-
nativeToken: {
29-
...config.nativeCurrency,
30-
chainId: config.chainId,
31-
address: "0x0000000000000000000000000000000000000000",
32-
coinKey: config.nativeCurrency.symbol,
33-
},
34-
metamask: {
35-
chainId: `0x${config.chainId.toString(16)}`,
36-
chainName: config.name,
37-
nativeCurrency: config.nativeCurrency,
38-
rpcUrls: [config.rpcUrl],
39-
blockExplorerUrls: [config.blockExplorerUrl],
40-
},
41-
diamondAddress: "0x0000000000000000000000000000000000000000",
42-
coin: config.nativeCurrency.symbol,
43-
mainnet: true,
44-
})) as ExtendedChain[],
74+
chains: Object.values(getChainConfigs(this.walletProvider.runtime))
75+
.map((config) => ({
76+
id: config.chainId,
77+
name: config.name,
78+
key: config.name.toLowerCase(),
79+
chainType: "EVM" as const,
80+
nativeToken: {
81+
...config.nativeCurrency,
82+
chainId: config.chainId,
83+
address: "0x0000000000000000000000000000000000000000",
84+
coinKey: config.nativeCurrency.symbol,
85+
},
86+
metamask: {
87+
chainId: `0x${config.chainId.toString(16)}`,
88+
chainName: config.name,
89+
nativeCurrency: config.nativeCurrency,
90+
rpcUrls: [config.rpcUrl],
91+
blockExplorerUrls: [config.blockExplorerUrl],
92+
},
93+
diamondAddress: "0x0000000000000000000000000000000000000000",
94+
coin: config.nativeCurrency.symbol,
95+
mainnet: true,
96+
})) as ExtendedChain[],
97+
providers: [
98+
EVM(evmProviderConfig)
99+
]
45100
});
46101
}
47102

48-
async bridge(params: BridgeParams): Promise<Transaction> {
103+
async bridge(
104+
runtime: IAgentRuntime,
105+
params: BridgeParams
106+
): Promise<Transaction> {
107+
console.log("🌉 Starting bridge with params:", params);
108+
109+
// Validate amount
110+
if (!params.amount || isNaN(Number(params.amount)) || Number(params.amount) <= 0) {
111+
throw new Error(`Invalid amount: ${params.amount}. Must be a positive number.`);
112+
}
113+
114+
// Get current balance
49115
const walletClient = this.walletProvider.getWalletClient();
50116
const [fromAddress] = await walletClient.getAddresses();
117+
console.log("💳 From address:", fromAddress);
51118

52-
const routes = await getRoutes({
53-
fromChainId: getChainConfigs(this.walletProvider.runtime)[
54-
params.fromChain
55-
].chainId as ChainId,
56-
toChainId: getChainConfigs(this.walletProvider.runtime)[
57-
params.toChain
58-
].chainId as ChainId,
59-
fromTokenAddress: params.fromToken,
60-
toTokenAddress: params.toToken,
61-
fromAmount: params.amount,
62-
fromAddress: fromAddress,
63-
toAddress: params.toAddress || fromAddress,
64-
});
119+
// Switch to source chain and check balance
120+
await this.walletProvider.switchChain(runtime, params.fromChain);
121+
const balance = await this.walletProvider.getWalletBalance();
122+
console.log("💰 Current balance:", balance ? formatEther(balance) : "0");
65123

66-
if (!routes.routes.length) throw new Error("No routes found");
124+
// Validate sufficient balance
125+
const amountInWei = parseEther(params.amount);
126+
if (!balance || balance < amountInWei) {
127+
throw new Error(
128+
`Insufficient balance. Required: ${params.amount} ETH, Available: ${
129+
balance ? formatEther(balance) : "0"
130+
} ETH`
131+
);
132+
}
67133

68-
const execution = await executeRoute(routes.routes[0], this.config);
69-
const process = execution.steps[0]?.execution?.process[0];
134+
console.log("💵 Amount to bridge (in Wei):", amountInWei.toString());
70135

71-
if (!process?.status || process.status === "FAILED") {
72-
throw new Error("Transaction failed");
73-
}
136+
try {
137+
console.log("🔍 Finding bridge routes...");
138+
const routes = await getRoutes({
139+
fromChainId: getChainConfigs(runtime)[params.fromChain].chainId as ChainId,
140+
toChainId: getChainConfigs(runtime)[params.toChain].chainId as ChainId,
141+
fromTokenAddress: params.fromToken ?? "0x0000000000000000000000000000000000000000",
142+
toTokenAddress: params.toToken ?? "0x0000000000000000000000000000000000000000",
143+
fromAmount: amountInWei.toString(),
144+
fromAddress: fromAddress,
145+
toAddress: params.toAddress || fromAddress,
146+
});
74147

75-
return {
76-
hash: process.txHash as `0x${string}`,
77-
from: fromAddress,
78-
to: routes.routes[0].steps[0].estimate
79-
.approvalAddress as `0x${string}`,
80-
value: BigInt(params.amount),
81-
chainId: getChainConfigs(this.walletProvider.runtime)[
82-
params.fromChain
83-
].chainId,
84-
};
148+
if (!routes.routes.length) {
149+
throw new Error("No bridge routes found. The requested bridge path might not be supported.");
150+
}
151+
152+
// Log route details
153+
const selectedRoute = routes.routes[0];
154+
console.log("🛣️ Selected route:", {
155+
steps: selectedRoute.steps.length,
156+
estimatedGas: selectedRoute.gasCostUSD,
157+
estimatedTime: selectedRoute.steps[0].estimate.executionDuration,
158+
});
159+
160+
console.log("✨ Executing bridge transaction...");
161+
const execution = await executeRoute(selectedRoute, this.config);
162+
const process = execution.steps[0]?.execution?.process[0];
163+
164+
if (!process?.status || process.status === "FAILED") {
165+
throw new Error(`Bridge transaction failed. Status: ${process?.status}, Error: ${process?.error}`);
166+
}
167+
168+
console.log("✅ Bridge initiated successfully!", {
169+
hash: process.txHash,
170+
from: fromAddress,
171+
to: selectedRoute.steps[0].estimate.approvalAddress,
172+
value: params.amount,
173+
estimatedTime: selectedRoute.steps[0].estimate.executionDuration
174+
});
175+
176+
return {
177+
hash: process.txHash as `0x${string}`,
178+
from: fromAddress,
179+
to: selectedRoute.steps[0].estimate.approvalAddress as `0x${string}`,
180+
value: amountInWei.toString(),
181+
chainId: getChainConfigs(runtime)[params.fromChain].chainId,
182+
};
183+
} catch (error) {
184+
console.error("❌ Bridge failed with error:", {
185+
message: error.message,
186+
code: error.code,
187+
details: error.details,
188+
stack: error.stack
189+
});
190+
throw new Error(`Bridge failed: ${error.message}`);
191+
}
85192
}
86193
}
87194

88195
export const bridgeAction = {
89196
name: "bridge",
90-
description: "Bridge tokens between different chains",
197+
description: "Bridge tokens between different chains via the LiFi SDK",
91198
handler: async (
92199
runtime: IAgentRuntime,
93200
message: Memory,
94201
state: State,
95-
options: any
202+
_options: any,
203+
callback?: HandlerCallback
96204
) => {
97-
const walletProvider = new WalletProvider(runtime);
98-
const action = new BridgeAction(walletProvider);
99-
return action.bridge(options);
205+
try {
206+
// Compose state if not provided
207+
if (!state) {
208+
state = (await runtime.composeState(message)) as State;
209+
} else {
210+
state = await runtime.updateRecentMessageState(state);
211+
}
212+
213+
// Get wallet info for context
214+
const walletInfo = await evmWalletProvider.get(runtime, message, state);
215+
state.walletInfo = walletInfo;
216+
217+
// Generate structured content from natural language
218+
const bridgeContext = composeContext({
219+
state,
220+
template: bridgeTemplate,
221+
});
222+
223+
const content = await generateObject({
224+
runtime,
225+
context: bridgeContext,
226+
modelClass: ModelClass.LARGE,
227+
});
228+
229+
console.log("Generated content:", content);
230+
231+
// Validate the generated content
232+
if (!isBridgeContent(content)) {
233+
throw new Error("Invalid content structure for bridge action");
234+
}
235+
236+
const walletProvider = new WalletProvider(runtime);
237+
const action = new BridgeAction(walletProvider);
238+
const result = await action.bridge(runtime, content);
239+
240+
if (callback) {
241+
callback({
242+
text: `Successfully bridged ${content.amount} from ${content.fromChain} to ${content.toChain}. Transaction hash: ${result.hash}`,
243+
content: {
244+
transaction: {
245+
...result,
246+
value: result.value.toString(),
247+
}
248+
}
249+
});
250+
}
251+
252+
return true;
253+
} catch (error) {
254+
console.error("Error in bridge handler:", error);
255+
if (callback) {
256+
callback({ text: `Error: ${error.message}` });
257+
}
258+
return false;
259+
}
100260
},
101261
template: bridgeTemplate,
102262
validate: async (runtime: IAgentRuntime) => {
@@ -113,6 +273,15 @@ export const bridgeAction = {
113273
},
114274
},
115275
],
276+
[
277+
{
278+
user: "user",
279+
content: {
280+
text: "Send 0.5 ETH from Base to Ethereum",
281+
action: "CROSS_CHAIN_TRANSFER",
282+
},
283+
},
284+
],
116285
],
117286
similes: ["CROSS_CHAIN_TRANSFER", "CHAIN_BRIDGE", "MOVE_CROSS_CHAIN"],
118-
}; // TODO: add more examples / similies
287+
};

0 commit comments

Comments
 (0)