Эх сурвалжийг харах

Encapsulate parameters to proposeX methods (#870)

* refactor

* stuff

* i think this works

* cleanup

* fix
Jayant Krishnamurthy 2 жил өмнө
parent
commit
3ffcf12a70

+ 13 - 10
governance/xc_admin/packages/proposer_server/src/index.ts

@@ -10,10 +10,8 @@ import {
 import {
   envOrErr,
   getMultisigCluster,
-  isRemoteCluster,
+  MultisigVault,
   PRICE_FEED_MULTISIG,
-  proposeInstructions,
-  WORMHOLE_ADDRESS,
 } from "xc_admin_common";
 import * as fs from "fs";
 import { getPythClusterApiUrl, PythCluster } from "@pythnetwork/client";
@@ -65,19 +63,24 @@ app.post("/api/propose", async (req: Request, res: Response) => {
 
     const cluster: PythCluster = req.body.cluster;
 
+    const wallet = new NodeWallet(KEYPAIR);
     const proposeSquads: SquadsMesh = new SquadsMesh({
       connection: new Connection(RPC_URLS[getMultisigCluster(cluster)]),
-      wallet: new NodeWallet(KEYPAIR),
+      wallet,
     });
 
-    const proposalPubkey = await proposeInstructions(
+    const vault = new MultisigVault(
+      wallet,
+      getMultisigCluster(cluster),
       proposeSquads,
-      PRICE_FEED_MULTISIG[getMultisigCluster(cluster)],
-      instructions,
-      isRemoteCluster(cluster),
-      WORMHOLE_ADDRESS[getMultisigCluster(cluster)]
+      PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
     );
-    res.status(200).json({ proposalPubkey });
+
+    // preserve the existing API by returning only the first pubkey
+    const proposalPubkey = (
+      await vault.proposeInstructions(instructions, cluster)
+    )[0];
+    res.status(200).json({ proposalPubkey: proposalPubkey });
   } catch (error) {
     if (error instanceof Error) {
       res.status(500).json(error.message);

+ 55 - 165
governance/xc_admin/packages/xc_admin_cli/src/index.ts

@@ -7,6 +7,7 @@ import {
   AccountMeta,
   SystemProgram,
   LAMPORTS_PER_SOL,
+  Connection,
 } from "@solana/web3.js";
 import { program } from "commander";
 import {
@@ -22,13 +23,9 @@ import {
   BPF_UPGRADABLE_LOADER,
   getMultisigCluster,
   getProposalInstructions,
-  isRemoteCluster,
-  mapKey,
   MultisigParser,
   PROGRAM_AUTHORITY_ESCROW,
-  proposeArbitraryPayload,
-  proposeInstructions,
-  WORMHOLE_ADDRESS,
+  MultisigVault,
 } from "xc_admin_common";
 import { pythOracleProgram } from "@pythnetwork/client";
 import { Wallet } from "@coral-xyz/anchor/dist/cjs/provider";
@@ -56,6 +53,26 @@ export async function loadHotWalletOrLedger(
   }
 }
 
+async function loadVaultFromOptions(options: any): Promise<MultisigVault> {
+  const wallet = await loadHotWalletOrLedger(
+    options.wallet,
+    options.ledgerDerivationAccount,
+    options.ledgerDerivationChange
+  );
+  // This is the cluster where we want to perform the action
+  const cluster: PythCluster = options.cluster;
+  // This is the cluster where the multisig lives that can perform actions on ^
+  const multisigCluster = getMultisigCluster(cluster);
+  const vault: PublicKey = new PublicKey(options.vault);
+
+  const squad = SquadsMesh.endpoint(
+    getPythClusterApiUrl(multisigCluster),
+    wallet
+  );
+
+  return new MultisigVault(wallet, multisigCluster, squad, vault);
+}
+
 const multisigCommand = (name: string, description: string) =>
   program
     .command(name)
@@ -97,44 +114,21 @@ multisigCommand(
   )
 
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
-    const cluster: PythCluster = options.cluster;
+    const vault = await loadVaultFromOptions(options);
+    const targetCluster: PythCluster = options.cluster;
+
     const programId: PublicKey = new PublicKey(options.programId);
     const current: PublicKey = new PublicKey(options.current);
-    const vault: PublicKey = new PublicKey(options.vault);
-
-    const isRemote = isRemoteCluster(cluster);
-    const squad = SquadsMesh.endpoint(
-      getPythClusterApiUrl(getMultisigCluster(cluster)),
-      wallet
-    );
-    const msAccount = await squad.getMultisig(vault);
-    const vaultAuthority = squad.getAuthorityPDA(
-      msAccount.publicKey,
-      msAccount.authorityIndex
-    );
 
     const programAuthorityEscrowIdl = await Program.fetchIdl(
       PROGRAM_AUTHORITY_ESCROW,
-      new AnchorProvider(
-        squad.connection,
-        squad.wallet,
-        AnchorProvider.defaultOptions()
-      )
+      vault.getAnchorProvider()
     );
 
     const programAuthorityEscrow = new Program(
       programAuthorityEscrowIdl!,
       PROGRAM_AUTHORITY_ESCROW,
-      new AnchorProvider(
-        squad.connection,
-        squad.wallet,
-        AnchorProvider.defaultOptions()
-      )
+      vault.getAnchorProvider()
     );
     const programDataAccount = PublicKey.findProgramAddressSync(
       [programId.toBuffer()],
@@ -145,20 +139,14 @@ multisigCommand(
       .accept()
       .accounts({
         currentAuthority: current,
-        newAuthority: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
+        newAuthority: await vault.getVaultAuthorityPDA(targetCluster),
         programAccount: programId,
         programDataAccount,
         bpfUpgradableLoader: BPF_UPGRADABLE_LOADER,
       })
       .instruction();
 
-    await proposeInstructions(
-      squad,
-      vault,
-      [proposalInstruction],
-      isRemote,
-      WORMHOLE_ADDRESS[getMultisigCluster(cluster)]
-    );
+    await vault.proposeInstructions([proposalInstruction], targetCluster);
   });
 
 multisigCommand("upgrade-program", "Upgrade a program from a buffer")
@@ -169,26 +157,10 @@ multisigCommand("upgrade-program", "Upgrade a program from a buffer")
   .requiredOption("-b, --buffer <pubkey>", "buffer account")
 
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
+    const vault = await loadVaultFromOptions(options);
     const cluster: PythCluster = options.cluster;
     const programId: PublicKey = new PublicKey(options.programId);
     const buffer: PublicKey = new PublicKey(options.buffer);
-    const vault: PublicKey = new PublicKey(options.vault);
-
-    const isRemote = isRemoteCluster(cluster);
-    const squad = SquadsMesh.endpoint(
-      getPythClusterApiUrl(getMultisigCluster(cluster)),
-      wallet
-    );
-    const msAccount = await squad.getMultisig(vault);
-    const vaultAuthority = squad.getAuthorityPDA(
-      msAccount.publicKey,
-      msAccount.authorityIndex
-    );
 
     const programDataAccount = PublicKey.findProgramAddressSync(
       [programId.toBuffer()],
@@ -204,24 +176,18 @@ multisigCommand("upgrade-program", "Upgrade a program from a buffer")
         { pubkey: programDataAccount, isSigner: false, isWritable: true },
         { pubkey: programId, isSigner: false, isWritable: true },
         { pubkey: buffer, isSigner: false, isWritable: true },
-        { pubkey: wallet.publicKey, isSigner: false, isWritable: true },
+        { pubkey: vault.wallet.publicKey, isSigner: false, isWritable: true },
         { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
         { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
         {
-          pubkey: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
+          pubkey: await vault.getVaultAuthorityPDA(cluster),
           isSigner: true,
           isWritable: false,
         },
       ],
     };
 
-    await proposeInstructions(
-      squad,
-      vault,
-      [proposalInstruction],
-      isRemote,
-      WORMHOLE_ADDRESS[getMultisigCluster(cluster)]
-    );
+    await vault.proposeInstructions([proposalInstruction], cluster);
   });
 
 multisigCommand(
@@ -231,36 +197,22 @@ multisigCommand(
   .requiredOption("-p, --price <pubkey>", "Price account to modify")
   .requiredOption("-e, --exponent <number>", "New exponent")
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
+    const vault = await loadVaultFromOptions(options);
     const cluster: PythCluster = options.cluster;
-    const vault: PublicKey = new PublicKey(options.vault);
     const priceAccount: PublicKey = new PublicKey(options.price);
     const exponent = options.exponent;
-    const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
-
-    const msAccount = await squad.getMultisig(vault);
-    const vaultAuthority = squad.getAuthorityPDA(
-      msAccount.publicKey,
-      msAccount.authorityIndex
-    );
 
-    const provider = new AnchorProvider(
-      squad.connection,
-      wallet,
-      AnchorProvider.defaultOptions()
-    );
     const proposalInstruction: TransactionInstruction = await pythOracleProgram(
       getPythProgramKeyForCluster(cluster),
-      provider
+      vault.getAnchorProvider()
     )
       .methods.setExponent(exponent, 1)
-      .accounts({ fundingAccount: vaultAuthority, priceAccount })
+      .accounts({
+        fundingAccount: await vault.getVaultAuthorityPDA(cluster),
+        priceAccount,
+      })
       .instruction();
-    await proposeInstructions(squad, vault, [proposalInstruction], false);
+    await vault.proposeInstructions([proposalInstruction], cluster);
   });
 
 program
@@ -305,16 +257,9 @@ multisigCommand("approve", "Approve a transaction sitting in the multisig")
     "address of the outstanding transaction"
   )
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
+    const vault = await loadVaultFromOptions(options);
     const transaction: PublicKey = new PublicKey(options.transaction);
-    const cluster: PythCluster = options.cluster;
-
-    const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
-    await squad.approveTransaction(transaction);
+    await vault.squad.approveTransaction(transaction);
   });
 
 multisigCommand("propose-token-transfer", "Propose token transfer")
@@ -326,34 +271,23 @@ multisigCommand("propose-token-transfer", "Propose token transfer")
     "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" // default value is solana mainnet USDC SPL
   )
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
+    const vault = await loadVaultFromOptions(options);
 
     const cluster: PythCluster = options.cluster;
+    const connection = new Connection(getPythClusterApiUrl(cluster)); // from cluster
     const destination: PublicKey = new PublicKey(options.destination);
     const mint: PublicKey = new PublicKey(options.mint);
-    const vault: PublicKey = new PublicKey(options.vault);
     const amount: number = options.amount;
 
-    const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
-    const msAccount = await squad.getMultisig(vault);
-    const vaultAuthority = squad.getAuthorityPDA(
-      msAccount.publicKey,
-      msAccount.authorityIndex
-    );
-
     const mintAccount = await getMint(
-      squad.connection,
+      connection,
       mint,
       undefined,
       TOKEN_PROGRAM_ID
     );
     const sourceTokenAccount = await getAssociatedTokenAddress(
       mint,
-      vaultAuthority,
+      await vault.getVaultAuthorityPDA(cluster),
       true
     );
     const destinationTokenAccount = await getAssociatedTokenAddress(
@@ -365,79 +299,43 @@ multisigCommand("propose-token-transfer", "Propose token transfer")
       createTransferInstruction(
         sourceTokenAccount,
         destinationTokenAccount,
-        vaultAuthority,
+        await vault.getVaultAuthorityPDA(cluster),
         BigInt(amount) * BigInt(10) ** BigInt(mintAccount.decimals)
       );
 
-    await proposeInstructions(squad, vault, [proposalInstruction], false);
+    await vault.proposeInstructions([proposalInstruction], cluster);
   });
 
 multisigCommand("propose-sol-transfer", "Propose sol transfer")
   .requiredOption("-a, --amount <number>", "amount in sol")
   .requiredOption("-d, --destination <pubkey>", "destination address")
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
+    const vault = await loadVaultFromOptions(options);
 
     const cluster: PythCluster = options.cluster;
