包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
542 lines
26 KiB
TypeScript
542 lines
26 KiB
TypeScript
"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<string>("deposit");
|
||
const [selectedToken, setSelectedToken] = useState<string>('USDC');
|
||
const [selectedTokenObj, setSelectedTokenObj] = useState<Token | undefined>();
|
||
const [outputToken, setOutputToken] = useState<string>('USDC');
|
||
const [outputTokenObj, setOutputTokenObj] = useState<Token | undefined>();
|
||
const [depositAmount, setDepositAmount] = useState<string>("");
|
||
const [withdrawAmount, setWithdrawAmount] = useState<string>("");
|
||
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 (
|
||
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 flex flex-col overflow-hidden h-full">
|
||
{/* Content */}
|
||
<div className="flex flex-col gap-6 p-6 flex-1">
|
||
{/* Title */}
|
||
<div className="flex flex-col gap-1">
|
||
<h2 className="text-heading-h3 font-bold text-text-primary dark:text-white">
|
||
{t("alp.liquidityPool")}
|
||
</h2>
|
||
<p className="text-body-small text-text-tertiary dark:text-gray-400">
|
||
{t("alp.subtitle")}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Tabs */}
|
||
<Tabs
|
||
selectedKey={activeTab}
|
||
onSelectionChange={(key) => 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",
|
||
}}
|
||
>
|
||
<Tab key="deposit" title={t("alp.deposit")}>
|
||
{/* Deposit Panel */}
|
||
<div className="flex flex-col gap-4 mt-4">
|
||
{/* Input Area */}
|
||
<div className="flex flex-col gap-2">
|
||
<div className="bg-bg-subtle dark:bg-gray-700 rounded-xl border border-border-gray dark:border-gray-600 p-4 flex flex-col gap-3">
|
||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-caption-tiny font-medium text-[#4b5563] dark:text-gray-400">
|
||
{t("mintSwap.deposit")}
|
||
</span>
|
||
<div className="flex items-center gap-1 sm:hidden">
|
||
<Image src="/icons/ui/wallet-icon.svg" alt="" width={12} height={12} />
|
||
<span className="text-caption-tiny font-medium text-[#4b5563] dark:text-gray-400">
|
||
{!mounted ? '0' : (isBalanceLoading ? '...' : parseFloat(truncateDecimals(depositBalance, depositDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: depositDisplayDecimals }))} {selectedToken}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-1.5">
|
||
<button onClick={() => handleDepositPercentage(25)} className={pctBtnClass}>25%</button>
|
||
<button onClick={() => handleDepositPercentage(50)} className={pctBtnClass}>50%</button>
|
||
<button onClick={() => handleDepositPercentage(75)} className={pctBtnClass}>75%</button>
|
||
<button onClick={() => handleDepositPercentage(100)} className={pctBtnClass}>{t("mintSwap.max")}</button>
|
||
<Image src="/icons/ui/wallet-icon.svg" alt="" width={12} height={12} className="ml-1 hidden sm:block" />
|
||
<span className="text-caption-tiny font-medium text-[#4b5563] dark:text-gray-400 hidden sm:block">
|
||
{!mounted ? '0' : (isBalanceLoading ? '...' : parseFloat(truncateDecimals(depositBalance, depositDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: depositDisplayDecimals }))} {selectedToken}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between gap-2">
|
||
<div className="flex flex-col items-start flex-1">
|
||
<input
|
||
type="text" inputMode="decimal"
|
||
placeholder="0.00"
|
||
value={depositAmount}
|
||
onChange={(e) => 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"
|
||
/>
|
||
<span className="text-caption-tiny font-regular text-text-tertiary dark:text-gray-400">
|
||
{depositAmount ? `≈ $${depositAmount}` : "--"}
|
||
</span>
|
||
</div>
|
||
<div className="flex flex-col items-end gap-1">
|
||
<TokenSelector
|
||
selectedToken={selectedTokenObj}
|
||
onSelect={handleYTTokenSelect}
|
||
filterTypes={['stablecoin', 'yield-token']}
|
||
defaultSymbol={selectedToken}
|
||
/>
|
||
<span className="text-caption-tiny font-medium text-text-secondary dark:text-gray-400">
|
||
{!mounted ? `0 ${selectedToken}` : (isBalanceLoading ? '...' : `${parseFloat(truncateDecimals(depositBalance, depositDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: depositDisplayDecimals })} ${selectedToken}`)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Pool Info */}
|
||
<div className="bg-bg-subtle dark:bg-gray-700 rounded-xl border border-border-gray dark:border-gray-600 p-4 flex flex-col gap-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-body-small font-regular text-text-tertiary dark:text-gray-400">
|
||
{t("alp.estimatedLP")}
|
||
</span>
|
||
<span className="text-body-small font-bold text-[#10b981] dark:text-green-400">
|
||
{estLPTokens != null
|
||
? `≈ ${estLPTokens.toLocaleString('en-US', { maximumFractionDigits: 6 })} LP`
|
||
: depositAmount ? (isDepositEstLoading ? '...' : '--') : '--'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Tab>
|
||
|
||
<Tab key="withdraw" title={t("alp.withdraw")}>
|
||
{/* Withdraw Panel */}
|
||
<div className="flex flex-col gap-4 mt-4">
|
||
{/* Input Area */}
|
||
<div className="flex flex-col gap-2">
|
||
<div className="bg-bg-subtle dark:bg-gray-700 rounded-xl border border-border-gray dark:border-gray-600 p-4 flex flex-col gap-3">
|
||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-caption-tiny font-medium text-[#4b5563] dark:text-gray-400">
|
||
{t("alp.amountToWithdraw")}
|
||
</span>
|
||
<div className="flex items-center gap-1 sm:hidden">
|
||
<Image src="/icons/ui/wallet-icon.svg" alt="" width={12} height={12} />
|
||
<span className="text-caption-tiny font-medium text-[#4b5563] dark:text-gray-400">
|
||
{parseFloat(truncateDecimals(lpBalance, withdrawDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: withdrawDisplayDecimals })} YT LP
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-1.5">
|
||
<button onClick={() => handleWithdrawPercentage(25)} className={pctBtnClass}>25%</button>
|
||
<button onClick={() => handleWithdrawPercentage(50)} className={pctBtnClass}>50%</button>
|
||
<button onClick={() => handleWithdrawPercentage(75)} className={pctBtnClass}>75%</button>
|
||
<button onClick={() => handleWithdrawPercentage(100)} className={pctBtnClass}>{t("mintSwap.max")}</button>
|
||
<Image src="/icons/ui/wallet-icon.svg" alt="" width={12} height={12} className="ml-1 hidden sm:block" />
|
||
<span className="text-caption-tiny font-medium text-[#4b5563] dark:text-gray-400 hidden sm:block">
|
||
{parseFloat(truncateDecimals(lpBalance, withdrawDisplayDecimals)).toLocaleString('en-US', { maximumFractionDigits: withdrawDisplayDecimals })} YT LP
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between gap-2">
|
||
<div className="flex flex-col items-start flex-1">
|
||
<input
|
||
type="text" inputMode="decimal"
|
||
placeholder="0.00"
|
||
value={withdrawAmount}
|
||
onChange={(e) => 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"
|
||
/>
|
||
<span className="text-caption-tiny font-regular text-text-tertiary dark:text-gray-400">
|
||
{withdrawAmount ? `≈ ${withdrawAmount} LP` : "--"}
|
||
</span>
|
||
</div>
|
||
<div className="flex flex-col items-end gap-1">
|
||
<TokenSelector
|
||
selectedToken={outputTokenObj}
|
||
onSelect={handleOutputTokenSelect}
|
||
filterTypes={['stablecoin', 'yield-token']}
|
||
defaultSymbol={outputToken}
|
||
/>
|
||
<span className="text-caption-tiny font-medium text-text-secondary dark:text-gray-400">
|
||
{!mounted ? `0 ${outputToken}` : `${parseFloat(truncateDecimals(outputTokenBalance, 6)).toLocaleString('en-US', { maximumFractionDigits: 6 })} ${outputToken}`}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Withdraw Info */}
|
||
<div className="bg-bg-subtle dark:bg-gray-700 rounded-xl border border-border-gray dark:border-gray-600 p-4 flex flex-col gap-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-body-small font-regular text-text-tertiary dark:text-gray-400">
|
||
{t("alp.youWillReceive")}
|
||
</span>
|
||
<span className="text-body-small font-bold text-[#10b981] dark:text-green-400">
|
||
{estAmountOut != null
|
||
? `≈ ${estAmountOut.toLocaleString('en-US', { maximumFractionDigits: 6 })} ${outputToken}`
|
||
: withdrawAmount ? (isWithdrawEstLoading ? '...' : '--') : '--'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Tab>
|
||
</Tabs>
|
||
|
||
{/* Submit Button */}
|
||
{(() => {
|
||
const amountInvalid = !!currentAmount && !isValidAmount(currentAmount);
|
||
return (
|
||
<Button
|
||
isDisabled={!mounted || !isConnected || !isValidAmount(currentAmount) || isLoading || (activeTab === 'withdraw' && withdrawCooldownLeft > 0)}
|
||
color="default"
|
||
variant="solid"
|
||
className={buttonStyles({ intent: "theme" })}
|
||
endContent={<Image src="/icons/actions/arrow-right-icon.svg" alt="" width={20} height={20} />}
|
||
onPress={activeTab === 'deposit' ? handleDeposit : handleWithdraw}
|
||
>
|
||
{!mounted && t("common.connectWallet")}
|
||
{mounted && !isConnected && t("common.connectWallet")}
|
||
{mounted && isConnected && currentStatus === 'idle' && amountInvalid && t("common.invalidAmount")}
|
||
{mounted && isConnected && currentStatus === 'idle' && !amountInvalid && activeTab === 'deposit' && t("alp.addLiquidity")}
|
||
{mounted && isConnected && currentStatus === 'idle' && !amountInvalid && activeTab === 'withdraw' && withdrawCooldownLeft > 0 && `15-min Withdrawal Lock (${String(Math.floor(withdrawCooldownLeft / 60)).padStart(2, '0')}:${String(withdrawCooldownLeft % 60).padStart(2, '0')})`}
|
||
{mounted && isConnected && currentStatus === 'idle' && !amountInvalid && activeTab === 'withdraw' && withdrawCooldownLeft === 0 && t("alp.removeLiquidity")}
|
||
{mounted && currentStatus === 'approving' && t("common.approving")}
|
||
{mounted && currentStatus === 'approved' && (activeTab === 'deposit' ? t("alp.approvedAdding") : t("alp.approvedRemoving"))}
|
||
{mounted && (currentStatus === 'depositing' || currentStatus === 'withdrawing') && (activeTab === 'deposit' ? t("alp.adding") : t("alp.removing"))}
|
||
{mounted && currentStatus === 'success' && t("common.success")}
|
||
{mounted && currentStatus === 'error' && t("common.failed")}
|
||
</Button>
|
||
);
|
||
})()}
|
||
|
||
{/* Info Note */}
|
||
<div className="text-center">
|
||
<span className="text-caption-tiny font-regular text-[#9ca1af] dark:text-gray-400">
|
||
{activeTab === 'deposit' ? t("alp.depositNote") : t("alp.withdrawNote")}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|