swap functionality
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user