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>
|
|||
|
|
);
|
|||
|
|
}
|