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
35 changes: 18 additions & 17 deletions src/constants/chains.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
import {
type Chain,
arbitrum,
arbitrumSepolia,
avalanche,
base,
baseSepolia,
blast,
bsc,
celo,
mainnet,
optimism,
polygon,
sepolia,
unichain,
unichainSepolia,
worldchain,
zksync,
zora,
} from "wagmi/chains";
// uniswap supported chains
/*
Ethereum
Arbitrum
Optimism
Polygon
Base
BNB
Avalanche C-Chain
CELO
Blast
ZKsync
Zora
WorldChain
*/

export const supportedChains = [
arbitrum,
Expand All @@ -40,11 +31,21 @@ export const supportedChains = [
zksync,
zora,
worldchain,
unichain,
mainnet,
] as const;

export const getChainById = (chainId: number) => {
const chain = supportedChains.find((chain) => chain.id === chainId);
export const testChains = [
unichainSepolia,
sepolia,
baseSepolia,
arbitrumSepolia,
] as const;

export const getChainById = (chainId: number): Chain => {
const chain = [...supportedChains, ...testChains].find(
(chain) => chain.id === chainId,
);
if (!chain) {
throw new Error(`Chain with id ${chainId} not found`);
}
Expand Down
52 changes: 52 additions & 0 deletions src/helpers/positions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Decodes a signed 24-bit integer (`int24`) from a packed `bigint` at a given bit offset.
*
* @param info - The packed `bigint` containing the encoded data.
* @param shift - The bit offset where the `int24` value begins.
* @returns The decoded signed 24-bit integer as a number.
*
* @example
* ```ts
* const tickLower = decodeInt24FromInfo(info, 8);
* const tickUpper = decodeInt24FromInfo(info, 32);
* ```
*/
export function decodeInt24FromInfo(info: bigint, shift: number): number {
const raw = (info >> BigInt(shift)) & 0xffffffn; // Extract 24 bits
return raw >= 0x800000n ? Number(raw - 0x1000000n) : Number(raw); // Handle sign bit
}

/**
* Decodes position metadata packed in a `uint256` returned by Uniswap V4's `getPoolAndPositionInfo`.
* The structure of the encoded info is:
* - bits 0–7: `hasSubscriber` flag (boolean in practice, stored as `uint8`)
* - bits 8–31: `tickLower` (int24)
* - bits 32–55: `tickUpper` (int24)
* - bits 56–255: `poolId` (bytes25, used as bytes32 with padding)
*
* @param info - The packed position info as a `bigint`
* @returns An object containing:
* - `hasSubscriber`: number (usually 0 or 1)
* - `tickLower`: number (int24)
* - `tickUpper`: number (int24)
* - `poolId`: bigint (25-byte identifier of the pool)
*
* @example
* ```ts
* const decoded = decodePositionInfo(info);
* console.log(decoded.tickLower, decoded.poolId.toString(16));
* ```
*/
export function decodePositionInfo(info: bigint): {
hasSubscriber: number;
tickLower: number;
tickUpper: number;
poolId: bigint;
} {
return {
hasSubscriber: Number(info & 0xffn),
tickLower: decodeInt24FromInfo(info, 8),
tickUpper: decodeInt24FromInfo(info, 32),
poolId: info >> 56n,
};
}
47 changes: 47 additions & 0 deletions src/hooks/useGetPosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type {
PositionResult,
UseGetPositionOptions,
} from "@/types/hooks/useGetPosition";
import { getPosition } from "@/utils/getPosition";
import { useQuery } from "@tanstack/react-query";

/**
* React hook for fetching Uniswap V4 position data using React Query.
* Handles caching, loading states, and error handling automatically.
*
* @param options - Configuration options for the hook
* @returns Query result containing position data, loading state, error state, and refetch function
*
* @example
* ```tsx
* const { data, isLoading, error, refetch } = useGetPosition({
* tokenId: 123,
* chainId: 1,
* queryOptions: {
* enabled: true,
* staleTime: 30000,
* gcTime: 300000,
* retry: 3,
* onSuccess: (data) => console.log('Position data received:', data)
* }
* });
* ```
*/
export function useGetPosition({
tokenId,
chainId,
queryOptions = {},
}: UseGetPositionOptions = {}) {
if (!tokenId) throw new Error("No tokenId provided");

return useQuery<
PositionResult | undefined,
Error,
PositionResult | undefined,
unknown[]
>({
queryKey: ["position", tokenId, chainId],
queryFn: () => getPosition(tokenId, chainId),
...queryOptions,
});
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from "@/core/uniDevKitV4Factory";

// Hooks
export * from "@/hooks/useGetPool";
export * from "@/hooks/useGetPosition";
export * from "@/hooks/useGetQuote";

// Utils
Expand Down
158 changes: 158 additions & 0 deletions src/test/hooks/useGetPosition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { useGetPosition } from "@/hooks/useGetPosition";
import { getPosition } from "@/utils/getPosition";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { renderHook, waitFor } from "@testing-library/react";
import { Token } from "@uniswap/sdk-core";
import { jsx as _jsx } from "react/jsx-runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";

// Mock getPosition
vi.mock("@/utils/getPosition", () => ({
getPosition: vi.fn(),
}));

describe("useGetPosition", () => {
let queryClient: QueryClient;

beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
vi.resetAllMocks();
});

const wrapper = ({ children }: { children: React.ReactNode }) =>
_jsx(QueryClientProvider, { client: queryClient, children });

it("should fetch position data successfully", async () => {
const mockPosition = {
token0: new Token(
1,
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6,
"USDC",
"USD Coin",
),
token1: new Token(
1,
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
18,
"WETH",
"Wrapped Ether",
),
amounts: {
amount0: "1000000",
amount1: "1000000000000000000",
},
tickLower: -100,
tickUpper: 100,
liquidity: BigInt("1000000000000000000"),
poolId: "0x1234567890123456789012345678901234567890",
};

(getPosition as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
mockPosition,
);

const { result } = renderHook(
() =>
useGetPosition({
tokenId: "123",
chainId: 1,
}),
{ wrapper },
);

expect(result.current.isLoading).toBe(true);
expect(result.current.data).toBeUndefined();

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.data).toEqual(mockPosition);
expect(getPosition).toHaveBeenCalledWith("123", 1);
});

it("should handle errors", async () => {
const error = new Error("Failed to fetch position");
(getPosition as unknown as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
error,
);

const { result } = renderHook(
() =>
useGetPosition({
tokenId: "123",
chainId: 1,
}),
{ wrapper },
);

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.error).toEqual(error);
});

it("should throw error if no tokenId provided", () => {
expect(() => {
renderHook(() => useGetPosition(), { wrapper });
}).toThrow("No tokenId provided");
});

it("should handle custom query options", async () => {
const mockPosition = {
token0: new Token(
1,
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6,
"USDC",
"USD Coin",
),
token1: new Token(
1,
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
18,
"WETH",
"Wrapped Ether",
),
amounts: {
amount0: "1000000",
amount1: "1000000000000000000",
},
tickLower: -100,
tickUpper: 100,
liquidity: BigInt("1000000000000000000"),
poolId: "0x1234567890123456789012345678901234567890",
};

(getPosition as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
mockPosition,
);

const { result } = renderHook(
() =>
useGetPosition({
tokenId: "123",
chainId: 1,
queryOptions: {
enabled: true,
staleTime: 30000,
},
}),
{ wrapper },
);

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.data).toEqual(mockPosition);
});
});
Loading