From 9df78648626a07f36a5c676bf4d7c4cfb5ecad35 Mon Sep 17 00:00:00 2001 From: Joey Meere <100378695+joeymeere@users.noreply.github.com> Date: Thu, 17 Oct 2024 22:28:46 -0400 Subject: [PATCH 1/9] wip(sdk): add action builders --- sdk/multisig/src/actions/common.ts | 215 +++++++++++ sdk/multisig/src/actions/createBatch.ts | 1 + .../src/actions/createConfigTransaction.ts | 343 ++++++++++++++++++ sdk/multisig/src/actions/createMultisig.ts | 206 +++++++++++ .../src/actions/createTransactionMultiStep.ts | 1 + .../src/actions/createVaultTransaction.ts | 320 ++++++++++++++++ sdk/multisig/src/actions/index.ts | 8 + sdk/multisig/src/actions/members.ts | 23 ++ sdk/multisig/src/index.ts | 2 + sdk/multisig/src/types.ts | 4 + 10 files changed, 1123 insertions(+) create mode 100644 sdk/multisig/src/actions/common.ts create mode 100644 sdk/multisig/src/actions/createBatch.ts create mode 100644 sdk/multisig/src/actions/createConfigTransaction.ts create mode 100644 sdk/multisig/src/actions/createMultisig.ts create mode 100644 sdk/multisig/src/actions/createTransactionMultiStep.ts create mode 100644 sdk/multisig/src/actions/createVaultTransaction.ts create mode 100644 sdk/multisig/src/actions/index.ts create mode 100644 sdk/multisig/src/actions/members.ts diff --git a/sdk/multisig/src/actions/common.ts b/sdk/multisig/src/actions/common.ts new file mode 100644 index 00000000..b6621515 --- /dev/null +++ b/sdk/multisig/src/actions/common.ts @@ -0,0 +1,215 @@ +import { + Connection, + PublicKey, + SendOptions, + Signer, + TransactionInstruction, + TransactionMessage, + TransactionSignature, + VersionedTransaction, +} from "@solana/web3.js"; +import { PROGRAM_ID, instructions } from ".."; + +export interface BaseBuilderArgs { + connection: Connection; + creator: PublicKey; +} + +export interface BuildResult { + instructions: TransactionInstruction[]; +} + +export abstract class BaseBuilder< + T extends BuildResult, + U extends BaseBuilderArgs = BaseBuilderArgs +> { + protected connection: Connection; + protected instructions: TransactionInstruction[] = []; + protected creator: PublicKey = PublicKey.default; + protected buildPromise: Promise; + protected args: Omit; + + constructor(args: U) { + this.connection = args.connection; + this.creator = args.creator; + this.args = this.extractAdditionalArgs(args); + this.buildPromise = this.build(); + } + + private extractAdditionalArgs(args: U): Omit { + const { connection, creator, ...additionalArgs } = args; + return additionalArgs; + } + + protected abstract build(): Promise; + + /** + * Creates a transaction containing the corresponding instruction(s). + * @args feePayer - Optional signer to pay the transaction fee. + * @returns `VersionedTransaction`. + */ + 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; + }); + } + + /** + * Builds a transaction with the corresponding instruction(s), and sends it. + * @args feePayer - Optional signer to pay the transaction fee. + * @returns `TransactionSignature` + */ + async send( + feePayer?: Signer, + options?: SendOptions + ): 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]); + } + const signature = await this.connection.sendTransaction(tx, 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. + * @returns `TransactionSignature` + */ + async sendAndConfirm( + feePayer?: Signer, + options?: SendOptions + ): 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]); + } + const signature = await this.connection.sendTransaction(tx, options); + await this.connection.getSignatureStatuses([signature]); + return signature; + }); + } + + then( + onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: any) => TResult2 | PromiseLike) | null + ): Promise { + return this.buildPromise.then(onfulfilled, onrejected); + } +} + +export interface CreateProposalActionArgs { + /** The public key of the multisig config account */ + multisig: PublicKey; + /** The public key of the creator */ + creator: PublicKey; + /** Transaction index of the resulting Proposal */ + transactionIndex: number; + /** The public key of the fee payer, defaults to the creator */ + rentPayer?: PublicKey; + /** Whether the proposal should be initialized with status `Draft`. */ + isDraft?: boolean; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +export interface VoteActionArgs { + /** The public key of the multisig config account */ + multisig: PublicKey; + /** The public key of the approving member */ + member: PublicKey; + /** Transaction index of the resulting Proposal */ + transactionIndex: number; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +export interface ProposalResult { + /** `proposalCreate` instruction */ + instruction: TransactionInstruction; +} + +export async function createProposalCore( + args: CreateProposalActionArgs +): Promise { + const { + multisig, + creator, + transactionIndex, + rentPayer, + isDraft = false, + programId = PROGRAM_ID, + } = args; + + const ix = instructions.proposalCreate({ + multisigPda: multisig, + transactionIndex: BigInt(transactionIndex), + creator: creator, + isDraft, + rentPayer, + programId: programId, + }); + + return { + instruction: ix, + }; +} + +export async function createApprovalCore( + args: VoteActionArgs +): Promise { + const { multisig, member, transactionIndex, programId = PROGRAM_ID } = args; + + const ix = instructions.proposalApprove({ + multisigPda: multisig, + member: member, + transactionIndex: BigInt(transactionIndex), + programId: programId, + }); + + return { + instruction: ix, + }; +} + +export async function createRejectionCore( + args: VoteActionArgs +): Promise { + const { multisig, member, transactionIndex, programId = PROGRAM_ID } = args; + + const ix = instructions.proposalReject({ + multisigPda: multisig, + member: member, + transactionIndex: BigInt(transactionIndex), + programId: programId, + }); + + return { + instruction: ix, + }; +} diff --git a/sdk/multisig/src/actions/createBatch.ts b/sdk/multisig/src/actions/createBatch.ts new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/sdk/multisig/src/actions/createBatch.ts @@ -0,0 +1 @@ +// TODO diff --git a/sdk/multisig/src/actions/createConfigTransaction.ts b/sdk/multisig/src/actions/createConfigTransaction.ts new file mode 100644 index 00000000..ff2995d1 --- /dev/null +++ b/sdk/multisig/src/actions/createConfigTransaction.ts @@ -0,0 +1,343 @@ +import { Connection, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { instructions, accounts, getTransactionPda } from ".."; +import { ConfigAction, ConfigTransaction, PROGRAM_ID } from "../generated"; +import { + BaseBuilder, + createApprovalCore, + createProposalCore, + createRejectionCore, +} from "./common"; + +interface CreateConfigTransactionActionArgs { + /** The connection to an SVM network cluster */ + connection: Connection; + /** The public key of the multisig config account */ + multisig: PublicKey; + /** The public key of the creator */ + creator: PublicKey; + /** Transaction message containing the instructions to execute */ + actions: ConfigAction[]; + /** The public key of the fee payer, defaults to the creator */ + rentPayer?: PublicKey; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +interface CreateConfigTransactionResult { + /** `configTransactionCreate` instruction */ + instructions: TransactionInstruction[]; + /** Transaction index of the resulting ConfigTransaction */ + index: number; +} + +interface ExecuteConfigTransactionActionArgs { + /** The public key of the multisig config account */ + multisig: PublicKey; + /** Member who is executing the transaction */ + member: PublicKey; + /** Transaction index of the ConfigTransaction to execute */ + index: number; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +interface ExecuteConfigTransactionResult { + /** `configTransactionExecute` instruction */ + instruction: TransactionInstruction; +} + +/** + * Builds an instruction to create a new ConfigTransaction and returns the instruction with the corresponding transaction index. + * Can optionally chain additional methods for transactions, and sending. + * + * @param args - Object of type `CreateConfigTransactionActionArgs` that contains the necessary information to create a new ConfigTransaction. + * @returns `{ instruction: TransactionInstruction, index: number }` - object with the `configTransactionCreate` instruction and the transaction index of the resulting ConfigTransaction. + * + * @example + * // Basic usage (no chaining): + * const result = await createConfigTransaction({ + * connection, + * creator: creatorPublicKey, + * threshold: 2, + * members: membersList, + * timeLock: 3600, + * }); + * console.log(result.instruction); + * console.log(result.createKey); + * + * @example + * // Using the transaction() method: + * const transaction = await createConfigTransaction({ + * // ... args + * }).transaction(); + * + * @example + * // Using the rpc() method: + * const signature = await createConfigTransaction({ + * // ... args + * }).send(); + * + * @throws Will throw an error if required parameters are missing or invalid. + * + * @since 2.1.4 + */ +export function createConfigTransaction( + args: CreateConfigTransactionActionArgs +): ConfigTransactionBuilder { + return new ConfigTransactionBuilder(args); +} + +class ConfigTransactionBuilder extends BaseBuilder< + CreateConfigTransactionResult, + CreateConfigTransactionActionArgs +> { + public instructions: TransactionInstruction[] = []; + public index: number = 1; + + constructor(args: CreateConfigTransactionActionArgs) { + super(args); + } + + async build() { + const { multisig, actions, rentPayer, memo, programId } = this.args; + const result = await createConfigTransactionCore({ + connection: this.connection, + multisig, + creator: this.creator, + actions, + rentPayer, + memo, + programId, + }); + + this.instructions = [...result.instructions]; + this.index = result.index; + 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({ + multisig: this.args.multisig, + creator: this.creator, + transactionIndex: this.index, + isDraft, + programId: this.args.programId, + }); + + return { + index: this.index, + instruction: [...this.instructions, instruction], + }; + } + + /** + * Creates a transaction containing the ConfigTransaction creation instruction. + * @args feePayer - Optional signer to pay the transaction fee. + * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + */ + async withApproval(member?: PublicKey) { + const { instruction } = await createApprovalCore({ + multisig: this.args.multisig, + member: member ?? this.creator, + transactionIndex: this.index, + programId: this.args.programId, + }); + + return { + index: this.index, + instructions: [...this.instructions, instruction], + }; + } + + /** + * Creates a transaction containing the ConfigTransaction creation instruction. + * @args feePayer - Optional signer to pay the transaction fee. + * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + */ + async withRejection(member?: PublicKey) { + const { instruction } = await createRejectionCore({ + multisig: this.args.multisig, + member: member ?? this.creator, + transactionIndex: this.index, + programId: this.args.programId, + }); + + return { + index: this.index, + instructions: [...this.instructions, instruction], + }; + } + + /** + * Creates a transaction containing the ConfigTransaction creation instruction. + * @args feePayer - Optional signer to pay the transaction fee. + * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + */ + async execute(member?: PublicKey) { + const { instruction } = await executeConfigTransactionCore({ + multisig: this.args.multisig, + member: member ?? this.creator, + index: this.index, + programId: this.args.programId, + }); + + return { + index: this.index, + instructions: [...this.instructions, instruction], + }; + } +} + +export async function isConfigTransaction( + connection: Connection, + key: PublicKey +) { + try { + await ConfigTransaction.fromAccountAddress(connection, key); + return true; + } catch (err) { + return false; + } +} + +export const ConfigActions = { + AddMember: (newMember: PublicKey) => [ + { + __kind: "AddMember", + newMember, + }, + ], + RemoveMember: (oldMember: PublicKey) => [ + { + __kind: "RemoveMember", + oldMember, + }, + ], + ChangeThreshold: (newThreshold: number) => [ + { + __kind: "ChangeThreshold", + newThreshold, + }, + ], + SetTimeLock: (newTimeLock: number) => [ + { + __kind: "SetTimeLock", + newTimeLock, + }, + ], + AddSpendingLimit: (spendingLimit: SpendingLimit) => [ + { + __kind: "AddSpendingLimit", + ...spendingLimit, + }, + ], + RemoveSpendingLimit: (spendingLimit: PublicKey) => [ + { + __kind: "RemoveSpendingLimit", + spendingLimit, + }, + ], + SetRentCollector: (rentCollector: PublicKey) => [ + { + __kind: "SetRentCollector", + newRentCollector: rentCollector, + }, + ], +} as const; + +export interface SpendingLimit { + createKey: PublicKey; + vaultIndex: number; + mint: PublicKey; + amount: number; + period: number; + members: PublicKey[]; + destinations: PublicKey[]; +} + +async function createConfigTransactionCore( + args: CreateConfigTransactionActionArgs +): Promise { + const { + connection, + multisig, + creator, + actions, + rentPayer = creator, + memo, + programId = PROGRAM_ID, + } = args; + + const multisigInfo = await accounts.Multisig.fromAccountAddress( + connection, + multisig + ); + + const currentTransactionIndex = Number(multisigInfo.transactionIndex); + const index = BigInt(currentTransactionIndex + 1); + + const ix = instructions.configTransactionCreate({ + multisigPda: multisig, + transactionIndex: index, + creator: creator, + actions, + memo: memo, + rentPayer, + programId: programId, + }); + + return { instructions: [ix], index: Number(index) }; +} + +async function executeConfigTransactionCore( + args: ExecuteConfigTransactionActionArgs +): Promise { + const { multisig, index, member, programId = PROGRAM_ID } = args; + const ix = instructions.configTransactionExecute({ + multisigPda: multisig, + member: member, + transactionIndex: BigInt(index), + programId: programId, + }); + + return { instruction: ix }; +} diff --git a/sdk/multisig/src/actions/createMultisig.ts b/sdk/multisig/src/actions/createMultisig.ts new file mode 100644 index 00000000..b75a6775 --- /dev/null +++ b/sdk/multisig/src/actions/createMultisig.ts @@ -0,0 +1,206 @@ +import { + Connection, + Signer, + Keypair, + PublicKey, + TransactionInstruction, + TransactionMessage, + VersionedTransaction, +} 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; + /** The number of approvals required to approve transactions */ + threshold: number; + /** The list of members in the multisig, with their associated permissions */ + members: Member[]; + /** Optional time lock in seconds */ + timeLock?: number; + /** Optional config authority key that can override consensus for ConfigTransactions */ + configAuthority?: PublicKey; + /** Optional rent collector where completed transaction rent will go after reclaim */ + rentCollector?: PublicKey; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +interface CreateMultisigResult { + /** `multisigCreateV2` instruction */ + instructions: TransactionInstruction[]; + /** Keypair seed that is required to sign the transaction */ + createKey: Keypair; +} + +/** + * Builds an instruction to create a new Multisig, and returns the instruction and `createKey` + * with the option to chain additional methods for building transactions, and sending. + * + * @param args - Object of type `CreateMultisigActionArgs` that contains the necessary information to create a new multisig. + * @returns `{ instruction: TransactionInstruction, createKey: Keypair }` - object with the `multisigCreateV2` instruction and the `createKey` that is required to sign the transaction. + * + * @example + * // Basic usage (no chaining): + * const result = await createMultisig({ + * connection, + * creator: creatorPublicKey, + * threshold: 2, + * members: membersList, + * }); + * console.log(result.instruction); + * console.log(result.createKey); + * + * @example + * // Using the transaction() method: + * const transaction = await createMultisig({ + * // ... args + * }).transaction(); + * + * @example + * // Using the rpc() method: + * const signature = await createMultisig({ + * // ... args + * }).send(); + * + * @throws Will throw an error if required parameters are missing or invalid. + * + * @since 2.1.4 + */ +export function createMultisig( + args: CreateMultisigActionArgs +): MultisigBuilder { + return new MultisigBuilder(args); +} + +class MultisigBuilder extends BaseBuilder< + CreateMultisigResult, + CreateMultisigActionArgs +> { + public instructions: TransactionInstruction[] = []; + public createKey: Keypair = Keypair.generate(); + + constructor(args: CreateMultisigActionArgs) { + super(args); + } + + async build() { + const { + threshold, + members, + timeLock = 0, + configAuthority, + rentCollector, + programId = PROGRAM_ID, + } = this.args; + const result = await createMultisigCore({ + connection: this.connection, + creator: this.creator, + threshold, + members, + timeLock, + configAuthority, + rentCollector, + programId, + }); + + this.instructions = [...result.instructions]; + this.createKey = result.createKey; + return this; + } + + getInstructions(): TransactionInstruction[] { + return this.instructions; + } + + getCreateKey(): Keypair { + return this.createKey; + } + + getMultisigKey(): PublicKey { + const [multisigPda] = getMultisigPda({ + createKey: this.createKey.publicKey, + }); + + return multisigPda; + } + + getMultisigAccount(key: PublicKey) { + return this.buildPromise.then(async () => { + const multisigAccount = await Multisig.fromAccountAddress( + this.connection, + key + ); + + return multisigAccount; + }); + } +} + +export async function createMultisigCore( + args: CreateMultisigActionArgs +): Promise { + const { + connection, + creator, + threshold, + members, + timeLock = 0, + configAuthority, + rentCollector, + programId = PROGRAM_ID, + } = args; + + const createKey = Keypair.generate(); + const [multisigPda] = getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + const programConfigPda = getProgramConfigPda({ programId })[0]; + + const programConfig = await ProgramConfig.fromAccountAddress( + connection, + programConfigPda + ); + + const ix = instructions.multisigCreateV2({ + creator, + threshold, + members, + multisigPda: multisigPda, + treasury: programConfig.treasury, + createKey: createKey.publicKey, + timeLock: timeLock ?? 0, + rentCollector: rentCollector ?? null, + configAuthority: configAuthority ?? null, + programId: programId ?? PROGRAM_ID, + }); + + return { + instructions: [ix], + createKey: createKey, + }; +} + +async function Example() { + const connection = new Connection("https://api.mainnet-beta.solana.com"); + const multisig = createMultisig({ + connection, + members: createMembers([ + { key: PublicKey.default, permissions: SquadPermissions.All }, + ]), + creator: PublicKey.default, + threshold: 2, + }); + + const a = await multisig.getInstructions(); + const b = multisig.getMultisigKey(); + const c = await multisig.getMultisigAccount(b!); + + console.log(c.members); +} diff --git a/sdk/multisig/src/actions/createTransactionMultiStep.ts b/sdk/multisig/src/actions/createTransactionMultiStep.ts new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/sdk/multisig/src/actions/createTransactionMultiStep.ts @@ -0,0 +1 @@ +// TODO diff --git a/sdk/multisig/src/actions/createVaultTransaction.ts b/sdk/multisig/src/actions/createVaultTransaction.ts new file mode 100644 index 00000000..d83b6925 --- /dev/null +++ b/sdk/multisig/src/actions/createVaultTransaction.ts @@ -0,0 +1,320 @@ +import { + Connection, + PublicKey, + TransactionInstruction, + TransactionMessage, + SystemProgram, + AddressLookupTableAccount, +} from "@solana/web3.js"; +import { instructions, accounts, getTransactionPda } from ".."; +import { PROGRAM_ID, VaultTransaction } from "../generated"; +import { + BaseBuilder, + createApprovalCore, + createRejectionCore, + createProposalCore, +} from "./common"; + +interface CreateVaultTransactionActionArgs { + /** The connection to an SVM network cluster */ + connection: Connection; + /** The public key of the multisig config account */ + multisig: PublicKey; + /** The public key of the creator */ + creator: PublicKey; + /** Transaction message containing the instructions to execute */ + message: TransactionMessage; + /** Index of the vault to target. Defaults to 0 */ + vaultIndex?: number; + /** Specify a number of ephemeral signers to include. + * Useful if the underlying transaction requires more than one signer. + */ + ephemeralSigners?: number; + /** The public key of the fee payer, defaults to the creator */ + rentPayer?: PublicKey; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +interface CreateVaultTransactionResult { + /** `vaultTransactionCreate` instruction */ + instructions: TransactionInstruction[]; + /** Transaction index of the resulting VaultTransaction */ + index: number; +} + +interface ExecuteVaultTransactionActionArgs { + /** The connection to an SVM network cluster */ + connection: Connection; + /** The public key of the multisig config account */ + multisig: PublicKey; + /** Member who is executing the transaction */ + member: PublicKey; + /** Transaction index of the VaultTransaction to execute */ + index: number; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +interface ExecuteVaultTransactionResult { + /** `vaultTransactionExecute` instruction */ + instruction: TransactionInstruction; + /** AddressLookupTableAccounts for the transaction */ + lookupTableAccounts: AddressLookupTableAccount[]; +} + +/** + * Builds an instruction to create a new VaultTransaction and returns the instruction with the corresponding transaction index. + * Can optionally chain additional methods for transactions, and sending. + * + * @param args - Object of type `CreateVaultTransactionActionArgs` that contains the necessary information to create a new VaultTransaction. + * @returns `{ instruction: TransactionInstruction, index: number }` - object with the `vaultTransactionCreate` instruction and the transaction index of the resulting VaultTransaction. + * + * @example + * // Basic usage (no chaining): + * const result = await createVaultTransaction({ + * connection, + * creator: creatorPublicKey, + * threshold: 2, + * members: membersList, + * timeLock: 3600, + * }); + * console.log(result.instruction); + * console.log(result.createKey); + * + * @example + * // Using the transaction() method: + * const transaction = await createVaultTransaction({ + * // ... args + * }).transaction(); + * + * @example + * // Using the send() method: + * const signature = await createVaultTransaction({ + * // ... args + * }).send(); + * + * @throws Will throw an error if required parameters are missing or invalid. + * + * @since 2.1.4 + */ +export function createVaultTransaction( + args: CreateVaultTransactionActionArgs +): VaultTransactionBuilder { + return new VaultTransactionBuilder(args); +} + +class VaultTransactionBuilder extends BaseBuilder< + CreateVaultTransactionResult, + CreateVaultTransactionActionArgs +> { + public instructions: TransactionInstruction[] = []; + public index: number = 1; + + constructor(args: CreateVaultTransactionActionArgs) { + super(args); + } + + async build() { + const { + multisig, + message, + vaultIndex = 0, + ephemeralSigners = 0, + rentPayer = this.creator, + memo, + programId = PROGRAM_ID, + } = this.args; + const result = await createVaultTransactionCore({ + connection: this.connection, + multisig, + creator: this.creator, + message, + vaultIndex, + ephemeralSigners, + rentPayer, + memo, + programId, + }); + + 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; + }); + } + + /** + * Creates a transaction containing the VaultTransaction 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({ + multisig: this.args.multisig, + creator: this.creator, + transactionIndex: this.index, + programId: this.args.programId, + isDraft, + }); + + return { + index: this.index, + instruction: [...this.instructions, instruction], + }; + } + + /** + * Creates a transaction containing the VaultTransaction creation instruction. + * @args feePayer - Optional signer to pay the transaction fee. + * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + */ + async withApproval(member?: PublicKey) { + const { instruction } = await createApprovalCore({ + multisig: this.args.multisig, + member: member ?? this.creator, + transactionIndex: this.index, + programId: this.args.programId, + }); + + return { + index: this.index, + instructions: [...this.instructions, instruction], + }; + } + + /** + * Creates a transaction containing the VaultTransaction creation instruction. + * @args feePayer - Optional signer to pay the transaction fee. + * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + */ + async withRejection(member?: PublicKey) { + const { instruction } = await createRejectionCore({ + multisig: this.args.multisig, + member: member ?? this.creator, + transactionIndex: this.index, + programId: this.args.programId, + }); + + return { + index: this.index, + instructions: [...this.instructions, instruction], + }; + } + + /** + * Creates a transaction containing the VaultTransaction creation instruction. + * @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, + }); + + return { + index: this.index, + instructions: [...this.instructions, instruction], + }; + } +} + +export async function isVaultTransaction( + connection: Connection, + key: PublicKey +) { + try { + await VaultTransaction.fromAccountAddress(connection, key); + return true; + } catch (err) { + return false; + } +} + +async function createVaultTransactionCore( + args: CreateVaultTransactionActionArgs +): Promise { + const { + connection, + multisig, + creator, + message, + vaultIndex = 0, + ephemeralSigners = 0, + rentPayer = creator, + memo, + programId = PROGRAM_ID, + } = args; + + const multisigInfo = await accounts.Multisig.fromAccountAddress( + connection, + multisig + ); + + const currentTransactionIndex = Number(multisigInfo.transactionIndex); + const index = BigInt(currentTransactionIndex + 1); + + const ix = instructions.vaultTransactionCreate({ + multisigPda: multisig, + transactionIndex: index, + creator: creator, + vaultIndex: vaultIndex, + ephemeralSigners: ephemeralSigners, + transactionMessage: message, + memo: memo, + rentPayer, + programId: programId, + }); + + return { instructions: [ix], index: Number(index) }; +} + +async function executeVaultTransactionCore( + args: ExecuteVaultTransactionActionArgs +): Promise { + const { connection, multisig, index, member, programId = PROGRAM_ID } = args; + const ix = instructions.vaultTransactionExecute({ + connection, + multisigPda: multisig, + member: member, + transactionIndex: BigInt(index), + programId: programId, + }); + + return { ...ix }; +} diff --git a/sdk/multisig/src/actions/index.ts b/sdk/multisig/src/actions/index.ts new file mode 100644 index 00000000..3ee6847f --- /dev/null +++ b/sdk/multisig/src/actions/index.ts @@ -0,0 +1,8 @@ +export * from "./createMultisig"; +export * from "./createVaultTransaction"; +export * from "./createConfigTransaction"; +// WIP +//export * from "./createTransactionMultiStep"; + +// WIP +//export * from "./createBatch"; diff --git a/sdk/multisig/src/actions/members.ts b/sdk/multisig/src/actions/members.ts new file mode 100644 index 00000000..c9a138a7 --- /dev/null +++ b/sdk/multisig/src/actions/members.ts @@ -0,0 +1,23 @@ +import { PublicKey } from "@solana/web3.js"; +import { Permissions } from "../types"; + +export enum SquadPermissions { + Proposer = 1, + Voter = 2, + Executor = 4, + ProposerAndVoter = 3, + VoterAndExecutor = 5, + ProposerAndExecutor = 6, + All = 7, +} + +export function createMembers( + members: { key: PublicKey; permissions: number }[] +) { + return members.map((member) => { + return { + key: member.key, + permissions: Permissions.fromMask(member.permissions), + }; + }); +} diff --git a/sdk/multisig/src/index.ts b/sdk/multisig/src/index.ts index 5364731d..94fcfd95 100644 --- a/sdk/multisig/src/index.ts +++ b/sdk/multisig/src/index.ts @@ -12,6 +12,8 @@ export * as rpc from "./rpc"; 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"; /** Additional types */ export * as types from "./types.js"; /** Utils for the multisig program. */ diff --git a/sdk/multisig/src/types.ts b/sdk/multisig/src/types.ts index baa18812..5c5606a7 100644 --- a/sdk/multisig/src/types.ts +++ b/sdk/multisig/src/types.ts @@ -39,6 +39,10 @@ export class Permissions implements IPermissions { ); } + static fromMask(mask: number) { + return new Permissions(mask); + } + static all() { return new Permissions( Object.values(Permission).reduce( From a205ebbc0f70ef418d934240cecb8685571e33fd Mon Sep 17 00:00:00 2001 From: Joey Meere <100378695+joeymeere@users.noreply.github.com> Date: Fri, 18 Oct 2024 00:23:48 -0400 Subject: [PATCH 2/9] feat(sdk): scaffold batch action builder --- sdk/multisig/src/actions/createBatch.ts | 423 +++++++++++++++++- sdk/multisig/src/actions/createMultisig.ts | 16 +- .../src/actions/createVaultTransaction.ts | 1 - 3 files changed, 427 insertions(+), 13 deletions(-) diff --git a/sdk/multisig/src/actions/createBatch.ts b/sdk/multisig/src/actions/createBatch.ts index 70b786d1..2159aac2 100644 --- a/sdk/multisig/src/actions/createBatch.ts +++ b/sdk/multisig/src/actions/createBatch.ts @@ -1 +1,422 @@ -// TODO +import { + Connection, + PublicKey, + TransactionInstruction, + TransactionMessage, + AddressLookupTableAccount, +} from "@solana/web3.js"; +import { + instructions, + accounts, + getBatchTransactionPda, + getTransactionPda, +} from ".."; +import { Batch, PROGRAM_ID, VaultBatchTransaction } from "../generated"; +import { + BaseBuilder, + createApprovalCore, + createRejectionCore, + createProposalCore, +} from "./common"; + +interface CreateBatchActionArgs { + /** The connection to an SVM network cluster */ + connection: Connection; + /** 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 */ + rentPayer?: PublicKey; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +interface CreateBatchResult { + /** `vaultTransactionCreate` instruction */ + instructions: TransactionInstruction[]; + /** Transaction index of the resulting VaultTransaction */ + index: number; +} + +interface BatchAddTransactionActionArgs { + /** The public key of the multisig config account */ + multisig: PublicKey; + /** Member who is executing the transaction */ + member: PublicKey; + /** Transaction index of the Batch created. */ + globalIndex: number; + /** Local transaction index of a transaction inside of the Batch. */ + innerIndex: number; + /** Transaction message containing the instructions to execute */ + message: TransactionMessage; + /** Index of the vault to target. Defaults to 0 */ + vaultIndex?: number; + /** Specify a number of ephemeral signers to include. + * Useful if the underlying transaction requires more than one signer. + */ + ephemeralSigners?: number; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +interface BatchAddTransactionResult { + /** `batchAddTransaction` instruction */ + instruction: TransactionInstruction; +} + +interface ExecuteBatchActionArgs { + /** The connection to an SVM network cluster */ + connection: Connection; + /** The public key of the multisig config account */ + multisig: PublicKey; + /** Member who is executing the transaction */ + member: PublicKey; + /** Transaction index of the VaultTransaction to execute */ + index: number; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +interface ExecuteBatchResult { + /** `vaultTransactionExecute` instruction */ + instruction: TransactionInstruction; + /** AddressLookupTableAccounts for the transaction */ + lookupTableAccounts: AddressLookupTableAccount[]; +} + +/** + * Builds an instruction to create a new VaultTransaction and returns the instruction with the corresponding transaction index. + * Can optionally chain additional methods for transactions, and sending. + * + * @param args - Object of type `CreateVaultTransactionActionArgs` that contains the necessary information to create a new VaultTransaction. + * @returns `{ instruction: TransactionInstruction, index: number }` - object with the `vaultTransactionCreate` instruction and the transaction index of the resulting VaultTransaction. + * + * @example + * // Basic usage (no chaining): + * const result = await createVaultTransaction({ + * connection, + * creator: creatorPublicKey, + * threshold: 2, + * members: membersList, + * timeLock: 3600, + * }); + * console.log(result.instruction); + * console.log(result.createKey); + * + * @example + * // Using the transaction() method: + * const transaction = await createVaultTransaction({ + * // ... args + * }).transaction(); + * + * @example + * // Using the send() method: + * const signature = await createVaultTransaction({ + * // ... args + * }).send(); + * + * @throws Will throw an error if required parameters are missing or invalid. + * + * @since 2.1.4 + */ +export function createBatch(args: CreateBatchActionArgs): BatchBuilder { + return new BatchBuilder(args); +} + +class BatchBuilder extends BaseBuilder< + CreateBatchResult, + CreateBatchActionArgs +> { + public instructions: TransactionInstruction[] = []; + public index: number = 1; + public innerIndex: number = 0; + + constructor(args: CreateBatchActionArgs) { + super(args); + } + + async build() { + const { + multisig, + vaultIndex = 0, + rentPayer = this.creator, + memo, + programId = PROGRAM_ID, + } = this.args; + const result = await createBatchCore({ + connection: this.connection, + multisig, + creator: this.creator, + vaultIndex, + rentPayer, + memo, + programId, + }); + + this.instructions = [...result.instructions]; + this.index = result.index; + return this; + } + + getInstructions(): TransactionInstruction[] { + return this.instructions; + } + + getIndex(): number { + return this.index; + } + + getBatchKey(): PublicKey | null { + const index = this.index; + const [batchPda] = getTransactionPda({ + multisigPda: this.args.multisig, + index: BigInt(index ?? 1), + }); + + return batchPda; + } + + getBatchTransactionKey(innerIndex: number): PublicKey | null { + const index = this.index; + const [batchPda] = getBatchTransactionPda({ + multisigPda: this.args.multisig, + batchIndex: BigInt(index ?? 1), + transactionIndex: this.innerIndex ?? 1, + }); + + return batchPda; + } + + getAllBatchTransactionKeys(localIndex: number): PublicKey[] | null { + const index = this.index; + const transactions = []; + for (let i = 1; i <= localIndex; i++) { + const [batchPda] = getBatchTransactionPda({ + multisigPda: this.args.multisig, + batchIndex: BigInt(index ?? 1), + transactionIndex: i, + }); + + transactions.push(batchPda); + } + + return transactions; + } + + async getBatchAccount(key: PublicKey) { + return this.buildPromise.then(async () => { + const batchAccount = await Batch.fromAccountAddress(this.connection, key); + + return batchAccount; + }); + } + + /** + * Creates a transaction containing the VaultTransaction creation instruction. + * @args feePayer - Optional signer to pay the transaction fee. + * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + */ + async addTransaction(message: TransactionMessage, member?: PublicKey) { + this.innerIndex++; + const { instruction } = await addBatchTransactionCore({ + multisig: this.args.multisig, + member: member ?? this.creator, + globalIndex: this.index, + innerIndex: this.innerIndex, + message, + // vaultIndex: this.vaultIndex, + // ephemeralSigners: this.ephemeralSigners, + programId: this.args.programId, + }); + + return { + innerIndex: this.innerIndex, + instructions: [...this.instructions, instruction], + }; + } + + /** + * Creates a transaction containing the VaultTransaction 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({ + multisig: this.args.multisig, + creator: this.creator, + transactionIndex: this.index, + programId: this.args.programId, + isDraft, + }); + + return { + index: this.index, + instruction: [...this.instructions, instruction], + }; + } + + /** + * Creates a transaction containing the VaultTransaction creation instruction. + * @args feePayer - Optional signer to pay the transaction fee. + * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + */ + async withApproval(member?: PublicKey) { + const { instruction } = await createApprovalCore({ + multisig: this.args.multisig, + member: member ?? this.creator, + transactionIndex: this.index, + programId: this.args.programId, + }); + + return { + index: this.index, + instructions: [...this.instructions, instruction], + }; + } + + /** + * Creates a transaction containing the VaultTransaction creation instruction. + * @args feePayer - Optional signer to pay the transaction fee. + * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + */ + async withRejection(member?: PublicKey) { + const { instruction } = await createRejectionCore({ + multisig: this.args.multisig, + member: member ?? this.creator, + transactionIndex: this.index, + programId: this.args.programId, + }); + + return { + index: this.index, + instructions: [...this.instructions, instruction], + }; + } + + /** + * Creates a transaction containing the VaultTransaction creation instruction. + * @args feePayer - Optional signer to pay the transaction fee. + * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + */ + async execute(member?: PublicKey) { + const { instruction } = await executeBatchTransactionCore({ + connection: this.connection, + multisig: this.args.multisig, + member: member ?? this.creator, + index: this.index, + programId: this.args.programId, + }); + + return { + index: this.index, + instructions: [...this.instructions, instruction], + }; + } +} + +export async function isBatch(connection: Connection, key: PublicKey) { + try { + await Batch.fromAccountAddress(connection, key); + return true; + } catch (err) { + return false; + } +} + +export async function isBatchTransaction( + connection: Connection, + key: PublicKey +) { + try { + await VaultBatchTransaction.fromAccountAddress(connection, key); + return true; + } catch (err) { + return false; + } +} + +async function createBatchCore( + args: CreateBatchActionArgs +): Promise { + const { + connection, + multisig, + creator, + vaultIndex = 0, + rentPayer = creator, + memo, + programId = PROGRAM_ID, + } = args; + + const multisigInfo = await accounts.Multisig.fromAccountAddress( + connection, + multisig + ); + + const currentTransactionIndex = Number(multisigInfo.transactionIndex); + const index = BigInt(currentTransactionIndex + 1); + + const ix = instructions.batchCreate({ + multisigPda: multisig, + batchIndex: index, + creator: creator, + vaultIndex: vaultIndex, + memo: memo, + rentPayer, + programId: programId, + }); + + return { instructions: [ix], index: Number(index) }; +} + +async function addBatchTransactionCore( + args: BatchAddTransactionActionArgs +): Promise { + const { + multisig, + globalIndex, + innerIndex, + message, + vaultIndex, + ephemeralSigners, + member, + programId = PROGRAM_ID, + } = args; + const ix = instructions.batchAddTransaction({ + multisigPda: multisig, + member: member, + batchIndex: BigInt(globalIndex), + transactionIndex: innerIndex, + transactionMessage: message, + vaultIndex: vaultIndex ?? 0, + ephemeralSigners: ephemeralSigners ?? 0, + programId: programId, + }); + + return { instruction: ix }; +} + +async function executeBatchTransactionCore( + args: ExecuteBatchActionArgs +): Promise { + const { connection, multisig, index, member, programId = PROGRAM_ID } = args; + const ix = instructions.batchExecuteTransaction({ + connection, + multisigPda: multisig, + member: member, + batchIndex: BigInt(index), + transactionIndex: index, + programId: programId, + }); + + return { ...ix }; +} diff --git a/sdk/multisig/src/actions/createMultisig.ts b/sdk/multisig/src/actions/createMultisig.ts index b75a6775..dbed1efd 100644 --- a/sdk/multisig/src/actions/createMultisig.ts +++ b/sdk/multisig/src/actions/createMultisig.ts @@ -1,11 +1,8 @@ import { Connection, - Signer, Keypair, PublicKey, TransactionInstruction, - TransactionMessage, - VersionedTransaction, } from "@solana/web3.js"; import { Member, Multisig, PROGRAM_ID, ProgramConfig } from "../generated"; import { getMultisigPda, getProgramConfigPda, instructions } from ".."; @@ -187,20 +184,17 @@ export async function createMultisigCore( }; } +/* async function Example() { const connection = new Connection("https://api.mainnet-beta.solana.com"); - const multisig = createMultisig({ + const feePayer = Keypair.generate(); + const signature = await createMultisig({ connection, members: createMembers([ { key: PublicKey.default, permissions: SquadPermissions.All }, ]), creator: PublicKey.default, threshold: 2, - }); - - const a = await multisig.getInstructions(); - const b = multisig.getMultisigKey(); - const c = await multisig.getMultisigAccount(b!); - - console.log(c.members); + }).sendAndConfirm(feePayer); } +*/ diff --git a/sdk/multisig/src/actions/createVaultTransaction.ts b/sdk/multisig/src/actions/createVaultTransaction.ts index d83b6925..bf6cc519 100644 --- a/sdk/multisig/src/actions/createVaultTransaction.ts +++ b/sdk/multisig/src/actions/createVaultTransaction.ts @@ -3,7 +3,6 @@ import { PublicKey, TransactionInstruction, TransactionMessage, - SystemProgram, AddressLookupTableAccount, } from "@solana/web3.js"; import { instructions, accounts, getTransactionPda } from ".."; From ec367d2799d35c4e2895d20a07d4ce04974cd2e1 Mon Sep 17 00:00:00 2001 From: Joey Meere <100378695+joeymeere@users.noreply.github.com> Date: Fri, 18 Oct 2024 19:27:10 -0400 Subject: [PATCH 3/9] feat(sdk): batch create actions + enforce method order someone shouldn't be able to call `txBuilder.withProposal().withApproval().withProposal()`, Typescript should stop them --- sdk/multisig/src/actions/actionTypes.ts | 106 ++++++++ sdk/multisig/src/actions/common.ts | 238 +++++++++++++++--- sdk/multisig/src/actions/createBatch.ts | 79 +++--- .../src/actions/createConfigTransaction.ts | 89 +++---- sdk/multisig/src/actions/createMultisig.ts | 41 ++- .../src/actions/createVaultTransaction.ts | 166 +++++++----- tests/suites/examples/actions.ts | 113 +++++++++ 7 files changed, 613 insertions(+), 219 deletions(-) create mode 100644 sdk/multisig/src/actions/actionTypes.ts create mode 100644 tests/suites/examples/actions.ts 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); + }); +}); From 50ca3b7b6aa2698ea371e2e17cc6bb79fb727ec8 Mon Sep 17 00:00:00 2001 From: Joey Meere <100378695+joeymeere@users.noreply.github.com> Date: Fri, 18 Oct 2024 23:10:36 -0400 Subject: [PATCH 4/9] refactor(sdk): fix async handling prelim tests passing --- sdk/multisig/src/actions/common.ts | 181 +++++++++++------- sdk/multisig/src/actions/createBatch.ts | 43 ++--- .../src/actions/createConfigTransaction.ts | 6 +- sdk/multisig/src/actions/createMultisig.ts | 101 ++++++---- .../src/actions/createVaultTransaction.ts | 7 +- sdk/multisig/src/actions/index.ts | 1 + sdk/multisig/src/index.ts | 2 +- tests/index.ts | 1 + tests/suites/examples/actions.ts | 73 +++---- 9 files changed, 241 insertions(+), 174 deletions(-) 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!)); }); }); From 0bee728904a20fe0d46f8129d3c0132365b1a9bc Mon Sep 17 00:00:00 2001 From: Joey Meere <100378695+joeymeere@users.noreply.github.com> Date: Sat, 19 Oct 2024 00:18:30 -0400 Subject: [PATCH 5/9] feat(sdk): create builder instance from account key ex: `buildFromVaultTransaction` takes a key for a `VaultTransaction` account, deserializes it, and creates a builder instance from it --- sdk/multisig/src/actions/actionTypes.ts | 6 +- sdk/multisig/src/actions/common.ts | 7 + .../src/actions/createConfigTransaction.ts | 70 ++++++---- .../src/actions/createTransactionMultiStep.ts | 1 - .../src/actions/createVaultTransaction.ts | 129 ++++++++++++++++-- sdk/multisig/src/actions/members.ts | 11 +- tests/suites/examples/actions.ts | 43 +++++- 7 files changed, 224 insertions(+), 43 deletions(-) delete mode 100644 sdk/multisig/src/actions/createTransactionMultiStep.ts diff --git a/sdk/multisig/src/actions/actionTypes.ts b/sdk/multisig/src/actions/actionTypes.ts index 8e67f339..3f1948a5 100644 --- a/sdk/multisig/src/actions/actionTypes.ts +++ b/sdk/multisig/src/actions/actionTypes.ts @@ -15,11 +15,12 @@ type BaseMethodKeys = | "sendAndConfirm" | "customSend"; -type BaseGetKeys = "getInstructions"; type BaseSendKeys = "send" | "sendAndConfirm" | "customSend"; +// TODO: Split between sync and async getters. type TransactionGetKeys = | "getIndex" + | "getInstructions" | "getTransactionKey" | "getProposalKey" | "getTransactionAccount" @@ -31,7 +32,9 @@ type TransactionActionKeys = | "withRejection" | "withExecute"; +// TODO: Split between sync and async getters. type BatchGetKeys = + | "getInstructions" | "getBatchKey" | "getBatchTransactionKey" | "getAllBatchTransactionKeys" @@ -61,6 +64,7 @@ type MethodProgression = { | BaseSendKeys | TransactionGetKeys; withExecute: BaseSendKeys | TransactionGetKeys; + reclaimRent: BaseSendKeys | TransactionGetKeys; // Synchronous Getters getInstructions: BaseMethodKeys | BaseSendKeys; getIndex: diff --git a/sdk/multisig/src/actions/common.ts b/sdk/multisig/src/actions/common.ts index 608d7039..0484c5aa 100644 --- a/sdk/multisig/src/actions/common.ts +++ b/sdk/multisig/src/actions/common.ts @@ -39,6 +39,8 @@ export abstract class BaseBuilder< protected buildPromise: Promise; protected args: Omit; private built: boolean = false; + // Use this as an indicator to clear all instructions? + private sent: boolean = false; constructor(args: U) { this.connection = args.connection; @@ -145,6 +147,8 @@ export abstract class BaseBuilder< tx, settings?.options ); + this.sent = true; + return signature; } @@ -202,6 +206,7 @@ export abstract class BaseBuilder< sent = true; } } + this.sent = true; return signature; } @@ -243,6 +248,8 @@ export abstract class BaseBuilder< }); const signature = await callback(message); + this.sent = true; + return signature; } diff --git a/sdk/multisig/src/actions/createConfigTransaction.ts b/sdk/multisig/src/actions/createConfigTransaction.ts index 864e94e1..ffd60e63 100644 --- a/sdk/multisig/src/actions/createConfigTransaction.ts +++ b/sdk/multisig/src/actions/createConfigTransaction.ts @@ -8,6 +8,7 @@ import { createRejectionCore, } from "./common"; import { Methods } from "./actionTypes"; +import { Member } from "../types"; interface CreateConfigTransactionActionArgs { /** The connection to an SVM network cluster */ @@ -119,6 +120,21 @@ class ConfigTransactionBuilder extends BaseTransactionBuilder< this.index = result.index; } + /** + * Fetches deserialized account data for the corresponding `ConfigTransaction` account after it is built and sent. + * + * @returns `ConfigTransaction` + */ + async getTransactionAccount(key: PublicKey) { + this.ensureBuilt(); + 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. @@ -200,6 +216,10 @@ class ConfigTransactionBuilder extends BaseTransactionBuilder< return this; } + + async reclaimRent() { + // TODO + } } export async function isConfigTransaction( @@ -215,48 +235,48 @@ export async function isConfigTransaction( } export const ConfigActions = { - AddMember: (newMember: PublicKey) => [ + AddMember: (newMember: Member) => [ { __kind: "AddMember", newMember, }, ], - RemoveMember: (oldMember: PublicKey) => [ - { + RemoveMember: (oldMember: PublicKey) => { + return { __kind: "RemoveMember", oldMember, - }, - ], - ChangeThreshold: (newThreshold: number) => [ - { + } as ConfigAction; + }, + ChangeThreshold: (newThreshold: number) => { + return { __kind: "ChangeThreshold", newThreshold, - }, - ], - SetTimeLock: (newTimeLock: number) => [ - { + } as ConfigAction; + }, + SetTimeLock: (newTimeLock: number) => { + return { __kind: "SetTimeLock", newTimeLock, - }, - ], - AddSpendingLimit: (spendingLimit: SpendingLimit) => [ - { + } as ConfigAction; + }, + AddSpendingLimit: (spendingLimit: SpendingLimit) => { + return { __kind: "AddSpendingLimit", ...spendingLimit, - }, - ], - RemoveSpendingLimit: (spendingLimit: PublicKey) => [ - { + } as ConfigAction; + }, + RemoveSpendingLimit: (spendingLimit: PublicKey) => { + return { __kind: "RemoveSpendingLimit", spendingLimit, - }, - ], - SetRentCollector: (rentCollector: PublicKey) => [ - { + } as ConfigAction; + }, + SetRentCollector: (rentCollector: PublicKey) => { + return { __kind: "SetRentCollector", newRentCollector: rentCollector, - }, - ], + } as ConfigAction; + }, } as const; export interface SpendingLimit { diff --git a/sdk/multisig/src/actions/createTransactionMultiStep.ts b/sdk/multisig/src/actions/createTransactionMultiStep.ts deleted file mode 100644 index 70b786d1..00000000 --- a/sdk/multisig/src/actions/createTransactionMultiStep.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO diff --git a/sdk/multisig/src/actions/createVaultTransaction.ts b/sdk/multisig/src/actions/createVaultTransaction.ts index 7409be56..0d41c998 100644 --- a/sdk/multisig/src/actions/createVaultTransaction.ts +++ b/sdk/multisig/src/actions/createVaultTransaction.ts @@ -4,14 +4,22 @@ import { TransactionInstruction, TransactionMessage, AddressLookupTableAccount, + Message, } from "@solana/web3.js"; import { instructions, accounts } from ".."; -import { PROGRAM_ID, VaultTransaction } from "../generated"; +import { + PROGRAM_ID, + VaultTransaction, + multisigCompiledInstructionBeet, + vaultTransactionMessageBeet, +} from "../generated"; import { createApprovalCore, createRejectionCore, createProposalCore, BaseTransactionBuilder, + BuildResult, + ProposalResult, } from "./common"; import { Methods } from "./actionTypes"; @@ -24,23 +32,23 @@ interface CreateVaultTransactionActionArgs { creator: PublicKey; /** Transaction message containing the instructions to execute */ message: TransactionMessage; - /** Index of the vault to target. Defaults to 0 */ + /** (Optional) Index of the transaction to build. If omitted, this will be fetched from the multisig account. */ + transactionIndex?: number; + /** (Optional) Index of the vault to target. Defaults to 0 */ vaultIndex?: number; - /** Specify a number of ephemeral signers to include. + /** (Optional) Specify a number of ephemeral signers to include. * Useful if the underlying transaction requires more than one signer. */ ephemeralSigners?: number; - /** The public key of the fee payer, defaults to the creator */ + /** (Optional) The public key of the fee payer, defaults to the creator */ rentPayer?: PublicKey; - /** Optional memo for indexing purposes */ + /** (Optional) UTF-8 Memo for indexing purposes */ memo?: string; - /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + /** (Optional) Squads Program ID (defaults to Solana mainnet-beta/devnet Program ID) */ programId?: PublicKey; } -interface CreateVaultTransactionResult { - /** `vaultTransactionCreate` instruction */ - instructions: TransactionInstruction[]; +interface CreateVaultTransactionResult extends BuildResult { /** Transaction index of the resulting VaultTransaction */ index: number; } @@ -67,6 +75,19 @@ interface ExecuteVaultTransactionResult { lookupTableAccounts: AddressLookupTableAccount[]; } +interface ReclaimRentActionArgs { + /** The connection to an SVM network cluster */ + connection: Connection; + /** The public key of the multisig config account */ + multisig: PublicKey; + /** Transaction index of the VaultTransaction to execute */ + index: number; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + /** * Builds an instruction to create a new VaultTransaction and returns the instruction with the corresponding transaction index. * Can optionally chain additional methods for transactions, and sending. @@ -146,6 +167,21 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< this.index = result.index; } + /** + * Fetches deserialized account data for the corresponding `VaultTransaction` account after it is built and sent. + * + * @returns `VaultTransaction` + */ + async getTransactionAccount(key: PublicKey) { + this.ensureBuilt(); + const txAccount = await VaultTransaction.fromAccountAddress( + this.connection, + key + ); + + return txAccount; + } + /** * Creates a transaction containing the VaultTransaction creation instruction. * @args feePayer - Optional signer to pay the transaction fee. @@ -231,6 +267,56 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< return this; } + + async reclaimRent(): Promise< + Pick> + > { + const { instruction } = await reclaimRentCore({ + connection: this.connection, + multisig: this.args.multisig, + index: this.index, + programId: this.args.programId, + }); + + this.instructions.push(instruction); + return this; + } +} + +/** + * Creates a transaction builder instance from an existing `VaultTransaction` account key. + * @args `{ connection: Connection, transaction: PublicKey, programId?: PublicKey }` + * @returns `VaultTransactionBuilder` + */ +export async function buildFromVaultTransaction({ + connection, + transaction, + programId, +}: { + connection: Connection; + transaction: PublicKey; + programId?: PublicKey; +}) { + const txAccount = await VaultTransaction.fromAccountAddress( + connection, + transaction + ); + + const compiledMessage = Message.from( + vaultTransactionMessageBeet.serialize(txAccount.message)[0] + ); + + const message = TransactionMessage.decompile(compiledMessage); + + const builder = createVaultTransaction({ + connection, + multisig: txAccount.multisig, + creator: txAccount.creator, + message: message, + programId: programId, + }); + + return builder; } export async function isVaultTransaction( @@ -300,6 +386,31 @@ async function executeVaultTransactionCore( }; } +async function reclaimRentCore( + args: ReclaimRentActionArgs +): Promise { + const { connection, multisig, index, programId = PROGRAM_ID } = args; + const multisigInfo = await accounts.Multisig.fromAccountAddress( + connection, + multisig + ); + + if (!multisigInfo.rentCollector) { + throw new Error("No rent collector found in Multisig config."); + } + + const ix = instructions.vaultTransactionAccountsClose({ + multisigPda: multisig, + rentCollector: multisigInfo.rentCollector, + transactionIndex: BigInt(index), + programId: programId, + }); + + return { + instruction: ix, + }; +} + /* async function Example() { const connection = new Connection("https://api.mainnet-beta.solana.com"); diff --git a/sdk/multisig/src/actions/members.ts b/sdk/multisig/src/actions/members.ts index c9a138a7..975ebc81 100644 --- a/sdk/multisig/src/actions/members.ts +++ b/sdk/multisig/src/actions/members.ts @@ -1,5 +1,5 @@ import { PublicKey } from "@solana/web3.js"; -import { Permissions } from "../types"; +import { Member, Permissions } from "../types"; export enum SquadPermissions { Proposer = 1, @@ -11,6 +11,13 @@ export enum SquadPermissions { All = 7, } +export function createMember(member: { key: PublicKey; permissions: number }) { + return { + key: member.key, + permissions: Permissions.fromMask(member.permissions), + } as Member; +} + export function createMembers( members: { key: PublicKey; permissions: number }[] ) { @@ -19,5 +26,5 @@ export function createMembers( key: member.key, permissions: Permissions.fromMask(member.permissions), }; - }); + }) as Member[]; } diff --git a/tests/suites/examples/actions.ts b/tests/suites/examples/actions.ts index ff292d96..890de6ca 100644 --- a/tests/suites/examples/actions.ts +++ b/tests/suites/examples/actions.ts @@ -8,8 +8,16 @@ import { getTestProgramId, TestMembers, } from "../../utils"; -import { createMultisig, createVaultTransaction } from "@sqds/multisig"; +import { + createMultisig, + createVaultTransaction, + createConfigTransaction, + isConfigTransaction, + ConfigActions, +} from "@sqds/multisig"; import assert from "assert"; +import { SquadPermissions } from "@sqds/multisig"; +import { ConfigAction } from "@sqds/multisig/lib/generated"; const { Permission, Permissions } = multisig.types; @@ -20,15 +28,16 @@ describe("Examples / End2End Actions", () => { let multisigPda: PublicKey = PublicKey.default; let transactionPda: PublicKey | null = null; + let configTransactionPda: PublicKey | null = null; let members: TestMembers; let outsider: Keypair; + before(async () => { outsider = await generateFundedKeypair(connection); members = await generateMultisigMembers(connection); }); it("should create a multisig", async () => { - console.log("Creating a multisig with 2 members"); const builder = createMultisig({ connection, creator: members.almighty.publicKey, @@ -55,7 +64,7 @@ describe("Examples / End2End Actions", () => { }); }); - it("should create & send a vault transaction", async () => { + it("should create a vault transaction", async () => { const [vaultPda] = multisig.getVaultPda({ multisigPda: multisigPda, index: 0, @@ -99,6 +108,7 @@ describe("Examples / End2End Actions", () => { createTestTransferInstruction(vaultPda, outsider.publicKey), ], }); + const txBuilder = createVaultTransaction({ connection, multisig: multisigPda, @@ -107,8 +117,8 @@ describe("Examples / End2End Actions", () => { programId, }); - (await txBuilder.withProposal()).withApproval(members.almighty.publicKey); - // await txBuilder.withExecute(members.executor.publicKey); + await txBuilder.withProposal(); + txBuilder.withApproval(members.almighty.publicKey); await txBuilder.sendAndConfirm({ feePayer: members.almighty, @@ -119,4 +129,27 @@ describe("Examples / End2End Actions", () => { it("is this a vault transaction?", async () => { assert.ok(multisig.isVaultTransaction(connection, transactionPda!)); }); + + it("should create a config transaction", async () => { + const configBuilder = createConfigTransaction({ + connection, + multisig: multisigPda, + creator: members.proposer.publicKey, + actions: [ + ConfigActions.SetTimeLock(300), + ConfigActions.SetRentCollector(members.almighty.publicKey), + ], + programId, + }); + + await configBuilder.withProposal(); + configBuilder.withApproval(members.almighty.publicKey); + + configTransactionPda = configBuilder.getTransactionKey(); + + await configBuilder.sendAndConfirm({ + feePayer: members.proposer, + options: { preflightCommitment: "finalized" }, + }); + }); }); From 7850ae23be9f863c5fe2992d09fba5e5f33db221 Mon Sep 17 00:00:00 2001 From: Joey Meere <100378695+joeymeere@users.noreply.github.com> Date: Sat, 19 Oct 2024 15:17:26 -0400 Subject: [PATCH 6/9] wip: testing quirks --- package.json | 55 ++++----- sdk/multisig/src/actions/common.ts | 66 ++++++---- .../src/actions/createConfigTransaction.ts | 2 +- sdk/multisig/src/actions/createMultisig.ts | 9 ++ .../src/actions/createVaultTransaction.ts | 10 +- sdk/multisig/src/actions/members.ts | 13 +- tests/index.ts | 2 + tests/suites/examples/actions.ts | 114 ++++++++++++++---- 8 files changed, 185 insertions(+), 86 deletions(-) diff --git a/package.json b/package.json index 97c06d20..61f3c1c1 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,30 @@ { - "private": true, - "workspaces": [ - "sdk/*" - ], - "scripts": { - "build": "turbo run build", - "test": "turbo run build && anchor test -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"", - "pretest": "mkdir -p target/deploy && cp ./test-program-keypair.json ./target/deploy/squads_multisig_program-keypair.json", - "ts": "turbo run ts && yarn tsc --noEmit" - }, - "devDependencies": { - "@solana/spl-token": "*", - "@solana/spl-memo": "^0.2.3", - "@types/bn.js": "5.1.0", - "@types/mocha": "10.0.1", - "@types/node-fetch": "2.6.2", - "mocha": "10.2.0", - "prettier": "2.6.2", - "ts-node": "10.9.1", - "turbo": "1.6.3", - "typescript": "*" - }, - "resolutions": { - "@solana/web3.js": "1.70.3", - "@solana/spl-token": "0.3.6", - "typescript": "4.9.4" - } + "private": true, + "workspaces": [ + "sdk/*" + ], + "scripts": { + "build": "turbo run build", + "test": "turbo run build && anchor test -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"", + "test:detached": "turbo run build && anchor test --skip-local-validator -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"", + "pretest": "mkdir -p target/deploy && cp ./test-program-keypair.json ./target/deploy/squads_multisig_program-keypair.json", + "ts": "turbo run ts && yarn tsc --noEmit" + }, + "devDependencies": { + "@solana/spl-token": "*", + "@solana/spl-memo": "^0.2.3", + "@types/bn.js": "5.1.0", + "@types/mocha": "10.0.1", + "@types/node-fetch": "2.6.2", + "mocha": "10.2.0", + "prettier": "2.6.2", + "ts-node": "10.9.1", + "turbo": "1.6.3", + "typescript": "*" + }, + "resolutions": { + "@solana/web3.js": "1.70.3", + "@solana/spl-token": "0.3.6", + "typescript": "4.9.4" + } } diff --git a/sdk/multisig/src/actions/common.ts b/sdk/multisig/src/actions/common.ts index 0484c5aa..edc9a0fd 100644 --- a/sdk/multisig/src/actions/common.ts +++ b/sdk/multisig/src/actions/common.ts @@ -68,7 +68,8 @@ export abstract class BaseBuilder< protected abstract build(): Promise; - getInstructions(): TransactionInstruction[] { + async getInstructions(): Promise { + await this.ensureBuilt(); return this.instructions; } @@ -92,7 +93,12 @@ export abstract class BaseBuilder< * }).transaction(); * */ - async transaction(feePayer?: Signer): Promise { + async transaction( + /** (Optional) Fee paying signer keypair. Sufficient if only one signer is needed */ + feePayer?: Signer, + /** (Optional) Array of multiple signing keypairs. Used for if multiple signers are needed. */ + signers?: Signer[] + ): Promise { await this.ensureBuilt(); const message = new TransactionMessage({ payerKey: feePayer?.publicKey ?? this.creator, @@ -104,6 +110,9 @@ export abstract class BaseBuilder< if (feePayer) { tx.sign([feePayer]); } + if (signers) { + tx.sign([...signers]); + } return tx; } @@ -116,10 +125,15 @@ export abstract class BaseBuilder< * @returns `TransactionSignature` */ async send(settings?: { + /** (Optional) Extra instructions to prepend before specified builder instructions. */ preInstructions?: TransactionInstruction[]; + /** (Optional) Extra instructions to append after specified builder instructions. */ postInstructions?: TransactionInstruction[]; + /** (Optional) Fee paying signer keypair. Sufficient if only one signer is needed */ feePayer?: Signer; + /** (Optional) Array of multiple signing keypairs. Used for if multiple signers are needed. */ signers?: Signer[]; + /** (Optional) `SendOptions` object from web3.js. Defaults to `{ preflightCommitment: "finalized" }` */ options?: SendOptions; }): Promise { await this.ensureBuilt(); @@ -161,10 +175,15 @@ export abstract class BaseBuilder< * @returns `TransactionSignature` */ async sendAndConfirm(settings?: { + /** (Optional) Extra instructions to prepend before specified builder instructions. */ preInstructions?: TransactionInstruction[]; + /** (Optional) Extra instructions to append after specified builder instructions. */ postInstructions?: TransactionInstruction[]; + /** (Optional) Fee paying signer keypair. Sufficient if only one signer is needed */ feePayer?: Signer; + /** (Optional) Array of multiple signing keypairs. Used for if multiple signers are needed. */ signers?: Signer[]; + /** (Optional) `SendOptions` object from web3.js. Defaults to `{ preflightCommitment: "finalized" }` */ options?: SendOptions; }): Promise { await this.ensureBuilt(); @@ -196,17 +215,28 @@ export abstract class BaseBuilder< 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" - ) { + const maxAttempts = 10; + const delayMs = 1000; + for (let attempt = 0; attempt < maxAttempts && !sent; attempt++) { + const status = await this.connection.getSignatureStatus(signature); + console.log(status); + if (status?.value?.confirmationStatus === commitment || "finalized") { + console.log(status.value); + await new Promise((resolve) => setTimeout(resolve, delayMs)); sent = true; + } else { + console.log(status.value); + await new Promise((resolve) => setTimeout(resolve, delayMs)); } } - this.sent = true; + + if (!sent) { + throw new Error( + "Transaction was not confirmed within the expected timeframe" + ); + } + + console.log("Transaction confirmed!, Signature:", signature); return signature; } @@ -315,22 +345,6 @@ export abstract class BaseTransactionBuilder< 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( diff --git a/sdk/multisig/src/actions/createConfigTransaction.ts b/sdk/multisig/src/actions/createConfigTransaction.ts index ffd60e63..9a28f285 100644 --- a/sdk/multisig/src/actions/createConfigTransaction.ts +++ b/sdk/multisig/src/actions/createConfigTransaction.ts @@ -125,7 +125,7 @@ class ConfigTransactionBuilder extends BaseTransactionBuilder< * * @returns `ConfigTransaction` */ - async getTransactionAccount(key: PublicKey) { + async getTransactionAccount(key: PublicKey): Promise { this.ensureBuilt(); const txAccount = await ConfigTransaction.fromAccountAddress( this.connection, diff --git a/sdk/multisig/src/actions/createMultisig.ts b/sdk/multisig/src/actions/createMultisig.ts index eafc142e..de44c517 100644 --- a/sdk/multisig/src/actions/createMultisig.ts +++ b/sdk/multisig/src/actions/createMultisig.ts @@ -196,6 +196,15 @@ export async function createMultisigCore( }; } +export async function isMultisig(connection: Connection, key: PublicKey) { + try { + await Multisig.fromAccountAddress(connection, key); + return true; + } catch (err) { + return false; + } +} + async function Example() { const connection = new Connection("https://api.mainnet-beta.solana.com"); const feePayer = Keypair.generate(); diff --git a/sdk/multisig/src/actions/createVaultTransaction.ts b/sdk/multisig/src/actions/createVaultTransaction.ts index 0d41c998..3d284c4e 100644 --- a/sdk/multisig/src/actions/createVaultTransaction.ts +++ b/sdk/multisig/src/actions/createVaultTransaction.ts @@ -6,11 +6,9 @@ import { AddressLookupTableAccount, Message, } from "@solana/web3.js"; -import { instructions, accounts } from ".."; import { PROGRAM_ID, VaultTransaction, - multisigCompiledInstructionBeet, vaultTransactionMessageBeet, } from "../generated"; import { @@ -21,6 +19,7 @@ import { BuildResult, ProposalResult, } from "./common"; +import { instructions, accounts } from ".."; import { Methods } from "./actionTypes"; interface CreateVaultTransactionActionArgs { @@ -176,7 +175,8 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< this.ensureBuilt(); const txAccount = await VaultTransaction.fromAccountAddress( this.connection, - key + key, + "finalized" ); return txAccount; @@ -284,7 +284,7 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< } /** - * Creates a transaction builder instance from an existing `VaultTransaction` account key. + * WIP: Creates a transaction builder instance from an existing `VaultTransaction` account key. * @args `{ connection: Connection, transaction: PublicKey, programId?: PublicKey }` * @returns `VaultTransactionBuilder` */ @@ -324,7 +324,7 @@ export async function isVaultTransaction( key: PublicKey ) { try { - await VaultTransaction.fromAccountAddress(connection, key); + await VaultTransaction.fromAccountAddress(connection, key, "finalized"); return true; } catch (err) { return false; diff --git a/sdk/multisig/src/actions/members.ts b/sdk/multisig/src/actions/members.ts index 975ebc81..1576b494 100644 --- a/sdk/multisig/src/actions/members.ts +++ b/sdk/multisig/src/actions/members.ts @@ -1,5 +1,5 @@ import { PublicKey } from "@solana/web3.js"; -import { Member, Permissions } from "../types"; +import { Member, Permission, Permissions } from "../types"; export enum SquadPermissions { Proposer = 1, @@ -11,20 +11,23 @@ export enum SquadPermissions { All = 7, } -export function createMember(member: { key: PublicKey; permissions: number }) { +export function createMember(member: { + key: PublicKey; + permissions: SquadPermissions; +}) { return { key: member.key, - permissions: Permissions.fromMask(member.permissions), + permissions: Permissions.fromMask(member.permissions) as Permissions, } as Member; } export function createMembers( - members: { key: PublicKey; permissions: number }[] + members: { key: PublicKey; permissions: SquadPermissions }[] ) { return members.map((member) => { return { key: member.key, - permissions: Permissions.fromMask(member.permissions), + permissions: Permissions.fromMask(member.permissions) as Permissions, }; }) as Member[]; } diff --git a/tests/index.ts b/tests/index.ts index fa416199..c4fe908a 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,5 +1,6 @@ // The order of imports is the order the test suite will run in. import "./suites/program-config-init"; +/* import "./suites/instructions/multisigCreate"; import "./suites/instructions/multisigCreateV2"; import "./suites/instructions/multisigSetRentCollector"; @@ -14,4 +15,5 @@ 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 890de6ca..ebdb5e67 100644 --- a/tests/suites/examples/actions.ts +++ b/tests/suites/examples/actions.ts @@ -1,5 +1,10 @@ import * as multisig from "@sqds/multisig"; -import { PublicKey, TransactionMessage, Keypair } from "@solana/web3.js"; +import { + PublicKey, + TransactionMessage, + Keypair, + Connection, +} from "@solana/web3.js"; import { createLocalhostConnection, createTestTransferInstruction, @@ -12,23 +17,36 @@ import { createMultisig, createVaultTransaction, createConfigTransaction, - isConfigTransaction, ConfigActions, + createMembers, + buildFromVaultTransaction, + isVaultTransaction, + isMultisig, } from "@sqds/multisig"; import assert from "assert"; import { SquadPermissions } from "@sqds/multisig"; -import { ConfigAction } from "@sqds/multisig/lib/generated"; const { Permission, Permissions } = multisig.types; const programId = getTestProgramId(); +const getLogs = async ( + connection: Connection, + signature: string +): Promise => { + const tx = await connection.getTransaction(signature, { + commitment: "confirmed", + maxSupportedTransactionVersion: 0, + }); + return tx?.meta?.logMessages ?? null; +}; + describe("Examples / End2End Actions", () => { const connection = createLocalhostConnection(); let multisigPda: PublicKey = PublicKey.default; - let transactionPda: PublicKey | null = null; - let configTransactionPda: PublicKey | null = null; + let transactionPda: PublicKey; + let configTransactionPda: PublicKey; let members: TestMembers; let outsider: Keypair; @@ -41,16 +59,14 @@ describe("Examples / End2End Actions", () => { const builder = createMultisig({ connection, creator: members.almighty.publicKey, - members: [ - { - key: members.almighty.publicKey, - permissions: Permissions.all(), - }, + members: createMembers([ + { key: members.almighty.publicKey, permissions: SquadPermissions.All }, { - key: members.voter.publicKey, - permissions: Permissions.all(), + key: members.proposer.publicKey, + permissions: SquadPermissions.Proposer, }, - ], + { key: members.voter.publicKey, permissions: SquadPermissions.Voter }, + ]), threshold: 1, programId, }); @@ -58,10 +74,14 @@ describe("Examples / End2End Actions", () => { multisigPda = await builder.getMultisigKey(); const createKey = await builder.getCreateKey(); - await builder.sendAndConfirm({ + const signature = await builder.sendAndConfirm({ signers: [members.almighty, createKey], - options: { preflightCommitment: "finalized" }, + options: { skipPreflight: true, preflightCommitment: "finalized" }, }); + + const logs = await getLogs(connection, signature); + + console.log(logs); }); it("should create a vault transaction", async () => { @@ -90,10 +110,14 @@ describe("Examples / End2End Actions", () => { transactionPda = txBuilder.getTransactionKey(); - await txBuilder.sendAndConfirm({ + const signature = await txBuilder.sendAndConfirm({ feePayer: members.almighty, - options: { preflightCommitment: "finalized" }, + options: { skipPreflight: true, preflightCommitment: "finalized" }, }); + + const logs = await getLogs(connection, signature); + + console.log(logs); }); it("should create a vault transaction & vote", async () => { @@ -122,12 +146,38 @@ describe("Examples / End2End Actions", () => { await txBuilder.sendAndConfirm({ feePayer: members.almighty, - options: { preflightCommitment: "finalized" }, + options: { skipPreflight: true, preflightCommitment: "finalized" }, }); }); + it("is this a multisig?", async () => { + async function retryCheck(maxAttempts = 10, delayMs = 1000) { + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const get = await isMultisig(connection, multisigPda); + if (get) return true; + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + return false; + } + + const get = await retryCheck(); + + assert.ok(get); + }); + it("is this a vault transaction?", async () => { - assert.ok(multisig.isVaultTransaction(connection, transactionPda!)); + async function retryCheck(maxAttempts = 10, delayMs = 1000) { + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const get = await isVaultTransaction(connection, transactionPda); + if (get) return true; + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + return false; + } + + const get = await retryCheck(); + + assert.ok(get); }); it("should create a config transaction", async () => { @@ -142,13 +192,33 @@ describe("Examples / End2End Actions", () => { programId, }); + configTransactionPda = configBuilder.getTransactionKey(); + await configBuilder.withProposal(); configBuilder.withApproval(members.almighty.publicKey); - configTransactionPda = configBuilder.getTransactionKey(); - await configBuilder.sendAndConfirm({ - feePayer: members.proposer, + signers: [members.almighty, members.proposer], + options: { preflightCommitment: "finalized" }, + }); + }); + + it("should create a vault transaction builder from key", async () => { + const get = await isVaultTransaction(connection, transactionPda); + + assert.ok(get); + + const builder = await buildFromVaultTransaction({ + connection, + transaction: transactionPda, + programId, + }); + + await builder.withProposal(); + builder.withApproval(members.almighty.publicKey); + + const signature = await builder.sendAndConfirm({ + feePayer: members.almighty, options: { preflightCommitment: "finalized" }, }); }); From 5690d2efb954bfa75908061d5ff8ba9db5f5adda Mon Sep 17 00:00:00 2001 From: Joey Meere <100378695+joeymeere@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:13:32 -0400 Subject: [PATCH 7/9] all tests passing --- package.json | 2 +- sdk/multisig/src/actions/common.ts | 42 +++-- sdk/multisig/src/actions/createBatch.ts | 3 + .../src/actions/createVaultTransaction.ts | 47 +++--- tests/index.ts | 2 - tests/suites/examples/actions.ts | 149 +++++++++--------- 6 files changed, 128 insertions(+), 117 deletions(-) diff --git a/package.json b/package.json index 61f3c1c1..cc8e6b19 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "turbo run build", "test": "turbo run build && anchor test -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"", - "test:detached": "turbo run build && anchor test --skip-local-validator -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"", + "test:detached": "turbo run build && anchor test --detach -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"", "pretest": "mkdir -p target/deploy && cp ./test-program-keypair.json ./target/deploy/squads_multisig_program-keypair.json", "ts": "turbo run ts && yarn tsc --noEmit" }, diff --git a/sdk/multisig/src/actions/common.ts b/sdk/multisig/src/actions/common.ts index edc9a0fd..3df15461 100644 --- a/sdk/multisig/src/actions/common.ts +++ b/sdk/multisig/src/actions/common.ts @@ -1,4 +1,5 @@ import { + AddressLookupTableAccount, Connection, Keypair, PublicKey, @@ -94,6 +95,8 @@ export abstract class BaseBuilder< * */ async transaction( + /** (Optional) Any address lookup table accounts to be added to the transaction. */ + addressLookupTableAccounts?: AddressLookupTableAccount[], /** (Optional) Fee paying signer keypair. Sufficient if only one signer is needed */ feePayer?: Signer, /** (Optional) Array of multiple signing keypairs. Used for if multiple signers are needed. */ @@ -104,7 +107,7 @@ export abstract class BaseBuilder< payerKey: feePayer?.publicKey ?? this.creator, recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, instructions: [...this.instructions], - }).compileToV0Message(); + }).compileToV0Message(addressLookupTableAccounts); const tx = new VersionedTransaction(message); if (feePayer) { @@ -125,10 +128,14 @@ export abstract class BaseBuilder< * @returns `TransactionSignature` */ async send(settings?: { + /** (Optional) Clear all current instructions after sending, so subsequent actions can be done with the same builder. */ + clearInstructions?: boolean; /** (Optional) Extra instructions to prepend before specified builder instructions. */ preInstructions?: TransactionInstruction[]; /** (Optional) Extra instructions to append after specified builder instructions. */ postInstructions?: TransactionInstruction[]; + /** (Optional) Any address lookup table accounts to be added to the transaction. */ + addressLookupTableAccounts?: AddressLookupTableAccount[]; /** (Optional) Fee paying signer keypair. Sufficient if only one signer is needed */ feePayer?: Signer; /** (Optional) Array of multiple signing keypairs. Used for if multiple signers are needed. */ @@ -148,7 +155,7 @@ export abstract class BaseBuilder< payerKey: settings?.feePayer?.publicKey ?? this.creator, recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, instructions: [...instructions], - }).compileToV0Message(); + }).compileToV0Message(settings?.addressLookupTableAccounts); const tx = new VersionedTransaction(message); if (settings?.feePayer) { @@ -163,6 +170,10 @@ export abstract class BaseBuilder< ); this.sent = true; + if (settings?.clearInstructions) { + this.instructions = []; + } + return signature; } @@ -175,10 +186,14 @@ export abstract class BaseBuilder< * @returns `TransactionSignature` */ async sendAndConfirm(settings?: { + /** (Optional) Clear all current instructions after sending, so subsequent actions can be done with the same builder. */ + clearInstructions?: boolean; /** (Optional) Extra instructions to prepend before specified builder instructions. */ preInstructions?: TransactionInstruction[]; /** (Optional) Extra instructions to append after specified builder instructions. */ postInstructions?: TransactionInstruction[]; + /** (Optional) Any address lookup table accounts to be added to the transaction. */ + addressLookupTableAccounts?: AddressLookupTableAccount[]; /** (Optional) Fee paying signer keypair. Sufficient if only one signer is needed */ feePayer?: Signer; /** (Optional) Array of multiple signing keypairs. Used for if multiple signers are needed. */ @@ -198,7 +213,7 @@ export abstract class BaseBuilder< payerKey: settings?.feePayer?.publicKey ?? this.creator, recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, instructions: [...instructions], - }).compileToV0Message(); + }).compileToV0Message(settings?.addressLookupTableAccounts); const tx = new VersionedTransaction(message); if (settings?.feePayer) { @@ -219,13 +234,10 @@ export abstract class BaseBuilder< const delayMs = 1000; for (let attempt = 0; attempt < maxAttempts && !sent; attempt++) { const status = await this.connection.getSignatureStatus(signature); - console.log(status); - if (status?.value?.confirmationStatus === commitment || "finalized") { - console.log(status.value); + if (status?.value?.confirmationStatus === commitment || "confirmed") { await new Promise((resolve) => setTimeout(resolve, delayMs)); sent = true; } else { - console.log(status.value); await new Promise((resolve) => setTimeout(resolve, delayMs)); } } @@ -236,7 +248,9 @@ export abstract class BaseBuilder< ); } - console.log("Transaction confirmed!, Signature:", signature); + if (settings?.clearInstructions) { + this.instructions = []; + } return signature; } @@ -282,19 +296,11 @@ export abstract class BaseBuilder< 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 { multisig: PublicKey; + programId?: PublicKey; } export interface TransactionBuildResult extends BuildResult { @@ -325,6 +331,7 @@ export abstract class BaseTransactionBuilder< const [transactionPda] = getTransactionPda({ multisigPda: this.args.multisig, index: BigInt(index ?? 1), + programId: this.args.programId ?? PROGRAM_ID, }); return transactionPda; @@ -340,6 +347,7 @@ export abstract class BaseTransactionBuilder< const [proposalPda] = getProposalPda({ multisigPda: this.args.multisig, transactionIndex: BigInt(index ?? 1), + programId: this.args.programId ?? PROGRAM_ID, }); return proposalPda; diff --git a/sdk/multisig/src/actions/createBatch.ts b/sdk/multisig/src/actions/createBatch.ts index a1dda7ca..e3c1f2ae 100644 --- a/sdk/multisig/src/actions/createBatch.ts +++ b/sdk/multisig/src/actions/createBatch.ts @@ -169,6 +169,7 @@ class BatchBuilder extends BaseBuilder< const [batchPda] = getTransactionPda({ multisigPda: this.args.multisig, index: BigInt(index ?? 1), + programId: this.args.programId ?? PROGRAM_ID, }); return batchPda; @@ -181,6 +182,7 @@ class BatchBuilder extends BaseBuilder< multisigPda: this.args.multisig, batchIndex: BigInt(index ?? 1), transactionIndex: innerIndex ?? this.innerIndex, + programId: this.args.programId ?? PROGRAM_ID, }); return batchPda; @@ -195,6 +197,7 @@ class BatchBuilder extends BaseBuilder< multisigPda: this.args.multisig, batchIndex: BigInt(index ?? 1), transactionIndex: i, + programId: this.args.programId ?? PROGRAM_ID, }); transactions.push(batchPda); diff --git a/sdk/multisig/src/actions/createVaultTransaction.ts b/sdk/multisig/src/actions/createVaultTransaction.ts index 3d284c4e..8371c82e 100644 --- a/sdk/multisig/src/actions/createVaultTransaction.ts +++ b/sdk/multisig/src/actions/createVaultTransaction.ts @@ -134,7 +134,7 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< > { public instructions: TransactionInstruction[] = []; public addressLookupTableAccounts: AddressLookupTableAccount[] = []; - public index: number = 1; + static index: number; constructor(args: CreateVaultTransactionActionArgs) { super(args); @@ -150,17 +150,20 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< memo, programId = PROGRAM_ID, } = this.args; - const result = await createVaultTransactionCore({ - connection: this.connection, - multisig, - creator: this.creator, - message, - vaultIndex, - ephemeralSigners, - rentPayer, - memo, - programId, - }); + const result = await createVaultTransactionCore( + { + connection: this.connection, + multisig, + creator: this.creator, + message, + vaultIndex, + ephemeralSigners, + rentPayer, + memo, + programId, + }, + this.index + ); this.instructions = [...result.instructions]; this.index = result.index; @@ -175,8 +178,7 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< this.ensureBuilt(); const txAccount = await VaultTransaction.fromAccountAddress( this.connection, - key, - "finalized" + key ); return txAccount; @@ -288,6 +290,7 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< * @args `{ connection: Connection, transaction: PublicKey, programId?: PublicKey }` * @returns `VaultTransactionBuilder` */ +/* export async function buildFromVaultTransaction({ connection, transaction, @@ -318,13 +321,14 @@ export async function buildFromVaultTransaction({ return builder; } +*/ export async function isVaultTransaction( connection: Connection, key: PublicKey ) { try { - await VaultTransaction.fromAccountAddress(connection, key, "finalized"); + await VaultTransaction.fromAccountAddress(connection, key); return true; } catch (err) { return false; @@ -332,7 +336,8 @@ export async function isVaultTransaction( } async function createVaultTransactionCore( - args: CreateVaultTransactionActionArgs + args: CreateVaultTransactionActionArgs, + transactionIndex?: number ): Promise { const { connection, @@ -350,9 +355,13 @@ async function createVaultTransactionCore( connection, multisig ); - - const currentTransactionIndex = Number(multisigInfo.transactionIndex); - const index = BigInt(currentTransactionIndex + 1); + let index; + if (transactionIndex) { + index = BigInt(transactionIndex); + } else { + const currentTransactionIndex = Number(multisigInfo.transactionIndex); + index = BigInt(currentTransactionIndex + 1); + } const ix = instructions.vaultTransactionCreate({ multisigPda: multisig, diff --git a/tests/index.ts b/tests/index.ts index c4fe908a..fa416199 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,6 +1,5 @@ // The order of imports is the order the test suite will run in. import "./suites/program-config-init"; -/* import "./suites/instructions/multisigCreate"; import "./suites/instructions/multisigCreateV2"; import "./suites/instructions/multisigSetRentCollector"; @@ -15,5 +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 ebdb5e67..4af878a5 100644 --- a/tests/suites/examples/actions.ts +++ b/tests/suites/examples/actions.ts @@ -4,6 +4,8 @@ import { TransactionMessage, Keypair, Connection, + LAMPORTS_PER_SOL, + TransactionInstruction, } from "@solana/web3.js"; import { createLocalhostConnection, @@ -19,15 +21,13 @@ import { createConfigTransaction, ConfigActions, createMembers, - buildFromVaultTransaction, isVaultTransaction, + isConfigTransaction, isMultisig, } from "@sqds/multisig"; import assert from "assert"; import { SquadPermissions } from "@sqds/multisig"; -const { Permission, Permissions } = multisig.types; - const programId = getTestProgramId(); const getLogs = async ( @@ -66,6 +66,10 @@ describe("Examples / End2End Actions", () => { permissions: SquadPermissions.Proposer, }, { key: members.voter.publicKey, permissions: SquadPermissions.Voter }, + { + key: members.executor.publicKey, + permissions: SquadPermissions.Executor, + }, ]), threshold: 1, programId, @@ -76,12 +80,15 @@ describe("Examples / End2End Actions", () => { const signature = await builder.sendAndConfirm({ signers: [members.almighty, createKey], - options: { skipPreflight: true, preflightCommitment: "finalized" }, }); - const logs = await getLogs(connection, signature); + const [vaultPda] = multisig.getVaultPda({ + multisigPda: multisigPda, + index: 0, + }); + await connection.requestAirdrop(vaultPda, 10 * LAMPORTS_PER_SOL); - console.log(logs); + assert.ok(signature); }); it("should create a vault transaction", async () => { @@ -112,114 +119,100 @@ describe("Examples / End2End Actions", () => { const signature = await txBuilder.sendAndConfirm({ feePayer: members.almighty, - options: { skipPreflight: true, preflightCommitment: "finalized" }, }); - const logs = await getLogs(connection, signature); - - console.log(logs); + assert.ok(signature); }); - it("should create a vault transaction & vote", 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({ + it("should create a config transaction", async () => { + const configBuilder = createConfigTransaction({ connection, multisig: multisigPda, - creator: members.almighty.publicKey, - message, + creator: members.proposer.publicKey, + actions: [ + ConfigActions.SetTimeLock(300), + ConfigActions.SetRentCollector(members.almighty.publicKey), + ], programId, }); - await txBuilder.withProposal(); - txBuilder.withApproval(members.almighty.publicKey); + await configBuilder.withProposal(); - await txBuilder.sendAndConfirm({ - feePayer: members.almighty, - options: { skipPreflight: true, preflightCommitment: "finalized" }, + configTransactionPda = configBuilder.getTransactionKey(); + + const signature = await configBuilder.sendAndConfirm({ + signers: [members.proposer], }); + + assert.ok(signature); }); it("is this a multisig?", async () => { - async function retryCheck(maxAttempts = 10, delayMs = 1000) { - for (let attempt = 0; attempt < maxAttempts; attempt++) { - const get = await isMultisig(connection, multisigPda); - if (get) return true; - await new Promise((resolve) => setTimeout(resolve, delayMs)); - } - return false; - } - - const get = await retryCheck(); + const get = await isMultisig(connection, multisigPda); assert.ok(get); }); it("is this a vault transaction?", async () => { - async function retryCheck(maxAttempts = 10, delayMs = 1000) { - for (let attempt = 0; attempt < maxAttempts; attempt++) { - const get = await isVaultTransaction(connection, transactionPda); - if (get) return true; - await new Promise((resolve) => setTimeout(resolve, delayMs)); - } - return false; - } + const get = await isVaultTransaction(connection, transactionPda); + + assert.ok(get); + }); - const get = await retryCheck(); + it("is this a config transaction?", async () => { + const get = await isConfigTransaction(connection, configTransactionPda); assert.ok(get); }); - it("should create a config transaction", async () => { - const configBuilder = createConfigTransaction({ + it("should create, vote on & execute a vault transaction", async () => { + const message = new TransactionMessage({ + payerKey: members.almighty.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ + new TransactionInstruction({ + keys: [ + { + pubkey: members.almighty.publicKey, + isSigner: true, + isWritable: true, + }, + ], + data: Buffer.from("Hello from the action builder!", "utf-8"), + programId: new PublicKey( + "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" + ), + }), + ], + }); + + const txBuilder = createVaultTransaction({ connection, multisig: multisigPda, - creator: members.proposer.publicKey, - actions: [ - ConfigActions.SetTimeLock(300), - ConfigActions.SetRentCollector(members.almighty.publicKey), - ], + creator: members.almighty.publicKey, + message, programId, }); - configTransactionPda = configBuilder.getTransactionKey(); - - await configBuilder.withProposal(); - configBuilder.withApproval(members.almighty.publicKey); + await txBuilder.withProposal(); + txBuilder.withApproval(members.almighty.publicKey); - await configBuilder.sendAndConfirm({ - signers: [members.almighty, members.proposer], + const signature = await txBuilder.sendAndConfirm({ + signers: [members.almighty], + clearInstructions: true, options: { preflightCommitment: "finalized" }, }); - }); - it("should create a vault transaction builder from key", async () => { - const get = await isVaultTransaction(connection, transactionPda); + assert.ok(signature); - assert.ok(get); + await txBuilder.withExecute(members.almighty.publicKey); - const builder = await buildFromVaultTransaction({ - connection, - transaction: transactionPda, - programId, + const signature2 = await txBuilder.sendAndConfirm({ + signers: [members.almighty], + addressLookupTableAccounts: txBuilder.addressLookupTableAccounts, + options: { skipPreflight: true }, }); - await builder.withProposal(); - builder.withApproval(members.almighty.publicKey); - - const signature = await builder.sendAndConfirm({ - feePayer: members.almighty, - options: { preflightCommitment: "finalized" }, - }); + assert.ok(signature2); }); }); From 291100e3c16b3f92176178945a5b0cf47c388c5b Mon Sep 17 00:00:00 2001 From: Joey Meere <100378695+joeymeere@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:50:20 -0400 Subject: [PATCH 8/9] feat(test): refactor + add granular tests --- sdk/multisig/src/actions/actionTypes.ts | 110 ---- sdk/multisig/src/actions/common.ts | 453 ----------------- sdk/multisig/src/actions/common/base.ts | 289 +++++++++++ .../src/actions/common/baseTransaction.ts | 71 +++ sdk/multisig/src/actions/common/multisig.ts | 50 ++ sdk/multisig/src/actions/common/proposal.ts | 63 +++ .../src/actions/common/transaction.ts | 235 +++++++++ sdk/multisig/src/actions/common/types.ts | 463 +++++++++++++++++ sdk/multisig/src/actions/createBatch.ts | 291 ++++------- .../src/actions/createConfigTransaction.ts | 209 ++------ sdk/multisig/src/actions/createMultisig.ts | 135 ++--- .../src/actions/createVaultTransaction.ts | 313 ++---------- sdk/multisig/src/actions/index.ts | 6 +- tests/suites/examples/actions.ts | 472 +++++++++++++++++- 14 files changed, 1848 insertions(+), 1312 deletions(-) delete mode 100644 sdk/multisig/src/actions/actionTypes.ts delete mode 100644 sdk/multisig/src/actions/common.ts create mode 100644 sdk/multisig/src/actions/common/base.ts create mode 100644 sdk/multisig/src/actions/common/baseTransaction.ts create mode 100644 sdk/multisig/src/actions/common/multisig.ts create mode 100644 sdk/multisig/src/actions/common/proposal.ts create mode 100644 sdk/multisig/src/actions/common/transaction.ts create mode 100644 sdk/multisig/src/actions/common/types.ts diff --git a/sdk/multisig/src/actions/actionTypes.ts b/sdk/multisig/src/actions/actionTypes.ts deleted file mode 100644 index 3f1948a5..00000000 --- a/sdk/multisig/src/actions/actionTypes.ts +++ /dev/null @@ -1,110 +0,0 @@ -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 BaseSendKeys = "send" | "sendAndConfirm" | "customSend"; - -// TODO: Split between sync and async getters. -type TransactionGetKeys = - | "getIndex" - | "getInstructions" - | "getTransactionKey" - | "getProposalKey" - | "getTransactionAccount" - | "getProposalAccount"; - -type TransactionActionKeys = - | "withProposal" - | "withApproval" - | "withRejection" - | "withExecute"; - -// TODO: Split between sync and async getters. -type BatchGetKeys = - | "getInstructions" - | "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; - reclaimRent: 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 deleted file mode 100644 index 3df15461..00000000 --- a/sdk/multisig/src/actions/common.ts +++ /dev/null @@ -1,453 +0,0 @@ -import { - AddressLookupTableAccount, - Connection, - Keypair, - PublicKey, - SendOptions, - Signer, - TransactionInstruction, - TransactionMessage, - TransactionSignature, - VersionedTransaction, -} from "@solana/web3.js"; -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; -} - -export interface BuildResult { - instructions: TransactionInstruction[]; -} - -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 args: Omit; - private built: boolean = false; - // Use this as an indicator to clear all instructions? - private sent: boolean = false; - - constructor(args: U) { - this.connection = args.connection; - this.creator = args.creator; - this.args = this.extractAdditionalArgs(args); - 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 { - const { connection, creator, ...additionalArgs } = args; - return additionalArgs; - } - - protected abstract build(): Promise; - - async getInstructions(): Promise { - await this.ensureBuilt(); - return this.instructions; - } - - /** - * Creates a transaction containing the corresponding instruction(s). - * - * @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( - /** (Optional) Any address lookup table accounts to be added to the transaction. */ - addressLookupTableAccounts?: AddressLookupTableAccount[], - /** (Optional) Fee paying signer keypair. Sufficient if only one signer is needed */ - feePayer?: Signer, - /** (Optional) Array of multiple signing keypairs. Used for if multiple signers are needed. */ - signers?: Signer[] - ): Promise { - await this.ensureBuilt(); - const message = new TransactionMessage({ - payerKey: feePayer?.publicKey ?? this.creator, - recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, - instructions: [...this.instructions], - }).compileToV0Message(addressLookupTableAccounts); - - const tx = new VersionedTransaction(message); - if (feePayer) { - tx.sign([feePayer]); - } - if (signers) { - tx.sign([...signers]); - } - return tx; - } - - /** - * Builds a transaction with the corresponding instruction(s), and sends it. - * - * **NOTE: Not wallet-adapter compatible.** - * - * @args `settings` - Optional pre/post instructions, fee payer keypair, and send options. - * @returns `TransactionSignature` - */ - async send(settings?: { - /** (Optional) Clear all current instructions after sending, so subsequent actions can be done with the same builder. */ - clearInstructions?: boolean; - /** (Optional) Extra instructions to prepend before specified builder instructions. */ - preInstructions?: TransactionInstruction[]; - /** (Optional) Extra instructions to append after specified builder instructions. */ - postInstructions?: TransactionInstruction[]; - /** (Optional) Any address lookup table accounts to be added to the transaction. */ - addressLookupTableAccounts?: AddressLookupTableAccount[]; - /** (Optional) Fee paying signer keypair. Sufficient if only one signer is needed */ - feePayer?: Signer; - /** (Optional) Array of multiple signing keypairs. Used for if multiple signers are needed. */ - signers?: Signer[]; - /** (Optional) `SendOptions` object from web3.js. Defaults to `{ preflightCommitment: "finalized" }` */ - options?: SendOptions; - }): Promise { - 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(settings?.addressLookupTableAccounts); - - 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 - ); - this.sent = true; - - if (settings?.clearInstructions) { - this.instructions = []; - } - - return signature; - } - - /** - * Builds a transaction with the corresponding instruction(s), sends it, and confirms the transaction. - * - * **NOTE: Not wallet-adapter compatible.** - * - * @args `settings` - Optional pre/post instructions, fee payer keypair, and send options. - * @returns `TransactionSignature` - */ - async sendAndConfirm(settings?: { - /** (Optional) Clear all current instructions after sending, so subsequent actions can be done with the same builder. */ - clearInstructions?: boolean; - /** (Optional) Extra instructions to prepend before specified builder instructions. */ - preInstructions?: TransactionInstruction[]; - /** (Optional) Extra instructions to append after specified builder instructions. */ - postInstructions?: TransactionInstruction[]; - /** (Optional) Any address lookup table accounts to be added to the transaction. */ - addressLookupTableAccounts?: AddressLookupTableAccount[]; - /** (Optional) Fee paying signer keypair. Sufficient if only one signer is needed */ - feePayer?: Signer; - /** (Optional) Array of multiple signing keypairs. Used for if multiple signers are needed. */ - signers?: Signer[]; - /** (Optional) `SendOptions` object from web3.js. Defaults to `{ preflightCommitment: "finalized" }` */ - options?: SendOptions; - }): Promise { - 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(settings?.addressLookupTableAccounts); - - 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; - const maxAttempts = 10; - const delayMs = 1000; - for (let attempt = 0; attempt < maxAttempts && !sent; attempt++) { - const status = await this.connection.getSignatureStatus(signature); - if (status?.value?.confirmationStatus === commitment || "confirmed") { - await new Promise((resolve) => setTimeout(resolve, delayMs)); - sent = true; - } else { - await new Promise((resolve) => setTimeout(resolve, delayMs)); - } - } - - if (!sent) { - throw new Error( - "Transaction was not confirmed within the expected timeframe" - ); - } - - if (settings?.clearInstructions) { - this.instructions = []; - } - - return signature; - } - - /** - * 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 { - await this.ensureBuilt(); - const message = new TransactionMessage({ - payerKey: this.creator, - recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, - instructions: [...this.instructions], - }); - - const signature = await callback(message); - this.sent = true; - - return signature; - } -} - -export interface TransactionBuilderArgs extends BaseBuilderArgs { - multisig: PublicKey; - programId?: 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), - programId: this.args.programId ?? PROGRAM_ID, - }); - - 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), - programId: this.args.programId ?? PROGRAM_ID, - }); - - return proposalPda; - } - - 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; - /** The public key of the creator */ - creator: PublicKey; - /** Transaction index of the resulting Proposal */ - transactionIndex: number; - /** The public key of the fee payer, defaults to the creator */ - rentPayer?: PublicKey; - /** Whether the proposal should be initialized with status `Draft`. */ - isDraft?: boolean; - /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ - programId?: PublicKey; -} - -export interface VoteActionArgs { - /** The public key of the multisig config account */ - multisig: PublicKey; - /** The public key of the approving member */ - member: PublicKey; - /** Transaction index of the resulting Proposal */ - transactionIndex: number; - /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ - programId?: PublicKey; -} - -export interface ProposalResult { - /** `proposalCreate` instruction */ - instruction: TransactionInstruction; -} - -export function createProposalCore( - args: CreateProposalActionArgs -): ProposalResult { - const { - multisig, - creator, - transactionIndex, - rentPayer, - isDraft = false, - programId = PROGRAM_ID, - } = args; - - const ix = instructions.proposalCreate({ - multisigPda: multisig, - transactionIndex: BigInt(transactionIndex), - creator: creator, - isDraft, - rentPayer, - programId: programId, - }); - - return { - instruction: ix, - }; -} - -export function createApprovalCore(args: VoteActionArgs): ProposalResult { - const { multisig, member, transactionIndex, programId = PROGRAM_ID } = args; - - const ix = instructions.proposalApprove({ - multisigPda: multisig, - member: member, - transactionIndex: BigInt(transactionIndex), - programId: programId, - }); - - return { - instruction: ix, - }; -} - -export function createRejectionCore(args: VoteActionArgs): ProposalResult { - const { multisig, member, transactionIndex, programId = PROGRAM_ID } = args; - - const ix = instructions.proposalReject({ - multisigPda: multisig, - member: member, - transactionIndex: BigInt(transactionIndex), - programId: programId, - }); - - return { - instruction: ix, - }; -} diff --git a/sdk/multisig/src/actions/common/base.ts b/sdk/multisig/src/actions/common/base.ts new file mode 100644 index 00000000..d56d9899 --- /dev/null +++ b/sdk/multisig/src/actions/common/base.ts @@ -0,0 +1,289 @@ +import { + Connection, + Keypair, + PublicKey, + TransactionInstruction, + TransactionMessage, + TransactionSignature, + VersionedTransaction, +} from "@solana/web3.js"; +import { + BaseBuilderArgs, + BuildResult, + BuildTransactionSettings, + SendSettings, +} from "./types"; + +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 args: Omit; + private built: boolean = false; + // Use this as an indicator to clear all instructions? + private sent: boolean = false; + + constructor(args: U, options: { generateCreateKey?: boolean } = {}) { + this.connection = args.connection; + this.creator = args.creator; + this.args = this.extractAdditionalArgs(args); + if (options.generateCreateKey) { + 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 { + const { connection, creator, ...additionalArgs } = args; + return additionalArgs; + } + + protected abstract build(): Promise; + + /** + * Fetches built instructions. Will always contain at least one instruction corresponding to + * the builder you are using, unless cleared after sending. + * @returns `Promise` - An array of built instructions. + */ + async getInstructions(): Promise { + await this.ensureBuilt(); + return this.instructions; + } + + /** + * Creates a `VersionedTransaction` containing the corresponding instruction(s). + * + * @args `BuildTransactionSettings` - **(Optional)** Address Lookup Table accounts, signers, a custom fee-payer to add to the transaction. + * @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( + settings?: BuildTransactionSettings + ): Promise { + await this.ensureBuilt(); + const message = new TransactionMessage({ + payerKey: settings?.feePayer?.publicKey ?? this.creator, + recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, + instructions: [...this.instructions], + }).compileToV0Message(settings?.addressLookupTableAccounts); + + const tx = new VersionedTransaction(message); + if (settings?.feePayer) { + tx.sign([settings?.feePayer]); + } + if (settings?.signers) { + tx.sign([...settings?.signers]); + } + return tx; + } + + /** + * Builds a transaction with the corresponding instruction(s), and sends it. + * + * **NOTE: Not wallet-adapter compatible.** + * + * @args `settings` - Optional pre/post instructions, fee payer keypair, and send options. + * @returns `TransactionSignature` + * @example + * const builder = createMultisig({ + * // ... args + * }); + * const signature = await builder.send(); + * @example + * const builder = createMultisig({ + * // ... args + * }); + * + * // With settings + * const signature = await builder.send({ + * preInstructions: [...preInstructions], + * postInstructions: [...postInstructions], + * feePayer: someKeypair, + * options: { skipPreflight: true }, + * }); + */ + async send(settings?: SendSettings): Promise { + 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(settings?.addressLookupTableAccounts); + + 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 + ); + this.sent = true; + + if (settings?.clearInstructions) { + this.instructions = []; + } + + return signature; + } + + /** + * Builds a transaction with the corresponding instruction(s), sends it, and confirms the transaction. + * + * **NOTE: Not wallet-adapter compatible.** + * + * @args `settings` - Optional pre/post instructions, fee payer keypair, and send options. + * @returns `TransactionSignature` + * @example + * const builder = createMultisig({ + * // ... args + * }); + * const signature = await builder.sendAndConfirm(); + * @example + * const builder = createMultisig({ + * // ... args + * }); + * + * // With settings + * const signature = await builder.sendAndConfirm({ + * preInstructions: [...preInstructions], + * postInstructions: [...postInstructions], + * feePayer: someKeypair, + * options: { skipPreflight: true }, + * }); + */ + async sendAndConfirm(settings?: SendSettings): Promise { + 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(settings?.addressLookupTableAccounts); + + 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; + const maxAttempts = 10; + const delayMs = 1000; + for (let attempt = 0; attempt < maxAttempts && !sent; attempt++) { + const status = await this.connection.getSignatureStatus(signature); + if (status?.value?.confirmationStatus === commitment || "confirmed") { + await new Promise((resolve) => setTimeout(resolve, delayMs)); + sent = true; + } else { + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } + + if (!sent) { + throw new Error( + "Transaction was not confirmed within the expected timeframe" + ); + } + + if (settings?.clearInstructions) { + this.instructions = []; + } + + return signature; + } + + /** + * 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 { + await this.ensureBuilt(); + const message = new TransactionMessage({ + payerKey: this.creator, + recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, + instructions: [...this.instructions], + }); + + const signature = await callback(message); + this.sent = true; + + return signature; + } +} diff --git a/sdk/multisig/src/actions/common/baseTransaction.ts b/sdk/multisig/src/actions/common/baseTransaction.ts new file mode 100644 index 00000000..13a7602c --- /dev/null +++ b/sdk/multisig/src/actions/common/baseTransaction.ts @@ -0,0 +1,71 @@ +import { PublicKey } from "@solana/web3.js"; +import { TransactionBuildResult, TransactionBuilderArgs } from "./types"; +import { PROGRAM_ID, Proposal } from "../../generated"; +import { getProposalPda, getTransactionPda } from "../../pda"; +import { BaseBuilder } from "./base"; + +export abstract class BaseTransactionBuilder< + T extends TransactionBuildResult, + U extends TransactionBuilderArgs +> extends BaseBuilder { + public index: number = 1; + public vaultIndex: number = 0; + + constructor(args: U) { + super(args); + } + + async getIndex(): Promise { + await this.ensureBuilt(); + return this.index; + } + + /** + * Fetches the `PublicKey` of the corresponding account for the transaction being built. + * + * @returns `PublicKey` + */ + async getTransactionKey(): Promise { + await this.ensureBuilt(); + const index = this.index; + const [transactionPda] = getTransactionPda({ + multisigPda: this.args.multisig, + index: BigInt(index ?? 1), + programId: this.args.programId ?? PROGRAM_ID, + }); + + 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), + programId: this.args.programId ?? PROGRAM_ID, + }); + + return proposalPda; + } + + /** + * Fetches and deserializes the `Proposal` account after it is built and sent. + * @args `key` - The public key of the `Proposal` account. + * @returns `Proposal` - Deserialized `Proposal` account data. + */ + async getProposalAccount(key: PublicKey) { + return this.buildPromise.then(async () => { + const propAccount = await Proposal.fromAccountAddress( + this.connection, + key + ); + + return propAccount; + }); + } +} diff --git a/sdk/multisig/src/actions/common/multisig.ts b/sdk/multisig/src/actions/common/multisig.ts new file mode 100644 index 00000000..1db42c18 --- /dev/null +++ b/sdk/multisig/src/actions/common/multisig.ts @@ -0,0 +1,50 @@ +import { Keypair } from "@solana/web3.js"; +import { instructions } from "../.."; +import { PROGRAM_ID, ProgramConfig } from "../../generated"; +import { getMultisigPda, getProgramConfigPda } from "../../pda"; +import { CreateMultisigActionArgs, CreateMultisigResult } from "./types"; + +export async function createMultisigCore( + args: CreateMultisigActionArgs, + createKey: Keypair +): Promise { + const { + connection, + creator, + threshold, + members, + timeLock = 0, + configAuthority, + rentCollector, + programId = PROGRAM_ID, + } = args; + + const [multisigPda] = getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + const programConfigPda = getProgramConfigPda({ programId })[0]; + + const programConfig = await ProgramConfig.fromAccountAddress( + connection, + programConfigPda + ); + + const ix = instructions.multisigCreateV2({ + creator, + threshold, + members, + multisigPda: multisigPda, + treasury: programConfig.treasury, + createKey: createKey.publicKey, + timeLock: timeLock ?? 0, + rentCollector: rentCollector ?? null, + configAuthority: configAuthority ?? null, + programId: programId ?? PROGRAM_ID, + }); + + return { + instructions: [ix], + multisigKey: multisigPda, + }; +} diff --git a/sdk/multisig/src/actions/common/proposal.ts b/sdk/multisig/src/actions/common/proposal.ts new file mode 100644 index 00000000..e0b742c7 --- /dev/null +++ b/sdk/multisig/src/actions/common/proposal.ts @@ -0,0 +1,63 @@ +import { instructions } from "../.."; +import { PROGRAM_ID } from "../../generated"; +import { + CreateProposalActionArgs, + ProposalResult, + VoteActionArgs, +} from "./types"; + +export function createProposalCore( + args: CreateProposalActionArgs +): ProposalResult { + const { + multisig, + creator, + transactionIndex, + rentPayer, + isDraft = false, + programId = PROGRAM_ID, + } = args; + + const ix = instructions.proposalCreate({ + multisigPda: multisig, + transactionIndex: BigInt(transactionIndex), + creator: creator, + isDraft, + rentPayer, + programId: programId, + }); + + return { + instruction: ix, + }; +} + +export function createApprovalCore(args: VoteActionArgs): ProposalResult { + const { multisig, member, transactionIndex, programId = PROGRAM_ID } = args; + + const ix = instructions.proposalApprove({ + multisigPda: multisig, + member: member, + transactionIndex: BigInt(transactionIndex), + programId: programId, + }); + + return { + instruction: ix, + }; +} + +export function createRejectionCore(args: VoteActionArgs): ProposalResult { + const { multisig, member, transactionIndex, programId = PROGRAM_ID } = args; + + const ix = instructions.proposalReject({ + multisigPda: multisig, + member: member, + transactionIndex: BigInt(transactionIndex), + programId: programId, + }); + + return { + instruction: ix, + }; +} diff --git a/sdk/multisig/src/actions/common/transaction.ts b/sdk/multisig/src/actions/common/transaction.ts new file mode 100644 index 00000000..be7ad91f --- /dev/null +++ b/sdk/multisig/src/actions/common/transaction.ts @@ -0,0 +1,235 @@ +import { PROGRAM_ID, accounts, instructions } from "../.."; +import { + BatchAddTransactionActionArgs, + BatchAddTransactionResult, + CreateBatchActionArgs, + CreateBatchResult, + CreateConfigTransactionActionArgs, + CreateConfigTransactionResult, + CreateVaultTransactionActionArgs, + CreateVaultTransactionResult, + ExecuteBatchActionArgs, + ExecuteBatchResult, + ExecuteConfigTransactionActionArgs, + ExecuteConfigTransactionResult, + ExecuteVaultTransactionActionArgs, + ExecuteVaultTransactionResult, + ProposalResult, + ReclaimRentActionArgs, +} from "./types"; + +//region VaultTransaction +export async function createVaultTransactionCore( + args: CreateVaultTransactionActionArgs, + transactionIndex?: number +): Promise { + const { + connection, + multisig, + creator, + message, + vaultIndex = 0, + ephemeralSigners = 0, + rentPayer = creator, + memo, + programId = PROGRAM_ID, + } = args; + + const multisigInfo = await accounts.Multisig.fromAccountAddress( + connection, + multisig + ); + let index; + if (transactionIndex) { + index = BigInt(transactionIndex); + } else { + const currentTransactionIndex = Number(multisigInfo.transactionIndex); + index = BigInt(currentTransactionIndex + 1); + } + + const ix = instructions.vaultTransactionCreate({ + multisigPda: multisig, + transactionIndex: index, + creator: creator, + vaultIndex: vaultIndex, + ephemeralSigners: ephemeralSigners, + transactionMessage: message, + memo: memo, + rentPayer, + programId: programId, + }); + + return { instructions: [ix], index: Number(index) }; +} + +export async function executeVaultTransactionCore( + args: ExecuteVaultTransactionActionArgs +): Promise { + const { connection, multisig, index, member, programId = PROGRAM_ID } = args; + const ix = await instructions.vaultTransactionExecute({ + connection, + multisigPda: multisig, + member: member, + transactionIndex: BigInt(index), + programId: programId, + }); + + return { + ...ix, + }; +} + +export async function reclaimRentCore( + args: ReclaimRentActionArgs +): Promise { + const { connection, multisig, index, programId = PROGRAM_ID } = args; + const multisigInfo = await accounts.Multisig.fromAccountAddress( + connection, + multisig + ); + + if (!multisigInfo.rentCollector) { + throw new Error("No rent collector found in Multisig config."); + } + + const ix = instructions.vaultTransactionAccountsClose({ + multisigPda: multisig, + rentCollector: multisigInfo.rentCollector, + transactionIndex: BigInt(index), + programId: programId, + }); + + return { + instruction: ix, + }; +} +//endregion + +//region ConfigTransaction +export async function createConfigTransactionCore( + args: CreateConfigTransactionActionArgs +): Promise { + const { + connection, + multisig, + creator, + actions, + rentPayer = creator, + memo, + programId = PROGRAM_ID, + } = args; + + const multisigInfo = await accounts.Multisig.fromAccountAddress( + connection, + multisig + ); + + const currentTransactionIndex = Number(multisigInfo.transactionIndex); + const index = BigInt(currentTransactionIndex + 1); + + const ix = instructions.configTransactionCreate({ + multisigPda: multisig, + transactionIndex: index, + creator: creator, + actions, + memo: memo, + rentPayer, + programId: programId, + }); + + return { instructions: [ix], index: Number(index) }; +} + +export async function executeConfigTransactionCore( + args: ExecuteConfigTransactionActionArgs +): Promise { + const { multisig, index, member, programId = PROGRAM_ID } = args; + const ix = instructions.configTransactionExecute({ + multisigPda: multisig, + member: member, + transactionIndex: BigInt(index), + programId: programId, + }); + + return { instruction: ix }; +} +//endregion + +//region Batch +export async function createBatchCore( + args: CreateBatchActionArgs +): Promise { + const { + connection, + multisig, + creator, + vaultIndex = 0, + rentPayer = creator, + memo, + programId = PROGRAM_ID, + } = args; + + const multisigInfo = await accounts.Multisig.fromAccountAddress( + connection, + multisig + ); + + const currentTransactionIndex = Number(multisigInfo.transactionIndex); + const index = BigInt(currentTransactionIndex + 1); + + const ix = instructions.batchCreate({ + multisigPda: multisig, + batchIndex: index, + creator: creator, + vaultIndex: vaultIndex, + memo: memo, + rentPayer, + programId: programId, + }); + + return { instructions: [ix], index: Number(index) }; +} + +export async function addBatchTransactionCore( + args: BatchAddTransactionActionArgs +): Promise { + const { + multisig, + globalIndex, + innerIndex, + message, + vaultIndex, + ephemeralSigners, + member, + programId = PROGRAM_ID, + } = args; + const ix = instructions.batchAddTransaction({ + multisigPda: multisig, + member: member, + batchIndex: BigInt(globalIndex), + transactionIndex: innerIndex, + transactionMessage: message, + vaultIndex: vaultIndex ?? 0, + ephemeralSigners: ephemeralSigners ?? 0, + programId: programId, + }); + + return { instruction: ix }; +} + +export async function executeBatchTransactionCore( + args: ExecuteBatchActionArgs +): Promise { + const { connection, multisig, index, member, programId = PROGRAM_ID } = args; + const ix = instructions.batchExecuteTransaction({ + connection, + multisigPda: multisig, + member: member, + batchIndex: BigInt(index), + transactionIndex: index, + programId: programId, + }); + + return { ...ix }; +} +//endregion diff --git a/sdk/multisig/src/actions/common/types.ts b/sdk/multisig/src/actions/common/types.ts new file mode 100644 index 00000000..68ea5b2d --- /dev/null +++ b/sdk/multisig/src/actions/common/types.ts @@ -0,0 +1,463 @@ +import { + AddressLookupTableAccount, + Connection, + PublicKey, + SendOptions, + Signer, + TransactionInstruction, + TransactionMessage, +} from "@solana/web3.js"; +import { ConfigAction, Member } from "../../generated"; + +//region BaseBuilder +export interface BaseBuilderArgs { + /** The connection to an SVM network cluster */ + connection: Connection; + /** The public key of the creator */ + creator: PublicKey; +} + +export interface BuildResult { + instructions: TransactionInstruction[]; +} + +export interface SendSettings { + /** (Optional) Clear all current instructions after sending, so subsequent actions can be done with the same builder. */ + clearInstructions?: boolean; + /** (Optional) Extra instructions to prepend before specified builder instructions. */ + preInstructions?: TransactionInstruction[]; + /** (Optional) Extra instructions to append after specified builder instructions. */ + postInstructions?: TransactionInstruction[]; + /** (Optional) Any address lookup table accounts to be added to the transaction. */ + addressLookupTableAccounts?: AddressLookupTableAccount[]; + /** (Optional) Fee paying signer keypair. Sufficient if only one signer is needed */ + feePayer?: Signer; + /** (Optional) Array of multiple signing keypairs. Used for if multiple signers are needed. */ + signers?: Signer[]; + /** (Optional) `SendOptions` object from web3.js. Defaults to `{ preflightCommitment: "finalized" }` */ + options?: SendOptions; +} + +export interface BuildTransactionSettings { + /** **(Optional)** Any address lookup table accounts to be added to the transaction. */ + addressLookupTableAccounts?: AddressLookupTableAccount[]; + /** **(Optional)** Fee paying signer keypair. Sufficient if only one signer is needed */ + feePayer?: Signer; + /** **(Optional)** Array of multiple signing keypairs. Used for if multiple signers are needed. */ + signers?: Signer[]; +} +//endregion + +//region BaseTransactionBuilder +export interface TransactionBuilderArgs extends BaseBuilderArgs { + multisig: PublicKey; + programId?: PublicKey; +} + +export interface TransactionBuildResult extends BuildResult { + index: number; +} +//endregion + +//region Multisig +export 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 */ + members: Member[]; + /** Optional time lock in seconds */ + timeLock?: number; + /** Optional config authority key that can override consensus for ConfigTransactions */ + configAuthority?: PublicKey; + /** Optional rent collector where completed transaction rent will go after reclaim */ + rentCollector?: PublicKey; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +export interface CreateMultisigResult extends BuildResult { + multisigKey: PublicKey; +} +//endregion + +//region Proposals +export interface CreateProposalActionArgs { + /** The public key of the multisig config account */ + multisig: PublicKey; + /** The public key of the creator */ + creator: PublicKey; + /** Transaction index of the resulting Proposal */ + transactionIndex: number; + /** The public key of the fee payer, defaults to the creator */ + rentPayer?: PublicKey; + /** Whether the proposal should be initialized with status `Draft`. */ + isDraft?: boolean; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +export interface VoteActionArgs { + /** The public key of the multisig config account */ + multisig: PublicKey; + /** The public key of the approving member */ + member: PublicKey; + /** Transaction index of the resulting Proposal */ + transactionIndex: number; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +export interface ProposalResult { + /** `proposalCreate` instruction */ + instruction: TransactionInstruction; +} +//endregion + +//region VaultTransaction +export interface CreateVaultTransactionActionArgs { + /** The connection to an SVM network cluster */ + connection: Connection; + /** The public key of the multisig config account */ + multisig: PublicKey; + /** The public key of the creator */ + creator: PublicKey; + /** Transaction message containing the instructions to execute */ + message: TransactionMessage; + /** (Optional) Index of the transaction to build. If omitted, this will be fetched from the multisig account. */ + transactionIndex?: number; + /** (Optional) Index of the vault to target. Defaults to 0 */ + vaultIndex?: number; + /** (Optional) Specify a number of ephemeral signers to include. + * Useful if the underlying transaction requires more than one signer. + */ + ephemeralSigners?: number; + /** (Optional) The public key of the fee payer, defaults to the creator */ + rentPayer?: PublicKey; + /** (Optional) UTF-8 Memo for indexing purposes */ + memo?: string; + /** (Optional) Squads Program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +export interface CreateVaultTransactionResult extends BuildResult { + /** Transaction index of the resulting VaultTransaction */ + index: number; +} + +export interface ExecuteVaultTransactionActionArgs { + /** The connection to an SVM network cluster */ + connection: Connection; + /** The public key of the multisig config account */ + multisig: PublicKey; + /** Member who is executing the transaction */ + member: PublicKey; + /** Transaction index of the VaultTransaction to execute */ + index: number; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +export interface ExecuteVaultTransactionResult { + /** `vaultTransactionExecute` instruction */ + instruction: TransactionInstruction; + /** AddressLookupTableAccounts for the transaction */ + lookupTableAccounts: AddressLookupTableAccount[]; +} + +export interface ReclaimRentActionArgs { + /** The connection to an SVM network cluster */ + connection: Connection; + /** The public key of the multisig config account */ + multisig: PublicKey; + /** Transaction index of the VaultTransaction to execute */ + index: number; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} +//endregion + +//region ConfigTransaction +export interface CreateConfigTransactionActionArgs { + /** The connection to an SVM network cluster */ + connection: Connection; + /** The public key of the multisig config account */ + multisig: PublicKey; + /** The public key of the creator */ + creator: PublicKey; + /** Transaction message containing the instructions to execute */ + actions: ConfigAction[]; + /** The public key of the fee payer, defaults to the creator */ + rentPayer?: PublicKey; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +export interface CreateConfigTransactionResult { + /** `configTransactionCreate` instruction */ + instructions: TransactionInstruction[]; + /** Transaction index of the resulting ConfigTransaction */ + index: number; +} + +export interface ExecuteConfigTransactionActionArgs { + /** The public key of the multisig config account */ + multisig: PublicKey; + /** Member who is executing the transaction */ + member: PublicKey; + /** Transaction index of the ConfigTransaction to execute */ + index: number; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +export interface ExecuteConfigTransactionResult { + /** `configTransactionExecute` instruction */ + instruction: TransactionInstruction; +} + +export const ConfigActions = { + AddMember: (newMember: Member) => [ + { + __kind: "AddMember", + newMember, + }, + ], + RemoveMember: (oldMember: PublicKey) => { + return { + __kind: "RemoveMember", + oldMember, + } as ConfigAction; + }, + ChangeThreshold: (newThreshold: number) => { + return { + __kind: "ChangeThreshold", + newThreshold, + } as ConfigAction; + }, + SetTimeLock: (newTimeLock: number) => { + return { + __kind: "SetTimeLock", + newTimeLock, + } as ConfigAction; + }, + AddSpendingLimit: (spendingLimit: SpendingLimit) => { + return { + __kind: "AddSpendingLimit", + ...spendingLimit, + } as ConfigAction; + }, + RemoveSpendingLimit: (spendingLimit: PublicKey) => { + return { + __kind: "RemoveSpendingLimit", + spendingLimit, + } as ConfigAction; + }, + SetRentCollector: (rentCollector: PublicKey) => { + return { + __kind: "SetRentCollector", + newRentCollector: rentCollector, + } as ConfigAction; + }, +} as const; + +export interface SpendingLimit { + createKey: PublicKey; + vaultIndex: number; + mint: PublicKey; + amount: number; + period: number; + members: PublicKey[]; + destinations: PublicKey[]; +} +//endregion + +//region Batch +export interface CreateBatchActionArgs extends BaseBuilderArgs { + /** The public key of the multisig config account */ + multisig: PublicKey; + /** Index of the vault to target. Defaults to 0 */ + vaultIndex?: number; + /** The public key of the fee payer, defaults to the creator */ + rentPayer?: PublicKey; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +export interface CreateBatchResult extends BuildResult { + /** Transaction index of the resulting VaultTransaction */ + index: number; +} + +export interface BatchAddTransactionActionArgs { + /** The public key of the multisig config account */ + multisig: PublicKey; + /** Member who is executing the transaction */ + member: PublicKey; + /** Transaction index of the Batch created. */ + globalIndex: number; + /** Local transaction index of a transaction inside of the Batch. */ + innerIndex: number; + /** Transaction message containing the instructions to execute */ + message: TransactionMessage; + /** Index of the vault to target. Defaults to 0 */ + vaultIndex?: number; + /** Specify a number of ephemeral signers to include. + * Useful if the underlying transaction requires more than one signer. + */ + ephemeralSigners?: number; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +export interface BatchAddTransactionResult { + /** `batchAddTransaction` instruction */ + instruction: TransactionInstruction; +} + +export interface ExecuteBatchActionArgs { + /** The connection to an SVM network cluster */ + connection: Connection; + /** The public key of the multisig config account */ + multisig: PublicKey; + /** Member who is executing the transaction */ + member: PublicKey; + /** Transaction index of the VaultTransaction to execute */ + index: number; + /** Optional memo for indexing purposes */ + memo?: string; + /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ + programId?: PublicKey; +} + +export interface ExecuteBatchResult { + /** `vaultTransactionExecute` instruction */ + instruction: TransactionInstruction; + /** AddressLookupTableAccounts for the transaction */ + lookupTableAccounts: AddressLookupTableAccount[]; +} +//endregion + +//region Methods +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 BaseSendKeys = "send" | "sendAndConfirm" | "customSend"; + +// TODO: Split between sync and async getters. +type TransactionGetKeys = + | "getIndex" + | "getInstructions" + | "getTransactionKey" + | "getProposalKey" + | "getTransactionAccount" + | "getProposalAccount"; + +type TransactionActionKeys = + | "withProposal" + | "withApproval" + | "withRejection" + | "withExecute"; + +// TODO: Split between sync and async getters. +type BatchGetKeys = + | "getInstructions" + | "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; + reclaimRent: 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; +}; +//endregion diff --git a/sdk/multisig/src/actions/createBatch.ts b/sdk/multisig/src/actions/createBatch.ts index e3c1f2ae..0b4bd6ab 100644 --- a/sdk/multisig/src/actions/createBatch.ts +++ b/sdk/multisig/src/actions/createBatch.ts @@ -3,98 +3,32 @@ import { PublicKey, TransactionInstruction, TransactionMessage, - AddressLookupTableAccount, } from "@solana/web3.js"; -import { - instructions, - accounts, - getBatchTransactionPda, - getTransactionPda, -} from ".."; +import { getBatchTransactionPda, getTransactionPda } from ".."; import { Batch, PROGRAM_ID, VaultBatchTransaction } from "../generated"; import { - BaseBuilder, + addBatchTransactionCore, + createBatchCore, + executeBatchTransactionCore, +} from "./common/transaction"; +import { + BatchMethods, + CreateBatchActionArgs, + CreateBatchResult, +} from "./common/types"; +import { BaseTransactionBuilder } from "./common/baseTransaction"; +import { createApprovalCore, - createRejectionCore, createProposalCore, - BaseBuilderArgs, - BuildResult, -} from "./common"; -import { BatchMethods } from "./actionTypes"; - -interface CreateBatchActionArgs extends BaseBuilderArgs { - /** The public key of the multisig config account */ - multisig: PublicKey; - /** Index of the vault to target. Defaults to 0 */ - vaultIndex?: number; - /** The public key of the fee payer, defaults to the creator */ - rentPayer?: PublicKey; - /** Optional memo for indexing purposes */ - memo?: string; - /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ - programId?: PublicKey; -} - -interface CreateBatchResult extends BuildResult { - /** Transaction index of the resulting VaultTransaction */ - index: number; -} - -interface BatchAddTransactionActionArgs { - /** The public key of the multisig config account */ - multisig: PublicKey; - /** Member who is executing the transaction */ - member: PublicKey; - /** Transaction index of the Batch created. */ - globalIndex: number; - /** Local transaction index of a transaction inside of the Batch. */ - innerIndex: number; - /** Transaction message containing the instructions to execute */ - message: TransactionMessage; - /** Index of the vault to target. Defaults to 0 */ - vaultIndex?: number; - /** Specify a number of ephemeral signers to include. - * Useful if the underlying transaction requires more than one signer. - */ - ephemeralSigners?: number; - /** Optional memo for indexing purposes */ - memo?: string; - /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ - programId?: PublicKey; -} - -interface BatchAddTransactionResult { - /** `batchAddTransaction` instruction */ - instruction: TransactionInstruction; -} - -interface ExecuteBatchActionArgs { - /** The connection to an SVM network cluster */ - connection: Connection; - /** The public key of the multisig config account */ - multisig: PublicKey; - /** Member who is executing the transaction */ - member: PublicKey; - /** Transaction index of the VaultTransaction to execute */ - index: number; - /** Optional memo for indexing purposes */ - memo?: string; - /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ - programId?: PublicKey; -} - -interface ExecuteBatchResult { - /** `vaultTransactionExecute` instruction */ - instruction: TransactionInstruction; - /** AddressLookupTableAccounts for the transaction */ - lookupTableAccounts: AddressLookupTableAccount[]; -} + createRejectionCore, +} from "./common/proposal"; /** - * Builds an instruction to create a new VaultTransaction and returns the instruction with the corresponding transaction index. - * Can optionally chain additional methods for transactions, and sending. + * Builds an instruction to create a new Batch. + * Also includes the ability to chain instructions for creating/voting on proposals, and adding transactions, as well as sending + * a built transaction. * - * @param args - Object of type `CreateVaultTransactionActionArgs` that contains the necessary information to create a new VaultTransaction. + * @param args - Object of type `CreateBatchActionArgs` that contains the necessary information to create a new VaultTransaction. * @returns `{ instruction: TransactionInstruction, index: number }` - object with the `vaultTransactionCreate` instruction and the transaction index of the resulting VaultTransaction. * * @example @@ -129,7 +63,7 @@ export function createBatch(args: CreateBatchActionArgs): BatchBuilder { return new BatchBuilder(args); } -class BatchBuilder extends BaseBuilder< +class BatchBuilder extends BaseTransactionBuilder< CreateBatchResult, CreateBatchActionArgs > { @@ -163,6 +97,20 @@ class BatchBuilder extends BaseBuilder< this.index = result.index; } + /** + * Fetches the current index of transactions inside of the `Batch` account. + * @returns `Promise` + */ + async getInnerIndex(): Promise { + this.ensureBuilt(); + + return this.innerIndex; + } + + /** + * Fetches the PublicKey of the built `Batch` account. + * @returns `Promise` - PublicKey of the `Batch` account. + */ async getBatchKey(): Promise { this.ensureBuilt(); const index = this.index; @@ -175,6 +123,11 @@ class BatchBuilder extends BaseBuilder< return batchPda; } + /** + * Fetches the PublicKey of a transaction inside of the built `Batch` account. + * @args `innerIndex` - Number denoting the index of the transaction inside of the batch. + * @returns `Promise` - PublicKey of the `VaultBatchTransaction` account. + */ async getBatchTransactionKey(innerIndex?: number): Promise { this.ensureBuilt(); const index = this.index; @@ -188,6 +141,10 @@ class BatchBuilder extends BaseBuilder< return batchPda; } + /** + * Fetches and returns an array of PublicKeys for all transactions added to the batch. + * @returns `Promise` - An array of `VaultBatchTransaction` PublicKeys. + */ async getAllBatchTransactionKeys(): Promise { this.ensureBuilt(); const index = this.index; @@ -206,6 +163,11 @@ class BatchBuilder extends BaseBuilder< return transactions; } + /** + * Fetches and deserializes the `Batch` account after it is built and sent. + * @args `key` - The public key of the `Batch` account. + * @returns `Batch` - Deserialized `Batch` account data. + */ async getBatchAccount( key: PublicKey ): Promise>> { @@ -215,15 +177,37 @@ class BatchBuilder extends BaseBuilder< return batchAccount; } + /** + * Fetches and deserializes a `VaultBatchTransaction` account after it is added to the `Batch`. + * @args `key` - The public key of the `Batch` account. + * @returns `VaultBatchTransaction` - Deserialized `VaultBatchTransaction` account data. + */ + async getBatchTransactionAccount( + key: PublicKey + ): Promise>> { + this.ensureBuilt(); + const batchTxAccount = await VaultBatchTransaction.fromAccountAddress( + this.connection, + key + ); + + return batchTxAccount; + } + /** * Creates a transaction containing the VaultTransaction creation instruction. * @args feePayer - Optional signer to pay the transaction fee. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ - async addTransaction( - message: TransactionMessage, - member?: PublicKey - ): Promise>> { + async addTransaction({ + message, + member, + ephemeralSigners, + }: { + message: TransactionMessage; + member?: PublicKey; + ephemeralSigners?: number; + }): Promise>> { this.ensureBuilt(); const { instruction } = await addBatchTransactionCore({ multisig: this.args.multisig, @@ -231,8 +215,8 @@ class BatchBuilder extends BaseBuilder< globalIndex: this.index, innerIndex: this.innerIndex, message, - // vaultIndex: this.vaultIndex, - // ephemeralSigners: this.ephemeralSigners, + vaultIndex: this.vaultIndex, + ephemeralSigners: ephemeralSigners ?? 0, programId: this.args.programId, }); @@ -244,20 +228,19 @@ class BatchBuilder extends BaseBuilder< } /** - * Creates a transaction containing the VaultTransaction creation instruction. - * @args feePayer - Optional signer to pay the transaction fee. - * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + * Pushes a `proposalCreate` instruction to the builder. + * @args `isDraft` - **(Optional)** Whether the proposal is a draft or not, defaults to `false`. */ - async withProposal( - isDraft?: boolean - ): Promise>> { + async withProposal({ isDraft }: { isDraft?: boolean } = {}): Promise< + Pick> + > { this.ensureBuilt(); const { instruction } = createProposalCore({ multisig: this.args.multisig, creator: this.creator, transactionIndex: this.index, programId: this.args.programId, - isDraft, + isDraft: isDraft ?? false, }); this.instructions.push(instruction); @@ -266,13 +249,13 @@ class BatchBuilder extends BaseBuilder< } /** - * Creates a transaction containing the VaultTransaction creation instruction. - * @args feePayer - Optional signer to pay the transaction fee. - * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + * Pushes a `proposalApprove` instruction to the builder. + * @args `member` - **(Optional)** Specify the approving member, will default to the creator. */ - withApproval( - member?: PublicKey - ): Pick> { + withApproval({ member }: { member?: PublicKey } = {}): Pick< + BatchBuilder, + BatchMethods<"withApproval"> + > { const { instruction } = createApprovalCore({ multisig: this.args.multisig, member: member ?? this.creator, @@ -286,13 +269,13 @@ class BatchBuilder extends BaseBuilder< } /** - * Creates a transaction containing the VaultTransaction creation instruction. - * @args feePayer - Optional signer to pay the transaction fee. - * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + * Pushes a `proposalReject` instruction to the builder. + * @args `member` - **(Optional)** Specify the rejecting member, will default to the creator. */ - withRejection( - member?: PublicKey - ): Pick> { + withRejection({ member }: { member?: PublicKey } = {}): Pick< + BatchBuilder, + BatchMethods<"withRejection"> + > { const { instruction } = createRejectionCore({ multisig: this.args.multisig, member: member ?? this.creator, @@ -306,13 +289,12 @@ class BatchBuilder extends BaseBuilder< } /** - * Creates a transaction containing the VaultTransaction creation instruction. - * @args feePayer - Optional signer to pay the transaction fee. - * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + * Pushes a `vaultTransactionExecute` instruction to the builder. + * @args `member` - **(Optional)** Specify the executing member, will default to the creator. */ - async withExecute( - member?: PublicKey - ): Promise>> { + async withExecute({ member }: { member?: PublicKey } = {}): Promise< + Pick> + > { await this.ensureBuilt(); const { instruction } = await executeBatchTransactionCore({ connection: this.connection, @@ -348,80 +330,3 @@ export async function isBatchTransaction( return false; } } - -async function createBatchCore( - args: CreateBatchActionArgs -): Promise { - const { - connection, - multisig, - creator, - vaultIndex = 0, - rentPayer = creator, - memo, - programId = PROGRAM_ID, - } = args; - - const multisigInfo = await accounts.Multisig.fromAccountAddress( - connection, - multisig - ); - - const currentTransactionIndex = Number(multisigInfo.transactionIndex); - const index = BigInt(currentTransactionIndex + 1); - - const ix = instructions.batchCreate({ - multisigPda: multisig, - batchIndex: index, - creator: creator, - vaultIndex: vaultIndex, - memo: memo, - rentPayer, - programId: programId, - }); - - return { instructions: [ix], index: Number(index) }; -} - -async function addBatchTransactionCore( - args: BatchAddTransactionActionArgs -): Promise { - const { - multisig, - globalIndex, - innerIndex, - message, - vaultIndex, - ephemeralSigners, - member, - programId = PROGRAM_ID, - } = args; - const ix = instructions.batchAddTransaction({ - multisigPda: multisig, - member: member, - batchIndex: BigInt(globalIndex), - transactionIndex: innerIndex, - transactionMessage: message, - vaultIndex: vaultIndex ?? 0, - ephemeralSigners: ephemeralSigners ?? 0, - programId: programId, - }); - - return { instruction: ix }; -} - -async function executeBatchTransactionCore( - args: ExecuteBatchActionArgs -): Promise { - const { connection, multisig, index, member, programId = PROGRAM_ID } = args; - const ix = instructions.batchExecuteTransaction({ - connection, - multisigPda: multisig, - member: member, - batchIndex: BigInt(index), - transactionIndex: index, - programId: programId, - }); - - return { ...ix }; -} diff --git a/sdk/multisig/src/actions/createConfigTransaction.ts b/sdk/multisig/src/actions/createConfigTransaction.ts index 9a28f285..87147d7a 100644 --- a/sdk/multisig/src/actions/createConfigTransaction.ts +++ b/sdk/multisig/src/actions/createConfigTransaction.ts @@ -1,56 +1,20 @@ import { Connection, PublicKey, TransactionInstruction } from "@solana/web3.js"; -import { instructions, accounts } from ".."; -import { ConfigAction, ConfigTransaction, PROGRAM_ID } from "../generated"; +import { ConfigTransaction } from "../generated"; +import { + createConfigTransactionCore, + executeConfigTransactionCore, +} from "./common/transaction"; +import { BaseTransactionBuilder } from "./common/baseTransaction"; +import { + CreateConfigTransactionActionArgs, + CreateConfigTransactionResult, + Methods, +} from "./common/types"; import { - BaseTransactionBuilder, createApprovalCore, createProposalCore, createRejectionCore, -} from "./common"; -import { Methods } from "./actionTypes"; -import { Member } from "../types"; - -interface CreateConfigTransactionActionArgs { - /** The connection to an SVM network cluster */ - connection: Connection; - /** The public key of the multisig config account */ - multisig: PublicKey; - /** The public key of the creator */ - creator: PublicKey; - /** Transaction message containing the instructions to execute */ - actions: ConfigAction[]; - /** The public key of the fee payer, defaults to the creator */ - rentPayer?: PublicKey; - /** Optional memo for indexing purposes */ - memo?: string; - /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ - programId?: PublicKey; -} - -interface CreateConfigTransactionResult { - /** `configTransactionCreate` instruction */ - instructions: TransactionInstruction[]; - /** Transaction index of the resulting ConfigTransaction */ - index: number; -} - -interface ExecuteConfigTransactionActionArgs { - /** The public key of the multisig config account */ - multisig: PublicKey; - /** Member who is executing the transaction */ - member: PublicKey; - /** Transaction index of the ConfigTransaction to execute */ - index: number; - /** Optional memo for indexing purposes */ - memo?: string; - /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ - programId?: PublicKey; -} - -interface ExecuteConfigTransactionResult { - /** `configTransactionExecute` instruction */ - instruction: TransactionInstruction; -} +} from "./common/proposal"; /** * Builds an instruction to create a new ConfigTransaction and returns the instruction with the corresponding transaction index. @@ -136,13 +100,12 @@ class ConfigTransactionBuilder extends BaseTransactionBuilder< } /** - * Creates a transaction containing the ConfigTransaction creation instruction. - * @args feePayer - Optional signer to pay the transaction fee. - * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + * Pushes a `proposalCreate` instruction to the builder. + * @args `isDraft` - **(Optional)** Whether the proposal is a draft or not, defaults to `false`. */ - async withProposal( - isDraft?: boolean - ): Promise>> { + async withProposal({ isDraft }: { isDraft?: boolean } = {}): Promise< + Pick> + > { await this.ensureBuilt(); const { instruction } = createProposalCore({ multisig: this.args.multisig, @@ -158,13 +121,13 @@ class ConfigTransactionBuilder extends BaseTransactionBuilder< } /** - * Creates a transaction containing the ConfigTransaction creation instruction. - * @args feePayer - Optional signer to pay the transaction fee. - * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + * Pushes a `proposalApprove` instruction to the builder. + * @args `member` - **(Optional)** Specify the approving member, will default to the creator. */ - withApproval( - member?: PublicKey - ): Pick> { + withApproval({ member }: { member?: PublicKey } = {}): Pick< + ConfigTransactionBuilder, + Methods<"withApproval"> + > { const { instruction } = createApprovalCore({ multisig: this.args.multisig, member: member ?? this.creator, @@ -178,13 +141,13 @@ class ConfigTransactionBuilder extends BaseTransactionBuilder< } /** - * Creates a transaction containing the ConfigTransaction creation instruction. - * @args feePayer - Optional signer to pay the transaction fee. - * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + * Pushes a `proposalReject` instruction to the builder. + * @args `member` - **(Optional)** Specify the rejecting member, will default to the creator. */ - withRejection( - member?: PublicKey - ): Pick> { + withRejection({ member }: { member?: PublicKey } = {}): Pick< + ConfigTransactionBuilder, + Methods<"withRejection"> + > { const { instruction } = createRejectionCore({ multisig: this.args.multisig, member: member ?? this.creator, @@ -198,13 +161,12 @@ class ConfigTransactionBuilder extends BaseTransactionBuilder< } /** - * Creates a transaction containing the ConfigTransaction creation instruction. - * @args feePayer - Optional signer to pay the transaction fee. - * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + * Pushes a `vaultTransactionExecute` instruction to the builder. + * @args `member` - **(Optional)** Specify the executing member, will default to the creator. */ - async withExecute( - member?: PublicKey - ): Promise>> { + async withExecute({ member }: { member?: PublicKey } = {}): Promise< + Pick> + > { const { instruction } = await executeConfigTransactionCore({ multisig: this.args.multisig, member: member ?? this.creator, @@ -233,106 +195,3 @@ export async function isConfigTransaction( return false; } } - -export const ConfigActions = { - AddMember: (newMember: Member) => [ - { - __kind: "AddMember", - newMember, - }, - ], - RemoveMember: (oldMember: PublicKey) => { - return { - __kind: "RemoveMember", - oldMember, - } as ConfigAction; - }, - ChangeThreshold: (newThreshold: number) => { - return { - __kind: "ChangeThreshold", - newThreshold, - } as ConfigAction; - }, - SetTimeLock: (newTimeLock: number) => { - return { - __kind: "SetTimeLock", - newTimeLock, - } as ConfigAction; - }, - AddSpendingLimit: (spendingLimit: SpendingLimit) => { - return { - __kind: "AddSpendingLimit", - ...spendingLimit, - } as ConfigAction; - }, - RemoveSpendingLimit: (spendingLimit: PublicKey) => { - return { - __kind: "RemoveSpendingLimit", - spendingLimit, - } as ConfigAction; - }, - SetRentCollector: (rentCollector: PublicKey) => { - return { - __kind: "SetRentCollector", - newRentCollector: rentCollector, - } as ConfigAction; - }, -} as const; - -export interface SpendingLimit { - createKey: PublicKey; - vaultIndex: number; - mint: PublicKey; - amount: number; - period: number; - members: PublicKey[]; - destinations: PublicKey[]; -} - -async function createConfigTransactionCore( - args: CreateConfigTransactionActionArgs -): Promise { - const { - connection, - multisig, - creator, - actions, - rentPayer = creator, - memo, - programId = PROGRAM_ID, - } = args; - - const multisigInfo = await accounts.Multisig.fromAccountAddress( - connection, - multisig - ); - - const currentTransactionIndex = Number(multisigInfo.transactionIndex); - const index = BigInt(currentTransactionIndex + 1); - - const ix = instructions.configTransactionCreate({ - multisigPda: multisig, - transactionIndex: index, - creator: creator, - actions, - memo: memo, - rentPayer, - programId: programId, - }); - - return { instructions: [ix], index: Number(index) }; -} - -async function executeConfigTransactionCore( - args: ExecuteConfigTransactionActionArgs -): Promise { - const { multisig, index, member, programId = PROGRAM_ID } = args; - const ix = instructions.configTransactionExecute({ - multisigPda: multisig, - member: member, - transactionIndex: BigInt(index), - programId: programId, - }); - - return { instruction: ix }; -} diff --git a/sdk/multisig/src/actions/createMultisig.ts b/sdk/multisig/src/actions/createMultisig.ts index de44c517..7e2f5aee 100644 --- a/sdk/multisig/src/actions/createMultisig.ts +++ b/sdk/multisig/src/actions/createMultisig.ts @@ -5,30 +5,17 @@ import { SendOptions, Signer, TransactionInstruction, + VersionedTransaction, } 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 */ - threshold: number; - /** The list of members in the multisig, with their associated permissions */ - members: Member[]; - /** Optional time lock in seconds */ - timeLock?: number; - /** Optional config authority key that can override consensus for ConfigTransactions */ - configAuthority?: PublicKey; - /** Optional rent collector where completed transaction rent will go after reclaim */ - rentCollector?: PublicKey; - /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ - programId?: PublicKey; -} - -interface CreateMultisigResult extends BuildResult { - multisigKey: PublicKey; -} +import { Multisig, PROGRAM_ID } from "../generated"; +import { createMultisigCore } from "./common/multisig"; +import { + BuildTransactionSettings, + CreateMultisigActionArgs, + CreateMultisigResult, + SendSettings, +} from "./common/types"; +import { BaseBuilder } from "./common/base"; /** * Builds an instruction to create a new Multisig, and returns the instruction and `createKey` @@ -81,7 +68,7 @@ class MultisigBuilder extends BaseBuilder< public multisigKey: PublicKey = PublicKey.default; constructor(args: CreateMultisigActionArgs) { - super(args); + super(args, { generateCreateKey: true }); } protected async build(): Promise { @@ -104,7 +91,7 @@ class MultisigBuilder extends BaseBuilder< rentCollector, programId, }, - this.createKey + this.createKey! ); this.instructions = [...result.instructions]; @@ -113,7 +100,7 @@ class MultisigBuilder extends BaseBuilder< async getCreateKey(): Promise { await this.ensureBuilt(); - return this.createKey; + return this.createKey!; } async getMultisigKey(): Promise { @@ -131,69 +118,46 @@ class MultisigBuilder extends BaseBuilder< return multisigAccount; } - async sendAndConfirm(settings?: { - preInstructions?: TransactionInstruction[] | undefined; - postInstructions?: TransactionInstruction[] | undefined; - feePayer?: Signer | undefined; - signers?: Signer[] | undefined; - options?: SendOptions | undefined; - }): Promise { + async transaction( + settings?: BuildTransactionSettings + ): Promise { await this.ensureBuilt(); if (settings?.signers) { - settings.signers.push(this.createKey); + settings.signers.push(this.createKey!); } else { settings = { - signers: [this.createKey], + signers: [this.createKey!], ...settings, }; } - return await super.sendAndConfirm(settings); + return await super.transaction(settings); } -} -export async function createMultisigCore( - args: CreateMultisigActionArgs, - createKey: Keypair -): Promise { - const { - connection, - creator, - threshold, - members, - timeLock = 0, - configAuthority, - rentCollector, - programId = PROGRAM_ID, - } = args; - - const [multisigPda] = getMultisigPda({ - createKey: createKey.publicKey, - programId, - }); - const programConfigPda = getProgramConfigPda({ programId })[0]; - - const programConfig = await ProgramConfig.fromAccountAddress( - connection, - programConfigPda - ); - - const ix = instructions.multisigCreateV2({ - creator, - threshold, - members, - multisigPda: multisigPda, - treasury: programConfig.treasury, - createKey: createKey.publicKey, - timeLock: timeLock ?? 0, - rentCollector: rentCollector ?? null, - configAuthority: configAuthority ?? null, - programId: programId ?? PROGRAM_ID, - }); + async send(settings?: SendSettings): Promise { + await this.ensureBuilt(); + if (settings?.signers) { + settings.signers.push(this.createKey!); + } else { + settings = { + signers: [this.createKey!], + ...settings, + }; + } + return await super.send(settings); + } - return { - instructions: [ix], - multisigKey: multisigPda, - }; + async sendAndConfirm(settings?: SendSettings): 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 isMultisig(connection: Connection, key: PublicKey) { @@ -204,16 +168,3 @@ export async function isMultisig(connection: Connection, key: PublicKey) { return false; } } - -async function Example() { - const connection = new Connection("https://api.mainnet-beta.solana.com"); - const feePayer = Keypair.generate(); - const signature = createMultisig({ - connection, - members: createMembers([ - { key: PublicKey.default, permissions: SquadPermissions.All }, - ]), - creator: PublicKey.default, - threshold: 2, - }); -} diff --git a/sdk/multisig/src/actions/createVaultTransaction.ts b/sdk/multisig/src/actions/createVaultTransaction.ts index 8371c82e..29e79ba4 100644 --- a/sdk/multisig/src/actions/createVaultTransaction.ts +++ b/sdk/multisig/src/actions/createVaultTransaction.ts @@ -2,90 +2,25 @@ import { Connection, PublicKey, TransactionInstruction, - TransactionMessage, AddressLookupTableAccount, - Message, } from "@solana/web3.js"; +import { PROGRAM_ID, VaultTransaction } from "../generated"; import { - PROGRAM_ID, - VaultTransaction, - vaultTransactionMessageBeet, -} from "../generated"; + createVaultTransactionCore, + executeVaultTransactionCore, + reclaimRentCore, +} from "./common/transaction"; +import { + CreateVaultTransactionActionArgs, + CreateVaultTransactionResult, + Methods, +} from "./common/types"; +import { BaseTransactionBuilder } from "./common/baseTransaction"; import { createApprovalCore, - createRejectionCore, createProposalCore, - BaseTransactionBuilder, - BuildResult, - ProposalResult, -} from "./common"; -import { instructions, accounts } from ".."; -import { Methods } from "./actionTypes"; - -interface CreateVaultTransactionActionArgs { - /** The connection to an SVM network cluster */ - connection: Connection; - /** The public key of the multisig config account */ - multisig: PublicKey; - /** The public key of the creator */ - creator: PublicKey; - /** Transaction message containing the instructions to execute */ - message: TransactionMessage; - /** (Optional) Index of the transaction to build. If omitted, this will be fetched from the multisig account. */ - transactionIndex?: number; - /** (Optional) Index of the vault to target. Defaults to 0 */ - vaultIndex?: number; - /** (Optional) Specify a number of ephemeral signers to include. - * Useful if the underlying transaction requires more than one signer. - */ - ephemeralSigners?: number; - /** (Optional) The public key of the fee payer, defaults to the creator */ - rentPayer?: PublicKey; - /** (Optional) UTF-8 Memo for indexing purposes */ - memo?: string; - /** (Optional) Squads Program ID (defaults to Solana mainnet-beta/devnet Program ID) */ - programId?: PublicKey; -} - -interface CreateVaultTransactionResult extends BuildResult { - /** Transaction index of the resulting VaultTransaction */ - index: number; -} - -interface ExecuteVaultTransactionActionArgs { - /** The connection to an SVM network cluster */ - connection: Connection; - /** The public key of the multisig config account */ - multisig: PublicKey; - /** Member who is executing the transaction */ - member: PublicKey; - /** Transaction index of the VaultTransaction to execute */ - index: number; - /** Optional memo for indexing purposes */ - memo?: string; - /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ - programId?: PublicKey; -} - -interface ExecuteVaultTransactionResult { - /** `vaultTransactionExecute` instruction */ - instruction: TransactionInstruction; - /** AddressLookupTableAccounts for the transaction */ - lookupTableAccounts: AddressLookupTableAccount[]; -} - -interface ReclaimRentActionArgs { - /** The connection to an SVM network cluster */ - connection: Connection; - /** The public key of the multisig config account */ - multisig: PublicKey; - /** Transaction index of the VaultTransaction to execute */ - index: number; - /** Optional memo for indexing purposes */ - memo?: string; - /** Optional program ID (defaults to Solana mainnet-beta/devnet Program ID) */ - programId?: PublicKey; -} + createRejectionCore, +} from "./common/proposal"; /** * Builds an instruction to create a new VaultTransaction and returns the instruction with the corresponding transaction index. @@ -185,13 +120,12 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< } /** - * Creates a transaction containing the VaultTransaction creation instruction. - * @args feePayer - Optional signer to pay the transaction fee. - * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + * Pushes a `proposalCreate` instruction to the builder. + * @args `isDraft` - **(Optional)** Whether the proposal is a draft or not, defaults to `false`. */ - async withProposal( - isDraft?: boolean - ): Promise>> { + async withProposal({ isDraft }: { isDraft?: boolean } = {}): Promise< + Pick> + > { await this.ensureBuilt(); const { instruction } = createProposalCore({ multisig: this.args.multisig, @@ -207,13 +141,13 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< } /** - * Creates a transaction containing the VaultTransaction creation instruction. - * @args feePayer - Optional signer to pay the transaction fee. - * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + * Pushes a `proposalApprove` instruction to the builder. + * @args `member` - **(Optional)** Specify the approving member, will default to the creator. */ - withApproval( - member?: PublicKey - ): Pick> { + withApproval({ member }: { member?: PublicKey } = {}): Pick< + VaultTransactionBuilder, + Methods<"withApproval"> + > { const { instruction } = createApprovalCore({ multisig: this.args.multisig, member: member ?? this.creator, @@ -227,13 +161,13 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< } /** - * Creates a transaction containing the VaultTransaction creation instruction. - * @args feePayer - Optional signer to pay the transaction fee. - * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + * Pushes a `proposalReject` instruction to the builder. + * @args `member` - **(Optional)** Specify the rejecting member, will default to the creator. */ - withRejection( - member?: PublicKey - ): Pick> { + withRejection({ member }: { member?: PublicKey } = {}): Pick< + VaultTransactionBuilder, + Methods<"withRejection"> + > { const { instruction } = createRejectionCore({ multisig: this.args.multisig, member: member ?? this.creator, @@ -247,13 +181,12 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< } /** - * Creates a transaction containing the VaultTransaction creation instruction. - * @args feePayer - Optional signer to pay the transaction fee. - * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. + * Pushes a `vaultTransactionExecute` instruction to the builder. + * @args `member` - **(Optional)** Specify the executing member, will default to the creator. */ - async withExecute( - member?: PublicKey - ): Promise>> { + async withExecute({ member }: { member?: PublicKey } = {}): Promise< + Pick> + > { await this.ensureBuilt(); const { instruction, lookupTableAccounts } = await executeVaultTransactionCore({ @@ -285,44 +218,6 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< } } -/** - * WIP: Creates a transaction builder instance from an existing `VaultTransaction` account key. - * @args `{ connection: Connection, transaction: PublicKey, programId?: PublicKey }` - * @returns `VaultTransactionBuilder` - */ -/* -export async function buildFromVaultTransaction({ - connection, - transaction, - programId, -}: { - connection: Connection; - transaction: PublicKey; - programId?: PublicKey; -}) { - const txAccount = await VaultTransaction.fromAccountAddress( - connection, - transaction - ); - - const compiledMessage = Message.from( - vaultTransactionMessageBeet.serialize(txAccount.message)[0] - ); - - const message = TransactionMessage.decompile(compiledMessage); - - const builder = createVaultTransaction({ - connection, - multisig: txAccount.multisig, - creator: txAccount.creator, - message: message, - programId: programId, - }); - - return builder; -} -*/ - export async function isVaultTransaction( connection: Connection, key: PublicKey @@ -334,141 +229,3 @@ export async function isVaultTransaction( return false; } } - -async function createVaultTransactionCore( - args: CreateVaultTransactionActionArgs, - transactionIndex?: number -): Promise { - const { - connection, - multisig, - creator, - message, - vaultIndex = 0, - ephemeralSigners = 0, - rentPayer = creator, - memo, - programId = PROGRAM_ID, - } = args; - - const multisigInfo = await accounts.Multisig.fromAccountAddress( - connection, - multisig - ); - let index; - if (transactionIndex) { - index = BigInt(transactionIndex); - } else { - const currentTransactionIndex = Number(multisigInfo.transactionIndex); - index = BigInt(currentTransactionIndex + 1); - } - - const ix = instructions.vaultTransactionCreate({ - multisigPda: multisig, - transactionIndex: index, - creator: creator, - vaultIndex: vaultIndex, - ephemeralSigners: ephemeralSigners, - transactionMessage: message, - memo: memo, - rentPayer, - programId: programId, - }); - - return { instructions: [ix], index: Number(index) }; -} - -async function executeVaultTransactionCore( - args: ExecuteVaultTransactionActionArgs -): Promise { - const { connection, multisig, index, member, programId = PROGRAM_ID } = args; - const ix = await instructions.vaultTransactionExecute({ - connection, - multisigPda: multisig, - member: member, - transactionIndex: BigInt(index), - programId: programId, - }); - - return { - ...ix, - }; -} - -async function reclaimRentCore( - args: ReclaimRentActionArgs -): Promise { - const { connection, multisig, index, programId = PROGRAM_ID } = args; - const multisigInfo = await accounts.Multisig.fromAccountAddress( - connection, - multisig - ); - - if (!multisigInfo.rentCollector) { - throw new Error("No rent collector found in Multisig config."); - } - - const ix = instructions.vaultTransactionAccountsClose({ - multisigPda: multisig, - rentCollector: multisigInfo.rentCollector, - transactionIndex: BigInt(index), - programId: programId, - }); - - return { - instruction: 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/sdk/multisig/src/actions/index.ts b/sdk/multisig/src/actions/index.ts index 1f6e240d..5c4e2144 100644 --- a/sdk/multisig/src/actions/index.ts +++ b/sdk/multisig/src/actions/index.ts @@ -1,9 +1,7 @@ export * from "./createMultisig"; export * from "./createVaultTransaction"; export * from "./createConfigTransaction"; +export * from "./createBatch"; export * from "./members"; -// WIP -//export * from "./createTransactionMultiStep"; -// WIP -//export * from "./createBatch"; +export { ConfigActions } from "./common/types"; diff --git a/tests/suites/examples/actions.ts b/tests/suites/examples/actions.ts index 4af878a5..d11088d9 100644 --- a/tests/suites/examples/actions.ts +++ b/tests/suites/examples/actions.ts @@ -19,6 +19,7 @@ import { createMultisig, createVaultTransaction, createConfigTransaction, + createBatch, ConfigActions, createMembers, isVaultTransaction, @@ -45,8 +46,12 @@ describe("Examples / End2End Actions", () => { const connection = createLocalhostConnection(); let multisigPda: PublicKey = PublicKey.default; + let transactionPda: PublicKey; let configTransactionPda: PublicKey; + let batchPda: PublicKey; + let batchWithTx: PublicKey; + let members: TestMembers; let outsider: Keypair; @@ -55,7 +60,32 @@ describe("Examples / End2End Actions", () => { members = await generateMultisigMembers(connection); }); + //region Multisig it("should create a multisig", async () => { + const builder = createMultisig({ + connection, + creator: members.almighty.publicKey, + members: createMembers([ + { key: members.almighty.publicKey, permissions: SquadPermissions.All }, + ]), + threshold: 1, + programId, + }); + + const signature = await builder.sendAndConfirm({ + signers: [members.almighty], + }); + + const [vaultPda] = multisig.getVaultPda({ + multisigPda: multisigPda, + index: 0, + }); + await connection.requestAirdrop(vaultPda, 10 * LAMPORTS_PER_SOL); + + assert.ok(signature); + }); + + it("should create a multi-member multisig", async () => { const builder = createMultisig({ connection, creator: members.almighty.publicKey, @@ -76,22 +106,72 @@ describe("Examples / End2End Actions", () => { }); multisigPda = await builder.getMultisigKey(); - const createKey = await builder.getCreateKey(); const signature = await builder.sendAndConfirm({ - signers: [members.almighty, createKey], + signers: [members.almighty], + }); + + assert.ok(signature); + }); + + it("should get multisig account", async () => { + const builder = createMultisig({ + connection, + creator: members.almighty.publicKey, + members: createMembers([ + { key: members.almighty.publicKey, permissions: SquadPermissions.All }, + ]), + threshold: 1, + programId, + }); + + let multisigKey = await builder.getMultisigKey(); + + const signature = await builder.sendAndConfirm({ + signers: [members.almighty], }); + assert.ok(signature); + + const account = await builder.getMultisigAccount(multisigKey); + + assert.ok(account instanceof multisig.accounts.Multisig); + }); + //endregion + + //region Vault Transactions + it("should create a vault transaction", async () => { const [vaultPda] = multisig.getVaultPda({ multisigPda: multisigPda, index: 0, }); + await connection.requestAirdrop(vaultPda, 10 * LAMPORTS_PER_SOL); + 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, + }); + + const signature = await txBuilder.sendAndConfirm({ + feePayer: members.almighty, + }); + assert.ok(signature); }); - it("should create a vault transaction", async () => { + it("should create a vault transaction w/ proposal", async () => { const [vaultPda] = multisig.getVaultPda({ multisigPda: multisigPda, index: 0, @@ -115,7 +195,7 @@ describe("Examples / End2End Actions", () => { await txBuilder.withProposal(); - transactionPda = txBuilder.getTransactionKey(); + transactionPda = await txBuilder.getTransactionKey(); const signature = await txBuilder.sendAndConfirm({ feePayer: members.almighty, @@ -124,7 +204,126 @@ describe("Examples / End2End Actions", () => { assert.ok(signature); }); + it("should create a vault transaction w/ proposal & approve", 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, + }); + + await txBuilder.withProposal(); + txBuilder.withApproval({ member: members.almighty.publicKey }); + + const signature = await txBuilder.sendAndConfirm({ + feePayer: members.almighty, + }); + + assert.ok(signature); + }); + + it("should create a vault transaction w/ proposal & reject", 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, + }); + + await txBuilder.withProposal(); + txBuilder.withRejection({ member: members.almighty.publicKey }); + + transactionPda = await txBuilder.getTransactionKey(); + + const signature = await txBuilder.sendAndConfirm({ + feePayer: members.almighty, + }); + + assert.ok(signature); + }); + + it("should get vault transaction account", 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, + }); + + const transactionKey = await txBuilder.getTransactionKey(); + + const signature = await txBuilder.sendAndConfirm({ + feePayer: members.almighty, + }); + + assert.ok(signature); + + const account = await txBuilder.getTransactionAccount(transactionKey); + + assert.ok(account instanceof multisig.accounts.VaultTransaction); + }); + //endregion + + //region Config Transactions it("should create a config transaction", async () => { + const configBuilder = createConfigTransaction({ + connection, + multisig: multisigPda, + creator: members.proposer.publicKey, + actions: [ConfigActions.SetTimeLock(10)], + programId, + }); + + const signature = await configBuilder.sendAndConfirm({ + signers: [members.proposer], + }); + + assert.ok(signature); + }); + + it("should create a config transaction w/ multiple actions", async () => { const configBuilder = createConfigTransaction({ connection, multisig: multisigPda, @@ -136,17 +335,274 @@ describe("Examples / End2End Actions", () => { programId, }); + const signature = await configBuilder.sendAndConfirm({ + signers: [members.proposer], + }); + + assert.ok(signature); + }); + + it("should create a config transaction w/ proposal", async () => { + const configBuilder = createConfigTransaction({ + connection, + multisig: multisigPda, + creator: members.proposer.publicKey, + actions: [ + ConfigActions.SetTimeLock(300), + ConfigActions.SetRentCollector(members.almighty.publicKey), + ], + programId, + }); + + configTransactionPda = await configBuilder.getTransactionKey(); + + await configBuilder.withProposal(); + + const signature = await configBuilder.sendAndConfirm({ + signers: [members.proposer], + }); + + assert.ok(signature); + }); + + it("should create a config transaction w/ proposal & approve", async () => { + const configBuilder = createConfigTransaction({ + connection, + multisig: multisigPda, + creator: members.almighty.publicKey, + actions: [ + ConfigActions.SetTimeLock(300), + ConfigActions.SetRentCollector(members.almighty.publicKey), + ], + programId, + }); + + await configBuilder.withProposal(); + configBuilder.withApproval({ member: members.almighty.publicKey }); + + const signature = await configBuilder.sendAndConfirm({ + signers: [members.almighty], + }); + + assert.ok(signature); + }); + + it("should create a config transaction w/ proposal & reject", async () => { + const configBuilder = createConfigTransaction({ + connection, + multisig: multisigPda, + creator: members.almighty.publicKey, + actions: [ + ConfigActions.SetTimeLock(300), + ConfigActions.SetRentCollector(members.almighty.publicKey), + ], + programId, + }); + await configBuilder.withProposal(); + configBuilder.withRejection({ member: members.almighty.publicKey }); + + const signature = await configBuilder.sendAndConfirm({ + signers: [members.almighty], + }); + + assert.ok(signature); + }); - configTransactionPda = configBuilder.getTransactionKey(); + it("should get config transaction account", async () => { + const configBuilder = createConfigTransaction({ + connection, + multisig: multisigPda, + creator: members.proposer.publicKey, + actions: [ConfigActions.SetTimeLock(10)], + programId, + }); const signature = await configBuilder.sendAndConfirm({ signers: [members.proposer], }); assert.ok(signature); + + const account = await configBuilder.getTransactionAccount( + configTransactionPda + ); + + assert.ok(account instanceof multisig.accounts.ConfigTransaction); + }); + //endregion + + //region Batches + it("should create a batch", async () => { + const batchBuilder = createBatch({ + connection, + creator: members.almighty.publicKey, + multisig: multisigPda, + programId, + }); + + const signature = await batchBuilder.sendAndConfirm({ + feePayer: members.almighty, + }); + + assert.ok(signature); + }); + + it("should create a batch with proposal", async () => { + const batchBuilder = createBatch({ + connection, + creator: members.almighty.publicKey, + multisig: multisigPda, + programId, + }); + + batchPda = await batchBuilder.getBatchKey(); + + await batchBuilder.withProposal(); + + const signature = await batchBuilder.sendAndConfirm({ + feePayer: members.almighty, + }); + + assert.ok(signature); + }); + + it("should create a batch with proposal & approval", async () => { + const batchBuilder = createBatch({ + connection, + creator: members.almighty.publicKey, + multisig: multisigPda, + programId, + }); + + await batchBuilder.withProposal(); + batchBuilder.withApproval({ member: members.almighty.publicKey }); + + const signature = await batchBuilder.sendAndConfirm({ + feePayer: members.almighty, + }); + + assert.ok(signature); + }); + + it("should create a batch with proposal & reject", async () => { + const batchBuilder = createBatch({ + connection, + creator: members.almighty.publicKey, + multisig: multisigPda, + programId, + }); + + await batchBuilder.withProposal(); + batchBuilder.withRejection({ member: members.almighty.publicKey }); + + const signature = await batchBuilder.sendAndConfirm({ + feePayer: members.almighty, + }); + + assert.ok(signature); + }); + + it("should create a batch & add a 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 batchBuilder = createBatch({ + connection, + creator: members.almighty.publicKey, + multisig: multisigPda, + programId, + }); + + await batchBuilder.withProposal({ isDraft: true }); + await batchBuilder.addTransaction({ + message, + member: members.almighty.publicKey, + }); + + const signature = await batchBuilder.sendAndConfirm({ + feePayer: members.almighty, + }); + + assert.ok(signature); + }); + + it("should get batch account", async () => { + const batchBuilder = createBatch({ + connection, + creator: members.almighty.publicKey, + multisig: multisigPda, + programId, + }); + + const signature = await batchBuilder.sendAndConfirm({ + feePayer: members.almighty, + }); + + assert.ok(signature); + + const account = await batchBuilder.getBatchAccount(batchPda); + + assert.ok(account instanceof multisig.accounts.Batch); + }); + + it("should get batch transaction account", 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 batchBuilder = createBatch({ + connection, + creator: members.almighty.publicKey, + multisig: multisigPda, + programId, + }); + + await batchBuilder.withProposal({ isDraft: true }); + await batchBuilder.addTransaction({ + message, + member: members.almighty.publicKey, + }); + + const innerIndex = await batchBuilder.getInnerIndex(); + const transactionKey = await batchBuilder.getBatchTransactionKey( + innerIndex - 1 + ); + + const signature = await batchBuilder.sendAndConfirm({ + feePayer: members.almighty, + }); + + assert.ok(signature); + + const account = await batchBuilder.getBatchTransactionAccount( + transactionKey + ); + + assert.ok(account instanceof multisig.accounts.VaultBatchTransaction); }); + //endregion + //region Account checks it("is this a multisig?", async () => { const get = await isMultisig(connection, multisigPda); @@ -164,7 +620,9 @@ describe("Examples / End2End Actions", () => { assert.ok(get); }); + //endregion + //region Complete actions it("should create, vote on & execute a vault transaction", async () => { const message = new TransactionMessage({ payerKey: members.almighty.publicKey, @@ -195,7 +653,7 @@ describe("Examples / End2End Actions", () => { }); await txBuilder.withProposal(); - txBuilder.withApproval(members.almighty.publicKey); + txBuilder.withApproval({ member: members.almighty.publicKey }); const signature = await txBuilder.sendAndConfirm({ signers: [members.almighty], @@ -205,7 +663,7 @@ describe("Examples / End2End Actions", () => { assert.ok(signature); - await txBuilder.withExecute(members.almighty.publicKey); + await txBuilder.withExecute({ member: members.almighty.publicKey }); const signature2 = await txBuilder.sendAndConfirm({ signers: [members.almighty], From 960ff2e8aad1687a3e322f2fb43aa974f7cd5653 Mon Sep 17 00:00:00 2001 From: Joey Meere <100378695+joeymeere@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:13:22 -0400 Subject: [PATCH 9/9] chore(docs): refine typedocs & tests --- sdk/multisig/src/actions/common/base.ts | 6 +- .../src/actions/common/baseTransaction.ts | 4 +- sdk/multisig/src/actions/createBatch.ts | 76 ++++++---- .../src/actions/createConfigTransaction.ts | 52 +++++-- sdk/multisig/src/actions/createMultisig.ts | 133 ++++++++++++++++-- .../src/actions/createVaultTransaction.ts | 53 +++++-- tests/suites/examples/actions.ts | 66 +++------ 7 files changed, 269 insertions(+), 121 deletions(-) diff --git a/sdk/multisig/src/actions/common/base.ts b/sdk/multisig/src/actions/common/base.ts index d56d9899..f2bbc152 100644 --- a/sdk/multisig/src/actions/common/base.ts +++ b/sdk/multisig/src/actions/common/base.ts @@ -69,7 +69,7 @@ export abstract class BaseBuilder< /** * Creates a `VersionedTransaction` containing the corresponding instruction(s). * - * @args `BuildTransactionSettings` - **(Optional)** Address Lookup Table accounts, signers, a custom fee-payer to add to the transaction. + * @args {@link BuildTransactionSettings} - **(Optional)** Address Lookup Table accounts, signers, a custom fee-payer to add to the transaction. * @returns `VersionedTransaction` * * @example @@ -110,7 +110,7 @@ export abstract class BaseBuilder< * * **NOTE: Not wallet-adapter compatible.** * - * @args `settings` - Optional pre/post instructions, fee payer keypair, and send options. + * @args {@link SendSettings} - Optional pre/post instructions, fee payer, and send options. * @returns `TransactionSignature` * @example * const builder = createMultisig({ @@ -170,7 +170,7 @@ export abstract class BaseBuilder< * * **NOTE: Not wallet-adapter compatible.** * - * @args `settings` - Optional pre/post instructions, fee payer keypair, and send options. + * @args {@link SendSettings} - Optional pre/post instructions, fee payer keypair, and send options. * @returns `TransactionSignature` * @example * const builder = createMultisig({ diff --git a/sdk/multisig/src/actions/common/baseTransaction.ts b/sdk/multisig/src/actions/common/baseTransaction.ts index 13a7602c..6d94a89f 100644 --- a/sdk/multisig/src/actions/common/baseTransaction.ts +++ b/sdk/multisig/src/actions/common/baseTransaction.ts @@ -38,7 +38,7 @@ export abstract class BaseTransactionBuilder< } /** - * Fetches the `PublicKey` of the corresponding proposal account for the transaction being built. + * Fetches the `PublicKey` of the corresponding {@link Proposal} account for the transaction being built. * * @returns `PublicKey` */ @@ -54,7 +54,7 @@ export abstract class BaseTransactionBuilder< } /** - * Fetches and deserializes the `Proposal` account after it is built and sent. + * Fetches and deserializes the {@link Proposal} account after it is built and sent. * @args `key` - The public key of the `Proposal` account. * @returns `Proposal` - Deserialized `Proposal` account data. */ diff --git a/sdk/multisig/src/actions/createBatch.ts b/sdk/multisig/src/actions/createBatch.ts index 0b4bd6ab..4f21abbc 100644 --- a/sdk/multisig/src/actions/createBatch.ts +++ b/sdk/multisig/src/actions/createBatch.ts @@ -24,36 +24,52 @@ import { } from "./common/proposal"; /** - * Builds an instruction to create a new Batch. - * Also includes the ability to chain instructions for creating/voting on proposals, and adding transactions, as well as sending - * a built transaction. + * Builds an instruction to create a new {@link Batch}, + * with the option to chain additional methods for adding transactions, adding proposals, voting, building transactions, and sending. * - * @param args - Object of type `CreateBatchActionArgs` that contains the necessary information to create a new VaultTransaction. - * @returns `{ instruction: TransactionInstruction, index: number }` - object with the `vaultTransactionCreate` instruction and the transaction index of the resulting VaultTransaction. + * @args {@link CreateBatchActionArgs} + * @returns - {@link BatchBuilder} or if awaited {@link CreateBatchResult} * * @example - * // Basic usage (no chaining): - * const result = await createVaultTransaction({ + * const batchBuilder = createBatch({ * connection, - * creator: creatorPublicKey, - * threshold: 2, - * members: membersList, - * timeLock: 3600, + * creator: creator, + * multisig: multisigPda, + * // Can also include vaultIndex, rentPayer, programId, and memo. + * }); + * + * // Chain proposal creations, and votes + * await batchBuilder.withProposal({ isDraft: true }); + * await batchBuilder.withApproval(); + * + * // Get instructions and the computed transaction indexes. + * const instructions = batchBuilder.getInstructions(); + * const index = batchBuilder.getIndex(); + * const innerIndex = batchBuilder.getInnerIndex(); + * + * @example + * // Run the builder async to get the result immediately. + * const result = await createBatch({ + * connection, + * creator: creator, + * multisig: multisigPda, * }); - * console.log(result.instruction); - * console.log(result.createKey); * * @example - * // Using the transaction() method: - * const transaction = await createVaultTransaction({ + * // Using the `transaction()` method: + * const transaction = await createBatch({ * // ... args * }).transaction(); * * @example - * // Using the send() method: - * const signature = await createVaultTransaction({ + * // Using the `send()` or `sendAndConfirm()` methods: + * const signature = await createBatch({ * // ... args - * }).send(); + * }).sendAndConfirm({ + * // Options for fee-payer, pre/post instructions, and signers. + * signers: [signer1, signer2], + * options: { skipPreflight: true }, + * }); * * @throws Will throw an error if required parameters are missing or invalid. * @@ -98,7 +114,7 @@ class BatchBuilder extends BaseTransactionBuilder< } /** - * Fetches the current index of transactions inside of the `Batch` account. + * Fetches the current index of transactions inside of the {@link Batch} account. * @returns `Promise` */ async getInnerIndex(): Promise { @@ -108,7 +124,7 @@ class BatchBuilder extends BaseTransactionBuilder< } /** - * Fetches the PublicKey of the built `Batch` account. + * Fetches the PublicKey of the built {@link Batch} account. * @returns `Promise` - PublicKey of the `Batch` account. */ async getBatchKey(): Promise { @@ -124,7 +140,7 @@ class BatchBuilder extends BaseTransactionBuilder< } /** - * Fetches the PublicKey of a transaction inside of the built `Batch` account. + * Fetches the PublicKey of a transaction inside of the built {@link Batch} account. * @args `innerIndex` - Number denoting the index of the transaction inside of the batch. * @returns `Promise` - PublicKey of the `VaultBatchTransaction` account. */ @@ -143,7 +159,7 @@ class BatchBuilder extends BaseTransactionBuilder< /** * Fetches and returns an array of PublicKeys for all transactions added to the batch. - * @returns `Promise` - An array of `VaultBatchTransaction` PublicKeys. + * @returns `Promise` */ async getAllBatchTransactionKeys(): Promise { this.ensureBuilt(); @@ -164,7 +180,7 @@ class BatchBuilder extends BaseTransactionBuilder< } /** - * Fetches and deserializes the `Batch` account after it is built and sent. + * Fetches and deserializes the {@link Batch} account after it is built and sent. * @args `key` - The public key of the `Batch` account. * @returns `Batch` - Deserialized `Batch` account data. */ @@ -178,7 +194,7 @@ class BatchBuilder extends BaseTransactionBuilder< } /** - * Fetches and deserializes a `VaultBatchTransaction` account after it is added to the `Batch`. + * Fetches and deserializes a {@link VaultBatchTransaction} account after it is added to the `Batch`. * @args `key` - The public key of the `Batch` account. * @returns `VaultBatchTransaction` - Deserialized `VaultBatchTransaction` account data. */ @@ -195,8 +211,8 @@ class BatchBuilder extends BaseTransactionBuilder< } /** - * Creates a transaction containing the VaultTransaction creation instruction. - * @args feePayer - Optional signer to pay the transaction fee. + * Pushes a `batchAddTransaction` instruction to the builder. Increments the batch's inner index. + * @args `{ message: TransactionMessage, member?: PublicKey, ephemeralSigners?: number }` - Specify the `TransactionMessage` to add to the batch, the member conducting the action, and the number of ephemeral signers to include. * @returns `VersionedTransaction` with the `vaultTransactionCreate` instruction. */ async addTransaction({ @@ -310,6 +326,10 @@ class BatchBuilder extends BaseTransactionBuilder< } } +/** + * Attempts to fetch and deserialize the {@link Batch} account, and returns a boolean indicating if it was successful. + * @args `connection: Connection, key: PublicKey` - Specify a cluster connection, and the `PublicKey` of the `Batch` account. + */ export async function isBatch(connection: Connection, key: PublicKey) { try { await Batch.fromAccountAddress(connection, key); @@ -319,6 +339,10 @@ export async function isBatch(connection: Connection, key: PublicKey) { } } +/** + * Attempts to fetch and deserialize the {@link VaultBatchTransaction} account, and returns a boolean indicating if it was successful. + * @args `connection: Connection, key: PublicKey` - Specify a cluster connection, and the `PublicKey` of the `VaultBatchTransaction` account. + */ export async function isBatchTransaction( connection: Connection, key: PublicKey diff --git a/sdk/multisig/src/actions/createConfigTransaction.ts b/sdk/multisig/src/actions/createConfigTransaction.ts index 87147d7a..78911857 100644 --- a/sdk/multisig/src/actions/createConfigTransaction.ts +++ b/sdk/multisig/src/actions/createConfigTransaction.ts @@ -17,35 +17,53 @@ import { } from "./common/proposal"; /** - * Builds an instruction to create a new ConfigTransaction and returns the instruction with the corresponding transaction index. - * Can optionally chain additional methods for transactions, and sending. + * Builds an instruction to create a new {@link ConfigTransaction}, + * with the option to chain additional methods for adding proposals, voting, building transactions, and sending. * - * @param args - Object of type `CreateConfigTransactionActionArgs` that contains the necessary information to create a new ConfigTransaction. - * @returns `{ instruction: TransactionInstruction, index: number }` - object with the `configTransactionCreate` instruction and the transaction index of the resulting ConfigTransaction. + * @args {@link CreateConfigTransactionActionArgs} + * @returns - {@link ConfigTransactionBuilder} or if awaited {@link CreateConfigTransactionResult} * * @example - * // Basic usage (no chaining): + * const configBuilder = createConfigTransaction({ + * connection, + * multisig: multisigPda, + * creator: creator, + * actions: [ConfigActions.SetTimeLock(100)], + * // Can also include rentPayer, programId, and memo. + * }); + * + * // Chain proposal creations, and votes + * await configBuilder.withProposal(); + * await configBuilder.withApproval(); + * + * // Get instructions and the computed transaction index. + * const instructions = configBuilder.getInstructions(); + * const index = configBuilder.getIndex(); + * + * @example + * // Run the builder async to get the result immediately. * const result = await createConfigTransaction({ * connection, - * creator: creatorPublicKey, - * threshold: 2, - * members: membersList, - * timeLock: 3600, + * multisig: multisigPda, + * creator: creator, + * actions: [ConfigActions.SetTimeLock(100)], * }); - * console.log(result.instruction); - * console.log(result.createKey); * * @example - * // Using the transaction() method: + * // Using the `transaction()` method: * const transaction = await createConfigTransaction({ * // ... args * }).transaction(); * * @example - * // Using the rpc() method: + * // Using the `send()` or `sendAndConfirm()` methods: * const signature = await createConfigTransaction({ * // ... args - * }).send(); + * }).sendAndConfirm({ + * // Options for fee-payer, pre/post instructions, and signers. + * signers: [signer1, signer2], + * options: { skipPreflight: true }, + * }); * * @throws Will throw an error if required parameters are missing or invalid. * @@ -85,7 +103,7 @@ class ConfigTransactionBuilder extends BaseTransactionBuilder< } /** - * Fetches deserialized account data for the corresponding `ConfigTransaction` account after it is built and sent. + * Fetches deserialized account data for the corresponding {@link ConfigTransaction} account after it is built and sent. * * @returns `ConfigTransaction` */ @@ -184,6 +202,10 @@ class ConfigTransactionBuilder extends BaseTransactionBuilder< } } +/** + * Attempts to fetch and deserialize the {@link ConfigTransaction} account, and returns a boolean indicating if it was successful. + * @args `connection: Connection, key: PublicKey` - Specify a cluster connection, and the `PublicKey` of the `ConfigTransaction` account. + */ export async function isConfigTransaction( connection: Connection, key: PublicKey diff --git a/sdk/multisig/src/actions/createMultisig.ts b/sdk/multisig/src/actions/createMultisig.ts index 7e2f5aee..a3d389c8 100644 --- a/sdk/multisig/src/actions/createMultisig.ts +++ b/sdk/multisig/src/actions/createMultisig.ts @@ -18,37 +18,57 @@ import { import { BaseBuilder } from "./common/base"; /** - * Builds an instruction to create a new Multisig, and returns the instruction and `createKey` + * Builds an instruction to create a new {@link Multisig}, * with the option to chain additional methods for building transactions, and sending. * - * @param args - Object of type `CreateMultisigActionArgs` that contains the necessary information to create a new multisig. - * @returns `{ instruction: TransactionInstruction, createKey: Keypair }` - object with the `multisigCreateV2` instruction and the `createKey` that is required to sign the transaction. + * @args {@link CreateMultisigActionArgs} + * @returns - {@link MultisigBuilder} or if awaited {@link CreateMultisigResult} * * @example - * // Basic usage (no chaining): - * const builder = await createMultisig({ + * const builder = createMultisig({ * connection, - * creator: creatorPublicKey, - * threshold: 2, - * members: membersList, + * creator: creator, + * threshold: 1, + * members: createMembers([ + * { + * key: creator, permissions: SquadPermissions.All + * }, + * ]), + * // Can also include timeLock, configAuthority, rentCollector, and programId. * }); * - * const instructions = result.instructions; - * const createKey = result.createKey; + * // Get the built instructions and the generated createKey. + * const instructions = builder.getInstructions(); + * const createKey = builder.getCreateKey(); * - * const signature = await builder.sendAndConfirm(); + * @example + * // Run the builder async to get the result immediately. + * const result = await createMultisig({ + * connection, + * creator: creator, + * threshold: 1, + * members: createMembers([ + * { + * key: creator, permissions: SquadPermissions.All + * }, + * ]), + * }); * * @example - * // Using the transaction() method: + * // Using the `transaction()` method: * const transaction = await createMultisig({ * // ... args * }).transaction(); * * @example - * // Using the send() method: + * // Using the `send()` or `sendAndConfirm()` methods: * const signature = await createMultisig({ * // ... args - * }).send(); + * }).sendAndConfirm({ + * // Options for fee-payer, pre/post instructions, and signers. + * signers: [signer1, signer2], + * options: { skipPreflight: true }, + * }); * * @throws Will throw an error if required parameters are missing or invalid. * @@ -98,16 +118,28 @@ class MultisigBuilder extends BaseBuilder< this.multisigKey = result.multisigKey; } + /** + * Fetches the generated `createKey` used to generate the {@link Multisig} PDA. + * @returns `Keypair` + */ async getCreateKey(): Promise { await this.ensureBuilt(); return this.createKey!; } + /** + * Fetches the generated {@link Multisig} PDA. + * @returns `PublicKey` + */ async getMultisigKey(): Promise { await this.ensureBuilt(); return this.multisigKey; } + /** + * Fetches deserialized account data for the corresponding {@link Multisig} account after it is built and sent. + * @returns `Multisig` + */ async getMultisigAccount(key: PublicKey) { await this.ensureBuilt(); const multisigAccount = await Multisig.fromAccountAddress( @@ -118,6 +150,25 @@ class MultisigBuilder extends BaseBuilder< return multisigAccount; } + /** + * Creates a `VersionedTransaction` containing the corresponding instruction(s), and signs it with the generated `createKey`. + * + * @args {@link BuildTransactionSettings} - **(Optional)** Address Lookup Table accounts, signers, a custom fee-payer to add to the transaction. + * @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( settings?: BuildTransactionSettings ): Promise { @@ -133,6 +184,31 @@ class MultisigBuilder extends BaseBuilder< return await super.transaction(settings); } + /** + * Builds a transaction with the corresponding instruction(s), signs it with the generated `createKey`, and sends it. + * + * **NOTE: Not wallet-adapter compatible.** + * + * @args {@link SendSettings} - Optional pre/post instructions, fee payer, and send options. + * @returns `TransactionSignature` + * @example + * const builder = createMultisig({ + * // ... args + * }); + * const signature = await builder.send(); + * @example + * const builder = createMultisig({ + * // ... args + * }); + * + * // With settings + * const signature = await builder.send({ + * preInstructions: [...preInstructions], + * postInstructions: [...postInstructions], + * feePayer: someKeypair, + * options: { skipPreflight: true }, + * }); + */ async send(settings?: SendSettings): Promise { await this.ensureBuilt(); if (settings?.signers) { @@ -146,6 +222,31 @@ class MultisigBuilder extends BaseBuilder< return await super.send(settings); } + /** + * Builds a transaction with the corresponding instruction(s), signs it with the generated `createKey`, sends it, and confirms the transaction. + * + * **NOTE: Not wallet-adapter compatible.** + * + * @args {@link SendSettings} - Optional pre/post instructions, fee payer keypair, and send options. + * @returns `TransactionSignature` + * @example + * const builder = createMultisig({ + * // ... args + * }); + * const signature = await builder.sendAndConfirm(); + * @example + * const builder = createMultisig({ + * // ... args + * }); + * + * // With settings + * const signature = await builder.sendAndConfirm({ + * preInstructions: [...preInstructions], + * postInstructions: [...postInstructions], + * feePayer: someKeypair, + * options: { skipPreflight: true }, + * }); + */ async sendAndConfirm(settings?: SendSettings): Promise { await this.ensureBuilt(); if (settings?.signers) { @@ -160,6 +261,10 @@ class MultisigBuilder extends BaseBuilder< } } +/** + * Attempts to fetch and deserialize the {@link Multisig} account, and returns a boolean indicating if it was successful. + * @args `connection: Connection, key: PublicKey` - Specify a cluster connection, and the `PublicKey` of the `Multisig` account. + */ export async function isMultisig(connection: Connection, key: PublicKey) { try { await Multisig.fromAccountAddress(connection, key); diff --git a/sdk/multisig/src/actions/createVaultTransaction.ts b/sdk/multisig/src/actions/createVaultTransaction.ts index 29e79ba4..4454bb67 100644 --- a/sdk/multisig/src/actions/createVaultTransaction.ts +++ b/sdk/multisig/src/actions/createVaultTransaction.ts @@ -23,35 +23,54 @@ import { } from "./common/proposal"; /** - * Builds an instruction to create a new VaultTransaction and returns the instruction with the corresponding transaction index. - * Can optionally chain additional methods for transactions, and sending. + * Builds an instruction to create a new {@link VaultTransaction}, + * with the option to chain additional methods for adding proposals, voting, building transactions, and sending. * - * @param args - Object of type `CreateVaultTransactionActionArgs` that contains the necessary information to create a new VaultTransaction. - * @returns `{ instruction: TransactionInstruction, index: number }` - object with the `vaultTransactionCreate` instruction and the transaction index of the resulting VaultTransaction. + * @args {@link CreateVaultTransactionActionArgs} + * @returns - {@link VaultTransactionBuilder} or if awaited {@link CreateVaultTransactionResult} * * @example - * // Basic usage (no chaining): + * const txBuilder = createVaultTransaction({ + * connection, + * multisig: multisigPda, + * creator: creator, + * message: message, + * // Can also include ephemeral signers, vaultIndex, + * // rentPayer, programId, and memo. + * }); + * + * // Chain proposal creations, and votes + * await txBuilder.withProposal(); + * await txBuilder.withApproval(); + * + * // Get the built instructions and the computed transaction index. + * const instructions = txBuilder.getInstructions(); + * const index = txBuilder.getIndex(); + * + * @example + * // Run the builder async to get the result immediately. * const result = await createVaultTransaction({ * connection, - * creator: creatorPublicKey, - * threshold: 2, - * members: membersList, - * timeLock: 3600, + * multisig: multisigPda, + * creator: creator, + * message: message, * }); - * console.log(result.instruction); - * console.log(result.createKey); * * @example - * // Using the transaction() method: + * // Using the `transaction()` method: * const transaction = await createVaultTransaction({ * // ... args * }).transaction(); * * @example - * // Using the send() method: + * // Using the `send()` or `sendAndConfirm()` methods: * const signature = await createVaultTransaction({ * // ... args - * }).send(); + * }).sendAndConfirm({ + * // Options for fee-payer, pre/post instructions, and signers. + * signers: [signer1, signer2], + * options: { skipPreflight: true }, + * }); * * @throws Will throw an error if required parameters are missing or invalid. * @@ -105,7 +124,7 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< } /** - * Fetches deserialized account data for the corresponding `VaultTransaction` account after it is built and sent. + * Fetches deserialized account data for the corresponding {@link VaultTransaction} account after it is built and sent. * * @returns `VaultTransaction` */ @@ -218,6 +237,10 @@ class VaultTransactionBuilder extends BaseTransactionBuilder< } } +/** + * Attempts to fetch and deserialize the {@link VaultTransaction} account, and returns a boolean indicating if it was successful. + * @args `connection: Connection, key: PublicKey` - Specify a cluster connection, and the `PublicKey` of the `VaultTransaction` account. + */ export async function isVaultTransaction( connection: Connection, key: PublicKey diff --git a/tests/suites/examples/actions.ts b/tests/suites/examples/actions.ts index d11088d9..2d9593cb 100644 --- a/tests/suites/examples/actions.ts +++ b/tests/suites/examples/actions.ts @@ -25,6 +25,7 @@ import { isVaultTransaction, isConfigTransaction, isMultisig, + isBatch, } from "@sqds/multisig"; import assert from "assert"; import { SquadPermissions } from "@sqds/multisig"; @@ -50,7 +51,7 @@ describe("Examples / End2End Actions", () => { let transactionPda: PublicKey; let configTransactionPda: PublicKey; let batchPda: PublicKey; - let batchWithTx: PublicKey; + let batchTxPda: PublicKey; let members: TestMembers; let outsider: Keypair; @@ -530,6 +531,9 @@ describe("Examples / End2End Actions", () => { member: members.almighty.publicKey, }); + const innerIndex = await batchBuilder.getInnerIndex(); + batchTxPda = await batchBuilder.getBatchTransactionKey(innerIndex - 1); + const signature = await batchBuilder.sendAndConfirm({ feePayer: members.almighty, }); @@ -555,51 +559,6 @@ describe("Examples / End2End Actions", () => { assert.ok(account instanceof multisig.accounts.Batch); }); - - it("should get batch transaction account", 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 batchBuilder = createBatch({ - connection, - creator: members.almighty.publicKey, - multisig: multisigPda, - programId, - }); - - await batchBuilder.withProposal({ isDraft: true }); - await batchBuilder.addTransaction({ - message, - member: members.almighty.publicKey, - }); - - const innerIndex = await batchBuilder.getInnerIndex(); - const transactionKey = await batchBuilder.getBatchTransactionKey( - innerIndex - 1 - ); - - const signature = await batchBuilder.sendAndConfirm({ - feePayer: members.almighty, - }); - - assert.ok(signature); - - const account = await batchBuilder.getBatchTransactionAccount( - transactionKey - ); - - assert.ok(account instanceof multisig.accounts.VaultBatchTransaction); - }); //endregion //region Account checks @@ -620,6 +579,21 @@ describe("Examples / End2End Actions", () => { assert.ok(get); }); + + it("is this a batch?", async () => { + const get = await isBatch(connection, batchPda); + + assert.ok(get); + }); + + /* + // WIP + it("is this a batch transaction?", async () => { + const get = await isBatchTransaction(connection, batchTxPda); + + assert.ok(get); + }); + */ //endregion //region Complete actions