init: 初始化 AssetX 项目仓库

包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
This commit is contained in:
2026-03-27 11:26:43 +00:00
commit 2ee4553b71
634 changed files with 988255 additions and 0 deletions

View File

@@ -0,0 +1,317 @@
"use client";
import Image from "next/image";
import { useEffect, useMemo } from "react";
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 LiquidityAllocationTable({ refreshTrigger }: { refreshTrigger?: number }) {
const { t } = useApp();
const { chainId } = useAccount();
// Fetch all products from backend; filter non-stablecoins to build YT token list
const { data: products = [] } = useQuery<any[]>({
queryKey: ['products'],
queryFn: async () => {
const res = await fetch('/api/fundmarket/products?token_list=1');
const json = await res.json();
return json.data ?? [];
},
staleTime: 5 * 60 * 1000,
});
// Build dynamic YT token list from DB products (only yt_token role)
// Use contractAddress and chainId directly from product data
const ytTokens = useMemo(() =>
products
.filter((p: any) => p.token_role === 'yt_token' && p.contractAddress)
.map((p: any) => ({
key: p.tokenSymbol as string,
address: p.contractAddress as string,
chainId: p.chainId as number,
meta: p,
})),
[products]);
// Derive target chainId from products, fallback to connected wallet
const targetChainId = ytTokens[0]?.chainId ?? chainId;
// System contract addresses from registry
const ytVaultAddr = targetChainId ? getContractAddress('YTVault', targetChainId) : undefined;
const usdcAddr = targetChainId ? getContractAddress('USDC', targetChainId) : undefined;
const priceFeedAddr = targetChainId ? getContractAddress('YTPriceFeed', targetChainId) : undefined;
const N = ytTokens.length;
const addressesReady = !!(ytVaultAddr && usdcAddr && priceFeedAddr && N > 0 && ytTokens.every(t => t.address));
// Dynamic batch on-chain reads — indices scale with N = ytTokens.length:
// [0] YTVault.getPoolValue(true)
// [1..N] YTVault.usdyAmounts(ytToken[i])
// [N+1..2N] YTPriceFeed.getPrice(ytToken[i], false)
// [2N+1] USDC.balanceOf(ytVaultAddr)
// [2N+2] YTPriceFeed.getPrice(usdcAddr, false)
// [2N+3] YTVault.totalTokenWeights()
// [2N+4..3N+3] YTVault.tokenWeights(ytToken[i])
// [3N+4] YTVault.tokenWeights(usdcAddr)
const { data: chainData, refetch: refetchChain } = useReadContracts({
contracts: [
{
address: ytVaultAddr,
abi: abis.YTVault as any,
functionName: 'getPoolValue',
args: [true],
chainId: targetChainId,
} as const,
...ytTokens.map(tk => ({
address: ytVaultAddr,
abi: abis.YTVault as any,
functionName: 'usdyAmounts',
args: [tk.address],
chainId: targetChainId,
} as const)),
...ytTokens.map(tk => ({
address: priceFeedAddr,
abi: abis.YTPriceFeed as any,
functionName: 'getPrice',
args: [tk.address, false],
chainId: targetChainId,
} as const)),
{
address: usdcAddr,
abi: abis.USDY as any,
functionName: 'balanceOf',
args: [ytVaultAddr],
chainId: targetChainId,
} as const,
{
address: priceFeedAddr,
abi: abis.YTPriceFeed as any,
functionName: 'getPrice',
args: [usdcAddr, false],
chainId: targetChainId,
} as const,
{
address: ytVaultAddr,
abi: abis.YTVault as any,
functionName: 'totalTokenWeights',
chainId: targetChainId,
} as const,
...ytTokens.map(tk => ({
address: ytVaultAddr,
abi: abis.YTVault as any,
functionName: 'tokenWeights',
args: [tk.address],
chainId: targetChainId,
} as const)),
{
address: ytVaultAddr,
abi: abis.YTVault as any,
functionName: 'tokenWeights',
args: [usdcAddr],
chainId: targetChainId,
} as const,
],
query: { refetchInterval: 30000, enabled: addressesReady },
});
useEffect(() => {
if (refreshTrigger) refetchChain();
}, [refreshTrigger]);
// Parse results with dynamic indices
const totalPoolRaw = chainData?.[0]?.result as bigint | undefined;
const totalWeightsRaw = chainData?.[2 * N + 3]?.result as bigint | undefined;
const totalPool = totalPoolRaw ? parseFloat(formatUnits(totalPoolRaw, 18)) : 0;
const totalWeights = totalWeightsRaw ? Number(totalWeightsRaw) : 0;
const ytAllocations = ytTokens.map((token, i) => {
const usdyRaw = chainData?.[1 + i]?.result as bigint | undefined;
const priceRaw = chainData?.[N + 1 + i]?.result as bigint | undefined;
const twRaw = chainData?.[2 * N + 4 + i]?.result as bigint | undefined;
const poolSizeUSD = usdyRaw ? parseFloat(formatUnits(usdyRaw, 18)) : 0;
const ytPrice = priceRaw ? parseFloat(formatUnits(priceRaw, 30)) : 0;
const balance = ytPrice > 0 ? poolSizeUSD / ytPrice : 0;
const currentWeight = totalPool > 0 ? poolSizeUSD / totalPool * 100 : 0;
const targetWeight = totalWeights > 0 && twRaw != null ? Number(twRaw) / totalWeights * 100 : 0;
return {
key: token.key,
name: token.meta?.name ?? token.key,
category: token.meta?.category ?? '--',
iconUrl: token.meta?.iconUrl,
poolSizeUSD,
balance,
ytPrice,
currentWeight,
targetWeight,
isStable: false,
};
});
const usdcRaw = chainData?.[2 * N + 1]?.result as bigint | undefined;
const usdcPriceRaw = chainData?.[2 * N + 2]?.result as bigint | undefined;
const usdcTWRaw = chainData?.[3 * N + 4]?.result as bigint | undefined;
const usdcBalance = usdcRaw ? parseFloat(formatUnits(usdcRaw, 18)) : 0;
const usdcPrice = usdcPriceRaw ? parseFloat(formatUnits(usdcPriceRaw, 30)) : 0;
const usdcPoolSize = usdcBalance * usdcPrice;
const usdcCurWeight = totalPool > 0 ? usdcPoolSize / totalPool * 100 : 0;
const usdcTgtWeight = totalWeights > 0 && usdcTWRaw != null ? Number(usdcTWRaw) / totalWeights * 100 : 0;
const usdcMeta = products.find((p: any) => p.tokenSymbol === 'USDC');
const allocations = [
...ytAllocations,
{
key: 'USDC',
name: usdcMeta?.name ?? 'USD Coin',
category: usdcMeta?.category ?? 'Stablecoin',
iconUrl: usdcMeta?.iconUrl,
poolSizeUSD: usdcPoolSize,
balance: usdcBalance,
ytPrice: usdcPrice,
currentWeight: usdcCurWeight,
targetWeight: usdcTgtWeight,
isStable: true,
},
];
return (
<div className="flex flex-col gap-3">
{/* Title */}
<h2 className="text-heading-h3 font-bold text-text-primary dark:text-white">
{t("alp.liquidityAllocation")}
</h2>
{/* 移动端:卡片布局 */}
<div className="md:hidden flex flex-col gap-3">
{allocations.map((item) => (
<div
key={item.key}
className="bg-bg-surface dark:bg-gray-800 rounded-2xl border border-border-gray dark:border-gray-700 p-4 flex flex-col gap-3"
>
{/* Token Header */}
<div className="flex items-center gap-3">
<Image
src={item.iconUrl || '/assets/tokens/default.svg'}
alt={item.key}
width={36}
height={36}
className="rounded-full"
onError={(e) => { (e.target as HTMLImageElement).src = '/assets/tokens/default.svg'; }}
/>
<div className="flex flex-col">
<span className="text-body-small font-bold text-text-primary dark:text-white">{item.name}</span>
<span className="text-caption-tiny text-text-tertiary dark:text-gray-400">{item.category}</span>
</div>
</div>
{/* Info Grid */}
<div className="grid grid-cols-3 gap-2">
<div className="flex flex-col gap-0.5">
<span className="text-caption-tiny text-text-tertiary dark:text-gray-400">{t("alp.poolSize")}</span>
<span className="text-body-small font-bold text-text-primary dark:text-white">
{item.poolSizeUSD > 0 ? formatUSD(item.poolSizeUSD) : '--'}
</span>
<span className="text-caption-tiny text-[#6b7280] dark:text-gray-400">
{item.balance > 0 ? `${item.balance.toFixed(2)} ${item.key}` : '--'}
</span>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-caption-tiny text-text-tertiary dark:text-gray-400">Weight</span>
<span className="text-body-small font-bold text-text-primary dark:text-white">
{totalPool > 0 ? `${item.currentWeight.toFixed(1)}%` : '--'}
</span>
<span className="text-caption-tiny text-text-tertiary dark:text-gray-400">
{item.targetWeight > 0 ? `${item.targetWeight.toFixed(1)}%` : '--'}
</span>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-caption-tiny text-text-tertiary dark:text-gray-400">{t("alp.currentPrice")}</span>
<span className="text-body-small font-bold text-text-primary dark:text-white">
{item.ytPrice > 0 ? `$${item.ytPrice.toFixed(4)}` : '--'}
</span>
</div>
</div>
</div>
))}
</div>
{/* 桌面端:表格布局 */}
<div className="hidden md:block bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 overflow-hidden">
<div className="flex flex-col">
{/* Header */}
<div className="flex border-b border-border-gray dark:border-gray-700 flex-shrink-0">
<div className="flex-1 px-6 py-4">
<div className="text-caption-tiny font-medium text-[#4b5563] dark:text-gray-400">{t("alp.token")}</div>
</div>
<div className="flex-1 px-6 py-4">
<div className="text-caption-tiny font-medium text-[#4b5563] dark:text-gray-400">{t("alp.poolSize")}</div>
</div>
<div className="flex-1 px-6 py-4">
<div className="text-caption-tiny font-medium text-[#4b5563] dark:text-gray-400">Weight (Current / Target)</div>
</div>
<div className="flex-1 px-6 py-4">
<div className="text-caption-tiny font-medium text-[#4b5563] dark:text-gray-400">{t("alp.currentPrice")}</div>
</div>
</div>
{/* Body */}
{allocations.map((item) => (
<div key={item.key} className="flex items-center border-b border-border-gray dark:border-gray-700 last:border-b-0">
<div className="flex-1 px-6 py-4 flex items-center gap-3">
<Image
src={item.iconUrl || '/assets/tokens/default.svg'}
alt={item.key}
width={32}
height={32}
className="rounded-full"
onError={(e) => { (e.target as HTMLImageElement).src = '/assets/tokens/default.svg'; }}
/>
<div className="flex flex-col">
<span className="text-body-small font-bold text-text-primary dark:text-white">{item.name}</span>
<span className="text-caption-tiny font-regular text-text-tertiary dark:text-gray-400">{item.category}</span>
</div>
</div>
<div className="flex-1 px-6 py-4">
<div className="flex flex-col">
<span className="text-body-small font-bold text-text-primary dark:text-white">
{item.poolSizeUSD > 0 ? formatUSD(item.poolSizeUSD) : '--'}
</span>
<span className="text-caption-tiny font-regular text-[#6b7280] dark:text-gray-400">
{item.balance > 0 ? `${item.balance.toFixed(2)} ${item.key}` : '--'}
</span>
</div>
</div>
<div className="flex-1 px-6 py-4">
<span className="text-body-small font-bold text-text-primary dark:text-white">
{totalPool > 0 ? (
<>
{item.currentWeight.toFixed(1)}%
<span className="font-regular text-text-tertiary dark:text-gray-400">
{' / '}{item.targetWeight > 0 ? `${item.targetWeight.toFixed(1)}%` : '--'}
</span>
</>
) : '--'}
</span>
</div>
<div className="flex-1 px-6 py-4">
<span className="text-body-small font-bold text-text-primary dark:text-white">
{item.ytPrice > 0 ? `$${item.ytPrice.toFixed(4)}` : '--'}
</span>
</div>
</div>
))}
</div>
</div>
</div>
);
}