index.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982
  1. import { ixFromRust, setDefaultWasm } from "@certusone/wormhole-sdk";
  2. import * as anchor from "@project-serum/anchor";
  3. import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet";
  4. import {
  5. AccountMeta,
  6. Keypair,
  7. LAMPORTS_PER_SOL,
  8. PublicKey,
  9. SystemProgram,
  10. TransactionInstruction,
  11. } from "@solana/web3.js";
  12. import Squads from "@sqds/mesh";
  13. import { getIxAuthorityPDA } from "@sqds/mesh";
  14. import { InstructionAccount } from "@sqds/mesh/lib/types";
  15. import bs58 from "bs58";
  16. import { program } from "commander";
  17. import * as fs from "fs";
  18. import { LedgerNodeWallet } from "./wallet";
  19. import lodash from "lodash";
  20. import {
  21. getActiveProposals,
  22. getManyProposalsInstructions,
  23. getProposalInstructions,
  24. } from "./multisig";
  25. import {
  26. WormholeNetwork,
  27. loadWormholeTools,
  28. WormholeTools,
  29. parse,
  30. } from "./wormhole";
  31. setDefaultWasm("node");
  32. // NOTE(2022-11-30): Naming disambiguation:
  33. // - "mainnet" - always means a public production environment
  34. //
  35. // - "testnet" in Wormhole context - a collection of public testnets
  36. // of the supported blockchain
  37. // - "testnet" in Solana context - Never used here; The public solana
  38. // cluster called "testnet" at https://api.testnet.solana.com
  39. //
  40. // - "devnet" in Wormhole context - local Tilt devnet
  41. // - "devnet" in Solana context - The "devnet" public Solana cluster
  42. // at https://api.devnet.solana.com
  43. //
  44. // - "localdevnet" - always means the Tilt devnet
  45. export type Cluster = "devnet" | "mainnet" | "localdevnet";
  46. type Config = {
  47. wormholeClusterName: WormholeNetwork;
  48. wormholeRpcEndpoint: string;
  49. vault: PublicKey;
  50. };
  51. export const CONFIG: Record<Cluster, Config> = {
  52. devnet: {
  53. wormholeClusterName: "TESTNET",
  54. vault: new PublicKey("6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3"),
  55. wormholeRpcEndpoint: "https://wormhole-v2-testnet-api.certus.one",
  56. },
  57. mainnet: {
  58. wormholeClusterName: "MAINNET",
  59. vault: new PublicKey("FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj"),
  60. wormholeRpcEndpoint: "https://wormhole-v2-mainnet-api.certus.one",
  61. },
  62. localdevnet: {
  63. wormholeClusterName: "DEVNET",
  64. vault: new PublicKey("DFkA5ubJSETKiFnniAsm8qRXUa7RrnnE7U9awTzbcrJF"),
  65. wormholeRpcEndpoint: "http://guardian:7071",
  66. },
  67. };
  68. program
  69. .name("pyth-multisig")
  70. .description("CLI to creating and executing multisig transactions for pyth")
  71. .version("0.1.0");
  72. program
  73. .command("init-vault")
  74. .description(
  75. "Initialize a new multisig vault. NOTE: It's unlikely that you need run this manually. Primarily used in the Tilt local devnet"
  76. )
  77. .requiredOption(
  78. "-k --create-key <address>",
  79. "Vault create key. It's a pubkey used to seed the vault's address"
  80. )
  81. .requiredOption(
  82. "-x --external-authority <address>",
  83. "External authority address"
  84. )
  85. .option("-c --cluster <network>", "solana cluster to use", "devnet")
  86. .option("-p --payer <filepath>", "payer keypair file")
  87. .option(
  88. "-t --threshold <threshold_number>",
  89. "Approval quorum threshold for the vault",
  90. "2"
  91. )
  92. .requiredOption(
  93. "-i --initial-members <comma_separated_members>",
  94. "comma-separated list of initial multisig members, without spaces"
  95. )
  96. .option(
  97. "-r --solana-rpc <url>",
  98. "Solana RPC address to use",
  99. "http://localhost:8899"
  100. )
  101. .action(async (options: any) => {
  102. let cluster: Cluster = options.cluster;
  103. let createKeyAddr: PublicKey = new PublicKey(options.createKey);
  104. let extAuthorityAddr: PublicKey = new PublicKey(options.externalAuthority);
  105. let threshold: number = parseInt(options.threshold, 10);
  106. let initialMembers = options.initialMembers
  107. .split(",")
  108. .map((m: string) => new PublicKey(m));
  109. let mesh = await getSquadsClient(
  110. cluster,
  111. options.ledger,
  112. options.ledgerDerivationAccount,
  113. options.ledgerDerivationChange,
  114. options.payer,
  115. cluster == "localdevnet" ? options.solanaRpc : undefined
  116. );
  117. let vaultAddr = CONFIG[cluster].vault;
  118. console.log("Creating new vault at", vaultAddr.toString());
  119. try {
  120. let _multisig = await mesh.getMultisig(vaultAddr);
  121. // NOTE(2022-12-08): If this check prevents you from iterating dev
  122. // work in tilt, restart solana-devnet.
  123. console.log(
  124. "Reached an existing vault under the address, refusing to create."
  125. );
  126. process.exit(17); // EEXIST
  127. } catch (e: any) {}
  128. console.log("No existing vault found, creating...");
  129. await mesh.createMultisig(
  130. extAuthorityAddr,
  131. threshold,
  132. createKeyAddr,
  133. initialMembers
  134. );
  135. });
  136. program
  137. .command("create")
  138. .description("Create a new multisig transaction")
  139. .option("-c, --cluster <network>", "solana cluster to use", "devnet")
  140. .option("-l, --ledger", "use ledger")
  141. .option(
  142. "-lda, --ledger-derivation-account <number>",
  143. "ledger derivation account to use"
  144. )
  145. .option(
  146. "-ldc, --ledger-derivation-change <number>",
  147. "ledger derivation change to use"
  148. )
  149. .option(
  150. "-w, --wallet <filepath>",
  151. "multisig wallet secret key filepath",
  152. "keys/key.json"
  153. )
  154. .option("-p, --payload <hex-string>", "payload to sign", "0xdeadbeef")
  155. .option("-s, --skip-duplicate-check", "Skip checking duplicates")
  156. .action(async (options) => {
  157. const cluster: Cluster = options.cluster;
  158. const squad = await getSquadsClient(
  159. cluster,
  160. options.ledger,
  161. options.ledgerDerivationAccount,
  162. options.ledgerDerivationChange,
  163. options.wallet
  164. );
  165. const wormholeTools = await loadWormholeTools(cluster, squad.connection);
  166. if (!options.skipDuplicateCheck) {
  167. const activeProposals = await getActiveProposals(
  168. squad,
  169. CONFIG[cluster].vault
  170. );
  171. const activeInstructions = await getManyProposalsInstructions(
  172. squad,
  173. activeProposals
  174. );
  175. const msAccount = await squad.getMultisig(CONFIG[cluster].vault);
  176. const emitter = squad.getAuthorityPDA(
  177. msAccount.publicKey,
  178. msAccount.authorityIndex
  179. );
  180. for (let i = 0; i < activeProposals.length; i++) {
  181. if (
  182. hasWormholePayload(
  183. squad,
  184. emitter,
  185. activeProposals[i].publicKey,
  186. options.payload,
  187. activeInstructions[i],
  188. wormholeTools
  189. )
  190. ) {
  191. console.log(
  192. `❌ Skipping, payload ${options.payload} matches instructions at ${activeProposals[i].publicKey}`
  193. );
  194. return;
  195. }
  196. }
  197. }
  198. await createWormholeMsgMultisigTx(
  199. options.cluster,
  200. squad,
  201. CONFIG[cluster].vault,
  202. options.payload,
  203. wormholeTools
  204. );
  205. });
  206. program
  207. .command("verify")
  208. .description("Verify given wormhole transaction has the given payload")
  209. .option("-c, --cluster <network>", "solana cluster to use", "devnet")
  210. .option("-l, --ledger", "use ledger")
  211. .option(
  212. "-lda, --ledger-derivation-account <number>",
  213. "ledger derivation account to use"
  214. )
  215. .option(
  216. "-ldc, --ledger-derivation-change <number>",
  217. "ledger derivation change to use"
  218. )
  219. .option(
  220. "-w, --wallet <filepath>",
  221. "multisig wallet secret key filepath",
  222. "keys/key.json"
  223. )
  224. .requiredOption("-p, --payload <hex-string>", "expected payload")
  225. .requiredOption("-t, --tx-pda <address>", "transaction PDA")
  226. .action(async (options) => {
  227. const cluster: Cluster = options.cluster;
  228. const squad = await getSquadsClient(
  229. cluster,
  230. options.ledger,
  231. options.ledgerDerivationAccount,
  232. options.ledgerDerivationChange,
  233. options.wallet
  234. );
  235. const wormholeTools = await loadWormholeTools(cluster, squad.connection);
  236. let onChainInstructions = await getProposalInstructions(
  237. squad,
  238. await squad.getTransaction(new PublicKey(options.txPda))
  239. );
  240. const msAccount = await squad.getMultisig(CONFIG[cluster].vault);
  241. const emitter = squad.getAuthorityPDA(
  242. msAccount.publicKey,
  243. msAccount.authorityIndex
  244. );
  245. if (
  246. hasWormholePayload(
  247. squad,
  248. emitter,
  249. new PublicKey(options.txPda),
  250. options.payload,
  251. onChainInstructions,
  252. wormholeTools
  253. )
  254. ) {
  255. console.log(
  256. "✅ This proposal is verified to be created with the given payload."
  257. );
  258. } else {
  259. console.log("❌ This proposal does not match the given payload.");
  260. }
  261. });
  262. program
  263. .command("set-is-active")
  264. .description(
  265. "Create a new multisig transaction to set the attester is-active flag"
  266. )
  267. .option("-c, --cluster <network>", "solana cluster to use", "devnet")
  268. .option("-l, --ledger", "use ledger")
  269. .option(
  270. "-lda, --ledger-derivation-account <number>",
  271. "ledger derivation account to use"
  272. )
  273. .option(
  274. "-ldc, --ledger-derivation-change <number>",
  275. "ledger derivation change to use"
  276. )
  277. .option(
  278. "-w, --wallet <filepath>",
  279. "multisig wallet secret key filepath",
  280. "keys/key.json"
  281. )
  282. .option("-a, --attester <program id>")
  283. .option(
  284. "-i, --is-active <true/false>",
  285. "set the isActive field to this value",
  286. "true"
  287. )
  288. .action(async (options) => {
  289. const cluster = options.cluster as Cluster;
  290. const squad = await getSquadsClient(
  291. cluster,
  292. options.ledger,
  293. options.ledgerDerivationAccount,
  294. options.ledgerDerivationChange,
  295. options.wallet
  296. );
  297. const vaultPubkey = CONFIG[cluster].vault;
  298. const msAccount = await squad.getMultisig(vaultPubkey);
  299. const vaultAuthority = squad.getAuthorityPDA(
  300. msAccount.publicKey,
  301. msAccount.authorityIndex
  302. );
  303. const attesterProgramId = new PublicKey(options.attester);
  304. const txKey = await createTx(squad, vaultPubkey);
  305. let isActive = undefined;
  306. if (options.isActive === "true") {
  307. isActive = true;
  308. } else if (options.isActive === "false") {
  309. isActive = false;
  310. } else {
  311. throw new Error(
  312. `Illegal argument for --is-active. Expected "true" or "false", got "${options.isActive}"`
  313. );
  314. }
  315. const squadIxs: SquadInstruction[] = [
  316. {
  317. instruction: await setIsActiveIx(
  318. vaultAuthority,
  319. vaultAuthority,
  320. attesterProgramId,
  321. isActive
  322. ),
  323. },
  324. ];
  325. await addInstructionsToTx(
  326. options.cluster,
  327. squad,
  328. msAccount.publicKey,
  329. txKey,
  330. squadIxs
  331. );
  332. });
  333. program
  334. .command("execute")
  335. .description("Execute a multisig transaction that is ready")
  336. .option("-c, --cluster <network>", "solana cluster to use", "devnet")
  337. .option("-l, --ledger", "use ledger")
  338. .option(
  339. "-lda, --ledger-derivation-account <number>",
  340. "ledger derivation account to use"
  341. )
  342. .option(
  343. "-ldc, --ledger-derivation-change <number>",
  344. "ledger derivation change to use"
  345. )
  346. .option(
  347. "-w, --wallet <filepath>",
  348. "multisig wallet secret key filepath",
  349. "keys/key.json"
  350. )
  351. .requiredOption("-t, --tx-pda <address>", "transaction PDA")
  352. .action(async (options) => {
  353. const cluster: Cluster = options.cluster;
  354. const squad = await getSquadsClient(
  355. cluster,
  356. options.ledger,
  357. options.ledgerDerivationAccount,
  358. options.ledgerDerivationChange,
  359. options.wallet
  360. );
  361. executeMultisigTx(
  362. cluster,
  363. squad,
  364. CONFIG[cluster].vault,
  365. new PublicKey(options.txPda),
  366. CONFIG[cluster].wormholeRpcEndpoint,
  367. await loadWormholeTools(cluster, squad.connection)
  368. );
  369. });
  370. program
  371. .command("change-threshold")
  372. .description("Change threshold of multisig")
  373. .option("-c, --cluster <network>", "solana cluster to use", "devnet")
  374. .option("-l, --ledger", "use ledger")
  375. .option(
  376. "-lda, --ledger-derivation-account <number>",
  377. "ledger derivation account to use"
  378. )
  379. .option(
  380. "-ldc, --ledger-derivation-change <number>",
  381. "ledger derivation change to use"
  382. )
  383. .option(
  384. "-w, --wallet <filepath>",
  385. "multisig wallet secret key filepath",
  386. "keys/key.json"
  387. )
  388. .option("-t, --threshold <number>", "new threshold")
  389. .action(async (options) => {
  390. const cluster: Cluster = options.cluster;
  391. const squad = await getSquadsClient(
  392. cluster,
  393. options.ledger,
  394. options.ledgerDerivationAccount,
  395. options.ledgerDerivationChange,
  396. options.wallet
  397. );
  398. await changeThreshold(
  399. options.cluster,
  400. squad,
  401. CONFIG[cluster].vault,
  402. options.threshold
  403. );
  404. });
  405. program
  406. .command("add-member")
  407. .description("Add member to multisig")
  408. .option("-c, --cluster <network>", "solana cluster to use", "devnet")
  409. .option("-l, --ledger", "use ledger")
  410. .option(
  411. "-lda, --ledger-derivation-account <number>",
  412. "ledger derivation account to use"
  413. )
  414. .option(
  415. "-ldc, --ledger-derivation-change <number>",
  416. "ledger derivation change to use"
  417. )
  418. .option(
  419. "-w, --wallet <filepath>",
  420. "multisig wallet secret key filepath",
  421. "keys/key.json"
  422. )
  423. .option("-m, --member <address>", "new member address")
  424. .action(async (options) => {
  425. const cluster: Cluster = options.cluster;
  426. const squad = await getSquadsClient(
  427. cluster,
  428. options.ledger,
  429. options.ledgerDerivationAccount,
  430. options.ledgerDerivationChange,
  431. options.wallet
  432. );
  433. await addMember(
  434. options.cluster,
  435. squad,
  436. CONFIG[cluster].vault,
  437. new PublicKey(options.member)
  438. );
  439. });
  440. program
  441. .command("remove-member")
  442. .description("Remove member from multisig")
  443. .option("-c, --cluster <network>", "solana cluster to use", "devnet")
  444. .option("-l, --ledger", "use ledger")
  445. .option(
  446. "-lda, --ledger-derivation-account <number>",
  447. "ledger derivation account to use"
  448. )
  449. .option(
  450. "-ldc, --ledger-derivation-change <number>",
  451. "ledger derivation change to use"
  452. )
  453. .option(
  454. "-w, --wallet <filepath>",
  455. "multisig wallet secret key filepath",
  456. "keys/key.json"
  457. )
  458. .option("-m, --member <address>", "old member address")
  459. .action(async (options) => {
  460. const cluster: Cluster = options.cluster;
  461. const squad = await getSquadsClient(
  462. cluster,
  463. options.ledger,
  464. options.ledgerDerivationAccount,
  465. options.ledgerDerivationChange,
  466. options.wallet
  467. );
  468. await removeMember(
  469. options.cluster,
  470. squad,
  471. CONFIG[cluster].vault,
  472. new PublicKey(options.member)
  473. );
  474. });
  475. // TODO: add subcommand for creating governance messages in the right format
  476. program.parse();
  477. async function getSquadsClient(
  478. cluster: Cluster,
  479. ledger: boolean,
  480. ledgerDerivationAccount: number | undefined,
  481. ledgerDerivationChange: number | undefined,
  482. walletPath: string,
  483. solRpcUrl?: string
  484. ) {
  485. let wallet: LedgerNodeWallet | NodeWallet;
  486. if (ledger) {
  487. console.log("Please connect to ledger...");
  488. wallet = await LedgerNodeWallet.createWallet(
  489. ledgerDerivationAccount,
  490. ledgerDerivationChange
  491. );
  492. console.log(`Ledger connected! ${wallet.publicKey.toBase58()}`);
  493. } else {
  494. wallet = new NodeWallet(
  495. Keypair.fromSecretKey(
  496. Uint8Array.from(JSON.parse(fs.readFileSync(walletPath, "ascii")))
  497. )
  498. );
  499. console.log(`Loaded wallet with address: ${wallet.publicKey.toBase58()}`);
  500. }
  501. switch (cluster) {
  502. case "devnet": {
  503. return Squads.devnet(wallet);
  504. break;
  505. }
  506. case "mainnet": {
  507. return Squads.mainnet(wallet);
  508. break;
  509. }
  510. case "localdevnet": {
  511. if (solRpcUrl) {
  512. return Squads.endpoint(solRpcUrl, wallet);
  513. } else {
  514. return Squads.localnet(wallet);
  515. }
  516. }
  517. default: {
  518. throw `ERROR: unrecognized cluster ${cluster}`;
  519. }
  520. }
  521. }
  522. async function createTx(squad: Squads, vault: PublicKey): Promise<PublicKey> {
  523. const msAccount = await squad.getMultisig(vault);
  524. console.log("Creating new transaction...");
  525. const newTx = await squad.createTransaction(
  526. msAccount.publicKey,
  527. msAccount.authorityIndex
  528. );
  529. console.log(`Tx Address: ${newTx.publicKey.toBase58()}`);
  530. return newTx.publicKey;
  531. }
  532. type SquadInstruction = {
  533. instruction: anchor.web3.TransactionInstruction;
  534. authorityIndex?: number;
  535. authorityBump?: number;
  536. authorityType?: string;
  537. };
  538. /** Adds the given instructions to the squads transaction at `txKey` and activates the transaction (makes it ready for signing). */
  539. async function addInstructionsToTx(
  540. cluster: Cluster,
  541. squad: Squads,
  542. vault: PublicKey,
  543. txKey: PublicKey,
  544. instructions: SquadInstruction[]
  545. ) {
  546. for (let i = 0; i < instructions.length; i++) {
  547. console.log(
  548. `Adding instruction ${i + 1}/${instructions.length} to transaction...`
  549. );
  550. await squad.addInstruction(
  551. txKey,
  552. instructions[i].instruction,
  553. instructions[i].authorityIndex,
  554. instructions[i].authorityBump,
  555. instructions[i].authorityType
  556. );
  557. }
  558. console.log("Activating transaction...");
  559. await squad.activateTransaction(txKey);
  560. console.log("Transaction created.");
  561. console.log("Approving transaction...");
  562. await squad.approveTransaction(txKey);
  563. console.log("Transaction approved.");
  564. console.log(`Tx key: ${txKey}`);
  565. console.log(
  566. `Tx URL: https://mesh${
  567. cluster === "devnet" ? "-devnet" : ""
  568. }.squads.so/transactions/${vault.toBase58()}/tx/${txKey.toBase58()}`
  569. );
  570. }
  571. async function setIsActiveIx(
  572. payerKey: PublicKey,
  573. opsOwnerKey: PublicKey,
  574. attesterProgramId: PublicKey,
  575. isActive: boolean
  576. ): Promise<TransactionInstruction> {
  577. const [configKey, _bump] = PublicKey.findProgramAddressSync(
  578. [Buffer.from("pyth2wormhole-config-v3")],
  579. attesterProgramId
  580. );
  581. const config: AccountMeta = {
  582. pubkey: configKey,
  583. isSigner: false,
  584. isWritable: true,
  585. };
  586. const opsOwner: AccountMeta = {
  587. pubkey: opsOwnerKey,
  588. isSigner: true,
  589. isWritable: true,
  590. };
  591. const payer: AccountMeta = {
  592. pubkey: payerKey,
  593. isSigner: true,
  594. isWritable: true,
  595. };
  596. const isActiveInt = isActive ? 1 : 0;
  597. // first byte is the isActive instruction, second byte is true/false
  598. const data = Buffer.from([4, isActiveInt]);
  599. return {
  600. keys: [config, opsOwner, payer],
  601. programId: attesterProgramId,
  602. data: data,
  603. };
  604. }
  605. function getWormholeMessageIx(
  606. payer: PublicKey,
  607. emitter: PublicKey,
  608. message: PublicKey,
  609. payload: string,
  610. wormholeTools: WormholeTools
  611. ) {
  612. if (payload.startsWith("0x")) {
  613. payload = payload.substring(2);
  614. }
  615. return [
  616. SystemProgram.transfer({
  617. fromPubkey: payer,
  618. toPubkey: wormholeTools.feeCollector,
  619. lamports: wormholeTools.bridgeFee,
  620. }),
  621. ixFromRust(
  622. wormholeTools.post_message_ix(
  623. wormholeTools.wormholeAddress.toBase58(),
  624. payer.toBase58(),
  625. emitter.toBase58(),
  626. message.toBase58(),
  627. 0,
  628. Uint8Array.from(Buffer.from(payload, "hex")),
  629. "CONFIRMED"
  630. )
  631. ),
  632. ];
  633. }
  634. async function createWormholeMsgMultisigTx(
  635. cluster: Cluster,
  636. squad: Squads,
  637. vault: PublicKey,
  638. payload: string,
  639. wormholeTools: WormholeTools
  640. ) {
  641. const msAccount = await squad.getMultisig(vault);
  642. const emitter = squad.getAuthorityPDA(
  643. msAccount.publicKey,
  644. msAccount.authorityIndex
  645. );
  646. console.log(`Emitter Address: ${emitter.toBase58()}`);
  647. const txKey = await createTx(squad, vault);
  648. const [messagePDA, messagePdaBump] = getIxAuthorityPDA(
  649. txKey,
  650. new anchor.BN(1),
  651. squad.multisigProgramId
  652. );
  653. console.log("Creating wormhole instructions...");
  654. const wormholeIxs = getWormholeMessageIx(
  655. emitter,
  656. emitter,
  657. messagePDA,
  658. payload,
  659. wormholeTools
  660. );
  661. console.log("Wormhole instructions created.");
  662. const squadIxs: SquadInstruction[] = [
  663. { instruction: wormholeIxs[0] },
  664. {
  665. instruction: wormholeIxs[1],
  666. authorityIndex: 1,
  667. authorityBump: messagePdaBump,
  668. authorityType: "custom",
  669. },
  670. ];
  671. await addInstructionsToTx(
  672. cluster,
  673. squad,
  674. msAccount.publicKey,
  675. txKey,
  676. squadIxs
  677. );
  678. }
  679. function hasWormholePayload(
  680. squad: Squads,
  681. emitter: PublicKey,
  682. txPubkey: PublicKey,
  683. payload: string,
  684. onChainInstructions: InstructionAccount[],
  685. wormholeTools: WormholeTools
  686. ): boolean {
  687. if (onChainInstructions.length !== 2) {
  688. console.debug(
  689. `Expected 2 instructions in the transaction, found ${onChainInstructions.length}`
  690. );
  691. return false;
  692. }
  693. const [messagePDA] = getIxAuthorityPDA(
  694. txPubkey,
  695. new anchor.BN(1),
  696. squad.multisigProgramId
  697. );
  698. const wormholeIxs = getWormholeMessageIx(
  699. emitter,
  700. emitter,
  701. messagePDA,
  702. payload,
  703. wormholeTools
  704. );
  705. return (
  706. isEqualOnChainInstruction(
  707. wormholeIxs[0],
  708. onChainInstructions[0] as InstructionAccount
  709. ) &&
  710. isEqualOnChainInstruction(
  711. wormholeIxs[1],
  712. onChainInstructions[1] as InstructionAccount
  713. )
  714. );
  715. }
  716. function isEqualOnChainInstruction(
  717. instruction: TransactionInstruction,
  718. onChainInstruction: InstructionAccount
  719. ): boolean {
  720. if (!instruction.programId.equals(onChainInstruction.programId)) {
  721. console.debug(
  722. `Program id mismatch: Expected ${instruction.programId.toBase58()}, found ${onChainInstruction.programId.toBase58()}`
  723. );
  724. return false;
  725. }
  726. if (!lodash.isEqual(instruction.keys, onChainInstruction.keys)) {
  727. console.debug(
  728. `Instruction accounts mismatch. Expected ${instruction.keys}, found ${onChainInstruction.keys}`
  729. );
  730. return false;
  731. }
  732. const onChainData = onChainInstruction.data as Buffer;
  733. if (!instruction.data.equals(onChainData)) {
  734. console.debug(
  735. `Instruction data mismatch. Expected ${instruction.data.toString(
  736. "hex"
  737. )}, Found ${onChainData.toString("hex")}`
  738. );
  739. return false;
  740. }
  741. return true;
  742. }
  743. async function executeMultisigTx(
  744. cluster: string,
  745. squad: Squads,
  746. vault: PublicKey,
  747. txPDA: PublicKey,
  748. rpcUrl: string,
  749. wormholeTools: WormholeTools
  750. ) {
  751. const msAccount = await squad.getMultisig(vault);
  752. const emitter = squad.getAuthorityPDA(
  753. msAccount.publicKey,
  754. msAccount.authorityIndex
  755. );
  756. const tx = await squad.getTransaction(txPDA);
  757. if ((tx.status as any).executeReady === undefined) {
  758. console.log(
  759. `Transaction is either executed or not ready yet. Status: ${JSON.stringify(
  760. tx.status
  761. )}`
  762. );
  763. return;
  764. }
  765. const executeIx = await squad.buildExecuteTransaction(
  766. txPDA,
  767. squad.wallet.publicKey
  768. );
  769. // airdrop 0.1 SOL to emitter if on devnet
  770. if (cluster === "devnet") {
  771. console.log("Airdropping 0.1 SOL to emitter...");
  772. const airdropSignature = await squad.connection.requestAirdrop(
  773. emitter,
  774. 0.1 * LAMPORTS_PER_SOL
  775. );
  776. const { blockhash, lastValidBlockHeight } =
  777. await squad.connection.getLatestBlockhash();
  778. await squad.connection.confirmTransaction({
  779. blockhash,
  780. lastValidBlockHeight,
  781. signature: airdropSignature,
  782. });
  783. console.log("Airdropped 0.1 SOL to emitter");
  784. }
  785. const { blockhash, lastValidBlockHeight } =
  786. await squad.connection.getLatestBlockhash();
  787. const executeTx = new anchor.web3.Transaction({
  788. blockhash,
  789. lastValidBlockHeight,
  790. feePayer: squad.wallet.publicKey,
  791. });
  792. const provider = new anchor.AnchorProvider(squad.connection, squad.wallet, {
  793. commitment: "confirmed",
  794. preflightCommitment: "confirmed",
  795. });
  796. executeTx.add(executeIx);
  797. console.log("Sending transaction...");
  798. const signature = await provider.sendAndConfirm(executeTx);
  799. console.log(
  800. `Executed tx: https://explorer.solana.com/tx/${signature}${
  801. cluster === "devnet" ? "?cluster=devnet" : ""
  802. }`
  803. );
  804. console.log(
  805. "Sleeping for 10 seconds to allow guardians enough time to get the sequence number..."
  806. );
  807. await new Promise((resolve) => setTimeout(resolve, 10000));
  808. const txDetails = await squad.connection.getParsedTransaction(
  809. signature,
  810. "confirmed"
  811. );
  812. const txLog = txDetails?.meta?.logMessages?.find((s) =>
  813. s.includes("Sequence")
  814. );
  815. const substr = "Sequence: ";
  816. const sequenceNumber = Number(
  817. txLog?.substring(txLog.indexOf(substr) + substr.length)
  818. );
  819. console.log(`Sequence number: ${sequenceNumber}`);
  820. console.log(
  821. "Sleeping for 10 seconds to allow guardians enough time to create VAA..."
  822. );
  823. await new Promise((resolve) => setTimeout(resolve, 10000));
  824. // fetch VAA
  825. console.log("Fetching VAA...");
  826. const response = await fetch(
  827. `${rpcUrl}/v1/signed_vaa/1/${Buffer.from(
  828. bs58.decode(emitter.toBase58())
  829. ).toString("hex")}/${sequenceNumber}`
  830. );
  831. const { vaaBytes } = await response.json();
  832. console.log(`VAA (Base64): ${vaaBytes}`);
  833. console.log(`VAA (Hex): ${Buffer.from(vaaBytes, "base64").toString("hex")}`);
  834. const parsedVaa = parse(vaaBytes, wormholeTools);
  835. console.log(`Emitter chain: ${parsedVaa.emitter_chain}`);
  836. console.log(`Nonce: ${parsedVaa.nonce}`);
  837. console.log(`Payload: ${Buffer.from(parsedVaa.payload).toString("hex")}`);
  838. }
  839. async function changeThreshold(
  840. cluster: Cluster,
  841. squad: Squads,
  842. vault: PublicKey,
  843. threshold: number
  844. ) {
  845. const msAccount = await squad.getMultisig(vault);
  846. const txKey = await createTx(squad, vault);
  847. const ix = await squad.buildChangeThresholdMember(
  848. msAccount.publicKey,
  849. msAccount.externalAuthority,
  850. threshold
  851. );
  852. const squadIxs: SquadInstruction[] = [{ instruction: ix }];
  853. await addInstructionsToTx(
  854. cluster,
  855. squad,
  856. msAccount.publicKey,
  857. txKey,
  858. squadIxs
  859. );
  860. }
  861. async function addMember(
  862. cluster: Cluster,
  863. squad: Squads,
  864. vault: PublicKey,
  865. member: PublicKey
  866. ) {
  867. const msAccount = await squad.getMultisig(vault);
  868. const txKey = await createTx(squad, vault);
  869. const ix = await squad.buildAddMember(
  870. msAccount.publicKey,
  871. msAccount.externalAuthority,
  872. member
  873. );
  874. const squadIxs: SquadInstruction[] = [{ instruction: ix }];
  875. await addInstructionsToTx(
  876. cluster,
  877. squad,
  878. msAccount.publicKey,
  879. txKey,
  880. squadIxs
  881. );
  882. }
  883. async function removeMember(
  884. cluster: Cluster,
  885. squad: Squads,
  886. vault: PublicKey,
  887. member: PublicKey
  888. ) {
  889. const msAccount = await squad.getMultisig(vault);
  890. const txKey = await createTx(squad, vault);
  891. const ix = await squad.buildRemoveMember(
  892. msAccount.publicKey,
  893. msAccount.externalAuthority,
  894. member
  895. );
  896. const squadIxs: SquadInstruction[] = [{ instruction: ix }];
  897. await addInstructionsToTx(
  898. cluster,
  899. squad,
  900. msAccount.publicKey,
  901. txKey,
  902. squadIxs
  903. );
  904. }