866 lines
38 KiB
TypeScript
866 lines
38 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ChevronDown, CheckCircle, XCircle, Loader2, ArrowDownUp } from 'lucide-react';
|
|
import { useAccount, usePublicClient } from 'wagmi';
|
|
import { useGetAllPools, useTokenDetails, useSwapMintAmounts, useBurnSwapAmounts, useLPTokenBalance, useBurnAmounts, type PoolDetails, type TokenDetails, type BurnSwapAmounts } from '@/hooks/usePartyPlanner';
|
|
import { useSwapMint, useBurnSwap, useBurn, type ActualSwapMintAmounts, type ActualBurnSwapAmounts, type ActualBurnAmounts } from '@/hooks/usePartyPool';
|
|
import { formatUnits, parseUnits } from 'viem';
|
|
import IPartyPoolABI from '@/contracts/IPartyPoolABI';
|
|
import { ERC20ABI } from '@/contracts/ERC20ABI';
|
|
|
|
type TransactionStatus = 'idle' | 'pending' | 'success' | 'error';
|
|
type Mode = 'stake' | 'unstake';
|
|
|
|
interface StakeFormProps {
|
|
defaultMode?: Mode;
|
|
}
|
|
|
|
interface TokenInfo {
|
|
address: `0x${string}`;
|
|
symbol: string;
|
|
decimals: number;
|
|
}
|
|
|
|
export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
|
|
const { t } = useTranslation();
|
|
const { isConnected, address } = useAccount();
|
|
const publicClient = usePublicClient();
|
|
const [mode, setMode] = useState<Mode>(defaultMode);
|
|
const [stakeAmount, setStakeAmount] = useState('');
|
|
const [selectedPool, setSelectedPool] = useState<PoolDetails | null>(null);
|
|
const [selectedToken, setSelectedToken] = useState<TokenDetails | null>(null);
|
|
const [isPoolDropdownOpen, setIsPoolDropdownOpen] = useState(false);
|
|
const [isTokenDropdownOpen, setIsTokenDropdownOpen] = useState(false);
|
|
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus>('idle');
|
|
const [transactionError, setTransactionError] = useState<string | null>(null);
|
|
const [actualSwapMintAmounts, setActualSwapMintAmounts] = useState<ActualSwapMintAmounts | null>(null);
|
|
const [actualBurnSwapAmounts, setActualBurnSwapAmounts] = useState<ActualBurnSwapAmounts | null>(null);
|
|
const [actualBurnAmounts, setActualBurnAmounts] = useState<ActualBurnAmounts | null>(null);
|
|
const [redeemAll, setRedeemAll] = useState(false);
|
|
const [poolTokens, setPoolTokens] = useState<TokenInfo[]>([]);
|
|
const poolDropdownRef = useRef<HTMLDivElement>(null);
|
|
const tokenDropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Fetch all pools using the new hook
|
|
const { poolDetails, loading: poolsLoading } = useGetAllPools();
|
|
|
|
// Get token details for the user
|
|
const { tokenDetails, loading: tokensLoading } = useTokenDetails(address);
|
|
|
|
// Initialize swap mint and burn swap hooks
|
|
const { executeSwapMint, isSwapMinting } = useSwapMint();
|
|
const { executeBurnSwap, isBurnSwapping } = useBurnSwap();
|
|
const { executeBurn, isBurning } = useBurn();
|
|
|
|
// Fetch LP token balance (for unstake mode) - must be before isAmountExceedingBalance
|
|
const { lpBalance } = useLPTokenBalance(
|
|
mode === 'unstake' ? selectedPool?.address : undefined,
|
|
address
|
|
);
|
|
|
|
// Get available tokens for staking based on selected pool (for unstake mode)
|
|
const availableTokensForPool = selectedPool && tokenDetails
|
|
? tokenDetails.filter(token =>
|
|
selectedPool.tokens.some(poolToken =>
|
|
poolToken.toLowerCase() === token.address.toLowerCase()
|
|
)
|
|
)
|
|
: [];
|
|
|
|
// Get available pools for staking based on selected token (for stake mode)
|
|
const availablePoolsForToken = selectedToken && poolDetails
|
|
? poolDetails.filter(pool =>
|
|
pool.tokens.some(poolToken =>
|
|
poolToken.toLowerCase() === selectedToken.address.toLowerCase()
|
|
)
|
|
)
|
|
: [];
|
|
|
|
// Check if amount exceeds balance
|
|
const isAmountExceedingBalance = useMemo(() => {
|
|
if (!stakeAmount) return false;
|
|
|
|
try {
|
|
if (mode === 'stake') {
|
|
if (!selectedToken) return false;
|
|
const amountInWei = parseUnits(stakeAmount, selectedToken.decimals);
|
|
return amountInWei > selectedToken.balance;
|
|
} else {
|
|
// Unstake mode - check against LP balance
|
|
if (!lpBalance) return false;
|
|
const amountInWei = parseUnits(stakeAmount, 18); // LP tokens have 18 decimals
|
|
return amountInWei > lpBalance;
|
|
}
|
|
} catch {
|
|
// If parseUnits fails (invalid input), don't show error
|
|
return false;
|
|
}
|
|
}, [stakeAmount, selectedToken, mode, lpBalance]);
|
|
|
|
// Get the input token index in the selected pool
|
|
const inputTokenIndex = useMemo(() => {
|
|
if (!selectedPool || !selectedToken) return undefined;
|
|
|
|
const index = selectedPool.tokens.findIndex(
|
|
token => token.toLowerCase() === selectedToken.address.toLowerCase()
|
|
);
|
|
|
|
return index !== -1 ? index : undefined;
|
|
}, [selectedPool, selectedToken]);
|
|
|
|
// Parse the stake amount to Wei
|
|
const maxAmountIn = useMemo(() => {
|
|
if (!stakeAmount) return undefined;
|
|
|
|
try {
|
|
// For unstake mode, LP tokens always have 18 decimals
|
|
if (mode === 'unstake') {
|
|
return parseUnits(stakeAmount, 18);
|
|
}
|
|
if (!selectedToken) return undefined;
|
|
return parseUnits(stakeAmount, selectedToken.decimals);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}, [stakeAmount, selectedToken, mode]);
|
|
|
|
// Fetch swap mint amounts (for stake mode)
|
|
const { swapMintAmounts, loading: swapMintLoading } = useSwapMintAmounts(
|
|
mode === 'stake' ? selectedPool?.address : undefined,
|
|
mode === 'stake' ? inputTokenIndex : undefined,
|
|
mode === 'stake' ? maxAmountIn : undefined
|
|
);
|
|
|
|
// Fetch burn swap amounts (for unstake mode, only when not redeeming all)
|
|
const { burnSwapAmounts, loading: burnSwapLoading } = useBurnSwapAmounts(
|
|
mode === 'unstake' && !redeemAll ? selectedPool?.address : undefined,
|
|
mode === 'unstake' && !redeemAll ? maxAmountIn : undefined,
|
|
mode === 'unstake' && !redeemAll ? inputTokenIndex : undefined
|
|
);
|
|
|
|
// Fetch burn amounts (for unstake mode when redeeming all)
|
|
const { burnAmounts, loading: burnAmountsLoading } = useBurnAmounts(
|
|
mode === 'unstake' && redeemAll ? selectedPool?.address : undefined,
|
|
mode === 'unstake' && redeemAll ? maxAmountIn : undefined
|
|
);
|
|
|
|
// Fetch token details for the selected pool when Redeem All is active
|
|
useEffect(() => {
|
|
if (!publicClient || !selectedPool || mode !== 'unstake' || !redeemAll) {
|
|
setPoolTokens([]);
|
|
return;
|
|
}
|
|
|
|
const fetchTokenDetails = async () => {
|
|
const tokenInfos: TokenInfo[] = [];
|
|
|
|
for (const tokenAddress of selectedPool.tokens) {
|
|
try {
|
|
const [symbol, decimals] = await Promise.all([
|
|
publicClient.readContract({
|
|
address: tokenAddress as `0x${string}`,
|
|
abi: ERC20ABI,
|
|
functionName: 'symbol',
|
|
}) as Promise<string>,
|
|
publicClient.readContract({
|
|
address: tokenAddress as `0x${string}`,
|
|
abi: ERC20ABI,
|
|
functionName: 'decimals',
|
|
}) as Promise<number>,
|
|
]);
|
|
|
|
tokenInfos.push({
|
|
address: tokenAddress as `0x${string}`,
|
|
symbol,
|
|
decimals,
|
|
});
|
|
} catch (err) {
|
|
console.error(`Error fetching token details for ${tokenAddress}:`, err);
|
|
}
|
|
}
|
|
|
|
setPoolTokens(tokenInfos);
|
|
};
|
|
|
|
fetchTokenDetails();
|
|
}, [publicClient, selectedPool, mode, redeemAll]);
|
|
|
|
// Close dropdowns when clicking outside
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (poolDropdownRef.current && !poolDropdownRef.current.contains(event.target as Node)) {
|
|
setIsPoolDropdownOpen(false);
|
|
}
|
|
if (tokenDropdownRef.current && !tokenDropdownRef.current.contains(event.target as Node)) {
|
|
setIsTokenDropdownOpen(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
const handleStake = async () => {
|
|
if (!selectedPool || !stakeAmount || !maxAmountIn) {
|
|
console.error('Missing required parameters');
|
|
return;
|
|
}
|
|
if (mode === 'unstake' && !redeemAll) {
|
|
if (!selectedToken || inputTokenIndex === undefined) {
|
|
console.error('Missing required unstake parameters');
|
|
return;
|
|
}
|
|
}
|
|
|
|
setTransactionStatus('pending');
|
|
setTransactionError(null);
|
|
|
|
try {
|
|
if (mode === 'stake') {
|
|
if (!selectedToken || inputTokenIndex === undefined) throw new Error('Missing stake parameters');
|
|
// Execute the swap mint transaction and capture actual amounts
|
|
const result = await executeSwapMint(
|
|
selectedPool.address,
|
|
selectedToken.address,
|
|
inputTokenIndex,
|
|
maxAmountIn
|
|
);
|
|
|
|
// Store actual swap mint amounts if available
|
|
if (result?.actualSwapMintAmounts) {
|
|
setActualSwapMintAmounts(result.actualSwapMintAmounts);
|
|
}
|
|
} else if (redeemAll) {
|
|
// Execute full burn (redeem all tokens from the pool)
|
|
const result = await executeBurn(
|
|
selectedPool.address,
|
|
maxAmountIn,
|
|
false // unwrap = false by default
|
|
);
|
|
if (result?.actualBurnAmounts) {
|
|
setActualBurnAmounts(result.actualBurnAmounts);
|
|
}
|
|
} else {
|
|
// Execute the burn swap transaction and capture actual amounts
|
|
if (inputTokenIndex === undefined) throw new Error('Missing input token index');
|
|
const result = await executeBurnSwap(
|
|
selectedPool.address,
|
|
maxAmountIn,
|
|
inputTokenIndex,
|
|
false // unwrap = false by default
|
|
);
|
|
|
|
// Store actual burn swap amounts if available
|
|
if (result?.actualBurnSwapAmounts) {
|
|
setActualBurnSwapAmounts(result.actualBurnSwapAmounts);
|
|
}
|
|
}
|
|
|
|
setTransactionStatus('success');
|
|
} catch (err) {
|
|
console.error(`${mode === 'stake' ? 'Stake' : redeemAll ? 'Redeem' : 'Unstake'} failed:`, err);
|
|
setTransactionError(err instanceof Error ? err.message : 'Transaction failed');
|
|
setTransactionStatus('error');
|
|
}
|
|
};
|
|
|
|
const handleCloseModal = () => {
|
|
if (transactionStatus === 'success') {
|
|
// Clear the form after successful stake
|
|
setStakeAmount('');
|
|
setSelectedPool(null);
|
|
setSelectedToken(null);
|
|
setRedeemAll(false);
|
|
setActualBurnAmounts(null);
|
|
setActualBurnSwapAmounts(null);
|
|
setActualSwapMintAmounts(null);
|
|
}
|
|
setTransactionStatus('idle');
|
|
setTransactionError(null);
|
|
};
|
|
|
|
const toggleMode = () => {
|
|
setMode(mode === 'stake' ? 'unstake' : 'stake');
|
|
// Clear selections when switching modes
|
|
setStakeAmount('');
|
|
setSelectedPool(null);
|
|
setSelectedToken(null);
|
|
setRedeemAll(false);
|
|
setActualBurnAmounts(null);
|
|
setActualBurnSwapAmounts(null);
|
|
setActualSwapMintAmounts(null);
|
|
};
|
|
|
|
return (
|
|
<Card className="w-full max-w-md mx-auto relative">
|
|
<CardHeader>
|
|
<div className="flex justify-between items-center">
|
|
<CardTitle>{mode === 'stake' ? 'Stake' : 'Unstake'}</CardTitle>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* First Selection - depends on mode */}
|
|
{mode === 'stake' ? (
|
|
/* Pool Selection (Stake Mode) */
|
|
<div className="space-y-2">
|
|
<div className="text-sm">
|
|
<label className="text-muted-foreground">{t('stake.selectPool')}</label>
|
|
</div>
|
|
<div className="relative" ref={poolDropdownRef}>
|
|
<Button
|
|
variant="secondary"
|
|
className="w-full h-16 justify-between"
|
|
onClick={() => setIsPoolDropdownOpen(!isPoolDropdownOpen)}
|
|
>
|
|
{selectedPool ? (
|
|
<div className="flex flex-col items-start">
|
|
<span className="font-medium">{selectedPool.symbol}</span>
|
|
<span className="text-xs text-muted-foreground">{selectedPool.name}</span>
|
|
</div>
|
|
) : (
|
|
t('stake.selectPool')
|
|
)}
|
|
<ChevronDown className="h-4 w-4 ml-2" />
|
|
</Button>
|
|
{isPoolDropdownOpen && (
|
|
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
|
{poolDetails && poolDetails.length > 0 ? (
|
|
poolDetails.map((pool) => (
|
|
<button
|
|
key={pool.address}
|
|
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
|
|
onClick={() => {
|
|
setSelectedPool(pool);
|
|
setIsPoolDropdownOpen(false);
|
|
setSelectedToken(null);
|
|
}}
|
|
>
|
|
<div className="flex justify-between items-center w-full">
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{pool.symbol}</span>
|
|
<span className="text-xs text-muted-foreground">{pool.name}</span>
|
|
</div>
|
|
{(pool.price || pool.tvl) && (
|
|
<div className="flex flex-col items-end">
|
|
{pool.price && (
|
|
<span className="text-sm font-medium text-muted-foreground">{pool.price}</span>
|
|
)}
|
|
{pool.tvl && (
|
|
<span className="text-xs text-muted-foreground">TVL: {pool.tvl}</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</button>
|
|
))
|
|
) : (
|
|
<div className="px-4 py-3 text-sm text-muted-foreground">
|
|
{poolsLoading ? 'Loading pools...' : 'No pools available'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
/* Pool Selection (Unstake Mode) */
|
|
<div className="space-y-2">
|
|
<div className="text-sm">
|
|
<label className="text-muted-foreground">{t('stake.selectPool')}</label>
|
|
</div>
|
|
<div className="relative" ref={poolDropdownRef}>
|
|
<Button
|
|
variant="secondary"
|
|
className="w-full h-16 justify-between"
|
|
onClick={() => setIsPoolDropdownOpen(!isPoolDropdownOpen)}
|
|
>
|
|
{selectedPool ? (
|
|
<div className="flex flex-col items-start">
|
|
<span className="font-medium">{selectedPool.symbol}</span>
|
|
<span className="text-xs text-muted-foreground">{selectedPool.name}</span>
|
|
</div>
|
|
) : (
|
|
t('stake.selectPool')
|
|
)}
|
|
<ChevronDown className="h-4 w-4 ml-2" />
|
|
</Button>
|
|
{isPoolDropdownOpen && (
|
|
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
|
{poolDetails && poolDetails.length > 0 ? (
|
|
poolDetails.map((pool) => (
|
|
<button
|
|
key={pool.address}
|
|
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
|
|
onClick={() => {
|
|
setSelectedPool(pool);
|
|
setIsPoolDropdownOpen(false);
|
|
setSelectedToken(null);
|
|
}}
|
|
>
|
|
<div className="flex justify-between items-center w-full">
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{pool.symbol}</span>
|
|
<span className="text-xs text-muted-foreground">{pool.name}</span>
|
|
</div>
|
|
{(pool.price || pool.tvl) && (
|
|
<div className="flex flex-col items-end">
|
|
{pool.price && (
|
|
<span className="text-sm font-medium text-muted-foreground">{pool.price}</span>
|
|
)}
|
|
{pool.tvl && (
|
|
<span className="text-xs text-muted-foreground">TVL: {pool.tvl}</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</button>
|
|
))
|
|
) : (
|
|
<div className="px-4 py-3 text-sm text-muted-foreground">
|
|
{poolsLoading ? 'Loading pools...' : 'No pools available'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Toggle Button */}
|
|
<div className="flex justify-center">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={toggleMode}
|
|
className="rounded-full"
|
|
title={mode === 'stake' ? 'Switch to Unstake' : 'Switch to Stake'}
|
|
>
|
|
<ArrowDownUp className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Second Selection - depends on mode */}
|
|
{mode === 'stake' ? (
|
|
/* Token Selection (Stake Mode) */
|
|
<div className="space-y-2">
|
|
<div className="text-sm">
|
|
<label className="text-muted-foreground">{t('stake.selectToken')}</label>
|
|
</div>
|
|
<div className="relative" ref={tokenDropdownRef}>
|
|
<Button
|
|
variant="secondary"
|
|
className="w-full h-16 justify-between"
|
|
onClick={() => setIsTokenDropdownOpen(!isTokenDropdownOpen)}
|
|
disabled={!selectedPool}
|
|
>
|
|
{selectedToken ? (
|
|
<span className="font-medium">{selectedToken.symbol}</span>
|
|
) : (
|
|
t('stake.selectToken')
|
|
)}
|
|
<ChevronDown className="h-4 w-4 ml-2" />
|
|
</Button>
|
|
<div className="text-xs text-muted-foreground text-right mt-1">
|
|
{t('swap.balance')}: {selectedToken ? formatUnits(selectedToken.balance, selectedToken.decimals) : '0.00'}
|
|
</div>
|
|
{isTokenDropdownOpen && (
|
|
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
|
{availableTokensForPool.length > 0 ? (
|
|
availableTokensForPool.map((token) => (
|
|
<button
|
|
key={token.address}
|
|
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
|
|
onClick={() => {
|
|
setSelectedToken(token);
|
|
setIsTokenDropdownOpen(false);
|
|
}}
|
|
>
|
|
<span className="font-medium">{token.symbol}</span>
|
|
</button>
|
|
))
|
|
) : selectedPool ? (
|
|
<div className="px-4 py-3 text-sm text-muted-foreground">
|
|
{tokensLoading ? 'Loading tokens...' : 'No tokens available for this pool'}
|
|
</div>
|
|
) : (
|
|
<div className="px-4 py-3 text-sm text-muted-foreground">
|
|
Select a pool first
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
/* Token Selection (Unstake Mode) */
|
|
<div className="space-y-2">
|
|
<div className="text-sm">
|
|
<label className="text-muted-foreground">{t('stake.selectToken')}</label>
|
|
</div>
|
|
<div className="relative" ref={tokenDropdownRef}>
|
|
<Button
|
|
variant="secondary"
|
|
className="w-full h-16 justify-between"
|
|
onClick={() => setIsTokenDropdownOpen(!isTokenDropdownOpen)}
|
|
disabled={!selectedPool || redeemAll}
|
|
>
|
|
{redeemAll ? (
|
|
<span className="font-medium">All Tokens (Redeem All)</span>
|
|
) : selectedToken ? (
|
|
<span className="font-medium">{selectedToken.symbol}</span>
|
|
) : (
|
|
t('stake.selectToken')
|
|
)}
|
|
<ChevronDown className="h-4 w-4 ml-2" />
|
|
</Button>
|
|
<div className="text-xs text-muted-foreground text-right mt-1">
|
|
{t('swap.balance')}: {lpBalance !== null && selectedPool ? formatUnits(lpBalance, 18) : '0.00'} {selectedPool?.symbol || 'LP'}
|
|
</div>
|
|
{!redeemAll && isTokenDropdownOpen && (
|
|
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
|
{availableTokensForPool.length > 0 ? (
|
|
availableTokensForPool.map((token) => (
|
|
<button
|
|
key={token.address}
|
|
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
|
|
onClick={() => {
|
|
setSelectedToken(token);
|
|
setIsTokenDropdownOpen(false);
|
|
}}
|
|
>
|
|
<span className="font-medium">{token.symbol}</span>
|
|
</button>
|
|
))
|
|
) : selectedPool ? (
|
|
<div className="px-4 py-3 text-sm text-muted-foreground">
|
|
{tokensLoading ? 'Loading tokens...' : 'No tokens available for this pool'}
|
|
</div>
|
|
) : (
|
|
<div className="px-4 py-3 text-sm text-muted-foreground">
|
|
Select a pool first
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Amount Input */}
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between items-center text-sm">
|
|
<label className="text-muted-foreground">{t('stake.amount')}</label>
|
|
<div className="flex gap-2">
|
|
{mode === 'unstake' && (
|
|
<Button
|
|
variant={redeemAll ? 'default' : 'outline'}
|
|
size="sm"
|
|
className="h-6 px-2 text-xs"
|
|
onClick={() => {
|
|
setRedeemAll(!redeemAll);
|
|
if (!redeemAll && lpBalance !== null) {
|
|
setStakeAmount(formatUnits(lpBalance, 18));
|
|
}
|
|
}}
|
|
disabled={!selectedPool}
|
|
title="Burn entire LP token and receive all underlying tokens"
|
|
>
|
|
{redeemAll ? 'Redeem All: ON' : 'Redeem All'}
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 px-2 text-xs"
|
|
onClick={() => {
|
|
if (mode === 'stake' && selectedToken) {
|
|
setStakeAmount(formatUnits(selectedToken.balance, selectedToken.decimals));
|
|
} else if (mode === 'unstake' && lpBalance !== null) {
|
|
setStakeAmount(formatUnits(lpBalance, 18));
|
|
}
|
|
}}
|
|
disabled={!selectedPool || (mode === 'stake' ? !selectedToken : lpBalance === null)}
|
|
>
|
|
MAX
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<Input
|
|
type="number"
|
|
placeholder="0.0"
|
|
value={stakeAmount}
|
|
onChange={(e) => setStakeAmount(e.target.value)}
|
|
className="text-2xl h-16"
|
|
disabled={!selectedPool || (mode === 'stake' && !selectedToken)}
|
|
/>
|
|
{isAmountExceedingBalance && (
|
|
<p className="text-sm text-destructive">
|
|
{t('stake.insufficientBalance')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Swap Mint Amounts Display (Stake Mode) */}
|
|
{mode === 'stake' && swapMintAmounts && selectedToken && !isAmountExceedingBalance && (
|
|
<div className="px-4 py-3 bg-muted/30 rounded-lg space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">{t('stake.amountUsed')}:</span>
|
|
<span className="font-medium">
|
|
{swapMintLoading ? 'Calculating...' : `${formatUnits(swapMintAmounts.amountInUsed, selectedToken.decimals)} ${selectedToken.symbol}`}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">{t('stake.fee')}:</span>
|
|
<span className="font-medium">
|
|
{swapMintLoading ? 'Calculating...' : `${formatUnits(swapMintAmounts.fee, selectedToken.decimals)} ${selectedToken.symbol}`}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">{t('stake.lpMinted')}:</span>
|
|
<span className="font-medium">
|
|
{swapMintLoading ? 'Calculating...' : `${formatUnits(swapMintAmounts.lpMinted, 18)} ${selectedPool?.symbol || 'LP'}`}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Burn Swap Amounts Display (Unstake Mode) */}
|
|
{mode === 'unstake' && !redeemAll && burnSwapAmounts && selectedToken && !isAmountExceedingBalance && (
|
|
<div className="px-4 py-3 bg-muted/30 rounded-lg space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">{t('stake.amountOut')}:</span>
|
|
<span className="font-medium">
|
|
{burnSwapLoading ? 'Calculating...' : `${Number(formatUnits(burnSwapAmounts.amountOut, selectedToken.decimals)).toLocaleString('en-US', { maximumFractionDigits: 6 })} ${selectedToken.symbol}`}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">{t('stake.fee')}:</span>
|
|
<span className="font-medium">
|
|
{burnSwapLoading ? 'Calculating...' : `${Number(formatUnits(burnSwapAmounts.outFee, selectedToken.decimals)).toLocaleString('en-US', { maximumFractionDigits: 6 })} ${selectedToken.symbol}`}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Redeem All Tokens Display */}
|
|
{mode === 'unstake' && redeemAll && poolTokens.length > 0 && !isAmountExceedingBalance && (
|
|
<div className="px-4 py-3 bg-muted/30 rounded-lg space-y-2">
|
|
<div className="text-sm font-medium mb-2">You will receive:</div>
|
|
<div className="space-y-1">
|
|
{poolTokens.map((token, index) => (
|
|
<div key={token.address} className="text-sm flex justify-between">
|
|
<span className="text-muted-foreground">{token.symbol}:</span>
|
|
<span className="font-medium">
|
|
{burnAmountsLoading ? 'Calculating...' : burnAmounts && burnAmounts[index]
|
|
? `${Number(formatUnits(burnAmounts[index], token.decimals)).toLocaleString('en-US', { maximumFractionDigits: 6 })}`
|
|
: '(proportional to pool composition)'
|
|
}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Stake/Unstake Button */}
|
|
<Button
|
|
className="w-full h-14 text-lg"
|
|
onClick={handleStake}
|
|
disabled={
|
|
!isConnected ||
|
|
!stakeAmount ||
|
|
!selectedPool ||
|
|
isAmountExceedingBalance ||
|
|
(mode === 'stake'
|
|
? (!selectedToken || isSwapMinting)
|
|
: (redeemAll
|
|
? isBurning
|
|
: (!selectedToken || inputTokenIndex === undefined || isBurnSwapping)))
|
|
}
|
|
>
|
|
{!isConnected
|
|
? t('swap.connectWalletToSwap')
|
|
: (mode === 'stake' && isSwapMinting)
|
|
? 'Staking...'
|
|
: (mode === 'unstake' && (isBurnSwapping || isBurning))
|
|
? (redeemAll ? 'Redeeming...' : 'Unstaking...')
|
|
: mode === 'stake'
|
|
? t('stake.stakeButton')
|
|
: (redeemAll ? 'Redeem All' : 'Unstake')}
|
|
</Button>
|
|
</CardContent>
|
|
|
|
{/* Transaction Modal Overlay */}
|
|
{transactionStatus !== 'idle' && (
|
|
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50 rounded-lg">
|
|
<div className="bg-card border rounded-lg p-8 max-w-sm w-full mx-4 shadow-lg">
|
|
{transactionStatus === 'pending' && (
|
|
<div className="flex flex-col items-center space-y-4">
|
|
<Loader2 className="h-16 w-16 animate-spin text-primary" />
|
|
<h3 className="text-xl font-semibold text-center">
|
|
{mode === 'stake' ? 'Approving Stake' : redeemAll ? 'Approving Redeem All' : 'Approving Unstake'}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground text-center">
|
|
{mode === 'stake'
|
|
? `Staking ${stakeAmount} ${selectedToken?.symbol} to ${selectedPool?.symbol}`
|
|
: redeemAll
|
|
? `Redeeming ${stakeAmount} ${selectedPool?.symbol} LP for all pool tokens`
|
|
: `Unstaking ${stakeAmount} ${selectedPool?.symbol} LP for ${selectedToken?.symbol}`
|
|
}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground text-center">
|
|
Please confirm the transactions in your wallet
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{transactionStatus === 'success' && (
|
|
<div className="flex flex-col items-center space-y-4">
|
|
<CheckCircle className="h-16 w-16 text-green-500" />
|
|
<h3 className="text-xl font-semibold text-center">
|
|
{mode === 'stake' ? 'Stake Confirmed!' : redeemAll ? 'Redeem Confirmed!' : 'Unstake Confirmed!'}
|
|
</h3>
|
|
|
|
{/* Display actual amounts or estimates */}
|
|
{mode === 'stake' ? (
|
|
// Stake mode success message
|
|
<div className="w-full space-y-3">
|
|
{actualSwapMintAmounts && selectedToken && selectedPool ? (
|
|
// Show actual amounts from transaction
|
|
<>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">Token Used:</span>
|
|
<span className="font-medium">{formatUnits(actualSwapMintAmounts.amountInUsed, selectedToken.decimals)} {selectedToken.symbol}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">LP Received:</span>
|
|
<span className="font-medium">{formatUnits(actualSwapMintAmounts.lpMinted, 18)} {selectedPool.symbol}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">Fee:</span>
|
|
<span className="font-medium">{formatUnits(actualSwapMintAmounts.lpFee + actualSwapMintAmounts.protocolFee, selectedToken.decimals)} {selectedToken.symbol}</span>
|
|
</div>
|
|
</>
|
|
) : (
|
|
// Fallback to estimates
|
|
<>
|
|
<p className="text-sm text-muted-foreground text-center">
|
|
Successfully staked {stakeAmount} {selectedToken?.symbol} to {selectedPool?.symbol}
|
|
<br />
|
|
<span className="text-xs italic opacity-70">
|
|
*Disclaimer: This is an estimate from the protocol. The actual amounts might be slightly different due to slippage.
|
|
</span>
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
) : redeemAll ? (
|
|
// Redeem All mode success message
|
|
<div className="w-full space-y-3">
|
|
{actualBurnAmounts && selectedPool ? (
|
|
// Show actual amounts from transaction
|
|
<>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">LP Burned:</span>
|
|
<span className="font-medium">{formatUnits(actualBurnAmounts.lpBurned, 18)} {selectedPool.symbol}</span>
|
|
</div>
|
|
<div className="text-sm font-medium mt-3 mb-2">Tokens Received:</div>
|
|
{actualBurnAmounts.withdrawAmounts.map((amount, index) => {
|
|
const token = poolTokens[index];
|
|
if (!token) return null;
|
|
|
|
return (
|
|
<div key={index} className="flex justify-between text-sm pl-2">
|
|
<span className="text-muted-foreground">{token.symbol}:</span>
|
|
<span className="font-medium">
|
|
{formatUnits(amount, token.decimals)}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</>
|
|
) : (
|
|
// Fallback to estimates
|
|
<>
|
|
<p className="text-sm text-muted-foreground text-center">
|
|
Successfully redeemed {stakeAmount} {selectedPool?.symbol} LP for all pool tokens
|
|
<br />
|
|
<span className="text-xs italic opacity-70">
|
|
*Disclaimer: This is an estimate from the protocol. The actual amounts might be slightly different due to slippage.
|
|
</span>
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
) : (
|
|
// Unstake mode success message
|
|
<div className="w-full space-y-3">
|
|
{actualBurnSwapAmounts && selectedToken && selectedPool ? (
|
|
// Show actual amounts from transaction
|
|
<>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">LP Burned:</span>
|
|
<span className="font-medium">{formatUnits(actualBurnSwapAmounts.amountIn, 18)} {selectedPool.symbol}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">Token Received:</span>
|
|
<span className="font-medium">{formatUnits(actualBurnSwapAmounts.amountOut, selectedToken.decimals)} {selectedToken.symbol}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">Fee:</span>
|
|
<span className="font-medium">{formatUnits(actualBurnSwapAmounts.lpFee + actualBurnSwapAmounts.protocolFee, selectedToken.decimals)} {selectedToken.symbol}</span>
|
|
</div>
|
|
</>
|
|
) : (
|
|
// Fallback to estimates
|
|
<>
|
|
<p className="text-sm text-muted-foreground text-center">
|
|
Successfully unstaked {stakeAmount} {selectedPool?.symbol} LP for {selectedToken?.symbol}
|
|
<br />
|
|
<span className="text-xs italic opacity-70">
|
|
*Disclaimer: This is an estimate from the protocol. The actual amounts might be slightly different due to slippage.
|
|
</span>
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
onClick={handleCloseModal}
|
|
className="w-full mt-4"
|
|
>
|
|
Close
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{transactionStatus === 'error' && (
|
|
<div className="flex flex-col items-center space-y-4">
|
|
<XCircle className="h-16 w-16 text-destructive" />
|
|
<h3 className="text-xl font-semibold text-center">
|
|
{mode === 'stake' ? 'Stake Failed' : 'Unstake Failed'}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground text-center break-words">
|
|
{transactionError || 'Transaction failed'}
|
|
</p>
|
|
<Button
|
|
onClick={handleCloseModal}
|
|
variant="outline"
|
|
className="w-full mt-4"
|
|
>
|
|
Close
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
} |