'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'; import { calculateSlippage } from './usePartyPool'; // 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) console.log('fetching pool price') 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, lpTokenPrice?: number, // Market price of the LP token in decimal format inputTokenDecimals?: number // 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) { setLoading(false); return; } try { setLoading(true); setError(null); // Get chain ID and contract address const chainId = await publicClient.getChainId(); // @ts-ignore const partyInfoAddress = (chainInfo as Record)[chainId.toString()]?.v1?.PartyInfo; if (!partyInfoAddress) { setError('PartyInfo contract not found for current chain'); setSwapMintAmounts(null); return; } // Call swapMintAmounts function 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 the market price if not provided let marketPrice = lpTokenPrice; if (marketPrice === undefined) { try { console.log('input token index', inputTokenIndex) // Get the pool price (price of the pool in terms of the input token) 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) // poolPrice returns how much 1 LP token is worth in terms of the input token marketPrice = Number(poolPriceInt128) / 2 ** 64; console.log('swapMintAmounts fetched marketPrice (poolPrice)', marketPrice); } catch (priceErr) { console.error('Error fetching poolPrice:', priceErr); } } // Calculate slippage if market price is available let calculatedSlippage: number | undefined; if (marketPrice !== undefined) { try { // For swapMint: // - marketPrice: how much 1 LP token is worth in input tokens (from poolPrice) // - swapOutputAmount: LP tokens minted (result[2]) // - swapInputAmount: input token amount used (maxAmountIn) // - inFee: fee charged (result[1]) console.log('LP minted', result[1]) // Convert result[1] to token price using poolPrice: result[1] * poolPrice const lpMintedInTokenPrice = BigInt(Math.floor(Number(result[1]) * marketPrice)); calculatedSlippage = calculateSlippage(marketPrice, lpMintedInTokenPrice, result[0], result[2]); console.log('🎯 swapMint calculatedSlippage: ' + calculatedSlippage.toFixed(4) + '%'); } catch (slippageErr) { console.error(`Error calculating slippage for swapMint:`, slippageErr); } } 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, lpTokenPrice]); return { swapMintAmounts, loading, error, isReady: mounted, }; } export function useBurnSwapAmounts( poolAddress: `0x${string}` | undefined, lpAmount: bigint | undefined, inputTokenIndex: number | undefined, lpTokenPrice?: number, // Market price of the LP token in decimal format tokenDecimals?: number // 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; } console.log('fetching swap amounts 2') // Log inputs console.log('🔍 burnSwapAmounts INPUTS:', { chainId: chainId.toString(), rpcUrl: publicClient.transport?.url || 'Unknown', poolAddress, lpAmount: lpAmount.toString(), inputTokenIndex, partyInfoAddress, }); // 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]; // Log raw result console.log('📊 burnSwapAmounts RAW RESULT:', { resultArray: result, amountOut: result[0].toString(), outFee: result[1].toString(), }); // 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) const marketPrice = Number(poolPriceInt128) / 2 ** 64; // 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; // Calculate slippage percentage: ((swapPrice - marketPrice) / marketPrice) * 100 calculatedSlippage = ((swapPrice - marketPrice) / marketPrice) * 100; console.log('burnSwap slippage calculation:', { marketPrice, 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, }; // Log parsed result console.log('✅ burnSwapAmounts PARSED:', { amountOut: parsedAmounts.amountOut.toString(), outFee: parsedAmounts.outFee.toString(), calculatedSlippage: parsedAmounts.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, lpTokenPrice, 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, }; }