'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 { ArrowDownUp, ChevronDown, Settings, CheckCircle, XCircle, Loader2 } from 'lucide-react'; import { useAccount } from 'wagmi'; import { useTokenDetails, useGetPoolsByToken, type TokenDetails } from '@/hooks/usePartyPlanner'; import { useSwapAmounts, useSwap, selectBestSwapRoute, type ActualSwapAmounts } from '@/hooks/usePartyPool'; import { formatUnits, parseUnits } from 'viem'; import { SwapReviewModal } from './swap-review-modal'; import UniswapQuote from './uniswap-quote'; type TransactionStatus = 'idle' | 'pending' | 'success' | 'error'; export function SwapForm() { const { t } = useTranslation(); const { isConnected, address } = useAccount(); const [fromAmount, setFromAmount] = useState(''); const [toAmount, setToAmount] = useState(''); const [selectedFromToken, setSelectedFromToken] = useState(null); const [selectedToToken, setSelectedToToken] = useState(null); const [isFromDropdownOpen, setIsFromDropdownOpen] = useState(false); const [isToDropdownOpen, setIsToDropdownOpen] = useState(false); const [maxSlippage, setMaxSlippage] = useState('1'); // Default 1% const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isReviewModalOpen, setIsReviewModalOpen] = useState(false); const [transactionStatus, setTransactionStatus] = useState('idle'); const [transactionError, setTransactionError] = useState(null); const [actualSwapAmounts, setActualSwapAmounts] = useState(null); const fromDropdownRef = useRef(null); const toDropdownRef = useRef(null); // Use the custom hook to get all token details with a single batched RPC call const { tokenDetails, loading } = useTokenDetails(address); // Get available tokens for the selected "from" token const { availableTokens, error: poolsError } = useGetPoolsByToken(selectedFromToken?.address); // Only calculate swap amounts when both tokens are selected // Use useMemo to prevent creating a new array reference on every render const filteredAvailableTokens = useMemo(() => { if (selectedFromToken && selectedToToken && availableTokens) { return availableTokens.filter(token => token.address.toLowerCase() === selectedToToken.address.toLowerCase() ); } return null; }, [selectedFromToken, selectedToToken, availableTokens]); // Get the current slippage value const currentSlippage = parseFloat(maxSlippage) || 1; // Calculate swap amounts for the selected token pair only const { swapAmounts } = useSwapAmounts( filteredAvailableTokens, fromAmount, selectedFromToken?.decimals || 18, currentSlippage ); // Check if user has insufficient balance const hasInsufficientBalance = useMemo(() => { if (!selectedFromToken || !fromAmount || fromAmount === '') { return false; } try { const inputAmount = parseUnits(fromAmount, selectedFromToken.decimals); return inputAmount > selectedFromToken.balance; } catch { return false; } }, [selectedFromToken, fromAmount]); // Check if calculated slippage exceeds 5% const slippageExceedsLimit = useMemo(() => { if (!swapAmounts || swapAmounts.length === 0 || swapAmounts[0].calculatedSlippage === undefined) { return false; } return Math.abs(swapAmounts[0].calculatedSlippage) > 5; }, [swapAmounts]); // Initialize swap hook const { executeSwap, estimateGas, isSwapping, gasEstimate, isEstimatingGas } = useSwap(); // Update "You Receive" amount when swap calculation completes useEffect(() => { if (hasInsufficientBalance) { setToAmount(''); return; } if (swapAmounts && swapAmounts.length > 0 && selectedToToken) { const swapResult = swapAmounts[0]; // Get the first (and should be only) result const formattedAmount = formatUnits(swapResult.amountOut, selectedToToken.decimals); setToAmount(formattedAmount); } else { setToAmount(''); } }, [swapAmounts, selectedToToken, hasInsufficientBalance]); // Close dropdowns when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (fromDropdownRef.current && !fromDropdownRef.current.contains(event.target as Node)) { setIsFromDropdownOpen(false); } if (toDropdownRef.current && !toDropdownRef.current.contains(event.target as Node)) { setIsToDropdownOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleSwap = async () => { if (!swapAmounts || swapAmounts.length === 0) { console.error('No swap amounts available'); return; } if (!selectedFromToken || !selectedToToken) { console.error('Tokens not selected'); return; } setTransactionStatus('pending'); setTransactionError(null); try { // Use the shared helper to select the best swap route const bestRoute = selectBestSwapRoute(swapAmounts); if (!bestRoute) { console.error('No valid swap route found'); setTransactionError('No valid swap route found'); setTransactionStatus('error'); return; } // Use the user's input directly as maxAmountIn const maxAmountIn = parseUnits(fromAmount, selectedFromToken.decimals); // Execute the swap and capture actual amounts from the transaction const result = await executeSwap( bestRoute.poolAddress, selectedFromToken.address, bestRoute.inputTokenIndex, bestRoute.outputTokenIndex, maxAmountIn, currentSlippage ); // Store the actual swap amounts if available if (result?.actualSwapAmounts) { setActualSwapAmounts(result.actualSwapAmounts); } setTransactionStatus('success'); } catch (err) { console.error('Swap failed:', err); setTransactionError(err instanceof Error ? err.message : 'Transaction failed'); setTransactionStatus('error'); } }; const handleCloseModal = () => { if (transactionStatus === 'success') { // Clear the form after successful swap setFromAmount(''); setToAmount(''); setSelectedFromToken(null); setSelectedToToken(null); } setTransactionStatus('idle'); setTransactionError(null); }; const switchTokens = () => { // Switch both tokens and amounts const tempFromAmount = fromAmount; const tempFromToken = selectedFromToken; setFromAmount(toAmount); setToAmount(tempFromAmount); setSelectedFromToken(selectedToToken); setSelectedToToken(tempFromToken); }; // Estimate gas when swap parameters change useEffect(() => { const estimateSwapGas = async () => { if (!swapAmounts || swapAmounts.length === 0 || !selectedFromToken || !selectedToToken || !fromAmount || !isConnected) { return; } await estimateGas(); }; estimateSwapGas(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [swapAmounts, selectedFromToken, selectedToToken, fromAmount, currentSlippage, isConnected]); return (
Swap
{/* From Token */}
setFromAmount(e.target.value)} className="text-2xl h-16" />
{t('swap.balance')}: {selectedFromToken ? formatUnits(selectedFromToken.balance, selectedFromToken.decimals) : '0.00'}
{isFromDropdownOpen && (
{tokenDetails && tokenDetails.length > 0 ? ( tokenDetails.map((token) => ( )) ) : (
{loading ? 'Loading tokens...' : 'No tokens available'}
)}
)}
{/* Switch Button */}
{/* To Token */}
setToAmount(e.target.value)} className="text-2xl h-16" disabled={!selectedFromToken} readOnly />
{t('swap.balance')}: {selectedToToken ? formatUnits(selectedToToken.balance, selectedToToken.decimals) : '0.00'}
{isToDropdownOpen && (
{availableTokens && availableTokens.length > 0 && tokenDetails ? ( // Filter tokenDetails to only show tokens in availableTokens tokenDetails .filter((token) => availableTokens.some((availToken) => availToken.address.toLowerCase() === token.address.toLowerCase() ) ) .map((token) => ( )) ) : selectedFromToken ? (
{loading ? 'Loading available tokens...' : poolsError || 'No tokens available for swap'}
) : (
Select a token in "You Pay" first
)}
)}
{/* Error message for unsupported tokens */} {poolsError && selectedFromToken && (

{poolsError}

)} {/* Error message for insufficient balance */} {hasInsufficientBalance && (

Insufficient balance

)} {/* Error message for slippage exceeding 5% */} {slippageExceedsLimit && (

⚠️ Slippage Exceeds 5%

The estimated slippage for this swap is {Math.abs(swapAmounts![0].calculatedSlippage!).toFixed(2)}%. We cannot process this swap as you may lose too much money due to the high slippage.

)} {/* High slippage warning - show if calculated slippage exceeds max slippage but is under 5% */} {!slippageExceedsLimit && swapAmounts && swapAmounts.length > 0 && swapAmounts[0].calculatedSlippage !== undefined && ( Math.abs(swapAmounts[0].calculatedSlippage) > currentSlippage ) && (

⚠️ Slippage Exceeds Your Tolerance

The estimated slippage for this swap is {Math.abs(swapAmounts[0].calculatedSlippage).toFixed(2)}%, which exceeds your maximum slippage setting of {currentSlippage}%. This swap may result in less favorable pricing than expected due to low liquidity.

)} {/* Uniswap Quote */} {fromAmount && ( )} {/* Gas Estimate, Slippage, and Fees */} {isConnected && fromAmount && toAmount && (
Estimated Gas Cost: {isEstimatingGas ? 'Calculating...' : gasEstimate ? (gasEstimate.estimatedCostUsd.startsWith('<') ? gasEstimate.estimatedCostUsd : `$${gasEstimate.estimatedCostUsd}`) : '-'}
Max Slippage: {maxSlippage}%
{swapAmounts && swapAmounts.length > 0 && selectedFromToken && fromAmount && ( <>
Total Amount In: {formatUnits(swapAmounts[0].amountIn, selectedFromToken.decimals)} {selectedFromToken.symbol}
Fee: {formatUnits(swapAmounts[0].fee, selectedFromToken.decimals)} {selectedFromToken.symbol} {' '} ({((Number(swapAmounts[0].fee) / Number(swapAmounts[0].amountIn)) * 100).toFixed(2)}%)
)}
)} {/* Review Button */}
{/* Settings Modal */} {isSettingsOpen && ( <>
setIsSettingsOpen(false)} />

Settings

setMaxSlippage(e.target.value)} className="pr-8" step="0.1" min="0" max="100" /> %

