// executor.ts import { Connection, Keypair, PublicKey, AddressLookupTableProgram, VersionedTransaction, Transaction, TransactionInstruction, TransactionMessage, SystemProgram, ComputeBudgetProgram, LAMPORTS_PER_SOL, AddressLookupTableAccount, } from '@solana/web3.js'; import { getAssociatedTokenAddress } from '@solana/spl-token'; import bs58 from 'bs58'; import https from 'https'; import * as dotenv from 'dotenv'; dotenv.config(); import Decimal from 'decimal.js'; // Kamino SDK import { KaminoMarket, getFlashLoanInstructions, PROGRAM_ID as KAMINO_PROGRAM_ID, } from '@kamino-finance/klend-sdk'; import BN from 'bn.js'; // ---------------------- CONFIG ---------------------- const SOLANA_RPC = process.env.SOLANA_RPC || 'https://api.mainnet-beta.solana.com'; const JUPITER_SWAP_INSTRUCTIONS_API = 'https://api.jup.ag/swap/v1/swap-instructions'; const JITO_API = process.env.JITO_API || 'https://mainnet.block-engine.jito.wtf/api/v1/bundles'; const KAMINO_MARKET_PUBKEY = new PublicKey( process.env.KAMINO_MARKET ?? '7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF' ); const WSOL_MINT = new PublicKey('So11111111111111111111111111111111111111112'); const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'); const JITO_TIP_ACCOUNT = new PublicKey( process.env.JITO_TIP_ACCOUNT ?? '96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNs3Rj2Z' ); const JITO_TIP_AMOUNT = Math.floor( (parseFloat(process.env.JITO_TIP_SOL ?? '0.005') || 0.005) * LAMPORTS_PER_SOL ); // ---------------------------------------------------- function loadKeypairFromEnv(): Keypair { const pk = process.env.PRIVATE_KEY; if (!pk) throw new Error('PRIVATE_KEY missing in .env'); try { if (pk.trim().startsWith('[')) { const arr = JSON.parse(pk); return Keypair.fromSecretKey(Uint8Array.from(arr)); } else { const secret = bs58.decode(pk); return Keypair.fromSecretKey(secret); } } catch (e) { throw new Error('Failed to parse PRIVATE_KEY: ' + String(e)); } } interface SwapInstructionsResponse { tokenLedgerInstruction?: any; computeBudgetInstructions?: any[]; setupInstructions?: any[]; swapInstruction: any; cleanupInstruction?: any; addressLookupTableAddresses?: string[]; error?: string; } /** * Get swap instructions from Jupiter v6 /swap-instructions endpoint */ async function getJupiterSwapInstructions( quote: any, userPubkey: string ): Promise { const payload = { quoteResponse: quote, userPublicKey: userPubkey, wrapAndUnwrapSol: false, // We handle WSOL manually in flash loan dynamicComputeUnitLimit: true, prioritizationFeeLamports: 'auto', }; const data = JSON.stringify(payload); console.log('πŸ“„ Jupiter Swap Instructions Request:', { url: JUPITER_SWAP_INSTRUCTIONS_API, inputMint: quote.inputMint, outputMint: quote.outputMint, inAmount: quote.inAmount, }); return new Promise((resolve, reject) => { const req = https.request( JUPITER_SWAP_INSTRUCTIONS_API, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.JUPITER_API_KEY || '', 'Content-Length': data.length, }, timeout: 12_000, }, (res) => { let body = ''; res.on('data', (c) => (body += c)); res.on('end', () => { console.log('πŸ“‘ Jupiter Response:', { status: res.statusCode, statusMessage: res.statusMessage, }); if (res.statusCode !== 200) { console.error(`❌ Jupiter API Error: ${res.statusCode} ${res.statusMessage}`); console.error(`Response: ${body}`); return resolve(null); } try { const parsed = JSON.parse(body); if (parsed.error) { console.error('❌ Jupiter returned error:', parsed.error); return resolve(null); } console.log('βœ… Jupiter Instructions received successfully'); return resolve(parsed as SwapInstructionsResponse); } catch (e) { console.error('❌ Error parsing Jupiter response:', e); return resolve(null); } }); } ); req.on('error', (err) => { console.error('❌ Jupiter Request Error:', err.message); reject(err); }); req.on('timeout', () => { console.error('⏰ Jupiter Request Timeout'); req.destroy(); resolve(null); }); req.write(data); req.end(); }); } /** * Convert instruction payload to TransactionInstruction */ function deserializeInstruction(instruction: any): TransactionInstruction { return new TransactionInstruction({ programId: new PublicKey(instruction.programId), keys: instruction.accounts.map((key: any) => ({ pubkey: new PublicKey(key.pubkey), isSigner: key.isSigner, isWritable: key.isWritable, })), data: Buffer.from(instruction.data, 'base64'), }); } /** * Fetch address lookup table accounts */ async function getAddressLookupTableAccounts( connection: Connection, keys: string[] ): Promise { const addressLookupTableAccountInfos = await connection.getMultipleAccountsInfo( keys.map((key) => new PublicKey(key)) ); return addressLookupTableAccountInfos.reduce((acc, accountInfo, index) => { const addressLookupTableAddress = keys[index]; if (accountInfo) { const addressLookupTableAccount = new AddressLookupTableAccount({ key: new PublicKey(addressLookupTableAddress), state: AddressLookupTableAccount.deserialize(accountInfo.data), }); acc.push(addressLookupTableAccount); } return acc; }, [] as AddressLookupTableAccount[]); } function safeDeserializeIx( ix: any | undefined, debugName: string ): TransactionInstruction { if (!ix || typeof ix !== 'object') { throw new Error(`Jupiter returned empty instruction for "${debugName}"`); } if (!ix.programId) { console.error('Raw instruction that failed:', JSON.stringify(ix, null, 2)); throw new Error(`Instruction "${debugName}" is missing programId`); } return deserializeInstruction(ix); } // ALT: new helper ---------------------------------------------------------- function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } async function ensureAlt( connection: Connection, payer: Keypair, addresses: PublicKey[] ): Promise { // 0️⃣ Hard validation const uniq = [ ...new Set( addresses .filter((k): k is PublicKey => k instanceof PublicKey) .map(k => k.toBase58()) ), ].map(k => new PublicKey(k)); if (uniq.length === 0) { throw new Error('ALT requested with empty or invalid address list'); } // 1️⃣ Create + extend ALT const recentSlot = await connection.getSlot('finalized'); const [createIx, altAddress] = AddressLookupTableProgram.createLookupTable({ authority: payer.publicKey, payer: payer.publicKey, recentSlot, }); const extendIx = AddressLookupTableProgram.extendLookupTable({ lookupTable: altAddress, authority: payer.publicKey, payer: payer.publicKey, addresses: uniq, }); const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized'); const tx = new VersionedTransaction( new TransactionMessage({ payerKey: payer.publicKey, recentBlockhash: blockhash, instructions: [createIx, extendIx], }).compileToV0Message() ); tx.sign([payer]); // 2️⃣ Send ONCE const sig = await connection.sendTransaction(tx, { skipPreflight: true, }); // 3️⃣ Wait for FINALIZATION await connection.confirmTransaction( { signature: sig, blockhash, lastValidBlockHeight }, 'finalized' ); // 4️⃣ Retry fetch until ALT is live let acc = null; for (let i = 0; i < 10; i++) { acc = await connection.getAccountInfo(altAddress, 'finalized'); if (acc?.owner.equals(AddressLookupTableProgram.programId)) break; await sleep(300); } if (!acc) { throw new Error('ALT account not found after creation'); } if (!acc.owner.equals(AddressLookupTableProgram.programId)) { throw new Error( `Account exists but is not ALT (owner=${acc.owner.toBase58()})` ); } // 5️⃣ Safe deserialize return new AddressLookupTableAccount({ key: altAddress, state: AddressLookupTableAccount.deserialize(acc.data), }); } let cachedAlt: AddressLookupTableAccount | null = null; async function getAlt( connection: Connection, payer: Keypair, addresses: PublicKey[] ) { if (cachedAlt) return cachedAlt; cachedAlt = await ensureAlt(connection, payer, addresses); return cachedAlt; } /** Patch Kamino SDK bug: replace undefined pubkey with a dummy key */ function fixKaminoInstruction(ix: TransactionInstruction): TransactionInstruction { const dummy = SystemProgram.programId; ix.keys = ix.keys.map(k => { // if pubkey is missing or invalid, replace with dummy and make sure it is not a signer if (!k.pubkey || typeof k.pubkey.toBase58 !== 'function') { return { ...k, pubkey: dummy, isSigner: false, isWritable: Boolean(k.isWritable), // preserve writable if desired }; } // otherwise, ensure the pubkey is a PublicKey instance (some libs return strings) if (typeof k.pubkey === 'string') { return { ...k, pubkey: new PublicKey(k.pubkey) }; } return k; }); return ix; } /** Build a *writable* transfer instruction so Jito accepts the bundle */ const HELIUS_TIP_ACCOUNTS = [ 'wyvPkWjVZz1M8fHQnMMCDTQDbkManefNNhweYk5WkcF', '3KCKozbAaF75qEU33jtzozcJ29yJuaLJTy2jFdzUY8bT', '2q5pghRs6arqVjRvT5gfgWfWcHWmw1ZuCzphgd5KfWGJ', '5VY91ws6B2hMmBFRsXkoAAdsPHBJwRfBht4DXox3xkwn', '4vieeGHPYPG2MmyPRcYjdiDmmhN3ww7hsFNap8pVN3Ey', '4ACfpUFoaSD9bfPdeu6DBt89gB6ENTeHBXCAi87NhDEE', '9bnz4RShgq1hAnLnZbP8kbgBg1kEmcJBYQq3gQbmnSta', 'D2L6yPZ2FmmmTKPgzaMKdhu6EWZcTpLy1Vhx8uvZe7NZ', '4TQLFNWK8AovT1gFvda5jfw2oJeRMKEmw7aH6MGBJ3or', '2nyhqdwKcJZR2vcqCyrYsaPVdAnFoJjiksCXJ7hfEYgD', 'D1Mc6j9xQWgR1o1Z7yU5nVVXFQiAYx7FG9AW1aVfwrUM' ]; const TIP_LAMPORTS = 200_000; // 0.002 SOL minimum /** * Build an atomic VersionedTransaction that wraps: * [ flashBorrowIxn, ...computeBudget, ...setup, swap1, swap2, cleanup, flashRepayIxn, jito tip ] */ async function buildAtomicFlashloanTransaction( connection: Connection, wallet: Keypair, market: any, reserve: any, flashLoanAmountLamports: bigint, swapInstructions1: SwapInstructionsResponse, swapInstructions2: SwapInstructionsResponse ): Promise { // 1) Derive destination ATA for borrowed mint (WSOL) const destinationAta = await getAssociatedTokenAddress(WSOL_MINT, wallet.publicKey, false); // 2) Get lending market authority const lendingMarketAuthority = await market.getLendingMarketAuthority(); // 3) Build flash borrow & repay instructions const { flashBorrowIxn, flashRepayIxn } = getFlashLoanInstructions({ borrowIxIndex: 0, userTransferAuthority: wallet.publicKey, lendingMarketAuthority, lendingMarketAddress: market.getAddress(), reserve, amountLamports: flashLoanAmountLamports, destinationAta, referrerAccount: undefined, referrerTokenState: undefined, programId: KAMINO_PROGRAM_ID, }); // right after getFlashLoanInstructions(...) and the destructure console.log('--- flashBorrowIxn keys ---'); flashBorrowIxn.keys.forEach((k, i) => console.log(i, k.pubkey?.toBase58?.() ?? String(k.pubkey), 'isSigner=', k.isSigner, 'isWritable=', k.isWritable) ); console.log('--- flashRepayIxn keys ---'); flashRepayIxn.keys.forEach((k, i) => console.log(i, k.pubkey?.toBase58?.() ?? String(k.pubkey), 'isSigner=', k.isSigner, 'isWritable=', k.isWritable) ); // Insert after the getFlashLoanInstructions(...) call and before pushing instructions // 1) Dump raw keys shape (useful for debugging) console.log('RAW flashBorrowIxn.keys (inspect):', JSON.stringify(flashBorrowIxn.keys, null, 2)); console.log('RAW flashRepayIxn.keys (inspect):', JSON.stringify(flashRepayIxn.keys, null, 2)); // 2) Sanitizer: ensure every AccountMeta has a PublicKey instance function sanitizeKaminoIxn(ix: TransactionInstruction, walletPubkey: PublicKey): TransactionInstruction { if (!ix || !Array.isArray(ix.keys)) throw new Error('Bad instruction returned by Kamino SDK'); const sanitizedKeys = ix.keys.map((k: any, i: number) => { // Keep isSigner/isWritable flags robust const isSigner = Boolean(k?.isSigner); const isWritable = Boolean(k?.isWritable); // Determine a safe pubkey value: let pub = k?.pubkey; // If it's already a PublicKey instance, great if (pub && typeof pub.toBase58 === 'function') { return { pubkey: pub, isSigner, isWritable }; } // If it's a string (some libs return base58 strings), convert it if (typeof pub === 'string' && pub.length > 0) { try { return { pubkey: new PublicKey(pub), isSigner, isWritable }; } catch (e) { /* fallthrough to fallback */ } } // If missing and this entry requires a signer, assume it's the userTransferAuthority if (isSigner) { console.warn(`sanitize: instruction key #${i} was missing but marked signer -> using wallet.pubkey`); return { pubkey: walletPubkey, isSigner: true, isWritable }; } // Otherwise replace with SystemProgram so compilation won't crash. // This is a safe dummy for non-signer entries (will surface a logical error later if it's wrong). console.warn(`sanitize: instruction key #${i} missing pubkey -> substituting SystemProgram (non-signer)`); return { pubkey: KAMINO_PROGRAM_ID, isSigner: false, isWritable }; }); // Build a clean TransactionInstruction (preserve programId and data) return new TransactionInstruction({ programId: ix.programId instanceof PublicKey ? ix.programId : new PublicKey(String(ix.programId)), keys: sanitizedKeys as any, data: Buffer.isBuffer(ix.data) ? ix.data : Buffer.from(ix.data ?? ''), }); } // 3) Sanitize both instructions const borrowIxnSan = sanitizeKaminoIxn(flashBorrowIxn, wallet.publicKey); const repayIxnSan = sanitizeKaminoIxn(flashRepayIxn, wallet.publicKey); // 4) Optionally log sanitized keys for verification console.log('--- SANITIZED flashBorrowIxn keys ---'); borrowIxnSan.keys.forEach((k, i) => console.log(i, k.pubkey.toBase58(), 'isSigner=', k.isSigner, 'isWritable=', k.isWritable) ); console.log('--- SANITIZED flashRepayIxn keys ---'); repayIxnSan.keys.forEach((k, i) => console.log(i, k.pubkey.toBase58(), 'isSigner=', k.isSigner, 'isWritable=', k.isWritable) ); // 4) Assemble all instructions in order (PRUNED) const allInstructions: TransactionInstruction[] = []; // Then push sanitized instructions instead of originals // ... // and later for repay: // --- flash borrow (must be first) --- allInstructions.push(borrowIxnSan); // --- swap 1 (WSOL β†’ USDC) NO setup, NO cleanup --- allInstructions.push( safeDeserializeIx(swapInstructions1.swapInstruction, 'swap1.swap') ); // --- swap 2 (USDC β†’ WSOL) NO setup, NO cleanup --- allInstructions.push( safeDeserializeIx(swapInstructions2.swapInstruction, 'swap2.swap') ); // --- flash repay (must be last) --- allInstructions.push(repayIxnSan); allInstructions.push( SystemProgram.transfer({ fromPubkey: wallet.publicKey, toPubkey: new PublicKey(HELIUS_TIP_ACCOUNTS[0]), lamports: TIP_LAMPORTS, }) ); // sanity gate console.log('πŸ“ Final instruction count:', allInstructions.length); if (allInstructions.length > 8) throw new Error('TX too large – abort'); // 5) Get address lookup tables from both swaps const lookupTableAddresses = [ ...(swapInstructions1.addressLookupTableAddresses || []), ...(swapInstructions2.addressLookupTableAddresses || []), ]; let addressLookupTableAccounts: AddressLookupTableAccount[] = []; if (lookupTableAddresses.length > 0) { addressLookupTableAccounts = await getAddressLookupTableAccounts( connection, lookupTableAddresses ); } // 6) Get recent blockhash const { blockhash } = await connection.getLatestBlockhash('finalized'); // ALT: 1) collect every address we will ever touch ------------------------- const allKeys = [ wallet.publicKey, market.getAddress(), reserve.address, lendingMarketAuthority, destinationAta, WSOL_MINT, USDC_MINT, ]; // ALT: 2) ensure ALT exists (cache it in prod) ----------------------------- const alt = await getAlt(connection, wallet, allKeys); const jupAlts = await getAddressLookupTableAccounts( connection, lookupTableAddresses ); // ALT: 3) build v0 message -------------------------------------------------- const messageV0 = new TransactionMessage({ payerKey: wallet.publicKey, recentBlockhash: blockhash, instructions: allInstructions, }).compileToV0Message([...jupAlts, alt]); // <-- ALT injected here const serialized = messageV0.serialize(); console.log("TX SIZE:", serialized.length); // ALT: 4) create VersionedTransaction -------------------------------------- const vtx = new VersionedTransaction(messageV0); vtx.sign([wallet]); // payer signature return vtx; // now returns VersionedTransaction } /** Send single signed VersionedTransaction to Jito as a bundle */ /** * Send the already-signed VersionedTransaction through Helius Sender * (it automatically adds Jito tip + routes to Jito + Helius validators) */ async function sendWithSender(signedTx: Transaction | VersionedTransaction): Promise { const base64 = Buffer.from(signedTx.serialize()).toString('base64'); console.log('DEBUG tx size:', base64.length, 'chars'); const resp = await fetch( `https://sender.helius-rpc.com/fast?api-key=${process.env.HELIUS_API_KEY}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: Date.now().toString(), method: 'sendTransaction', params: [ base64, { encoding: 'base64', skipPreflight: true, maxRetries: 0 }, ], }), } ); const raw = await resp.json(); console.log('DEBUG Sender response:', JSON.stringify(raw, null, 2)); // ← add this const { result, signature, error } = raw; const txid = signature ?? result; if (error) throw new Error(error.message); console.log('βœ… Bundle landed:', txid); return txid; } /** * Main executor entry. Called from scanner when opportunity is found. * @param quote1 - WSOL -> USDC quote from scanner * @param quote2 - USDC -> WSOL quote from scanner * @param tradeAmountSol - Amount in SOL */ export async function onOpportunityFound(quote1: any, quote2: any, tradeAmountSol: number) { console.log('\n🎯 ================================================'); console.log('🎯 EXECUTOR: Opportunity handler invoked'); console.log('🎯 ================================================\n'); try { const wallet = loadKeypairFromEnv(); const connection = new Connection(SOLANA_RPC, 'confirmed'); console.log('πŸ’Ό Wallet:', wallet.publicKey.toBase58()); // Load Kamino market console.log('🏦 Loading Kamino market...'); const market = await KaminoMarket.load(connection, KAMINO_MARKET_PUBKEY); // Find WSOL reserve const reserve = market.getReserveByMint(WSOL_MINT); if (!reserve) { throw new Error('WSOL reserve not found in Kamino market'); } console.log('βœ… Kamino market loaded successfully'); // 1) Get swap instructions from Jupiter for both swaps console.log('\nπŸ“‹ Fetching swap instructions from Jupiter...'); const swapInstructions1 = await getJupiterSwapInstructions(quote1, wallet.publicKey.toBase58()); if (!swapInstructions1) { throw new Error('Failed to fetch swap instructions for WSOL -> USDC'); } console.log('βœ… Swap 1 instructions received (WSOL -> USDC)'); const swapInstructions2 = await getJupiterSwapInstructions(quote2, wallet.publicKey.toBase58()); if (!swapInstructions2) { throw new Error('Failed to fetch swap instructions for USDC -> WSOL'); } console.log('βœ… Swap 2 instructions received (USDC -> WSOL)'); // 2) Compute borrow size const amountLamports = new Decimal(tradeAmountSol).mul(new Decimal(1e9)).floor(); console.log('\nπŸ’° Flash loan amount:', tradeAmountSol, 'SOL (', amountLamports.toString(), 'lamports)'); // 3) Build atomic transaction console.log('\nπŸ”¨ Building atomic flash loan transaction...'); const vtx = await buildAtomicFlashloanTransaction( connection, wallet, market, reserve, amountLamports, swapInstructions1, swapInstructions2 ); // 4) Send to Jito helius console.log('\nπŸ“€ Sending transaction via jito helius...'); await sendWithSender(vtx); console.log('\nβœ… ================================================'); console.log('βœ… EXECUTOR: Bundle sent successfully!'); console.log('βœ… ================================================\n'); } catch (error) { console.error('\n❌ ================================================'); console.error('❌ EXECUTOR FAILED:', error); console.error('❌ ================================================\n'); throw error; } } //close ALT.ts import { Connection, Keypair, PublicKey, Transaction, AddressLookupTableProgram, } from "@solana/web3.js"; import bs58 from "bs58"; import * as dotenv from "dotenv"; dotenv.config(); /* ================= CONFIG ================= */ const RPC = process.env.SOLANA_RPC ?? "https://api.mainnet-beta.solana.com"; // πŸ”΄ PUT THE ALT YOU WANT TO CLOSE HERE const ALT_ADDRESS = new PublicKey( process.env.ALT_ADDRESS ?? "" ); /* ========================================= */ function loadKeypairFromEnv(): Keypair { const pk = process.env.PRIVATE_KEY; if (!pk) throw new Error("PRIVATE_KEY missing in .env"); if (pk.trim().startsWith("[")) { return Keypair.fromSecretKey( Uint8Array.from(JSON.parse(pk)) ); } return Keypair.fromSecretKey(bs58.decode(pk)); } async function sleep(ms: number) { return new Promise((r) => setTimeout(r, ms)); } async function main() { if (!process.env.ALT_ADDRESS) { throw new Error("ALT_ADDRESS missing in .env"); } const connection = new Connection(RPC, "confirmed"); const wallet = loadKeypairFromEnv(); console.log("πŸ‘› Wallet:", wallet.publicKey.toBase58()); console.log("πŸ“¦ ALT:", ALT_ADDRESS.toBase58()); /* 1️⃣ Deactivate ALT */ console.log("⏸️ Deactivating ALT..."); const deactivateTx = new Transaction().add( AddressLookupTableProgram.deactivateLookupTable({ lookupTable: ALT_ADDRESS, authority: wallet.publicKey, }) ); const deactivateSig = await connection.sendTransaction( deactivateTx, [wallet] ); console.log("βœ… Deactivate tx:", deactivateSig); /* 2️⃣ Wait cooldown (~512 slots) */ console.log("⏳ Waiting cooldown (~3 minutes)..."); await sleep(180_000); /* 3️⃣ Close ALT */ console.log("πŸ—‘οΈ Closing ALT..."); const closeTx = new Transaction().add( AddressLookupTableProgram.closeLookupTable({ lookupTable: ALT_ADDRESS, authority: wallet.publicKey, recipient: wallet.publicKey, }) ); const closeSig = await connection.sendTransaction( closeTx, [wallet] ); console.log("πŸ’° ALT closed, rent refunded"); console.log("βœ… Close tx:", closeSig); } main().catch((e) => { console.error("❌ Failed:", e); process.exit(1); });