import { SDK_VERSION } from './version'; import { BestRouteResponse, BirdeyePriceData, BlockhashWithExpiry, compileRouteIxs, deserializeRouteOutput, ExecutePerRouteParams, ExecuteRouteParams, GetJupiterPriceParams, GetJupiterPriceResponse, getLimoLogsIxsForRoute, getQuoteParamsKeys, QuoteParams, RouteInstructions, RouteOrQuoteParams, RouteOutput, RouteOutputSerialized, RouteParams, Router, RouterContext, RoutesResponse, serializeQuoteParams, serializeRouteParams, SwapType, } from './swap_api_utils'; import { Axios } from 'axios'; import { EventStreamDecoder } from './utils/EventStreamDecoder'; import { buildCompiledTransaction, createAddExtraComputeUnitsTransaction, getAllMintTokenPrograms, pick, PromiseWithName, removeComputeBudgetIxs, simulateSwapsWithATABalances, SwapSimulationResult, withAtLeastOneTimeout, withTimeout, } from './utils'; import { DEFAULT_TIMEOUT_MS, EXCLUDED_QUOTES_ROUTERS, getRouterTypeID, JUP_V6_BASE_URL, RouterType, SUPPORTED_ROUTER_TYPES, } from './consts'; import { executeJupiterZTransaction, fetchJupiterPrice, JupiterUltraRouter } from './router/jupiterUltra'; import BN from 'bn.js'; import { LimoClient, WRAPPED_SOL_MINT } from '@kamino-finance/limo-sdk'; import { includeLimoLogs } from './swap_api_utils/limoLogs'; import { createJupiterApiClient, QuoteGetRequest, QuoteResponse, SwapInstructionsResponse, SwapRequest, SwapResponse, } from '@jup-ag/api'; import { addAsyncDataToRouteOutput, addPricesAndPriceImpactToRouteOutput, getAsyncDataFromApiRouteOutput, } from './utils/calcs'; import { filterArrayRouterTypes, isRequestedRouterType } from './swap_api_utils/utils'; import { getLimoLedgerIxsForRoute, includeLedgerIxs } from './swap_api_utils/limoLedgerIxs'; import { Logger, LogLevel } from './utils/Logger'; import { HttpClientError } from './utils/HttpClientError'; import { addBeforeRequestInterceptor, addErrorHandlingInterceptor, addHttpRequestLoggingInterceptor, addHttpResponseLoggingInterceptor, addRequestErrorInterceptor, type BeforeRequestCallback, type OnRequestErrorCallback, ResponseLogConfig, } from './utils/axiosUtils'; import { Address, FullySignedTransaction, getBase58Decoder, getBase64EncodedWireTransaction, Instruction, Rpc, RpcSubscriptions, signature, Signature, SolanaRpcApi, SolanaRpcSubscriptionsApi, Transaction, TransactionSigner, } from '@solana/kit'; import { findAssociatedTokenPda } from '@solana-program/token-2022'; import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token'; const DEFAULT_HEADERS = { 'x-sdk-client': `kswap-ts-sdk/${SDK_VERSION}`, 'Accept-Encoding': 'gzip, compress, deflate, br', }; export type LoggerConfig = { logger?: Logger; requestLogLevel?: LogLevel; responseLogConfig?: ResponseLogConfig; beforeRequest?: BeforeRequestCallback; // Called before each API request (for rate limiting) onRequestError?: OnRequestErrorCallback; // Called when request fails (for handling 429s, network errors) }; export type RoutesStreamHandle = { close: () => void; }; export class KswapSdk { readonly apiBaseUrl: string; readonly connection: Rpc; readonly subscription: RpcSubscriptions; private readonly _apiClient: Axios; private readonly _defaultHeaders: Record; private readonly limoClient: LimoClient; private routers: Router[] = []; private readonly logger: Logger; // All available router types - using the central constant readonly allRouterTypes: RouterType[] = [...SUPPORTED_ROUTER_TYPES]; constructor( apiBaseUrl: string, connection: Rpc, subscription: RpcSubscriptions, apiKey?: string, { logger = console, requestLogLevel = LogLevel.NONE, responseLogConfig = { responseLogLevel: LogLevel.NONE, responseBodyLogLevel: LogLevel.NONE, responseErrorLogLevel: LogLevel.NONE, responseErrorBodyLogLevel: LogLevel.NONE, }, beforeRequest, onRequestError, }: LoggerConfig = {}, ) { this.logger = logger; this.apiBaseUrl = apiBaseUrl; this.connection = connection; this.subscription = subscription; this._defaultHeaders = { ...DEFAULT_HEADERS }; if (apiKey) { this._defaultHeaders['x-api-key'] = apiKey; } this._apiClient = new Axios({ baseURL: apiBaseUrl, headers: this._defaultHeaders, transformResponse: [ (data) => { try { return JSON.parse(data); } catch { return data; } }, ], validateStatus: (status) => status >= 200 && status < 300, }); // Response interceptors run in reverse order (LIFO) addRequestErrorInterceptor(this._apiClient, onRequestError); addErrorHandlingInterceptor(this._apiClient); this.addHttpLogInterceptors(this._apiClient, this.logger, requestLogLevel, responseLogConfig); addBeforeRequestInterceptor(this._apiClient, beforeRequest); this.routers.push(new JupiterUltraRouter(this.connection, logger)); this.limoClient = new LimoClient(this.connection, this.subscription, undefined); this.logger = logger; } streamRoutes( params: RouteParams, onRoute: (route: RouteOutput) => void, onError?: (error: Error) => void, options?: { keepAlive?: boolean }, ): RoutesStreamHandle { const { keepAlive = true } = options ?? {}; const abortController = new AbortController(); const serialized = serializeRouteParams(params); const buildSearchParams = () => { const searchParams = new URLSearchParams(); for (const [k, v] of Object.entries(serialized)) { if (v === undefined || v === null) { continue; } else if (Array.isArray(v)) { for (const item of v) searchParams.append(k, String(item)); } else { searchParams.set(k, String(v)); } } return searchParams; }; const connect = async () => { try { do { const response = await fetch(`${this.apiBaseUrl}/all-routes?${buildSearchParams()}`, { headers: { ...this._defaultHeaders, Accept: 'text/event-stream' }, signal: abortController.signal, }); if (!response.ok || !response.body) { onError?.(new Error(`HTTP error ${response.status}`)); return; } for await (const { eventType, data } of EventStreamDecoder.decode(response.body)) { if (eventType === 'route') { const route = await deserializeRouteOutput(data.route); if (route) { onRoute(route); } else { onError?.(new Error('Failed to deserialize route from stream')); } } else if (eventType === 'error') { onError?.(new Error(data.error ?? 'Streaming error')); return; } } } while (keepAlive && !abortController.signal.aborted); } catch (err) { if (!(err instanceof Error) || err.name !== 'AbortError') { onError?.(err instanceof Error ? err : new Error(String(err))); } } }; void connect(); return { close: () => abortController.abort() }; } private static isValidRouteOutput(routeOutput: RouteOutput[] | null | undefined): routeOutput is RouteOutput[] { return routeOutput !== undefined && routeOutput !== null && routeOutput.length > 0; } private static shouldExcludeJupiterZRoute(routeOutput: RouteOutput[], params: { includeRfq?: boolean }): boolean { return routeOutput[0].routerType === 'jupiterZ' && params.includeRfq !== undefined && !params.includeRfq; } async getAllRoutesOrQuotes(params: RouteOrQuoteParams, ctx: RouterContext): Promise { // If no filter is provided, use all router types const routerFilter = params.routerTypes && params.routerTypes.length > 0 ? params.routerTypes : this.allRouterTypes; if (!params.executor) { // Use type-safe runtime pick to ensure only QuoteParams fields are passed through const pickedParams = pick(params, getQuoteParamsKeys()); const quoteParams: QuoteParams = { ...pickedParams, routerTypes: routerFilter, executor: undefined, // Explicitly set to undefined for quotes }; return this.getAllQuotes(quoteParams); } else { // Use spread operator to ensure all parameters are passed through const routeParams: RouteParams = { ...params, executor: params.executor, // This is guaranteed to exist due to the if condition routerTypes: routerFilter, }; this.logger.debug('Getting all routes for:', routeParams); return this.getAllRoutes(routeParams, ctx); } } async getAllRoutes(params: RouteParams, ctx: RouterContext): Promise { const endpointUrl = '/all-routes'; const paramsWithFilter = { ...params }; const routerFilter = params.routerTypes && params.routerTypes.length > 0 ? params.routerTypes : this.allRouterTypes; paramsWithFilter.routerTypes = routerFilter as RouterType[]; const paramsSerialized = serializeRouteParams(paramsWithFilter); const timeoutMs = params.timeoutMs ? params.timeoutMs : DEFAULT_TIMEOUT_MS; try { const clientSideRouteOutputs: PromiseWithName[] = this.getClientSideRouteOutputPromises(paramsWithFilter, ctx, timeoutMs); const responsePromise = this._apiClient.get(endpointUrl, { params: paramsSerialized }); const [response, routeOutputsClientSide, blockhash] = await Promise.all([ responsePromise, paramsWithFilter.atLeastOneNoMoreThanTimeoutMS !== undefined ? withAtLeastOneTimeout( clientSideRouteOutputs, paramsWithFilter.atLeastOneNoMoreThanTimeoutMS, timeoutMs, this.logger, ) : Promise.all(clientSideRouteOutputs.map((p) => p.promise)), this.connection.getLatestBlockhash().send(), ]); const routeOutputSerializedArray = response.data.data as RouteOutputSerialized[]; const routeOutputPromises = routeOutputSerializedArray.map((routeOutputSerialized) => deserializeRouteOutput(routeOutputSerialized), ); const routeOutputs = await Promise.all(routeOutputPromises); const definedRouteOutputs = routeOutputs.filter((routeOutput) => routeOutput !== undefined); // Handle client side routers if included in the router types const finalRouteOutputs: RouteOutput[] = await this.processClientSideRouteOutput( definedRouteOutputs, routeOutputsClientSide, paramsWithFilter, blockhash.value, ); return { routes: finalRouteOutputs, traceId: response.data.traceId || '' }; } catch (error) { this.logger.error('Error fetching all routes', error); return { routes: [], traceId: '', error }; } } async getAllQuotes(params: QuoteParams): Promise { const endpointUrl = '/all-quotes'; const paramsWithFilter = { ...params }; paramsWithFilter.routerTypes = this.filterRouterTypes(params.routerTypes); // Exclude routes from quotes paramsWithFilter.routerTypes = paramsWithFilter.routerTypes.filter( (routerType) => !EXCLUDED_QUOTES_ROUTERS.includes(routerType), ); const paramsSerialized = serializeQuoteParams(paramsWithFilter); const timeoutMs = paramsWithFilter.timeoutMs ? paramsWithFilter.timeoutMs : DEFAULT_TIMEOUT_MS; try { const clientSideRouteOutputPromises = this.getClientSideQuoteRouteOutputPromises(paramsWithFilter, timeoutMs); const responsePromise = this._apiClient.get(endpointUrl, { params: paramsSerialized }); const [response, clientSideRouteOutputs] = await Promise.all([ responsePromise, paramsWithFilter.atLeastOneNoMoreThanTimeoutMS !== undefined ? withAtLeastOneTimeout( clientSideRouteOutputPromises, paramsWithFilter.atLeastOneNoMoreThanTimeoutMS, timeoutMs, this.logger, ) : Promise.all(clientSideRouteOutputPromises.map((p) => p.promise)), ]); const routeOutputSerializedArray = response.data.data as RouteOutputSerialized[]; const routeOutputPromises = routeOutputSerializedArray.map((routeOutputSerialized) => deserializeRouteOutput(routeOutputSerialized), ); const routeOutputs = await Promise.all(routeOutputPromises); const definedRouteOutputs = routeOutputs.filter((routeOutput) => routeOutput !== undefined); const finalRouteOutputs = this.processClientSideQuoteRouteOutput( definedRouteOutputs, clientSideRouteOutputs, paramsWithFilter, ); return { routes: finalRouteOutputs, traceId: response.data.traceId || '' }; } catch (error) { this.logger.error('Error fetching all routes', error); return { routes: [], traceId: '', error }; } } async getBestRoute(params: RouteParams): Promise { const endpointUrl = '/best-route'; const paramsWithFilter = { ...params }; paramsWithFilter.routerTypes = this.filterRouterTypes(params.routerTypes); const paramsSerialized = serializeRouteParams(paramsWithFilter); try { const response = await this._apiClient.get(endpointUrl, { params: paramsSerialized }); const routeOutputSerialized = response.data.data as RouteOutputSerialized; const routeOutput = await deserializeRouteOutput(routeOutputSerialized); return { bestRoute: routeOutput, traceId: response.data.traceId, }; } catch (error) { if (error instanceof HttpClientError) { if (error.status === 404 && error.data.error && error.data.error === 'No route found') { // expecting 404 when no route is found return { bestRoute: undefined, traceId: error.data.traceId }; } } this.logger.error('Error fetching best route', error); throw error; } } async getBatchTokenPrices(tokenMints: Address[]): Promise> { const endpointUrl = '/batch-token-prices'; const tokenStrings = tokenMints.map((tokenMint) => tokenMint.toString()); this.logger.debug('Fetching batch token prices for:', tokenStrings); try { const response = await this._apiClient.get(endpointUrl, { params: { tokens: tokenStrings }, paramsSerializer: (params) => { return Object.entries(params) .map(([key, value]) => { if (Array.isArray(value)) { return value.map((val) => `${key}=${encodeURIComponent(val)}`).join('&'); } return `${key}=${encodeURIComponent(value as string)}`; }) .join('&'); }, }); const tokenPrices = response.data.data as { [tokenAddress: string]: BirdeyePriceData }; const tokenPricesMap = new Map(); Object.entries(tokenPrices).forEach(([address, data]) => { tokenPricesMap.set(address, data); }); return tokenPricesMap; } catch (error) { this.logger.error('Error fetching token prices', error); return new Map(); } } async getAllAggregatorsFromApi(): Promise> { const endpointUrl = '/aggregators'; try { const response = await this._apiClient.get(endpointUrl); return response.data.data; } catch (error) { this.logger.error('Error fetching all aggregators', error); return {}; } } async getLedgerIxs( userWallet: TransactionSigner, inTokenAccount: Address, outTokenAccount: Address, maxInputAmountChange: BN, minOutputAmountChange: BN, ): Promise<{ preIx: Instruction; postIx: Instruction }> { const assertSwapBalancesIxs = await this.limoClient.assertUserSwapBalancesIxs({ user: userWallet, inputTa: inTokenAccount, outputTa: outTokenAccount, maxInputAmountChange, minOutputAmountChange, }); return { preIx: assertSwapBalancesIxs.beforeSwapIx, postIx: assertSwapBalancesIxs.afterSwapIx }; } async getJupiterPriceWithFallback(getJupiterPriceParams: GetJupiterPriceParams): Promise { try { const jupiterPriceResponse = await fetchJupiterPrice(getJupiterPriceParams); return jupiterPriceResponse; } catch { // Fallback to KSWAP API endpoint const endpointUrl = '/jupiter/price'; try { const response = await this._apiClient.get(endpointUrl, { params: getJupiterPriceParams }); const jupiterPriceResponse = response.data as GetJupiterPriceResponse; return jupiterPriceResponse; } catch (error) { this.logger.error('Error fetching Jupiter price', error); throw new Error('Failed to fetch prices from both Jupiter and Birdeye APIs', { cause: error }); } } } async getJupiterQuoteWithFallback(getJupiterQuoteParams: QuoteGetRequest): Promise { try { // Use the environment variable value const jupiterClient = createJupiterApiClient({ basePath: JUP_V6_BASE_URL }); const quoteResponse = await jupiterClient.quoteGet(getJupiterQuoteParams); return quoteResponse; } catch { // Fallback to KSWAP API endpoint const endpointUrl = '/jupiter/quote'; try { const response = await this._apiClient.get(endpointUrl, { params: getJupiterQuoteParams }); const jupiterQuoteResponse = response.data as QuoteResponse; return jupiterQuoteResponse; } catch (error) { this.logger.error('Error fetching Jupiter quote', error); throw new Error('Failed to fetch quotes from Jupiter', { cause: error }); } } } async getJupiterSwapInstructionsPostWithFallback(swapPostParams: SwapRequest): Promise { try { // Use the environment variable value const jupiterClient = createJupiterApiClient({ basePath: JUP_V6_BASE_URL }); const swapInstructionsResponse = await jupiterClient.swapInstructionsPost({ swapRequest: swapPostParams }); return swapInstructionsResponse; } catch { // Fallback to KSWAP API endpoint const endpointUrl = '/jupiter/swapInstructionsPost'; try { const jsonParams = JSON.stringify(swapPostParams); const response = await this._apiClient.post(endpointUrl, jsonParams, { headers: { 'Content-Type': 'application/json', }, }); const swapInstructionsResponse = response.data as SwapInstructionsResponse; return swapInstructionsResponse; } catch (error) { this.logger.error('Error fetching Jupiter quote', error); throw new Error('Failed to fetch quotes from Jupiter', { cause: error }); } } } async getJupiterSwapPostWithFallback(swapPostParams: SwapRequest): Promise { try { // Use the environment variable value const jupiterClient = createJupiterApiClient({ basePath: JUP_V6_BASE_URL }); const swapInstructionsResponse = await jupiterClient.swapPost({ swapRequest: swapPostParams }); return swapInstructionsResponse; } catch { // Fallback to KSWAP API endpoint const endpointUrl = '/jupiter/swapPost'; try { const jsonParams = JSON.stringify(swapPostParams); const response = await this._apiClient.post(endpointUrl, jsonParams, { headers: { 'Content-Type': 'application/json', }, }); const swapInstructionsResponse = response.data as SwapResponse; return swapInstructionsResponse; } catch (error) { this.logger.error('Error fetching Jupiter quote', error); throw new Error('Failed to fetch quotes from Jupiter', { cause: error }); } } } private async getExecutePerRoute(params: ExecutePerRouteParams): Promise { const endpointUrl = '/execute-per-route'; try { const response = await this._apiClient.post(endpointUrl, JSON.stringify(params), { headers: { 'Content-Type': 'application/json', }, }); if (response.status !== 200) { this.logger.error('Error response executing per route', response); return undefined; } this.logger.debug('Response from /execute-per-route:', response.data); const sig: string = response.data.data.signature; return signature(sig); } catch (error) { this.logger.error('Error fetching all routes', error); return undefined; } } async simulateRouteAndGetNewSimulatedValuesRouteOutput(params: { routeOutput: RouteOutput; executor: Address; tokenIn: Address; tokenOut: Address; swapType: SwapType; bh: BlockhashWithExpiry; }): Promise<{ routeOutput: RouteOutput; simulationResult: SwapSimulationResult }> { const routeOutputSimulated = { ...params.routeOutput }; const computeBudgetIxs = createAddExtraComputeUnitsTransaction(1_800_000); const simulationIxs = [...computeBudgetIxs, ...params.routeOutput.ixsRouter!]; const compiledTx = buildCompiledTransaction( simulationIxs, params.executor, params.bh, params.routeOutput.lookupTableAccounts, ); let inputMintProgramOwner: Address; let outputMintProgramOwner: Address; if (!params.routeOutput.inputMintProgramOwner || !params.routeOutput.outputMintProgramOwner) { [inputMintProgramOwner, outputMintProgramOwner] = await getAllMintTokenPrograms(this.connection, [ params.tokenIn, params.tokenOut, ]); } else { inputMintProgramOwner = params.routeOutput.inputMintProgramOwner; outputMintProgramOwner = params.routeOutput.outputMintProgramOwner; } const [simulation] = await simulateSwapsWithATABalances( this.connection, [compiledTx], params.executor, params.tokenIn, params.tokenOut, inputMintProgramOwner, outputMintProgramOwner, this.logger, ); if (simulation !== undefined && simulation.outputToken !== null && simulation.inputToken !== null) { const amountOutSimulatedChange = params.tokenOut === WRAPPED_SOL_MINT ? simulation.outputToken.change.add(simulation.nativeChangeAmount) : simulation.outputToken.change; const amountInSimulatedChange = params.tokenIn === WRAPPED_SOL_MINT ? simulation.inputToken.change.add(simulation.nativeChangeAmount) : simulation.inputToken.change; if (params.swapType === 'exactIn') { routeOutputSimulated.amountsExactIn.amountOutSimulated = amountOutSimulatedChange; } else { routeOutputSimulated.amountsExactOut.amountInSimulated = amountInSimulatedChange; } } return { routeOutput: routeOutputSimulated, simulationResult: simulation }; } async executeRoute(params: ExecuteRouteParams): Promise { this.logger.info(`Executing best routes per ${params.router.routerType} ...`); if (params.router.routerType === 'per') { return this.executePerRoute(params); } else if (params.router.routerType === 'jupiterZ') { return this.executeJupiterZRoute(params); } else { let ixs = compileRouteIxs(params.router.instructions!); // Set compute budget instructions if provided try { if (params.computeBudgetInstructions && params.computeBudgetInstructions.length > 0) { ixs = removeComputeBudgetIxs(ixs); ixs.unshift(...params.computeBudgetInstructions); } } catch (error) { this.logger.debug('Error setting compute budget instructions', error); throw new Error('Failed to set compute budget instructions', { cause: error }); } let compiledTx: Transaction; try { compiledTx = buildCompiledTransaction( ixs, params.userToExecute, params.recentBlockhash, params.router.lookupTableAccounts, ); } catch (error) { this.logger.debug('Error compiling transaction message', error); throw new Error('Failed to compile transaction message', { cause: error }); } let signedTx: FullySignedTransaction; try { signedTx = await params.signTransaction(compiledTx); } catch (error) { this.logger.debug('Error signing transaction', error); throw new Error('Failed to sign transaction', { cause: error }); } let sig: Signature; try { sig = await params.executeTransaction(signedTx); } catch (error) { this.logger.debug('Error executing transaction', error); throw new Error('Failed to execute transaction', { cause: error }); } await params.confirmTransaction(sig); this.logger.info('Transaction executed successfully', { signature: sig }); return sig; } } private async getClientSideRouteOutputSimulationAndPriceImpact( apiRouteOutputs: RouteOutput[], clientSideRouteOutputs: RouteOutput[], params: RouteParams, blockhash: BlockhashWithExpiry, ): Promise { const routeOutputAsyncData = getAsyncDataFromApiRouteOutput(apiRouteOutputs); const resultRouteOutputs = await Promise.all( clientSideRouteOutputs.map(async (routeOutput) => { routeOutput = addAsyncDataToRouteOutput(routeOutput, routeOutputAsyncData); if (includeLedgerIxs(params) && routeOutput.instructions && routeOutput.routerType !== 'jupiterZ') { const [inputTa] = await findAssociatedTokenPda({ mint: params.tokenIn, owner: params.executor, tokenProgram: routeOutputAsyncData.inputMintProgramOwner ?? TOKEN_PROGRAM_ADDRESS, }); let outputTa: Address; if (params.destinationTokenAccount) { outputTa = params.destinationTokenAccount; } else { [outputTa] = await findAssociatedTokenPda({ mint: params.tokenOut, owner: params.executor, tokenProgram: routeOutputAsyncData.outputMintProgramOwner ?? TOKEN_PROGRAM_ADDRESS, }); } const ledgerIxs = await getLimoLedgerIxsForRoute({ params, limoClient: this.limoClient, inputTa, outputTa, maxInputAmountChange: routeOutput.amountsExactIn.amountIn, minOutputAmountChange: routeOutput.amountsExactIn.amountOutGuaranteed, }); const instructionsWithLedgerIxs: RouteInstructions = { ...routeOutput.instructions, limoLedgerStartIxs: [ledgerIxs.preIx], limoLedgerEndIxs: [ledgerIxs.postIx], }; routeOutput.instructions = instructionsWithLedgerIxs; routeOutput.ixsRouter = compileRouteIxs(instructionsWithLedgerIxs); } // Simulate route let routeOutputSimulated: RouteOutput; // Since jupiterZ acts as an RFQ we skip simulation as the transaction can't be decomposed if (params.withSimulation === undefined || params.withSimulation === true) { if (routeOutput.routerType === 'jupiterZ') { routeOutputSimulated = routeOutput; if (params.swapType === 'exactIn') { routeOutputSimulated.amountsExactIn.amountOutSimulated = routeOutputSimulated.amountsExactIn.amountOut; } else { routeOutputSimulated.amountsExactOut.amountInSimulated = routeOutputSimulated.amountsExactOut.amountIn; } } else { const simulateResults = await this.simulateRouteAndGetNewSimulatedValuesRouteOutput({ routeOutput: routeOutput, executor: params.executor, tokenIn: params.tokenIn, tokenOut: params.tokenOut, swapType: params.swapType, bh: blockhash, }); routeOutputSimulated = simulateResults.routeOutput; } } else { routeOutputSimulated = routeOutput; } // Add price impact to route return addPricesAndPriceImpactToRouteOutput(params.swapType, routeOutputSimulated); }), ); return resultRouteOutputs; } private async addOrReplaceLimoLogsInstructionWithNextBest( routeOutputs: RouteOutput[], params: RouteParams, ): Promise { try { let nextBestAmountOutSimulated = new BN(0); let nextBestRouterTypeId = 0; let bestAmountOutSimulated = new BN(0); let bestRouterTypeId = 0; const routeOutputAsyncData = getAsyncDataFromApiRouteOutput(routeOutputs); routeOutputs.forEach((route) => { if (route.amountsExactIn.amountOutSimulated !== undefined) { const amountOutSimulated = route.amountsExactIn.amountOutSimulated; // Calculating next best route for exactIn swap out of simulated routes if (amountOutSimulated.gt(bestAmountOutSimulated)) { nextBestAmountOutSimulated = bestAmountOutSimulated; nextBestRouterTypeId = bestRouterTypeId; bestAmountOutSimulated = amountOutSimulated; bestRouterTypeId = getRouterTypeID(route.routerType); } else if ( amountOutSimulated.gt(nextBestAmountOutSimulated) && amountOutSimulated.lte(bestAmountOutSimulated) ) { nextBestAmountOutSimulated = amountOutSimulated; nextBestRouterTypeId = getRouterTypeID(route.routerType); } } }); const routeProcessingPromises = routeOutputs.map(async (route) => { if ( includeLimoLogs(params.includeLimoLogs, route.routerType) && route.instructions && !route.skipLimoLogsForRoute ) { const [inputTa] = await findAssociatedTokenPda({ mint: params.tokenIn, owner: params.executor, tokenProgram: routeOutputAsyncData.inputMintProgramOwner ?? TOKEN_PROGRAM_ADDRESS, }); let outputTa: Address; if (params.destinationTokenAccount) { outputTa = params.destinationTokenAccount; } else { [outputTa] = await findAssociatedTokenPda({ mint: params.tokenOut, owner: params.executor, tokenProgram: routeOutputAsyncData.outputMintProgramOwner ?? TOKEN_PROGRAM_ADDRESS, }); } const limoLogsIxs = await getLimoLogsIxsForRoute({ route, params, limoClient: this.limoClient, inputTokenAccount: inputTa, outputTokenAccount: outputTa, nextBestAmountOutSimulated, nextBestRouterTypeId, }); // replace with new log ixs route.instructions = { ...route.instructions, limoLogsStartIxs: [limoLogsIxs.preIx], limoLogsEndIxs: [limoLogsIxs.postIx], }; route.ixsRouter = compileRouteIxs(route.instructions); } return route; }); const processedRoutes = await Promise.all(routeProcessingPromises); // Update the original routeOutputs array with processed routes processedRoutes.forEach((processedRoute, index) => { routeOutputs[index] = processedRoute; }); return routeOutputs; } catch { return routeOutputs; } } private async executePerRoute(params: ExecuteRouteParams): Promise { const tx = params.router.transaction; if (!tx) { throw new Error('Transaction not found in router output'); } const userPublicKey = params.userToExecute; const txUserSigned = await params.signPartialTransaction(tx); let userSignatureBs58: string = ''; for (const [publicKey, signatureBytes] of Object.entries(txUserSigned.signatures)) { if (!signatureBytes) { this.logger.warn('Signature bytes is null'); continue; } if (publicKey === userPublicKey) { userSignatureBs58 = getBase58Decoder().decode(signatureBytes); break; } } if (!userSignatureBs58) { throw new Error('User signature not found after signing'); } if (params.router.perReferenceId === undefined) { throw new Error('PER reference ID not found'); } const sig = await this.getExecutePerRoute({ userSignature: userSignatureBs58, perReferenceId: params.router.perReferenceId, userWallet: params.userToExecute, quoteExpiryMs: params.router.expiryTime, }); if (!sig) { throw new Error('Failed to submit transaction'); } await params.confirmTransaction(sig); return sig; } private async executeJupiterZRoute(params: ExecuteRouteParams): Promise { const tx = params.router.transaction; if (!tx) { throw new Error('Transaction not found in router output'); } if (params.router.jupRequestId! === undefined) { throw new Error('Jupiter reference ID not found'); } const txUserSigned = await params.signPartialTransaction(tx); const signedTransactionSerialized = getBase64EncodedWireTransaction(txUserSigned); const result = await executeJupiterZTransaction({ signedTransaction: signedTransactionSerialized, requestId: params.router.jupRequestId, logger: this.logger, }); if (result.status !== 'Success' || !result.signature) { throw new Error('Failed to submit Jupiter Z transaction'); } const sig = signature(result.signature); await params.confirmTransaction(sig); return sig; } private getClientSideRouteOutputPromises( params: RouteParams, ctx: RouterContext, timeoutMs: number, ): PromiseWithName[] { const validFilterArray = filterArrayRouterTypes( params.routerTypes, params.includeRfq !== undefined ? params.includeRfq : true, ); const routerPromises = this.routers .map((router) => { if (!isRequestedRouterType(validFilterArray, router.routerType)) { return undefined; } // Only apply withTimeout if we're not using withAtLeastOneTimeout const promise = params.atLeastOneNoMoreThanTimeoutMS !== undefined ? router.route(params, ctx) : withTimeout(router.route(params, ctx), timeoutMs, () => this.logger.debug(`Router ${router.routerType || 'unknown'} timed out after ${timeoutMs}ms`), ); return { promise, name: router.routerType, }; }) .filter((routerPromise) => routerPromise !== undefined) as PromiseWithName[]; return routerPromises; } private getClientSideQuoteRouteOutputPromises( params: QuoteParams, timeoutMs: number, ): PromiseWithName[] { const validFilterArray = filterArrayRouterTypes( params.routerTypes, params.includeRfq !== undefined ? params.includeRfq : true, ); const routerPromises = this.routers .map((router) => { if (!isRequestedRouterType(validFilterArray, router.routerType)) { return undefined; } // Only apply withTimeout if we're not using withAtLeastOneTimeout const promise = params.atLeastOneNoMoreThanTimeoutMS !== undefined ? router.quote(params) : withTimeout(router.quote(params), timeoutMs, () => this.logger.debug(`Router ${router.routerType || 'unknown'} timed out after ${timeoutMs}ms`), ); return { promise, name: router.routerType, }; }) .filter((routerPromise) => routerPromise !== undefined) as PromiseWithName[]; return routerPromises; } private async processClientSideRouteOutput( apiRouteOutputs: RouteOutput[], routeOutputs: (RouteOutput[] | undefined | null)[], paramsWithFilter: RouteParams, blockhash: BlockhashWithExpiry, ): Promise { try { const cleanRouteOutputs = routeOutputs .filter( (routeOutput) => KswapSdk.isValidRouteOutput(routeOutput) && !KswapSdk.shouldExcludeJupiterZRoute(routeOutput, paramsWithFilter), ) .flat() as RouteOutput[]; const clientSideRouteOutputs = await this.getClientSideRouteOutputSimulationAndPriceImpact( apiRouteOutputs, cleanRouteOutputs, paramsWithFilter, blockhash, ); let finalRouteOutputs: RouteOutput[] = []; if (clientSideRouteOutputs.length > 0) { apiRouteOutputs.push(...clientSideRouteOutputs); if (includeLimoLogs(paramsWithFilter.includeLimoLogs)) { finalRouteOutputs = await this.addOrReplaceLimoLogsInstructionWithNextBest(apiRouteOutputs, paramsWithFilter); } else { finalRouteOutputs = apiRouteOutputs; } } else { // If no client side route outputs, we can return the original route outputs finalRouteOutputs = apiRouteOutputs; } return finalRouteOutputs; } catch { return apiRouteOutputs; // Return the original outputs in case of error } } private processClientSideQuoteRouteOutput( apiRouteOutputs: RouteOutput[], routeOutputs: (RouteOutput[] | undefined | null)[], paramsWithFilter: QuoteParams, ): RouteOutput[] { const cleanRouteOutputs = routeOutputs .filter( (routeOutput) => KswapSdk.isValidRouteOutput(routeOutput) && !KswapSdk.shouldExcludeJupiterZRoute(routeOutput, paramsWithFilter), ) .flat() as RouteOutput[]; const routeOutputAsyncData = getAsyncDataFromApiRouteOutput(apiRouteOutputs); const clientRouteOutputs = cleanRouteOutputs.map((routeOutput) => { routeOutput = addAsyncDataToRouteOutput(routeOutput, routeOutputAsyncData); // Add price impact to route return addPricesAndPriceImpactToRouteOutput(paramsWithFilter.swapType, routeOutput); }); apiRouteOutputs.push(...clientRouteOutputs); return apiRouteOutputs; } private filterRouterTypes(routerTypes: RouterType[] | undefined): RouterType[] { return routerTypes && routerTypes.length > 0 ? routerTypes : this.allRouterTypes; } private addHttpLogInterceptors( client: Axios, logger: Logger, requestLogLevel: LogLevel, responseLogConfig: ResponseLogConfig, ): void { addHttpRequestLoggingInterceptor(client, logger, requestLogLevel, 'Kswap'); addHttpResponseLoggingInterceptor(client, logger, responseLogConfig, 'Kswap'); } }