Files
assetx/webapp/components/lending/LendingHeader.tsx
default 2ee4553b71 init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
2026-03-27 11:26:43 +00:00

154 lines
5.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}