424 lines
15 KiB
TypeScript
424 lines
15 KiB
TypeScript
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";
|
|
|
|
type Network = "MAINNET" | "TESTNET" | "DEVNET"
|
|
|
|
interface Package {
|
|
meta_file: string,
|
|
mv_files: string[]
|
|
}
|
|
|
|
interface PackageBCS {
|
|
meta: Uint8Array,
|
|
bytecodes: Uint8Array,
|
|
codeHash: Uint8Array
|
|
}
|
|
|
|
const network_options = {
|
|
alias: "n",
|
|
describe: "network",
|
|
type: "string",
|
|
choices: ["mainnet", "testnet", "devnet"],
|
|
required: true,
|
|
} as const;
|
|
|
|
const rpc_description = {
|
|
alias: "r",
|
|
describe: "Override default rpc endpoint url",
|
|
type: "string",
|
|
required: false,
|
|
} as const;
|
|
|
|
const named_addresses = {
|
|
describe: "Named addresses in the format addr1=0x0,addr2=0x1,...",
|
|
type: "string",
|
|
require: false
|
|
} as const;
|
|
|
|
// TODO(csongor): this could be useful elsewhere
|
|
function assertNetwork(n: string): asserts n is Network {
|
|
if (
|
|
n !== "MAINNET" &&
|
|
n !== "TESTNET" &&
|
|
n !== "DEVNET"
|
|
) {
|
|
throw Error(`Unknown network: ${n}`);
|
|
}
|
|
}
|
|
|
|
exports.command = 'aptos';
|
|
exports.desc = 'Aptos utilities';
|
|
exports.builder = function(y: typeof yargs) {
|
|
return y
|
|
// NOTE: there's no init-nft-bridge, because the native module initialiser
|
|
// functionality has stabilised on mainnet, so we just use that one (which
|
|
// gets called automatically)
|
|
.command("init-token-bridge", "Init token bridge contract", (yargs) => {
|
|
return yargs
|
|
.option("network", network_options)
|
|
.option("rpc", rpc_description)
|
|
}, async (argv) => {
|
|
const network = argv.network.toUpperCase();
|
|
assertNetwork(network);
|
|
const contract_address = evm_address(CONTRACTS[network].aptos.token_bridge);
|
|
const rpc = argv.rpc ?? NETWORKS[network]["aptos"].rpc;
|
|
await callEntryFunc(network, rpc, `${contract_address}::token_bridge`, "init", [], []);
|
|
})
|
|
.command("init-wormhole", "Init Wormhole core contract", (yargs) => {
|
|
return yargs
|
|
.option("network", network_options)
|
|
.option("rpc", rpc_description)
|
|
.option("chain-id", {
|
|
describe: "Chain id",
|
|
type: "number",
|
|
default: CHAIN_ID_APTOS,
|
|
required: false
|
|
})
|
|
.option("governance-chain-id", {
|
|
describe: "Governance chain id",
|
|
type: "number",
|
|
default: CHAIN_ID_SOLANA,
|
|
required: false
|
|
})
|
|
.option("governance-address", {
|
|
describe: "Governance address",
|
|
type: "string",
|
|
default: "0x0000000000000000000000000000000000000000000000000000000000000004",
|
|
required: false
|
|
})
|
|
.option("guardian-address", {
|
|
alias: "g",
|
|
required: true,
|
|
describe: "Initial guardian's addresses (CSV)",
|
|
type: "string",
|
|
})
|
|
}, async (argv) => {
|
|
const network = argv.network.toUpperCase();
|
|
assertNetwork(network);
|
|
|
|
const contract_address = evm_address(CONTRACTS[network].aptos.core);
|
|
const guardian_addresses = argv["guardian-address"].split(",").map(address => evm_address(address).substring(24));
|
|
const chain_id = argv["chain-id"];
|
|
const governance_address = evm_address(argv["governance-address"]);
|
|
const governance_chain_id = argv["governance-chain-id"];
|
|
|
|
const guardians_serializer = new BCS.Serializer();
|
|
guardians_serializer.serializeU32AsUleb128(guardian_addresses.length);
|
|
guardian_addresses.forEach(address => guardians_serializer.serializeBytes(Buffer.from(address, "hex")));
|
|
|
|
const args = [
|
|
BCS.bcsSerializeUint64(chain_id),
|
|
BCS.bcsSerializeUint64(governance_chain_id),
|
|
BCS.bcsSerializeBytes(Buffer.from(governance_address, "hex")),
|
|
guardians_serializer.getBytes()
|
|
]
|
|
const rpc = argv.rpc ?? NETWORKS[network]["aptos"].rpc;
|
|
await callEntryFunc(network, rpc, `${contract_address}::wormhole`, "init", [], args);
|
|
})
|
|
.command("deploy <package-dir>", "Deploy an Aptos package", (yargs) => {
|
|
return yargs
|
|
.positional("package-dir", {
|
|
type: "string"
|
|
})
|
|
.option("network", network_options)
|
|
.option("rpc", rpc_description)
|
|
.option("named-addresses", named_addresses)
|
|
}, async (argv) => {
|
|
const network = argv.network.toUpperCase();
|
|
assertNetwork(network);
|
|
checkAptosBinary();
|
|
const p = buildPackage(argv["package-dir"], argv["named-addresses"]);
|
|
const b = serializePackage(p);
|
|
const rpc = argv.rpc ?? NETWORKS[network]["aptos"].rpc;
|
|
await callEntryFunc(network, rpc, "0x1::code", "publish_package_txn", [], [b.meta, b.bytecodes])
|
|
console.log("Deployed:", p.mv_files)
|
|
})
|
|
.command("deploy-resource <seed> <package-dir>", "Deploy an Aptos package using a resource account", (yargs) => {
|
|
return yargs
|
|
.positional("seed", {
|
|
type: "string"
|
|
})
|
|
.positional("package-dir", {
|
|
type: "string"
|
|
})
|
|
.option("network", network_options)
|
|
.option("rpc", rpc_description)
|
|
.option("named-addresses", named_addresses)
|
|
}, async (argv) => {
|
|
const network = argv.network.toUpperCase();
|
|
assertNetwork(network);
|
|
checkAptosBinary();
|
|
const p = buildPackage(argv["package-dir"], argv["named-addresses"]);
|
|
const b = serializePackage(p);
|
|
const seed = Buffer.from(argv["seed"], "ascii")
|
|
|
|
// TODO(csongor): use deployer address from sdk (when it's there)
|
|
let module_name = "0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b::deployer";
|
|
if (network == "TESTNET" || network == "MAINNET") {
|
|
module_name = "0x0108bc32f7de18a5f6e1e7d6ee7aff9f5fc858d0d87ac0da94dd8d2a5d267d6b::deployer";
|
|
}
|
|
const rpc = argv.rpc ?? NETWORKS[network]["aptos"].rpc;
|
|
await callEntryFunc(
|
|
network,
|
|
rpc,
|
|
module_name,
|
|
"deploy_derived",
|
|
[],
|
|
[
|
|
b.meta,
|
|
b.bytecodes,
|
|
BCS.bcsSerializeBytes(seed)
|
|
])
|
|
console.log("Deployed:", p.mv_files)
|
|
})
|
|
.command("send-example-message <message>", "Send example message", (yargs) => {
|
|
return yargs
|
|
.positional("message", {
|
|
type: "string"
|
|
})
|
|
.option("network", network_options)
|
|
}, async (argv) => {
|
|
const network = argv.network.toUpperCase();
|
|
assertNetwork(network);
|
|
const rpc = NETWORKS[network]["aptos"].rpc;
|
|
// TODO(csongor): use sdk address
|
|
let module_name = "0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b::sender";
|
|
if (network == "TESTNET" || network == "MAINNET") {
|
|
module_name = "0x0108bc32f7de18a5f6e1e7d6ee7aff9f5fc858d0d87ac0da94dd8d2a5d267d6b::sender";
|
|
}
|
|
await callEntryFunc(network, rpc, module_name, "send_message", [], [BCS.bcsSerializeBytes(Buffer.from(argv["message"], "ascii"))])
|
|
})
|
|
.command("derive-resource-account <account> <seed>", "Derive resource account address", (yargs) => {
|
|
return yargs
|
|
.positional("account", {
|
|
type: "string"
|
|
})
|
|
.positional("seed", {
|
|
type: "string"
|
|
})
|
|
}, async (argv) => {
|
|
console.log(deriveResourceAccount(Buffer.from(hex(argv['account']).substring(2), 'hex'), argv['seed']))
|
|
})
|
|
.command("derive-wrapped-address <chain> <origin-address>", "Derive wrapped coin type", (yargs) => {
|
|
return yargs
|
|
.positional("chain", {
|
|
type: "string"
|
|
})
|
|
.positional("origin-address", {
|
|
type: "string"
|
|
})
|
|
.option("network", network_options)
|
|
}, async (argv) => {
|
|
const network = argv.network.toUpperCase();
|
|
assertNetwork(network);
|
|
let address = CONTRACTS[network].aptos.token_bridge;
|
|
if (address.startsWith("0x")) address = address.substring(2);
|
|
const token_bridge_address = Buffer.from(address, "hex");
|
|
assertChain(argv["chain"]);
|
|
const chain = coalesceChainId(argv["chain"]);
|
|
const origin_address = Buffer.from(evm_address(argv["origin-address"]), "hex");
|
|
console.log(deriveWrappedAssetAddress(token_bridge_address, chain, origin_address))
|
|
})
|
|
.command("hash-contracts <package-dir>", "Hash contract bytecodes for upgrade", (yargs) => {
|
|
return yargs
|
|
.positional("seed", {
|
|
type: "string"
|
|
})
|
|
.positional("package-dir", {
|
|
type: "string"
|
|
})
|
|
.option("named-addresses", named_addresses)
|
|
}, (argv) => {
|
|
checkAptosBinary();
|
|
const p = buildPackage(argv["package-dir"], argv["named-addresses"]);
|
|
const b = serializePackage(p);
|
|
console.log(Buffer.from(b.codeHash).toString("hex"));
|
|
})
|
|
.command("upgrade <package-dir>", "Perform upgrade after VAA has been submitted", (_yargs) => {
|
|
return yargs
|
|
.positional("package-dir", {
|
|
type: "string"
|
|
})
|
|
// TODO(csongor): once the sdk has the addresses, just look that up
|
|
// based on the module
|
|
.option("contract-address", {
|
|
alias: "a",
|
|
required: true,
|
|
describe: "Address where the wormhole module is deployed",
|
|
type: "string",
|
|
})
|
|
.option("network", network_options)
|
|
.option("rpc", rpc_description)
|
|
.option("named-addresses", named_addresses)
|
|
}, async (argv) => {
|
|
const network = argv.network.toUpperCase();
|
|
assertNetwork(network);
|
|
checkAptosBinary();
|
|
const p = buildPackage(argv["package-dir"], argv["named-addresses"]);
|
|
const b = serializePackage(p);
|
|
const rpc = argv.rpc ?? NETWORKS[network]["aptos"].rpc;
|
|
// TODO(csongor): use deployer address from sdk (when it's there)
|
|
const hash = await callEntryFunc(
|
|
network,
|
|
rpc,
|
|
`${argv["contract-address"]}::contract_upgrade`,
|
|
"upgrade",
|
|
[],
|
|
[
|
|
b.meta,
|
|
b.bytecodes,
|
|
])
|
|
console.log("Deployed:", p.mv_files)
|
|
console.log(hash)
|
|
})
|
|
.command("migrate", "Perform migration after contract upgrade", (_yargs) => {
|
|
return yargs
|
|
// TODO(csongor): once the sdk has the addresses, just look that up
|
|
// based on the module
|
|
.option("contract-address", {
|
|
alias: "a",
|
|
required: true,
|
|
describe: "Address where the wormhole module is deployed",
|
|
type: "string",
|
|
})
|
|
.option("network", network_options)
|
|
.option("rpc", rpc_description)
|
|
}, async (argv) => {
|
|
const network = argv.network.toUpperCase();
|
|
assertNetwork(network);
|
|
checkAptosBinary();
|
|
const rpc = argv.rpc ?? NETWORKS[network]["aptos"].rpc;
|
|
// TODO(csongor): use deployer address from sdk (when it's there)
|
|
const hash = await callEntryFunc(
|
|
network,
|
|
rpc,
|
|
`${argv["contract-address"]}::contract_upgrade`,
|
|
"migrate",
|
|
[],
|
|
[])
|
|
console.log(hash)
|
|
})
|
|
// TODO - make faucet support testnet in additional to localnet
|
|
.command("faucet", "Request money from the faucet for a given account", (yargs) => {
|
|
return yargs
|
|
.option("rpc", rpc_description)
|
|
.option("faucet", {
|
|
alias: "f",
|
|
required: false,
|
|
describe: "Faucet url",
|
|
type: "string",
|
|
})
|
|
.option("amount", {
|
|
alias: "m",
|
|
required: false,
|
|
describe: "Amount to request",
|
|
type: "number",
|
|
})
|
|
.option("account", {
|
|
alias: "a",
|
|
required: false,
|
|
describe: "Account to fund",
|
|
type: "string",
|
|
})
|
|
},
|
|
async (argv) => {
|
|
let NODE_URL = "http://0.0.0.0:8080/v1";
|
|
let FAUCET_URL = "http://0.0.0.0:8081";
|
|
let account = "0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b";
|
|
let amount = 40000000;
|
|
|
|
if (argv.faucet != undefined) {
|
|
FAUCET_URL = argv.faucet as string;
|
|
}
|
|
if (argv.rpc != undefined) {
|
|
NODE_URL = argv.rpc as string;
|
|
}
|
|
if (argv.amount != undefined) {
|
|
amount = argv.amount as number;
|
|
}
|
|
if (argv.account != undefined) {
|
|
account = argv.account as string;
|
|
}
|
|
const faucetClient = new FaucetClient(NODE_URL, FAUCET_URL);
|
|
await faucetClient.fundAccount(account, amount);
|
|
console.log(`Funded ${account} with ${amount} coins`);
|
|
})
|
|
.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"]);
|
|
if (aptos.status !== 0) {
|
|
console.error("aptos is not installed. Please install aptos and try again.");
|
|
console.error(`See ${dir}/README.md for instructions.`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function buildPackage(dir: string, addrs?: string): Package {
|
|
const named_addresses =
|
|
addrs
|
|
? ["--named-addresses", addrs]
|
|
: [];
|
|
const aptos = spawnSync("aptos",
|
|
["move", "compile", "--save-metadata", "--included-artifacts", "none", "--package-dir", dir, ...named_addresses])
|
|
if (aptos.status !== 0) {
|
|
console.error(aptos.stderr.toString('utf8'))
|
|
console.error(aptos.stdout.toString('utf8'))
|
|
process.exit(1)
|
|
}
|
|
|
|
const result: any = JSON.parse(aptos.stdout.toString('utf8'))
|
|
const buildDirs =
|
|
fs.readdirSync(`${dir}/build`, { withFileTypes: true })
|
|
.filter(dirent => dirent.isDirectory())
|
|
.map(dirent => dirent.name)
|
|
if (buildDirs.length !== 1) {
|
|
console.error(`Unexpected directory structure in ${dir}/build: expected a single directory`)
|
|
process.exit(1)
|
|
}
|
|
const buildDir = `${dir}/build/${buildDirs[0]}`
|
|
return {
|
|
meta_file: `${buildDir}/package-metadata.bcs`,
|
|
mv_files: result["Result"].map((mod: string) => `${buildDir}/bytecode_modules/${mod.split("::")[1]}.mv`)
|
|
}
|
|
}
|
|
|
|
function serializePackage(p: Package): PackageBCS {
|
|
const metaBytes = fs.readFileSync(p.meta_file);
|
|
const packageMetadataSerializer = new BCS.Serializer();
|
|
packageMetadataSerializer.serializeBytes(metaBytes)
|
|
const serializedPackageMetadata = packageMetadataSerializer.getBytes();
|
|
|
|
const modules = p.mv_files.map(file => fs.readFileSync(file))
|
|
const serializer = new BCS.Serializer();
|
|
serializer.serializeU32AsUleb128(modules.length);
|
|
modules.forEach(module => serializer.serializeBytes(module));
|
|
const serializedModules = serializer.getBytes();
|
|
|
|
const hashes = [metaBytes].concat(modules).map((x) => Buffer.from(sha3.keccak256(x), "hex"));
|
|
const codeHash = Buffer.from(sha3.keccak256(Buffer.concat(hashes)), "hex")
|
|
|
|
return {
|
|
meta: serializedPackageMetadata,
|
|
bytecodes: serializedModules,
|
|
codeHash
|
|
}
|
|
}
|