Guest

Sample code

Jan 19th, 2026
50
0
Never
Not a member of gistpad yet? Sign Up, it unlocks many cool features!
None 58.17 KB | Cryptocurrency | 0 0
  1. // executor.ts
  2. import {
  3. Connection,
  4. Keypair,
  5. PublicKey,
  6. AddressLookupTableProgram,
  7. VersionedTransaction,
  8. Transaction,
  9. TransactionInstruction,
  10. TransactionMessage,
  11. SystemProgram,
  12. ComputeBudgetProgram,
  13. LAMPORTS_PER_SOL,
  14. AddressLookupTableAccount,
  15. } from '@solana/web3.js';
  16. import { getAssociatedTokenAddress } from '@solana/spl-token';
  17. import bs58 from 'bs58';
  18. import https from 'https';
  19. import * as dotenv from 'dotenv';
  20. dotenv.config();
  21. import Decimal from 'decimal.js';
  22. // Kamino SDK
  23. import {
  24. KaminoMarket,
  25. getFlashLoanInstructions,
  26. PROGRAM_ID as KAMINO_PROGRAM_ID,
  27. } from '@kamino-finance/klend-sdk';
  28. import BN from 'bn.js';
  29.  
  30.  
  31. // ---------------------- CONFIG ----------------------
  32. const SOLANA_RPC = process.env.SOLANA_RPC || 'https://api.mainnet-beta.solana.com';
  33. const JUPITER_SWAP_INSTRUCTIONS_API = 'https://api.jup.ag/swap/v1/swap-instructions';
  34. const JITO_API = process.env.JITO_API || 'https://mainnet.block-engine.jito.wtf/api/v1/bundles';
  35.  
  36. const KAMINO_MARKET_PUBKEY = new PublicKey(
  37. process.env.KAMINO_MARKET ?? '7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF'
  38. );
  39.  
  40. const WSOL_MINT = new PublicKey('So11111111111111111111111111111111111111112');
  41. const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
  42.  
  43. const JITO_TIP_ACCOUNT = new PublicKey(
  44. process.env.JITO_TIP_ACCOUNT ?? '96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNs3Rj2Z'
  45. );
  46. const JITO_TIP_AMOUNT = Math.floor(
  47. (parseFloat(process.env.JITO_TIP_SOL ?? '0.005') || 0.005) * LAMPORTS_PER_SOL
  48. );
  49.  
  50. // ----------------------------------------------------
  51.  
  52. function loadKeypairFromEnv(): Keypair {
  53. const pk = process.env.PRIVATE_KEY;
  54. if (!pk) throw new Error('PRIVATE_KEY missing in .env');
  55.  
  56. try {
  57. if (pk.trim().startsWith('[')) {
  58. const arr = JSON.parse(pk);
  59. return Keypair.fromSecretKey(Uint8Array.from(arr));
  60. } else {
  61. const secret = bs58.decode(pk);
  62. return Keypair.fromSecretKey(secret);
  63. }
  64. } catch (e) {
  65. throw new Error('Failed to parse PRIVATE_KEY: ' + String(e));
  66. }
  67. }
  68.  
  69. interface SwapInstructionsResponse {
  70. tokenLedgerInstruction?: any;
  71. computeBudgetInstructions?: any[];
  72. setupInstructions?: any[];
  73. swapInstruction: any;
  74. cleanupInstruction?: any;
  75. addressLookupTableAddresses?: string[];
  76. error?: string;
  77. }
  78.  
  79. /**
  80. * Get swap instructions from Jupiter v6 /swap-instructions endpoint
  81. */
  82. async function getJupiterSwapInstructions(
  83. quote: any,
  84. userPubkey: string
  85. ): Promise<SwapInstructionsResponse | null> {
  86. const payload = {
  87. quoteResponse: quote,
  88. userPublicKey: userPubkey,
  89. wrapAndUnwrapSol: false, // We handle WSOL manually in flash loan
  90. dynamicComputeUnitLimit: true,
  91. prioritizationFeeLamports: 'auto',
  92. };
  93. const data = JSON.stringify(payload);
  94.  
  95. console.log('πŸ“„ Jupiter Swap Instructions Request:', {
  96. url: JUPITER_SWAP_INSTRUCTIONS_API,
  97. inputMint: quote.inputMint,
  98. outputMint: quote.outputMint,
  99. inAmount: quote.inAmount,
  100. });
  101.  
  102. return new Promise((resolve, reject) => {
  103. const req = https.request(
  104. JUPITER_SWAP_INSTRUCTIONS_API,
  105. {
  106. method: 'POST',
  107. headers: {
  108. 'Content-Type': 'application/json',
  109. 'x-api-key': process.env.JUPITER_API_KEY || '',
  110. 'Content-Length': data.length,
  111. },
  112. timeout: 12_000,
  113. },
  114. (res) => {
  115. let body = '';
  116. res.on('data', (c) => (body += c));
  117. res.on('end', () => {
  118. console.log('πŸ“‘ Jupiter Response:', {
  119. status: res.statusCode,
  120. statusMessage: res.statusMessage,
  121. });
  122.  
  123. if (res.statusCode !== 200) {
  124. console.error(`❌ Jupiter API Error: ${res.statusCode} ${res.statusMessage}`);
  125. console.error(`Response: ${body}`);
  126. return resolve(null);
  127. }
  128.  
  129. try {
  130. const parsed = JSON.parse(body);
  131.  
  132. if (parsed.error) {
  133. console.error('❌ Jupiter returned error:', parsed.error);
  134. return resolve(null);
  135. }
  136.  
  137. console.log('βœ… Jupiter Instructions received successfully');
  138. return resolve(parsed as SwapInstructionsResponse);
  139. } catch (e) {
  140. console.error('❌ Error parsing Jupiter response:', e);
  141. return resolve(null);
  142. }
  143. });
  144. }
  145. );
  146.  
  147. req.on('error', (err) => {
  148. console.error('❌ Jupiter Request Error:', err.message);
  149. reject(err);
  150. });
  151.  
  152. req.on('timeout', () => {
  153. console.error('⏰ Jupiter Request Timeout');
  154. req.destroy();
  155. resolve(null);
  156. });
  157.  
  158. req.write(data);
  159. req.end();
  160. });
  161. }
  162.  
  163. /**
  164. * Convert instruction payload to TransactionInstruction
  165. */
  166. function deserializeInstruction(instruction: any): TransactionInstruction {
  167. return new TransactionInstruction({
  168. programId: new PublicKey(instruction.programId),
  169. keys: instruction.accounts.map((key: any) => ({
  170. pubkey: new PublicKey(key.pubkey),
  171. isSigner: key.isSigner,
  172. isWritable: key.isWritable,
  173. })),
  174. data: Buffer.from(instruction.data, 'base64'),
  175. });
  176. }
  177.  
  178. /**
  179. * Fetch address lookup table accounts
  180. */
  181. async function getAddressLookupTableAccounts(
  182. connection: Connection,
  183. keys: string[]
  184. ): Promise<AddressLookupTableAccount[]> {
  185. const addressLookupTableAccountInfos = await connection.getMultipleAccountsInfo(
  186. keys.map((key) => new PublicKey(key))
  187. );
  188.  
  189. return addressLookupTableAccountInfos.reduce((acc, accountInfo, index) => {
  190. const addressLookupTableAddress = keys[index];
  191. if (accountInfo) {
  192. const addressLookupTableAccount = new AddressLookupTableAccount({
  193. key: new PublicKey(addressLookupTableAddress),
  194. state: AddressLookupTableAccount.deserialize(accountInfo.data),
  195. });
  196. acc.push(addressLookupTableAccount);
  197. }
  198. return acc;
  199. }, [] as AddressLookupTableAccount[]);
  200. }
  201. function safeDeserializeIx(
  202. ix: any | undefined,
  203. debugName: string
  204. ): TransactionInstruction {
  205. if (!ix || typeof ix !== 'object') {
  206. throw new Error(`Jupiter returned empty instruction for "${debugName}"`);
  207. }
  208. if (!ix.programId) {
  209. console.error('Raw instruction that failed:', JSON.stringify(ix, null, 2));
  210. throw new Error(`Instruction "${debugName}" is missing programId`);
  211. }
  212. return deserializeInstruction(ix);
  213. }
  214. // ALT: new helper ----------------------------------------------------------
  215. function sleep(ms: number): Promise<void> {
  216. return new Promise(resolve => setTimeout(resolve, ms));
  217. }
  218. async function ensureAlt(
  219. connection: Connection,
  220. payer: Keypair,
  221. addresses: PublicKey[]
  222. ): Promise<AddressLookupTableAccount> {
  223.  
  224. // 0️⃣ Hard validation
  225. const uniq = [
  226. ...new Set(
  227. addresses
  228. .filter((k): k is PublicKey => k instanceof PublicKey)
  229. .map(k => k.toBase58())
  230. ),
  231. ].map(k => new PublicKey(k));
  232.  
  233. if (uniq.length === 0) {
  234. throw new Error('ALT requested with empty or invalid address list');
  235. }
  236.  
  237. // 1️⃣ Create + extend ALT
  238. const recentSlot = await connection.getSlot('finalized');
  239.  
  240. const [createIx, altAddress] =
  241. AddressLookupTableProgram.createLookupTable({
  242. authority: payer.publicKey,
  243. payer: payer.publicKey,
  244. recentSlot,
  245. });
  246.  
  247. const extendIx = AddressLookupTableProgram.extendLookupTable({
  248. lookupTable: altAddress,
  249. authority: payer.publicKey,
  250. payer: payer.publicKey,
  251. addresses: uniq,
  252. });
  253.  
  254. const { blockhash, lastValidBlockHeight } =
  255. await connection.getLatestBlockhash('finalized');
  256.  
  257. const tx = new VersionedTransaction(
  258. new TransactionMessage({
  259. payerKey: payer.publicKey,
  260. recentBlockhash: blockhash,
  261. instructions: [createIx, extendIx],
  262. }).compileToV0Message()
  263. );
  264.  
  265. tx.sign([payer]);
  266.  
  267. // 2️⃣ Send ONCE
  268. const sig = await connection.sendTransaction(tx, {
  269. skipPreflight: true,
  270. });
  271.  
  272. // 3️⃣ Wait for FINALIZATION
  273. await connection.confirmTransaction(
  274. { signature: sig, blockhash, lastValidBlockHeight },
  275. 'finalized'
  276. );
  277.  
  278. // 4️⃣ Retry fetch until ALT is live
  279. let acc = null;
  280. for (let i = 0; i < 10; i++) {
  281. acc = await connection.getAccountInfo(altAddress, 'finalized');
  282. if (acc?.owner.equals(AddressLookupTableProgram.programId)) break;
  283. await sleep(300);
  284. }
  285.  
  286. if (!acc) {
  287. throw new Error('ALT account not found after creation');
  288. }
  289.  
  290. if (!acc.owner.equals(AddressLookupTableProgram.programId)) {
  291. throw new Error(
  292. `Account exists but is not ALT (owner=${acc.owner.toBase58()})`
  293. );
  294. }
  295.  
  296. // 5️⃣ Safe deserialize
  297. return new AddressLookupTableAccount({
  298. key: altAddress,
  299. state: AddressLookupTableAccount.deserialize(acc.data),
  300. });
  301. }
  302. let cachedAlt: AddressLookupTableAccount | null = null;
  303.  
  304. async function getAlt(
  305. connection: Connection,
  306. payer: Keypair,
  307. addresses: PublicKey[]
  308. ) {
  309. if (cachedAlt) return cachedAlt;
  310. cachedAlt = await ensureAlt(connection, payer, addresses);
  311. return cachedAlt;
  312. }
  313. /** Patch Kamino SDK bug: replace undefined pubkey with a dummy key */
  314. function fixKaminoInstruction(ix: TransactionInstruction): TransactionInstruction {
  315. const dummy = SystemProgram.programId;
  316. ix.keys = ix.keys.map(k => {
  317. // if pubkey is missing or invalid, replace with dummy and make sure it is not a signer
  318. if (!k.pubkey || typeof k.pubkey.toBase58 !== 'function') {
  319. return {
  320. ...k,
  321. pubkey: dummy,
  322. isSigner: false,
  323. isWritable: Boolean(k.isWritable), // preserve writable if desired
  324. };
  325. }
  326. // otherwise, ensure the pubkey is a PublicKey instance (some libs return strings)
  327. if (typeof k.pubkey === 'string') {
  328. return { ...k, pubkey: new PublicKey(k.pubkey) };
  329. }
  330. return k;
  331. });
  332. return ix;
  333. }
  334. /** Build a *writable* transfer instruction so Jito accepts the bundle */
  335. const HELIUS_TIP_ACCOUNTS = [
  336. 'wyvPkWjVZz1M8fHQnMMCDTQDbkManefNNhweYk5WkcF',
  337. '3KCKozbAaF75qEU33jtzozcJ29yJuaLJTy2jFdzUY8bT',
  338. '2q5pghRs6arqVjRvT5gfgWfWcHWmw1ZuCzphgd5KfWGJ',
  339. '5VY91ws6B2hMmBFRsXkoAAdsPHBJwRfBht4DXox3xkwn',
  340. '4vieeGHPYPG2MmyPRcYjdiDmmhN3ww7hsFNap8pVN3Ey',
  341. '4ACfpUFoaSD9bfPdeu6DBt89gB6ENTeHBXCAi87NhDEE',
  342. '9bnz4RShgq1hAnLnZbP8kbgBg1kEmcJBYQq3gQbmnSta',
  343. 'D2L6yPZ2FmmmTKPgzaMKdhu6EWZcTpLy1Vhx8uvZe7NZ',
  344. '4TQLFNWK8AovT1gFvda5jfw2oJeRMKEmw7aH6MGBJ3or',
  345. '2nyhqdwKcJZR2vcqCyrYsaPVdAnFoJjiksCXJ7hfEYgD',
  346. 'D1Mc6j9xQWgR1o1Z7yU5nVVXFQiAYx7FG9AW1aVfwrUM'
  347. ];
  348. const TIP_LAMPORTS = 200_000; // 0.002 SOL minimum
  349.  
  350.  
  351.  
  352. /**
  353. * Build an atomic VersionedTransaction that wraps:
  354. * [ flashBorrowIxn, ...computeBudget, ...setup, swap1, swap2, cleanup, flashRepayIxn, jito tip ]
  355. */
  356. async function buildAtomicFlashloanTransaction(
  357. connection: Connection,
  358. wallet: Keypair,
  359. market: any,
  360. reserve: any,
  361. flashLoanAmountLamports: bigint,
  362. swapInstructions1: SwapInstructionsResponse,
  363. swapInstructions2: SwapInstructionsResponse
  364. ): Promise<VersionedTransaction> {
  365. // 1) Derive destination ATA for borrowed mint (WSOL)
  366. const destinationAta = await getAssociatedTokenAddress(WSOL_MINT, wallet.publicKey, false);
  367.  
  368. // 2) Get lending market authority
  369. const lendingMarketAuthority = await market.getLendingMarketAuthority();
  370.  
  371. // 3) Build flash borrow & repay instructions
  372. const { flashBorrowIxn, flashRepayIxn } = getFlashLoanInstructions({
  373. borrowIxIndex: 0,
  374. userTransferAuthority: wallet.publicKey,
  375. lendingMarketAuthority,
  376. lendingMarketAddress: market.getAddress(),
  377. reserve,
  378. amountLamports: flashLoanAmountLamports,
  379. destinationAta,
  380. referrerAccount: undefined,
  381. referrerTokenState: undefined,
  382. programId: KAMINO_PROGRAM_ID,
  383. });
  384. // right after getFlashLoanInstructions(...) and the destructure
  385. console.log('--- flashBorrowIxn keys ---');
  386. flashBorrowIxn.keys.forEach((k, i) =>
  387. console.log(i, k.pubkey?.toBase58?.() ?? String(k.pubkey), 'isSigner=', k.isSigner, 'isWritable=', k.isWritable)
  388. );
  389. console.log('--- flashRepayIxn keys ---');
  390. flashRepayIxn.keys.forEach((k, i) =>
  391. console.log(i, k.pubkey?.toBase58?.() ?? String(k.pubkey), 'isSigner=', k.isSigner, 'isWritable=', k.isWritable)
  392. );
  393. // Insert after the getFlashLoanInstructions(...) call and before pushing instructions
  394.  
  395. // 1) Dump raw keys shape (useful for debugging)
  396. console.log('RAW flashBorrowIxn.keys (inspect):', JSON.stringify(flashBorrowIxn.keys, null, 2));
  397. console.log('RAW flashRepayIxn.keys (inspect):', JSON.stringify(flashRepayIxn.keys, null, 2));
  398.  
  399. // 2) Sanitizer: ensure every AccountMeta has a PublicKey instance
  400. function sanitizeKaminoIxn(ix: TransactionInstruction, walletPubkey: PublicKey): TransactionInstruction {
  401. if (!ix || !Array.isArray(ix.keys)) throw new Error('Bad instruction returned by Kamino SDK');
  402.  
  403. const sanitizedKeys = ix.keys.map((k: any, i: number) => {
  404. // Keep isSigner/isWritable flags robust
  405. const isSigner = Boolean(k?.isSigner);
  406. const isWritable = Boolean(k?.isWritable);
  407.  
  408. // Determine a safe pubkey value:
  409. let pub = k?.pubkey;
  410.  
  411. // If it's already a PublicKey instance, great
  412. if (pub && typeof pub.toBase58 === 'function') {
  413. return { pubkey: pub, isSigner, isWritable };
  414. }
  415.  
  416. // If it's a string (some libs return base58 strings), convert it
  417. if (typeof pub === 'string' && pub.length > 0) {
  418. try {
  419. return { pubkey: new PublicKey(pub), isSigner, isWritable };
  420. } catch (e) {
  421. /* fallthrough to fallback */
  422. }
  423. }
  424.  
  425. // If missing and this entry requires a signer, assume it's the userTransferAuthority
  426. if (isSigner) {
  427. console.warn(`sanitize: instruction key #${i} was missing but marked signer -> using wallet.pubkey`);
  428. return { pubkey: walletPubkey, isSigner: true, isWritable };
  429. }
  430.  
  431. // Otherwise replace with SystemProgram so compilation won't crash.
  432. // This is a safe dummy for non-signer entries (will surface a logical error later if it's wrong).
  433. console.warn(`sanitize: instruction key #${i} missing pubkey -> substituting SystemProgram (non-signer)`);
  434. return { pubkey: KAMINO_PROGRAM_ID, isSigner: false, isWritable };
  435. });
  436.  
  437. // Build a clean TransactionInstruction (preserve programId and data)
  438. return new TransactionInstruction({
  439. programId: ix.programId instanceof PublicKey ? ix.programId : new PublicKey(String(ix.programId)),
  440. keys: sanitizedKeys as any,
  441. data: Buffer.isBuffer(ix.data) ? ix.data : Buffer.from(ix.data ?? ''),
  442. });
  443. }
  444.  
  445. // 3) Sanitize both instructions
  446. const borrowIxnSan = sanitizeKaminoIxn(flashBorrowIxn, wallet.publicKey);
  447. const repayIxnSan = sanitizeKaminoIxn(flashRepayIxn, wallet.publicKey);
  448.  
  449. // 4) Optionally log sanitized keys for verification
  450. console.log('--- SANITIZED flashBorrowIxn keys ---');
  451. borrowIxnSan.keys.forEach((k, i) =>
  452. console.log(i, k.pubkey.toBase58(), 'isSigner=', k.isSigner, 'isWritable=', k.isWritable)
  453. );
  454. console.log('--- SANITIZED flashRepayIxn keys ---');
  455. repayIxnSan.keys.forEach((k, i) =>
  456. console.log(i, k.pubkey.toBase58(), 'isSigner=', k.isSigner, 'isWritable=', k.isWritable)
  457. );
  458.  
  459. // 4) Assemble all instructions in order (PRUNED)
  460. const allInstructions: TransactionInstruction[] = [];
  461.  
  462.  
  463. // Then push sanitized instructions instead of originals
  464.  
  465. // ...
  466. // and later for repay:
  467.  
  468.  
  469. // --- flash borrow (must be first) ---
  470. allInstructions.push(borrowIxnSan);
  471.  
  472. // --- swap 1 (WSOL β†’ USDC) NO setup, NO cleanup ---
  473. allInstructions.push(
  474. safeDeserializeIx(swapInstructions1.swapInstruction, 'swap1.swap')
  475. );
  476.  
  477. // --- swap 2 (USDC β†’ WSOL) NO setup, NO cleanup ---
  478. allInstructions.push(
  479. safeDeserializeIx(swapInstructions2.swapInstruction, 'swap2.swap')
  480. );
  481.  
  482. // --- flash repay (must be last) ---
  483. allInstructions.push(repayIxnSan);
  484. allInstructions.push(
  485. SystemProgram.transfer({
  486. fromPubkey: wallet.publicKey,
  487. toPubkey: new PublicKey(HELIUS_TIP_ACCOUNTS[0]),
  488. lamports: TIP_LAMPORTS,
  489. })
  490. );
  491. // sanity gate
  492. console.log('πŸ“ Final instruction count:', allInstructions.length);
  493. if (allInstructions.length > 8) throw new Error('TX too large – abort');
  494.  
  495. // 5) Get address lookup tables from both swaps
  496. const lookupTableAddresses = [
  497. ...(swapInstructions1.addressLookupTableAddresses || []),
  498. ...(swapInstructions2.addressLookupTableAddresses || []),
  499. ];
  500.  
  501. let addressLookupTableAccounts: AddressLookupTableAccount[] = [];
  502. if (lookupTableAddresses.length > 0) {
  503. addressLookupTableAccounts = await getAddressLookupTableAccounts(
  504. connection,
  505. lookupTableAddresses
  506. );
  507. }
  508.  
  509. // 6) Get recent blockhash
  510. const { blockhash } = await connection.getLatestBlockhash('finalized');
  511. // ALT: 1) collect every address we will ever touch -------------------------
  512. const allKeys = [
  513. wallet.publicKey,
  514. market.getAddress(),
  515. reserve.address,
  516. lendingMarketAuthority,
  517. destinationAta,
  518. WSOL_MINT,
  519. USDC_MINT,
  520. ];
  521.  
  522. // ALT: 2) ensure ALT exists (cache it in prod) -----------------------------
  523. const alt = await getAlt(connection, wallet, allKeys);
  524. const jupAlts = await getAddressLookupTableAccounts(
  525. connection,
  526. lookupTableAddresses
  527. );
  528.  
  529.  
  530. // ALT: 3) build v0 message --------------------------------------------------
  531. const messageV0 = new TransactionMessage({
  532. payerKey: wallet.publicKey,
  533. recentBlockhash: blockhash,
  534. instructions: allInstructions,
  535. }).compileToV0Message([...jupAlts, alt]); // <-- ALT injected here
  536. const serialized = messageV0.serialize();
  537. console.log("TX SIZE:", serialized.length);
  538. // ALT: 4) create VersionedTransaction --------------------------------------
  539. const vtx = new VersionedTransaction(messageV0);
  540. vtx.sign([wallet]); // payer signature
  541. return vtx; // now returns VersionedTransaction
  542.  
  543. }
  544.  
  545. /** Send single signed VersionedTransaction to Jito as a bundle */
  546. /**
  547. * Send the already-signed VersionedTransaction through Helius Sender
  548. * (it automatically adds Jito tip + routes to Jito + Helius validators)
  549. */
  550.  
  551.  
  552.  
  553. async function sendWithSender(signedTx: Transaction | VersionedTransaction): Promise<string> {
  554. const base64 = Buffer.from(signedTx.serialize()).toString('base64');
  555. console.log('DEBUG tx size:', base64.length, 'chars');
  556.  
  557.  
  558.  
  559. const resp = await fetch(
  560. {
  561. method: 'POST',
  562. headers: { 'Content-Type': 'application/json' },
  563. body: JSON.stringify({
  564. jsonrpc: '2.0',
  565. id: Date.now().toString(),
  566. method: 'sendTransaction',
  567. params: [
  568. base64,
  569. { encoding: 'base64', skipPreflight: true, maxRetries: 0 },
  570. ],
  571. }),
  572. }
  573. );
  574.  
  575. const raw = await resp.json();
  576. console.log('DEBUG Sender response:', JSON.stringify(raw, null, 2)); // ← add this
  577. const { result, signature, error } = raw;
  578.  
  579. const txid = signature ?? result;
  580. if (error) throw new Error(error.message);
  581. console.log('βœ… Bundle landed:', txid);
  582. return txid;
  583. }
  584.  
  585.  
  586.  
  587. /**
  588. * Main executor entry. Called from scanner when opportunity is found.
  589. * @param quote1 - WSOL -> USDC quote from scanner
  590. * @param quote2 - USDC -> WSOL quote from scanner
  591. * @param tradeAmountSol - Amount in SOL
  592. */
  593. export async function onOpportunityFound(quote1: any, quote2: any, tradeAmountSol: number) {
  594. console.log('\n🎯 ================================================');
  595. console.log('🎯 EXECUTOR: Opportunity handler invoked');
  596. console.log('🎯 ================================================\n');
  597.  
  598. try {
  599. const wallet = loadKeypairFromEnv();
  600. const connection = new Connection(SOLANA_RPC, 'confirmed');
  601.  
  602. console.log('πŸ’Ό Wallet:', wallet.publicKey.toBase58());
  603.  
  604. // Load Kamino market
  605. console.log('🏦 Loading Kamino market...');
  606. const market = await KaminoMarket.load(connection, KAMINO_MARKET_PUBKEY);
  607.  
  608. // Find WSOL reserve
  609. const reserve = market.getReserveByMint(WSOL_MINT);
  610. if (!reserve) {
  611. throw new Error('WSOL reserve not found in Kamino market');
  612. }
  613. console.log('βœ… Kamino market loaded successfully');
  614.  
  615. // 1) Get swap instructions from Jupiter for both swaps
  616. console.log('\nπŸ“‹ Fetching swap instructions from Jupiter...');
  617.  
  618. const swapInstructions1 = await getJupiterSwapInstructions(quote1, wallet.publicKey.toBase58());
  619. if (!swapInstructions1) {
  620. throw new Error('Failed to fetch swap instructions for WSOL -> USDC');
  621. }
  622. console.log('βœ… Swap 1 instructions received (WSOL -> USDC)');
  623.  
  624. const swapInstructions2 = await getJupiterSwapInstructions(quote2, wallet.publicKey.toBase58());
  625. if (!swapInstructions2) {
  626. throw new Error('Failed to fetch swap instructions for USDC -> WSOL');
  627. }
  628. console.log('βœ… Swap 2 instructions received (USDC -> WSOL)');
  629.  
  630. // 2) Compute borrow size
  631. const amountLamports = new Decimal(tradeAmountSol).mul(new Decimal(1e9)).floor();
  632. console.log('\nπŸ’° Flash loan amount:', tradeAmountSol, 'SOL (', amountLamports.toString(), 'lamports)');
  633.  
  634. // 3) Build atomic transaction
  635. console.log('\nπŸ”¨ Building atomic flash loan transaction...');
  636. const vtx = await buildAtomicFlashloanTransaction(
  637. connection,
  638. wallet,
  639. market,
  640. reserve,
  641. amountLamports,
  642. swapInstructions1,
  643. swapInstructions2
  644. );
  645.  
  646. // 4) Send to Jito helius
  647. console.log('\nπŸ“€ Sending transaction via jito helius...');
  648. await sendWithSender(vtx);
  649.  
  650.  
  651. console.log('\nβœ… ================================================');
  652. console.log('βœ… EXECUTOR: Bundle sent successfully!');
  653. console.log('βœ… ================================================\n');
  654. } catch (error) {
  655. console.error('\n❌ ================================================');
  656. console.error('❌ EXECUTOR FAILED:', error);
  657. console.error('❌ ================================================\n');
  658. throw error;
  659. }
  660. }
  661.  
  662. //close ALT.ts
  663. import {
  664. Connection,
  665. Keypair,
  666. PublicKey,
  667. Transaction,
  668. AddressLookupTableProgram,
  669. } from "@solana/web3.js";
  670. import bs58 from "bs58";
  671. import * as dotenv from "dotenv";
  672.  
  673. dotenv.config();
  674.  
  675. /* ================= CONFIG ================= */
  676.  
  677. const RPC =
  678. process.env.SOLANA_RPC ??
  679.  
  680. // πŸ”΄ PUT THE ALT YOU WANT TO CLOSE HERE
  681. const ALT_ADDRESS = new PublicKey(
  682. process.env.ALT_ADDRESS ?? ""
  683. );
  684.  
  685. /* ========================================= */
  686.  
  687. function loadKeypairFromEnv(): Keypair {
  688. const pk = process.env.PRIVATE_KEY;
  689. if (!pk) throw new Error("PRIVATE_KEY missing in .env");
  690.  
  691. if (pk.trim().startsWith("[")) {
  692. return Keypair.fromSecretKey(
  693. Uint8Array.from(JSON.parse(pk))
  694. );
  695. }
  696.  
  697. return Keypair.fromSecretKey(bs58.decode(pk));
  698. }
  699.  
  700. async function sleep(ms: number) {
  701. return new Promise((r) => setTimeout(r, ms));
  702. }
  703.  
  704. async function main() {
  705. if (!process.env.ALT_ADDRESS) {
  706. throw new Error("ALT_ADDRESS missing in .env");
  707. }
  708.  
  709. const connection = new Connection(RPC, "confirmed");
  710. const wallet = loadKeypairFromEnv();
  711.  
  712. console.log("πŸ‘› Wallet:", wallet.publicKey.toBase58());
  713. console.log("πŸ“¦ ALT:", ALT_ADDRESS.toBase58());
  714.  
  715. /* 1️⃣ Deactivate ALT */
  716. console.log("⏸️ Deactivating ALT...");
  717.  
  718. const deactivateTx = new Transaction().add(
  719. AddressLookupTableProgram.deactivateLookupTable({
  720. lookupTable: ALT_ADDRESS,
  721. authority: wallet.publicKey,
  722. })
  723. );
  724.  
  725. const deactivateSig = await connection.sendTransaction(
  726. deactivateTx,
  727. [wallet]
  728. );
  729.  
  730. console.log("βœ… Deactivate tx:", deactivateSig);
  731.  
  732. /* 2️⃣ Wait cooldown (~512 slots) */
  733. console.log("⏳ Waiting cooldown (~3 minutes)...");
  734. await sleep(180_000);
  735.  
  736. /* 3️⃣ Close ALT */
  737. console.log("πŸ—‘οΈ Closing ALT...");
  738.  
  739. const closeTx = new Transaction().add(
  740. AddressLookupTableProgram.closeLookupTable({
  741. lookupTable: ALT_ADDRESS,
  742. authority: wallet.publicKey,
  743. recipient: wallet.publicKey,
  744. })
  745. );
  746.  
  747. const closeSig = await connection.sendTransaction(
  748. closeTx,
  749. [wallet]
  750. );
  751.  
  752. console.log("πŸ’° ALT closed, rent refunded");
  753. console.log("βœ… Close tx:", closeSig);
  754. }
  755.  
  756. main().catch((e) => {
  757. console.error("❌ Failed:", e);
  758. process.exit(1);
  759. });
RAW Gist Data Copied