'use client'; import { useState, useEffect } from 'react'; import { usePublicClient } from 'wagmi'; import chainInfo from '@/contracts/liqp-deployments.json'; import IPartyPlannerABI from '@/contracts/IPartyPlannerABI'; import IPartyPoolABI from '@/contracts/IPartyPoolABI'; import IPartyPoolViewerABI from '@/contracts/IPartyPoolViewerABI'; import IPartyInfoABI from '@/contracts/IPartyInfoABI'; import { ERC20ABI } from '@/contracts/ERC20ABI'; // Helper function to format large numbers with K, M, B suffixes function formatTVL(value: number): string { if (value >= 1_000_000_000) { return `$${(value / 1_000_000_000).toFixed(2)}B`; } else if (value >= 1_000_000) { return `$${(value / 1_000_000).toFixed(2)}M`; } else if (value >= 1_000) { return `$${(value / 1_000).toFixed(2)}K`; } else { return `$${value.toFixed(2)}`; } } export function useGetAllTokens(offset: number = 0, limit: number = 100) { const publicClient = usePublicClient(); const [mounted, setMounted] = useState(false); const [tokens, setTokens] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Handle hydration for Next.js static export useEffect(() => { setMounted(true); }, []); useEffect(() => { if (!mounted) return; const fetchTokens = async () => { if (!publicClient) { setLoading(false); return; } try { setLoading(true); setError(null); // Get chain ID and contract address const chainId = await publicClient.getChainId(); // @ts-ignore const address = (chainInfo as Record)[chainId.toString()]?.v1?.PartyPlanner; if (!address) { setError('IPartyPlanner contract not found for current chain'); setTokens([]); return; } // Call getAllTokens function const result = await publicClient.readContract({ address: address as `0x${string}`, abi: IPartyPlannerABI, functionName: 'getAllTokens', args: [BigInt(offset), BigInt(limit)], }); setTokens(result); } catch (err) { console.error('Error calling getAllTokens:', err); setError(err instanceof Error ? err.message : 'Failed to fetch tokens'); } finally { setLoading(false); } }; fetchTokens(); }, [publicClient, mounted, offset, limit]); return { tokens, loading, error, isReady: mounted, }; } export interface TokenDetails { address: `0x${string}`; name: string; symbol: string; decimals: number; balance: bigint; index: number; } export interface SwapRoute { poolAddress: `0x${string}`; inputTokenIndex: number; outputTokenIndex: number; } export interface AvailableToken { address: `0x${string}`; symbol: string; swapRoutes: SwapRoute[]; } export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offset: number = 0, limit: number = 100) { const publicClient = usePublicClient(); const [mounted, setMounted] = useState(false); const [availableTokens, setAvailableTokens] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Handle hydration for Next.js static export useEffect(() => { setMounted(true); }, []); useEffect(() => { if (!mounted || !tokenAddress) { setLoading(false); return; } const fetchPoolsFromTokens = async () => { if (!publicClient) { setLoading(false); return; } try { setLoading(true); setError(null); // Get chain ID and contract addresses const chainId = await publicClient.getChainId(); const plannerAddress = (chainInfo as Record)[chainId.toString()]?.v1?.PartyPlanner; const partyInfoAddress = (chainInfo as Record)[chainId.toString()]?.v1?.PartyInfo; if (!plannerAddress || !partyInfoAddress) { setError('IPartyPlanner or PartyInfo contract not found for current chain'); setAvailableTokens([]); return; } // Call getPoolsByToken function const poolsResult = await publicClient.readContract({ address: plannerAddress as `0x${string}`, abi: IPartyPlannerABI, functionName: 'getPoolsByToken', args: [tokenAddress, BigInt(offset), BigInt(limit)], }); // Get the symbol of the originally selected token const selectedTokenSymbol = await publicClient.readContract({ address: tokenAddress, abi: ERC20ABI, functionName: 'symbol', }).catch(() => null); if (!selectedTokenSymbol) { setAvailableTokens([]); return; } // Filter pools to only working ones const workingPools: `0x${string}`[] = []; for (const poolAddress of poolsResult) { try { const isWorking = await publicClient.readContract({ address: partyInfoAddress as `0x${string}`, abi: IPartyInfoABI, functionName: 'working', args: [poolAddress], }) as boolean; if (isWorking) { workingPools.push(poolAddress); } } catch (err) { console.error(`Error checking if pool ${poolAddress} is working:`, err); } } // If no working pools found, set error message if (workingPools.length === 0 && poolsResult.length > 0) { setError('This token is no longer supported. All pools containing this token are currently inactive.'); setAvailableTokens([]); return; } else if (workingPools.length === 0) { setError('No pools found for this token.'); setAvailableTokens([]); return; } // Map to store available tokens with their swap routes const tokenRoutesMap = new Map(); // For each working pool, fetch all tokens and track indices for (const poolAddress of workingPools) { try { const tokensInPool = await publicClient.readContract({ address: poolAddress, abi: IPartyPoolABI, functionName: 'allTokens', }) as readonly `0x${string}`[]; // Find the input token index in this pool const inputTokenIndex = tokensInPool.findIndex( (token) => token.toLowerCase() === tokenAddress.toLowerCase() ); if (inputTokenIndex === -1) { console.error('Input token not found in pool', poolAddress); continue; } // Process each token in the pool for (let outputTokenIndex = 0; outputTokenIndex < tokensInPool.length; outputTokenIndex++) { const outputTokenAddress = tokensInPool[outputTokenIndex]; // Skip if it's the same as the input token if (outputTokenIndex === inputTokenIndex) { continue; } // Get the symbol of this token const outputTokenSymbol = await publicClient.readContract({ address: outputTokenAddress, abi: ERC20ABI, functionName: 'symbol', }).catch(() => null); // Skip tokens with the same symbol as the selected token if (!outputTokenSymbol || outputTokenSymbol === selectedTokenSymbol) { continue; } // Create or update the available token entry const tokenKey = outputTokenAddress.toLowerCase(); if (!tokenRoutesMap.has(tokenKey)) { tokenRoutesMap.set(tokenKey, { address: outputTokenAddress, symbol: outputTokenSymbol, swapRoutes: [], }); } // Add this swap route tokenRoutesMap.get(tokenKey)!.swapRoutes.push({ poolAddress, inputTokenIndex, outputTokenIndex, }); } } catch (err) { console.error('Error fetching tokens from pool', poolAddress, err); } } const availableTokensList = Array.from(tokenRoutesMap.values()); setAvailableTokens(availableTokensList); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch pools and tokens'); } finally { setLoading(false); } }; fetchPoolsFromTokens(); }, [publicClient, mounted, tokenAddress, offset, limit]); return { availableTokens, loading, error, isReady: mounted, }; } export function useTokenDetails(userAddress: `0x${string}` | undefined) { const publicClient = usePublicClient(); const { tokens, loading: tokensLoading, isReady } = useGetAllTokens(); const [tokenDetails, setTokenDetails] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { if (!isReady || tokensLoading || !tokens || !publicClient || !userAddress) { setLoading(tokensLoading || !isReady); return; } const fetchTokenDetails = async () => { try { setLoading(true); setError(null); if (tokens.length === 0) { setTokenDetails([]); return; } const details: TokenDetails[] = []; // Make individual calls for each token for (let i = 0; i < tokens.length; i++) { const tokenAddress = tokens[i]; try { const [name, symbol, decimals, balance] = await Promise.all([ publicClient.readContract({ address: tokenAddress, abi: ERC20ABI, functionName: 'name', }).catch(() => 'Unknown'), publicClient.readContract({ address: tokenAddress, abi: ERC20ABI, functionName: 'symbol', }).catch(() => '???'), publicClient.readContract({ address: tokenAddress, abi: ERC20ABI, functionName: 'decimals', }).catch(() => 18), publicClient.readContract({ address: tokenAddress, abi: ERC20ABI, functionName: 'balanceOf', args: [userAddress], }).catch(() => BigInt(0)), ]); details.push({ address: tokenAddress, name: name as string, symbol: symbol as string, decimals: Number(decimals), balance: balance as bigint, index: i, }); } catch (err) { // Add token with fallback values if individual call fails details.push({ address: tokenAddress, name: 'Unknown', symbol: '???', decimals: 18, balance: BigInt(0), index: i, }); } } setTokenDetails(details); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch token details'); } finally { setLoading(false); } }; fetchTokenDetails(); }, [tokens, tokensLoading, publicClient, isReady, userAddress]); return { tokenDetails, loading, error, }; } export interface PoolDetails { address: `0x${string}`; name: string; symbol: string; tokens: readonly `0x${string}`[]; price?: string; // Formatted price string tvl?: string; // Formatted TVL string (e.g., "$1.2M") isKilled: boolean; // Whether the pool has been killed } export function useGetAllPools(offset: number = 0, limit: number = 100) { const publicClient = usePublicClient(); const [mounted, setMounted] = useState(false); const [pools, setPools] = useState(null); const [poolDetails, setPoolDetails] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Handle hydration for Next.js static export useEffect(() => { setMounted(true); }, []); useEffect(() => { if (!mounted) return; const fetchPools = async () => { if (!publicClient) { setLoading(false); return; } try { setLoading(true); setError(null); // Get chain ID and contract addresses const chainId = await publicClient.getChainId(); const plannerAddress = (chainInfo as Record)[chainId.toString()]?.v1?.PartyPlanner; const partyInfoAddress = (chainInfo as Record)[chainId.toString()]?.v1?.PartyInfo; if (!plannerAddress || !partyInfoAddress) { setError('IPartyPlanner or PartyInfo contract not found for current chain'); setPools([]); setPoolDetails([]); return; } // Call getAllPools function const result = await publicClient.readContract({ address: plannerAddress as `0x${string}`, abi: IPartyPlannerABI, functionName: 'getAllPools', args: [BigInt(offset), BigInt(limit)], }); setPools(result); // Fetch details for each pool and check if it's working const details: PoolDetails[] = []; for (const poolAddress of result) { try { const [name, symbol, tokens, isWorking] = await Promise.all([ publicClient.readContract({ address: poolAddress, abi: ERC20ABI, functionName: 'name', }).catch(() => 'Unknown Pool'), publicClient.readContract({ address: poolAddress, abi: ERC20ABI, functionName: 'symbol', }).catch(() => 'POOL'), publicClient.readContract({ address: poolAddress, abi: IPartyPoolABI, functionName: 'allTokens', }).catch(() => [] as readonly `0x${string}`[]), publicClient.readContract({ address: partyInfoAddress as `0x${string}`, abi: IPartyInfoABI, functionName: 'working', args: [poolAddress], }).catch(() => false), ]); // Fetch pool price and TVL (only for working pools) let priceStr: string | undefined; let tvlStr: string | undefined; if (isWorking) { // Fetch pool price (use first token as quote, index 0) try { const priceRaw = await publicClient.readContract({ address: partyInfoAddress as `0x${string}`, abi: IPartyInfoABI, functionName: 'poolPrice', args: [poolAddress, BigInt(0)], }); const price = BigInt(priceRaw as bigint | number); if (price === 0n) { priceStr = undefined; } else { // Convert Q64 format to decimal (price = priceValue / 2^64) const Q64 = 2n ** 64n; const isNegative = price < 0n; const absPrice = isNegative ? -price : price; const priceFloat = Number(absPrice) / Number(Q64); const finalPrice = isNegative ? -priceFloat : priceFloat; priceStr = `$${finalPrice.toFixed(4)}`; } } catch (err) { console.error(`Error fetching pool price for ${poolAddress}:`, err); priceStr = undefined; } // Calculate TVL (approximate by getting first token balance and doubling it) try { if (tokens && tokens.length > 0) { const firstTokenAddress = tokens[0]; // Get token decimals and balance const [decimals, balance] = await Promise.all([ publicClient.readContract({ address: firstTokenAddress as `0x${string}`, abi: ERC20ABI, functionName: 'decimals', }) as Promise, publicClient.readContract({ address: firstTokenAddress as `0x${string}`, abi: ERC20ABI, functionName: 'balanceOf', args: [poolAddress], }) as Promise, ]); // Convert balance to float and double it for total TVL approximation const tokenBalance = Number(balance) / Math.pow(10, decimals); const approximateTVL = tokenBalance * 3; tvlStr = formatTVL(approximateTVL); } } catch (err) { console.error(`Error fetching TVL for ${poolAddress}:`, err); tvlStr = undefined; } } // Add all pools (both working and killed) details.push({ address: poolAddress, name: name as string, symbol: symbol as string, tokens: tokens as readonly `0x${string}`[], price: priceStr, tvl: tvlStr, isKilled: !isWorking, }); } catch (err) { console.error('Error fetching pool details for', poolAddress, err); // Skip pools that fail to load } } setPoolDetails(details); } catch (err) { console.error('Error calling getAllPools:', err); setError(err instanceof Error ? err.message : 'Failed to fetch pools'); } finally { setLoading(false); } }; fetchPools(); }, [publicClient, mounted, offset, limit]); return { pools, poolDetails, loading, error, isReady: mounted, }; } export interface SwapMintAmounts { amountInUsed: bigint; fee: bigint; lpMinted: bigint; calculatedSlippage?: number; // Percentage, e.g. 5.5 means 5.5% } export interface BurnSwapAmounts { amountOut: bigint; outFee: bigint; calculatedSlippage?: number; // Percentage, e.g. 5.5 means 5.5% } export function useSwapMintAmounts( poolAddress: `0x${string}` | undefined, inputTokenIndex: number | undefined, maxAmountIn: bigint | undefined, inputTokenDecimals: number | undefined // Decimals of the input token ) { const publicClient = usePublicClient(); const [mounted, setMounted] = useState(false); const [swapMintAmounts, setSwapMintAmounts] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Handle hydration for Next.js static export useEffect(() => { setMounted(true); }, []); useEffect(() => { if (!mounted || !poolAddress || inputTokenIndex === undefined || !maxAmountIn || maxAmountIn === BigInt(0)) { setLoading(false); setSwapMintAmounts(null); return; } const fetchSwapMintAmounts = async () => { if (!publicClient) return; // Early validation if (inputTokenDecimals === undefined) { setError('inputTokenDecimals is required but was undefined'); return; } try { setLoading(true); setError(null); const chainId = await publicClient.getChainId(); const partyInfoAddress = (chainInfo as Record)[chainId.toString()]?.v1?.PartyInfo; if (!partyInfoAddress) { setError('PartyInfo contract not found for current chain'); setSwapMintAmounts(null); return; } const result = await publicClient.readContract({ address: partyInfoAddress as `0x${string}`, abi: IPartyPoolViewerABI, functionName: 'swapMintAmounts', args: [poolAddress, BigInt(inputTokenIndex), maxAmountIn], }) as readonly [bigint, bigint, bigint]; // Fetch and calculate pool price let poolPrice: number | undefined; let calculatedSlippage: number | undefined; try { console.log('input token index', inputTokenIndex); const poolPriceInt128 = await publicClient.readContract({ address: partyInfoAddress as `0x${string}`, abi: IPartyInfoABI, functionName: 'poolPrice', args: [poolAddress, BigInt(inputTokenIndex)], }) as bigint; const basePrice = Number(poolPriceInt128) / (2 ** 64); poolPrice = 1 / (basePrice * Math.pow(10, 18 - inputTokenDecimals)); // Calculate slippage const decimalsMultiplier = Math.pow(10, inputTokenDecimals); const lpMinted = Number(result[1]) / Math.pow(10, 18); const amountIn = Number(result[0]) / decimalsMultiplier; const fee = Number(result[2]) / decimalsMultiplier; const swapPrice = lpMinted / (amountIn - fee); calculatedSlippage = ((poolPrice - swapPrice) / poolPrice) * 100; } catch (priceErr) { console.error('Error fetching poolPrice or calculating slippage:', priceErr); } setSwapMintAmounts({ amountInUsed: result[0], fee: result[2], lpMinted: result[1], calculatedSlippage, }); } catch (err) { console.error('Error calling swapMintAmounts:', err); setError(err instanceof Error ? err.message : 'Failed to fetch swap mint amounts'); setSwapMintAmounts(null); } finally { setLoading(false); } }; fetchSwapMintAmounts(); }, [publicClient, mounted, poolAddress, inputTokenIndex, maxAmountIn, inputTokenDecimals]); return { swapMintAmounts, loading, error, isReady: mounted, }; } export function useBurnSwapAmounts( poolAddress: `0x${string}` | undefined, lpAmount: bigint | undefined, inputTokenIndex: number | undefined, tokenDecimals: number | undefined // Decimals of the output token ) { const publicClient = usePublicClient(); const [mounted, setMounted] = useState(false); const [burnSwapAmounts, setBurnSwapAmounts] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Handle hydration for Next.js static export useEffect(() => { setMounted(true); }, []); useEffect(() => { if (!mounted || !poolAddress || !lpAmount || lpAmount === BigInt(0) || inputTokenIndex === undefined) { setLoading(false); setBurnSwapAmounts(null); return; } const fetchBurnSwapAmounts = async () => { if (!publicClient) { setLoading(false); return; } try { setLoading(true); setError(null); console.log('fetching swap amounts') // Get chain ID and contract address const chainId = await publicClient.getChainId(); // @ts-ignore const partyInfoAddress = (chainInfo as Record)[chainId.toString()]?.v1?.PartyInfo; if (!partyInfoAddress) { console.log('errores here') setError('PartyInfo contract not found for current chain'); setBurnSwapAmounts(null); return; } // Call burnSwapAmounts function - returns [amountOut, outFee] const result = await publicClient.readContract({ address: partyInfoAddress as `0x${string}`, abi: IPartyPoolViewerABI, functionName: 'burnSwapAmounts', args: [poolAddress, lpAmount, BigInt(inputTokenIndex)], }) as readonly [bigint, bigint]; // Calculate slippage for burnSwap using poolPrice let calculatedSlippage: number | undefined; if (tokenDecimals !== undefined) { try { // Get the market price from poolPrice (quoteTokenIndex = 0) const poolPriceInt128 = await publicClient.readContract({ address: partyInfoAddress as `0x${string}`, abi: IPartyInfoABI, functionName: 'poolPrice', args: [poolAddress, BigInt(inputTokenIndex)], }) as bigint; // Convert Q64 format to decimal (price = priceValue / 2^64) let poolPrice = Number(poolPriceInt128) / 2 ** 64; poolPrice = (poolPrice * Math.pow(10, 18 - tokenDecimals)); // For burnSwap: swapPrice = (outAmount + fee) / lpAmount // Need to apply decimal corrections: outAmount and fee are in token decimals, lpAmount is in 18 decimals const outAmountDecimal = Number(result[0]) / Math.pow(10, tokenDecimals); const feeDecimal = Number(result[1]) / Math.pow(10, tokenDecimals); const lpAmountDecimal = Number(lpAmount) / Math.pow(10, 18); // LP tokens have 18 decimals const swapPrice = (outAmountDecimal + feeDecimal) / lpAmountDecimal; calculatedSlippage = ((poolPrice - swapPrice) / poolPrice) * 100; console.log('burnSwap slippage calculation:', { poolPrice, swapPrice, calculatedSlippage, outAmountDecimal, feeDecimal, lpAmountDecimal, outAmount: result[0].toString(), fee: result[1].toString(), lpAmount: lpAmount.toString(), }); } catch (slippageErr) { console.error(`Error calculating slippage for burnSwap:`, slippageErr); } } const parsedAmounts = { amountOut: result[0], outFee: result[1], calculatedSlippage, }; setBurnSwapAmounts(parsedAmounts); } catch (err) { console.error('Error calling burnSwapAmounts:', err); setError(err instanceof Error ? err.message : 'Failed to fetch burn swap amounts'); setBurnSwapAmounts(null); } finally { setLoading(false); } }; fetchBurnSwapAmounts(); }, [publicClient, mounted, poolAddress, lpAmount, inputTokenIndex, tokenDecimals]); return { burnSwapAmounts, loading, error, isReady: mounted, }; } export function useLPTokenBalance( poolAddress: `0x${string}` | undefined, userAddress: `0x${string}` | undefined ) { const publicClient = usePublicClient(); const [mounted, setMounted] = useState(false); const [lpBalance, setLpBalance] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Handle hydration for Next.js static export useEffect(() => { setMounted(true); }, []); useEffect(() => { if (!mounted || !poolAddress || !userAddress) { setLoading(false); setLpBalance(null); return; } const fetchLPBalance = async () => { if (!publicClient) { setLoading(false); return; } try { setLoading(true); setError(null); // Call balanceOf on the pool (which is an ERC20 LP token) const balance = await publicClient.readContract({ address: poolAddress, abi: ERC20ABI, functionName: 'balanceOf', args: [userAddress], }) as bigint; setLpBalance(balance); } catch (err) { console.error('Error fetching LP token balance:', err); setError(err instanceof Error ? err.message : 'Failed to fetch LP balance'); setLpBalance(null); } finally { setLoading(false); } }; fetchLPBalance(); }, [publicClient, mounted, poolAddress, userAddress]); return { lpBalance, loading, error, isReady: mounted, }; } export function useBurnAmounts( poolAddress: `0x${string}` | undefined, lpTokenAmount: bigint | undefined ) { const publicClient = usePublicClient(); const [mounted, setMounted] = useState(false); const [burnAmounts, setBurnAmounts] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { setMounted(true); }, []); useEffect(() => { if (!mounted || !poolAddress || !lpTokenAmount || lpTokenAmount === 0n) { setBurnAmounts(null); setLoading(false); return; } const fetchBurnAmounts = async () => { if (!publicClient) { setLoading(false); return; } try { setLoading(true); setError(null); // Get chain ID and PartyInfo contract address const chainId = await publicClient.getChainId(); const partyInfoAddress = (chainInfo as Record)[chainId.toString()]?.v1?.PartyInfo; if (!partyInfoAddress) { setError('PartyInfo contract not found for current chain'); setBurnAmounts(null); return; } // Call burnAmounts function const amounts = await publicClient.readContract({ address: partyInfoAddress as `0x${string}`, abi: IPartyPoolViewerABI, functionName: 'burnAmounts', args: [poolAddress, lpTokenAmount], }) as bigint[]; setBurnAmounts(amounts); } catch (err) { console.error('Error fetching burn amounts:', err); setError(err instanceof Error ? err.message : 'Failed to fetch burn amounts'); setBurnAmounts(null); } finally { setLoading(false); } }; fetchBurnAmounts(); }, [publicClient, mounted, poolAddress, lpTokenAmount]); return { burnAmounts, loading, error, isReady: mounted, }; }