-    const isRemote = isRemoteCluster(cluster);
     const destination: PublicKey = new PublicKey(options.destination);
-    const vault: PublicKey = new PublicKey(options.vault);
     const amount: number = options.amount;
 
-    const squad = SquadsMesh.endpoint(
-      getPythClusterApiUrl(getMultisigCluster(cluster)),
-      wallet
-    );
-    const msAccount = await squad.getMultisig(vault);
-    const vaultAuthority = squad.getAuthorityPDA(
-      msAccount.publicKey,
-      msAccount.authorityIndex
-    );
-
     const proposalInstruction: TransactionInstruction = SystemProgram.transfer({
-      fromPubkey: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
+      fromPubkey: await vault.getVaultAuthorityPDA(cluster),
       toPubkey: destination,
       lamports: amount * LAMPORTS_PER_SOL,
     });
 
-    await proposeInstructions(
-      squad,
-      vault,
-      [proposalInstruction],
-      isRemote,
-      WORMHOLE_ADDRESS[getMultisigCluster(cluster)]
-    );
+    await vault.proposeInstructions([proposalInstruction], cluster);
   });
 
 multisigCommand("propose-arbitrary-payload", "Propose arbitrary payload")
   .option("-p, --payload <hex-string>", "Wormhole VAA payload")
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
-
-    const cluster: PythCluster = options.cluster;
-    const vault: PublicKey = new PublicKey(options.vault);
-
-    const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
+    const vault = await loadVaultFromOptions(options);
 
     let payload = options.payload;
     if (payload.startsWith("0x")) {
       payload = payload.substring(2);
     }
 
