// 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 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<SwapInstructionsResponse | null> {
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<AddressLookupTableAccount[]> {
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<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function ensureAlt(
connection: Connection,
payer: Keypair,
addresses: PublicKey[]
): Promise<AddressLookupTableAccount> {
// 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<VersionedTransaction> {
// 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<string> {
const base64 = Buffer.from(signedTx.serialize()).toString('base64');
console.log('DEBUG tx size:', base64.length, 'chars');
const resp = await fetch(
{
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 ??
// π΄ 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);
});