import { test, describe, expect, beforeAll, mock, afterEach } from 'bun:test'; import { ChainHopperClient } from '../src/client'; import { Protocol, BridgeType, MigrationMethod, NATIVE_ETH_ADDRESS, DEFAULT_SLIPPAGE_IN_BPS, } from '../src/utils/constants'; import type { PositionWithFees, ExactMigrationResponse, RequestExactMigrationParams, RequestMigrationParams, } from '../src/types/sdk'; import { Position as V4Position, Pool as V4Pool } from '@uniswap/v4-sdk'; import { Position as V3Position, Pool as V3Pool } from '@uniswap/v3-sdk'; import { Quote } from '@across-protocol/app-sdk'; import { ModuleMocker } from './ModuleMocker'; import { zeroAddress } from 'viem'; import { Ether, Token } from '@uniswap/sdk-core'; import { TickMath } from '@uniswap/v3-sdk'; import { positionValue, toSDKPosition } from '../src/utils/position'; let client: ReturnType; const moduleMocker = new ModuleMocker(); const ownerAddress = '0x0000000000000000000000000000000000000001'; beforeAll(() => { const rpcUrls = { 1: Bun.env.MAINNET_RPC_URL!, 10: Bun.env.OPTIMISM_RPC_URL!, 130: Bun.env.UNICHAIN_RPC_URL!, 8453: Bun.env.BASE_RPC_URL!, 42161: Bun.env.ARBITRUM_RPC_URL!, }; client = ChainHopperClient.create({ rpcUrls }); }); afterEach(() => { moduleMocker.clear(); }); const validateMigrationResponse = (params: RequestExactMigrationParams, result: ExactMigrationResponse): void => { const { sourcePosition, destination } = params; const { migration } = result; const { position } = migration; // check correct output chain expect(result.sourcePosition.pool.chainId).toBe(sourcePosition.chainId); expect(position.pool.chainId).toBe(destination.chainId); // check correct output protocol expect(position.pool.protocol).toBe(destination.protocol); if (destination.token0 == NATIVE_ETH_ADDRESS) { expect(position.pool.token0.address === NATIVE_ETH_ADDRESS).toBe(true); } else { expect(position.pool.token0.address).toBe(destination.token0); } expect(position.pool.token1.address).toBe(destination.token1); // check correct output ticks expect(position.tickLower).toBe(destination.tickLower); expect(position.tickUpper).toBe(destination.tickUpper); const tickSpacing = migration.position.pool.tickSpacing; // need Math.abs because -0 != 0 in bun test matching expect(Math.abs(position.tickLower % tickSpacing)).toEqual(0); expect(Math.abs(position.tickUpper % tickSpacing)).toEqual(0); // check correct output pool const pool: V4Pool = result.migration.position.pool as unknown as V4Pool; if ('fee' in destination) expect(pool.fee).toBe(destination.fee); if ('hooks' in destination) expect(pool.hooks).toBe(destination.hooks as string); if ('tickSpacing' in destination) expect(pool.tickSpacing).toBe(destination.tickSpacing as number); const amount0 = position.amount0; const amount1 = position.amount1; const amount0Min = position.amount0Min ? position.amount0Min : 0; const amount1Min = position.amount1Min ? position.amount1Min : 0; // check correct output amounts within slippage expect(amount0).toBeGreaterThanOrEqual(amount0Min); expect(amount1).toBeGreaterThanOrEqual(amount1Min); // check fees const senderShareBps = params.senderShareBps ? BigInt(params.senderShareBps) : 0n; // don't want to call getSettlerFees excessively, can update if this changes: const protocolShareBps = 0n; expect(migration.migrationFees.sender.bps).toBe(Number(senderShareBps)); expect(migration.migrationFees.protocol.bps).toBe(Number(protocolShareBps)); expect(migration.migrationFees.total.bps).toBe(Number(protocolShareBps + senderShareBps)); expect(migration.migrationFees.total.amount0).toBe( migration.migrationFees.sender.amount0 + migration.migrationFees.protocol.amount0 ); expect(migration.migrationFees.total.amount1).toBe( migration.migrationFees.sender.amount1 + migration.migrationFees.protocol.amount1 ); if (migration.exactPath.migrationMethod === MigrationMethod.SingleToken) { expect( migration.migrationFees.protocol.amount0 === (migration.routes[0].outputAmount * protocolShareBps) / 10_000n || migration.migrationFees.protocol.amount1 === (migration.routes[0].outputAmount * protocolShareBps) / 10_000n ); expect(migration.migrationFees.protocol.amount1 === 0n || migration.migrationFees.protocol.amount0 === 0n); } else if (migration.exactPath.migrationMethod === MigrationMethod.DualToken) { expect( migration.migrationFees.protocol.amount0 === (migration.routes[0].outputAmount * protocolShareBps) / 10_000n || migration.migrationFees.protocol.amount0 === (migration.routes[1].outputAmount * protocolShareBps) / 10_000n ); expect( migration.migrationFees.protocol.amount1 === (migration.routes[0].outputAmount * protocolShareBps) / 10_000n || migration.migrationFees.protocol.amount1 === (migration.routes[1].outputAmount * protocolShareBps) / 10_000n ); expect( migration.migrationFees.sender.amount0 === (migration.routes[0].outputAmount * senderShareBps) / 10_000n || migration.migrationFees.sender.amount0 === (migration.routes[1].outputAmount * senderShareBps) / 10_000n ); expect( migration.migrationFees.sender.amount1 === (migration.routes[0].outputAmount * senderShareBps) / 10_000n || migration.migrationFees.sender.amount1 === (migration.routes[1].outputAmount * senderShareBps) / 10_000n ); } // check execution params const executionParams = migration.executionParams; expect(executionParams.functionName).toBe('safeTransferFrom'); expect(executionParams.args[0]).toBe(result.sourcePosition.owner); expect(executionParams.args[1]).toBeDefined(); expect(executionParams.args[2]).toBe(sourcePosition.tokenId); if (sourcePosition.protocol === Protocol.UniswapV3) { expect(executionParams.address).toBe( client.chainConfigs[sourcePosition.chainId].v3NftPositionManagerContract.address ); if (migration.exactPath.bridgeType === BridgeType.Direct) { expect(executionParams.args[1]).toBe( client.chainConfigs[sourcePosition.chainId].UniswapV3DirectMigrator as `0x${string}` ); } else { expect(executionParams.args[1]).toBe( client.chainConfigs[sourcePosition.chainId].UniswapV3AcrossMigrator as `0x${string}` ); } } else if (sourcePosition.protocol === Protocol.UniswapV4) { expect(executionParams.address).toBe(client.chainConfigs[sourcePosition.chainId].v4PositionManagerContract.address); if (migration.exactPath.bridgeType === BridgeType.Direct) { expect(executionParams.args[1]).toBe( client.chainConfigs[sourcePosition.chainId].UniswapV4DirectMigrator as `0x${string}` ); } else { expect(executionParams.args[1]).toBe( client.chainConfigs[sourcePosition.chainId].UniswapV4AcrossMigrator as `0x${string}` ); } } else if (sourcePosition.protocol === Protocol.Aerodrome) { expect(executionParams.address).toBe( client.chainConfigs[sourcePosition.chainId].aerodromeNftPositionManagerContract!.address ); expect(executionParams.args[1]).toBe( client.chainConfigs[sourcePosition.chainId].AerodromeAcrossMigrator as `0x${string}` ); } }; describe('invalid migrations', () => { test('reject single token v3 migration with invalid bridge type', async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: 8453, tokenId: 104758n, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x532f27101965dd16442E59d40670FaF5eBB142E4', tickLower: 62200, tickUpper: 103800, fee: 10000, tickSpacing: 200, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: 'nobridge' as BridgeType, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; try { await client.requestExactMigration(params); } catch (e) { expect(e.message.includes('Bridge type not supported')); } }); test('reject dual token v3 migration with invalid bridge type', async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: 8453, tokenId: 104758n, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x532f27101965dd16442E59d40670FaF5eBB142E4', tickLower: 62200, tickUpper: 103800, fee: 10000, tickSpacing: 200, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: 'nobridge' as BridgeType, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; try { await client.requestExactMigration(params); } catch (e) { expect(e.message.includes('Bridge type not supported')); } }); test('reject single token v4 migration with invalid bridge type', async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: 8453, tokenId: 104758n, protocol: Protocol.UniswapV4, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x532f27101965dd16442E59d40670FaF5eBB142E4', tickLower: 62200, tickUpper: 103800, fee: 10000, tickSpacing: 200, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: 'nobridge' as BridgeType, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; try { await client.requestExactMigration(params); } catch (e) { expect(e.message.includes('Bridge type not supported')); } }); test('reject dual token v4 migration with invalid bridge type', async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: 8453, tokenId: 104758n, protocol: Protocol.UniswapV4, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x532f27101965dd16442E59d40670FaF5eBB142E4', tickLower: 62200, tickUpper: 103800, fee: 10000, tickSpacing: 200, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: 'nobridge' as BridgeType, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; try { await client.requestExactMigration(params); } catch (e) { expect(e.message.includes('Bridge type not supported')); } }); test('reject migration that are too large for across', async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: 8453, tokenId: 104758n, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: client.chainConfigs[130].usdcAddress, tickLower: 62200, tickUpper: 103800, fee: 10000, tickSpacing: 200, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; await moduleMocker.mock('@across-protocol/app-sdk', () => ({ AcrossClient: { create: mock(() => { return { getQuote: (): Promise => { throw new Error("doesn't have enough funds to support this deposit"); }, }; }), }, })); expect(async () => await client.requestExactMigration(params)).toThrow( "doesn't have enough funds to support this deposit" ); }); test("reject migration where a token can't be found on the destination chain", async () => { const sourceChainId = 8453; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = 100; const liquidity = 1_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 18, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, ...toSDKPosition({ chainConfig: client.chainConfigs[8453], position: new V3Position({ pool, liquidity: 1_000_000_000_000, tickLower: 10, tickUpper: 500, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 8453, tokenId: 104758n, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x532f27101965dd16442E59d40670FaF5eBB142E4', // this is the base address for BRETT bc it doesn't exist tickLower: 62200, tickUpper: 103800, fee: 10000, tickSpacing: 200, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; try { await client.requestExactMigration(params); } catch (e) { expect(e.message).toContain('Failed to get token'); } }); test("reject migration where a token can't be bridged", async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: 8453, tokenId: 104758n, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x532f27101965dd16442E59d40670FaF5eBB142E4', // this is the base address for BRETT bc it doesn't exist tickLower: 62200, tickUpper: 103800, fee: 10000, tickSpacing: 200, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; await moduleMocker.mock('@across-protocol/app-sdk', () => ({ AcrossClient: { create: mock(() => { return { getQuote: (): Promise => { throw new Error('Unsupported token on given origin chain'); }, }; }), }, })); try { await client.requestExactMigration(params); } catch (e) { expect(e.message).toContain('Unsupported token on given origin chain'); } }); test('reject migration with an invalid token order', async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: 1, tokenId: 963499n, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', token1: NATIVE_ETH_ADDRESS, tickLower: -203450, tickUpper: -193130, fee: 500, tickSpacing: 10, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; try { await client.requestExactMigration(params); } catch (e) { expect(e.message).toContain('token0 and token1 must be distinct addresses in alphabetical order'); } }); test('reject migration with two of the same token', async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: 1, tokenId: 963499n, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', token1: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', tickLower: -203450, tickUpper: -193130, fee: 500, tickSpacing: 10, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; try { await client.requestExactMigration(params); } catch (e) { expect(e.message).toContain('token0 and token1 must be distinct addresses in alphabetical order'); } }); test('reject migration to v3 requesting native token', async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: 130, tokenId: 1000n, protocol: Protocol.UniswapV4, }, destination: { chainId: 8453, protocol: Protocol.UniswapV3, token0: NATIVE_ETH_ADDRESS, token1: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', tickLower: -202230, tickUpper: -199380, fee: 500, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; expect(async () => await client.requestExactMigration(params)).toThrow('Native tokens not supported on Uniswap v3'); }); test('reject migration from v3 where neither token is weth', async () => { const sourceChainId = 1; const token0 = '0x078D782b760474a361dDA0AF3839290b0EF57AD6'; const token1 = '0x588CE4F028D8e7B53B687865d6A67b3A54C75518'; const fee = 100; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -276325; const liquidity = 1000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'RANDOM1'), new Token(sourceChainId, token1, 18, 'RANDOM2'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: liquidity.toString(), tickLower: -276352, tickUpper: -276299, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); try { const params: RequestExactMigrationParams = { sourcePosition: { chainId: 1, tokenId: 949124n, protocol: Protocol.UniswapV3, }, destination: { chainId: 8453, protocol: Protocol.UniswapV4, token0: zeroAddress, token1: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', tickLower: -276352, tickUpper: -276299, fee: 100, tickSpacing: 1, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; await client.requestExactMigration(params); } catch (e) { expect(e.message).toContain('WETH not found in position'); } }); test('reject migration from v4 where neither token is weth or eth', async () => { const token0 = '0x078D782b760474a361dDA0AF3839290b0EF57AD6'; const token1 = '0x588CE4F028D8e7B53B687865d6A67b3A54C75518'; const fee = 500; const tickSpacing = 10; const hooks = '0x0000000000000000000000000000000000000000'; await moduleMocker.mock('../src/actions/getV4Position.ts', () => ({ getV4Position: mock(() => { const tickCurrent = 10; const liquidity = 1000n; const pool = new V4Pool( new Token(1, token0, 18, 'RANDOM1'), new Token(1, token1, 18, 'RANDOM2'), fee, tickSpacing, hooks, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, ...toSDKPosition({ chainConfig: client.chainConfigs[130], position: new V4Position({ pool, liquidity: liquidity.toString(), tickLower: 0, tickUpper: 100, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 8453, tokenId: 10249n, protocol: Protocol.UniswapV4, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: zeroAddress, token1, tickLower: -88700, tickUpper: 88700, fee, tickSpacing, hooks, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; expect(async () => await client.requestExactMigration(params)).toThrow('ETH/WETH not found in position'); }); test('reject migration when neither token is weth or eth in destination', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 3000; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -199980; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 963499n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 10_000_000, tickLower: -886980, tickUpper: 886980, }), }), feeAmount0: 1000n, feeAmount1: 2000n, }; }), })); try { const params: RequestExactMigrationParams = { sourcePosition: { chainId: 1, tokenId: 963499n, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: '0x0555E30da8f98308EdB960aa94C0Db47230d2B9c', token1: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', tickLower: -887000, tickUpper: 887000, fee: 3000, tickSpacing: 60, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; await client.requestExactMigration(params); } catch (e) { expect(e.message).toContain('destination must specify either ETH or WETH as one of token0 or token1'); } }); }); describe('in-range v3→ migrations', () => { let v3ChainId: number; let v3TokenId: bigint; beforeAll(async () => { v3ChainId = 1; v3TokenId = 963499n; // https://app.uniswap.org/positions/v3/ethereum/963499 }); test('generate valid mainnet v3 → unichain v4 single-token migration', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickSpacing = 10; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -199000; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: v3TokenId, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 10_000_000, tickLower, tickUpper, }), }), feeAmount0: 1000n, feeAmount1: 2000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: v3ChainId, tokenId: v3TokenId, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', tickLower: -1 * tickUpper, tickUpper: -1 * tickLower, fee: fee, tickSpacing: tickSpacing, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); test('generate valid mainnet v3 → aerodrome single-token migration - tickSpacing 1', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 100; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -199000; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: v3TokenId, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 1_000_000, tickLower, tickUpper, }), }), feeAmount0: 1000n, feeAmount1: 2000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: v3ChainId, tokenId: v3TokenId, protocol: Protocol.UniswapV3, }, destination: { chainId: 8453, protocol: Protocol.Aerodrome, token0: '0x4200000000000000000000000000000000000006', token1: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', tickLower: -203500, tickUpper: -193100, tickSpacing: 1, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); test('generate valid mainnet v3 → aerodrome single-token migration - tickSpacing 100', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -199000; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: v3TokenId, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 1_000_000, tickLower, tickUpper, }), }), feeAmount0: 1000n, feeAmount1: 2000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: v3ChainId, tokenId: v3TokenId, protocol: Protocol.UniswapV3, }, destination: { chainId: 8453, protocol: Protocol.Aerodrome, token0: '0x4200000000000000000000000000000000000006', token1: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', tickLower: -203500, tickUpper: -193100, tickSpacing: 100, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); test('generate valid mainnet v3 → aerodrome single-token migration - tickSpacing 2000', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -199000; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: v3TokenId, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 10000, tickLower, tickUpper, }), }), feeAmount0: 1000n, feeAmount1: 2000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: v3ChainId, tokenId: v3TokenId, protocol: Protocol.UniswapV3, }, destination: { chainId: 8453, protocol: Protocol.Aerodrome, token0: '0x4200000000000000000000000000000000000006', token1: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', tickLower: -204000, tickUpper: -194000, tickSpacing: 2000, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS * 10, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); test('generate valid mainnet v3 → aerodrome dual-token migration', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -199000; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: v3TokenId, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 10_000_000, tickLower, tickUpper, }), }), feeAmount0: 10000000n, feeAmount1: 20000000000000000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: v3ChainId, tokenId: v3TokenId, protocol: Protocol.UniswapV3, }, destination: { chainId: 8453, protocol: Protocol.Aerodrome, token0: '0x4200000000000000000000000000000000000006', token1: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', tickLower: 193100, tickUpper: 203500, tickSpacing: 100, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); test('generate valid aerodrome → unichain v4 single-token migration', async () => { const sourceChainId = 8453; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 100; const tickLower = 193130; const tickUpper = 203450; await moduleMocker.mock('../src/actions/getAerodromePosition.ts', () => ({ getAerodromePosition: mock(() => { const tickCurrent = 199000; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 12345n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 1000000, tickLower, tickUpper, }), }), feeAmount0: 200000000000000n, feeAmount1: 10000000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 8453, tokenId: 12345n, protocol: Protocol.Aerodrome, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', tickLower: tickLower, tickUpper: tickUpper, fee: 500, tickSpacing: 10, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); test('generate valid aerodrome → unichain v4 dual-token migration', async () => { const sourceChainId = 8453; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 100; const tickLower = 193130; const tickUpper = 203450; await moduleMocker.mock('../src/actions/getAerodromePosition.ts', () => ({ getAerodromePosition: mock(() => { const tickCurrent = 199000; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 12345n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 10_000_000, tickLower, tickUpper, }), }), feeAmount0: 20000000000000000n, feeAmount1: 10000000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 8453, tokenId: 12345n, protocol: Protocol.Aerodrome, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', tickLower: tickLower, tickUpper: tickUpper, fee: 500, tickSpacing: 10, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); test('v3 → v4 single-token and dual-token migration with pathFilter returns ordered by position value desc', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickSpacing = 10; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -199000; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 1000000, tickLower, tickUpper, }), }), feeAmount0: 100000000n, feeAmount1: 200000000000000000n, }; }), })); const params: RequestMigrationParams = { sourcePosition: { chainId: v3ChainId, tokenId: v3TokenId, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', tickLower: -1 * tickUpper, tickUpper: -1 * tickLower, fee: fee, tickSpacing: tickSpacing, hooks: '0x0000000000000000000000000000000000000000', }, path: { bridgeType: BridgeType.Across, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; const response = await client.requestMigration(params); expect(response.migrations.length).toBe(2); expect(positionValue(response.migrations[0], 1, false)).toBeGreaterThan( positionValue(response.migrations[1], 1, false) ); }); test('v3 → v3 single-token and dual-token migration with pathFilter returns ordered by position value desc', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -191000; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: '1000000000000000000000', tickLower, tickUpper, }), }), feeAmount0: 100000000n, feeAmount1: 0n, }; }), })); const params: RequestMigrationParams = { sourcePosition: { chainId: v3ChainId, tokenId: v3TokenId, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV3, token0: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', token1: '0x4200000000000000000000000000000000000006', tickLower: -1 * tickUpper, tickUpper: -1 * tickLower, fee: fee, }, path: { bridgeType: BridgeType.Across, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; const response = await client.requestMigration(params); expect(response.migrations.length).toBe(2); expect(positionValue(response.migrations[0], 1, false)).toBeGreaterThan( positionValue(response.migrations[1], 1, false) ); }); test('generate valid mainnet v3 → unichain v4 dual-token migration', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickSpacing = 10; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -199000; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 963499n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 1000000, tickLower, tickUpper, }), }), feeAmount0: 1000000n, feeAmount1: 200000000000000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: v3ChainId, tokenId: v3TokenId, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', tickLower: -1 * tickUpper, tickUpper: -1 * tickLower, fee: fee, tickSpacing: tickSpacing, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); test('generate valid mainnet v3 → unichain v3 single-token migration', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -199000; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 963499n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 10000, tickLower, tickUpper, }), }), feeAmount0: 1000000n, feeAmount1: 2000000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: v3ChainId, tokenId: v3TokenId, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV3, token0: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', token1: '0x4200000000000000000000000000000000000006', tickLower: tickLower, tickUpper: tickUpper, fee: fee, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); test('generate valid mainnet v3 → unichain v3 dual-token migration', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -199000; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 963499n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 1000000, tickLower, tickUpper, }), }), feeAmount0: 1000000n, feeAmount1: 200000000000000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: v3ChainId, tokenId: 963499n, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV3, token0: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', token1: '0x4200000000000000000000000000000000000006', tickLower: tickLower, tickUpper: tickUpper, fee: fee, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); test('generate valid base v3 → arbitrum v4 dual-token migration with (w)eth as token0', async () => { const sourceChainId = 8453; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -195000; const liquidity = 100_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 2825070n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 100_000_000_000, tickLower, tickUpper, }), }), feeAmount0: 10000000000000000n, feeAmount1: 2000000000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 8453, tokenId: 2825070n, protocol: Protocol.UniswapV3, }, destination: { chainId: 42161, protocol: Protocol.UniswapV4, token0: '0x0000000000000000000000000000000000000000', token1: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', tickLower: -201230, tickUpper: -187780, fee: 500, tickSpacing: 10, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); }); describe('in-range v4→ migrations', () => { let v4ChainId: number; let v4TokenId: bigint; let v4Response: PositionWithFees; let v4Pool: V4Pool; beforeAll(async () => { v4ChainId = 130; v4TokenId = 5000n; // https://app.uniswap.org/positions/v4/unichain/5000 v4Response = await client.getV4Position({ chainId: v4ChainId, tokenId: v4TokenId, }); v4Pool = v4Response.pool as unknown as V4Pool; }); test('generate valid unichain v4 → base v3 single-token migration', async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: v4ChainId, tokenId: v4TokenId, protocol: Protocol.UniswapV4, }, destination: { chainId: 8453, protocol: Protocol.UniswapV3, token0: client.chainConfigs[8453].wethAddress, token1: client.chainConfigs[8453].usdcAddress, tickLower: v4Response.tickLower, tickUpper: v4Response.tickUpper, fee: v4Pool.fee, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); test('generate valid unichain v4 → base v3 dual-token migration with senderShareBps set', async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: v4ChainId, tokenId: v4TokenId, protocol: Protocol.UniswapV4, }, destination: { chainId: 8453, protocol: Protocol.UniswapV3, token0: '0x4200000000000000000000000000000000000006', token1: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', tickLower: v4Response.tickLower, tickUpper: v4Response.tickUpper, fee: v4Pool.fee, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, senderShareBps: 7, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); test('reject unichain v4 → base v4 single-token migration with high slippage on destination swap', async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: v4ChainId, tokenId: v4TokenId, protocol: Protocol.UniswapV4, }, destination: { chainId: 8453, protocol: Protocol.UniswapV4, token0: zeroAddress, token1: client.chainConfigs[8453].usdcAddress, tickLower: v4Response.tickLower, tickUpper: v4Response.tickUpper, fee: 10000, tickSpacing: 200, hooks: zeroAddress, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: 2, }, }; await moduleMocker.mock('../src/actions/getV4CombinedQuote', () => ({ getV4CombinedQuote: mock(() => { return Promise.resolve({ amountOut: 100n, sqrtPriceX96After: 10000000000n }); }), })); expect(async () => await client.requestExactMigration(params)).toThrow('Price impact exceeds slippage'); }); test('generate valid unichain v4 → base v4 dual-token migration with senderShareBps set', async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: v4ChainId, tokenId: v4TokenId, protocol: Protocol.UniswapV4, }, destination: { chainId: 8453, protocol: Protocol.UniswapV4, token0: zeroAddress, token1: client.chainConfigs[8453].usdcAddress, tickLower: v4Response.tickLower, tickUpper: v4Response.tickUpper, fee: v4Pool.fee, tickSpacing: v4Pool.tickSpacing, hooks: zeroAddress, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, senderShareBps: 35, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); }); describe('flipped token order between chains', () => { test('generate valid base v4 → unichain v3 dual-token migration', async () => { const sourceChainId = 8453; await moduleMocker.mock('../src/actions/getV4Position.ts', () => ({ getV4Position: mock(() => { const tickCurrent = -200000; const liquidity = 1_000_000_000_000n; const pool = new V4Pool( new Token(sourceChainId, zeroAddress, 18, 'ETH'), new Token(sourceChainId, client.chainConfigs[sourceChainId].usdcAddress, 6, 'USDC'), 500, 10, '0x0000000000000000000000000000000000000000', BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 17447n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V4Position({ pool, liquidity: liquidity.toString(), tickLower: -250000, tickUpper: -109900, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: sourceChainId, tokenId: 17447n, protocol: Protocol.UniswapV4, }, destination: { chainId: 130, protocol: Protocol.UniswapV3, token0: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', token1: '0x4200000000000000000000000000000000000006', tickLower: 201320, tickUpper: 201870, fee: 500, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); test('generate valid arbitrum v4 → unichain v4 dual-token migration', async () => { const params: RequestExactMigrationParams = { sourcePosition: { chainId: 42161, tokenId: 4n, protocol: Protocol.UniswapV4, }, destination: { chainId: 10, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x68f180fcCe6836688e9084f035309E29Bf0A2095', tickLower: -887220, tickUpper: 887220, fee: 3000, hooks: '0x0000000000000000000000000000000000000000', tickSpacing: 60, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); }); describe('out of range v3→ migrations', () => { let v3ChainId: number; let v3TokenId: bigint; beforeAll(async () => { v3ChainId = 1; v3TokenId = 893202n; }); describe('single token', () => { describe('current price below requested range', () => { test('generate valid mainnet v3 → unichain v4 migration', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 3000; const tickSpacing = 60; const tickLower = -276000; const tickUpper = -267000; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -300000; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), '100000000000000', tickCurrent ); return { owner: ownerAddress, tokenId: 893202n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: '0', tickLower, tickUpper, }), }), feeAmount0: 0n, feeAmount1: 5000000000000000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: v3ChainId, tokenId: v3TokenId, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x927B51f251480a681271180DA4de28D44EC4AfB8', tickLower: -1 * tickUpper, tickUpper: -1 * tickLower, fee: fee, tickSpacing: tickSpacing, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); }); describe('current price above requested range', () => { test('generate valid mainnet v3 → unichain v4 migration', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 3000; const tickSpacing = 60; const tickLower = -306000; // Position range that would map to destination -300000 to -289980 const tickUpper = -295800; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -280000; // Current price above range, so position has only token0 const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), '1000000000000', tickCurrent ); return { owner: ownerAddress, tokenId: 893202n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: '1000000000000', tickLower, tickUpper, }), }), feeAmount0: 5000000n, // Only token0 fees since position is out of range feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: v3ChainId, tokenId: v3TokenId, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x927B51f251480a681271180DA4de28D44EC4AfB8', tickLower: -300000, tickUpper: -289980, fee: fee, tickSpacing: tickSpacing, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); }); }); describe('dual token', () => { test('mainnet v3 → unichain v4 migration throws unsupported token address', async () => { const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 3000; const tickSpacing = 60; const tickLower = -306000; const tickUpper = -295800; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -300900; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), '1000000000000', tickCurrent ); return { owner: ownerAddress, tokenId: 893202n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: '1000000000000', tickLower, tickUpper, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: v3ChainId, tokenId: v3TokenId, protocol: Protocol.UniswapV3, }, destination: { chainId: 130, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x927B51f251480a681271180DA4de28D44EC4AfB8', tickLower: -1 * tickUpper, tickUpper: -1 * tickLower, fee: fee, tickSpacing: tickSpacing, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; try { validateMigrationResponse(params, await client.requestExactMigration(params)); } catch (e) { expect(e.message).toContain('Unsupported token address on given destination chain'); } }); }); }); describe('out of range v4→ migrations', () => { let v4ChainId: number; let v4TokenId: bigint; let v4Response: PositionWithFees; let v4Pool: V4Pool; beforeAll(async () => { v4ChainId = 130; v4TokenId = 64594n; v4Response = await client.getV4Position({ chainId: v4ChainId, tokenId: v4TokenId, }); v4Pool = v4Response.pool as unknown as V4Pool; }); describe('single token', () => { describe('current price below requested range', () => { test('generate valid unichain v4 → base v4 migration', async () => { const sourceChainId = 130; // const token0 = NATIVE_ETH_ADDRESS; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickSpacing = 10; const hooks = '0x0000000000000000000000000000000000000000'; await moduleMocker.mock('../src/actions/getV4Position.ts', () => ({ getV4Position: mock(() => { const tickCurrent = 10; const liquidity = 1_000_000_000n; const pool = new V4Pool( Ether.onChain(sourceChainId), new Token(sourceChainId, token1, 18, 'USDC'), fee, tickSpacing, hooks, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: v4TokenId, ...toSDKPosition({ chainConfig: client.chainConfigs[130], position: new V4Position({ pool, liquidity: '1000000000000000000', tickLower: 50, tickUpper: 100, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: sourceChainId, tokenId: v4TokenId, protocol: Protocol.UniswapV4, }, destination: { chainId: 8453, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', tickLower: -199230, tickUpper: -197230, fee: v4Pool.fee, tickSpacing: v4Pool.tickSpacing, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); }); describe('current price above requested range', () => { test('generate valid unichain v4 → base v4 migration', async () => { const sourceChainId = 130; // const token0 = NATIVE_ETH_ADDRESS; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickSpacing = 10; const hooks = '0x0000000000000000000000000000000000000000'; await moduleMocker.mock('../src/actions/getV4Position.ts', () => ({ getV4Position: mock(() => { const tickCurrent = 100; const liquidity = 1_000_000_000n; const pool = new V4Pool( Ether.onChain(sourceChainId), new Token(sourceChainId, token1, 18, 'USDC'), fee, tickSpacing, hooks, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: v4TokenId, ...toSDKPosition({ chainConfig: client.chainConfigs[130], position: new V4Position({ pool, liquidity: 1_000_000_000_000, tickLower: 10, tickUpper: 50, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 130, tokenId: v4TokenId, protocol: Protocol.UniswapV4, }, destination: { chainId: 8453, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', tickLower: -206230, tickUpper: -202230, fee: v4Pool.fee, tickSpacing: v4Pool.tickSpacing, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; validateMigrationResponse(params, await client.requestExactMigration(params)); }); }); }); describe('dual token', () => { test('mainnet v4 → unichain v4 migration throws unsupported token address', async () => { const sourceChainId = 130; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickSpacing = 10; const hooks = '0x0000000000000000000000000000000000000000'; await moduleMocker.mock('../src/actions/getV4Position.ts', () => ({ getV4Position: mock(() => { const tickCurrent = 100; const liquidity = 1_000_000_000n; const pool = new V4Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 18, 'USDC'), fee, tickSpacing, hooks, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, ...toSDKPosition({ chainConfig: client.chainConfigs[130], position: new V4Position({ pool, liquidity: 1_000_000_000, tickLower: 10, tickUpper: 500, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 130, tokenId: v4TokenId, protocol: Protocol.UniswapV4, }, destination: { chainId: 8453, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0x078D782b760474a361dDA0AF3839290b0EF57AD6', tickLower: -206230, tickUpper: -202230, fee: v4Pool.fee, tickSpacing: v4Pool.tickSpacing, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; expect(async () => await client.requestExactMigration(params)).toThrow( 'Unsupported token address on given destination chain' ); }); }); }); describe('Direct Transfer bridging', () => { describe('same-chain v3 → v3 migration', () => { test('generate valid base v3 → base v3 single-token migration with Direct bridge', async () => { const sourceChainId = 8453; const destChainId = 8453; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -195000; const liquidity = 100_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 2825070n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 100_000_000_000, tickLower, tickUpper, }), }), feeAmount0: 10000000000000000n, feeAmount1: 2000000000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: sourceChainId, tokenId: 2825070n, protocol: Protocol.UniswapV3, }, destination: { chainId: destChainId, protocol: Protocol.UniswapV3, token0: token0, token1: token1, tickLower: 87670, tickUpper: 298660, fee, }, exactPath: { bridgeType: BridgeType.Direct, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, debug: true, }; const result = await client.requestExactMigration(params); validateMigrationResponse(params, result); // Verify Direct Transfer specific properties expect(result.migration.exactPath.bridgeType).toBe(BridgeType.Direct); expect(result.migration.settlerExecutionParams?.[0]?.functionName).toBe('handleDirectTransfer'); expect(result.migration.routes[0]).not.toHaveProperty('fillDeadlineOffset'); expect(result.migration.routes[0]).not.toHaveProperty('exclusivityDeadline'); expect(result.migration.routes[0]).not.toHaveProperty('exclusiveRelayer'); }); test('generate valid base v3 → base v3 dual-token migration with Direct bridge', async () => { const sourceChainId = 8453; const destChainId = 8453; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -195000; const liquidity = 100_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 2825070n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 100_000_000_000, tickLower, tickUpper, }), }), feeAmount0: 10000000000000000n, feeAmount1: 2000000000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: sourceChainId, tokenId: 2825070n, protocol: Protocol.UniswapV3, }, destination: { chainId: destChainId, protocol: Protocol.UniswapV3, token0: token0, token1: token1, tickLower: 87670, tickUpper: 298660, fee, }, exactPath: { bridgeType: BridgeType.Direct, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, debug: true, }; const result = await client.requestExactMigration(params); validateMigrationResponse(params, result); // Verify Direct Transfer specific properties expect(result.migration.exactPath.bridgeType).toBe(BridgeType.Direct); expect(result.migration.settlerExecutionParams?.[0]?.functionName).toBe('handleDirectTransfer'); expect(result.migration.settlerExecutionParams?.[1]?.functionName).toBe('handleDirectTransfer'); expect(result.migration.routes.length).toBe(2); result.migration.routes.forEach((route) => { expect(route).not.toHaveProperty('fillDeadlineOffset'); expect(route).not.toHaveProperty('exclusivityDeadline'); expect(route).not.toHaveProperty('exclusiveRelayer'); }); }); test('reject cross-chain migration with Direct bridge', async () => { const sourceChainId = 8453; const destChainId = 130; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickLower = -203450; const tickUpper = -193130; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -195000; const liquidity = 100_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 2825070n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 100_000_000_000, tickLower, tickUpper, }), }), feeAmount0: 10000000000000000n, feeAmount1: 2000000000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: sourceChainId, tokenId: 2825070n, protocol: Protocol.UniswapV3, }, destination: { chainId: destChainId, protocol: Protocol.UniswapV3, token0: token0, token1: token1, tickLower: 87670, tickUpper: 298660, fee, }, exactPath: { bridgeType: BridgeType.Direct, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, debug: true, }; await expect(client.requestExactMigration(params)).rejects.toThrow( 'DirectTransfer bridge only supports same-chain migrations' ); }); }); describe('same-chain v4 → v4 migration', () => { test('generate valid mainnet v4 → mainnet v4 single-token migration with Direct bridge', async () => { const sourceChainId = 8453; const destChainId = 8453; // Same chain for Direct Transfer const token0 = zeroAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickSpacing = 10; const hooks = '0x0000000000000000000000000000000000000000'; const tickLower = -203450; const tickUpper = -193130; const v4TokenId = 1n; await moduleMocker.mock('../src/actions/getV4Position.ts', () => ({ getV4Position: mock(() => { const tickCurrent = -195000; const liquidity = 100_000_000_000n; const pool = new V4Pool( new Token(sourceChainId, token0, 18, 'ETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, tickSpacing, hooks, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: v4TokenId, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V4Position({ pool, liquidity: 100_000_000_000, tickLower, tickUpper, }), }), feeAmount0: 100000000000000000n, feeAmount1: 100000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: sourceChainId, tokenId: v4TokenId, protocol: Protocol.UniswapV4, }, destination: { chainId: destChainId, protocol: Protocol.UniswapV4, token0: token0, token1: token1, tickLower: 87670, tickUpper: 298660, fee, tickSpacing, hooks, }, exactPath: { bridgeType: BridgeType.Direct, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, debug: true, }; const result = await client.requestExactMigration(params); validateMigrationResponse(params, result); // Verify Direct Transfer specific properties expect(result.migration.exactPath.bridgeType).toBe(BridgeType.Direct); expect(result.migration.settlerExecutionParams?.[0]?.functionName).toBe('handleDirectTransfer'); }); test('generate valid mainnet v4 → mainnet v4 dual-token migration with Direct bridge', async () => { const sourceChainId = 8453; const destChainId = 8453; // Same chain for Direct Transfer const token0 = zeroAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickSpacing = 10; const hooks = '0x0000000000000000000000000000000000000000'; const tickLower = -203450; const tickUpper = -193130; const v4TokenId = 1n; await moduleMocker.mock('../src/actions/getV4Position.ts', () => ({ getV4Position: mock(() => { const tickCurrent = -195000; const liquidity = 100_000_000_000n; const pool = new V4Pool( new Token(sourceChainId, token0, 18, 'ETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, tickSpacing, hooks, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: v4TokenId, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V4Position({ pool, liquidity: 100_000_000_000, tickLower, tickUpper, }), }), feeAmount0: 100000000000000000n, feeAmount1: 100000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: sourceChainId, tokenId: v4TokenId, protocol: Protocol.UniswapV4, }, destination: { chainId: destChainId, protocol: Protocol.UniswapV4, token0: token0, token1: token1, tickLower: 87670, tickUpper: 298660, fee, tickSpacing, hooks, }, exactPath: { bridgeType: BridgeType.Direct, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, debug: true, }; const result = await client.requestExactMigration(params); validateMigrationResponse(params, result); // Verify Direct Transfer specific properties expect(result.migration.exactPath.bridgeType).toBe(BridgeType.Direct); expect(result.migration.settlerExecutionParams?.[0]?.functionName).toBe('handleDirectTransfer'); expect(result.migration.settlerExecutionParams?.[1]?.functionName).toBe('handleDirectTransfer'); }); }); describe('same-chain v3 → v4 migration', () => { test('generate valid base v3 → base v4 single-token migration with Direct bridge', async () => { const sourceChainId = 8453; const destChainId = 8453; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickLower = -203450; const tickUpper = -193130; const tickSpacing = 10; const hooks = '0x0000000000000000000000000000000000000000'; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -195000; const liquidity = 100_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 2825070n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 100_000_000_000, tickLower, tickUpper, }), }), feeAmount0: 10000000000000000n, feeAmount1: 2000000000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: sourceChainId, tokenId: 2825070n, protocol: Protocol.UniswapV3, }, destination: { chainId: destChainId, protocol: Protocol.UniswapV4, token0: token0, token1: token1, tickLower: 87670, tickUpper: 298660, fee, tickSpacing, hooks, }, exactPath: { bridgeType: BridgeType.Direct, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, debug: true, }; const result = await client.requestExactMigration(params); validateMigrationResponse(params, result); // Verify Direct Transfer specific properties expect(result.migration.exactPath.bridgeType).toBe(BridgeType.Direct); expect(result.migration.settlerExecutionParams?.[0]?.functionName).toBe('handleDirectTransfer'); }); test('generate valid base v3 → base v4 dual-token migration with Direct bridge', async () => { const sourceChainId = 8453; const destChainId = 8453; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; const tickLower = -203450; const tickUpper = -193130; const tickSpacing = 10; const hooks = '0x0000000000000000000000000000000000000000'; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -195000; const liquidity = 100_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 2825070n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 100_000_000_000, tickLower, tickUpper, }), }), feeAmount0: 10000000000000000n, feeAmount1: 2000000000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: sourceChainId, tokenId: 2825070n, protocol: Protocol.UniswapV3, }, destination: { chainId: destChainId, protocol: Protocol.UniswapV4, token0: token0, token1: token1, tickLower: 87670, tickUpper: 298660, fee, tickSpacing, hooks, }, exactPath: { bridgeType: BridgeType.Direct, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, debug: true, }; const result = await client.requestExactMigration(params); validateMigrationResponse(params, result); // Verify Direct Transfer specific properties expect(result.migration.exactPath.bridgeType).toBe(BridgeType.Direct); expect(result.migration.settlerExecutionParams?.[0]?.functionName).toBe('handleDirectTransfer'); expect(result.migration.settlerExecutionParams?.[1]?.functionName).toBe('handleDirectTransfer'); }); }); }); describe('pool creation:', () => { describe('v4 settler ', () => { const mockNoV4Pool = async (): Promise => { await moduleMocker.mock('../src/actions/getV4Pool.ts', () => ({ fetchRawV4PoolData: mock(async () => { return [ { result: [0n, 0, 0, 0], status: 'success' }, { result: 0n, status: 'success' }, ]; }), })); }; test('does not create pool if no sqrtPriceX96 provided', async () => { await mockNoV4Pool(); const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = 191728; const liquidity = 2751742179046000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 891583n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: '2751742179046', tickLower: 187670, tickUpper: 198660, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 1, tokenId: 891583n, protocol: Protocol.UniswapV3, }, destination: { chainId: 42161, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', tickLower: 187670, tickUpper: 198660, fee: 10000, tickSpacing: 200, hooks: '0x0000000000000000000000000000000000000000', }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; expect(async () => await client.requestExactMigration(params)).toThrow( 'Destination pool does not exist and no sqrtPriceX96 provided for initialization' ); }); test('single token migration does not create pool if swap needed', async () => { await mockNoV4Pool(); const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -199000; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 891583n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 10_000_000, tickLower: -203450, tickUpper: -193130, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 1, tokenId: 891583n, protocol: Protocol.UniswapV3, }, destination: { chainId: 42161, protocol: Protocol.UniswapV4, token0: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', token1: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', tickLower: -887220, tickUpper: 887220, fee: 10000, tickSpacing: 200, hooks: '0x0000000000000000000000000000000000000000', sqrtPriceX96: 736087614829673861315061733n, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; expect(async () => await client.requestExactMigration(params)).toThrow( 'No liquidity for required swap in destination pool' ); }); test('dual token migration creates pool if sqrtPriceX96 provided', async () => { await mockNoV4Pool(); const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = 191728; const liquidity = 2751742179046000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 891583n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: '2751742179046', tickLower: 187670, tickUpper: 198660, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 1, tokenId: 891583n, protocol: Protocol.UniswapV3, }, destination: { chainId: 42161, protocol: Protocol.UniswapV4, token0: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', token1: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', tickLower: 187600, tickUpper: 198600, fee: 10000, tickSpacing: 200, hooks: '0x0000000000000000000000000000000000000000', sqrtPriceX96: 5442280774602240075777764n, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; const response = await client.requestExactMigration(params); expect(response.migration.position.pool.liquidity).toBe(0n); validateMigrationResponse(params, response); }); test('single token migration creates pool if no swap is needed', async () => { mockNoV4Pool(); const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -199000; const liquidity = 10_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 891583n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 10_000, tickLower: -203450, tickUpper: -193130, }), }), feeAmount0: 1000n, feeAmount1: 2000n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 1, tokenId: 891583n, protocol: Protocol.UniswapV3, }, destination: { chainId: 42161, protocol: Protocol.UniswapV4, token0: NATIVE_ETH_ADDRESS, token1: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', tickLower: 887000, tickUpper: 887200, fee: 10000, tickSpacing: 200, hooks: '0x0000000000000000000000000000000000000000', sqrtPriceX96: 736087614829673861315061733n, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; const response = await client.requestExactMigration(params); expect(response.migration.position.pool.liquidity).toBe(0n); validateMigrationResponse(params, response); }); }); const mockNoV3Pool = async (): Promise => { await moduleMocker.mock('../src/actions/getV3Pool.ts', () => ({ fetchRawV3PoolData: mock(async () => { return [{ status: 'failure' }, { status: 'failure' }]; }), })); }; describe('v3 settler ', () => { test('does not create pool if no sqrtPriceX96 provided', async () => { await mockNoV3Pool(); const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = 191728; const liquidity = 2751742179046000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 891583n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: '2751742179046', tickLower: 187670, tickUpper: 198660, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 1, tokenId: 891583n, protocol: Protocol.UniswapV3, }, destination: { chainId: 42161, protocol: Protocol.UniswapV3, token0: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', token1: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', tickLower: -198660, tickUpper: -187670, fee: 500, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; expect(async () => await client.requestExactMigration(params)).toThrow( 'Destination pool does not exist and no sqrtPriceX96 provided for initialization' ); }); test('single token migration does not create pool if swap needed', async () => { await mockNoV3Pool(); const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = 191728; const liquidity = 2751742179046000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 891583n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: '2751742179046', tickLower: 187670, tickUpper: 198660, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 1, tokenId: 891583n, protocol: Protocol.UniswapV3, }, destination: { chainId: 42161, protocol: Protocol.UniswapV3, token0: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', token1: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', tickLower: -887220, tickUpper: 887220, fee: 500, sqrtPriceX96: 736087614829673861315061733n, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; expect(async () => await client.requestExactMigration(params)).toThrow( 'No liquidity for required swap in destination pool' ); }); test('dual token migration creates pool if sqrtPriceX96 provided', async () => { await mockNoV3Pool(); const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = 191728; const liquidity = 10_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token0, 18, 'WETH'), new Token(sourceChainId, token1, 6, 'USDC'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 891583n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: 2751742179046, tickLower: 187670, tickUpper: 198660, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 1, tokenId: 891583n, protocol: Protocol.UniswapV3, }, destination: { chainId: 42161, protocol: Protocol.UniswapV3, token0: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', token1: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', tickLower: -198660, tickUpper: -187670, fee: 500, sqrtPriceX96: 5442280774602240075777764n, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.DualToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; const response = await client.requestExactMigration(params); expect(response.migration.position.pool.liquidity).toBe(0n); validateMigrationResponse(params, response); }); test('single token migration creates pool if no swap is needed', async () => { await mockNoV3Pool(); const sourceChainId = 1; const token0 = client.chainConfigs[sourceChainId].wethAddress; const token1 = client.chainConfigs[sourceChainId].usdcAddress; const fee = 500; await moduleMocker.mock('../src/actions/getV3Position.ts', () => ({ getV3Position: mock(() => { const tickCurrent = -191000; const liquidity = 10_000_000_000_000n; const pool = new V3Pool( new Token(sourceChainId, token1, 6, 'USDC'), new Token(sourceChainId, token0, 18, 'WETH'), fee, BigInt(TickMath.getSqrtRatioAtTick(tickCurrent).toString()).toString(), liquidity.toString(), tickCurrent ); return { owner: ownerAddress, tokenId: 891583n, ...toSDKPosition({ chainConfig: client.chainConfigs[sourceChainId], position: new V3Position({ pool, liquidity: '1000000000000000000000', tickLower: -203450, tickUpper: -193130, }), }), feeAmount0: 0n, feeAmount1: 0n, }; }), })); const params: RequestExactMigrationParams = { sourcePosition: { chainId: 1, tokenId: 891583n, protocol: Protocol.UniswapV3, }, destination: { chainId: 42161, protocol: Protocol.UniswapV3, token0: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', token1: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', tickLower: 193130, tickUpper: 203450, fee: 500, sqrtPriceX96: 736087614829673861315061733n, }, exactPath: { bridgeType: BridgeType.Across, migrationMethod: MigrationMethod.SingleToken, slippageInBps: DEFAULT_SLIPPAGE_IN_BPS, }, }; const response = await client.requestExactMigration(params); expect(response.migration.position.pool.liquidity).toBe(0n); validateMigrationResponse(params, response); }); }); });