diff --git a/sdk/multisig/src/actions/common.ts b/sdk/multisig/src/actions/common.ts index 8ab6f269..608d7039 100644 --- a/sdk/multisig/src/actions/common.ts +++ b/sdk/multisig/src/actions/common.ts @@ -1,5 +1,6 @@ import { Connection, + Keypair, PublicKey, SendOptions, Signer, @@ -31,17 +32,31 @@ export abstract class BaseBuilder< T extends BuildResult, U extends BaseBuilderArgs = BaseBuilderArgs > { + public createKey: Keypair; protected connection: Connection; protected instructions: TransactionInstruction[] = []; protected creator: PublicKey = PublicKey.default; - protected buildPromise: Promise; + protected buildPromise: Promise; protected args: Omit; + private built: boolean = false; constructor(args: U) { this.connection = args.connection; this.creator = args.creator; this.args = this.extractAdditionalArgs(args); - this.buildPromise = this.build(); + this.createKey = Keypair.generate(); + this.buildPromise = this.initializeBuild(); + } + + private async initializeBuild(): Promise { + await this.build(); + this.built = true; + } + + protected async ensureBuilt(): Promise { + if (!this.built) { + await this.buildPromise; + } } private extractAdditionalArgs(args: U): Omit { @@ -49,7 +64,7 @@ export abstract class BaseBuilder< return additionalArgs; } - protected abstract build(): Promise; + protected abstract build(): Promise; getInstructions(): TransactionInstruction[] { return this.instructions; @@ -76,19 +91,18 @@ export abstract class BaseBuilder< * */ async transaction(feePayer?: Signer): Promise { - return this.buildPromise.then(async () => { - const message = new TransactionMessage({ - payerKey: feePayer?.publicKey ?? this.creator!, - recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, - instructions: [...this.instructions!], - }).compileToV0Message(); - - const tx = new VersionedTransaction(message); - if (feePayer) { - tx.sign([feePayer]); - } - return tx; - }); + await this.ensureBuilt(); + const message = new TransactionMessage({ + payerKey: feePayer?.publicKey ?? this.creator, + recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, + instructions: [...this.instructions], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + if (feePayer) { + tx.sign([feePayer]); + } + return tx; } /** @@ -103,32 +117,35 @@ export abstract class BaseBuilder< preInstructions?: TransactionInstruction[]; postInstructions?: TransactionInstruction[]; feePayer?: Signer; + signers?: 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: settings?.feePayer?.publicKey ?? this.creator, - recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, - instructions: [...instructions], - }).compileToV0Message(); - - const tx = new VersionedTransaction(message); - if (settings?.feePayer) { - tx.sign([settings.feePayer]); - } - const signature = await this.connection.sendTransaction( - tx, - settings?.options - ); - return signature; - }); + await this.ensureBuilt(); + const instructions = [...this.instructions]; + if (settings?.preInstructions) { + instructions.unshift(...settings.preInstructions); + } + if (settings?.postInstructions) { + instructions.push(...settings.postInstructions); + } + const message = new TransactionMessage({ + payerKey: settings?.feePayer?.publicKey ?? this.creator, + recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, + instructions: [...instructions], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + if (settings?.feePayer) { + tx.sign([settings.feePayer]); + } + if (settings?.signers) { + tx.sign([...settings.signers]); + } + const signature = await this.connection.sendTransaction( + tx, + settings?.options + ); + return signature; } /** @@ -143,33 +160,50 @@ export abstract class BaseBuilder< preInstructions?: TransactionInstruction[]; postInstructions?: TransactionInstruction[]; feePayer?: Signer; + signers?: 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: settings?.feePayer?.publicKey ?? this.creator, - recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, - instructions: [...instructions], - }).compileToV0Message(); - - const tx = new VersionedTransaction(message); - if (settings?.feePayer) { - tx.sign([settings.feePayer]); + await this.ensureBuilt(); + const instructions = [...this.instructions]; + if (settings?.preInstructions) { + instructions.unshift(...settings.preInstructions); + } + if (settings?.postInstructions) { + instructions.push(...settings.postInstructions); + } + const message = new TransactionMessage({ + payerKey: settings?.feePayer?.publicKey ?? this.creator, + recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, + instructions: [...instructions], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + if (settings?.feePayer) { + tx.sign([settings.feePayer]); + } + if (settings?.signers) { + tx.sign([...settings.signers]); + } + const signature = await this.connection.sendTransaction( + tx, + settings?.options + ); + + let commitment = settings?.options?.preflightCommitment; + + let sent = false; + while (sent === false) { + const status = await this.connection.getSignatureStatuses([signature]); + if ( + commitment + ? status.value[0]?.confirmationStatus === commitment + : status.value[0]?.confirmationStatus === "finalized" + ) { + sent = true; } - const signature = await this.connection.sendTransaction( - tx, - settings?.options - ); - await this.connection.getSignatureStatuses([signature]); - return signature; - }); + } + + return signature; } /** @@ -201,24 +235,25 @@ export abstract class BaseBuilder< 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; + await this.ensureBuilt(); + 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 { return this.buildPromise.then(onfulfilled, onrejected); } + */ } export interface TransactionBuilderArgs extends BaseBuilderArgs { diff --git a/sdk/multisig/src/actions/createBatch.ts b/sdk/multisig/src/actions/createBatch.ts index 3cb8ee32..a1dda7ca 100644 --- a/sdk/multisig/src/actions/createBatch.ts +++ b/sdk/multisig/src/actions/createBatch.ts @@ -135,13 +135,13 @@ class BatchBuilder extends BaseBuilder< > { public instructions: TransactionInstruction[] = []; public index: number = 1; - public innerIndex: number = 0; + public innerIndex: number = 1; constructor(args: CreateBatchActionArgs) { super(args); } - async build() { + protected async build() { const { multisig, vaultIndex = 0, @@ -161,18 +161,10 @@ class BatchBuilder extends BaseBuilder< this.instructions = [...result.instructions]; this.index = result.index; - return this; - } - - getInstructions(): TransactionInstruction[] { - return this.instructions; - } - - getIndex(): number { - return this.index; } - getBatchKey(): PublicKey | null { + async getBatchKey(): Promise { + this.ensureBuilt(); const index = this.index; const [batchPda] = getTransactionPda({ multisigPda: this.args.multisig, @@ -182,21 +174,23 @@ class BatchBuilder extends BaseBuilder< return batchPda; } - getBatchTransactionKey(innerIndex: number): PublicKey | null { + async getBatchTransactionKey(innerIndex?: number): Promise { + this.ensureBuilt(); const index = this.index; const [batchPda] = getBatchTransactionPda({ multisigPda: this.args.multisig, batchIndex: BigInt(index ?? 1), - transactionIndex: this.innerIndex ?? 1, + transactionIndex: innerIndex ?? this.innerIndex, }); return batchPda; } - getAllBatchTransactionKeys(localIndex: number): PublicKey[] | null { + async getAllBatchTransactionKeys(): Promise { + this.ensureBuilt(); const index = this.index; const transactions = []; - for (let i = 1; i <= localIndex; i++) { + for (let i = 1; i <= this.innerIndex; i++) { const [batchPda] = getBatchTransactionPda({ multisigPda: this.args.multisig, batchIndex: BigInt(index ?? 1), @@ -212,11 +206,10 @@ class BatchBuilder extends BaseBuilder< async getBatchAccount( key: PublicKey ): Promise>> { - return this.buildPromise.then(async () => { - const batchAccount = await Batch.fromAccountAddress(this.connection, key); + this.ensureBuilt(); + const batchAccount = await Batch.fromAccountAddress(this.connection, key); - return batchAccount; - }); + return batchAccount; } /** @@ -228,7 +221,7 @@ class BatchBuilder extends BaseBuilder< message: TransactionMessage, member?: PublicKey ): Promise>> { - this.innerIndex++; + this.ensureBuilt(); const { instruction } = await addBatchTransactionCore({ multisig: this.args.multisig, member: member ?? this.creator, @@ -242,6 +235,8 @@ class BatchBuilder extends BaseBuilder< this.instructions.push(instruction); + this.innerIndex++; + return this; } @@ -250,9 +245,10 @@ class BatchBuilder extends BaseBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - withProposal( + async withProposal( isDraft?: boolean - ): Pick> { + ): Promise>> { + this.ensureBuilt(); const { instruction } = createProposalCore({ multisig: this.args.multisig, creator: this.creator, @@ -314,6 +310,7 @@ class BatchBuilder extends BaseBuilder< async withExecute( member?: PublicKey ): Promise>> { + await this.ensureBuilt(); const { instruction } = await executeBatchTransactionCore({ connection: this.connection, multisig: this.args.multisig, diff --git a/sdk/multisig/src/actions/createConfigTransaction.ts b/sdk/multisig/src/actions/createConfigTransaction.ts index 049b24ea..864e94e1 100644 --- a/sdk/multisig/src/actions/createConfigTransaction.ts +++ b/sdk/multisig/src/actions/createConfigTransaction.ts @@ -117,7 +117,6 @@ class ConfigTransactionBuilder extends BaseTransactionBuilder< this.instructions = [...result.instructions]; this.index = result.index; - return this; } /** @@ -125,9 +124,10 @@ class ConfigTransactionBuilder extends BaseTransactionBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - withProposal( + async withProposal( isDraft?: boolean - ): Pick> { + ): Promise>> { + await this.ensureBuilt(); const { instruction } = createProposalCore({ multisig: this.args.multisig, creator: this.creator, diff --git a/sdk/multisig/src/actions/createMultisig.ts b/sdk/multisig/src/actions/createMultisig.ts index 09f9dc78..eafc142e 100644 --- a/sdk/multisig/src/actions/createMultisig.ts +++ b/sdk/multisig/src/actions/createMultisig.ts @@ -1,7 +1,15 @@ -import { Keypair, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { + Connection, + Keypair, + PublicKey, + SendOptions, + Signer, + TransactionInstruction, +} from "@solana/web3.js"; import { Member, Multisig, PROGRAM_ID, ProgramConfig } from "../generated"; import { getMultisigPda, getProgramConfigPda, instructions } from ".."; import { BaseBuilder, BaseBuilderArgs, BuildResult } from "./common"; +import { SquadPermissions, createMembers } from "./members"; interface CreateMultisigActionArgs extends BaseBuilderArgs { /** The number of approvals required to approve transactions */ @@ -19,8 +27,7 @@ interface CreateMultisigActionArgs extends BaseBuilderArgs { } interface CreateMultisigResult extends BuildResult { - /** Keypair seed that is required to sign the transaction */ - createKey: Keypair; + multisigKey: PublicKey; } /** @@ -71,13 +78,13 @@ class MultisigBuilder extends BaseBuilder< CreateMultisigActionArgs > { public instructions: TransactionInstruction[] = []; - public createKey: Keypair = Keypair.generate(); + public multisigKey: PublicKey = PublicKey.default; constructor(args: CreateMultisigActionArgs) { super(args); } - protected async build(): Promise { + protected async build(): Promise { const { threshold, members, @@ -86,48 +93,67 @@ class MultisigBuilder extends BaseBuilder< rentCollector, programId = PROGRAM_ID, } = this.args; - const result = await createMultisigCore({ - connection: this.connection, - creator: this.creator, - threshold, - members, - timeLock, - configAuthority, - rentCollector, - programId, - }); + const result = await createMultisigCore( + { + connection: this.connection, + creator: this.creator, + threshold, + members, + timeLock, + configAuthority, + rentCollector, + programId, + }, + this.createKey + ); this.instructions = [...result.instructions]; - this.createKey = result.createKey; - return this as MultisigBuilder; + this.multisigKey = result.multisigKey; } - getCreateKey(): Keypair { + async getCreateKey(): Promise { + await this.ensureBuilt(); return this.createKey; } - getMultisigKey(): PublicKey { - const [multisigPda] = getMultisigPda({ - createKey: this.createKey.publicKey, - }); - - return multisigPda; + async getMultisigKey(): Promise { + await this.ensureBuilt(); + return this.multisigKey; } - getMultisigAccount(key: PublicKey) { - return this.buildPromise.then(async () => { - const multisigAccount = await Multisig.fromAccountAddress( - this.connection, - key - ); + async getMultisigAccount(key: PublicKey) { + await this.ensureBuilt(); + const multisigAccount = await Multisig.fromAccountAddress( + this.connection, + key + ); - return multisigAccount; - }); + return multisigAccount; + } + + async sendAndConfirm(settings?: { + preInstructions?: TransactionInstruction[] | undefined; + postInstructions?: TransactionInstruction[] | undefined; + feePayer?: Signer | undefined; + signers?: Signer[] | undefined; + options?: SendOptions | undefined; + }): Promise { + await this.ensureBuilt(); + if (settings?.signers) { + settings.signers.push(this.createKey); + } else { + settings = { + signers: [this.createKey], + ...settings, + }; + } + return await super.sendAndConfirm(settings); } } export async function createMultisigCore( - args: CreateMultisigActionArgs + args: CreateMultisigActionArgs, + createKey: Keypair ): Promise { const { connection, @@ -140,7 +166,6 @@ export async function createMultisigCore( programId = PROGRAM_ID, } = args; - const createKey = Keypair.generate(); const [multisigPda] = getMultisigPda({ createKey: createKey.publicKey, programId, @@ -167,21 +192,19 @@ export async function createMultisigCore( return { instructions: [ix], - createKey: createKey, + multisigKey: multisigPda, }; } -/* async function Example() { const connection = new Connection("https://api.mainnet-beta.solana.com"); const feePayer = Keypair.generate(); - const signature = await createMultisig({ + const signature = createMultisig({ connection, members: createMembers([ { key: PublicKey.default, permissions: SquadPermissions.All }, ]), creator: PublicKey.default, threshold: 2, - }).sendAndConfirm(feePayer); + }); } -*/ diff --git a/sdk/multisig/src/actions/createVaultTransaction.ts b/sdk/multisig/src/actions/createVaultTransaction.ts index 2bf03b9f..7409be56 100644 --- a/sdk/multisig/src/actions/createVaultTransaction.ts +++ b/sdk/multisig/src/actions/createVaultTransaction.ts @@ -144,7 +144,6 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< this.instructions = [...result.instructions]; this.index = result.index; - return this as VaultTransactionBuilder; } /** @@ -152,9 +151,10 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - withProposal( + async withProposal( isDraft?: boolean - ): Pick> { + ): Promise>> { + await this.ensureBuilt(); const { instruction } = createProposalCore({ multisig: this.args.multisig, creator: this.creator, @@ -216,6 +216,7 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< async withExecute( member?: PublicKey ): Promise>> { + await this.ensureBuilt(); const { instruction, lookupTableAccounts } = await executeVaultTransactionCore({ connection: this.connection, diff --git a/sdk/multisig/src/actions/index.ts b/sdk/multisig/src/actions/index.ts index 3ee6847f..1f6e240d 100644 --- a/sdk/multisig/src/actions/index.ts +++ b/sdk/multisig/src/actions/index.ts @@ -1,6 +1,7 @@ export * from "./createMultisig"; export * from "./createVaultTransaction"; export * from "./createConfigTransaction"; +export * from "./members"; // WIP //export * from "./createTransactionMultiStep"; diff --git a/sdk/multisig/src/index.ts b/sdk/multisig/src/index.ts index 94fcfd95..dcaaefef 100644 --- a/sdk/multisig/src/index.ts +++ b/sdk/multisig/src/index.ts @@ -13,7 +13,7 @@ export * as transactions from "./transactions"; /** Instructions for the multisig program. */ export * as instructions from "./instructions/index.js"; /** Builders and chainable actions for the multisig program. */ -export * as actions from "./actions/index.js"; +export * from "./actions/index.js"; /** Additional types */ export * as types from "./types.js"; /** Utils for the multisig program. */ diff --git a/tests/index.ts b/tests/index.ts index af1b1757..fa416199 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -14,3 +14,4 @@ import "./suites/examples/batch-sol-transfer"; import "./suites/examples/create-mint"; import "./suites/examples/immediate-execution"; import "./suites/examples/spending-limits"; +import "./suites/examples/actions"; diff --git a/tests/suites/examples/actions.ts b/tests/suites/examples/actions.ts index a7b6711f..ff292d96 100644 --- a/tests/suites/examples/actions.ts +++ b/tests/suites/examples/actions.ts @@ -8,18 +8,18 @@ import { getTestProgramId, TestMembers, } from "../../utils"; -import { - createMultisig, - createVaultTransaction, -} from "@sqds/multisig/src/actions"; +import { createMultisig, createVaultTransaction } from "@sqds/multisig"; import assert from "assert"; +const { Permission, Permissions } = multisig.types; + const programId = getTestProgramId(); describe("Examples / End2End Actions", () => { const connection = createLocalhostConnection(); let multisigPda: PublicKey = PublicKey.default; + let transactionPda: PublicKey | null = null; let members: TestMembers; let outsider: Keypair; before(async () => { @@ -28,32 +28,31 @@ describe("Examples / End2End Actions", () => { }); it("should create a multisig", async () => { + console.log("Creating a multisig with 2 members"); const builder = createMultisig({ connection, creator: members.almighty.publicKey, - members: members as any, - threshold: 2, + members: [ + { + key: members.almighty.publicKey, + permissions: Permissions.all(), + }, + { + key: members.voter.publicKey, + permissions: Permissions.all(), + }, + ], + threshold: 1, programId, }); - multisigPda = builder.getMultisigKey(); - await builder.sendAndConfirm(); - }); + multisigPda = await builder.getMultisigKey(); + const createKey = await builder.getCreateKey(); - it("should create a multisig", async () => { - const builder = createMultisig({ - connection, - creator: members.almighty.publicKey, - members: members as any, - threshold: 2, - programId, + await builder.sendAndConfirm({ + signers: [members.almighty, createKey], + options: { preflightCommitment: "finalized" }, }); - - 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 () => { @@ -61,6 +60,7 @@ describe("Examples / End2End Actions", () => { multisigPda: multisigPda, index: 0, }); + const message = new TransactionMessage({ payerKey: vaultPda, recentBlockhash: (await connection.getLatestBlockhash()).blockhash, @@ -68,6 +68,7 @@ describe("Examples / End2End Actions", () => { createTestTransferInstruction(vaultPda, outsider.publicKey), ], }); + const txBuilder = createVaultTransaction({ connection, multisig: multisigPda, @@ -75,15 +76,18 @@ describe("Examples / End2End Actions", () => { message, programId, }); - txBuilder.withProposal(); - txBuilder.withApproval(); - const signature = await txBuilder.sendAndConfirm(); + await txBuilder.withProposal(); + + transactionPda = txBuilder.getTransactionKey(); - console.log(signature); + await txBuilder.sendAndConfirm({ + feePayer: members.almighty, + options: { preflightCommitment: "finalized" }, + }); }); - it("should create, vote & execute a vault transaction", async () => { + it("should create a vault transaction & vote", async () => { const [vaultPda] = multisig.getVaultPda({ multisigPda: multisigPda, index: 0, @@ -102,12 +106,17 @@ describe("Examples / End2End Actions", () => { message, programId, }); - txBuilder.withProposal(); - txBuilder.withApproval(); - txBuilder.execute(); - const signature = await txBuilder.sendAndConfirm(); + (await txBuilder.withProposal()).withApproval(members.almighty.publicKey); + // await txBuilder.withExecute(members.executor.publicKey); + + await txBuilder.sendAndConfirm({ + feePayer: members.almighty, + options: { preflightCommitment: "finalized" }, + }); + }); - console.log(signature); + it("is this a vault transaction?", async () => { + assert.ok(multisig.isVaultTransaction(connection, transactionPda!)); }); });