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 { CompilableTransactionMessage } 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[], ): CompilableTransactionMessage { 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)[]> { return new Promise((resolve) => { const results: (T | null)[] = new Array(promises.length).fill(null); const completedPromises = new Set(); let completedCount = 0; let initialTimeoutExpired = false; let finalTimeoutExpired = false; const hasValidRoute = () => { return results.some( (result) => result !== null && result !== undefined && Array.isArray(result) && result.length > 0, ); }; const logIncompletePromises = (timeoutType: string) => { const incompletePromises = promises .map((promiseWithName, index) => ({ name: promiseWithName.name, index })) .filter(({ index }) => !completedPromises.has(index)) .map(({ name }) => name); if (incompletePromises.length > 0) { logger.warn(`${timeoutType} timeout: Incomplete promises: ${incompletePromises.join(', ')}`); } }; const checkAndResolve = () => { if (finalTimeoutExpired || (initialTimeoutExpired && hasValidRoute())) { resolve(results); if (timeoutCallback) { promises.forEach((promiseWithName, index) => { if (!completedPromises.has(index)) { timeoutCallback(promiseWithName.name); } }); } } }; promises.forEach((promise, index) => { promise.promise .then((result) => { if (!finalTimeoutExpired) { results[index] = result; } }) .catch(() => { if (!finalTimeoutExpired) { results[index] = null; } }) .finally(() => { if (!finalTimeoutExpired) { completedPromises.add(index); completedCount++; if (completedCount === promises.length) { finalTimeoutExpired = true; resolve(results); } } }); }); // Initial timeout (smaller, e.g., 700ms) setTimeout(() => { if (!finalTimeoutExpired) { initialTimeoutExpired = true; logger.info(`Initial timeout expired after ${initialTimeoutMs}ms`); logIncompletePromises('Initial'); checkAndResolve(); } }, initialTimeoutMs); // Final timeout (larger, e.g., 2000ms) - only used if we got at least one result setTimeout(() => { if (!finalTimeoutExpired) { finalTimeoutExpired = true; logger.info(`Final timeout expired after ${atLeastOneNoMoreThanTimeoutMS}ms`); logIncompletePromises('Final'); checkAndResolve(); } }, atLeastOneNoMoreThanTimeoutMS); }); } 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; }