"use client"; import { useState, useEffect, useCallback } from "react"; import Image from "next/image"; import { Button, Tabs, Tab } from "@heroui/react"; import { useApp } from "@/contexts/AppContext"; import { buttonStyles } from "@/lib/buttonStyles"; import { useAccount, useReadContract } from 'wagmi'; import { parseUnits, formatUnits } from 'viem'; import { useTokenBalance, useYTLPBalance } from '@/hooks/useBalance'; import { usePoolDeposit } from '@/hooks/usePoolDeposit'; import { usePoolWithdraw } from '@/hooks/usePoolWithdraw'; import { toast } from "sonner"; import TokenSelector from '@/components/common/TokenSelector'; import { Token } from '@/lib/api/tokens'; import { useTokenDecimalsFromAddress } from '@/hooks/useTokenDecimals'; import { useTokenBySymbol } from '@/hooks/useTokenBySymbol'; import { getContractAddress, abis } from '@/lib/contracts'; interface PoolDepositPanelProps { onSuccess?: () => void; } export default function PoolDepositPanel({ onSuccess }: PoolDepositPanelProps) { const { t } = useApp(); const [activeTab, setActiveTab] = useState("deposit"); const [selectedToken, setSelectedToken] = useState('USDC'); const [selectedTokenObj, setSelectedTokenObj] = useState(); const [outputToken, setOutputToken] = useState('USDC'); const [outputTokenObj, setOutputTokenObj] = useState(); const [depositAmount, setDepositAmount] = useState(""); const [withdrawAmount, setWithdrawAmount] = useState(""); const [mounted, setMounted] = useState(false); const [withdrawCooldownLeft, setWithdrawCooldownLeft] = useState(0); // seconds remaining useEffect(() => { setMounted(true); }, []); // Track 15-min withdraw cooldown after deposit useEffect(() => { const COOLDOWN_MS = 15 * 60 * 1000; const tick = () => { const lastDeposit = parseInt(localStorage.getItem('alp_last_deposit_time') ?? '0', 10); if (!lastDeposit) { setWithdrawCooldownLeft(0); return; } const remaining = Math.ceil((lastDeposit + COOLDOWN_MS - Date.now()) / 1000); setWithdrawCooldownLeft(remaining > 0 ? remaining : 0); }; tick(); const interval = setInterval(tick, 1000); return () => clearInterval(interval); }, []); // 处理存入 Token 选择 const handleYTTokenSelect = useCallback((token: Token) => { setSelectedTokenObj(token); setSelectedToken(token.symbol); setDepositAmount(""); }, []); // 处理输出 Token 选择 const handleOutputTokenSelect = useCallback((token: Token) => { setOutputTokenObj(token); setOutputToken(token.symbol); setWithdrawAmount(""); }, []); // Web3 集成 const { address, isConnected, chainId } = useAccount(); // 优先从产品合约地址读取精度,失败时回退到 token.decimals const depositInputDecimals = useTokenDecimalsFromAddress( selectedTokenObj?.contractAddress, selectedTokenObj?.decimals ?? 18 ); const depositDisplayDecimals = Math.min(depositInputDecimals, 6); const ytLPAddress = chainId ? getContractAddress('YTLPToken', chainId) : undefined; const poolManagerAddress = chainId ? getContractAddress('YTPoolManager', chainId) : undefined; const withdrawDecimals = useTokenDecimalsFromAddress(ytLPAddress, 18); const withdrawDisplayDecimals = Math.min(withdrawDecimals, 6); const outputDecimals = useTokenDecimalsFromAddress(outputTokenObj?.contractAddress, outputTokenObj?.decimals ?? 18); // 存入估算:getAddLiquidityOutput(token, amount) → [usdyAmount, ytLPMintAmount] const depositAmountWei = depositAmount !== '' && parseFloat(depositAmount) > 0 && selectedTokenObj ? parseUnits(depositAmount, depositInputDecimals) : undefined; const { data: depositEstData, isLoading: isDepositEstLoading } = useReadContract({ address: poolManagerAddress, abi: abis.YTPoolManager as any, functionName: 'getAddLiquidityOutput', args: [selectedTokenObj?.contractAddress as `0x${string}`, depositAmountWei ?? 0n], query: { enabled: !!poolManagerAddress && !!selectedTokenObj?.contractAddress && !!depositAmountWei }, }); const estLPTokens = depositEstData ? parseFloat(formatUnits((depositEstData as [bigint, bigint])[1], 18)) : null; // 提出估算:getRemoveLiquidityOutput(tokenOut, ytLPAmount) → [usdyAmount, amountOut] const withdrawAmountWei = withdrawAmount !== '' && parseFloat(withdrawAmount) > 0 ? parseUnits(withdrawAmount, withdrawDecimals) : undefined; const { data: withdrawEstData, isLoading: isWithdrawEstLoading } = useReadContract({ address: poolManagerAddress, abi: abis.YTPoolManager as any, functionName: 'getRemoveLiquidityOutput', args: [outputTokenObj?.contractAddress as `0x${string}`, withdrawAmountWei ?? 0n], query: { enabled: !!poolManagerAddress && !!outputTokenObj?.contractAddress && !!withdrawAmountWei }, }); const estAmountOut = withdrawEstData ? parseFloat(formatUnits((withdrawEstData as [bigint, bigint])[1], outputDecimals)) : null; const { formattedBalance: depositBalance, isLoading: isBalanceLoading, refetch: refetchBalance } = useTokenBalance( selectedTokenObj?.contractAddress, depositInputDecimals ); const { formattedBalance: outputTokenBalance } = useTokenBalance( outputTokenObj?.contractAddress, outputDecimals ); const { formattedBalance: lpBalance, refetch: refetchLP } = useYTLPBalance(); // Deposit hooks const { status: depositStatus, error: depositError, isLoading: isDepositLoading, approveHash: depositApproveHash, depositHash, executeApproveAndDeposit, reset: resetDeposit, } = usePoolDeposit(); // Withdraw hooks const { status: withdrawStatus, error: withdrawError, isLoading: isWithdrawLoading, approveHash: withdrawApproveHash, withdrawHash: withdrawTxHash, executeApproveAndWithdraw, reset: resetWithdraw, } = usePoolWithdraw(); // 存款成功后刷新余额,并记录 15 分钟提现冷却开始时间 useEffect(() => { if (depositStatus === 'success') { toast.success(t("alp.toast.liquidityAdded"), { description: t("alp.toast.liquidityAddedDesc"), duration: 5000, }); localStorage.setItem('alp_last_deposit_time', Date.now().toString()); refetchBalance(); refetchLP(); onSuccess?.(); const timer = setTimeout(() => { resetDeposit(); setDepositAmount(""); }, 3000); return () => clearTimeout(timer); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [depositStatus]); // 提款成功后刷新余额 useEffect(() => { if (withdrawStatus === 'success') { toast.success(t("alp.toast.liquidityRemoved"), { description: t("alp.toast.liquidityRemovedDesc"), duration: 5000, }); refetchBalance(); refetchLP(); onSuccess?.(); const timer = setTimeout(() => { resetWithdraw(); setWithdrawAmount(""); }, 3000); return () => clearTimeout(timer); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [withdrawStatus]); // 显示错误提示 useEffect(() => { if (depositError) { if (depositError === 'Transaction cancelled') { toast.warning(t("alp.toast.txCancelled"), { description: t("alp.toast.txCancelledDesc"), duration: 3000, }); } else { toast.error(t("alp.toast.depositFailed"), { description: depositError, duration: 5000, }); } } }, [depositError]); useEffect(() => { if (withdrawError) { if (withdrawError === 'Transaction cancelled') { toast.warning(t("alp.toast.txCancelled"), { description: t("alp.toast.txCancelledDesc"), duration: 3000, }); } else { toast.error(t("alp.toast.withdrawFailed"), { description: withdrawError, duration: 5000, }); } } }, [withdrawError]); // 显示交易hash toast useEffect(() => { if (depositApproveHash && depositStatus === 'approving') { toast.info(t("alp.toast.approvalSubmitted"), { description: t("alp.toast.approvingTokens"), action: { label: t("alp.toast.viewTx"), onClick: () => window.open(`https://testnet.bscscan.com/tx/${depositApproveHash}`, '_blank'), }, duration: 10000, }); } }, [depositApproveHash, depositStatus]); useEffect(() => { if (depositHash && depositStatus === 'depositing') { toast.info(t("alp.toast.depositSubmitted"), { description: t("alp.toast.addingLiquidity"), action: { label: t("alp.toast.viewTx"), onClick: () => window.open(`https://testnet.bscscan.com/tx/${depositHash}`, '_blank'), }, duration: 10000, }); } }, [depositHash, depositStatus]); useEffect(() => { if (withdrawApproveHash && withdrawStatus === 'approving') { toast.info(t("alp.toast.approvalSubmitted"), { description: t("alp.toast.approvingLP"), action: { label: t("alp.toast.viewTx"), onClick: () => window.open(`https://testnet.bscscan.com/tx/${withdrawApproveHash}`, '_blank'), }, duration: 10000, }); } }, [withdrawApproveHash, withdrawStatus]); useEffect(() => { if (withdrawTxHash && withdrawStatus === 'withdrawing') { toast.info(t("alp.toast.withdrawSubmitted"), { description: t("alp.toast.removingLiquidity"), action: { label: t("alp.toast.viewTx"), onClick: () => window.open(`https://testnet.bscscan.com/tx/${withdrawTxHash}`, '_blank'), }, duration: 10000, }); } }, [withdrawTxHash, withdrawStatus]); // 切换Tab时重置错误状态和输入框 useEffect(() => { resetDeposit(); resetWithdraw(); setDepositAmount(""); setWithdrawAmount(""); // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeTab]); 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; const handleDepositPercentage = (pct: number) => { const bal = parseFloat(depositBalance); if (bal > 0) setDepositAmount(truncateDecimals((bal * pct / 100).toString(), depositDisplayDecimals)); }; const handleWithdrawPercentage = (pct: number) => { const bal = parseFloat(lpBalance); if (bal > 0) setWithdrawAmount(truncateDecimals((bal * pct / 100).toString(), withdrawDisplayDecimals)); }; const pctBtnClass = "flex-1 sm:flex-none bg-[#e5e7eb] dark:bg-gray-600 rounded-full px-2 sm:px-2.5 h-[22px] text-[10px] font-medium text-text-primary dark:text-white hover:bg-[#d1d5db] dark:hover:bg-gray-500 transition-colors"; const handleDepositAmountChange = (value: string) => { if (value === '') { setDepositAmount(value); return; } if (!/^\d*\.?\d*$/.test(value)) return; const parts = value.split('.'); if (parts.length > 1 && parts[1].length > depositDisplayDecimals) return; if (parseFloat(value) > parseFloat(depositBalance)) { setDepositAmount(truncateDecimals(depositBalance, depositDisplayDecimals)); return; } setDepositAmount(value); }; const handleWithdrawAmountChange = (value: string) => { if (value === '') { setWithdrawAmount(value); return; } if (!/^\d*\.?\d*$/.test(value)) return; const parts = value.split('.'); if (parts.length > 1 && parts[1].length > withdrawDisplayDecimals) return; if (parseFloat(value) > parseFloat(lpBalance)) { setWithdrawAmount(truncateDecimals(lpBalance, withdrawDisplayDecimals)); return; } setWithdrawAmount(value); }; const handleDeposit = () => { if (depositAmount && parseFloat(depositAmount) > 0 && selectedTokenObj) { executeApproveAndDeposit(selectedTokenObj.contractAddress, depositInputDecimals, depositAmount); } }; const handleWithdraw = () => { if (withdrawAmount && parseFloat(withdrawAmount) > 0 && outputTokenObj) { executeApproveAndWithdraw(outputTokenObj.contractAddress, outputTokenObj.decimals ?? 18, withdrawAmount); } }; const currentStatus = activeTab === 'deposit' ? depositStatus : withdrawStatus; const isLoading = activeTab === 'deposit' ? isDepositLoading : isWithdrawLoading; const currentAmount = activeTab === 'deposit' ? depositAmount : withdrawAmount; return (
{/* Content */}
{/* Title */}

{t("alp.liquidityPool")}

{t("alp.subtitle")}

{/* Tabs */} setActiveTab(key as string)} classNames={{ base: "w-full", tabList: "w-full bg-bg-subtle dark:bg-gray-700 p-1 rounded-xl", tab: "h-10", cursor: "bg-white dark:bg-gray-600", tabContent: "group-data-[selected=true]:text-text-primary dark:group-data-[selected=true]:text-white", }} > {/* Deposit Panel */}
{/* Input Area */}
{t("mintSwap.deposit")}
{!mounted ? '0' : (isBalanceLoading ? '...' : parseFloat(truncateDecimals(depositBalance, depositDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: depositDisplayDecimals }))} {selectedToken}
{!mounted ? '0' : (isBalanceLoading ? '...' : parseFloat(truncateDecimals(depositBalance, depositDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: depositDisplayDecimals }))} {selectedToken}
handleDepositAmountChange(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" /> {depositAmount ? `≈ $${depositAmount}` : "--"}
{!mounted ? `0 ${selectedToken}` : (isBalanceLoading ? '...' : `${parseFloat(truncateDecimals(depositBalance, depositDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: depositDisplayDecimals })} ${selectedToken}`)}
{/* Pool Info */}
{t("alp.estimatedLP")} {estLPTokens != null ? `≈ ${estLPTokens.toLocaleString('en-US', { maximumFractionDigits: 6 })} LP` : depositAmount ? (isDepositEstLoading ? '...' : '--') : '--'}
{/* Withdraw Panel */}
{/* Input Area */}
{t("alp.amountToWithdraw")}
{parseFloat(truncateDecimals(lpBalance, withdrawDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: withdrawDisplayDecimals })} YT LP
{parseFloat(truncateDecimals(lpBalance, withdrawDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: withdrawDisplayDecimals })} YT LP
handleWithdrawAmountChange(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" /> {withdrawAmount ? `≈ ${withdrawAmount} LP` : "--"}
{!mounted ? `0 ${outputToken}` : `${parseFloat(truncateDecimals(outputTokenBalance, 6)).toLocaleString('en-US', { maximumFractionDigits: 6 })} ${outputToken}`}
{/* Withdraw Info */}
{t("alp.youWillReceive")} {estAmountOut != null ? `≈ ${estAmountOut.toLocaleString('en-US', { maximumFractionDigits: 6 })} ${outputToken}` : withdrawAmount ? (isWithdrawEstLoading ? '...' : '--') : '--'}
{/* Submit Button */} {(() => { const amountInvalid = !!currentAmount && !isValidAmount(currentAmount); return ( ); })()} {/* Info Note */}
{activeTab === 'deposit' ? t("alp.depositNote") : t("alp.withdrawNote")}
); }