Exact Amount Swaps Guide

Overview

JAMM DEX supports two exact amount swap modes: Exact Input and Exact Output. These modes meet different trading needs, allowing users to precisely control either the input or output amount of swaps.

Exact Input Swaps

Basic Concept

Exact input swaps allow users to specify the exact input amount, and the system calculates and returns the corresponding output amount. This is the most commonly used swap mode.

Features:

  • Users know exactly how many tokens they will pay

  • Output amount is calculated based on current market price

  • Requires setting minimum output amount for slippage protection

Implementation

Token to Token Exact Input

async function swapExactTokensForTokens(
    tokenIn,
    tokenOut,
    amountIn,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    // 1. Query expected output
    const path = [tokenIn, tokenOut];
    const fees = [100]; // 1% fee rate
    const amounts = await router.getAmountsOut(amountIn, path, fees);
    const expectedOutput = amounts[1];
    
    // 2. Calculate minimum output (slippage protection)
    const slippageBps = Math.floor(slippagePercent * 100); // Convert to basis points
    const amountOutMin = expectedOutput.mul(10000 - slippageBps).div(10000);
    
    // 3. Set deadline
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes
    
    // 4. Execute swap
    const tx = await router.swapExactTokensForTokens(
        amountIn,
        amountOutMin,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero, // No referrer
        deadline
    );
    
    console.log("Exact input swap submitted:", tx.hash);
    const receipt = await tx.wait();
    
    return {
        transactionHash: tx.hash,
        blockNumber: receipt.blockNumber,
        expectedOutput,
        actualOutput: await getActualOutput(receipt) // Parse actual output from events
    };
}

JU to Token Exact Input

async function swapExactJUForTokens(
    tokenOut,
    juAmount,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    const path = [WJU_ADDRESS, tokenOut];
    const fees = [100];
    
    // Query expected output
    const amounts = await router.getAmountsOut(juAmount, path, fees);
    const expectedOutput = amounts[1];
    
    // Calculate minimum output
    const slippageBps = Math.floor(slippagePercent * 100);
    const amountOutMin = expectedOutput.mul(10000 - slippageBps).div(10000);
    
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    const tx = await router.swapExactETHForTokens(
        amountOutMin,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline,
        { value: juAmount } // Send JU
    );
    
    return tx;
}

// Usage example
await swapExactJUForTokens(
    TOKEN_ADDRESS,
    ethers.utils.parseEther("1.0"), // Exact input 1 JU
    1, // 1% slippage
    userAddress,
    signer
);

Token to JU Exact Input

async function swapExactTokensForJU(
    tokenIn,
    amountIn,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    const path = [tokenIn, WJU_ADDRESS];
    const fees = [100];
    
    const amounts = await router.getAmountsOut(amountIn, path, fees);
    const expectedOutput = amounts[1];
    
    const slippageBps = Math.floor(slippagePercent * 100);
    const amountOutMin = expectedOutput.mul(10000 - slippageBps).div(10000);
    
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    const tx = await router.swapExactTokensForETH(
        amountIn,
        amountOutMin,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline
    );
    
    return tx;
}

Exact Output Swaps

Basic Concept

Exact output swaps allow users to specify the exact output amount, and the system calculates and requires the corresponding input amount. This mode is suitable for scenarios where you need to receive a precise amount of tokens.

Features:

  • Users know exactly how many tokens they will receive

  • Input amount is calculated based on current market price

  • Requires setting maximum input amount for slippage protection

Implementation

Token to Token Exact Output

async function swapTokensForExactTokens(
    tokenIn,
    tokenOut,
    amountOut,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    // 1. Query required input
    const path = [tokenIn, tokenOut];
    const fees = [100];
    const amounts = await router.getAmountsIn(amountOut, path, fees);
    const requiredInput = amounts[0];
    
    // 2. Calculate maximum input (slippage protection)
    const slippageBps = Math.floor(slippagePercent * 100);
    const amountInMax = requiredInput.mul(10000 + slippageBps).div(10000);
    
    // 3. Check user balance
    const tokenContract = new ethers.Contract(tokenIn, erc20ABI, signer);
    const userBalance = await tokenContract.balanceOf(userAddress);
    
    if (userBalance.lt(amountInMax)) {
        throw new Error(`Insufficient balance. Need: ${ethers.utils.formatEther(amountInMax)}, Have: ${ethers.utils.formatEther(userBalance)}`);
    }
    
    // 4. Check allowance
    const allowance = await tokenContract.allowance(userAddress, ROUTER_ADDRESS);
    if (allowance.lt(amountInMax)) {
        console.log("Need to increase allowance...");
        const approveTx = await tokenContract.approve(ROUTER_ADDRESS, amountInMax);
        await approveTx.wait();
    }
    
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    // 5. Execute swap
    const tx = await router.swapTokensForExactTokens(
        amountOut,
        amountInMax,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline
    );
    
    console.log("Exact output swap submitted:", tx.hash);
    const receipt = await tx.wait();
    
    return {
        transactionHash: tx.hash,
        blockNumber: receipt.blockNumber,
        requiredInput,
        actualInput: await getActualInput(receipt) // Parse actual input from events
    };
}

