'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 } from 'lucide-react'; import { useAccount } from 'wagmi'; import { useTokenDetails, useGetPoolsByToken, type TokenDetails } from '@/hooks/usePartyPlanner'; import { useSwapAmounts, useSwap, selectBestSwapRoute } from '@/hooks/usePartyPool'; import { formatUnits, parseUnits } from 'viem'; import { SwapReviewModal } from './swap-review-modal'; 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 [slippage, setSlippage] = useState(0.5); // Default 0.5% const [customSlippage, setCustomSlippage] = useState(''); const [isCustomSlippage, setIsCustomSlippage] = useState(false); const [isReviewModalOpen, setIsReviewModalOpen] = useState(false); 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 } = 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 (either custom or preset) const currentSlippage = isCustomSlippage ? parseFloat(customSlippage) || 0.5 : slippage; // Calculate swap amounts for the selected token pair only const { swapAmounts } = useSwapAmounts( filteredAvailableTokens, fromAmount, selectedFromToken?.decimals || 18, currentSlippage ); // Initialize swap hook const { executeSwap, estimateGas, isSwapping, gasEstimate, isEstimatingGas } = useSwap(); // Update "You Receive" amount when swap calculation completes useEffect(() => { 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); } }, [swapAmounts, selectedToToken]); // 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; } try { // Use the shared helper to select the best swap route const bestRoute = selectBestSwapRoute(swapAmounts); if (!bestRoute) { console.error('No valid swap route found'); return; } // Convert fromAmount to Wei const maxAmountIn = parseUnits(fromAmount, selectedFromToken.decimals); // Execute the swap await executeSwap( bestRoute.poolAddress, selectedFromToken.address, bestRoute.inputTokenIndex, bestRoute.outputTokenIndex, maxAmountIn, currentSlippage ); } catch (err) { console.error('Swap failed:', err); } }; const switchTokens = () => { // Switch tokens logic setFromAmount(toAmount); setToAmount(fromAmount); }; // 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 ( {t('swap.title')} {/* From Token */}
{t('swap.balance')}: {selectedFromToken ? formatUnits(selectedFromToken.balance, selectedFromToken.decimals) : '0.00'}
setFromAmount(e.target.value)} className="text-2xl h-16" />
{isFromDropdownOpen && (
{tokenDetails && tokenDetails.length > 0 ? ( tokenDetails.map((token) => ( )) ) : (
{loading ? 'Loading tokens...' : 'No tokens available'}
)}
)}
{/* Switch Button */}
{/* To Token */}
{t('swap.balance')}: {selectedToToken ? formatUnits(selectedToToken.balance, selectedToToken.decimals) : '0.00'}
setToAmount(e.target.value)} className="text-2xl h-16" disabled={!selectedFromToken} />
{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...' : 'No tokens available for swap'}
) : (
Select a token in "You Pay" first
)}
)}
{/* Slippage Tolerance */}
{isCustomSlippage ? customSlippage || '0' : slippage}%
{[0.1, 0.2, 0.3, 1, 2, 3].map((percent) => ( ))}
{ setCustomSlippage(e.target.value); setIsCustomSlippage(true); }} onFocus={() => setIsCustomSlippage(true)} className={`h-9 pr-6 ${isCustomSlippage ? 'border-primary' : ''}`} step="0.01" /> {isCustomSlippage && ( % )}
{/* Gas Estimate */} {isConnected && fromAmount && toAmount && gasEstimate && !isEstimatingGas && (
Estimated Gas Cost: ${gasEstimate.estimatedCostUsd}
)} {isConnected && fromAmount && toAmount && isEstimatingGas && (
Estimated Gas Cost: Calculating...
)} {/* Review Button */}
{/* Review Modal */} { await handleSwap(); setIsReviewModalOpen(false); }} isSwapping={isSwapping} />
); }