diff --git a/clients/js/package-lock.json b/clients/js/package-lock.json index 464863596..4669b16f1 100644 --- a/clients/js/package-lock.json +++ b/clients/js/package-lock.json @@ -18,6 +18,7 @@ "@injectivelabs/utils": "^1.10.5", "@mysten/sui.js": "^0.32.2", "@sei-js/core": "^1.3.2", + "@solana/spl-token": "^0.3.5", "@solana/web3.js": "^1.22.0", "@terra-money/terra.js": "^3.1.3", "@types/config": "^3.3.0", @@ -3223,9 +3224,9 @@ } }, "node_modules/@solana/spl-token": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.7.tgz", - "integrity": "sha512-bKGxWTtIw6VDdCBngjtsGlKGLSmiu/8ghSt/IOYJV24BsymRbgq7r12GToeetpxmPaZYLddKwAz7+EwprLfkfg==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.5.tgz", + "integrity": "sha512-0bGC6n415lGjKu02gkLOIpP1wzndSP0SHwN9PefJ+wKAhmfU1rl3AV1Pa41uap2kzSCD6F9642ngNO8KXPvh/g==", "dependencies": { "@solana/buffer-layout": "^4.0.0", "@solana/buffer-layout-utils": "^0.2.0", @@ -10594,9 +10595,9 @@ } }, "@solana/spl-token": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.7.tgz", - "integrity": "sha512-bKGxWTtIw6VDdCBngjtsGlKGLSmiu/8ghSt/IOYJV24BsymRbgq7r12GToeetpxmPaZYLddKwAz7+EwprLfkfg==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.5.tgz", + "integrity": "sha512-0bGC6n415lGjKu02gkLOIpP1wzndSP0SHwN9PefJ+wKAhmfU1rl3AV1Pa41uap2kzSCD6F9642ngNO8KXPvh/g==", "requires": { "@solana/buffer-layout": "^4.0.0", "@solana/buffer-layout-utils": "^0.2.0", diff --git a/clients/js/package.json b/clients/js/package.json index c9c9ecd88..8f67b0785 100644 --- a/clients/js/package.json +++ b/clients/js/package.json @@ -38,6 +38,7 @@ "@injectivelabs/utils": "^1.10.5", "@mysten/sui.js": "^0.32.2", "@sei-js/core": "^1.3.2", + "@solana/spl-token": "^0.3.5", "@solana/web3.js": "^1.22.0", "@terra-money/terra.js": "^3.1.3", "@types/config": "^3.3.0", diff --git a/clients/js/src/algorand.ts b/clients/js/src/algorand.ts index 51b3ae236..7cfb51b8d 100644 --- a/clients/js/src/algorand.ts +++ b/clients/js/src/algorand.ts @@ -2,11 +2,16 @@ import { _submitVAAAlgorand, signSendAndConfirmAlgorand, } from "@certusone/wormhole-sdk/lib/esm/algorand"; -import { CONTRACTS } from "@certusone/wormhole-sdk/lib/esm/utils/consts"; +import { + CONTRACTS, + ChainName, +} from "@certusone/wormhole-sdk/lib/esm/utils/consts"; import { Account, Algodv2, mnemonicToSecretKey } from "algosdk"; import { NETWORKS } from "./consts"; import { Network } from "./utils"; import { Payload, impossible } from "./vaa"; +import { transferFromAlgorand } from "@certusone/wormhole-sdk/lib/esm/token_bridge/transfer"; +import { tryNativeToHexString } from "@certusone/wormhole-sdk/lib/esm/utils"; export async function execute_algorand( payload: Payload, @@ -25,16 +30,6 @@ export async function execute_algorand( const contracts = CONTRACTS[network][chainName]; console.log("contracts", contracts); - const ALGORAND_HOST = { - algodToken: "", - algodServer: rpc, - algodPort: "", - }; - if (network === "DEVNET") { - ALGORAND_HOST.algodToken = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - ALGORAND_HOST.algodPort = "4001"; - } let target_contract: string; switch (payload.module) { @@ -127,11 +122,7 @@ export async function execute_algorand( const target = BigInt(parseInt(target_contract)); const CORE_ID = BigInt(parseInt(contracts.core)); - const algodClient = new Algodv2( - ALGORAND_HOST.algodToken, - ALGORAND_HOST.algodServer, - ALGORAND_HOST.algodPort - ); + const algodClient = getClient(network, rpc); const algoWallet: Account = mnemonicToSecretKey(key); // Create transaction @@ -147,3 +138,59 @@ export async function execute_algorand( const result = await signSendAndConfirmAlgorand(algodClient, txs, algoWallet); console.log("Confirmed in round:", result["confirmed-round"]); } + +export async function transferAlgorand( + dstChain: ChainName, + dstAddress: string, + tokenAddress: string, + amount: string, + network: Network, + rpc: string +) { + const { key } = NETWORKS[network].algorand; + if (!key) { + throw Error(`No ${network} key defined for Algorand`); + } + const contracts = CONTRACTS[network].algorand; + const client = getClient(network, rpc); + const wallet: Account = mnemonicToSecretKey(key); + const CORE_ID = BigInt(parseInt(contracts.core)); + const TOKEN_BRIDGE_ID = BigInt(parseInt(contracts.token_bridge)); + const recipient = tryNativeToHexString(dstAddress, dstChain); + if (!recipient) { + throw new Error("Failed to convert recipient address"); + } + const assetId = tokenAddress === "native" ? BigInt(0) : BigInt(tokenAddress); + const txs = await transferFromAlgorand( + client, + TOKEN_BRIDGE_ID, + CORE_ID, + wallet.addr, + assetId, + BigInt(amount), + recipient, + dstChain, + BigInt(0) + ); + const result = await signSendAndConfirmAlgorand(client, txs, wallet); + console.log("Confirmed in round:", result["confirmed-round"]); +} + +function getClient(network: Network, rpc: string) { + const ALGORAND_HOST = { + algodToken: "", + algodServer: rpc, + algodPort: "", + }; + if (network === "DEVNET") { + ALGORAND_HOST.algodToken = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + ALGORAND_HOST.algodPort = "4001"; + } + const client = new Algodv2( + ALGORAND_HOST.algodToken, + ALGORAND_HOST.algodServer, + ALGORAND_HOST.algodPort + ); + return client; +} diff --git a/clients/js/src/aptos.ts b/clients/js/src/aptos.ts index cc808e9c2..61b04d791 100644 --- a/clients/js/src/aptos.ts +++ b/clients/js/src/aptos.ts @@ -1,9 +1,11 @@ import { CONTRACTS, ChainId, + ChainName, assertChain, } from "@certusone/wormhole-sdk/lib/esm/utils/consts"; -import { AptosAccount, AptosClient, BCS, TxnBuilderTypes } from "aptos"; +import { transferFromAptos } from "@certusone/wormhole-sdk/lib/esm/token_bridge/transfer"; +import { AptosAccount, AptosClient, BCS, TxnBuilderTypes, Types } from "aptos"; import { ethers } from "ethers"; import { sha3_256 } from "js-sha3"; import { NETWORKS } from "./consts"; @@ -11,6 +13,10 @@ import { Network } from "./utils"; import { Payload, impossible } from "./vaa"; import { CHAINS, ensureHexPrefix } from "@certusone/wormhole-sdk"; import { TokenBridgeState } from "@certusone/wormhole-sdk/lib/esm/aptos/types"; +import { + generateSignAndSubmitEntryFunction, + tryNativeToUint8Array, +} from "@certusone/wormhole-sdk/lib/esm/utils"; export async function execute_aptos( payload: Payload, @@ -236,6 +242,44 @@ export async function execute_aptos( } } +export async function transferAptos( + dstChain: ChainName, + dstAddress: string, + tokenAddress: string, + amount: string, + network: Network, + rpc: string +) { + const { key } = NETWORKS[network].aptos; + if (!key) { + throw new Error("No key for aptos"); + } + rpc = rpc ?? NETWORKS[network].aptos.rpc; + if (!rpc) { + throw new Error("No rpc for aptos"); + } + const { token_bridge } = CONTRACTS[network].aptos; + if (!token_bridge) { + throw new Error("token bridge contract is undefined"); + } + const account = new AptosAccount(new Uint8Array(Buffer.from(key, "hex"))); + const client = new AptosClient(rpc); + const transferPayload = transferFromAptos( + token_bridge, + tokenAddress === "native" ? "0x1::aptos_coin::AptosCoin" : tokenAddress, + amount, + dstChain, + tryNativeToUint8Array(dstAddress, dstChain) + ); + const tx = (await generateSignAndSubmitEntryFunction( + client, + account, + transferPayload + )) as Types.UserTransaction; + await client.waitForTransaction(tx.hash); + console.log(`hash: ${tx.hash}`); +} + export function deriveWrappedAssetAddress( token_bridge_address: Uint8Array, // 32 bytes origin_chain: ChainId, diff --git a/clients/js/src/chains/sui/submit.ts b/clients/js/src/chains/sui/submit.ts index 51acc66c2..017a33934 100644 --- a/clients/js/src/chains/sui/submit.ts +++ b/clients/js/src/chains/sui/submit.ts @@ -129,7 +129,7 @@ export const submit = async ( console.log(` Type ${getWrappedCoinType(coinPackageId)}`); if (!rpc && network !== "DEVNET") { - // Wait for wrapped asset creation to be propogated to other + // Wait for wrapped asset creation to be propagated to other // nodes in case this complete registration call is load balanced // to another node. await sleep(5000); diff --git a/clients/js/src/chains/sui/transfer.ts b/clients/js/src/chains/sui/transfer.ts new file mode 100644 index 000000000..848aafc9b --- /dev/null +++ b/clients/js/src/chains/sui/transfer.ts @@ -0,0 +1,53 @@ +import { transferFromSui } from "@certusone/wormhole-sdk/lib/esm/token_bridge/transfer"; +import { + executeTransactionBlock, + getProvider, + getSigner, + setMaxGasBudgetDevnet, +} from "./utils"; +import { + CONTRACTS, + ChainName, + Network, + tryNativeToUint8Array, +} from "@certusone/wormhole-sdk/lib/esm/utils"; + +export async function transferSui( + dstChain: ChainName, + dstAddress: string, + tokenAddress: string, + amount: string, + network: Network, + rpc: string +) { + const { core, token_bridge } = CONTRACTS[network]["sui"]; + if (!core) { + throw Error("Core bridge object ID is undefined"); + } + if (!token_bridge) { + throw new Error("Token bridge object ID is undefined"); + } + const provider = getProvider(network, rpc); + const signer = getSigner(provider, network); + const owner = await signer.getAddress(); + const coinType = tokenAddress === "native" ? "0x2::sui::SUI" : tokenAddress; + const coins = ( + await provider.getCoins({ + owner, + coinType, + }) + ).data; + const tx = await transferFromSui( + provider, + core, + token_bridge, + coins, + coinType, + BigInt(amount), + dstChain, + tryNativeToUint8Array(dstAddress, dstChain) + ); + setMaxGasBudgetDevnet(network, tx); + const result = await executeTransactionBlock(signer, tx); + console.log(JSON.stringify(result)); +} diff --git a/clients/js/src/cmds/transfer.ts b/clients/js/src/cmds/transfer.ts new file mode 100644 index 000000000..f12b30d24 --- /dev/null +++ b/clients/js/src/cmds/transfer.ts @@ -0,0 +1,146 @@ +import { + isCosmWasmChain, + isEVMChain, + isTerraChain, +} from "@certusone/wormhole-sdk/lib/esm/utils/consts"; +import yargs from "yargs"; +import { impossible } from "../vaa"; +import { transferEVM } from "../evm"; +import { CHAIN_NAME_CHOICES, NETWORK_OPTIONS, NETWORKS } from "../consts"; +import { assertNetwork } from "../utils"; +import { transferTerra } from "../terra"; +import { transferInjective } from "../injective"; +import { transferXpla } from "../xpla"; +import { transferSolana } from "../solana"; +import { transferAlgorand } from "../algorand"; +import { transferNear } from "../near"; +import { transferSui } from "../chains/sui/transfer"; +import { transferAptos } from "../aptos"; + +export const command = "transfer"; +export const desc = "Transfer a token"; +export const builder = (y: typeof yargs) => + y + .option("src-chain", { + describe: "source chain", + choices: CHAIN_NAME_CHOICES, + demandOption: true, + }) + .option("dst-chain", { + describe: "destination chain", + choices: CHAIN_NAME_CHOICES, + demandOption: true, + }) + .option("dst-addr", { + describe: "destination address", + type: "string", + demandOption: true, + }) + .option("token-addr", { + describe: "token address", + type: "string", + default: "native", + defaultDescription: "native token", + demandOption: false, + }) + .option("amount", { + describe: "token amount", + type: "string", + demandOption: true, + }) + .option("network", NETWORK_OPTIONS) + .option("rpc", { + describe: "RPC endpoint", + type: "string", + demandOption: false, + }); + +export const handler = async ( + argv: Awaited["argv"]> +) => { + const srcChain = argv["src-chain"]; + const dstChain = argv["dst-chain"]; + if (srcChain === "unset") { + throw new Error("source chain is unset"); + } + if (dstChain === "unset") { + throw new Error("destination chain is unset"); + } + // TODO: support transfers to sei + if (dstChain === "sei") { + throw new Error("transfer to sei currently unsupported"); + } + if (srcChain === dstChain) { + throw new Error("source and destination chains can't be the same"); + } + const amount = argv.amount; + if (BigInt(amount) <= 0) { + throw new Error("amount must be greater than 0"); + } + const tokenAddr = argv["token-addr"]; + if (tokenAddr === "native" && isCosmWasmChain(srcChain)) { + throw new Error(`token-addr must be specified for ${srcChain}`); + } + const dstAddr = argv["dst-addr"]; + const network = argv.network.toUpperCase(); + assertNetwork(network); + const rpc = argv.rpc ?? NETWORKS[network][srcChain].rpc; + if (!rpc) { + throw new Error(`No ${network} rpc defined for ${srcChain}`); + } + if (isEVMChain(srcChain)) { + await transferEVM( + srcChain, + dstChain, + dstAddr, + tokenAddr, + amount, + network, + rpc + ); + } else if (isTerraChain(srcChain)) { + await transferTerra( + srcChain, + dstChain, + dstAddr, + tokenAddr, + amount, + network, + rpc + ); + } else if (srcChain === "solana" || srcChain === "pythnet") { + await transferSolana( + srcChain, + dstChain, + dstAddr, + tokenAddr, + amount, + network, + rpc + ); + } else if (srcChain === "algorand") { + await transferAlgorand(dstChain, dstAddr, tokenAddr, amount, network, rpc); + } else if (srcChain === "near") { + await transferNear(dstChain, dstAddr, tokenAddr, amount, network, rpc); + } else if (srcChain === "injective") { + await transferInjective(dstChain, dstAddr, tokenAddr, amount, network, rpc); + } else if (srcChain === "xpla") { + await transferXpla(dstChain, dstAddr, tokenAddr, amount, network, rpc); + } else if (srcChain === "sei") { + throw new Error("sei is not supported yet"); + } else if (srcChain === "osmosis") { + throw Error("OSMOSIS is not supported yet"); + } else if (srcChain === "sui") { + await transferSui(dstChain, dstAddr, tokenAddr, amount, network, rpc); + } else if (srcChain === "aptos") { + await transferAptos(dstChain, dstAddr, tokenAddr, amount, network, rpc); + } else if (srcChain === "wormchain") { + throw Error("Wormchain is not supported yet"); + } else if (srcChain === "btc") { + throw Error("btc is not supported yet"); + } else { + // If you get a type error here, hover over `chain`'s type and it tells you + // which cases are not handled + impossible(srcChain); + } +}; diff --git a/clients/js/src/consts/yargs.ts b/clients/js/src/consts/yargs.ts index 880209121..8de88081d 100644 --- a/clients/js/src/consts/yargs.ts +++ b/clients/js/src/consts/yargs.ts @@ -42,3 +42,7 @@ export const CHAIN_ID_OR_NAME_CHOICES = [ ...Object.keys(CHAINS), ...Object.values(CHAINS), ] as (ChainName | ChainId)[]; + +export const CHAIN_NAME_CHOICES = Object.keys(CHAINS).filter( + (c) => c !== "unset" +) as ChainName[]; diff --git a/clients/js/src/evm.ts b/clients/js/src/evm.ts index 5fe8e06e3..bae4f175d 100644 --- a/clients/js/src/evm.ts +++ b/clients/js/src/evm.ts @@ -3,16 +3,16 @@ import { BridgeImplementation__factory, Implementation__factory, NFTBridgeImplementation__factory, - WormholeRelayer__factory + WormholeRelayer__factory, } from "@certusone/wormhole-sdk/lib/esm/ethers-contracts"; -import { - getWormholeRelayerAddress -} from "@certusone/wormhole-sdk/lib/esm/relayer" +import { getWormholeRelayerAddress } from "@certusone/wormhole-sdk/lib/esm/relayer"; import { CHAINS, CONTRACTS, + ChainName, Contracts, EVMChainName, + toChainId, } from "@certusone/wormhole-sdk/lib/esm/utils/consts"; import axios from "axios"; import { ethers } from "ethers"; @@ -20,6 +20,13 @@ import { solidityKeccak256 } from "ethers/lib/utils"; import { NETWORKS } from "./consts"; import { Network } from "./utils"; import { Encoding, Payload, encode, impossible, typeWidth } from "./vaa"; +import { + approveEth, + getAllowanceEth, + transferFromEth, + transferFromEthNative, +} from "@certusone/wormhole-sdk/lib/esm/token_bridge/transfer"; +import { tryNativeToUint8Array } from "@certusone/wormhole-sdk/lib/esm/utils"; const _IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; @@ -264,6 +271,39 @@ export async function getImplementation( )[0]; } +async function getSigner(chain: EVMChainName, key: string, rpc: string) { + let provider: ethers.providers.JsonRpcProvider; + let signer: ethers.Wallet; + if (chain === "celo") { + provider = new celo.CeloProvider(rpc); + await provider.ready; + signer = new celo.CeloWallet(key, provider); + } else { + provider = new ethers.providers.JsonRpcProvider(rpc); + signer = new ethers.Wallet(key, provider); + } + // Here we apply a set of chain-specific overrides. + // NOTE: some of these might have only been tested on mainnet. If it fails in + // testnet (or devnet), they might require additional guards + let overrides: ethers.Overrides = {}; + if (chain === "karura" || chain == "acala") { + overrides = await getKaruraGasParams(rpc); + } else if (chain === "polygon") { + const feeData = await provider.getFeeData(); + overrides = { + maxFeePerGas: feeData.maxFeePerGas?.mul(50) || undefined, + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas?.mul(50) || undefined, + }; + } else if (chain === "klaytn" || chain === "fantom") { + overrides = { gasPrice: (await signer.getGasPrice()).toString() }; + } + return { + signer, + provider, + overrides, + }; +} + export async function execute_evm( payload: Payload, vaa: Buffer, @@ -284,32 +324,7 @@ export async function execute_evm( const key: string = n.key; const contracts: Contracts = CONTRACTS[network][chain]; - let provider: ethers.providers.JsonRpcProvider; - let signer: ethers.Wallet; - if (chain === "celo") { - provider = new celo.CeloProvider(rpc); - await provider.ready; - signer = new celo.CeloWallet(key, provider); - } else { - provider = new ethers.providers.JsonRpcProvider(rpc); - signer = new ethers.Wallet(key, provider); - } - - // Here we apply a set of chain-specific overrides. - // NOTE: some of these might have only been tested on mainnet. If it fails in - // testnet (or devnet), they might require additional guards - let overrides: ethers.Overrides = {}; - if (chain === "karura" || chain == "acala") { - overrides = await getKaruraGasParams(rpc); - } else if (chain === "polygon") { - const feeData = await provider.getFeeData(); - overrides = { - maxFeePerGas: feeData.maxFeePerGas?.mul(50) || undefined, - maxPriorityFeePerGas: feeData.maxPriorityFeePerGas?.mul(50) || undefined, - }; - } else if (chain === "klaytn" || chain === "fantom") { - overrides = { gasPrice: (await signer.getGasPrice()).toString() }; - } + const { signer, overrides } = await getSigner(chain, key, rpc); switch (payload.module) { case "Core": { @@ -434,38 +449,95 @@ export async function execute_evm( break; } case "WormholeRelayer": - contract_address = contract_address + contract_address = contract_address ? contract_address : getWormholeRelayerAddress(chain, network); - if (contract_address === undefined) { - throw Error(`Unknown Wormhole Relayer contract on ${network} for ${chain}`) - } - let rb = WormholeRelayer__factory.connect(contract_address, signer) - switch (payload.type) { - case "ContractUpgrade": - console.log("Upgrading contract") - console.log("Hash: " + (await rb.submitContractUpgrade(vaa, overrides)).hash) - console.log("Don't forget to verify the new implementation! See ethereum/VERIFY.md for instructions") - break - case "RegisterChain": - console.log("Registering chain") - console.log("Hash: " + (await rb.registerWormholeRelayerContract(vaa, overrides)).hash) - break - case "SetDefaultDeliveryProvider": - console.log("Setting default relay provider") - console.log("Hash: " + (await rb.setDefaultDeliveryProvider(vaa, overrides)).hash) - break - default: - impossible(payload) - break - - } - break + if (contract_address === undefined) { + throw Error( + `Unknown Wormhole Relayer contract on ${network} for ${chain}` + ); + } + let rb = WormholeRelayer__factory.connect(contract_address, signer); + switch (payload.type) { + case "ContractUpgrade": + console.log("Upgrading contract"); + console.log( + "Hash: " + (await rb.submitContractUpgrade(vaa, overrides)).hash + ); + console.log( + "Don't forget to verify the new implementation! See ethereum/VERIFY.md for instructions" + ); + break; + case "RegisterChain": + console.log("Registering chain"); + console.log( + "Hash: " + + (await rb.registerWormholeRelayerContract(vaa, overrides)).hash + ); + break; + case "SetDefaultDeliveryProvider": + console.log("Setting default relay provider"); + console.log( + "Hash: " + + (await rb.setDefaultDeliveryProvider(vaa, overrides)).hash + ); + break; + default: + impossible(payload); + break; + } + break; default: impossible(payload); } } +export async function transferEVM( + srcChain: EVMChainName, + dstChain: ChainName, + dstAddress: string, + tokenAddress: string, + amount: string, + network: Network, + rpc: string +) { + const n = NETWORKS[network][srcChain]; + if (!n.key) { + throw Error(`No ${network} key defined for ${srcChain} (see networks.ts)`); + } + const { token_bridge } = CONTRACTS[network][srcChain]; + if (!token_bridge) { + throw Error(`Unknown token bridge contract on ${network} for ${srcChain}`); + } + const { signer, overrides } = await getSigner(srcChain, n.key, rpc); + let tx; + if (tokenAddress === "native") { + tx = await transferFromEthNative( + token_bridge, + signer, + amount, + toChainId(dstChain), + tryNativeToUint8Array(dstAddress, dstChain) + ); + } else { + const allowance = await getAllowanceEth(token_bridge, tokenAddress, signer); + if (allowance.lt(amount)) { + await approveEth(token_bridge, tokenAddress, signer, amount, overrides); + } + tx = await transferFromEth( + token_bridge, + signer, + tokenAddress, + amount, + dstChain, + tryNativeToUint8Array(dstAddress, dstChain), + undefined, + overrides + ); + } + console.log(`Hash: ${tx.transactionHash}`); +} + /** * * Hijack a core contract. This function is useful when working with a mainnet diff --git a/clients/js/src/injective.ts b/clients/js/src/injective.ts index f8f051272..89623c107 100644 --- a/clients/js/src/injective.ts +++ b/clients/js/src/injective.ts @@ -1,6 +1,7 @@ import { CHAINS, CONTRACTS, + ChainName, } from "@certusone/wormhole-sdk/lib/esm/utils/consts"; import { getNetworkInfo, @@ -11,6 +12,7 @@ import { ChainRestAuthApi, createTransaction, MsgExecuteContractCompat, + Msgs, PrivateKey, TxGrpcApi, } from "@injectivelabs/sdk-ts"; @@ -19,6 +21,8 @@ import { fromUint8Array } from "js-base64"; import { NETWORKS } from "./consts"; import { Network } from "./utils"; import { impossible, Payload } from "./vaa"; +import { transferFromInjective } from "@certusone/wormhole-sdk/lib/esm/token_bridge/injective"; +import { tryNativeToUint8Array } from "@certusone/wormhole-sdk/lib/esm/utils"; export async function execute_injective( payload: Payload, @@ -157,11 +161,61 @@ export async function execute_injective( }); console.log("transaction:", transaction); + await signAndSendTx(walletPK, network, transaction); +} + +export async function transferInjective( + dstChain: ChainName, + dstAddress: string, + tokenAddress: string, + amount: string, + network: Network, + rpc: string +) { + if (network === "DEVNET") { + throw new Error("Injective is not supported in DEVNET"); + } + const chain = "injective"; + const { key } = NETWORKS[network][chain]; + if (!key) { + throw Error(`No ${network} key defined for Injective`); + } + const { token_bridge } = CONTRACTS[network][chain]; + if (token_bridge == undefined) { + throw Error(`Unknown token bridge contract on ${network} for ${chain}`); + } + + const walletPK = PrivateKey.fromMnemonic(key); + const walletInjAddr = walletPK.toBech32(); + + const msgs = await transferFromInjective( + walletInjAddr, + token_bridge, + tokenAddress, + amount, + dstChain, + tryNativeToUint8Array(dstAddress, dstChain) + ); + + await signAndSendTx(walletPK, network, msgs); +} + +async function signAndSendTx( + walletPK: PrivateKey, + network: string, + msgs: Msgs | Msgs[] +) { + const endPoint = + network === "MAINNET" + ? InjectiveNetwork.MainnetK8s + : InjectiveNetwork.TestnetK8s; + const networkInfo = getNetworkInfo(endPoint); + const walletPublicKey = walletPK.toPublicKey().toBase64(); const accountDetails = await new ChainRestAuthApi( networkInfo.rest - ).fetchAccount(walletInjAddr); + ).fetchAccount(walletPK.toBech32()); const { signBytes, txRaw } = createTransaction({ - message: transaction, + message: msgs, memo: "", fee: getStdFee((parseInt(DEFAULT_STD_FEE.gas, 10) * 2.5).toString()), pubKey: walletPublicKey, diff --git a/clients/js/src/main.ts b/clients/js/src/main.ts index a1b99e593..082c37d4b 100644 --- a/clients/js/src/main.ts +++ b/clients/js/src/main.ts @@ -15,6 +15,7 @@ import * as parse from "./cmds/parse"; import * as recover from "./cmds/recover"; import * as submit from "./cmds/submit"; import * as sui from "./cmds/sui"; +import * as transfer from "./cmds/transfer"; import * as verifyVaa from "./cmds/verifyVaa"; yargs(hideBin(process.argv)) @@ -30,6 +31,7 @@ yargs(hideBin(process.argv)) .command(recover) .command(submit) .command(sui) + .command(transfer) .command(verifyVaa) .strict() .demandCommand().argv; diff --git a/clients/js/src/near.ts b/clients/js/src/near.ts index ecb14b6d3..ecca8768c 100644 --- a/clients/js/src/near.ts +++ b/clients/js/src/near.ts @@ -1,10 +1,18 @@ -import { CONTRACTS } from "@certusone/wormhole-sdk/lib/esm/utils/consts"; +import { + ChainName, + CONTRACTS, +} from "@certusone/wormhole-sdk/lib/esm/utils/consts"; import BN from "bn.js"; import { Account, connect, KeyPair } from "near-api-js"; import { InMemoryKeyStore } from "near-api-js/lib/key_stores"; import { NETWORKS } from "./consts"; import { Network } from "./utils"; import { impossible, Payload } from "./vaa"; +import { + transferNearFromNear, + transferTokenFromNear, +} from "@certusone/wormhole-sdk/lib/esm/token_bridge/transfer"; +import { tryNativeToUint8Array } from "@certusone/wormhole-sdk/lib/esm/utils"; export const execute_near = async ( payload: Payload, @@ -142,3 +150,62 @@ export const execute_near = async ( const txHash = result1.transaction.hash + ":" + result2.transaction.hash; console.log("Hash: " + txHash); }; + +export async function transferNear( + dstChain: ChainName, + dstAddress: string, + tokenAddress: string, + amount: string, + network: Network, + rpc: string +) { + const { key, networkId, deployerAccount } = NETWORKS[network].near; + if (!key) { + throw Error(`No ${network} key defined for NEAR`); + } + const { core, token_bridge } = CONTRACTS[network].near; + if (core === undefined) { + throw Error(`Unknown core contract on ${network} for NEAR`); + } + if (token_bridge === undefined) { + throw Error(`Unknown token bridge contract on ${network} for NEAR`); + } + const keyStore = new InMemoryKeyStore(); + keyStore.setKey(networkId, deployerAccount, KeyPair.fromString(key)); + const near = await connect({ + keyStore, + networkId, + nodeUrl: rpc, + headers: {}, + }); + const nearAccount = new Account(near.connection, deployerAccount); + if (tokenAddress === "native") { + const msg = await transferNearFromNear( + near.connection.provider, + core, + token_bridge, + BigInt(amount), + tryNativeToUint8Array(dstAddress, dstChain), + dstChain, + BigInt(0) + ); + const result = await nearAccount.functionCall(msg); + console.log(result.transaction.hash); + } else { + const msgs = await transferTokenFromNear( + near.connection.provider, + nearAccount.accountId, + core, + token_bridge, + tokenAddress, + BigInt(amount), + tryNativeToUint8Array(dstAddress, dstChain), + dstChain, + BigInt(0) + ); + for (const msg of msgs) { + const result = await nearAccount.functionCall(msg); + console.log(result.transaction.hash); + } + } +} diff --git a/clients/js/src/solana.ts b/clients/js/src/solana.ts index b5f724c9c..1e8dfaf06 100644 --- a/clients/js/src/solana.ts +++ b/clients/js/src/solana.ts @@ -19,15 +19,25 @@ import { import { CHAINS, CONTRACTS, + ChainName, + Network, SolanaChainName, } from "@certusone/wormhole-sdk/lib/esm/utils/consts"; import * as web3s from "@solana/web3.js"; import base58 from "bs58"; import { NETWORKS } from "./consts"; import { Payload, VAA, impossible } from "./vaa"; -import { ChainName, hexToUint8Array } from "@certusone/wormhole-sdk"; import { getEmitterAddress } from "./emitter"; -import { Network } from "./utils"; +import { + transferFromSolana, + transferNativeSol, +} from "@certusone/wormhole-sdk/lib/esm/token_bridge/transfer"; +import { + hexToUint8Array, + tryNativeToUint8Array, +} from "@certusone/wormhole-sdk/lib/esm/utils"; +import { PublicKey } from "@solana/web3.js"; +import { getAssociatedTokenAddress } from "@solana/spl-token"; export async function execute_solana( v: VAA, @@ -217,6 +227,84 @@ export async function execute_solana( console.log("SIGNATURE", signature); } +export async function transferSolana( + srcChain: SolanaChainName, + dstChain: ChainName, + dstAddress: string, + tokenAddress: string, + amount: string, + network: Network, + rpc: string +) { + const { key } = NETWORKS[network][srcChain]; + if (!key) { + throw Error(`No ${network} key defined for ${srcChain}`); + } + + const connection = setupConnection(rpc); + const keypair = web3s.Keypair.fromSecretKey(base58.decode(key)); + + const { core, token_bridge } = CONTRACTS[network][srcChain]; + if (!core) { + throw new Error( + `Core bridge address not defined for ${srcChain} ${network}` + ); + } + if (!token_bridge) { + throw new Error( + `Token bridge address not defined for ${srcChain} ${network}` + ); + } + + const bridgeId = new web3s.PublicKey(core); + const tokenBridgeId = new web3s.PublicKey(token_bridge); + const payerAddress = keypair.publicKey.toString(); + + let transaction; + if (tokenAddress === "native") { + transaction = await transferNativeSol( + connection, + bridgeId, + tokenBridgeId, + payerAddress, + BigInt(amount), + tryNativeToUint8Array(dstAddress, dstChain), + dstChain + ); + } else { + // find the associated token account + const fromAddress = ( + await getAssociatedTokenAddress( + new PublicKey(tokenAddress), + keypair.publicKey + ) + ).toString(); + transaction = await transferFromSolana( + connection, + bridgeId, + tokenBridgeId, + payerAddress, + fromAddress, + tokenAddress, // mintAddress + BigInt(amount), + tryNativeToUint8Array(dstAddress, dstChain), + dstChain + ); + } + + // sign, send, and confirm transaction + transaction.partialSign(keypair); + const signature = await connection.sendRawTransaction( + transaction.serialize() + ); + await connection.confirmTransaction(signature); + const info = await connection.getTransaction(signature); + if (!info) { + throw new Error("An error occurred while fetching the transaction info"); + } + console.log("SIGNATURE", signature); +} + const setupConnection = (rpc: string): web3s.Connection => new web3s.Connection(rpc, "confirmed"); diff --git a/clients/js/src/terra.ts b/clients/js/src/terra.ts index 9bb63e29b..06d163715 100644 --- a/clients/js/src/terra.ts +++ b/clients/js/src/terra.ts @@ -1,6 +1,7 @@ import { CHAINS, CONTRACTS, + ChainName, TerraChainName, } from "@certusone/wormhole-sdk/lib/esm/utils/consts"; import { @@ -9,12 +10,15 @@ import { LCDClient, MnemonicKey, MsgExecuteContract, + Wallet, } from "@terra-money/terra.js"; import axios from "axios"; import { fromUint8Array } from "js-base64"; import { NETWORKS } from "./consts"; import { Network } from "./utils"; import { Payload, impossible } from "./vaa"; +import { transferFromTerra } from "@certusone/wormhole-sdk/lib/esm/token_bridge/transfer"; +import { tryNativeToUint8Array } from "@certusone/wormhole-sdk/lib/esm/utils"; export async function execute_terra( payload: Payload, @@ -152,6 +156,55 @@ export async function execute_terra( { uluna: 1000 } ); + await signAndSendTx(terra, wallet, [transaction]); +} + +export async function transferTerra( + srcChain: TerraChainName, + dstChain: ChainName, + dstAddress: string, + tokenAddress: string, + amount: string, + network: Network, + rpc: string +) { + const n = NETWORKS[network][srcChain]; + if (!n.key) { + throw Error(`No ${network} key defined for ${srcChain} (see networks.ts)`); + } + const { token_bridge } = CONTRACTS[network][srcChain]; + if (!token_bridge) { + throw Error(`Unknown token bridge contract on ${network} for ${srcChain}`); + } + + const terra = new LCDClient({ + URL: rpc, + chainID: n.chain_id, + isClassic: srcChain === "terra", + }); + + const wallet = terra.wallet( + new MnemonicKey({ + mnemonic: n.key, + }) + ); + + const msgs = await transferFromTerra( + wallet.key.accAddress, + token_bridge, + tokenAddress, + amount, + dstChain, + tryNativeToUint8Array(dstAddress, dstChain) + ); + await signAndSendTx(terra, wallet, msgs); +} + +async function signAndSendTx( + terra: LCDClient, + wallet: Wallet, + msgs: MsgExecuteContract[] +) { const feeDenoms = ["uluna"]; const gasPrices = await axios .get("https://terra-classic-fcd.publicnode.com/v1/txs/gas_prices") @@ -164,7 +217,7 @@ export async function execute_terra( }, ], { - msgs: [transaction], + msgs, memo: "", feeDenoms, gasPrices, @@ -173,7 +226,7 @@ export async function execute_terra( return wallet .createAndSignTx({ - msgs: [transaction], + msgs, memo: "", fee: new Fee( feeEstimate.gas_limit, diff --git a/clients/js/src/xpla.ts b/clients/js/src/xpla.ts index f71f3c2d0..79afea397 100644 --- a/clients/js/src/xpla.ts +++ b/clients/js/src/xpla.ts @@ -1,15 +1,21 @@ -import { CONTRACTS } from "@certusone/wormhole-sdk/lib/esm/utils/consts"; +import { + CONTRACTS, + ChainName, +} from "@certusone/wormhole-sdk/lib/esm/utils/consts"; import { Coin, Fee, LCDClient, MnemonicKey, MsgExecuteContract, + Wallet, } from "@xpla/xpla.js"; import { fromUint8Array } from "js-base64"; import { NETWORKS } from "./consts"; import { Network } from "./utils"; import { Payload, impossible } from "./vaa"; +import { transferFromXpla } from "@certusone/wormhole-sdk/lib/esm/token_bridge/transfer"; +import { tryNativeToUint8Array } from "@certusone/wormhole-sdk/lib/esm/utils"; export async function execute_xpla( payload: Payload, @@ -146,6 +152,50 @@ export async function execute_xpla( { axpla: "1700000000000000000" } ); + await signAndSendTx(client, wallet, [transaction]); +} + +export async function transferXpla( + dstChain: ChainName, + dstAddress: string, + tokenAddress: string, + amount: string, + network: Network, + rpc: string +) { + const { key, chain_id } = NETWORKS[network].xpla; + if (!key) { + throw Error(`No ${network} key defined for XPLA`); + } + const { token_bridge } = CONTRACTS[network].xpla; + if (token_bridge == undefined) { + throw Error(`Unknown token bridge contract on ${network} for XPLA`); + } + const client = new LCDClient({ + URL: rpc, + chainID: chain_id, + }); + const wallet = client.wallet( + new MnemonicKey({ + mnemonic: key, + }) + ); + const msgs = transferFromXpla( + wallet.key.accAddress, + token_bridge, + tokenAddress, + amount, + dstChain, + tryNativeToUint8Array(dstAddress, dstChain) + ); + await signAndSendTx(client, wallet, msgs); +} + +async function signAndSendTx( + client: LCDClient, + wallet: Wallet, + msgs: MsgExecuteContract[] +) { const feeDenoms = ["axpla"]; // const gasPrices = await axios // .get("https://dimension-lcd.xpla.dev/v1/txs/gas_prices") @@ -158,7 +208,7 @@ export async function execute_xpla( }, ], { - msgs: [transaction], + msgs, memo: "", feeDenoms, // gasPrices, @@ -167,7 +217,7 @@ export async function execute_xpla( wallet .createAndSignTx({ - msgs: [transaction], + msgs, memo: "", fee: new Fee( feeEstimate.gas_limit, diff --git a/sdk/js/src/token_bridge/transfer.ts b/sdk/js/src/token_bridge/transfer.ts index c3f601fd4..54ff2e3f0 100644 --- a/sdk/js/src/token_bridge/transfer.ts +++ b/sdk/js/src/token_bridge/transfer.ts @@ -145,7 +145,7 @@ export async function transferFromEthNative( tokenBridgeAddress: string, signer: ethers.Signer, amount: ethers.BigNumberish, - recipientChain: ChainId | ChainId, + recipientChain: ChainId | ChainName, recipientAddress: Uint8Array, relayerFee: ethers.BigNumberish = 0, overrides: PayableOverrides & { from?: string | Promise } = {},