@@ -11,8 +11,10 @@ import type {
11
11
} from "../types.js" ;
12
12
import type {
13
13
AttestationReceipt ,
14
+ AttestedTransferReceipt ,
14
15
Chain ,
15
16
ChainContext ,
17
+ CompletedTransferReceipt ,
16
18
Network ,
17
19
Signer ,
18
20
SourceInitiatedTransferReceipt ,
@@ -26,19 +28,19 @@ import {
26
28
Wormhole ,
27
29
amount ,
28
30
canonicalAddress ,
29
- chainToPlatform ,
30
31
contracts ,
31
32
isAttested ,
32
- isNative ,
33
+ isSourceFinalized ,
33
34
isSourceInitiated ,
34
35
resolveWrappedToken ,
35
36
signSendWait ,
36
37
} from "./../../index.js" ;
37
- import type { ChainAddress } from "@wormhole-foundation/sdk-definitions" ;
38
+ import type { ChainAddress , WormholeMessageId } from "@wormhole-foundation/sdk-definitions" ;
38
39
import type { RouteTransferRequest } from "../request.js" ;
39
40
40
41
export const SLIPPAGE_BPS = 15n ; // 0.15%
41
- export const BPS_PER_HUNDRED_PERCENT = 10000n ;
42
+ export const MAX_SLIPPAGE_BPS = 100n ; // 1%
43
+ export const BPS_PER_HUNDRED_PERCENT = 10_000n ;
42
44
43
45
export namespace PorticoRoute {
44
46
export type Options = { } ;
@@ -78,8 +80,6 @@ export class AutomaticPorticoRoute<N extends Network>
78
80
name : "AutomaticPortico" ,
79
81
} ;
80
82
81
- private static _supportedTokens = [ "WETH" , "WSTETH" ] ;
82
-
83
83
static supportedNetworks ( ) : Network [ ] {
84
84
return [ "Mainnet" ] ;
85
85
}
@@ -92,19 +92,12 @@ export class AutomaticPorticoRoute<N extends Network>
92
92
}
93
93
94
94
static async supportedSourceTokens ( fromChain : ChainContext < Network > ) : Promise < TokenId [ ] > {
95
- const { chain } = fromChain ;
96
- const supported = this . _supportedTokens
97
- . map ( ( symbol ) => {
98
- return filters . bySymbol ( fromChain . config . tokenMap ! , symbol ) ?? [ ] ;
99
- } )
100
- . flat ( )
101
- . filter ( ( td ) => {
102
- const localOrEth = ! td . original || td . original === "Ethereum" ;
103
- const isAvax = chain === "Avalanche" && isNative ( td . address ) ;
104
- return localOrEth && ! isAvax ;
105
- } ) ;
106
-
107
- return supported . map ( ( td ) => Wormhole . tokenId ( chain , td . address ) ) ;
95
+ const pb = await fromChain . getPorticoBridge ( ) ;
96
+ const { tokenMap } = fromChain . config ;
97
+ return pb
98
+ . supportedTokens ( )
99
+ . filter ( ( t ) => ! tokenMap || filters . byAddress ( tokenMap , canonicalAddress ( t . token ) ) )
100
+ . map ( ( t ) => t . token ) ;
108
101
}
109
102
110
103
static async supportedDestinationTokens < N extends Network > (
@@ -119,51 +112,39 @@ export class AutomaticPorticoRoute<N extends Network>
119
112
) ;
120
113
const tokenAddress = canonicalAddress ( srcTokenAddress ) ;
121
114
122
- // The token that will be used to bridge
123
115
const pb = await fromChain . getPorticoBridge ( ) ;
124
- const transferrableToken = pb . getTransferrableToken ( tokenAddress ) ;
125
116
126
- // The tokens that _will_ be received on redemption
127
- const redeemToken = await TokenTransfer . lookupDestinationToken (
128
- fromChain ,
129
- toChain ,
130
- transferrableToken ,
131
- ) ;
117
+ try {
118
+ // The highway token that will be used to bridge
119
+ const transferrableToken = await pb . getTransferrableToken ( tokenAddress ) ;
120
+ // Make sure it exists on the destination chain
121
+ await TokenTransfer . lookupDestinationToken ( fromChain , toChain , transferrableToken ) ;
122
+ } catch {
123
+ return [ ] ;
124
+ }
132
125
133
- // Grab the symbol for the token that gets redeemed
134
- const redeemTokenDetails = filters . byAddress (
135
- toChain . config . tokenMap ! ,
136
- canonicalAddress ( redeemToken ) ,
137
- ) ! ;
138
-
139
- // Find the local/native version of the same token by symbol
140
- const locallyRedeemable = (
141
- filters . bySymbol ( toChain . config . tokenMap ! , redeemTokenDetails . symbol ) ?? [ ]
142
- )
143
- . filter ( ( td ) => {
144
- return ! td . original ;
145
- } )
146
- . map ( ( td ) => {
147
- switch ( td . symbol ) {
148
- case "ETH" :
149
- case "WETH" :
150
- return Wormhole . tokenId ( toChain . chain , td . address ) ;
151
- case "WSTETH" :
152
- return Wormhole . tokenId ( toChain . chain , td . address ) ;
153
- default :
154
- throw new Error ( "Unknown symbol: " + redeemTokenDetails . symbol ) ;
155
- }
156
- } ) ;
157
-
158
- return locallyRedeemable ;
126
+ // Find the destination token(s) in the same group
127
+ const toPb = await toChain . getPorticoBridge ( ) ;
128
+ const tokens = toPb . supportedTokens ( ) ;
129
+ const { tokenMap } = toChain . config ;
130
+ const group = pb . getTokenGroup ( tokenAddress ) ;
131
+ return tokens
132
+ . filter (
133
+ ( t ) =>
134
+ ( t . group === group ||
135
+ // ETH/WETH supports wrapping/unwrapping
136
+ ( t . group === "ETH" && group === "WETH" ) ||
137
+ ( t . group === "WETH" && group === "ETH" ) ) &&
138
+ ( ! tokenMap || filters . byAddress ( tokenMap , canonicalAddress ( t . token ) ) ) ,
139
+ )
140
+ . map ( ( t ) => t . token ) ;
159
141
}
160
142
161
143
static isProtocolSupported < N extends Network > ( chain : ChainContext < N > ) : boolean {
162
144
return chain . supportsPorticoBridge ( ) ;
163
145
}
164
146
165
147
async isAvailable ( ) : Promise < boolean > {
166
- // TODO:
167
148
return true ;
168
149
}
169
150
@@ -174,10 +155,10 @@ export class AutomaticPorticoRoute<N extends Network>
174
155
async validate ( request : RouteTransferRequest < N > , params : TP ) : Promise < VR > {
175
156
try {
176
157
if (
177
- chainToPlatform ( request . fromChain . chain ) !== "Evm" ||
178
- chainToPlatform ( request . toChain . chain ) !== "Evm"
158
+ ! AutomaticPorticoRoute . isProtocolSupported ( request . fromChain ) ||
159
+ ! AutomaticPorticoRoute . isProtocolSupported ( request . toChain )
179
160
) {
180
- throw new Error ( "Only EVM chains are supported" ) ;
161
+ throw new Error ( "Protocol not supported" ) ;
181
162
}
182
163
183
164
const { fromChain, toChain, source, destination } = request ;
@@ -190,9 +171,11 @@ export class AutomaticPorticoRoute<N extends Network>
190
171
const fromPb = await fromChain . getPorticoBridge ( ) ;
191
172
const toPb = await toChain . getPorticoBridge ( ) ;
192
173
193
- const canonicalSourceToken = fromPb . getTransferrableToken ( canonicalAddress ( sourceToken ) ) ;
174
+ const canonicalSourceToken = await fromPb . getTransferrableToken (
175
+ canonicalAddress ( sourceToken ) ,
176
+ ) ;
194
177
195
- const canonicalDestinationToken = toPb . getTransferrableToken (
178
+ const canonicalDestinationToken = await toPb . getTransferrableToken (
196
179
canonicalAddress ( destinationToken ) ,
197
180
) ;
198
181
@@ -216,7 +199,25 @@ export class AutomaticPorticoRoute<N extends Network>
216
199
217
200
async quote ( request : RouteTransferRequest < N > , params : VP ) : Promise < QR > {
218
201
try {
219
- const swapAmounts = await this . quoteUniswap ( request , params ) ;
202
+ const swapAmounts = await this . fetchSwapQuote ( request , params ) ;
203
+
204
+ // destination token may have a different number of decimals than the source token
205
+ // so we need to scale the amounts to the token with the most decimals
206
+ // before comparing them
207
+ const maxDecimals = Math . max ( request . source . decimals , request . destination . decimals ) ;
208
+ const scaledAmount = amount . units ( amount . scale ( params . normalizedParams . amount , maxDecimals ) ) ;
209
+ const scaledMinAmountFinish = amount . units (
210
+ amount . scale (
211
+ amount . fromBaseUnits ( swapAmounts . minAmountFinish , request . destination . decimals ) ,
212
+ maxDecimals ,
213
+ ) ,
214
+ ) ;
215
+ // if the slippage is more than 100bps, this likely means that the pools are unbalanced
216
+ if (
217
+ scaledMinAmountFinish <
218
+ scaledAmount - ( scaledAmount * MAX_SLIPPAGE_BPS ) / BPS_PER_HUNDRED_PERCENT
219
+ )
220
+ throw new Error ( "Slippage too high" ) ;
220
221
221
222
const pb = await request . toChain . getPorticoBridge ( ) ;
222
223
@@ -230,9 +231,9 @@ export class AutomaticPorticoRoute<N extends Network>
230
231
relayerFee : fee ,
231
232
} ;
232
233
233
- let destinationAmount = details . swapAmounts . minAmountFinish - fee ;
234
+ const destinationAmount = details . swapAmounts . minAmountFinish - fee ;
234
235
235
- if ( Number ( destinationAmount ) < 0 ) {
236
+ if ( destinationAmount < 0n ) {
236
237
return {
237
238
success : false ,
238
239
error : new Error (
@@ -275,13 +276,17 @@ export class AutomaticPorticoRoute<N extends Network>
275
276
const destToken = request . destination ! . id ;
276
277
277
278
const fromPorticoBridge = await request . fromChain . getPorticoBridge ( ) ;
279
+ const tokenGroup = fromPorticoBridge . getTokenGroup ( sourceToken . toString ( ) ) ;
280
+ const toPorticoBridge = await request . toChain . getPorticoBridge ( ) ;
281
+ const destPorticoAddress = toPorticoBridge . getPorticoAddress ( tokenGroup ) ;
278
282
279
283
const xfer = fromPorticoBridge . transfer (
280
284
Wormhole . parseAddress ( sender . chain ( ) , sender . address ( ) ) ,
281
285
to ,
282
286
sourceToken ,
283
287
amount . units ( params . normalizedParams . amount ) ,
284
288
destToken ! ,
289
+ destPorticoAddress ,
285
290
details ! ,
286
291
) ;
287
292
@@ -296,14 +301,49 @@ export class AutomaticPorticoRoute<N extends Network>
296
301
}
297
302
298
303
async * track ( receipt : R , timeout ?: number ) {
299
- if ( ! isSourceInitiated ( receipt ) ) throw new Error ( "Source must be initiated" ) ;
304
+ if ( isSourceInitiated ( receipt ) || isSourceFinalized ( receipt ) ) {
305
+ const { txid } = receipt . originTxs [ receipt . originTxs . length - 1 ] ! ;
306
+
307
+ const vaa = await this . wh . getVaa ( txid , "PorticoBridge:Transfer" , timeout ) ;
308
+ if ( ! vaa ) throw new Error ( "No VAA found for transaction: " + txid ) ;
309
+
310
+ const msgId : WormholeMessageId = {
311
+ chain : vaa . emitterChain ,
312
+ emitter : vaa . emitterAddress ,
313
+ sequence : vaa . sequence ,
314
+ } ;
315
+
316
+ receipt = {
317
+ ...receipt ,
318
+ state : TransferState . Attested ,
319
+ attestation : {
320
+ id : msgId ,
321
+ attestation : vaa ,
322
+ } ,
323
+ } satisfies AttestedTransferReceipt < AttestationReceipt < "PorticoBridge" > > ;
300
324
301
- const { txid } = receipt . originTxs [ receipt . originTxs . length - 1 ] ! ;
302
- const vaa = await this . wh . getVaa ( txid , "TokenBridge:TransferWithPayload" , timeout ) ;
303
- if ( ! vaa ) throw new Error ( "No VAA found for transaction: " + txid ) ;
325
+ yield receipt ;
326
+ }
327
+
328
+ if ( isAttested ( receipt ) ) {
329
+ const toChain = this . wh . getChain ( receipt . to ) ;
330
+ const toPorticoBridge = await toChain . getPorticoBridge ( ) ;
331
+ const isCompleted = await toPorticoBridge . isTransferCompleted (
332
+ receipt . attestation . attestation ,
333
+ ) ;
334
+ if ( isCompleted ) {
335
+ receipt = {
336
+ ...receipt ,
337
+ state : TransferState . DestinationFinalized ,
338
+ } satisfies CompletedTransferReceipt < AttestationReceipt < "PorticoBridge" > > ;
339
+
340
+ yield receipt ;
341
+ }
342
+ }
343
+
344
+ // TODO: handle swap failed case (highway token received)
304
345
305
- const parsed = PorticoBridge . deserializePayload ( vaa . payload . payload ) ;
306
- yield { ...receipt , vaa, parsed } ;
346
+ yield receipt ;
307
347
}
308
348
309
349
async complete ( signer : Signer < N > , receipt : R ) : Promise < TransactionId [ ] > {
@@ -316,47 +356,37 @@ export class AutomaticPorticoRoute<N extends Network>
316
356
return await signSendWait ( toChain , xfer , signer ) ;
317
357
}
318
358
319
- private async quoteUniswap ( request : RouteTransferRequest < N > , params : VP ) {
320
- const fromPorticoBridge = await request . fromChain . getPorticoBridge ( ) ;
321
- const startQuote = await fromPorticoBridge . quoteSwap (
359
+ private async fetchSwapQuote ( request : RouteTransferRequest < N > , params : VP ) {
360
+ const fromPb = await request . fromChain . getPorticoBridge ( ) ;
361
+ const xferAmount = amount . units ( params . normalizedParams . amount ) ;
362
+ const tokenGroup = fromPb . getTokenGroup ( canonicalAddress ( params . normalizedParams . sourceToken ) ) ;
363
+ const startQuote = await fromPb . quoteSwap (
322
364
params . normalizedParams . sourceToken . address ,
323
365
params . normalizedParams . canonicalSourceToken . address ,
324
- amount . units ( params . normalizedParams . amount ) ,
366
+ tokenGroup ,
367
+ xferAmount ,
325
368
) ;
326
369
const startSlippage = ( startQuote * SLIPPAGE_BPS ) / BPS_PER_HUNDRED_PERCENT ;
327
370
328
371
if ( startSlippage >= startQuote ) throw new Error ( "Start slippage too high" ) ;
329
372
330
- const toPorticoBridge = await request . toChain . getPorticoBridge ( ) ;
373
+ const toPb = await request . toChain . getPorticoBridge ( ) ;
331
374
const minAmountStart = startQuote - startSlippage ;
332
- const finishQuote = await toPorticoBridge . quoteSwap (
375
+ const finishQuote = await toPb . quoteSwap (
333
376
params . normalizedParams . canonicalDestinationToken . address ,
334
377
params . normalizedParams . destinationToken . address ,
378
+ tokenGroup ,
335
379
minAmountStart ,
336
380
) ;
337
381
const finishSlippage = ( finishQuote * SLIPPAGE_BPS ) / BPS_PER_HUNDRED_PERCENT ;
338
382
339
383
if ( finishSlippage >= finishQuote ) throw new Error ( "Finish slippage too high" ) ;
340
384
341
385
const minAmountFinish = finishQuote - finishSlippage ;
342
- const amountFinishQuote = await toPorticoBridge . quoteSwap (
343
- params . normalizedParams . canonicalDestinationToken . address ,
344
- params . normalizedParams . destinationToken . address ,
345
- startQuote , // no slippage
346
- ) ;
347
- // the expected receive amount is the amount out from the swap
348
- // minus 5bps slippage
349
- const amountFinishSlippage = ( amountFinishQuote * 5n ) / BPS_PER_HUNDRED_PERCENT ;
350
- if ( amountFinishSlippage >= amountFinishQuote )
351
- throw new Error ( "Amount finish slippage too high" ) ;
352
-
353
- const amountFinish = amountFinishQuote - amountFinishSlippage ;
354
- if ( amountFinish <= minAmountFinish ) throw new Error ( "Amount finish too low" ) ;
355
386
356
387
return {
357
388
minAmountStart : minAmountStart ,
358
389
minAmountFinish : minAmountFinish ,
359
- amountFinish : amountFinish ,
360
390
} ;
361
391
}
362
392
}
0 commit comments