diff --git a/sdk/multisig/src/actions/actionTypes.ts b/sdk/multisig/src/actions/actionTypes.ts new file mode 100644 index 00000000..8e67f339 --- /dev/null +++ b/sdk/multisig/src/actions/actionTypes.ts @@ -0,0 +1,106 @@ +export type Methods = { + [K in keyof MethodProgression]: T extends K ? MethodProgression[K] : never; +}[keyof MethodProgression]; + +export type BatchMethods = { + [K in keyof BatchMethodProgression]: T extends K + ? BatchMethodProgression[K] + : never; +}[keyof BatchMethodProgression]; + +type BaseMethodKeys = + | "getInstructions" + | "transaction" + | "send" + | "sendAndConfirm" + | "customSend"; + +type BaseGetKeys = "getInstructions"; +type BaseSendKeys = "send" | "sendAndConfirm" | "customSend"; + +type TransactionGetKeys = + | "getIndex" + | "getTransactionKey" + | "getProposalKey" + | "getTransactionAccount" + | "getProposalAccount"; + +type TransactionActionKeys = + | "withProposal" + | "withApproval" + | "withRejection" + | "withExecute"; + +type BatchGetKeys = + | "getBatchKey" + | "getBatchTransactionKey" + | "getAllBatchTransactionKeys" + | "getBatchAccount"; + +type BatchActionKeys = "addTransaction" | TransactionActionKeys; + +type MethodProgression = { + // Senders + send: never; + sendAndConfirm: never; + customSend: never; + // Transaction Actions + withProposal: + | "withApproval" + | "withRejection" + | BaseSendKeys + | TransactionGetKeys; + withApproval: + | "withExecute" + | "withRejection" + | BaseSendKeys + | TransactionGetKeys; + withRejection: + | "withExecute" + | "withApproval" + | BaseSendKeys + | TransactionGetKeys; + withExecute: BaseSendKeys | TransactionGetKeys; + // Synchronous Getters + getInstructions: BaseMethodKeys | BaseSendKeys; + getIndex: + | BaseMethodKeys + | TransactionActionKeys + | TransactionGetKeys + | BatchActionKeys + | BatchGetKeys; + getTransactionKey: + | BaseMethodKeys + | TransactionActionKeys + | TransactionGetKeys + | BatchActionKeys + | BatchGetKeys; + getProposalKey: + | BaseMethodKeys + | TransactionActionKeys + | TransactionGetKeys + | BatchActionKeys + | BatchGetKeys; + // Asynchronous Getters + getTransactionAccount: never; + getProposalAccount: never; +}; + +type BatchMethodProgression = { + send: never; + sendAndConfirm: never; + customSend: never; + withProposal: "withApproval" | "withRejection" | BaseSendKeys; + withApproval: "withExecute" | "withRejection" | BaseSendKeys | BatchGetKeys; + withRejection: "withExecute" | "withApproval" | BaseSendKeys; + withExecute: BaseSendKeys; + getBatchKey: + | BaseMethodKeys + | TransactionActionKeys + | TransactionGetKeys + | BatchActionKeys + | BatchGetKeys; + getBatchTransactionKey: BatchActionKeys | BatchGetKeys; + getBatchAccount: never; + addTransaction: never; +}; diff --git a/sdk/multisig/src/actions/common.ts b/sdk/multisig/src/actions/common.ts index b6621515..8ab6f269 100644 --- a/sdk/multisig/src/actions/common.ts +++ b/sdk/multisig/src/actions/common.ts @@ -8,10 +8,18 @@ import { TransactionSignature, VersionedTransaction, } from "@solana/web3.js"; -import { PROGRAM_ID, instructions } from ".."; +import { + PROGRAM_ID, + getProposalPda, + getTransactionPda, + instructions, +} from ".."; +import { Proposal, VaultTransaction } from "../accounts"; export interface BaseBuilderArgs { + /** The connection to an SVM network cluster */ connection: Connection; + /** The public key of the creator */ creator: PublicKey; } @@ -43,10 +51,29 @@ export abstract class BaseBuilder< protected abstract build(): Promise; + getInstructions(): TransactionInstruction[] { + return this.instructions; + } + /** * Creates a transaction containing the corresponding instruction(s). - * @args feePayer - Optional signer to pay the transaction fee. - * @returns `VersionedTransaction`. + * + * @args `feePayer` - Optional signer to pay the transaction fee. + * @returns `VersionedTransaction` + * + * @example + * // Get pre-built transaction from builder instance. + * const builder = createMultisig({ + * // ... args + * }); + * const transaction = await builder.transaction(); + * @example + * // Run chained async method to return the + * // transaction all in one go. + * const transaction = await createMultisig({ + * // ... args + * }).transaction(); + * */ async transaction(feePayer?: Signer): Promise { return this.buildPromise.then(async () => { @@ -66,56 +93,127 @@ export abstract class BaseBuilder< /** * Builds a transaction with the corresponding instruction(s), and sends it. - * @args feePayer - Optional signer to pay the transaction fee. + * + * **NOTE: Not wallet-adapter compatible.** + * + * @args `settings` - Optional pre/post instructions, fee payer keypair, and send options. * @returns `TransactionSignature` */ - async send( - feePayer?: Signer, - options?: SendOptions - ): Promise { + async send(settings?: { + preInstructions?: TransactionInstruction[]; + postInstructions?: TransactionInstruction[]; + feePayer?: Signer; + options?: SendOptions; + }): Promise { return this.buildPromise.then(async () => { + const instructions = [...this.instructions]; + if (settings?.preInstructions) { + instructions.unshift(...settings.preInstructions); + } + if (settings?.postInstructions) { + instructions.push(...settings.postInstructions); + } const message = new TransactionMessage({ - payerKey: feePayer?.publicKey ?? this.creator!, + payerKey: settings?.feePayer?.publicKey ?? this.creator, recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, - instructions: [...this.instructions], + instructions: [...instructions], }).compileToV0Message(); const tx = new VersionedTransaction(message); - if (feePayer) { - tx.sign([feePayer]); + if (settings?.feePayer) { + tx.sign([settings.feePayer]); } - const signature = await this.connection.sendTransaction(tx, options); + const signature = await this.connection.sendTransaction( + tx, + settings?.options + ); return signature; }); } /** * Builds a transaction with the corresponding instruction(s), sends it, and confirms the transaction. - * @args feePayer - Optional signer to pay the transaction fee. + * + * **NOTE: Not wallet-adapter compatible.** + * + * @args `settings` - Optional pre/post instructions, fee payer keypair, and send options. * @returns `TransactionSignature` */ - async sendAndConfirm( - feePayer?: Signer, - options?: SendOptions - ): Promise { + async sendAndConfirm(settings?: { + preInstructions?: TransactionInstruction[]; + postInstructions?: TransactionInstruction[]; + feePayer?: Signer; + options?: SendOptions; + }): Promise { return this.buildPromise.then(async () => { + const instructions = [...this.instructions]; + if (settings?.preInstructions) { + instructions.unshift(...settings.preInstructions); + } + if (settings?.postInstructions) { + instructions.push(...settings.postInstructions); + } const message = new TransactionMessage({ - payerKey: feePayer?.publicKey ?? this.creator!, + payerKey: settings?.feePayer?.publicKey ?? this.creator, recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, - instructions: [...this.instructions], + instructions: [...instructions], }).compileToV0Message(); const tx = new VersionedTransaction(message); - if (feePayer) { - tx.sign([feePayer]); + if (settings?.feePayer) { + tx.sign([settings.feePayer]); } - const signature = await this.connection.sendTransaction(tx, options); + const signature = await this.connection.sendTransaction( + tx, + settings?.options + ); await this.connection.getSignatureStatuses([signature]); return signature; }); } - then( + /** + * We build a message with the corresponding instruction(s), you give us a callback + * for post-processing, sending, and confirming. + * + * @args `callback` - Async function with `TransactionMessage` as argument, and `TransactionSignature` as return value. + * @returns `TransactionSignature` + * + * @example + * const txBuilder = createVaultTransaction({ + * connection, + * creator: creator, + * message: message + * multisig: multisig, + * vaultIndex: 0, + * }); + * + * await txBuilder + * .withProposal() + * .withApproval() + * .withExecute(); + * + * const signature = await txBuilder.customSend( + * // Callback with transaction message, and your function. + * async (msg) => await customSender(msg, connection) + * ); + */ + async customSend( + callback: (args: TransactionMessage) => Promise + ): Promise { + return this.buildPromise.then(async () => { + const message = new TransactionMessage({ + payerKey: this.creator, + recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, + instructions: [...this.instructions], + }); + + const signature = await callback(message); + return signature; + }); + } + + protected then( onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | null ): Promise { @@ -123,6 +221,86 @@ export abstract class BaseBuilder< } } +export interface TransactionBuilderArgs extends BaseBuilderArgs { + multisig: PublicKey; +} + +export interface TransactionBuildResult extends BuildResult { + index: number; +} + +export abstract class BaseTransactionBuilder< + T extends TransactionBuildResult, + U extends TransactionBuilderArgs +> extends BaseBuilder { + public index: number = 1; + + constructor(args: U) { + super(args); + } + + getIndex(): number { + return this.index; + } + + /** + * Fetches the `PublicKey` of the corresponding account for the transaction being built. + * + * @returns `PublicKey` + */ + getTransactionKey(): PublicKey { + const index = this.index; + const [transactionPda] = getTransactionPda({ + multisigPda: this.args.multisig, + index: BigInt(index ?? 1), + }); + + return transactionPda; + } + + /** + * Fetches the `PublicKey` of the corresponding proposal account for the transaction being built. + * + * @returns `PublicKey` + */ + getProposalKey(): PublicKey { + const index = this.index; + const [proposalPda] = getProposalPda({ + multisigPda: this.args.multisig, + transactionIndex: BigInt(index ?? 1), + }); + + return proposalPda; + } + + /** + * Fetches the `PublicKey` of the corresponding proposal account for the transaction being built. + * + * @returns `PublicKey` + */ + async getTransactionAccount(key: PublicKey) { + return this.buildPromise.then(async () => { + const txAccount = await VaultTransaction.fromAccountAddress( + this.connection, + key + ); + + return txAccount; + }); + } + + async getProposalAccount(key: PublicKey) { + return this.buildPromise.then(async () => { + const propAccount = await Proposal.fromAccountAddress( + this.connection, + key + ); + + return propAccount; + }); + } +} + export interface CreateProposalActionArgs { /** The public key of the multisig config account */ multisig: PublicKey; @@ -154,9 +332,9 @@ export interface ProposalResult { instruction: TransactionInstruction; } -export async function createProposalCore( +export function createProposalCore( args: CreateProposalActionArgs -): Promise { +): ProposalResult { const { multisig, creator, @@ -180,9 +358,7 @@ export async function createProposalCore( }; } -export async function createApprovalCore( - args: VoteActionArgs -): Promise { +export function createApprovalCore(args: VoteActionArgs): ProposalResult { const { multisig, member, transactionIndex, programId = PROGRAM_ID } = args; const ix = instructions.proposalApprove({ @@ -197,9 +373,7 @@ export async function createApprovalCore( }; } -export async function createRejectionCore( - args: VoteActionArgs -): Promise { +export function createRejectionCore(args: VoteActionArgs): ProposalResult { const { multisig, member, transactionIndex, programId = PROGRAM_ID } = args; const ix = instructions.proposalReject({ diff --git a/sdk/multisig/src/actions/createBatch.ts b/sdk/multisig/src/actions/createBatch.ts index 2159aac2..3cb8ee32 100644 --- a/sdk/multisig/src/actions/createBatch.ts +++ b/sdk/multisig/src/actions/createBatch.ts @@ -17,15 +17,14 @@ import { createApprovalCore, createRejectionCore, createProposalCore, + BaseBuilderArgs, + BuildResult, } from "./common"; +import { BatchMethods } from "./actionTypes"; -interface CreateBatchActionArgs { - /** The connection to an SVM network cluster */ - connection: Connection; +interface CreateBatchActionArgs extends BaseBuilderArgs { /** The public key of the multisig config account */ multisig: PublicKey; - /** The public key of the creator */ - creator: PublicKey; /** Index of the vault to target. Defaults to 0 */ vaultIndex?: number; /** The public key of the fee payer, defaults to the creator */ @@ -36,9 +35,7 @@ interface CreateBatchActionArgs { programId?: PublicKey; } -interface CreateBatchResult { - /** `vaultTransactionCreate` instruction */ - instructions: TransactionInstruction[]; +interface CreateBatchResult extends BuildResult { /** Transaction index of the resulting VaultTransaction */ index: number; } @@ -212,7 +209,9 @@ class BatchBuilder extends BaseBuilder< return transactions; } - async getBatchAccount(key: PublicKey) { + async getBatchAccount( + key: PublicKey + ): Promise>> { return this.buildPromise.then(async () => { const batchAccount = await Batch.fromAccountAddress(this.connection, key); @@ -225,7 +224,10 @@ class BatchBuilder extends BaseBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async addTransaction(message: TransactionMessage, member?: PublicKey) { + async addTransaction( + message: TransactionMessage, + member?: PublicKey + ): Promise>> { this.innerIndex++; const { instruction } = await addBatchTransactionCore({ multisig: this.args.multisig, @@ -238,10 +240,9 @@ class BatchBuilder extends BaseBuilder< programId: this.args.programId, }); - return { - innerIndex: this.innerIndex, - instructions: [...this.instructions, instruction], - }; + this.instructions.push(instruction); + + return this; } /** @@ -249,8 +250,10 @@ class BatchBuilder extends BaseBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async withProposal(isDraft?: boolean) { - const { instruction } = await createProposalCore({ + withProposal( + isDraft?: boolean + ): Pick> { + const { instruction } = createProposalCore({ multisig: this.args.multisig, creator: this.creator, transactionIndex: this.index, @@ -258,10 +261,9 @@ class BatchBuilder extends BaseBuilder< isDraft, }); - return { - index: this.index, - instruction: [...this.instructions, instruction], - }; + this.instructions.push(instruction); + + return this; } /** @@ -269,18 +271,19 @@ class BatchBuilder extends BaseBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async withApproval(member?: PublicKey) { - const { instruction } = await createApprovalCore({ + withApproval( + member?: PublicKey + ): Pick> { + const { instruction } = createApprovalCore({ multisig: this.args.multisig, member: member ?? this.creator, transactionIndex: this.index, programId: this.args.programId, }); - return { - index: this.index, - instructions: [...this.instructions, instruction], - }; + this.instructions.push(instruction); + + return this; } /** @@ -288,18 +291,19 @@ class BatchBuilder extends BaseBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async withRejection(member?: PublicKey) { - const { instruction } = await createRejectionCore({ + withRejection( + member?: PublicKey + ): Pick> { + const { instruction } = createRejectionCore({ multisig: this.args.multisig, member: member ?? this.creator, transactionIndex: this.index, programId: this.args.programId, }); - return { - index: this.index, - instructions: [...this.instructions, instruction], - }; + this.instructions.push(instruction); + + return this; } /** @@ -307,7 +311,9 @@ class BatchBuilder extends BaseBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async execute(member?: PublicKey) { + async withExecute( + member?: PublicKey + ): Promise>> { const { instruction } = await executeBatchTransactionCore({ connection: this.connection, multisig: this.args.multisig, @@ -316,10 +322,9 @@ class BatchBuilder extends BaseBuilder< programId: this.args.programId, }); - return { - index: this.index, - instructions: [...this.instructions, instruction], - }; + this.instructions.push(instruction); + + return this; } } diff --git a/sdk/multisig/src/actions/createConfigTransaction.ts b/sdk/multisig/src/actions/createConfigTransaction.ts index ff2995d1..049b24ea 100644 --- a/sdk/multisig/src/actions/createConfigTransaction.ts +++ b/sdk/multisig/src/actions/createConfigTransaction.ts @@ -1,12 +1,13 @@ import { Connection, PublicKey, TransactionInstruction } from "@solana/web3.js"; -import { instructions, accounts, getTransactionPda } from ".."; +import { instructions, accounts } from ".."; import { ConfigAction, ConfigTransaction, PROGRAM_ID } from "../generated"; import { - BaseBuilder, + BaseTransactionBuilder, createApprovalCore, createProposalCore, createRejectionCore, } from "./common"; +import { Methods } from "./actionTypes"; interface CreateConfigTransactionActionArgs { /** The connection to an SVM network cluster */ @@ -91,7 +92,7 @@ export function createConfigTransaction( return new ConfigTransactionBuilder(args); } -class ConfigTransactionBuilder extends BaseBuilder< +class ConfigTransactionBuilder extends BaseTransactionBuilder< CreateConfigTransactionResult, CreateConfigTransactionActionArgs > { @@ -102,7 +103,7 @@ class ConfigTransactionBuilder extends BaseBuilder< super(args); } - async build() { + protected async build() { const { multisig, actions, rentPayer, memo, programId } = this.args; const result = await createConfigTransactionCore({ connection: this.connection, @@ -119,43 +120,15 @@ class ConfigTransactionBuilder extends BaseBuilder< return this; } - getInstructions(): TransactionInstruction[] { - return this.instructions; - } - - getIndex(): number | null { - return this.index; - } - - getTransactionKey(): PublicKey { - const index = this.index; - const [transactionPda] = getTransactionPda({ - multisigPda: this.args.multisig, - index: BigInt(index ?? 1), - programId: this.args.programId, - }); - - return transactionPda; - } - - async getTransactionAccount(key: PublicKey) { - return this.buildPromise.then(async () => { - const txAccount = await ConfigTransaction.fromAccountAddress( - this.connection, - key - ); - - return txAccount; - }); - } - /** * Creates a transaction containing the ConfigTransaction creation instruction. * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async withProposal(isDraft?: boolean) { - const { instruction } = await createProposalCore({ + withProposal( + isDraft?: boolean + ): Pick> { + const { instruction } = createProposalCore({ multisig: this.args.multisig, creator: this.creator, transactionIndex: this.index, @@ -163,10 +136,9 @@ class ConfigTransactionBuilder extends BaseBuilder< programId: this.args.programId, }); - return { - index: this.index, - instruction: [...this.instructions, instruction], - }; + this.instructions.push(instruction); + + return this; } /** @@ -174,18 +146,19 @@ class ConfigTransactionBuilder extends BaseBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async withApproval(member?: PublicKey) { - const { instruction } = await createApprovalCore({ + withApproval( + member?: PublicKey + ): Pick> { + const { instruction } = createApprovalCore({ multisig: this.args.multisig, member: member ?? this.creator, transactionIndex: this.index, programId: this.args.programId, }); - return { - index: this.index, - instructions: [...this.instructions, instruction], - }; + this.instructions.push(instruction); + + return this; } /** @@ -193,18 +166,19 @@ class ConfigTransactionBuilder extends BaseBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async withRejection(member?: PublicKey) { - const { instruction } = await createRejectionCore({ + withRejection( + member?: PublicKey + ): Pick> { + const { instruction } = createRejectionCore({ multisig: this.args.multisig, member: member ?? this.creator, transactionIndex: this.index, programId: this.args.programId, }); - return { - index: this.index, - instructions: [...this.instructions, instruction], - }; + this.instructions.push(instruction); + + return this; } /** @@ -212,7 +186,9 @@ class ConfigTransactionBuilder extends BaseBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async execute(member?: PublicKey) { + async withExecute( + member?: PublicKey + ): Promise>> { const { instruction } = await executeConfigTransactionCore({ multisig: this.args.multisig, member: member ?? this.creator, @@ -220,10 +196,9 @@ class ConfigTransactionBuilder extends BaseBuilder< programId: this.args.programId, }); - return { - index: this.index, - instructions: [...this.instructions, instruction], - }; + this.instructions.push(instruction); + + return this; } } diff --git a/sdk/multisig/src/actions/createMultisig.ts b/sdk/multisig/src/actions/createMultisig.ts index dbed1efd..09f9dc78 100644 --- a/sdk/multisig/src/actions/createMultisig.ts +++ b/sdk/multisig/src/actions/createMultisig.ts @@ -1,19 +1,9 @@ -import { - Connection, - Keypair, - PublicKey, - TransactionInstruction, -} from "@solana/web3.js"; +import { Keypair, PublicKey, TransactionInstruction } from "@solana/web3.js"; import { Member, Multisig, PROGRAM_ID, ProgramConfig } from "../generated"; import { getMultisigPda, getProgramConfigPda, instructions } from ".."; -import { BaseBuilder } from "./common"; -import { SquadPermissions, createMembers } from "./members"; - -interface CreateMultisigActionArgs { - /** The connection to an SVM network cluster */ - connection: Connection; - /** The public key of the creator */ - creator: PublicKey; +import { BaseBuilder, BaseBuilderArgs, BuildResult } from "./common"; + +interface CreateMultisigActionArgs extends BaseBuilderArgs { /** The number of approvals required to approve transactions */ threshold: number; /** The list of members in the multisig, with their associated permissions */ @@ -28,9 +18,7 @@ interface CreateMultisigActionArgs { programId?: PublicKey; } -interface CreateMultisigResult { - /** `multisigCreateV2` instruction */ - instructions: TransactionInstruction[]; +interface CreateMultisigResult extends BuildResult { /** Keypair seed that is required to sign the transaction */ createKey: Keypair; } @@ -44,14 +32,17 @@ interface CreateMultisigResult { * * @example * // Basic usage (no chaining): - * const result = await createMultisig({ + * const builder = await createMultisig({ * connection, * creator: creatorPublicKey, * threshold: 2, * members: membersList, * }); - * console.log(result.instruction); - * console.log(result.createKey); + * + * const instructions = result.instructions; + * const createKey = result.createKey; + * + * const signature = await builder.sendAndConfirm(); * * @example * // Using the transaction() method: @@ -60,7 +51,7 @@ interface CreateMultisigResult { * }).transaction(); * * @example - * // Using the rpc() method: + * // Using the send() method: * const signature = await createMultisig({ * // ... args * }).send(); @@ -86,7 +77,7 @@ class MultisigBuilder extends BaseBuilder< super(args); } - async build() { + protected async build(): Promise { const { threshold, members, @@ -108,11 +99,7 @@ class MultisigBuilder extends BaseBuilder< this.instructions = [...result.instructions]; this.createKey = result.createKey; - return this; - } - - getInstructions(): TransactionInstruction[] { - return this.instructions; + return this as MultisigBuilder; } getCreateKey(): Keypair { diff --git a/sdk/multisig/src/actions/createVaultTransaction.ts b/sdk/multisig/src/actions/createVaultTransaction.ts index bf6cc519..2bf03b9f 100644 --- a/sdk/multisig/src/actions/createVaultTransaction.ts +++ b/sdk/multisig/src/actions/createVaultTransaction.ts @@ -5,14 +5,15 @@ import { TransactionMessage, AddressLookupTableAccount, } from "@solana/web3.js"; -import { instructions, accounts, getTransactionPda } from ".."; +import { instructions, accounts } from ".."; import { PROGRAM_ID, VaultTransaction } from "../generated"; import { - BaseBuilder, createApprovalCore, createRejectionCore, createProposalCore, + BaseTransactionBuilder, } from "./common"; +import { Methods } from "./actionTypes"; interface CreateVaultTransactionActionArgs { /** The connection to an SVM network cluster */ @@ -107,18 +108,19 @@ export function createVaultTransaction( return new VaultTransactionBuilder(args); } -class VaultTransactionBuilder extends BaseBuilder< +class VaultTransactionBuilder extends BaseTransactionBuilder< CreateVaultTransactionResult, CreateVaultTransactionActionArgs > { public instructions: TransactionInstruction[] = []; + public addressLookupTableAccounts: AddressLookupTableAccount[] = []; public index: number = 1; constructor(args: CreateVaultTransactionActionArgs) { super(args); } - async build() { + protected async build() { const { multisig, message, @@ -142,36 +144,7 @@ class VaultTransactionBuilder extends BaseBuilder< this.instructions = [...result.instructions]; this.index = result.index; - return this; - } - - getInstructions(): TransactionInstruction[] { - return this.instructions; - } - - getIndex(): number { - return this.index; - } - - getTransactionKey(): PublicKey | null { - const index = this.index; - const [transactionPda] = getTransactionPda({ - multisigPda: this.args.multisig, - index: BigInt(index ?? 1), - }); - - return transactionPda; - } - - async getTransactionAccount(key: PublicKey) { - return this.buildPromise.then(async () => { - const txAccount = await VaultTransaction.fromAccountAddress( - this.connection, - key - ); - - return txAccount; - }); + return this as VaultTransactionBuilder; } /** @@ -179,8 +152,10 @@ class VaultTransactionBuilder extends BaseBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async withProposal(isDraft?: boolean) { - const { instruction } = await createProposalCore({ + withProposal( + isDraft?: boolean + ): Pick> { + const { instruction } = createProposalCore({ multisig: this.args.multisig, creator: this.creator, transactionIndex: this.index, @@ -188,10 +163,9 @@ class VaultTransactionBuilder extends BaseBuilder< isDraft, }); - return { - index: this.index, - instruction: [...this.instructions, instruction], - }; + this.instructions.push(instruction); + + return this; } /** @@ -199,18 +173,19 @@ class VaultTransactionBuilder extends BaseBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async withApproval(member?: PublicKey) { - const { instruction } = await createApprovalCore({ + withApproval( + member?: PublicKey + ): Pick> { + const { instruction } = createApprovalCore({ multisig: this.args.multisig, member: member ?? this.creator, transactionIndex: this.index, programId: this.args.programId, }); - return { - index: this.index, - instructions: [...this.instructions, instruction], - }; + this.instructions.push(instruction); + + return this; } /** @@ -218,18 +193,19 @@ class VaultTransactionBuilder extends BaseBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async withRejection(member?: PublicKey) { - const { instruction } = await createRejectionCore({ + withRejection( + member?: PublicKey + ): Pick> { + const { instruction } = createRejectionCore({ multisig: this.args.multisig, member: member ?? this.creator, transactionIndex: this.index, programId: this.args.programId, }); - return { - index: this.index, - instructions: [...this.instructions, instruction], - }; + this.instructions.push(instruction); + + return this; } /** @@ -237,19 +213,22 @@ class VaultTransactionBuilder extends BaseBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async execute(member?: PublicKey) { - const { instruction } = await executeVaultTransactionCore({ - connection: this.connection, - multisig: this.args.multisig, - member: member ?? this.creator, - index: this.index, - programId: this.args.programId, - }); + async withExecute( + member?: PublicKey + ): Promise>> { + const { instruction, lookupTableAccounts } = + await executeVaultTransactionCore({ + connection: this.connection, + multisig: this.args.multisig, + member: member ?? this.creator, + index: this.index, + programId: this.args.programId, + }); + + this.instructions.push(instruction); + this.addressLookupTableAccounts.push(...lookupTableAccounts); - return { - index: this.index, - instructions: [...this.instructions, instruction], - }; + return this; } } @@ -307,7 +286,7 @@ async function executeVaultTransactionCore( args: ExecuteVaultTransactionActionArgs ): Promise { const { connection, multisig, index, member, programId = PROGRAM_ID } = args; - const ix = instructions.vaultTransactionExecute({ + const ix = await instructions.vaultTransactionExecute({ connection, multisigPda: multisig, member: member, @@ -315,5 +294,60 @@ async function executeVaultTransactionCore( programId: programId, }); - return { ...ix }; + return { + ...ix, + }; +} + +/* +async function Example() { + const connection = new Connection("https://api.mainnet-beta.solana.com"); + const feePayer = Keypair.generate(); + const txBuilder = createVaultTransaction({ + connection, + creator: PublicKey.default, + message: new TransactionMessage({ + payerKey: PublicKey.default, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 200_000, + }), + ], + }), + multisig: PublicKey.default, + vaultIndex: 0, + ephemeralSigners: 0, + memo: "Transfer 2 SOL to a test account", + programId: PROGRAM_ID, + }); + + txBuilder.withProposal().withApproval() + const proposalKey = txBuilder.getProposalKey(); + await txBuilder.withProposal(); + + const signature = await txBuilder.customSend( + async (msg) => await customSender(msg, connection) + ); + /* + .sendAndConfirm({ + preInstructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 200_000, + }), + ], + options: { skipPreflight: true }, + }); } + +const customSender = async ( + msg: TransactionMessage, + connection: Connection +) => { + const transaction = new VersionedTransaction(msg.compileToV0Message()); + const signature = await connection.sendTransaction(transaction); + await connection.getSignatureStatuses([signature]); + + return signature; +}; +*/ diff --git a/tests/suites/examples/actions.ts b/tests/suites/examples/actions.ts new file mode 100644 index 00000000..a7b6711f --- /dev/null +++ b/tests/suites/examples/actions.ts @@ -0,0 +1,113 @@ +import * as multisig from "@sqds/multisig"; +import { PublicKey, TransactionMessage, Keypair } from "@solana/web3.js"; +import { + createLocalhostConnection, + createTestTransferInstruction, + generateFundedKeypair, + generateMultisigMembers, + getTestProgramId, + TestMembers, +} from "../../utils"; +import { + createMultisig, + createVaultTransaction, +} from "@sqds/multisig/src/actions"; +import assert from "assert"; + +const programId = getTestProgramId(); + +describe("Examples / End2End Actions", () => { + const connection = createLocalhostConnection(); + + let multisigPda: PublicKey = PublicKey.default; + let members: TestMembers; + let outsider: Keypair; + before(async () => { + outsider = await generateFundedKeypair(connection); + members = await generateMultisigMembers(connection); + }); + + it("should create a multisig", async () => { + const builder = createMultisig({ + connection, + creator: members.almighty.publicKey, + members: members as any, + threshold: 2, + programId, + }); + + multisigPda = builder.getMultisigKey(); + await builder.sendAndConfirm(); + }); + + it("should create a multisig", async () => { + const builder = createMultisig({ + connection, + creator: members.almighty.publicKey, + members: members as any, + threshold: 2, + programId, + }); + + multisigPda = builder.getMultisigKey(); + const signature = await builder.sendAndConfirm(); + const account = await builder.getMultisigAccount(multisigPda); + + assert.strictEqual(account.threshold, 2); + }); + + it("should create & send a vault transaction", async () => { + const [vaultPda] = multisig.getVaultPda({ + multisigPda: multisigPda, + index: 0, + }); + const message = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ + createTestTransferInstruction(vaultPda, outsider.publicKey), + ], + }); + const txBuilder = createVaultTransaction({ + connection, + multisig: multisigPda, + creator: members.almighty.publicKey, + message, + programId, + }); + txBuilder.withProposal(); + txBuilder.withApproval(); + + const signature = await txBuilder.sendAndConfirm(); + + console.log(signature); + }); + + it("should create, vote & execute a vault transaction", async () => { + const [vaultPda] = multisig.getVaultPda({ + multisigPda: multisigPda, + index: 0, + }); + const message = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ + createTestTransferInstruction(vaultPda, outsider.publicKey), + ], + }); + const txBuilder = createVaultTransaction({ + connection, + multisig: multisigPda, + creator: members.almighty.publicKey, + message, + programId, + }); + txBuilder.withProposal(); + txBuilder.withApproval(); + txBuilder.execute(); + + const signature = await txBuilder.sendAndConfirm(); + + console.log(signature); + }); +});