Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tokenBridge protocol: add token address conversion methods #649

Merged
merged 2 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 51 additions & 32 deletions connect/src/protocols/tokenBridge/tokenTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
AutomaticTokenBridge,
ChainContext,
Signer,
NativeAddress,
TokenId,
TokenTransferDetails,
TransactionId,
Expand All @@ -14,6 +15,7 @@ import type {
} from "@wormhole-foundation/sdk-definitions";
import {
TokenBridge,
UniversalAddress,
deserialize,
isNative,
isTokenId,
Expand Down Expand Up @@ -501,7 +503,6 @@ export namespace TokenTransfer {
dstChain: ChainContext<N, DC>,
token: TokenId<SC>,
): Promise<TokenId<DC>> {
// that will be minted when the transfer is redeemed
let lookup: TokenId;
const tb = await srcChain.getTokenBridge();
if (isNative(token.address)) {
Expand All @@ -514,20 +515,37 @@ export namespace TokenTransfer {
} else {
try {
// otherwise, check to see if it is a wrapped token locally
lookup = await tb.getOriginalAsset(token.address);
} catch (e) {
let address: NativeAddress<SC>;
if (UniversalAddress.instanceof(token.address)) {
address = (await tb.getWrappedAsset(token)) as NativeAddress<SC>;
} else {
address = token.address;
}
lookup = await tb.getOriginalAsset(address);
} catch (e: any) {
if (!e.message.includes("not a wrapped asset")) throw e;
// not a from-chain native wormhole-wrapped one
lookup = { chain: token.chain, address: await tb.getTokenUniversalAddress(token.address) };
let address: NativeAddress<SC>;
if (UniversalAddress.instanceof(token.address)) {
address = await tb.getTokenNativeAddress(srcChain.chain, token.address);
} else {
address = token.address;
}
lookup = { chain: token.chain, address: await tb.getTokenUniversalAddress(address) };
}
}

// if the token id is actually native to the destination, return it
const dstTb = await dstChain.getTokenBridge();
if (lookup.chain === dstChain.chain) {
return lookup as TokenId<DC>;
const nativeAddress = await dstTb.getTokenNativeAddress(
lookup.chain,
lookup.address as UniversalAddress,
);
return { chain: dstChain.chain, address: nativeAddress };
}

// otherwise, figure out what the token address representing the wormhole-wrapped token we're transferring
const dstTb = await dstChain.getTokenBridge();
const dstAddress = await dstTb.getWrappedAsset(lookup);
return { chain: dstChain.chain, address: dstAddress };
}
Expand Down Expand Up @@ -627,14 +645,27 @@ export namespace TokenTransfer {
dstChain: ChainContext<N, Chain>,
transfer: Omit<TokenTransferDetails, "from" | "to">,
): Promise<TransferQuote> {
const srcDecimals = await srcChain.getDecimals(transfer.token.address);
const srcTb = await srcChain.getTokenBridge();
let srcToken: NativeAddress<Chain>;
if (isNative(transfer.token.address)) {
srcToken = await srcTb.getWrappedNative();
} else if (UniversalAddress.instanceof(transfer.token.address)) {
try {
srcToken = (await srcTb.getWrappedAsset(transfer.token)) as NativeAddress<Chain>;
} catch (e: any) {
if (!e.message.includes("not a wrapped asset")) throw e;
srcToken = await srcTb.getTokenNativeAddress(srcChain.chain, transfer.token.address);
emreboga marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
srcToken = transfer.token.address;
}
// @ts-ignore: TS2339
const srcTokenId = Wormhole.tokenId(srcChain.chain, srcToken.toString());

const srcDecimals = await srcChain.getDecimals(srcToken);
const srcAmount = amount.fromBaseUnits(transfer.amount, srcDecimals);
const srcAmountTruncated = amount.truncate(srcAmount, TokenTransfer.MAX_DECIMALS);

const srcToken = isNative(transfer.token.address)
? await srcChain.getNativeWrappedTokenId()
: transfer.token;

// Ensure the transfer would not violate governor transfer limits
const [tokens, limits] = await Promise.all([
getGovernedTokens(wh.config.api),
Expand All @@ -643,13 +674,11 @@ export namespace TokenTransfer {

const warnings: QuoteWarning[] = [];
if (limits !== null && srcChain.chain in limits && tokens !== null) {
const srcTb = await srcChain.getTokenBridge();

let origAsset: TokenId;
if (isNative(transfer.token.address)) {
origAsset = {
chain: srcChain.chain,
address: await srcTb.getTokenUniversalAddress(srcToken.address),
address: await srcTb.getTokenUniversalAddress(srcToken),
};
} else {
try {
Expand All @@ -658,7 +687,7 @@ export namespace TokenTransfer {
if (!e.message.includes("not a wrapped asset")) throw e;
origAsset = {
chain: srcChain.chain,
address: await srcTb.getTokenUniversalAddress(srcToken.address),
address: await srcTb.getTokenUniversalAddress(srcToken),
};
}
}
Expand All @@ -685,26 +714,16 @@ export namespace TokenTransfer {
}

const dstToken = await TokenTransfer.lookupDestinationToken(srcChain, dstChain, transfer.token);
// TODO: this is a hack to get the aptos native gas token decimals
// which requires us to pass in a token address in canonical form
// but the `dstToken.address` here is in universal form
if (dstChain.chain === "Aptos" && dstToken.chain === "Aptos") {
const dstTb = await dstChain.getTokenBridge();
const wrappedNative = await dstTb.getWrappedNative();
if (
dstToken.address.toString() ===
(await dstTb.getTokenUniversalAddress(wrappedNative)).toString()
) {
dstToken.address = wrappedNative;
}
}
const dstDecimals = await dstChain.getDecimals(dstToken.address);
const dstAmountReceivable = amount.scale(srcAmountTruncated, dstDecimals);

const eta = finality.estimateFinalityTime(srcChain.chain);
if (!transfer.automatic) {
return {
sourceToken: { token: srcToken, amount: amount.units(srcAmountTruncated) },
sourceToken: {
token: srcTokenId,
amount: amount.units(srcAmountTruncated),
},
destinationToken: { token: dstToken, amount: amount.units(dstAmountReceivable) },
warnings: warnings.length > 0 ? warnings : undefined,
eta,
Expand All @@ -716,7 +735,7 @@ export namespace TokenTransfer {
// The fee is removed from the amount transferred
// quoted on the source chain
const stb = await srcChain.getAutomaticTokenBridge();
const fee = await stb.getRelayerFee(dstChain.chain, srcToken.address);
const fee = await stb.getRelayerFee(dstChain.chain, srcToken);
const feeAmountDest = amount.scale(
amount.truncate(amount.fromBaseUnits(fee, srcDecimals), TokenTransfer.MAX_DECIMALS),
dstDecimals,
Expand Down Expand Up @@ -791,11 +810,11 @@ export namespace TokenTransfer {

return {
sourceToken: {
token: srcToken,
token: srcTokenId,
amount: amount.units(srcAmountTruncated),
},
destinationToken: { token: dstToken, amount: destAmountLessFee },
relayFee: { token: srcToken, amount: fee },
relayFee: { token: srcTokenId, amount: fee },
destinationNativeGas,
warnings: warnings.length > 0 ? warnings : undefined,
eta,
Expand Down
33 changes: 33 additions & 0 deletions connect/src/wormhole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
getVaaBytesWithRetry,
getVaaWithRetry,
} from "./whscan-api.js";
import { UniversalAddress } from "@wormhole-foundation/sdk-definitions";

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

/**
* Returns the UniversalAddress of the token. This may require fetching on-chain data.
* @param chain The chain to get the UniversalAddress for
* @param token The address to get the UniversalAddress for
* @returns The UniversalAddress of the token
*/
async getTokenUniversalAddress<C extends Chain>(
emreboga marked this conversation as resolved.
Show resolved Hide resolved
chain: C,
token: NativeAddress<C>,
): Promise<UniversalAddress> {
const ctx = this.getChain(chain);
const tb = await ctx.getTokenBridge();
return await tb.getTokenUniversalAddress(token);
}

/**
* Returns the native address of the token. This may require fetching on-chain data.
* @param chain The chain to get the native address for
* @param originChain The chain the token is from / native to
* @param token The address to get the native address for
* @returns The native address of the token
*/
async getTokenNativeAddress<C extends Chain>(
emreboga marked this conversation as resolved.
Show resolved Hide resolved
chain: C,
originChain: Chain,
token: UniversalAddress,
): Promise<NativeAddress<C>> {
const ctx = this.getChain(chain);
const tb = await ctx.getTokenBridge();
return await tb.getTokenNativeAddress(originChain, token);
}

/**
* Gets the number of decimals for a token on a given chain
*
Expand Down
19 changes: 13 additions & 6 deletions core/definitions/src/protocols/tokenBridge/tokenBridge.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Chain, Network } from "@wormhole-foundation/sdk-base";
import { lazyInstantiate } from "@wormhole-foundation/sdk-base";
import type { AccountAddress, ChainAddress } from "../../address.js";
import { UniversalAddress } from "../../universalAddress.js";
import type { AccountAddress, ChainAddress, NativeAddress } from "../../address.js";
import type { UniversalAddress } from "../../universalAddress.js";
import type { TokenAddress, TokenId } from "../../types.js";
import type { UnsignedTransaction } from "../../unsignedTransaction.js";
import type { ProtocolPayload, ProtocolVAA } from "./../../vaa/index.js";
Expand Down Expand Up @@ -135,19 +135,26 @@ export interface TokenBridge<N extends Network = Network, C extends Chain = Chai
*/
getOriginalAsset(nativeAddress: TokenAddress<C>): Promise<TokenId<Chain>>;
/**
* Returns the UniversalAddress of the token. This may require retrieving data on-chain.
* Returns the UniversalAddress of the token. This may require fetching on-chain data.
*
* @param nativeAddress The address to get the UniversalAddress for
* @param token The address to get the UniversalAddress for
* @returns The UniversalAddress of the token
*/
getTokenUniversalAddress(nativeAddress: TokenAddress<C>): Promise<UniversalAddress>;
getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress>;
/**
* Returns the native address of the token. This may require fetching on-chain data.
* @param originChain The chain the token is from / native to
* @param token The address to get the native address for
* @returns The native address of the token
*/
getTokenNativeAddress(originChain: Chain, token: UniversalAddress): Promise<NativeAddress<C>>;
/**
* returns the wrapped version of the native asset
*
* @returns The address of the native gas token that has been wrapped
* for use where the gas token is not possible to use (e.g. bridging)
*/
getWrappedNative(): Promise<TokenAddress<C>>;
getWrappedNative(): Promise<NativeAddress<C>>;
/**
* Check to see if a foreign token has a wrapped version
*
Expand Down
7 changes: 5 additions & 2 deletions core/definitions/src/testing/mocks/tokenBridge.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Network, Platform, PlatformToChains } from "@wormhole-foundation/sdk-base";
import type { Chain, Network, Platform, PlatformToChains } from "@wormhole-foundation/sdk-base";
import type {
ChainAddress,
NativeAddress,
Expand All @@ -20,7 +20,10 @@ export class MockTokenBridge<N extends Network, P extends Platform, C extends Pl
getOriginalAsset(token: TokenAddress<C>): Promise<ChainAddress> {
throw new Error("Method not implemented.");
}
getTokenUniversalAddress(nativeAddress: TokenAddress<C>): Promise<UniversalAddress> {
getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress> {
throw new Error("Method not implemented.");
}
getTokenNativeAddress(originChain: Chain, token: UniversalAddress): Promise<NativeAddress<C>> {
throw new Error("Method not implemented.");
}
hasWrappedAsset(original: ChainAddress): Promise<boolean> {
Expand Down
10 changes: 9 additions & 1 deletion platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,20 @@ export class AlgorandTokenBridge<N extends Network, C extends AlgorandChains>
return { chain, address };
}

async getTokenUniversalAddress(token: TokenAddress<C>): Promise<UniversalAddress> {
async getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress> {
return new AlgorandAddress(token).toUniversalAddress();
}

async getTokenNativeAddress(
originChain: Chain,
token: UniversalAddress,
): Promise<NativeAddress<C>> {
return new AlgorandAddress(token).toNative() as NativeAddress<C>;
}

// Returns the address of the native version of this asset
async getWrappedAsset(token: TokenId<Chain>): Promise<NativeAddress<C>> {
if (isNative(token.address)) throw new Error("native asset cannot be a wrapped asset");
const storageAccount = StorageLogicSig.forWrappedAsset(this.tokenBridgeAppId, token);
const data = await StorageLogicSig.decodeLocalState(
this.connection,
Expand Down
2 changes: 1 addition & 1 deletion platforms/algorand/src/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { _platform, safeBigIntToNumber } from "./types.js";
export const AlgorandZeroAddress = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ";

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

export class AlgorandAddress implements Address {
static readonly byteSize = 32;
Expand Down
18 changes: 17 additions & 1 deletion platforms/aptos/protocols/tokenBridge/src/tokenBridge.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type {
Chain,
ChainAddress,
ChainId,
ChainsConfig,
Contracts,
NativeAddress,
Network,
TokenBridge,
TokenId,
Expand Down Expand Up @@ -98,10 +100,23 @@ export class AptosTokenBridge<N extends Network, C extends AptosChains>
return { chain, address };
}

async getTokenUniversalAddress(token: AnyAptosAddress): Promise<UniversalAddress> {
async getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress> {
return new UniversalAddress(encoding.hex.encode(sha3_256(token.toString()), true));
}

async getTokenNativeAddress(
originChain: Chain,
token: UniversalAddress,
): Promise<NativeAddress<C>> {
const assetType =
originChain === this.chain
? await this.getTypeFromExternalAddress(token.toString())
: await this.getAssetFullyQualifiedType({ chain: originChain, address: token });

if (!assetType) throw new Error("Invalid asset address.");
return new AptosAddress(assetType) as NativeAddress<C>;
}

async hasWrappedAsset(token: TokenId): Promise<boolean> {
try {
await this.getWrappedAsset(token);
Expand All @@ -111,6 +126,7 @@ export class AptosTokenBridge<N extends Network, C extends AptosChains>
}

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

Expand Down
10 changes: 9 additions & 1 deletion platforms/cosmwasm/protocols/tokenBridge/src/tokenBridge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import type {
Chain,
ChainAddress,
ChainsConfig,
Contracts,
Expand Down Expand Up @@ -125,10 +126,17 @@ export class CosmwasmTokenBridge<N extends Network, C extends CosmwasmChains>
};
}

async getTokenUniversalAddress(token: AnyCosmwasmAddress): Promise<UniversalAddress> {
async getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress> {
return new CosmwasmAddress(token).toUniversalAddress();
}

async getTokenNativeAddress(
originChain: Chain,
token: UniversalAddress,
): Promise<NativeAddress<C>> {
return new CosmwasmAddress(token).toNative() as NativeAddress<C>;
}

async isTransferCompleted(vaa: TokenBridge.TransferVAA): Promise<boolean> {
const data = encoding.b64.encode(serialize(vaa));
const result = await this.rpc.queryContractSmart(this.tokenBridge, {
Expand Down
Loading
Loading