JU to Token Exact Output

async function swapJUForExactTokens(
    tokenOut,
    amountOut,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    const path = [WJU_ADDRESS, tokenOut];
    const fees = [100];
    
    // Query required JU amount
    const amounts = await router.getAmountsIn(amountOut, path, fees);
    const requiredJU = amounts[0];
    
    // Calculate maximum JU input (including slippage)
    const slippageBps = Math.floor(slippagePercent * 100);
    const maxJUInput = requiredJU.mul(10000 + slippageBps).div(10000);
    
    // Check JU balance
    const juBalance = await signer.getBalance();
    if (juBalance.lt(maxJUInput)) {
        throw new Error(`Insufficient JU balance. Need: ${ethers.utils.formatEther(maxJUInput)}, Have: ${ethers.utils.formatEther(juBalance)}`);
    }
    
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    const tx = await router.swapETHForExactTokens(
        amountOut,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline,
        { value: maxJUInput } // Send maximum JU amount
    );
    
    return tx;
}

// Usage example
await swapJUForExactTokens(
    TOKEN_ADDRESS,
    ethers.utils.parseEther("100"), // Exact output 100 tokens
    2, // 2% slippage
    userAddress,
    signer
);

Token to JU Exact Output

async function swapTokensForExactJU(
    tokenIn,
    juAmountOut,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    const path = [tokenIn, WJU_ADDRESS];
    const fees = [100];
    
    const amounts = await router.getAmountsIn(juAmountOut, path, fees);
    const requiredInput = amounts[0];
    
    const slippageBps = Math.floor(slippagePercent * 100);
    const amountInMax = requiredInput.mul(10000 + slippageBps).div(10000);
    
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    const tx = await router.swapTokensForExactETH(
        juAmountOut,
        amountInMax,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline
    );
    
    return tx;
}

Advanced Features

Multi-Hop Exact Swaps

async function multiHopExactInput(
    path,
    fees,
    amountIn,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    // Validate path and fees arrays
    if (path.length - fees.length !== 1) {
        throw new Error("Path and fees array length mismatch");
    }
    
    // Calculate multi-hop output
    const amounts = await router.getAmountsOut(amountIn, path, fees);
    const expectedOutput = amounts[amounts.length - 1];
    
    const slippageBps = Math.floor(slippagePercent * 100);
    const amountOutMin = expectedOutput.mul(10000 - slippageBps).div(10000);
    
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    const tx = await router.swapExactTokensForTokens(
        amountIn,
        amountOutMin,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline
    );
    
    return tx;
}

async function multiHopExactOutput(
    path,
    fees,
    amountOut,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    // Calculate multi-hop input
    const amounts = await router.getAmountsIn(amountOut, path, fees);
    const requiredInput = amounts[0];
    
    const slippageBps = Math.floor(slippagePercent * 100);
    const amountInMax = requiredInput.mul(10000 + slippageBps).div(10000);
    
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    const tx = await router.swapTokensForExactTokens(
        amountOut,
        amountInMax,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline
    );
    
    return tx;
}

// Usage example: USDC → WJU → TokenX
const path = [USDC_ADDRESS, WJU_ADDRESS, TOKENX_ADDRESS];
const fees = [50, 100]; // 0.5% and 1% fee rates

// Exact input 100 USDC
await multiHopExactInput(
    path,
    fees,
    ethers.utils.parseUnits("100", 6), // 100 USDC
    1, // 1% slippage
    userAddress,
    signer
);

// Exact output 100 TokenX
await multiHopExactOutput(
    path,
    fees,
    ethers.utils.parseEther("100"), // 100 TokenX
    2, // 2% slippage
    userAddress,
    signer
);

Batch Exact Swaps

class BatchExactSwapper {
    constructor(routerAddress, signer) {
        this.router = new ethers.Contract(routerAddress, routerABI, signer);
        this.signer = signer;
    }
    
    async batchExactInput(swaps) {
        const results = [];
        
        for (const swap of swaps) {
            try {
                const result = await this.executeExactInput(swap);
                results.push({ success: true, ...result });
            } catch (error) {
                results.push({ 
                    success: false, 
                    error: error.message,
                    swap: swap
                });
            }
        }
        
        return results;
    }
    
