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, address, Instruction, Transaction } from '@solana/kit'; import { AddressLookupTable } from '@solana-program/address-lookup-table'; import { fromLegacyInstruction, fromLegacyLookupTable, fromLegacyVersionedTransaction, toLegacyInstructions, toLegacyLookupTables, toLegacyVersionedTransaction, } from '../utils'; import { AddressLookupTableAccount, AddressLookupTableState, PublicKey, TransactionInstruction, VersionedTransaction, } from '@solana/web3.js'; // Serialize VersionedTransaction to base64 string function serializeVersionedTransaction(tx: Transaction, lookupTableAccounts: Account[]): string { const versionedTx = toLegacyVersionedTransaction(tx, lookupTableAccounts); const serializedTx = versionedTx.serialize(); return Buffer.from(serializedTx).toString('base64'); } // Deserialize VersionedTransaction from base64 string function deserializeVersionedTransaction( base64Tx: string | undefined, lookupTableAccounts: Account[], ): Transaction | undefined { if (!base64Tx) { return undefined; } const txBuffer = Buffer.from(base64Tx, 'base64'); return fromLegacyVersionedTransaction(VersionedTransaction.deserialize(txBuffer), lookupTableAccounts); } // 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 ? [] : toLegacyInstructions(...instructions).map((ix) => ({ programId: ix.programId.toBase58(), data: ix.data.toString('base64'), keys: ix.keys.map((key) => ({ pubkey: key.pubkey.toBase58(), isSigner: key.isSigner, isWritable: key.isWritable, })), })); } // Deserialize plain object array back to TransactionInstruction[] function deserializeTransactionInstructions(instructions: InstructionSerialized[] | undefined): Instruction[] { return !instructions ? [] : instructions.map((ix) => { const { programId, data, keys } = ix; const decodedProgramId = new PublicKey(programId); const decodedData = Buffer.from(data, 'base64'); const decodedKeys = keys.map((key: any) => ({ pubkey: new PublicKey(key.pubkey), isSigner: key.isSigner, isWritable: key.isWritable, })); return fromLegacyInstruction( new TransactionInstruction({ programId: decodedProgramId, data: decodedData, keys: decodedKeys, }), ); }); } // Function to serialize AddressLookupTableState and AddressLookupTableAccountArgs function serializeAddressLookupTableResponse( addressLookupTableAccounts: Account[], ): AddressLookupTableAccountSerialized[] { return toLegacyLookupTables(...addressLookupTableAccounts).map((lutAccount) => { return { key: lutAccount.key.toBase58(), // Convert PublicKey to base58 string state: { deactivationSlot: lutAccount.state.deactivationSlot.toString(), // Convert bigint to string lastExtendedSlot: lutAccount.state.lastExtendedSlot, lastExtendedSlotStartIndex: lutAccount.state.lastExtendedSlotStartIndex, authority: lutAccount.state.authority ? lutAccount.state.authority.toBase58() : undefined, // Convert PublicKey to base58 string addresses: lutAccount.state.addresses.map((address) => address.toBase58()), // Convert each PublicKey in the array to base58 string }, }; }); } // Deserialize the AddressLookupTableState and AddressLookupTableAccountArgs response function deserializeAddressLookupTableResponse( addressLookupTableAccountsSerialized: AddressLookupTableAccountSerialized[], ): Account[] { return addressLookupTableAccountsSerialized.map((lutAccount) => { // Deserialize the state const deserializedState: AddressLookupTableState = { deactivationSlot: BigInt(lutAccount.state.deactivationSlot), // Convert deactivationSlot to bigint lastExtendedSlot: lutAccount.state.lastExtendedSlot, lastExtendedSlotStartIndex: lutAccount.state.lastExtendedSlotStartIndex, authority: lutAccount.state.authority ? new PublicKey(lutAccount.state.authority) : undefined, // Convert PublicKey if present addresses: lutAccount.state.addresses.map((address: string) => new PublicKey(address)), // Convert each address to PublicKey }; // Deserialize the key as PublicKey const deserializedKey = new PublicKey(lutAccount.key); // Return the deserialized AddressLookupTableAccountArgs return fromLegacyLookupTable( new AddressLookupTableAccount({ key: deserializedKey, state: deserializedState, }), ); }); } export function serializeRouteOutput(routeOutput: RouteOutput): RouteOutputSerialized { const baseOutput: RouteOutputSerialized = { instructions: serializeRouteInstructions(routeOutput.instructions), ixsRouterBs58: serializeTransactionInstructions(routeOutput.ixsRouter), 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, 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, routeOutput.lookupTableAccounts), 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, }; } function deserializeRouteSimulationResult(simulationResult: RouteSimulationResultSerialized): RouteSimulationResult { return { simulationTimestamp: simulationResult.simulationTimestamp, errorReason: simulationResult.errorReason ? deserializeSimulationErrorReason(simulationResult.errorReason) : undefined, logs: simulationResult.logs, success: simulationResult.success, }; } function deserializeSimulationErrorReason(errorReason: string): SimulationErrorReason { const reason = errorReason as SimulationErrorReason; switch (reason) { case SimulationErrorReason.SlippageExceeded: return SimulationErrorReason.SlippageExceeded; case SimulationErrorReason.Unknown: return SimulationErrorReason.Unknown; case SimulationErrorReason.TooManyAccountLocks: return SimulationErrorReason.TooManyAccountLocks; case SimulationErrorReason.TransactionTooLarge: return SimulationErrorReason.TransactionTooLarge; default: // Compile-time check: if a new enum member is added and not handled, this will error // eslint-disable-next-line no-case-declarations,@typescript-eslint/no-unused-vars const _: never = reason; throw new Error(`Unknown SimulationErrorReason ${errorReason}`); } } export async function deserializeRouteOutput(routeOutput: RouteOutputSerialized): Promise { if (!isValidRouterType(routeOutput.routerType)) { return undefined; } const lookupTableAccounts = deserializeAddressLookupTableResponse(routeOutput.lookupTableAccountsBs58 || []); return { instructions: deserializeRouteInstructions(routeOutput.instructions), ixsRouter: deserializeTransactionInstructions(routeOutput.ixsRouterBs58), transaction: deserializeVersionedTransaction(routeOutput.transactionBs58, lookupTableAccounts), 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 ? address(routeOutput.inputMintProgramOwner) : undefined, outputMintProgramOwner: routeOutput.outputMintProgramOwner ? address(routeOutput.outputMintProgramOwner) : undefined, inputTokenDecimals: routeOutput.inputTokenDecimals, outputTokenDecimals: routeOutput.outputTokenDecimals, jupRequestId: routeOutput.jupRequestId, 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', filterFailedSimulations: routeParams.filterFailedSimulations !== undefined && routeParams.filterFailedSimulations ? '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 simulateWithTopHolder: routeParams.simulateWithTopHolder ? '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; }