wormhole/clients/js/main.ts

947 lines
30 KiB
JavaScript

#!/usr/bin/env node
// <sigh>
// when the native secp256k1 is missing, the eccrypto library decides TO PRINT A MESSAGE TO STDOUT:
// https://github.com/bitchan/eccrypto/blob/a4f4a5f85ef5aa1776dfa1b7801cad808264a19c/index.js#L23
//
// do you use a CLI tool that depends on that library and try to pipe the output
// of the tool into another? tough luck
//
// for lack of a better way to stop this, we patch the console.info function to
// drop that particular message...
// </sigh>
const info = console.info;
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(
"\x1b[33m%s\x1b[0m",
"WARNING: 'worm' is out of date. Run 'worm update' to update."
);
}
const GOVERNANCE_CHAIN = 1;
const GOVERNANCE_EMITTER =
"0000000000000000000000000000000000000000000000000000000000000004";
const OVERRIDES = {
MAINNET: {
aptos: {
token_bridge:
"0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f",
core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625",
},
},
TESTNET: {
aptos: {
token_bridge:
"0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f",
core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625",
},
},
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<Payload> {
let v: VAA<Payload> = {
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<typeof module> = {
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 <vaa>",
"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 <digest> <signature>",
"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 <network> <chain> <module>",
"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 <chain>",
"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 <network> <chain>",
"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 <file>",
"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 <file>",
"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 <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 <vaa>",
"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);
}
}