Files
assetx/webapp/hooks/useTokenList.ts

97 lines
3.5 KiB
TypeScript
Raw Permalink Normal View History

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<number, number>()
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<string, Token> = Object.fromEntries(tokens.map(t => [t.symbol, t]))
return { tokens, stablecoins, yieldTokens, bySymbol, isLoading: isProductsLoading }
}