    async executeExactInput(swap) {
        const { tokenIn, tokenOut, amountIn, slippage } = swap;
        
        const path = [tokenIn, tokenOut];
        const fees = [100];
        
        const amounts = await this.router.getAmountsOut(amountIn, path, fees);
        const expectedOutput = amounts[1];
        
        const slippageBps = Math.floor(slippage * 100);
        const amountOutMin = expectedOutput.mul(10000 - slippageBps).div(10000);
        
        const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
        
        const tx = await this.router.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            path,
            fees,
            await this.signer.getAddress(),
            ethers.constants.AddressZero,
            deadline
        );
        
        return {
            transactionHash: tx.hash,
            expectedOutput,
            path,
            fees
        };
    }
}

// Usage example
const batchSwapper = new BatchExactSwapper(ROUTER_ADDRESS, signer);

const swaps = [
    {
        tokenIn: TOKEN_A_ADDRESS,
        tokenOut: TOKEN_B_ADDRESS,
        amountIn: ethers.utils.parseEther("10"),
        slippage: 1
    },
    {
        tokenIn: TOKEN_C_ADDRESS,
        tokenOut: TOKEN_D_ADDRESS,
        amountIn: ethers.utils.parseEther("5"),
        slippage: 1.5
    }
];

const results = await batchSwapper.batchExactInput(swaps);
console.log("Batch swap results:", results);

Price Calculation and Preview

Exact Input Price Preview

async function previewExactInput(tokenIn, tokenOut, amountIn) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
    
    const path = [tokenIn, tokenOut];
    const fees = [100];
    
    try {
        const amounts = await router.getAmountsOut(amountIn, path, fees);
        const outputAmount = amounts[1];
        
        // Calculate price
        const price = outputAmount.mul(ethers.utils.parseEther("1")).div(amountIn);
        
        // Calculate price impact
        const pair = await getPairContract(tokenIn, tokenOut, fees[0]);
        const reserves = await pair.getReserves();
        const priceImpact = calculatePriceImpact(amountIn, reserves, tokenIn, tokenOut);
        
        return {
            inputAmount: amountIn,
            outputAmount: outputAmount,
            price: price,
            priceImpact: priceImpact,
            path: path,
            fees: fees
        };
    } catch (error) {
        throw new Error(`Price preview failed: ${error.message}`);
    }
}

Exact Output Price Preview

async function previewExactOutput(tokenIn, tokenOut, amountOut) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
    
    const path = [tokenIn, tokenOut];
    const fees = [100];
    
    try {
        const amounts = await router.getAmountsIn(amountOut, path, fees);
        const inputAmount = amounts[0];
        
        const price = amountOut.mul(ethers.utils.parseEther("1")).div(inputAmount);
        
        const pair = await getPairContract(tokenIn, tokenOut, fees[0]);
        const reserves = await pair.getReserves();
        const priceImpact = calculatePriceImpact(inputAmount, reserves, tokenIn, tokenOut);
        
        return {
            inputAmount: inputAmount,
            outputAmount: amountOut,
            price: price,
            priceImpact: priceImpact,
            path: path,
            fees: fees
        };
    } catch (error) {
        throw new Error(`Price preview failed: ${error.message}`);
    }
}

Price Impact Calculation

function calculatePriceImpact(amountIn, reserves, tokenIn, tokenOut) {
    const [reserveIn, reserveOut] = tokenIn < tokenOut 
        ? [reserves.reserve0, reserves.reserve1]
        : [reserves.reserve1, reserves.reserve0];
    
    // Price before swap
    const priceBefore = reserveOut.mul(ethers.utils.parseEther("1")).div(reserveIn);
    
    // Simulate reserves after swap
    const amountInWithFee = amountIn.mul(9900); // Assume 1% fee
    const numerator = amountInWithFee.mul(reserveOut);
    const denominator = reserveIn.mul(10000).add(amountInWithFee);
    const amountOut = numerator.div(denominator);
    
    const newReserveIn = reserveIn.add(amountIn);
    const newReserveOut = reserveOut.sub(amountOut);
    
    // Price after swap
    const priceAfter = newReserveOut.mul(ethers.utils.parseEther("1")).div(newReserveIn);
    
    // Price impact = (price before - price after) / price before
    const priceImpact = priceBefore.sub(priceAfter).mul(10000).div(priceBefore);
    
    return priceImpact; // Returns basis points
}

Utility Functions

Swap Result Parsing