You will be warned if the slippage exceeds this setting, and you can choose whether to proceed with the trade.

)} {/* Review Modal */} 0 ? swapAmounts[0].fee : null} onConfirm={async () => { await handleSwap(); setIsReviewModalOpen(false); }} isSwapping={isSwapping} /> {/* Transaction Modal Overlay */} {transactionStatus !== 'idle' && (
{transactionStatus === 'pending' && (

Approving Swap

Swapping {fromAmount} {selectedFromToken?.symbol} → {toAmount} {selectedToToken?.symbol}

Please confirm the transactions in your wallet

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

Swap Confirmed!

{/* Display actual amounts or estimates */}
{actualSwapAmounts && selectedFromToken && selectedToToken ? ( // Show actual amounts from transaction <>
Swapped: {formatUnits(actualSwapAmounts.amountIn, selectedFromToken.decimals)} {selectedFromToken.symbol}
Received: {formatUnits(actualSwapAmounts.amountOut, selectedToToken.decimals)} {selectedToToken.symbol}
Fee: {formatUnits(actualSwapAmounts.lpFee + actualSwapAmounts.protocolFee, selectedFromToken.decimals)} {selectedFromToken.symbol}
) : ( // Fallback to estimates <>

Successfully swapped {fromAmount} {selectedFromToken?.symbol} to {toAmount} {selectedToToken?.symbol}
*Disclaimer: This is an estimate from the protocol. The actual amounts might be slightly different due to slippage.

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

Swap Failed

{transactionError || 'Transaction failed'}

)}
)} ); }