精确数量交换

JAMM DEX支持两种精确数量交换模式:精确输入(Exact Input)和精确输出(Exact Output)。这两种模式满足不同的交易需求,让用户能够精确控制交换的输入或输出数量。

精确输入交换 (Exact Input)

基本概念

精确输入交换允许用户指定确切的输入数量,系统计算并返回相应的输出数量。这是最常用的交换模式。

特点

  • 用户确切知道要支付多少代币

  • 输出数量根据当前市场价格计算

  • 需要设置最小输出数量作为滑点保护

实现方式

代币到代币的精确输入

async function swapExactTokensForTokens(
    tokenIn,
    tokenOut,
    amountIn,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    // 1. 查询预期输出
    const path = [tokenIn, tokenOut];
    const fees = [100]; // 1%费率
    const amounts = await router.getAmountsOut(amountIn, path, fees);
    const expectedOutput = amounts[1];
    
    // 2. 计算最小输出(滑点保护)
    const slippageBps = Math.floor(slippagePercent * 100); // 转换为基点
    const amountOutMin = expectedOutput.mul(10000 - slippageBps).div(10000);
    
    // 3. 设置截止时间
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20分钟
    
    // 4. 执行交换
    const tx = await router.swapExactTokensForTokens(
        amountIn,
        amountOutMin,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero, // 无推荐人
        deadline
    );
    
    console.log("精确输入交换已提交:", tx.hash);
    const receipt = await tx.wait();
    
    return {
        transactionHash: tx.hash,
        blockNumber: receipt.blockNumber,
        expectedOutput,
        actualOutput: await getActualOutput(receipt) // 从事件中解析实际输出
    };
}

JU到代币的精确输入

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];
    
    // 查询预期输出
    const amounts = await router.getAmountsOut(juAmount, 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.swapExactETHForTokens(
        amountOutMin,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline,
        { value: juAmount } // 发送JU
    );
    
    return tx;
}

// 使用示例
await swapExactJUForTokens(
    TOKEN_ADDRESS,
    ethers.utils.parseEther("1.0"), // 精确输入1个JU
    1, // 1%滑点
    userAddress,
    signer
);

代币到JU的精确输入

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)

基本概念

精确输出交换允许用户指定确切的输出数量,系统计算并要求相应的输入数量。这种模式适合需要获得精确数量代币的场景。

特点

  • 用户确切知道要获得多少代币

  • 输入数量根据当前市场价格计算

  • 需要设置最大输入数量作为滑点保护

实现方式

代币到代币的精确输出

async function swapTokensForExactTokens(
    tokenIn,
    tokenOut,
    amountOut,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    // 1. 查询所需输入
    const path = [tokenIn, tokenOut];
    const fees = [100];
    const amounts = await router.getAmountsIn(amountOut, path, fees);
    const requiredInput = amounts[0];
    
    // 2. 计算最大输入(滑点保护)
    const slippageBps = Math.floor(slippagePercent * 100);
    const amountInMax = requiredInput.mul(10000 + slippageBps).div(10000);
    
    // 3. 检查用户余额
    const tokenContract = new ethers.Contract(tokenIn, erc20ABI, signer);
    const userBalance = await tokenContract.balanceOf(userAddress);
    
    if (userBalance.lt(amountInMax)) {
        throw new Error(`余额不足。需要: ${ethers.utils.formatEther(amountInMax)}, 拥有: ${ethers.utils.formatEther(userBalance)}`);
    }
    
    // 4. 检查授权
    const allowance = await tokenContract.allowance(userAddress, ROUTER_ADDRESS);
    if (allowance.lt(amountInMax)) {
        console.log("需要增加授权...");
        const approveTx = await tokenContract.approve(ROUTER_ADDRESS, amountInMax);
        await approveTx.wait();
    }
    
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    // 5. 执行交换
    const tx = await router.swapTokensForExactTokens(
        amountOut,
        amountInMax,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline
    );
    
    console.log("精确输出交换已提交:", tx.hash);
    const receipt = await tx.wait();
    
    return {
        transactionHash: tx.hash,
        blockNumber: receipt.blockNumber,
        requiredInput,
        actualInput: await getActualInput(receipt) // 从事件中解析实际输入
    };
}

