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:
Exact Input Swaps: Specify input amount, calculate output amount
Exact Output Swaps: Specify output amount, calculate input amount
JU Token Swaps: Special handling for native token swaps
Multi-Hop Swaps: Exact amount swaps through complex paths
Price Preview: Price calculation and impact analysis before swaps
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.