Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
62 changes: 52 additions & 10 deletions src/core/uniDevKitV4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ 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 } from '@/utils/getPosition'
import { getPositionInfo } from '@/utils/getPositionInfo'
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'
Expand All @@ -16,7 +18,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 {
Expand All @@ -28,6 +30,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'
Expand Down Expand Up @@ -127,18 +130,57 @@ export class UniDevKitV4 {
}

/**
* Fetches detailed position information from the V4 PositionManager contract.
* Fetches tick information for a given pool key and tick from V4 StateView.
*
* 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 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 - Tick query parameters including pool key and tick index
* @returns Promise<TickInfoResponse> - 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<TickInfoResponse> {
return getTickInfo(args, this.instance)
}

/**
* Retrieves a complete Uniswap V4 position instance with pool and token 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<GetPositionResponse> - 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<GetPositionResponse> {
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<GetPositionDetailsResponse> - Position details including tick range, liquidity, and pool key
* @returns Promise<GetPositionInfoResponse> - 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<GetPositionDetailsResponse> {
return getPositionDetails(tokenId, this.instance)
public async getPositionInfo(tokenId: string): Promise<GetPositionInfoResponse> {
return getPositionInfo(tokenId, this.instance)
}

/**
Expand Down Expand Up @@ -195,7 +237,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.
*
Expand Down
12 changes: 6 additions & 6 deletions src/helpers/tokens.ts
Original file line number Diff line number Diff line change
@@ -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]
}
29 changes: 18 additions & 11 deletions src/test/helpers/testFactories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
}
}
7 changes: 4 additions & 3 deletions src/test/utils/buildAddLiquidityCallData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand All @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion src/test/utils/buildRemoveLiquidityCallData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
42 changes: 21 additions & 21 deletions src/test/utils/getPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ vi.mock('wagmi', () => ({

describe('getPool', () => {
// USDC and WETH on Mainnet
const mockTokens: [Address, Address] = [
const mockCurrencies: [Address, Address] = [
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
]
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Loading