From 96ac7b44d1b18d4331963afac9e95f38c461b1c1 Mon Sep 17 00:00:00 2001 From: nicosampler Date: Fri, 31 Oct 2025 11:22:30 -0300 Subject: [PATCH 1/7] getTickInfo --- src/core/uniDevKitV4.ts | 18 +++++ src/test/utils/getTickInfo.test.ts | 126 +++++++++++++++++++++++++++++ src/types/utils/getTickInfo.ts | 13 +++ src/types/utils/index.ts | 1 + src/utils/getTickInfo.ts | 53 ++++++++++++ 5 files changed, 211 insertions(+) create mode 100644 src/test/utils/getTickInfo.test.ts create mode 100644 src/types/utils/getTickInfo.ts create mode 100644 src/utils/getTickInfo.ts diff --git a/src/core/uniDevKitV4.ts b/src/core/uniDevKitV4.ts index f6f02c5..10d9b2a 100644 --- a/src/core/uniDevKitV4.ts +++ b/src/core/uniDevKitV4.ts @@ -7,6 +7,7 @@ import { buildSwapCallData } from '@/utils/buildSwapCallData' import { getPool } from '@/utils/getPool' import { getPositionDetails } from '@/utils/getPosition' import { getQuote } from '@/utils/getQuote' +import { getTickInfo } from '@/utils/getTickInfo' import { getTokens } from '@/utils/getTokens' import { preparePermit2BatchData } from '@/utils/preparePermit2BatchData' import { preparePermit2Data } from '@/utils/preparePermit2Data' @@ -28,6 +29,7 @@ import type { import type { BuildCollectFeesCallDataArgs } from '@/types/utils/buildCollectFeesCallData' import type { BuildRemoveLiquidityCallDataArgs } from '@/types/utils/buildRemoveLiquidityCallData' import type { PoolArgs } from '@/types/utils/getPool' +import type { GetTickInfoArgs, TickInfoResponse } from '@/types/utils/getTickInfo' import type { Currency } from '@uniswap/sdk-core' import type { Pool } from '@uniswap/v4-sdk' import { type Address, createPublicClient, http, type PublicClient } from 'viem' @@ -126,6 +128,22 @@ export class UniDevKitV4 { return getQuote(args, this.instance) } + /** + * Fetches tick information for a given pool key and tick from V4 StateView. + * + * This method uses client.readContract() to call V4StateView.getTickInfo() and retrieve + * tick data including liquidity and fee growth information. It first creates Token instances + * from the pool key currencies, computes the PoolId, and then reads the tick info from the + * blockchain. + * + * @param args @type {GetTickInfoArgs} - Tick query parameters including pool key and tick index + * @returns Promise - Tick information including liquidity and fee growth data + * @throws Error if tick data cannot be fetched or contract call reverts + */ + public async getTickInfo(args: GetTickInfoArgs): Promise { + return getTickInfo(args, this.instance) + } + /** * Fetches detailed position information from the V4 PositionManager contract. * diff --git a/src/test/utils/getTickInfo.test.ts b/src/test/utils/getTickInfo.test.ts new file mode 100644 index 0000000..5784a8d --- /dev/null +++ b/src/test/utils/getTickInfo.test.ts @@ -0,0 +1,126 @@ +import { Pool } from '@uniswap/v4-sdk' +import { Token } from '@uniswap/sdk-core' +import type { Address } from 'viem' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createMockSdkInstance } from '@/test/helpers/sdkInstance' +import { getTickInfo } from '@/utils/getTickInfo' +import type { GetTickInfoArgs } from '@/types/utils/getTickInfo' + +vi.mock('@/utils/getTokens', () => ({ + getTokens: vi.fn(), +})) + +vi.mock('@uniswap/v4-sdk', async () => { + const actual = await vi.importActual('@uniswap/v4-sdk') + return { + ...actual, + Pool: { + ...actual.Pool, + getPoolId: vi.fn(), + }, + } +}) + +describe('getTickInfo', () => { + // USDC and WETH on Mainnet + const mockTokens: [Address, Address] = [ + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC + '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH + ] + + const mockTickInfoArgs: GetTickInfoArgs = { + poolKey: { + currency0: mockTokens[0], + currency1: mockTokens[1], + fee: 3000, + tickSpacing: 60, + hooks: '0x0000000000000000000000000000000000000000', + }, + tick: 0, + } + + const mockDeps = createMockSdkInstance() + + beforeEach(() => { + vi.resetAllMocks() + }) + + it('should throw error if SDK instance not found', async () => { + const mockDeps = createMockSdkInstance() + const { getTokens } = await import('@/utils/getTokens') + vi.mocked(getTokens).mockRejectedValueOnce(new Error()) + + await expect(getTickInfo(mockTickInfoArgs, mockDeps)).rejects.toThrow() + }) + + it('should complete full flow and verify function calls', async () => { + const mockTokenInstances = [ + new Token(1, mockTokens[0], 6, 'USDC', 'USD Coin'), + new Token(1, mockTokens[1], 18, 'WETH', 'Wrapped Ether'), + ] + + const mockPoolId = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + const mockTickInfoResult: [bigint, bigint, bigint, bigint] = [ + BigInt('1000000000000000000'), // liquidityGross + BigInt('500000000000000000'), // liquidityNet + BigInt('2000000000000000000'), // feeGrowthOutside0X128 + BigInt('1500000000000000000'), // feeGrowthOutside1X128 + ] + + const { getTokens } = await import('@/utils/getTokens') + vi.mocked(getTokens).mockResolvedValueOnce(mockTokenInstances) + vi.mocked(Pool.getPoolId).mockReturnValueOnce(mockPoolId as `0x${string}`) + mockDeps.client.readContract = vi.fn().mockResolvedValueOnce(mockTickInfoResult) + + const result = await getTickInfo(mockTickInfoArgs, mockDeps) + + // Verify getTokens was called with correct parameters + expect(getTokens).toHaveBeenCalledWith( + { + addresses: [mockTickInfoArgs.poolKey.currency0, mockTickInfoArgs.poolKey.currency1], + }, + mockDeps, + ) + + // Verify Pool.getPoolId was called with correct parameters + expect(Pool.getPoolId).toHaveBeenCalledWith( + mockTokenInstances[0], + mockTokenInstances[1], + mockTickInfoArgs.poolKey.fee, + mockTickInfoArgs.poolKey.tickSpacing, + mockTickInfoArgs.poolKey.hooks, + ) + + // Verify readContract was called with correct parameters + expect(mockDeps.client.readContract).toHaveBeenCalledWith({ + address: mockDeps.contracts.stateView, + abi: expect.any(Object), + functionName: 'getTickInfo', + args: [mockPoolId, mockTickInfoArgs.tick], + }) + + // Verify result structure + expect(result).toEqual({ + liquidityGross: mockTickInfoResult[0], + liquidityNet: mockTickInfoResult[1], + feeGrowthOutside0X128: mockTickInfoResult[2], + feeGrowthOutside1X128: mockTickInfoResult[3], + }) + }) + + it('should throw error if readContract fails', async () => { + const mockTokenInstances = [ + new Token(1, mockTokens[0], 6, 'USDC', 'USD Coin'), + new Token(1, mockTokens[1], 18, 'WETH', 'Wrapped Ether'), + ] + + const mockPoolId = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + + const { getTokens } = await import('@/utils/getTokens') + vi.mocked(getTokens).mockResolvedValueOnce(mockTokenInstances) + vi.mocked(Pool.getPoolId).mockReturnValueOnce(mockPoolId as `0x${string}`) + mockDeps.client.readContract = vi.fn().mockRejectedValueOnce(new Error()) + + await expect(getTickInfo(mockTickInfoArgs, mockDeps)).rejects.toThrow() + }) +}) diff --git a/src/types/utils/getTickInfo.ts b/src/types/utils/getTickInfo.ts new file mode 100644 index 0000000..64a3aa3 --- /dev/null +++ b/src/types/utils/getTickInfo.ts @@ -0,0 +1,13 @@ +import type { SwapExactInSingle as UniswapSwapExactInSingle } from '@uniswap/v4-sdk' + +export interface GetTickInfoArgs { + poolKey: UniswapSwapExactInSingle['poolKey'] + tick: number +} + +export interface TickInfoResponse { + liquidityGross: bigint + liquidityNet: bigint + feeGrowthOutside0X128: bigint + feeGrowthOutside1X128: bigint +} diff --git a/src/types/utils/index.ts b/src/types/utils/index.ts index 1cc1e6c..618cb71 100644 --- a/src/types/utils/index.ts +++ b/src/types/utils/index.ts @@ -3,3 +3,4 @@ export * from '@/types/utils/getPool' export * from '@/types/utils/getPoolKeyFromPoolId' export * from '@/types/utils/getPosition' export * from '@/types/utils/getQuote' +export * from '@/types/utils/getTickInfo' diff --git a/src/utils/getTickInfo.ts b/src/utils/getTickInfo.ts new file mode 100644 index 0000000..d52df27 --- /dev/null +++ b/src/utils/getTickInfo.ts @@ -0,0 +1,53 @@ +import { Pool } from '@uniswap/v4-sdk' +import V4StateViewAbi from '@/constants/abis/V4StateView' +import { getTokens } from '@/utils/getTokens' +import type { UniDevKitV4Instance } from '@/types/core' +import type { GetTickInfoArgs, TickInfoResponse } from '@/types/utils/getTickInfo' + +/** + * Reads tick info for a given pool key and tick from V4 StateView. + */ +export async function getTickInfo( + args: GetTickInfoArgs, + instance: UniDevKitV4Instance, +): Promise { + const { client, contracts } = instance + const { stateView } = contracts + + const { poolKey, tick } = args + + // Create Token instances for currency0 and currency1 in the provided order + const [token0, token1] = await getTokens( + { addresses: [poolKey.currency0 as `0x${string}`, poolKey.currency1 as `0x${string}`] }, + instance, + ) + + // Compute PoolId from PoolKey components + const poolId32Bytes = Pool.getPoolId( + token0, + token1, + poolKey.fee, + poolKey.tickSpacing, + poolKey.hooks as `0x${string}`, + ) as `0x${string}` + + // Read tick info + const result = await client.readContract({ + address: stateView, + abi: V4StateViewAbi, + functionName: 'getTickInfo', + args: [poolId32Bytes, tick], + }) + + // V4 StateView getTickInfo returns: + // (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128) + const [liquidityGross, liquidityNet, feeGrowthOutside0X128, feeGrowthOutside1X128] = + result as unknown as [bigint, bigint, bigint, bigint] + + return { + liquidityGross, + liquidityNet, + feeGrowthOutside0X128, + feeGrowthOutside1X128, + } +} From d03bbe77e4b2cf4797d43ffac0b5695b4d762ba7 Mon Sep 17 00:00:00 2001 From: nicosampler Date: Mon, 3 Nov 2025 16:20:58 -0300 Subject: [PATCH 2/7] rename & UT fixes --- README.md | 22 ++- src/core/uniDevKitV4.ts | 43 +++-- src/helpers/tokens.ts | 12 +- src/test/helpers/testFactories.ts | 29 +-- .../utils/buildAddLiquidityCallData.test.ts | 7 +- .../buildRemoveLiquidityCallData.test.ts | 2 +- src/test/utils/getPool.test.ts | 42 ++--- src/test/utils/getPosition.test.ts | 114 +++++++----- src/types/utils/buildAddLiquidityCallData.ts | 4 +- src/types/utils/getPosition.ts | 39 ++-- src/utils/buildAddLiquidityCallData.ts | 16 +- src/utils/buildCollectFeesCallData.ts | 2 +- src/utils/buildRemoveLiquidityCallData.ts | 2 +- src/utils/getPosition.ts | 167 ++++++++++++------ src/utils/getTickInfo.ts | 6 +- src/utils/preparePermit2BatchData.ts | 2 +- src/utils/preparePermit2Data.ts | 2 +- 17 files changed, 329 insertions(+), 182 deletions(-) diff --git a/README.md b/README.md index d65c784..7636095 100644 --- a/README.md +++ b/README.md @@ -95,14 +95,26 @@ const quote = await uniDevKit.getQuote({ // Returns { amountOut, estimatedGasUsed, timestamp } ``` -#### `getPositionDetails` -Fetches position state from the PositionManager and decodes the tick range, liquidity, and pool key. Uses multicall to batch `V4PositionManager.getPoolAndPositionInfo()` and `V4PositionManager.getPositionLiquidity()` calls, and handles data decoding. +#### `getPositionInfo` +Fetches basic position information without creating SDK instances. Returns raw position data from the blockchain including tick range, liquidity, pool key, and current pool state. Uses multicall to efficiently batch contract calls and decodes packed position data. -**Without this SDK:** Call getPoolAndPositionInfo() and getPositionLiquidity() separately, decode packed position data, extract tick bounds and pool key manually. +Use this when you only need position metadata without SDK operations. For SDK instances (Position, Pool objects), use `getPosition()` instead. + +**Without this SDK:** Call getPoolAndPositionInfo() and getPositionLiquidity() separately, decode packed position data, extract tick bounds and pool key manually, fetch slot0 and pool liquidity separately. + +```ts +const positionInfo = await uniDevKit.getPositionInfo("123"); +// Returns { tokenId, tickLower, tickUpper, liquidity, poolKey, currentTick, slot0, poolLiquidity } +``` + +#### `getPosition` +Fetches complete position data with initialized SDK instances. Returns fully usable Position and Pool objects from the Uniswap V4 SDK, ready for swaps, calculations, and other operations. Validates that the position has liquidity. + +**Without this SDK:** Do everything from `getPositionInfo()` plus create Position and Pool instances manually using the SDK constructors. ```ts -const position = await uniDevKit.getPositionDetails("123"); -// Returns { tokenId, tickLower, tickUpper, liquidity, poolKey } +const position = await uniDevKit.getPosition("123"); +// Returns { position: Position, pool: Pool, currency0, currency1, poolId, tokenId, currentTick } ``` ### Swap Operations diff --git a/src/core/uniDevKitV4.ts b/src/core/uniDevKitV4.ts index 10d9b2a..647efc9 100644 --- a/src/core/uniDevKitV4.ts +++ b/src/core/uniDevKitV4.ts @@ -5,7 +5,7 @@ import { buildCollectFeesCallData } from '@/utils/buildCollectFeesCallData' import { buildRemoveLiquidityCallData } from '@/utils/buildRemoveLiquidityCallData' import { buildSwapCallData } from '@/utils/buildSwapCallData' import { getPool } from '@/utils/getPool' -import { getPositionDetails } from '@/utils/getPosition' +import { getPosition, getPositionInfo } from '@/utils/getPosition' import { getQuote } from '@/utils/getQuote' import { getTickInfo } from '@/utils/getTickInfo' import { getTokens } from '@/utils/getTokens' @@ -17,7 +17,7 @@ import type { BuildAddLiquidityArgs, BuildAddLiquidityCallDataResult, } from '@/types/utils/buildAddLiquidityCallData' -import type { GetPositionDetailsResponse } from '@/types/utils/getPosition' +import type { GetPositionInfoResponse, GetPositionResponse } from '@/types/utils/getPosition' import type { QuoteResponse, SwapExactInSingle } from '@/types/utils/getQuote' import type { GetTokensArgs } from '@/types/utils/getTokens' import type { @@ -145,18 +145,41 @@ export class UniDevKitV4 { } /** - * Fetches detailed position information from the V4 PositionManager contract. + * Retrieves a complete Uniswap V4 position instance with pool and token information. * - * This method uses multicall to efficiently call V4PositionManager.getPoolAndPositionInfo() and - * getPositionLiquidity() in a single transaction. It retrieves the position's tick range, liquidity, - * and associated pool key, then decodes the raw position data to provide structured information. + * This method fetches position details and builds a fully initialized Position instance + * using the Uniswap V4 SDK. It includes the pool state, token metadata, position + * liquidity data, and current pool tick, providing a comprehensive view of the position. * * @param tokenId - The NFT token ID of the position - * @returns Promise - Position details including tick range, liquidity, and pool key + * @returns Promise - Complete position data including position instance, pool, tokens, pool ID, and current tick + * @throws Error if position data cannot be fetched, position doesn't exist, or liquidity is 0 + */ + public async getPosition(tokenId: string): Promise { + return getPosition(tokenId, this.instance) + } + + /** + * Retrieves basic position information without SDK instances. + * + * This method fetches raw position data from the blockchain and returns it without creating + * SDK instances. It's more efficient when you only need position metadata (tick range, liquidity, + * pool key) without requiring Position or Pool objects. Also fetches pool state (slot0 and + * liquidity) to avoid redundant calls when building full position instances. + * + * Use this method when: + * - Displaying position information in a UI + * - Checking if a position exists + * - Getting position metadata without SDK operations + * + * Use `getPosition()` instead when you need SDK instances for swaps, calculations, or other operations. + * + * @param tokenId - The NFT token ID of the position + * @returns Promise - Basic position information with pool state * @throws Error if position data cannot be fetched or position doesn't exist */ - public async getPositionDetails(tokenId: string): Promise { - return getPositionDetails(tokenId, this.instance) + public async getPositionInfo(tokenId: string): Promise { + return getPositionInfo(tokenId, this.instance) } /** @@ -213,7 +236,7 @@ export class UniDevKitV4 { * Generates V4PositionManager calldata for collecting accumulated fees from positions. * * This method uses V4PositionManager.collectCallParameters to create calldata for - * collecting fees earned by a liquidity position. It handles both token0 and token1 + * collecting fees earned by a liquidity position. It handles both currency0 and currency1 * fee collection with proper recipient addressing. No blockchain calls are made - * this is purely a calldata generation method. * diff --git a/src/helpers/tokens.ts b/src/helpers/tokens.ts index f420664..c5f5c01 100644 --- a/src/helpers/tokens.ts +++ b/src/helpers/tokens.ts @@ -1,12 +1,12 @@ /** * Sorts two tokens in a consistent order (lexicographically by address) - * @param token0 First token address - * @param token1 Second token address - * @returns Tuple of [token0, token1] in sorted order + * @param currency0 First currency/token address + * @param currency1 Second currency/token address + * @returns Tuple of [currency0, currency1] in sorted order */ export function sortTokens( - token0: `0x${string}`, - token1: `0x${string}`, + currency0: `0x${string}`, + currency1: `0x${string}`, ): [`0x${string}`, `0x${string}`] { - return token0.toLowerCase() < token1.toLowerCase() ? [token0, token1] : [token1, token0] + return currency0.toLowerCase() < currency1.toLowerCase() ? [currency0, currency1] : [currency1, currency0] } diff --git a/src/test/helpers/testFactories.ts b/src/test/helpers/testFactories.ts index aab42a4..58d66fe 100644 --- a/src/test/helpers/testFactories.ts +++ b/src/test/helpers/testFactories.ts @@ -26,10 +26,10 @@ export const TEST_ADDRESSES = { } as const // Factory functions -export const createTestPool = (token0 = USDC, token1 = WETH) => +export const createTestPool = (currency0 = USDC, currency1 = WETH) => new Pool( - token0, - token1, + currency0, + currency1, 3000, // fee 60, // tickSpacing TEST_ADDRESSES.hooks, @@ -49,11 +49,18 @@ export const createTestPosition = (pool = createTestPool()) => export const createMockPositionData = ( pool = createTestPool(), position = createTestPosition(pool), -) => ({ - position, - pool, - token0: pool.token0, - token1: pool.token1, - poolId: TEST_ADDRESSES.hooks, - tokenId: '1', -}) +) => { + // Extract currencies from pool + const currency0 = pool.currency0 + const currency1 = pool.currency1 + + return { + position, + pool, + currency0, + currency1, + poolId: TEST_ADDRESSES.hooks, + tokenId: '1', + currentTick: 0, // Mock current tick (matching the test pool's tick parameter) + } +} diff --git a/src/test/utils/buildAddLiquidityCallData.test.ts b/src/test/utils/buildAddLiquidityCallData.test.ts index 509b8cd..cf48013 100644 --- a/src/test/utils/buildAddLiquidityCallData.test.ts +++ b/src/test/utils/buildAddLiquidityCallData.test.ts @@ -369,9 +369,10 @@ describe('buildAddLiquidityCallData', () => { }) it('should call V4PositionManager.addCallParameters with native currency when pool has native token', async () => { - // Create a pool with native token (WETH as native) + // Create a pool with native token const nativePool = createTestPool() - Object.defineProperty(nativePool.token0, 'isNative', { + const currency0 = nativePool.currency0 + Object.defineProperty(currency0, 'isNative', { value: true, writable: true, }) @@ -388,7 +389,7 @@ describe('buildAddLiquidityCallData', () => { expect(mockAddCallParameters).toHaveBeenCalledTimes(1) const [, options] = mockAddCallParameters.mock.calls[0] - expect(options.useNative).toBe(nativePool.token0) + expect(options.useNative).toBe(currency0) }) it('should throw error when neither amount0 nor amount1 is provided', async () => { diff --git a/src/test/utils/buildRemoveLiquidityCallData.test.ts b/src/test/utils/buildRemoveLiquidityCallData.test.ts index 2b35a2b..af1dbd3 100644 --- a/src/test/utils/buildRemoveLiquidityCallData.test.ts +++ b/src/test/utils/buildRemoveLiquidityCallData.test.ts @@ -92,7 +92,7 @@ describe('buildRemoveLiquidityCallData', () => { const result = await buildRemoveLiquidityCallData(params, instance) // Verify getPosition was called with correct tokenId - expect(mockGetPosition).toHaveBeenCalledWith({ tokenId: MOCK_TOKEN_ID }, instance) + expect(mockGetPosition).toHaveBeenCalledWith(MOCK_TOKEN_ID, instance) // Verify getDefaultDeadline was NOT called since custom deadline was provided expect(mockGetDefaultDeadline).not.toHaveBeenCalled() diff --git a/src/test/utils/getPool.test.ts b/src/test/utils/getPool.test.ts index 79e1268..1d6752e 100644 --- a/src/test/utils/getPool.test.ts +++ b/src/test/utils/getPool.test.ts @@ -24,7 +24,7 @@ vi.mock('wagmi', () => ({ describe('getPool', () => { // USDC and WETH on Mainnet - const mockTokens: [Address, Address] = [ + const mockCurrencies: [Address, Address] = [ '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', ] @@ -36,25 +36,25 @@ describe('getPool', () => { }) it('should throw error if pool does not exist', async () => { - const mockTokenInstances = [ - new Token(1, mockTokens[0], 18, 'TOKEN0', 'Token 0'), - new Token(1, mockTokens[1], 18, 'TOKEN1', 'Token 1'), + const mockCurrencyInstances = [ + new Token(1, mockCurrencies[0], 18, 'CURRENCY0', 'Currency 0'), + new Token(1, mockCurrencies[1], 18, 'CURRENCY1', 'Currency 1'), ] const mockPoolData = [ - [mockTokens[0], mockTokens[1], FeeTier.MEDIUM, 0, zeroAddress], // poolKeys with 0 tickSpacing + [mockCurrencies[0], mockCurrencies[1], FeeTier.MEDIUM, 0, zeroAddress], // poolKeys with 0 tickSpacing null, // slot0 null, // liquidity ] - mockGetTokens.mockResolvedValueOnce(mockTokenInstances) + mockGetTokens.mockResolvedValueOnce(mockCurrencyInstances) vi.mocked(mockDeps.client.multicall).mockResolvedValueOnce(mockPoolData) await expect( getPool( { - currencyA: mockTokens[0], - currencyB: mockTokens[1], + currencyA: mockCurrencies[0], + currencyB: mockCurrencies[1], fee: FeeTier.MEDIUM, }, mockDeps, @@ -63,9 +63,9 @@ describe('getPool', () => { }) it('should return pool when it exists', async () => { - const mockTokenInstances = [ - new Token(1, mockTokens[0], 6, 'USDC', 'USD Coin'), - new Token(1, mockTokens[1], 18, 'WETH', 'Wrapped Ether'), + const mockCurrencyInstances = [ + new Token(1, mockCurrencies[0], 6, 'USDC', 'USD Coin'), + new Token(1, mockCurrencies[1], 18, 'WETH', 'Wrapped Ether'), ] // Mock the multicall response with the correct structure @@ -74,13 +74,13 @@ describe('getPool', () => { const mockPoolData = [mockSlot0Data, mockLiquidityData] - mockGetTokens.mockResolvedValueOnce(mockTokenInstances) + mockGetTokens.mockResolvedValueOnce(mockCurrencyInstances) vi.mocked(mockDeps.client.multicall).mockResolvedValueOnce(mockPoolData) const result = await getPool( { - currencyA: mockTokens[0], - currencyB: mockTokens[1], + currencyA: mockCurrencies[0], + currencyB: mockCurrencies[1], fee: FeeTier.MEDIUM, }, mockDeps, @@ -91,25 +91,25 @@ describe('getPool', () => { }) it('should throw error if pool creation fails', async () => { - const mockTokenInstances = [ - new Token(1, mockTokens[0], 18, 'TOKEN0', 'Token 0'), - new Token(1, mockTokens[1], 18, 'TOKEN1', 'Token 1'), + const mockCurrencyInstances = [ + new Token(1, mockCurrencies[0], 18, 'CURRENCY0', 'Currency 0'), + new Token(1, mockCurrencies[1], 18, 'CURRENCY1', 'Currency 1'), ] const mockPoolData = [ - [mockTokens[0], mockTokens[1], FeeTier.MEDIUM, 60, zeroAddress], + [mockCurrencies[0], mockCurrencies[1], FeeTier.MEDIUM, 60, zeroAddress], ['invalid', 0, 0, 0, 0, 0], // invalid sqrtPriceX96 '1000000000000000000', ] - mockGetTokens.mockResolvedValueOnce(mockTokenInstances) + mockGetTokens.mockResolvedValueOnce(mockCurrencyInstances) vi.mocked(mockDeps.client.multicall).mockResolvedValueOnce(mockPoolData) await expect( getPool( { - currencyA: mockTokens[0], - currencyB: mockTokens[1], + currencyA: mockCurrencies[0], + currencyB: mockCurrencies[1], fee: FeeTier.MEDIUM, }, mockDeps, diff --git a/src/test/utils/getPosition.test.ts b/src/test/utils/getPosition.test.ts index fc581e2..16ad49c 100644 --- a/src/test/utils/getPosition.test.ts +++ b/src/test/utils/getPosition.test.ts @@ -15,7 +15,7 @@ vi.mock('@/helpers/positions', () => ({ describe('getPosition', () => { // USDC and WETH on Mainnet - const mockTokens: [Address, Address] = [ + const mockCurrencies: [Address, Address] = [ '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH ] @@ -29,57 +29,82 @@ describe('getPosition', () => { const mockDeps = createMockSdkInstance() mockDeps.client.multicall = vi.fn().mockRejectedValueOnce(new Error('SDK not initialized')) - await expect(getPosition({ tokenId: '1' }, mockDeps)).rejects.toThrow('SDK not initialized') + await expect(getPosition('1', mockDeps)).rejects.toThrow('SDK not initialized') }) it('should throw error if tokens not found', async () => { const mockDeps = createMockSdkInstance() - mockDeps.client.multicall = vi.fn().mockResolvedValueOnce([ - [ - { - currency0: '0x123', - currency1: '0x456', - fee: 3000, - tickSpacing: 60, - hooks: validHooks, - }, - {}, - ], - 1000000n, - ]) - - await expect(getPosition({ tokenId: '1' }, mockDeps)).rejects.toThrow('Tokens not found') + // First multicall: position info + mockDeps.client.multicall = vi + .fn() + .mockResolvedValueOnce([ + [ + { + currency0: '0x123', + currency1: '0x456', + fee: 3000, + tickSpacing: 60, + hooks: validHooks, + }, + {}, + ], + 1000000n, + ]) + // Second multicall: slot0 and poolLiquidity + .mockResolvedValueOnce([[79228162514264337593543950336n, 0], 1000000n]) + + const { getTokens } = await import('@/utils/getTokens') + vi.mocked(getTokens).mockResolvedValueOnce([]) + + await expect(getPosition('1', mockDeps)).rejects.toThrow('Failed to fetch token instances') }) it('should throw error if liquidity is 0', async () => { const mockDeps = createMockSdkInstance() - mockDeps.client.multicall = vi.fn().mockResolvedValueOnce([ - [ - { - currency0: '0x123', - currency1: '0x456', - fee: 3000, - tickSpacing: 60, - hooks: validHooks, - }, - {}, - ], - 0n, - ]) - - await expect(getPosition({ tokenId: '1' }, mockDeps)).rejects.toThrow('Liquidity is 0') + // Use valid addresses for tokens + const testCurrency0 = '0x1234567890123456789012345678901234567890' + const testCurrency1 = '0x0987654321098765432109876543210987654321' + + // First multicall: position info (from getPositionInfo) + mockDeps.client.multicall = vi + .fn() + .mockResolvedValueOnce([ + [ + { + currency0: testCurrency0, + currency1: testCurrency1, + fee: 3000, + tickSpacing: 60, + hooks: validHooks, + }, + {}, + ], + 0n, + ]) + // Second multicall: slot0 and poolLiquidity (from getPositionInfo) + .mockResolvedValueOnce([[79228162514264337593543950336n, 0], 1000000n]) + + // Mock getTokens - called twice: once in getPositionInfo, once in getPosition + const mockCurrency0 = new Token(1, testCurrency0, 6, 'USDC', 'USD Coin') + const mockCurrency1 = new Token(1, testCurrency1, 18, 'WETH', 'Wrapped Ether') + const { getTokens } = await import('@/utils/getTokens') + vi.mocked(getTokens) + .mockResolvedValueOnce([mockCurrency0, mockCurrency1]) // First call in getPositionInfo + .mockResolvedValueOnce([mockCurrency0, mockCurrency1]) // Second call in getPosition + + await expect(getPosition('1', mockDeps)).rejects.toThrow('Position has no liquidity') }) it('should return position data when position exists', async () => { const mockDeps = createMockSdkInstance() - // Primer multicall: [poolAndPositionInfo, liquidity] + // First multicall: position info [poolAndPositionInfo, liquidity] (from getPositionInfo) mockDeps.client.multicall = vi .fn() .mockResolvedValueOnce([ [ { - currency0: mockTokens[0], - currency1: mockTokens[1], + currency0: mockCurrencies[0], + currency1: mockCurrencies[1], fee: 3000, tickSpacing: 60, hooks: validHooks, @@ -88,22 +113,25 @@ describe('getPosition', () => { ], 1000000n, ]) - // Segundo multicall: [slot0, poolLiquidity] + // Second multicall: pool state [slot0, poolLiquidity] (from getPositionInfo) .mockResolvedValueOnce([[79228162514264337593543950336n, 0], 1000000n]) - // Mock getTokens para devolver instancias reales de Token - const mockToken0 = new Token(1, mockTokens[0], 6, 'USDC', 'USD Coin') - const mockToken1 = new Token(1, mockTokens[1], 18, 'WETH', 'Wrapped Ether') + // Mock getTokens - called twice: once in getPositionInfo, once in getPosition + const mockCurrency0 = new Token(1, mockCurrencies[0], 6, 'USDC', 'USD Coin') + const mockCurrency1 = new Token(1, mockCurrencies[1], 18, 'WETH', 'Wrapped Ether') const { getTokens } = await import('@/utils/getTokens') - vi.mocked(getTokens).mockResolvedValueOnce([mockToken0, mockToken1]) + vi.mocked(getTokens) + .mockResolvedValueOnce([mockCurrency0, mockCurrency1]) // First call in getPositionInfo + .mockResolvedValueOnce([mockCurrency0, mockCurrency1]) // Second call in getPosition - const result = await getPosition({ tokenId: '1' }, mockDeps) + const result = await getPosition('1', mockDeps) expect(result).toHaveProperty('position') expect(result).toHaveProperty('pool') - expect(result).toHaveProperty('token0') - expect(result).toHaveProperty('token1') + expect(result).toHaveProperty('currency0') + expect(result).toHaveProperty('currency1') expect(result).toHaveProperty('poolId') expect(result).toHaveProperty('tokenId', '1') + expect(result).toHaveProperty('currentTick') }) }) diff --git a/src/types/utils/buildAddLiquidityCallData.ts b/src/types/utils/buildAddLiquidityCallData.ts index 5261b92..45dacd8 100644 --- a/src/types/utils/buildAddLiquidityCallData.ts +++ b/src/types/utils/buildAddLiquidityCallData.ts @@ -10,12 +10,12 @@ type BaseAddLiquidityArgs = { pool: Pool /** - * Amount of token0 to add. + * Amount of currency0 to add. */ amount0?: string /** - * Amount of token1 to add. + * Amount of currency1 to add. */ amount1?: string diff --git a/src/types/utils/getPosition.ts b/src/types/utils/getPosition.ts index 24f7c7d..c1e4ac7 100644 --- a/src/types/utils/getPosition.ts +++ b/src/types/utils/getPosition.ts @@ -4,36 +4,53 @@ import type { Pool, PoolKey, Position } from '@uniswap/v4-sdk' /** * Parameters required for retrieving a Uniswap V4 position instance. */ -export interface GetPositionParams { - /** The unique identifier of the position */ - tokenId: string -} /** - * Response structure for retrieving a Uniswap V4 position instance. + * Basic position information without SDK instances. + * Returns raw position data from the blockchain. */ -export interface GetPositionDetailsResponse { +export interface GetPositionInfoResponse { + /** The unique identifier of the position */ tokenId: string + /** Lower tick boundary of the position */ tickLower: number + /** Upper tick boundary of the position */ tickUpper: number + /** Current liquidity amount in the position */ liquidity: bigint + /** Pool configuration (currencies, fee, tick spacing, hooks) */ poolKey: PoolKey + /** Current price tick of the pool */ + currentTick: number + /** Slot0 data from the pool (sqrtPriceX96, tick, protocolFee, lpFee) */ + slot0: readonly [bigint, number, number, number] + /** Current total liquidity in the pool */ + poolLiquidity: bigint + /** The unique identifier of the pool */ + poolId: `0x${string}` + /** The first token in the pool pair */ + currency0: Currency + /** The second token in the pool pair */ + currency1: Currency } /** - * Response structure for retrieving a Uniswap V4 position instance. + * Complete position data with initialized SDK instances. + * Returns fully usable Position and Pool objects from the Uniswap V4 SDK. */ export interface GetPositionResponse { - /** The position instance */ + /** The position instance from Uniswap V4 SDK */ position: Position - /** The pool instance associated with the position */ + /** The pool instance from Uniswap V4 SDK with current state */ pool: Pool /** The first token in the pool pair */ - token0: Currency + currency0: Currency /** The second token in the pool pair */ - token1: Currency + currency1: Currency /** The unique identifier of the pool */ poolId: `0x${string}` /** The unique identifier of the position */ tokenId: string + /** The current price tick of the pool */ + currentTick: number } diff --git a/src/utils/buildAddLiquidityCallData.ts b/src/utils/buildAddLiquidityCallData.ts index 92d6bc5..515d080 100644 --- a/src/utils/buildAddLiquidityCallData.ts +++ b/src/utils/buildAddLiquidityCallData.ts @@ -26,8 +26,8 @@ import type { * both `amount0` and `amount1` are required in order to compute the initial price * (`sqrtPriceX96`) using `encodeSqrtRatioX96(amount1, amount0)`. * - * - The amounts must be matching the pool's token0 and token1. - * - The amounts must be in the same decimals as the pool's token0 and token1. + * - The amounts must be matching the pool's currency0 and currency1. + * - The amounts must be in the same decimals as the pool's currency0 and currency1. * * The function also supports optional parameters for tick range, slippage tolerance, * deadline, and Permit2 batch signature for token approvals. @@ -128,11 +128,13 @@ export async function buildAddLiquidityCallData( throw new Error('Invalid input: at least one of amount0 or amount1 must be defined.') } - // Get native currency - const nativeCurrency = pool.token0.isNative - ? pool.token0 - : pool.token1.isNative - ? pool.token1 + // Get native currency from pool currencies + const currency0 = pool.currency0 + const currency1 = pool.currency1 + const nativeCurrency = currency0.isNative + ? currency0 + : currency1.isNative + ? currency1 : undefined // Build calldata diff --git a/src/utils/buildCollectFeesCallData.ts b/src/utils/buildCollectFeesCallData.ts index 6760f8b..c0e58c2 100644 --- a/src/utils/buildCollectFeesCallData.ts +++ b/src/utils/buildCollectFeesCallData.ts @@ -32,7 +32,7 @@ export async function buildCollectFeesCallData( { tokenId, recipient, deadline: deadlineParam }: BuildCollectFeesCallDataArgs, instance: UniDevKitV4Instance, ) { - const positionData = await getPosition({ tokenId }, instance) + const positionData = await getPosition(tokenId, instance) if (!positionData) { throw new Error('Position not found') } diff --git a/src/utils/buildRemoveLiquidityCallData.ts b/src/utils/buildRemoveLiquidityCallData.ts index 81b32e5..f48d32a 100644 --- a/src/utils/buildRemoveLiquidityCallData.ts +++ b/src/utils/buildRemoveLiquidityCallData.ts @@ -36,7 +36,7 @@ export async function buildRemoveLiquidityCallData( instance: UniDevKitV4Instance, ) { // Get position data - const positionData = await getPosition({ tokenId }, instance) + const positionData = await getPosition(tokenId, instance) if (!positionData) { throw new Error('Position not found') } diff --git a/src/utils/getPosition.ts b/src/utils/getPosition.ts index bc494b5..6451930 100644 --- a/src/utils/getPosition.ts +++ b/src/utils/getPosition.ts @@ -3,76 +3,66 @@ import V4PositionManagerAbi from '@/constants/abis/V4PositionMananger' import V4StateViewAbi from '@/constants/abis/V4StateView' import { decodePositionInfo } from '@/helpers/positions' import type { UniDevKitV4Instance } from '@/types/core' -import type { - GetPositionDetailsResponse, - GetPositionParams, - GetPositionResponse, -} from '@/types/utils/getPosition' +import type { GetPositionInfoResponse, GetPositionResponse } from '@/types/utils/getPosition' import { getTokens } from '@/utils/getTokens' /** - * Retrieves a Uniswap V4 position instance for a given token ID. + * Retrieves a complete Uniswap V4 position with initialized SDK instances. + * + * This method fetches position information and creates fully initialized Position and Pool + * instances from the Uniswap V4 SDK. It validates that the position has liquidity and returns + * objects ready for use in swaps, calculations, and other SDK operations. + * * @param params Position parameters including token ID * @param instance UniDevKitV4Instance - * @returns Promise resolving to position data - * @throws Error if SDK instance is not found or if position data is invalid + * @returns Promise - Complete position with SDK instances + * @throws Error if position data cannot be fetched, position doesn't exist, or liquidity is 0 */ export async function getPosition( - params: GetPositionParams, + tokenId: string, instance: UniDevKitV4Instance, ): Promise { - const { client, contracts } = instance - - const { stateView } = contracts - - // Get position details using the dedicated function - const positionDetails = await getPositionDetails(params.tokenId, instance) + // Get position info (includes slot0 and poolLiquidity to avoid redundant calls) + const positionInfo = await getPositionInfo(tokenId, instance) - const { poolKey, liquidity, tickLower, tickUpper } = positionDetails - const { currency0, currency1, fee, tickSpacing, hooks } = poolKey + const { poolKey, liquidity, tickLower, tickUpper, slot0, poolLiquidity } = positionInfo + const { + currency0: currency0Address, + currency1: currency1Address, + fee, + tickSpacing, + hooks, + } = poolKey + // Validate that position has liquidity if (liquidity === 0n) { - throw new Error('Liquidity is 0') + throw new Error('Position has no liquidity') } + // Get token instances const tokens = await getTokens( { - addresses: [currency0 as `0x${string}`, currency1 as `0x${string}`], + addresses: [currency0Address as `0x${string}`, currency1Address as `0x${string}`], }, instance, ) - if (!tokens) { - throw new Error('Tokens not found') + if (!tokens || tokens.length < 2) { + throw new Error('Failed to fetch token instances') } - const [token0, token1] = tokens + const [currency0, currency1] = tokens - const poolId = Pool.getPoolId(token0, token1, fee, tickSpacing, hooks) as `0x${string}` - - const [slot0, poolLiquidity] = await client.multicall({ - allowFailure: false, - contracts: [ - { - address: stateView, - abi: V4StateViewAbi, - functionName: 'getSlot0', - args: [poolId], - }, - { - address: stateView, - abi: V4StateViewAbi, - functionName: 'getLiquidity', - args: [poolId], - }, - ], - }) + // Compute pool ID + const poolId = Pool.getPoolId(currency0, currency1, fee, tickSpacing, hooks) as `0x${string}` + // Extract slot0 data (already fetched in getPositionInfo) const [sqrtPriceX96, tick] = slot0 + // Create Pool instance with current state const pool = new Pool( - token0, - token1, + currency0, + currency1, fee, tickSpacing, hooks, @@ -81,6 +71,7 @@ export async function getPosition( tick, ) + // Create Position instance const position = new V4Position({ pool, liquidity: liquidity.toString(), @@ -91,28 +82,42 @@ export async function getPosition( return { position, pool, - token0, - token1, + currency0, + currency1, poolId, - tokenId: params.tokenId, + tokenId, + currentTick: Number(tick), } } /** - * Retrieves a Uniswap V4 position instance for a given token ID. - * @param params Position parameters including token ID - * @param instance UniDevKitV4Instance - * @returns Promise + * Retrieves basic position information. + * + * This method fetches raw position data from the blockchain and returns it without creating + * SDK instances. It's more efficient when you only need position metadata (tick range, liquidity, + * pool key) without requiring Position or Pool objects. Also fetches pool state (slot0 and liquidity) + * + * Use this method when: + * - Displaying position information in a UI + * - Checking if a position exists + * - Getting position metadata without SDK operations + * + * Use `getPosition()` instead when you need SDK instances for swaps, calculations, or other operations. + * + * @param tokenId - The NFT token ID of the position + * @param instance - UniDevKitV4Instance + * @returns Promise - Basic position information with pool state + * @throws Error if position data cannot be fetched or position doesn't exist */ -export async function getPositionDetails( +export async function getPositionInfo( tokenId: string, instance: UniDevKitV4Instance, -): Promise { +): Promise { const { client, contracts } = instance - const { positionManager } = contracts + const { positionManager, stateView } = contracts - // Fetch poolKey and raw position info + // Fetch poolKey and raw position info using multicall const [poolAndPositionInfo, liquidity] = await client.multicall({ allowFailure: false, contracts: [ @@ -131,13 +136,65 @@ export async function getPositionDetails( ], }) + // Decode packed position data to extract tick range const positionInfo = decodePositionInfo(poolAndPositionInfo[1]) + const poolKey = poolAndPositionInfo[0] + + // Get token instances to compute poolId + const tokens = await getTokens( + { + addresses: [poolKey.currency0 as `0x${string}`, poolKey.currency1 as `0x${string}`], + }, + instance, + ) + + if (!tokens || tokens.length < 2) { + throw new Error('Failed to fetch token instances') + } + + const [currency0, currency1] = tokens + + // Compute pool ID from pool key components + const poolId = Pool.getPoolId( + currency0, + currency1, + poolKey.fee, + poolKey.tickSpacing, + poolKey.hooks, + ) as `0x${string}` + + // Fetch pool state (slot0 and liquidity) in a single multicall + const [slot0Result, poolLiquidityResult] = await client.multicall({ + allowFailure: false, + contracts: [ + { + address: stateView, + abi: V4StateViewAbi, + functionName: 'getSlot0', + args: [poolId], + }, + { + address: stateView, + abi: V4StateViewAbi, + functionName: 'getLiquidity', + args: [poolId], + }, + ], + }) + + const [, tick] = slot0Result return { tokenId, tickLower: positionInfo.tickLower, tickUpper: positionInfo.tickUpper, liquidity, - poolKey: poolAndPositionInfo[0], + poolKey, + currentTick: Number(tick), + slot0: slot0Result, + poolLiquidity: poolLiquidityResult, + poolId, + currency0, + currency1, } } diff --git a/src/utils/getTickInfo.ts b/src/utils/getTickInfo.ts index d52df27..3509d7e 100644 --- a/src/utils/getTickInfo.ts +++ b/src/utils/getTickInfo.ts @@ -17,15 +17,15 @@ export async function getTickInfo( const { poolKey, tick } = args // Create Token instances for currency0 and currency1 in the provided order - const [token0, token1] = await getTokens( + const [currency0, currency1] = await getTokens( { addresses: [poolKey.currency0 as `0x${string}`, poolKey.currency1 as `0x${string}`] }, instance, ) // Compute PoolId from PoolKey components const poolId32Bytes = Pool.getPoolId( - token0, - token1, + currency0, + currency1, poolKey.fee, poolKey.tickSpacing, poolKey.hooks as `0x${string}`, diff --git a/src/utils/preparePermit2BatchData.ts b/src/utils/preparePermit2BatchData.ts index 2f54a45..cd3728c 100644 --- a/src/utils/preparePermit2BatchData.ts +++ b/src/utils/preparePermit2BatchData.ts @@ -18,7 +18,7 @@ import { zeroAddress } from 'viem' /** * 1. Prepare the permit data: * ```typescript * const permitData = await preparePermit2BatchData({ - * tokens: [token0, token1], + * tokens: [currency0, currency1], * spender: positionManagerAddress, * owner: userAddress * }, instance) diff --git a/src/utils/preparePermit2Data.ts b/src/utils/preparePermit2Data.ts index 4f0b1e3..95edba8 100644 --- a/src/utils/preparePermit2Data.ts +++ b/src/utils/preparePermit2Data.ts @@ -45,7 +45,7 @@ export const allowanceAbi = [ * 1. Prepare the permit data: * ```typescript * const permitData = await preparePermit2Data({ - * token: token0, + * token: currency0, * spender: positionManagerAddress, * owner: userAddress * }, instance) From 8ac4da222b9a19a7acdebc82f4e902fd1344688f Mon Sep 17 00:00:00 2001 From: nicosampler Date: Mon, 3 Nov 2025 17:29:25 -0300 Subject: [PATCH 3/7] getPosition UT refactor --- src/core/uniDevKitV4.ts | 3 +- src/test/utils/getPosition.test.ts | 287 +++++++++++++++++------------ src/utils/getPosition.ts | 115 +----------- src/utils/getPositionInfo.ts | 117 ++++++++++++ 4 files changed, 294 insertions(+), 228 deletions(-) create mode 100644 src/utils/getPositionInfo.ts diff --git a/src/core/uniDevKitV4.ts b/src/core/uniDevKitV4.ts index 647efc9..072ec00 100644 --- a/src/core/uniDevKitV4.ts +++ b/src/core/uniDevKitV4.ts @@ -5,7 +5,8 @@ import { buildCollectFeesCallData } from '@/utils/buildCollectFeesCallData' import { buildRemoveLiquidityCallData } from '@/utils/buildRemoveLiquidityCallData' import { buildSwapCallData } from '@/utils/buildSwapCallData' import { getPool } from '@/utils/getPool' -import { getPosition, getPositionInfo } from '@/utils/getPosition' +import { getPosition } from '@/utils/getPosition' +import { getPositionInfo } from '@/utils/getPositionInfo' import { getQuote } from '@/utils/getQuote' import { getTickInfo } from '@/utils/getTickInfo' import { getTokens } from '@/utils/getTokens' diff --git a/src/test/utils/getPosition.test.ts b/src/test/utils/getPosition.test.ts index 16ad49c..f204fde 100644 --- a/src/test/utils/getPosition.test.ts +++ b/src/test/utils/getPosition.test.ts @@ -1,137 +1,196 @@ +import { Pool, Position as V4Position } from '@uniswap/v4-sdk' import { Token } from '@uniswap/sdk-core' import type { Address } from 'viem' import { beforeEach, describe, expect, it, vi } from 'vitest' import { createMockSdkInstance } from '@/test/helpers/sdkInstance' -import { getPosition } from '@/utils/getPosition' +import type { GetPositionInfoResponse } from '@/types/utils/getPosition' + +const mockGetPositionInfo = vi.fn() +const mockGetTokens = vi.fn() vi.mock('@/utils/getTokens', () => ({ - getTokens: vi.fn(), + getTokens: mockGetTokens, })) -// Mock decodePositionInfo para devolver ticks vĂ¡lidos -vi.mock('@/helpers/positions', () => ({ - decodePositionInfo: () => ({ tickLower: -887220, tickUpper: 887220 }), +vi.mock('@/utils/getPositionInfo', () => ({ + getPositionInfo: mockGetPositionInfo, })) +// Mock Pool and V4Position constructors +vi.mock('@uniswap/v4-sdk', () => { + const MockPool = vi.fn() as unknown as typeof Pool + MockPool.getPoolId = vi.fn() + + const MockPosition = vi.fn() as unknown as typeof V4Position + + return { + Pool: MockPool, + Position: MockPosition, + } +}) + describe('getPosition', () => { - // USDC and WETH on Mainnet - const mockCurrencies: [Address, Address] = [ - '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC - '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH - ] - const validHooks = '0x000000000000000000000000000000000000dead' - - beforeEach(() => { + const mockTokenId = '1' + const mockCurrency0Address = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Address // USDC + const mockCurrency1Address = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' as Address // WETH + const mockFee = 3000 + const mockTickSpacing = 60 + const mockHooks = '0x000000000000000000000000000000000000dead' as Address + const mockTickLower = -887220 + const mockTickUpper = 887220 + const mockLiquidity = 1000000n + const mockSqrtPriceX96 = 79228162514264337593543950336n + const mockTick = 0 + const mockProtocolFee = 0 + const mockLpFee = 0 + const mockPoolLiquidity = 1000000n + const mockPoolId = + '0x1234567890123456789012345678901234567890123456789012345678901234' as `0x${string}` + + let mockDeps: ReturnType + let mockCurrency0: Token + let mockCurrency1: Token + let mockPositionInfo: GetPositionInfoResponse + let mockPool: Pool + let mockPosition: V4Position + let getPosition: typeof import('@/utils/getPosition').getPosition + + beforeEach(async () => { vi.resetAllMocks() + vi.clearAllMocks() + + // Import getPosition after mocks are set up + const positionModule = await import('@/utils/getPosition') + getPosition = positionModule.getPosition + + mockDeps = createMockSdkInstance() + mockCurrency0 = new Token(1, mockCurrency0Address, 6, 'USDC', 'USD Coin') + mockCurrency1 = new Token(1, mockCurrency1Address, 18, 'WETH', 'Wrapped Ether') + + mockPositionInfo = { + tokenId: mockTokenId, + tickLower: mockTickLower, + tickUpper: mockTickUpper, + liquidity: mockLiquidity, + poolKey: { + currency0: mockCurrency0Address, + currency1: mockCurrency1Address, + fee: mockFee, + tickSpacing: mockTickSpacing, + hooks: mockHooks, + }, + currentTick: mockTick, + slot0: [mockSqrtPriceX96, mockTick, mockProtocolFee, mockLpFee], + poolLiquidity: mockPoolLiquidity, + poolId: mockPoolId, + currency0: mockCurrency0, + currency1: mockCurrency1, + } + + // Create mock instances + mockPool = { + currency0: mockCurrency0, + currency1: mockCurrency1, + } as unknown as Pool + + mockPosition = { + pool: mockPool, + liquidity: mockLiquidity.toString(), + } as unknown as V4Position + + // Mock constructors to return our mock instances + vi.mocked(Pool).mockReturnValue(mockPool) + vi.mocked(V4Position).mockReturnValue(mockPosition) + + // Mock Pool.getPoolId to return our mock pool ID + vi.mocked(Pool.getPoolId).mockReturnValue(mockPoolId) }) - it('should throw error if SDK instance not found', async () => { - const mockDeps = createMockSdkInstance() - mockDeps.client.multicall = vi.fn().mockRejectedValueOnce(new Error('SDK not initialized')) + it('should throw error if liquidity is 0', async () => { + mockGetPositionInfo.mockResolvedValueOnce({ + ...mockPositionInfo, + liquidity: 0n, + }) - await expect(getPosition('1', mockDeps)).rejects.toThrow('SDK not initialized') + await expect(getPosition(mockTokenId, mockDeps)).rejects.toThrow('Position has no liquidity') }) - it('should throw error if tokens not found', async () => { - const mockDeps = createMockSdkInstance() - // First multicall: position info - mockDeps.client.multicall = vi - .fn() - .mockResolvedValueOnce([ - [ - { - currency0: '0x123', - currency1: '0x456', - fee: 3000, - tickSpacing: 60, - hooks: validHooks, - }, - {}, - ], - 1000000n, - ]) - // Second multicall: slot0 and poolLiquidity - .mockResolvedValueOnce([[79228162514264337593543950336n, 0], 1000000n]) - - const { getTokens } = await import('@/utils/getTokens') - vi.mocked(getTokens).mockResolvedValueOnce([]) - - await expect(getPosition('1', mockDeps)).rejects.toThrow('Failed to fetch token instances') + it('should throw error if tokens is null or undefined', async () => { + mockGetPositionInfo.mockResolvedValueOnce(mockPositionInfo) + mockGetTokens.mockResolvedValueOnce(null as never) + + await expect(getPosition(mockTokenId, mockDeps)).rejects.toThrow( + 'Failed to fetch token instances', + ) }) - it('should throw error if liquidity is 0', async () => { - const mockDeps = createMockSdkInstance() - // Use valid addresses for tokens - const testCurrency0 = '0x1234567890123456789012345678901234567890' - const testCurrency1 = '0x0987654321098765432109876543210987654321' - - // First multicall: position info (from getPositionInfo) - mockDeps.client.multicall = vi - .fn() - .mockResolvedValueOnce([ - [ - { - currency0: testCurrency0, - currency1: testCurrency1, - fee: 3000, - tickSpacing: 60, - hooks: validHooks, - }, - {}, - ], - 0n, - ]) - // Second multicall: slot0 and poolLiquidity (from getPositionInfo) - .mockResolvedValueOnce([[79228162514264337593543950336n, 0], 1000000n]) - - // Mock getTokens - called twice: once in getPositionInfo, once in getPosition - const mockCurrency0 = new Token(1, testCurrency0, 6, 'USDC', 'USD Coin') - const mockCurrency1 = new Token(1, testCurrency1, 18, 'WETH', 'Wrapped Ether') - const { getTokens } = await import('@/utils/getTokens') - vi.mocked(getTokens) - .mockResolvedValueOnce([mockCurrency0, mockCurrency1]) // First call in getPositionInfo - .mockResolvedValueOnce([mockCurrency0, mockCurrency1]) // Second call in getPosition - - await expect(getPosition('1', mockDeps)).rejects.toThrow('Position has no liquidity') + it('should throw error if tokens array length is less than 2', async () => { + mockGetPositionInfo.mockResolvedValueOnce(mockPositionInfo) + mockGetTokens.mockResolvedValueOnce([mockCurrency0]) + + await expect(getPosition(mockTokenId, mockDeps)).rejects.toThrow( + 'Failed to fetch token instances', + ) }) - it('should return position data when position exists', async () => { - const mockDeps = createMockSdkInstance() - // First multicall: position info [poolAndPositionInfo, liquidity] (from getPositionInfo) - mockDeps.client.multicall = vi - .fn() - .mockResolvedValueOnce([ - [ - { - currency0: mockCurrencies[0], - currency1: mockCurrencies[1], - fee: 3000, - tickSpacing: 60, - hooks: validHooks, - }, - 1n, - ], - 1000000n, - ]) - // Second multicall: pool state [slot0, poolLiquidity] (from getPositionInfo) - .mockResolvedValueOnce([[79228162514264337593543950336n, 0], 1000000n]) - - // Mock getTokens - called twice: once in getPositionInfo, once in getPosition - const mockCurrency0 = new Token(1, mockCurrencies[0], 6, 'USDC', 'USD Coin') - const mockCurrency1 = new Token(1, mockCurrencies[1], 18, 'WETH', 'Wrapped Ether') - const { getTokens } = await import('@/utils/getTokens') - vi.mocked(getTokens) - .mockResolvedValueOnce([mockCurrency0, mockCurrency1]) // First call in getPositionInfo - .mockResolvedValueOnce([mockCurrency0, mockCurrency1]) // Second call in getPosition - - const result = await getPosition('1', mockDeps) - - expect(result).toHaveProperty('position') - expect(result).toHaveProperty('pool') - expect(result).toHaveProperty('currency0') - expect(result).toHaveProperty('currency1') - expect(result).toHaveProperty('poolId') - expect(result).toHaveProperty('tokenId', '1') - expect(result).toHaveProperty('currentTick') + it('should return correct data when all operations succeed', async () => { + mockGetPositionInfo.mockResolvedValueOnce(mockPositionInfo) + mockGetTokens.mockResolvedValueOnce([mockCurrency0, mockCurrency1]) + + const result = await getPosition(mockTokenId, mockDeps) + + // Verify getPositionInfo was called with correct parameters + expect(mockGetPositionInfo).toHaveBeenCalledWith(mockTokenId, mockDeps) + expect(mockGetPositionInfo).toHaveBeenCalledTimes(1) + + // Verify getTokens was called with correct parameters + expect(mockGetTokens).toHaveBeenCalledWith( + { + addresses: [mockCurrency0Address, mockCurrency1Address], + }, + mockDeps, + ) + expect(mockGetTokens).toHaveBeenCalledTimes(1) + + // Verify Pool.getPoolId was called with correct parameters + expect(Pool.getPoolId).toHaveBeenCalledWith( + mockCurrency0, + mockCurrency1, + mockFee, + mockTickSpacing, + mockHooks, + ) + expect(Pool.getPoolId).toHaveBeenCalledTimes(1) + + // Verify Pool constructor was called with correct parameters + expect(Pool).toHaveBeenCalledWith( + mockCurrency0, + mockCurrency1, + mockFee, + mockTickSpacing, + mockHooks, + mockSqrtPriceX96.toString(), + mockPoolLiquidity.toString(), + mockTick, + ) + + // Verify V4Position constructor was called with correct parameters + expect(V4Position).toHaveBeenCalledWith({ + pool: mockPool, + liquidity: mockLiquidity.toString(), + tickLower: mockTickLower, + tickUpper: mockTickUpper, + }) + + // Verify return value + expect(result).toEqual({ + position: mockPosition, + pool: mockPool, + currency0: mockCurrency0, + currency1: mockCurrency1, + poolId: mockPoolId, + tokenId: mockTokenId, + currentTick: mockTick, + }) }) }) diff --git a/src/utils/getPosition.ts b/src/utils/getPosition.ts index 6451930..aca75c5 100644 --- a/src/utils/getPosition.ts +++ b/src/utils/getPosition.ts @@ -1,9 +1,7 @@ import { Pool, Position as V4Position } from '@uniswap/v4-sdk' -import V4PositionManagerAbi from '@/constants/abis/V4PositionMananger' -import V4StateViewAbi from '@/constants/abis/V4StateView' -import { decodePositionInfo } from '@/helpers/positions' import type { UniDevKitV4Instance } from '@/types/core' -import type { GetPositionInfoResponse, GetPositionResponse } from '@/types/utils/getPosition' +import type { GetPositionResponse } from '@/types/utils/getPosition' +import { getPositionInfo } from '@/utils/getPositionInfo' import { getTokens } from '@/utils/getTokens' /** @@ -89,112 +87,3 @@ export async function getPosition( currentTick: Number(tick), } } - -/** - * Retrieves basic position information. - * - * This method fetches raw position data from the blockchain and returns it without creating - * SDK instances. It's more efficient when you only need position metadata (tick range, liquidity, - * pool key) without requiring Position or Pool objects. Also fetches pool state (slot0 and liquidity) - * - * Use this method when: - * - Displaying position information in a UI - * - Checking if a position exists - * - Getting position metadata without SDK operations - * - * Use `getPosition()` instead when you need SDK instances for swaps, calculations, or other operations. - * - * @param tokenId - The NFT token ID of the position - * @param instance - UniDevKitV4Instance - * @returns Promise - Basic position information with pool state - * @throws Error if position data cannot be fetched or position doesn't exist - */ -export async function getPositionInfo( - tokenId: string, - instance: UniDevKitV4Instance, -): Promise { - const { client, contracts } = instance - - const { positionManager, stateView } = contracts - - // Fetch poolKey and raw position info using multicall - const [poolAndPositionInfo, liquidity] = await client.multicall({ - allowFailure: false, - contracts: [ - { - address: positionManager, - abi: V4PositionManagerAbi, - functionName: 'getPoolAndPositionInfo', - args: [BigInt(tokenId)], - }, - { - address: positionManager, - abi: V4PositionManagerAbi, - functionName: 'getPositionLiquidity', - args: [BigInt(tokenId)], - }, - ], - }) - - // Decode packed position data to extract tick range - const positionInfo = decodePositionInfo(poolAndPositionInfo[1]) - const poolKey = poolAndPositionInfo[0] - - // Get token instances to compute poolId - const tokens = await getTokens( - { - addresses: [poolKey.currency0 as `0x${string}`, poolKey.currency1 as `0x${string}`], - }, - instance, - ) - - if (!tokens || tokens.length < 2) { - throw new Error('Failed to fetch token instances') - } - - const [currency0, currency1] = tokens - - // Compute pool ID from pool key components - const poolId = Pool.getPoolId( - currency0, - currency1, - poolKey.fee, - poolKey.tickSpacing, - poolKey.hooks, - ) as `0x${string}` - - // Fetch pool state (slot0 and liquidity) in a single multicall - const [slot0Result, poolLiquidityResult] = await client.multicall({ - allowFailure: false, - contracts: [ - { - address: stateView, - abi: V4StateViewAbi, - functionName: 'getSlot0', - args: [poolId], - }, - { - address: stateView, - abi: V4StateViewAbi, - functionName: 'getLiquidity', - args: [poolId], - }, - ], - }) - - const [, tick] = slot0Result - - return { - tokenId, - tickLower: positionInfo.tickLower, - tickUpper: positionInfo.tickUpper, - liquidity, - poolKey, - currentTick: Number(tick), - slot0: slot0Result, - poolLiquidity: poolLiquidityResult, - poolId, - currency0, - currency1, - } -} diff --git a/src/utils/getPositionInfo.ts b/src/utils/getPositionInfo.ts new file mode 100644 index 0000000..66aa7fc --- /dev/null +++ b/src/utils/getPositionInfo.ts @@ -0,0 +1,117 @@ +import { Pool } from '@uniswap/v4-sdk' +import V4PositionManagerAbi from '@/constants/abis/V4PositionMananger' +import V4StateViewAbi from '@/constants/abis/V4StateView' +import { decodePositionInfo } from '@/helpers/positions' +import type { UniDevKitV4Instance } from '@/types/core' +import type { GetPositionInfoResponse } from '@/types/utils/getPosition' +import { getTokens } from '@/utils/getTokens' + +/** + * Retrieves basic position information. + * + * This method fetches raw position data from the blockchain and returns it without creating + * SDK instances. It's more efficient when you only need position metadata (tick range, liquidity, + * pool key) without requiring Position or Pool objects. Also fetches pool state (slot0 and liquidity) + * + * Use this method when: + * - Displaying position information in a UI + * - Checking if a position exists + * - Getting position metadata without SDK operations + * + * Use `getPosition()` instead when you need SDK instances for swaps, calculations, or other operations. + * + * @param tokenId - The NFT token ID of the position + * @param instance - UniDevKitV4Instance + * @returns Promise - Basic position information with pool state + * @throws Error if position data cannot be fetched or position doesn't exist + */ +export async function getPositionInfo( + tokenId: string, + instance: UniDevKitV4Instance, +): Promise { + const { client, contracts } = instance + + const { positionManager, stateView } = contracts + + // Fetch poolKey and raw position info using multicall + const [poolAndPositionInfo, liquidity] = await client.multicall({ + allowFailure: false, + contracts: [ + { + address: positionManager, + abi: V4PositionManagerAbi, + functionName: 'getPoolAndPositionInfo', + args: [BigInt(tokenId)], + }, + { + address: positionManager, + abi: V4PositionManagerAbi, + functionName: 'getPositionLiquidity', + args: [BigInt(tokenId)], + }, + ], + }) + + // Decode packed position data to extract tick range + const positionInfo = decodePositionInfo(poolAndPositionInfo[1]) + const poolKey = poolAndPositionInfo[0] + + // Get token instances to compute poolId + const tokens = await getTokens( + { + addresses: [poolKey.currency0 as `0x${string}`, poolKey.currency1 as `0x${string}`], + }, + instance, + ) + + if (!tokens || tokens.length < 2) { + throw new Error('Failed to fetch token instances') + } + + const [currency0, currency1] = tokens + + // Compute pool ID from pool key components + const poolId = Pool.getPoolId( + currency0, + currency1, + poolKey.fee, + poolKey.tickSpacing, + poolKey.hooks, + ) as `0x${string}` + + // Fetch pool state (slot0 and liquidity) in a single multicall + const [slot0Result, poolLiquidityResult] = await client.multicall({ + allowFailure: false, + contracts: [ + { + address: stateView, + abi: V4StateViewAbi, + functionName: 'getSlot0', + args: [poolId], + }, + { + address: stateView, + abi: V4StateViewAbi, + functionName: 'getLiquidity', + args: [poolId], + }, + ], + }) + + const [, tick] = slot0Result + + return { + tokenId, + tickLower: positionInfo.tickLower, + tickUpper: positionInfo.tickUpper, + liquidity, + poolKey, + currentTick: Number(tick), + slot0: slot0Result, + poolLiquidity: poolLiquidityResult, + poolId, + currency0, + currency1, + } +} + From 3ab4c6a088ed0c4703f684df48869444a57f02d2 Mon Sep 17 00:00:00 2001 From: nicosampler Date: Mon, 3 Nov 2025 17:38:23 -0300 Subject: [PATCH 4/7] getPositionInfo UT --- src/test/utils/getPositionInfo.test.ts | 217 +++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 src/test/utils/getPositionInfo.test.ts diff --git a/src/test/utils/getPositionInfo.test.ts b/src/test/utils/getPositionInfo.test.ts new file mode 100644 index 0000000..a19eace --- /dev/null +++ b/src/test/utils/getPositionInfo.test.ts @@ -0,0 +1,217 @@ +import { Pool } from '@uniswap/v4-sdk' +import { Token } from '@uniswap/sdk-core' +import type { Address } from 'viem' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createMockSdkInstance } from '@/test/helpers/sdkInstance' + +const mockGetTokens = vi.fn() +const mockDecodePositionInfo = vi.fn() + +vi.mock('@/utils/getTokens', () => ({ + getTokens: mockGetTokens, +})) + +vi.mock('@/helpers/positions', () => ({ + decodePositionInfo: mockDecodePositionInfo, +})) + +// Mock Pool.getPoolId +vi.mock('@uniswap/v4-sdk', () => { + const MockPool = vi.fn() as unknown as typeof Pool + MockPool.getPoolId = vi.fn() + + return { + Pool: MockPool, + } +}) + +describe('getPositionInfo', () => { + const mockTokenId = '1' + const mockCurrency0Address = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Address // USDC + const mockCurrency1Address = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' as Address // WETH + const mockFee = 3000 + const mockTickSpacing = 60 + const mockHooks = '0x000000000000000000000000000000000000dead' as Address + const mockTickLower = -887220 + const mockTickUpper = 887220 + const mockLiquidity = 1000000n + const mockSqrtPriceX96 = 79228162514264337593543950336n + const mockTick = 0 + const mockProtocolFee = 0 + const mockLpFee = 0 + const mockPoolLiquidity = 1000000n + const mockPoolId = + '0x1234567890123456789012345678901234567890123456789012345678901234' as `0x${string}` + + let mockDeps: ReturnType + let mockCurrency0: Token + let mockCurrency1: Token + let mockPoolKey: { + currency0: Address + currency1: Address + fee: number + tickSpacing: number + hooks: Address + } + let mockPoolAndPositionInfo: [ + { + currency0: Address + currency1: Address + fee: number + tickSpacing: number + hooks: Address + }, + unknown, + ] + let getPositionInfo: typeof import('@/utils/getPositionInfo').getPositionInfo + + beforeEach(async () => { + vi.resetAllMocks() + vi.clearAllMocks() + + // Import getPositionInfo after mocks are set up + const positionInfoModule = await import('@/utils/getPositionInfo') + getPositionInfo = positionInfoModule.getPositionInfo + + mockDeps = createMockSdkInstance() + mockCurrency0 = new Token(1, mockCurrency0Address, 6, 'USDC', 'USD Coin') + mockCurrency1 = new Token(1, mockCurrency1Address, 18, 'WETH', 'Wrapped Ether') + + mockPoolKey = { + currency0: mockCurrency0Address, + currency1: mockCurrency1Address, + fee: mockFee, + tickSpacing: mockTickSpacing, + hooks: mockHooks, + } + + mockPoolAndPositionInfo = [mockPoolKey, {}] + + // Mock decodePositionInfo + mockDecodePositionInfo.mockReturnValue({ + tickLower: mockTickLower, + tickUpper: mockTickUpper, + }) + + // Mock Pool.getPoolId + vi.mocked(Pool.getPoolId).mockReturnValue(mockPoolId) + }) + + it('should throw error if tokens is null or undefined', async () => { + // First multicall: position info + vi.spyOn(mockDeps.client, 'multicall').mockResolvedValueOnce([ + mockPoolAndPositionInfo, + mockLiquidity, + ]) + + mockGetTokens.mockResolvedValueOnce(null as never) + + await expect(getPositionInfo(mockTokenId, mockDeps)).rejects.toThrow( + 'Failed to fetch token instances', + ) + }) + + it('should throw error if tokens array length is less than 2', async () => { + // First multicall: position info + vi.spyOn(mockDeps.client, 'multicall').mockResolvedValueOnce([ + mockPoolAndPositionInfo, + mockLiquidity, + ]) + + mockGetTokens.mockResolvedValueOnce([mockCurrency0]) + + await expect(getPositionInfo(mockTokenId, mockDeps)).rejects.toThrow( + 'Failed to fetch token instances', + ) + }) + + it('should return correct data when all operations succeed', async () => { + const mockSlot0 = [mockSqrtPriceX96, mockTick, mockProtocolFee, mockLpFee] as const + + // First multicall: position info + vi.spyOn(mockDeps.client, 'multicall') + .mockResolvedValueOnce([mockPoolAndPositionInfo, mockLiquidity]) + // Second multicall: pool state + .mockResolvedValueOnce([mockSlot0, mockPoolLiquidity]) + + mockGetTokens.mockResolvedValueOnce([mockCurrency0, mockCurrency1]) + + const result = await getPositionInfo(mockTokenId, mockDeps) + + // Verify first multicall was called with correct parameters + expect(mockDeps.client.multicall).toHaveBeenNthCalledWith(1, { + allowFailure: false, + contracts: [ + { + address: mockDeps.contracts.positionManager, + abi: expect.any(Object), + functionName: 'getPoolAndPositionInfo', + args: [BigInt(mockTokenId)], + }, + { + address: mockDeps.contracts.positionManager, + abi: expect.any(Object), + functionName: 'getPositionLiquidity', + args: [BigInt(mockTokenId)], + }, + ], + }) + + // Verify decodePositionInfo was called with correct parameters + expect(mockDecodePositionInfo).toHaveBeenCalledWith(mockPoolAndPositionInfo[1]) + expect(mockDecodePositionInfo).toHaveBeenCalledTimes(1) + + // Verify getTokens was called with correct parameters + expect(mockGetTokens).toHaveBeenCalledWith( + { + addresses: [mockCurrency0Address, mockCurrency1Address], + }, + mockDeps, + ) + expect(mockGetTokens).toHaveBeenCalledTimes(1) + + // Verify Pool.getPoolId was called with correct parameters + expect(Pool.getPoolId).toHaveBeenCalledWith( + mockCurrency0, + mockCurrency1, + mockFee, + mockTickSpacing, + mockHooks, + ) + expect(Pool.getPoolId).toHaveBeenCalledTimes(1) + + // Verify second multicall was called with correct parameters + expect(mockDeps.client.multicall).toHaveBeenNthCalledWith(2, { + allowFailure: false, + contracts: [ + { + address: mockDeps.contracts.stateView, + abi: expect.any(Object), + functionName: 'getSlot0', + args: [mockPoolId], + }, + { + address: mockDeps.contracts.stateView, + abi: expect.any(Object), + functionName: 'getLiquidity', + args: [mockPoolId], + }, + ], + }) + + // Verify return value + expect(result).toEqual({ + tokenId: mockTokenId, + tickLower: mockTickLower, + tickUpper: mockTickUpper, + liquidity: mockLiquidity, + poolKey: mockPoolKey, + currentTick: mockTick, + slot0: mockSlot0, + poolLiquidity: mockPoolLiquidity, + poolId: mockPoolId, + currency0: mockCurrency0, + currency1: mockCurrency1, + }) + }) +}) From bf89aa220c4608c3bb8f0fb3c31ae5b262bdd6b5 Mon Sep 17 00:00:00 2001 From: nicosampler Date: Tue, 4 Nov 2025 11:06:53 -0300 Subject: [PATCH 5/7] update package.json version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b4e6fe..222c09b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uniswap-dev-kit", - "version": "1.1.1", + "version": "2.1.1", "description": "A modern TypeScript library for integrating Uniswap into your dapp.", "main": "dist/index.js", "types": "dist/index.d.ts", From d96978c1e80bfbae44f474b5ce06f6fce4e8495c Mon Sep 17 00:00:00 2001 From: nicosampler Date: Tue, 4 Nov 2025 12:26:05 -0300 Subject: [PATCH 6/7] copilot bot feedback --- src/core/uniDevKitV4.ts | 2 +- src/utils/getTickInfo.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/core/uniDevKitV4.ts b/src/core/uniDevKitV4.ts index 072ec00..23dd47c 100644 --- a/src/core/uniDevKitV4.ts +++ b/src/core/uniDevKitV4.ts @@ -137,7 +137,7 @@ export class UniDevKitV4 { * from the pool key currencies, computes the PoolId, and then reads the tick info from the * blockchain. * - * @param args @type {GetTickInfoArgs} - Tick query parameters including pool key and tick index + * @param args - Tick query parameters including pool key and tick index * @returns Promise - Tick information including liquidity and fee growth data * @throws Error if tick data cannot be fetched or contract call reverts */ diff --git a/src/utils/getTickInfo.ts b/src/utils/getTickInfo.ts index 3509d7e..31c2fab 100644 --- a/src/utils/getTickInfo.ts +++ b/src/utils/getTickInfo.ts @@ -17,11 +17,17 @@ export async function getTickInfo( const { poolKey, tick } = args // Create Token instances for currency0 and currency1 in the provided order - const [currency0, currency1] = await getTokens( + const tokens = await getTokens( { addresses: [poolKey.currency0 as `0x${string}`, poolKey.currency1 as `0x${string}`] }, instance, ) + if (!tokens || tokens.length < 2) { + throw new Error('Failed to fetch token instances') + } + + const [currency0, currency1] = tokens + // Compute PoolId from PoolKey components const poolId32Bytes = Pool.getPoolId( currency0, From dbbc0c2659d4324c83dbfdeac528446a6248e97c Mon Sep 17 00:00:00 2001 From: nicosampler Date: Tue, 4 Nov 2025 15:19:07 -0300 Subject: [PATCH 7/7] feat!: refactoring SDK to be aligned with uniswap v4 documentations BREAKING CHANGE: breaking changes, refactoring SDK to be aligned with uniswap v4 documentations, methods and argument names. Also including some new helpers and refactoring some existing one.