'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(defaultMode); const [stakeAmount, setStakeAmount] = useState(''); const [selectedPool, setSelectedPool] = useState(null); const [selectedToken, setSelectedToken] = useState(null); const [isPoolDropdownOpen, setIsPoolDropdownOpen] = useState(false); const [isTokenDropdownOpen, setIsTokenDropdownOpen] = useState(false); const [transactionStatus, setTransactionStatus] = useState('idle'); const [transactionError, setTransactionError] = useState(null); const [actualSwapMintAmounts, setActualSwapMintAmounts] = useState(null); const [actualBurnSwapAmounts, setActualBurnSwapAmounts] = useState(null); const [actualBurnAmounts, setActualBurnAmounts] = useState(null); const [redeemAll, setRedeemAll] = useState(false); const [poolTokens, setPoolTokens] = useState([]); const poolDropdownRef = useRef(null); const tokenDropdownRef = useRef(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, publicClient.readContract({ address: tokenAddress as `0x${string}`, abi: ERC20ABI, functionName: 'decimals', }) as Promise, ]); 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 (
{mode === 'stake' ? 'Stake' : 'Unstake'}
{/* First Selection - depends on mode */} {mode === 'stake' ? ( /* Pool Selection (Stake Mode) */
{isPoolDropdownOpen && (
{poolDetails && poolDetails.length > 0 ? ( poolDetails.map((pool) => ( )) ) : (
{poolsLoading ? 'Loading pools...' : 'No pools available'}
)}
)}
) : ( /* Pool Selection (Unstake Mode) */
{isPoolDropdownOpen && (
{poolDetails && poolDetails.length > 0 ? ( poolDetails.map((pool) => ( )) ) : (
{poolsLoading ? 'Loading pools...' : 'No pools available'}
)}
)}
)} {/* Toggle Button */}
{/* Second Selection - depends on mode */} {mode === 'stake' ? ( /* Token Selection (Stake Mode) */
{t('swap.balance')}: {selectedToken ? formatUnits(selectedToken.balance, selectedToken.decimals) : '0.00'}
{isTokenDropdownOpen && (
{availableTokensForPool.length > 0 ? ( availableTokensForPool.map((token) => ( )) ) : selectedPool ? (
{tokensLoading ? 'Loading tokens...' : 'No tokens available for this pool'}
) : (
Select a pool first
)}
)}
) : ( /* Token Selection (Unstake Mode) */
{t('swap.balance')}: {lpBalance !== null && selectedPool ? formatUnits(lpBalance, 18) : '0.00'} {selectedPool?.symbol || 'LP'}
{!redeemAll && isTokenDropdownOpen && (
{availableTokensForPool.length > 0 ? ( availableTokensForPool.map((token) => ( )) ) : selectedPool ? (
{tokensLoading ? 'Loading tokens...' : 'No tokens available for this pool'}
) : (
Select a pool first
)}
)}
)} {/* Amount Input */}
{mode === 'unstake' && ( )}
setStakeAmount(e.target.value)} className="text-2xl h-16" disabled={!selectedPool || (mode === 'stake' && !selectedToken)} /> {isAmountExceedingBalance && (

{t('stake.insufficientBalance')}

)}
{/* Swap Mint Amounts Display (Stake Mode) */} {mode === 'stake' && swapMintAmounts && selectedToken && !isAmountExceedingBalance && (
{t('stake.amountUsed')}: {swapMintLoading ? 'Calculating...' : `${formatUnits(swapMintAmounts.amountInUsed, selectedToken.decimals)} ${selectedToken.symbol}`}
{t('stake.fee')}: {swapMintLoading ? 'Calculating...' : `${formatUnits(swapMintAmounts.fee, selectedToken.decimals)} ${selectedToken.symbol}`}
{t('stake.lpMinted')}: {swapMintLoading ? 'Calculating...' : `${formatUnits(swapMintAmounts.lpMinted, 18)} ${selectedPool?.symbol || 'LP'}`}
)} {/* Burn Swap Amounts Display (Unstake Mode) */} {mode === 'unstake' && !redeemAll && burnSwapAmounts && selectedToken && !isAmountExceedingBalance && (
{t('stake.amountOut')}: {burnSwapLoading ? 'Calculating...' : `${Number(formatUnits(burnSwapAmounts.amountOut, selectedToken.decimals)).toLocaleString('en-US', { maximumFractionDigits: 6 })} ${selectedToken.symbol}`}
{t('stake.fee')}: {burnSwapLoading ? 'Calculating...' : `${Number(formatUnits(burnSwapAmounts.outFee, selectedToken.decimals)).toLocaleString('en-US', { maximumFractionDigits: 6 })} ${selectedToken.symbol}`}
)} {/* Redeem All Tokens Display */} {mode === 'unstake' && redeemAll && poolTokens.length > 0 && !isAmountExceedingBalance && (
You will receive:
{poolTokens.map((token, index) => (
{token.symbol}: {burnAmountsLoading ? 'Calculating...' : burnAmounts && burnAmounts[index] ? `${Number(formatUnits(burnAmounts[index], token.decimals)).toLocaleString('en-US', { maximumFractionDigits: 6 })}` : '(proportional to pool composition)' }
))}
)} {/* Stake/Unstake Button */}
{/* Transaction Modal Overlay */} {transactionStatus !== 'idle' && (
{transactionStatus === 'pending' && (

{mode === 'stake' ? 'Approving Stake' : redeemAll ? 'Approving Redeem All' : 'Approving Unstake'}

{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}` }

Please confirm the transactions in your wallet

)} {transactionStatus === 'success' && (

{mode === 'stake' ? 'Stake Confirmed!' : redeemAll ? 'Redeem Confirmed!' : 'Unstake Confirmed!'}

{/* Display actual amounts or estimates */} {mode === 'stake' ? ( // Stake mode success message
{actualSwapMintAmounts && selectedToken && selectedPool ? ( // Show actual amounts from transaction <>
Token Used: {formatUnits(actualSwapMintAmounts.amountInUsed, selectedToken.decimals)} {selectedToken.symbol}
LP Received: {formatUnits(actualSwapMintAmounts.lpMinted, 18)} {selectedPool.symbol}
LP Fee: {formatUnits(actualSwapMintAmounts.lpFee, selectedToken.decimals)} {selectedToken.symbol}
Protocol Fee: {formatUnits(actualSwapMintAmounts.protocolFee, selectedToken.decimals)} {selectedToken.symbol}
) : ( // Fallback to estimates <>

Successfully staked {stakeAmount} {selectedToken?.symbol} to {selectedPool?.symbol}
*Disclaimer: This is an estimate from the protocol. The actual amounts might be slightly different due to slippage.

)}
) : redeemAll ? ( // Redeem All mode success message
{actualBurnAmounts && selectedPool ? ( // Show actual amounts from transaction <>
LP Burned: {formatUnits(actualBurnAmounts.lpBurned, 18)} {selectedPool.symbol}
Tokens Received:
{actualBurnAmounts.withdrawAmounts.map((amount, index) => { const token = poolTokens[index]; if (!token) return null; return (
{token.symbol}: {formatUnits(amount, token.decimals)}
); })} ) : ( // Fallback to estimates <>

Successfully redeemed {stakeAmount} {selectedPool?.symbol} LP for all pool tokens
*Disclaimer: This is an estimate from the protocol. The actual amounts might be slightly different due to slippage.

)}
) : ( // Unstake mode success message
{actualBurnSwapAmounts && selectedToken && selectedPool ? ( // Show actual amounts from transaction <>
LP Burned: {formatUnits(actualBurnSwapAmounts.amountIn, 18)} {selectedPool.symbol}
Token Received: {formatUnits(actualBurnSwapAmounts.amountOut, selectedToken.decimals)} {selectedToken.symbol}
LP Fee: {formatUnits(actualBurnSwapAmounts.lpFee, selectedToken.decimals)} {selectedToken.symbol}
Protocol Fee: {formatUnits(actualBurnSwapAmounts.protocolFee, selectedToken.decimals)} {selectedToken.symbol}
) : ( // Fallback to estimates <>

Successfully unstaked {stakeAmount} {selectedPool?.symbol} LP for {selectedToken?.symbol}
*Disclaimer: This is an estimate from the protocol. The actual amounts might be slightly different due to slippage.

)}
)}
)} {transactionStatus === 'error' && (

{mode === 'stake' ? 'Stake Failed' : 'Unstake Failed'}

{transactionError || 'Transaction failed'}

)}
)}
); }