[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,55 +177,103 @@ program
options.ledgerDerivationChange, options.ledgerDerivationChange,
options.wallet options.wallet
); );
const wormholeTools = await loadWormholeTools(cluster, squad.connection);
if (!options.skipDuplicateCheck) { if (options.payload && options.file) {
const activeProposals = await getActiveProposals( console.log("Only one of --payload or --file must be provided");
squad, return;
CONFIG[cluster].vault
);
const activeInstructions = await getManyProposalsInstructions(
squad,
activeProposals
);
const msAccount = await squad.getMultisig(CONFIG[cluster].vault);
const emitter = squad.getAuthorityPDA(
msAccount.publicKey,
msAccount.authorityIndex
);
for (let i = 0; i < activeProposals.length; i++) {
if (
hasWormholePayload(
squad,
emitter,
activeProposals[i].publicKey,
options.payload,
activeInstructions[i],
wormholeTools
)
) {
console.log(
`❌ Skipping, payload ${options.payload} matches instructions at ${activeProposals[i].publicKey}`
);
return;
}
}
} }
await createWormholeMsgMultisigTx( if (options.payload) {
options.cluster, const wormholeTools = await loadWormholeTools(cluster, squad.connection);
squad,
CONFIG[cluster].vault, if (!options.skipDuplicateCheck) {
options.payload, const activeProposals = await getActiveProposals(
wormholeTools squad,
); CONFIG[cluster].vault
);
const activeInstructions = await getManyProposalsInstructions(
squad,
activeProposals
);
const msAccount = await squad.getMultisig(CONFIG[cluster].vault);
const emitter = squad.getAuthorityPDA(
msAccount.publicKey,
msAccount.authorityIndex
);
for (let i = 0; i < activeProposals.length; i++) {
if (
hasWormholePayload(
squad,
emitter,
activeProposals[i].publicKey,
options.payload,
activeInstructions[i],
wormholeTools
)
) {
console.log(
`❌ Skipping, payload ${options.payload} matches instructions at ${activeProposals[i].publicKey}`
);
return;
}
}
}
await createWormholeMsgMultisigTx(
options.cluster,
squad,
CONFIG[cluster].vault,
options.payload,
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,21 +320,42 @@ program
msAccount.authorityIndex msAccount.authorityIndex
); );
if ( if (options.payload) {
hasWormholePayload( if (
squad, hasWormholePayload(
emitter, squad,
new PublicKey(options.txPda), emitter,
options.payload, new PublicKey(options.txPda),
onChainInstructions, options.payload,
wormholeTools onChainInstructions,
) wormholeTools
) { )
console.log( ) {
"✅ This proposal is verified to be created with the given payload." console.log(
"✅ This proposal is verified to be created with the given payload."
);
} else {
console.log("❌ This proposal does not match the given payload.");
}
}
if (options.file) {
const instructions: SquadInstruction[] = loadInstructionsFromJson(
options.file
); );
} else {
console.log("❌ This proposal does not match the given payload."); 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.");
}
} }
}); });
@ -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;
}