import { Decimal } from 'decimal.js'; import axios from 'axios'; import { Account, address, Address, addSignersToTransactionMessage, appendTransactionMessageInstructions, compileTransaction, compressTransactionMessageUsingAddressLookupTables, createKeyPairFromBytes, createSignerFromKeyPair, createTransactionMessage, getBase58Decoder, getSignatureFromTransaction, Instruction, KeyPairSigner, pipe, Rpc, RpcSubscriptions, sendAndConfirmTransactionFactory, setTransactionMessageFeePayer, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, SolanaRpcApi, SolanaRpcSubscriptionsApi, Transaction, TransactionSigner, TransactionWithLifetime, } from '@solana/kit'; import { COMPUTE_BUDGET_PROGRAM_ADDRESS } from '@solana-program/compute-budget'; import { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, fetchAllMaybeMint, fetchMint, findAssociatedTokenPda, getCreateAssociatedTokenIdempotentInstruction, TOKEN_2022_PROGRAM_ADDRESS, } from '@solana-program/token-2022'; import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token'; import BN from 'bn.js'; import { AddressLookupTable } from '@solana-program/address-lookup-table'; import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system'; import { Logger } from './Logger'; import { BlockhashWithExpiry } from '../swap_api_utils'; import { transformFromLookupTableAccounts } from './lookupTable'; import { BaseTransactionMessage, TransactionMessageWithFeePayer } from '@solana/transaction-messages'; export function removeComputeBudgetProgram(ixs: Instruction[]): Instruction[] { const filteredIxs = ixs.filter((ix) => { if (ix.programAddress === COMPUTE_BUDGET_PROGRAM_ADDRESS) { return false; } return true; }); return filteredIxs; } export function removeSetupIxs(ixs: Instruction[]): Instruction[] { const filteredIxs = ixs.filter((ix) => { if ( ix.programAddress === ASSOCIATED_TOKEN_PROGRAM_ADDRESS || ix.programAddress === SYSTEM_PROGRAM_ADDRESS || ix.programAddress === TOKEN_PROGRAM_ADDRESS || ix.programAddress === TOKEN_2022_PROGRAM_ADDRESS ) { return false; } return true; }); return filteredIxs; } export async function parseKeypairFile(file: string): Promise { // eslint-disable-next-line @typescript-eslint/no-require-imports const keypairFile = require('fs').readFileSync(file); const keypairBytes = new Uint8Array(JSON.parse(keypairFile.toString())); const keypair = await createKeyPairFromBytes(keypairBytes); return createSignerFromKeyPair(keypair); } export function amountToLamportsBN(amount: Decimal, decimals: number): BN { const factor = Math.pow(10, decimals); return new BN(amount.mul(factor).floor().toString()); } export function amountToLamportsDecimal(amount: Decimal, decimals: number): Decimal { const factor = Math.pow(10, decimals); return amount.mul(factor); } export function lamportsToAmountDecimal(amount: Decimal, decimals: number): Decimal { const factor = Math.pow(10, decimals); return amount.div(factor); } export async function getMintDecimals(connection: Rpc, mint: Address): Promise { const info = await fetchMint(connection, mint); return info.data.decimals; } export async function getAssociatedTokenAddress( ownerAddress: Address, tokenMintAddress: Address, tokenProgramAddress: Address = TOKEN_PROGRAM_ADDRESS, ): Promise
{ const [associatedTokenAddress] = await findAssociatedTokenPda({ mint: tokenMintAddress, owner: ownerAddress, tokenProgram: tokenProgramAddress, }); return associatedTokenAddress; } export async function createAssociatedTokenAccountIdempotentInstruction( owner: Address, mint: Address, payer: TransactionSigner, tokenProgramAddress: Address = TOKEN_PROGRAM_ADDRESS, ata?: Address, ): Promise<[Address, Instruction]> { let ataAddress = ata; if (!ataAddress) { ataAddress = await getAssociatedTokenAddress(owner, mint, tokenProgramAddress); } const idempotentCreateUserTokenAccountIx = getCreateAssociatedTokenIdempotentInstruction( { payer, ata: ataAddress, mint, owner, tokenProgram: tokenProgramAddress, systemProgram: SYSTEM_PROGRAM_ADDRESS, }, { programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS }, ); return [ataAddress, idempotentCreateUserTokenAccountIx]; } export async function createAtaIdempotent( user: Address, payer: TransactionSigner, mint: Address, mintTokenProgramAddress: Address = TOKEN_PROGRAM_ADDRESS, ): Promise<{ ata: Address; createAtaIx: Instruction }> { const [ata, createAtaIx] = await createAssociatedTokenAccountIdempotentInstruction( user, mint, payer, mintTokenProgramAddress, ); return { ata, createAtaIx }; } export function buildTransactionMessage( ixns: Instruction[], payer: Address, blockhash: BlockhashWithExpiry, addressLookupTables?: Account[], ): BaseTransactionMessage & TransactionMessageWithFeePayer { const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayer(payer, tx), (tx) => appendTransactionMessageInstructions(ixns, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(blockhash, tx), ); if (addressLookupTables) { const compressedTransactionMessage = compressTransactionMessageUsingAddressLookupTables( transactionMessage, transformFromLookupTableAccounts(addressLookupTables), ); return compressedTransactionMessage; } return transactionMessage; } export function buildCompiledTransaction( ixns: Instruction[], payer: Address, blockhash: BlockhashWithExpiry, addressLookupTables?: Account[], ): Transaction & TransactionWithLifetime { const transactionMessage = buildTransactionMessage(ixns, payer, blockhash, addressLookupTables); return compileTransaction(transactionMessage); } export async function executeTransaction( rpc: Rpc, rpcWs: RpcSubscriptions, ixns: Instruction[], signer: TransactionSigner, extraSigners: TransactionSigner[] = [], ): Promise { const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(signer, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions(ixns, tx), ); const transactionsMessageWithSigners = extraSigners.length ? addSignersToTransactionMessage(extraSigners, transactionMessage) : transactionMessage; const signedTransaction = await signTransactionMessageWithSigners(transactionsMessageWithSigners); await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions: rpcWs })(signedTransaction, { commitment: 'confirmed', }); return getSignatureFromTransaction(signedTransaction); } export function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } export const getEnvOrDefault = (envVarName: string, defaultValue: string): string => { if (envVarName in process.env) { return process.env[envVarName] as string; } return defaultValue; }; export async function getMintTokenProgram(connection: Rpc, mint: Address): Promise
{ const mintAccount = await connection.getAccountInfo(mint).send(); if (!mintAccount.value) { throw new Error('Mint not found'); } return mintAccount.value.owner; } export async function getAllMintTokenPrograms(connection: Rpc, mints: Address[]): Promise { const mintAccounts = await fetchAllMaybeMint(connection, mints); const programAddresses: Address[] = []; for (const mint of mintAccounts) { if (!mint.exists) { throw new Error(`Mint ${mint.address} not found`); } programAddresses.push(mint.programAddress); } return programAddresses; } // Helper function to create a timeout promise function createTimeout(ms: number): Promise { return new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Request timed out after ${ms}ms`)); }, ms); }); } // Function to wrap a promise with a timeout export async function withTimeout( promise: Promise, timeoutMs: number, loggerCallback: () => any, ): Promise { if (timeoutMs <= 0) { return promise; } try { return await Promise.race([promise, createTimeout(timeoutMs)]); } catch (error) { if (error instanceof Error && error.message.includes('timed out')) { loggerCallback(); return null; // Return null for timed-out routes } throw error; // Re-throw other errors } } export async function withAtLeastOneTimeout( promises: PromiseWithName[], atLeastOneNoMoreThanTimeoutMS: number, initialTimeoutMs: number, logger: Logger, timeoutCallback?: (name: string) => undefined, ): Promise<(T | null)[]> { // Nothing to wait for — resolve immediately instead of stalling on the timers. if (promises.length === 0) { return []; } const results: (T | null)[] = new Array(promises.length).fill(null); const completed = new Set(); const isValidRoute = (result: T | null): boolean => Array.isArray(result) && result.length > 0; // Each input records its own result as it lands; these never reject, so // Promise.all over them resolves exactly when every input has settled. const recorded = promises.map(({ promise }, index) => promise.then( (result) => { results[index] = result; completed.add(index); }, () => { completed.add(index); }, ), ); const allSettled = Promise.all(recorded); // Fulfils only once an input produces a valid route, and stays pending // otherwise — so Promise.any over these fires on the first valid route and // never rejects. // // TODO(vicentiu): This is a hack, actually relying on promises only // refering to routes, nothing else. If the input type changes, this will break. // // Investigate all call sites and don't rely on this assumption. const anyValidRoute = Promise.any( promises.map(({ promise }) => promise.then( (result) => (isValidRoute(result) ? undefined : new Promise(() => {})), () => new Promise(() => {}), ), ), ); // Two timer promises resolving to a tag so the races below know who won. let initialTimeoutHandle: ReturnType | undefined; let finalTimeoutHandle: ReturnType | undefined; const initialTimeout = new Promise<'initial'>((resolve) => { initialTimeoutHandle = setTimeout(() => resolve('initial'), initialTimeoutMs); }); const finalTimeout = new Promise<'final'>((resolve) => { finalTimeoutHandle = setTimeout(() => resolve('final'), atLeastOneNoMoreThanTimeoutMS); }); const logIncompletePromises = (timeoutType: string) => { const incompletePromises = promises.filter((_, index) => !completed.has(index)).map(({ name }) => name); if (incompletePromises.length > 0) { logger.warn(`${timeoutType} timeout: Incomplete promises: ${incompletePromises.join(', ')}`); } }; try { // Phase 1: return as soon as everything settled, or wait out the initial window. const firstPhase = await Promise.race([allSettled.then(() => 'all' as const), initialTimeout]); if (firstPhase === 'initial') { logIncompletePromises('Initial'); // Phase 2: unless a valid route is already in, wait for the first valid // route, for everything to settle without one, or for the final ceiling. if (!results.some(isValidRoute)) { const secondPhase = await Promise.race([ anyValidRoute.then(() => 'valid' as const), allSettled.then(() => 'all' as const), finalTimeout, ]); if (secondPhase === 'final') { logIncompletePromises('Final'); } } } // Snapshot: stragglers keep writing into `results` after we return, but the // caller's array must not change once handed over. return [...results]; } finally { clearTimeout(initialTimeoutHandle); clearTimeout(finalTimeoutHandle); if (timeoutCallback) { promises.forEach(({ name }, index) => { if (!completed.has(index)) { timeoutCallback(name); } }); } } } export interface PromiseWithName { promise: Promise; name: string; } // Define the request interface interface FindMinimalLUTRequest { addresses: string[]; verify?: boolean; } // Define the response interface interface FindMinimalLUTResponse { lutAddresses: string[]; } export function generateUniqueMockAddress(): Address { const randomBytes = new Uint8Array(32); crypto.getRandomValues(randomBytes); return address(getBase58Decoder().decode(randomBytes)); } export async function getLookupTablesFromLutApi(mintAddresses: Address[]): Promise { try { const response = await axios.post( 'https://api.kamino.finance/luts/find-minimal', { addresses: mintAddresses, verify: true, } as FindMinimalLUTRequest, { headers: { 'Content-Type': 'application/json', }, }, ); if (!response.data || !Array.isArray(response.data)) { return []; } return response.data.map((addr) => address(addr)); } catch (error) { if (axios.isAxiosError(error)) { console.error('API Error:', error.response?.data || error.message); throw new Error(`Failed to find minimal LUT addresses`, { cause: error }); } throw error; } } export function getAccountsNotInLookupTables( lookupTables: Account[], instructions: Instruction[], ): Address[] { // Create a set of all accounts in lookup tables for fast lookup const lookupTableAccounts = new Set(); for (const lookupTable of lookupTables) { for (const account of lookupTable.data.addresses) { lookupTableAccounts.add(account); } } const accountsNotInLookupTables = new Set(); for (const instruction of instructions) { // Skip associated token program instructions to avoid user atas if (instruction.programAddress === ASSOCIATED_TOKEN_PROGRAM_ADDRESS) { continue; } for (const accountMeta of instruction.accounts ?? []) { const accountKey = accountMeta.address; if (!lookupTableAccounts.has(accountKey)) { accountsNotInLookupTables.add(accountKey); } } } return Array.from(accountsNotInLookupTables).map((key) => address(key)); } export function pick(obj: T, keys: K[]): Pick { const result = {} as Pick; for (const key of keys) { if (key in obj) { result[key] = obj[key]; } } return result; }