init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
This commit is contained in:
105
webapp/components/alp/ALPStatsCards.tsx
Normal file
105
webapp/components/alp/ALPStatsCards.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import { useReadContracts } from 'wagmi';
|
||||
import { useAccount } from 'wagmi';
|
||||
import { formatUnits } from 'viem';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useApp } from "@/contexts/AppContext";
|
||||
import { getContractAddress, abis } from '@/lib/contracts';
|
||||
|
||||
function formatUSD(v: number): string {
|
||||
if (v >= 1_000_000) return `$${(v / 1_000_000).toFixed(2)}M`;
|
||||
if (v >= 1_000) return `$${(v / 1_000).toFixed(2)}K`;
|
||||
return `$${v.toFixed(2)}`;
|
||||
}
|
||||
|
||||
export default function ALPStatsCards() {
|
||||
const { t } = useApp();
|
||||
const { chainId } = useAccount();
|
||||
|
||||
const poolManagerAddress = chainId ? getContractAddress('YTPoolManager', chainId) : undefined;
|
||||
const lpTokenAddress = chainId ? getContractAddress('YTLPToken', chainId) : undefined;
|
||||
|
||||
const { data: tvlData, isLoading } = useReadContracts({
|
||||
contracts: [
|
||||
{
|
||||
address: poolManagerAddress,
|
||||
abi: abis.YTPoolManager as any,
|
||||
functionName: 'getAumInUsdy',
|
||||
args: [true],
|
||||
chainId: chainId,
|
||||
},
|
||||
{
|
||||
address: lpTokenAddress,
|
||||
abi: abis.YTLPToken as any,
|
||||
functionName: 'totalSupply',
|
||||
args: [],
|
||||
chainId: chainId,
|
||||
},
|
||||
],
|
||||
query: { refetchInterval: 30000, enabled: !!poolManagerAddress && !!lpTokenAddress && !!chainId },
|
||||
});
|
||||
|
||||
const aumRaw = tvlData?.[0]?.result as bigint | undefined;
|
||||
const supplyRaw = tvlData?.[1]?.result as bigint | undefined;
|
||||
const tvlValue = aumRaw ? formatUSD(parseFloat(formatUnits(aumRaw, 18))) : (isLoading ? '...' : '--');
|
||||
const alpPrice = aumRaw && supplyRaw && supplyRaw > 0n
|
||||
? `$${(parseFloat(formatUnits(aumRaw, 18)) / parseFloat(formatUnits(supplyRaw, 18))).toFixed(4)}`
|
||||
: (isLoading ? '...' : '--');
|
||||
|
||||
// Fetch APR from backend (requires historical snapshots)
|
||||
const { data: aprData } = useQuery({
|
||||
queryKey: ['alp-stats'],
|
||||
queryFn: async () => {
|
||||
const res = await fetch('/api/alp/stats');
|
||||
const json = await res.json();
|
||||
return json.data as { poolAPR: number; rewardAPR: number };
|
||||
},
|
||||
refetchInterval: 5 * 60 * 1000, // refresh every 5 min
|
||||
});
|
||||
|
||||
const poolAPR = aprData?.poolAPR != null
|
||||
? `${aprData.poolAPR.toFixed(4)}%`
|
||||
: '--';
|
||||
|
||||
const stats = [
|
||||
{
|
||||
label: t("stats.totalValueLocked"),
|
||||
value: tvlValue,
|
||||
isGreen: false,
|
||||
},
|
||||
{
|
||||
label: t("alp.price"),
|
||||
value: alpPrice,
|
||||
isGreen: false,
|
||||
},
|
||||
{
|
||||
label: t("alp.poolAPR"),
|
||||
value: poolAPR,
|
||||
isGreen: aprData?.poolAPR != null && aprData.poolAPR > 0,
|
||||
},
|
||||
{
|
||||
label: t("alp.rewardAPR"),
|
||||
value: "--",
|
||||
isGreen: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4">
|
||||
{stats.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-bg-subtle dark:bg-gray-800 rounded-2xl border border-border-gray dark:border-gray-700 px-4 md:px-6 py-4 flex flex-col gap-2"
|
||||
>
|
||||
<div className="text-caption-tiny font-bold text-text-tertiary dark:text-gray-400">
|
||||
{stat.label}
|
||||
</div>
|
||||
<div className={`text-xl md:text-[32px] font-bold leading-[130%] tracking-[-0.01em] ${stat.isGreen ? 'text-[#10b981]' : 'text-text-primary dark:text-white'}`}>
|
||||
{stat.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user