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