154 lines
5.4 KiB
TypeScript
154 lines
5.4 KiB
TypeScript
|
|
"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>
|
|||
|
|
);
|
|||
|
|
}
|