Skip to content

Commit 3aa953e

Browse files
authored
[xc-admin] Add support for arbitrary wormhole payloads (#759)
* Add vaa * Remove the command from multisig_wh_message_builder * Add comment to the multisig addresses
1 parent 99ed193 commit 3aa953e

File tree

3 files changed

+127
-134
lines changed

3 files changed

+127
-134
lines changed

governance/multisig_wh_message_builder/src/index.ts

Lines changed: 1 addition & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@ import { program } from "commander";
1717
import * as fs from "fs";
1818
import { LedgerNodeWallet } from "./wallet";
1919
import lodash from "lodash";
20-
import {
21-
getActiveProposals,
22-
getManyProposalsInstructions,
23-
getProposalInstructions,
24-
} from "./multisig";
20+
import { getProposalInstructions } from "./multisig";
2521
import {
2622
WormholeNetwork,
2723
loadWormholeTools,
@@ -151,130 +147,6 @@ program
151147
);
152148
});
153149

154-
program
155-
.command("create")
156-
.description("Create a new multisig transaction")
157-
.option("-c, --cluster <network>", "solana cluster to use", "devnet")
158-
.option("-l, --ledger", "use ledger")
159-
.option(
160-
"-lda, --ledger-derivation-account <number>",
161-
"ledger derivation account to use"
162-
)
163-
.option(
164-
"-ldc, --ledger-derivation-change <number>",
165-
"ledger derivation change to use"
166-
)
167-
.option(
168-
"-w, --wallet <filepath>",
169-
"multisig wallet secret key filepath",
170-
"keys/key.json"
171-
)
172-
.option("-f, --file <filepath>", "Path to a json file with instructions")
173-
.option("-p, --payload <hex-string>", "Wormhole VAA payload")
174-
.option("-s, --skip-duplicate-check", "Skip checking duplicates")
175-
.action(async (options) => {
176-
const cluster: Cluster = options.cluster;
177-
const squad = await getSquadsClient(
178-
cluster,
179-
options.ledger,
180-
options.ledgerDerivationAccount,
181-
options.ledgerDerivationChange,
182-
options.wallet
183-
);
184-
185-
if (options.payload && options.file) {
186-
console.log("Only one of --payload or --file must be provided");
187-
return;
188-
}
189-
190-
if (options.payload) {
191-
const wormholeTools = await loadWormholeTools(cluster, squad.connection);
192-
193-
if (!options.skipDuplicateCheck) {
194-
const activeProposals = await getActiveProposals(
195-
squad,
196-
CONFIG[cluster].vault
197-
);
198-
const activeInstructions = await getManyProposalsInstructions(
199-
squad,
200-
activeProposals
201-
);
202-
203-
const msAccount = await squad.getMultisig(CONFIG[cluster].vault);
204-
const emitter = squad.getAuthorityPDA(
205-
msAccount.publicKey,
206-
msAccount.authorityIndex
207-
);
208-
209-
for (let i = 0; i < activeProposals.length; i++) {
210-
if (
211-
hasWormholePayload(
212-
squad,
213-
emitter,
214-
activeProposals[i].publicKey,
215-
options.payload,
216-
activeInstructions[i],
217-
wormholeTools
218-
)
219-
) {
220-
console.log(
221-
`❌ Skipping, payload ${options.payload} matches instructions at ${activeProposals[i].publicKey}`
222-
);
223-
return;
224-
}
225-
}
226-
}
227-
228-
await createWormholeMsgMultisigTx(
229-
options.cluster,
230-
squad,
231-
CONFIG[cluster].vault,
232-
options.payload,
233-
wormholeTools
234-
);
235-
}
236-
237-
if (options.file) {
238-
const instructions: SquadInstruction[] = loadInstructionsFromJson(
239-
options.file
240-
);
241-
242-
if (!options.skipDuplicateCheck) {
243-
const activeProposals = await getActiveProposals(
244-
squad,
245-
CONFIG[cluster].vault
246-
);
247-
const activeInstructions = await getManyProposalsInstructions(
248-
squad,
249-
activeProposals
250-
);
251-
252-
for (let i = 0; i < activeProposals.length; i++) {
253-
if (
254-
areEqualOnChainInstructions(
255-
instructions.map((ix) => ix.instruction),
256-
activeInstructions[i]
257-
)
258-
) {
259-
console.log(
260-
`❌ Skipping, instructions from ${options.file} match instructions at ${activeProposals[i].publicKey}`
261-
);
262-
return;
263-
}
264-
}
265-
}
266-
267-
const txKey = await createTx(squad, CONFIG[cluster].vault);
268-
await addInstructionsToTx(
269-
cluster,
270-
squad,
271-
CONFIG[cluster].vault,
272-
txKey,
273-
instructions
274-
);
275-
}
276-
});
277-
278150
program
279151
.command("verify")
280152
.description("Verify given proposal matches a payload")

