287 lines
8.1 KiB
TypeScript
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;
|
|
});
|
|
}
|