import { Fraction } from '@uniswap/sdk-core'; import { TickMath } from '@uniswap/v3-sdk'; import { BaseRouter, calculateOptimalT0PerT1Variant, } from '../../src/routers/base/base-router'; import { QuoteEngine } from '../../src/engines/quote-engine'; import { Position } from '../../src/types/api'; const makePool = (sqrtPriceX96: bigint) => ({ slot0: jest.fn().mockResolvedValue({ sqrtPriceX96, tick: 0 }), globalState: jest.fn().mockResolvedValue({ price: sqrtPriceX96, tick: 0 }), }); const makeEngine = (overrides: Partial = {}): QuoteEngine => { return { quoteExactInputSingle: jest.fn().mockImplementation(({ amountIn }) => { return Promise.resolve({ amountOut: amountIn, sqrtPriceX96After: overrides.getCurrentPoolState ? 0n : 0n, }); }), ...overrides, } as QuoteEngine; }; const positions: Position[] = [ { lowerTick: -60, upperTick: 60, weight: 1, }, ]; describe('BaseRouter', () => { it('returns zero when both balances are zero', async () => { const sqrtPriceX96 = BigInt(TickMath.getSqrtRatioAtTick(0).toString()); const pool = makePool(sqrtPriceX96); const engine = makeEngine({ getCurrentPoolState: jest.fn().mockResolvedValue({ sqrtPriceX96 }), }); const router = new BaseRouter(engine); const result = await router.getSwapAmount({ pool, desiredPositions: positions, inputT0Bal: 0n, inputT1Bal: 0n, t0Address: '0x1', t1Address: '0x2', fee: 3000, maxIterations: 8, }); expect(result).toEqual({ amountToSwap: 0n, zeroForOne: false }); }); it('falls back to pool slot0 when engine pool state fails', async () => { const sqrtPriceX96 = BigInt(TickMath.getSqrtRatioAtTick(0).toString()); const pool = makePool(sqrtPriceX96); const engine = makeEngine({ getCurrentPoolState: jest.fn().mockRejectedValue(new Error('fail')), quoteExactInputSingle: jest.fn().mockResolvedValue({ amountOut: 1n, sqrtPriceX96After: sqrtPriceX96, }), }); const router = new BaseRouter(engine); const result = await router.getSwapAmount({ pool, desiredPositions: positions, inputT0Bal: 200n, inputT1Bal: 50n, t0Address: '0x1', t1Address: '0x2', fee: 3000, maxIterations: 5, }); expect(pool.slot0).toHaveBeenCalled(); expect(result.amountToSwap).toBeGreaterThanOrEqual(0n); }); it('returns zero when price impact exceeds max bps', async () => { const sqrtPriceX96 = BigInt(TickMath.getSqrtRatioAtTick(0).toString()); const pool = makePool(sqrtPriceX96); const engine = makeEngine({ getCurrentPoolState: jest.fn().mockResolvedValue({ sqrtPriceX96 }), quoteExactInputSingle: jest.fn().mockResolvedValue({ amountOut: 1n, sqrtPriceX96After: sqrtPriceX96 * 1000n, }), }); const router = new BaseRouter(engine); const result = await router.getSwapAmount({ pool, desiredPositions: positions, inputT0Bal: 500n, inputT1Bal: 100n, t0Address: '0x1', t1Address: '0x2', fee: 3000, maxIterations: 6, maxPriceImpactBps: 1, }); expect(result.amountToSwap).toBe(0n); }); it('returns zero when within tolerance', async () => { const sqrtPriceX96 = BigInt(TickMath.getSqrtRatioAtTick(0).toString()); const pool = makePool(sqrtPriceX96); const engine = makeEngine({ getCurrentPoolState: jest.fn().mockResolvedValue({ sqrtPriceX96 }), quoteExactInputSingle: jest.fn().mockResolvedValue({ amountOut: 1n, sqrtPriceX96After: sqrtPriceX96, }), }); const router = new BaseRouter(engine); const result = await router.getSwapAmount({ pool, desiredPositions: positions, inputT0Bal: 100n, inputT1Bal: 100n, t0Address: '0x1', t1Address: '0x2', fee: 3000, maxIterations: 4, ratioErrorTolerance: new Fraction(1, 1), }); expect(result.amountToSwap).toBe(0n); }); it('uses output search and returns zero when output quotes revert', async () => { const sqrtPriceX96 = BigInt(TickMath.getSqrtRatioAtTick(0).toString()); const pool = makePool(sqrtPriceX96); const engine = makeEngine({ getCurrentPoolState: jest.fn().mockResolvedValue({ sqrtPriceX96 }), quoteExactOutputSingle: jest.fn().mockRejectedValue(new Error('revert')), quoteExactInputSingle: jest.fn().mockResolvedValue({ amountOut: 1n, sqrtPriceX96After: sqrtPriceX96, }), }); const router = new BaseRouter(engine); const result = await router.getSwapAmount({ pool, desiredPositions: positions, inputT0Bal: 300n, inputT1Bal: 40n, t0Address: '0x1', t1Address: '0x2', fee: 3000, maxIterations: 4, searchMode: 'output', }); expect(result.amountToSwap).toBe(0n); }); it('falls back to input search when parallel mode lacks output quotes', async () => { const sqrtPriceX96 = BigInt(TickMath.getSqrtRatioAtTick(0).toString()); const pool = makePool(sqrtPriceX96); const engine = makeEngine({ getCurrentPoolState: jest.fn().mockResolvedValue({ sqrtPriceX96 }), quoteExactInputSingle: jest.fn().mockResolvedValue({ amountOut: 2n, sqrtPriceX96After: sqrtPriceX96, }), }); const router = new BaseRouter(engine); const result = await router.getSwapAmount({ pool, desiredPositions: positions, inputT0Bal: 250n, inputT1Bal: 60n, t0Address: '0x1', t1Address: '0x2', fee: 3000, maxIterations: 4, searchMode: 'parallel', }); expect(engine.quoteExactInputSingle).toHaveBeenCalled(); expect(result.amountToSwap).toBeGreaterThanOrEqual(0n); }); it('returns zero when swap direction lacks balance', async () => { const sqrtPriceX96 = BigInt(TickMath.getSqrtRatioAtTick(0).toString()); const pool = makePool(sqrtPriceX96); const engine = makeEngine({ getCurrentPoolState: jest.fn().mockResolvedValue({ sqrtPriceX96 }), }); const router = new BaseRouter(engine); const result = await router.getSwapAmount({ pool, desiredPositions: positions, inputT0Bal: 0n, inputT1Bal: 100n, t0Address: '0x1', t1Address: '0x2', fee: 3000, maxIterations: 4, }); expect(result.amountToSwap).toBe(0n); }); it('picks best result in parallel search when both succeed', async () => { const sqrtPriceX96 = BigInt(TickMath.getSqrtRatioAtTick(0).toString()); const pool = makePool(sqrtPriceX96); const engine = makeEngine({ getCurrentPoolState: jest.fn().mockResolvedValue({ sqrtPriceX96 }), quoteExactInputSingle: jest.fn().mockResolvedValue({ amountOut: 2n, sqrtPriceX96After: sqrtPriceX96, }), quoteExactOutputSingle: jest.fn().mockResolvedValue({ amountIn: 1n, sqrtPriceX96After: sqrtPriceX96, }), }); const router = new BaseRouter(engine); const result = await router.getSwapAmount({ pool, desiredPositions: positions, inputT0Bal: 200n, inputT1Bal: 50n, t0Address: '0x1', t1Address: '0x2', fee: 3000, maxIterations: 4, searchMode: 'parallel', }); expect(result.amountToSwap).toBe(1n); }); it('uses globalState when slot0 is unavailable', async () => { const sqrtPriceX96 = BigInt(TickMath.getSqrtRatioAtTick(0).toString()); const pool = { slot0: undefined, globalState: jest .fn() .mockResolvedValue({ price: sqrtPriceX96, tick: 0 }), } as any; const engine = makeEngine({ quoteExactInputSingle: jest.fn().mockResolvedValue({ amountOut: 1n, sqrtPriceX96After: sqrtPriceX96, }), }); const router = new BaseRouter(engine); const result = await router.getSwapAmount({ pool, desiredPositions: positions, inputT0Bal: 200n, inputT1Bal: 50n, t0Address: '0x1', t1Address: '0x2', fee: 3000, maxIterations: 3, }); expect(pool.globalState).toHaveBeenCalled(); expect(result.amountToSwap).toBeGreaterThanOrEqual(0n); }); it('uses slot0 when engine has no pool state method', async () => { const sqrtPriceX96 = BigInt(TickMath.getSqrtRatioAtTick(0).toString()); const pool = makePool(sqrtPriceX96); const engine = makeEngine({ quoteExactInputSingle: jest.fn().mockResolvedValue({ amountOut: 1n, sqrtPriceX96After: sqrtPriceX96, }), }); const router = new BaseRouter(engine); const result = await router.getSwapAmount({ pool, desiredPositions: positions, inputT0Bal: 200n, inputT1Bal: 50n, t0Address: '0x1', t1Address: '0x2', fee: 3000, maxIterations: 3, }); expect(pool.slot0).toHaveBeenCalled(); expect(result.amountToSwap).toBeGreaterThanOrEqual(0n); }); it('returns zero when output search exceeds available balance', async () => { const sqrtPriceX96 = BigInt(TickMath.getSqrtRatioAtTick(0).toString()); const pool = makePool(sqrtPriceX96); const engine = makeEngine({ getCurrentPoolState: jest.fn().mockResolvedValue({ sqrtPriceX96 }), quoteExactOutputSingle: jest.fn().mockResolvedValue({ amountIn: 999n, sqrtPriceX96After: sqrtPriceX96, }), quoteExactInputSingle: jest.fn().mockResolvedValue({ amountOut: 1n, sqrtPriceX96After: sqrtPriceX96, }), }); const router = new BaseRouter(engine); const result = await router.getSwapAmount({ pool, desiredPositions: positions, inputT0Bal: 10n, inputT1Bal: 10n, t0Address: '0x1', t1Address: '0x2', fee: 3000, maxIterations: 3, searchMode: 'output', }); expect(result.amountToSwap).toBe(0n); }); it('falls back to input search when output mode lacks output quotes', async () => { const sqrtPriceX96 = BigInt(TickMath.getSqrtRatioAtTick(0).toString()); const pool = makePool(sqrtPriceX96); const engine = makeEngine({ getCurrentPoolState: jest.fn().mockResolvedValue({ sqrtPriceX96 }), quoteExactInputSingle: jest.fn().mockResolvedValue({ amountOut: 2n, sqrtPriceX96After: sqrtPriceX96, }), }); const router = new BaseRouter(engine); const result = await router.getSwapAmount({ pool, desiredPositions: positions, inputT0Bal: 250n, inputT1Bal: 60n, t0Address: '0x1', t1Address: '0x2', fee: 3000, maxIterations: 4, searchMode: 'output', }); expect(engine.quoteExactInputSingle).toHaveBeenCalled(); expect(result.amountToSwap).toBeGreaterThanOrEqual(0n); }); it('returns zero when input quotes always revert at smallest amount', async () => { const sqrtPriceX96 = BigInt(TickMath.getSqrtRatioAtTick(0).toString()); const pool = makePool(sqrtPriceX96); const engine = makeEngine({ getCurrentPoolState: jest.fn().mockResolvedValue({ sqrtPriceX96 }), quoteExactInputSingle: jest.fn().mockRejectedValue(new Error('revert')), }); const router = new BaseRouter(engine); const result = await router.getSwapAmount({ pool, desiredPositions: positions, inputT0Bal: 10n, inputT1Bal: 1n, t0Address: '0x1', t1Address: '0x2', fee: 3000, maxIterations: 1, }); expect(result.amountToSwap).toBe(0n); }); }); describe('calculateOptimalT0PerT1Variant', () => { it('clamps by price when outside bounds', () => { const positionsLocal: Position[] = [ { lowerTick: -120, upperTick: -60, weight: 1 }, ]; const highSqrt = TickMath.getSqrtRatioAtTick(120); const frac = calculateOptimalT0PerT1Variant( positionsLocal, highSqrt, 'price', ); expect(frac).toBeInstanceOf(Fraction); expect(frac.denominator.toString()).not.toBe('0'); }); it('clamps by tick when outside bounds', () => { const positionsLocal: Position[] = [ { lowerTick: 0, upperTick: 60, weight: 1 }, ]; const lowSqrt = TickMath.getSqrtRatioAtTick(-120); const frac = calculateOptimalT0PerT1Variant( positionsLocal, lowSqrt, 'tick', ); expect(frac).toBeInstanceOf(Fraction); expect(frac.denominator.toString()).toBe('0'); }); });