Fetching Positions
Introduction
This guide covers how to fetch and analyze liquidity positions in Uniswap v4 using the v4-sdk.
For this guide, the following Uniswap packages are used:
Key Differences from v3
The v4 PositionManager does not implement ERC721Enumerable, so tokenOfOwnerByIndex is not available. This requires using the subgraph to discover position IDs. Additionally, v4 uses a packed data format for position information.
Setup
import { createPublicClient, http, Address, zeroAddress } from 'viem'
import { unichain } from 'viem/chains'
import request from 'graphql-request'
const POSITION_MANAGER_ADDRESS = '0x4529a01c7a0410167c5740c487a8de60232617bf' //unichain
const publicClient = createPublicClient({
  chain: unichain,
  transport: http(),
})
Fetching Position IDs
interface SubgraphPosition {
  id: string
  tokenId: string
  owner: string
}
const GET_POSITIONS_QUERY = `
  query GetPositions($owner: String!) {
    positions(where: { owner: $owner }) {
      tokenId
      owner
      id
    }
  }
`
const UNICHAIN_SUBGRAPH_URL =
  'https://gateway.thegraph.com/api/subgraphs/id/EoCvJ5tyMLMJcTnLQwWpjAtPdn74PcrZgzfcT5bYxNBH'
async function getPositionIds(owner: Address): Promise<bigint[]> {
  // You can explore queries at: https://thegraph.com/explorer/subgraphs/EoCvJ5tyMLMJcTnLQwWpjAtPdn74PcrZgzfcT5bYxNBH?view=Query&chain=arbitrum-one
  const headers = {
    Authorization: 'Bearer ' + process.env.GRAPH_KEY, // Get your API key from https://thegraph.com/studio/apikeys/
  }
  const response = await request<{ positions: SubgraphPosition[] }>(
    UNICHAIN_SUBGRAPH_URL,
    GET_POSITIONS_QUERY,
    { owner: owner.toLowerCase() },
    headers
  )
  return response.positions.map((p) => BigInt(p.tokenId))
}
Decoding Packed Position Data
v4 stores position information in a packed format. Here's how to decode it:
interface PackedPositionInfo {
  getTickUpper(): number
  getTickLower(): number
  hasSubscriber(): boolean
}
function decodePositionInfo(value: bigint): PackedPositionInfo {
  return {
    getTickUpper: () => {
      const raw = Number((value >> 32n) & 0xffffffn)
      return raw >= 0x800000 ? raw - 0x1000000 : raw
    },
    getTickLower: () => {
      const raw = Number((value >> 8n) & 0xffffffn)
      return raw >= 0x800000 ? raw - 0x1000000 : raw
    },
    hasSubscriber: () => (value & 0xffn) !== 0n,
  }
}
Position Details Interface
interface PositionDetails {
  tokenId: bigint
  tickLower: number
  tickUpper: number
  liquidity: bigint
  poolKey: {
    currency0: Address
    currency1: Address
    fee: number
    tickSpacing: number
    hooks: Address
  }
}
Contract ABI
const POSITION_MANAGER_ABI = [
  {
    name: 'getPoolAndPositionInfo',
    type: 'function',
    inputs: [{ name: 'tokenId', type: 'uint256' }],
    outputs: [
      {
        name: 'poolKey',
        type: 'tuple',
        components: [
          { name: 'currency0', type: 'address' },
          { name: 'currency1', type: 'address' },
          { name: 'fee', type: 'uint24' },
          { name: 'tickSpacing', type: 'int24' },
          { name: 'hooks', type: 'address' },
        ],
      },
      { name: 'info', type: 'uint256' },
    ],
  },
  {
    name: 'getPositionLiquidity',
    type: 'function',
    inputs: [{ name: 'tokenId', type: 'uint256' }],
    outputs: [{ name: 'liquidity', type: 'uint128' }],
  },
] as const
Fetching Position Details
async function getPositionDetails(tokenId: bigint): Promise<PositionDetails> {
  // Get pool key and packed position info
  // Get pool key and packed position info
  const [poolKey, infoValue] = (await publicClient.readContract({
    address: POSITION_MANAGER_ADDRESS,
    abi: POSITION_MANAGER_ABI,
    functionName: 'getPoolAndPositionInfo',
    args: [tokenId],
  })) as readonly [
    {
      currency0: Address
      currency1: Address
      fee: number
      tickSpacing: number
      hooks: Address
    },
    bigint
  ]
  // Get current liquidity
  const liquidity = (await publicClient.readContract({
    address: POSITION_MANAGER_ADDRESS,
    abi: POSITION_MANAGER_ABI,
    functionName: 'getPositionLiquidity',
    args: [tokenId],
  })) as bigint
  // Decode packed position info
  const positionInfo = decodePositionInfo(infoValue)
  return {
    tokenId,
    tickLower: positionInfo.getTickLower(),
    tickUpper: positionInfo.getTickUpper(),
    liquidity,
    poolKey,
  }
}
Usage Example
async function fetchUserPositions(userAddress: Address) {
  try {
    // Get position IDs from subgraph
    const tokenIds = await getPositionIds(userAddress)
    console.log(`Found ${tokenIds.length} positions on Unichain`)
    // Fetch details for each position
    for (const tokenId of tokenIds) {
      const details = await getPositionDetails(tokenId)
      console.log(`Position ${tokenId}:`)
      console.log(`  Token0: ${details.poolKey.currency0}`)
      console.log(`  Token1: ${details.poolKey.currency1}`)
      console.log(`  Fee: ${details.poolKey.fee / 10000}%`)
      console.log(`  Range: ${details.tickLower} to ${details.tickUpper}`)
      console.log(`  Liquidity: ${details.liquidity.toString()}`)
      console.log(`  Hooks: ${details.poolKey.hooks}`)
      console.log('---')
    }
  } catch (error) {
    console.error('Error:', error)
  }
}
// Example usage
fetchUserPositions('0xYourAddress' as Address)