Jayant Krishnamurthy 2 年 前
コミット
6ca2bdd0a1

+ 5 - 0
governance/xc_admin/packages/xc_admin_cli/src/index.ts

@@ -398,11 +398,16 @@ multisigCommand("propose-sol-transfer", "Propose sol transfer")
       msAccount.authorityIndex
     );
 
+    const admin = new PythAdmin(wallet, cluster, squad, vault);
+    admin;
+
+    /*
     const proposalInstruction: TransactionInstruction = SystemProgram.transfer({
       fromPubkey: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
       toPubkey: destination,
       lamports: amount * LAMPORTS_PER_SOL,
     });
+     */
 
     await proposeInstructions(
       squad,

+ 484 - 0
governance/xc_admin/packages/xc_admin_common/src/PythAdmin.ts

@@ -0,0 +1,484 @@
+import { PythCluster } from "@pythnetwork/client/lib/cluster";
+import { Wallet } from "@coral-xyz/anchor/dist/cjs/provider";
+import SquadsMesh, { getIxAuthorityPDA, getTxPDA } from "@sqds/mesh";
+import {
+  PublicKey,
+  SystemProgram,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import {
+  batchIntoExecutorPayload,
+  MAX_INSTRUCTIONS_PER_PROPOSAL,
+  wrapAsRemoteInstruction,
+} from "./propose";
+import { MultisigAccount } from "@sqds/mesh/lib/types";
+import { BN } from "bn.js";
+
+class PythComponentManager {
+  public tokenAccount(cluster: PythCluster) {}
+
+  public oracleProgram(cluster: PythCluster) {}
+}
+
+export interface TxWrapper {
+  wrap: (
+    instructions: TransactionInstruction[]
+  ) => Promise<TransactionInstruction[]>;
+}
+
+export class SquadWrapper {
+  private admin: PythAdmin;
+
+  constructor(admin: PythAdmin) {
+    this.admin = admin;
+  }
+
+  public async wrap(
+    instructions: TransactionInstruction[]
+  ): Promise<TransactionInstruction[]> {
+    const ixToSend: TransactionInstruction[] = [];
+    const newProposals = [];
+
+    const msAccount = await this.admin.getMultisigAccount();
+    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 this.admin.squad.buildCreateTransaction(
+          msAccount.publicKey,
+          msAccount.authorityIndex,
+          proposalIndex
+        )
+      );
+      const newProposalAddress = getTxPDA(
+        this.admin.vault,
+        new BN(proposalIndex),
+        this.admin.squad.multisigProgramId
+      )[0];
+      newProposals.push(newProposalAddress);
+
+      for (let [i, instruction] of instructions
+        .slice(j, j + MAX_INSTRUCTIONS_PER_PROPOSAL)
+        .entries()) {
+        ixToSend.push(
+          await this.admin.squad.buildAddInstruction(
+            this.admin.vault,
+            newProposalAddress,
+            instruction,
+            i + 1
+          )
+        );
+      }
+      ixToSend.push(
+        await this.admin.squad.buildActivateTransaction(
+          this.admin.vault,
+          newProposalAddress
+        )
+      );
+      ixToSend.push(
+        await this.admin.squad.buildApproveTransaction(
+          this.admin.vault,
+          newProposalAddress
+        )
+      );
+    }
+
+    return ixToSend;
+  }
+}
+
+export class RemoteWrapper implements TxWrapper {
+  private admin: PythAdmin;
+
+  constructor(admin: PythAdmin) {
+    this.admin = admin;
+  }
+
+  public async wrap(
+    instructions: TransactionInstruction[]
+  ): Promise<TransactionInstruction[]> {
+    const ixToSend: TransactionInstruction[] = [];
+    const newProposals = [];
+
+    const msAccount = await this.admin.getMultisigAccount();
+    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 this.admin.squad.buildCreateTransaction(
+          msAccount.publicKey,
+          msAccount.authorityIndex,
+          proposalIndex
+        )
+      );
+      const newProposalAddress = getTxPDA(
+        this.admin.vault,
+        new BN(proposalIndex),
+        this.admin.squad.multisigProgramId
+      )[0];
+      newProposals.push(newProposalAddress);
+
+      for (let [i, instruction] of instructions
+        .slice(j, j + MAX_INSTRUCTIONS_PER_PROPOSAL)
+        .entries()) {
+        ixToSend.push(
+          await this.admin.squad.buildAddInstruction(
+            this.admin.vault,
+            newProposalAddress,
+            instruction,
+            i + 1
+          )
+        );
+      }
+      ixToSend.push(
+        await this.admin.squad.buildActivateTransaction(
+          this.admin.vault,
+          newProposalAddress
+        )
+      );
+      ixToSend.push(
+        await this.admin.squad.buildApproveTransaction(
+          this.admin.vault,
+          newProposalAddress
+        )
+      );
+    }
+
+    return ixToSend;
+  }
+}
+
+class DefaultBuilder implements TxBuilder {
+  public build() {}
+}
+
+class TokenAccountTxBuilder implements TxBuilder {
+  private admin: PythAdmin;
+
+  // Tokens will be sent from this account / cluster.
+  private fromPubkey: PublicKey;
+
+  private instructions: TransactionInstruction[];
+
+  private wrapper: TxWrapper;
+
+  constructor(admin: PythAdmin) {
+    this.admin = admin;
+    this.instructions = [];
+  }
+
+  // TODO: this needs to be a bignumber
+  public transferSol(qtyLamports: number, to: PublicKey) {
+    const proposalInstruction: TransactionInstruction = SystemProgram.transfer({
+      fromPubkey: this.fromPubkey,
+      toPubkey: to,
+      lamports: qtyLamports,
+    });
+
+    this.instructions.push(proposalInstruction);
+  }
+
+  public build(): TransactionInstruction[] {
+    return this.wrapper.wrap(this.instructions);
+  }
+}
+
+class RemoteExecutorTxBuilder {
+  private admin: PythAdmin;
+
+  constructor(admin: PythAdmin) {
+    this.admin = admin;
+  }
+}
+
+class OracleProgramTxBuilder {
+  private admin: PythAdmin;
+
+  constructor(admin: PythAdmin) {
+    this.admin = admin;
+  }
+}
+
+// todo extract to an interface
+class PythAdmin {
+  public wallet: Wallet;
+  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;
+  }
+
+  public async getMultisigAccount(): Promise<MultisigAccount> {
+    return this.squad.getMultisig(this.vault);
+  }
+
+  public async getAuthorityPDA(authorityIndex: number = 1): Promise<PublicKey> {
+    return await this.squad.getAuthorityPDA(this.vault, authorityIndex);
+  }
+
+  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(
+      this.vault,
+      new BN(proposalIndex),
+      this.squad.multisigProgramId
+    )[0];
+
+    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
+    );
+  }
+
+  public tokenAccount() {}
+
+  public remoteExecutor() {
+    return new RemoteExecutorTxBuilder(this);
+  }
+}
+
+export class MultisigBuilder {
+  private admin: PythAdmin;
+  // msAccount.transactionIndex + 1
+  private nextProposalIndex: number;
+
+  private proposals: ProposalBuilder[];
+
+  public async addProposal(): Promise<ProposalBuilder> {
+    const curProposalIndex = this.nextProposalIndex;
+    this.nextProposalIndex += 1;
+
+    const [ix, proposalAddress] = await this.admin.createProposalIx(
+      curProposalIndex
+    );
+    return new ProposalBuilder(
+      this.admin,
+      curProposalIndex,
+      proposalAddress,
+      ix
+    );
+  }
+
+  public async build(): Promise<TransactionInstruction[]> {
+    const ixs = [];
+    for (const proposal of this.proposals) {
+      ixs.push(...(await proposal.build()));
+    }
+    return ixs;
+  }
+
+  /*
+  public async buildTxs(): Promise<Transaction[]> {
+
+  }
+   */
+}
+
+export interface IProposalBuilder {
+  addInstruction(instruction: TransactionInstruction): Promise<void>;
+  addInstructionWithAuthority(
+    factory: (authority: SquadsAuthority) => Promise<TransactionInstruction>
+  ): Promise<void>;
+  build(): Promise<TransactionInstruction[]>;
+}
+
+export class ProposalBuilder implements IProposalBuilder {
+  private admin: PythAdmin;
+  public proposalIndex: number;
+
+  public proposalAddress: PublicKey;
+  private instructions: TransactionInstruction[];
+
+  constructor(
+    admin: PythAdmin,
+    proposalIndex: number,
+    proposalAddress: PublicKey,
+    createProposalIx: TransactionInstruction
+  ) {
+    this.admin = admin;
+    this.proposalIndex = proposalIndex;
+    this.proposalAddress = proposalAddress;
+
+    this.instructions = [createProposalIx];
+  }
+
+  public async addInstruction(instruction: TransactionInstruction) {
+    this.instructions.push(
+      await this.admin.squad.buildAddInstruction(
+        this.admin.vault,
+        this.proposalAddress,
+        instruction,
+        this.instructions.length
+      )
+    );
+  }
+
+  // Each instruction within a proposal can sign with its own PDA
+  public async addInstructionWithAuthority(
+    factory: (authority: SquadsAuthority) => Promise<TransactionInstruction>
+  ) {
+    const instructionIndex = this.instructions.length;
+    const authorityType = "custom";
+    const [pda, bump] = getIxAuthorityPDA(
+      this.proposalAddress,
+      new BN(instructionIndex),
+      this.admin.squad.multisigProgramId
+    );
+    const innerInstruction = await factory({
+      pda,
+      index: instructionIndex,
+      bump,
+      type: authorityType,
+    });
+    const instruction = await this.admin.squad.buildAddInstruction(
+      this.admin.vault,
+      this.proposalAddress,
+      innerInstruction,
+      instructionIndex,
+      instructionIndex,
+      bump,
+      authorityType
+    );
+    this.instructions.push(instruction);
+  }
+
+  public async build(): Promise<TransactionInstruction[]> {
+    // TODO: maybe this should be a separate method ?
+    this.instructions.push(
+      await this.admin.activateProposalIx(this.proposalAddress)
+    );
+    this.instructions.push(
+      await this.admin.approveProposalIx(this.proposalAddress)
+    );
+
+    return this.instructions;
+  }
+
+  public length() {
+    // FIXME: this fails once you call build
+    return this.instructions.length - 1;
+  }
+}
+
+export class BatchedBuilder implements IProposalBuilder {
+  private builder: MultisigBuilder;
+  private currentProposal: ProposalBuilder | undefined;
+
+  private async advanceProposalIfNeeded() {
+    if (this.currentProposal === undefined) {
+      this.currentProposal = await this.builder.addProposal();
+    } else if (this.currentProposal.length() == MAX_INSTRUCTIONS_PER_PROPOSAL) {
+      this.currentProposal = await this.builder.addProposal();
+    }
+  }
+
+  public async addInstruction(instruction: TransactionInstruction) {
+    await this.advanceProposalIfNeeded();
+    await this.currentProposal!.addInstruction(instruction);
+  }
+
+  public async addInstructionWithAuthority(
+    factory: (authority: SquadsAuthority) => Promise<TransactionInstruction>
+  ) {
+    await this.advanceProposalIfNeeded();
+    await this.currentProposal!.addInstructionWithAuthority(factory);
+  }
+
+  public async build(): Promise<TransactionInstruction[]> {
+    return await this.builder.build();
+  }
+}
+
+/**
+ * Executes instructions on a remote Solana network (e.g., Pythnet) using
+ * the remote executor program.
+ */
+export class RemoteBuilder {
+  private builder: IProposalBuilder;
+  private wormholeAddress: PublicKey;
+
+  private instructions: TransactionInstruction[];
+
+  public async addInstruction(instruction: TransactionInstruction) {
+    this.instructions.push(instruction);
+  }
+
+  public async build(): Promise<TransactionInstruction[]> {
+    const batches = batchIntoExecutorPayload(this.instructions);
+    for (const [i, batch] of batches.entries()) {
+      this.builder.addInstructionWithAuthority(
+        async (authority: SquadsAuthority) => {
+          return await wrapAsRemoteInstruction(
+            this.builder.admin,
+            authority,
+            this.wormholeAddress,
+            batch
+          );
+        }
+      );
+    }
+
+    return await this.builder.build();
+  }
+}
+
+export interface SquadsAuthority {
+  pda: PublicKey;
+  index: number;
+  bump: number;
+  type: string;
+}
+
+/*
+class RemoteTxBuilder {
+  private builder MultisigBuilder;
+
+  private curProposal: ProposalBuilder;
+
+  public async addInstructionDynamic(factory: (proposalAddress: PublicKey, instructionIndex: number) => TransactionInstruction) {
+
+  }
+}
+*/

