import { AsyncData, RouteOutput, SwapType } from '../swap_api_utils'; import Decimal from 'decimal.js'; const MAX_BPS = 10_000; export function addPricesAndPriceImpactToRouteOutput(swapType: SwapType, route: RouteOutput): RouteOutput { if (!route || !route.inputTokenDecimals || !route.outputTokenDecimals) { return route; } calculateBirdeyePriceDifference(swapType, route); calculateSpotPriceImpact(swapType, route); return route; } /** * Calculate Birdeye-based price difference (legacy method for backwards compatibility) */ function calculateBirdeyePriceDifference(swapType: SwapType, route: RouteOutput): void { if (!route.birdeyeTokenInPriceInSol || !route.birdeyeTokenOutPriceInSol) { return; } const inDecimals = new Decimal(10).pow(route.inputTokenDecimals!); const outDecimals = new Decimal(10).pow(route.outputTokenDecimals!); // Extract normalized amounts based on swap type // // For exactIn: fixed=tokenIn, expected=tokenOut // For exactOut: fixed=tokenOut, expected=tokenIn let sign: number; let baseFixedTokenPrice: number; let baseExpectedTokenPrice: number; let fixedAmountNormalized: Decimal; let actualAmountNormalized: Decimal; let guaranteedAmountNormalized: Decimal; let simulatedAmountNormalized: Decimal | undefined; if (swapType === 'exactIn') { sign = 1; baseFixedTokenPrice = route.birdeyeTokenInPriceInSol; baseExpectedTokenPrice = route.birdeyeTokenOutPriceInSol; fixedAmountNormalized = new Decimal(route.amountsExactIn.amountIn.toString()).div(inDecimals); actualAmountNormalized = new Decimal(route.amountsExactIn.amountOut.toString()).div(outDecimals); guaranteedAmountNormalized = new Decimal(route.amountsExactIn.amountOutGuaranteed.toString()).div(outDecimals); if (route.amountsExactIn.amountOutSimulated) { simulatedAmountNormalized = new Decimal(route.amountsExactIn.amountOutSimulated.toString()).div(outDecimals); } } else { // exactOut sign = -1; baseFixedTokenPrice = route.birdeyeTokenOutPriceInSol; baseExpectedTokenPrice = route.birdeyeTokenInPriceInSol; fixedAmountNormalized = new Decimal(route.amountsExactOut.amountOut.toString()).div(outDecimals); actualAmountNormalized = new Decimal(route.amountsExactOut.amountIn.toString()).div(inDecimals); guaranteedAmountNormalized = new Decimal(route.amountsExactOut.amountInGuaranteed.toString()).div(inDecimals); if (route.amountsExactOut.amountInSimulated) { simulatedAmountNormalized = new Decimal(route.amountsExactOut.amountInSimulated.toString()).div(inDecimals); } } route.priceDifferenceFromPriceSourceBps = calculatePriceImpactBps( baseFixedTokenPrice, baseExpectedTokenPrice, fixedAmountNormalized, actualAmountNormalized, sign, ); route.guaranteedPriceDifferenceFromPriceSourceBps = calculatePriceImpactBps( baseFixedTokenPrice, baseExpectedTokenPrice, fixedAmountNormalized, guaranteedAmountNormalized, sign, ); route.simulatedPriceDifferenceFromPriceSourceBps = simulatedAmountNormalized ? calculatePriceImpactBps( baseFixedTokenPrice, baseExpectedTokenPrice, fixedAmountNormalized, simulatedAmountNormalized, sign, ) : undefined; } /** * Calculate spot-price-based price impact (new method) * Works for both exactIn and exactOut swaps using the same spot price */ function calculateSpotPriceImpact(swapType: SwapType, route: RouteOutput): void { if (!route.spotPriceTokenInAmount || !route.spotPriceTokenOutAmount) { return; } const spotAmountIn = new Decimal(route.spotPriceTokenInAmount.toString()); const spotAmountOut = new Decimal(route.spotPriceTokenOutAmount.toString()); // Extract amounts based on swap type let sign: number; let baseFixedTokenPrice: Decimal; let baseExpectedTokenPrice: Decimal; let fixedAmount: Decimal; let actualAmount: Decimal; let guaranteedAmount: Decimal; let simulatedAmount: Decimal | undefined; if (swapType === 'exactIn') { sign = 1; baseFixedTokenPrice = spotAmountOut; baseExpectedTokenPrice = spotAmountIn; fixedAmount = new Decimal(route.amountsExactIn.amountIn.toString()); actualAmount = new Decimal(route.amountsExactIn.amountOut.toString()); guaranteedAmount = new Decimal(route.amountsExactIn.amountOutGuaranteed.toString()); simulatedAmount = route.amountsExactIn.amountOutSimulated ? new Decimal(route.amountsExactIn.amountOutSimulated.toString()) : undefined; } else { // exactOut sign = -1; baseFixedTokenPrice = spotAmountIn; baseExpectedTokenPrice = spotAmountOut; fixedAmount = new Decimal(route.amountsExactOut.amountOut.toString()); actualAmount = new Decimal(route.amountsExactOut.amountIn.toString()); guaranteedAmount = new Decimal(route.amountsExactOut.amountInGuaranteed.toString()); simulatedAmount = route.amountsExactOut.amountInSimulated ? new Decimal(route.amountsExactOut.amountInSimulated.toString()) : undefined; } // Calculate price impacts route.priceImpactBps = calculatePriceImpactBps( baseFixedTokenPrice, baseExpectedTokenPrice, fixedAmount, actualAmount, sign, ); route.guaranteedPriceImpactBps = calculatePriceImpactBps( baseFixedTokenPrice, baseExpectedTokenPrice, fixedAmount, guaranteedAmount, sign, ); route.simulatedPriceImpactBps = simulatedAmount ? calculatePriceImpactBps(baseFixedTokenPrice, baseExpectedTokenPrice, fixedAmount, simulatedAmount, sign) : undefined; } export function calculatePriceImpactBps( baseFixedTokenPrice: number | Decimal, baseExpectedTokenPrice: number | Decimal, amountFixedToken: Decimal, amountExpectedToken: Decimal, sign: number = 1, ): number | undefined { // Ensure price impact may be calculated baseExpectedTokenPrice = new Decimal(baseExpectedTokenPrice); if (baseExpectedTokenPrice.eq(0)) { return undefined; } // Calculate expected amount based on base prices const expectedAmount = amountFixedToken.mul(baseFixedTokenPrice).div(baseExpectedTokenPrice); if (expectedAmount.eq(0)) { return undefined; } // Price impact = (expected - actual) / expected * MAX_BPS // Positive means unfavorable (getting less than expected) return sign * expectedAmount.sub(amountExpectedToken).div(expectedAmount).mul(MAX_BPS).toNumber(); } export function getAsyncDataFromApiRouteOutput(routes: RouteOutput[]): AsyncData { const asyncData: AsyncData = { inputMintProgramOwner: undefined, outputMintProgramOwner: undefined, inputTokenDecimals: undefined, outputTokenDecimals: undefined, birdeyeTokenInPriceInSol: undefined, birdeyeTokenOutPriceInSol: undefined, }; routes.forEach((route) => { if (route.inputMintProgramOwner) { asyncData.inputMintProgramOwner = route.inputMintProgramOwner; } if (route.outputMintProgramOwner) { asyncData.outputMintProgramOwner = route.outputMintProgramOwner; } if (route.inputTokenDecimals) { asyncData.inputTokenDecimals = route.inputTokenDecimals; } if (route.outputTokenDecimals) { asyncData.outputTokenDecimals = route.outputTokenDecimals; } if (route.birdeyeTokenInPriceInSol) { asyncData.birdeyeTokenInPriceInSol = route.birdeyeTokenInPriceInSol; } if (route.birdeyeTokenOutPriceInSol) { asyncData.birdeyeTokenOutPriceInSol = route.birdeyeTokenOutPriceInSol; } }); return asyncData; } export function addAsyncDataToRouteOutput(route: RouteOutput, asyncData: AsyncData): RouteOutput { route.inputMintProgramOwner = asyncData.inputMintProgramOwner; route.outputMintProgramOwner = asyncData.outputMintProgramOwner; route.inputTokenDecimals = asyncData.inputTokenDecimals; route.outputTokenDecimals = asyncData.outputTokenDecimals; route.birdeyeTokenInPriceInSol = asyncData.birdeyeTokenInPriceInSol; route.birdeyeTokenOutPriceInSol = asyncData.birdeyeTokenOutPriceInSol; return route; }