import { SwapCalculator } from '../src/SwapCalculator'; jest.mock('../src/helpers', () => { class ProtocolDetector { isShadowVault() { return false; } isAlgebraIntegral19Vault() { return false; } } class AbiManager {} class ContractFactory { createPoolContract = jest.fn().mockReturnValue({}); createQuoterContract = jest.fn().mockReturnValue({}); createVaultContract = jest.fn().mockReturnValue({}); detectVaultUpgrade = jest.fn().mockResolvedValue(true); isMultiPositionVault = jest.fn().mockReturnValue(false); } class PositionExtractor { extractNewPositions = jest.fn().mockReturnValue([]); getCurrentVaultPositions = jest.fn().mockResolvedValue([]); } class BalanceQuerier { getVaultBalances = jest.fn().mockResolvedValue({ bal0: 0n, bal1: 0n }); } class PoolStateQuerier { getSlot0 = jest.fn().mockResolvedValue({}); extractSqrtPriceX96 = jest.fn().mockReturnValue(0n); getTickSpacing = jest.fn().mockResolvedValue(60); } const getNetworkConfig = jest.fn().mockReturnValue({}); const hasNetwork = jest.fn().mockReturnValue(true); return { ProtocolDetector, AbiManager, ContractFactory, PositionExtractor, BalanceQuerier, PoolStateQuerier, getNetworkConfig, hasNetwork, }; }); jest.mock('../src/routers/uniswap/uniswap-v3-router', () => { class UniswapCustomRouter { getSwapAmount = jest.fn().mockResolvedValue({ amountToSwap: 1n, zeroForOne: true, }); } return { UniswapCustomRouter }; }); jest.mock('../src/factory', () => { const { UniswapCustomRouter, } = require('../src/routers/uniswap/uniswap-v3-router'); class CustomRouterFactory { constructor() {} createCustomRouter = jest.fn().mockImplementation(() => { const err = (global as any).__swapCalculatorRouterError; if (err) { throw err; } return Promise.resolve(new UniswapCustomRouter()); }); } return { CustomRouterFactory }; }); describe('SwapCalculator', () => { const baseParams = { poolAddress: '0xpool', beaconName: 'Uniswap_v3', newPositions: [], bal0: 100n, bal1: 50n, token0: '0x1', token1: '0x2', poolFee: 3000, sqrtPriceX96: 100n, slippage: 0.5, maxIterations: 4, }; it('returns early when skipSwap is true', async () => { const calc = new SwapCalculator({} as any, 'arbitrum' as any); const result = await calc.calculateSwapForVault({ ...baseParams, executionResult: { skipSwap: true }, } as any); expect(result).toEqual({ amount: 0n, sqrtPrice: 0n, isSwapRouterFailed: false, resetPosition: null, }); }); it('returns early when specifySwap has amount', async () => { const calc = new SwapCalculator({} as any, 'arbitrum' as any); const result = await calc.calculateSwapForVault({ ...baseParams, executionResult: { specifySwap: { amount: 12, sqrtPrice: '42' }, }, } as any); expect(result.amount).toBe(12); expect(result.sqrtPrice).toBe(42n); }); it('applies slippage and returns signed amount', async () => { const calc = new SwapCalculator({} as any, 'arbitrum' as any); const result = await calc.calculateSwapForVault(baseParams as any); expect(result.amount).toBe(1n); expect(result.sqrtPrice).toBe(99n); }); it('handles fallback for unsupported network', async () => { (global as any).__swapCalculatorRouterError = new Error('fail'); const calc = new SwapCalculator({} as any, 'unknown' as any); await expect( calc.calculateSwapForVault({ ...baseParams, beaconName: 'Uniswap_v3', } as any), ).rejects.toThrow('not supported for fallback'); (global as any).__swapCalculatorRouterError = undefined; }); it('handles fallback for supported network and in-range single position', async () => { (global as any).__swapCalculatorRouterError = new Error('fail'); const calc = new SwapCalculator({} as any, 'arbitrum' as any); const result = await calc.calculateSwapForVault({ ...baseParams, poolFee: 3000, currentSlot: { tick: 0 }, vaultCurrentSinglePosition: { lowerTick: -10, upperTick: 10 }, executionResult: { forceError: true }, } as any); expect(result.isSwapRouterFailed).toBe(true); expect(result.resetPosition).toEqual({ position: { lowerTick: -10, upperTick: 10 }, isMultiVault: false, }); (global as any).__swapCalculatorRouterError = undefined; }); it('handles fallback for supported network and in-range multi position', async () => { (global as any).__swapCalculatorRouterError = new Error('fail'); const calc = new SwapCalculator({} as any, 'arbitrum' as any); const result = await calc.calculateSwapForVault({ ...baseParams, isMultiPosition: true, vaultCurrentMultiPositions: [ [-10, 0], [10, 20], [50, 50], ], currentSlot: { tick: 0 }, } as any); expect(result.isSwapRouterFailed).toBe(true); expect(result.resetPosition?.isMultiVault).toBe(true); (global as any).__swapCalculatorRouterError = undefined; }); it('throws when BalanceQuerier is not initialized', async () => { const calc = new SwapCalculator({} as any, 'arbitrum' as any); await expect(calc.getVaultBalances({} as any)).rejects.toThrow( 'BalanceQuerier not initialized', ); }); it('delegates to helpers for passthrough methods', async () => { const calc = new SwapCalculator({} as any, 'arbitrum' as any); expect(calc.extractPositions({} as any)).toEqual([]); await expect(calc.getCurrentPositions({} as any, false)).resolves.toEqual( [], ); await expect(calc.getPoolSlot0('beacon', {} as any)).resolves.toEqual({}); expect(calc.extractSqrtPriceX96('beacon', {} as any)).toBe(0n); await expect(calc.getTickSpacing('0xpool')).resolves.toBe(60); expect(calc.createPoolContract('0xpool', 'beacon')).toEqual({}); expect(calc.createVaultContract('0xvault', 'beacon', true)).toEqual({}); await expect(calc.detectVaultUpgrade('0xvault', 'beacon')).resolves.toBe( true, ); expect(calc.isMultiPositionVault('beacon')).toBe(false); }); it('uses custom max iterations for shadow-like vaults', async () => { const helpers = require('../src/helpers'); helpers.ProtocolDetector.prototype.isShadowVault = jest .fn() .mockReturnValue(true); const calc = new SwapCalculator({} as any, 'arbitrum' as any); const result = await calc.calculateSwapForVault({ ...baseParams, maxIterations: undefined, } as any); expect(result.amount).toBe(1n); }); });