+ 40 - 169
governance/xc_admin/packages/xc_admin_common/src/propose.ts

@@ -18,66 +18,31 @@ import {
 } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
 import { ExecutePostedVaa } from "./governance_payload/ExecutePostedVaa";
 import { getOpsKey, PRICE_FEED_OPS_KEY } from "./multisig";
+import {BatchedBuilder, MultisigBuilder, SquadsAuthority} from "./PythAdmin";
 
 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;
 export const MAX_INSTRUCTIONS_PER_PROPOSAL = 256 - 1;
 
-type SquadInstruction = {
-  instruction: TransactionInstruction;
-  authorityIndex?: number;
-  authorityBump?: number;
-  authorityType?: string;
-};
-
 export async function proposeArbitraryPayload(
   squad: Squads,
   vault: PublicKey,
   payload: Buffer,
   wormholeAddress: PublicKey
 ): Promise<PublicKey> {
-  const msAccount = await squad.getMultisig(vault);
-
-  let ixToSend: TransactionInstruction[] = [];
-  const proposalIndex = msAccount.transactionIndex + 1;
-  ixToSend.push(
-    await squad.buildCreateTransaction(
-      msAccount.publicKey,
-      msAccount.authorityIndex,
-      proposalIndex
-    )
-  );
+  const builder: MultisigBuilder = null;
+  const proposal = await builder.addProposal();
 
-  const newProposalAddress = getTxPDA(
-    vault,
-    new BN(proposalIndex),
-    squad.multisigProgramId
-  )[0];
-
-  const instructionToPropose = await getPostMessageInstruction(
-    squad,
-    vault,
-    newProposalAddress,
-    1,
-    wormholeAddress,
-    payload
-  );
-  ixToSend.push(
-    await squad.buildAddInstruction(
-      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));
+  proposal.addInstructionWithAuthority(async authority => {
+    return await getPostMessageInstruction(
+      builder.admin,
+      authority,
+      wormholeAddress,
+      payload
+    );
+  });
 
+  const ixToSend = await builder.build();
   const txToSend = batchIntoTransactions(ixToSend);
 
   for (let i = 0; i < txToSend.length; i += SIZE_OF_SIGNED_BATCH) {
@@ -100,7 +65,7 @@ export async function proposeArbitraryPayload(
  * @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
+ * @returns the newly created proposal's pubkeyopoer
  */
 export async function proposeInstructions(
   squad: Squads,
@@ -109,106 +74,28 @@ export async function proposeInstructions(
   remote: boolean,
   wormholeAddress?: PublicKey
 ): Promise<PublicKey> {
-  const msAccount = await squad.getMultisig(vault);
-  const newProposals = [];
+  const builder: BatchedBuilder = null;
 
-  let ixToSend: TransactionInstruction[] = [];
   if (remote) {
     if (!wormholeAddress) {
       throw new Error("Need wormhole address");
     }
-    const batches = batchIntoExecutorPayload(instructions);
+    const remoteBuilder = new RemoteBuilder(builder, wormholeAddress!);
+    for (const instruction of instructions) {
+      remoteBuilder.addInstruction(instruction);
+    }
 
-    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,
-          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,
-            newProposalAddress,
-            squadIx.instruction,
-            i + 1,
-            squadIx.authorityIndex,
-            squadIx.authorityBump,
-            squadIx.authorityType
-          )
-        );
-      }
-      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,
-          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(
-          await squad.buildAddInstruction(
-            vault,
-            newProposalAddress,
-            instruction,
-            i + 1
-          )
-        );
+      for (let [i, instruction] of instructions.entries()) {
+        builder.addInstruction(instruction);
       }
-      ixToSend.push(
-        await squad.buildActivateTransaction(vault, newProposalAddress)
-      );
-      ixToSend.push(
-        await squad.buildApproveTransaction(vault, newProposalAddress)
-      );
     }
   }
 
+  // fixme needs to be remoteBuilder
+  const ixToSend = await builder.build();
+
   const txToSend = batchIntoTransactions(ixToSend);
 
   for (let i = 0; i < txToSend.length; i += SIZE_OF_SIGNED_BATCH) {
@@ -347,19 +234,16 @@ export function getSizeOfCompressedU16(n: number) {
  * @returns an instruction to be proposed
  */
 export async function wrapAsRemoteInstruction(
-  squad: Squads,
-  vault: PublicKey,
-  proposalAddress: PublicKey,
+  admin: PythAdmin,
+  authority: SquadsAuthority,
+  wormholeAddress: PublicKey,
   instructions: TransactionInstruction[],
-  instructionIndex: number,
-  wormholeAddress: PublicKey
-): Promise<SquadInstruction> {
+): Promise<TransactionInstruction> {
   const buffer: Buffer = new ExecutePostedVaa("pythnet", instructions).encode();
+
   return await getPostMessageInstruction(
-    squad,
-    vault,
-    proposalAddress,
-    instructionIndex,
+    admin,
+    authority,
     wormholeAddress,
     buffer
   );
@@ -375,23 +259,15 @@ export async function wrapAsRemoteInstruction(
  * @param payload the payload to be posted
  */
 async function getPostMessageInstruction(
-  squad: Squads,
-  vault: PublicKey,
-  proposalAddress: PublicKey,
-  instructionIndex: number,
+  admin: PythAdmin,
+  authority: SquadsAuthority,
   wormholeAddress: PublicKey,
   payload: Buffer
-): Promise<SquadInstruction> {
-  const [messagePDA, messagePdaBump] = getIxAuthorityPDA(
-    proposalAddress,
-    new BN(instructionIndex),
-    squad.multisigProgramId
-  );
-
-  const emitter = squad.getAuthorityPDA(vault, 1);
+): Promise<TransactionInstruction> {
+  const emitter = admin.getAuthorityPDA();
   const provider = new AnchorProvider(
-    squad.connection,
-    squad.wallet,
+    admin.squad.connection,
+    admin.squad.wallet,
     AnchorProvider.defaultOptions()
   );
   const wormholeProgram = createWormholeProgramInterface(
@@ -402,19 +278,14 @@ async function getPostMessageInstruction(
   const accounts = getPostMessageAccounts(
     wormholeAddress,
     emitter,
-    getOpsKey(vault),
-    messagePDA
+    getOpsKey(admin.vault),
+    authority.pda
   );
 
-  return {
-    instruction: await wormholeProgram.methods
+  return await wormholeProgram.methods
       .postMessage(0, payload, 0)
       .accounts(accounts)
-      .instruction(),
-    authorityIndex: instructionIndex,
-    authorityBump: messagePdaBump,
-    authorityType: "custom",
-  };
+      .instruction();
 }
 
 function getPostMessageAccounts(