Files
assetx/webapp/components/lending/LendingHeader.tsx

154 lines
5.4 KiB
TypeScript
Raw Normal View History

"use client";
import { useApp } from "@/contexts/AppContext";
import { useReadContract, useReadContracts } from "wagmi";
import { useAccount } from "wagmi";
import { formatUnits } from "viem";
import { abis, getContractAddress } from "@/lib/contracts";
import { useTokenBySymbol } from "@/hooks/useTokenBySymbol";
import { useTokenList } from "@/hooks/useTokenList";
interface LendingHeaderProps {
market: 'USDC' | 'USDT';
}
export default function LendingHeader({ market }: LendingHeaderProps) {
const { t } = useApp();
const { chainId } = useAccount();
const lendingProxyAddress = chainId ? getContractAddress('lendingProxy', chainId) : undefined;
const usdcToken = useTokenBySymbol('USDC');
const usdcDecimals = usdcToken?.onChainDecimals ?? usdcToken?.decimals ?? 18;
// 链上总供应量
const { data: totalSupply } = useReadContract({
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'getTotalSupply',
query: { enabled: !!lendingProxyAddress },
});
// 链上利用率1e18 精度)
const { data: utilization } = useReadContract({
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'getUtilization',
query: { enabled: !!lendingProxyAddress },
});
// 全市场抵押品总量USD
// 用 ERC20.balanceOf(lendingProxy) 获取合约实际持有的 YT token 数量
// (用户存入的抵押品都在合约里,这是最准确的总量)
const { yieldTokens } = useTokenList();
const ytAddresses = yieldTokens.map(t => t.contractAddress).filter(Boolean) as `0x${string}`[];
const erc20BalanceAbi = [{
name: 'balanceOf',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ name: '', type: 'uint256' }],
}] as const;
// 批量读每个 YT token 合约里 lendingProxy 的余额
const { data: balancesData } = useReadContracts({
contracts: ytAddresses.map(addr => ({
address: addr,
abi: erc20BalanceAbi,
functionName: 'balanceOf' as const,
args: [lendingProxyAddress as `0x${string}`],
chainId,
})),
query: { enabled: !!lendingProxyAddress && ytAddresses.length > 0 },
});
// 批量读每个 YT token 的价格30 位精度)
const { data: pricesData } = useReadContracts({
contracts: ytAddresses.map(addr => ({
address: addr as `0x${string}`,
abi: abis.YTToken as any,
functionName: 'ytPrice' as const,
args: [],
chainId,
})),
query: { enabled: ytAddresses.length > 0 },
});
// 计算总抵押品 USD 价值:Σ(balance[i] / 1e18 * price[i] / 1e30)
const totalCollateralUSD = (() => {
if (!balancesData || !pricesData) return null;
let sum = 0;
ytAddresses.forEach((_, i) => {
const bal = balancesData[i];
const pri = pricesData[i];
if (bal?.status !== 'success' || pri?.status !== 'success') return;
const balance = Number(formatUnits(bal.result as bigint, 18));
const price = Number(formatUnits(pri.result as bigint, 30));
sum += balance * price;
});
return sum;
})();
const displayTotalCollateral = totalCollateralUSD !== null
? `$${totalCollateralUSD.toLocaleString('en-US', { maximumFractionDigits: 2 })}`
: '--';
// 格式化总供应量
const displaySupply = totalSupply != null
? `$${parseFloat(formatUnits(totalSupply as bigint, usdcDecimals)).toLocaleString('en-US', { maximumFractionDigits: 2 })}`
: '--';
// 格式化利用率
const displayUtilization = utilization != null
? `${(Number(formatUnits(utilization as bigint, 18)) * 100).toFixed(1)}%`
: '--';
const stats = [
{
label: t("lending.totalUsdcSupply"),
value: displaySupply,
valueColor: "text-text-primary dark:text-white",
},
{
label: t("lending.utilization"),
value: displayUtilization,
valueColor: "text-[#10b981] dark:text-green-400",
},
{
label: t("lending.totalCollateral"),
value: displayTotalCollateral,
valueColor: "text-text-primary dark:text-white",
},
];
return (
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-4 md:p-8 flex flex-col gap-4 md:gap-6">
{/* Title Section */}
<div className="flex flex-col gap-0">
<h1 className="text-2xl md:text-heading-h2 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.01em]">
{market} {t("lending.title")}
</h1>
<p className="text-caption-tiny font-regular text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
{t("lending.subtitle")}
</p>
</div>
{/* Stats Cards移动端2列桌面端3列 */}
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 md:gap-4 w-full">
{stats.map((stat, index) => (
<div
key={index}
className="bg-bg-subtle dark:bg-gray-700 rounded-2xl border border-border-gray dark:border-gray-600 p-3 md:p-4 flex flex-col gap-2"
>
<span className="text-caption-tiny font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
{stat.label}
</span>
<span className={`text-xl md:text-heading-h2 font-bold leading-[130%] tracking-[-0.01em] ${stat.valueColor}`}>
{stat.value}
</span>
</div>
))}
</div>
</div>
);
}