governance/xc_admin/packages/xc_admin_cli/src/index.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
mapKey,
2727
MultisigParser,
2828
PROGRAM_AUTHORITY_ESCROW,
29+
proposeArbitraryPayload,
2930
proposeInstructions,
3031
WORMHOLE_ADDRESS,
3132
} from "xc_admin_common";
@@ -64,7 +65,10 @@ const multisigCommand = (name: string, description: string) =>
6465
"-w, --wallet <filepath>",
6566
'path to the operations key or "ledger"'
6667
)
67-
.requiredOption("-v, --vault <pubkey>", "multisig address")
68+
.requiredOption(
69+
"-v, --vault <pubkey>",
70+
"multisig address, all the addresses can be found in xc_admin_common/src/multisig.ts"
71+
)
6872
.option(
6973
"-lda, --ledger-derivation-account <number>",
7074
"ledger derivation account to use"
@@ -409,6 +413,33 @@ multisigCommand("propose-sol-transfer", "Propose sol transfer")
409413
);
410414
});
411415

416+
multisigCommand("propose-arbitrary-payload", "Propose arbitrary payload")
417+
.option("-p, --payload <hex-string>", "Wormhole VAA payload")
418+
.action(async (options: any) => {
419+
const wallet = await loadHotWalletOrLedger(
420+
options.wallet,
421+
options.ledgerDerivationAccount,
422+
options.ledgerDerivationChange
423+
);
424+
425+
const cluster: PythCluster = options.cluster;
426+
const vault: PublicKey = new PublicKey(options.vault);
427+
428+
const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
429+
430+
let payload = options.payload;
431+
if (payload.startsWith("0x")) {
432+
payload = payload.substring(2);
433+
}
434+
435+
await proposeArbitraryPayload(
436+
squad,
437+
vault,
438+
Buffer.from(payload, "hex"),
439+
WORMHOLE_ADDRESS[cluster]!
440+
);
441+
});
442+
412443
/**
413444
* Activate proposal, mostly useful for cleaning up draft proposals that happen when the browser wallet fails to send all transactions succesfully
414445
*/

governance/xc_admin/packages/xc_admin_common/src/propose.ts

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,70 @@ type SquadInstruction = {
3030
authorityType?: string;
3131
};
3232

