diff --git a/clients/js/algorand.ts b/clients/js/algorand.ts index c56b7c6ec..24e38033f 100644 --- a/clients/js/algorand.ts +++ b/clients/js/algorand.ts @@ -45,6 +45,8 @@ export async function execute_algorand( case "ContractUpgrade": console.log("Upgrading core contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on algorand") default: impossible(payload); } @@ -61,6 +63,8 @@ export async function execute_algorand( case "ContractUpgrade": console.log("Upgrading contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on algorand") case "RegisterChain": console.log("Registering chain"); break; @@ -80,6 +84,8 @@ export async function execute_algorand( case "ContractUpgrade": console.log("Upgrading contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on algorand") case "RegisterChain": console.log("Registering chain"); break; diff --git a/clients/js/aptos.ts b/clients/js/aptos.ts index 0be87b17a..a2e4f9aee 100644 --- a/clients/js/aptos.ts +++ b/clients/js/aptos.ts @@ -34,6 +34,8 @@ export async function execute_aptos( console.log("Upgrading core contract") await callEntryFunc(network, rpc, `${contract}::contract_upgrade`, "submit_vaa_entry", [], [bcsVAA]); break + case "RecoverChainId": + throw new Error("RecoverChainId not supported on aptos") default: impossible(payload) } @@ -48,6 +50,8 @@ export async function execute_aptos( console.log("Upgrading contract") await callEntryFunc(network, rpc, `${contract}::contract_upgrade`, "submit_vaa_entry", [], [bcsVAA]); break + case "RecoverChainId": + throw new Error("RecoverChainId not supported on aptos") case "RegisterChain": console.log("Registering chain") await callEntryFunc(network, rpc, `${contract}::register_chain`, "submit_vaa_entry", [], [bcsVAA]); @@ -57,6 +61,8 @@ export async function execute_aptos( await callEntryFunc(network, rpc, `${contract}::complete_transfer`, "submit_vaa_entry", [], [bcsVAA]); break } + default: + impossible(payload) } break case "TokenBridge": @@ -69,6 +75,8 @@ export async function execute_aptos( console.log("Upgrading contract") await callEntryFunc(network, rpc, `${contract}::contract_upgrade`, "submit_vaa_entry", [], [bcsVAA]); break + case "RecoverChainId": + throw new Error("RecoverChainId not supported on aptos") case "RegisterChain": console.log("Registering chain") await callEntryFunc(network, rpc, `${contract}::register_chain`, "submit_vaa_entry", [], [bcsVAA]); diff --git a/clients/js/cmds/aptos.ts b/clients/js/cmds/aptos.ts index b235b1fca..5052e4f46 100644 --- a/clients/js/cmds/aptos.ts +++ b/clients/js/cmds/aptos.ts @@ -1,13 +1,13 @@ import { assertChain, CHAIN_ID_APTOS, CHAIN_ID_SOLANA, coalesceChainId, CONTRACTS } from "@certusone/wormhole-sdk/lib/cjs/utils/consts"; import { BCS, FaucetClient } from "aptos"; import { spawnSync } from 'child_process'; -import { ethers } from "ethers"; import fs from 'fs'; import sha3 from 'js-sha3'; import yargs from "yargs"; import { callEntryFunc, deriveResourceAccount, deriveWrappedAssetAddress } from "../aptos"; import { config } from '../config'; import { NETWORKS } from "../networks"; +import { evm_address, hex } from "../consts"; type Network = "MAINNET" | "TESTNET" | "DEVNET" @@ -354,13 +354,6 @@ exports.builder = function(y: typeof yargs) { .strict().demandCommand(); } -function hex(x: string): string { - return ethers.utils.hexlify(x, { allowMissingPrefix: true }); -} -function evm_address(x: string): string { - return hex(x).substring(2).padStart(64, "0"); -} - export function checkAptosBinary(): void { const dir = `${config.wormholeDir}/aptos`; const aptos = spawnSync("aptos", ["--version"]); diff --git a/clients/js/cmds/chainId.ts b/clients/js/cmds/chainId.ts new file mode 100644 index 000000000..f2566a8df --- /dev/null +++ b/clients/js/cmds/chainId.ts @@ -0,0 +1,21 @@ +import yargs from "yargs"; +import { + CHAINS, + assertChain, + coalesceChainId, +} from "@certusone/wormhole-sdk/lib/cjs/utils/consts"; + +exports.command = "chain-id "; +exports.desc = + "Print the wormhole chain ID integer associated with the specified chain name"; +exports.builder = (y: typeof yargs) => { + return y.positional("chain", { + describe: "Chain to query", + type: "string", + choices: Object.keys(CHAINS), + }); +}; +exports.handler = (argv) => { + assertChain(argv["chain"]); + console.log(coalesceChainId(argv["chain"])); +}; diff --git a/clients/js/cmds/contractAddress.ts b/clients/js/cmds/contractAddress.ts new file mode 100644 index 000000000..f7f8a000a --- /dev/null +++ b/clients/js/cmds/contractAddress.ts @@ -0,0 +1,75 @@ +import yargs from "yargs"; +import { + CHAINS, + assertChain, + isCosmWasmChain, +} from "@certusone/wormhole-sdk/lib/cjs/utils/consts"; +import { impossible } from "../vaa"; +import { CONTRACTS } from "../consts"; + +exports.command = "contract "; +exports.desc = "Print contract address"; +exports.builder = (y: typeof yargs) => { + return y + .positional("network", { + describe: "network", + type: "string", + choices: ["mainnet", "testnet", "devnet"], + }) + .positional("chain", { + describe: "Chain to query", + type: "string", + choices: Object.keys(CHAINS), + }) + .positional("module", { + describe: "Module to query", + type: "string", + choices: ["Core", "NFTBridge", "TokenBridge"], + }) + .option("emitter", { + alias: "e", + describe: "Print in emitter address format", + type: "boolean", + default: false, + required: false, + }); +}; +exports.handler = async (argv) => { + assertChain(argv["chain"]); + const network = argv.network.toUpperCase(); + if (network !== "MAINNET" && network !== "TESTNET" && network !== "DEVNET") { + throw Error(`Unknown network: ${network}`); + } + let chain = argv["chain"]; + let module = argv["module"] as "Core" | "NFTBridge" | "TokenBridge"; + let addr = ""; + switch (module) { + case "Core": + addr = CONTRACTS[network][chain]["core"]; + break; + case "NFTBridge": + addr = CONTRACTS[network][chain]["nft_bridge"]; + break; + case "TokenBridge": + addr = CONTRACTS[network][chain]["token_bridge"]; + break; + default: + impossible(module); + } + if (argv["emitter"]) { + const emitter = require("@certusone/wormhole-sdk/lib/cjs/bridge/getEmitterAddress"); + if (chain === "solana" || chain === "pythnet") { + // TODO: Create an isSolanaChain() + addr = await emitter.getEmitterAddressSolana(addr); + } else if (isCosmWasmChain(chain)) { + addr = await emitter.getEmitterAddressTerra(addr); + } else if (chain === "algorand") { + addr = emitter.getEmitterAddressAlgorand(BigInt(addr)); + } else if (chain === "near") { + addr = emitter.getEmitterAddressNear(addr); + } else { + addr = emitter.getEmitterAddressEth(addr); + } + } + console.log(addr); +}; diff --git a/clients/js/cmds/evm.ts b/clients/js/cmds/evm.ts new file mode 100644 index 000000000..4bea7a139 --- /dev/null +++ b/clients/js/cmds/evm.ts @@ -0,0 +1,198 @@ +import yargs from "yargs"; +import { ethers } from "ethers"; +import { NETWORKS } from "../networks"; +import { + assertChain, + assertEVMChain, + CHAINS, + CONTRACTS, + isEVMChain, + toChainName, +} from "@certusone/wormhole-sdk/lib/cjs/utils/consts"; +import { evm_address } from "../consts"; + +exports.command = "evm"; +exports.desc = "EVM utilities"; +exports.builder = function (y: typeof yargs) { + const evm = require("../evm"); + return y + .option("rpc", { + describe: "RPC endpoint", + type: "string", + required: false, + }) + .command( + "address-from-secret ", + "Compute a 20 byte eth address from a 32 byte private key", + (yargs) => { + return yargs.positional("secret", { + type: "string", + describe: "Secret key (32 bytes)", + }); + }, + (argv) => { + console.log(ethers.utils.computeAddress(argv["secret"])); + } + ) + .command( + "storage-update", + "Update a storage slot on an EVM fork during testing (anvil or hardhat)", + (yargs) => { + return yargs + .option("contract-address", { + alias: "a", + describe: "Contract address", + type: "string", + required: true, + }) + .option("storage-slot", { + alias: "k", + describe: "Storage slot to modify", + type: "string", + required: true, + }) + .option("value", { + alias: "v", + describe: "Value to write into the slot (32 bytes)", + type: "string", + required: true, + }); + }, + async (argv) => { + const result = await evm.setStorageAt( + argv["rpc"], + evm_address(argv["contract-address"]), + argv["storage-slot"], + ["uint256"], + [argv["value"]] + ); + console.log(result); + } + ) + .command("chains", "Return all EVM chains", async (_) => { + console.log( + Object.values(CHAINS) + .map((id) => toChainName(id)) + .filter((name) => isEVMChain(name)) + .join(" ") + ); + }) + .command( + "info", + "Query info about the on-chain state of the contract", + (yargs) => { + return yargs + .option("chain", { + alias: "c", + describe: "Chain to query", + type: "string", + choices: Object.keys(CHAINS), + required: true, + }) + .option("module", { + alias: "m", + describe: "Module to query", + type: "string", + choices: ["Core", "NFTBridge", "TokenBridge"], + required: true, + }) + .option("network", { + alias: "n", + describe: "network", + type: "string", + choices: ["mainnet", "testnet", "devnet"], + required: true, + }) + .option("contract-address", { + alias: "a", + describe: "Contract to query (override config)", + type: "string", + required: false, + }) + .option("implementation-only", { + alias: "i", + describe: "Only query implementation (faster)", + type: "boolean", + default: false, + required: false, + }); + }, + async (argv) => { + assertChain(argv["chain"]); + assertEVMChain(argv["chain"]); + const network = argv.network.toUpperCase(); + if ( + network !== "MAINNET" && + network !== "TESTNET" && + network !== "DEVNET" + ) { + throw Error(`Unknown network: ${network}`); + } + let module = argv["module"] as "Core" | "NFTBridge" | "TokenBridge"; + let rpc = argv["rpc"] ?? NETWORKS[network][argv["chain"]].rpc; + if (argv["implementation-only"]) { + console.log( + await evm.getImplementation( + network, + argv["chain"], + module, + argv["contract-address"], + rpc + ) + ); + } else { + console.log( + JSON.stringify( + await evm.query_contract_evm( + network, + argv["chain"], + module, + argv["contract-address"], + rpc + ), + null, + 2 + ) + ); + } + } + ) + .command( + "hijack", + "Override the guardian set of the core bridge contract during testing (anvil or hardhat)", + (yargs) => { + return yargs + .option("core-contract-address", { + alias: "a", + describe: "Core contract address", + type: "string", + default: CONTRACTS.MAINNET.ethereum.core, + }) + .option("guardian-address", { + alias: "g", + required: true, + describe: "Guardians' public addresses (CSV)", + type: "string", + }) + .option("guardian-set-index", { + alias: "i", + required: false, + describe: + "New guardian set index (if unspecified, default to overriding the current index)", + type: "number", + }); + }, + async (argv) => { + const guardian_addresses = argv["guardian-address"].split(","); + let rpc = argv["rpc"] ?? NETWORKS.DEVNET.ethereum.rpc; + await evm.hijack_evm( + rpc, + argv["core-contract-address"], + guardian_addresses, + argv["guardian-set-index"] + ); + } + ) + .strict() + .demandCommand(); +}; diff --git a/clients/js/cmds/generate.ts b/clients/js/cmds/generate.ts new file mode 100644 index 000000000..368f4b165 --- /dev/null +++ b/clients/js/cmds/generate.ts @@ -0,0 +1,316 @@ +import { + CHAINS, + assertChain, + toChainId, + ChainName, + isCosmWasmChain, + isEVMChain, +} from "@certusone/wormhole-sdk/lib/cjs/utils/consts"; +import { sha3_256 } from "js-sha3"; +import yargs from "yargs"; +import { + ContractUpgrade, + Payload, + PortalRegisterChain, + RecoverChainId, + TokenBridgeAttestMeta, + VAA, + impossible, + serialiseVAA, + sign, +} from "../vaa"; +import { fromBech32, toHex } from "@cosmjs/encoding"; +import base58 from "bs58"; +import { evm_address, hex } from "../consts"; + +function makeVAA( + emitterChain: number, + emitterAddress: string, + signers: string[], + p: Payload +): VAA { + let v: VAA = { + version: 1, + guardianSetIndex: 0, + signatures: [], + timestamp: 1, + nonce: 1, + emitterChain: emitterChain, + emitterAddress: emitterAddress, + sequence: BigInt(Math.floor(Math.random() * 100000000)), + consistencyLevel: 0, + payload: p, + }; + v.signatures = sign(signers, v); + return v; +} + +const GOVERNANCE_CHAIN = 1; +const GOVERNANCE_EMITTER = + "0000000000000000000000000000000000000000000000000000000000000004"; + +exports.command = "generate"; +exports.desc = "generate VAAs (devnet and testnet only)"; +exports.builder = function (y: typeof yargs) { + return ( + y + .option("guardian-secret", { + alias: "g", + required: true, + describe: "Guardians' secret keys (CSV)", + type: "string", + }) + // Registration + .command( + "registration", + "Generate registration VAA", + (yargs) => { + return yargs + .option("chain", { + alias: "c", + describe: "Chain to register", + type: "string", + choices: Object.keys(CHAINS), + required: true, + }) + .option("contract-address", { + alias: "a", + describe: "Contract to register", + type: "string", + required: true, + }) + .option("module", { + alias: "m", + describe: "Module to upgrade", + type: "string", + choices: ["NFTBridge", "TokenBridge"], + required: true, + }); + }, + (argv) => { + let module = argv["module"] as "NFTBridge" | "TokenBridge"; + assertChain(argv["chain"]); + let payload: PortalRegisterChain = { + module, + type: "RegisterChain", + chain: 0, + emitterChain: toChainId(argv["chain"]), + emitterAddress: parseAddress( + argv["chain"], + argv["contract-address"] + ), + }; + let v = makeVAA( + GOVERNANCE_CHAIN, + GOVERNANCE_EMITTER, + argv["guardian-secret"].split(","), + payload + ); + console.log(serialiseVAA(v)); + } + ) + // Upgrade + .command( + "upgrade", + "Generate contract upgrade VAA", + (yargs) => { + return yargs + .option("chain", { + alias: "c", + describe: "Chain to upgrade", + type: "string", + choices: Object.keys(CHAINS), + required: true, + }) + .option("contract-address", { + alias: "a", + describe: "Contract to upgrade to", + type: "string", + required: true, + }) + .option("module", { + alias: "m", + describe: "Module to upgrade", + type: "string", + choices: ["Core", "NFTBridge", "TokenBridge"], + required: true, + }); + }, + (argv) => { + assertChain(argv["chain"]); + let module = argv["module"] as "Core" | "NFTBridge" | "TokenBridge"; + let payload: ContractUpgrade = { + module, + type: "ContractUpgrade", + chain: toChainId(argv["chain"]), + address: parseCodeAddress(argv["chain"], argv["contract-address"]), + }; + let v = makeVAA( + GOVERNANCE_CHAIN, + GOVERNANCE_EMITTER, + argv["guardian-secret"].split(","), + payload + ); + console.log(serialiseVAA(v)); + } + ) + .command( + "attestation", + "Generate a token attestation VAA", + (yargs) => { + return yargs + .option("emitter-chain", { + alias: "e", + describe: "Emitter chain of the VAA", + type: "string", + choices: Object.keys(CHAINS), + required: true, + }) + .option("emitter-address", { + alias: "f", + describe: "Emitter address of the VAA", + type: "string", + required: true, + }) + .option("chain", { + alias: "c", + describe: "Token's chain", + type: "string", + choices: Object.keys(CHAINS), + required: true, + }) + .option("token-address", { + alias: "a", + describe: "Token's address", + type: "string", + required: true, + }) + .option("decimals", { + alias: "d", + describe: "Token's decimals", + type: "number", + required: true, + }) + .option("symbol", { + alias: "s", + describe: "Token's symbol", + type: "string", + required: true, + }) + .option("name", { + alias: "n", + describe: "Token's name", + type: "string", + required: true, + }); + }, + (argv) => { + let emitter_chain = argv["emitter-chain"] as string; + assertChain(argv["chain"]); + assertChain(emitter_chain); + let payload: TokenBridgeAttestMeta = { + module: "TokenBridge", + type: "AttestMeta", + chain: 0, + tokenAddress: parseAddress(argv["chain"], argv["token-address"]), + tokenChain: toChainId(argv["chain"]), + decimals: argv["decimals"], + symbol: argv["symbol"], + name: argv["name"], + }; + let v = makeVAA( + toChainId(emitter_chain), + parseAddress(emitter_chain, argv["emitter-address"] as string), + argv["guardian-secret"].split(","), + payload + ); + console.log(serialiseVAA(v)); + } + ) + // RecoverChainId + .command( + "recover-chain-id", + "Generate a recover chain ID VAA", + (yargs) => { + return yargs + .option("module", { + alias: "m", + describe: "Module to upgrade", + type: "string", + choices: ["Core", "NFTBridge", "TokenBridge"], + required: true, + }) + .option("evm-chain-id", { + alias: "e", + describe: "EVM chain ID to set", + type: "string", + required: true, + }) + .option("new-chain-id", { + alias: "c", + describe: "New chain ID to set", + type: "number", + required: true, + }); + }, + (argv) => { + let module = argv["module"] as "Core" | "NFTBridge" | "TokenBridge"; + let payload: RecoverChainId = { + module, + type: "RecoverChainId", + evmChainId: BigInt(argv["evm-chain-id"]), + newChainId: argv["new-chain-id"], + }; + let v = makeVAA( + GOVERNANCE_CHAIN, + GOVERNANCE_EMITTER, + argv["guardian-secret"].split(","), + payload + ); + console.log(serialiseVAA(v)); + } + ) + ); +}; + +function parseAddress(chain: ChainName, address: string): string { + if (chain === "unset") { + throw Error("Chain unset"); + } else if (isEVMChain(chain)) { + return "0x" + evm_address(address); + } else if (isCosmWasmChain(chain)) { + return "0x" + toHex(fromBech32(address).data).padStart(64, "0"); + } else if (chain === "solana" || chain === "pythnet") { + return "0x" + toHex(base58.decode(address)).padStart(64, "0"); + } else if (chain === "algorand") { + // TODO: is there a better native format for algorand? + return "0x" + evm_address(address); + } else if (chain === "near") { + return "0x" + hex(address).substring(2).padStart(64, "0"); + } else if (chain === "osmosis") { + throw Error("OSMOSIS is not supported yet"); + } else if (chain === "sui") { + throw Error("SUI is not supported yet"); + } else if (chain === "aptos") { + if (/^(0x)?[0-9a-fA-F]+$/.test(address)) { + return "0x" + evm_address(address); + } + + return sha3_256(Buffer.from(address)); // address is hash of fully qualified type + } else if (chain === "wormchain") { + const sdk = require("@certusone/wormhole-sdk/lib/cjs/utils/array"); + return "0x" + sdk.tryNativeToHexString(address, chain); + } else if (chain === "btc") { + throw Error("btc is not supported yet"); + } else { + impossible(chain); + } +} + +function parseCodeAddress(chain: ChainName, address: string): string { + if (isCosmWasmChain(chain)) { + return "0x" + parseInt(address, 10).toString(16).padStart(64, "0"); + } else { + return parseAddress(chain, address); + } +} diff --git a/clients/js/cmds/near.ts b/clients/js/cmds/near.ts new file mode 100644 index 000000000..2b8b6eba6 --- /dev/null +++ b/clients/js/cmds/near.ts @@ -0,0 +1,74 @@ +import yargs from "yargs"; + +// Near utilities +exports.command = "near"; +exports.desc = "NEAR utilities"; +exports.builder = function (y: typeof yargs) { + const near = require("../near"); + return y + .option("module", { + alias: "m", + describe: "Module to query", + type: "string", + choices: ["Core", "NFTBridge", "TokenBridge"], + required: false, + }) + .option("network", { + alias: "n", + describe: "network", + type: "string", + choices: ["mainnet", "testnet", "devnet"], + required: true, + }) + .option("account", { + describe: "near deployment account", + type: "string", + required: true, + }) + .option("attach", { + describe: "attach some near", + type: "string", + required: false, + }) + .option("target", { + describe: "near account to upgrade", + type: "string", + required: false, + }) + .option("mnemonic", { + describe: "near private keys", + type: "string", + required: false, + }) + .option("keys", { + describe: "near private keys", + type: "string", + required: false, + }) + .command( + "contract-update ", + "Submit a contract update using our specific APIs", + (yargs) => { + return yargs.positional("file", { + type: "string", + describe: "wasm", + }); + }, + async (argv) => { + await near.upgrade_near(argv); + } + ) + .command( + "deploy ", + "Submit a contract update using near APIs", + (yargs) => { + return yargs.positional("file", { + type: "string", + describe: "wasm", + }); + }, + async (argv) => { + await near.deploy_near(argv); + } + ); +}; diff --git a/clients/js/cmds/parse.ts b/clients/js/cmds/parse.ts new file mode 100644 index 000000000..4d3c9aa21 --- /dev/null +++ b/clients/js/cmds/parse.ts @@ -0,0 +1,29 @@ +import yargs from "yargs"; +import { parse, vaaDigest } from "../vaa"; + +exports.command = "parse "; +exports.desc = "Parse a VAA (can be in either hex or base64 format)"; +exports.builder = (y: typeof yargs) => { + return y.positional("vaa", { + describe: "vaa", + type: "string", + }); +}; +exports.handler = (argv) => { + let buf: Buffer; + try { + buf = Buffer.from(String(argv.vaa), "hex"); + if (buf.length == 0) { + throw Error("Couldn't parse VAA as hex"); + } + } catch (e) { + buf = Buffer.from(String(argv.vaa), "base64"); + if (buf.length == 0) { + throw Error("Couldn't parse VAA as base64 or hex"); + } + } + const parsed_vaa = parse(buf); + let parsed_vaa_with_digest = parsed_vaa; + parsed_vaa_with_digest["digest"] = vaaDigest(parsed_vaa); + console.log(parsed_vaa_with_digest); +}; diff --git a/clients/js/cmds/recover.ts b/clients/js/cmds/recover.ts new file mode 100644 index 000000000..8134d9753 --- /dev/null +++ b/clients/js/cmds/recover.ts @@ -0,0 +1,22 @@ +import yargs from "yargs"; +import { ethers } from "ethers"; +import { hex } from "../consts"; + +exports.command = "recover "; +exports.desc = "Recover an address from a signature"; +exports.builder = (y: typeof yargs) => { + return y + .positional("digest", { + describe: "digest", + type: "string", + }) + .positional("signature", { + describe: "signature", + type: "string", + }); +}; +exports.handler = async (argv) => { + console.log( + ethers.utils.recoverAddress(hex(argv["digest"]), hex(argv["signature"])) + ); +}; diff --git a/clients/js/cmds/rpc.ts b/clients/js/cmds/rpc.ts new file mode 100644 index 000000000..348afb493 --- /dev/null +++ b/clients/js/cmds/rpc.ts @@ -0,0 +1,30 @@ +import yargs from "yargs"; +import { + CHAINS, + assertChain, +} from "@certusone/wormhole-sdk/lib/cjs/utils/consts"; +import { NETWORKS } from "../networks"; + +exports.command = "rpc "; +exports.desc = "Print RPC address"; +exports.builder = (y: typeof yargs) => { + return y + .positional("network", { + describe: "network", + type: "string", + choices: ["mainnet", "testnet", "devnet"], + }) + .positional("chain", { + describe: "Chain to query", + type: "string", + choices: Object.keys(CHAINS), + }); +}; +exports.handler = async (argv) => { + assertChain(argv["chain"]); + const network = argv.network.toUpperCase(); + if (network !== "MAINNET" && network !== "TESTNET" && network !== "DEVNET") { + throw Error(`Unknown network: ${network}`); + } + console.log(NETWORKS[network][argv["chain"]].rpc); +}; diff --git a/clients/js/cmds/submit.ts b/clients/js/cmds/submit.ts new file mode 100644 index 000000000..3e0e96eee --- /dev/null +++ b/clients/js/cmds/submit.ts @@ -0,0 +1,155 @@ +import yargs from "yargs"; +import { + CHAINS, + assertChain, + toChainName, + ChainName, + isEVMChain, + isTerraChain, + coalesceChainName, +} from "@certusone/wormhole-sdk/lib/cjs/utils/consts"; +import * as vaa from "../vaa"; + +exports.command = "submit "; +exports.desc = "Execute a VAA"; +exports.builder = (y: typeof yargs) => { + return y + .positional("vaa", { + describe: "vaa", + type: "string", + required: true, + }) + .option("chain", { + alias: "c", + describe: "chain name", + type: "string", + choices: Object.keys(CHAINS), + required: false, + }) + .option("network", { + alias: "n", + describe: "network", + type: "string", + choices: ["mainnet", "testnet", "devnet"], + required: true, + }) + .option("contract-address", { + alias: "a", + describe: "Contract to submit VAA to (override config)", + type: "string", + required: false, + }) + .option("rpc", { + describe: "RPC endpoint", + type: "string", + required: false, + }); +}; +exports.handler = async (argv) => { + const vaa_hex = String(argv.vaa); + const buf = Buffer.from(vaa_hex, "hex"); + const parsed_vaa = vaa.parse(buf); + + vaa.assertKnownPayload(parsed_vaa); + + console.log(parsed_vaa.payload); + + const network = argv.network.toUpperCase(); + if (network !== "MAINNET" && network !== "TESTNET" && network !== "DEVNET") { + throw Error(`Unknown network: ${network}`); + } + + // We figure out the target chain to submit the VAA to. + // The VAA might specify this itself (for example a contract upgrade VAA + // or a token transfer VAA), in which case we just submit the VAA to + // that target chain. + // + // If the VAA does not have a target (e.g. chain registration VAAs or + // guardian set upgrade VAAs), we require the '--chain' argument to be + // set on the command line. + // + // As a sanity check, in the event that the VAA does specify a target + // and the '--chain' argument is also set, we issue an error if those + // two don't agree instead of silently taking the VAA's target chain. + + // get VAA chain + const vaa_chain_id = + "chain" in parsed_vaa.payload ? parsed_vaa.payload.chain : 0; + assertChain(vaa_chain_id); + const vaa_chain = toChainName(vaa_chain_id); + + // get chain from command line arg + const cli_chain = argv["chain"]; + + let chain: ChainName; + if (cli_chain !== undefined) { + assertChain(cli_chain); + if (vaa_chain !== "unset" && cli_chain !== vaa_chain) { + throw Error( + `Specified target chain (${cli_chain}) does not match VAA target chain (${vaa_chain})` + ); + } + chain = coalesceChainName(cli_chain); + } else { + chain = vaa_chain; + } + + if (chain === "unset") { + throw Error( + "This VAA does not specify the target chain, please provide it by hand using the '--chain' flag." + ); + } else if (isEVMChain(chain)) { + const evm = require("../evm"); + await evm.execute_evm( + parsed_vaa.payload, + buf, + network, + chain, + argv["contract-address"], + argv["rpc"] + ); + } else if (isTerraChain(chain)) { + const terra = require("../terra"); + await terra.execute_terra(parsed_vaa.payload, buf, network, chain); + } else if (chain === "solana" || chain === "pythnet") { + const solana = require("../solana"); + await solana.execute_solana(parsed_vaa, buf, network, chain); + } else if (chain === "algorand") { + const algorand = require("../algorand"); + await algorand.execute_algorand( + parsed_vaa.payload, + new Uint8Array(Buffer.from(vaa_hex, "hex")), + network + ); + } else if (chain === "near") { + const near = require("../near"); + await near.execute_near(parsed_vaa.payload, vaa_hex, network); + } else if (chain === "injective") { + const injective = require("../injective"); + await injective.execute_injective(parsed_vaa.payload, buf, network); + } else if (chain === "xpla") { + const xpla = require("../xpla"); + await xpla.execute_xpla(parsed_vaa.payload, buf, network); + } else if (chain === "osmosis") { + throw Error("OSMOSIS is not supported yet"); + } else if (chain === "sui") { + throw Error("SUI is not supported yet"); + } else if (chain === "aptos") { + const aptos = require("../aptos"); + await aptos.execute_aptos( + parsed_vaa.payload, + buf, + network, + argv["contract-address"], + argv["rpc"] + ); + } else if (chain === "wormchain") { + throw Error("Wormchain is not supported yet"); + } else if (chain === "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 + vaa.impossible(chain); + } +}; diff --git a/clients/js/consts.ts b/clients/js/consts.ts new file mode 100644 index 000000000..7e3321a1b --- /dev/null +++ b/clients/js/consts.ts @@ -0,0 +1,46 @@ +import { CONTRACTS as SDK_CONTRACTS } from "@certusone/wormhole-sdk/lib/cjs/utils/consts"; +import { ethers } from "ethers"; + +const OVERRIDES = { + MAINNET: { + aptos: { + token_bridge: + "0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f", + core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625", + nft_bridge: + "0x1bdffae984043833ed7fe223f7af7a3f8902d04129b14f801823e64827da7130", + }, + }, + TESTNET: { + aptos: { + token_bridge: + "0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f", + core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625", + nft_bridge: + "0x1bdffae984043833ed7fe223f7af7a3f8902d04129b14f801823e64827da7130", + }, + }, + DEVNET: { + aptos: { + token_bridge: + "0x84a5f374d29fc77e370014dce4fd6a55b58ad608de8074b0be5571701724da31", + core: "0xde0036a9600559e295d5f6802ef6f3f802f510366e0c23912b0655d972166017", + nft_bridge: + "0x46da3d4c569388af61f951bdd1153f4c875f90c2991f6b2d0a38e2161a40852c", + }, + }, +}; + +export const CONTRACTS = { + MAINNET: { ...SDK_CONTRACTS.MAINNET, ...OVERRIDES.MAINNET }, + TESTNET: { ...SDK_CONTRACTS.TESTNET, ...OVERRIDES.TESTNET }, + DEVNET: { ...SDK_CONTRACTS.DEVNET, ...OVERRIDES.DEVNET }, +}; + +export function evm_address(x: string): string { + return hex(x).substring(2).padStart(64, "0"); +} + +export function hex(x: string): string { + return ethers.utils.hexlify(x, { allowMissingPrefix: true }); +} diff --git a/clients/js/evm.ts b/clients/js/evm.ts index ee4c93e1d..6314520c7 100644 --- a/clients/js/evm.ts +++ b/clients/js/evm.ts @@ -283,6 +283,10 @@ export async function execute_evm( console.log("Upgrading core contract") console.log("Hash: " + (await cb.submitContractUpgrade(vaa, overrides)).hash) break + case "RecoverChainId": + console.log("Recovering chain ID") + console.log("Hash: " + (await cb.submitRecoverChainId(vaa, overrides)).hash) + break default: impossible(payload) } @@ -300,6 +304,10 @@ export async function execute_evm( console.log("Hash: " + (await nb.upgrade(vaa, overrides)).hash) console.log("Don't forget to verify the new implementation! See ethereum/VERIFY.md for instructions") break + case "RecoverChainId": + console.log("Recovering chain ID") + console.log("Hash: " + (await nb.submitRecoverChainId(vaa, overrides)).hash) + break case "RegisterChain": console.log("Registering chain") console.log("Hash: " + (await nb.registerChain(vaa, overrides)).hash) @@ -326,6 +334,10 @@ export async function execute_evm( console.log("Hash: " + (await tb.upgrade(vaa, overrides)).hash) console.log("Don't forget to verify the new implementation! See ethereum/VERIFY.md for instructions") break + case "RecoverChainId": + console.log("Recovering chain ID") + console.log("Hash: " + (await tb.submitRecoverChainId(vaa, overrides)).hash) + break case "RegisterChain": console.log("Registering chain") console.log("Hash: " + (await tb.registerChain(vaa, overrides)).hash) @@ -567,7 +579,7 @@ export async function setStorageAt(rpc: string, contract_address: string, storag })).data } -async function maybeUnsupported(query: Promise): Promise { +async function maybeUnsupported(query: Promise): Promise { try { return await query } catch (e) { diff --git a/clients/js/injective.ts b/clients/js/injective.ts index b898f3436..0337cf148 100644 --- a/clients/js/injective.ts +++ b/clients/js/injective.ts @@ -55,6 +55,8 @@ export async function execute_injective( case "ContractUpgrade": console.log("Upgrading core contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on injective") default: impossible(payload); } @@ -77,6 +79,8 @@ export async function execute_injective( case "ContractUpgrade": console.log("Upgrading contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on injective") case "RegisterChain": console.log("Registering chain"); break; @@ -103,6 +107,8 @@ export async function execute_injective( case "ContractUpgrade": console.log("Upgrading contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on injective") case "RegisterChain": console.log("Registering chain"); break; diff --git a/clients/js/main.ts b/clients/js/main.ts index 79499cd72..7a4fc3d79 100644 --- a/clients/js/main.ts +++ b/clients/js/main.ts @@ -11,25 +11,15 @@ // drop that particular message... // const info = console.info; -console.info = function(x: string) { +console.info = function (x: string) { if (x != "secp256k1 unavailable, reverting to browser version") { info(x); } }; import yargs from "yargs"; - import { hideBin } from "yargs/helpers"; - -import { fromBech32, toHex } from "@cosmjs/encoding"; -import * as vaa from "./vaa"; -import { impossible, Payload, serialiseVAA, VAA } from "./vaa"; -import { ethers } from "ethers"; -import { NETWORKS } from "./networks"; -import base58 from "bs58"; -import { sha3_256 } from "js-sha3"; import { isOutdated } from "./cmds/update"; -import { assertChain, assertEVMChain, ChainName, CHAINS, CONTRACTS as SDK_CONTRACTS, isCosmWasmChain, isEVMChain, isTerraChain, toChainId, toChainName } from "@certusone/wormhole-sdk/lib/cjs/utils/consts"; if (isOutdated()) { console.error( @@ -38,911 +28,4 @@ if (isOutdated()) { ); } -const GOVERNANCE_CHAIN = 1; -const GOVERNANCE_EMITTER = - "0000000000000000000000000000000000000000000000000000000000000004"; - -const OVERRIDES = { - MAINNET: { - aptos: { - token_bridge: - "0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f", - core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625", - nft_bridge: "0x1bdffae984043833ed7fe223f7af7a3f8902d04129b14f801823e64827da7130", - }, - }, - TESTNET: { - aptos: { - token_bridge: - "0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f", - core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625", - nft_bridge: "0x1bdffae984043833ed7fe223f7af7a3f8902d04129b14f801823e64827da7130", - }, - }, - DEVNET: { - aptos: { - token_bridge: - "0x84a5f374d29fc77e370014dce4fd6a55b58ad608de8074b0be5571701724da31", - core: "0xde0036a9600559e295d5f6802ef6f3f802f510366e0c23912b0655d972166017", - nft_bridge: "0x46da3d4c569388af61f951bdd1153f4c875f90c2991f6b2d0a38e2161a40852c" - }, - }, -}; - -export const CONTRACTS = { - MAINNET: { ...SDK_CONTRACTS.MAINNET, ...OVERRIDES.MAINNET }, - TESTNET: { ...SDK_CONTRACTS.TESTNET, ...OVERRIDES.TESTNET }, - DEVNET: { ...SDK_CONTRACTS.DEVNET, ...OVERRIDES.DEVNET }, -}; - -function makeVAA( - emitterChain: number, - emitterAddress: string, - signers: string[], - p: Payload -): VAA { - let v: VAA = { - version: 1, - guardianSetIndex: 0, - signatures: [], - timestamp: 1, - nonce: 1, - emitterChain: emitterChain, - emitterAddress: emitterAddress, - sequence: BigInt(Math.floor(Math.random() * 100000000)), - consistencyLevel: 0, - payload: p, - }; - v.signatures = vaa.sign(signers, v); - return v; -} - -yargs(hideBin(process.argv)) - //TODO(csongor): refactor all commands into the directory structure. - .commandDir("cmds") - //////////////////////////////////////////////////////////////////////////////// - // Generate - .command( - "generate", - "generate VAAs (devnet and testnet only)", - (yargs) => { - return ( - yargs - .option("guardian-secret", { - alias: "g", - required: true, - describe: "Guardians' secret keys (CSV)", - type: "string", - }) - // Registration - .command( - "registration", - "Generate registration VAA", - (yargs) => { - return yargs - .option("chain", { - alias: "c", - describe: "Chain to register", - type: "string", - choices: Object.keys(CHAINS), - required: true, - }) - .option("contract-address", { - alias: "a", - describe: "Contract to register", - type: "string", - required: true, - }) - .option("module", { - alias: "m", - describe: "Module to upgrade", - type: "string", - choices: ["NFTBridge", "TokenBridge"], - required: true, - }); - }, - (argv) => { - let module = argv["module"] as "NFTBridge" | "TokenBridge"; - assertChain(argv["chain"]); - let payload: vaa.PortalRegisterChain = { - module, - type: "RegisterChain", - chain: 0, - emitterChain: toChainId(argv["chain"]), - emitterAddress: parseAddress( - argv["chain"], - argv["contract-address"] - ), - }; - let v = makeVAA( - GOVERNANCE_CHAIN, - GOVERNANCE_EMITTER, - argv["guardian-secret"].split(","), - payload - ); - console.log(serialiseVAA(v)); - } - ) - // Upgrade - .command( - "upgrade", - "Generate contract upgrade VAA", - (yargs) => { - return yargs - .option("chain", { - alias: "c", - describe: "Chain to upgrade", - type: "string", - choices: Object.keys(CHAINS), - required: true, - }) - .option("contract-address", { - alias: "a", - describe: "Contract to upgrade to", - type: "string", - required: true, - }) - .option("module", { - alias: "m", - describe: "Module to upgrade", - type: "string", - choices: ["Core", "NFTBridge", "TokenBridge"], - required: true, - }); - }, - (argv) => { - assertChain(argv["chain"]); - let module = argv["module"] as - | "Core" - | "NFTBridge" - | "TokenBridge"; - let payload: vaa.ContractUpgrade = { - module, - type: "ContractUpgrade", - chain: toChainId(argv["chain"]), - address: parseCodeAddress( - argv["chain"], - argv["contract-address"] - ), - }; - let v = makeVAA( - GOVERNANCE_CHAIN, - GOVERNANCE_EMITTER, - argv["guardian-secret"].split(","), - payload - ); - console.log(serialiseVAA(v)); - } - ) - .command( - "attestation", - "Generate a token attestation VAA", - // TODO: putting 'any' here is a workaround for the following error: - // - // Type instantiation is excessively deep and possibly infinite. - // - // The type of the yargs builder grows too big for typescript's - // liking, and there's no way to increase the limit. So we - // overapproximate with the 'any' type which reduces the typechecking stack. - // This is not a great solution, and instead we should move toward - // breaking up the commands into multiple modules in the 'cmds' folder. - (yargs: any) => { - return yargs - .option("emitter-chain", { - alias: "e", - describe: "Emitter chain of the VAA", - type: "string", - choices: Object.keys(CHAINS), - required: true, - }) - .option("emitter-address", { - alias: "f", - describe: "Emitter address of the VAA", - type: "string", - required: true, - }) - .option("chain", { - alias: "c", - describe: "Token's chain", - type: "string", - choices: Object.keys(CHAINS), - required: true, - }) - .option("token-address", { - alias: "a", - describe: "Token's address", - type: "string", - required: true, - }) - .option("decimals", { - alias: "d", - describe: "Token's decimals", - type: "number", - required: true, - }) - .option("symbol", { - alias: "s", - describe: "Token's symbol", - type: "string", - required: true, - }) - .option("name", { - alias: "n", - describe: "Token's name", - type: "string", - required: true, - }); - }, - (argv) => { - let emitter_chain = argv["emitter-chain"] as string; - assertChain(argv["chain"]); - assertChain(emitter_chain); - let payload: vaa.TokenBridgeAttestMeta = { - module: "TokenBridge", - type: "AttestMeta", - chain: 0, - // TODO: remove these casts (only here because of the workaround above) - tokenAddress: parseAddress( - argv["chain"], - argv["token-address"] as string - ), - tokenChain: toChainId(argv["chain"]), - decimals: argv["decimals"] as number, - symbol: argv["symbol"] as string, - name: argv["name"] as string, - }; - let v = makeVAA( - toChainId(emitter_chain), - parseAddress(emitter_chain, argv["emitter-address"] as string), - argv["guardian-secret"].split(","), - payload - ); - console.log(serialiseVAA(v)); - } - ) - ); - }, - (_) => { - yargs.showHelp(); - } - ) - //////////////////////////////////////////////////////////////////////////////// - // Misc - .command( - "parse ", - "Parse a VAA (can be in either hex or base64 format)", - (yargs) => { - return yargs.positional("vaa", { - describe: "vaa", - type: "string", - }); - }, - async (argv) => { - let buf: Buffer; - try { - buf = Buffer.from(String(argv.vaa), "hex"); - if (buf.length == 0) { - throw Error("Couldn't parse VAA as hex"); - } - } catch (e) { - buf = Buffer.from(String(argv.vaa), "base64"); - if (buf.length == 0) { - throw Error("Couldn't parse VAA as base64 or hex"); - } - } - const parsed_vaa = vaa.parse(buf); - let parsed_vaa_with_digest = parsed_vaa; - parsed_vaa_with_digest["digest"] = vaa.vaaDigest(parsed_vaa); - console.log(parsed_vaa_with_digest); - } - ) - .command( - "recover ", - "Recover an address from a signature", - (yargs) => { - return yargs - .positional("digest", { - describe: "digest", - type: "string", - }) - .positional("signature", { - describe: "signature", - type: "string", - }); - }, - async (argv) => { - console.log( - ethers.utils.recoverAddress(hex(argv["digest"]), hex(argv["signature"])) - ); - } - ) - .command( - "contract ", - "Print contract address", - (yargs) => { - return yargs - .positional("network", { - describe: "network", - type: "string", - choices: ["mainnet", "testnet", "devnet"], - }) - .positional("chain", { - describe: "Chain to query", - type: "string", - choices: Object.keys(CHAINS), - }) - .positional("module", { - describe: "Module to query", - type: "string", - choices: ["Core", "NFTBridge", "TokenBridge"], - }) - .option("emitter", { - alias: "e", - describe: "Print in emitter address format", - type: "boolean", - default: false, - required: false, - }); - }, - async (argv) => { - assertChain(argv["chain"]); - const network = argv.network.toUpperCase(); - if ( - network !== "MAINNET" && - network !== "TESTNET" && - network !== "DEVNET" - ) { - throw Error(`Unknown network: ${network}`); - } - let chain = argv["chain"]; - let module = argv["module"] as "Core" | "NFTBridge" | "TokenBridge"; - let addr = ""; - switch (module) { - case "Core": - addr = CONTRACTS[network][chain]["core"]; - break; - case "NFTBridge": - addr = CONTRACTS[network][chain]["nft_bridge"]; - break; - case "TokenBridge": - addr = CONTRACTS[network][chain]["token_bridge"]; - break; - default: - impossible(module); - } - if (argv["emitter"]) { - const emitter = require("@certusone/wormhole-sdk/lib/cjs/bridge/getEmitterAddress") - if (chain === "solana" || chain === "pythnet") { - // TODO: Create an isSolanaChain() - addr = await emitter.getEmitterAddressSolana(addr); - } else if (isCosmWasmChain(chain)) { - addr = await emitter.getEmitterAddressTerra(addr); - } else if (chain === "algorand") { - addr = emitter.getEmitterAddressAlgorand(BigInt(addr)); - } else if (chain === "near") { - addr = emitter.getEmitterAddressNear(addr); - } else { - addr = emitter.getEmitterAddressEth(addr); - } - } - console.log(addr); - } - ) - .command( - "chain-id ", - "Print the wormhole chain ID integer associated with the specified chain name", - (yargs) => { - return yargs.positional("chain", { - describe: "Chain to query", - type: "string", - choices: Object.keys(CHAINS), - }); - }, - async (argv) => { - assertChain(argv["chain"]); - console.log(toChainId(argv["chain"])); - } - ) - .command( - "rpc ", - "Print RPC address", - (yargs) => { - return yargs - .positional("network", { - describe: "network", - type: "string", - choices: ["mainnet", "testnet", "devnet"], - }) - .positional("chain", { - describe: "Chain to query", - type: "string", - choices: Object.keys(CHAINS), - }); - }, - async (argv) => { - assertChain(argv["chain"]); - const network = argv.network.toUpperCase(); - if ( - network !== "MAINNET" && - network !== "TESTNET" && - network !== "DEVNET" - ) { - throw Error(`Unknown network: ${network}`); - } - console.log(NETWORKS[network][argv["chain"]].rpc); - } - ) - //////////////////////////////////////////////////////////////////////////////// - // Near utilities - .command( - "near", - "NEAR utilites", - (yargs) => { - const near = require("./near") - return ( - yargs - .option("module", { - alias: "m", - describe: "Module to query", - type: "string", - choices: ["Core", "NFTBridge", "TokenBridge"], - required: false, - }) - .option("network", { - alias: "n", - describe: "network", - type: "string", - choices: ["mainnet", "testnet", "devnet"], - required: true, - }) - .option("account", { - describe: "near deployment account", - type: "string", - required: true, - }) - .option("attach", { - describe: "attach some near", - type: "string", - required: false, - }) - .option("target", { - describe: "near account to upgrade", - type: "string", - required: false, - }) - .option("mnemonic", { - describe: "near private keys", - type: "string", - required: false, - }) - .option("keys", { - describe: "near private keys", - type: "string", - required: false, - }) - .command( - "contract-update ", - "Submit a contract update using our specific APIs", - (yargs) => { - return yargs.positional("file", { - type: "string", - describe: "wasm", - }); - }, - async (argv) => { - await near.upgrade_near(argv); - } - ) - .command( - "deploy ", - "Submit a contract update using near APIs", - (yargs) => { - return yargs.positional("file", { - type: "string", - describe: "wasm", - }); - }, - async (argv) => { - await near.deploy_near(argv); - } - ) - ); - }, - (_) => { - yargs.showHelp(); - } - ) - - //////////////////////////////////////////////////////////////////////////////// - // Evm utilities - .command( - "evm", - "EVM utilites", - (yargs) => { - const evm = require("./evm") - return yargs - .option("rpc", { - describe: "RPC endpoint", - type: "string", - required: false, - }) - .command( - "address-from-secret ", - "Compute a 20 byte eth address from a 32 byte private key", - (yargs) => { - return yargs.positional("secret", { - type: "string", - describe: "Secret key (32 bytes)", - }); - }, - (argv) => { - console.log(ethers.utils.computeAddress(argv["secret"])); - } - ) - .command( - "storage-update", - "Update a storage slot on an EVM fork during testing (anvil or hardhat)", - (yargs) => { - return yargs - .option("contract-address", { - alias: "a", - describe: "Contract address", - type: "string", - required: true, - }) - .option("storage-slot", { - alias: "k", - describe: "Storage slot to modify", - type: "string", - required: true, - }) - .option("value", { - alias: "v", - describe: "Value to write into the slot (32 bytes)", - type: "string", - required: true, - }); - }, - async (argv) => { - const result = await evm.setStorageAt( - argv["rpc"], - evm_address(argv["contract-address"]), - argv["storage-slot"], - ["uint256"], - [argv["value"]] - ); - console.log(result); - } - ) - .command("chains", "Return all EVM chains", async (_) => { - console.log( - Object.values(CHAINS) - .map((id) => toChainName(id)) - .filter((name) => isEVMChain(name)) - .join(" ") - ); - }) - .command( - "info", - "Query info about the on-chain state of the contract", - (yargs) => { - return yargs - .option("chain", { - alias: "c", - describe: "Chain to query", - type: "string", - choices: Object.keys(CHAINS), - required: true, - }) - .option("module", { - alias: "m", - describe: "Module to query", - type: "string", - choices: ["Core", "NFTBridge", "TokenBridge"], - required: true, - }) - .option("network", { - alias: "n", - describe: "network", - type: "string", - choices: ["mainnet", "testnet", "devnet"], - required: true, - }) - .option("contract-address", { - alias: "a", - describe: "Contract to query (override config)", - type: "string", - required: false, - }) - .option("implementation-only", { - alias: "i", - describe: "Only query implementation (faster)", - type: "boolean", - default: false, - required: false, - }); - }, - async (argv) => { - assertChain(argv["chain"]); - assertEVMChain(argv["chain"]); - const network = argv.network.toUpperCase(); - if ( - network !== "MAINNET" && - network !== "TESTNET" && - network !== "DEVNET" - ) { - throw Error(`Unknown network: ${network}`); - } - let module = argv["module"] as "Core" | "NFTBridge" | "TokenBridge"; - let rpc = argv["rpc"] ?? NETWORKS[network][argv["chain"]].rpc; - if (argv["implementation-only"]) { - console.log( - await evm.getImplementation( - network, - argv["chain"], - module, - argv["contract-address"], - rpc - ) - ); - } else { - console.log( - JSON.stringify( - await evm.query_contract_evm( - network, - argv["chain"], - module, - argv["contract-address"], - rpc - ), - null, - 2 - ) - ); - } - } - ) - .command( - "hijack", - "Override the guardian set of the core bridge contract during testing (anvil or hardhat)", - (yargs) => { - return yargs - .option("core-contract-address", { - alias: "a", - describe: "Core contract address", - type: "string", - default: CONTRACTS.MAINNET.ethereum.core, - }) - .option("guardian-address", { - alias: "g", - required: true, - describe: "Guardians' public addresses (CSV)", - type: "string", - }) - .option("guardian-set-index", { - alias: "i", - required: false, - describe: - "New guardian set index (if unspecified, default to overriding the current index)", - type: "number", - }); - }, - async (argv) => { - const guardian_addresses = argv["guardian-address"].split(","); - let rpc = argv["rpc"] ?? NETWORKS.DEVNET.ethereum.rpc; - await evm.hijack_evm( - rpc, - argv["core-contract-address"], - guardian_addresses, - argv["guardian-set-index"] - ); - } - ); - }, - (_) => { - yargs.showHelp(); - } - ) - //////////////////////////////////////////////////////////////////////////////// - // Submit - .command( - "submit ", - "Execute a VAA", - (yargs) => { - // @ts-ignore - return yargs - .positional("vaa", { - describe: "vaa", - type: "string", - required: true, - }) - .option("chain", { - alias: "c", - describe: "chain name", - type: "string", - choices: Object.keys(CHAINS), - required: false, - }) - .option("network", { - alias: "n", - describe: "network", - type: "string", - choices: ["mainnet", "testnet", "devnet"], - required: true, - }) - .option("contract-address", { - alias: "a", - describe: "Contract to submit VAA to (override config)", - type: "string", - required: false, - }) - .option("rpc", { - describe: "RPC endpoint", - type: "string", - required: false, - }); - }, - async (argv) => { - const vaa_hex = String(argv.vaa); - const buf = Buffer.from(vaa_hex, "hex"); - const parsed_vaa = vaa.parse(buf); - - vaa.assertKnownPayload(parsed_vaa); - - console.log(parsed_vaa.payload); - - const network = argv.network.toUpperCase(); - if ( - network !== "MAINNET" && - network !== "TESTNET" && - network !== "DEVNET" - ) { - throw Error(`Unknown network: ${network}`); - } - - // We figure out the target chain to submit the VAA to. - // The VAA might specify this itself (for example a contract upgrade VAA - // or a token transfer VAA), in which case we just submit the VAA to - // that target chain. - // - // If the VAA does not have a target (e.g. chain registration VAAs or - // guardian set upgrade VAAs), we require the '--chain' argument to be - // set on the command line. - // - // As a sanity check, in the event that the VAA does specify a target - // and the '--chain' argument is also set, we issue an error if those - // two don't agree instead of silently taking the VAA's target chain. - - // get VAA chain - const vaa_chain_id = parsed_vaa.payload.chain; - assertChain(vaa_chain_id); - const vaa_chain = toChainName(vaa_chain_id); - - // get chain from command line arg - const cli_chain = argv["chain"]; - - let chain: ChainName; - if (cli_chain !== undefined) { - assertChain(cli_chain); - if (vaa_chain !== "unset" && cli_chain !== vaa_chain) { - throw Error( - `Specified target chain (${cli_chain}) does not match VAA target chain (${vaa_chain})` - ); - } - chain = cli_chain; - } else { - chain = vaa_chain; - } - - if (chain === "unset") { - throw Error( - "This VAA does not specify the target chain, please provide it by hand using the '--chain' flag." - ); - } else if (isEVMChain(chain)) { - const evm = require("./evm") - await evm.execute_evm( - parsed_vaa.payload, - buf, - network, - chain, - argv["contract-address"], - argv["rpc"] - ); - } else if (isTerraChain(chain)) { - const terra = require("./terra") - await terra.execute_terra(parsed_vaa.payload, buf, network, chain); - } else if (chain === "solana" || chain === "pythnet") { - const solana = require("./solana") - await solana.execute_solana(parsed_vaa, buf, network, chain); - } else if (chain === "algorand") { - const algorand = require("./algorand") - await algorand.execute_algorand( - parsed_vaa.payload, - new Uint8Array(Buffer.from(vaa_hex, "hex")), - network - ); - } else if (chain === "near") { - const near = require("./near") - await near.execute_near(parsed_vaa.payload, vaa_hex, network); - } else if (chain === "injective") { - const injective = require("./injective") - await injective.execute_injective(parsed_vaa.payload, buf, network); - } else if (chain === "xpla") { - const xpla = require("./xpla") - await xpla.execute_xpla(parsed_vaa.payload, buf, network); - } else if (chain === "osmosis") { - throw Error("OSMOSIS is not supported yet"); - } else if (chain === "sui") { - throw Error("SUI is not supported yet"); - } else if (chain === "aptos") { - const aptos = require("./aptos") - await aptos.execute_aptos( - parsed_vaa.payload, - buf, - network, - argv["contract-address"], - argv["rpc"] - ); - } else if (chain === "wormchain") { - throw Error("Wormchain is not supported yet"); - } else if (chain === "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(chain); - } - } - ) - .strict() - .demandCommand().argv; - -function hex(x: string): string { - return ethers.utils.hexlify(x, { allowMissingPrefix: true }); -} - -function evm_address(x: string): string { - return hex(x).substring(2).padStart(64, "0"); -} - -function parseAddress(chain: ChainName, address: string): string { - if (chain === "unset") { - throw Error("Chain unset"); - } else if (isEVMChain(chain)) { - return "0x" + evm_address(address); - } else if (isCosmWasmChain(chain)) { - return "0x" + toHex(fromBech32(address).data).padStart(64, "0"); - } else if (chain === "solana" || chain === "pythnet") { - return "0x" + toHex(base58.decode(address)).padStart(64, "0"); - } else if (chain === "algorand") { - // TODO: is there a better native format for algorand? - return "0x" + evm_address(address); - } else if (chain === "near") { - return "0x" + hex(address).substring(2).padStart(64, "0"); - } else if (chain === "osmosis") { - throw Error("OSMOSIS is not supported yet"); - } else if (chain === "sui") { - throw Error("SUI is not supported yet"); - } else if (chain === "aptos") { - if (/^(0x)?[0-9a-fA-F]+$/.test(address)) { - return "0x" + evm_address(address); - } - - return sha3_256(Buffer.from(address)); // address is hash of fully qualified type - } else if (chain === "wormchain") { - const sdk = require("@certusone/wormhole-sdk/lib/cjs/utils/array") - return "0x" + sdk.tryNativeToHexString(address, chain); - } else if (chain == "btc") { - throw Error("BTC is not supported yet") - } else { - impossible(chain); - } -} - -function parseCodeAddress(chain: ChainName, address: string): string { - if (isCosmWasmChain(chain)) { - return "0x" + parseInt(address, 10).toString(16).padStart(64, "0"); - } else { - return parseAddress(chain, address); - } -} +yargs(hideBin(process.argv)).commandDir("cmds").strict().demandCommand().argv; diff --git a/clients/js/near.ts b/clients/js/near.ts index 09366f213..3ed127170 100644 --- a/clients/js/near.ts +++ b/clients/js/near.ts @@ -131,6 +131,8 @@ export async function execute_near( case "ContractUpgrade": console.log("Upgrading core contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on near") default: impossible(payload); } @@ -145,6 +147,8 @@ export async function execute_near( case "ContractUpgrade": console.log("Upgrading contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on near") case "RegisterChain": console.log("Registering chain"); break; @@ -165,6 +169,8 @@ export async function execute_near( case "ContractUpgrade": console.log("Upgrading contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on near") case "RegisterChain": console.log("Registering chain"); break; diff --git a/clients/js/solana.ts b/clients/js/solana.ts index 1412cda3c..4e5478529 100644 --- a/clients/js/solana.ts +++ b/clients/js/solana.ts @@ -66,6 +66,8 @@ export async function execute_solana( vaa ); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on solana") default: ix = impossible(v.payload); } @@ -84,6 +86,8 @@ export async function execute_solana( vaa ); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on solana") case "RegisterChain": console.log("Registering chain"); ix = createNFTBridgeRegisterChainInstruction( @@ -115,6 +119,8 @@ export async function execute_solana( vaa ); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on solana") case "RegisterChain": console.log("Registering chain"); ix = createTokenBridgeRegisterChainInstruction( diff --git a/clients/js/terra.ts b/clients/js/terra.ts index 2e6414469..82f054cf4 100644 --- a/clients/js/terra.ts +++ b/clients/js/terra.ts @@ -51,6 +51,8 @@ export async function execute_terra( case "ContractUpgrade": console.log("Upgrading core contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on terra") default: impossible(payload); } @@ -72,6 +74,8 @@ export async function execute_terra( case "ContractUpgrade": console.log("Upgrading contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on terra") case "RegisterChain": console.log("Registering chain"); break; @@ -93,6 +97,8 @@ export async function execute_terra( case "ContractUpgrade": console.log("Upgrading contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on terra") case "RegisterChain": console.log("Registering chain"); break; diff --git a/clients/js/vaa.ts b/clients/js/vaa.ts index 862869f6a..51f40b808 100644 --- a/clients/js/vaa.ts +++ b/clients/js/vaa.ts @@ -70,12 +70,20 @@ export type Payload = | TokenBridgeTransferWithPayload | TokenBridgeAttestMeta | NFTBridgeTransfer + | CoreContractRecoverChainId + | PortalContractRecoverChainId<"TokenBridge"> + | PortalContractRecoverChainId<"NFTBridge"> export type ContractUpgrade = CoreContractUpgrade | PortalContractUpgrade<"TokenBridge"> | PortalContractUpgrade<"NFTBridge"> +export type RecoverChainId = + CoreContractRecoverChainId + | PortalContractRecoverChainId<"TokenBridge"> + | PortalContractRecoverChainId<"NFTBridge"> + export function parse(buffer: Buffer): VAA { const vaa = parseEnvelope(buffer) const parser = guardianSetUpgradeParser @@ -88,6 +96,9 @@ export function parse(buffer: Buffer): VAA { .or(tokenBridgeTransferWithPayloadParser()) .or(tokenBridgeAttestMetaParser()) .or(nftBridgeTransferParser()) + .or(coreContractRecoverChainId()) + .or(portalContractRecoverChainId("TokenBridge")) + .or(portalContractRecoverChainId("NFTBridge")) let payload : Payload | Other | null = parser.parse(vaa.payload) if (payload === null) { payload = {type: "Other", hex: Buffer.from(vaa.payload).toString("hex"), ascii: Buffer.from(vaa.payload).toString('utf8')} @@ -192,6 +203,9 @@ function vaaBody(vaa: VAA) { case "ContractUpgrade": payload_str = serialiseCoreContractUpgrade(payload) break + case "RecoverChainId": + payload_str = serialiseCoreContractRecoverChainId(payload) + break default: impossible(payload) break @@ -202,6 +216,9 @@ function vaaBody(vaa: VAA) { case "ContractUpgrade": payload_str = serialisePortalContractUpgrade(payload) break + case "RecoverChainId": + payload_str = serialisePortalContractRecoverChainId(payload) + break case "RegisterChain": payload_str = serialisePortalRegisterChain(payload) break @@ -218,6 +235,9 @@ function vaaBody(vaa: VAA) { case "ContractUpgrade": payload_str = serialisePortalContractUpgrade(payload) break + case "RecoverChainId": + payload_str = serialisePortalContractRecoverChainId(payload) + break case "RegisterChain": payload_str = serialisePortalRegisterChain(payload) break @@ -468,6 +488,95 @@ function serialisePortalRegisterChain { + return new P(new Parser() + .endianess("big") + .string("module", { + length: 32, + encoding: "hex", + assert: Buffer.from("Core").toString("hex").padStart(64, "0"), + formatter: (_str) => "Core" + }) + .uint8("type", { + assert: 5, + formatter: (_action) => "RecoverChainId" + }) + .array("evmChainId", { + type: "uint8", + lengthInBytes: 32, + formatter: (bytes) => BigNumber.from(bytes).toBigInt() + }) + .uint16("newChainId") + .string("end", { + greedy: true, + assert: str => str === "" + })) +} + +function serialiseCoreContractRecoverChainId(payload: CoreContractRecoverChainId): string { + const body = [ + encode("bytes32", encodeString(payload.module)), + encode("uint8", 5), + encode("uint256", payload.evmChainId), + encode("uint16", payload.newChainId) + ] + return body.join("") +} + +export interface PortalContractRecoverChainId { + module: Module + type: "RecoverChainId" + evmChainId: bigint + newChainId: number +} + +// Parse a portal contract recoverChainId payload +function portalContractRecoverChainId(module: Module): P> { + return new P(new Parser() + .endianess("big") + .string("module", { + length: 32, + encoding: "hex", + assert: Buffer.from(module).toString("hex").padStart(64, "0"), + formatter: (_str: string) => module + }) + .uint8("type", { + assert: 3, + formatter: (_action: number) => "RecoverChainId" + }) + .array("evmChainId", { + type: "uint8", + lengthInBytes: 32, + formatter: (bytes) => BigNumber.from(bytes).toBigInt() + }) + .uint16("newChainId") + .string("end", { + greedy: true, + assert: str => str === "" + })) +} + +function serialisePortalContractRecoverChainId(payload: PortalContractRecoverChainId): string { + const body = [ + encode("bytes32", encodeString(payload.module)), + encode("uint8", 3), + encode("uint256", payload.evmChainId), + encode("uint16", payload.newChainId) + ] + return body.join("") +} + //////////////////////////////////////////////////////////////////////////////// // Token bridge diff --git a/clients/js/xpla.ts b/clients/js/xpla.ts index c5cc41fe2..eb56305fa 100644 --- a/clients/js/xpla.ts +++ b/clients/js/xpla.ts @@ -48,6 +48,8 @@ export async function execute_xpla( case "ContractUpgrade": console.log("Upgrading core contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on XPLA") default: impossible(payload); } @@ -57,7 +59,7 @@ export async function execute_xpla( // NOTE: this code can safely be removed once the terra NFT bridge is // released, but it's fine for it to stay, as the condition will just be // skipped once 'contracts.nft_bridge' is defined - throw new Error("NFT bridge not supported yet for terra"); + throw new Error("NFT bridge not supported yet for XPLA"); } target_contract = contracts.nft_bridge; execute_msg = { @@ -69,6 +71,8 @@ export async function execute_xpla( case "ContractUpgrade": console.log("Upgrading contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on XPLA") case "RegisterChain": console.log("Registering chain"); break; @@ -90,6 +94,8 @@ export async function execute_xpla( case "ContractUpgrade": console.log("Upgrading contract"); break; + case "RecoverChainId": + throw new Error("RecoverChainId not supported on XPLA") case "RegisterChain": console.log("Registering chain"); break;