UNPKG

snapper-sdk

Version:

An SDK for building applications on top of Snapper.

993 lines (921 loc) 37.1 kB
import { Commitment, Connection, PublicKey, sendAndConfirmTransaction, Signer, Transaction, TransactionInstruction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js"; import axios from "axios"; import { Api } from "../../api"; import { ComputeBudgetConfig, SignAllTransactions } from "../../raydium/type"; import { Cluster } from "../../solana"; import { Owner } from "../owner"; import { CacheLTA, getMultipleLookupTableInfo, LOOKUP_TABLE_CACHE } from "./lookupTable"; import { TxVersion } from "./txType"; import { addComputeBudget, checkLegacyTxSize, checkV0TxSize, confirmTransaction, getRecentBlockHash, printSimulate, } from "./txUtils"; interface SolanaFeeInfo { min: number; max: number; avg: number; priorityTx: number; nonVotes: number; priorityRatio: number; avgCuPerBlock: number; blockspaceUsageRatio: number; } type SolanaFeeInfoJson = { "1": SolanaFeeInfo; "5": SolanaFeeInfo; "15": SolanaFeeInfo; }; interface ExecuteParams { skipPreflight?: boolean; recentBlockHash?: string; sendAndConfirm?: boolean; } interface TxBuilderInit { connection: Connection; feePayer: PublicKey; cluster: Cluster; owner?: Owner; blockhashCommitment?: Commitment; api?: Api; signAllTransactions?: SignAllTransactions; } export interface AddInstructionParam { addresses?: Record<string, PublicKey>; instructions?: TransactionInstruction[]; endInstructions?: TransactionInstruction[]; lookupTableAddress?: string[]; signers?: Signer[]; instructionTypes?: string[]; endInstructionTypes?: string[]; } export interface TxBuildData<T = Record<string, any>> { builder: TxBuilder; transaction: Transaction; instructionTypes: string[]; signers: Signer[]; execute: (params?: ExecuteParams) => Promise<{ txId: string; signedTx: Transaction }>; extInfo: T; } export interface TxV0BuildData<T = Record<string, any>> extends Omit<TxBuildData<T>, "transaction" | "execute"> { builder: TxBuilder; transaction: VersionedTransaction; buildProps?: { lookupTableCache?: CacheLTA; lookupTableAddress?: string[]; }; execute: (params?: ExecuteParams) => Promise<{ txId: string; signedTx: VersionedTransaction }>; } type TxUpdateParams = { txId: string; status: "success" | "error" | "sent"; signedTx: Transaction | VersionedTransaction; }; export interface MultiTxExecuteParam extends ExecuteParams { sequentially: boolean; onTxUpdate?: (completeTxs: TxUpdateParams[]) => void; } export interface MultiTxBuildData<T = Record<string, any>> { builder: TxBuilder; transactions: Transaction[]; instructionTypes: string[]; signers: Signer[][]; execute: (executeParams?: MultiTxExecuteParam) => Promise<{ txIds: string[]; signedTxs: Transaction[] }>; extInfo: T; } export interface MultiTxV0BuildData<T = Record<string, any>> extends Omit<MultiTxBuildData<T>, "transactions" | "execute"> { builder: TxBuilder; transactions: VersionedTransaction[]; buildProps?: { lookupTableCache?: CacheLTA; lookupTableAddress?: string[]; }; execute: (executeParams?: MultiTxExecuteParam) => Promise<{ txIds: string[]; signedTxs: VersionedTransaction[] }>; } export type MakeMultiTxData<T = TxVersion.LEGACY, O = Record<string, any>> = T extends TxVersion.LEGACY ? MultiTxBuildData<O> : MultiTxV0BuildData<O>; export type MakeTxData<T = TxVersion.LEGACY, O = Record<string, any>> = T extends TxVersion.LEGACY ? TxBuildData<O> : TxV0BuildData<O>; export class TxBuilder { private connection: Connection; private owner?: Owner; private instructions: TransactionInstruction[] = []; private endInstructions: TransactionInstruction[] = []; private lookupTableAddress: string[] = []; private signers: Signer[] = []; private instructionTypes: string[] = []; private endInstructionTypes: string[] = []; private feePayer: PublicKey; private cluster: Cluster; private signAllTransactions?: SignAllTransactions; private blockhashCommitment?: Commitment; private api?: Api; constructor(params: TxBuilderInit) { this.connection = params.connection; this.feePayer = params.feePayer; this.signAllTransactions = params.signAllTransactions; this.owner = params.owner; this.cluster = params.cluster; this.blockhashCommitment = params.blockhashCommitment; this.api = params.api; } get AllTxData(): { instructions: TransactionInstruction[]; endInstructions: TransactionInstruction[]; signers: Signer[]; instructionTypes: string[]; endInstructionTypes: string[]; lookupTableAddress: string[]; } { return { instructions: this.instructions, endInstructions: this.endInstructions, signers: this.signers, instructionTypes: this.instructionTypes, endInstructionTypes: this.endInstructionTypes, lookupTableAddress: this.lookupTableAddress, }; } get allInstructions(): TransactionInstruction[] { return [...this.instructions, ...this.endInstructions]; } public async getComputeBudgetConfig(): Promise<ComputeBudgetConfig | undefined> { const json = ( await axios.get<SolanaFeeInfoJson>(`https://solanacompass.com/api/fees?cacheFreshTime=${5 * 60 * 1000}`) ).data; const { avg } = json?.[15] ?? {}; if (!avg) return undefined; return { units: 600000, microLamports: Math.min(Math.ceil((avg * 1000000) / 600000), 25000), }; } public addCustomComputeBudget(config?: ComputeBudgetConfig): boolean { if (config) { const { instructions, instructionTypes } = addComputeBudget(config); this.instructions.unshift(...instructions); this.instructionTypes.unshift(...instructionTypes); return true; } return false; } public async calComputeBudget({ config: propConfig, defaultIns, }: { config?: ComputeBudgetConfig; defaultIns?: TransactionInstruction[]; }): Promise<void> { try { const config = propConfig || (await this.getComputeBudgetConfig()); if (this.addCustomComputeBudget(config)) return; defaultIns && this.instructions.unshift(...defaultIns); } catch { defaultIns && this.instructions.unshift(...defaultIns); } } public addInstruction({ instructions = [], endInstructions = [], signers = [], instructionTypes = [], endInstructionTypes = [], lookupTableAddress = [], }: AddInstructionParam): TxBuilder { this.instructions.push(...instructions); this.endInstructions.push(...endInstructions); this.signers.push(...signers); this.instructionTypes.push(...instructionTypes); this.endInstructionTypes.push(...endInstructionTypes); this.lookupTableAddress.push(...lookupTableAddress.filter((address) => address !== PublicKey.default.toString())); return this; } public async versionBuild<O = Record<string, any>>({ txVersion, extInfo, }: { txVersion?: TxVersion; extInfo?: O; }): Promise<MakeTxData<TxVersion.LEGACY, O> | MakeTxData<TxVersion.V0, O>> { if (txVersion === TxVersion.V0) return (await this.buildV0({ ...(extInfo || {}) })) as MakeTxData<TxVersion.V0, O>; return this.build<O>(extInfo) as MakeTxData<TxVersion.LEGACY, O>; } public build<O = Record<string, any>>(extInfo?: O): MakeTxData<TxVersion.LEGACY, O> { const transaction = new Transaction(); if (this.allInstructions.length) transaction.add(...this.allInstructions); transaction.feePayer = this.feePayer; if (this.owner?.signer && !this.signers.some((s) => s.publicKey.equals(this.owner!.publicKey))) this.signers.push(this.owner.signer); return { builder: this, transaction, signers: this.signers, instructionTypes: [...this.instructionTypes, ...this.endInstructionTypes], execute: async (params) => { const { recentBlockHash: propBlockHash, skipPreflight = true, sendAndConfirm } = params || {}; const recentBlockHash = propBlockHash ?? (await getRecentBlockHash(this.connection, this.blockhashCommitment)); transaction.recentBlockhash = recentBlockHash; if (this.signers.length) transaction.sign(...this.signers); printSimulate([transaction]); if (this.owner?.isKeyPair) { const txId = sendAndConfirm ? await sendAndConfirmTransaction( this.connection, transaction, this.signers.find((s) => s.publicKey.equals(this.owner!.publicKey)) ? this.signers : [...this.signers, this.owner.signer!], { skipPreflight }, ) : await this.connection.sendRawTransaction(transaction.serialize(), { skipPreflight }); return { txId, signedTx: transaction, }; } if (this.signAllTransactions) { const txs = await this.signAllTransactions([transaction]); return { txId: await this.connection.sendRawTransaction(txs[0].serialize(), { skipPreflight }), signedTx: txs[0], }; } throw new Error("please provide owner in keypair format or signAllTransactions function"); }, extInfo: extInfo || ({} as O), }; } public buildMultiTx<T = Record<string, any>>(params: { extraPreBuildData?: MakeTxData<TxVersion.LEGACY>[]; extInfo?: T; }): MultiTxBuildData { const { extraPreBuildData = [], extInfo } = params; const { transaction } = this.build(extInfo); const filterExtraBuildData = extraPreBuildData.filter((data) => data.transaction.instructions.length > 0); const allTransactions: Transaction[] = [transaction, ...filterExtraBuildData.map((data) => data.transaction)]; const allSigners: Signer[][] = [this.signers, ...filterExtraBuildData.map((data) => data.signers)]; const allInstructionTypes: string[] = [ ...this.instructionTypes, ...filterExtraBuildData.map((data) => data.instructionTypes).flat(), ]; if (this.owner?.signer) { allSigners.forEach((signers) => { if (!signers.some((s) => s.publicKey.equals(this.owner!.publicKey))) this.signers.push(this.owner!.signer!); }); } return { builder: this, transactions: allTransactions, signers: allSigners, instructionTypes: allInstructionTypes, execute: async (executeParams?: MultiTxExecuteParam) => { const { sequentially, onTxUpdate, recentBlockHash: propBlockHash, skipPreflight = true } = executeParams || {}; const recentBlockHash = propBlockHash ?? (await getRecentBlockHash(this.connection, this.blockhashCommitment)); if (this.owner?.isKeyPair) { if (sequentially) { const txIds: string[] = []; for (const tx of allTransactions) { const txId = await sendAndConfirmTransaction( this.connection, tx, this.signers.find((s) => s.publicKey.equals(this.owner!.publicKey)) ? this.signers : [...this.signers, this.owner.signer!], { skipPreflight }, ); txIds.push(txId); } return { txIds, signedTxs: allTransactions, }; } return { txIds: await await Promise.all( allTransactions.map(async (tx) => { tx.recentBlockhash = recentBlockHash; return await this.connection.sendRawTransaction(tx.serialize(), { skipPreflight }); }), ), signedTxs: allTransactions, }; } if (this.signAllTransactions) { const partialSignedTxs = allTransactions.map((tx, idx) => { tx.recentBlockhash = recentBlockHash; if (allSigners[idx].length) tx.sign(...allSigners[idx]); return tx; }); printSimulate(partialSignedTxs); const signedTxs = await this.signAllTransactions(partialSignedTxs); if (sequentially) { let i = 0; const processedTxs: TxUpdateParams[] = []; const checkSendTx = async (): Promise<void> => { if (!signedTxs[i]) return; const txId = await this.connection.sendRawTransaction(signedTxs[i].serialize(), { skipPreflight }); processedTxs.push({ txId, status: "sent", signedTx: signedTxs[i] }); onTxUpdate?.([...processedTxs]); i++; this.connection.onSignature( txId, (signatureResult) => { const targetTxIdx = processedTxs.findIndex((tx) => tx.txId === txId); if (targetTxIdx > -1) processedTxs[targetTxIdx].status = signatureResult.err ? "error" : "success"; onTxUpdate?.([...processedTxs]); if (!signatureResult.err) checkSendTx(); }, "processed", ); this.connection.getSignatureStatus(txId); }; await checkSendTx(); return { txIds: processedTxs.map((d) => d.txId), signedTxs, }; } else { const txIds: string[] = []; for (let i = 0; i < signedTxs.length; i += 1) { const txId = await this.connection.sendRawTransaction(signedTxs[i].serialize(), { skipPreflight }); txIds.push(txId); } return { txIds, signedTxs, }; } } throw new Error("please provide owner in keypair format or signAllTransactions function"); }, extInfo: extInfo || {}, }; } public async versionMultiBuild<T extends TxVersion, O = Record<string, any>>({ extraPreBuildData, txVersion, extInfo, }: { extraPreBuildData?: MakeTxData<TxVersion.V0>[] | MakeTxData<TxVersion.LEGACY>[]; txVersion?: T; extInfo?: O; }): Promise<MakeMultiTxData<T, O>> { if (txVersion === TxVersion.V0) return (await this.buildV0MultiTx({ extraPreBuildData: extraPreBuildData as MakeTxData<TxVersion.V0>[], buildProps: extInfo || {}, })) as MakeMultiTxData<T, O>; return this.buildMultiTx<O>({ extraPreBuildData: extraPreBuildData as MakeTxData<TxVersion.LEGACY>[], extInfo, }) as MakeMultiTxData<T, O>; } public async buildV0<O = Record<string, any>>( props?: O & { lookupTableCache?: CacheLTA; lookupTableAddress?: string[]; forerunCreate?: boolean; recentBlockhash?: string; }, ): Promise<MakeTxData<TxVersion.V0, O>> { const { lookupTableCache = {}, lookupTableAddress = [], forerunCreate, recentBlockhash: propRecentBlockhash, ...extInfo } = props || {}; const lookupTableAddressAccount = { ...(this.cluster === "devnet" ? {} : LOOKUP_TABLE_CACHE), ...lookupTableCache, }; const allLTA = Array.from(new Set<string>([...lookupTableAddress, ...this.lookupTableAddress])); const needCacheLTA: PublicKey[] = []; for (const item of allLTA) { if (lookupTableAddressAccount[item] === undefined) needCacheLTA.push(new PublicKey(item)); } const newCacheLTA = await getMultipleLookupTableInfo({ connection: this.connection, address: needCacheLTA }); for (const [key, value] of Object.entries(newCacheLTA)) lookupTableAddressAccount[key] = value; const recentBlockhash = forerunCreate ? PublicKey.default.toBase58() : propRecentBlockhash ?? (await getRecentBlockHash(this.connection, this.blockhashCommitment)); const messageV0 = new TransactionMessage({ payerKey: this.feePayer, recentBlockhash, instructions: [...this.allInstructions], }).compileToV0Message(Object.values(lookupTableAddressAccount)); if (this.owner?.signer && !this.signers.some((s) => s.publicKey.equals(this.owner!.publicKey))) this.signers.push(this.owner.signer); const transaction = new VersionedTransaction(messageV0); transaction.sign(this.signers); return { builder: this, transaction, signers: this.signers, instructionTypes: [...this.instructionTypes, ...this.endInstructionTypes], execute: async (params) => { const { skipPreflight = true, sendAndConfirm } = params || {}; printSimulate([transaction]); if (this.owner?.isKeyPair) { const txId = await this.connection.sendTransaction(transaction, { skipPreflight }); if (sendAndConfirm) { await confirmTransaction(this.connection, txId); } return { txId, signedTx: transaction, }; } if (this.signAllTransactions) { const txs = await this.signAllTransactions<VersionedTransaction>([transaction]); return { txId: await this.connection.sendTransaction(txs[0], { skipPreflight }), signedTx: txs[0], }; } throw new Error("please provide owner in keypair format or signAllTransactions function"); }, extInfo: (extInfo || {}) as O, }; } public async buildV0MultiTx<T = Record<string, any>>(params: { extraPreBuildData?: MakeTxData<TxVersion.V0>[]; buildProps?: T & { lookupTableCache?: CacheLTA; lookupTableAddress?: string[]; forerunCreate?: boolean; recentBlockhash?: string; }; }): Promise<MultiTxV0BuildData> { const { extraPreBuildData = [], buildProps } = params; const { transaction } = await this.buildV0(buildProps); const filterExtraBuildData = extraPreBuildData.filter((data) => data.builder.instructions.length > 0); const allTransactions: VersionedTransaction[] = [ transaction, ...filterExtraBuildData.map((data) => data.transaction), ]; const allSigners: Signer[][] = [this.signers, ...filterExtraBuildData.map((data) => data.signers)]; const allInstructionTypes: string[] = [ ...this.instructionTypes, ...filterExtraBuildData.map((data) => data.instructionTypes).flat(), ]; if (this.owner?.signer) { allSigners.forEach((signers) => { if (!signers.some((s) => s.publicKey.equals(this.owner!.publicKey))) this.signers.push(this.owner!.signer!); }); } allTransactions.forEach(async (tx, idx) => { tx.sign(allSigners[idx]); }); return { builder: this, transactions: allTransactions, signers: allSigners, instructionTypes: allInstructionTypes, buildProps, execute: async (executeParams?: MultiTxExecuteParam) => { const { sequentially, onTxUpdate, recentBlockHash: propBlockHash, skipPreflight = true } = executeParams || {}; if (propBlockHash) allTransactions.forEach((tx) => (tx.message.recentBlockhash = propBlockHash)); printSimulate(allTransactions); if (this.owner?.isKeyPair) { if (sequentially) { const txIds: string[] = []; for (const tx of allTransactions) { const txId = await this.connection.sendTransaction(tx, { skipPreflight }); await confirmTransaction(this.connection, txId); txIds.push(txId); } return { txIds, signedTxs: allTransactions }; } return { txIds: await Promise.all( allTransactions.map(async (tx) => { return await this.connection.sendTransaction(tx, { skipPreflight }); }), ), signedTxs: allTransactions, }; } if (this.signAllTransactions) { const signedTxs = await this.signAllTransactions(allTransactions); if (sequentially) { let i = 0; const processedTxs: TxUpdateParams[] = []; const checkSendTx = async (): Promise<void> => { if (!signedTxs[i]) return; const txId = await this.connection.sendTransaction(signedTxs[i], { skipPreflight }); processedTxs.push({ txId, status: "sent", signedTx: signedTxs[i] }); onTxUpdate?.([...processedTxs]); i++; this.connection.onSignature( txId, (signatureResult) => { const targetTxIdx = processedTxs.findIndex((tx) => tx.txId === txId); if (targetTxIdx > -1) processedTxs[targetTxIdx].status = signatureResult.err ? "error" : "success"; onTxUpdate?.([...processedTxs]); if (!signatureResult.err) checkSendTx(); }, "processed", ); this.connection.getSignatureStatus(txId); }; checkSendTx(); return { txIds: [], signedTxs, }; } else { const txIds: string[] = []; for (let i = 0; i < signedTxs.length; i += 1) { const txId = await this.connection.sendTransaction(signedTxs[i], { skipPreflight }); txIds.push(txId); } return { txIds, signedTxs }; } } throw new Error("please provide owner in keypair format or signAllTransactions function"); }, extInfo: buildProps || {}, }; } public async sizeCheckBuild( props?: Record<string, any> & { computeBudgetConfig?: ComputeBudgetConfig }, ): Promise<MultiTxBuildData> { const { computeBudgetConfig, ...extInfo } = props || {}; const computeBudgetData: { instructions: TransactionInstruction[]; instructionTypes: string[] } = computeBudgetConfig ? addComputeBudget(computeBudgetConfig) : { instructions: [], instructionTypes: [], }; const signerKey: { [key: string]: Signer } = this.signers.reduce( (acc, cur) => ({ ...acc, [cur.publicKey.toBase58()]: cur }), {}, ); const allTransactions: Transaction[] = []; const allSigners: Signer[][] = []; let instructionQueue: TransactionInstruction[] = []; this.allInstructions.forEach((item) => { const _itemIns = [...instructionQueue, item]; const _itemInsWithCompute = computeBudgetConfig ? [...computeBudgetData.instructions, ..._itemIns] : _itemIns; const _signerStrs = new Set<string>( _itemIns.map((i) => i.keys.filter((ii) => ii.isSigner).map((ii) => ii.pubkey.toString())).flat(), ); const _signer = [..._signerStrs.values()].map((i) => new PublicKey(i)); if ( (instructionQueue.length < 12 && checkLegacyTxSize({ instructions: _itemInsWithCompute, payer: this.feePayer, signers: _signer })) || checkLegacyTxSize({ instructions: _itemIns, payer: this.feePayer, signers: _signer }) ) { // current ins add to queue still not exceed tx size limit instructionQueue.push(item); } else { if (instructionQueue.length === 0) throw Error("item ins too big"); // if add computeBudget still not exceed tx size limit if ( checkLegacyTxSize({ instructions: computeBudgetConfig ? [...computeBudgetData.instructions, ...instructionQueue] : [...instructionQueue], payer: this.feePayer, signers: _signer, }) ) { allTransactions.push(new Transaction().add(...computeBudgetData.instructions, ...instructionQueue)); } else { allTransactions.push(new Transaction().add(...instructionQueue)); } allSigners.push( Array.from( new Set<string>( instructionQueue.map((i) => i.keys.filter((ii) => ii.isSigner).map((ii) => ii.pubkey.toString())).flat(), ), ) .map((i) => signerKey[i]) .filter((i) => i !== undefined), ); instructionQueue = [item]; } }); if (instructionQueue.length > 0) { const _signerStrs = new Set<string>( instructionQueue.map((i) => i.keys.filter((ii) => ii.isSigner).map((ii) => ii.pubkey.toString())).flat(), ); const _signers = [..._signerStrs.values()].map((i) => signerKey[i]).filter((i) => i !== undefined); if ( checkLegacyTxSize({ instructions: computeBudgetConfig ? [...computeBudgetData.instructions, ...instructionQueue] : [...instructionQueue], payer: this.feePayer, signers: _signers.map((s) => s.publicKey), }) ) { allTransactions.push(new Transaction().add(...computeBudgetData.instructions, ...instructionQueue)); } else { allTransactions.push(new Transaction().add(...instructionQueue)); } allSigners.push(_signers); } allTransactions.forEach((tx) => (tx.feePayer = this.feePayer)); if (this.owner?.signer) { allSigners.forEach((signers) => { if (!signers.some((s) => s.publicKey.equals(this.owner!.publicKey))) signers.push(this.owner!.signer!); }); } return { builder: this, transactions: allTransactions, signers: allSigners, instructionTypes: this.instructionTypes, execute: async (executeParams?: MultiTxExecuteParam) => { const { sequentially, onTxUpdate, recentBlockHash: propBlockHash, skipPreflight = true } = executeParams || {}; const recentBlockHash = propBlockHash ?? (await getRecentBlockHash(this.connection, this.blockhashCommitment)); allTransactions.forEach(async (tx, idx) => { tx.recentBlockhash = recentBlockHash; if (allSigners[idx].length) tx.sign(...allSigners[idx]); }); printSimulate(allTransactions); if (this.owner?.isKeyPair) { if (sequentially) { const txIds: string[] = []; for (const tx of allTransactions) { const txId = await sendAndConfirmTransaction( this.connection, tx, this.signers.find((s) => s.publicKey.equals(this.owner!.publicKey)) ? this.signers : [...this.signers, this.owner.signer!], { skipPreflight }, ); txIds.push(txId); } return { txIds, signedTxs: allTransactions, }; } return { txIds: await Promise.all( allTransactions.map(async (tx) => { return await this.connection.sendRawTransaction(tx.serialize(), { skipPreflight }); }), ), signedTxs: allTransactions, }; } if (this.signAllTransactions) { const signedTxs = await this.signAllTransactions(allTransactions); if (sequentially) { let i = 0; const processedTxs: TxUpdateParams[] = []; const checkSendTx = async (): Promise<void> => { if (!signedTxs[i]) return; const txId = await this.connection.sendRawTransaction(signedTxs[i].serialize(), { skipPreflight }); processedTxs.push({ txId, status: "sent", signedTx: signedTxs[i] }); onTxUpdate?.([...processedTxs]); i++; this.connection.onSignature( txId, (signatureResult) => { const targetTxIdx = processedTxs.findIndex((tx) => tx.txId === txId); if (targetTxIdx > -1) processedTxs[targetTxIdx].status = signatureResult.err ? "error" : "success"; onTxUpdate?.([...processedTxs]); if (!signatureResult.err) checkSendTx(); }, "processed", ); this.connection.getSignatureStatus(txId); }; await checkSendTx(); return { txIds: processedTxs.map((d) => d.txId), signedTxs, }; } else { const txIds: string[] = []; for (let i = 0; i < signedTxs.length; i += 1) { const txId = await this.connection.sendRawTransaction(signedTxs[i].serialize(), { skipPreflight }); txIds.push(txId); } return { txIds, signedTxs }; } } throw new Error("please provide owner in keypair format or signAllTransactions function"); }, extInfo: extInfo || {}, }; } public async sizeCheckBuildV0( props?: Record<string, any> & { computeBudgetConfig?: ComputeBudgetConfig; lookupTableCache?: CacheLTA; lookupTableAddress?: string[]; }, ): Promise<MultiTxV0BuildData> { const { computeBudgetConfig, lookupTableCache = {}, lookupTableAddress = [], ...extInfo } = props || {}; const lookupTableAddressAccount = { ...(this.cluster === "devnet" ? {} : LOOKUP_TABLE_CACHE), ...lookupTableCache, }; const allLTA = Array.from(new Set<string>([...this.lookupTableAddress, ...lookupTableAddress])); const needCacheLTA: PublicKey[] = []; for (const item of allLTA) { if (lookupTableAddressAccount[item] === undefined) needCacheLTA.push(new PublicKey(item)); } const newCacheLTA = await getMultipleLookupTableInfo({ connection: this.connection, address: needCacheLTA }); for (const [key, value] of Object.entries(newCacheLTA)) lookupTableAddressAccount[key] = value; const computeBudgetData: { instructions: TransactionInstruction[]; instructionTypes: string[] } = computeBudgetConfig ? addComputeBudget(computeBudgetConfig) : { instructions: [], instructionTypes: [], }; const blockHash = await getRecentBlockHash(this.connection, this.blockhashCommitment); const signerKey: { [key: string]: Signer } = this.signers.reduce( (acc, cur) => ({ ...acc, [cur.publicKey.toBase58()]: cur }), {}, ); const allTransactions: VersionedTransaction[] = []; const allSigners: Signer[][] = []; let instructionQueue: TransactionInstruction[] = []; this.allInstructions.forEach((item) => { const _itemIns = [...instructionQueue, item]; const _itemInsWithCompute = computeBudgetConfig ? [...computeBudgetData.instructions, ..._itemIns] : _itemIns; if ( (instructionQueue.length < 12 && checkV0TxSize({ instructions: _itemInsWithCompute, payer: this.feePayer, lookupTableAddressAccount })) || checkV0TxSize({ instructions: _itemIns, payer: this.feePayer, lookupTableAddressAccount }) ) { // current ins add to queue still not exceed tx size limit instructionQueue.push(item); } else { if (instructionQueue.length === 0) throw Error("item ins too big"); const lookupTableAddress: undefined | CacheLTA = {}; for (const item of [...new Set<string>(allLTA)]) { if (lookupTableAddressAccount[item] !== undefined) lookupTableAddress[item] = lookupTableAddressAccount[item]; } // if add computeBudget still not exceed tx size limit if ( computeBudgetConfig && checkV0TxSize({ instructions: [...computeBudgetData.instructions, ...instructionQueue], payer: this.feePayer, lookupTableAddressAccount, recentBlockhash: blockHash, }) ) { const messageV0 = new TransactionMessage({ payerKey: this.feePayer, recentBlockhash: blockHash, instructions: [...computeBudgetData.instructions, ...instructionQueue], }).compileToV0Message(Object.values(lookupTableAddressAccount)); allTransactions.push(new VersionedTransaction(messageV0)); } else { const messageV0 = new TransactionMessage({ payerKey: this.feePayer, recentBlockhash: blockHash, instructions: [...instructionQueue], }).compileToV0Message(Object.values(lookupTableAddressAccount)); allTransactions.push(new VersionedTransaction(messageV0)); } allSigners.push( Array.from( new Set<string>( instructionQueue.map((i) => i.keys.filter((ii) => ii.isSigner).map((ii) => ii.pubkey.toString())).flat(), ), ) .map((i) => signerKey[i]) .filter((i) => i !== undefined), ); instructionQueue = [item]; } }); if (instructionQueue.length > 0) { const _signerStrs = new Set<string>( instructionQueue.map((i) => i.keys.filter((ii) => ii.isSigner).map((ii) => ii.pubkey.toString())).flat(), ); const _signers = [..._signerStrs.values()].map((i) => signerKey[i]).filter((i) => i !== undefined); if ( computeBudgetConfig && checkV0TxSize({ instructions: [...computeBudgetData.instructions, ...instructionQueue], payer: this.feePayer, lookupTableAddressAccount, recentBlockhash: blockHash, }) ) { const messageV0 = new TransactionMessage({ payerKey: this.feePayer, recentBlockhash: blockHash, instructions: [...computeBudgetData.instructions, ...instructionQueue], }).compileToV0Message(Object.values(lookupTableAddressAccount)); allTransactions.push(new VersionedTransaction(messageV0)); } else { const messageV0 = new TransactionMessage({ payerKey: this.feePayer, recentBlockhash: blockHash, instructions: [...instructionQueue], }).compileToV0Message(Object.values(lookupTableAddressAccount)); allTransactions.push(new VersionedTransaction(messageV0)); } allSigners.push(_signers); } if (this.owner?.signer) { allSigners.forEach((signers) => { if (!signers.some((s) => s.publicKey.equals(this.owner!.publicKey))) signers.push(this.owner!.signer!); }); } return { builder: this, transactions: allTransactions, buildProps: props, signers: allSigners, instructionTypes: this.instructionTypes, execute: async (executeParams?: MultiTxExecuteParam) => { const { sequentially, onTxUpdate, recentBlockHash: propBlockHash, skipPreflight = true } = executeParams || {}; allTransactions.map(async (tx, idx) => { if (allSigners[idx].length) tx.sign(allSigners[idx]); if (propBlockHash) tx.message.recentBlockhash = propBlockHash; }); printSimulate(allTransactions); if (this.owner?.isKeyPair) { if (sequentially) { const txIds: string[] = []; for (const tx of allTransactions) { const txId = await this.connection.sendTransaction(tx, { skipPreflight }); await confirmTransaction(this.connection, txId); txIds.push(txId); } return { txIds, signedTxs: allTransactions }; } return { txIds: await Promise.all( allTransactions.map(async (tx) => { return await this.connection.sendTransaction(tx, { skipPreflight }); }), ), signedTxs: allTransactions, }; } if (this.signAllTransactions) { const signedTxs = await this.signAllTransactions(allTransactions); if (sequentially) { let i = 0; const processedTxs: TxUpdateParams[] = []; const checkSendTx = async (): Promise<void> => { if (!signedTxs[i]) return; const txId = await this.connection.sendTransaction(signedTxs[i], { skipPreflight }); processedTxs.push({ txId, status: "sent", signedTx: signedTxs[i] }); onTxUpdate?.([...processedTxs]); i++; this.connection.onSignature( txId, (signatureResult) => { const targetTxIdx = processedTxs.findIndex((tx) => tx.txId === txId); if (targetTxIdx > -1) processedTxs[targetTxIdx].status = signatureResult.err ? "error" : "success"; onTxUpdate?.([...processedTxs]); if (!signatureResult.err) checkSendTx(); }, "processed", ); this.connection.getSignatureStatus(txId); }; checkSendTx(); return { txIds: [], signedTxs, }; } else { const txIds: string[] = []; for (let i = 0; i < signedTxs.length; i += 1) { const txId = await this.connection.sendTransaction(signedTxs[i], { skipPreflight }); txIds.push(txId); } return { txIds, signedTxs }; } } throw new Error("please provide owner in keypair format or signAllTransactions function"); }, extInfo: extInfo || {}, }; } }