605 lines
25 KiB
TypeScript
605 lines
25 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 { 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';
|
||
|
||
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<TokenDetails | null>(null);
|
||
const [selectedToToken, setSelectedToToken] = useState<TokenDetails | null>(null);
|
||
const [isFromDropdownOpen, setIsFromDropdownOpen] = useState(false);
|
||
const [isToDropdownOpen, setIsToDropdownOpen] = useState(false);
|
||
const [maxSlippage, setMaxSlippage] = useState<string>('1'); // Default 1%
|
||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||
const [isReviewModalOpen, setIsReviewModalOpen] = useState(false);
|
||
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus>('idle');
|
||
const [transactionError, setTransactionError] = useState<string | null>(null);
|
||
const [actualSwapAmounts, setActualSwapAmounts] = useState<ActualSwapAmounts | null>(null);
|
||
const fromDropdownRef = useRef<HTMLDivElement>(null);
|
||
const toDropdownRef = useRef<HTMLDivElement>(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);
|
||
}
|
||
}, [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 (
|
||
<Card className="w-full max-w-md mx-auto relative">
|
||
<CardHeader>
|
||
<div className="flex justify-between items-center">
|
||
<CardTitle>Swap</CardTitle>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => setIsSettingsOpen(true)}
|
||
className="h-8 w-8"
|
||
>
|
||
<Settings className="h-5 w-5" />
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{/* From Token */}
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between items-center text-sm">
|
||
<label className="text-muted-foreground">{t('swap.youPay')}</label>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-6 px-2 text-xs"
|
||
onClick={() => {
|
||
if (selectedFromToken) {
|
||
setFromAmount(formatUnits(selectedFromToken.balance, selectedFromToken.decimals));
|
||
}
|
||
}}
|
||
disabled={!selectedFromToken}
|
||
>
|
||
MAX
|
||
</Button>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Input
|
||
type="number"
|
||
placeholder="0.0"
|
||
value={fromAmount}
|
||
onChange={(e) => setFromAmount(e.target.value)}
|
||
className="text-2xl h-16"
|
||
/>
|
||
<div className="relative min-w-[160px] space-y-1" ref={fromDropdownRef}>
|
||
<Button
|
||
variant="secondary"
|
||
className="w-full h-16 justify-between"
|
||
onClick={() => setIsFromDropdownOpen(!isFromDropdownOpen)}
|
||
>
|
||
{selectedFromToken ? (
|
||
<span className="font-medium">{selectedFromToken.symbol}</span>
|
||
) : (
|
||
t('swap.selectToken')
|
||
)}
|
||
<ChevronDown className="h-4 w-4 ml-2" />
|
||
</Button>
|
||
<div className="text-xs text-muted-foreground text-right">
|
||
{t('swap.balance')}: {selectedFromToken ? formatUnits(selectedFromToken.balance, selectedFromToken.decimals) : '0.00'}
|
||
</div>
|
||
{isFromDropdownOpen && (
|
||
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
||
{tokenDetails && tokenDetails.length > 0 ? (
|
||
tokenDetails.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={() => {
|
||
setSelectedFromToken(token);
|
||
setIsFromDropdownOpen(false);
|
||
}}
|
||
>
|
||
<span className="font-medium">{token.symbol}</span>
|
||
</button>
|
||
))
|
||
) : (
|
||
<div className="px-4 py-3 text-sm text-muted-foreground">
|
||
{loading ? 'Loading tokens...' : 'No tokens available'}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Switch Button */}
|
||
<div className="flex justify-center">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={switchTokens}
|
||
className="rounded-full"
|
||
>
|
||
<ArrowDownUp className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
|
||
{/* To Token */}
|
||
<div className="space-y-2">
|
||
<div className="text-sm">
|
||
<label className="text-muted-foreground">{t('swap.youReceive')}</label>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Input
|
||
type="number"
|
||
placeholder="0.0"
|
||
value={toAmount}
|
||
onChange={(e) => setToAmount(e.target.value)}
|
||
className="text-2xl h-16"
|
||
disabled={!selectedFromToken}
|
||
/>
|
||
<div className="relative min-w-[160px] space-y-1" ref={toDropdownRef}>
|
||
<Button
|
||
variant="secondary"
|
||
className="w-full h-16 justify-between"
|
||
onClick={() => setIsToDropdownOpen(!isToDropdownOpen)}
|
||
disabled={!selectedFromToken}
|
||
>
|
||
{selectedToToken ? (
|
||
<span className="font-medium">{selectedToToken.symbol}</span>
|
||
) : (
|
||
t('swap.selectToken')
|
||
)}
|
||
<ChevronDown className="h-4 w-4 ml-2" />
|
||
</Button>
|
||
<div className="text-xs text-muted-foreground text-right">
|
||
{t('swap.balance')}: {selectedToToken ? formatUnits(selectedToToken.balance, selectedToToken.decimals) : '0.00'}
|
||
</div>
|
||
{isToDropdownOpen && (
|
||
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
||
{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) => (
|
||
<button
|
||
key={token.address}
|
||
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
|
||
onClick={() => {
|
||
setSelectedToToken(token);
|
||
setIsToDropdownOpen(false);
|
||
}}
|
||
>
|
||
<span className="font-medium">{token.symbol}</span>
|
||
</button>
|
||
))
|
||
) : selectedFromToken ? (
|
||
<div className="px-4 py-3 text-sm text-muted-foreground">
|
||
{loading ? 'Loading available tokens...' : poolsError || 'No tokens available for swap'}
|
||
</div>
|
||
) : (
|
||
<div className="px-4 py-3 text-sm text-muted-foreground">
|
||
Select a token in "You Pay" first
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Error message for unsupported tokens */}
|
||
{poolsError && selectedFromToken && (
|
||
<div className="px-4 py-3 bg-destructive/10 border border-destructive/20 rounded-lg">
|
||
<p className="text-sm text-destructive">{poolsError}</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Error message for insufficient balance */}
|
||
{hasInsufficientBalance && (
|
||
<div className="px-4 py-3 bg-destructive/10 border border-destructive/20 rounded-lg">
|
||
<p className="text-sm text-destructive">Insufficient balance</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Error message for slippage exceeding 5% */}
|
||
{slippageExceedsLimit && (
|
||
<div className="px-4 py-3 bg-destructive/10 border border-destructive/20 rounded-lg">
|
||
<p className="text-sm text-destructive font-medium">⚠️ Slippage Exceeds 5%</p>
|
||
<p className="text-xs text-destructive/80 mt-1">
|
||
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.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 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
|
||
) && (
|
||
<div className="px-4 py-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
|
||
<p className="text-sm text-yellow-600 dark:text-yellow-500 font-medium">⚠️ High Slippage Warning</p>
|
||
<p className="text-xs text-yellow-600/80 dark:text-yellow-500/80 mt-1">
|
||
The estimated slippage for this swap is {Math.abs(swapAmounts[0].calculatedSlippage).toFixed(2)}%. You may lose money due to low liquidity in this pool.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Gas Estimate, Slippage, and Fees */}
|
||
{isConnected && fromAmount && toAmount && (
|
||
<div className="px-4 py-2 bg-muted/30 rounded-lg space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Estimated Gas Cost:</span>
|
||
<span className="font-medium">
|
||
{isEstimatingGas ? 'Calculating...' : gasEstimate ? (gasEstimate.estimatedCostUsd.startsWith('<') ? gasEstimate.estimatedCostUsd : `$${gasEstimate.estimatedCostUsd}`) : '-'}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Max Slippage:</span>
|
||
<span className="font-medium">{maxSlippage}%</span>
|
||
</div>
|
||
{swapAmounts && swapAmounts.length > 0 && selectedFromToken && fromAmount && (
|
||
<>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Total Amount In:</span>
|
||
<span className="font-medium">
|
||
{formatUnits(swapAmounts[0].amountIn, selectedFromToken.decimals)} {selectedFromToken.symbol}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Fee:</span>
|
||
<span className="font-medium">
|
||
{formatUnits(swapAmounts[0].fee, selectedFromToken.decimals)} {selectedFromToken.symbol}
|
||
{' '}
|
||
({((Number(swapAmounts[0].fee) / Number(swapAmounts[0].amountIn)) * 100).toFixed(2)}%)
|
||
</span>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Review Button */}
|
||
<Button
|
||
className="w-full h-14 text-lg"
|
||
onClick={() => setIsReviewModalOpen(true)}
|
||
disabled={!isConnected || !fromAmount || !toAmount || !!poolsError || hasInsufficientBalance || slippageExceedsLimit}
|
||
>
|
||
{!isConnected
|
||
? t('swap.connectWalletToSwap')
|
||
: hasInsufficientBalance
|
||
? 'Insufficient Balance'
|
||
: slippageExceedsLimit
|
||
? 'Slippage Too High'
|
||
: 'Review'}
|
||
</Button>
|
||
</CardContent>
|
||
|
||
{/* Settings Modal */}
|
||
{isSettingsOpen && (
|
||
<>
|
||
<div
|
||
className="fixed inset-0 z-50 bg-black/80 animate-in fade-in-0"
|
||
onClick={() => setIsSettingsOpen(false)}
|
||
/>
|
||
<div className="fixed left-[50%] top-[50%] z-50 w-full max-w-sm translate-x-[-50%] translate-y-[-50%] animate-in fade-in-0 zoom-in-95">
|
||
<div className="bg-background border rounded-lg shadow-lg p-6">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h2 className="text-lg font-semibold">Settings</h2>
|
||
<button
|
||
onClick={() => setIsSettingsOpen(false)}
|
||
className="rounded-sm opacity-70 hover:opacity-100 transition-opacity"
|
||
>
|
||
<ChevronDown className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="text-sm font-medium block mb-2">
|
||
Max Slippage
|
||
</label>
|
||
<div className="relative">
|
||
<Input
|
||
type="number"
|
||
value={maxSlippage}
|
||
onChange={(e) => setMaxSlippage(e.target.value)}
|
||
className="pr-8"
|
||
step="0.1"
|
||
min="0"
|
||
max="100"
|
||
/>
|
||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
|
||
%
|
||
</span>
|
||
</div>
|
||
<p className="text-xs text-muted-foreground mt-2">
|
||
Your transaction will revert if the price changes unfavorably by more than this percentage.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Review Modal */}
|
||
<SwapReviewModal
|
||
open={isReviewModalOpen}
|
||
onOpenChange={setIsReviewModalOpen}
|
||
fromToken={selectedFromToken}
|
||
toToken={selectedToToken}
|
||
fromAmount={fromAmount}
|
||
toAmount={toAmount}
|
||
slippage={currentSlippage}
|
||
gasEstimate={gasEstimate}
|
||
fee={swapAmounts && swapAmounts.length > 0 ? swapAmounts[0].fee : null}
|
||
onConfirm={async () => {
|
||
await handleSwap();
|
||
setIsReviewModalOpen(false);
|
||
}}
|
||
isSwapping={isSwapping}
|
||
/>
|
||
|
||
{/* 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">Approving Swap</h3>
|
||
<p className="text-sm text-muted-foreground text-center">
|
||
Swapping {fromAmount} {selectedFromToken?.symbol} → {toAmount} {selectedToToken?.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">Swap Confirmed!</h3>
|
||
|
||
{/* Display actual amounts or estimates */}
|
||
<div className="w-full space-y-3">
|
||
{actualSwapAmounts && selectedFromToken && selectedToToken ? (
|
||
// Show actual amounts from transaction
|
||
<>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Swapped:</span>
|
||
<span className="font-medium">{formatUnits(actualSwapAmounts.amountIn, selectedFromToken.decimals)} {selectedFromToken.symbol}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Received:</span>
|
||
<span className="font-medium">{formatUnits(actualSwapAmounts.amountOut, selectedToToken.decimals)} {selectedToToken.symbol}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Fee:</span>
|
||
<span className="font-medium">{formatUnits(actualSwapAmounts.lpFee + actualSwapAmounts.protocolFee, selectedFromToken.decimals)} {selectedFromToken.symbol}</span>
|
||
</div>
|
||
</>
|
||
) : (
|
||
// Fallback to estimates
|
||
<>
|
||
<p className="text-sm text-muted-foreground text-center">
|
||
Successfully swapped {fromAmount} {selectedFromToken?.symbol} to {toAmount} {selectedToToken?.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">Swap 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>
|
||
);
|
||
}
|