From 7f427133cc471a425095717579295f8858cc50cc Mon Sep 17 00:00:00 2001 From: Csongor Kiss Date: Thu, 5 May 2022 18:35:11 +0200 Subject: [PATCH] sdk: improve type safety + add documentation (#1102) * sdk/js: improve type safety + add documentation * sdk/js: add support for chain names * sdk/js: fix boolean conditional bug in transferFromSolana * sdk/js: remove unsafe ChainId coercions in sdk * sdk/js: Chain id 0 This is a legit chain id that some governance VAAs target * sdk/js: Add mainnet and testnet contract addresses * sdk/js: bump version to 0.3.0 * sdk/js: limit minor version changes * sdk/js: update contracts and add devnet Co-authored-by: Csongor Kiss Co-authored-by: Evan Gray --- sdk/js/CHANGELOG.md | 32 + sdk/js/package.json | 2 +- sdk/js/src/algorand/Algorand.ts | 6 +- .../src/nft_bridge/__tests__/integration.ts | 45 +- sdk/js/src/nft_bridge/getForeignAsset.ts | 17 +- sdk/js/src/nft_bridge/getOriginalAsset.ts | 13 +- sdk/js/src/nft_bridge/transfer.ts | 25 +- sdk/js/src/rpc/getSignedVAA.ts | 6 +- sdk/js/src/rpc/getSignedVAAWithRetry.ts | 7 +- .../src/token_bridge/__tests__/integration.ts | 38 +- sdk/js/src/token_bridge/attest.ts | 6 +- sdk/js/src/token_bridge/createWrapped.ts | 6 +- sdk/js/src/token_bridge/getForeignAsset.ts | 35 +- .../token_bridge/getIsTransferCompleted.ts | 6 +- sdk/js/src/token_bridge/getIsWrappedAsset.ts | 7 +- sdk/js/src/token_bridge/getOriginalAsset.ts | 7 +- sdk/js/src/token_bridge/transfer.ts | 43 +- sdk/js/src/utils/array.ts | 263 +++++--- sdk/js/src/utils/consts.ts | 568 +++++++++++++++++- sdk/js/src/utils/parseVaa.ts | 2 + 20 files changed, 923 insertions(+), 211 deletions(-) diff --git a/sdk/js/CHANGELOG.md b/sdk/js/CHANGELOG.md index de247f853..3d4ad289f 100644 --- a/sdk/js/CHANGELOG.md +++ b/sdk/js/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## 0.3.0 + +### Added + +Added `tryNativeToHexString` + +Added `tryNativeToUint8Array` + +Added `tryHexToNativeString` + +Added `tryUint8ArrayToNative` + +Added support for passing in chain names wherever a chain is expected + +Added chain id 0 (unset) + +Added contract addresses to the `consts` module + +### Changed + +Deprecated `nativeToHexString` + +Deprecated `hexToNativeString` + +Deprecated `hexToNativeAssetString` + +Deprecated `uint8ArrayToNative` + +`isEVMChain` now performs type narrowing + +`CHAIN_ID_*` constants now have literal types + ## 0.2.7 ### Added diff --git a/sdk/js/package.json b/sdk/js/package.json index af70cafed..225999c08 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -1,6 +1,6 @@ { "name": "@certusone/wormhole-sdk", - "version": "0.2.7", + "version": "0.3.0", "description": "SDK for interacting with Wormhole", "homepage": "https://wormholenetwork.com", "main": "./lib/cjs/index.js", diff --git a/sdk/js/src/algorand/Algorand.ts b/sdk/js/src/algorand/Algorand.ts index 6cf4bf75c..f789860a6 100644 --- a/sdk/js/src/algorand/Algorand.ts +++ b/sdk/js/src/algorand/Algorand.ts @@ -882,8 +882,12 @@ export async function _submitVAAAlgorand( return txs; } +export function uint8ArrayToNativeStringAlgorand(a: Uint8Array): string { + return encodeAddress(a); +} + export function hexToNativeStringAlgorand(s: string): string { - return encodeAddress(hexToUint8Array(s)); + return uint8ArrayToNativeStringAlgorand(hexToUint8Array(s)); } export function nativeStringToHexAlgorand(s: string): string { diff --git a/sdk/js/src/nft_bridge/__tests__/integration.ts b/sdk/js/src/nft_bridge/__tests__/integration.ts index c9d173631..07c4f4b86 100644 --- a/sdk/js/src/nft_bridge/__tests__/integration.ts +++ b/sdk/js/src/nft_bridge/__tests__/integration.ts @@ -18,8 +18,6 @@ import { CHAIN_ID_TERRA, getEmitterAddressEth, getEmitterAddressTerra, - hexToUint8Array, - nativeToHexString, parseSequenceFromLogEth, parseSequenceFromLogTerra, nft_bridge, @@ -29,7 +27,7 @@ import { parseNFTPayload } from "../.."; import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry"; -import { importCoreWasm, importNftWasm, setDefaultWasm } from "../../solana/wasm"; +import { importCoreWasm, setDefaultWasm } from "../../solana/wasm"; import { ETH_CORE_BRIDGE_ADDRESS, ETH_NODE_URL, @@ -55,6 +53,7 @@ import { import sha3 from "js-sha3"; import { Connection, Keypair, PublicKey, TransactionResponse } from "@solana/web3.js"; import { postVaaSolanaWithRetry } from "../../solana"; +import { tryNativeToUint8Array } from "../../utils"; const ERC721 = require("@openzeppelin/contracts/build/contracts/ERC721PresetMinterPauserAutoId.json"); setDefaultWasm("node"); @@ -111,9 +110,8 @@ describe("Integration Tests", () => { // Check we have the wrapped NFT contract const terra_addr = await nft_bridge.getForeignAssetTerra(TERRA_NFT_BRIDGE_ADDRESS, lcd, CHAIN_ID_ETH, - hexToUint8Array( - nativeToHexString(erc721.address, CHAIN_ID_ETH) || "" - )); + tryNativeToUint8Array(erc721.address, CHAIN_ID_ETH) + ); if (!terra_addr) { throw new Error("Terra address is null"); } @@ -174,9 +172,8 @@ describe("Integration Tests", () => { // Check we have the wrapped NFT contract const eth_addr = await nft_bridge.getForeignAssetEth(ETH_NFT_BRIDGE_ADDRESS, provider, CHAIN_ID_TERRA, - hexToUint8Array( - nativeToHexString(cw721, CHAIN_ID_TERRA) || "" - )); + tryNativeToUint8Array(cw721, CHAIN_ID_TERRA) + ); if (!eth_addr) { throw new Error("Ethereum address is null"); } @@ -239,9 +236,8 @@ describe("Integration Tests", () => { await _redeemOnTerra(signedVAA); const terra_addr = await nft_bridge.getForeignAssetTerra(TERRA_NFT_BRIDGE_ADDRESS, lcd, CHAIN_ID_SOLANA, - hexToUint8Array( - nativeToHexString(TEST_SOLANA_TOKEN, CHAIN_ID_SOLANA) || "" - )); + tryNativeToUint8Array(TEST_SOLANA_TOKEN, CHAIN_ID_SOLANA) + ); if (!terra_addr) { throw new Error("Terra address is null"); } @@ -251,9 +247,8 @@ describe("Integration Tests", () => { await _redeemOnEth(signedVAA); const eth_addr = await nft_bridge.getForeignAssetEth(ETH_NFT_BRIDGE_ADDRESS, provider, CHAIN_ID_SOLANA, - hexToUint8Array( - nativeToHexString(TEST_SOLANA_TOKEN, CHAIN_ID_SOLANA) || "" - )); + tryNativeToUint8Array(TEST_SOLANA_TOKEN, CHAIN_ID_SOLANA) + ); if (!eth_addr) { throw new Error("Ethereum address is null"); } @@ -291,9 +286,8 @@ describe("Integration Tests", () => { let signedVAA = await waitUntilEthTxObserved(transaction); await _redeemOnTerra(signedVAA); const terra_addr = await nft_bridge.getForeignAssetTerra(TERRA_NFT_BRIDGE_ADDRESS, lcd, CHAIN_ID_ETH, - hexToUint8Array( - nativeToHexString(erc721.address, CHAIN_ID_ETH) || "" - )); + tryNativeToUint8Array(erc721.address, CHAIN_ID_ETH) + ); if (!terra_addr) { throw new Error("Terra address is null"); } @@ -371,8 +365,8 @@ async function estimateTerraFee(gasPrices: Coins.Input, msgs: Msg[]): Promise { @@ -527,7 +520,7 @@ async function _transferFromTerra(terra_addr: string, token_id: string, address: terra_addr, token_id, chain, - hexToUint8Array(nativeToHexString(address, chain) || "")); + tryNativeToUint8Array(address, chain)) const tx = await terraWallet.createAndSignTx({ msgs: msgs, memo: "test", @@ -546,9 +539,7 @@ async function _transferFromSolana(fromAddress: PublicKey, targetAddress: string payerAddress, fromAddress.toString(), TEST_SOLANA_TOKEN, - hexToUint8Array( - nativeToHexString(targetAddress, chain) || "" - ), + tryNativeToUint8Array(targetAddress, chain), chain ); // sign, send, and confirm transaction diff --git a/sdk/js/src/nft_bridge/getForeignAsset.ts b/sdk/js/src/nft_bridge/getForeignAsset.ts index a39b228cc..a1528e71e 100644 --- a/sdk/js/src/nft_bridge/getForeignAsset.ts +++ b/sdk/js/src/nft_bridge/getForeignAsset.ts @@ -5,7 +5,7 @@ import { fromUint8Array } from "js-base64"; import { CHAIN_ID_SOLANA } from ".."; import { NFTBridge__factory } from "../ethers-contracts"; import { importNftWasm } from "../solana/wasm"; -import { ChainId } from "../utils"; +import { ChainId, ChainName, coalesceChainId } from "../utils"; /** * Returns a foreign asset address on Ethereum for a provided native chain and asset address, AddressZero if it does not exist @@ -18,12 +18,13 @@ import { ChainId } from "../utils"; export async function getForeignAssetEth( tokenBridgeAddress: string, provider: ethers.Signer | ethers.providers.Provider, - originChain: ChainId, + originChain: ChainId | ChainName, originAsset: Uint8Array ): Promise { + const originChainId = coalesceChainId(originChain); const tokenBridge = NFTBridge__factory.connect(tokenBridgeAddress, provider); try { - if (originChain === CHAIN_ID_SOLANA) { + if (originChainId === CHAIN_ID_SOLANA) { // All NFTs from Solana are minted to the same address, the originAsset is encoded as the tokenId as // BigNumber.from(new PublicKey(originAsset).toBytes()).toString() const addr = await tokenBridge.wrappedAsset( @@ -32,7 +33,7 @@ export async function getForeignAssetEth( ); return addr; } - return await tokenBridge.wrappedAsset(originChain, originAsset); + return await tokenBridge.wrappedAsset(originChainId, originAsset); } catch (e) { return null; } @@ -52,6 +53,7 @@ export async function getForeignAssetTerra( originChain: ChainId, originAsset: Uint8Array ): Promise { + const originChainId = coalesceChainId(originChain); try { const address = originChain == CHAIN_ID_SOLANA @@ -61,7 +63,7 @@ export async function getForeignAssetTerra( tokenBridgeAddress, { wrapped_registry: { - chain: originChain, + chain: originChainId, address, }, } @@ -81,15 +83,16 @@ export async function getForeignAssetTerra( */ export async function getForeignAssetSol( tokenBridgeAddress: string, - originChain: ChainId, + originChain: ChainId | ChainName, originAsset: Uint8Array, tokenId: Uint8Array ): Promise { + const originChainId = coalesceChainId(originChain); const { wrapped_address } = await importNftWasm(); const wrappedAddress = wrapped_address( tokenBridgeAddress, originAsset, - originChain, + originChainId, tokenId ); const wrappedAddressPK = new PublicKey(wrappedAddress); diff --git a/sdk/js/src/nft_bridge/getOriginalAsset.ts b/sdk/js/src/nft_bridge/getOriginalAsset.ts index d8f052569..fdb25bc11 100644 --- a/sdk/js/src/nft_bridge/getOriginalAsset.ts +++ b/sdk/js/src/nft_bridge/getOriginalAsset.ts @@ -5,9 +5,16 @@ import { arrayify, zeroPad } from "ethers/lib/utils"; import { canonicalAddress, WormholeWrappedInfo } from ".."; import { TokenImplementation__factory } from "../ethers-contracts"; import { importNftWasm } from "../solana/wasm"; -import { ChainId, CHAIN_ID_SOLANA, CHAIN_ID_TERRA } from "../utils"; +import { + ChainId, + ChainName, + CHAIN_ID_SOLANA, + CHAIN_ID_TERRA, + coalesceChainId, +} from "../utils"; import { getIsWrappedAssetEth } from "./getIsWrappedAsset"; +// TODO: remove `as ChainId` and return number in next minor version as we can't ensure it will match our type definition export interface WormholeWrappedNFTInfo { isWrapped: boolean; chainId: ChainId; @@ -27,7 +34,7 @@ export async function getOriginalAssetEth( provider: ethers.Signer | ethers.providers.Provider, wrappedAddress: string, tokenId: string, - lookupChainId: ChainId + lookupChain: ChainId | ChainName ): Promise { const isWrapped = await getIsWrappedAssetEth( tokenBridgeAddress, @@ -53,7 +60,7 @@ export async function getOriginalAssetEth( } return { isWrapped: false, - chainId: lookupChainId, + chainId: coalesceChainId(lookupChain), assetAddress: zeroPad(arrayify(wrappedAddress), 32), tokenId, }; diff --git a/sdk/js/src/nft_bridge/transfer.ts b/sdk/js/src/nft_bridge/transfer.ts index 7504c5ace..5aae70106 100644 --- a/sdk/js/src/nft_bridge/transfer.ts +++ b/sdk/js/src/nft_bridge/transfer.ts @@ -8,17 +8,18 @@ import { } from "../ethers-contracts"; import { getBridgeFeeIx, ixFromRust } from "../solana"; import { importNftWasm } from "../solana/wasm"; -import { ChainId, CHAIN_ID_SOLANA, createNonce } from "../utils"; +import { ChainId, ChainName, CHAIN_ID_SOLANA, coalesceChainId, createNonce } from "../utils"; export async function transferFromEth( tokenBridgeAddress: string, signer: ethers.Signer, tokenAddress: string, tokenID: ethers.BigNumberish, - recipientChain: ChainId, + recipientChain: ChainId | ChainName, recipientAddress: Uint8Array, overrides: Overrides & { from?: string | Promise } = {} ): Promise { + const recipientChainId = coalesceChainId(recipientChain) //TODO: should we check if token attestation exists on the target chain const token = NFTImplementation__factory.connect(tokenAddress, signer); await (await token.approve(tokenBridgeAddress, tokenID, overrides)).wait(); @@ -26,7 +27,7 @@ export async function transferFromEth( const v = await bridge.transferNFT( tokenAddress, tokenID, - recipientChain, + recipientChainId, recipientAddress, createNonce(), overrides @@ -43,11 +44,12 @@ export async function transferFromSolana( fromAddress: string, mintAddress: string, targetAddress: Uint8Array, - targetChain: ChainId, + targetChain: ChainId | ChainName, originAddress?: Uint8Array, - originChain?: ChainId, + originChain?: ChainId | ChainName, originTokenId?: Uint8Array ): Promise { + const originChainId: ChainId | undefined = originChain ? coalesceChainId(originChain) : undefined const nonce = createNonce().readUInt32LE(0); const transferIx = await getBridgeFeeIx( connection, @@ -70,7 +72,7 @@ export async function transferFromSolana( let messageKey = Keypair.generate(); const isSolanaNative = originChain === undefined || originChain === CHAIN_ID_SOLANA; - if (!isSolanaNative && !originAddress && !originTokenId) { + if (!isSolanaNative && (!originAddress || !originTokenId)) { throw new Error( "originAddress and originTokenId are required when specifying originChain" ); @@ -86,7 +88,7 @@ export async function transferFromSolana( mintAddress, nonce, targetAddress, - targetChain + coalesceChainId(targetChain) ) : transfer_wrapped_ix( tokenBridgeAddress, @@ -95,12 +97,12 @@ export async function transferFromSolana( messageKey.publicKey.toString(), fromAddress, payerAddress, - originChain as number, // checked by isSolanaNative + originChainId as number, // checked by isSolanaNative originAddress as Uint8Array, // checked by throw originTokenId as Uint8Array, // checked by throw nonce, targetAddress, - targetChain + coalesceChainId(targetChain) ) ); const transaction = new Transaction().add(transferIx, approvalIx, ix); @@ -116,9 +118,10 @@ export async function transferFromTerra( tokenBridgeAddress: string, tokenAddress: string, tokenID: string, - recipientChain: ChainId, + recipientChain: ChainId | ChainName, recipientAddress: Uint8Array ): Promise { + const recipientChainId = coalesceChainId(recipientChain) const nonce = Math.round(Math.random() * 100000); return [ new MsgExecuteContract( @@ -139,7 +142,7 @@ export async function transferFromTerra( initiate_transfer: { contract_addr: tokenAddress, token_id: tokenID, - recipient_chain: recipientChain, + recipient_chain: recipientChainId, recipient: Buffer.from(recipientAddress).toString("base64"), nonce: nonce, }, diff --git a/sdk/js/src/rpc/getSignedVAA.ts b/sdk/js/src/rpc/getSignedVAA.ts index bb7723137..b67bedb06 100644 --- a/sdk/js/src/rpc/getSignedVAA.ts +++ b/sdk/js/src/rpc/getSignedVAA.ts @@ -1,4 +1,4 @@ -import { ChainId } from "../utils/consts"; +import { ChainId, ChainName, coalesceChainId } from "../utils/consts"; import { GrpcWebImpl, PublicRPCServiceClientImpl, @@ -6,7 +6,7 @@ import { export async function getSignedVAA( host: string, - emitterChain: ChainId, + emitterChain: ChainId | ChainName, emitterAddress: string, sequence: string, extraGrpcOpts = {} @@ -15,7 +15,7 @@ export async function getSignedVAA( const api = new PublicRPCServiceClientImpl(rpc); return await api.GetSignedVAA({ messageId: { - emitterChain, + emitterChain: coalesceChainId(emitterChain), emitterAddress, sequence, }, diff --git a/sdk/js/src/rpc/getSignedVAAWithRetry.ts b/sdk/js/src/rpc/getSignedVAAWithRetry.ts index 227598f33..88c390d06 100644 --- a/sdk/js/src/rpc/getSignedVAAWithRetry.ts +++ b/sdk/js/src/rpc/getSignedVAAWithRetry.ts @@ -1,8 +1,9 @@ -import { ChainId, getSignedVAA } from ".."; +import { ChainId, ChainName, getSignedVAA } from ".."; +import { coalesceChainId } from "../utils"; export async function getSignedVAAWithRetry( hosts: string[], - emitterChain: ChainId, + emitterChain: ChainId | ChainName, emitterAddress: string, sequence: string, extraGrpcOpts = {}, @@ -19,7 +20,7 @@ export async function getSignedVAAWithRetry( try { result = await getSignedVAA( hosts[getNextRpcHost()], - emitterChain, + coalesceChainId(emitterChain), emitterAddress, sequence, extraGrpcOpts diff --git a/sdk/js/src/token_bridge/__tests__/integration.ts b/sdk/js/src/token_bridge/__tests__/integration.ts index 5b2005251..80d381530 100644 --- a/sdk/js/src/token_bridge/__tests__/integration.ts +++ b/sdk/js/src/token_bridge/__tests__/integration.ts @@ -71,6 +71,8 @@ import { transferFromEth, transferFromSolana, transferFromTerra, + tryNativeToHexString, + tryNativeToUint8Array, uint8ArrayToHex, updateWrappedOnEth, WormholeWrappedInfo, @@ -211,7 +213,7 @@ describe("Integration Tests", () => { connection, SOLANA_TOKEN_BRIDGE_ADDRESS, CHAIN_ID_ETH, - hexToUint8Array(nativeToHexString(TEST_ERC20, CHAIN_ID_ETH) || "") + tryNativeToUint8Array(TEST_ERC20, CHAIN_ID_ETH) ); const solanaMintKey = new PublicKey(SolanaForeignAsset || ""); const recipient = await Token.getAssociatedTokenAddress( @@ -294,9 +296,7 @@ describe("Integration Tests", () => { TEST_ERC20, amount, CHAIN_ID_SOLANA, - hexToUint8Array( - nativeToHexString(recipient.toString(), CHAIN_ID_SOLANA) || "" - ) + tryNativeToUint8Array(recipient.toString(), CHAIN_ID_SOLANA) ); // get the sequence from the logs (needed to fetch the vaa) const sequence = parseSequenceFromLogEth( @@ -503,7 +503,7 @@ describe("Integration Tests", () => { // Get the initial wallet balance on Eth const ETH_TEST_WALLET_PUBLIC_KEY = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"; - const originAssetHex = nativeToHexString( + const originAssetHex = tryNativeToHexString( TEST_SOLANA_TOKEN, CHAIN_ID_SOLANA ); @@ -538,9 +538,7 @@ describe("Integration Tests", () => { fromAddress, TEST_SOLANA_TOKEN, amount, - hexToUint8Array( - nativeToHexString(targetAddress, CHAIN_ID_ETH) || "" - ), + tryNativeToUint8Array(targetAddress, CHAIN_ID_ETH), CHAIN_ID_ETH ); // sign, send, and confirm transaction @@ -737,7 +735,7 @@ describe("Integration Tests", () => { ); // Get initial balance of ERC20 on Terra - const originAssetHex = nativeToHexString(ERC20, CHAIN_ID_ETH); + const originAssetHex = tryNativeToHexString(ERC20, CHAIN_ID_ETH); if (!originAssetHex) { throw new Error("originAssetHex is null"); } @@ -794,9 +792,7 @@ describe("Integration Tests", () => { TEST_ERC20, amount, CHAIN_ID_TERRA, - hexToUint8Array( - nativeToHexString(wallet.key.accAddress, CHAIN_ID_TERRA) || "" - ) + tryNativeToUint8Array(wallet.key.accAddress, CHAIN_ID_TERRA) ); // get the sequence from the logs (needed to fetch the vaa) const sequence = parseSequenceFromLogEth( @@ -1279,7 +1275,7 @@ describe("Integration Tests", () => { ETH_NODE_URL ) as any; const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider); - const originAssetHex = nativeToHexString(Asset, CHAIN_ID_TERRA); + const originAssetHex = tryNativeToHexString(Asset, CHAIN_ID_TERRA); if (!originAssetHex) { throw new Error("originAssetHex is null"); } @@ -1308,7 +1304,7 @@ describe("Integration Tests", () => { ); // Start transfer from Terra to Ethereum - const hexStr = nativeToHexString( + const hexStr = tryNativeToHexString( ETH_TEST_WALLET_PUBLIC_KEY, CHAIN_ID_ETH ); @@ -1427,7 +1423,7 @@ describe("Integration Tests", () => { ETH_NODE_URL ) as any; const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider); - const originAssetHex = nativeToHexString(Asset, CHAIN_ID_TERRA); + const originAssetHex = tryNativeToHexString(Asset, CHAIN_ID_TERRA); if (!originAssetHex) { throw new Error("originAssetHex is null"); } @@ -1470,9 +1466,7 @@ describe("Integration Tests", () => { foreignAsset, Amount, CHAIN_ID_TERRA, - hexToUint8Array( - nativeToHexString(wallet.key.accAddress, CHAIN_ID_TERRA) || "" - ) + tryNativeToUint8Array(wallet.key.accAddress, CHAIN_ID_TERRA) ); console.log("Transfer gas used: ", receipt.gasUsed); @@ -1659,7 +1653,7 @@ describe("Integration Tests", () => { console.log("Initial Terra balance of", FeeAsset, initialFeeBalance); // Get wallet on eth - const originAssetHex = nativeToHexString(CW20, CHAIN_ID_TERRA); + const originAssetHex = tryNativeToHexString(CW20, CHAIN_ID_TERRA); console.log("CW20 originAssetHex: ", originAssetHex); if (!originAssetHex) { throw new Error("originAssetHex is null"); @@ -1706,7 +1700,7 @@ describe("Integration Tests", () => { "CW20 balance on Terra before transfer = ", initialCW20BalOnTerra ); - const hexStr = nativeToHexString( + const hexStr = tryNativeToHexString( ETH_TEST_WALLET_PUBLIC_KEY, CHAIN_ID_ETH ); @@ -1831,9 +1825,7 @@ describe("Integration Tests", () => { foreignAsset, Amount, CHAIN_ID_TERRA, - hexToUint8Array( - nativeToHexString(wallet.key.accAddress, CHAIN_ID_TERRA) || "" - ) + tryNativeToUint8Array(wallet.key.accAddress, CHAIN_ID_TERRA) ); console.log("Transfer gas used: ", receipt.gasUsed); diff --git a/sdk/js/src/token_bridge/attest.ts b/sdk/js/src/token_bridge/attest.ts index c9b902893..49a5e23d4 100644 --- a/sdk/js/src/token_bridge/attest.ts +++ b/sdk/js/src/token_bridge/attest.ts @@ -25,7 +25,7 @@ export async function attestFromEth( signer: ethers.Signer, tokenAddress: string, overrides: PayableOverrides & { from?: string | Promise } = {} -) { +): Promise { const bridge = Bridge__factory.connect(tokenBridgeAddress, signer); const v = await bridge.attestToken(tokenAddress, createNonce(), overrides); const receipt = await v.wait(); @@ -36,7 +36,7 @@ export async function attestFromTerra( tokenBridgeAddress: string, walletAddress: string, asset: string -) { +): Promise { const nonce = Math.round(Math.random() * 100000); const isNativeAsset = isNativeDenom(asset); return new MsgExecuteContract(walletAddress, tokenBridgeAddress, { @@ -61,7 +61,7 @@ export async function attestFromSolana( tokenBridgeAddress: string, payerAddress: string, mintAddress: string -) { +): Promise { const nonce = createNonce().readUInt32LE(0); const transferIx = await getBridgeFeeIx( connection, diff --git a/sdk/js/src/token_bridge/createWrapped.ts b/sdk/js/src/token_bridge/createWrapped.ts index 107136fd7..66017a59f 100644 --- a/sdk/js/src/token_bridge/createWrapped.ts +++ b/sdk/js/src/token_bridge/createWrapped.ts @@ -13,7 +13,7 @@ export async function createWrappedOnEth( signer: ethers.Signer, signedVAA: Uint8Array, overrides: Overrides & { from?: string | Promise } = {} -) { +): Promise { const bridge = Bridge__factory.connect(tokenBridgeAddress, signer); const v = await bridge.createWrapped(signedVAA, overrides); const receipt = await v.wait(); @@ -24,7 +24,7 @@ export async function createWrappedOnTerra( tokenBridgeAddress: string, walletAddress: string, signedVAA: Uint8Array -) { +): Promise { return new MsgExecuteContract(walletAddress, tokenBridgeAddress, { submit_vaa: { data: fromUint8Array(signedVAA), @@ -38,7 +38,7 @@ export async function createWrappedOnSolana( tokenBridgeAddress: string, payerAddress: string, signedVAA: Uint8Array -) { +): Promise { const { create_wrapped_ix } = await importTokenWasm(); const ix = ixFromRust( create_wrapped_ix( diff --git a/sdk/js/src/token_bridge/getForeignAsset.ts b/sdk/js/src/token_bridge/getForeignAsset.ts index 8e2665bc2..b65c9c2aa 100644 --- a/sdk/js/src/token_bridge/getForeignAsset.ts +++ b/sdk/js/src/token_bridge/getForeignAsset.ts @@ -10,7 +10,12 @@ import { } from "../algorand"; import { Bridge__factory } from "../ethers-contracts"; import { importTokenWasm } from "../solana/wasm"; -import { ChainId, CHAIN_ID_ALGORAND } from "../utils"; +import { + ChainId, + ChainName, + CHAIN_ID_ALGORAND, + coalesceChainId, +} from "../utils"; /** * Returns a foreign asset address on Ethereum for a provided native chain and asset address, AddressZero if it does not exist @@ -23,12 +28,15 @@ import { ChainId, CHAIN_ID_ALGORAND } from "../utils"; export async function getForeignAssetEth( tokenBridgeAddress: string, provider: ethers.Signer | ethers.providers.Provider, - originChain: ChainId, + originChain: ChainId | ChainName, originAsset: Uint8Array -) { +): Promise { const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider); try { - return await tokenBridge.wrappedAsset(originChain, originAsset); + return await tokenBridge.wrappedAsset( + coalesceChainId(originChain), + originAsset + ); } catch (e) { return null; } @@ -37,15 +45,15 @@ export async function getForeignAssetEth( export async function getForeignAssetTerra( tokenBridgeAddress: string, client: LCDClient, - originChain: ChainId, + originChain: ChainId | ChainName, originAsset: Uint8Array -) { +): Promise { try { const result: { address: string } = await client.wasm.contractQuery( tokenBridgeAddress, { wrapped_registry: { - chain: originChain, + chain: coalesceChainId(originChain), address: fromUint8Array(originAsset), }, } @@ -67,14 +75,14 @@ export async function getForeignAssetTerra( export async function getForeignAssetSolana( connection: Connection, tokenBridgeAddress: string, - originChain: ChainId, + originChain: ChainId | ChainName, originAsset: Uint8Array -) { +): Promise { const { wrapped_address } = await importTokenWasm(); const wrappedAddress = wrapped_address( tokenBridgeAddress, originAsset, - originChain + coalesceChainId(originChain) ); const wrappedAddressPK = new PublicKey(wrappedAddress); const wrappedAssetAccountInfo = await connection.getAccountInfo( @@ -86,16 +94,17 @@ export async function getForeignAssetSolana( export async function getForeignAssetAlgorand( client: Algodv2, tokenBridgeId: bigint, - chain: ChainId, + chain: ChainId | ChainName, contract: string ): Promise { - if (chain === CHAIN_ID_ALGORAND) { + const chainId = coalesceChainId(chain); + if (chainId === CHAIN_ID_ALGORAND) { return hexToNativeAssetBigIntAlgorand(contract); } else { let { lsa, doesExist } = await calcLogicSigAccount( client, tokenBridgeId, - BigInt(chain), + BigInt(chainId), contract ); if (!doesExist) { diff --git a/sdk/js/src/token_bridge/getIsTransferCompleted.ts b/sdk/js/src/token_bridge/getIsTransferCompleted.ts index c74a03832..bddf90923 100644 --- a/sdk/js/src/token_bridge/getIsTransferCompleted.ts +++ b/sdk/js/src/token_bridge/getIsTransferCompleted.ts @@ -20,7 +20,7 @@ export async function getIsTransferCompletedEth( tokenBridgeAddress: string, provider: ethers.Signer | ethers.providers.Provider, signedVAA: Uint8Array -) { +): Promise { const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider); const signedVAAHash = await getSignedVAAHash(signedVAA); return await tokenBridge.isTransferCompleted(signedVAAHash); @@ -31,7 +31,7 @@ export async function getIsTransferCompletedTerra( signedVAA: Uint8Array, client: LCDClient, gasPriceUrl: string -) { +): Promise { const msg = await redeemOnTerra( tokenBridgeAddress, TERRA_REDEEMED_CHECK_WALLET_ADDRESS, @@ -68,7 +68,7 @@ export async function getIsTransferCompletedSolana( tokenBridgeAddress: string, signedVAA: Uint8Array, connection: Connection -) { +): Promise { const { claim_address } = await importCoreWasm(); const claimAddress = await claim_address(tokenBridgeAddress, signedVAA); const claimInfo = await connection.getAccountInfo( diff --git a/sdk/js/src/token_bridge/getIsWrappedAsset.ts b/sdk/js/src/token_bridge/getIsWrappedAsset.ts index e9c3b9548..6d1bcfb95 100644 --- a/sdk/js/src/token_bridge/getIsWrappedAsset.ts +++ b/sdk/js/src/token_bridge/getIsWrappedAsset.ts @@ -17,17 +17,18 @@ export async function getIsWrappedAssetEth( tokenBridgeAddress: string, provider: ethers.Signer | ethers.providers.Provider, assetAddress: string -) { +): Promise { if (!assetAddress) return false; const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider); return await tokenBridge.isWrappedAsset(assetAddress); } +// TODO: this doesn't seem right export async function getIsWrappedAssetTerra( tokenBridgeAddress: string, client: LCDClient, assetAddress: string -) { +): Promise { return false; } @@ -42,7 +43,7 @@ export async function getIsWrappedAssetSol( connection: Connection, tokenBridgeAddress: string, mintAddress: string -) { +): Promise { if (!mintAddress) return false; const { wrapped_meta_address } = await importTokenWasm(); const wrappedMetaAddress = wrapped_meta_address( diff --git a/sdk/js/src/token_bridge/getOriginalAsset.ts b/sdk/js/src/token_bridge/getOriginalAsset.ts index b4cf05fcf..404fce2be 100644 --- a/sdk/js/src/token_bridge/getOriginalAsset.ts +++ b/sdk/js/src/token_bridge/getOriginalAsset.ts @@ -9,9 +9,11 @@ import { importTokenWasm } from "../solana/wasm"; import { buildNativeId, canonicalAddress, isNativeDenom } from "../terra"; import { ChainId, + ChainName, CHAIN_ID_ALGORAND, CHAIN_ID_SOLANA, CHAIN_ID_TERRA, + coalesceChainId, hexToUint8Array, } from "../utils"; import { safeBigIntToNumber } from "../utils/bigint"; @@ -20,6 +22,7 @@ import { getIsWrappedAssetEth, } from "./getIsWrappedAsset"; +// TODO: remove `as ChainId` and return number in next minor version as we can't ensure it will match our type definition export interface WormholeWrappedInfo { isWrapped: boolean; chainId: ChainId; @@ -37,7 +40,7 @@ export async function getOriginalAssetEth( tokenBridgeAddress: string, provider: ethers.Signer | ethers.providers.Provider, wrappedAddress: string, - lookupChainId: ChainId + lookupChain: ChainId | ChainName ): Promise { const isWrapped = await getIsWrappedAssetEth( tokenBridgeAddress, @@ -59,7 +62,7 @@ export async function getOriginalAssetEth( } return { isWrapped: false, - chainId: lookupChainId, + chainId: coalesceChainId(lookupChain), assetAddress: zeroPad(arrayify(wrappedAddress), 32), }; } diff --git a/sdk/js/src/token_bridge/transfer.ts b/sdk/js/src/token_bridge/transfer.ts index 1b8c3775c..9efae8bbb 100644 --- a/sdk/js/src/token_bridge/transfer.ts +++ b/sdk/js/src/token_bridge/transfer.ts @@ -35,7 +35,9 @@ import { getBridgeFeeIx, ixFromRust } from "../solana"; import { importTokenWasm } from "../solana/wasm"; import { ChainId, + ChainName, CHAIN_ID_SOLANA, + coalesceChainId, createNonce, hexToUint8Array, textToUint8Array, @@ -73,16 +75,17 @@ export async function transferFromEth( signer: ethers.Signer, tokenAddress: string, amount: ethers.BigNumberish, - recipientChain: ChainId, + recipientChain: ChainId | ChainName, recipientAddress: Uint8Array, relayerFee: ethers.BigNumberish = 0, overrides: PayableOverrides & { from?: string | Promise } = {} ) { + const recipientChainId = coalesceChainId(recipientChain); const bridge = Bridge__factory.connect(tokenBridgeAddress, signer); const v = await bridge.transferTokens( tokenAddress, amount, - recipientChain, + recipientChainId, recipientAddress, relayerFee, createNonce(), @@ -96,14 +99,15 @@ export async function transferFromEthNative( tokenBridgeAddress: string, signer: ethers.Signer, amount: ethers.BigNumberish, - recipientChain: ChainId, + recipientChain: ChainId | ChainId, recipientAddress: Uint8Array, relayerFee: ethers.BigNumberish = 0, overrides: PayableOverrides & { from?: string | Promise } = {} ) { + const recipientChainId = coalesceChainId(recipientChain); const bridge = Bridge__factory.connect(tokenBridgeAddress, signer); const v = await bridge.wrapAndTransferETH( - recipientChain, + recipientChainId, recipientAddress, relayerFee, createNonce(), @@ -121,10 +125,11 @@ export async function transferFromTerra( tokenBridgeAddress: string, tokenAddress: string, amount: string, - recipientChain: ChainId, + recipientChain: ChainId | ChainName, recipientAddress: Uint8Array, relayerFee: string = "0" ) { + const recipientChainId = coalesceChainId(recipientChain); const nonce = Math.round(Math.random() * 100000); const isNativeAsset = isNativeDenom(tokenAddress); return isNativeAsset @@ -150,7 +155,7 @@ export async function transferFromTerra( }, }, }, - recipient_chain: recipientChain, + recipient_chain: recipientChainId, recipient: Buffer.from(recipientAddress).toString("base64"), fee: relayerFee, nonce: nonce, @@ -187,7 +192,7 @@ export async function transferFromTerra( }, }, }, - recipient_chain: recipientChain, + recipient_chain: recipientChainId, recipient: Buffer.from(recipientAddress).toString("base64"), fee: relayerFee, nonce: nonce, @@ -205,7 +210,7 @@ export async function transferNativeSol( payerAddress: string, amount: BigInt, targetAddress: Uint8Array, - targetChain: ChainId, + targetChain: ChainId | ChainName, relayerFee: BigInt = BigInt(0) ) { //https://github.com/solana-labs/solana-program-library/blob/master/token/js/client/token.js @@ -268,7 +273,7 @@ export async function transferNativeSol( amount, relayerFee, targetAddress, - targetChain + coalesceChainId(targetChain) ) ); @@ -304,12 +309,15 @@ export async function transferFromSolana( mintAddress: string, amount: BigInt, targetAddress: Uint8Array, - targetChain: ChainId, + targetChain: ChainId | ChainName, originAddress?: Uint8Array, - originChain?: ChainId, + originChain?: ChainId | ChainName, fromOwnerAddress?: string, relayerFee: BigInt = BigInt(0) ) { + const originChainId: ChainId | undefined = originChain + ? coalesceChainId(originChain) + : undefined; const nonce = createNonce().readUInt32LE(0); const transferIx = await getBridgeFeeIx( connection, @@ -331,7 +339,7 @@ export async function transferFromSolana( ); let messageKey = Keypair.generate(); const isSolanaNative = - originChain === undefined || originChain === CHAIN_ID_SOLANA; + originChainId === undefined || originChainId === CHAIN_ID_SOLANA; if (!isSolanaNative && !originAddress) { throw new Error("originAddress is required when specifying originChain"); } @@ -348,7 +356,7 @@ export async function transferFromSolana( amount, relayerFee, targetAddress, - targetChain + coalesceChainId(targetChain) ) : transfer_wrapped_ix( tokenBridgeAddress, @@ -357,13 +365,13 @@ export async function transferFromSolana( messageKey.publicKey.toString(), fromAddress, fromOwnerAddress || payerAddress, - originChain as number, // checked by isSolanaNative + originChainId as number, // checked by isSolanaNative originAddress as Uint8Array, // checked by throw nonce, amount, relayerFee, targetAddress, - targetChain + coalesceChainId(targetChain) ) ); const transaction = new SolanaTransaction().add(transferIx, approvalIx, ix); @@ -395,9 +403,10 @@ export async function transferFromAlgorand( assetId: bigint, qty: bigint, receiver: string, - chain: ChainId, + chain: ChainId | ChainName, fee: bigint ): Promise { + const recipientChainId = coalesceChainId(chain); const tokenAddr: string = getApplicationAddress(tokenBridgeId); const applAddr: string = getEmitterAddressAlgorand(tokenBridgeId); const txs: TransactionSignerPair[] = []; @@ -512,7 +521,7 @@ export async function transferFromAlgorand( bigIntToBytes(assetId, 8), bigIntToBytes(qty, 8), hexToUint8Array(receiver), - bigIntToBytes(chain, 8), + bigIntToBytes(recipientChainId, 8), bigIntToBytes(fee, 8), ]; let acTxn = makeApplicationCallTxnFromObject({ diff --git a/sdk/js/src/utils/array.ts b/sdk/js/src/utils/array.ts index ebb62a72c..96571c93c 100644 --- a/sdk/js/src/utils/array.ts +++ b/sdk/js/src/utils/array.ts @@ -3,98 +3,172 @@ import { PublicKey } from "@solana/web3.js"; import { hexValue, hexZeroPad, stripZeros } from "ethers/lib/utils"; import { hexToNativeAssetStringAlgorand, - hexToNativeStringAlgorand, nativeStringToHexAlgorand, + uint8ArrayToNativeStringAlgorand, } from "../algorand"; import { canonicalAddress, humanAddress, isNativeDenom } from "../terra"; import { ChainId, - CHAIN_ID_ACALA, + ChainName, CHAIN_ID_ALGORAND, - CHAIN_ID_AURORA, - CHAIN_ID_AVAX, - CHAIN_ID_BSC, - CHAIN_ID_CELO, - CHAIN_ID_ETH, - CHAIN_ID_ETHEREUM_ROPSTEN, - CHAIN_ID_FANTOM, - CHAIN_ID_KARURA, - CHAIN_ID_KLAYTN, CHAIN_ID_NEAR, - CHAIN_ID_OASIS, - CHAIN_ID_POLYGON, CHAIN_ID_SOLANA, CHAIN_ID_TERRA, + CHAIN_ID_UNSET, + coalesceChainId, + isEVMChain, } from "./consts"; -export const isEVMChain = (chainId: ChainId) => { - return ( - chainId === CHAIN_ID_ETH || - chainId === CHAIN_ID_BSC || - chainId === CHAIN_ID_ETHEREUM_ROPSTEN || - chainId === CHAIN_ID_AVAX || - chainId === CHAIN_ID_POLYGON || - chainId === CHAIN_ID_OASIS || - chainId === CHAIN_ID_AURORA || - chainId === CHAIN_ID_FANTOM || - chainId === CHAIN_ID_KARURA || - chainId === CHAIN_ID_ACALA || - chainId === CHAIN_ID_KLAYTN || - chainId === CHAIN_ID_CELO - ); +/** + * + * Returns true iff the hex string represents a native Terra denom. + * + * Native assets on terra don't have an associated smart contract address, just + * like eth isn't an ERC-20 contract on Ethereum. + * + * The difference is that the EVM implementations of Portal don't support eth + * directly, and instead require swapping to an ERC-20 wrapped eth (WETH) + * contract first. + * + * The Terra implementation instead supports Terra-native denoms without + * wrapping to CW-20 token first. As these denoms don't have an address, they + * are encoded in the Portal payloads by the setting the first byte to 1. This + * encoding is safe, because the first 12 bytes of the 32-byte wormhole address + * space are not used on Terra otherwise, as cosmos addresses are 20 bytes wide. + */ +export const isHexNativeTerra = (h: string): boolean => h.startsWith("01"); + +export const nativeTerraHexToDenom = (h: string): string => + Buffer.from(stripZeros(hexToUint8Array(h.substr(2)))).toString("ascii"); + +export const uint8ArrayToHex = (a: Uint8Array): string => + Buffer.from(a).toString("hex"); + +export const hexToUint8Array = (h: string): Uint8Array => + new Uint8Array(Buffer.from(h, "hex")); + +/** + * + * Convert an address in a wormhole's 32-byte array representation into a chain's + * native string representation. + * + * @throws if address is not the right length for the given chain + */ + +export const tryUint8ArrayToNative = ( + a: Uint8Array, + chain: ChainId | ChainName +): string => { + const chainId = coalesceChainId(chain); + if (isEVMChain(chainId)) { + return hexZeroPad(hexValue(a), 20); + } else if (chainId === CHAIN_ID_SOLANA) { + return new PublicKey(a).toString(); + } else if (chainId === CHAIN_ID_TERRA) { + const h = uint8ArrayToHex(a); + if (isHexNativeTerra(h)) { + return nativeTerraHexToDenom(h); + } else { + return humanAddress(a.slice(-20)); // terra expects 20 bytes, not 32 + } + } else if (chainId === CHAIN_ID_ALGORAND) { + return uint8ArrayToNativeStringAlgorand(a); + } else if (chainId === CHAIN_ID_NEAR) { + throw Error("uint8ArrayToNative: Near not supported yet."); + } else if (chainId === CHAIN_ID_UNSET) { + throw Error("uint8ArrayToNative: Chain id unset"); + } else { + // This case is never reached + const _: never = chainId; + throw Error("Don't know how to convert address for chain " + chainId); + } }; -export const isHexNativeTerra = (h: string) => h.startsWith("01"); -export const nativeTerraHexToDenom = (h: string) => - Buffer.from(stripZeros(hexToUint8Array(h.substr(2)))).toString("ascii"); -export const uint8ArrayToHex = (a: Uint8Array) => - Buffer.from(a).toString("hex"); -export const hexToUint8Array = (h: string) => - new Uint8Array(Buffer.from(h, "hex")); -export const hexToNativeString = (h: string | undefined, c: ChainId) => { +/** + * + * Convert an address in a wormhole's 32-byte hex representation into a chain's native + * string representation. + * + * @throws if address is not the right length for the given chain + */ +export const tryHexToNativeAssetString = (h: string, c: ChainId): string => + c === CHAIN_ID_ALGORAND + ? // Algorand assets are represented by their asset ids, not an address + hexToNativeAssetStringAlgorand(h) + : tryHexToNativeString(h, c); + +/** + * + * Convert an address in a wormhole's 32-byte hex representation into a chain's native + * string representation. + * + * @deprecated since 0.3.0, use [[tryHexToNativeString]] instead. + */ +export const hexToNativeAssetString = ( + h: string | undefined, + c: ChainId +): string | undefined => { + if (!h) { + return undefined; + } try { - return !h - ? undefined - : c === CHAIN_ID_SOLANA - ? new PublicKey(hexToUint8Array(h)).toString() - : isEVMChain(c) - ? hexZeroPad(hexValue(hexToUint8Array(h)), 20) - : c === CHAIN_ID_TERRA - ? isHexNativeTerra(h) - ? nativeTerraHexToDenom(h) - : humanAddress(hexToUint8Array(h.substr(24))) // terra expects 20 bytes, not 32 - : c === CHAIN_ID_ALGORAND - ? hexToNativeStringAlgorand(h) - : h; - } catch (e) {} - return undefined; -}; -export const hexToNativeAssetString = (h: string | undefined, c: ChainId) => { - try { - return !h - ? undefined - : // Algorand assets are represented by their asset ids, not an address - c === CHAIN_ID_ALGORAND - ? hexToNativeAssetStringAlgorand(h) - : hexToNativeString(h, c); + return tryHexToNativeAssetString(h, c); } catch (e) { return undefined; } }; -export const nativeToHexString = ( - address: string | undefined, - chain: ChainId -) => { - if (!address || !chain) { - return null; +/** + * + * Convert an address in a wormhole's 32-byte hex representation into a chain's native + * string representation. + * + * @throws if address is not the right length for the given chain + */ +export const tryHexToNativeString = ( + h: string, + c: ChainId | ChainName +): string => tryUint8ArrayToNative(hexToUint8Array(h), c); + +/** + * + * Convert an address in a wormhole's 32-byte hex representation into a chain's native + * string representation. + * + * @deprecated since 0.3.0, use [[tryHexToNativeString]] instead. + */ +export const hexToNativeString = ( + h: string | undefined, + c: ChainId | ChainName +): string | undefined => { + if (!h) { + return undefined; } - if (isEVMChain(chain)) { + try { + return tryHexToNativeString(h, c); + } catch (e) { + return undefined; + } +}; + +/** + * + * Convert an address in a chain's native representation into a 32-byte hex string + * understood by wormhole. + * + * @throws if address is a malformed string for the given chain id + */ +export const tryNativeToHexString = ( + address: string, + chain: ChainId | ChainName +): string => { + const chainId = coalesceChainId(chain); + if (isEVMChain(chainId)) { return uint8ArrayToHex(zeroPad(arrayify(address), 32)); - } else if (chain === CHAIN_ID_SOLANA) { + } else if (chainId === CHAIN_ID_SOLANA) { return uint8ArrayToHex(zeroPad(new PublicKey(address).toBytes(), 32)); - } else if (chain === CHAIN_ID_TERRA) { + } else if (chainId === CHAIN_ID_TERRA) { if (isNativeDenom(address)) { return ( "01" + @@ -105,13 +179,60 @@ export const nativeToHexString = ( } else { return uint8ArrayToHex(zeroPad(canonicalAddress(address), 32)); } - } else if (chain === CHAIN_ID_ALGORAND) { + } else if (chainId === CHAIN_ID_ALGORAND) { return nativeStringToHexAlgorand(address); + } else if (chainId === CHAIN_ID_NEAR) { + throw Error("hexToNativeString: Near not supported yet."); + } else if (chainId === CHAIN_ID_UNSET) { + throw Error("hexToNativeString: Chain id unset"); } else { - return null; + // If this case is reached + const _: never = chainId; + throw Error("Don't know how to convert address from chain " + chainId); } }; +/** + * + * Convert an address in a chain's native representation into a 32-byte hex string + * understood by wormhole. + * + * @deprecated since 0.3.0, use [[tryNativeToHexString]] instead. + * @throws if address is a malformed string for the given chain id + */ +export const nativeToHexString = ( + address: string | undefined, + chain: ChainId | ChainName +): string | null => { + if (!address) { + return null; + } + return tryNativeToHexString(address, chain); +}; + +/** + * + * Convert an address in a chain's native representation into a 32-byte array + * understood by wormhole. + * + * @throws if address is a malformed string for the given chain id + */ +export function tryNativeToUint8Array( + address: string, + chain: ChainId | ChainName +): Uint8Array { + const chainId = coalesceChainId(chain); + return hexToUint8Array(tryNativeToHexString(address, chainId)); +} + +/** + * + * Convert an address in a chain's native representation into a 32-byte hex string + * understood by wormhole. + * + * @deprecated since 0.3.0, use [[tryUint8ArrayToNative]] instead. + * @throws if address is a malformed string for the given chain id + */ export const uint8ArrayToNative = (a: Uint8Array, chainId: ChainId) => hexToNativeString(uint8ArrayToHex(a), chainId); diff --git a/sdk/js/src/utils/consts.ts b/sdk/js/src/utils/consts.ts index 6a267da74..96f5f51a0 100644 --- a/sdk/js/src/utils/consts.ts +++ b/sdk/js/src/utils/consts.ts @@ -1,20 +1,501 @@ -export type ChainId = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 10001; -export const CHAIN_ID_SOLANA: ChainId = 1; -export const CHAIN_ID_ETH: ChainId = 2; -export const CHAIN_ID_TERRA: ChainId = 3; -export const CHAIN_ID_BSC: ChainId = 4; -export const CHAIN_ID_POLYGON: ChainId = 5; -export const CHAIN_ID_AVAX: ChainId = 6; -export const CHAIN_ID_OASIS: ChainId = 7; -export const CHAIN_ID_ALGORAND: ChainId = 8; -export const CHAIN_ID_AURORA: ChainId = 9; -export const CHAIN_ID_FANTOM: ChainId = 10; -export const CHAIN_ID_KARURA: ChainId = 11; -export const CHAIN_ID_ACALA: ChainId = 12; -export const CHAIN_ID_KLAYTN: ChainId = 13; -export const CHAIN_ID_CELO: ChainId = 14; -export const CHAIN_ID_NEAR: ChainId = 15; -export const CHAIN_ID_ETHEREUM_ROPSTEN: ChainId = 10001; +export const CHAINS = { + unset: 0, + solana: 1, + ethereum: 2, + terra: 3, + bsc: 4, + polygon: 5, + avalanche: 6, + oasis: 7, + algorand: 8, + aurora: 9, + fantom: 10, + karura: 11, + acala: 12, + klaytn: 13, + celo: 14, + near: 15, + ropsten: 10001, +} as const; + +export type ChainName = keyof typeof CHAINS; +export type ChainId = typeof CHAINS[ChainName]; + +/** + * + * All the EVM-based chain names that Wormhole supports + */ +export type EVMChainName = + | "ethereum" + | "bsc" + | "polygon" + | "avalanche" + | "oasis" + | "aurora" + | "fantom" + | "karura" + | "acala" + | "klaytn" + | "celo" + | "ropsten"; + +export type Contracts = { + core: string | undefined; + token_bridge: string | undefined; + nft_bridge: string | undefined; +}; + +export type ChainContracts = { + [chain in ChainName]: Contracts; +}; + +const MAINNET = { + unset: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + solana: { + core: "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth", + token_bridge: "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb", + nft_bridge: "WnFt12ZrnzZrFZkt2xsNsaNWoQribnuQ5B5FrDbwDhD", + }, + ethereum: { + core: "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B", + token_bridge: "0x3ee18B2214AFF97000D974cf647E7C347E8fa585", + nft_bridge: "0x6FFd7EdE62328b3Af38FCD61461Bbfc52F5651fE", + }, + terra: { + core: "terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5", + token_bridge: "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf", + nft_bridge: undefined, + }, + bsc: { + core: "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B", + token_bridge: "0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7", + nft_bridge: "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE", + }, + polygon: { + core: "0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7", + token_bridge: "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE", + nft_bridge: "0x90BBd86a6Fe93D3bc3ed6335935447E75fAb7fCf", + }, + avalanche: { + core: "0x54a8e5f9c4CbA08F9943965859F6c34eAF03E26c", + token_bridge: "0x0e082F06FF657D94310cB8cE8B0D9a04541d8052", + nft_bridge: "0xf7B6737Ca9c4e08aE573F75A97B73D7a813f5De5", + }, + oasis: { + core: "0xfE8cD454b4A1CA468B57D79c0cc77Ef5B6f64585", + token_bridge: "0x5848C791e09901b40A9Ef749f2a6735b418d7564", + nft_bridge: "0x04952D522Ff217f40B5Ef3cbF659EcA7b952a6c1", + }, + algorand: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + aurora: { + core: "0xa321448d90d4e5b0A732867c18eA198e75CAC48E", + token_bridge: "0x51b5123a7b0F9b2bA265f9c4C8de7D78D52f510F", + nft_bridge: "0x6dcC0484472523ed9Cdc017F711Bcbf909789284", + }, + fantom: { + core: "0x126783A6Cb203a3E35344528B26ca3a0489a1485", + token_bridge: "0x7C9Fc5741288cDFdD83CeB07f3ea7e22618D79D2", + nft_bridge: "0xA9c7119aBDa80d4a4E0C06C8F4d8cF5893234535", + }, + karura: { + core: "0xa321448d90d4e5b0A732867c18eA198e75CAC48E", + token_bridge: "0xae9d7fe007b3327AA64A32824Aaac52C42a6E624", + nft_bridge: "0xb91e3638F82A1fACb28690b37e3aAE45d2c33808", + }, + acala: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + klaytn: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + celo: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + near: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + ropsten: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, +}; + +const TESTNET = { + unset: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + solana: { + core: "3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5", + token_bridge: "DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe", + nft_bridge: "2rHhojZ7hpu1zA91nvZmT8TqWWvMcKmmNBCr2mKTtMq4", + }, + terra: { + core: "terra1pd65m0q9tl3v8znnz5f5ltsfegyzah7g42cx5v", + token_bridge: "terra1pseddrv0yfsn76u4zxrjmtf45kdlmalswdv39a", + nft_bridge: undefined, + }, + ethereum: { + core: "0x706abc4E45D419950511e474C7B9Ed348A4a716c", + token_bridge: "0xF890982f9310df57d00f659cf4fd87e65adEd8d7", + nft_bridge: "0xD8E4C2DbDd2e2bd8F1336EA691dBFF6952B1a6eB", + }, + bsc: { + core: "0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D", + token_bridge: "0x9dcF9D205C9De35334D646BeE44b2D2859712A09", + nft_bridge: "0xcD16E5613EF35599dc82B24Cb45B5A93D779f1EE", + }, + polygon: { + core: "0x0CBE91CF822c73C2315FB05100C2F714765d5c20", + token_bridge: "0x377D55a7928c046E18eEbb61977e714d2a76472a", + nft_bridge: "0x51a02d0dcb5e52F5b92bdAA38FA013C91c7309A9", + }, + avalanche: { + core: "0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C", + token_bridge: "0x61E44E506Ca5659E6c0bba9b678586fA2d729756", + nft_bridge: "0xD601BAf2EEE3C028344471684F6b27E789D9075D", + }, + oasis: { + core: "0xc1C338397ffA53a2Eb12A7038b4eeb34791F8aCb", + token_bridge: "0x88d8004A9BdbfD9D28090A02010C19897a29605c", + nft_bridge: "0xC5c25B41AB0b797571620F5204Afa116A44c0ebA", + }, + algorand: { + core: "86525623", + token_bridge: "86525641", + nft_bridge: undefined, + }, + aurora: { + core: "0xBd07292de7b505a4E803CEe286184f7Acf908F5e", + token_bridge: "0xD05eD3ad637b890D68a854d607eEAF11aF456fba", + nft_bridge: "0x8F399607E9BA2405D87F5f3e1B78D950b44b2e24", + }, + fantom: { + core: "0x1BB3B4119b7BA9dfad76B0545fb3F531383c3bB7", + token_bridge: "0x599CEa2204B4FaECd584Ab1F2b6aCA137a0afbE8", + nft_bridge: "0x63eD9318628D26BdCB15df58B53BB27231D1B227", + }, + karura: { + core: "0xE4eacc10990ba3308DdCC72d985f2a27D20c7d03", + token_bridge: "0xd11De1f930eA1F7Dd0290Fe3a2e35b9C91AEFb37", + nft_bridge: "0x0A693c2D594292B6Eb89Cb50EFe4B0b63Dd2760D", + }, + acala: { + core: "0x4377B49d559c0a9466477195C6AdC3D433e265c0", + token_bridge: "0xebA00cbe08992EdD08ed7793E07ad6063c807004", + nft_bridge: "0x96f1335e0AcAB3cfd9899B30b2374e25a2148a6E", + }, + klaytn: { + core: "0x1830CC6eE66c84D2F177B94D544967c774E624cA", + token_bridge: "0xC7A13BE098720840dEa132D860fDfa030884b09A", + nft_bridge: "0x94c994fC51c13101062958b567e743f1a04432dE", + }, + celo: { + core: "0x88505117CA88e7dd2eC6EA1E13f0948db2D50D56", + token_bridge: "0x05ca6037eC51F8b712eD2E6Fa72219FEaE74E153", + nft_bridge: "0xaCD8190F647a31E56A656748bC30F69259f245Db", + }, + near: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + ropsten: { + core: "0x210c5F5e2AF958B4defFe715Dc621b7a3BA888c5", + token_bridge: "0xF174F9A837536C449321df1Ca093Bb96948D5386", + nft_bridge: "0x2b048Da40f69c8dc386a56705915f8E966fe1eba", + }, +}; + +const DEVNET = { + unset: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + solana: { + core: "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o", + token_bridge: "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE", + nft_bridge: "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA", + }, + terra: { + core: "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5", + token_bridge: "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4", + nft_bridge: undefined, + }, + ethereum: { + core: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550", + token_bridge: "0x0290FB167208Af455bB137780163b7B7a9a10C16", + nft_bridge: "0x26b4afb60d6c903165150c6f0aa14f8016be4aec", + }, + bsc: { + core: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550", + token_bridge: "0x0290FB167208Af455bB137780163b7B7a9a10C16", + nft_bridge: "0x26b4afb60d6c903165150c6f0aa14f8016be4aec", + }, + polygon: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + avalanche: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + oasis: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + algorand: { + core: "4", + token_bridge: "6", + nft_bridge: undefined, + }, + aurora: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + fantom: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + karura: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + acala: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + klaytn: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + celo: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + near: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, + ropsten: { + core: undefined, + token_bridge: undefined, + nft_bridge: undefined, + }, +}; + +/** + * + * If you get a type error here, it means that a chain you just added does not + * have an entry in TESTNET. + * This is implemented as an ad-hoc type assertion instead of a type annotation + * on TESTNET so that e.g. + * + * ```typescript + * TESTNET['solana'].core + * ``` + * has type 'string' instead of 'string | undefined'. + * + * (Do not delete this declaration!) + */ +const isTestnetContracts: ChainContracts = TESTNET; + +/** + * + * See [[isTestnetContracts]] + */ +const isMainnetContracts: ChainContracts = MAINNET; + +/** + * + * See [[isTestnetContracts]] + */ +const isDevnetContracts: ChainContracts = DEVNET; + +/** + * + * Contracts addresses on testnet and mainnet + */ +export const CONTRACTS = { MAINNET, TESTNET, DEVNET }; + +// We don't specify the types of the below consts to be [[ChainId]]. This way, +// the inferred type will be a singleton (or literal) type, which is more precise and allows +// typescript to perform context-sensitive narrowing when checking against them. +// See the [[isEVMChain]] for an example. +export const CHAIN_ID_UNSET = CHAINS["unset"]; +export const CHAIN_ID_SOLANA = CHAINS["solana"]; +export const CHAIN_ID_ETH = CHAINS["ethereum"]; +export const CHAIN_ID_TERRA = CHAINS["terra"]; +export const CHAIN_ID_BSC = CHAINS["bsc"]; +export const CHAIN_ID_POLYGON = CHAINS["polygon"]; +export const CHAIN_ID_AVAX = CHAINS["avalanche"]; +export const CHAIN_ID_OASIS = CHAINS["oasis"]; +export const CHAIN_ID_ALGORAND = CHAINS["algorand"]; +export const CHAIN_ID_AURORA = CHAINS["aurora"]; +export const CHAIN_ID_FANTOM = CHAINS["fantom"]; +export const CHAIN_ID_KARURA = CHAINS["karura"]; +export const CHAIN_ID_ACALA = CHAINS["acala"]; +export const CHAIN_ID_KLAYTN = CHAINS["klaytn"]; +export const CHAIN_ID_CELO = CHAINS["celo"]; +export const CHAIN_ID_NEAR = CHAINS["near"]; +export const CHAIN_ID_ETHEREUM_ROPSTEN = CHAINS["ropsten"]; + +// This inverts the [[CHAINS]] object so that we can look up a chain by id +export type ChainIdToName = { + -readonly [key in keyof typeof CHAINS as typeof CHAINS[key]]: key; +}; +export const CHAIN_ID_TO_NAME: ChainIdToName = Object.entries(CHAINS).reduce( + (obj, [name, id]) => { + obj[id] = name; + return obj; + }, + {} as any +) as ChainIdToName; + +/** + * + * All the EVM-based chain ids that Wormhole supports + */ +export type EVMChainId = typeof CHAINS[EVMChainName]; + +/** + * + * Returns true when called with a valid chain, and narrows the type in the + * "true" branch to [[ChainId]] or [[ChainName]] thanks to the type predicate in + * the return type. + * + * A typical use-case might look like + * ```typescript + * foo = isChain(c) ? doSomethingWithChainId(c) : handleInvalidCase() + * ``` + */ +export function isChain(chain: number | string): chain is ChainId | ChainName { + if (typeof chain === "number") { + return chain in CHAIN_ID_TO_NAME; + } else { + return chain in CHAINS; + } +} + +/** + * + * Asserts that the given number or string is a valid chain, and throws otherwise. + * After calling this function, the type of chain will be narrowed to + * [[ChainId]] or [[ChainName]] thanks to the type assertion in the return type. + * + * A typical use-case might look like + * ```typescript + * // c has type 'string' + * assertChain(c) + * // c now has type 'ChainName' + * ``` + */ +export function assertChain( + chain: number | string +): asserts chain is ChainId | ChainName { + if (!isChain(chain)) { + if (typeof chain === "number") { + throw Error(`Unknown chain id: ${chain}`); + } else { + throw Error(`Unknown chain: ${chain}`); + } + } +} + +export function toChainId(chainName: ChainName): ChainId { + return CHAINS[chainName]; +} + +export function toChainName(chainId: ChainId): ChainName { + return CHAIN_ID_TO_NAME[chainId]; +} + +export function coalesceChainId(chain: ChainId | ChainName): ChainId { + // this is written in a way that for invalid inputs (coming from vanilla + // javascript or someone doing type casting) it will always return undefined. + return typeof chain === "number" && isChain(chain) ? chain : toChainId(chain); +} + +export function coalesceChainName(chain: ChainId | ChainName): ChainName { + // this is written in a way that for invalid inputs (coming from vanilla + // javascript or someone doing type casting) it will always return undefined. + return toChainName(coalesceChainId(chain)); +} + +/** + * + * Returns true when called with an [[EVMChainId]] or [[EVMChainName]], and false otherwise. + * Importantly, after running this check, the chain's type will be narrowed to + * either the EVM subset, or the non-EVM subset thanks to the type predicate in + * the return type. + */ +export function isEVMChain( + chain: ChainId | ChainName +): chain is EVMChainId | EVMChainName { + let chainId = coalesceChainId(chain); + if ( + chainId === CHAIN_ID_ETH || + chainId === CHAIN_ID_BSC || + chainId === CHAIN_ID_AVAX || + chainId === CHAIN_ID_POLYGON || + chainId === CHAIN_ID_OASIS || + chainId === CHAIN_ID_AURORA || + chainId === CHAIN_ID_FANTOM || + chainId === CHAIN_ID_KARURA || + chainId === CHAIN_ID_ACALA || + chainId === CHAIN_ID_KLAYTN || + chainId === CHAIN_ID_CELO || + chainId === CHAIN_ID_ETHEREUM_ROPSTEN + ) { + return isEVM(chainId); + } else { + return notEVM(chainId); + } +} + +/** + * + * Asserts that the given chain id or chain name is an EVM chain, and throws otherwise. + * After calling this function, the type of chain will be narrowed to + * [[EVMChainId]] or [[EVMChainName]] thanks to the type assertion in the return type. + * + */ +export function assertEVMChain( + chain: ChainId | ChainName +): asserts chain is EVMChainId | EVMChainName { + if (!isEVMChain(chain)) { + throw Error(`Expected an EVM chain, but ${chain} is not`); + } +} export const WSOL_ADDRESS = "So11111111111111111111111111111111111111112"; export const WSOL_DECIMALS = 9; @@ -22,3 +503,56 @@ export const MAX_VAA_DECIMALS = 8; export const TERRA_REDEEMED_CHECK_WALLET_ADDRESS = "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v"; + +//////////////////////////////////////////////////////////////////////////////// +// Utilities + +/** + * The [[isEVM]] and [[notEVM]] functions improve type-safety in [[isEVMChain]]. + * + * As it turns out, typescript type predicates are unsound on their own, + * allowing us to write something like this: + * + * ```typescript + * function unsafeCoerce(n: number): n is 1 { + * return true + * } + * ``` + * + * which is completely bogus. This happens presumably because the typescript + * authors think of the type predicate mechanism as an escape hatch mechanism. + * We want a more principled function though, that keeps us honest. + * + * in [[isEVMChain]], checking that disjunctive boolean expression actually + * refines the type of chainId in both branches. In the "true" branch, + * the type of chainId is narrowed to exactly the EVM chains, so calling + * [[isEVM]] on it will typecheck, and similarly the "false" branch for the negation. + * However, if we extend the [[EVMChainId]] type with a new EVM chain, this + * function will no longer compile until the condition is extended. + */ + +/** + * + * Returns true when called with an [[EVMChainId]] or [[EVMChainName]], and fails to compile + * otherwise + */ +function isEVM(_: EVMChainId | EVMChainName): true { + return true; +} + +/** + * + * Returns false when called with a non-[[EVMChainId]] and non-[[EVMChainName]] + * argument, and fails to compile otherwise + */ +function notEVM(_: T extends EVMChainId | EVMChainName ? never : T): false { + return false; +} + +// This just serves as a type assertion to ensure that [[EVMChainName]] is a +// subset of [[ChainName]], since typescript provides no built-in way to express +// this. +function evm_chain_subset(e: EVMChainName): ChainName { + // will fail to compile if 'e' can't be typed as a [[ChainName]] + return e; +} diff --git a/sdk/js/src/utils/parseVaa.ts b/sdk/js/src/utils/parseVaa.ts index c4b3b627b..a172a15f4 100644 --- a/sdk/js/src/utils/parseVaa.ts +++ b/sdk/js/src/utils/parseVaa.ts @@ -3,6 +3,8 @@ import { ChainId } from "./consts"; export const METADATA_REPLACE = new RegExp("\u0000", "g"); +// TODO: remove `as ChainId` in next minor version as we can't ensure it will match our type definition + // note: actual first byte is message type // 0 [u8; 32] token_address // 32 u16 token_chain