import { AddressLookupTableAccountSerialized, InstructionSerialized, QuoteParams, QuoteParamsSerialized, RouteInstructions, RouteInstructionsSerialized, RouteOutput, RouteOutputSerialized, RouteParams, RouteParamsSerialized, RouteSimulationResult, RouteSimulationResultSerialized, SimulationErrorReason, SwapType, } from './types'; import { BN } from '@coral-xyz/anchor'; import { limoId } from '@kamino-finance/limo-sdk'; import { isValidRouterType } from '../consts'; import { Account, AccountRole, Address, getBase64EncodedWireTransaction, Instruction, isSome, lamports, none, some, Transaction, } from '@solana/kit'; import { ADDRESS_LOOKUP_TABLE_PROGRAM_ADDRESS, AddressLookupTable } from '@solana-program/address-lookup-table'; import { base64ToTransaction } from '../utils/decodeTransaction'; import { determineRole } from '../utils'; import { compileRouteIxs } from './compileRouteIxs'; // Serialize a kit Transaction straight to its base64 wire bytes. Fully kit-native // and no lookup tables: kit's transaction encoder writes the signatures compact-array // (zero-filling unset signers) followed by `messageBytes`, so the LUTs were never // needed here. The old web3.js round-trip only resolved account keys to order the // signatures, but a kit Transaction's `signatures` map is already in signer order by // construction, so `getBase64EncodedWireTransaction` produces identical wire bytes. export function serializeVersionedTransaction(tx: Transaction): string { return getBase64EncodedWireTransaction(tx); } // Deserialize a kit Transaction straight from its base64 wire bytes. Fully // kit-native — no web3.js and no lookup tables: a Transaction is just // `{ messageBytes, signatures }`, so the LUTs were never actually needed here // (the old web3.js round-trip only carried them to stop `getAccountKeys` throwing). function deserializeVersionedTransaction(base64Tx: string | undefined): Transaction | undefined { return base64Tx ? base64ToTransaction(base64Tx) : undefined; } // Serialize plain object back to RouteInstructions function serializeRouteInstructions(de: RouteInstructions | undefined): RouteInstructionsSerialized | undefined { if (de === undefined) { return undefined; } return { createInAtaIxs: serializeTransactionInstructions(de?.createInAtaIxs), createOutAtaIxs: serializeTransactionInstructions(de?.createOutAtaIxs), wrapSolIxs: serializeTransactionInstructions(de?.wrapSolIxs), limoLogsStartIxs: serializeTransactionInstructions(de?.limoLogsStartIxs), limoLedgerStartIxs: serializeTransactionInstructions(de?.limoLedgerStartIxs), swapIxs: serializeTransactionInstructions(de?.swapIxs), limoLedgerEndIxs: serializeTransactionInstructions(de?.limoLedgerEndIxs), limoLogsEndIxs: serializeTransactionInstructions(de?.limoLogsEndIxs), unwrapSolIxs: serializeTransactionInstructions(de?.unwrapSolIxs), }; } // Deserialize plain object back to RouteInstructions function deserializeRouteInstructions(ser: RouteInstructionsSerialized | undefined): RouteInstructions | undefined { if (ser === undefined) { return undefined; } return { createInAtaIxs: deserializeTransactionInstructions(ser?.createInAtaIxs), createOutAtaIxs: deserializeTransactionInstructions(ser?.createOutAtaIxs), wrapSolIxs: deserializeTransactionInstructions(ser?.wrapSolIxs), limoLogsStartIxs: deserializeTransactionInstructions(ser?.limoLogsStartIxs), limoLedgerStartIxs: deserializeTransactionInstructions(ser?.limoLedgerStartIxs), swapIxs: deserializeTransactionInstructions(ser?.swapIxs), limoLedgerEndIxs: deserializeTransactionInstructions(ser?.limoLedgerEndIxs), limoLogsEndIxs: deserializeTransactionInstructions(ser?.limoLogsEndIxs), unwrapSolIxs: deserializeTransactionInstructions(ser?.unwrapSolIxs), }; } // Serialize TransactionInstruction[] to plain object array function serializeTransactionInstructions(instructions: Instruction[] | undefined): InstructionSerialized[] { return !instructions ? [] : instructions.map((ix) => { return { programId: ix.programAddress, data: ix.data ? Buffer.from(ix.data).toString('base64') : '', keys: (ix.accounts || []).map((key) => ({ pubkey: key.address, isSigner: key.role === AccountRole.WRITABLE_SIGNER || key.role === AccountRole.READONLY_SIGNER, isWritable: key.role === AccountRole.WRITABLE_SIGNER || key.role === AccountRole.WRITABLE, })), }; }); } // Deserialize plain object array back to kit Instruction[]. // The cache stores addresses as base58 strings, which ARE kit Addresses, so we // brand them directly instead of round-tripping through web3.js PublicKey. The // old path did ~3 O(n²) base58 conversions per address (decode + encode + revalidate), // which dominated the /all-routes cached-route hot path and pegged the event loop. function deserializeTransactionInstructions(instructions: InstructionSerialized[] | undefined): Instruction[] { return !instructions ? [] : instructions.map((ix) => ({ programAddress: ix.programId as Address, accounts: ix.keys.map((key) => ({ address: key.pubkey as Address, role: determineRole(key.isSigner, key.isWritable), })), data: Buffer.from(ix.data, 'base64'), })); } // Function to serialize AddressLookupTableState and AddressLookupTableAccountArgs function serializeAddressLookupTableResponse( addressLookupTableAccounts: Account[], ): AddressLookupTableAccountSerialized[] { return addressLookupTableAccounts.map((lutAccount) => { return { key: lutAccount.address, state: { deactivationSlot: lutAccount.data.deactivationSlot.toString(), lastExtendedSlot: Number(lutAccount.data.lastExtendedSlot), lastExtendedSlotStartIndex: lutAccount.data.lastExtendedSlotStartIndex, authority: isSome(lutAccount.data.authority) ? lutAccount.data.authority.value : undefined, addresses: lutAccount.data.addresses, }, }; }); } // Deserialize the AddressLookupTableState response straight into kit-native // Account. Same rationale as deserializeTransactionInstructions: // the serialized addresses are base58 strings == kit Addresses, so brand them // directly. A LUT holds up to 256 addresses, so the old web3.js round-trip was the // single biggest base58 cost on the cached-route path. function deserializeAddressLookupTableResponse( addressLookupTableAccountsSerialized: AddressLookupTableAccountSerialized[], ): Account[] { return addressLookupTableAccountsSerialized.map((lutAccount) => ({ address: lutAccount.key as Address, programAddress: ADDRESS_LOOKUP_TABLE_PROGRAM_ADDRESS, executable: false, lamports: lamports(0n), space: 0n, data: { padding: 0, lastExtendedSlotStartIndex: lutAccount.state.lastExtendedSlotStartIndex, lastExtendedSlot: BigInt(lutAccount.state.lastExtendedSlot), addresses: lutAccount.state.addresses.map((addr) => addr as Address), authority: lutAccount.state.authority ? some(lutAccount.state.authority as Address) : none(), discriminator: 0, deactivationSlot: BigInt(lutAccount.state.deactivationSlot), }, })); } export function serializeRouteOutput(routeOutput: RouteOutput): RouteOutputSerialized { const baseOutput: RouteOutputSerialized = { instructions: serializeRouteInstructions(routeOutput.instructions), ixsRouterBs58: routeOutput.instructions ? serializeTransactionInstructions(compileRouteIxs(routeOutput.instructions)) : [], amountsExactIn: { amountIn: routeOutput.amountsExactIn.amountIn.toString(), amountOutGuaranteed: routeOutput.amountsExactIn.amountOutGuaranteed.toString(), amountOut: routeOutput.amountsExactIn.amountOut.toString(), amountOutSimulated: routeOutput.amountsExactIn.amountOutSimulated ? routeOutput.amountsExactIn.amountOutSimulated.toString() : undefined, }, amountsExactOut: { amountOut: routeOutput.amountsExactOut.amountOut.toString(), amountInGuaranteed: routeOutput.amountsExactOut.amountInGuaranteed.toString(), amountIn: routeOutput.amountsExactOut.amountIn.toString(), amountInSimulated: routeOutput.amountsExactOut.amountInSimulated ? routeOutput.amountsExactOut.amountInSimulated.toString() : undefined, }, swapType: routeOutput.swapType, responseTimeGetQuoteMs: routeOutput.responseTimeGetQuoteMs, responseTimeSwapIxsMs: routeOutput.responseTimeSwapIxsMs, routerType: routeOutput.routerType, priceImpactBps: routeOutput.priceImpactBps, simulatedPriceImpactBps: routeOutput.simulatedPriceImpactBps, guaranteedPriceImpactBps: routeOutput.guaranteedPriceImpactBps, priceDifferenceFromPriceSourceBps: routeOutput.priceDifferenceFromPriceSourceBps, simulatedPriceDifferenceFromPriceSourceBps: routeOutput.simulatedPriceDifferenceFromPriceSourceBps, guaranteedPriceDifferenceFromPriceSourceBps: routeOutput.guaranteedPriceDifferenceFromPriceSourceBps, expiryTime: routeOutput.expiryTime, perReferenceId: routeOutput.perReferenceId, birdeyeTokenInPriceInSol: routeOutput.birdeyeTokenInPriceInSol, birdeyeTokenOutPriceInSol: routeOutput.birdeyeTokenOutPriceInSol, spotPriceTokenInAmount: routeOutput.spotPriceTokenInAmount?.toString(), spotPriceTokenOutAmount: routeOutput.spotPriceTokenOutAmount?.toString(), inputMintProgramOwner: routeOutput.inputMintProgramOwner ? routeOutput.inputMintProgramOwner : undefined, outputMintProgramOwner: routeOutput.outputMintProgramOwner ? routeOutput.outputMintProgramOwner : undefined, inputTokenDecimals: routeOutput.inputTokenDecimals, outputTokenDecimals: routeOutput.outputTokenDecimals, jupRequestId: routeOutput.jupRequestId, krfqBidId: routeOutput.krfqBidId, cloverDexType: routeOutput.cloverDexType, skipLimoLogsForRoute: routeOutput.skipLimoLogsForRoute ? 'true' : 'false', simulationResult: routeOutput.simulationResult ? serializeRouteSimulationResult(routeOutput.simulationResult) : undefined, requestedMaxAccounts: routeOutput.requestedMaxAccounts, }; // Handle the discriminated union based on whether transaction exists if (routeOutput.transaction) { return { ...baseOutput, transactionBs58: serializeVersionedTransaction(routeOutput.transaction), lookupTableAccountsBs58: routeOutput.lookupTableAccounts?.length ? serializeAddressLookupTableResponse(routeOutput.lookupTableAccounts) : [], }; } else { return { ...baseOutput, lookupTableAccountsBs58: routeOutput.lookupTableAccounts?.length ? serializeAddressLookupTableResponse(routeOutput.lookupTableAccounts) : undefined, }; } } function serializeRouteSimulationResult(simulationResult: RouteSimulationResult): RouteSimulationResultSerialized { return { simulationTimestamp: simulationResult.simulationTimestamp, errorReason: simulationResult.errorReason, logs: simulationResult.logs, success: simulationResult.success, transactionBs58: simulationResult.transaction ? getBase64EncodedWireTransaction(simulationResult.transaction) : undefined, executor: simulationResult.executor, }; } function deserializeRouteSimulationResult(simulationResult: RouteSimulationResultSerialized): RouteSimulationResult { let transaction: Transaction | undefined; try { transaction = deserializeVersionedTransaction(simulationResult.transactionBs58); } catch { transaction = undefined; } return { simulationTimestamp: simulationResult.simulationTimestamp, errorReason: simulationResult.errorReason as SimulationErrorReason | undefined, logs: simulationResult.logs, success: simulationResult.success, transaction, executor: simulationResult.executor, }; } export async function deserializeRouteOutput(routeOutput: RouteOutputSerialized): Promise { if (!isValidRouterType(routeOutput.routerType)) { return undefined; } const lookupTableAccounts = deserializeAddressLookupTableResponse(routeOutput.lookupTableAccountsBs58 || []); const instructions = deserializeRouteInstructions(routeOutput.instructions); return { instructions, ixsRouter: deserializeTransactionInstructions(routeOutput.ixsRouterBs58) || (instructions ? compileRouteIxs(instructions) : undefined), transaction: deserializeVersionedTransaction(routeOutput.transactionBs58), amountsExactIn: { amountIn: new BN(routeOutput.amountsExactIn.amountIn), amountOutGuaranteed: new BN(routeOutput.amountsExactIn.amountOutGuaranteed), amountOut: new BN(routeOutput.amountsExactIn.amountOut), amountOutSimulated: routeOutput.amountsExactIn.amountOutSimulated ? new BN(routeOutput.amountsExactIn.amountOutSimulated) : undefined, }, amountsExactOut: { amountOut: new BN(routeOutput.amountsExactOut.amountOut), amountInGuaranteed: new BN(routeOutput.amountsExactOut.amountInGuaranteed), amountIn: new BN(routeOutput.amountsExactOut.amountIn), amountInSimulated: routeOutput.amountsExactOut.amountInSimulated ? new BN(routeOutput.amountsExactOut.amountInSimulated) : undefined, }, swapType: routeOutput.swapType as SwapType, responseTimeGetQuoteMs: routeOutput.responseTimeGetQuoteMs, responseTimeSwapIxsMs: routeOutput.responseTimeSwapIxsMs, routerType: routeOutput.routerType, lookupTableAccounts: lookupTableAccounts, expiryTime: routeOutput.expiryTime, perReferenceId: routeOutput.perReferenceId, priceImpactBps: routeOutput.priceImpactBps, simulatedPriceImpactBps: routeOutput.simulatedPriceImpactBps, guaranteedPriceImpactBps: routeOutput.guaranteedPriceImpactBps, priceDifferenceFromPriceSourceBps: routeOutput.priceDifferenceFromPriceSourceBps, simulatedPriceDifferenceFromPriceSourceBps: routeOutput.simulatedPriceDifferenceFromPriceSourceBps, guaranteedPriceDifferenceFromPriceSourceBps: routeOutput.guaranteedPriceDifferenceFromPriceSourceBps, birdeyeTokenInPriceInSol: routeOutput.birdeyeTokenInPriceInSol, birdeyeTokenOutPriceInSol: routeOutput.birdeyeTokenOutPriceInSol, spotPriceTokenInAmount: routeOutput.spotPriceTokenInAmount ? new BN(routeOutput.spotPriceTokenInAmount) : undefined, spotPriceTokenOutAmount: routeOutput.spotPriceTokenOutAmount ? new BN(routeOutput.spotPriceTokenOutAmount) : undefined, inputMintProgramOwner: (routeOutput.inputMintProgramOwner as Address | undefined) || undefined, outputMintProgramOwner: (routeOutput.outputMintProgramOwner as Address | undefined) || undefined, inputTokenDecimals: routeOutput.inputTokenDecimals, outputTokenDecimals: routeOutput.outputTokenDecimals, jupRequestId: routeOutput.jupRequestId, krfqBidId: routeOutput.krfqBidId, cloverDexType: routeOutput.cloverDexType, skipLimoLogsForRoute: routeOutput.skipLimoLogsForRoute === 'true', // Deserialize string to boolean simulationResult: routeOutput.simulationResult ? deserializeRouteSimulationResult(routeOutput.simulationResult) : undefined, requestedMaxAccounts: routeOutput.requestedMaxAccounts, }; } export function serializeRouteParams(routeParams: RouteParams): RouteParamsSerialized { const res: RouteParamsSerialized = { tokenIn: routeParams.tokenIn, tokenOut: routeParams.tokenOut, amount: routeParams.amount.toString(), swapType: routeParams.swapType, executor: routeParams.executor, referrerPda: routeParams.referrerPda ? routeParams.referrerPda : limoId, maxSlippageBps: routeParams.maxSlippageBps, includeSetupIxs: routeParams.includeSetupIxs !== undefined && !routeParams.includeSetupIxs ? 'false' : 'true', wrapAndUnwrapSol: routeParams.wrapAndUnwrapSol !== undefined && !routeParams.wrapAndUnwrapSol ? 'false' : 'true', routerTypes: routeParams.routerTypes, includeLimoLogs: routeParams.includeLimoLogs !== undefined && !routeParams.includeLimoLogs ? 'false' : 'true', includeRfq: routeParams.includeRfq !== undefined && !routeParams.includeRfq ? 'false' : 'true', timeoutMs: routeParams.timeoutMs, atLeastOneNoMoreThanTimeoutMS: routeParams.atLeastOneNoMoreThanTimeoutMS, withSimulation: routeParams.withSimulation !== undefined && !routeParams.withSimulation ? 'false' : 'true', includeFailedSimulations: routeParams.includeFailedSimulations !== undefined && routeParams.includeFailedSimulations ? 'true' : 'false', destinationTokenAccount: routeParams.destinationTokenAccount ? routeParams.destinationTokenAccount : undefined, preferredMaxAccounts: routeParams.preferredMaxAccounts, requestPriceImpact: routeParams.requestPriceImpact ? 'true' : 'false', perMinimumQuoteLifetimeSeconds: routeParams.perMinimumQuoteLifetimeSeconds, assertSwapBalances: routeParams.assertSwapBalances ? 'true' : 'false', overrideAssertMaxInputAmountChange: routeParams.overrideAssertMaxInputAmountChange ? routeParams.overrideAssertMaxInputAmountChange.toString() : undefined, overrideAssertMinOutputAmountChange: routeParams.overrideAssertMinOutputAmountChange ? routeParams.overrideAssertMinOutputAmountChange.toString() : undefined, simulateWithMockInputAmount: routeParams.simulateWithMockInputAmount ? 'true' : 'false', // Serialize boolean as string includeCachedRoutes: routeParams.includeCachedRoutes ? 'true' : 'false', }; Object.keys(routeParams).forEach((key) => { if (!Object.hasOwn(res, key)) { throw new Error('serializeRouteParams: not all keys of routeParams have been serialized missing key: ' + key); } }); return res; } export function serializeQuoteParams(quoteParams: QuoteParams): QuoteParamsSerialized { const res: QuoteParamsSerialized = { tokenIn: quoteParams.tokenIn, tokenOut: quoteParams.tokenOut, amount: quoteParams.amount.toString(), swapType: quoteParams.swapType, maxSlippageBps: quoteParams.maxSlippageBps, routerTypes: quoteParams.routerTypes, includeRfq: quoteParams.includeRfq !== undefined && !quoteParams.includeRfq ? 'false' : 'true', timeoutMs: quoteParams.timeoutMs, atLeastOneNoMoreThanTimeoutMS: quoteParams.atLeastOneNoMoreThanTimeoutMS, preferredMaxAccounts: quoteParams.preferredMaxAccounts, requestPriceImpact: quoteParams.requestPriceImpact, executor: quoteParams.executor, // Always undefined for quotes }; Object.keys(quoteParams).forEach((key) => { if (!Object.hasOwn(res, key)) { throw new Error('serializeQuoteParams: not all keys of quoteParams have been serialized missing key: ' + key); } }); return res; }