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 <ckiss@jumptrading.com>
Co-authored-by: Evan Gray <battledingo@gmail.com>
This commit is contained in:
Csongor Kiss 2022-05-05 18:35:11 +02:00 committed by GitHub
parent c13bdff820
commit 7f427133cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 923 additions and 211 deletions

View File

@ -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

View File

@ -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",

View File

@ -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 {

View File

@ -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<Fe
const feeEstimate = await lcd.tx.estimateFee(
[
{
sequenceNumber: await terraWallet.sequence(),
publicKey: terraWallet.key.publicKey
sequenceNumber: await terraWallet.sequence(),
publicKey: terraWallet.key.publicKey
}
],
{
@ -514,9 +508,8 @@ async function _transferFromEth(erc721: string, token_id: BigNumberish, address:
erc721,
token_id,
chain,
hexToUint8Array(
nativeToHexString(address, chain) || ""
));
tryNativeToUint8Array(address, chain)
);
}
async function _transferFromTerra(terra_addr: string, token_id: string, address: string, chain: ChainId): Promise<BlockTxBroadcastResult> {
@ -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

View File

@ -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<string | null> {
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<string | null> {
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<string> {
const originChainId = coalesceChainId(originChain);
const { wrapped_address } = await importNftWasm();
const wrappedAddress = wrapped_address(
tokenBridgeAddress,
originAsset,
originChain,
originChainId,
tokenId
);
const wrappedAddressPK = new PublicKey(wrappedAddress);

View File

@ -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<WormholeWrappedNFTInfo> {
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,
};

View File

@ -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<string> } = {}
): Promise<ethers.ContractReceipt> {
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<Transaction> {
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<MsgExecuteContract[]> {
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,
},

View File

@ -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,
},

View File

@ -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

View File

@ -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);

View File

@ -25,7 +25,7 @@ export async function attestFromEth(
signer: ethers.Signer,
tokenAddress: string,
overrides: PayableOverrides & { from?: string | Promise<string> } = {}
) {
): Promise<ethers.ContractReceipt> {
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<MsgExecuteContract> {
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<Transaction> {
const nonce = createNonce().readUInt32LE(0);
const transferIx = await getBridgeFeeIx(
connection,

View File

@ -13,7 +13,7 @@ export async function createWrappedOnEth(
signer: ethers.Signer,
signedVAA: Uint8Array,
overrides: Overrides & { from?: string | Promise<string> } = {}
) {
): Promise<ethers.ContractReceipt> {
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<MsgExecuteContract> {
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<Transaction> {
const { create_wrapped_ix } = await importTokenWasm();
const ix = ixFromRust(
create_wrapped_ix(

View File

@ -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<string | null> {
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<string | null> {
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<string | null> {
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<bigint | null> {
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) {

View File

@ -20,7 +20,7 @@ export async function getIsTransferCompletedEth(
tokenBridgeAddress: string,
provider: ethers.Signer | ethers.providers.Provider,
signedVAA: Uint8Array
) {
): Promise<boolean> {
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<boolean> {
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<boolean> {
const { claim_address } = await importCoreWasm();
const claimAddress = await claim_address(tokenBridgeAddress, signedVAA);
const claimInfo = await connection.getAccountInfo(

View File

@ -17,17 +17,18 @@ export async function getIsWrappedAssetEth(
tokenBridgeAddress: string,
provider: ethers.Signer | ethers.providers.Provider,
assetAddress: string
) {
): Promise<boolean> {
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<boolean> {
return false;
}
@ -42,7 +43,7 @@ export async function getIsWrappedAssetSol(
connection: Connection,
tokenBridgeAddress: string,
mintAddress: string
) {
): Promise<boolean> {
if (!mintAddress) return false;
const { wrapped_meta_address } = await importTokenWasm();
const wrappedMetaAddress = wrapped_meta_address(

View File

@ -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<WormholeWrappedInfo> {
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),
};
}

View File

@ -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<string> } = {}
) {
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<string> } = {}
) {
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<TransactionSignerPair[]> {
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({

View File

@ -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);

View File

@ -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>(_: 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;
}

View File

@ -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