155 lines
6.5 KiB
TypeScript
155 lines
6.5 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
import { useState, useEffect, useMemo } from 'react';
|
|||
|
|
import { useAccount, useReadContract } from 'wagmi';
|
|||
|
|
import { bscTestnet } from 'wagmi/chains';
|
|||
|
|
import { useQuery } from '@tanstack/react-query';
|
|||
|
|
import { useTokenBalance } from '@/hooks/useBalance';
|
|||
|
|
import { useTokenBySymbol } from '@/hooks/useTokenBySymbol';
|
|||
|
|
import { useApp } from '@/contexts/AppContext';
|
|||
|
|
import { fetchContracts } from '@/lib/api/contracts';
|
|||
|
|
import { abis } from '@/lib/contracts';
|
|||
|
|
import ProductHeader from "./ProductHeader";
|
|||
|
|
import StatsCards from "@/components/fundmarket/StatsCards";
|
|||
|
|
import AssetOverviewCard from "./AssetOverviewCard";
|
|||
|
|
import APYHistoryCard from "./APYHistoryCard";
|
|||
|
|
import AssetDescriptionCard from "./AssetDescriptionCard";
|
|||
|
|
import MintSwapPanel from "./MintSwapPanel";
|
|||
|
|
import ProtocolInformation from "./ProtocolInformation";
|
|||
|
|
import PerformanceAnalysis from "./PerformanceAnalysis";
|
|||
|
|
import Season1Rewards from "./Season1Rewards";
|
|||
|
|
import AssetCustodyVerification from "./AssetCustodyVerification";
|
|||
|
|
import { ProductDetail } from "@/lib/api/fundmarket";
|
|||
|
|
|
|||
|
|
interface OverviewTabProps {
|
|||
|
|
product: ProductDetail;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const formatUSD = (v: number) =>
|
|||
|
|
"$" + v.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|||
|
|
|
|||
|
|
export default function OverviewTab({ product }: OverviewTabProps) {
|
|||
|
|
const { t } = useApp();
|
|||
|
|
const { isConnected, chainId } = useAccount();
|
|||
|
|
const ytToken = useTokenBySymbol(product.tokenSymbol);
|
|||
|
|
const { formattedBalance: ytBalance } = useTokenBalance(ytToken?.contractAddress, ytToken?.decimals ?? 18);
|
|||
|
|
|
|||
|
|
// Shared getVaultInfo read — single source of truth for all children
|
|||
|
|
const { data: contractConfigs = [] } = useQuery({
|
|||
|
|
queryKey: ['contract-registry'],
|
|||
|
|
queryFn: fetchContracts,
|
|||
|
|
staleTime: 5 * 60 * 1000,
|
|||
|
|
});
|
|||
|
|
const factoryAddress = useMemo(() => {
|
|||
|
|
const c = contractConfigs.find(c => c.name === 'YTAssetFactory' && c.chain_id === product.chainId);
|
|||
|
|
return c?.address as `0x${string}` | undefined;
|
|||
|
|
}, [contractConfigs, product.chainId]);
|
|||
|
|
|
|||
|
|
const hasContract = !!factoryAddress && !!product.contractAddress;
|
|||
|
|
const { data: vaultInfo, isLoading: isVaultLoading, refetch: refetchVault } = useReadContract({
|
|||
|
|
address: factoryAddress,
|
|||
|
|
abi: abis.YTAssetFactory as any,
|
|||
|
|
functionName: 'getVaultInfo',
|
|||
|
|
args: product.contractAddress ? [product.contractAddress as `0x${string}`] : undefined,
|
|||
|
|
chainId: product.chainId,
|
|||
|
|
query: { enabled: hasContract },
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 3s timeout — after this, fall back to DB snapshot values
|
|||
|
|
const [vaultTimedOut, setVaultTimedOut] = useState(false);
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!hasContract) return;
|
|||
|
|
setVaultTimedOut(false);
|
|||
|
|
const t = setTimeout(() => setVaultTimedOut(true), 3000);
|
|||
|
|
return () => clearTimeout(t);
|
|||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|||
|
|
}, [hasContract]);
|
|||
|
|
|
|||
|
|
const vaultReady = !hasContract || vaultInfo !== undefined || vaultTimedOut;
|
|||
|
|
|
|||
|
|
// Real-time TVL: getVaultInfo[1] = totalAssets (USDC)
|
|||
|
|
const usdcDecimals = product.chainId === 421614 ? 6 : 18;
|
|||
|
|
const totalAssetsRaw: bigint = vaultInfo ? ((vaultInfo as any[])[1] as bigint) ?? 0n : 0n;
|
|||
|
|
const liveTVL = totalAssetsRaw > 0n ? Number(totalAssetsRaw) / Math.pow(10, usdcDecimals) : null;
|
|||
|
|
|
|||
|
|
const tvlDisplay = !vaultReady
|
|||
|
|
? "..."
|
|||
|
|
: liveTVL !== null
|
|||
|
|
? formatUSD(liveTVL)
|
|||
|
|
: formatUSD(product.tvlUsd);
|
|||
|
|
|
|||
|
|
const balanceUSD = parseFloat(ytBalance) * (product.currentPrice || 1);
|
|||
|
|
const balanceDisplay = !isConnected
|
|||
|
|
? "--"
|
|||
|
|
: chainId !== bscTestnet.id
|
|||
|
|
? "-- (Switch to BSC)"
|
|||
|
|
: formatUSD(balanceUSD);
|
|||
|
|
|
|||
|
|
const volume24hDisplay = product.volume24hUsd > 0 ? formatUSD(product.volume24hUsd) : "--";
|
|||
|
|
const volChange = product.volumeChangeVsAvg ?? 0;
|
|||
|
|
const volChangeStr = volChange !== 0
|
|||
|
|
? `${volChange > 0 ? "↑" : "↓"} ${Math.abs(volChange).toFixed(0)}% vs Avg`
|
|||
|
|
: "";
|
|||
|
|
|
|||
|
|
const stats = [
|
|||
|
|
{ label: t("productPage.totalValueLocked"), value: tvlDisplay, change: "", isPositive: true },
|
|||
|
|
{ label: t("productPage.volume24h"), value: volume24hDisplay, change: volChangeStr, isPositive: volChange >= 0 },
|
|||
|
|
{ label: t("productPage.cumulativeYield"), value: formatUSD(0), change: "", isPositive: true },
|
|||
|
|
{ label: t("productPage.yourTotalBalance"), value: balanceDisplay, change: "", isPositive: true },
|
|||
|
|
{ label: t("productPage.yourTotalEarning"), value: "--", change: "", isPositive: true },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8 w-full">
|
|||
|
|
|
|||
|
|
{/* ① Header + Stats — 始终在最顶部,桌面端跨3列 */}
|
|||
|
|
<div className="order-1 md:col-span-3 min-w-0">
|
|||
|
|
<div className="bg-white dark:bg-gray-800 border border-[#f3f4f6] dark:border-gray-700 flex flex-col overflow-hidden rounded-2xl md:rounded-3xl p-4 md:p-8 gap-5 md:gap-6">
|
|||
|
|
<ProductHeader product={product} />
|
|||
|
|
<StatsCards stats={stats} />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* ② 交易板 + Protocol — 移动端排第2,桌面端在右列 */}
|
|||
|
|
<div className="order-2 md:order-3 md:col-span-1 min-w-0 flex flex-col gap-6 md:gap-8">
|
|||
|
|
<MintSwapPanel
|
|||
|
|
tokenType={product.tokenSymbol}
|
|||
|
|
decimals={product.decimals}
|
|||
|
|
onVaultRefresh={refetchVault}
|
|||
|
|
/>
|
|||
|
|
<ProtocolInformation product={product} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* ③ Asset Overview + APY + Description — 移动端排第3,桌面端在左列 */}
|
|||
|
|
<div className="order-3 md:order-2 md:col-span-2 min-w-0 flex flex-col gap-6 md:gap-8">
|
|||
|
|
<AssetOverviewCard
|
|||
|
|
product={product}
|
|||
|
|
vaultInfo={vaultInfo as any[] | undefined}
|
|||
|
|
isVaultLoading={isVaultLoading}
|
|||
|
|
vaultTimedOut={vaultTimedOut}
|
|||
|
|
/>
|
|||
|
|
<APYHistoryCard productId={product.id} />
|
|||
|
|
<div id="asset-description">
|
|||
|
|
<AssetDescriptionCard product={product} />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* ④ Season 1 Rewards — 全宽 */}
|
|||
|
|
<div className="order-4 md:col-span-3 min-w-0">
|
|||
|
|
<Season1Rewards />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* ⑤ Performance Analysis (Daily Net Returns) — 全宽 */}
|
|||
|
|
<div id="performance-analysis" className="order-5 md:col-span-3 min-w-0">
|
|||
|
|
<PerformanceAnalysis productId={product.id} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* ⑥ Asset Custody & Verification — 全宽 */}
|
|||
|
|
<div id="asset-custody" className="order-6 md:col-span-3 min-w-0">
|
|||
|
|
<AssetCustodyVerification product={product} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|