wormhole/clients/js/cmds/edit-vaa.ts

287 lines
8.1 KiB
TypeScript

// The edit-vaa command allows the user to create, update or sign a VAA. It queries the core contract on Ethereum
// to get the guardian set. It can take signature data from wormscan or (in the case of testnet or devnet) it can
// take a guardian secret as input.
//
// Sign a VAA using signatures from wormscan:
// worm edit-vaa -n mainnet --vaa $VAA --wormscanurl https://api.wormscan.io/api/v1/observations/1/0000000000000000000000000000000000000000000000000000000000000004/651169458827220885
//
// Create the same VAA from scratch:
// worm edit-vaa -n mainnet \
// --ec 1 --ea 0x0000000000000000000000000000000000000000000000000000000000000004 \
// --gsi 3 --sequence 651169458827220885 --nonce 2166843495 --cl 32 \
// --payload 000000000000000000000000000000436972636c65496e746567726174696f6e020002000600000000000000000000000009fb06a271faff70a651047395aaeb6265265f1300000001 \
// --wormscanurl https://api.wormscan.io/api/v1/observations/1/0000000000000000000000000000000000000000000000000000000000000004/651169458827220885
//
// Sign a VAA using the testnet guardian key:
// worm edit-vaa --vaa $VAA --gs $TESTNET_GUARDIAN_SECRET
//
import yargs from "yargs";
import axios from "axios";
import { ethers } from "ethers";
import { Other } from "@certusone/wormhole-sdk";
import { Implementation__factory } from "@certusone/wormhole-sdk/lib/cjs/ethers-contracts";
import { CONTRACTS } from "@certusone/wormhole-sdk/lib/cjs/utils/consts";
import { NETWORKS } from "../networks";
import { parse, Payload, serialiseVAA, sign, Signature, VAA } from "../vaa";
exports.command = "edit-vaa";
exports.desc = "Edits or generates a VAA";
exports.builder = (y: typeof yargs) => {
return y
.option("vaa", {
alias: "v",
describe: "vaa in hex format",
type: "string",
})
.option("network", {
alias: "n",
describe: "network",
type: "string",
choices: ["mainnet", "testnet", "devnet"],
})
.option("guardian-set-index", {
alias: "gsi",
describe: "guardian set index",
type: "number",
})
.option("signatures", {
alias: "sigs",
describe: "comma separated list of signatures",
type: "string",
})
.option("wormscanurl", {
alias: "wsu",
describe: "url to wormscan entry for the vaa that includes signatures",
type: "string",
})
.option("wormscanfile", {
alias: "wsf",
describe:
"json file containing wormscan entry for the vaa that includes signatures",
type: "string",
})
.option("emitter-chain-id", {
alias: "ec",
describe: "emitter chain id to be used in the vaa",
type: "number",
required: false,
})
.option("emitter-address", {
alias: "ea",
describe: "emitter address to be used in the vaa",
type: "string",
})
.option("nonce", {
alias: "no",
describe: "nonce to be used in the vaa",
type: "number",
})
.option("sequence", {
alias: "seq",
describe: "sequence number to be used in the vaa",
type: "string",
})
.option("consistency-level", {
alias: "cl",
describe: "consistency level to be used in the vaa",
type: "number",
})
.option("timestamp", {
alias: "ts",
describe: "timestamp to be used in the vaa in unix seconds",
type: "number",
})
.option("payload", {
alias: "p",
describe: "payload in hex format",
type: "string",
})
.option("guardian-secret", {
alias: "gs",
describe: "Guardian's secret key",
type: "string",
});
};
exports.handler = async (argv) => {
let numSigs = 0;
if (argv["signatures"]) {
numSigs += 1;
}
if (argv["wormscanfile"]) {
numSigs += 1;
}
if (argv["wormscanurl"]) {
numSigs += 1;
}
if (argv["guardian-secret"]) {
numSigs += 1;
}
if (numSigs > 1) {
throw new Error(
`may only specify one of "--signatures", "--wormscanfile", "--wormscanurl" or "--guardian-secret"`
);
}
let vaa: VAA<Payload | Other>;
if (argv["vaa"]) {
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");
}
}
vaa = parse(buf);
} else {
vaa = {
version: 1,
guardianSetIndex: 0,
signatures: [],
timestamp: 0,
nonce: 0,
emitterChain: 0,
emitterAddress: "0x0",
sequence: BigInt(Math.floor(Math.random() * 100000000)),
consistencyLevel: 0,
payload: {
type: "Other",
hex: `00`,
},
};
}
if (argv["guardian-set-index"]) {
vaa.guardianSetIndex = Number(argv["guardian-set-index"]);
}
if (argv["signatures"]) {
vaa.signatures = argv["signatures"].split(",");
} else if (argv["wormscanfile"]) {
const wormscanData = require(argv["wormscanfile"]);
const guardianSet = await getGuardianSet(
argv["network"],
vaa.guardianSetIndex
);
vaa.signatures = await getSigsFromWormscanData(wormscanData, guardianSet);
} else if (argv["wormscanurl"]) {
const wormscanData = await axios.get(argv["wormscanurl"]);
const guardianSet = await getGuardianSet(
argv["network"],
vaa.guardianSetIndex
);
vaa.signatures = await getSigsFromWormscanData(
wormscanData.data,
guardianSet
);
} else if (argv["guardian-secret"]) {
vaa.guardianSetIndex = 0;
vaa.signatures = sign([argv["guardian-secret"]], vaa as VAA<Payload>);
}
if (argv["emitter-chain-id"]) {
vaa.emitterChain = argv["emitter-chain-id"];
}
if (argv["emitter-address"]) {
vaa.emitterAddress = argv["emitter-address"];
}
if (argv["nonce"]) {
vaa.nonce = argv["nonce"];
}
if (argv["sequence"]) {
vaa.sequence = BigInt(argv["sequence"]);
}
if (argv["consistency-level"]) {
vaa.consistencyLevel = argv["consistency-level"];
}
if (argv["timestamp"]) {
vaa.timestamp = argv["timestamp"];
}
if (argv["payload"]) {
vaa.payload = {
type: "Other",
hex: argv["payload"],
};
}
console.log(serialiseVAA(vaa as unknown as VAA<Payload>));
};
// getGuardianSet queries the core contract on Ethereum for the guardian set and returns it.
async function getGuardianSet(
nwork: string,
guardianSetIndex: number
): Promise<string[]> {
if (!nwork) {
throw Error(`"--network" is required to read guardian set`);
}
const network = nwork.toUpperCase();
if (network !== "MAINNET" && network !== "TESTNET" && network !== "DEVNET") {
throw Error(`Unknown network: ${network}`);
}
let n = NETWORKS[network]["ethereum"];
let contract_address = CONTRACTS[network]["ethereum"].core;
if (contract_address === undefined) {
throw Error(`Unknown core contract on ${network} for ethereum`);
}
const provider = new ethers.providers.JsonRpcProvider(n.rpc);
const contract = Implementation__factory.connect(contract_address, provider);
const result = await contract.getGuardianSet(guardianSetIndex);
return result[0];
}
// getSigsFromWormscanData reads the guardian address / signature pairs from the wormscan data
// and generates an array of signature objects. It then sorts them into order by address.
function getSigsFromWormscanData(
wormscanData: any,
guardianSet: string[]
): any {
let sigs: Signature[] = [];
for (let data in wormscanData) {
let guardianAddr = wormscanData[data].guardianAddr;
let gsi = -1;
for (let idx = 0; idx < guardianSet.length; idx++) {
if (guardianSet[idx] === guardianAddr) {
gsi = idx;
break;
}
}
if (gsi < 0) {
throw new Error("Failed to look up guardian address " + guardianAddr);
}
let sig: Signature = {
guardianSetIndex: gsi,
signature: Buffer.from(wormscanData[data].signature, "base64").toString(
"hex"
),
};
sigs.push(sig);
}
return sigs.sort((s1, s2) => {
if (s1.guardianSetIndex > s2.guardianSetIndex) {
return 1;
}
if (s1.guardianSetIndex < s2.guardianSetIndex) {
return -1;
}
return 0;
});
}