import { useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { useReadContracts } from 'wagmi' import { fetchProducts, Product } from '@/lib/api/fundmarket' import { Token } from '@/lib/api/tokens' import { getDynamicOverride, getDynamicOverrideByName } from '@/lib/contracts/registry' const ERC20_DECIMALS_ABI = [ { name: 'decimals', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint8' }] }, ] as const function productToToken(p: Product): Token { return { symbol: p.tokenSymbol, name: p.name, decimals: p.decimals ?? 18, iconUrl: p.iconUrl, contractAddress: p.contractAddress ?? '', chainId: p.chainId ?? 0, tokenType: p.token_role === 'stablecoin' ? 'stablecoin' : 'yield-token', } } /** * Global token list hook — fetches once, cached 5 min. * All components share the same ['token-list'] React Query cache. * Each token has onChainDecimals (live from chain) with decimals as fallback. */ export function useTokenList() { const { data: products = [], isLoading: isProductsLoading } = useQuery({ queryKey: ['token-list'], queryFn: () => fetchProducts(true), staleTime: 5 * 60 * 1000, }) const baseTokens: Token[] = useMemo( () => products .filter(p => p.token_role === 'stablecoin' || p.token_role === 'yt_token') .map(productToToken), [products] ) // 批量读取每个 token 的链上 decimals,按 token 配置的 chainId 发起请求 // 仅包含有合约地址的 token,记录原始索引以便回写 const tokensWithAddress = useMemo( () => baseTokens .map((t, i) => ({ t, i })) .filter(({ t }) => !!t.contractAddress), [baseTokens] ) const decimalsContracts = useMemo( () => tokensWithAddress.map(({ t }) => ({ address: t.contractAddress as `0x${string}`, abi: ERC20_DECIMALS_ABI, functionName: 'decimals' as const, chainId: t.chainId, })), [tokensWithAddress] ) const { data: onChainData } = useReadContracts({ contracts: decimalsContracts, query: { enabled: decimalsContracts.length > 0 }, }) // 将链上精度回写到对应 token(按原始索引匹配,避免过滤后索引错位) const onChainDecimalsMap = new Map() tokensWithAddress.forEach(({ i }, j) => { if (onChainData?.[j]?.status === 'success') { onChainDecimalsMap.set(i, Number(onChainData[j].result)) } }) // 合并链上精度 + 合约地址(contractAddress 为空时回退到注册表) const tokens: Token[] = baseTokens.map((t, i) => { // DB 未配置地址/chainId 时,先按精确 chainId 查注册表,再按 name 跨链搜索兜底 let contractAddress = t.contractAddress || getDynamicOverride(t.symbol, t.chainId) || '' let chainId = t.chainId if (!contractAddress) { const found = getDynamicOverrideByName(t.symbol) if (found) { contractAddress = found.address chainId = found.chainId // 同时修正 chainId } } const onChainDecimals = onChainDecimalsMap.has(i) ? onChainDecimalsMap.get(i) : undefined return { ...t, contractAddress, chainId, onChainDecimals } }) const stablecoins = tokens.filter(t => t.tokenType === 'stablecoin') const yieldTokens = tokens.filter(t => t.tokenType === 'yield-token') const bySymbol: Record = Object.fromEntries(tokens.map(t => [t.symbol, t])) return { tokens, stablecoins, yieldTokens, bySymbol, isLoading: isProductsLoading } }