import JSBI from 'jsbi'; import { Fraction } from '@uniswap/sdk-core'; import { SqrtPriceMath, TickMath } from '@uniswap/v3-sdk'; import { QuoteEngine } from '../../engines/quote-engine'; import { GetSwapAmountOptions, Position, SearchMode, SwapResult, } from '../../types/api'; import { absRatioDiffX128, compareRatioX128, ratioX128FromAmounts, ratioX128FromFraction, withinToleranceByX128, X128, MAX_UINT256, } from '../../utils/ratio'; import { Logger, NoopLogger } from '../../utils/logger'; const PRECISION = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18)); const toSafeNumber = (value: unknown, label: string): number => { const next = typeof value === 'number' ? value : typeof value === 'bigint' ? Number(value) : Number((value as { toString?: () => string } | null | undefined)?.toString?.() ?? value) if (!Number.isFinite(next)) { throw new Error(`Invalid numeric value for ${label}: ${String(value)}`) } return next } const normalizeDesiredPositions = ( positions: ReadonlyArray<{ lowerTick: unknown; upperTick: unknown; weight: unknown }>, ): Array => positions.map((position, index) => ({ lowerTick: toSafeNumber(position.lowerTick, `desiredPositions[${index}].lowerTick`), upperTick: toSafeNumber(position.upperTick, `desiredPositions[${index}].upperTick`), weight: toSafeNumber(position.weight, `desiredPositions[${index}].weight`), })) export type ClampMode = 'price' | 'tick'; export class BaseRouter { constructor( private engine: QuoteEngine, private logger: Logger = NoopLogger, ) {} public async getSwapAmount(opts: GetSwapAmountOptions): Promise { const { pool, desiredPositions, inputT0Bal: inputT0BalRaw, inputT1Bal: inputT1BalRaw, t0Address, t1Address, fee, maxIterations, ratioErrorTolerance, maxPriceImpactBps, searchMode, clampMode, engineExtras, } = opts; // Normalize to native BigInt — callers may pass ethers.js BigNumber or other // non-primitive types that cause "Cannot mix BigInt and other types" at runtime. const inputT0Bal = BigInt(inputT0BalRaw.toString()); const inputT1Bal = BigInt(inputT1BalRaw.toString()); const normalizedPositions = normalizeDesiredPositions(desiredPositions); const tol = ratioErrorTolerance ?? new Fraction(1, 1000); // Get current pool state - prefer from engine if available, fallback to direct pool call let sqrtPriceX96: bigint; if (this.engine.getCurrentPoolState) { try { const poolState = await this.engine.getCurrentPoolState(); sqrtPriceX96 = poolState.sqrtPriceX96; } catch (error) { this.logger.info( { logType: 'custom-router' }, `Failed to get pool state from engine, falling back to direct pool call: ${error}`, ); // Fallback to direct pool call const slot0 = (await pool.slot0?.()) ?? (await pool.globalState?.()); sqrtPriceX96 = BigInt(slot0.sqrtPriceX96 ?? slot0.price); } } else { // Engine doesn't support getCurrentPoolState, use direct pool call const slot0 = (await pool.slot0?.()) ?? (await pool.globalState?.()); sqrtPriceX96 = BigInt(slot0.sqrtPriceX96 ?? slot0.price); } if (inputT0Bal === 0n && inputT1Bal === 0n) { return { amountToSwap: 0n, zeroForOne: false }; } const desiredNow = calculateOptimalT0PerT1Variant(normalizedPositions, JSBI.BigInt(sqrtPriceX96.toString()), clampMode ?? 'tick'); const heldRatioX128 = ratioX128FromAmounts(inputT0Bal, inputT1Bal); const desiredRatioX128 = ratioX128FromFraction(desiredNow); const needZeroForOne = heldRatioX128 > desiredRatioX128; if (withinToleranceByX128(inputT0Bal, inputT1Bal, desiredNow, tol)) { return { amountToSwap: 0n, zeroForOne: needZeroForOne }; } if (needZeroForOne && inputT0Bal === 0n) { return { amountToSwap: 0n, zeroForOne: needZeroForOne }; } if (!needZeroForOne && inputT1Bal === 0n) { return { amountToSwap: 0n, zeroForOne: needZeroForOne }; } const mode: SearchMode = searchMode ?? 'input'; if (mode === 'parallel') { if (!this.engine.quoteExactOutputSingle) { return this.searchExactInput({ sqrtPriceX96, desiredNow, needZeroForOne, inputT0Bal, inputT1Bal, t0Address, t1Address, fee, maxIterations, desiredPositions: normalizedPositions, maxPriceImpactBps, tol, engineExtras, }); } const [a, b] = await Promise.all([ this.searchExactInput({ sqrtPriceX96, desiredNow, needZeroForOne, inputT0Bal, inputT1Bal, t0Address, t1Address, fee, maxIterations, desiredPositions: normalizedPositions, maxPriceImpactBps, tol, engineExtras, }), this.searchExactOutput({ sqrtPriceX96, desiredNow, needZeroForOne, inputT0Bal, inputT1Bal, t0Address, t1Address, fee, maxIterations, desiredPositions: normalizedPositions, maxPriceImpactBps, tol, engineExtras, }).catch(() => null), ]); if (b) return a.amountToSwap <= b.amountToSwap ? a : b; return a; } if (mode === 'output') { if (!this.engine.quoteExactOutputSingle) { return this.searchExactInput({ sqrtPriceX96, desiredNow, needZeroForOne, inputT0Bal, inputT1Bal, t0Address, t1Address, fee, maxIterations, desiredPositions: normalizedPositions, maxPriceImpactBps, tol, engineExtras, }); } return this.searchExactOutput({ sqrtPriceX96, desiredNow, needZeroForOne, inputT0Bal, inputT1Bal, t0Address, t1Address, fee, maxIterations, desiredPositions, maxPriceImpactBps, tol, engineExtras, }); } return this.searchExactInput({ sqrtPriceX96, desiredNow, needZeroForOne, inputT0Bal, inputT1Bal, t0Address, t1Address, fee, maxIterations, desiredPositions: normalizedPositions, maxPriceImpactBps, tol, engineExtras, }); } private async searchExactInput(args: { sqrtPriceX96: bigint; desiredNow: Fraction; needZeroForOne: boolean; inputT0Bal: bigint; inputT1Bal: bigint; t0Address: string; t1Address: string; fee?: number; maxIterations: number; desiredPositions: Position[]; maxPriceImpactBps?: number; tol: Fraction; engineExtras?: Record; }): Promise { const { sqrtPriceX96, desiredNow, needZeroForOne, inputT0Bal, inputT1Bal, t0Address, t1Address, fee, maxIterations, desiredPositions, maxPriceImpactBps, tol, engineExtras, } = args; const desiredRatioX128 = ratioX128FromFraction(desiredNow); const availableIn = needZeroForOne ? inputT0Bal : inputT1Bal; type EvalIn = { ok: boolean; reverted: boolean; sign: number; absDiffX128: bigint; postT0: bigint; postT1: bigint; sqrtAfter: bigint; }; const cacheIn = new Map(); const evaluateIn = async (amountIn: bigint): Promise => { const cacheKey = `${amountIn.toString()}|${ needZeroForOne ? 'Z1' : 'O1' }|${fee ?? 0}`; if (cacheIn.has(cacheKey)) return cacheIn.get(cacheKey)!; if (amountIn === 0n) { const ok = withinToleranceByX128( inputT0Bal, inputT1Bal, desiredNow, tol, ); const diff = absRatioDiffX128( ratioX128FromAmounts(inputT0Bal, inputT1Bal), desiredRatioX128, ); const res: EvalIn = { ok, reverted: false, sign: compareRatioX128( ratioX128FromAmounts(inputT0Bal, inputT1Bal), ratioX128FromFraction(desiredNow), ), absDiffX128: diff, postT0: inputT0Bal, postT1: inputT1Bal, sqrtAfter: sqrtPriceX96, }; cacheIn.set(cacheKey, res); return res; } try { this.logger.info( { logType: 'custom-router' }, `quoteExactInputSingle: amountIn=${amountIn.toString()}`, ); const q = await this.engine.quoteExactInputSingle({ tokenIn: needZeroForOne ? t0Address : t1Address, tokenOut: needZeroForOne ? t1Address : t0Address, amountIn, fee, sqrtPriceLimitX96: 0, extras: { ...(engineExtras || {}), zeroForOne: needZeroForOne, priceLimit: needZeroForOne ? 0 : BigInt('1461501637330902918203684832716283019655932542975'), }, }); this.logger.info( { logType: 'custom-router' }, `quoteExactInputSingle: amountIn=${amountIn.toString()}, sqrtPriceX96After=${q.sqrtPriceX96After.toString()}, amountOut=${q.amountOut.toString()}`, ); const newSqrt = q.sqrtPriceX96After; const amountOut = q.amountOut; const postT0 = needZeroForOne ? inputT0Bal - amountIn : inputT0Bal + amountOut; const postT1 = needZeroForOne ? inputT1Bal + amountOut : inputT1Bal - amountIn; const desiredAtPost = calculateOptimalT0PerT1Variant( desiredPositions, JSBI.BigInt(newSqrt.toString()), 'tick', ); if ( maxPriceImpactBps !== undefined && computePriceImpactBps(sqrtPriceX96, newSqrt) > maxPriceImpactBps ) { const res = { ok: false, reverted: true, sign: 0, absDiffX128: MAX_UINT256, postT0: inputT0Bal, postT1: inputT1Bal, sqrtAfter: sqrtPriceX96, }; cacheIn.set(cacheKey, res); return res; } const ok = withinToleranceByX128(postT0, postT1, desiredAtPost, tol); const sign = compareRatioX128( ratioX128FromAmounts(postT0, postT1), ratioX128FromFraction(desiredAtPost), ); const absDiffX128Val = absRatioDiffX128( ratioX128FromAmounts(postT0, postT1), ratioX128FromFraction(desiredAtPost), ); const res: EvalIn = { ok, reverted: false, sign, absDiffX128: absDiffX128Val, postT0, postT1, sqrtAfter: newSqrt, }; cacheIn.set(cacheKey, res); return res; } catch (e) { const res: EvalIn = { ok: false, reverted: true, sign: 0, absDiffX128: MAX_UINT256, postT0: inputT0Bal, postT1: inputT1Bal, sqrtAfter: sqrtPriceX96, }; cacheIn.set(cacheKey, res); return res; } }; const loAmount = 0n; const loEval = await evaluateIn(loAmount); const loSign = loEval.sign; const one = 1n; let hiAmount = availableIn / 10n > one ? availableIn / 10n : one; if (hiAmount > availableIn) hiAmount = availableIn; let hiEval = await evaluateIn(hiAmount); while (hiEval.reverted && hiAmount > one) { hiAmount = hiAmount / 2n; hiEval = await evaluateIn(hiAmount); } if (hiEval.reverted && hiAmount === one) { return { amountToSwap: 0n, zeroForOne: needZeroForOne }; } let expandCounter = 0; while ( hiEval.sign === loSign && hiAmount < availableIn && !hiEval.reverted && expandCounter < 50 ) { const next = hiAmount * 2n; hiAmount = next > availableIn ? availableIn : next; const evalRes = await evaluateIn(hiAmount); hiEval = evalRes; expandCounter++; if (hiEval.ok && hiEval.sign === 0) break; if (hiEval.reverted) break; } if (hiEval.sign === loSign) { const best = hiEval.absDiffX128 < loEval.absDiffX128 ? { amount: hiAmount, eval: hiEval } : { amount: loAmount, eval: loEval }; return { amountToSwap: best.amount, zeroForOne: needZeroForOne }; } let bestAmount = loEval.absDiffX128 <= hiEval.absDiffX128 ? loAmount : hiAmount; let bestError = loEval.absDiffX128 <= hiEval.absDiffX128 ? loEval.absDiffX128 : hiEval.absDiffX128; let lAmt = loAmount; let rAmt = hiAmount; const lSign = loSign; for (let i = 0; i < maxIterations; i++) { if (rAmt <= lAmt + 1n) break; const mid = (lAmt + rAmt) / 2n; const midEval = await evaluateIn(mid); if (midEval.reverted) { rAmt = mid === lAmt ? mid + 1n : mid; continue; } if (midEval.absDiffX128 < bestError) { bestError = midEval.absDiffX128; bestAmount = mid; } if (midEval.ok) { bestAmount = mid; break; } if (midEval.sign === lSign) { lAmt = mid; } else { rAmt = mid; } } return { amountToSwap: bestAmount, zeroForOne: needZeroForOne }; } private async searchExactOutput(args: { sqrtPriceX96: bigint; desiredNow: Fraction; needZeroForOne: boolean; inputT0Bal: bigint; inputT1Bal: bigint; t0Address: string; t1Address: string; fee?: number; maxIterations: number; desiredPositions: Position[]; maxPriceImpactBps?: number; tol: Fraction; engineExtras?: Record; }): Promise { const { sqrtPriceX96, desiredNow, needZeroForOne, inputT0Bal, inputT1Bal, t0Address, t1Address, fee, maxIterations, desiredPositions, maxPriceImpactBps, tol, engineExtras, } = args; if (!this.engine.quoteExactOutputSingle) { return this.searchExactInput(args); } const availableIn = needZeroForOne ? inputT0Bal : inputT1Bal; const one = 1n; const token1PerToken0X128 = (sqrtPriceX96 * sqrtPriceX96) / 2n ** 64n; const approxOut = needZeroForOne ? (inputT0Bal * token1PerToken0X128) / X128 : (inputT1Bal * X128) / token1PerToken0X128; let hiAmountOut = approxOut / 10n > one ? approxOut / 10n : one; type EvalOut = { ok: boolean; reverted: boolean; sign: number; absDiffX128: bigint; postT0: bigint; postT1: bigint; sqrtAfter: bigint; amountIn: bigint; }; const cacheOut = new Map(); const evaluateOut = async (amountOut: bigint): Promise => { const cacheKey = `${amountOut.toString()}|${ needZeroForOne ? 'Z1' : 'O1' }|${fee ?? 0}`; if (cacheOut.has(cacheKey)) return cacheOut.get(cacheKey)!; if (amountOut === 0n) { const ok = withinToleranceByX128( inputT0Bal, inputT1Bal, desiredNow, tol, ); const res: EvalOut = { ok, reverted: false, sign: compareRatioX128( ratioX128FromAmounts(inputT0Bal, inputT1Bal), ratioX128FromFraction(desiredNow), ), absDiffX128: absRatioDiffX128( ratioX128FromAmounts(inputT0Bal, inputT1Bal), ratioX128FromFraction(desiredNow), ), postT0: inputT0Bal, postT1: inputT1Bal, sqrtAfter: sqrtPriceX96, amountIn: 0n, }; cacheOut.set(cacheKey, res); return res; } try { this.logger.info( { logType: 'custom-router' }, `quoteExactOutputSingle: amountOut=${amountOut.toString()}`, ); const q = await ( this.engine.quoteExactOutputSingle as NonNullable< typeof this.engine.quoteExactOutputSingle > )({ tokenIn: needZeroForOne ? t0Address : t1Address, tokenOut: needZeroForOne ? t1Address : t0Address, amountOut, fee, sqrtPriceLimitX96: 0, extras: { ...(engineExtras || {}), zeroForOne: needZeroForOne, priceLimit: needZeroForOne ? 0 : BigInt('1461501637330902918203684832716283019655932542975'), }, }); const newSqrt = q.sqrtPriceX96After; const amountIn = q.amountIn; if (amountIn > availableIn) { const res = { ok: false, reverted: true, sign: 0, absDiffX128: MAX_UINT256, postT0: inputT0Bal, postT1: inputT1Bal, sqrtAfter: sqrtPriceX96, amountIn, }; cacheOut.set(cacheKey, res); return res; } if ( maxPriceImpactBps !== undefined && computePriceImpactBps(sqrtPriceX96, newSqrt) > maxPriceImpactBps ) { const res = { ok: false, reverted: true, sign: 0, absDiffX128: MAX_UINT256, postT0: inputT0Bal, postT1: inputT1Bal, sqrtAfter: sqrtPriceX96, amountIn, }; cacheOut.set(cacheKey, res); return res; } const postT0 = needZeroForOne ? inputT0Bal - amountIn : inputT0Bal + amountOut; const postT1 = needZeroForOne ? inputT1Bal + amountOut : inputT1Bal - amountIn; const desiredAtPost = calculateOptimalT0PerT1Variant( desiredPositions, JSBI.BigInt(newSqrt.toString()), 'tick', ); const ok = withinToleranceByX128(postT0, postT1, desiredAtPost, tol); const sign = compareRatioX128( ratioX128FromAmounts(postT0, postT1), ratioX128FromFraction(desiredAtPost), ); const absDiffX128Val = absRatioDiffX128( ratioX128FromAmounts(postT0, postT1), ratioX128FromFraction(desiredAtPost), ); const res: EvalOut = { ok, reverted: false, sign, absDiffX128: absDiffX128Val, postT0, postT1, sqrtAfter: newSqrt, amountIn, }; cacheOut.set(cacheKey, res); return res; } catch (e) { const res: EvalOut = { ok: false, reverted: true, sign: 0, absDiffX128: MAX_UINT256, postT0: inputT0Bal, postT1: inputT1Bal, sqrtAfter: sqrtPriceX96, amountIn: 0n, }; cacheOut.set(cacheKey, res); return res; } }; const loAmountOut = 0n; const loEval = await evaluateOut(loAmountOut); const loSign = loEval.sign; let hiEval = await evaluateOut(hiAmountOut); while (hiEval.reverted && hiAmountOut > one) { hiAmountOut = hiAmountOut / 2n; hiEval = await evaluateOut(hiAmountOut); } if (hiEval.reverted && hiAmountOut === one) { return { amountToSwap: 0n, zeroForOne: needZeroForOne }; } let expandCounter = 0; while (hiEval.sign === loSign && !hiEval.reverted && expandCounter < 20) { const next = hiAmountOut * 2n; hiAmountOut = next; const evalRes = await evaluateOut(hiAmountOut); hiEval = evalRes; expandCounter++; if (hiEval.ok && hiEval.sign === 0) break; if (hiEval.reverted) break; } if (hiEval.sign === loSign) { const best = hiEval.absDiffX128 < loEval.absDiffX128 ? hiEval : loEval; const amountIn = best.amountIn ?? 0n; return { amountToSwap: amountIn, zeroForOne: needZeroForOne }; } let bestOut = loEval.absDiffX128 <= hiEval.absDiffX128 ? loAmountOut : hiAmountOut; let bestErr = loEval.absDiffX128 <= hiEval.absDiffX128 ? loEval.absDiffX128 : hiEval.absDiffX128; let lAmt = loAmountOut; let rAmt = hiAmountOut; const lSign2 = loSign; for (let i = 0; i < maxIterations; i++) { if (rAmt <= lAmt + 1n) break; const mid = (lAmt + rAmt) / 2n; const midEval = await evaluateOut(mid); if (midEval.reverted) { rAmt = mid === lAmt ? mid + 1n : mid; continue; } if (midEval.absDiffX128 < bestErr) { bestErr = midEval.absDiffX128; bestOut = mid; } if (midEval.ok) { bestOut = mid; break; } if (midEval.sign === lSign2) { lAmt = mid; } else { rAmt = mid; } } const finalEval = await evaluateOut(bestOut); const amountIn = (finalEval && finalEval.amountIn) || 0n; return { amountToSwap: amountIn, zeroForOne: needZeroForOne }; } } export function calculateOptimalT0PerT1Variant( positions: Position[], sqrtRatioX96: JSBI, clampMode: ClampMode, ): Fraction { let totalT0Needed = JSBI.BigInt(0); let totalT1Needed = JSBI.BigInt(0); for (let i = 0; i < positions.length; i++) { const position = positions[i]; const upperSqrtRatioX96 = TickMath.getSqrtRatioAtTick(position.upperTick); const lowerSqrtRatioX96 = TickMath.getSqrtRatioAtTick(position.lowerTick); let workingSqrtRatioX96 = sqrtRatioX96; if (clampMode === 'price') { if (JSBI.greaterThan(sqrtRatioX96, upperSqrtRatioX96)) { workingSqrtRatioX96 = upperSqrtRatioX96; } else if (JSBI.lessThan(sqrtRatioX96, lowerSqrtRatioX96)) { workingSqrtRatioX96 = lowerSqrtRatioX96; } } else { const currentTick = TickMath.getTickAtSqrtRatio(workingSqrtRatioX96); const upperTick = position.upperTick; const lowerTick = position.lowerTick; if (currentTick > upperTick) { workingSqrtRatioX96 = upperSqrtRatioX96; } else if (currentTick < lowerTick) { workingSqrtRatioX96 = lowerSqrtRatioX96; } } const t0Needed = SqrtPriceMath.getAmount0Delta( workingSqrtRatioX96, upperSqrtRatioX96, JSBI.multiply(PRECISION, JSBI.BigInt(position.weight)), true, ); const t1Needed = SqrtPriceMath.getAmount1Delta( lowerSqrtRatioX96, workingSqrtRatioX96, JSBI.multiply(PRECISION, JSBI.BigInt(position.weight)), true, ); totalT0Needed = JSBI.add(totalT0Needed, t0Needed); totalT1Needed = JSBI.add(totalT1Needed, t1Needed); } return new Fraction(totalT0Needed.toString(), totalT1Needed.toString()); } function computePriceImpactBps( beforeSqrtX96: bigint, afterSqrtX96: bigint, ): number { const before2 = beforeSqrtX96 * beforeSqrtX96; const after2 = afterSqrtX96 * afterSqrtX96; const ratioX128 = (after2 * X128) / before2; const diffX128 = ratioX128 >= X128 ? ratioX128 - X128 : X128 - ratioX128; const bps = Number((diffX128 * 10000n) / X128); return bps; }