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" ;
2
10
import {
3
11
ChainId ,
4
12
createConfig ,
5
13
executeRoute ,
6
14
ExtendedChain ,
7
15
getRoutes ,
16
+ EVM ,
17
+ EVMProviderOptions ,
8
18
} from "@lifi/sdk" ;
9
- import { getChainConfigs , WalletProvider } from "../providers/wallet" ;
19
+ import { WalletProvider , evmWalletProvider , getChainConfigs } from "../providers/wallet" ;
10
20
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
+
12
24
13
25
export { bridgeTemplate } ;
14
26
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
+
15
45
export class BridgeAction {
16
46
private config ;
17
47
18
48
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
+
19
72
this . config = createConfig ( {
20
73
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
+ ]
45
100
} ) ;
46
101
}
47
102
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
49
115
const walletClient = this . walletProvider . getWalletClient ( ) ;
50
116
const [ fromAddress ] = await walletClient . getAddresses ( ) ;
117
+ console . log ( "💳 From address:" , fromAddress ) ;
51
118
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" ) ;
65
123
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
+ }
67
133
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 ( ) ) ;
70
135
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
+ } ) ;
74
147
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
+ }
85
192
}
86
193
}
87
194
88
195
export const bridgeAction = {
89
196
name : "bridge" ,
90
- description : "Bridge tokens between different chains" ,
197
+ description : "Bridge tokens between different chains via the LiFi SDK " ,
91
198
handler : async (
92
199
runtime : IAgentRuntime ,
93
200
message : Memory ,
94
201
state : State ,
95
- options : any
202
+ _options : any ,
203
+ callback ?: HandlerCallback
96
204
) => {
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
+ }
100
260
} ,
101
261
template : bridgeTemplate ,
102
262
validate : async ( runtime : IAgentRuntime ) => {
@@ -113,6 +273,15 @@ export const bridgeAction = {
113
273
} ,
114
274
} ,
115
275
] ,
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
+ ] ,
116
285
] ,
117
286
similes : [ "CROSS_CHAIN_TRANSFER" , "CHAIN_BRIDGE" , "MOVE_CROSS_CHAIN" ] ,
118
- } ; // TODO: add more examples / similies
287
+ } ;
0 commit comments