包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
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 }
|
||
}
|