Optimizing transactions on Solana is crucial for ensuring their inclusion in blocks, especially during periods of high network traffic. This document outlines key strategies for improving transaction performance and reliability.
This guide is available in Solana Web3.js v3, Solana Kit, and Solana Web3.js v1 (Legacy) versions. Select the appropriate tab to view the code snippets and instructions for your preferred library:
- Solana Web3.js (v3.x)
- Solana Kit
- Solana Web3.js (Legacy)
The explanations throughout this guide primarily reference the Solana Web3.js (Legacy) code examples. The Solana Web3.js v3 examples use the same code with v3's changes applied: key generation, signing, and legacy Transaction.serialize() are awaited, RPC values such as block heights are bigint, and maxRetries takes a bigint. The Solana Kit implementations follow the same conceptual patterns and logic with updated syntax and method calls.
Install the library for the tab you follow:
# Solana Web3.js (v3.x)
npm install @solana/web3.js@3
# Solana Web3.js (Legacy)
npm install @solana/web3.js@1
The retry example in A Retry Pattern Code Example also uses @noble/ed25519 and @noble/hashes. For everything that changed between v1 and v3, see How to Migrate from Solana web3.js v1 to v3.
There are several strategies you can take to increase the likelihood of your transaction being included in a block and confirmed by the network quickly. These include:
- Utilize priority fees and dynamically update them using the Priority Fee API
- Simulate transactions to set compute unit limits accurately
- Optimize Transaction Assembly
- Implement proper blockhash management and expiration handling
- Use advanced retry strategies for better success rates
- Consider using Jito Bundles for complex, multi-transaction operations
Use Priority Feesā
Solana's priority fees allow you to set an additional fee on top of the base fee for a transaction, which gives your transaction a higher priority in the leader's queue. By bidding more for priority status, your transaction will be more likely to be confirmed quickly by the network.
Priority fees can be added to your transaction by including a setComputeUnitPrice instruction in your transaction:
The examples in this section build legacy and version 0 transactions. A version 1 transaction sets its priority fee in the message's transactionConfig instead, as an absolute lamport amount for the whole transaction rather than a microlamport price per compute unit. A setComputeUnitPrice instruction in a version 1 transaction is accepted but has no effect. See Version 1 Transactions.
- Solana Web3.js (v3.x)
- Solana Kit
- Solana Web3.js (Legacy)
import { Transaction, ComputeBudgetProgram } from '@solana/web3.js'
const transaction = new Transaction()
// Add your transaction instructions here
const priorityFeeInstruction = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: priorityFeeAmount,
})
transaction.add(priorityFeeInstruction)
import {
Rpc,
createDefaultRpcTransport,
createRpc,
RpcTransport,
createJsonRpcApi,
RpcRequest
} from "@solana/kit";
import {
EstimatePriorityFeesResponse,
SolanaKitEstimatePriorityFeesParams
} from "./types";
interface createQuickNodeTransportParams {
endpoint: string;
}
type PriorityFeeApi = {
qn_estimatePriorityFees(params: SolanaKitEstimatePriorityFeesParams): EstimatePriorityFeesResponse;
// Add other methods here
}
function createQuickNodeTransport({ endpoint }: createQuickNodeTransportParams): RpcTransport {
const jsonRpcTransport = createDefaultRpcTransport({ url: endpoint });
return async <TResponse>(...args: Parameters<RpcTransport>): Promise<TResponse> => {
return await jsonRpcTransport(...args);
};
}
export function createPriorityFeeApi(endpoint: string): Rpc<PriorityFeeApi> {
const api = createJsonRpcApi<PriorityFeeApi>({
requestTransformer: (request: RpcRequest<any>) => {
const transformedRequest = {
...request,
params: request.params[0]
};
return transformedRequest;
},
responseTransformer: (response: any) => response.result,
});
const transport = createQuickNodeTransport({
endpoint,
});
return createRpc({ api, transport });
}
async function main() {
const quickNodeRpc = createPriorityFeeApi(''); // š Replace with your Quicknode endpoint
const priorityFees = await quickNodeRpc.qn_estimatePriorityFees({
last_n_blocks: 100,
account: 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4',
api_version: 2
}).send();
console.log(priorityFees);
}
main();
import { Transaction, ComputeBudgetProgram } from '@solana/web3.js'
const transaction = new Transaction()
// Add your transaction instructions here
const priorityFeeInstruction = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: priorityFeeAmount,
})
transaction.add(priorityFeeInstruction)
Calculating the Right Priority Feeā
Quicknode provides a Priority Fee API (Add-on Details | Docs) to fetch recent priority fees for a given Program over a specified number of blocks.
curl https://docs-demo.solana-mainnet.quiknode.pro/ \
-X POST \
-H "Content-Type: application/json" \
-H "x-qn-api-version: 1" \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "qn_estimatePriorityFees",
"params": {
"last_n_blocks": 100,
"account": "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
"api_version": 2
}
}'
The method returns an object with a per_compute_unit property that includes estimates for priority fees (in microlamports) based on recent network activity. The response provides fee estimates in four categories: low, medium, high, and extreme. Additionally, the response includes a recommended property that provides a recommended fee level for each compute unit based on the current network conditions.
These estimates are priced per compute unit in microlamports, which is what setComputeUnitPrice expects on a legacy or version 0 transaction. A version 1 transaction sets one absolute fee for the whole transaction, so multiply the per-compute-unit estimate by your computeUnitLimit and divide by 1,000,000 to get the priorityFee in lamports.
Priority Fees Resourcesā
- Understand Solana Priority Fees: Land Transactions Faster
- Solana Priority Fees Add-on
- Priority Fees Add-on Documentation
- Guide: How to Optimize Solana Transactions - Priority Fees
- Sample Code - Solana Kit
- Sample Code - Solana Web3.js (Legacy)
Optimize Compute Unitsā
On your client-side, you can simulate the transaction to determine the compute units it will consume and set the limit accordingly.
To set a compute unit limit on a legacy or version 0 transaction, you can use the setComputeUnitLimit instruction. This value is what is evaluated in a block's compute limits. Version 1 transactions set the limit in transactionConfig instead, covered in Version 1 Transactions below.
- Solana Web3.js (v3.x)
- Solana Kit
- Solana Web3.js (Legacy)
const testTransaction = new Transaction().add(testInstruction)
const computeUnitInstruction = ComputeBudgetProgram.setComputeUnitLimit({
units: targetComputeUnitsAmount,
})
testTransaction.add(computeUnitInstruction)
import {
pipe,
createTransactionMessage,
appendTransactionMessageInstructions,
} from "@solana/kit";
import {
getSetComputeUnitLimitInstruction,
getSetComputeUnitPriceInstruction,
} from "@solana-program/compute-budget";
// Increase CU limit from default 200,000 to 400,000
const computeUnitLimitInstruction = getSetComputeUnitLimitInstruction({
units: 400_000,
});
// Set priority fee (optional - without this, incremental cost per CU is 0)
const computeUnitPriceInstruction = getSetComputeUnitPriceInstruction({
microLamports: 10_000,
});
// Build transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions([
computeUnitLimitInstruction,
computeUnitPriceInstruction,
...yourInstructions, // Add your other instructions here
], tx)
);
const testTransaction = new Transaction().add(testInstruction)
const computeUnitInstruction = ComputeBudgetProgram.setComputeUnitLimit({
units: targetComputeUnitsAmount,
})
testTransaction.add(computeUnitInstruction)
Version 1 Transactionsā
Version 1 transactions do not use ComputeBudget instructions. Every compute and fee setting moves onto the message itself, into a transactionConfig object:
| Field | Required | Value |
|---|---|---|
computeUnitLimit | Yes | The maximum compute units the transaction may consume |
loadedAccountsDataSizeLimit | Yes | The maximum total size in bytes of the account data the transaction may load |
priorityFee | No | An absolute lamport amount for the whole transaction |
heapSize | No | The requested heap size in bytes |
Both limits are mandatory in practice, because an unset value is zero and there is no 200,000 compute unit default to fall back on. Omit computeUnitLimit and the transaction fails with ComputationalBudgetExceeded; omit loadedAccountsDataSizeLimit and it fails with MaxLoadedAccountsDataSizeExceeded.
ComputeBudget instructions remain valid instructions in a version 1 transaction, so including them raises no error, but they set nothing. They only consume the compute units it costs to run them.
Solana Kit writes transactionConfig through dedicated setters, available from @solana/kit v7.0.0:
import {
pipe,
createTransactionMessage,
appendTransactionMessageInstructions,
setTransactionMessageComputeUnitLimit,
setTransactionMessageLoadedAccountsDataSizeLimit,
setTransactionMessagePriorityFeeLamports,
lamports,
} from "@solana/kit";
const transactionMessage = pipe(
createTransactionMessage({ version: 1 }),
(tx) => appendTransactionMessageInstructions(yourInstructions, tx),
(tx) => setTransactionMessageComputeUnitLimit(400_000, tx),
(tx) => setTransactionMessageLoadedAccountsDataSizeLimit(64 * 1024, tx),
(tx) => setTransactionMessagePriorityFeeLamports(lamports(10_000n), tx)
);
Solana Web3.js v3 builds the same message from a TransactionMessage with compileToV1Message(), which takes the limits and fee directly. Solana Web3.js v1 (Legacy) can read version 1 transactions but cannot build them, so there is no legacy equivalent:
import { TransactionMessage, VersionedTransaction } from "@solana/web3.js";
const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: yourInstructions,
}).compileToV1Message({
computeUnitLimit: 400_000,
loadedAccountsDataSizeLimit: 64 * 1024,
priorityFeeLamports: 10_000n,
});
const transaction = new VersionedTransaction(message);
await transaction.sign([payer]);
Simulating for compute units works the same way as it does for version 0, with one difference: give the simulated transaction a computeUnitLimit high enough to complete, then set the measured value plus a margin on the transaction you send.
A version 1 transaction can be up to 4,096 bytes, against 1,232 for legacy and version 0. Base58 is capped at 1,232 decoded bytes whatever the version, so anything larger has to be submitted as base64.
Compute Units Resourcesā
- Guide: How to Optimize Solana Transactions - Priority Fees
- Optimize Compute in Solana Programs
- Introduction to Solana Compute Units and Transaction Fees
Transaction Assembly Best Practicesā
Refer to the code below that combines priority fees and compute unit optimization during transaction building. Both examples build a version 0 transaction; to build the same thing as version 1, replace the two ComputeBudget instructions with the transactionConfig setters shown in Version 1 Transactions:
- Solana Web3.js (v3.x)
- Solana Kit
- Solana Web3.js (Legacy)
import { Connection, Keypair, Transaction, ComputeBudgetProgram } from "@solana/web3.js";
import { fetchEstimatePriorityFees, getSimulationUnits } from "./helpers"; // Update with your path to our methods
const endpoint = YOUR_QUICKNODE_ENDPOINT; // Replace with your Quicknode endpoint
async function main(){
// Key generation is async in web3.js v3
const keyPair = await Keypair.generate(); // derive your keypair from your secret key with await Keypair.fromSecretKey(...)
// 1. Establish a connection to the Solana cluster
const connection = new Connection(endpoint, 'finalized');
// 2. Create your transaction
const transaction = new Transaction();
// ... add instructions to the transaction
// 3. Fetch the recent priority fees
const { result } = await fetchEstimatePriorityFees({ endpoint });
const priorityFee = result.recommended; // Replace with your priority fee level based on your business requirements, e.g. result.per_compute_unit['high']
// 4. Create a PriorityFee instruction and add it to your transaction
const priorityFeeInstruction = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: priorityFee,
});
transaction.add(priorityFeeInstruction);
// 5. Simulate the transaction and add the compute unit limit instruction to your transaction
let [units, recentBlockhash] =
await Promise.all([
getSimulationUnits(
connection,
transaction.instructions,
keyPair.publicKey
),
connection.getLatestBlockhash(),
]);
if (units) {
units = Math.ceil(units * 1.05); // margin of error
transaction.add(ComputeBudgetProgram.setComputeUnitLimit({ units }));
}
// 6. Sign and send your transaction (signing and serializing are async in web3.js v3)
transaction.feePayer = keyPair.publicKey;
transaction.recentBlockhash = recentBlockhash.blockhash;
await transaction.sign(keyPair);
const hash = await connection.sendRawTransaction(
await transaction.serialize(),
{ skipPreflight: true, maxRetries: 0n }
);
return hash;
}
import {
Instruction,
Rpc,
SolanaRpcApi,
pipe,
createTransactionMessage,
appendTransactionMessageInstructions,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
getBase64EncodedWireTransaction,
createSolanaRpc,
lamports,
generateKeyPairSigner,
getAddressFromPublicKey,
signTransactionMessageWithSigners,
createDefaultRpcTransport,
createRpc,
RpcTransport,
createJsonRpcApi,
RpcRequest,
airdropFactory,
createSolanaRpcSubscriptions
} from "@solana/kit";
import {
getSetComputeUnitLimitInstruction,
getSetComputeUnitPriceInstruction,
} from "@solana-program/compute-budget";
import {
getTransferSolInstruction,
} from "@solana-program/system";
import {
EstimatePriorityFeesResponse,
SolanaKitEstimatePriorityFeesParams
} from "./types";
// Types for Quicknode priority fee estimation
type PriorityFeeApi = {
qn_estimatePriorityFees(params: SolanaKitEstimatePriorityFeesParams): EstimatePriorityFeesResponse;
};
// Helper function to create Quicknode RPC transport
function createQuickNodeTransport({ endpoint }: { endpoint: string }): RpcTransport {
const jsonRpcTransport = createDefaultRpcTransport({ url: endpoint });
return async <TResponse>(...args: Parameters<RpcTransport>): Promise<TResponse> => {
return await jsonRpcTransport(...args);
};
}
// Create priority fee API client
function createPriorityFeeApi(endpoint: string): Rpc<PriorityFeeApi> {
const api = createJsonRpcApi<PriorityFeeApi>({
requestTransformer: (request: RpcRequest<any>) => {
const transformedRequest = {
...request,
params: request.params[0]
};
return transformedRequest;
},
responseTransformer: (response: any) => response.result,
});
const transport = createQuickNodeTransport({ endpoint });
return createRpc({ api, transport });
}
// Simulate transaction to get compute units
async function getSimulationUnits(
rpc: Rpc<SolanaRpcApi>,
instructions: Instruction[],
payerSigner: any
): Promise<number | undefined> {
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const testInstructions = [
getSetComputeUnitLimitInstruction({ units: 1_400_000 }),
...instructions,
];
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions(testInstructions, tx),
(tx) => setTransactionMessageFeePayerSigner(payerSigner, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
);
const transaction = await signTransactionMessageWithSigners(transactionMessage);
const rawTx = getBase64EncodedWireTransaction(transaction);
try {
const simulation = await rpc.simulateTransaction(rawTx, {
commitment: 'finalized',
encoding: 'base64',
sigVerify: false,
replaceRecentBlockhash: true,
}).send();
if (simulation.value.err != null || simulation.value.unitsConsumed == null) {
console.warn('Simulation failed:', simulation.value.err);
return undefined;
}
return Number(simulation.value.unitsConsumed);
} catch (error) {
console.error('Error during simulation:', error);
return undefined;
}
}
// Main function - Optimized transaction with priority fees and compute units
async function main() {
// Use devnet for testing (has faucet for funding accounts)
const rpc = createSolanaRpc('http://127.0.0.1:8899');
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900');
const airdrop = airdropFactory({ rpc, rpcSubscriptions });
const QUICKNODE_ENDPOINT = "YOUR_QUICKNODE_ENDPOINT";
console.log('=== GENERATING ACCOUNTS ===');
// Generate sender and receiver accounts
const senderSigner = await generateKeyPairSigner();
const recipientSigner = await generateKeyPairSigner();
const senderAddress = await getAddressFromPublicKey(senderSigner.keyPair.publicKey);
const recipientAddress = await getAddressFromPublicKey(recipientSigner.keyPair.publicKey);
console.log('Sender:', senderAddress);
console.log('Recipient:', recipientAddress);
console.log('\n=== FUNDING SENDER ACCOUNT ===');
await airdrop({
recipientAddress: senderAddress,
lamports: lamports(1_000_000_000n),
commitment: 'finalized',
});
console.log('\n=== CREATING TRANSFER INSTRUCTION ===');
// Create transfer instruction
const instructions: Instruction[] = [
getTransferSolInstruction({
source: senderSigner,
destination: recipientAddress,
amount: lamports(1_000_000n), // 0.001 SOL
}),
];
console.log('\n=== FETCHING PRIORITY FEES ===');
// Fetch priority fee using Quicknode API
const quickNodeRpc = createPriorityFeeApi(QUICKNODE_ENDPOINT);
const priorityFeesResponse = await quickNodeRpc.qn_estimatePriorityFees({
last_n_blocks: 100,
api_version: 2
}).send();
const priorityFee = priorityFeesResponse.recommended;
console.log('\n=== ADDING PRIORITY FEE INSTRUCTION ===');
// Add priority fee instruction
const priorityFeeInstruction = getSetComputeUnitPriceInstruction({
microLamports: priorityFee,
});
// Add priority fee instruction to the beginning of the instructions array
instructions.unshift(priorityFeeInstruction);
console.log(`Added priority fee instruction with ${priorityFee} microlamports`);
console.log('\n=== SIMULATING FOR COMPUTE UNITS ===');
// Simulate the transaction and add the compute unit limit instruction
const computeUnits = await getSimulationUnits(rpc, instructions, senderSigner);
if (computeUnits) {
console.log(`Transaction would consume: ${computeUnits} compute units`);
const adjustedUnits = Math.ceil(computeUnits * 1.1); // 10% margin
console.log(`Using compute units with 10% margin: ${adjustedUnits}`);
const computeUnitLimitInstruction = getSetComputeUnitLimitInstruction({
units: adjustedUnits
});
instructions.unshift(computeUnitLimitInstruction);
console.log('Added compute unit limit instruction');
} else {
console.log('Could not determine compute units from simulation, using default limit');
const computeUnitLimitInstruction = getSetComputeUnitLimitInstruction({
units: 200_000 // Default fallback
});
instructions.unshift(computeUnitLimitInstruction);
console.log('Added default compute unit limit instruction');
}
console.log('\n=== BUILDING TRANSACTION ===');
// Get latest blockhash
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
console.log('Latest blockhash:', latestBlockhash.blockhash);
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions(instructions, tx),
(tx) => setTransactionMessageFeePayerSigner(senderSigner, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
);
console.log('\n=== SIGNING AND SENDING ===');
// Sign transaction
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
const serializedTransaction = getBase64EncodedWireTransaction(signedTransaction);
// Send transaction
const signature = await rpc
.sendTransaction(serializedTransaction, {
preflightCommitment: 'finalized',
encoding: 'base64',
skipPreflight: false,
maxRetries: 0n
})
.send();
console.log('\nā
TRANSACTION COMPLETED!');
console.log('Signature:', signature);
}
main()
import { Connection, Keypair, Transaction, ComputeBudgetProgram } from "@solana/web3.js";
import { fetchEstimatePriorityFees, getSimulationUnits } from "./helpers"; // Update with your path to our methods
const endpoint = YOUR_QUICKNODE_ENDPOINT; // Replace with your Quicknode endpoint
const keyPair = Keypair.generate();// derive your keypair from your secret key
async function main(){
// 1. Establish a connection to the Solana cluster
const connection = new Connection(endpoint);
// 2. Create your transaction
const transaction = new Transaction();
// ... add instructions to the transaction
// 3. Fetch the recent priority fees
const { result } = await fetchEstimatePriorityFees({ endpoint });
const priorityFee = result.recommended; // Replace with your priority fee level based on your business requirements, e.g. result.per_compute_unit['high']
// 4. Create a PriorityFee instruction and add it to your transaction
const priorityFeeInstruction = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: priorityFee,
});
transaction.add(priorityFeeInstruction);
// 5. Simulate the transaction and add the compute unit limit instruction to your transaction
let [units, recentBlockhash] =
await Promise.all([
getSimulationUnits(
connection,
transaction.instructions,
keyPair.publicKey
),
connection.getLatestBlockhash(),
]);
if (units) {
units = Math.ceil(units * 1.05); // margin of error
transaction.add(ComputeBudgetProgram.setComputeUnitLimit({ units }));
}
// 6. Sign and send your transaction
transaction.feePayer = keyPair.publicKey;
transaction.recentBlockhash = recentBlockhash.blockhash;
transaction.sign(keyPair);
const hash = await connection.sendRawTransaction(
transaction.serialize(),
{ skipPreflight: true, maxRetries: 0 }
);
return hash;
}
Blockhash Management & Expirationā
Solana transactions include a recent blockhash, which validators use to ensure the transaction is both fresh and unique. This mechanism prevents replay attacks, where the same transaction could be resubmitted multiple times. Blockhashes expire after approximately 151 blocks (about 30 seconds at ~200 ms per block), after which any transaction referencing them becomes invalid. If a transaction is not finalized within this window, it must be rebuilt with a new blockhash before being retried.
Commitment Levelsā
Commitment levels describe a block's progress. Under Alpenglow, confirmed and finalized provide the same guarantees:
| Property | Processed | Confirmed | Finalized |
|---|---|---|---|
| Received block | X | X | X |
| Block contains target tx | X | X | X |
| 60%+ stake voted finalize or 80%+ stake voted notarize on block | - | X | X |
| Target tx is committed to the permanent ledger | - | X | X |
Source: Anza Docs - Commitments
Under Alpenglow, confirmed and finalized provide equivalent commitment guarantees. Use finalized explicitly; confirmed remains supported for compatibility but is scheduled for removal in a later release. See Solanaās Alpenglow upgrade page.
Transaction Retry Strategiesā
Implement custom retry logic for better transaction success rates during network congestion. The retry logic itself is the same for every transaction version; only the compute and fee setup differs, as described in Version 1 Transactions.
The maxRetries Strategyā
Take manual control of transaction rebroadcasting during network congestion.
- Solana Web3.js (v3.x)
- Solana Kit
- Solana Web3.js (Legacy)
// Disable RPC retry for manual control (maxRetries is a bigint in web3.js v3)
const signature = await connection.sendRawTransaction(
await transaction.serialize(),
{
skipPreflight: false,
maxRetries: 0n
}
);
const signature = await rpc
.sendTransaction(serializedTransaction, {
preflightCommitment: 'finalized',
encoding: 'base64',
skipPreflight: false,
maxRetries: 0n
})
.send();
// Disable RPC retry for manual control
const signature = await connection.sendRawTransaction(
transaction.serialize(),
{
skipPreflight: false,
maxRetries: 0
}
);
A Retry Pattern Code Exampleā
Complete implementation showing constant interval rebroadcasting with expiration handling.
- Solana Web3.js (v3.x)
- Solana Kit
- Solana Web3.js (Legacy)
import {
Keypair,
Connection,
LAMPORTS_PER_SOL,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { sha512 } from "@noble/hashes/sha2.js";
import * as ed from "@noble/ed25519";
import { setTimeout as delay } from "timers/promises";
ed.hashes.sha512 = sha512;
const BUFFER = 10n; // block heights are bigint in web3.js v3
const { secretKey, publicKey } = await ed.keygenAsync();
const secretKey64 = new Uint8Array(64);
secretKey64.set(secretKey, 0);
secretKey64.set(publicKey, 32);
const payer = await Keypair.fromSecretKey(secretKey64);
const toAccount = (await Keypair.generate()).publicKey;
console.log("š§ Airdropping 1 SOL to payer...");
const connection = new Connection("https://api.devnet.solana.com", "finalized");
const airdropSignature = await connection.requestAirdrop(
payer.publicKey,
LAMPORTS_PER_SOL,
);
await connection.confirmTransaction(
{ signature: airdropSignature, ...(await connection.getLatestBlockhash()) },
"finalized",
);
const blockhashResponse = await connection.getLatestBlockhash();
const lastValidBlockHeight = blockhashResponse.lastValidBlockHeight - BUFFER;
const transaction = new Transaction({
feePayer: payer.publicKey,
blockhash: blockhashResponse.blockhash,
lastValidBlockHeight,
}).add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: toAccount,
lamports: 1_000_000,
}),
);
const message = transaction.serializeMessage();
const signature = ed.sign(message, secretKey);
transaction.addSignature(payer.publicKey, Buffer.from(signature));
const rawTransaction = await transaction.serialize();
let blockheight = await connection.getBlockHeight();
let attempts = 0;
let landed = false;
let executionError = null;
// Retry loop
while (blockheight < lastValidBlockHeight && !landed) {
attempts++;
console.log(
`\nš¤ Attempt #${attempts} ā current block: ${blockheight}, cutoff: ${lastValidBlockHeight}`,
);
try {
const signature = await connection.sendRawTransaction(rawTransaction, {
skipPreflight: true,
maxRetries: 0n,
});
console.log(`š Sent tx, signature: ${signature}`);
// Poll for confirmation after sending
const status = await connection.getSignatureStatus(signature, {
searchTransactionHistory: false,
});
console.log("š Status check:", status.value);
if (status.value?.err) {
executionError = status.value.err;
break;
}
if (status.value?.confirmationStatus === "finalized") {
console.log(`ā
Transaction finalized after ${attempts} attempt(s)!`);
landed = true;
break;
}
} catch (error) {
console.error("Error sending transaction:", error);
}
await delay(500);
blockheight = await connection.getBlockHeight();
}
if (executionError) {
throw new Error(`Transaction execution failed: ${JSON.stringify(executionError)}`);
}
if (!landed) {
console.warn("Transaction not finalized before blockhash expired.");
}
import {
Instruction,
Rpc,
SolanaRpcApi,
pipe,
createTransactionMessage,
appendTransactionMessageInstructions,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
getBase64EncodedWireTransaction,
createSolanaRpc,
lamports,
generateKeyPairSigner,
getAddressFromPublicKey,
signTransactionMessageWithSigners,
createDefaultRpcTransport,
createRpc,
RpcTransport,
createJsonRpcApi,
RpcRequest,
airdropFactory,
createSolanaRpcSubscriptions
} from "@solana/kit";
import {
getSetComputeUnitLimitInstruction,
getSetComputeUnitPriceInstruction,
} from "@solana-program/compute-budget";
import {
getTransferSolInstruction,
} from "@solana-program/system";
import {
EstimatePriorityFeesResponse,
SolanaKitEstimatePriorityFeesParams
} from "./types";
// Types for Quicknode priority fee estimation
type PriorityFeeApi = {
qn_estimatePriorityFees(params: SolanaKitEstimatePriorityFeesParams): EstimatePriorityFeesResponse;
};
// Helper function to create Quicknode RPC transport
function createQuickNodeTransport({ endpoint }: { endpoint: string }): RpcTransport {
const jsonRpcTransport = createDefaultRpcTransport({ url: endpoint });
return async <TResponse>(...args: Parameters<RpcTransport>): Promise<TResponse> => {
return await jsonRpcTransport(...args);
};
}
// Create priority fee API client
function createPriorityFeeApi(endpoint: string): Rpc<PriorityFeeApi> {
const api = createJsonRpcApi<PriorityFeeApi>({
requestTransformer: (request: RpcRequest<any>) => {
const transformedRequest = {
...request,
params: request.params[0]
};
return transformedRequest;
},
responseTransformer: (response: any) => response.result,
});
const transport = createQuickNodeTransport({ endpoint });
return createRpc({ api, transport });
}
// Retry logic with blockheight-based boundary
async function sendTransactionWithRetries(
rpc: Rpc<SolanaRpcApi>,
serializedTransaction: string,
lastValidBlockHeight: bigint,
maxRetryBuffer: number
): Promise<string> {
const maxRetryBlockHeight = lastValidBlockHeight - BigInt(maxRetryBuffer);
console.log(`=== RETRY CONFIGURATION ===`);
console.log(`Last valid block height: ${lastValidBlockHeight}`);
console.log(`Max retry block height (with ${maxRetryBuffer} buffer): ${maxRetryBlockHeight}`);
let attempt = 0;
while (true) {
attempt++;
// Get current block height
const currentBlockHeight = await rpc.getBlockHeight({ commitment: 'finalized' }).send();
console.log(`Attempt ${attempt}: Current block height: ${currentBlockHeight}`);
// Check if we've exceeded our retry boundary
if (currentBlockHeight > maxRetryBlockHeight) {
console.log(`ā Retry boundary exceeded. Current: ${currentBlockHeight}, Max retry: ${maxRetryBlockHeight}`);
throw new Error(`Transaction retry timeout: exceeded block height boundary (${currentBlockHeight} >= ${maxRetryBlockHeight})`);
}
console.log(`š¤ Sending transaction attempt ${attempt}...`);
// Send the transaction
const signature = await rpc.sendTransaction(serializedTransaction as any, {
encoding: 'base64',
skipPreflight: true,
maxRetries: 0n // We handle retries ourselves
}).send();
console.log(`ā
Transaction sent successfully on attempt ${attempt}!`);
console.log(`Signature: ${signature}`);
// Wait a bit for confirmation
console.log('Waiting for transaction confirmation...');
// Check transaction status
const status = await rpc.getSignatureStatuses([signature]).send();
const signatureStatus = status.value[0];
if (signatureStatus?.err) {
throw new Error(`Transaction execution failed: ${JSON.stringify(signatureStatus.err)}`);
} else if (signatureStatus?.confirmationStatus === 'finalized') {
console.log(`š Transaction finalized`);
return signature;
} else {
console.log(`ā³ Transaction still processing, will retry...`);
await new Promise(resolve => setTimeout(resolve, 500));
}
}
}
// Simulate transaction to get compute units
async function getSimulationUnits(
rpc: Rpc<SolanaRpcApi>,
instructions: Instruction[],
payerSigner: any
): Promise<number | undefined> {
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const testInstructions = [
...instructions,
];
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions(testInstructions, tx),
(tx) => setTransactionMessageFeePayerSigner(payerSigner, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
);
const transaction = await signTransactionMessageWithSigners(transactionMessage);
const rawTx = getBase64EncodedWireTransaction(transaction);
try {
const simulation = await rpc.simulateTransaction(rawTx, {
commitment: 'finalized',
encoding: 'base64',
sigVerify: false,
replaceRecentBlockhash: true,
}).send();
if (simulation.value.err != null || simulation.value.unitsConsumed == null) {
console.warn('Simulation failed:', simulation.value.err);
return undefined;
}
return Number(simulation.value.unitsConsumed);
} catch (error) {
console.error('Error during simulation:', error);
return undefined;
}
}
// Main function - Optimized transaction with priority fees and compute units
async function main() {
// Use devnet for testing (has faucet for funding accounts)
const rpc = createSolanaRpc('http://127.0.0.1:8899');
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900');
const airdrop = airdropFactory({ rpc, rpcSubscriptions });
const QUICKNODE_ENDPOINT = "YOUR_QUICKNODE_ENDPOINT";
console.log('=== GENERATING ACCOUNTS ===');
// Generate sender and receiver accounts
const senderSigner = await generateKeyPairSigner();
const recipientSigner = await generateKeyPairSigner();
const senderAddress = await getAddressFromPublicKey(senderSigner.keyPair.publicKey);
const recipientAddress = await getAddressFromPublicKey(recipientSigner.keyPair.publicKey);
console.log('Sender:', senderAddress);
console.log('Recipient:', recipientAddress);
await airdrop({
recipientAddress: senderAddress,
lamports: lamports(1_000_000_000n),
commitment: 'finalized',
});
// Create transfer instruction
const instructions: Instruction[] = [
getTransferSolInstruction({
source: senderSigner,
destination: recipientAddress,
amount: lamports(1_000_000n), // 0.001 SOL
}),
];
console.log('\n=== FETCHING PRIORITY FEES ===');
// Fetch priority fee using Quicknode API
const quickNodeRpc = createPriorityFeeApi(QUICKNODE_ENDPOINT);
const priorityFeesResponse = await quickNodeRpc.qn_estimatePriorityFees({
last_n_blocks: 100,
api_version: 2
}).send();
const priorityFee = priorityFeesResponse.recommended;
console.log('\n=== ADDING PRIORITY FEE INSTRUCTION ===');
// Add priority fee instruction
const priorityFeeInstruction = getSetComputeUnitPriceInstruction({
microLamports: priorityFee,
});
// Add priority fee instruction to the beginning of the instructions array
instructions.unshift(priorityFeeInstruction);
console.log(`Added priority fee instruction with ${priorityFee} microlamports`);
console.log('\n=== SIMULATING FOR COMPUTE UNITS ===');
// Simulate the transaction and add the compute unit limit instruction
const computeUnits = await getSimulationUnits(rpc, instructions, senderSigner);
if (computeUnits) {
console.log(`Transaction would consume: ${computeUnits} compute units`);
const adjustedUnits = Math.ceil(computeUnits * 1.1); // 10% margin
console.log(`Using compute units with 10% margin: ${adjustedUnits}`);
const computeUnitLimitInstruction = getSetComputeUnitLimitInstruction({
units: adjustedUnits
});
instructions.unshift(computeUnitLimitInstruction);
console.log('Added compute unit limit instruction');
} else {
console.log('Could not determine compute units from simulation, using default limit');
const computeUnitLimitInstruction = getSetComputeUnitLimitInstruction({
units: 200_000 // Default fallback
});
instructions.unshift(computeUnitLimitInstruction);
console.log('Added default compute unit limit instruction');
}
console.log('\n=== BUILDING TRANSACTION ===');
// Get latest blockhash with current block height info
const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'finalized' }).send();
const currentBlockHeight = await rpc.getBlockHeight({ commitment: 'finalized' }).send();
console.log('Latest blockhash:', latestBlockhash.blockhash);
console.log('Current block height:', currentBlockHeight);
console.log('Last valid block height:', latestBlockhash.lastValidBlockHeight);
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions(instructions, tx),
(tx) => setTransactionMessageFeePayerSigner(senderSigner, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
);
// Sign transaction
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
const serializedTransaction = getBase64EncodedWireTransaction(signedTransaction);
console.log('\n=== SENDING WITH RETRY LOGIC ===');
// Send transaction with retry logic
try {
const signature = await sendTransactionWithRetries(
rpc,
serializedTransaction,
latestBlockhash.lastValidBlockHeight,
10 // 10 block buffer
);
console.log('\nā
TRANSACTION COMPLETED!');
console.log('Final Signature:', signature);
} catch (error) {
console.error('\nā TRANSACTION FAILED AFTER ALL RETRIES');
console.error('Error:', error);
}
}
main().catch(console.error);
import {
Keypair,
Connection,
LAMPORTS_PER_SOL,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { sha512 } from "@noble/hashes/sha2.js";
import * as ed from "@noble/ed25519";
import { setTimeout as delay } from "timers/promises";
ed.hashes.sha512 = sha512;
const BUFFER = 10;
const { secretKey, publicKey } = await ed.keygenAsync();
const secretKey64 = new Uint8Array(64);
secretKey64.set(secretKey, 0);
secretKey64.set(publicKey, 32);
const payer = Keypair.fromSecretKey(secretKey64);
const toAccount = Keypair.generate().publicKey;
console.log("š§ Airdropping 1 SOL to payer...");
const connection = new Connection("https://api.devnet.solana.com", "finalized");
const airdropSignature = await connection.requestAirdrop(
payer.publicKey,
LAMPORTS_PER_SOL,
);
await connection.confirmTransaction(
{ signature: airdropSignature },
"finalized",
);
const blockhashResponse = await connection.getLatestBlockhash();
const lastValidBlockHeight = blockhashResponse.lastValidBlockHeight - BUFFER;
const transaction = new Transaction({
feePayer: payer.publicKey,
blockhash: blockhashResponse.blockhash,
lastValidBlockHeight,
}).add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: toAccount,
lamports: 1_000_000,
}),
);
const message = transaction.serializeMessage();
const signature = ed.sign(message, secretKey);
transaction.addSignature(payer.publicKey, Buffer.from(signature));
const rawTransaction = transaction.serialize();
let blockheight = await connection.getBlockHeight();
let attempts = 0;
let landed = false;
let executionError = null;
// Retry loop
while (blockheight < lastValidBlockHeight && !landed) {
attempts++;
console.log(
`\nš¤ Attempt #${attempts} ā current block: ${blockheight}, cutoff: ${lastValidBlockHeight}`,
);
try {
const signature = await connection.sendRawTransaction(rawTransaction, {
skipPreflight: true,
maxRetries: 0,
});
console.log(`š Sent tx, signature: ${signature}`);
// Poll for confirmation after sending
const status = await connection.getSignatureStatus(signature, {
searchTransactionHistory: false,
});
console.log("š Status check:", status.value);
if (status.value?.err) {
executionError = status.value.err;
break;
}
if (status.value?.confirmationStatus === "finalized") {
console.log(`ā
Transaction finalized after ${attempts} attempt(s)!`);
landed = true;
break;
}
} catch (error) {
console.error("Error sending transaction:", error);
}
await delay(500);
blockheight = await connection.getBlockHeight();
}
if (executionError) {
throw new Error(`Transaction execution failed: ${JSON.stringify(executionError)}`);
}
if (!landed) {
console.warn("Transaction not finalized before blockhash expired.");
}
Use Stake-Weighted Quality of Service (SWQoS)ā
Stake-Weighted Quality of Service (SWQoS) is a newer mechanism to increase the likelihood of your transaction being included in a block. Quicknode provides a variety of SWQoS options:
- Using Smart Transactions (or manually building the transaction following best practices) with a priority fee API setting of
recommendedor higher will use stake from Quicknode's own stake pool, and do the proper assembly of compute and priority fees for the user. Any user sending non-duplicate transactions withrecommendedpriority fees or higher will now be routed through Quicknode's own stake pool. - Using Jito bundles, i.e. (Lil' JIT's
sendTransactionorsendBundlemethods) will use Jito's own stake pool, which has significant stake in the Solana network.
Use Jito Bundlesā
For advanced transaction optimization and bundling, consider using the Lil' JIT add-on. This add-on enables the creation of Jito Bundles, allowing for atomic execution of multiple transactions.
The add-on enables you to provide a tip to Jito validator producing the next block to prioritize your transactions (or bundles of transactions). To use the add-on, you must:
- Include a SOL transfer instruction in your transaction (or last transaction if you are bundling multiple transactions) to a Jito Tip Account (accesible via
getTipAccountsmethod):
const tipAccounts = await rpc.getTipAccounts().send()
const jitoTipAddress =
tipAccounts[Math.floor(Math.random() * tipAccounts.length)]
- Serialize your transaction(s) into base64 strings. Base58 is capped at 1,232 decoded bytes, so a version 1 transaction above that size can only be submitted as base64:
const base64EncodedTransactions = signedTransactions.map(transaction =>
getBase64EncodedWireTransaction(transaction)
) as Base64EncodedWireTransaction[]
- Send the transaction(s) to the the Jito validator client using
sendTransactionorsendBundlemethods:
const bundleId = await lilJitRpc.sendBundle(base64EncodedTransactions).send()
- Lil' JIT Docs
- Lil' JIT Add-on
- Guide: Jito Bundles: What They Are and How to Use Them
- Sample Code: Send Jito Bundles
Wrapping Upā
For a comprehensive overview of Solana transaction optimization with detailed explanations, check out our complete transaction optimization guide.