33+
export async function proposeArbitraryPayload(
34+
squad: Squads,
35+
vault: PublicKey,
36+
payload: Buffer,
37+
wormholeAddress: PublicKey
38+
): Promise<PublicKey> {
39+
const msAccount = await squad.getMultisig(vault);
40+
41+
let ixToSend: TransactionInstruction[] = [];
42+
const proposalIndex = msAccount.transactionIndex + 1;
43+
ixToSend.push(
44+
await squad.buildCreateTransaction(
45+
msAccount.publicKey,
46+
msAccount.authorityIndex,
47+
proposalIndex
48+
)
49+
);
50+
51+
const newProposalAddress = getTxPDA(
52+
vault,
53+
new BN(proposalIndex),
54+
squad.multisigProgramId
55+
)[0];
56+
57+
const instructionToPropose = await getPostMessageInstruction(
58+
squad,
59+
vault,
60+
newProposalAddress,
61+
1,
62+
wormholeAddress,
63+
payload
64+
);
65+
ixToSend.push(
66+
await squad.buildAddInstruction(
67+
vault,
68+
newProposalAddress,
69+
instructionToPropose.instruction,
70+
1,
71+
instructionToPropose.authorityIndex,
72+
instructionToPropose.authorityBump,
73+
instructionToPropose.authorityType
74+
)
75+
);
76+
ixToSend.push(
77+
await squad.buildActivateTransaction(vault, newProposalAddress)
78+
);
79+
ixToSend.push(await squad.buildApproveTransaction(vault, newProposalAddress));
80+
81+
const txToSend = batchIntoTransactions(ixToSend);
82+
83+
for (let i = 0; i < txToSend.length; i += SIZE_OF_SIGNED_BATCH) {
84+
await new AnchorProvider(
85+
squad.connection,
86+
squad.wallet,
87+
AnchorProvider.defaultOptions()
88+
).sendAll(
89+
txToSend.slice(i, i + SIZE_OF_SIGNED_BATCH).map((tx) => {
90+
return { tx, signers: [] };
91+
})
92+
);
93+
}
94+
return newProposalAddress;
95+
}
96+
3397
/**
3498
* Propose an array of `TransactionInstructions` as a proposal
3599
* @param squad Squads client
@@ -291,14 +355,41 @@ export async function wrapAsRemoteInstruction(
291355
instructionIndex: number,
292356
wormholeAddress: PublicKey
293357
): Promise<SquadInstruction> {
294-
const emitter = squad.getAuthorityPDA(vault, 1);
358+
const buffer: Buffer = new ExecutePostedVaa("pythnet", instructions).encode();
359+
return await getPostMessageInstruction(
360+
squad,
361+
vault,
362+
proposalAddress,
363+
instructionIndex,
364+
wormholeAddress,
365+
buffer
366+
);
367+
}
295368

369+
/**
370+
* Returns a postMessage instruction that will post the provided payload to wormhole when the multisig approves the proposal
371+
* @param squad Squads client
372+
* @param vault vault public key (the id of the multisig where these instructions should be proposed)
373+
* @param proposalAddress address of the proposal
374+
* @param instructionIndex index of the instruction within the proposal
375+
* @param wormholeAddress address of the Wormhole bridge
376+
* @param payload the payload to be posted
377+
*/
378+
async function getPostMessageInstruction(
379+
squad: Squads,
380+
vault: PublicKey,
381+
proposalAddress: PublicKey,
382+
instructionIndex: number,
383+
wormholeAddress: PublicKey,
384+
payload: Buffer
385+
): Promise<SquadInstruction> {
296386
const [messagePDA, messagePdaBump] = getIxAuthorityPDA(
297387
proposalAddress,
298388
new BN(instructionIndex),
299389
squad.multisigProgramId
300390
);
301391

392+
const emitter = squad.getAuthorityPDA(vault, 1);
302393
const provider = new AnchorProvider(
303394
squad.connection,
304395
squad.wallet,
@@ -309,8 +400,6 @@ export async function wrapAsRemoteInstruction(
309400
provider
310401
);
311402

312-
const buffer: Buffer = new ExecutePostedVaa("pythnet", instructions).encode();
313-
314403
const accounts = getPostMessageAccounts(
315404
wormholeAddress,
316405
emitter,
@@ -320,14 +409,15 @@ export async function wrapAsRemoteInstruction(
320409

321410
return {
322411
instruction: await wormholeProgram.methods
323-
.postMessage(0, buffer, 0)
412+
.postMessage(0, payload, 0)
324413
.accounts(accounts)
325414
.instruction(),
326415
authorityIndex: instructionIndex,
327416
authorityBump: messagePdaBump,
328417
authorityType: "custom",
329418
};
330419
}
420+
331421
function getPostMessageAccounts(
332422
wormholeAddress: PublicKey,
333423
emitter: PublicKey,

0 commit comments

Comments
 (0)