"use client"; import { useState, useEffect, useMemo } from "react"; import Image from "next/image"; import { useApp } from "@/contexts/AppContext"; import { Tabs, Tab, Button } from "@heroui/react"; import ReviewModal from "@/components/common/ReviewModal"; import WithdrawModal from "@/components/common/WithdrawModal"; import { buttonStyles } from "@/lib/buttonStyles"; import { useAccount, useReadContract } from 'wagmi'; import { useQuery } from '@tanstack/react-query'; import { fetchContracts } from '@/lib/api/contracts'; import { abis } from '@/lib/contracts'; import { useUSDCBalance, useTokenBalance } from '@/hooks/useBalance'; import { useDeposit } from '@/hooks/useDeposit'; import { useWithdraw } from '@/hooks/useWithdraw'; import { getTxUrl } from '@/lib/contracts'; import { useTokenBySymbol } from '@/hooks/useTokenBySymbol'; import { toast } from "sonner"; import TokenSelector from '@/components/common/TokenSelector'; import { Token } from '@/lib/api/tokens'; import { useAppKit } from "@reown/appkit/react"; interface MintSwapPanelProps { tokenType?: string; decimals?: number; onVaultRefresh?: () => void; } export default function MintSwapPanel({ tokenType = 'YT-A', decimals, onVaultRefresh }: MintSwapPanelProps) { const { t } = useApp(); const [activeAction, setActiveAction] = useState<"deposit" | "withdraw">("deposit"); const [amount, setAmount] = useState(""); const [selectedToken, setSelectedToken] = useState(); const [isReviewModalOpen, setIsReviewModalOpen] = useState(false); const [isWithdrawModalOpen, setIsWithdrawModalOpen] = useState(false); const [mounted, setMounted] = useState(false); // 避免 hydration 错误 useEffect(() => { setMounted(true); }, []); // Web3 集成 const { isConnected } = useAccount(); const { open } = useAppKit(); const { formattedBalance: usdcBalance, isLoading: isBalanceLoading, refetch: refetchBalance } = useUSDCBalance(); const ytToken = useTokenBySymbol(tokenType ?? ''); const { formattedBalance: ytBalance, refetch: refetchYT } = useTokenBalance( ytToken?.contractAddress, ytToken?.decimals ?? decimals ?? 18 ); const { status: depositStatus, error: depositError, isLoading: isDepositLoading, approveHash, depositHash, executeApproveAndDeposit, reset: resetDeposit, } = useDeposit(ytToken); const { status: withdrawStatus, error: withdrawError, isLoading: isWithdrawLoading, approveHash: withdrawApproveHash, withdrawHash, executeApproveAndWithdraw, reset: resetWithdraw, } = useWithdraw(ytToken); // 从合约读取 nextRedemptionTime,复用 contract-registry 缓存 const { data: contractConfigs = [] } = useQuery({ queryKey: ['contract-registry'], queryFn: fetchContracts, staleTime: 5 * 60 * 1000, }); const vaultChainId = ytToken?.chainId ?? 97; const factoryAddress = useMemo(() => { const c = contractConfigs.find(c => c.name === 'YTAssetFactory' && c.chain_id === vaultChainId); return c?.address as `0x${string}` | undefined; }, [contractConfigs, vaultChainId]); const priceFeedAddress = useMemo(() => { const c = contractConfigs.find(c => c.name === 'YTPriceFeed' && c.chain_id === vaultChainId); return c?.address as `0x${string}` | undefined; }, [contractConfigs, vaultChainId]); // Read selected token price from YTPriceFeed (30 dec precision) const { data: tokenPriceRaw } = useReadContract({ address: priceFeedAddress, abi: abis.YTPriceFeed as any, functionName: 'getPrice', args: selectedToken?.contractAddress ? [selectedToken.contractAddress as `0x${string}`, false] : undefined, chainId: vaultChainId, query: { enabled: !!priceFeedAddress && !!selectedToken?.contractAddress }, }); const selectedTokenUSDPrice = tokenPriceRaw && (tokenPriceRaw as bigint) > 0n ? Number(tokenPriceRaw as bigint) / 1e30 : null; // null = price not yet loaded const { data: vaultInfo, isLoading: isVaultInfoLoading } = useReadContract({ address: factoryAddress, abi: abis.YTAssetFactory as any, functionName: 'getVaultInfo', args: ytToken?.contractAddress ? [ytToken.contractAddress as `0x${string}`] : undefined, chainId: vaultChainId, query: { enabled: !!factoryAddress && !!ytToken?.contractAddress }, }); // 超时 3 秒后不再等合约数据 const [vaultInfoTimedOut, setVaultInfoTimedOut] = useState(false); useEffect(() => { if (!factoryAddress || !ytToken?.contractAddress) return; setVaultInfoTimedOut(false); const t = setTimeout(() => setVaultInfoTimedOut(true), 3000); return () => clearTimeout(t); }, [factoryAddress, ytToken?.contractAddress]); // nextRedemptionTime: getVaultInfo 返回值 index[8] const nextRedemptionTime = vaultInfo ? Number((vaultInfo as any[])[8]) : 0; const isMatured = nextRedemptionTime > 0 && nextRedemptionTime * 1000 < Date.now(); // isFull: totalSupply[4] >= hardCap[5] const vaultTotalSupply: bigint = vaultInfo ? ((vaultInfo as any[])[4] as bigint) ?? 0n : 0n; const vaultHardCap: bigint = vaultInfo ? ((vaultInfo as any[])[5] as bigint) ?? 0n : 0n; const isFull = vaultHardCap > 0n && vaultTotalSupply >= vaultHardCap; const maturityDateStr = nextRedemptionTime > 0 ? new Date(nextRedemptionTime * 1000).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) : ''; // ytPrice: getVaultInfo 返回值 index[7],30 位小数 const ytPriceRaw: bigint = vaultInfo ? ((vaultInfo as any[])[7] as bigint) ?? 0n : 0n; const ytPriceDisplay = (() => { if (!ytPriceRaw || ytPriceRaw <= 0n) return '--'; const divisor = 10n ** 30n; const intPart = ytPriceRaw / divisor; const fracScaled = ((ytPriceRaw % divisor) * 1_000_000n) / divisor; return `$${intPart}.${fracScaled.toString().padStart(6, '0')}`; })(); const usdcToken = useTokenBySymbol('USDC'); const usdcDecimals = usdcToken?.onChainDecimals ?? usdcToken?.decimals ?? 18; const ytDecimals = ytToken?.onChainDecimals ?? ytToken?.decimals ?? decimals ?? 18; const inputDecimals = activeAction === 'deposit' ? usdcDecimals : ytDecimals; const displayDecimals = Math.min(inputDecimals, 6); const truncateDecimals = (s: string, d: number) => { const p = s.split('.'); return p.length > 1 ? `${p[0]}.${p[1].slice(0, d)}` : s; }; const isValidAmount = (v: string) => v !== '' && !isNaN(parseFloat(v)) && parseFloat(v) > 0; // ytPrice as float for You Get calculation (30 dec) const ytPrice = ytPriceRaw > 0n ? Number(ytPriceRaw) / 1e30 : 0; // You Get = selectedTokenUSDPrice × amount / ytPrice const depositYouGet = ytPrice > 0 && selectedTokenUSDPrice !== null && isValidAmount(amount) ? (selectedTokenUSDPrice * parseFloat(amount)) / ytPrice : null; const handleAmountChange = (value: string) => { if (value === '') { setAmount(value); return; } if (!/^\d*\.?\d*$/.test(value)) return; const parts = value.split('.'); if (parts.length > 1 && parts[1].length > displayDecimals) return; const maxBalance = activeAction === 'deposit' ? parseFloat(usdcBalance) : parseFloat(ytBalance); if (parseFloat(value) > maxBalance) { setAmount(truncateDecimals(activeAction === 'deposit' ? usdcBalance : ytBalance, displayDecimals)); return; } setAmount(value); }; const DEPOSIT_TOAST_ID = 'mint-deposit-tx'; const WITHDRAW_TOAST_ID = 'mint-withdraw-tx'; // Approve 交易提交 toast useEffect(() => { if (approveHash) { toast.loading(t("mintSwap.toast.approvalSubmitted"), { id: DEPOSIT_TOAST_ID, description: t("mintSwap.toast.waitingConfirmation"), action: { label: t("mintSwap.toast.viewTx"), onClick: () => window.open(getTxUrl(approveHash), '_blank') }, }); } }, [approveHash]); useEffect(() => { if (withdrawApproveHash) { toast.loading(t("mintSwap.toast.approvalSubmitted"), { id: WITHDRAW_TOAST_ID, description: t("mintSwap.toast.waitingConfirmation"), action: { label: t("mintSwap.toast.viewTx"), onClick: () => window.open(getTxUrl(withdrawApproveHash), '_blank') }, }); } }, [withdrawApproveHash]); // 主交易提交 toast useEffect(() => { if (depositHash && depositStatus === 'depositing') { toast.loading(t("mintSwap.toast.depositSubmitted"), { id: DEPOSIT_TOAST_ID, description: t("mintSwap.toast.waitingConfirmation"), action: { label: t("mintSwap.toast.viewTx"), onClick: () => window.open(getTxUrl(depositHash), '_blank') }, }); } }, [depositHash, depositStatus]); useEffect(() => { if (withdrawHash && withdrawStatus === 'withdrawing') { toast.loading(t("mintSwap.toast.withdrawSubmitted"), { id: WITHDRAW_TOAST_ID, description: t("mintSwap.toast.waitingConfirmation"), action: { label: t("mintSwap.toast.viewTx"), onClick: () => window.open(getTxUrl(withdrawHash), '_blank') }, }); } }, [withdrawHash, withdrawStatus]); // 存款成功后刷新余额 useEffect(() => { if (depositStatus === 'success') { toast.success(t("mintSwap.toast.depositSuccess"), { id: DEPOSIT_TOAST_ID, description: t("mintSwap.toast.depositSuccessDesc"), duration: 5000, }); refetchBalance(); refetchYT(); onVaultRefresh?.(); const timer1 = setTimeout(() => { refetchBalance(); refetchYT(); onVaultRefresh?.(); }, 3000); const timer2 = setTimeout(() => { resetDeposit(); setAmount(""); }, 6000); return () => { clearTimeout(timer1); clearTimeout(timer2); }; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [depositStatus]); // 取款成功后刷新余额 useEffect(() => { if (withdrawStatus === 'success') { toast.success(t("mintSwap.toast.withdrawSuccess"), { id: WITHDRAW_TOAST_ID, description: t("mintSwap.toast.withdrawSuccessDesc"), duration: 5000, }); refetchBalance(); refetchYT(); onVaultRefresh?.(); const timer1 = setTimeout(() => { refetchBalance(); refetchYT(); onVaultRefresh?.(); }, 3000); const timer2 = setTimeout(() => { resetWithdraw(); setAmount(""); }, 6000); return () => { clearTimeout(timer1); clearTimeout(timer2); }; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [withdrawStatus]); // 错误 toast useEffect(() => { if (depositError) { if (depositError === 'Transaction cancelled') { toast.dismiss(DEPOSIT_TOAST_ID); } else { toast.error(t("mintSwap.toast.depositFailed"), { id: DEPOSIT_TOAST_ID, duration: 5000 }); } } }, [depositError]); useEffect(() => { if (withdrawError) { if (withdrawError === 'Transaction cancelled') { toast.dismiss(WITHDRAW_TOAST_ID); } else { toast.error(t("mintSwap.toast.withdrawFailed"), { id: WITHDRAW_TOAST_ID, duration: 5000 }); } } }, [withdrawError]); return (
{/* Content */}
{/* Deposit/Withdraw Toggle */} { const next = key as "deposit" | "withdraw"; setActiveAction(next); setAmount(""); if (next === "deposit") { resetWithdraw(); } else { resetDeposit(); } }} variant="solid" classNames={{ base: "w-full", tabList: "bg-[#f9fafb] dark:bg-gray-700 rounded-xl p-1 gap-0 w-full", cursor: "bg-bg-surface dark:bg-gray-600 shadow-sm", tab: "h-8 px-4", tabContent: "text-body-small font-medium text-text-tertiary dark:text-gray-400 group-data-[selected=true]:font-bold group-data-[selected=true]:text-text-primary dark:group-data-[selected=true]:text-white", }} > {/* Input Area */}
{/* Label and Balance */}
{activeAction === 'deposit' ? t("mintSwap.deposit") : t("mintSwap.withdraw")}
{t("mintSwap.balance")}: {!mounted ? '0' : (isBalanceLoading ? '...' : activeAction === 'deposit' ? `$${parseFloat(truncateDecimals(usdcBalance, displayDecimals)).toLocaleString('en-US', { maximumFractionDigits: displayDecimals })}` : `${parseFloat(truncateDecimals(ytBalance, displayDecimals)).toLocaleString('en-US', { maximumFractionDigits: displayDecimals })} ${tokenType}`)}
{/* Input Row */}
handleAmountChange(e.target.value)} className="w-full text-left text-heading-h3 font-bold text-text-primary dark:text-white placeholder:text-[#d1d5db] dark:placeholder:text-gray-500 bg-transparent border-none outline-none" /> {amount ? (activeAction === 'deposit' ? `≈ $${amount}` : `≈ $${amount} USDC`) : "--"}
{activeAction === 'deposit' ? ( ) : (
{tokenType} { (e.target as HTMLImageElement).src = '/assets/tokens/default.svg'; }} /> {tokenType}
)}
{/* Your YT Balance */}
{t("mintSwap.yourTokenBalance").replace('{token}', tokenType)}
{t("mintSwap.currentBalance")} {!mounted ? '0' : `${parseFloat(ytBalance).toLocaleString('en-US', { maximumFractionDigits: 6 })} ${tokenType}`}
{t("mintSwap.valueUsdc")} {!mounted ? '$0' : `≈ $${parseFloat(ytBalance).toLocaleString('en-US', { maximumFractionDigits: 6 })}`}
{/* Transaction Summary */}
{t("mintSwap.transactionSummary")}
{t("mintSwap.youGet")} {isValidAmount(amount) ? activeAction === 'deposit' ? depositYouGet !== null ? `≈ ${depositYouGet.toLocaleString('en-US', { maximumFractionDigits: 6 })} ${tokenType}` : (!selectedToken ? '--' : '...') : `≈ $${parseFloat(amount).toLocaleString('en-US', { maximumFractionDigits: 6 })} USDC` : '--'}
{t("mintSwap.salesPrice")} {isVaultInfoLoading && !vaultInfoTimedOut ? '...' : ytPriceDisplay}
{/* Submit Button */} {/* Review Modal for Deposit */} setIsReviewModalOpen(false)} amount={amount} /> {/* Withdraw Modal */} setIsWithdrawModalOpen(false)} amount={amount} /> {/* Terms */}
{t("mintSwap.termsText")}{" "} {t("mintSwap.termsOfService")} {" "}{t("mintSwap.and")}
{t("mintSwap.privacyPolicy")}
); }