index.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. import {
  2. importCoreWasm,
  3. ixFromRust,
  4. setDefaultWasm,
  5. utils as wormholeUtils,
  6. } from "@certusone/wormhole-sdk";
  7. import * as anchor from "@project-serum/anchor";
  8. import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet";
  9. import {
  10. AccountMeta,
  11. Keypair,
  12. LAMPORTS_PER_SOL,
  13. PublicKey,
  14. SystemProgram,
  15. TransactionInstruction,
  16. } from "@solana/web3.js";
  17. import Squads from "@sqds/mesh";
  18. import bs58 from "bs58";
  19. import { program } from "commander";
  20. import * as fs from "fs";
  21. import { LedgerNodeWallet } from "./wallet";
  22. setDefaultWasm("node");
  23. program
  24. .name("pyth-multisig")
  25. .description("CLI to creating and executing multisig transactions for pyth")
  26. .version("0.1.0");
  27. program
  28. .command("create")
  29. .description("Create a new multisig transaction")
  30. .option("-c, --cluster <network>", "solana cluster to use", "devnet")
  31. .requiredOption("-v, --vault-address <address>", "multisig vault address")
  32. .option("-l, --ledger", "use ledger")
  33. .option(
  34. "-lda, --ledger-derivation-account <number>",
  35. "ledger derivation account to use"
  36. )
  37. .option(
  38. "-ldc, --ledger-derivation-change <number>",
  39. "ledger derivation change to use"
  40. )
  41. .option(
  42. "-w, --wallet <filepath>",
  43. "multisig wallet secret key filepath",
  44. "keys/key.json"
  45. )
  46. .option("-p, --payload <hex-string>", "payload to sign", "0xdeadbeef")
  47. .action(async (options) => {
  48. const squad = await getSquadsClient(
  49. options.cluster,
  50. options.ledger,
  51. options.ledgerDerivationAccount,
  52. options.ledgerDerivationChange,
  53. options.wallet
  54. );
  55. await createWormholeMsgMultisigTx(
  56. options.cluster,
  57. squad,
  58. options.ledger,
  59. new PublicKey(options.vaultAddress),
  60. options.payload
  61. );
  62. });
  63. program
  64. .command("set-is-active")
  65. .description(
  66. "Create a new multisig transaction to set the attester is-active flag"
  67. )
  68. .option("-c, --cluster <network>", "solana cluster to use", "devnet")
  69. .requiredOption("-v, --vault-address <address>", "multisig vault address")
  70. .option("-l, --ledger", "use ledger")
  71. .option(
  72. "-lda, --ledger-derivation-account <number>",
  73. "ledger derivation account to use"
  74. )
  75. .option(
  76. "-ldc, --ledger-derivation-change <number>",
  77. "ledger derivation change to use"
  78. )
  79. .option(
  80. "-w, --wallet <filepath>",
  81. "multisig wallet secret key filepath",
  82. "keys/key.json"
  83. )
  84. .option("-a, --attester <program id>")
  85. .option(
  86. "-i, --is-active <true/false>",
  87. "set the isActive field to this value",
  88. "true"
  89. )
  90. .action(async (options) => {
  91. const squad = await getSquadsClient(
  92. options.cluster,
  93. options.ledger,
  94. options.ledgerDerivationAccount,
  95. options.ledgerDerivationChange,
  96. options.wallet
  97. );
  98. const msAccount = await squad.getMultisig(new PublicKey(options.vaultAddress));
  99. const vaultAuthority = squad.getAuthorityPDA(
  100. msAccount.publicKey,
  101. msAccount.authorityIndex
  102. );
  103. const attesterProgramId = new PublicKey(options.attester);
  104. const txKey = await createTx(
  105. squad,
  106. options.ledger,
  107. new PublicKey(options.vaultAddress)
  108. );
  109. let isActive = undefined;
  110. if (options.isActive === 'true') {
  111. isActive = true;
  112. } else if (options.isActive === 'false') {
  113. isActive = false;
  114. } else {
  115. throw new Error(`Illegal argument for --is-active. Expected "true" or "false", got "${options.isActive}"`)
  116. }
  117. const squadIxs: SquadInstruction[] = [
  118. {
  119. instruction: await setIsActiveIx(
  120. vaultAuthority,
  121. vaultAuthority,
  122. attesterProgramId,
  123. isActive
  124. ),
  125. },
  126. ];
  127. await addInstructionsToTx(
  128. options.cluster,
  129. squad,
  130. options.ledger,
  131. msAccount.publicKey,
  132. txKey,
  133. squadIxs
  134. );
  135. });
  136. program
  137. .command("execute")
  138. .description("Execute a multisig transaction that is ready")
  139. .option("-c, --cluster <network>", "solana cluster to use", "devnet")
  140. .requiredOption("-v, --vault-address <address>", "multisig vault address")
  141. .option("-l, --ledger", "use ledger")
  142. .option(
  143. "-lda, --ledger-derivation-account <number>",
  144. "ledger derivation account to use"
  145. )
  146. .option(
  147. "-ldc, --ledger-derivation-change <number>",
  148. "ledger derivation change to use"
  149. )
  150. .option(
  151. "-w, --wallet <filepath>",
  152. "multisig wallet secret key filepath",
  153. "keys/key.json"
  154. )
  155. .requiredOption("-t, --tx-pda <address>", "transaction PDA")
  156. .requiredOption("-u, --rpc-url <url>", "wormhole RPC URL")
  157. .action((options) => {
  158. executeMultisigTx(
  159. options.cluster,
  160. new PublicKey(options.vaultAddress),
  161. options.ledger,
  162. options.ledgerDerivationAccount,
  163. options.ledgerDerivationChange,
  164. options.wallet,
  165. new PublicKey(options.txPda),
  166. options.rpcUrl
  167. );
  168. });
  169. // TODO: add subcommand for creating governance messages in the right format
  170. program.parse();
  171. // custom solana cluster type
  172. type Cluster = "devnet" | "mainnet";
  173. type WormholeNetwork = "TESTNET" | "MAINNET";
  174. // solana cluster mapping to wormhole cluster
  175. const solanaClusterMappingToWormholeNetwork: Record<Cluster, WormholeNetwork> =
  176. {
  177. devnet: "TESTNET",
  178. mainnet: "MAINNET",
  179. };
  180. async function getSquadsClient(
  181. cluster: Cluster,
  182. ledger: boolean,
  183. ledgerDerivationAccount: number | undefined,
  184. ledgerDerivationChange: number | undefined,
  185. walletPath: string
  186. ) {
  187. let wallet: LedgerNodeWallet | NodeWallet;
  188. if (ledger) {
  189. console.log("Please connect to ledger...");
  190. wallet = await LedgerNodeWallet.createWallet(
  191. ledgerDerivationAccount,
  192. ledgerDerivationChange
  193. );
  194. console.log(`Ledger connected! ${wallet.publicKey.toBase58()}`);
  195. } else {
  196. wallet = new NodeWallet(
  197. Keypair.fromSecretKey(
  198. Uint8Array.from(JSON.parse(fs.readFileSync(walletPath, "ascii")))
  199. )
  200. );
  201. console.log(`Loaded wallet with address: ${wallet.publicKey.toBase58()}`);
  202. }
  203. const squad =
  204. cluster === "devnet" ? Squads.devnet(wallet) : Squads.mainnet(wallet);
  205. return squad;
  206. }
  207. async function createTx(
  208. squad: Squads,
  209. ledger: boolean,
  210. vault: PublicKey
  211. ): Promise<PublicKey> {
  212. const msAccount = await squad.getMultisig(vault);
  213. console.log("Creating new transaction...");
  214. if (ledger) {
  215. console.log("Please approve the transaction on your ledger device...");
  216. }
  217. const newTx = await squad.createTransaction(
  218. msAccount.publicKey,
  219. msAccount.authorityIndex
  220. );
  221. console.log(`Tx Address: ${newTx.publicKey.toBase58()}`);
  222. return newTx.publicKey;
  223. }
  224. type SquadInstruction = {
  225. instruction: anchor.web3.TransactionInstruction;
  226. authorityIndex?: number;
  227. authorityBump?: number;
  228. authorityType?: string;
  229. };
  230. /** Adds the given instructions to the squads transaction at `txKey` and activates the transaction (makes it ready for signing). */
  231. async function addInstructionsToTx(
  232. cluster: Cluster,
  233. squad: Squads,
  234. ledger: boolean,
  235. vault: PublicKey,
  236. txKey: PublicKey,
  237. instructions: SquadInstruction[]
  238. ) {
  239. for (let i = 0; i < instructions.length; i++) {
  240. console.log(
  241. `Adding instruction ${i + 1}/${instructions.length} to transaction...`
  242. );
  243. if (ledger) {
  244. console.log("Please approve the transaction on your ledger device...");
  245. }
  246. await squad.addInstruction(
  247. txKey,
  248. instructions[i].instruction,
  249. instructions[i].authorityIndex,
  250. instructions[i].authorityBump,
  251. instructions[i].authorityType
  252. );
  253. }
  254. console.log("Activating transaction...");
  255. if (ledger)
  256. console.log("Please approve the transaction on your ledger device...");
  257. await squad.activateTransaction(txKey);
  258. console.log("Transaction created.");
  259. console.log("Approving transaction...");
  260. if (ledger)
  261. console.log("Please approve the transaction on your ledger device...");
  262. await squad.approveTransaction(txKey);
  263. console.log("Transaction approved.");
  264. console.log(
  265. `Tx URL: https://mesh${
  266. cluster === "devnet" ? "-devnet" : ""
  267. }.squads.so/transactions/${vault.toBase58()}/tx/${txKey.toBase58()}`
  268. );
  269. }
  270. async function setIsActiveIx(
  271. payerKey: PublicKey,
  272. opsOwnerKey: PublicKey,
  273. attesterProgramId: PublicKey,
  274. isActive: boolean
  275. ): Promise<TransactionInstruction> {
  276. const [configKey, _bump] = PublicKey.findProgramAddressSync(
  277. [Buffer.from("pyth2wormhole-config-v3")],
  278. attesterProgramId
  279. );
  280. const config: AccountMeta = {
  281. pubkey: configKey,
  282. isSigner: false,
  283. isWritable: true,
  284. };
  285. const opsOwner: AccountMeta = {
  286. pubkey: opsOwnerKey,
  287. isSigner: true,
  288. isWritable: true,
  289. };
  290. const payer: AccountMeta = {
  291. pubkey: payerKey,
  292. isSigner: true,
  293. isWritable: true,
  294. };
  295. const isActiveInt = isActive ? 1 : 0;
  296. // first byte is the isActive instruction, second byte is true/false
  297. const data = Buffer.from([4, isActiveInt]);
  298. return {
  299. keys: [config, opsOwner, payer],
  300. programId: attesterProgramId,
  301. data: data,
  302. };
  303. }
  304. async function getWormholeMessageIx(
  305. cluster: Cluster,
  306. payer: PublicKey,
  307. emitter: PublicKey,
  308. message: PublicKey,
  309. connection: anchor.web3.Connection,
  310. payload: string
  311. ) {
  312. const wormholeNetwork: WormholeNetwork =
  313. solanaClusterMappingToWormholeNetwork[cluster];
  314. const wormholeAddress = wormholeUtils.CONTRACTS[wormholeNetwork].solana.core;
  315. const { post_message_ix, fee_collector_address, state_address, parse_state } =
  316. await importCoreWasm();
  317. const feeCollector = new PublicKey(fee_collector_address(wormholeAddress));
  318. const bridgeState = new PublicKey(state_address(wormholeAddress));
  319. const bridgeAccountInfo = await connection.getAccountInfo(bridgeState);
  320. const bridgeStateParsed = parse_state(bridgeAccountInfo!.data);
  321. const bridgeFee = bridgeStateParsed.config.fee;
  322. if (payload.startsWith("0x")) {
  323. payload = payload.substring(2);
  324. }
  325. return [
  326. SystemProgram.transfer({
  327. fromPubkey: payer,
  328. toPubkey: feeCollector,
  329. lamports: bridgeFee,
  330. }),
  331. ixFromRust(
  332. post_message_ix(
  333. wormholeAddress,
  334. payer.toBase58(),
  335. emitter.toBase58(),
  336. message.toBase58(),
  337. 0,
  338. Uint8Array.from(Buffer.from(payload, "hex")),
  339. "CONFIRMED"
  340. )
  341. ),
  342. ];
  343. }
  344. const getIxAuthority = async (
  345. txPda: anchor.web3.PublicKey,
  346. index: anchor.BN,
  347. programId: anchor.web3.PublicKey
  348. ) => {
  349. return anchor.web3.PublicKey.findProgramAddress(
  350. [
  351. anchor.utils.bytes.utf8.encode("squad"),
  352. txPda.toBuffer(),
  353. index.toArrayLike(Buffer, "le", 4),
  354. anchor.utils.bytes.utf8.encode("ix_authority"),
  355. ],
  356. programId
  357. );
  358. };
  359. async function createWormholeMsgMultisigTx(
  360. cluster: Cluster,
  361. squad: Squads,
  362. ledger: boolean,
  363. vault: PublicKey,
  364. payload: string
  365. ) {
  366. const msAccount = await squad.getMultisig(vault);
  367. const emitter = squad.getAuthorityPDA(
  368. msAccount.publicKey,
  369. msAccount.authorityIndex
  370. );
  371. console.log(`Emitter Address: ${emitter.toBase58()}`);
  372. const txKey = await createTx(squad, ledger, vault);
  373. const [messagePDA, messagePdaBump] = await getIxAuthority(
  374. txKey,
  375. new anchor.BN(1),
  376. squad.multisigProgramId
  377. );
  378. console.log("Creating wormhole instructions...");
  379. const wormholeIxs = await getWormholeMessageIx(
  380. cluster,
  381. emitter,
  382. emitter,
  383. messagePDA,
  384. squad.connection,
  385. payload
  386. );
  387. console.log("Wormhole instructions created.");
  388. const squadIxs: SquadInstruction[] = [
  389. { instruction: wormholeIxs[0] },
  390. {
  391. instruction: wormholeIxs[1],
  392. authorityIndex: 1,
  393. authorityBump: messagePdaBump,
  394. authorityType: "custom",
  395. },
  396. ];
  397. await addInstructionsToTx(
  398. cluster,
  399. squad,
  400. ledger,
  401. msAccount.publicKey,
  402. txKey,
  403. squadIxs
  404. );
  405. }
  406. async function executeMultisigTx(
  407. cluster: string,
  408. vault: PublicKey,
  409. ledger: boolean,
  410. ledgerDerivationAccount: number | undefined,
  411. ledgerDerivationChange: number | undefined,
  412. walletPath: string,
  413. txPDA: PublicKey,
  414. rpcUrl: string
  415. ) {
  416. let wallet: LedgerNodeWallet | NodeWallet;
  417. if (ledger) {
  418. console.log("Please connect to ledger...");
  419. wallet = await LedgerNodeWallet.createWallet(
  420. ledgerDerivationAccount,
  421. ledgerDerivationChange
  422. );
  423. console.log(`Ledger connected! ${wallet.publicKey.toBase58()}`);
  424. } else {
  425. wallet = new NodeWallet(
  426. Keypair.fromSecretKey(
  427. Uint8Array.from(JSON.parse(fs.readFileSync(walletPath, "ascii")))
  428. )
  429. );
  430. console.log(`Loaded wallet with address: ${wallet.publicKey.toBase58()}`);
  431. }
  432. const squad =
  433. cluster === "devnet" ? Squads.devnet(wallet) : Squads.mainnet(wallet);
  434. const msAccount = await squad.getMultisig(vault);
  435. const emitter = squad.getAuthorityPDA(
  436. msAccount.publicKey,
  437. msAccount.authorityIndex
  438. );
  439. const executeIx = await squad.buildExecuteTransaction(
  440. txPDA,
  441. wallet.publicKey
  442. );
  443. // airdrop 0.1 SOL to emitter if on devnet
  444. if (cluster === "devnet") {
  445. console.log("Airdropping 0.1 SOL to emitter...");
  446. const airdropSignature = await squad.connection.requestAirdrop(
  447. emitter,
  448. 0.1 * LAMPORTS_PER_SOL
  449. );
  450. const { blockhash, lastValidBlockHeight } =
  451. await squad.connection.getLatestBlockhash();
  452. await squad.connection.confirmTransaction({
  453. blockhash,
  454. lastValidBlockHeight,
  455. signature: airdropSignature,
  456. });
  457. console.log("Airdropped 0.1 SOL to emitter");
  458. }
  459. const { blockhash, lastValidBlockHeight } =
  460. await squad.connection.getLatestBlockhash();
  461. const executeTx = new anchor.web3.Transaction({
  462. blockhash,
  463. lastValidBlockHeight,
  464. feePayer: wallet.publicKey,
  465. });
  466. const provider = new anchor.AnchorProvider(squad.connection, wallet, {
  467. commitment: "confirmed",
  468. preflightCommitment: "confirmed",
  469. });
  470. executeTx.add(executeIx);
  471. console.log("Sending transaction...");
  472. if (ledger)
  473. console.log("Please approve the transaction on your ledger device...");
  474. const signature = await provider.sendAndConfirm(executeTx);
  475. console.log(
  476. `Executed tx: https://explorer.solana.com/tx/${signature}${
  477. cluster === "devnet" ? "?cluster=devnet" : ""
  478. }`
  479. );
  480. console.log(
  481. "Sleeping for 10 seconds to allow guardians enough time to get the sequence number..."
  482. );
  483. await new Promise((resolve) => setTimeout(resolve, 10000));
  484. const txDetails = await squad.connection.getParsedTransaction(
  485. signature,
  486. "confirmed"
  487. );
  488. const txLog = txDetails?.meta?.logMessages?.find((s) =>
  489. s.includes("Sequence")
  490. );
  491. const substr = "Sequence: ";
  492. const sequenceNumber = Number(
  493. txLog?.substring(txLog.indexOf(substr) + substr.length)
  494. );
  495. console.log(`Sequence number: ${sequenceNumber}`);
  496. console.log(
  497. "Sleeping for 10 seconds to allow guardians enough time to create VAA..."
  498. );
  499. await new Promise((resolve) => setTimeout(resolve, 10000));
  500. // fetch VAA
  501. console.log("Fetching VAA...");
  502. const response = await fetch(
  503. `${rpcUrl}/v1/signed_vaa/1/${Buffer.from(
  504. bs58.decode(emitter.toBase58())
  505. ).toString("hex")}/${sequenceNumber}`
  506. );
  507. const { vaaBytes } = await response.json();
  508. console.log(`VAA (Base64): ${vaaBytes}`);
  509. console.log(`VAA (Hex): ${Buffer.from(vaaBytes).toString("hex")}`);
  510. const parsedVaa = await parse(vaaBytes);
  511. console.log(`Emitter chain: ${parsedVaa.emitter_chain}`);
  512. console.log(`Nonce: ${parsedVaa.nonce}`);
  513. console.log(`Payload: ${Buffer.from(parsedVaa.payload).toString("hex")}`);
  514. }
  515. async function parse(data: string) {
  516. const { parse_vaa } = await importCoreWasm();
  517. return parse_vaa(Uint8Array.from(Buffer.from(data, "base64")));
  518. }