From f543b2762074f89e9a71af90d4eb5a9142af3480 Mon Sep 17 00:00:00 2001 From: surbhi Date: Thu, 16 Oct 2025 16:56:59 -0400 Subject: [PATCH] swap functionality --- src/components/swap-form.tsx | 94 +++++---- src/hooks/usePartyPool.ts | 374 ++++++++++++++++++++++++++--------- 2 files changed, 331 insertions(+), 137 deletions(-) diff --git a/src/components/swap-form.tsx b/src/components/swap-form.tsx index 93febb7..5dd5048 100644 --- a/src/components/swap-form.tsx +++ b/src/components/swap-form.tsx @@ -7,9 +7,9 @@ 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, type AvailableToken } from '@/hooks/usePartyPlanner'; -import { useSwapAmounts } from '@/hooks/usePartyPool'; -import { formatUnits } from 'viem'; +import { useTokenDetails, useGetPoolsByToken, type TokenDetails } from '@/hooks/usePartyPlanner'; +import { useSwapAmounts, useSwap, selectBestSwapRoute } from '@/hooks/usePartyPool'; +import { formatUnits, parseUnits } from 'viem'; export function SwapForm() { const { t } = useTranslation(); @@ -27,7 +27,7 @@ export function SwapForm() { const toDropdownRef = useRef(null); // Use the custom hook to get all token details with a single batched RPC call - const { tokenDetails, loading, error } = useTokenDetails(address); + const { tokenDetails, loading } = useTokenDetails(address); // Get available tokens for the selected "from" token const { availableTokens } = useGetPoolsByToken(selectedFromToken?.address); @@ -43,26 +43,19 @@ export function SwapForm() { 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 + selectedFromToken?.decimals || 18, + currentSlippage ); - // Trigger the hook to fetch data when address is available - useEffect(() => { - if (tokenDetails) { - // console.log('Token details loaded in swap-form'); - } - }, [tokenDetails]); - - // Log swap amounts only once when user selects the "to" token - useEffect(() => { - if (swapAmounts && swapAmounts.length > 0 && selectedFromToken && selectedToToken) { - console.log('Swap amounts for', selectedFromToken.symbol, '→', selectedToToken.symbol, ':', swapAmounts); - } - }, [selectedToToken]); // Only fires when selectedToToken changes + // Initialize swap hook + const { executeSwap, isSwapping } = useSwap(); // Update "You Receive" amount when swap calculation completes useEffect(() => { @@ -88,22 +81,41 @@ export function SwapForm() { return () => document.removeEventListener('mousedown', handleClickOutside); }, []); - // Calculate and log limit price when amount or slippage changes - useEffect(() => { - if (fromAmount && parseFloat(fromAmount) > 0) { - const amount = parseFloat(fromAmount); - const slippagePercent = isCustomSlippage ? parseFloat(customSlippage) || 0 : slippage; - const limitPrice = amount * (1 + slippagePercent / 100); - console.log('Limit Price:', limitPrice); - console.log('From Amount:', amount); - console.log('Slippage %:', slippagePercent); - console.log('Additional Amount from Slippage:', limitPrice - amount); + const handleSwap = async () => { + if (!swapAmounts || swapAmounts.length === 0) { + console.error('No swap amounts available'); + return; } - }, [fromAmount, slippage, customSlippage, isCustomSlippage]); - const handleSwap = () => { - // Swap logic will be implemented later - console.log('Swap clicked'); + 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 = () => { @@ -153,16 +165,13 @@ export function SwapForm() { tokenDetails.map((token) => ( )) ) : ( @@ -232,16 +241,13 @@ export function SwapForm() { .map((token) => ( )) ) : selectedFromToken ? ( @@ -308,9 +314,13 @@ export function SwapForm() { diff --git a/src/hooks/usePartyPool.ts b/src/hooks/usePartyPool.ts index d509fbc..8fc7c3c 100644 --- a/src/hooks/usePartyPool.ts +++ b/src/hooks/usePartyPool.ts @@ -1,113 +1,297 @@ 'use client'; import { useState, useEffect } from 'react'; -import { usePublicClient } from 'wagmi'; +import { usePublicClient, useWalletClient } from 'wagmi'; import IPartyPoolABI from '@/contracts/IPartyPoolABI'; import type { AvailableToken } from './usePartyPlanner'; +// Q96 constant for price calculations +const Q96 = 1n << 96n; + export interface SwapAmountResult { - tokenAddress: `0x${string}`; - tokenSymbol: string; - amountIn: bigint; - amountOut: bigint; - fee: bigint; - poolAddress: `0x${string}`; + tokenAddress: `0x${string}`; + tokenSymbol: string; + amountIn: bigint; + amountOut: bigint; + fee: bigint; + poolAddress: `0x${string}`; + kappa: bigint; + inputTokenIndex: number; + outputTokenIndex: number; +} + +/** + * Selects the best swap route from an array of routes + * Primary criterion: lowest fee + * Secondary criterion: highest kappa (if fees are equal) + */ +export function selectBestSwapRoute(routes: SwapAmountResult[]): SwapAmountResult | null { + console.log('selectBestSwapRoute called with', routes.length, 'routes'); + + if (routes.length === 0) { + console.log('No routes available'); + return null; + } + + console.log('All routes:', routes.map(r => ({ + token: r.tokenSymbol, + pool: r.poolAddress, + fee: r.fee.toString(), + kappa: r.kappa.toString(), + amountOut: r.amountOut.toString(), + }))); + + const bestRoute = routes.reduce((best, current) => { + // Primary: lowest fee + if (current.fee < best.fee) return current; + if (current.fee > best.fee) return best; + + // Secondary: if fees are equal, highest kappa + if (current.kappa > best.kappa) return current; + return best; + }); + + console.log('Selected best route:', { + token: bestRoute.tokenSymbol, + pool: bestRoute.poolAddress, + fee: bestRoute.fee.toString(), + kappa: bestRoute.kappa.toString(), + amountOut: bestRoute.amountOut.toString(), + }); + + return bestRoute; } export function useSwapAmounts( - availableTokens: AvailableToken[] | null, - fromAmount: string, - fromTokenDecimals: number + availableTokens: AvailableToken[] | null, + fromAmount: string, + fromTokenDecimals: number, + slippagePercent: number ) { - const publicClient = usePublicClient(); - const [mounted, setMounted] = useState(false); - const [swapAmounts, setSwapAmounts] = useState(null); - const [loading, setLoading] = useState(false); + const publicClient = usePublicClient(); + const [mounted, setMounted] = useState(false); + const [swapAmounts, setSwapAmounts] = useState(null); + const [loading, setLoading] = useState(false); - useEffect(() => { - setMounted(true); - }, []); + useEffect(() => { + setMounted(true); + }, []); - useEffect(() => { - if (!mounted || !availableTokens || !fromAmount || parseFloat(fromAmount) <= 0) { - setSwapAmounts(null); - setLoading(false); - return; - } - - const calculateSwapAmounts = async () => { - if (!publicClient) { - setLoading(false); - return; - } - - try { - setLoading(true); - - // Parse the from amount to the token's decimals - const amountInWei = BigInt(Math.floor(parseFloat(fromAmount) * Math.pow(10, fromTokenDecimals))); - - // Use a very large limit price (essentially no limit) - user will replace later - // int128 max is 2^127 - 1, but we'll use a reasonable large number - const limitPrice = BigInt('170141183460469231731687303715884105727'); // max int128 - - const results: SwapAmountResult[] = []; - - // Calculate swap amounts for each available token using their first swap route - for (const token of availableTokens) { - if (token.swapRoutes.length === 0) continue; - - // Use the first swap route for now - const route = token.swapRoutes[0]; - - try { - const swapResult = await publicClient.readContract({ - address: route.poolAddress, - abi: IPartyPoolABI, - functionName: 'swapAmounts', - args: [ - BigInt(route.inputTokenIndex), - BigInt(route.outputTokenIndex), - amountInWei, - limitPrice, - ], - }) as readonly [bigint, bigint, bigint]; - - const [amountIn, amountOut, fee] = swapResult; - - results.push({ - tokenAddress: token.address, - tokenSymbol: token.symbol, - amountIn, - amountOut, - fee, - poolAddress: route.poolAddress, - }); - - console.log(`Swap ${token.symbol}:`, { - amountIn: amountIn.toString(), - amountOut: amountOut.toString(), - fee: fee.toString(), - pool: route.poolAddress, - }); - } catch (err) { - console.error(`Error calculating swap for ${token.symbol}:`, err); - } + useEffect(() => { + if (!mounted || !availableTokens || !fromAmount || parseFloat(fromAmount) <= 0) { + setSwapAmounts(null); + setLoading(false); + return; } - setSwapAmounts(results); - } catch (err) { - console.error('Error calculating swap amounts:', err); - } finally { - setLoading(false); - } + const calculateSwapAmounts = async () => { + if (!publicClient) { + setLoading(false); + return; + } + + try { + setLoading(true); + + const amountInWei = BigInt(Math.floor(parseFloat(fromAmount) * Math.pow(10, fromTokenDecimals))); + + // Calculate limit price based on slippage tolerance + // limitPrice in Q96 format = Q96 * (100 + slippage%) / 100 + // This represents the maximum acceptable price ratio (1 + slippage%) + const slippageBasisPoints = BigInt(Math.floor(slippagePercent * 100)); // Convert to basis points (0.5% = 50) + const limitPrice = (Q96 * (10000n + slippageBasisPoints)) / 10000n; + + console.log('Limit Price Calculation:', { + slippagePercent, + slippageBasisPoints: slippageBasisPoints.toString(), + limitPriceQ96: limitPrice.toString(), + Q96: Q96.toString(), + }); + + const results: SwapAmountResult[] = []; + + // Calculate swap amounts for ALL routes of each token + for (const token of availableTokens) { + if (token.swapRoutes.length === 0) continue; + + const routeResults: SwapAmountResult[] = []; + + // Evaluate ALL routes for this token + for (const route of token.swapRoutes) { + try { + // Get swap amounts + const swapResult = await publicClient.readContract({ + address: route.poolAddress, + abi: IPartyPoolABI, + functionName: 'swapAmounts', + args: [ + BigInt(route.inputTokenIndex), + BigInt(route.outputTokenIndex), + amountInWei, + limitPrice, + ], + }) as readonly [bigint, bigint, bigint]; + + const [amountIn, amountOut, fee] = swapResult; + + // Get kappa for this pool + const kappa = await publicClient.readContract({ + address: route.poolAddress, + abi: IPartyPoolABI, + functionName: 'kappa', + }) as bigint; + + routeResults.push({ + tokenAddress: token.address, + tokenSymbol: token.symbol, + amountIn, + amountOut, + fee, + poolAddress: route.poolAddress, + kappa, + inputTokenIndex: route.inputTokenIndex, + outputTokenIndex: route.outputTokenIndex, + }); + } catch (err) { + console.error(`Error calculating swap for ${token.symbol} via ${route.poolAddress}:`, err); + } + } + + // Select the best route for this token using the shared helper + const bestRoute = selectBestSwapRoute(routeResults); + if (bestRoute) { + results.push(bestRoute); + } + } + + setSwapAmounts(results); + } catch (err) { + console.error('Error calculating swap amounts:', err); + } finally { + setLoading(false); + } + }; + + calculateSwapAmounts(); + }, [publicClient, mounted, availableTokens, fromAmount, fromTokenDecimals, slippagePercent]); + + return { + swapAmounts, + loading, + }; +} + +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 executeSwap = async ( + poolAddress: `0x${string}`, + inputTokenAddress: `0x${string}`, + inputTokenIndex: number, + outputTokenIndex: number, + maxAmountIn: bigint, + slippagePercent: number + ) => { + if (!walletClient || !publicClient) { + setSwapError('Wallet not connected'); + return; + } + + try { + setIsSwapping(true); + setSwapError(null); + setSwapHash(null); + + const userAddress = walletClient.account.address; + + // STEP 1: Approve the pool to spend the input token + console.log('🔐 Approving token spend...'); + console.log('Token to approve:', inputTokenAddress); + console.log('Spender (pool):', poolAddress); + console.log('Amount:', maxAmountIn.toString()); + + const approvalHash = await walletClient.writeContract({ + address: inputTokenAddress, + abi: [ + { + name: 'approve', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { name: 'spender', type: 'address' }, + { name: 'amount', type: 'uint256' } + ], + outputs: [{ name: '', type: 'bool' }] + } + ], + functionName: 'approve', + args: [poolAddress, maxAmountIn], + }); + + console.log('✅ Approval transaction submitted:', approvalHash); + await publicClient.waitForTransactionReceipt({ hash: approvalHash }); + console.log('✅ Approval confirmed'); + + // STEP 2: Calculate limit price and deadline + const slippageBasisPoints = BigInt(Math.floor(slippagePercent * 100)); + const limitPrice = (Q96 * (10000n + slippageBasisPoints)) / 10000n; + const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200); + + console.log('🚀 Executing swap with params:', { + pool: poolAddress, + payer: userAddress, + receiver: userAddress, + inputTokenIndex, + outputTokenIndex, + maxAmountIn: maxAmountIn.toString(), + limitPrice: limitPrice.toString(), + deadline: deadline.toString(), + unwrap: false, + }); + + // STEP 3: Execute the swap transaction + const hash = await walletClient.writeContract({ + address: poolAddress, + abi: IPartyPoolABI, + functionName: 'swap', + args: [ + userAddress, // payer + userAddress, // receiver + BigInt(inputTokenIndex), + BigInt(outputTokenIndex), + maxAmountIn, + limitPrice, + deadline, + false, // unwrap + ], + }); + + setSwapHash(hash); + console.log('✅ Swap transaction submitted:', hash); + + // Wait for transaction confirmation + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + console.log('✅ Swap transaction confirmed:', receipt); + + return receipt; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Swap failed'; + setSwapError(errorMessage); + console.error('❌ Swap error:', err); + throw err; + } finally { + setIsSwapping(false); + } }; - calculateSwapAmounts(); - }, [publicClient, mounted, availableTokens, fromAmount, fromTokenDecimals]); - - return { - swapAmounts, - loading, - }; + return { + executeSwap, + isSwapping, + swapHash, + swapError, + }; } \ No newline at end of file