import { SDK_VERSION } from './version'; import { BestRouteResponse, BirdeyePriceData, BlockhashWithExpiry, compileRouteIxs, deserializeRouteOutput, ExecuteKrfqRouteParams, ExecutePerRouteParams, ExecuteRouteParams, GetJupiterPriceParams, GetJupiterPriceResponse, getLimoLogsIxsForRoute, getQuoteParamsKeys, QuoteParams, RouteInstructions, RouteOrQuoteParams, RouteOutput, RouteOutputSerialized, RouteParams, Router, RouterContext, RoutesResponse, serializeQuoteParams, serializeRouteParams, SimulationErrorReason, SwapType, TokenAccountMetadata, } from './swap_api_utils'; import { Axios } from 'axios'; import { EventStreamDecoder } from './utils/EventStreamDecoder'; import { buildCompiledTransaction, computeNetSolChange, createAddExtraComputeUnitsTransaction, getAllMintTokenPrograms, pick, PromiseWithName, removeComputeBudgetIxs, simulateSwapsWithATABalances, simulateTransactionsWithBalanceTracking, 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'; import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system'; 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() }; } async refreshRoutes( params: Pick, routes: RouteOutput[], ): Promise { if (routes.length === 0) { return []; } const simulationWallet: Address = params.executor !== SYSTEM_PROGRAM_ADDRESS ? params.executor : (routes.map((route) => route.simulationResult?.executor as Address | undefined).find(Boolean) ?? SYSTEM_PROGRAM_ADDRESS); const outputProgram = routes.map((route) => route.outputMintProgramOwner).find(Boolean); const inputProgram = routes.map((route) => route.inputMintProgramOwner).find(Boolean); // The "tracked" side is the one whose amount we report (output for exactIn, // input for exactOut). Its program owner is required. const trackedProgram = params.swapType === 'exactIn' ? outputProgram : inputProgram; if (!trackedProgram) { throw new Error( params.swapType === 'exactIn' ? 'refreshRoutes: no outputMintProgramOwner found on any route' : 'refreshRoutes: no inputMintProgramOwner found on any route', ); } const trackedMint = params.swapType === 'exactIn' ? params.tokenOut : params.tokenIn; const otherMint = params.swapType === 'exactIn' ? params.tokenIn : params.tokenOut; const otherProgram = params.swapType === 'exactIn' ? inputProgram : outputProgram; const [trackedAta] = await findAssociatedTokenPda({ mint: trackedMint, owner: simulationWallet, tokenProgram: trackedProgram, }); const tokenAccounts: TokenAccountMetadata[] = [ { address: trackedAta, mint: trackedMint, programAddress: trackedProgram }, ]; // Also track the other side's ATA so its rent (e.g. an input ATA the // simulation created and the wallet paid for) cancels out of `netSolChange` // instead of distorting a WSOL amount. Skip if its program owner is unknown. if (otherProgram && otherMint !== trackedMint) { const [otherAta] = await findAssociatedTokenPda({ mint: otherMint, owner: simulationWallet, tokenProgram: otherProgram, }); tokenAccounts.push({ address: otherAta, mint: otherMint, programAddress: otherProgram }); } // Only routes that carry a simulation transaction get simulated, so `transactions` // is shorter than `routes` whenever some routes lack one. Track which result slot // belongs to each route so we never read a neighbour's result by positional index. const routeIndexToResultIndex = new Map(); const transactions: Transaction[] = []; routes.forEach((route, idx) => { const transaction = route.simulationResult?.transaction; if (transaction) { routeIndexToResultIndex.set(idx, transactions.length); transactions.push(transaction); } }); const simulationResults = await simulateTransactionsWithBalanceTracking( this.connection, transactions, tokenAccounts, [simulationWallet], this.logger, ); return routes.map((route, idx) => { const resultIndex = routeIndexToResultIndex.get(idx); // Preserve route as-is if it was not scheduled for simulation if (resultIndex === undefined) { return route; } const transaction = transactions[resultIndex]; const simulationResult = simulationResults[resultIndex]; if (simulationResult.successful) { let simulatedAmount: BN; if (trackedMint === WRAPPED_SOL_MINT) { simulatedAmount = computeNetSolChange(simulationResult, simulationWallet); } else { const trackedChange = simulationResult.tokenBalanceChanges.find((change) => change.address === trackedAta); if (!trackedChange) { throw new Error( `refreshRoutes: tracked ATA ${trackedAta} (mint ${trackedMint}) missing from successful simulation token balance changes`, ); } simulatedAmount = trackedChange.change; } const newRoute: RouteOutput = params.swapType === 'exactIn' ? { ...route, amountsExactIn: { ...route.amountsExactIn, amountOutSimulated: simulatedAmount, }, simulationResult: { success: true, simulationTimestamp: simulationResult.simulationTimestamp, transaction, executor: simulationWallet, }, } : { ...route, amountsExactOut: { ...route.amountsExactOut, amountInSimulated: simulatedAmount, }, simulationResult: { success: true, simulationTimestamp: simulationResult.simulationTimestamp, transaction, executor: simulationWallet, }, }; addPricesAndPriceImpactToRouteOutput(params.swapType, newRoute); return newRoute; } else { return params.swapType === 'exactIn' ? { ...route, amountsExactIn: { ...route.amountsExactIn, amountOutSimulated: undefined }, simulatedPriceImpactBps: undefined, simulatedPriceDifferenceFromPriceSourceBps: undefined, simulationResult: { success: false, errorReason: SimulationErrorReason.Unknown, logs: simulationResult.simulationResponse?.value.logs ?? undefined, simulationTimestamp: simulationResult.simulationTimestamp, transaction, executor: simulationWallet, }, } : { ...route, amountsExactOut: { ...route.amountsExactOut, amountInSimulated: undefined }, simulatedPriceImpactBps: undefined, simulatedPriceDifferenceFromPriceSourceBps: undefined, simulationResult: { success: false, errorReason: SimulationErrorReason.Unknown, logs: simulationResult.simulationResponse?.value.logs ?? undefined, simulationTimestamp: simulationResult.simulationTimestamp, transaction, executor: simulationWallet, }, }; } }); } prepareRoutesList(routes: RouteOutput[], swapType: SwapType): RouteOutput[] { const sorted = [...routes].sort((a, b) => { const aAmount = swapType === 'exactIn' ? a.amountsExactIn.amountOutSimulated : a.amountsExactOut.amountInSimulated; const bAmount = swapType === 'exactIn' ? b.amountsExactIn.amountOutSimulated : b.amountsExactOut.amountInSimulated; if (aAmount === undefined && bAmount === undefined) return 0; if (aAmount === undefined) return 1; if (bAmount === undefined) return -1; // exactIn: higher amountOut is better; exactOut: lower amountIn is better. return swapType === 'exactIn' ? bAmount.cmp(aAmount) : aAmount.cmp(bAmount); }); const seen = new Set(); return sorted.filter((route) => { if (seen.has(route.routerType)) return false; seen.add(route.routerType); return true; }); } 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.headers?.['x-trace-id'] || 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.has(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.headers?.['x-trace-id'] || 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.headers?.['x-trace-id'] || response.data?.traceId || '', }; } catch (error) { if (error instanceof HttpClientError) { if (error.status === 404 && error.data?.error === 'No route found') { // expecting 404 when no route is found return { bestRoute: undefined, traceId: error.headers?.['x-trace-id'] || 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; } } private async getExecuteKrfqRoute(params: ExecuteKrfqRouteParams): Promise { const endpointUrl = '/execute-krfq-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 KRFQ route', response); return undefined; } this.logger.debug('Response from /execute-krfq-route:', response.data); const sig: string = response.data.data.signature; return signature(sig); } catch (error) { this.logger.error('Error executing KRFQ route', 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.netSolChange : simulation.outputToken.change; const amountInSimulatedChange = params.tokenIn === WRAPPED_SOL_MINT ? simulation.netSolChange : 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 === 'krfq') { return this.executeKrfqRoute(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 async executeKrfqRoute(params: ExecuteRouteParams): Promise { const tx = params.router.transaction; if (!tx) { throw new Error('Transaction not found in router output'); } if (params.router.krfqBidId === undefined) { throw new Error('KRFQ bid ID not found'); } const txUserSigned = await params.signPartialTransaction(tx); const signedTransactionHex = Buffer.from(getBase64EncodedWireTransaction(txUserSigned), 'base64').toString('hex'); const sig = await this.getExecuteKrfqRoute({ bidId: params.router.krfqBidId, signedTransactionHex, userWallet: params.userToExecute, quoteExpiryMs: params.router.expiryTime, }); if (!sig) { throw new Error('Failed to submit KRFQ transaction'); } 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'); } }