97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
|
|
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 }
|
|||
|
|
}
|