Skip to content

Commit 3f4c8e9

Browse files
authored
Added Portico Bridge USDT support (#552)
* Added Portico Bridge USDT support USDT uses PancakeSwap if available on the chain * Added native USDT tokens * removed unused portico api * add missing wstETHbsc token * removed wstETHbsc token (no portico pool) * progress * more progress * working * revert router example changes * added celo usdt * Revert "added celo usdt" This reverts commit 24a386e. * Reapply "added celo usdt" This reverts commit a0ad1e8. * added comment on decimals scaling * bump version to 0.12.0
1 parent 0e7e158 commit 3f4c8e9

File tree

41 files changed

+594
-630
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+594
-630
lines changed

connect/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@wormhole-foundation/sdk-connect",
3-
"version": "0.11.0",
3+
"version": "0.12.0",
44
"repository": {
55
"type": "git",
66
"url": "git+https://github.com/wormhole-foundation/connect-sdk.git"
@@ -98,8 +98,8 @@
9898
},
9999
"dependencies": {
100100
"axios": "^1.4.0",
101-
"@wormhole-foundation/sdk-base": "0.11.0",
102-
"@wormhole-foundation/sdk-definitions": "0.11.0"
101+
"@wormhole-foundation/sdk-base": "0.12.0",
102+
"@wormhole-foundation/sdk-definitions": "0.12.0"
103103
},
104104
"type": "module"
105105
}

connect/src/routes/portico/README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ The current table of input tokens, to bridging tokens,
4141
to final tokens is as follows
4242

4343
```
44-
| inputs | 'native' | ETH | wETH | wstETH
45-
| bridging token | xETH | wstETH
46-
| outputs | 'native' | ETH | wETH | wstETH
44+
| inputs | 'native' | ETH | wETH | wstETH | USDT |
45+
| bridging token | xETH | xwstETH | xUSDT |
46+
| outputs | 'native' | ETH | wETH | wstETH | USDT |
4747
```

connect/src/routes/portico/automatic.ts

+118-88
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import type {
1111
} from "../types.js";
1212
import type {
1313
AttestationReceipt,
14+
AttestedTransferReceipt,
1415
Chain,
1516
ChainContext,
17+
CompletedTransferReceipt,
1618
Network,
1719
Signer,
1820
SourceInitiatedTransferReceipt,
@@ -26,19 +28,19 @@ import {
2628
Wormhole,
2729
amount,
2830
canonicalAddress,
29-
chainToPlatform,
3031
contracts,
3132
isAttested,
32-
isNative,
33+
isSourceFinalized,
3334
isSourceInitiated,
3435
resolveWrappedToken,
3536
signSendWait,
3637
} from "./../../index.js";
37-
import type { ChainAddress } from "@wormhole-foundation/sdk-definitions";
38+
import type { ChainAddress, WormholeMessageId } from "@wormhole-foundation/sdk-definitions";
3839
import type { RouteTransferRequest } from "../request.js";
3940

4041
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;
4244

4345
export namespace PorticoRoute {
4446
export type Options = {};
@@ -78,8 +80,6 @@ export class AutomaticPorticoRoute<N extends Network>
7880
name: "AutomaticPortico",
7981
};
8082

81-
private static _supportedTokens = ["WETH", "WSTETH"];
82-
8383
static supportedNetworks(): Network[] {
8484
return ["Mainnet"];
8585
}
@@ -92,19 +92,12 @@ export class AutomaticPorticoRoute<N extends Network>
9292
}
9393

9494
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);
108101
}
109102

110103
static async supportedDestinationTokens<N extends Network>(
@@ -119,51 +112,39 @@ export class AutomaticPorticoRoute<N extends Network>
119112
);
120113
const tokenAddress = canonicalAddress(srcTokenAddress);
121114

122-
// The token that will be used to bridge
123115
const pb = await fromChain.getPorticoBridge();
124-
const transferrableToken = pb.getTransferrableToken(tokenAddress);
125116

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+
}
132125

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);
159141
}
160142

161143
static isProtocolSupported<N extends Network>(chain: ChainContext<N>): boolean {
162144
return chain.supportsPorticoBridge();
163145
}
164146

165147
async isAvailable(): Promise<boolean> {
166-
// TODO:
167148
return true;
168149
}
169150

