UNPKG

@solana-developers/helpers

Version:
372 lines 17 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DEFAULT_SEND_OPTIONS = exports.MAX_RETRIES = exports.RETRY_INTERVAL_INCREASE = exports.RETRY_INTERVAL_MS = exports.getSimulationComputeUnits = exports.confirmTransaction = void 0; exports.hasSetComputeLimitInstruction = hasSetComputeLimitInstruction; exports.hasSetComputeUnitPriceInstruction = hasSetComputeUnitPriceInstruction; exports.sendTransaction = sendTransaction; exports.sendVersionedTransaction = sendVersionedTransaction; exports.addComputeInstructions = addComputeInstructions; exports.createLookupTable = createLookupTable; const web3_js_1 = require("@solana/web3.js"); const logs_1 = require("./logs"); const web3_js_2 = require("@solana/web3.js"); const confirmTransaction = async (connection, signature, commitment = "confirmed") => { const block = await connection.getLatestBlockhash(commitment); const rpcResponse = await connection.confirmTransaction({ signature, ...block, }, commitment); (0, logs_1.getErrorFromRPCResponse)(rpcResponse); return signature; }; exports.confirmTransaction = confirmTransaction; /** * Check if a given instruction is a SetComputeUnitLimit instruction * See https://github.com/solana-program/compute-budget/blob/main/clients/js/src/generated/programs/computeBudget.ts#L29 */ function isSetComputeLimitInstruction(ix) { return (ix.programId.equals(web3_js_1.ComputeBudgetProgram.programId) && ix.data[0] === 2 // opcode for setComputeUnitLimit is 2 ); } /** * Check if a given instruction is a SetComputeUnitLimit instruction * See https://github.com/solana-program/compute-budget/blob/main/clients/js/src/generated/programs/computeBudget.ts#L30 */ function isSetComputeUnitPriceInstruction(ix) { return (ix.programId.equals(web3_js_1.ComputeBudgetProgram.programId) && ix.data[0] === 3 // opcode for setComputeUnitPrice is 3 ); } /** * Check if a given transaction contains a SetComputeUnitLimit instruction */ function hasSetComputeLimitInstruction(instructions) { return instructions.filter(isSetComputeLimitInstruction).length === 1; } /** * Check if a given transaction contains a SetComputeUnitLimit instruction */ function hasSetComputeUnitPriceInstruction(instructions) { return instructions.filter(isSetComputeUnitPriceInstruction).length === 1; } // Was getSimulationUnits // Credit https://twitter.com/stegabob, originally from // https://x.com/stegaBOB/status/1766662289392889920 const getSimulationComputeUnits = async (connection, instructions, payer, lookupTables, commitment = "confirmed") => { const simulationInstructions = [...instructions]; // Replace or add compute limit instruction const computeLimitIndex = simulationInstructions.findIndex(isSetComputeLimitInstruction); const simulationLimitIx = web3_js_1.ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000, }); if (computeLimitIndex >= 0) { simulationInstructions[computeLimitIndex] = simulationLimitIx; } else { simulationInstructions.unshift(simulationLimitIx); } const testTransaction = new web3_js_1.VersionedTransaction(new web3_js_1.TransactionMessage({ instructions: simulationInstructions, payerKey: payer, // RecentBlockhash can by any public key during simulation // since 'replaceRecentBlockhash' is set to 'true' below recentBlockhash: web3_js_1.PublicKey.default.toString(), }).compileToV0Message(lookupTables)); const rpcResponse = await connection.simulateTransaction(testTransaction, { replaceRecentBlockhash: true, sigVerify: false, commitment, }); if (rpcResponse?.value?.err) { const logs = rpcResponse.value.logs?.join("\n • ") || "No logs available"; throw new Error(`Transaction simulation failed:\n •${logs}` + JSON.stringify(rpcResponse?.value?.err)); } return rpcResponse.value.unitsConsumed || null; }; exports.getSimulationComputeUnits = getSimulationComputeUnits; /** * Constants for transaction retry configuration */ exports.RETRY_INTERVAL_MS = 2000; exports.RETRY_INTERVAL_INCREASE = 200; exports.MAX_RETRIES = 15; /** * Default configuration values for transaction sending */ exports.DEFAULT_SEND_OPTIONS = { maxRetries: exports.MAX_RETRIES, initialDelayMs: exports.RETRY_INTERVAL_MS, commitment: "confirmed", skipPreflight: true, }; /** * Sends a transaction with compute unit optimization and automatic retries * * @param connection - The Solana connection object * @param transaction - The transaction to send * @param signers - Array of signers needed for the transaction * @param priorityFee - Priority fee in microLamports (default: 10000) * @param options - Optional configuration for retry mechanism and compute units * @returns Promise that resolves to the transaction signature * * @example * ```typescript * const signature = await sendTransaction( * connection, * transaction, * [payer], * 10000, * { * computeUnitBuffer: { multiplier: 1.1 }, * onStatusUpdate: (status) => console.log(status), * } * ); * ``` */ async function sendTransaction(connection, transaction, signers, priorityFee = 10000, options = {}) { const { computeUnitBuffer: userComputeBuffer, commitment = "confirmed", ...sendOptions } = options; // Use user provided buffer or default to 1.1 multiplier const computeUnitBuffer = userComputeBuffer ?? { multiplier: 1.1 }; if (transaction.recentBlockhash === undefined) { console.log("No blockhash provided. Setting recent blockhash"); const { blockhash } = await connection.getLatestBlockhash(commitment); transaction.recentBlockhash = blockhash; } if (transaction.feePayer === undefined) { if (signers.length === 0) { throw new Error("No signers or fee payer provided"); } transaction.feePayer = signers[0].publicKey; } // Skip compute preparation if transaction is already signed or has compute instructions if (transaction.signatures.length > 0) { console.log("Transaction already signed, skipping compute preparation"); return sendRawTransactionWithRetry(connection, transaction.serialize(), { commitment, ...sendOptions, }); } transaction.instructions = await addComputeInstructions(connection, transaction.instructions, [], transaction.feePayer, priorityFee, computeUnitBuffer, commitment); transaction.sign(...signers); return sendRawTransactionWithRetry(connection, transaction.serialize(), { commitment, ...sendOptions, }); } /** * Sends a versioned transaction with compute unit optimization and automatic retries * * @param connection - The Solana connection object * @param instructions - Array of instructions to include in the transaction * @param signers - Array of signers needed for the transaction * @param priorityFee - Priority fee in microLamports (default: 10000) * @param lookupTables - Optional array of address lookup tables for account compression * @param options - Optional configuration for retry mechanism and compute units * @returns Promise that resolves to the transaction signature * * @remarks * This function: * 1. Automatically calculates and adds compute unit instructions if not present * 2. Creates a v0 transaction message with the provided instructions * 3. Signs and sends the transaction with automatic retries * 4. Provides status updates through the callback * * Status updates include: * - "computeUnitBufferAdded": Compute unit instructions were added * - "created": Transaction was created * - "signed": Transaction was signed * - "sent": Transaction was sent (includes signature) * - "confirmed": Transaction was confirmed * * @example * ```typescript * const signature = await sendVersionedTransaction( * connection, * instructions, * [payer], * 10000, * lookupTables, * { * computeUnitBuffer: { multiplier: 1.1 }, * onStatusUpdate: (status) => console.log(status), * } * ); * ``` */ async function sendVersionedTransaction(connection, instructions, signers, priorityFee = 10000, lookupTables, options = {}) { const { computeUnitBuffer: userComputeBuffer, // Rename to make clear it's user provided commitment = "confirmed", ...sendOptions } = options; const hasComputeLimitInstructions = hasSetComputeLimitInstruction(instructions); if (!hasComputeLimitInstructions) { const computeUnitBuffer = userComputeBuffer ?? { multiplier: 1.1 }; instructions = await addComputeInstructions(connection, instructions, lookupTables ?? [], signers[0].publicKey, priorityFee, computeUnitBuffer, commitment); } const messageV0 = new web3_js_1.TransactionMessage({ payerKey: signers[0].publicKey, recentBlockhash: (await connection.getLatestBlockhash(commitment)).blockhash, instructions, }).compileToV0Message(lookupTables); const transaction = new web3_js_1.VersionedTransaction(messageV0); transaction.sign(signers); return await sendRawTransactionWithRetry(connection, transaction.serialize(), sendOptions); } /** * Adds compute unit price and limit instructions and returns the updated instructions * * @param connection - The Solana connection object * @param instructions - Array of instructions to which compute unit instructions will be added * @param lookupTables - Optional array of address lookup tables for account compression * @param payer - The public key of the transaction payer * @param priorityFee - Priority fee in microLamports (default: 10000) * @param computeUnitBuffer - Optional buffer to add to simulated compute units * @param commitment - Desired commitment level for the transaction * @returns Array of instructions with compute unit instructions added */ async function addComputeInstructions(connection, instructions, lookupTables, payer, priorityFee = 10000, computeUnitBuffer = {}, commitment = "confirmed") { if (!hasSetComputeUnitPriceInstruction(instructions)) { instructions.push(web3_js_1.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priorityFee, })); } if (!hasSetComputeLimitInstruction(instructions)) { const simulatedCompute = await (0, exports.getSimulationComputeUnits)(connection, instructions, payer, lookupTables, commitment); if (simulatedCompute === null) { throw new Error("Failed to simulate compute units"); } console.log("Simulated compute units", simulatedCompute); // Apply buffer to compute units let finalComputeUnits = simulatedCompute; if (computeUnitBuffer.multiplier) { finalComputeUnits = Math.floor(finalComputeUnits * computeUnitBuffer.multiplier); } if (computeUnitBuffer.fixed) { finalComputeUnits += computeUnitBuffer.fixed; } console.log("Final compute units (with buffer)", finalComputeUnits); instructions.push(web3_js_1.ComputeBudgetProgram.setComputeUnitLimit({ units: finalComputeUnits, })); } return instructions; } /** * Internal helper to send a versioned transaction with automatic retries and status updates * * @param connection - The Solana connection object * @param transaction - The versioned transaction to send * @param options - Optional configuration for the retry mechanism * @returns Promise that resolves to the transaction signature * * @remarks * This function implements a robust retry mechanism that: * 1. Sends the transaction only once * 2. Monitors the transaction status until confirmation * 3. Retries on failure with increasing delay * 4. Provides detailed status updates through the callback * * The function uses default values that can be partially overridden through the options parameter. * Default values are defined in DEFAULT_SEND_OPTIONS. * * Retry behavior: * - Initial delay between retries is max(500ms, options.initialDelayMs) * - Delay increases by RETRY_INTERVAL_INCREASE (200ms) after each retry * - Maximum retries defined by options.maxRetries (default: 15) * * Status updates include: * - "created": Initial transaction state * - "signed": Transaction has been signed * - "sent": Transaction has been sent (includes signature) * - "retry": Transaction is being retried (includes last signature if any) * - "confirmed": Transaction is confirmed or finalized (includes status) * * @throws Error if the transaction fails after all retry attempts * * @internal This is an internal helper function used by sendVersionedTransaction */ async function sendRawTransactionWithRetry(connection, transaction, { maxRetries = exports.DEFAULT_SEND_OPTIONS.maxRetries, initialDelayMs = exports.DEFAULT_SEND_OPTIONS.initialDelayMs, commitment = exports.DEFAULT_SEND_OPTIONS.commitment, skipPreflight = exports.DEFAULT_SEND_OPTIONS.skipPreflight, onStatusUpdate = (status) => console.log("Transaction status:", status), } = {}) { onStatusUpdate?.({ status: "created" }); onStatusUpdate?.({ status: "signed" }); let signature = null; let status = null; let retries = 0; // Setting a minimum to decrease spam and for the confirmation to work let delayBetweenRetries = Math.max(initialDelayMs, 500); while (retries < maxRetries) { try { const isFirstSend = signature === null; // Send transaction if not sent yet signature = await connection.sendRawTransaction(transaction, { skipPreflight, preflightCommitment: commitment, maxRetries: 0, }); if (isFirstSend) { onStatusUpdate?.({ status: "sent", signature: signature ?? "" }); } // Poll for confirmation let pollTimeout = delayBetweenRetries; const timeBetweenPolls = 500; while (pollTimeout > 0) { await new Promise((resolve) => setTimeout(resolve, timeBetweenPolls)); const response = await connection.getSignatureStatus(signature); if (response?.value) { status = response.value; if (status.confirmationStatus === "confirmed" || status.confirmationStatus === "finalized") { onStatusUpdate?.({ status: "confirmed", result: status }); return signature ?? ""; } } pollTimeout -= timeBetweenPolls; } } catch (error) { if (error instanceof Error) { console.log(`Attempt ${retries + 1} failed:`, error.message); } else { console.log(`Attempt ${retries + 1} failed:`, error); } } retries++; if (retries < maxRetries) { onStatusUpdate?.({ status: "retry", signature: signature ?? null }); delayBetweenRetries += exports.RETRY_INTERVAL_INCREASE; } } throw new Error(`Transaction failed after ${maxRetries} attempts`); } /** * Creates a new address lookup table and extends it with additional addresses * * @param connection - The Solana connection object * @param sender - The keypair of the transaction sender * @param additionalAddresses - Array of additional addresses to include in the lookup table * @returns A tuple containing the lookup table address and the lookup table account */ async function createLookupTable(connection, sender, additionalAddresses, priorityFee = 10000) { const slot = await connection.getSlot(); const [lookupTableInst, lookupTableAddress] = web3_js_2.AddressLookupTableProgram.createLookupTable({ authority: sender.publicKey, payer: sender.publicKey, recentSlot: slot, }); const extendInstruction = web3_js_2.AddressLookupTableProgram.extendLookupTable({ payer: sender.publicKey, authority: sender.publicKey, lookupTable: lookupTableAddress, addresses: additionalAddresses, }); const lookupTableInstructions = [lookupTableInst, extendInstruction]; const lookupTableInstructionsSignature = await sendVersionedTransaction(connection, lookupTableInstructions, [sender], priorityFee); // Need to wait until the lookup table is active await (0, exports.confirmTransaction)(connection, lookupTableInstructionsSignature, "finalized"); console.log("Lookup table instructions signature", lookupTableInstructionsSignature); const lookupTableAccount = (await connection.getAddressLookupTable(lookupTableAddress, { commitment: "confirmed", })).value; if (!lookupTableAccount) { throw new Error("Failed to get lookup table account"); } return [lookupTableAddress, lookupTableAccount]; } //# sourceMappingURL=transaction.js.map