Created
June 11, 2025 04:25
-
-
Save korrio/80093e297b9186f2476df1fe65bae6bc to your computer and use it in GitHub Desktop.
Transaction size optimization techniques summary
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { | |
Connection, | |
PublicKey, | |
Keypair, | |
Transaction, | |
TransactionMessage, | |
VersionedTransaction, | |
AddressLookupTableAccount, | |
ComputeBudgetProgram, | |
SystemProgram, | |
} = require('@solana/web3.js'); | |
const { | |
getAssociatedTokenAddress, | |
createAssociatedTokenAccountInstruction, | |
TOKEN_PROGRAM_ID, | |
createCloseAccountInstruction, | |
} = require('@solana/spl-token'); | |
const { AmmImpl } = require('@meteora-ag/dlmm'); | |
const BN = require('bn.js'); | |
class OptimizedMeteoraSwapBot { | |
constructor(config) { | |
this.connection = new Connection(config.rpcUrl, 'confirmed'); | |
this.wallet = Keypair.fromSecretKey(new Uint8Array(config.privateKey)); | |
this.slippageBps = config.slippageBps || 100; | |
this.priorityFee = config.priorityFee || 100000; | |
// Pre-computed frequently used addresses | |
this.commonAddresses = new Map(); | |
this.initializeCommonAddresses(); | |
} | |
initializeCommonAddresses() { | |
// Cache common addresses to avoid repeated computations | |
this.commonAddresses.set('TOKEN_PROGRAM', TOKEN_PROGRAM_ID); | |
this.commonAddresses.set('SYSTEM_PROGRAM', SystemProgram.programId); | |
this.commonAddresses.set('WALLET', this.wallet.publicKey); | |
} | |
// ========== TECHNIQUE 1: INSTRUCTION BATCHING ========== | |
async batchInstructions(instructions, maxInstructionsPerTx = 10) { | |
const batches = []; | |
for (let i = 0; i < instructions.length; i += maxInstructionsPerTx) { | |
batches.push(instructions.slice(i, i + maxInstructionsPerTx)); | |
} | |
return batches; | |
} | |
// ========== TECHNIQUE 2: ACCOUNT PRE-CREATION ========== | |
async preCreateRequiredAccounts(tokenMints) { | |
console.log('🔧 Pre-creating required token accounts...'); | |
const instructions = []; | |
const existingAccounts = new Set(); | |
for (const mint of tokenMints) { | |
const ata = await getAssociatedTokenAddress( | |
new PublicKey(mint), | |
this.wallet.publicKey | |
); | |
try { | |
const accountInfo = await this.connection.getAccountInfo(ata); | |
if (accountInfo) { | |
existingAccounts.add(mint); | |
continue; | |
} | |
} catch (error) { | |
// Account doesn't exist | |
} | |
const createATAIx = createAssociatedTokenAccountInstruction( | |
this.wallet.publicKey, | |
ata, | |
this.wallet.publicKey, | |
new PublicKey(mint) | |
); | |
instructions.push(createATAIx); | |
} | |
if (instructions.length > 0) { | |
const batches = await this.batchInstructions(instructions, 8); | |
for (const batch of batches) { | |
const tx = new Transaction().add(...batch); | |
const signature = await this.connection.sendTransaction(tx, [this.wallet]); | |
await this.connection.confirmTransaction(signature, 'confirmed'); | |
console.log(`✅ Account creation batch sent: ${signature}`); | |
} | |
} | |
return existingAccounts; | |
} | |
// ========== TECHNIQUE 3: INSTRUCTION COMPRESSION ========== | |
compressInstructions(instructions) { | |
console.log('🗜️ Compressing instructions...'); | |
// Remove duplicate instructions | |
const uniqueInstructions = instructions.filter((instruction, index, self) => { | |
return self.findIndex(i => | |
i.programId.equals(instruction.programId) && | |
JSON.stringify(i.keys) === JSON.stringify(instruction.keys) && | |
Buffer.compare(i.data, instruction.data) === 0 | |
) === index; | |
}); | |
// Merge compatible compute budget instructions | |
const computeBudgetInstructions = uniqueInstructions.filter(ix => | |
ix.programId.equals(ComputeBudgetProgram.programId) | |
); | |
const otherInstructions = uniqueInstructions.filter(ix => | |
!ix.programId.equals(ComputeBudgetProgram.programId) | |
); | |
// Keep only the highest priority fee instruction if multiple exist | |
const priorityFeeInstructions = computeBudgetInstructions.filter(ix => | |
ix.data[0] === 3 // SetComputeUnitPrice instruction discriminator | |
); | |
const highestPriorityFee = priorityFeeInstructions.reduce((max, current) => { | |
const currentFee = new BN(current.data.slice(1, 9), 'le').toNumber(); | |
const maxFee = max ? new BN(max.data.slice(1, 9), 'le').toNumber() : 0; | |
return currentFee > maxFee ? current : max; | |
}, null); | |
const finalInstructions = []; | |
if (highestPriorityFee) { | |
finalInstructions.push(highestPriorityFee); | |
} | |
// Add compute unit limit if not present | |
const hasComputeLimit = computeBudgetInstructions.some(ix => ix.data[0] === 2); | |
if (!hasComputeLimit) { | |
finalInstructions.push( | |
ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }) | |
); | |
} | |
finalInstructions.push(...otherInstructions); | |
console.log(`📉 Reduced from ${instructions.length} to ${finalInstructions.length} instructions`); | |
return finalInstructions; | |
} | |
// ========== TECHNIQUE 4: MINIMAL DATA ENCODING ========== | |
createMinimalSwapInstruction(pool, swapParams) { | |
// Use minimal instruction data by only including essential parameters | |
const minimalParams = { | |
inTokenAccount: swapParams.inTokenAccount, | |
outTokenAccount: swapParams.outTokenAccount, | |
inAmount: swapParams.inAmount, | |
minimumOutAmount: swapParams.minimumOutAmount, | |
user: swapParams.user, | |
}; | |
return pool.swap(minimalParams); | |
} | |
// ========== TECHNIQUE 5: ACCOUNT CLEANUP ========== | |
async createAccountCleanupInstructions(temporaryAccounts) { | |
const cleanupInstructions = []; | |
for (const account of temporaryAccounts) { | |
try { | |
const accountInfo = await this.connection.getAccountInfo(account); | |
if (accountInfo && accountInfo.data.length > 0) { | |
// Only cleanup if account has zero balance | |
const balance = await this.connection.getTokenAccountBalance(account); | |
if (balance.value.uiAmount === 0) { | |
const closeIx = createCloseAccountInstruction( | |
account, | |
this.wallet.publicKey, | |
this.wallet.publicKey | |
); | |
cleanupInstructions.push(closeIx); | |
} | |
} | |
} catch (error) { | |
// Account doesn't exist or error reading it | |
} | |
} | |
return cleanupInstructions; | |
} | |
// ========== TECHNIQUE 6: TRANSACTION SPLITTING ========== | |
async executeTransactionWithSplitting(instructions, lookupTables = []) { | |
const MAX_TX_SIZE = 1232; // Solana's transaction limit | |
// Estimate transaction size | |
let estimatedSize = this.estimateTransactionSize(instructions, lookupTables); | |
if (estimatedSize <= MAX_TX_SIZE) { | |
// Single transaction | |
return await this.sendSingleTransaction(instructions, lookupTables); | |
} | |
console.log(`📦 Transaction too large (${estimatedSize} bytes), splitting...`); | |
// Split into multiple transactions | |
const criticalInstructions = instructions.filter(ix => | |
!ix.programId.equals(ComputeBudgetProgram.programId) | |
).slice(0, 1); // Keep only the main swap instruction | |
const setupInstructions = instructions.filter(ix => | |
ix.programId.equals(ComputeBudgetProgram.programId) || | |
ix.programId.equals(SystemProgram.programId) | |
); | |
// Send setup transaction first | |
if (setupInstructions.length > 0) { | |
await this.sendSingleTransaction(setupInstructions, []); | |
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait between transactions | |
} | |
// Send main transaction | |
return await this.sendSingleTransaction(criticalInstructions, lookupTables); | |
} | |
estimateTransactionSize(instructions, lookupTables) { | |
// Rough estimation of transaction size | |
let size = 64; // Base transaction overhead | |
for (const instruction of instructions) { | |
size += 1; // Program ID index or address | |
size += 1; // Accounts length | |
size += instruction.keys.length * (lookupTables.length > 0 ? 1 : 32); // Account indices or addresses | |
size += 4; // Data length | |
size += instruction.data.length; // Instruction data | |
} | |
return size; | |
} | |
async sendSingleTransaction(instructions, lookupTables) { | |
const { blockhash } = await this.connection.getLatestBlockhash(); | |
const messageV0 = new TransactionMessage({ | |
payerKey: this.wallet.publicKey, | |
recentBlockhash: blockhash, | |
instructions, | |
}).compileToV0Message(lookupTables); | |
const versionedTransaction = new VersionedTransaction(messageV0); | |
versionedTransaction.sign([this.wallet]); | |
const signature = await this.connection.sendTransaction(versionedTransaction); | |
await this.connection.confirmTransaction(signature, 'confirmed'); | |
return signature; | |
} | |
// ========== TECHNIQUE 7: DYNAMIC COMPUTE BUDGET ========== | |
async calculateOptimalComputeBudget(instructions, lookupTables) { | |
try { | |
// Create a test transaction for simulation | |
const { blockhash } = await this.connection.getLatestBlockhash(); | |
const testInstructions = [ | |
ComputeBudgetProgram.setComputeUnitLimit({ units: 1400000 }), // Max limit for simulation | |
...instructions | |
]; | |
const messageV0 = new TransactionMessage({ | |
payerKey: this.wallet.publicKey, | |
recentBlockhash: blockhash, | |
instructions: testInstructions, | |
}).compileToV0Message(lookupTables); | |
const versionedTransaction = new VersionedTransaction(messageV0); | |
versionedTransaction.sign([this.wallet]); | |
const simulation = await this.connection.simulateTransaction(versionedTransaction); | |
if (simulation.value.unitsConsumed) { | |
// Add 20% buffer to the consumed units | |
const optimalUnits = Math.ceil(simulation.value.unitsConsumed * 1.2); | |
console.log(`🧮 Optimal compute units: ${optimalUnits} (consumed: ${simulation.value.unitsConsumed})`); | |
return optimalUnits; | |
} | |
} catch (error) { | |
console.log('Could not calculate optimal compute budget, using default'); | |
} | |
return 300000; // Default fallback | |
} | |
// ========== MAIN OPTIMIZED SWAP FUNCTION ========== | |
async executeOptimizedSwap(poolAddress, inputMint, outputMint, amountIn) { | |
try { | |
console.log('\n🚀 Starting optimized swap execution...'); | |
// Step 1: Pre-create accounts if needed | |
await this.preCreateRequiredAccounts([inputMint, outputMint]); | |
// Step 2: Get pool and prepare basic instructions | |
const pool = await AmmImpl.create(this.connection, new PublicKey(poolAddress)); | |
const poolState = await pool.getPoolState(); | |
const isXtoY = poolState.tokenXMint.toString() === inputMint; | |
// Step 3: Get quote | |
const swapAmount = new BN(amountIn); | |
const quote = await pool.getSwapQuote(swapAmount, isXtoY, this.slippageBps); | |
// Step 4: Get token accounts | |
const inputATA = await getAssociatedTokenAddress( | |
new PublicKey(inputMint), | |
this.wallet.publicKey | |
); | |
const outputATA = await getAssociatedTokenAddress( | |
new PublicKey(outputMint), | |
this.wallet.publicKey | |
); | |
// Step 5: Create minimal swap instruction | |
const swapInstruction = await this.createMinimalSwapInstruction(pool, { | |
inTokenAccount: inputATA, | |
outTokenAccount: outputATA, | |
inAmount: swapAmount, | |
minimumOutAmount: quote.minOutAmount, | |
user: this.wallet.publicKey, | |
}); | |
const instructions = [swapInstruction]; | |
// Step 6: Create optimized lookup table | |
const lookupAddresses = [ | |
new PublicKey(poolAddress), | |
new PublicKey(inputMint), | |
new PublicKey(outputMint), | |
inputATA, | |
outputATA, | |
poolState.tokenXVault, | |
poolState.tokenYVault, | |
TOKEN_PROGRAM_ID, | |
]; | |
const lookupTable = await this.getOrCreateLookupTable(lookupAddresses); | |
// Step 7: Calculate optimal compute budget | |
const optimalComputeUnits = await this.calculateOptimalComputeBudget(instructions, [lookupTable]); | |
// Step 8: Add optimized compute budget instructions | |
const computeInstructions = [ | |
ComputeBudgetProgram.setComputeUnitLimit({ units: optimalComputeUnits }), | |
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: this.priorityFee }), | |
]; | |
// Step 9: Compress all instructions | |
const allInstructions = [...computeInstructions, ...instructions]; | |
const compressedInstructions = this.compressInstructions(allInstructions); | |
// Step 10: Execute with transaction splitting if needed | |
const signature = await this.executeTransactionWithSplitting( | |
compressedInstructions, | |
[lookupTable] | |
); | |
console.log('✅ Optimized swap completed successfully!'); | |
console.log(`📋 Transaction: ${signature}`); | |
console.log(`🔗 Explorer: https://solscan.io/tx/${signature}`); | |
return { | |
signature, | |
quote, | |
success: true, | |
optimizations: { | |
instructionCount: compressedInstructions.length, | |
computeUnits: optimalComputeUnits, | |
usedLookupTable: true, | |
} | |
}; | |
} catch (error) { | |
console.error('❌ Optimized swap failed:', error); | |
return { | |
error: error.message, | |
success: false | |
}; | |
} | |
} | |
// Placeholder for lookup table method (from previous implementation) | |
async getOrCreateLookupTable(addresses) { | |
// Implementation from previous code... | |
// This would be the same as in the previous implementation | |
return null; // Placeholder | |
} | |
// ========== TECHNIQUE 8: BULK OPERATIONS ========== | |
async executeBulkSwaps(swapParams) { | |
console.log(`🔄 Executing ${swapParams.length} bulk swaps...`); | |
const results = []; | |
const BATCH_SIZE = 3; // Process 3 swaps at a time to avoid rate limits | |
for (let i = 0; i < swapParams.length; i += BATCH_SIZE) { | |
const batch = swapParams.slice(i, i + BATCH_SIZE); | |
const batchPromises = batch.map(params => | |
this.executeOptimizedSwap( | |
params.poolAddress, | |
params.inputMint, | |
params.outputMint, | |
params.amountIn | |
) | |
); | |
const batchResults = await Promise.allSettled(batchPromises); | |
results.push(...batchResults); | |
// Wait between batches to avoid overwhelming the RPC | |
if (i + BATCH_SIZE < swapParams.length) { | |
await new Promise(resolve => setTimeout(resolve, 2000)); | |
} | |
} | |
return results; | |
} | |
// ========== TECHNIQUE 9: TRANSACTION RETRY WITH BACKOFF ========== | |
async sendTransactionWithRetry(versionedTransaction, maxRetries = 3) { | |
for (let attempt = 1; attempt <= maxRetries; attempt++) { | |
try { | |
console.log(`📤 Sending transaction (attempt ${attempt}/${maxRetries})...`); | |
const signature = await this.connection.sendTransaction(versionedTransaction, { | |
skipPreflight: attempt > 1, // Skip preflight on retries | |
maxRetries: 0, // Handle retries manually | |
}); | |
const confirmation = await this.connection.confirmTransaction(signature, 'confirmed'); | |
if (!confirmation.value.err) { | |
return signature; | |
} | |
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`); | |
} catch (error) { | |
console.log(`⚠️ Attempt ${attempt} failed: ${error.message}`); | |
if (attempt === maxRetries) { | |
throw error; | |
} | |
// Exponential backoff | |
const delay = Math.pow(2, attempt) * 1000; | |
console.log(`⏳ Waiting ${delay}ms before retry...`); | |
await new Promise(resolve => setTimeout(resolve, delay)); | |
// Update recent blockhash for retry | |
const { blockhash } = await this.connection.getLatestBlockhash(); | |
versionedTransaction.message.recentBlockhash = blockhash; | |
versionedTransaction.sign([this.wallet]); | |
} | |
} | |
} | |
} | |
// ========== USAGE EXAMPLES ========== | |
async function demonstrateOptimizations() { | |
const config = { | |
rpcUrl: process.env.RPC_URL || 'https://api.mainnet-beta.solana.com', | |
privateKey: JSON.parse(process.env.PRIVATE_KEY || '[]'), | |
slippageBps: 100, | |
priorityFee: 100000, | |
}; | |
const optimizedBot = new OptimizedMeteoraSwapBot(config); | |
// Example 1: Single optimized swap | |
const singleSwapResult = await optimizedBot.executeOptimizedSwap( | |
'ARwi1S4DaiTG5DX7S4M4ZsrXqpMD1MrTmbu9ue2tpmEq', // Pool address | |
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC | |
'So11111111111111111111111111111111111111112', // SOL | |
'1000000' // 1 USDC | |
); | |
console.log('Single swap result:', singleSwapResult); | |
// Example 2: Bulk swaps | |
const bulkSwapParams = [ | |
{ | |
poolAddress: 'ARwi1S4DaiTG5DX7S4M4ZsrXqpMD1MrTmbu9ue2tpmEq', | |
inputMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', | |
outputMint: 'So11111111111111111111111111111111111111112', | |
amountIn: '500000' | |
}, | |
{ | |
poolAddress: 'ARwi1S4DaiTG5DX7S4M4ZsrXqpMD1MrTmbu9ue2tpmEq', | |
inputMint: 'So11111111111111111111111111111111111111112', | |
outputMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', | |
amountIn: '100000000' // 0.1 SOL | |
} | |
]; | |
const bulkResults = await optimizedBot.executeBulkSwaps(bulkSwapParams); | |
console.log('Bulk swap results:', bulkResults); | |
} | |
module.exports = OptimizedMeteoraSwapBot; | |
// Export optimization techniques summary | |
module.exports.OptimizationTechniques = { | |
LOOKUP_TABLES: 'Use address lookup tables to reduce transaction size', | |
INSTRUCTION_BATCHING: 'Split large transactions into smaller batches', | |
ACCOUNT_PRE_CREATION: 'Create required accounts in advance', | |
INSTRUCTION_COMPRESSION: 'Remove duplicate and merge compatible instructions', | |
MINIMAL_DATA_ENCODING: 'Use minimal instruction data', | |
ACCOUNT_CLEANUP: 'Close temporary accounts to reclaim rent', | |
TRANSACTION_SPLITTING: 'Split oversized transactions automatically', | |
DYNAMIC_COMPUTE_BUDGET: 'Calculate optimal compute units through simulation', | |
BULK_OPERATIONS: 'Process multiple operations efficiently', | |
RETRY_WITH_BACKOFF: 'Implement retry logic with exponential backoff' | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment