[wip] adding swap amount conversion to the swam-form

This commit is contained in:
2025-10-15 18:42:23 -04:00
parent e2198c9b31
commit 7ead103f86
5 changed files with 251 additions and 34 deletions

View File

@@ -1,6 +1,6 @@
{
"31337": {
"IPartyPlanner": "0x536F14E49e1Bb927003E83aDEBF295F3682ff121",
"IPartyPlanner": "0xFc18426b71EDa3dC001dcc36ADC9C67bC6f38747",
"IPartyPoolViewer": "0xd85BdcdaE4db1FAEB8eF93331525FE68D7C8B3f0"
}
}

View File

@@ -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) => (

30
src/contracts/ERC20ABI.ts Normal file
View File

@@ -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;

View File

@@ -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<AvailableToken[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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<string, AvailableToken>();
// 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 {

113
src/hooks/usePartyPool.ts Normal file
View File

@@ -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<SwapAmountResult[] | null>(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,
};
}