swap functionality
This commit is contained in:
@@ -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<SwapAmountResult[] | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const publicClient = usePublicClient();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [swapAmounts, setSwapAmounts] = useState<SwapAmountResult[] | null>(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<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);
|
||||
}
|
||||
};
|
||||
|
||||
calculateSwapAmounts();
|
||||
}, [publicClient, mounted, availableTokens, fromAmount, fromTokenDecimals]);
|
||||
|
||||
return {
|
||||
swapAmounts,
|
||||
loading,
|
||||
};
|
||||
return {
|
||||
executeSwap,
|
||||
isSwapping,
|
||||
swapHash,
|
||||
swapError,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user