import { Fraction } from '@uniswap/sdk-core'; import { AerodromeCustomRouter, calculateAmountToSwap, fractionAbsoluteValue, } from '../../src/routers/aerodrome/aerodrome-router'; jest.mock('../../src/routers/base/base-router', () => { return { BaseRouter: jest.fn().mockImplementation(() => ({ getSwapAmount: jest .fn() .mockResolvedValue({ amountToSwap: 7n, zeroForOne: false }), })), }; }); const makePool = (overrides: Record = {}) => ({ tickSpacing: jest.fn().mockResolvedValue(10), slot0: jest.fn().mockResolvedValue({ sqrtPriceX96: 1n, unlocked: false }), token0: jest.fn().mockResolvedValue('0x1'), token1: jest.fn().mockResolvedValue('0x2'), ...overrides, }); const makeQuoter = () => ({ callStatic: { quoteExactInputSingle: jest.fn().mockResolvedValue({ sqrtPriceX96After: 1n, amountOut: 1n, }), }, getFunction: jest.fn().mockReturnValue({ staticCall: jest.fn().mockResolvedValue({ sqrtPriceX96After: 1n, amountOut: 1n, }), }), }); describe('Aerodrome router helpers', () => { it('fractionAbsoluteValue returns positive numerator and denominator', () => { const frac = new Fraction(-1, 2); const abs = fractionAbsoluteValue(frac); expect(abs.numerator.toString()).toBe('1'); expect(abs.denominator.toString()).toBe('2'); }); it('calculateAmountToSwap returns full amount when desired ratio is zero', () => { const desired = new Fraction(0, 1); expect(calculateAmountToSwap(desired, 1n, true, 10n, 20n)).toBe(10n); expect(() => calculateAmountToSwap(desired, 1n, false, 10n, 20n)).toThrow( 'wrong parameters', ); }); it('calculateAmountToSwap returns t1 amount when ratio is infinite', () => { const desired = new Fraction(1, 0); expect(calculateAmountToSwap(desired, 1n, false, 10n, 20n)).toBe(20n); expect(() => calculateAmountToSwap(desired, 1n, true, 10n, 20n)).toThrow( 'wrong parameters', ); }); }); describe('AerodromeCustomRouter', () => { const positions = [{ lowerTick: -60, upperTick: 60, weight: 1 }]; it('uses fallback when legacy flow fails', async () => { const pool = makePool(); const quoter = makeQuoter(); const logger = { info: jest.fn() } as any; const router = new AerodromeCustomRouter(quoter as any, logger); const result = await router.getSwapAmount( pool as any, positions as any, 10n, 20n, '0x1', '0x2', 3000, 3, ); const baseRouterModule = require('../../src/routers/base/base-router'); const baseRouterInstance = baseRouterModule.BaseRouter.mock.results[0].value; expect(baseRouterInstance.getSwapAmount).toHaveBeenCalled(); expect(result).toEqual({ amountToSwap: 7n, zeroForOne: false }); }); it('passes tickSpacing as engineExtras when available', async () => { const pool = makePool({ tickSpacing: jest.fn().mockResolvedValue(30), }); const quoter = makeQuoter(); const logger = { info: jest.fn() } as any; const router = new AerodromeCustomRouter(quoter as any, logger); await router.getSwapAmount( pool as any, positions as any, 10n, 20n, '0x1', '0x2', 3000, 3, ); const baseRouterModule = require('../../src/routers/base/base-router'); const baseRouterInstance = baseRouterModule.BaseRouter.mock.results[1].value; const args = baseRouterInstance.getSwapAmount.mock.calls[0][0]; expect(args.engineExtras).toEqual({ tickSpacing: 30 }); }); it('throws when fallback also fails', async () => { const baseRouterModule = require('../../src/routers/base/base-router'); baseRouterModule.BaseRouter.mockImplementationOnce(() => ({ getSwapAmount: jest.fn().mockRejectedValue(new Error('fallback failed')), })); const pool = makePool(); const quoter = makeQuoter(); const logger = { info: jest.fn() } as any; const router = new AerodromeCustomRouter(quoter as any, logger); await expect( router.getSwapAmount( pool as any, positions as any, 10n, 20n, '0x1', '0x2', 3000, 3, ), ).rejects.toThrow('fallback failed'); }); it('returns zero when already balanced', async () => { const pool = makePool({ slot0: jest.fn().mockResolvedValue({ sqrtPriceX96: 79228162514264337593543950336n, unlocked: true, }), }); const quoter = makeQuoter(); const logger = { info: jest.fn() } as any; const router = new AerodromeCustomRouter(quoter as any, logger); const result = await (router as any).computeSwapAmountLegacy({ pool, desiredPositions: positions, inputT0Bal: 1n, inputT1Bal: 1n, t0Address: '0x1', t1Address: '0x2', poolFee: 3000, maxIterations: 2, tolerance: new Fraction(1000, 1), }); expect(result).toEqual({ amountToSwap: 0n, zeroForOne: false }); }); it('returns zero when swap amount is non-positive', async () => { const pool = makePool({ slot0: jest.fn().mockResolvedValue({ sqrtPriceX96: 79228162514264337593543950336n, unlocked: true, }), }); const quoter = makeQuoter(); const logger = { info: jest.fn() } as any; const router = new AerodromeCustomRouter(quoter as any, logger); const module = require('../../src/routers/aerodrome/aerodrome-router'); const spy = jest.spyOn(module, 'calculateAmountToSwap').mockReturnValue(0n); const result = await (router as any).computeSwapAmountLegacy({ pool, desiredPositions: positions, inputT0Bal: 10n, inputT1Bal: 20n, t0Address: '0x1', t1Address: '0x2', poolFee: 3000, maxIterations: 1, tolerance: new Fraction(1, 1000), }); spy.mockRestore(); expect(result.amountToSwap).toBe(0n); }); it('throws when params are invalid', async () => { const pool = makePool({ slot0: jest.fn().mockResolvedValue({ sqrtPriceX96: 1n, unlocked: true }), }); const quoter = makeQuoter(); const logger = { info: jest.fn() } as any; const router = new AerodromeCustomRouter(quoter as any, logger); await expect( (router as any).computeSwapAmountLegacy({ pool, desiredPositions: [{ lowerTick: -60, upperTick: -60, weight: 1 }], inputT0Bal: 10n, inputT1Bal: 20n, t0Address: '0x1', t1Address: '0x2', poolFee: 3000, maxIterations: 1, tolerance: new Fraction(1, 1000), }), ).rejects.toThrow('lowerTick must be less than upperTick'); }); it('passes tickSpacing when pool returns string spacing in fallback', async () => { const pool = makePool({ tickSpacing: jest.fn().mockResolvedValue({ toString: () => '20' }), }); (pool as any).globalState = jest.fn().mockRejectedValue(new Error('fail')); const quoter = makeQuoter(); const logger = { info: jest.fn() } as any; const router = new AerodromeCustomRouter(quoter as any, logger); await router.getSwapAmount( pool as any, positions as any, 10n, 20n, '0x1', '0x2', 3000, 3, ); const baseRouterModule = require('../../src/routers/base/base-router'); const baseRouterInstance = baseRouterModule.BaseRouter.mock.results[ baseRouterModule.BaseRouter.mock.results.length - 1 ].value; const args = baseRouterInstance.getSwapAmount.mock.calls[0][0]; expect(args.engineExtras).toEqual({ tickSpacing: 20 }); }); });