-    await proposeArbitraryPayload(
-      squad,
-      vault,
-      Buffer.from(payload, "hex"),
-      WORMHOLE_ADDRESS[cluster]!
-    );
+    await vault.proposeWormholeMessage(Buffer.from(payload, "hex"));
   });
 
 /**
@@ -449,17 +347,9 @@ multisigCommand("activate", "Activate a transaction sitting in the multisig")
     "address of the draft transaction"
   )
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
-
+    const vault = await loadVaultFromOptions(options);
     const transaction: PublicKey = new PublicKey(options.transaction);
-    const cluster: PythCluster = options.cluster;
-
-    const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
-    await squad.activateTransaction(transaction);
+    await vault.squad.activateTransaction(transaction);
   });
 
 program.parse();

+ 260 - 170
governance/xc_admin/packages/xc_admin_common/src/propose.ts

@@ -1,4 +1,3 @@
-import Squads, { getIxAuthorityPDA, getTxPDA } from "@sqds/mesh";
 import {
   PublicKey,
   Transaction,
@@ -7,6 +6,7 @@ import {
   SYSVAR_CLOCK_PUBKEY,
   SystemProgram,
   PACKET_DATA_SIZE,
+  ConfirmOptions,
 } from "@solana/web3.js";
 import { BN } from "bn.js";
 import { AnchorProvider } from "@coral-xyz/anchor";
@@ -18,6 +18,12 @@ import {
 } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
 import { ExecutePostedVaa } from "./governance_payload/ExecutePostedVaa";
 import { getOpsKey, PRICE_FEED_OPS_KEY } from "./multisig";
+import { PythCluster } from "@pythnetwork/client/lib/cluster";
+import { Wallet } from "@coral-xyz/anchor/dist/cjs/provider";
+import SquadsMesh, { getIxAuthorityPDA, getTxPDA } from "@sqds/mesh";
+import { MultisigAccount } from "@sqds/mesh/lib/types";
+import { mapKey } from "./remote_executor";
+import { WORMHOLE_ADDRESS } from "./wormhole";
 
 export const MAX_EXECUTOR_PAYLOAD_SIZE = PACKET_DATA_SIZE - 687; // Bigger payloads won't fit in one addInstruction call when adding to the proposal
 export const SIZE_OF_SIGNED_BATCH = 30;
@@ -30,198 +36,282 @@ type SquadInstruction = {
   authorityType?: string;
 };
 
-export async function proposeArbitraryPayload(
-  squad: Squads,
-  vault: PublicKey,
-  payload: Buffer,
-  wormholeAddress: PublicKey
-): Promise<PublicKey> {
-  const msAccount = await squad.getMultisig(vault);
+/**
+ * A multisig vault can sign arbitrary instructions with various vault-controlled PDAs, if the multisig approves.
+ * This of course allows the vault to interact with programs on the same blockchain, but a vault also has two
+ * other significant capabilities:
+ * 1. It can execute arbitrary transactions on other blockchains that have the Remote Executor program.
+ *    This allows e.g., a vault on solana mainnet to control programs deployed on PythNet.
+ * 2. It can send wormhole messages from the vault authority. This allows the vault to control programs
+ *    on other chains using Pyth governance messages.
+ */
+export class MultisigVault {
+  public wallet: Wallet;
+  /// The cluster that this multisig lives on
+  public cluster: PythCluster;
+  public squad: SquadsMesh;
+  public vault: PublicKey;
+
+  constructor(
+    wallet: Wallet,
+    cluster: PythCluster,
+    squad: SquadsMesh,
+    vault: PublicKey
+  ) {
+    this.wallet = wallet;
+    this.cluster = cluster;
+    this.squad = squad;
+    this.vault = vault;
+  }
 
-  let ixToSend: TransactionInstruction[] = [];
-  const proposalIndex = msAccount.transactionIndex + 1;
-  ixToSend.push(
-    await squad.buildCreateTransaction(
+  public async getMultisigAccount(): Promise<MultisigAccount> {
+    return this.squad.getMultisig(this.vault);
+  }
+
+  /**
+   * Get the PDA that the vault can sign for on `cluster`. If `cluster` is remote, this PDA
+   * is the PDA of the remote executor program representing the vault's Wormhole emitter address.
+   * @param cluster
+   */
+  public async getVaultAuthorityPDA(cluster?: PythCluster): Promise<PublicKey> {
+    const msAccount = await this.getMultisigAccount();
+    const localAuthorityPDA = await this.squad.getAuthorityPDA(
+      msAccount.publicKey,
+      msAccount.authorityIndex
+    );
+
+    if (cluster === undefined || cluster === this.cluster) {
+      return localAuthorityPDA;
+    } else {
+      return mapKey(localAuthorityPDA);
+    }
+  }
+
+  public wormholeAddress(): PublicKey | undefined {
+    // TODO: we should configure the wormhole address as a vault parameter.
+    return WORMHOLE_ADDRESS[this.cluster];
+  }
+
+  // TODO: does this need a cluster argument?
+  public async getAuthorityPDA(authorityIndex: number = 1): Promise<PublicKey> {
+    return this.squad.getAuthorityPDA(this.vault, authorityIndex);
+  }
+
+  // NOTE: this function probably doesn't belong on this class, but it makes it easier to refactor so we'll leave
+  // it here for now.
+  public getAnchorProvider(opts?: ConfirmOptions): AnchorProvider {
+    if (opts === undefined) {
+      opts = AnchorProvider.defaultOptions();
+    }
+
+    return new AnchorProvider(this.squad.connection, this.squad.wallet, opts);
+  }
+
+  // Convenience wrappers around squads methods
+
+  public async createProposalIx(
+    proposalIndex: number
+  ): Promise<[TransactionInstruction, PublicKey]> {
+    const msAccount = await this.squad.getMultisig(this.vault);
+
+    const ix = await this.squad.buildCreateTransaction(
       msAccount.publicKey,
       msAccount.authorityIndex,
       proposalIndex
-    )
-  );
+    );
 
-  const newProposalAddress = getTxPDA(
-    vault,
-    new BN(proposalIndex),
-    squad.multisigProgramId
-  )[0];
+    const newProposalAddress = getTxPDA(
+      this.vault,
+      new BN(proposalIndex),
+      this.squad.multisigProgramId
+    )[0];
 
-  const instructionToPropose = await getPostMessageInstruction(
-    squad,
-    vault,
-    newProposalAddress,
-    1,
-    wormholeAddress,
-    payload
-  );
-  ixToSend.push(
-    await squad.buildAddInstruction(
-      vault,
+    return [ix, newProposalAddress];
+  }
+
+  public async activateProposalIx(
+    proposalAddress: PublicKey
+  ): Promise<TransactionInstruction> {
+    return await this.squad.buildActivateTransaction(
+      this.vault,
+      proposalAddress
+    );
+  }
+
+  public async approveProposalIx(
+    proposalAddress: PublicKey
+  ): Promise<TransactionInstruction> {
+    return await this.squad.buildApproveTransaction(
+      this.vault,
+      proposalAddress
+    );
+  }
+
+  // Propose instructions
+
+  /**
+   * Propose submitting `payload` as a wormhole message. If the proposal is approved, the sent message
+   * will have `this.getVaultAuthorityPda()` as its emitter address.
+   * @param payload the bytes to send as the wormhole message's payload.
+   * @returns the newly created proposal's public key
+   */
+  public async proposeWormholeMessage(payload: Buffer): Promise<PublicKey> {
+    const msAccount = await this.getMultisigAccount();
+
+    let ixToSend: TransactionInstruction[] = [];
+    const [proposalIx, newProposalAddress] = await this.createProposalIx(
+      msAccount.transactionIndex + 1
+    );
+
+    const proposalIndex = msAccount.transactionIndex + 1;
+    ixToSend.push(proposalIx);
+
+    const instructionToPropose = await getPostMessageInstruction(
+      this.squad,
+      this.vault,
       newProposalAddress,
-      instructionToPropose.instruction,
       1,
-      instructionToPropose.authorityIndex,
-      instructionToPropose.authorityBump,
-      instructionToPropose.authorityType
-    )
-  );
-  ixToSend.push(
-    await squad.buildActivateTransaction(vault, newProposalAddress)
-  );
-  ixToSend.push(await squad.buildApproveTransaction(vault, newProposalAddress));
-
-  const txToSend = batchIntoTransactions(ixToSend);
-
-  for (let i = 0; i < txToSend.length; i += SIZE_OF_SIGNED_BATCH) {
-    await new AnchorProvider(
-      squad.connection,
-      squad.wallet,
-      AnchorProvider.defaultOptions()
-    ).sendAll(
-      txToSend.slice(i, i + SIZE_OF_SIGNED_BATCH).map((tx) => {
-        return { tx, signers: [] };
-      })
+      this.wormholeAddress()!,
+      payload
     );
-  }
-  return newProposalAddress;
-}
+    ixToSend.push(
+      await this.squad.buildAddInstruction(
+        this.vault,
+        newProposalAddress,
+        instructionToPropose.instruction,
+        1,
+        instructionToPropose.authorityIndex,
+        instructionToPropose.authorityBump,
+        instructionToPropose.authorityType
+      )
+    );
+    ixToSend.push(await this.activateProposalIx(newProposalAddress));
+    ixToSend.push(await this.approveProposalIx(newProposalAddress));
 
-/**
- * Propose an array of `TransactionInstructions` as a proposal
- * @param squad Squads client
- * @param vault vault public key (the id of the multisig where these instructions should be proposed)
- * @param instructions instructions that will be proposed
- * @param remote whether the instructions should be executed in the chain of the multisig or remotely on Pythnet
- * @returns the newly created proposal's pubkey
- */
-export async function proposeInstructions(
-  squad: Squads,
-  vault: PublicKey,
-  instructions: TransactionInstruction[],
-  remote: boolean,
-  wormholeAddress?: PublicKey
-): Promise<PublicKey> {
-  const msAccount = await squad.getMultisig(vault);
-  const newProposals = [];
-
-  let ixToSend: TransactionInstruction[] = [];
-  if (remote) {
-    if (!wormholeAddress) {
-      throw new Error("Need wormhole address");
+    const txToSend = batchIntoTransactions(ixToSend);
+    for (let i = 0; i < txToSend.length; i += SIZE_OF_SIGNED_BATCH) {
+      await this.getAnchorProvider().sendAll(
+        txToSend.slice(i, i + SIZE_OF_SIGNED_BATCH).map((tx) => {
+          return { tx, signers: [] };
+        })
+      );
     }
-    const batches = batchIntoExecutorPayload(instructions);
-
-    for (let j = 0; j < batches.length; j += MAX_INSTRUCTIONS_PER_PROPOSAL) {
-      const proposalIndex =
-        msAccount.transactionIndex + 1 + j / MAX_INSTRUCTIONS_PER_PROPOSAL;
-      ixToSend.push(
-        await squad.buildCreateTransaction(
-          msAccount.publicKey,
-          msAccount.authorityIndex,
+    return newProposalAddress;
+  }
+
+  /**
+   * Propose an array of `TransactionInstructions` as one or more proposals
+   * @param instructions instructions that will be proposed
+   * @param targetCluster the cluster where the instructions should be executed. If the cluster is not the
+   * same as the one this multisig is on, execution will use wormhole and the remote executor program.
+   * @returns the newly created proposals' public keys
+   */
+  public async proposeInstructions(
+    instructions: TransactionInstruction[],
+    targetCluster?: PythCluster
+  ): Promise<PublicKey[]> {
+    const msAccount = await this.getMultisigAccount();
+    const newProposals = [];
+
+    const remote = targetCluster != this.cluster;
+
+    let ixToSend: TransactionInstruction[] = [];
+    if (remote) {
+      if (!this.wormholeAddress()) {
+        throw new Error("Need wormhole address");
+      }
+      const batches = batchIntoExecutorPayload(instructions);
+
+      for (let j = 0; j < batches.length; j += MAX_INSTRUCTIONS_PER_PROPOSAL) {
+        const proposalIndex =
+          msAccount.transactionIndex + 1 + j / MAX_INSTRUCTIONS_PER_PROPOSAL;
+        const [proposalIx, newProposalAddress] = await this.createProposalIx(
           proposalIndex
-        )
-      );
-      const newProposalAddress = getTxPDA(
-        vault,
-        new BN(proposalIndex),
-        squad.multisigProgramId
-      )[0];
-      newProposals.push(newProposalAddress);
-
-      for (const [i, batch] of batches
-        .slice(j, j + MAX_INSTRUCTIONS_PER_PROPOSAL)
-        .entries()) {
-        const squadIx = await wrapAsRemoteInstruction(
-          squad,
-          vault,
-          newProposalAddress,
-          batch,
-          i + 1,
-          wormholeAddress
         );
-        ixToSend.push(
-          await squad.buildAddInstruction(
-            vault,
+        ixToSend.push(proposalIx);
+        newProposals.push(newProposalAddress);
+
+        for (const [i, batch] of batches
+          .slice(j, j + MAX_INSTRUCTIONS_PER_PROPOSAL)
+          .entries()) {
+          const squadIx = await wrapAsRemoteInstruction(
+            this.squad,
+            this.vault,
             newProposalAddress,
-            squadIx.instruction,
+            batch,
             i + 1,
-            squadIx.authorityIndex,
-            squadIx.authorityBump,
-            squadIx.authorityType
-          )
-        );
+            this.wormholeAddress()!
+          );
+          ixToSend.push(
+            await this.squad.buildAddInstruction(
+              this.vault,
+              newProposalAddress,
+              squadIx.instruction,
+              i + 1,
+              squadIx.authorityIndex,
+              squadIx.authorityBump,
+              squadIx.authorityType
+            )
+          );
+        }
+        ixToSend.push(await this.activateProposalIx(newProposalAddress));
+        ixToSend.push(await this.approveProposalIx(newProposalAddress));
       }
-      ixToSend.push(
-        await squad.buildActivateTransaction(vault, newProposalAddress)
-      );
-      ixToSend.push(
-        await squad.buildApproveTransaction(vault, newProposalAddress)
-      );
-    }
-  } else {
-    for (
-      let j = 0;
-      j < instructions.length;
-      j += MAX_INSTRUCTIONS_PER_PROPOSAL
-    ) {
-      const proposalIndex =
-        msAccount.transactionIndex + 1 + j / MAX_INSTRUCTIONS_PER_PROPOSAL;
-      ixToSend.push(
-        await squad.buildCreateTransaction(
-          msAccount.publicKey,
-          msAccount.authorityIndex,
+    } else {
+      for (
+        let j = 0;
+        j < instructions.length;
+        j += MAX_INSTRUCTIONS_PER_PROPOSAL
+      ) {
+        const proposalIndex =
+          msAccount.transactionIndex + 1 + j / MAX_INSTRUCTIONS_PER_PROPOSAL;
+        const [proposalIx, newProposalAddress] = await this.createProposalIx(
           proposalIndex
-        )
-      );
-      const newProposalAddress = getTxPDA(
-        vault,
-        new BN(proposalIndex),
-        squad.multisigProgramId
-      )[0];
-      newProposals.push(newProposalAddress);
-
-      for (let [i, instruction] of instructions
-        .slice(j, j + MAX_INSTRUCTIONS_PER_PROPOSAL)
-        .entries()) {
+        );
+        ixToSend.push(proposalIx);
+        newProposals.push(newProposalAddress);
+
+        for (let [i, instruction] of instructions
+          .slice(j, j + MAX_INSTRUCTIONS_PER_PROPOSAL)
+          .entries()) {
+          ixToSend.push(
+            await this.squad.buildAddInstruction(
+              this.vault,
+              newProposalAddress,
+              instruction,
+              i + 1
+            )
+          );
+        }
         ixToSend.push(
-          await squad.buildAddInstruction(
-            vault,
-            newProposalAddress,
-            instruction,
-            i + 1
+          await this.squad.buildActivateTransaction(
+            this.vault,
+            newProposalAddress
+          )
+        );
+        ixToSend.push(
+          await this.squad.buildApproveTransaction(
+            this.vault,
+            newProposalAddress
           )
         );
       }
-      ixToSend.push(
-        await squad.buildActivateTransaction(vault, newProposalAddress)
-      );
-      ixToSend.push(
-        await squad.buildApproveTransaction(vault, newProposalAddress)
-      );
     }
-  }
 
-  const txToSend = batchIntoTransactions(ixToSend);
+    const txToSend = batchIntoTransactions(ixToSend);
 
-  for (let i = 0; i < txToSend.length; i += SIZE_OF_SIGNED_BATCH) {
-    await new AnchorProvider(squad.connection, squad.wallet, {
-      preflightCommitment: "processed",
-      commitment: "confirmed",
-    }).sendAll(
-      txToSend.slice(i, i + SIZE_OF_SIGNED_BATCH).map((tx) => {
-        return { tx, signers: [] };
-      })
-    );
+    for (let i = 0; i < txToSend.length; i += SIZE_OF_SIGNED_BATCH) {
+      await this.getAnchorProvider({
+        preflightCommitment: "processed",
+        commitment: "confirmed",
+      }).sendAll(
+        txToSend.slice(i, i + SIZE_OF_SIGNED_BATCH).map((tx) => {
+          return { tx, signers: [] };
+        })
+      );
+    }
+    return newProposals;
   }
-  return newProposals[0];
 }
 
 /**
@@ -347,7 +437,7 @@ export function getSizeOfCompressedU16(n: number) {
  * @returns an instruction to be proposed
  */
 export async function wrapAsRemoteInstruction(
-  squad: Squads,
+  squad: SquadsMesh,
   vault: PublicKey,
   proposalAddress: PublicKey,
   instructions: TransactionInstruction[],
@@ -375,7 +465,7 @@ export async function wrapAsRemoteInstruction(
  * @param payload the payload to be posted
  */
 async function getPostMessageInstruction(
-  squad: Squads,
+  squad: SquadsMesh,
   vault: PublicKey,
   proposalAddress: PublicKey,
   instructionIndex: number,

+ 0 - 1
governance/xc_admin/packages/xc_admin_frontend/components/PermissionDepermissionKey.tsx

@@ -14,7 +14,6 @@ import {
   isRemoteCluster,
   mapKey,
   PRICE_FEED_MULTISIG,
-  proposeInstructions,
   WORMHOLE_ADDRESS,
 } from 'xc_admin_common'
 import { ClusterContext } from '../contexts/ClusterContext'

+ 0 - 1
governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx

@@ -18,7 +18,6 @@ import {
   MESSAGE_BUFFER_PROGRAM_ID,
   MESSAGE_BUFFER_BUFFER_SIZE,
   PRICE_FEED_MULTISIG,
-  proposeInstructions,
   WORMHOLE_ADDRESS,
   PRICE_FEED_OPS_KEY,
   getMessageBufferAddressForPrice,

+ 9 - 6
governance/xc_admin/packages/xc_admin_frontend/components/tabs/UpdatePermissions.tsx

@@ -21,9 +21,9 @@ import {
   getMultisigCluster,
   isRemoteCluster,
   mapKey,
-  proposeInstructions,
   WORMHOLE_ADDRESS,
   UPGRADE_MULTISIG,
+  MultisigVault,
 } from 'xc_admin_common'
 import { ClusterContext } from '../../contexts/ClusterContext'
 import { useMultisigContext } from '../../contexts/MultisigContext'
@@ -266,13 +266,16 @@ const UpdatePermissions = () => {
           if (!isMultisigLoading) {
             setIsSendProposalButtonLoading(true)
             try {
-              const proposalPubkey = await proposeInstructions(
+              const vault = new MultisigVault(
+                proposeSquads.wallet,
+                getMultisigCluster(cluster),
                 proposeSquads,
-                UPGRADE_MULTISIG[getMultisigCluster(cluster)],
-                [instruction],
-                isRemoteCluster(cluster),
-                WORMHOLE_ADDRESS[getMultisigCluster(cluster)]
+                UPGRADE_MULTISIG[getMultisigCluster(cluster)]
               )
+
+              const proposalPubkey = (
+                await vault.proposeInstructions([instruction], cluster)
+              )[0]
               toast.success(
                 `Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`
               )