init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
This commit is contained in:
153
webapp/components/lending/LendingHeader.tsx
Normal file
153
webapp/components/lending/LendingHeader.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user