Skip to content

Commit 97d82a4

Browse files
authored
tokenBridge protocol: add token address conversion methods (#649)
* tokenBridge protocol: add token address conversion methods - Added getTokenUniversalAddress and getTokenNativeAddress methods to each platform's token bridge protocol implementation. - Some chains like Aptos and Sui require fetching on-chain data for token address conversions (universal to native and vice versa). - Fixed issue where lookupDestinationToken would return a universal address, causing issues in functions expecting a native address. - lookupDestinationToken now consistently returns a native address. - Resolved issues with transferring native Sui and Aptos tokens back to their origin chains. * check not a wrapped asset error
1 parent f9ff531 commit 97d82a4

File tree

12 files changed

+175
-53
lines changed

12 files changed

+175
-53
lines changed

connect/src/protocols/tokenBridge/tokenTransfer.ts

+51-32
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
AutomaticTokenBridge,
66
ChainContext,
77
Signer,
8+
NativeAddress,
89
TokenId,
910
TokenTransferDetails,
1011
TransactionId,
@@ -14,6 +15,7 @@ import type {
1415
} from "@wormhole-foundation/sdk-definitions";
1516
import {
1617
TokenBridge,
18+
UniversalAddress,
1719
deserialize,
1820
isNative,
1921
isTokenId,
@@ -501,7 +503,6 @@ export namespace TokenTransfer {
501503
dstChain: ChainContext<N, DC>,
502504
token: TokenId<SC>,
503505
): Promise<TokenId<DC>> {
504-
// that will be minted when the transfer is redeemed
505506
let lookup: TokenId;
506507
const tb = await srcChain.getTokenBridge();
507508
if (isNative(token.address)) {
@@ -514,20 +515,37 @@ export namespace TokenTransfer {
514515
} else {
515516
try {
516517
// otherwise, check to see if it is a wrapped token locally
517-
lookup = await tb.getOriginalAsset(token.address);
518-
} catch (e) {
518+
let address: NativeAddress<SC>;
519+
if (UniversalAddress.instanceof(token.address)) {
520+
address = (await tb.getWrappedAsset(token)) as NativeAddress<SC>;
521+
} else {
522+
address = token.address;
523+
}
524+
lookup = await tb.getOriginalAsset(address);
525+
} catch (e: any) {
526+
if (!e.message.includes("not a wrapped asset")) throw e;
519527
// not a from-chain native wormhole-wrapped one
520-
lookup = { chain: token.chain, address: await tb.getTokenUniversalAddress(token.address) };
528+
let address: NativeAddress<SC>;
529+
if (UniversalAddress.instanceof(token.address)) {
530+
address = await tb.getTokenNativeAddress(srcChain.chain, token.address);
531+
} else {
532+
address = token.address;
533+
}
534+
lookup = { chain: token.chain, address: await tb.getTokenUniversalAddress(address) };
521535
}
522536
}
523537

524538
// if the token id is actually native to the destination, return it
539+
const dstTb = await dstChain.getTokenBridge();
525540
if (lookup.chain === dstChain.chain) {
526-
return lookup as TokenId<DC>;
541+
const nativeAddress = await dstTb.getTokenNativeAddress(
542+
lookup.chain,
543+
lookup.address as UniversalAddress,
544+
);
545+
return { chain: dstChain.chain, address: nativeAddress };
527546
}
528547

529548
// otherwise, figure out what the token address representing the wormhole-wrapped token we're transferring
530-
const dstTb = await dstChain.getTokenBridge();
531549
const dstAddress = await dstTb.getWrappedAsset(lookup);
532550
return { chain: dstChain.chain, address: dstAddress };
533551
}
@@ -627,14 +645,27 @@ export namespace TokenTransfer {
627645
dstChain: ChainContext<N, Chain>,
628646
transfer: Omit<TokenTransferDetails, "from" | "to">,
629647
): Promise<TransferQuote> {
630-
const srcDecimals = await srcChain.getDecimals(transfer.token.address);
648+
const srcTb = await srcChain.getTokenBridge();
649+
let srcToken: NativeAddress<Chain>;
650+
if (isNative(transfer.token.address)) {
651+
srcToken = await srcTb.getWrappedNative();
652+
} else if (UniversalAddress.instanceof(transfer.token.address)) {
653+
try {
654+
srcToken = (await srcTb.getWrappedAsset(transfer.token)) as NativeAddress<Chain>;
655+
} catch (e: any) {
656+
if (!e.message.includes("not a wrapped asset")) throw e;
657+
srcToken = await srcTb.getTokenNativeAddress(srcChain.chain, transfer.token.address);
658+
}
659+
} else {
660+
srcToken = transfer.token.address;
661+
}
662+
// @ts-ignore: TS2339
663+
const srcTokenId = Wormhole.tokenId(srcChain.chain, srcToken.toString());
664+
665+
const srcDecimals = await srcChain.getDecimals(srcToken);
631666
const srcAmount = amount.fromBaseUnits(transfer.amount, srcDecimals);
632667
const srcAmountTruncated = amount.truncate(srcAmount, TokenTransfer.MAX_DECIMALS);
633668

634-
const srcToken = isNative(transfer.token.address)
635-
? await srcChain.getNativeWrappedTokenId()
636-
: transfer.token;
637-
638669
// Ensure the transfer would not violate governor transfer limits
639670
const [tokens, limits] = await Promise.all([
640671
getGovernedTokens(wh.config.api),
@@ -643,13 +674,11 @@ export namespace TokenTransfer {
643674

644675
const warnings: QuoteWarning[] = [];
645676
if (limits !== null && srcChain.chain in limits && tokens !== null) {
646-
const srcTb = await srcChain.getTokenBridge();
647-
648677
let origAsset: TokenId;
649678
if (isNative(transfer.token.address)) {
650679
origAsset = {
651680
chain: srcChain.chain,
652-
address: await srcTb.getTokenUniversalAddress(srcToken.address),
681+
address: await srcTb.getTokenUniversalAddress(srcToken),
653682
};
654683
} else {
655684
try {
@@ -658,7 +687,7 @@ export namespace TokenTransfer {
658687
if (!e.message.includes("not a wrapped asset")) throw e;
659688
origAsset = {
660689
chain: srcChain.chain,
661-
address: await srcTb.getTokenUniversalAddress(srcToken.address),
690+
address: await srcTb.getTokenUniversalAddress(srcToken),
662691
};
663692
}
664693
}
@@ -685,26 +714,16 @@ export namespace TokenTransfer {
685714
}
686715

687716
const dstToken = await TokenTransfer.lookupDestinationToken(srcChain, dstChain, transfer.token);
688-
// TODO: this is a hack to get the aptos native gas token decimals
689-
// which requires us to pass in a token address in canonical form
690-
// but the `dstToken.address` here is in universal form
691-
if (dstChain.chain === "Aptos" && dstToken.chain === "Aptos") {
692-
const dstTb = await dstChain.getTokenBridge();
693-
const wrappedNative = await dstTb.getWrappedNative();
694-
if (
695-
dstToken.address.toString() ===
696-
(await dstTb.getTokenUniversalAddress(wrappedNative)).toString()
697-
) {
698-
dstToken.address = wrappedNative;
699-
}
700-
}
701717
const dstDecimals = await dstChain.getDecimals(dstToken.address);
702718
const dstAmountReceivable = amount.scale(srcAmountTruncated, dstDecimals);
703719

704720
const eta = finality.estimateFinalityTime(srcChain.chain);
705721
if (!transfer.automatic) {
706722
return {
707-
sourceToken: { token: srcToken, amount: amount.units(srcAmountTruncated) },
723+
sourceToken: {
724+
token: srcTokenId,
725+
amount: amount.units(srcAmountTruncated),
726+
},
708727
destinationToken: { token: dstToken, amount: amount.units(dstAmountReceivable) },
709728
warnings: warnings.length > 0 ? warnings : undefined,
710729
eta,
@@ -716,7 +735,7 @@ export namespace TokenTransfer {
716735
// The fee is removed from the amount transferred
717736
// quoted on the source chain
718737
const stb = await srcChain.getAutomaticTokenBridge();
719-
const fee = await stb.getRelayerFee(dstChain.chain, srcToken.address);
738+
const fee = await stb.getRelayerFee(dstChain.chain, srcToken);
720739
const feeAmountDest = amount.scale(
721740
amount.truncate(amount.fromBaseUnits(fee, srcDecimals), TokenTransfer.MAX_DECIMALS),
722741
dstDecimals,
@@ -791,11 +810,11 @@ export namespace TokenTransfer {
791810

792811
return {
793812
sourceToken: {
794-
token: srcToken,
813+
token: srcTokenId,
795814
amount: amount.units(srcAmountTruncated),
796815
},
797816
destinationToken: { token: dstToken, amount: destAmountLessFee },
798-
relayFee: { token: srcToken, amount: fee },
817+
relayFee: { token: srcTokenId, amount: fee },
799818
destinationNativeGas,
800819
warnings: warnings.length > 0 ? warnings : undefined,
801820
eta,

connect/src/wormhole.ts

+33
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
getVaaBytesWithRetry,
4040
getVaaWithRetry,
4141
} from "./whscan-api.js";
42+
import { UniversalAddress } from "@wormhole-foundation/sdk-definitions";
4243

4344
type PlatformMap<N extends Network, P extends Platform = Platform> = Map<P, PlatformContext<N, P>>;
4445
type ChainMap<N extends Network, C extends Chain = Chain> = Map<C, ChainContext<N, C>>;
@@ -222,6 +223,38 @@ export class Wormhole<N extends Network> {
222223
return await tb.getOriginalAsset(token.address);
223224
}
224225

226+
/**
227+
* Returns the UniversalAddress of the token. This may require fetching on-chain data.
228+
* @param chain The chain to get the UniversalAddress for
229+
* @param token The address to get the UniversalAddress for
230+
* @returns The UniversalAddress of the token
231+
*/
232+
async getTokenUniversalAddress<C extends Chain>(
233+
chain: C,
234+
token: NativeAddress<C>,
235+
): Promise<UniversalAddress> {
236+
const ctx = this.getChain(chain);
237+
const tb = await ctx.getTokenBridge();
238+
return await tb.getTokenUniversalAddress(token);
239+
}
240+
241+
/**
242+
* Returns the native address of the token. This may require fetching on-chain data.
243+
* @param chain The chain to get the native address for
244+
* @param originChain The chain the token is from / native to
245+
* @param token The address to get the native address for
246+
* @returns The native address of the token
247+
*/
248+
async getTokenNativeAddress<C extends Chain>(
249+
chain: C,
250+
originChain: Chain,
251+
token: UniversalAddress,
252+
): Promise<NativeAddress<C>> {
253+
const ctx = this.getChain(chain);
254+
const tb = await ctx.getTokenBridge();
255+
return await tb.getTokenNativeAddress(originChain, token);
256+
}
257+
225258
/**
226259
* Gets the number of decimals for a token on a given chain
227260
*

core/definitions/src/protocols/tokenBridge/tokenBridge.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Chain, Network } from "@wormhole-foundation/sdk-base";
22
import { lazyInstantiate } from "@wormhole-foundation/sdk-base";
3-
import type { AccountAddress, ChainAddress } from "../../address.js";
4-
import { UniversalAddress } from "../../universalAddress.js";
3+
import type { AccountAddress, ChainAddress, NativeAddress } from "../../address.js";
4+
import type { UniversalAddress } from "../../universalAddress.js";
55
import type { TokenAddress, TokenId } from "../../types.js";
66
import type { UnsignedTransaction } from "../../unsignedTransaction.js";
77
import type { ProtocolPayload, ProtocolVAA } from "./../../vaa/index.js";
@@ -135,19 +135,26 @@ export interface TokenBridge<N extends Network = Network, C extends Chain = Chai
135135
*/
136136
getOriginalAsset(nativeAddress: TokenAddress<C>): Promise<TokenId<Chain>>;
137137
/**
138-
* Returns the UniversalAddress of the token. This may require retrieving data on-chain.
138+
* Returns the UniversalAddress of the token. This may require fetching on-chain data.
139139
*
140-
* @param nativeAddress The address to get the UniversalAddress for
140+
* @param token The address to get the UniversalAddress for
141141
* @returns The UniversalAddress of the token
142142
*/
143-
getTokenUniversalAddress(nativeAddress: TokenAddress<C>): Promise<UniversalAddress>;
143+
getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress>;
144+
/**
145+
* Returns the native address of the token. This may require fetching on-chain data.
146+
* @param originChain The chain the token is from / native to
147+
* @param token The address to get the native address for
148+
* @returns The native address of the token
149+
*/
150+
getTokenNativeAddress(originChain: Chain, token: UniversalAddress): Promise<NativeAddress<C>>;
144151
/**
145152
* returns the wrapped version of the native asset
146153
*
147154
* @returns The address of the native gas token that has been wrapped
148155
* for use where the gas token is not possible to use (e.g. bridging)
149156
*/
150-
getWrappedNative(): Promise<TokenAddress<C>>;
157+
getWrappedNative(): Promise<NativeAddress<C>>;
151158
/**
152159
* Check to see if a foreign token has a wrapped version
153160
*

core/definitions/src/testing/mocks/tokenBridge.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Network, Platform, PlatformToChains } from "@wormhole-foundation/sdk-base";
1+
import type { Chain, Network, Platform, PlatformToChains } from "@wormhole-foundation/sdk-base";
22
import type {
33
ChainAddress,
44
NativeAddress,
@@ -20,7 +20,10 @@ export class MockTokenBridge<N extends Network, P extends Platform, C extends Pl
2020
getOriginalAsset(token: TokenAddress<C>): Promise<ChainAddress> {
2121
throw new Error("Method not implemented.");
2222
}
23-
getTokenUniversalAddress(nativeAddress: TokenAddress<C>): Promise<UniversalAddress> {
23+
getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress> {
24+
throw new Error("Method not implemented.");
25+
}
26+
getTokenNativeAddress(originChain: Chain, token: UniversalAddress): Promise<NativeAddress<C>> {
2427
throw new Error("Method not implemented.");
2528
}
2629
hasWrappedAsset(original: ChainAddress): Promise<boolean> {

platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,20 @@ export class AlgorandTokenBridge<N extends Network, C extends AlgorandChains>
146146
return { chain, address };
147147
}
148148

149-
async getTokenUniversalAddress(token: TokenAddress<C>): Promise<UniversalAddress> {
149+
async getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress> {
150150
return new AlgorandAddress(token).toUniversalAddress();
151151
}
152152

153+
async getTokenNativeAddress(
154+
originChain: Chain,
155+
token: UniversalAddress,
156+
): Promise<NativeAddress<C>> {
157+
return new AlgorandAddress(token).toNative() as NativeAddress<C>;
158+
}
159+
153160
// Returns the address of the native version of this asset
154161
async getWrappedAsset(token: TokenId<Chain>): Promise<NativeAddress<C>> {
162+
if (isNative(token.address)) throw new Error("native asset cannot be a wrapped asset");
155163
const storageAccount = StorageLogicSig.forWrappedAsset(this.tokenBridgeAppId, token);
156164
const data = await StorageLogicSig.decodeLocalState(
157165
this.connection,

platforms/algorand/src/address.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { _platform, safeBigIntToNumber } from "./types.js";
88
export const AlgorandZeroAddress = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ";
99

1010
// Note: for ASA/App IDs we encode them as 8 bytes at the start of
11-
// the 32 byte adddress bytes.
11+
// the 32 byte address bytes.
1212

1313
export class AlgorandAddress implements Address {
1414
static readonly byteSize = 32;

platforms/aptos/protocols/tokenBridge/src/tokenBridge.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type {
2+
Chain,
23
ChainAddress,
34
ChainId,
45
ChainsConfig,
56
Contracts,
7+
NativeAddress,
68
Network,
79
TokenBridge,
810
TokenId,
@@ -98,10 +100,23 @@ export class AptosTokenBridge<N extends Network, C extends AptosChains>
98100
return { chain, address };
99101
}
100102

101-
async getTokenUniversalAddress(token: AnyAptosAddress): Promise<UniversalAddress> {
103+
async getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress> {
102104
return new UniversalAddress(encoding.hex.encode(sha3_256(token.toString()), true));
103105
}
104106

107+
async getTokenNativeAddress(
108+
originChain: Chain,
109+
token: UniversalAddress,
110+
): Promise<NativeAddress<C>> {
111+
const assetType =
112+
originChain === this.chain
113+
? await this.getTypeFromExternalAddress(token.toString())
114+
: await this.getAssetFullyQualifiedType({ chain: originChain, address: token });
115+
116+
if (!assetType) throw new Error("Invalid asset address.");
117+
return new AptosAddress(assetType) as NativeAddress<C>;
118+
}
119+
105120
async hasWrappedAsset(token: TokenId): Promise<boolean> {
106121
try {
107122
await this.getWrappedAsset(token);
@@ -111,6 +126,7 @@ export class AptosTokenBridge<N extends Network, C extends AptosChains>
111126
}
112127

113128
async getWrappedAsset(token: TokenId) {
129+
if (isNative(token.address)) throw new Error("native asset cannot be a wrapped asset");
114130
const assetFullyQualifiedType = await this.getAssetFullyQualifiedType(token);
115131
if (!assetFullyQualifiedType) throw new Error("Invalid asset address.");
116132

platforms/cosmwasm/protocols/tokenBridge/src/tokenBridge.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";
22
import type {
3+
Chain,
34
ChainAddress,
45
ChainsConfig,
56
Contracts,
@@ -125,10 +126,17 @@ export class CosmwasmTokenBridge<N extends Network, C extends CosmwasmChains>
125126
};
126127
}
127128

128-
async getTokenUniversalAddress(token: AnyCosmwasmAddress): Promise<UniversalAddress> {
129+
async getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress> {
129130
return new CosmwasmAddress(token).toUniversalAddress();
130131
}
131132

133+
async getTokenNativeAddress(
134+
originChain: Chain,
135+
token: UniversalAddress,
136+
): Promise<NativeAddress<C>> {
137+
return new CosmwasmAddress(token).toNative() as NativeAddress<C>;
138+
}
139+
132140
async isTransferCompleted(vaa: TokenBridge.TransferVAA): Promise<boolean> {
133141
const data = encoding.b64.encode(serialize(vaa));
134142
const result = await this.rpc.queryContractSmart(this.tokenBridge, {

0 commit comments

Comments
 (0)