async function parseSwapResult(transactionReceipt) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
    
    const swapEvents = transactionReceipt.logs
        .map(log => {
            try {
                return router.interface.parseLog(log);
            } catch {
                return null;
            }
        })
        .filter(event => event && event.name === 'Swap');
    
    if (swapEvents.length === 0) {
        throw new Error("No swap events found");
    }
    
    // Parse first and last swap events
    const firstSwap = swapEvents[0];
    const lastSwap = swapEvents[swapEvents.length - 1];
    
    return {
        inputAmount: firstSwap.args.amount0In.gt(0) ? firstSwap.args.amount0In : firstSwap.args.amount1In,
        outputAmount: lastSwap.args.amount0Out.gt(0) ? lastSwap.args.amount0Out : lastSwap.args.amount1Out,
        swapCount: swapEvents.length,
        gasUsed: transactionReceipt.gasUsed
    };
}

Slippage Calculator

class SlippageCalculator {
    static calculateMinOutput(expectedOutput, slippagePercent) {
        const slippageBps = Math.floor(slippagePercent * 100);
        return expectedOutput.mul(10000 - slippageBps).div(10000);
    }
    
    static calculateMaxInput(requiredInput, slippagePercent) {
        const slippageBps = Math.floor(slippagePercent * 100);
        return requiredInput.mul(10000 + slippageBps).div(10000);
    }
    
    static calculateActualSlippage(expected, actual, isInput = false) {
        if (isInput) {
            // For input, actual should be less than or equal to expected
            if (actual.gt(expected)) {
                return ethers.BigNumber.from(0); // No slippage, better than expected
            }
            return expected.sub(actual).mul(10000).div(expected);
        } else {
            // For output, actual should be greater than or equal to expected
            if (actual.gt(expected)) {
                return ethers.BigNumber.from(0); // No slippage, better than expected
            }
            return expected.sub(actual).mul(10000).div(expected);
        }
    }
}

Best Practices

1. Swap Mode Selection

function chooseSwapMode(userIntent, marketConditions) {
    if (userIntent.type === 'SPEND_EXACT') {
        // User wants to spend exact amount of tokens
        return 'EXACT_INPUT';
    } else if (userIntent.type === 'RECEIVE_EXACT') {
        // User wants to receive exact amount of tokens
        return 'EXACT_OUTPUT';
    } else if (marketConditions.volatility === 'HIGH') {
        // High volatility market, exact input is safer
        return 'EXACT_INPUT';
    } else {
        // Default to exact input
        return 'EXACT_INPUT';
    }
}

2. Dynamic Slippage Adjustment

function calculateDynamicSlippage(priceImpact, marketVolatility, tradeSize) {
    let baseSlippage = 0.5; // 0.5% base slippage
    
    // Adjust based on price impact
    if (priceImpact > 200) { // Greater than 2%
        baseSlippage += 1.0;
    } else if (priceImpact > 100) { // Greater than 1%
        baseSlippage += 0.5;
    }
    
    // Adjust based on market volatility
    baseSlippage += marketVolatility * 0.5;
    
    // Adjust based on trade size
    if (tradeSize === 'LARGE') {
        baseSlippage += 0.5;
    }
    
    return Math.min(baseSlippage, 5.0); // Maximum 5% slippage
}

3. Pre-Swap Validation

async function validateSwap(swapParams) {
    const validations = [];
    
    // Validate token addresses
    if (!ethers.utils.isAddress(swapParams.tokenIn) || !ethers.utils.isAddress(swapParams.tokenOut)) {
        validations.push("Invalid token addresses");
    }
    
    // Validate amounts
    if (swapParams.amount.lte(0)) {
        validations.push("Swap amount must be greater than 0");
    }
    
    // Validate slippage
    if (swapParams.slippage < 0 || swapParams.slippage > 50) {
        validations.push("Slippage must be between 0-50%");
    }
    
    // Validate deadline
    if (swapParams.deadline <= Math.floor(Date.now() / 1000)) {
        validations.push("Deadline has expired");
    }
    
    // Validate pair exists
    const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
    const pairAddress = await factory.getPair(swapParams.tokenIn, swapParams.tokenOut, swapParams.fee);
    if (pairAddress === ethers.constants.AddressZero) {
        validations.push("Trading pair does not exist");
    }
    
    return validations;
}

Summary

Exact amount swaps are a core feature of JAMM DEX. This guide covers:

  1. Exact Input Swaps: Specify input amount, calculate output amount

  2. Exact Output Swaps: Specify output amount, calculate input amount

  3. JU Token Swaps: Special handling for native token swaps

  4. Multi-Hop Swaps: Exact amount swaps through complex paths

  5. Price Preview: Price calculation and impact analysis before swaps

  6. Best Practices: Mode selection, slippage management, validation mechanisms

By understanding and correctly using these two swap modes, developers can provide users with more precise and flexible trading experiences.