diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 0ef8874..3abc01c 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,2 +1,3 @@ export { useGetPool } from "@/hooks/useGetPool"; +export { useGetPoolKeyFromPoolId } from "@/hooks/useGetPoolKeyFromPoolId"; export { useGetQuote } from "@/hooks/useGetQuote"; diff --git a/src/hooks/useGetPool.ts b/src/hooks/useGetPool.ts index 91fa064..ee60619 100644 --- a/src/hooks/useGetPool.ts +++ b/src/hooks/useGetPool.ts @@ -1,5 +1,4 @@ import type { UseGetPoolOptions } from "@/types/hooks/useGetPool"; -import type { PoolParams } from "@/types/utils/getPool"; import { getPool } from "@/utils/getPool"; import { useQuery } from "@tanstack/react-query"; import type { Pool } from "@uniswap/v4-sdk"; @@ -30,22 +29,21 @@ import type { Pool } from "@uniswap/v4-sdk"; * }); * ``` */ -function serializeParams(params?: PoolParams) { - if (!params) return undefined; - return { - ...params, - tokens: params.tokens.map((t) => t.toLowerCase()), - }; -} export function useGetPool({ params, chainId, queryOptions = {}, -}: UseGetPoolOptions = {}) { - if (!params) throw new Error("No params provided"); +}: UseGetPoolOptions) { return useQuery({ - queryKey: ["pool", serializeParams(params), chainId], + queryKey: [ + "pool", + params.fee, + params.tokens, + params.hooks, + params.tickSpacing, + chainId, + ], queryFn: () => getPool(params, chainId), ...queryOptions, }); diff --git a/src/hooks/useGetPoolKeyFromPoolId.ts b/src/hooks/useGetPoolKeyFromPoolId.ts new file mode 100644 index 0000000..be87c67 --- /dev/null +++ b/src/hooks/useGetPoolKeyFromPoolId.ts @@ -0,0 +1,37 @@ +import type { UseGetPoolKeyFromPoolIdOptions } from "@/types/hooks/useGetPoolKeyFromPoolId"; +import { getPoolKeyFromPoolId } from "@/utils/getPoolKeyFromPoolId"; +import { useQuery } from "@tanstack/react-query"; + +/** + * React hook for fetching Uniswap V4 pool key information using React Query. + * Handles caching, loading states, and error handling automatically. + * + * @param options - Configuration options for the hook + * @returns Query result containing pool key data, loading state, error state, and refetch function + * + * @example + * ```tsx + * const { data, isLoading, error, refetch } = useGetPoolKeyFromPoolId({ + * poolId: "0x1234...", + * chainId: 1, + * queryOptions: { + * enabled: true, + * staleTime: 30000, + * gcTime: 300000, + * retry: 3, + * onSuccess: (data) => console.log('Pool key data received:', data) + * } + * }); + * ``` + */ +export function useGetPoolKeyFromPoolId({ + poolId, + chainId, + queryOptions = {}, +}: UseGetPoolKeyFromPoolIdOptions) { + return useQuery({ + queryKey: ["poolKey", poolId, chainId], + queryFn: () => getPoolKeyFromPoolId({ poolId, chainId }), + ...queryOptions, + }); +} diff --git a/src/index.ts b/src/index.ts index f8ed3a6..c485721 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,10 +4,12 @@ export * from "@/core/uniDevKitV4Factory"; // Hooks export * from "@/hooks/useGetPool"; +export * from "@/hooks/useGetPoolKeyFromPoolId"; export * from "@/hooks/useGetPosition"; export * from "@/hooks/useGetQuote"; // Utils +export * from "@/utils/getPoolKeyFromPoolId"; export * from "@/utils/getQuote"; export * from "@/utils/getTokens"; diff --git a/src/test/hooks/useGetPool.test.ts b/src/test/hooks/useGetPool.test.ts index ab47b4f..c4b0137 100644 --- a/src/test/hooks/useGetPool.test.ts +++ b/src/test/hooks/useGetPool.test.ts @@ -1,5 +1,4 @@ import { useGetPool } from "@/hooks/useGetPool"; -import type { UseGetPoolOptions } from "@/types/hooks/useGetPool"; import { getPool } from "@/utils/getPool"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { renderHook, waitFor } from "@testing-library/react"; @@ -101,14 +100,6 @@ describe("useGetPool", () => { expect(result.current.status).toBe("error"); }); - it("should throw error if no params provided", () => { - expect(() => { - renderHook(() => useGetPool(undefined as unknown as UseGetPoolOptions), { - wrapper, - }); - }).toThrow("No params provided"); - }); - it("should handle custom query options", async () => { const mockPool = { token0: new Token(1, USDC, 6, "USDC", "USD Coin"), diff --git a/src/test/hooks/useGetPoolKeyFromPoolId.test.ts b/src/test/hooks/useGetPoolKeyFromPoolId.test.ts new file mode 100644 index 0000000..27d3f01 --- /dev/null +++ b/src/test/hooks/useGetPoolKeyFromPoolId.test.ts @@ -0,0 +1,121 @@ +import { useGetPoolKeyFromPoolId } from "@/hooks/useGetPoolKeyFromPoolId"; +import { getPoolKeyFromPoolId } from "@/utils/getPoolKeyFromPoolId"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { renderHook, waitFor } from "@testing-library/react"; +import { jsx as _jsx } from "react/jsx-runtime"; +import type { Mock } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock the getPoolKeyFromPoolId function +vi.mock("@/utils/getPoolKeyFromPoolId", () => ({ + getPoolKeyFromPoolId: vi.fn(), +})); + +describe("useGetPoolKeyFromPoolId", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + vi.clearAllMocks(); + }); + + const wrapper = ({ children }: { children: React.ReactNode }) => + _jsx(QueryClientProvider, { client: queryClient, children }); + + it("should fetch pool key data successfully", async () => { + const mockPoolKey = { + currency0: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + currency1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + fee: 3000, + tickSpacing: 60, + hooks: "0x0000000000000000000000000000000000000000", + }; + + (getPoolKeyFromPoolId as Mock).mockResolvedValue(mockPoolKey); + + const { result } = renderHook( + () => + useGetPoolKeyFromPoolId({ + poolId: "0x1234", + chainId: 1, + }), + { wrapper }, + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.data).toEqual(mockPoolKey); + expect(result.current.error).toBeNull(); + expect(result.current.isLoading).toBe(false); + expect(result.current.status).toBe("success"); + expect(getPoolKeyFromPoolId).toHaveBeenCalledWith({ + poolId: "0x1234", + chainId: 1, + }); + }); + + it("should handle errors", async () => { + const error = new Error("Failed to fetch pool key"); + (getPoolKeyFromPoolId as Mock).mockRejectedValue(error); + + const { result } = renderHook( + () => + useGetPoolKeyFromPoolId({ + poolId: "0x1234", + chainId: 1, + }), + { wrapper }, + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBe(error); + expect(result.current.isLoading).toBe(false); + expect(result.current.status).toBe("error"); + }); + + it("should handle custom query options", async () => { + const mockPoolKey = { + currency0: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + currency1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + fee: 3000, + tickSpacing: 60, + hooks: "0x0000000000000000000000000000000000000000", + }; + + (getPoolKeyFromPoolId as Mock).mockResolvedValue(mockPoolKey); + + const { result } = renderHook( + () => + useGetPoolKeyFromPoolId({ + poolId: "0x1234", + chainId: 1, + queryOptions: { + enabled: true, + staleTime: 5000, + }, + }), + { wrapper }, + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.data).toEqual(mockPoolKey); + expect(result.current.error).toBeNull(); + expect(result.current.isLoading).toBe(false); + expect(result.current.status).toBe("success"); + }); +}); diff --git a/src/test/utils/getPoolKeyFromPoolId.test.ts b/src/test/utils/getPoolKeyFromPoolId.test.ts new file mode 100644 index 0000000..f51a425 --- /dev/null +++ b/src/test/utils/getPoolKeyFromPoolId.test.ts @@ -0,0 +1,129 @@ +import type { UniDevKitV4 } from "@/core/uniDevKitV4"; +import { getInstance } from "@/core/uniDevKitV4Factory"; +import { getPoolKeyFromPoolId } from "@/utils/getPoolKeyFromPoolId"; +import { describe, expect, it, vi } from "vitest"; + +// Mock the SDK instance +vi.mock("@/core/uniDevKitV4Factory", () => ({ + getInstance: vi.fn(), +})); + +describe("getPoolKeyFromPoolId", () => { + const mockPoolId = + "0x1234567890123456789012345678901234567890123456789012345678901234"; + const mockChainId = 1; + const expectedPoolId25Bytes = + "0x12345678901234567890123456789012345678901234567890"; + + it("should throw error if SDK instance not found", async () => { + vi.mocked(getInstance).mockReturnValue(undefined as unknown as UniDevKitV4); + + await expect( + getPoolKeyFromPoolId({ poolId: mockPoolId, chainId: mockChainId }), + ).rejects.toThrow("SDK not initialized"); + }); + + it("should return pool key when SDK instance exists", async () => { + const mockPoolKey = [ + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + 3000, + 60, + "0x0000000000000000000000000000000000000000", + ]; + + const mockClient = { + readContract: vi.fn().mockResolvedValue(mockPoolKey), + }; + + const mockSdk = { + getClient: vi.fn().mockReturnValue(mockClient), + getContractAddress: vi.fn().mockReturnValue("0xPositionManager"), + instance: {}, + createInstance: vi.fn(), + createClient: vi.fn(), + getChainId: vi.fn(), + getContract: vi.fn(), + } as unknown as UniDevKitV4; + + vi.mocked(getInstance).mockReturnValue(mockSdk); + + const result = await getPoolKeyFromPoolId({ + poolId: mockPoolId, + chainId: mockChainId, + }); + + expect(result).toEqual({ + currency0: mockPoolKey[0], + currency1: mockPoolKey[1], + fee: mockPoolKey[2], + tickSpacing: mockPoolKey[3], + hooks: mockPoolKey[4], + }); + expect(mockClient.readContract).toHaveBeenCalledWith({ + address: "0xPositionManager", + abi: expect.any(Object), + functionName: "poolKeys", + args: [expectedPoolId25Bytes], + }); + }); + + it("should handle contract read errors", async () => { + const mockClient = { + readContract: vi + .fn() + .mockRejectedValue(new Error("Contract read failed")), + }; + + const mockSdk = { + getClient: vi.fn().mockReturnValue(mockClient), + getContractAddress: vi.fn().mockReturnValue("0xPositionManager"), + instance: {}, + createInstance: vi.fn(), + createClient: vi.fn(), + getChainId: vi.fn(), + getContract: vi.fn(), + } as unknown as UniDevKitV4; + + vi.mocked(getInstance).mockReturnValue(mockSdk); + + await expect( + getPoolKeyFromPoolId({ poolId: mockPoolId, chainId: mockChainId }), + ).rejects.toThrow("Contract read failed"); + }); + + it("should correctly convert poolId from 32 bytes to 25 bytes", async () => { + const mockPoolKey = [ + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + 3000, + 60, + "0x0000000000000000000000000000000000000000", + ]; + + const mockClient = { + readContract: vi.fn().mockResolvedValue(mockPoolKey), + }; + + const mockSdk = { + getClient: vi.fn().mockReturnValue(mockClient), + getContractAddress: vi.fn().mockReturnValue("0xPositionManager"), + instance: {}, + createInstance: vi.fn(), + createClient: vi.fn(), + getChainId: vi.fn(), + getContract: vi.fn(), + } as unknown as UniDevKitV4; + + vi.mocked(getInstance).mockReturnValue(mockSdk); + + await getPoolKeyFromPoolId({ poolId: mockPoolId, chainId: mockChainId }); + + // Verify that the poolId was correctly converted + expect(mockClient.readContract).toHaveBeenCalledWith( + expect.objectContaining({ + args: [expectedPoolId25Bytes], + }), + ); + }); +}); diff --git a/src/types/hooks/index.ts b/src/types/hooks/index.ts index 8bcb45f..c21a71a 100644 --- a/src/types/hooks/index.ts +++ b/src/types/hooks/index.ts @@ -1,3 +1,4 @@ export * from "@/types/hooks/useGetPool"; +export * from "@/types/hooks/useGetPoolKeyFromPoolId"; export * from "@/types/hooks/useGetPosition"; export * from "@/types/hooks/useGetQuote"; diff --git a/src/types/hooks/useGetPool.ts b/src/types/hooks/useGetPool.ts index 9a7c4bb..8cbdd0f 100644 --- a/src/types/hooks/useGetPool.ts +++ b/src/types/hooks/useGetPool.ts @@ -7,7 +7,7 @@ import type { Pool } from "@uniswap/v4-sdk"; */ export type UseGetPoolOptions = { /** Initial pool parameters */ - params?: PoolParams; + params: PoolParams; /** Chain ID */ chainId?: number; /** React Query options */ diff --git a/src/types/hooks/useGetPoolKeyFromPoolId.ts b/src/types/hooks/useGetPoolKeyFromPoolId.ts new file mode 100644 index 0000000..59e8f6a --- /dev/null +++ b/src/types/hooks/useGetPoolKeyFromPoolId.ts @@ -0,0 +1,17 @@ +import type { PoolKey } from "@/types/utils/getPoolKeyFromPoolId"; +import type { UseQueryOptions } from "@tanstack/react-query"; + +/** + * Configuration options for the useGetPoolKeyFromPoolId hook. + */ +export type UseGetPoolKeyFromPoolIdOptions = { + /** The 32-byte pool ID in hex format (0x...) */ + poolId: `0x${string}`; + /** Chain ID */ + chainId?: number; + /** React Query options */ + queryOptions?: Omit< + UseQueryOptions, + "queryKey" + >; +}; diff --git a/src/types/utils/getPoolKeyFromPoolId.ts b/src/types/utils/getPoolKeyFromPoolId.ts new file mode 100644 index 0000000..803e691 --- /dev/null +++ b/src/types/utils/getPoolKeyFromPoolId.ts @@ -0,0 +1,25 @@ +/** + * Parameters required for retrieving pool key information. + */ +export interface GetPoolKeyFromPoolIdParams { + /** The 32-byte pool ID in hex format (0x...) */ + poolId: `0x${string}`; + /** Optional chain ID where the pool exists */ + chainId?: number; +} + +/** + * Pool key information returned from the contract + */ +export interface PoolKey { + /** First token address in the pool */ + currency0: `0x${string}`; + /** Second token address in the pool */ + currency1: `0x${string}`; + /** Fee tier of the pool */ + fee: number; + /** Tick spacing of the pool */ + tickSpacing: number; + /** Hooks contract address */ + hooks: `0x${string}`; +} diff --git a/src/types/utils/index.ts b/src/types/utils/index.ts index 72fdf79..772a1ce 100644 --- a/src/types/utils/index.ts +++ b/src/types/utils/index.ts @@ -1,2 +1,4 @@ export * from "@/types/utils/getPool"; +export * from "@/types/utils/getPoolKeyFromPoolId"; +export * from "@/types/utils/getPosition"; export * from "@/types/utils/getQuote"; diff --git a/src/utils/getPoolKeyFromPoolId.ts b/src/utils/getPoolKeyFromPoolId.ts new file mode 100644 index 0000000..294543f --- /dev/null +++ b/src/utils/getPoolKeyFromPoolId.ts @@ -0,0 +1,41 @@ +import { V4PositionManagerAbi } from "@/constants/abis/V4PositionMananger"; +import { getInstance } from "@/core/uniDevKitV4Factory"; +import type { + GetPoolKeyFromPoolIdParams, + PoolKey, +} from "@/types/utils/getPoolKeyFromPoolId"; + +/** + * Retrieves the pool key information for a given pool ID. + * @param params Parameters containing the pool ID and optional chain ID + * @returns Promise resolving to the pool key containing currency0, currency1, fee, tickSpacing, and hooks + * @throws Error if SDK instance is not found + */ +export async function getPoolKeyFromPoolId({ + poolId, + chainId, +}: GetPoolKeyFromPoolIdParams): Promise { + const sdk = getInstance(chainId); + if (!sdk) throw new Error("SDK not initialized"); + + const client = sdk.getClient(); + const positionManager = sdk.getContractAddress("positionManager"); + + const poolId25Bytes = `0x${poolId.slice(2, 52)}` as `0x${string}`; + + const [currency0, currency1, fee, tickSpacing, hooks] = + await client.readContract({ + address: positionManager, + abi: V4PositionManagerAbi, + functionName: "poolKeys", + args: [poolId25Bytes], + }); + + return { + currency0, + currency1, + fee, + tickSpacing, + hooks, + }; +}