@solana-developers/helpers
Version:
Solana helper functions
372 lines • 17 kB
JavaScript
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
;