@@ -174,10 +155,10 @@ export class AutomaticPorticoRoute<N extends Network>
174155
async validate(request: RouteTransferRequest<N>, params: TP): Promise<VR> {
175156
try {
176157
if (
177-
chainToPlatform(request.fromChain.chain) !== "Evm" ||
178-
chainToPlatform(request.toChain.chain) !== "Evm"
158+
!AutomaticPorticoRoute.isProtocolSupported(request.fromChain) ||
159+
!AutomaticPorticoRoute.isProtocolSupported(request.toChain)
179160
) {
180-
throw new Error("Only EVM chains are supported");
161+
throw new Error("Protocol not supported");
181162
}
182163

183164
const { fromChain, toChain, source, destination } = request;
@@ -190,9 +171,11 @@ export class AutomaticPorticoRoute<N extends Network>
190171
const fromPb = await fromChain.getPorticoBridge();
191172
const toPb = await toChain.getPorticoBridge();
192173

193-
const canonicalSourceToken = fromPb.getTransferrableToken(canonicalAddress(sourceToken));
174+
const canonicalSourceToken = await fromPb.getTransferrableToken(
175+
canonicalAddress(sourceToken),
176+
);
194177

195-
const canonicalDestinationToken = toPb.getTransferrableToken(
178+
const canonicalDestinationToken = await toPb.getTransferrableToken(
196179
canonicalAddress(destinationToken),
197180
);
198181

@@ -216,7 +199,25 @@ export class AutomaticPorticoRoute<N extends Network>
216199

217200
async quote(request: RouteTransferRequest<N>, params: VP): Promise<QR> {
218201
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");
220221

221222
const pb = await request.toChain.getPorticoBridge();
222223

@@ -230,9 +231,9 @@ export class AutomaticPorticoRoute<N extends Network>
230231
relayerFee: fee,
231232
};
232233

233-
let destinationAmount = details.swapAmounts.minAmountFinish - fee;
234+
const destinationAmount = details.swapAmounts.minAmountFinish - fee;
234235

235-
if (Number(destinationAmount) < 0) {
236+
if (destinationAmount < 0n) {
236237
return {
237238
success: false,
238239
error: new Error(
@@ -275,13 +276,17 @@ export class AutomaticPorticoRoute<N extends Network>
275276
const destToken = request.destination!.id;
276277

277278
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);
278282

279283
const xfer = fromPorticoBridge.transfer(
280284
Wormhole.parseAddress(sender.chain(), sender.address()),
281285
to,
282286
sourceToken,
283287
amount.units(params.normalizedParams.amount),
284288
destToken!,
289+
destPorticoAddress,
285290
details!,
286291
);
287292

@@ -296,14 +301,49 @@ export class AutomaticPorticoRoute<N extends Network>
296301
}
297302

298303
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">>;
300324

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)
304345

305-
const parsed = PorticoBridge.deserializePayload(vaa.payload.payload);
306-
yield { ...receipt, vaa, parsed };
346+
yield receipt;
307347
}
308348

309349
async complete(signer: Signer<N>, receipt: R): Promise<TransactionId[]> {
@@ -316,47 +356,37 @@ export class AutomaticPorticoRoute<N extends Network>
316356
return await signSendWait(toChain, xfer, signer);
317357
}
318358

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(
322364
params.normalizedParams.sourceToken.address,
323365
params.normalizedParams.canonicalSourceToken.address,
324-
amount.units(params.normalizedParams.amount),
366+
tokenGroup,
367+
xferAmount,
325368
);
326369
const startSlippage = (startQuote * SLIPPAGE_BPS) / BPS_PER_HUNDRED_PERCENT;
327370

328371
if (startSlippage >= startQuote) throw new Error("Start slippage too high");
329372

330-
const toPorticoBridge = await request.toChain.getPorticoBridge();
373+
const toPb = await request.toChain.getPorticoBridge();
331374
const minAmountStart = startQuote - startSlippage;
332-
const finishQuote = await toPorticoBridge.quoteSwap(
375+
const finishQuote = await toPb.quoteSwap(
333376
params.normalizedParams.canonicalDestinationToken.address,
334377
params.normalizedParams.destinationToken.address,
378+
tokenGroup,
335379
minAmountStart,
336380
);
337381
const finishSlippage = (finishQuote * SLIPPAGE_BPS) / BPS_PER_HUNDRED_PERCENT;
338382

339383
if (finishSlippage >= finishQuote) throw new Error("Finish slippage too high");
340384

341385
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");
355386

356387
return {
357388
minAmountStart: minAmountStart,
358389
minAmountFinish: minAmountFinish,
359-
amountFinish: amountFinish,
360390
};
361391
}
362392
}

connect/src/routes/request.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,13 @@ export class RouteTransferRequest<N extends Network> {
5353
};
5454

5555
if (quote.relayFee) {
56+
const relayFeeChain =
57+
quote.relayFee.token.chain === this.fromChain.chain ? this.fromChain : this.toChain;
58+
const relayFeeDecimals = await relayFeeChain.getDecimals(quote.relayFee.token.address);
59+
5660
dq.relayFee = {
5761
token: quote.relayFee.token,
58-
amount: amount.fromBaseUnits(quote.relayFee.amount, this.source.decimals),
62+
amount: amount.fromBaseUnits(quote.relayFee.amount, relayFeeDecimals),
5963
};
6064
}
6165

0 commit comments

Comments
 (0)