Skip to content

Commit 8bdcc42

Browse files
authored
Merge pull request #4 from BootNodeDev/feat/usePosition
feat: getPosition/usePosition
2 parents 6d19ecf + 7e3d037 commit 8bdcc42

File tree

14 files changed

+710
-36
lines changed

14 files changed

+710
-36
lines changed

src/constants/chains.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,23 @@
11
import {
2+
type Chain,
23
arbitrum,
4+
arbitrumSepolia,
35
avalanche,
46
base,
7+
baseSepolia,
58
blast,
69
bsc,
710
celo,
811
mainnet,
912
optimism,
1013
polygon,
14+
sepolia,
15+
unichain,
16+
unichainSepolia,
1117
worldchain,
1218
zksync,
1319
zora,
1420
} from "wagmi/chains";
15-
// uniswap supported chains
16-
/*
17-
Ethereum
18-
Arbitrum
19-
Optimism
20-
Polygon
21-
Base
22-
BNB
23-
Avalanche C-Chain
24-
CELO
25-
Blast
26-
ZKsync
27-
Zora
28-
WorldChain
29-
*/
3021

3122
export const supportedChains = [
3223
arbitrum,
@@ -40,11 +31,21 @@ export const supportedChains = [
4031
zksync,
4132
zora,
4233
worldchain,
34+
unichain,
4335
mainnet,
4436
] as const;
4537

46-
export const getChainById = (chainId: number) => {
47-
const chain = supportedChains.find((chain) => chain.id === chainId);
38+
export const testChains = [
39+
unichainSepolia,
40+
sepolia,
41+
baseSepolia,
42+
arbitrumSepolia,
43+
] as const;
44+
45+
export const getChainById = (chainId: number): Chain => {
46+
const chain = [...supportedChains, ...testChains].find(
47+
(chain) => chain.id === chainId,
48+
);
4849
if (!chain) {
4950
throw new Error(`Chain with id ${chainId} not found`);
5051
}

src/helpers/positions.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Decodes a signed 24-bit integer (`int24`) from a packed `bigint` at a given bit offset.
3+
*
4+
* @param info - The packed `bigint` containing the encoded data.
5+
* @param shift - The bit offset where the `int24` value begins.
6+
* @returns The decoded signed 24-bit integer as a number.
7+
*
8+
* @example
9+
* ```ts
10+
* const tickLower = decodeInt24FromInfo(info, 8);
11+
* const tickUpper = decodeInt24FromInfo(info, 32);
12+
* ```
13+
*/
14+
export function decodeInt24FromInfo(info: bigint, shift: number): number {
15+
const raw = (info >> BigInt(shift)) & 0xffffffn; // Extract 24 bits
16+
return raw >= 0x800000n ? Number(raw - 0x1000000n) : Number(raw); // Handle sign bit
17+
}
18+
19+
/**
20+
* Decodes position metadata packed in a `uint256` returned by Uniswap V4's `getPoolAndPositionInfo`.
21+
* The structure of the encoded info is:
22+
* - bits 0–7: `hasSubscriber` flag (boolean in practice, stored as `uint8`)
23+
* - bits 8–31: `tickLower` (int24)
24+
* - bits 32–55: `tickUpper` (int24)
25+
* - bits 56–255: `poolId` (bytes25, used as bytes32 with padding)
26+
*
27+
* @param info - The packed position info as a `bigint`
28+
* @returns An object containing:
29+
* - `hasSubscriber`: number (usually 0 or 1)
30+
* - `tickLower`: number (int24)
31+
* - `tickUpper`: number (int24)
32+
* - `poolId`: bigint (25-byte identifier of the pool)
33+
*
34+
* @example
35+
* ```ts
36+
* const decoded = decodePositionInfo(info);
37+
* console.log(decoded.tickLower, decoded.poolId.toString(16));
38+
* ```
39+
*/
40+
export function decodePositionInfo(info: bigint): {
41+
hasSubscriber: number;
42+
tickLower: number;
43+
tickUpper: number;
44+
poolId: bigint;
45+
} {
46+
return {
47+
hasSubscriber: Number(info & 0xffn),
48+
tickLower: decodeInt24FromInfo(info, 8),
49+
tickUpper: decodeInt24FromInfo(info, 32),
50+
poolId: info >> 56n,
51+
};
52+
}

src/hooks/useGetPosition.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type {
2+
PositionResult,
3+
UseGetPositionOptions,
4+
} from "@/types/hooks/useGetPosition";
5+
import { getPosition } from "@/utils/getPosition";
6+
import { useQuery } from "@tanstack/react-query";
7+
8+
/**
9+
* React hook for fetching Uniswap V4 position data using React Query.
10+
* Handles caching, loading states, and error handling automatically.
11+
*
12+
* @param options - Configuration options for the hook
13+
* @returns Query result containing position data, loading state, error state, and refetch function
14+
*
15+
* @example
16+
* ```tsx
17+
* const { data, isLoading, error, refetch } = useGetPosition({
18+
* tokenId: 123,
19+
* chainId: 1,
20+
* queryOptions: {
21+
* enabled: true,
22+
* staleTime: 30000,
23+
* gcTime: 300000,
24+
* retry: 3,
25+
* onSuccess: (data) => console.log('Position data received:', data)
26+
* }
27+
* });
28+
* ```
29+
*/
30+
export function useGetPosition({
31+
tokenId,
32+
chainId,
33+
queryOptions = {},
34+
}: UseGetPositionOptions = {}) {
35+
if (!tokenId) throw new Error("No tokenId provided");
36+
37+
return useQuery<
38+
PositionResult | undefined,
39+
Error,
40+
PositionResult | undefined,
41+
unknown[]
42+
>({
43+
queryKey: ["position", tokenId, chainId],
44+
queryFn: () => getPosition(tokenId, chainId),
45+
...queryOptions,
46+
});
47+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from "@/core/uniDevKitV4Factory";
44

55
// Hooks
66
export * from "@/hooks/useGetPool";
7+
export * from "@/hooks/useGetPosition";
78
export * from "@/hooks/useGetQuote";
89

910
// Utils
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { useGetPosition } from "@/hooks/useGetPosition";
2+
import { getPosition } from "@/utils/getPosition";
3+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4+
import { renderHook, waitFor } from "@testing-library/react";
5+
import { Token } from "@uniswap/sdk-core";
6+
import { jsx as _jsx } from "react/jsx-runtime";
7+
import { beforeEach, describe, expect, it, vi } from "vitest";
8+
9+
// Mock getPosition
10+
vi.mock("@/utils/getPosition", () => ({
11+
getPosition: vi.fn(),
12+
}));
13+
14+
describe("useGetPosition", () => {
15+
let queryClient: QueryClient;
16+
17+
beforeEach(() => {
18+
queryClient = new QueryClient({
19+
defaultOptions: {
20+
queries: {
21+
retry: false,
22+
},
23+
},
24+
});
25+
vi.resetAllMocks();
26+
});
27+
28+
const wrapper = ({ children }: { children: React.ReactNode }) =>
29+
_jsx(QueryClientProvider, { client: queryClient, children });
30+
31+
it("should fetch position data successfully", async () => {
32+
const mockPosition = {
33+
token0: new Token(
34+
1,
35+
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
36+
6,
37+
"USDC",
38+
"USD Coin",
39+
),
40+
token1: new Token(
41+
1,
42+
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
43+
18,
44+
"WETH",
45+
"Wrapped Ether",
46+
),
47+
amounts: {
48+
amount0: "1000000",
49+
amount1: "1000000000000000000",
50+
},
51+
tickLower: -100,
52+
tickUpper: 100,
53+
liquidity: BigInt("1000000000000000000"),
54+
poolId: "0x1234567890123456789012345678901234567890",
55+
};
56+
57+
(getPosition as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
58+
mockPosition,
59+
);
60+
61+
const { result } = renderHook(
62+
() =>
63+
useGetPosition({
64+
tokenId: "123",
65+
chainId: 1,
66+
}),
67+
{ wrapper },
68+
);
69+
70+
expect(result.current.isLoading).toBe(true);
71+
expect(result.current.data).toBeUndefined();
72+
73+
await waitFor(() => {
74+
expect(result.current.isLoading).toBe(false);
75+
});
76+
77+
expect(result.current.data).toEqual(mockPosition);
78+
expect(getPosition).toHaveBeenCalledWith("123", 1);
79+
});
80+
81+
it("should handle errors", async () => {
82+
const error = new Error("Failed to fetch position");
83+
(getPosition as unknown as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
84+
error,
85+
);
86+
87+
const { result } = renderHook(
88+
() =>
89+
useGetPosition({
90+
tokenId: "123",
91+
chainId: 1,
92+
}),
93+
{ wrapper },
94+
);
95+
96+
await waitFor(() => {
97+
expect(result.current.isLoading).toBe(false);
98+
});
99+
100+
expect(result.current.error).toEqual(error);
101+
});
102+
103+
it("should throw error if no tokenId provided", () => {
104+
expect(() => {
105+
renderHook(() => useGetPosition(), { wrapper });
106+
}).toThrow("No tokenId provided");
107+
});
108+
109+
it("should handle custom query options", async () => {
110+
const mockPosition = {
111+
token0: new Token(
112+
1,
113+
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
114+
6,
115+
"USDC",
116+
"USD Coin",
117+
),
118+
token1: new Token(
119+
1,
120+
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
121+
18,
122+
"WETH",
123+
"Wrapped Ether",
124+
),
125+
amounts: {
126+
amount0: "1000000",
127+
amount1: "1000000000000000000",
128+
},
129+
tickLower: -100,
130+
tickUpper: 100,
131+
liquidity: BigInt("1000000000000000000"),
132+
poolId: "0x1234567890123456789012345678901234567890",
133+
};
134+
135+
(getPosition as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
136+
mockPosition,
137+
);
138+
139+
const { result } = renderHook(
140+
() =>
141+
useGetPosition({
142+
tokenId: "123",
143+
chainId: 1,
144+
queryOptions: {
145+
enabled: true,
146+
staleTime: 30000,
147+
},
148+
}),
149+
{ wrapper },
150+
);
151+
152+
await waitFor(() => {
153+
expect(result.current.isLoading).toBe(false);
154+
});
155+
156+
expect(result.current.data).toEqual(mockPosition);
157+
});
158+
});

0 commit comments

Comments
 (0)