"use client"; import Image from "next/image"; import { useEffect, useMemo } from "react"; import { useReadContracts } from 'wagmi'; import { useAccount } from 'wagmi'; import { formatUnits } from 'viem'; import { useQuery } from '@tanstack/react-query'; import { useApp } from "@/contexts/AppContext"; import { getContractAddress, abis } from '@/lib/contracts'; function formatUSD(v: number): string { if (v >= 1_000_000) return `$${(v / 1_000_000).toFixed(2)}M`; if (v >= 1_000) return `$${(v / 1_000).toFixed(2)}K`; return `$${v.toFixed(2)}`; } export default function LiquidityAllocationTable({ refreshTrigger }: { refreshTrigger?: number }) { const { t } = useApp(); const { chainId } = useAccount(); // Fetch all products from backend; filter non-stablecoins to build YT token list const { data: products = [] } = useQuery({ queryKey: ['products'], queryFn: async () => { const res = await fetch('/api/fundmarket/products?token_list=1'); const json = await res.json(); return json.data ?? []; }, staleTime: 5 * 60 * 1000, }); // Build dynamic YT token list from DB products (only yt_token role) // Use contractAddress and chainId directly from product data const ytTokens = useMemo(() => products .filter((p: any) => p.token_role === 'yt_token' && p.contractAddress) .map((p: any) => ({ key: p.tokenSymbol as string, address: p.contractAddress as string, chainId: p.chainId as number, meta: p, })), [products]); // Derive target chainId from products, fallback to connected wallet const targetChainId = ytTokens[0]?.chainId ?? chainId; // System contract addresses from registry const ytVaultAddr = targetChainId ? getContractAddress('YTVault', targetChainId) : undefined; const usdcAddr = targetChainId ? getContractAddress('USDC', targetChainId) : undefined; const priceFeedAddr = targetChainId ? getContractAddress('YTPriceFeed', targetChainId) : undefined; const N = ytTokens.length; const addressesReady = !!(ytVaultAddr && usdcAddr && priceFeedAddr && N > 0 && ytTokens.every(t => t.address)); // Dynamic batch on-chain reads — indices scale with N = ytTokens.length: // [0] YTVault.getPoolValue(true) // [1..N] YTVault.usdyAmounts(ytToken[i]) // [N+1..2N] YTPriceFeed.getPrice(ytToken[i], false) // [2N+1] USDC.balanceOf(ytVaultAddr) // [2N+2] YTPriceFeed.getPrice(usdcAddr, false) // [2N+3] YTVault.totalTokenWeights() // [2N+4..3N+3] YTVault.tokenWeights(ytToken[i]) // [3N+4] YTVault.tokenWeights(usdcAddr) const { data: chainData, refetch: refetchChain } = useReadContracts({ contracts: [ { address: ytVaultAddr, abi: abis.YTVault as any, functionName: 'getPoolValue', args: [true], chainId: targetChainId, } as const, ...ytTokens.map(tk => ({ address: ytVaultAddr, abi: abis.YTVault as any, functionName: 'usdyAmounts', args: [tk.address], chainId: targetChainId, } as const)), ...ytTokens.map(tk => ({ address: priceFeedAddr, abi: abis.YTPriceFeed as any, functionName: 'getPrice', args: [tk.address, false], chainId: targetChainId, } as const)), { address: usdcAddr, abi: abis.USDY as any, functionName: 'balanceOf', args: [ytVaultAddr], chainId: targetChainId, } as const, { address: priceFeedAddr, abi: abis.YTPriceFeed as any, functionName: 'getPrice', args: [usdcAddr, false], chainId: targetChainId, } as const, { address: ytVaultAddr, abi: abis.YTVault as any, functionName: 'totalTokenWeights', chainId: targetChainId, } as const, ...ytTokens.map(tk => ({ address: ytVaultAddr, abi: abis.YTVault as any, functionName: 'tokenWeights', args: [tk.address], chainId: targetChainId, } as const)), { address: ytVaultAddr, abi: abis.YTVault as any, functionName: 'tokenWeights', args: [usdcAddr], chainId: targetChainId, } as const, ], query: { refetchInterval: 30000, enabled: addressesReady }, }); useEffect(() => { if (refreshTrigger) refetchChain(); }, [refreshTrigger]); // Parse results with dynamic indices const totalPoolRaw = chainData?.[0]?.result as bigint | undefined; const totalWeightsRaw = chainData?.[2 * N + 3]?.result as bigint | undefined; const totalPool = totalPoolRaw ? parseFloat(formatUnits(totalPoolRaw, 18)) : 0; const totalWeights = totalWeightsRaw ? Number(totalWeightsRaw) : 0; const ytAllocations = ytTokens.map((token, i) => { const usdyRaw = chainData?.[1 + i]?.result as bigint | undefined; const priceRaw = chainData?.[N + 1 + i]?.result as bigint | undefined; const twRaw = chainData?.[2 * N + 4 + i]?.result as bigint | undefined; const poolSizeUSD = usdyRaw ? parseFloat(formatUnits(usdyRaw, 18)) : 0; const ytPrice = priceRaw ? parseFloat(formatUnits(priceRaw, 30)) : 0; const balance = ytPrice > 0 ? poolSizeUSD / ytPrice : 0; const currentWeight = totalPool > 0 ? poolSizeUSD / totalPool * 100 : 0; const targetWeight = totalWeights > 0 && twRaw != null ? Number(twRaw) / totalWeights * 100 : 0; return { key: token.key, name: token.meta?.name ?? token.key, category: token.meta?.category ?? '--', iconUrl: token.meta?.iconUrl, poolSizeUSD, balance, ytPrice, currentWeight, targetWeight, isStable: false, }; }); const usdcRaw = chainData?.[2 * N + 1]?.result as bigint | undefined; const usdcPriceRaw = chainData?.[2 * N + 2]?.result as bigint | undefined; const usdcTWRaw = chainData?.[3 * N + 4]?.result as bigint | undefined; const usdcBalance = usdcRaw ? parseFloat(formatUnits(usdcRaw, 18)) : 0; const usdcPrice = usdcPriceRaw ? parseFloat(formatUnits(usdcPriceRaw, 30)) : 0; const usdcPoolSize = usdcBalance * usdcPrice; const usdcCurWeight = totalPool > 0 ? usdcPoolSize / totalPool * 100 : 0; const usdcTgtWeight = totalWeights > 0 && usdcTWRaw != null ? Number(usdcTWRaw) / totalWeights * 100 : 0; const usdcMeta = products.find((p: any) => p.tokenSymbol === 'USDC'); const allocations = [ ...ytAllocations, { key: 'USDC', name: usdcMeta?.name ?? 'USD Coin', category: usdcMeta?.category ?? 'Stablecoin', iconUrl: usdcMeta?.iconUrl, poolSizeUSD: usdcPoolSize, balance: usdcBalance, ytPrice: usdcPrice, currentWeight: usdcCurWeight, targetWeight: usdcTgtWeight, isStable: true, }, ]; return (
{/* Title */}

{t("alp.liquidityAllocation")}

{/* 移动端:卡片布局 */}
{allocations.map((item) => (
{/* Token Header */}
{item.key} { (e.target as HTMLImageElement).src = '/assets/tokens/default.svg'; }} />
{item.name} {item.category}
{/* Info Grid */}
{t("alp.poolSize")} {item.poolSizeUSD > 0 ? formatUSD(item.poolSizeUSD) : '--'} {item.balance > 0 ? `${item.balance.toFixed(2)} ${item.key}` : '--'}
Weight {totalPool > 0 ? `${item.currentWeight.toFixed(1)}%` : '--'} {item.targetWeight > 0 ? `→ ${item.targetWeight.toFixed(1)}%` : '--'}
{t("alp.currentPrice")} {item.ytPrice > 0 ? `$${item.ytPrice.toFixed(4)}` : '--'}
))}
{/* 桌面端:表格布局 */}
{/* Header */}
{t("alp.token")}
{t("alp.poolSize")}
Weight (Current / Target)
{t("alp.currentPrice")}
{/* Body */} {allocations.map((item) => (
{item.key} { (e.target as HTMLImageElement).src = '/assets/tokens/default.svg'; }} />
{item.name} {item.category}
{item.poolSizeUSD > 0 ? formatUSD(item.poolSizeUSD) : '--'} {item.balance > 0 ? `${item.balance.toFixed(2)} ${item.key}` : '--'}
{totalPool > 0 ? ( <> {item.currentWeight.toFixed(1)}% {' / '}{item.targetWeight > 0 ? `${item.targetWeight.toFixed(1)}%` : '--'} ) : '--'}
{item.ytPrice > 0 ? `$${item.ytPrice.toFixed(4)}` : '--'}
))}
); }