diff --git a/README.md b/README.md index 3acf23c..3e7e6ef 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,14 @@ > Modern TypeScript SDK for integrating Uniswap V4 into your dapp. > **Early version:** API may change rapidly. ---- +## Features + +- πŸš€ Full TypeScript support +- πŸ”„ Multi-chain support out of the box +- πŸ“¦ Zero dependencies (except peer deps) +- πŸ” Comprehensive error handling +- πŸ§ͺ Fully tested +- πŸ“š Well documented ## Install @@ -22,10 +29,10 @@ npm install uniswap-dev-kit ### 1. Configure and create SDK instances ```ts -import { createInstance } from "uniswap-dev-kit"; +import { UniDevKitV4 } from "uniswap-dev-kit"; // Create instance for Ethereum mainnet -createInstance({ +const ethInstance = new UniDevKitV4({ chainId: 1, rpcUrl: "https://eth.llamarpc.com", contracts: { @@ -40,7 +47,7 @@ createInstance({ }); // Create instance for another chain (e.g., Base) -createInstance({ +const baseInstance = new UniDevKitV4({ chainId: 8453, rpcUrl: "https://mainnet.base.org", contracts: { @@ -49,96 +56,104 @@ createInstance({ }); ``` -The SDK automatically manages multiple instances based on chainId. When using hooks or utilities, just specify the chainId to use the corresponding instance: +### 2. Get a quote ```ts -// Will use Ethereum mainnet instance -const ethPool = await getPool({ tokens: [...] }, 1); - -// Will use Base instance -const basePool = await getPool({ tokens: [...] }, 8453); +import { parseEther } from "viem"; -// If you only have one instance, chainId is optional -const singleChainPool = await getPool({ tokens: [...] }); +const quote = await ethInstance.getQuote({ + tokens: [ + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH + ], + feeTier: 3000, + amountIn: parseEther("1"), + zeroForOne: true +}); +console.log(quote.amountOut); ``` -### 2. Get a quote (vanilla JS/TS) +### 3. Get a pool ```ts -import { getQuote } from "uniswap-dev-kit"; -import { parseEther } from "viem"; +const pool = await ethInstance.getPool({ + tokens: [ + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ], + feeTier: 3000 +}); +console.log(pool.liquidity.toString()); +``` -const quote = await getQuote( - { - tokens: [ - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH - ], - feeTier: 3000, - tickSpacing: 60, - amountIn: parseEther("1"), - }, - 1, - { - enabled: true, - staleTime: 30000, - gcTime: 300000, - retry: 3, - onSuccess: (data) => console.log("Quote received:", data), - }, -); -console.log(quote.amountOut); +### 4. Get a position + +```ts +const position = await ethInstance.getPosition({ + tokenId: "123" +}); +console.log({ + token0: position.token0.symbol, + token1: position.token1.symbol, + liquidity: position.position.liquidity.toString() +}); ``` -### 3. Use in React (with hooks) +## Advanced Usage -#### Get a quote +### Error Handling -```tsx -import { useGetQuote } from "uniswap-dev-kit"; -import { parseEther } from "viem"; +All SDK functions include comprehensive error handling: -function QuoteComponent() { - const { data, isLoading, error } = useGetQuote({ - params: { - tokens: [ - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - ], - feeTier: 3000, - tickSpacing: 60, - amountIn: parseEther("1"), - zeroForOne: true - }, - chainId: 1 +```ts +try { + const quote = await ethInstance.getQuote({ + tokens: [token0, token1], + feeTier: 3000, + amountIn: parseEther("1"), + zeroForOne: true }); - - if (isLoading) return Loading...; - if (error) return Error: {error.message}; - return Quote: {data?.amountOut?.toString()}; +} catch (error) { + // Handle specific error types + if (error.message.includes("insufficient liquidity")) { + // Handle liquidity error + } else if (error.message.includes("invalid pool")) { + // Handle pool error + } } ``` -#### Get a pool +### Using with React + +You can use the SDK with React Query for data fetching: ```tsx -import { useGetPool } from "uniswap-dev-kit"; +import { useQuery } from '@tanstack/react-query'; +import { UniDevKitV4 } from 'uniswap-dev-kit'; -function PoolComponent() { - const { data, isLoading, error } = useGetPool({ - params: { +// Create instance once +const sdk = new UniDevKitV4({ + chainId: 1, + rpcUrl: "https://eth.llamarpc.com", + contracts: { + // ... contract addresses + } +}); + +// Simple hook for quotes +function useQuote() { + return useQuery({ + queryKey: ['quote'], + queryFn: () => sdk.getQuote({ tokens: [ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" ], - fee: 3000, - }, - chainId: 1 + feeTier: 3000, + amountIn: parseEther("1"), + zeroForOne: true + }) }); - - if (isLoading) return Loading...; - if (error) return Error: {error.message}; - return Pool: {JSON.stringify(data)}; } ``` @@ -173,10 +188,8 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines. ## FAQ -- **Which React versions are supported?** - React 18+ (see peerDependencies) - **Does it work in Node and browser?** - Yes, but hooks are React-only. + Yes, works in both environments. - **Can I use my own ABIs?** Yes, but Uniswap V4 ABIs are included. diff --git a/package.json b/package.json index b4290ec..84349e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uniswap-dev-kit", - "version": "1.0.4", + "version": "1.0.7", "description": "A modern TypeScript library for integrating Uniswap into your dapp.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/core/uniDevKitV4.ts b/src/core/uniDevKitV4.ts index a590047..daaf8f4 100644 --- a/src/core/uniDevKitV4.ts +++ b/src/core/uniDevKitV4.ts @@ -1,5 +1,20 @@ import { getChainById } from "@/constants/chains"; import type { UniDevKitV4Config, UniDevKitV4Instance } from "@/types/core"; +import type { PoolParams } from "@/types/utils/getPool"; +import type { GetPoolKeyFromPoolIdParams } from "@/types/utils/getPoolKeyFromPoolId"; +import type { + GetPositionParams, + GetPositionResponse, +} from "@/types/utils/getPosition"; +import type { QuoteParams, QuoteResponse } from "@/types/utils/getQuote"; +import type { GetTokensParams } from "@/types/utils/getTokens"; +import { getPool } from "@/utils/getPool"; +import { getPoolKeyFromPoolId } from "@/utils/getPoolKeyFromPoolId"; +import { getPosition } from "@/utils/getPosition"; +import { getQuote } from "@/utils/getQuote"; +import { getTokens } from "@/utils/getTokens"; +import type { Token } from "@uniswap/sdk-core"; +import type { Pool, PoolKey } from "@uniswap/v4-sdk"; import { http, type Address, @@ -21,40 +36,19 @@ export class UniDevKitV4 { * @throws Will throw an error if the configuration is invalid. */ constructor(config: UniDevKitV4Config) { - this.instance = this.createInstance(config); - } + const chain = getChainById(config.chainId); + const client = createPublicClient({ + chain, + transport: http(config.rpcUrl || chain.rpcUrls.default.http[0]), + }); - /** - * Creates a new internal instance for the SDK. - * This method is used internally to reset the instance when the configuration changes. - * @param config - The complete configuration for the SDK. - * @returns A new instance of UniDevKitV4. - */ - private createInstance(config: UniDevKitV4Config): UniDevKitV4Instance { - const client = this.createClient(config); - return { + this.instance = { client: client as PublicClient, - chainId: config.chainId, + chain, contracts: config.contracts, }; } - /** - * Creates a new PublicClient for the specified chain ID and RPC URL. - * @param config - The complete configuration for the SDK. - * @returns A new PublicClient instance. - */ - private createClient(config: UniDevKitV4Config) { - const { chainId, rpcUrl } = config; - - const chain = getChainById(chainId); - - return createPublicClient({ - chain, - transport: http(rpcUrl || chain.rpcUrls.default.http[0]), - }); - } - /** * Returns the current PublicClient instance. * @returns The current PublicClient. @@ -68,7 +62,7 @@ export class UniDevKitV4 { * @returns The chain ID currently configured. */ getChainId(): number { - return this.instance.chainId; + return this.instance.chain.id; } /** @@ -94,12 +88,48 @@ export class UniDevKitV4 { } /** - * Updates the SDK configuration with a complete new set of parameters. - * This method will reset the client and instance to reflect the new configuration. - * @param config - The complete configuration for the SDK. - * @throws Will throw an error if the configuration is invalid. + * Retrieves a Uniswap V4 pool instance for a given token pair. + * @param params Pool parameters including tokens, fee tier, tick spacing, and hooks configuration + * @returns Promise resolving to pool data + * @throws Error if pool data cannot be fetched */ - updateConfig(config: UniDevKitV4Config): void { - this.instance = this.createInstance(config); + async getPool(params: PoolParams): Promise { + return getPool(params, this.instance); + } + + /** + * Retrieves token information for a given array of token addresses. + * @param params Parameters including token addresses + * @returns Promise resolving to Token instances for each token address. + * @throws Error if token data cannot be fetched + */ + async getTokens(params: GetTokensParams): Promise { + return getTokens(params, this.instance); + } + + /** + * Retrieves a Uniswap V4 position information for a given token ID. + * @param params Position parameters including token ID + * @returns Promise resolving to position data including pool, token0, token1, poolId, and tokenId + * @throws Error if SDK instance is not found or if position data is invalid + */ + async getPosition(params: GetPositionParams): Promise { + return getPosition(params, this.instance); + } + + /** + * Retrieves a Uniswap V4 quote for a given token pair and amount in. + * @param params Quote parameters including token pair and amount in + * @returns Promise resolving to quote data including amount out, estimated gas used, and timestamp + * @throws Error if SDK instance is not found or if quote data is invalid + */ + async getQuote(params: QuoteParams): Promise { + return getQuote(params, this.instance); + } + + async getPoolKeyFromPoolId( + params: GetPoolKeyFromPoolIdParams, + ): Promise { + return getPoolKeyFromPoolId(params, this.instance); } } diff --git a/src/core/uniDevKitV4Factory.ts b/src/core/uniDevKitV4Factory.ts deleted file mode 100644 index 73e5330..0000000 --- a/src/core/uniDevKitV4Factory.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { UniDevKitV4 } from "@/core/uniDevKitV4"; -import type { UniDevKitV4Config } from "@/types"; - -const instances = new Map(); - -/** - * Singleton factory for managing multiple UniDevKitV4 instances. - * This approach ensures that each chain has a single instance, - * reducing the risk of duplicate clients and configuration mismatches. - */ -export function createInstance(config: UniDevKitV4Config): UniDevKitV4 { - const { chainId } = config; - - // Return existing instance if it exists - if (instances.has(chainId)) { - console.warn( - `Instance for chain ID ${chainId} already exists. Reusing existing instance.`, - ); - const instance = instances.get(chainId); - if (!instance) - throw new Error("Unexpected: Instance not found after has check"); - return instance; - } - - // Create a new instance if it doesn't exist - const instance = new UniDevKitV4(config); - instances.set(chainId, instance); - return instance; -} - -/** - * Retrieves an existing UniDevKitV4 instance. - * If there is only one instance, it returns it without requiring a chain ID. - * If there are multiple instances, it requires a chain ID. - * @param chainId - (Optional) The chain ID to retrieve. - * @returns The existing UniDevKitV4 instance. - * @throws Will throw an error if the instance does not exist. - */ -export function getInstance(chainId?: number): UniDevKitV4 { - const instanceCount = instances.size; - - // Return the single instance if there's only one - if (instanceCount === 1 && !chainId) { - return Array.from(instances.values())[0]; - } - - // Require chainId if there are multiple instances - if (instanceCount > 1 && !chainId) { - throw new Error( - `Multiple instances found. Please specify a chain ID. Available chains: ${listChainIds().join(", ")}`, - ); - } - - if (!chainId) throw new Error("Chain ID is required"); - // Return the instance for the specified chain ID - const instance = instances.get(chainId); - if (!instance) { - throw new Error( - `No instance found for chain ID ${chainId}. Make sure to call createInstance() first.`, - ); - } - - return instance; -} - -/** - * Clears all registered instances. - */ -export function resetInstances(): void { - instances.clear(); -} - -/** - * Lists all registered chain IDs. - * @returns An array of all registered chain IDs. - */ -export function listChainIds(): number[] { - return Array.from(instances.keys()); -} - -/** - * Removes a specific instance by chain ID. - * @param chainId - The chain ID to remove. - */ -export function removeInstance(chainId: number): void { - instances.delete(chainId); -} diff --git a/src/hooks/index.ts b/src/hooks/index.ts deleted file mode 100644 index 3abc01c..0000000 --- a/src/hooks/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index ee60619..0000000 --- a/src/hooks/useGetPool.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { UseGetPoolOptions } from "@/types/hooks/useGetPool"; -import { getPool } from "@/utils/getPool"; -import { useQuery } from "@tanstack/react-query"; -import type { Pool } from "@uniswap/v4-sdk"; - -/** - * React hook for fetching Uniswap V4 pool data using React Query. - * Handles caching, loading states, and error handling automatically. - * - * @param options - Configuration options for the hook - * @returns Query result containing pool data, loading state, error state, and refetch function - * - * @example - * ```tsx - * const { data, isLoading, error, refetch } = useGetPool({ - * params: { - * tokens: [token0, token1], - * fee: FeeTier.MEDIUM, - * hooks: "0x0000000000000000000000000000000000000000" - * }, - * chainId: 1, - * queryOptions: { - * enabled: true, - * staleTime: 30000, - * gcTime: 300000, - * retry: 3, - * onSuccess: (data) => console.log('Pool data received:', data) - * } - * }); - * ``` - */ - -export function useGetPool({ - params, - chainId, - queryOptions = {}, -}: UseGetPoolOptions) { - return useQuery({ - 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 deleted file mode 100644 index be87c67..0000000 --- a/src/hooks/useGetPoolKeyFromPoolId.ts +++ /dev/null @@ -1,37 +0,0 @@ -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/hooks/useGetPosition.ts b/src/hooks/useGetPosition.ts deleted file mode 100644 index 4f9a0b1..0000000 --- a/src/hooks/useGetPosition.ts +++ /dev/null @@ -1,47 +0,0 @@ -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, - }); -} diff --git a/src/hooks/useGetQuote.ts b/src/hooks/useGetQuote.ts deleted file mode 100644 index 315e410..0000000 --- a/src/hooks/useGetQuote.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { UseGetQuoteOptions } from "@/types/hooks/useGetQuote"; -import type { QuoteParams, QuoteResponse } from "@/types/utils/getQuote"; -import { getQuote } from "@/utils/getQuote"; -import { useQuery } from "@tanstack/react-query"; - -/** - * React hook for fetching quotes from Uniswap V4 using React Query. - * Handles caching, loading states, and error handling automatically. - * - * @param options - Configuration options for the hook - * @returns Query result containing quote data, loading state, error state, and refetch function - * - * @example - * ```tsx - * const { data, isLoading, error, refetch } = useGetQuote({ - * params: { - * tokens: [token0, token1], - * feeTier: 3000, - * tickSpacing: 60, - * amountIn: parseEther("1"), - * zeroForOne: true - * }, - * queryOptions: { - * enabled: true, - * staleTime: 30000, - * gcTime: 300000, - * retry: 3, - * onSuccess: (data) => console.log('Quote received:', data) - * } - * }); - * ``` - */ -function serializeParams(params?: QuoteParams) { - if (!params) return undefined; - return { - ...params, - amountIn: params.amountIn?.toString(), - }; -} - -export function useGetQuote({ - params, - chainId, - queryOptions = {}, -}: UseGetQuoteOptions = {}) { - if (!params) throw new Error("No params provided"); - return useQuery({ - queryKey: ["quote", serializeParams(params), chainId], - queryFn: () => getQuote(params, chainId), - ...queryOptions, - }); -} diff --git a/src/index.ts b/src/index.ts index c485721..c462011 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,3 @@ -// Core export * from "@/core/uniDevKitV4"; -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"; - -// Types -export * from "@/types"; -export * from "@/types/core"; - -// Errors export * from "@/errors"; +export * from "@/types"; diff --git a/src/test/core/uniDevKitV4.test.ts b/src/test/core/uniDevKitV4.test.ts index 07d9e48..d74d4a6 100644 --- a/src/test/core/uniDevKitV4.test.ts +++ b/src/test/core/uniDevKitV4.test.ts @@ -43,7 +43,7 @@ describe("UniDevKitV4", () => { chainId: 8453, rpcUrl: "https://base-rpc.com", }; - sdk.updateConfig(newConfig); + sdk = new UniDevKitV4(newConfig); expect(sdk.getChainId()).toBe(newConfig.chainId); }); diff --git a/src/test/core/uniDevKitV4Factory.test.ts b/src/test/core/uniDevKitV4Factory.test.ts deleted file mode 100644 index de261ce..0000000 --- a/src/test/core/uniDevKitV4Factory.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { - createInstance, - getInstance, - listChainIds, - removeInstance, - resetInstances, -} from "@/core/uniDevKitV4Factory"; -import type { UniDevKitV4Config } from "@/types"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -describe("UniDevKitV4Factory", () => { - let config: UniDevKitV4Config; - - beforeEach(() => { - config = { - chainId: 1, - rpcUrl: "https://eth.llamarpc.com", - contracts: { - poolManager: "0x1234567890123456789012345678901234567890", - positionDescriptor: "0x1234567890123456789012345678901234567890", - positionManager: "0x1234567890123456789012345678901234567890", - quoter: "0x1234567890123456789012345678901234567890", - stateView: "0x1234567890123456789012345678901234567890", - universalRouter: "0x1234567890123456789012345678901234567890", - permit2: "0x1234567890123456789012345678901234567890", - }, - }; - }); - - afterEach(() => { - resetInstances(); - }); - - it("should create and get instance", () => { - const instance = createInstance(config); - const retrievedInstance = getInstance(config.chainId); - expect(retrievedInstance).toBe(instance); - }); - - it("should reuse existing instance", () => { - const instance1 = createInstance(config); - const instance2 = createInstance(config); - expect(instance1).toBe(instance2); - }); - - it("should list chain IDs", () => { - createInstance(config); - const chainIds = listChainIds(); - expect(chainIds).toContain(config.chainId); - }); - - it("should remove instance", () => { - createInstance(config); - removeInstance(config.chainId); - expect(() => getInstance(config.chainId)).toThrow(); - }); - - it("should reset all instances", () => { - createInstance(config); - resetInstances(); - expect(() => getInstance(config.chainId)).toThrow(); - }); - - it("should throw when getting non-existent instance", () => { - expect(() => getInstance(999)).toThrow(); - }); - - it("should get default instance when only one chain is configured", () => { - const instance = createInstance(config); - const defaultInstance = getInstance(); - expect(defaultInstance).toBe(instance); - }); - - it("should throw when getting default instance with multiple chains", () => { - createInstance(config); - createInstance({ ...config, chainId: 8453 }); - expect(() => getInstance()).toThrow( - "Multiple instances found. Please specify a chain ID. Available chains: 1, 8453", - ); - }); - - describe("Multiple chain configurations", () => { - it("should handle multiple chains with different RPCs", () => { - const mainnet = createInstance(config); - const base = createInstance({ - ...config, - chainId: 8453, - rpcUrl: "https://base-rpc.com", - }); - const arbitrum = createInstance({ - ...config, - chainId: 42161, - rpcUrl: "https://arbitrum-rpc.com", - }); - - expect(getInstance(1)).toBe(mainnet); - expect(getInstance(8453)).toBe(base); - expect(getInstance(42161)).toBe(arbitrum); - }); - - it("should handle multiple chains with different native currencies", () => { - const mainnet = createInstance(config); - const polygon = createInstance({ - ...config, - chainId: 137, - }); - - expect(getInstance(1)).toBe(mainnet); - expect(getInstance(137)).toBe(polygon); - }); - - it("should list all configured chain IDs", () => { - createInstance(config); // mainnet - createInstance({ ...config, chainId: 8453 }); // base - createInstance({ ...config, chainId: 137 }); // polygon - - const chainIds = listChainIds(); - expect(chainIds).toHaveLength(3); - expect(chainIds).toContain(1); - expect(chainIds).toContain(8453); - expect(chainIds).toContain(137); - }); - - it("should remove specific chain instance", () => { - createInstance(config); // mainnet - createInstance({ ...config, chainId: 8453 }); // base - createInstance({ ...config, chainId: 137 }); // polygon - - removeInstance(8453); - const chainIds = listChainIds(); - expect(chainIds).toHaveLength(2); - expect(chainIds).not.toContain(8453); - expect(() => getInstance(8453)).toThrow(); - }); - }); -}); diff --git a/src/test/helpers/sdkInstance.ts b/src/test/helpers/sdkInstance.ts new file mode 100644 index 0000000..3d9a4ca --- /dev/null +++ b/src/test/helpers/sdkInstance.ts @@ -0,0 +1,31 @@ +import type { UniDevKitV4Instance } from "@/types/core"; +import { http, createPublicClient } from "viem"; +import { mainnet } from "viem/chains"; +import { vi } from "vitest"; + +export const createMockSdkInstance = ( + overrides?: Partial, +): UniDevKitV4Instance => { + const client = createPublicClient({ + chain: mainnet, + transport: http(), + }); + + // Mock the multicall function + vi.spyOn(client, "multicall").mockImplementation(async () => []); + + return { + client, + chain: mainnet, + contracts: { + poolManager: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8", + positionDescriptor: "0x91a40C733c97c6Dc441A0071F8FbF4907dd13151", + positionManager: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", + quoter: "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", + stateView: "0x4eD4C8B7eF27d4d242c4D1267E1B1E39c14b9E73", + universalRouter: "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", + permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + }, + ...overrides, + }; +}; diff --git a/src/test/hooks/useGetPool.test.ts b/src/test/hooks/useGetPool.test.ts deleted file mode 100644 index c4b0137..0000000 --- a/src/test/hooks/useGetPool.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { useGetPool } from "@/hooks/useGetPool"; -import { getPool } from "@/utils/getPool"; -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 type { Mock } from "vitest"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -// Mock the getPool function -vi.mock("@/utils/getPool", () => ({ - getPool: vi.fn(), -})); - -// Mock tokens -const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as const; -const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" as const; - -describe("useGetPool", () => { - 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 data successfully", async () => { - const mockPool = { - token0: new Token(1, USDC, 6, "USDC", "USD Coin"), - token1: new Token(1, WETH, 18, "WETH", "Wrapped Ether"), - fee: 3000, - tickSpacing: 60, - }; - - (getPool as Mock).mockReturnValue(mockPool); - - const { result } = renderHook( - () => - useGetPool({ - params: { - tokens: [USDC, WETH], - fee: 3000, - }, - chainId: 1, - }), - { wrapper }, - ); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - expect(result.current.data).toEqual(mockPool); - expect(result.current.error).toBeNull(); - expect(result.current.isLoading).toBe(false); - expect(result.current.status).toBe("success"); - expect(getPool).toHaveBeenCalledWith( - { - tokens: [USDC, WETH], - fee: 3000, - }, - 1, - ); - }); - - it("should handle errors", async () => { - const error = new Error("Failed to fetch pool"); - (getPool as Mock).mockImplementation(() => { - throw error; - }); - - const { result } = renderHook( - () => - useGetPool({ - params: { - tokens: [USDC, WETH], - fee: 3000, - }, - 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 mockPool = { - token0: new Token(1, USDC, 6, "USDC", "USD Coin"), - token1: new Token(1, WETH, 18, "WETH", "Wrapped Ether"), - fee: 3000, - tickSpacing: 60, - }; - - (getPool as Mock).mockReturnValue(mockPool); - - const { result } = renderHook( - () => - useGetPool({ - params: { - tokens: [USDC, WETH], - fee: 3000, - }, - chainId: 1, - queryOptions: { - enabled: true, - staleTime: 5000, - }, - }), - { wrapper }, - ); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - expect(result.current.data).toEqual(mockPool); - expect(result.current.error).toBeNull(); - expect(result.current.isLoading).toBe(false); - expect(result.current.status).toBe("success"); - }); -}); diff --git a/src/test/hooks/useGetPoolKeyFromPoolId.test.ts b/src/test/hooks/useGetPoolKeyFromPoolId.test.ts deleted file mode 100644 index 27d3f01..0000000 --- a/src/test/hooks/useGetPoolKeyFromPoolId.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -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/hooks/useGetPosition.test.ts b/src/test/hooks/useGetPosition.test.ts deleted file mode 100644 index 258a9d7..0000000 --- a/src/test/hooks/useGetPosition.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -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", - ), - position: { - amounts: { - amount0: "1000000", - amount1: "1000000000000000000", - }, - tickLower: -100, - tickUpper: 100, - liquidity: BigInt("1000000000000000000"), - }, - pool: {}, - poolId: "0x1234567890123456789012345678901234567890", - tokenId: "123", - }; - - (getPosition as unknown as ReturnType).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({ tokenId: "123" }, 1); - }); - - it("should handle errors", async () => { - const error = new Error("Failed to fetch position"); - (getPosition as unknown as ReturnType).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", - ), - position: { - amounts: { - amount0: "1000000", - amount1: "1000000000000000000", - }, - tickLower: -100, - tickUpper: 100, - liquidity: BigInt("1000000000000000000"), - }, - pool: {}, - poolId: "0x1234567890123456789012345678901234567890", - tokenId: "123", - }; - - (getPosition as unknown as ReturnType).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); - }); -}); diff --git a/src/test/hooks/useGetQuote.test.ts b/src/test/hooks/useGetQuote.test.ts deleted file mode 100644 index 65bc1bb..0000000 --- a/src/test/hooks/useGetQuote.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { useGetQuote } from "@/hooks/useGetQuote"; -import { FeeTier } from "@/types/utils/getPool"; -import { getQuote } from "@/utils/getQuote"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { renderHook, waitFor } from "@testing-library/react"; -import { jsx as _jsx } from "react/jsx-runtime"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -// Mock getQuote -vi.mock("@/utils/getQuote", () => ({ - getQuote: vi.fn(), -})); - -// Real token addresses on Ethereum mainnet -const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; -const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; - -describe("useGetQuote", () => { - let queryClient: QueryClient; - - beforeEach(() => { - queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - }); - vi.resetAllMocks(); - }); - - const wrapper = ({ children }: { children: React.ReactNode }) => - _jsx(QueryClientProvider, { client: queryClient, children: children }); - - it("should fetch quote data successfully", async () => { - const mockQuote = { - amountOut: BigInt("1000000000000000000"), - estimatedGasUsed: BigInt("100000"), - timestamp: Date.now(), - }; - - (getQuote as unknown as ReturnType).mockResolvedValueOnce( - mockQuote, - ); - - const { result } = renderHook( - () => - useGetQuote({ - params: { - tokens: [USDC, WETH], - feeTier: FeeTier.MEDIUM, - tickSpacing: 60, - amountIn: BigInt("1000000"), - zeroForOne: true, - }, - 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(mockQuote); - expect(getQuote).toHaveBeenCalledWith( - { - tokens: [USDC, WETH], - feeTier: FeeTier.MEDIUM, - tickSpacing: 60, - amountIn: BigInt("1000000"), - zeroForOne: true, - }, - 1, - ); - }); - - it("should handle errors", async () => { - const error = new Error("Failed to fetch quote"); - (getQuote as unknown as ReturnType).mockRejectedValueOnce( - error, - ); - - const { result } = renderHook( - () => - useGetQuote({ - params: { - tokens: [USDC, WETH], - feeTier: FeeTier.MEDIUM, - tickSpacing: 60, - amountIn: BigInt("1000000"), - zeroForOne: true, - }, - chainId: 1, - }), - { wrapper }, - ); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - expect(result.current.error).toEqual(error); - }); - - it("should throw error if no params provided", () => { - expect(() => { - renderHook(() => useGetQuote(), { wrapper }); - }).toThrow("No params provided"); - }); - - it("should handle custom query options", async () => { - const mockQuote = { - amountOut: BigInt("1000000000000000000"), - estimatedGasUsed: BigInt("100000"), - timestamp: Date.now(), - }; - - (getQuote as unknown as ReturnType).mockResolvedValueOnce( - mockQuote, - ); - - const { result } = renderHook( - () => - useGetQuote({ - params: { - tokens: [USDC, WETH], - feeTier: FeeTier.MEDIUM, - tickSpacing: 60, - amountIn: BigInt("1000000"), - zeroForOne: true, - }, - chainId: 1, - queryOptions: { - enabled: true, - staleTime: 30000, - }, - }), - { wrapper }, - ); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - expect(result.current.data).toEqual(mockQuote); - }); -}); diff --git a/src/test/utils/getPool.test.ts b/src/test/utils/getPool.test.ts index ebf849f..b3f957c 100644 --- a/src/test/utils/getPool.test.ts +++ b/src/test/utils/getPool.test.ts @@ -1,17 +1,17 @@ +import { createMockSdkInstance } from "@/test/helpers/sdkInstance"; import { FeeTier } from "@/types/utils/getPool"; import { getPool } from "@/utils/getPool"; import { Token } from "@uniswap/sdk-core"; +import { Pool } from "@uniswap/v4-sdk"; import { type Address, zeroAddress } from "viem"; import { beforeEach, describe, expect, it, vi } from "vitest"; const mockGetInstance = vi.fn(); const mockGetTokens = vi.fn(); -const mockGetClient = vi.fn(); const mockUseReadContracts = vi.fn(); vi.mock("@/core/uniDevKitV4Factory", () => ({ getInstance: () => mockGetInstance(), - getClient: () => mockGetClient(), })); vi.mock("@/utils/getTokens", () => ({ @@ -22,38 +22,33 @@ vi.mock("wagmi", () => ({ useReadContracts: () => mockUseReadContracts(), })); -describe("useV4Pool", () => { +describe("getPool", () => { // USDC and WETH on Mainnet const mockTokens: [Address, Address] = [ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", ]; - const mockChainId = 1; + + const mockDeps = createMockSdkInstance(); beforeEach(() => { vi.resetAllMocks(); }); - it("should throw error if SDK instance not found", async () => { - mockGetInstance.mockReturnValueOnce(undefined); - await expect( - getPool( - { - tokens: mockTokens, - fee: FeeTier.MEDIUM, - }, - mockChainId, - ), - ).rejects.toThrow("SDK not found"); - }); + 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"), + ]; - it("should throw error if token instances not found", async () => { - mockGetInstance.mockReturnValueOnce({ - client: { multicall: vi.fn() }, - getClient: vi.fn(), - getContractAddress: vi.fn(), - }); - mockGetTokens.mockResolvedValueOnce(null); + const mockPoolData = [ + [mockTokens[0], mockTokens[1], FeeTier.MEDIUM, 0, zeroAddress], // poolKeys with 0 tickSpacing + null, // slot0 + null, // liquidity + ]; + + mockGetTokens.mockResolvedValueOnce(mockTokenInstances); + vi.mocked(mockDeps.client.multicall).mockResolvedValueOnce(mockPoolData); await expect( getPool( @@ -61,12 +56,12 @@ describe("useV4Pool", () => { tokens: mockTokens, fee: FeeTier.MEDIUM, }, - mockChainId, + mockDeps, ), - ).rejects.toThrow("Failed to fetch token instances"); + ).rejects.toThrow("Pool does not exist"); }); - it("should return pool data when pool exists", async () => { + it("should return pool when it exists", async () => { const mockTokenInstances = [ new Token(1, mockTokens[0], 18, "TOKEN0", "Token 0"), new Token(1, mockTokens[1], 18, "TOKEN1", "Token 1"), @@ -80,61 +75,22 @@ describe("useV4Pool", () => { "1000000000000000000", ]; - const mockClient = { - multicall: vi.fn().mockResolvedValueOnce(mockPoolData), - }; - mockGetInstance.mockReturnValueOnce({ - getClient: () => mockClient, - getContractAddress: vi.fn(() => "0xMockAddress"), - }); - mockGetTokens.mockResolvedValueOnce(mockTokenInstances); - mockUseReadContracts.mockReturnValueOnce({ - data: mockPoolData, - isLoading: false, - }); + vi.mocked(mockDeps.client.multicall).mockResolvedValueOnce(mockPoolData); const result = await getPool( { tokens: mockTokens, fee: FeeTier.MEDIUM, }, - mockChainId, + mockDeps, ); expect(result).toBeDefined(); + expect(result).toBeInstanceOf(Pool); }); - it("should return undefined data when 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 mockClient = { multicall: vi.fn() }; - mockGetInstance.mockReturnValueOnce({ - getClient: () => mockClient, - getContractAddress: vi.fn(() => "0xMockAddress"), - }); - - mockGetTokens.mockResolvedValueOnce(mockTokenInstances); - mockUseReadContracts.mockReturnValueOnce({ - data: null, - isLoading: false, - }); - - const result = await getPool( - { - tokens: mockTokens, - fee: FeeTier.MEDIUM, - }, - mockChainId, - ); - - expect(result).toBeUndefined(); - }); - - it("should handle pool creation error", async () => { + 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"), @@ -142,32 +98,21 @@ describe("useV4Pool", () => { const mockPoolData = [ [mockTokens[0], mockTokens[1], FeeTier.MEDIUM, 60, zeroAddress], - ["invalid", 0, 0, 0, 0, 0], + ["invalid", 0, 0, 0, 0, 0], // invalid sqrtPriceX96 "1000000000000000000", ]; - const mockClient = { - multicall: vi.fn().mockResolvedValueOnce(mockPoolData), - }; - mockGetInstance.mockReturnValueOnce({ - getClient: () => mockClient, - getContractAddress: vi.fn(() => "0xMockAddress"), - }); - mockGetTokens.mockResolvedValueOnce(mockTokenInstances); - mockUseReadContracts.mockReturnValueOnce({ - data: mockPoolData, - isLoading: false, - }); + vi.mocked(mockDeps.client.multicall).mockResolvedValueOnce(mockPoolData); - const result = await getPool( - { - tokens: mockTokens, - fee: FeeTier.MEDIUM, - }, - mockChainId, - ); - - expect(result).toBeUndefined(); + await expect( + getPool( + { + tokens: mockTokens, + fee: FeeTier.MEDIUM, + }, + mockDeps, + ), + ).rejects.toThrow("Error creating pool instance"); }); }); diff --git a/src/test/utils/getPoolKeyFromPoolId.test.ts b/src/test/utils/getPoolKeyFromPoolId.test.ts index f51a425..6bb985a 100644 --- a/src/test/utils/getPoolKeyFromPoolId.test.ts +++ b/src/test/utils/getPoolKeyFromPoolId.test.ts @@ -1,129 +1,56 @@ -import type { UniDevKitV4 } from "@/core/uniDevKitV4"; -import { getInstance } from "@/core/uniDevKitV4Factory"; +import { createMockSdkInstance } from "@/test/helpers/sdkInstance"; 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); + const mockDeps = createMockSdkInstance(); + mockDeps.client.readContract = vi + .fn() + .mockRejectedValueOnce(new Error("SDK not initialized")); await expect( - getPoolKeyFromPoolId({ poolId: mockPoolId, chainId: mockChainId }), + getPoolKeyFromPoolId({ poolId: "0x123" }, mockDeps), ).rejects.toThrow("SDK not initialized"); }); it("should return pool key when SDK instance exists", async () => { const mockPoolKey = [ - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - 3000, - 60, - "0x0000000000000000000000000000000000000000", + "0x123", // currency0 + "0x456", // currency1 + 3000, // fee + 60, // tickSpacing + "0x789", // hooks ]; - 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 mockDeps = createMockSdkInstance(); + mockDeps.client.readContract = vi.fn().mockResolvedValueOnce(mockPoolKey); - const result = await getPoolKeyFromPoolId({ - poolId: mockPoolId, - chainId: mockChainId, - }); + const result = await getPoolKeyFromPoolId({ poolId: "0x123" }, mockDeps); expect(result).toEqual({ - currency0: mockPoolKey[0], - currency1: mockPoolKey[1], - fee: mockPoolKey[2], - tickSpacing: mockPoolKey[3], - hooks: mockPoolKey[4], + currency0: "0x123", + currency1: "0x456", + fee: 3000, + tickSpacing: 60, + hooks: "0x789", }); - expect(mockClient.readContract).toHaveBeenCalledWith({ - address: "0xPositionManager", + expect(mockDeps.client.readContract).toHaveBeenCalledWith({ + address: mockDeps.contracts.positionManager, abi: expect.any(Object), functionName: "poolKeys", - args: [expectedPoolId25Bytes], + args: ["0x123"], }); }); 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); + const mockDeps = createMockSdkInstance(); + mockDeps.client.readContract = vi + .fn() + .mockRejectedValueOnce(new Error("Contract read failed")); await expect( - getPoolKeyFromPoolId({ poolId: mockPoolId, chainId: mockChainId }), + getPoolKeyFromPoolId({ poolId: "0x123" }, mockDeps), ).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/test/utils/getPosition.test.ts b/src/test/utils/getPosition.test.ts index 6eb5d3a..28fdd1c 100644 --- a/src/test/utils/getPosition.test.ts +++ b/src/test/utils/getPosition.test.ts @@ -1,20 +1,17 @@ +import { createMockSdkInstance } from "@/test/helpers/sdkInstance"; import { getPosition } from "@/utils/getPosition"; import { Token } from "@uniswap/sdk-core"; -import { type Address, zeroAddress } from "viem"; +import type { Address } from "viem"; import { beforeEach, describe, expect, it, vi } from "vitest"; -// Mock the SDK instance -vi.mock("@/core/uniDevKitV4Factory", () => ({ - getInstance: () => mockGetInstance(), -})); - -// Mock getTokens vi.mock("@/utils/getTokens", () => ({ - getTokens: () => mockGetTokens(), + getTokens: vi.fn(), })); -const mockGetInstance = vi.fn(); -const mockGetTokens = vi.fn(); +// Mock decodePositionInfo para devolver ticks vΓ‘lidos +vi.mock("@/helpers/positions", () => ({ + decodePositionInfo: () => ({ tickLower: -887220, tickUpper: 887220 }), +})); describe("getPosition", () => { // USDC and WETH on Mainnet @@ -22,129 +19,99 @@ describe("getPosition", () => { "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH ]; - const mockTokenId = "123"; - const mockChainId = 1; + const validHooks = "0x000000000000000000000000000000000000dead"; beforeEach(() => { vi.resetAllMocks(); }); it("should throw error if SDK instance not found", async () => { - mockGetInstance.mockReturnValueOnce(undefined); - await expect( - getPosition({ tokenId: mockTokenId }, mockChainId), - ).rejects.toThrow("SDK not found. Please create an instance first."); + const mockDeps = createMockSdkInstance(); + mockDeps.client.multicall = vi + .fn() + .mockRejectedValueOnce(new Error("SDK not initialized")); + + await expect(getPosition({ tokenId: "1" }, mockDeps)).rejects.toThrow( + "SDK not initialized", + ); }); it("should throw error if tokens not found", async () => { - const mockClient = { - multicall: vi.fn().mockResolvedValueOnce([ - [ - { - currency0: mockTokens[0], - currency1: mockTokens[1], - fee: 3000, - tickSpacing: 60, - hooks: zeroAddress, - }, - "0x", - ], - BigInt("1000000000000000000"), - ]), - }; - - mockGetInstance.mockReturnValueOnce({ - getClient: () => mockClient, - getContractAddress: vi.fn(() => "0xMockAddress"), - }); - mockGetTokens.mockResolvedValueOnce(null); + const mockDeps = createMockSdkInstance(); + mockDeps.client.multicall = vi.fn().mockResolvedValueOnce([ + [ + { + currency0: "0x123", + currency1: "0x456", + fee: 3000, + tickSpacing: 60, + hooks: validHooks, + }, + {}, + ], + 1000000n, + ]); - await expect( - getPosition({ tokenId: mockTokenId }, mockChainId), - ).rejects.toThrow("Tokens not found"); + await expect(getPosition({ tokenId: "1" }, mockDeps)).rejects.toThrow( + "Tokens not found", + ); }); it("should throw error if liquidity is 0", async () => { - const mockClient = { - multicall: vi.fn().mockResolvedValueOnce([ + const mockDeps = createMockSdkInstance(); + mockDeps.client.multicall = vi.fn().mockResolvedValueOnce([ + [ + { + currency0: "0x123", + currency1: "0x456", + fee: 3000, + tickSpacing: 60, + hooks: validHooks, + }, + {}, + ], + 0n, + ]); + + await expect(getPosition({ tokenId: "1" }, mockDeps)).rejects.toThrow( + "Liquidity is 0", + ); + }); + + it("should return position data when position exists", async () => { + const mockDeps = createMockSdkInstance(); + // Primer multicall: [poolAndPositionInfo, liquidity] + mockDeps.client.multicall = vi + .fn() + .mockResolvedValueOnce([ [ { currency0: mockTokens[0], currency1: mockTokens[1], fee: 3000, tickSpacing: 60, - hooks: zeroAddress, + hooks: validHooks, }, - "0x", + 1n, ], - BigInt(0), - ]), - }; - - mockGetInstance.mockReturnValueOnce({ - getClient: () => mockClient, - getContractAddress: vi.fn(() => "0xMockAddress"), - }); - - mockGetTokens.mockResolvedValueOnce([ - new Token(1, mockTokens[0], 6, "USDC", "USD Coin"), - new Token(1, mockTokens[1], 18, "WETH", "Wrapped Ether"), - ]); - - await expect( - getPosition({ tokenId: mockTokenId }, mockChainId), - ).rejects.toThrow("Liquidity is 0"); - }); - - it("should return position data when position exists", async () => { - const mockTokenInstances = [ - new Token(1, mockTokens[0], 6, "USDC", "USD Coin"), - new Token(1, mockTokens[1], 18, "WETH", "Wrapped Ether"), - ]; - - // Compose a valid rawInfo: hasSubscriber=1, tickLower=0, tickUpper=60, poolId=0n - const hasSubscriber = 1n; - const tickLower = 0n; - const tickUpper = 60n; - const poolId = 0n; - const rawInfo = - hasSubscriber | (tickLower << 8n) | (tickUpper << 32n) | (poolId << 56n); - const mockPoolKey = { - currency0: mockTokens[0], - currency1: mockTokens[1], - fee: 3000, - tickSpacing: 60, - hooks: zeroAddress, - }; - - const mockClient = { - multicall: vi - .fn() - .mockResolvedValueOnce([ - [mockPoolKey, rawInfo], - BigInt("1000000000000000000"), - ]) - .mockResolvedValueOnce([ - [BigInt("79228162514264337593543950336"), 0], - BigInt("1000000000000000000"), - ]), - }; - - mockGetInstance.mockReturnValueOnce({ - getClient: () => mockClient, - getContractAddress: vi.fn(() => "0xMockAddress"), - }); - - mockGetTokens.mockResolvedValueOnce(mockTokenInstances); - - const result = await getPosition({ tokenId: mockTokenId }, mockChainId); - - expect(result).toBeDefined(); - expect(result?.token0).toEqual(mockTokenInstances[0]); - expect(result?.token1).toEqual(mockTokenInstances[1]); - expect(result?.position).toBeDefined(); - expect(result?.pool).toBeDefined(); - expect(result?.poolId).toBeDefined(); - expect(result?.tokenId).toBe(mockTokenId); + 1000000n, + ]) + // Segundo multicall: [slot0, poolLiquidity] + .mockResolvedValueOnce([[79228162514264337593543950336n, 0], 1000000n]); + + // Mock getTokens para devolver instancias reales de Token + const mockToken0 = new Token(1, mockTokens[0], 6, "USDC", "USD Coin"); + const mockToken1 = new Token(1, mockTokens[1], 18, "WETH", "Wrapped Ether"); + const { getTokens } = await import("@/utils/getTokens"); + vi.mocked(getTokens).mockResolvedValueOnce([mockToken0, mockToken1]); + + const result = await getPosition({ tokenId: "1" }, mockDeps); + + expect(result).toHaveProperty("position"); + expect(result).toHaveProperty("pool"); + expect(result).toHaveProperty("token0"); + expect(result).toHaveProperty("token1"); + expect(result).toHaveProperty("poolId"); + expect(result).toHaveProperty("tokenId", "1"); }); }); diff --git a/src/test/utils/getQuote.test.ts b/src/test/utils/getQuote.test.ts index c9555e3..f0ffcad 100644 --- a/src/test/utils/getQuote.test.ts +++ b/src/test/utils/getQuote.test.ts @@ -1,65 +1,49 @@ +import { createMockSdkInstance } from "@/test/helpers/sdkInstance"; import { getQuote } from "@/utils/getQuote"; +import type { Abi } from "viem"; +import type { SimulateContractReturnType } from "viem/actions"; import { describe, expect, it, vi } from "vitest"; -const mockGetInstance = vi.fn(); - -vi.mock("@/core/uniDevKitV4Factory", () => ({ - getInstance: () => mockGetInstance(), -})); - describe("getQuote", () => { it("should throw error if SDK instance not found", async () => { - mockGetInstance.mockReturnValueOnce(undefined); - await expect( - getQuote({ - tokens: ["0x123", "0x456"], - feeTier: 3000, - tickSpacing: 60, - amountIn: BigInt(1000000), - zeroForOne: true, - }), - ).rejects.toThrow(); - }); + const mockDeps = createMockSdkInstance(); + mockDeps.client.simulateContract = vi + .fn() + .mockRejectedValueOnce(new Error("SDK not found")); - it("should sort tokens consistently", async () => { - mockGetInstance.mockReturnValueOnce({ - getClient: () => ({ - simulateContract: vi - .fn() - .mockResolvedValue({ result: [BigInt(1000000), BigInt(21000)] }), - }), - getContractAddress: vi.fn(() => "0xMockQuoterAddress"), - }); - const result = await getQuote({ - tokens: ["0x123", "0x456"], - feeTier: 3000, - tickSpacing: 60, - amountIn: BigInt(1000000), - zeroForOne: true, - }); - expect(result).toHaveProperty("amountOut", BigInt(1000000)); - expect(result).toHaveProperty("estimatedGasUsed", BigInt(21000)); - expect(result).toHaveProperty("timestamp"); + await expect( + getQuote( + { + tokens: ["0x123", "0x456"], + zeroForOne: true, + amountIn: BigInt(1000000), + feeTier: 3000, + }, + mockDeps, + ), + ).rejects.toThrow("SDK not found"); }); it("should handle quote simulation", async () => { - mockGetInstance.mockReturnValueOnce({ - getClient: () => ({ - simulateContract: vi - .fn() - .mockResolvedValue({ result: [BigInt(1000000), BigInt(21000)] }), - }), - getContractAddress: vi.fn(() => "0xMockQuoterAddress"), - }); - const result = await getQuote({ - tokens: ["0x123", "0x456"], - feeTier: 3000, - tickSpacing: 60, - amountIn: BigInt(1000000), - zeroForOne: true, + const mockDeps = createMockSdkInstance(); + mockDeps.client.simulateContract = vi.fn().mockResolvedValueOnce({ + result: [BigInt(1000000), BigInt(21000)], + } as SimulateContractReturnType); + + const result = await getQuote( + { + tokens: ["0x123", "0x456"], + zeroForOne: true, + amountIn: BigInt(1000000), + feeTier: 3000, + }, + mockDeps, + ); + + expect(result).toEqual({ + amountOut: BigInt(1000000), + estimatedGasUsed: BigInt(21000), + timestamp: expect.any(Number), }); - expect(result).toHaveProperty("amountOut", BigInt(1000000)); - expect(result).toHaveProperty("estimatedGasUsed", BigInt(21000)); - expect(result).toHaveProperty("timestamp"); }); }); diff --git a/src/test/utils/getTokens.test.ts b/src/test/utils/getTokens.test.ts index bc93b18..d0a81d7 100644 --- a/src/test/utils/getTokens.test.ts +++ b/src/test/utils/getTokens.test.ts @@ -1,7 +1,7 @@ -import { getInstance } from "@/core/uniDevKitV4Factory"; +import { createMockSdkInstance } from "@/test/helpers/sdkInstance"; import { getTokens } from "@/utils/getTokens"; import { Token } from "@uniswap/sdk-core"; -import { type Address, erc20Abi, zeroAddress } from "viem"; +import { type Address, zeroAddress } from "viem"; import { beforeEach, describe, expect, it, vi } from "vitest"; // Mock the SDK instance @@ -20,144 +20,79 @@ vi.mock("@/constants/chains", () => ({ })); describe("getTokens", () => { - const mockClient = { - multicall: vi.fn(), - }; + // USDC and WETH on Mainnet + const mockTokens: [Address, ...Address[]] = [ + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + ]; - const mockSdk = { - getClient: () => mockClient, - getChainId: () => 1, - }; + let mockDeps: ReturnType; beforeEach(() => { - vi.clearAllMocks(); - (getInstance as unknown as ReturnType).mockReturnValue( - mockSdk, - ); + vi.resetAllMocks(); + mockDeps = createMockSdkInstance(); }); - it("should return token instances for valid addresses", async () => { - const addresses: [Address, ...Address[]] = [ - "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984" as Address, // UNI - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" as Address, // WETH - ]; + it("should throw error if multicall fails", async () => { + vi.mocked(mockDeps.client.multicall).mockRejectedValueOnce( + new Error("Multicall failed"), + ); + + await expect( + getTokens( + { + addresses: mockTokens, + }, + mockDeps, + ), + ).rejects.toThrow("Failed to fetch token data: Multicall failed"); + }); + it("should handle native currency (zero address)", async () => { const mockResults = [ - "UNI", // symbol for UNI - "Uniswap", // name for UNI - 18, // decimals for UNI - "WETH", // symbol for WETH - "Wrapped Ether", // name for WETH - 18, // decimals for WETH + "USDC", // symbol for first token + "USD Coin", // name for first token + 6, // decimals for first token ]; - mockClient.multicall.mockResolvedValueOnce(mockResults); + vi.mocked(mockDeps.client.multicall).mockResolvedValueOnce(mockResults); - const result = await getTokens({ - addresses, - chainId: 1, - }); + const result = await getTokens( + { + addresses: [mockTokens[0], zeroAddress], + }, + mockDeps, + ); - expect(result).not.toBeNull(); expect(result).toHaveLength(2); - expect(result?.[0]).toBeInstanceOf(Token); - expect(result?.[1]).toBeInstanceOf(Token); - - // Verify UNI token - expect(result?.[0].symbol).toBe("UNI"); - expect(result?.[0].name).toBe("Uniswap"); - expect(result?.[0].decimals).toBe(18); - expect(result?.[0].chainId).toBe(1); - expect(result?.[0].address).toBe(addresses[0]); - - // Verify WETH token - expect(result?.[1].symbol).toBe("WETH"); - expect(result?.[1].name).toBe("Wrapped Ether"); - expect(result?.[1].decimals).toBe(18); - expect(result?.[1].chainId).toBe(1); - expect(result?.[1].address).toBe(addresses[1]); - - // Verify multicall was called with correct parameters - expect(mockClient.multicall).toHaveBeenCalledWith({ - contracts: [ - { address: addresses[0], abi: erc20Abi, functionName: "symbol" }, - { address: addresses[0], abi: erc20Abi, functionName: "name" }, - { address: addresses[0], abi: erc20Abi, functionName: "decimals" }, - { address: addresses[1], abi: erc20Abi, functionName: "symbol" }, - { address: addresses[1], abi: erc20Abi, functionName: "name" }, - { address: addresses[1], abi: erc20Abi, functionName: "decimals" }, - ], - allowFailure: false, - }); + expect(result[0]).toBeInstanceOf(Token); + expect(result[1]).toBeInstanceOf(Token); + expect(result[1].address).toBe(zeroAddress); }); - it("should handle native currency (zeroAddress)", async () => { - const addresses: [Address, ...Address[]] = [ - zeroAddress, - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" as Address, // WETH - ]; - + it("should return token instances for valid addresses", async () => { const mockResults = [ - "WETH", // symbol for WETH - "Wrapped Ether", // name for WETH - 18, // decimals for WETH + "USDC", // symbol for first token + "USD Coin", // name for first token + 6, // decimals for first token + "WETH", // symbol for second token + "Wrapped Ether", // name for second token + 18, // decimals for second token ]; - mockClient.multicall.mockResolvedValueOnce(mockResults); + vi.mocked(mockDeps.client.multicall).mockResolvedValueOnce(mockResults); - const result = await getTokens({ - addresses, - }); + const result = await getTokens( + { + addresses: mockTokens, + }, + mockDeps, + ); - expect(result).not.toBeNull(); expect(result).toHaveLength(2); - expect(result?.[0]).toBeInstanceOf(Token); - expect(result?.[1]).toBeInstanceOf(Token); - - // Verify native token - expect(result?.[0].symbol).toBe("ETH"); - expect(result?.[0].name).toBe("Ethereum"); - expect(result?.[0].decimals).toBe(18); - expect(result?.[0].chainId).toBe(1); - expect(result?.[0].address).toBe(zeroAddress); - - // Verify WETH token - expect(result?.[1].symbol).toBe("WETH"); - expect(result?.[1].name).toBe("Wrapped Ether"); - expect(result?.[1].decimals).toBe(18); - expect(result?.[1].chainId).toBe(1); - expect(result?.[1].address).toBe(addresses[1]); - - // Verify multicall was called only for non-native token - expect(mockClient.multicall).toHaveBeenCalledWith({ - contracts: [ - { address: addresses[1], abi: erc20Abi, functionName: "symbol" }, - { address: addresses[1], abi: erc20Abi, functionName: "name" }, - { address: addresses[1], abi: erc20Abi, functionName: "decimals" }, - ], - allowFailure: false, - }); - }); - - it("should throw error when SDK instance is not found", async () => { - (getInstance as unknown as ReturnType).mockReturnValue(null); - - await expect( - getTokens({ - addresses: ["0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984" as Address], - chainId: 1, - }), - ).rejects.toThrow("SDK not found. Please create an instance first."); - }); - - it("should return null when multicall fails", async () => { - mockClient.multicall.mockRejectedValueOnce(new Error("Multicall failed")); - - const result = await getTokens({ - addresses: ["0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984" as Address], - chainId: 1, - }); - - expect(result).toBeNull(); + expect(result[0]).toBeInstanceOf(Token); + expect(result[1]).toBeInstanceOf(Token); + expect(result[0].symbol).toBe("USDC"); + expect(result[1].symbol).toBe("WETH"); }); }); diff --git a/src/types/core.ts b/src/types/core.ts index 0210c0b..4beb1c1 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -1,4 +1,4 @@ -import type { Address, PublicClient } from "viem"; +import type { Address, Chain, PublicClient } from "viem"; /** * Configuration for V4 contracts. @@ -41,8 +41,8 @@ export type UniDevKitV4Config = { export type UniDevKitV4Instance = { /** Viem public client */ client: PublicClient; - /** Chain ID */ - chainId: number; + /** Chain */ + chain: Chain; /** Contract addresses */ contracts: V4Contracts; }; diff --git a/src/types/hooks/index.ts b/src/types/hooks/index.ts deleted file mode 100644 index c21a71a..0000000 --- a/src/types/hooks/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 8cbdd0f..0000000 --- a/src/types/hooks/useGetPool.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { PoolParams } from "@/types/utils/getPool"; -import type { UseQueryOptions } from "@tanstack/react-query"; -import type { Pool } from "@uniswap/v4-sdk"; - -/** - * Configuration options for the useGetPool hook. - */ -export type UseGetPoolOptions = { - /** Initial pool parameters */ - params: PoolParams; - /** Chain ID */ - chainId?: number; - /** React Query options */ - queryOptions?: Omit< - UseQueryOptions, - "queryKey" - >; -}; diff --git a/src/types/hooks/useGetPoolKeyFromPoolId.ts b/src/types/hooks/useGetPoolKeyFromPoolId.ts deleted file mode 100644 index 59e8f6a..0000000 --- a/src/types/hooks/useGetPoolKeyFromPoolId.ts +++ /dev/null @@ -1,17 +0,0 @@ -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/hooks/useGetPosition.ts b/src/types/hooks/useGetPosition.ts deleted file mode 100644 index 115741a..0000000 --- a/src/types/hooks/useGetPosition.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { UseQueryOptions } from "@tanstack/react-query"; -import type { Token } from "@uniswap/sdk-core"; -import type { Pool, Position } from "@uniswap/v4-sdk"; - -/** - * Result type for position data - */ -export interface PositionResult { - /** The position instance */ - position: Position; - /** The pool instance associated with the position */ - pool: Pool; - /** First token in the pair */ - token0: Token; - /** Second token in the pair */ - token1: Token; - /** Unique identifier for the pool */ - poolId: `0x${string}`; - /** The unique identifier of the position */ - tokenId: string; -} - -/** - * Configuration options for the useGetPosition hook - */ -export interface UseGetPositionOptions { - /** Token ID of the position */ - tokenId?: string; - /** Chain ID to use */ - chainId?: number; - /** React Query options */ - queryOptions?: Omit< - UseQueryOptions< - PositionResult | undefined, - Error, - PositionResult | undefined, - unknown[] - >, - "queryKey" - >; -} diff --git a/src/types/hooks/useGetQuote.ts b/src/types/hooks/useGetQuote.ts deleted file mode 100644 index fa7c0a1..0000000 --- a/src/types/hooks/useGetQuote.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { QuoteParams, QuoteResponse } from "@/types/utils/getQuote"; -import type { UseQueryOptions } from "@tanstack/react-query"; - -/** - * Configuration options for the useGetQuote hook. - */ -export type UseGetQuoteOptions = { - /** Initial quote parameters */ - params?: QuoteParams; - /** Chain ID to use */ - chainId?: number; - /** React Query options */ - queryOptions?: Omit< - UseQueryOptions, - "queryKey" - >; -}; diff --git a/src/types/index.ts b/src/types/index.ts index f0911fb..f5bd8a2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,2 @@ export * from "@/types/core"; -export * from "@/types/hooks"; export * from "@/types/utils"; diff --git a/src/types/utils/getPool.ts b/src/types/utils/getPool.ts index 9571278..2cf3652 100644 --- a/src/types/utils/getPool.ts +++ b/src/types/utils/getPool.ts @@ -31,6 +31,6 @@ export interface PoolParams { fee?: FeeTier; /** Optional tick spacing for the pool (default: derived from fee tier) */ tickSpacing?: number; - /** Optional hooks contract address (default: zero address) */ + /** Optional hooks contract address (default: DEFAULT_HOOKS) */ hooks?: `0x${string}`; } diff --git a/src/types/utils/getPoolKeyFromPoolId.ts b/src/types/utils/getPoolKeyFromPoolId.ts index 803e691..db67ccd 100644 --- a/src/types/utils/getPoolKeyFromPoolId.ts +++ b/src/types/utils/getPoolKeyFromPoolId.ts @@ -4,8 +4,6 @@ export interface GetPoolKeyFromPoolIdParams { /** The 32-byte pool ID in hex format (0x...) */ poolId: `0x${string}`; - /** Optional chain ID where the pool exists */ - chainId?: number; } /** diff --git a/src/types/utils/getTokens.ts b/src/types/utils/getTokens.ts new file mode 100644 index 0000000..0ade3fe --- /dev/null +++ b/src/types/utils/getTokens.ts @@ -0,0 +1,9 @@ +import type { Address } from "viem"; + +/** + * Parameters for getTokens function + */ +export interface GetTokensParams { + /** Array of token contract addresses (at least one) */ + addresses: [Address, ...Address[]]; +} diff --git a/src/utils/getPool.ts b/src/utils/getPool.ts index 185a1ee..2800158 100644 --- a/src/utils/getPool.ts +++ b/src/utils/getPool.ts @@ -1,8 +1,8 @@ import { V4PositionManagerAbi } from "@/constants/abis/V4PositionMananger"; import { V4StateViewAbi } from "@/constants/abis/V4StateView"; -import { getInstance } from "@/core/uniDevKitV4Factory"; import { getTickSpacingForFee } from "@/helpers/fees"; import { sortTokens } from "@/helpers/tokens"; +import type { UniDevKitV4Instance } from "@/types/core"; import { FeeTier, type PoolParams } from "@/types/utils/getPool"; import { getTokens } from "@/utils/getTokens"; import { Pool } from "@uniswap/v4-sdk"; @@ -13,19 +13,16 @@ const DEFAULT_HOOKS = zeroAddress; /** * Retrieves a Uniswap V4 pool instance for a given token pair, fee tier, tick spacing, and hooks configuration. * @param params Pool parameters including tokens, fee tier, tick spacing, and hooks configuration - * @param chainId The chain ID where the pool exists (optional) - * @returns Promise resolving to pool data, loading state, and any errors + * @param instance UniDevKitV4Instance + * @returns Promise resolving to pool data * @throws Error if SDK instance or token instances are not found or if pool data is not found */ export async function getPool( params: PoolParams, - chainId?: number, -): Promise { - const sdk = getInstance(chainId); - if (!sdk) { - throw new Error("SDK not found. Please create an instance first."); - } - const client = sdk.getClient(); + instance: UniDevKitV4Instance, +): Promise { + const { client, contracts } = instance; + const { positionManager, stateView } = contracts; const { tokens, @@ -38,16 +35,12 @@ export async function getPool( const finalTickSpacing = tickSpacing ?? getTickSpacingForFee(fee); const [token0, token1] = sortTokens(tokens[0], tokens[1]); - const tokenInstances = await getTokens({ - addresses: [token0, token1], - chainId, - }); - - console.log("tokenInstances", tokenInstances); - - if (!tokenInstances) { - throw new Error("Failed to fetch token instances"); - } + const tokenInstances = await getTokens( + { + addresses: [token0, token1], + }, + instance, + ); const poolId32Bytes = Pool.getPoolId( tokenInstances[0], @@ -57,27 +50,25 @@ export async function getPool( hooks, ) as `0x${string}`; - console.log("poolId32Bytes", poolId32Bytes); - const poolId25Bytes = slice(poolId32Bytes, 0, 25) as `0x${string}`; const poolData = await client.multicall({ allowFailure: false, contracts: [ { - address: sdk.getContractAddress("positionManager"), + address: positionManager, abi: V4PositionManagerAbi, functionName: "poolKeys", args: [poolId25Bytes], }, { - address: sdk.getContractAddress("stateView"), + address: stateView, abi: V4StateViewAbi, functionName: "getSlot0", args: [poolId32Bytes], }, { - address: sdk.getContractAddress("stateView"), + address: stateView, abi: V4StateViewAbi, functionName: "getLiquidity", args: [poolId32Bytes], @@ -86,7 +77,7 @@ export async function getPool( }); if (!poolData) { - return undefined; + throw new Error("Failed to fetch pool data"); } const [poolKeysData, slot0Data, liquidityData] = poolData; @@ -94,7 +85,7 @@ export async function getPool( poolKeysData && Number(poolKeysData[3]) > 0 && slot0Data && liquidityData; if (!poolExists) { - return undefined; + throw new Error("Pool does not exist"); } try { @@ -111,7 +102,8 @@ export async function getPool( return pool; } catch (error) { - console.error("Error fetching pool:", error); - return undefined; + throw new Error( + `Error creating pool instance: ${(error as Error).message}`, + ); } } diff --git a/src/utils/getPoolKeyFromPoolId.ts b/src/utils/getPoolKeyFromPoolId.ts index 294543f..f1123d5 100644 --- a/src/utils/getPoolKeyFromPoolId.ts +++ b/src/utils/getPoolKeyFromPoolId.ts @@ -1,5 +1,5 @@ import { V4PositionManagerAbi } from "@/constants/abis/V4PositionMananger"; -import { getInstance } from "@/core/uniDevKitV4Factory"; +import type { UniDevKitV4Instance } from "@/types/core"; import type { GetPoolKeyFromPoolIdParams, PoolKey, @@ -7,21 +7,18 @@ import type { /** * Retrieves the pool key information for a given pool ID. - * @param params Parameters containing the pool ID and optional chain ID + * @param params Parameters containing the pool 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"); +export async function getPoolKeyFromPoolId( + params: GetPoolKeyFromPoolIdParams, + instance: UniDevKitV4Instance, +): Promise { + const { client, contracts } = instance; + const { positionManager } = contracts; - const client = sdk.getClient(); - const positionManager = sdk.getContractAddress("positionManager"); - - const poolId25Bytes = `0x${poolId.slice(2, 52)}` as `0x${string}`; + const poolId25Bytes = `0x${params.poolId.slice(2, 52)}` as `0x${string}`; const [currency0, currency1, fee, tickSpacing, hooks] = await client.readContract({ diff --git a/src/utils/getPosition.ts b/src/utils/getPosition.ts index 5a5324b..dc3554d 100644 --- a/src/utils/getPosition.ts +++ b/src/utils/getPosition.ts @@ -1,7 +1,7 @@ import { V4PositionManagerAbi } from "@/constants/abis/V4PositionMananger"; import { V4StateViewAbi } from "@/constants/abis/V4StateView"; -import { getInstance } from "@/core/uniDevKitV4Factory"; import { decodePositionInfo } from "@/helpers/positions"; +import type { UniDevKitV4Instance } from "@/types/core"; import type { GetPositionParams, GetPositionResponse, @@ -12,22 +12,17 @@ import { Pool, Position as V4Position } from "@uniswap/v4-sdk"; /** * Retrieves a Uniswap V4 position instance for a given token ID. * @param params Position parameters including token ID - * @param chainId Optional chain ID where the position exists + * @param instance UniDevKitV4Instance * @returns Promise resolving to position data * @throws Error if SDK instance is not found or if position data is invalid */ export async function getPosition( params: GetPositionParams, - chainId?: number, + instance: UniDevKitV4Instance, ): Promise { - const sdk = getInstance(chainId); - if (!sdk) { - throw new Error("SDK not found. Please create an instance first."); - } + const { client, contracts } = instance; - const client = sdk.getClient(); - const positionManager = sdk.getContractAddress("positionManager"); - const stateView = sdk.getContractAddress("stateView"); + const { positionManager, stateView } = contracts; // Fetch poolKey and raw position info const [poolAndPositionInfo, liquidity] = await client.multicall({ @@ -55,10 +50,12 @@ export async function getPosition( throw new Error("Liquidity is 0"); } - const tokens = await getTokens({ - addresses: [currency0, currency1], - chainId, - }); + const tokens = await getTokens( + { + addresses: [currency0, currency1], + }, + instance, + ); if (!tokens) { throw new Error("Tokens not found"); diff --git a/src/utils/getQuote.ts b/src/utils/getQuote.ts index ad3daec..e4a3dac 100644 --- a/src/utils/getQuote.ts +++ b/src/utils/getQuote.ts @@ -1,6 +1,6 @@ import { V4QuoterAbi } from "@/constants/abis/V4Quoter"; -import { getInstance } from "@/core/uniDevKitV4Factory"; import { sortTokens } from "@/helpers/tokens"; +import type { UniDevKitV4Instance } from "@/types/core"; import { FeeTier, TICK_SPACING_BY_FEE } from "@/types/utils/getPool"; import type { QuoteParams, QuoteResponse } from "@/types/utils/getQuote"; import { zeroAddress } from "viem"; @@ -20,14 +20,10 @@ import { zeroAddress } from "viem"; */ export async function getQuote( params: QuoteParams, - chainId?: number, + instance: UniDevKitV4Instance, ): Promise { - const sdk = getInstance(chainId); - if (!sdk) { - throw new Error("SDK not found. Please create an instance first."); - } - const client = sdk.getClient(); - const quoterAddress = sdk.getContractAddress("quoter"); + const { client, contracts } = instance; + const { quoter } = contracts; try { // Sort tokens to ensure consistent pool key ordering @@ -59,7 +55,7 @@ export async function getQuote( // Simulate the quote to estimate the amount out const simulation = await client.simulateContract({ - address: quoterAddress, + address: quoter, abi: V4QuoterAbi, functionName: "quoteExactInputSingle", args: [quoteParams], diff --git a/src/utils/getTokens.ts b/src/utils/getTokens.ts index 5312f7b..b94b143 100644 --- a/src/utils/getTokens.ts +++ b/src/utils/getTokens.ts @@ -1,32 +1,21 @@ -import { getChainById } from "@/constants/chains"; -import { getInstance } from "@/core/uniDevKitV4Factory"; +import type { UniDevKitV4Instance } from "@/types/core"; +import type { GetTokensParams } from "@/types/utils/getTokens"; import { Token } from "@uniswap/sdk-core"; -import { type Address, erc20Abi, zeroAddress } from "viem"; +import { erc20Abi, zeroAddress } from "viem"; /** * Retrieves Token instances for a list of token addresses on a specific chain. - * @param {Object} params - The parameters object - * @param {[Address, ...Address[]]} params.addresses - Array of token contract addresses (at least one) - * @param {number} params.chainId - The chain ID where the tokens exist (optional) - * @returns {Promise} Array of Token instances or null if the operation fails - * @throws {Error} If SDK instance is not found + * @param params Parameters including token addresses + * @param instance UniDevKitV4Instance + * @returns Promise resolving to array of Token instances + * @throws Error if token data cannot be fetched */ -export async function getTokens({ - addresses, - chainId, -}: { - addresses: [Address, ...Address[]]; - chainId?: number; -}): Promise { - const sdk = getInstance(chainId); - const currentChainId = chainId || sdk.getChainId(); - const chain = getChainById(currentChainId); - - if (!sdk) { - throw new Error("SDK not found. Please create an instance first."); - } - - const client = sdk.getClient(); +export async function getTokens( + params: GetTokensParams, + instance: UniDevKitV4Instance, +): Promise { + const { addresses } = params; + const { client, chain } = instance; const calls = addresses .filter((address) => address !== zeroAddress) // filter out native currency @@ -51,7 +40,7 @@ export async function getTokens({ const nativeCurrency = chain.nativeCurrency; tokens.push( new Token( - currentChainId, + chain.id, address, nativeCurrency.decimals, nativeCurrency.symbol, @@ -63,13 +52,12 @@ export async function getTokens({ const symbol = results[resultIndex++] as string; const name = results[resultIndex++] as string; const decimals = results[resultIndex++] as number; - tokens.push(new Token(currentChainId, address, decimals, symbol, name)); + tokens.push(new Token(chain.id, address, decimals, symbol, name)); } } return tokens; } catch (err) { - console.error("getTokens failed:", err); - return null; + throw new Error(`Failed to fetch token data: ${(err as Error).message}`); } }