diff --git a/src/components/swap-form.tsx b/src/components/swap-form.tsx index 5dd5048..75bdef8 100644 --- a/src/components/swap-form.tsx +++ b/src/components/swap-form.tsx @@ -10,6 +10,7 @@ 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(); @@ -23,6 +24,7 @@ export function SwapForm() { 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); @@ -55,7 +57,7 @@ export function SwapForm() { ); // Initialize swap hook - const { executeSwap, isSwapping } = useSwap(); + const { executeSwap, estimateGas, isSwapping, gasEstimate, isEstimatingGas } = useSwap(); // Update "You Receive" amount when swap calculation completes useEffect(() => { @@ -124,6 +126,20 @@ export function SwapForm() { 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 ( @@ -310,19 +326,52 @@ export function SwapForm() { - {/* Swap Button */} + {/* 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} + />
); } diff --git a/src/components/swap-review-modal.tsx b/src/components/swap-review-modal.tsx new file mode 100644 index 0000000..6a4aec9 --- /dev/null +++ b/src/components/swap-review-modal.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { ArrowDown, X } from 'lucide-react'; +import type { TokenDetails } from '@/hooks/usePartyPlanner'; +import type { GasEstimate } from '@/hooks/usePartyPool'; + +interface SwapReviewModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + fromToken: TokenDetails | null; + toToken: TokenDetails | null; + fromAmount: string; + toAmount: string; + slippage: number; + gasEstimate: GasEstimate | null; + onConfirm: () => void; + isSwapping: boolean; +} + +export function SwapReviewModal({ + open, + onOpenChange, + fromToken, + toToken, + fromAmount, + toAmount, + slippage, + gasEstimate, + onConfirm, + isSwapping, +}: SwapReviewModalProps) { + if (!open) return null; + + return ( + <> + {/* Backdrop */} +
onOpenChange(false)} + /> + + {/* Modal */} +
+
+ {/* Header */} +
+

Review Swap

+ +
+ +
+ {/* From Token */} +
+
+
You pay
+
{fromAmount}
+
+
{fromToken?.symbol}
+
+ + {/* Arrow */} +
+ +
+ + {/* To Token */} +
+
+
You receive
+
{toAmount}
+
+
{toToken?.symbol}
+
+ + {/* Details */} +
+
+ Slippage Tolerance + {slippage}% +
+ + {gasEstimate && ( +
+ Network Fee + ${gasEstimate.estimatedCostUsd} +
+ )} +
+ + {/* Confirm Button */} + +
+
+
+ + ); +} \ No newline at end of file diff --git a/src/hooks/usePartyPlanner.ts b/src/hooks/usePartyPlanner.ts index 6bfd3b6..158120b 100644 --- a/src/hooks/usePartyPlanner.ts +++ b/src/hooks/usePartyPlanner.ts @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { usePublicClient } from 'wagmi'; -import chainInfo from '@/app/liquidity-party.json'; +import chainInfo from '../../../lmsr-amm/liqp-deployments.json'; import IPartyPlannerABI from '@/contracts/IPartyPlannerABI'; import IPartyPoolABI from '@/contracts/IPartyPoolABI'; import { ERC20ABI } from '@/contracts/ERC20ABI'; @@ -34,7 +34,7 @@ export function useGetAllTokens(offset: number = 0, limit: number = 100) { // Get chain ID and contract address const chainId = await publicClient.getChainId(); - const address = (chainInfo as Record)[chainId.toString()]?.IPartyPlanner; + const address = (chainInfo as Record)[chainId.toString()]?.v1?.PartyPlanner; if (!address) { setError('IPartyPlanner contract not found for current chain'); @@ -52,6 +52,7 @@ export function useGetAllTokens(offset: number = 0, limit: number = 100) { setTokens(result); } catch (err) { + console.error('Error calling getAllTokens:', err); setError(err instanceof Error ? err.message : 'Failed to fetch tokens'); } finally { setLoading(false); @@ -120,7 +121,7 @@ export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offs // Get chain ID and contract address const chainId = await publicClient.getChainId(); - const address = (chainInfo as Record)[chainId.toString()]?.IPartyPlanner; + const address = (chainInfo as Record)[chainId.toString()]?.v1?.PartyPlanner; if (!address) { setError('IPartyPlanner contract not found for current chain'); @@ -136,8 +137,6 @@ export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offs args: [tokenAddress, BigInt(offset), BigInt(limit)], }); - console.log('Pools for token', tokenAddress, ':', poolsResult); - // Get the symbol of the originally selected token const selectedTokenSymbol = await publicClient.readContract({ address: tokenAddress, @@ -216,7 +215,6 @@ export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offs } const availableTokensList = Array.from(tokenRoutesMap.values()); - console.log('Available tokens with swap routes:', availableTokensList); setAvailableTokens(availableTokensList); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch pools and tokens'); diff --git a/src/hooks/usePartyPool.ts b/src/hooks/usePartyPool.ts index 8fc7c3c..94c8a50 100644 --- a/src/hooks/usePartyPool.ts +++ b/src/hooks/usePartyPool.ts @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { usePublicClient, useWalletClient } from 'wagmi'; import IPartyPoolABI from '@/contracts/IPartyPoolABI'; import type { AvailableToken } from './usePartyPlanner'; @@ -181,12 +181,71 @@ export function useSwapAmounts( }; } +export interface GasEstimate { + totalGas: bigint; + gasPrice: bigint; + estimatedCostEth: string; + estimatedCostUsd: string; +} + export function useSwap() { const { data: walletClient } = useWalletClient(); const publicClient = usePublicClient(); const [isSwapping, setIsSwapping] = useState(false); const [swapHash, setSwapHash] = useState<`0x${string}` | null>(null); const [swapError, setSwapError] = useState(null); + const [gasEstimate, setGasEstimate] = useState(null); + const [isEstimatingGas, setIsEstimatingGas] = useState(false); + + const estimateGas = useCallback(async () => { + if (!publicClient) { + console.error('Public client not available'); + return null; + } + + try { + setIsEstimatingGas(true); + + // Get current gas price from the network + const gasPrice = await publicClient.getGasPrice(); + + // Use fixed, typical gas amounts for AMM operations + const approvalGas = 50000n; // ERC20 approval typically uses ~50k gas + const swapGas = 150000n; // AMM swap typically uses ~150k gas + + const totalGas = approvalGas + swapGas; + const estimatedCostWei = totalGas * gasPrice; + const estimatedCostEth = (Number(estimatedCostWei) / 1e18).toFixed(6); + + // Fetch ETH price in USD + let estimatedCostUsd = '0.00'; + try { + const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd'); + const data = await response.json(); + const ethPriceUsd = data.ethereum?.usd || 0; + const costInUsd = parseFloat(estimatedCostEth) * ethPriceUsd; + estimatedCostUsd = costInUsd.toFixed(2); + } catch (priceErr) { + console.error('Error fetching ETH price:', priceErr); + } + + const estimate: GasEstimate = { + totalGas, + gasPrice, + estimatedCostEth, + estimatedCostUsd, + }; + + setGasEstimate(estimate); + console.log('⛽ Gas estimate:', estimate); + return estimate; + } catch (err) { + console.error('Error estimating gas:', err); + return null; + } finally { + setIsEstimatingGas(false); + } + }, [publicClient]); const executeSwap = async ( poolAddress: `0x${string}`, @@ -290,8 +349,11 @@ export function useSwap() { return { executeSwap, + estimateGas, isSwapping, swapHash, swapError, + gasEstimate, + isEstimatingGas, }; } \ No newline at end of file