@@ -9,90 +9,266 @@ import {
9
9
executeRoute ,
10
10
ExtendedChain ,
11
11
getRoutes ,
12
+ Route ,
12
13
} from "@lifi/sdk" ;
13
14
14
15
import { initWalletProvider , WalletProvider } from "../providers/wallet" ;
15
16
import { swapTemplate } from "../templates" ;
16
- import type { SwapParams , Transaction } from "../types" ;
17
- import { parseEther } from "viem" ;
17
+ import type { SwapParams , SwapQuote , Transaction } from "../types" ;
18
+ import { Address , ByteArray , encodeFunctionData , Hex , parseAbi , parseEther , parseUnits } from "viem" ;
19
+ import { BebopRoute } from '../types/index' ;
18
20
19
21
export { swapTemplate } ;
20
22
21
23
export class SwapAction {
22
- private config ;
24
+ private lifiConfig ;
25
+ private bebopChainsMap ;
23
26
24
27
constructor ( private walletProvider : WalletProvider ) {
25
- this . config = createConfig ( {
26
- integrator : "eliza" ,
27
- chains : Object . values ( this . walletProvider . chains ) . map ( ( config ) => ( {
28
- id : config . id ,
29
- name : config . name ,
30
- key : config . name . toLowerCase ( ) ,
31
- chainType : "EVM" as const ,
32
- nativeToken : {
33
- ...config . nativeCurrency ,
34
- chainId : config . id ,
35
- address : "0x0000000000000000000000000000000000000000" ,
36
- coinKey : config . nativeCurrency . symbol ,
37
- priceUSD : "0" ,
38
- logoURI : "" ,
39
- symbol : config . nativeCurrency . symbol ,
40
- decimals : config . nativeCurrency . decimals ,
41
- name : config . nativeCurrency . name ,
42
- } ,
43
- rpcUrls : {
44
- public : { http : [ config . rpcUrls . default . http [ 0 ] ] } ,
45
- } ,
46
- blockExplorerUrls : [ config . blockExplorers . default . url ] ,
47
- metamask : {
48
- chainId : `0x${ config . id . toString ( 16 ) } ` ,
49
- chainName : config . name ,
50
- nativeCurrency : config . nativeCurrency ,
51
- rpcUrls : [ config . rpcUrls . default . http [ 0 ] ] ,
28
+ this . walletProvider = walletProvider ;
29
+ let lifiChains : ExtendedChain [ ] = [ ] ;
30
+ for ( const config of Object . values ( this . walletProvider . chains ) ) {
31
+ try {
32
+ lifiChains . push ( {
33
+ id : config . id ,
34
+ name : config . name ,
35
+ key : config . name . toLowerCase ( ) ,
36
+ chainType : "EVM" as const ,
37
+ nativeToken : {
38
+ ...config . nativeCurrency ,
39
+ chainId : config . id ,
40
+ address : "0x0000000000000000000000000000000000000000" ,
41
+ coinKey : config . nativeCurrency . symbol ,
42
+ priceUSD : "0" ,
43
+ logoURI : "" ,
44
+ symbol : config . nativeCurrency . symbol ,
45
+ decimals : config . nativeCurrency . decimals ,
46
+ name : config . nativeCurrency . name ,
47
+ } ,
48
+ rpcUrls : {
49
+ public : { http : [ config . rpcUrls . default . http [ 0 ] ] } ,
50
+ } ,
52
51
blockExplorerUrls : [ config . blockExplorers . default . url ] ,
53
- } ,
54
- coin : config . nativeCurrency . symbol ,
55
- mainnet : true ,
56
- diamondAddress : "0x0000000000000000000000000000000000000000" ,
57
- } ) ) as ExtendedChain [ ] ,
58
- } ) ;
52
+ metamask : {
53
+ chainId : `0x${ config . id . toString ( 16 ) } ` ,
54
+ chainName : config . name ,
55
+ nativeCurrency : config . nativeCurrency ,
56
+ rpcUrls : [ config . rpcUrls . default . http [ 0 ] ] ,
57
+ blockExplorerUrls : [ config . blockExplorers . default . url ] ,
58
+ } ,
59
+ coin : config . nativeCurrency . symbol ,
60
+ mainnet : true ,
61
+ diamondAddress : "0x0000000000000000000000000000000000000000" ,
62
+ } as ExtendedChain ) ;
63
+ } catch {
64
+ // Skip chains with missing config in viem
65
+ }
66
+ }
67
+ this . lifiConfig = createConfig ( {
68
+ integrator : "eliza" ,
69
+ chains : lifiChains
70
+ } )
71
+ this . bebopChainsMap = {
72
+ 'mainnet' : 'ethereum'
73
+ }
59
74
}
60
75
61
76
async swap ( params : SwapParams ) : Promise < Transaction > {
62
77
const walletClient = this . walletProvider . getWalletClient ( params . chain ) ;
63
78
const [ fromAddress ] = await walletClient . getAddresses ( ) ;
64
79
65
- const routes = await getRoutes ( {
66
- fromChainId : this . walletProvider . getChainConfigs ( params . chain ) . id ,
67
- toChainId : this . walletProvider . getChainConfigs ( params . chain ) . id ,
68
- fromTokenAddress : params . fromToken ,
69
- toTokenAddress : params . toToken ,
70
- fromAmount : parseEther ( params . amount ) . toString ( ) ,
71
- fromAddress : fromAddress ,
72
- options : {
73
- slippage : params . slippage || 0.5 ,
74
- order : "RECOMMENDED" ,
75
- } ,
80
+ // Getting quotes from different aggregators and sorting them by minAmount (amount after slippage)
81
+ const sortedQuotes : SwapQuote [ ] = await this . getSortedQuotes ( fromAddress , params ) ;
82
+
83
+ // Trying to execute the best quote by amount, fallback to the next one if it fails
84
+ for ( const quote of sortedQuotes ) {
85
+ let res ;
86
+ switch ( quote . aggregator ) {
87
+ case "lifi" :
88
+ res = await this . executeLifiQuote ( quote ) ;
89
+ break ;
90
+ case "bebop" :
91
+ res = await this . executeBebopQuote ( quote , params ) ;
92
+ break
93
+ default :
94
+ throw new Error ( "No aggregator found" ) ;
95
+ }
96
+ if ( res !== undefined ) return res ;
97
+ }
98
+ throw new Error ( "Execution failed" ) ;
99
+ }
100
+
101
+ private async getSortedQuotes ( fromAddress : Address , params : SwapParams ) : Promise < SwapQuote [ ] > {
102
+ const decimalsAbi = parseAbi ( [ 'function decimals() view returns (uint8)' ] ) ;
103
+ const decimals = await this . walletProvider . getPublicClient ( params . chain ) . readContract ( {
104
+ address : params . fromToken ,
105
+ abi : decimalsAbi ,
106
+ functionName : 'decimals' ,
76
107
} ) ;
108
+ const quotes : SwapQuote [ ] | undefined = await Promise . all ( [
109
+ this . getLifiQuote ( fromAddress , params , decimals ) ,
110
+ this . getBebopQuote ( fromAddress , params , decimals )
111
+ ] ) ;
112
+ const sortedQuotes : SwapQuote [ ] = quotes . filter ( ( quote ) => quote !== undefined ) as SwapQuote [ ] ;
113
+ sortedQuotes . sort ( ( a , b ) => BigInt ( a . minOutputAmount ) > BigInt ( b . minOutputAmount ) ? - 1 : 1 ) ;
114
+ if ( sortedQuotes . length === 0 ) throw new Error ( "No routes found" ) ;
115
+ return sortedQuotes ;
116
+ }
117
+
118
+ private async getLifiQuote ( fromAddress : Address , params : SwapParams , fromTokenDecimals : number ) : Promise < SwapQuote | undefined > {
119
+ try {
120
+ const routes = await getRoutes ( {
121
+ fromChainId : this . walletProvider . getChainConfigs ( params . chain ) . id ,
122
+ toChainId : this . walletProvider . getChainConfigs ( params . chain ) . id ,
123
+ fromTokenAddress : params . fromToken ,
124
+ toTokenAddress : params . toToken ,
125
+ fromAmount : parseUnits ( params . amount , fromTokenDecimals ) . toString ( ) ,
126
+ fromAddress : fromAddress ,
127
+ options : {
128
+ slippage : params . slippage / 100 || 0.005 ,
129
+ order : "RECOMMENDED" ,
130
+ } ,
131
+ } ) ;
132
+ if ( ! routes . routes . length ) throw new Error ( "No routes found" ) ;
133
+ return {
134
+ aggregator : "lifi" ,
135
+ minOutputAmount : routes . routes [ 0 ] . steps [ 0 ] . estimate . toAmountMin ,
136
+ swapData : routes . routes [ 0 ]
137
+ }
138
+ } catch ( error ) {
139
+ console . debug ( "Error in getLifiQuote:" , error . message ) ;
140
+ return undefined ;
141
+ }
142
+ }
77
143
78
- if ( ! routes . routes . length ) throw new Error ( "No routes found" ) ;
144
+ private async getBebopQuote ( fromAddress : Address , params : SwapParams , fromTokenDecimals : number ) : Promise < SwapQuote | undefined > {
145
+ try {
146
+ const url = `https://api.bebop.xyz/router/${ this . bebopChainsMap [ params . chain ] ?? params . chain } /v1/quote` ;
147
+ const reqParams = new URLSearchParams ( {
148
+ sell_tokens : params . fromToken ,
149
+ buy_tokens : params . toToken ,
150
+ sell_amounts : parseUnits ( params . amount , fromTokenDecimals ) . toString ( ) ,
151
+ taker_address : fromAddress ,
152
+ approval_type : 'Standard' ,
153
+ skip_validation : 'true' ,
154
+ gasless : 'false' ,
155
+ source : 'eliza'
156
+ } ) ;
157
+ const response = await fetch ( `${ url } ?${ reqParams . toString ( ) } ` , {
158
+ method : 'GET' ,
159
+ headers : { 'accept' : 'application/json' } ,
160
+ } ) ;
161
+ if ( ! response . ok ) {
162
+ throw Error ( response . statusText ) ;
163
+ }
164
+ const data = await response . json ( ) ;
165
+ const route : BebopRoute = {
166
+ data : data . routes [ 0 ] . quote . tx . data ,
167
+ sellAmount : parseUnits ( params . amount , fromTokenDecimals ) . toString ( ) ,
168
+ approvalTarget : data . routes [ 0 ] . quote . approvalTarget as `0x${string } `,
169
+ from : data . routes [ 0 ] . quote . tx . from as `0x${string } `,
170
+ value : data . routes [ 0 ] . quote . tx . value . toString ( ) ,
171
+ to : data . routes [ 0 ] . quote . tx . to as `0x${string } `,
172
+ gas : data . routes [ 0 ] . quote . tx . gas . toString ( ) ,
173
+ gasPrice : data . routes [ 0 ] . quote . tx . gasPrice . toString ( )
174
+ }
175
+ return {
176
+ aggregator : "bebop" ,
177
+ minOutputAmount : data . routes [ 0 ] . quote . buyTokens [ params . toToken ] . minimumAmount . toString ( ) ,
178
+ swapData : route
179
+ }
79
180
80
- const execution = await executeRoute ( routes . routes [ 0 ] , this . config ) ;
81
- const process = execution . steps [ 0 ] ?. execution ?. process [ 0 ] ;
181
+ } catch ( error ) {
182
+ console . debug ( "Error in getBebopQuote:" , error . message ) ;
183
+ return undefined ;
184
+ }
185
+ }
186
+
187
+ private async executeLifiQuote ( quote : SwapQuote ) : Promise < Transaction | undefined > {
188
+ try {
189
+ const route : Route = quote . swapData as Route ;
190
+ const execution = await executeRoute ( quote . swapData as Route , this . lifiConfig ) ;
191
+ const process = execution . steps [ 0 ] ?. execution ?. process [ 0 ] ;
82
192
83
- if ( ! process ?. status || process . status === "FAILED" ) {
84
- throw new Error ( "Transaction failed" ) ;
193
+ if ( ! process ?. status || process . status === "FAILED" ) {
194
+ throw new Error ( "Transaction failed" ) ;
195
+ }
196
+ return {
197
+ hash : process . txHash as `0x${string } `,
198
+ from : route . fromAddress ! as `0x${string } `,
199
+ to : route . steps [ 0 ] . estimate . approvalAddress as `0x${string } `,
200
+ value : 0n ,
201
+ data : process . data as `0x${string } `,
202
+ chainId : route . fromChainId
203
+ }
204
+ } catch ( error ) {
205
+ return undefined ;
85
206
}
207
+ }
86
208
87
- return {
88
- hash : process . txHash as `0x${string } `,
89
- from : fromAddress ,
90
- to : routes . routes [ 0 ] . steps [ 0 ] . estimate
91
- . approvalAddress as `0x${string } `,
92
- value : 0n ,
93
- data : process . data as `0x${string } `,
94
- chainId : this . walletProvider . getChainConfigs ( params . chain ) . id ,
95
- } ;
209
+ private async executeBebopQuote ( quote : SwapQuote , params : SwapParams ) : Promise < Transaction | undefined > {
210
+ try {
211
+ const bebopRoute : BebopRoute = quote . swapData as BebopRoute ;
212
+ const allowanceAbi = parseAbi ( [ 'function allowance(address,address) view returns (uint256)' ] ) ;
213
+ const allowance : bigint = await this . walletProvider . getPublicClient ( params . chain ) . readContract ( {
214
+ address : params . fromToken ,
215
+ abi : allowanceAbi ,
216
+ functionName : 'allowance' ,
217
+ args : [ bebopRoute . from , bebopRoute . approvalTarget ]
218
+ } ) ;
219
+ if ( allowance < BigInt ( bebopRoute . sellAmount ) ) {
220
+ const approvalData = encodeFunctionData ( {
221
+ abi : parseAbi ( [ 'function approve(address,uint256)' ] ) ,
222
+ functionName : 'approve' ,
223
+ args : [ bebopRoute . approvalTarget , BigInt ( bebopRoute . sellAmount ) ] ,
224
+ } ) ;
225
+ await this . walletProvider . getWalletClient ( params . chain ) . sendTransaction ( {
226
+ account : this . walletProvider . getWalletClient ( params . chain ) . account ,
227
+ to : params . fromToken ,
228
+ value : 0n ,
229
+ data : approvalData ,
230
+ kzg : {
231
+ blobToKzgCommitment : function ( _ : ByteArray ) : ByteArray {
232
+ throw new Error ( "Function not implemented." ) ;
233
+ } ,
234
+ computeBlobKzgProof : function (
235
+ _blob : ByteArray ,
236
+ _commitment : ByteArray
237
+ ) : ByteArray {
238
+ throw new Error ( "Function not implemented." ) ;
239
+ } ,
240
+ } ,
241
+ chain : undefined ,
242
+ } ) ;
243
+ }
244
+ const hash = await this . walletProvider . getWalletClient ( params . chain ) . sendTransaction ( {
245
+ account : this . walletProvider . getWalletClient ( params . chain ) . account ,
246
+ to : bebopRoute . to ,
247
+ value : BigInt ( bebopRoute . value ) ,
248
+ data : bebopRoute . data as Hex ,
249
+ kzg : {
250
+ blobToKzgCommitment : function ( _ : ByteArray ) : ByteArray {
251
+ throw new Error ( "Function not implemented." ) ;
252
+ } ,
253
+ computeBlobKzgProof : function (
254
+ _blob : ByteArray ,
255
+ _commitment : ByteArray
256
+ ) : ByteArray {
257
+ throw new Error ( "Function not implemented." ) ;
258
+ } ,
259
+ } ,
260
+ chain : undefined ,
261
+ } ) ;
262
+ return {
263
+ hash,
264
+ from : this . walletProvider . getWalletClient ( params . chain ) . account . address ,
265
+ to : bebopRoute . to ,
266
+ value : BigInt ( bebopRoute . value ) ,
267
+ data : bebopRoute . data as Hex ,
268
+ } ;
269
+ } catch ( error ) {
270
+ return undefined ;
271
+ }
96
272
}
97
273
}
98
274
0 commit comments