504 lines
22 KiB
TypeScript
504 lines
22 KiB
TypeScript
|
|
"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<string>("");
|
|||
|
|
const [selectedToken, setSelectedToken] = useState<Token | undefined>();
|
|||
|
|
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 (
|
|||
|
|
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 flex flex-col overflow-hidden">
|
|||
|
|
{/* Content */}
|
|||
|
|
<div className="flex flex-col gap-4 md:gap-6 p-4 md:p-6">
|
|||
|
|
{/* Deposit/Withdraw Toggle */}
|
|||
|
|
<Tabs
|
|||
|
|
selectedKey={activeAction}
|
|||
|
|
onSelectionChange={(key) => {
|
|||
|
|
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",
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Tab key="deposit" title={t("mintSwap.deposit")} />
|
|||
|
|
<Tab key="withdraw" title={t("mintSwap.withdraw")} />
|
|||
|
|
</Tabs>
|
|||
|
|
|
|||
|
|
{/* 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">
|
|||
|
|
{/* Label and Balance */}
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<span className="text-caption-tiny font-medium text-[#4b5563] dark:text-gray-400">
|
|||
|
|
{activeAction === 'deposit' ? t("mintSwap.deposit") : t("mintSwap.withdraw")}
|
|||
|
|
</span>
|
|||
|
|
<div className="flex items-center gap-1">
|
|||
|
|
<Image src="/components/common/icon0.svg" alt="" width={12} height={12} />
|
|||
|
|
<span className="text-caption-tiny font-medium text-[#4b5563] dark:text-gray-400">
|
|||
|
|
{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}`)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Input Row */}
|
|||
|
|
<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={amount}
|
|||
|
|
onChange={(e) => 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"
|
|||
|
|
/>
|
|||
|
|
<span className="text-caption-tiny font-regular text-text-tertiary dark:text-gray-400">
|
|||
|
|
{amount ? (activeAction === 'deposit' ? `≈ $${amount}` : `≈ $${amount} USDC`) : "--"}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<Button
|
|||
|
|
size="sm"
|
|||
|
|
className={buttonStyles({ intent: "max" })}
|
|||
|
|
onPress={() => setAmount(truncateDecimals(activeAction === 'deposit' ? usdcBalance : ytBalance, displayDecimals))}
|
|||
|
|
>
|
|||
|
|
{t("mintSwap.max")}
|
|||
|
|
</Button>
|
|||
|
|
{activeAction === 'deposit' ? (
|
|||
|
|
<TokenSelector
|
|||
|
|
selectedToken={selectedToken}
|
|||
|
|
onSelect={setSelectedToken}
|
|||
|
|
filterTypes={['stablecoin']}
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<div className="bg-white dark:bg-gray-700 rounded-full px-4 h-[46px] flex items-center gap-2">
|
|||
|
|
<Image
|
|||
|
|
src={ytToken?.iconUrl || '/assets/tokens/default.svg'}
|
|||
|
|
alt={tokenType}
|
|||
|
|
width={32}
|
|||
|
|
height={32}
|
|||
|
|
className="rounded-full"
|
|||
|
|
onError={(e) => { (e.target as HTMLImageElement).src = '/assets/tokens/default.svg'; }}
|
|||
|
|
/>
|
|||
|
|
<span className="text-body-default font-bold text-text-primary dark:text-white">{tokenType}</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Your YT Balance */}
|
|||
|
|
<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">
|
|||
|
|
<span className="text-body-small font-bold text-[#4b5563] dark:text-gray-300">
|
|||
|
|
{t("mintSwap.yourTokenBalance").replace('{token}', tokenType)}
|
|||
|
|
</span>
|
|||
|
|
<div className="flex items-center justify-between gap-2">
|
|||
|
|
<span className="text-body-small font-regular text-text-tertiary dark:text-gray-400 shrink-0">
|
|||
|
|
{t("mintSwap.currentBalance")}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-body-small font-bold text-[#10b981] dark:text-green-400 text-right min-w-0 truncate">
|
|||
|
|
{!mounted ? '0' : `${parseFloat(ytBalance).toLocaleString('en-US', { maximumFractionDigits: 6 })} ${tokenType}`}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center justify-between gap-2">
|
|||
|
|
<span className="text-body-small font-regular text-text-tertiary dark:text-gray-400 shrink-0">
|
|||
|
|
{t("mintSwap.valueUsdc")}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-body-small font-bold text-text-primary dark:text-white whitespace-nowrap">
|
|||
|
|
{!mounted ? '$0' : `≈ $${parseFloat(ytBalance).toLocaleString('en-US', { maximumFractionDigits: 6 })}`}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Transaction Summary */}
|
|||
|
|
<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">
|
|||
|
|
<span className="text-body-small font-bold text-[#4b5563] dark:text-gray-300">
|
|||
|
|
{t("mintSwap.transactionSummary")}
|
|||
|
|
</span>
|
|||
|
|
<div className="flex items-center justify-between gap-2">
|
|||
|
|
<span className="text-body-small font-regular text-text-tertiary dark:text-gray-400 shrink-0">
|
|||
|
|
{t("mintSwap.youGet")}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-body-small font-bold text-[#10b981] dark:text-green-400 text-right min-w-0 truncate">
|
|||
|
|
{isValidAmount(amount)
|
|||
|
|
? activeAction === 'deposit'
|
|||
|
|
? depositYouGet !== null
|
|||
|
|
? `≈ ${depositYouGet.toLocaleString('en-US', { maximumFractionDigits: 6 })} ${tokenType}`
|
|||
|
|
: (!selectedToken ? '--' : '...')
|
|||
|
|
: `≈ $${parseFloat(amount).toLocaleString('en-US', { maximumFractionDigits: 6 })} USDC`
|
|||
|
|
: '--'}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center justify-between gap-2">
|
|||
|
|
<span className="text-body-small font-regular text-text-tertiary dark:text-gray-400 shrink-0">
|
|||
|
|
{t("mintSwap.salesPrice")}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-body-small font-bold text-text-primary dark:text-white whitespace-nowrap">
|
|||
|
|
{isVaultInfoLoading && !vaultInfoTimedOut ? '...' : ytPriceDisplay}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Submit Button */}
|
|||
|
|
<Button
|
|||
|
|
isDisabled={isConnected && (
|
|||
|
|
!isValidAmount(amount) ||
|
|||
|
|
isDepositLoading ||
|
|||
|
|
isWithdrawLoading ||
|
|||
|
|
((isMatured || isFull) && activeAction === 'deposit') ||
|
|||
|
|
(!isMatured && activeAction === 'withdraw')
|
|||
|
|
)}
|
|||
|
|
color="default"
|
|||
|
|
variant="solid"
|
|||
|
|
className={buttonStyles({ intent: "theme" })}
|
|||
|
|
endContent={<Image src="/components/common/icon11.svg" alt="" width={20} height={20} />}
|
|||
|
|
onPress={() => {
|
|||
|
|
if (!isConnected) { open(); return; }
|
|||
|
|
if (amount && parseFloat(amount) > 0) {
|
|||
|
|
if (activeAction === "deposit") {
|
|||
|
|
executeApproveAndDeposit(amount);
|
|||
|
|
} else {
|
|||
|
|
executeApproveAndWithdraw(amount);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{!isConnected
|
|||
|
|
? t("common.connectWallet")
|
|||
|
|
: isFull && activeAction === 'deposit'
|
|||
|
|
? t("mintSwap.poolFull")
|
|||
|
|
: isMatured && activeAction === 'deposit'
|
|||
|
|
? t("mintSwap.productMatured").replace("{date}", maturityDateStr)
|
|||
|
|
: activeAction === 'deposit'
|
|||
|
|
? depositStatus === 'approving' ? t("common.approving")
|
|||
|
|
: depositStatus === 'approved' ? t("mintSwap.approvedDepositing")
|
|||
|
|
: depositStatus === 'depositing' ? t("mintSwap.depositing")
|
|||
|
|
: depositStatus === 'success' ? t("common.success")
|
|||
|
|
: depositStatus === 'error' ? t("common.failed")
|
|||
|
|
: !!amount && !isValidAmount(amount) ? t("common.invalidAmount")
|
|||
|
|
: t("mintSwap.approveDeposit")
|
|||
|
|
: !isMatured
|
|||
|
|
? nextRedemptionTime > 0
|
|||
|
|
? t("mintSwap.withdrawNotMatured").replace("{date}", maturityDateStr)
|
|||
|
|
: t("mintSwap.withdrawNoMaturity")
|
|||
|
|
: withdrawStatus === 'approving' ? t("common.approving")
|
|||
|
|
: withdrawStatus === 'approved' ? t("mintSwap.approvedWithdrawing")
|
|||
|
|
: withdrawStatus === 'withdrawing' ? t("mintSwap.withdrawing")
|
|||
|
|
: withdrawStatus === 'success' ? t("common.success")
|
|||
|
|
: withdrawStatus === 'error' ? t("common.failed")
|
|||
|
|
: !!amount && !isValidAmount(amount) ? t("common.invalidAmount")
|
|||
|
|
: t("mintSwap.approveWithdraw")
|
|||
|
|
}
|
|||
|
|
</Button>
|
|||
|
|
|
|||
|
|
{/* Review Modal for Deposit */}
|
|||
|
|
<ReviewModal
|
|||
|
|
isOpen={isReviewModalOpen}
|
|||
|
|
onClose={() => setIsReviewModalOpen(false)}
|
|||
|
|
amount={amount}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
{/* Withdraw Modal */}
|
|||
|
|
<WithdrawModal
|
|||
|
|
isOpen={isWithdrawModalOpen}
|
|||
|
|
onClose={() => setIsWithdrawModalOpen(false)}
|
|||
|
|
amount={amount}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
{/* Terms */}
|
|||
|
|
<div className="flex flex-col gap-0 text-center">
|
|||
|
|
<div className="text-caption-tiny font-regular">
|
|||
|
|
<span className="text-[#9ca1af] dark:text-gray-400">
|
|||
|
|
{t("mintSwap.termsText")}{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-[#10b981] dark:text-green-400">
|
|||
|
|
{t("mintSwap.termsOfService")}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-[#9ca1af] dark:text-gray-400">
|
|||
|
|
{" "}{t("mintSwap.and")}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<span className="text-caption-tiny font-regular text-[#10b981] dark:text-green-400">
|
|||
|
|
{t("mintSwap.privacyPolicy")}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|