[wip] adding swap amount conversion to the swam-form
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"31337": {
|
"31337": {
|
||||||
"IPartyPlanner": "0x536F14E49e1Bb927003E83aDEBF295F3682ff121",
|
"IPartyPlanner": "0xFc18426b71EDa3dC001dcc36ADC9C67bC6f38747",
|
||||||
"IPartyPoolViewer": "0xd85BdcdaE4db1FAEB8eF93331525FE68D7C8B3f0"
|
"IPartyPoolViewer": "0xd85BdcdaE4db1FAEB8eF93331525FE68D7C8B3f0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ArrowDownUp, ChevronDown } from 'lucide-react';
|
import { ArrowDownUp, ChevronDown } from 'lucide-react';
|
||||||
import { useAccount } from 'wagmi';
|
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';
|
import { formatUnits } from 'viem';
|
||||||
|
|
||||||
export function SwapForm() {
|
export function SwapForm() {
|
||||||
@@ -31,13 +32,47 @@ export function SwapForm() {
|
|||||||
// Get available tokens for the selected "from" token
|
// Get available tokens for the selected "from" token
|
||||||
const { availableTokens } = useGetPoolsByToken(selectedFromToken?.address);
|
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
|
// Trigger the hook to fetch data when address is available
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tokenDetails) {
|
if (tokenDetails) {
|
||||||
console.log('Token details loaded in swap-form');
|
// console.log('Token details loaded in swap-form');
|
||||||
}
|
}
|
||||||
}, [tokenDetails]);
|
}, [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
|
// Close dropdowns when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
@@ -191,7 +226,7 @@ export function SwapForm() {
|
|||||||
tokenDetails
|
tokenDetails
|
||||||
.filter((token) =>
|
.filter((token) =>
|
||||||
availableTokens.some((availToken) =>
|
availableTokens.some((availToken) =>
|
||||||
availToken.toLowerCase() === token.address.toLowerCase()
|
availToken.address.toLowerCase() === token.address.toLowerCase()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.map((token) => (
|
.map((token) => (
|
||||||
|
|||||||
30
src/contracts/ERC20ABI.ts
Normal file
30
src/contracts/ERC20ABI.ts
Normal 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;
|
||||||
@@ -78,10 +78,22 @@ export interface TokenDetails {
|
|||||||
index: number;
|
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) {
|
export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offset: number = 0, limit: number = 100) {
|
||||||
const publicClient = usePublicClient();
|
const publicClient = usePublicClient();
|
||||||
const [mounted, setMounted] = useState(false);
|
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 [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -138,8 +150,10 @@ export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offs
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each pool, fetch all tokens in that pool
|
// Map to store available tokens with their swap routes
|
||||||
const allTokensInPools: `0x${string}`[] = [];
|
const tokenRoutesMap = new Map<string, AvailableToken>();
|
||||||
|
|
||||||
|
// For each pool, fetch all tokens and track indices
|
||||||
for (const poolAddress of poolsResult) {
|
for (const poolAddress of poolsResult) {
|
||||||
try {
|
try {
|
||||||
const tokensInPool = await publicClient.readContract({
|
const tokensInPool = await publicClient.readContract({
|
||||||
@@ -148,37 +162,62 @@ export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offs
|
|||||||
functionName: 'allTokens',
|
functionName: 'allTokens',
|
||||||
}) as readonly `0x${string}`[];
|
}) as readonly `0x${string}`[];
|
||||||
|
|
||||||
// Add all tokens from this pool
|
// Find the input token index in this pool
|
||||||
allTokensInPools.push(...tokensInPool);
|
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) {
|
} catch (err) {
|
||||||
console.error('Error fetching tokens from pool', poolAddress, err);
|
console.error('Error fetching tokens from pool', poolAddress, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove duplicates by address
|
const availableTokensList = Array.from(tokenRoutesMap.values());
|
||||||
const uniqueTokenAddresses = Array.from(new Set(allTokensInPools));
|
console.log('Available tokens with swap routes:', availableTokensList);
|
||||||
|
setAvailableTokens(availableTokensList);
|
||||||
// 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);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to fetch pools and tokens');
|
setError(err instanceof Error ? err.message : 'Failed to fetch pools and tokens');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
113
src/hooks/usePartyPool.ts
Normal file
113
src/hooks/usePartyPool.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user