[multisig-cli] Add support for json (#430)

* Draft

* Add verify for instruction payload

* Typos

* Refactor json parsing
This commit is contained in:
guibescos 2022-12-15 21:45:58 +08:00 committed by GitHub
parent e484f5cbb7
commit 51754457a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 175 additions and 58 deletions

View File

@ -165,7 +165,8 @@ program
"multisig wallet secret key filepath", "multisig wallet secret key filepath",
"keys/key.json" "keys/key.json"
) )
.option("-p, --payload <hex-string>", "payload to sign", "0xdeadbeef") .option("-f, --file <filepath>", "Path to a json file with instructions")
.option("-p, --payload <hex-string>", "Wormhole VAA payload")
.option("-s, --skip-duplicate-check", "Skip checking duplicates") .option("-s, --skip-duplicate-check", "Skip checking duplicates")
.action(async (options) => { .action(async (options) => {
const cluster: Cluster = options.cluster; const cluster: Cluster = options.cluster;
@ -176,6 +177,13 @@ program
options.ledgerDerivationChange, options.ledgerDerivationChange,
options.wallet options.wallet
); );
if (options.payload && options.file) {
console.log("Only one of --payload or --file must be provided");
return;
}
if (options.payload) {
const wormholeTools = await loadWormholeTools(cluster, squad.connection); const wormholeTools = await loadWormholeTools(cluster, squad.connection);
if (!options.skipDuplicateCheck) { if (!options.skipDuplicateCheck) {
@ -220,11 +228,52 @@ program
options.payload, options.payload,
wormholeTools wormholeTools
); );
}
if (options.file) {
const instructions: SquadInstruction[] = loadInstructionsFromJson(
options.file
);
if (!options.skipDuplicateCheck) {
const activeProposals = await getActiveProposals(
squad,
CONFIG[cluster].vault
);
const activeInstructions = await getManyProposalsInstructions(
squad,
activeProposals
);
for (let i = 0; i < activeProposals.length; i++) {
if (
areEqualOnChainInstructions(
instructions.map((ix) => ix.instruction),
activeInstructions[i]
)
) {
console.log(
`❌ Skipping, instructions from ${options.file} match instructions at ${activeProposals[i].publicKey}`
);
return;
}
}
}
const txKey = await createTx(squad, CONFIG[cluster].vault);
await addInstructionsToTx(
cluster,
squad,
CONFIG[cluster].vault,
txKey,
instructions
);
}
}); });
program program
.command("verify") .command("verify")
.description("Verify given wormhole transaction has the given payload") .description("Verify given proposal matches a payload")
.option("-c, --cluster <network>", "solana cluster to use", "devnet") .option("-c, --cluster <network>", "solana cluster to use", "devnet")
.option("-l, --ledger", "use ledger") .option("-l, --ledger", "use ledger")
.option( .option(
@ -240,7 +289,8 @@ program
"multisig wallet secret key filepath", "multisig wallet secret key filepath",
"keys/key.json" "keys/key.json"
) )
.requiredOption("-p, --payload <hex-string>", "expected payload") .option("-p, --payload <hex-string>", "expected wormhole payload")
.option("-f, --file <filepath>", "Path to a json file with instructions")
.requiredOption("-t, --tx-pda <address>", "transaction PDA") .requiredOption("-t, --tx-pda <address>", "transaction PDA")
.action(async (options) => { .action(async (options) => {
const cluster: Cluster = options.cluster; const cluster: Cluster = options.cluster;
@ -251,6 +301,12 @@ program
options.ledgerDerivationChange, options.ledgerDerivationChange,
options.wallet options.wallet
); );
if (options.payload && options.file) {
console.log("Only one of --payload or --file must be provided");
return;
}
const wormholeTools = await loadWormholeTools(cluster, squad.connection); const wormholeTools = await loadWormholeTools(cluster, squad.connection);
let onChainInstructions = await getProposalInstructions( let onChainInstructions = await getProposalInstructions(
@ -264,6 +320,7 @@ program
msAccount.authorityIndex msAccount.authorityIndex
); );
if (options.payload) {
if ( if (
hasWormholePayload( hasWormholePayload(
squad, squad,
@ -280,6 +337,26 @@ program
} else { } else {
console.log("❌ This proposal does not match the given payload."); console.log("❌ This proposal does not match the given payload.");
} }
}
if (options.file) {
const instructions: SquadInstruction[] = loadInstructionsFromJson(
options.file
);
if (
areEqualOnChainInstructions(
instructions.map((ix) => ix.instruction),
onChainInstructions
)
) {
console.log(
"✅ This proposal is verified to be created with the given instructions."
);
} else {
console.log("❌ This proposal does not match the given instructions.");
}
}
}); });
program program
@ -730,6 +807,24 @@ async function createWormholeMsgMultisigTx(
); );
} }
function areEqualOnChainInstructions(
instructions: TransactionInstruction[],
onChainInstructions: InstructionAccount[]
): boolean {
if (instructions.length != onChainInstructions.length) {
console.debug(
`Proposals have a different number of instructions ${instructions.length} vs ${onChainInstructions.length}`
);
return false;
} else {
return lodash
.range(0, instructions.length)
.every((i) =>
isEqualOnChainInstruction(instructions[i], onChainInstructions[i])
);
}
}
function hasWormholePayload( function hasWormholePayload(
squad: Squads, squad: Squads,
emitter: PublicKey, emitter: PublicKey,
@ -980,3 +1075,25 @@ async function removeMember(
squadIxs squadIxs
); );
} }
function loadInstructionsFromJson(path: string): SquadInstruction[] {
const inputInstructions = JSON.parse(fs.readFileSync(path).toString());
const instructions: SquadInstruction[] = inputInstructions.map(
(ix: any): SquadInstruction => {
return {
instruction: new TransactionInstruction({
programId: new PublicKey(ix.program_id),
keys: ix.accounts.map((acc: any) => {
return {
pubkey: new PublicKey(acc.pubkey),
isSigner: acc.is_signer,
isWritable: acc.is_writable,
};
}),
data: Buffer.from(ix.data, "hex"),
}),
};
}
);
return instructions;
}