diff --git a/src/app/liquidity-party.json b/src/app/liquidity-party.json index ce32b17..4f57d58 100644 --- a/src/app/liquidity-party.json +++ b/src/app/liquidity-party.json @@ -1,6 +1,6 @@ { "31337": { - "IPartyPlanner": "0x536F14E49e1Bb927003E83aDEBF295F3682ff121", + "IPartyPlanner": "0xFc18426b71EDa3dC001dcc36ADC9C67bC6f38747", "IPartyPoolViewer": "0xd85BdcdaE4db1FAEB8eF93331525FE68D7C8B3f0" } } \ No newline at end of file diff --git a/src/components/swap-form.tsx b/src/components/swap-form.tsx index 779e89f..93febb7 100644 --- a/src/components/swap-form.tsx +++ b/src/components/swap-form.tsx @@ -1,13 +1,14 @@ 'use client'; -import { useState, useEffect, useRef } from 'react'; +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 { useTokenDetails, useGetPoolsByToken, type TokenDetails, type AvailableToken } from '@/hooks/usePartyPlanner'; +import { useSwapAmounts } from '@/hooks/usePartyPool'; import { formatUnits } from 'viem'; export function SwapForm() { @@ -31,13 +32,47 @@ export function SwapForm() { // 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]); + + // Calculate swap amounts for the selected token pair only + const { swapAmounts } = useSwapAmounts( + filteredAvailableTokens, + fromAmount, + selectedFromToken?.decimals || 18 + ); + // Trigger the hook to fetch data when address is available useEffect(() => { if (tokenDetails) { - console.log('Token details loaded in swap-form'); + // 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 + + // 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) => { @@ -191,7 +226,7 @@ export function SwapForm() { tokenDetails .filter((token) => availableTokens.some((availToken) => - availToken.toLowerCase() === token.address.toLowerCase() + availToken.address.toLowerCase() === token.address.toLowerCase() ) ) .map((token) => ( diff --git a/src/contracts/ERC20ABI.ts b/src/contracts/ERC20ABI.ts new file mode 100644 index 0000000..ffc4656 --- /dev/null +++ b/src/contracts/ERC20ABI.ts @@ -0,0 +1,30 @@ +export const ERC20ABI = [ + { + type: 'function', + name: 'name', + stateMutability: 'view', + inputs: [], + outputs: [{ name: '', type: 'string' }], + }, + { + type: 'function', + name: 'symbol', + stateMutability: 'view', + inputs: [], + outputs: [{ name: '', type: 'string' }], + }, + { + type: 'function', + name: 'decimals', + stateMutability: 'view', + inputs: [], + outputs: [{ name: '', type: 'uint8' }], + }, + { + type: 'function', + name: 'balanceOf', + stateMutability: 'view', + inputs: [{ name: 'account', type: 'address' }], + outputs: [{ name: '', type: 'uint256' }], + }, +] as const; \ No newline at end of file diff --git a/src/hooks/usePartyPlanner.ts b/src/hooks/usePartyPlanner.ts index 83f51b4..6bfd3b6 100644 --- a/src/hooks/usePartyPlanner.ts +++ b/src/hooks/usePartyPlanner.ts @@ -78,10 +78,22 @@ export interface TokenDetails { index: number; } +export interface SwapRoute { + poolAddress: `0x${string}`; + inputTokenIndex: number; + outputTokenIndex: number; +} + +export interface AvailableToken { + address: `0x${string}`; + symbol: string; + swapRoutes: SwapRoute[]; +} + export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offset: number = 0, limit: number = 100) { const publicClient = usePublicClient(); const [mounted, setMounted] = useState(false); - const [availableTokens, setAvailableTokens] = useState<`0x${string}`[] | null>(null); + const [availableTokens, setAvailableTokens] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -138,8 +150,10 @@ export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offs return; } - // For each pool, fetch all tokens in that pool - const allTokensInPools: `0x${string}`[] = []; + // Map to store available tokens with their swap routes + const tokenRoutesMap = new Map(); + + // For each pool, fetch all tokens and track indices for (const poolAddress of poolsResult) { try { const tokensInPool = await publicClient.readContract({ @@ -148,37 +162,62 @@ export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offs functionName: 'allTokens', }) as readonly `0x${string}`[]; - // Add all tokens from this pool - allTokensInPools.push(...tokensInPool); + // Find the input token index in this pool + const inputTokenIndex = tokensInPool.findIndex( + (token) => token.toLowerCase() === tokenAddress.toLowerCase() + ); + + if (inputTokenIndex === -1) { + console.error('Input token not found in pool', poolAddress); + continue; + } + + // Process each token in the pool + for (let outputTokenIndex = 0; outputTokenIndex < tokensInPool.length; outputTokenIndex++) { + const outputTokenAddress = tokensInPool[outputTokenIndex]; + + // Skip if it's the same as the input token + if (outputTokenIndex === inputTokenIndex) { + continue; + } + + // Get the symbol of this token + const outputTokenSymbol = await publicClient.readContract({ + address: outputTokenAddress, + abi: ERC20ABI, + functionName: 'symbol', + }).catch(() => null); + + // Skip tokens with the same symbol as the selected token + if (!outputTokenSymbol || outputTokenSymbol === selectedTokenSymbol) { + continue; + } + + // Create or update the available token entry + const tokenKey = outputTokenAddress.toLowerCase(); + if (!tokenRoutesMap.has(tokenKey)) { + tokenRoutesMap.set(tokenKey, { + address: outputTokenAddress, + symbol: outputTokenSymbol, + swapRoutes: [], + }); + } + + // Add this swap route + tokenRoutesMap.get(tokenKey)!.swapRoutes.push({ + poolAddress, + inputTokenIndex, + outputTokenIndex, + }); + } } catch (err) { console.error('Error fetching tokens from pool', poolAddress, err); } } - // Remove duplicates by address - const uniqueTokenAddresses = Array.from(new Set(allTokensInPools)); - - // Fetch symbols for all tokens and filter out those matching the selected token's symbol - const filteredTokens: `0x${string}`[] = []; - for (const token of uniqueTokenAddresses) { - try { - const tokenSymbol = await publicClient.readContract({ - address: token, - abi: ERC20ABI, - functionName: 'symbol', - }).catch(() => null); - - // Only include tokens with different symbols - if (tokenSymbol && tokenSymbol !== selectedTokenSymbol) { - filteredTokens.push(token); - } - } catch (err) { - console.error('Error fetching symbol for token', token, err); - } - } - - console.log('Available tokens to swap to (excluding', selectedTokenSymbol, '):', filteredTokens); - setAvailableTokens(filteredTokens); + 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'); } finally { diff --git a/src/hooks/usePartyPool.ts b/src/hooks/usePartyPool.ts new file mode 100644 index 0000000..d509fbc --- /dev/null +++ b/src/hooks/usePartyPool.ts @@ -0,0 +1,113 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { usePublicClient } from 'wagmi'; +import IPartyPoolABI from '@/contracts/IPartyPoolABI'; +import type { AvailableToken } from './usePartyPlanner'; + +export interface SwapAmountResult { + tokenAddress: `0x${string}`; + tokenSymbol: string; + amountIn: bigint; + amountOut: bigint; + fee: bigint; + poolAddress: `0x${string}`; +} + +export function useSwapAmounts( + availableTokens: AvailableToken[] | null, + fromAmount: string, + fromTokenDecimals: number +) { + const publicClient = usePublicClient(); + const [mounted, setMounted] = useState(false); + const [swapAmounts, setSwapAmounts] = useState(null); + const [loading, setLoading] = useState(false); + + 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); + } + } + + setSwapAmounts(results); + } catch (err) { + console.error('Error calculating swap amounts:', err); + } finally { + setLoading(false); + } + }; + + calculateSwapAmounts(); + }, [publicClient, mounted, availableTokens, fromAmount, fromTokenDecimals]); + + return { + swapAmounts, + loading, + }; +} \ No newline at end of file