JU到代币的精确输出

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];
    
    // 查询所需JU数量
    const amounts = await router.getAmountsIn(amountOut, path, fees);
    const requiredJU = amounts[0];
    
    // 计算最大JU输入(包含滑点)
    const slippageBps = Math.floor(slippagePercent * 100);
    const maxJUInput = requiredJU.mul(10000 + slippageBps).div(10000);
    
    // 检查JU余额
    const juBalance = await signer.getBalance();
    if (juBalance.lt(maxJUInput)) {
        throw new Error(`JU余额不足。需要: ${ethers.utils.formatEther(maxJUInput)}, 拥有: ${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 } // 发送最大JU数量
    );
    
    return tx;
}

// 使用示例
await swapJUForExactTokens(
    TOKEN_ADDRESS,
    ethers.utils.parseEther("100"), // 精确获得100个代币
    2, // 2%滑点
    userAddress,
    signer
);

代币到JU的精确输出

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;
}

高级功能

多跳精确交换

async function multiHopExactInput(
    path,
    fees,
    amountIn,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    // 验证路径和费率数组
    if (path.length - fees.length !== 1) {
        throw new Error("路径和费率数组长度不匹配");
    }
    
    // 计算多跳输出
    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);
    
    // 计算多跳输入
    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;
}

// 使用示例:USDC → WJU → TokenX
const path = [USDC_ADDRESS, WJU_ADDRESS, TOKENX_ADDRESS];
const fees = [50, 100]; // 0.5%和1%费率

// 精确输入100 USDC
await multiHopExactInput(
    path,
    fees,
    ethers.utils.parseUnits("100", 6), // 100 USDC
    1, // 1%滑点
    userAddress,
    signer
);

// 精确输出100 TokenX
await multiHopExactOutput(
    path,
    fees,
    ethers.utils.parseEther("100"), // 100 TokenX
    2, // 2%滑点
    userAddress,
    signer
);

批量精确交换

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
        };
    }
}

// 使用示例
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("批量交换结果:", results);

价格计算和预览

精确输入价格预览

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];
        
        // 计算价格
        const price = outputAmount.mul(ethers.utils.parseEther("1")).div(amountIn);
        
        // 计算价格影响
        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(`价格预览失败: ${error.message}`);
    }
}

精确输出价格预览

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(`价格预览失败: ${error.message}`);
    }
}

价格影响计算

function calculatePriceImpact(amountIn, reserves, tokenIn, tokenOut) {
    const [reserveIn, reserveOut] = tokenIn < tokenOut 
        ? [reserves.reserve0, reserves.reserve1]
        : [reserves.reserve1, reserves.reserve0];
    
    // 交换前价格
    const priceBefore = reserveOut.mul(ethers.utils.parseEther("1")).div(reserveIn);
    
    // 模拟交换后的储备量
    const amountInWithFee = amountIn.mul(9900); // 假设1%费率
    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);
    
    // 交换后价格
    const priceAfter = newReserveOut.mul(ethers.utils.parseEther("1")).div(newReserveIn);
    
    // 价格影响 = (交换前价格 - 交换后价格) / 交换前价格
    const priceImpact = priceBefore.sub(priceAfter).mul(10000).div(priceBefore);
    
    return priceImpact; // 返回基点
}

实用工具函数

交换结果解析

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("未找到交换事件");
    }
    
    // 解析第一个和最后一个交换事件
    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
    };
}

滑点计算器

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) {
            // 对于输入,实际值应该小于等于预期值
            if (actual.gt(expected)) {
                return ethers.BigNumber.from(0); // 没有滑点,反而更好
            }
            return expected.sub(actual).mul(10000).div(expected);
        } else {
            // 对于输出,实际值应该大于等于预期值
            if (actual.gt(expected)) {
                return ethers.BigNumber.from(0); // 没有滑点,反而更好
            }
            return expected.sub(actual).mul(10000).div(expected);
        }
    }
}

最佳实践

1. 交换模式选择

function chooseSwapMode(userIntent, marketConditions) {
    if (userIntent.type === 'SPEND_EXACT') {
        // 用户想花费确切数量的代币
        return 'EXACT_INPUT';
    } else if (userIntent.type === 'RECEIVE_EXACT') {
        // 用户想获得确切数量的代币
        return 'EXACT_OUTPUT';
    } else if (marketConditions.volatility === 'HIGH') {
        // 高波动市场,使用精确输入更安全
        return 'EXACT_INPUT';
    } else {
        // 默认使用精确输入
        return 'EXACT_INPUT';
    }
}

2. 动态滑点调整

function calculateDynamicSlippage(priceImpact, marketVolatility, tradeSize) {
    let baseSlippage = 0.5; // 0.5%基础滑点
    
    // 根据价格影响调整
    if (priceImpact > 200) { // 大于2%
        baseSlippage += 1.0;
    } else if (priceImpact > 100) { // 大于1%
        baseSlippage += 0.5;
    }
    
    // 根据市场波动调整
    baseSlippage += marketVolatility * 0.5;
    
    // 根据交易规模调整
    if (tradeSize === 'LARGE') {
        baseSlippage += 0.5;
    }
    
    return Math.min(baseSlippage, 5.0); // 最大5%滑点
}

3. 交换前验证

async function validateSwap(swapParams) {
    const validations = [];
    
    // 验证代币地址
    if (!ethers.utils.isAddress(swapParams.tokenIn) || !ethers.utils.isAddress(swapParams.tokenOut)) {
        validations.push("无效的代币地址");
    }
    
    // 验证数量
    if (swapParams.amount.lte(0)) {
        validations.push("交换数量必须大于0");
    }
    
    // 验证滑点
    if (swapParams.slippage < 0 || swapParams.slippage > 50) {
        validations.push("滑点必须在0-50%之间");
    }
    
    // 验证截止时间
    if (swapParams.deadline <= Math.floor(Date.now() / 1000)) {
        validations.push("截止时间已过期");
    }
    
    // 验证交易对存在
    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("交易对不存在");
    }
    
    return validations;
}

Last updated