swap functionality

This commit is contained in:
2025-10-16 16:56:59 -04:00
parent 7ead103f86
commit f543b27620
2 changed files with 331 additions and 137 deletions

View File

@@ -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<HTMLDivElement>(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) => (
<button
key={token.address}
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none flex items-center justify-between"
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
onClick={() => {
setSelectedFromToken(token);
setIsFromDropdownOpen(false);
}}
>
<span className="font-medium">{token.symbol}</span>
<span className="text-sm text-muted-foreground">
{formatUnits(token.balance, token.decimals)}
</span>
</button>
))
) : (
@@ -232,16 +241,13 @@ export function SwapForm() {
.map((token) => (
<button
key={token.address}
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none flex items-center justify-between"
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
onClick={() => {
setSelectedToToken(token);
setIsToDropdownOpen(false);
}}
>
<span className="font-medium">{token.symbol}</span>
<span className="text-sm text-muted-foreground">
{formatUnits(token.balance, token.decimals)}
</span>
</button>
))
) : selectedFromToken ? (
@@ -308,9 +314,13 @@ export function SwapForm() {
<Button
className="w-full h-14 text-lg"
onClick={handleSwap}
disabled={!isConnected || !fromAmount || !toAmount}
disabled={!isConnected || !fromAmount || !toAmount || isSwapping}
>
{!isConnected ? t('swap.connectWalletToSwap') : t('swap.swapButton')}
{!isConnected
? t('swap.connectWalletToSwap')
: isSwapping
? 'Swapping...'
: t('swap.swapButton')}
</Button>
</CardContent>
</Card>

View File

@@ -1,10 +1,13 @@
'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;
@@ -12,12 +15,58 @@ export interface SwapAmountResult {
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
fromTokenDecimals: number,
slippagePercent: number
) {
const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false);
@@ -44,23 +93,33 @@ export function useSwapAmounts(
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
// 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 each available token using their first swap route
// Calculate swap amounts for ALL routes of each token
for (const token of availableTokens) {
if (token.swapRoutes.length === 0) continue;
// Use the first swap route for now
const route = token.swapRoutes[0];
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,
@@ -75,23 +134,33 @@ export function useSwapAmounts(
const [amountIn, amountOut, fee] = swapResult;
results.push({
// 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,
});
console.log(`Swap ${token.symbol}:`, {
amountIn: amountIn.toString(),
amountOut: amountOut.toString(),
fee: fee.toString(),
pool: route.poolAddress,
kappa,
inputTokenIndex: route.inputTokenIndex,
outputTokenIndex: route.outputTokenIndex,
});
} catch (err) {
console.error(`Error calculating swap for ${token.symbol}:`, 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);
}
}
@@ -104,10 +173,125 @@ export function useSwapAmounts(
};
calculateSwapAmounts();
}, [publicClient, mounted, availableTokens, fromAmount, fromTokenDecimals]);
}, [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<string | null>(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);
}
};
return {
executeSwap,
isSwapping,
swapHash,
swapError,
};
}