代币交换

代币交换是JAMM DEX的核心功能,允许用户在不同ERC-20代币之间进行无需许可的交易。本指南详细介绍如何使用JAMM DEX进行各种类型的代币交换,包括精确输入、精确输出以及涉及JU代币的交换。

交换类型

精确输入交换 (Exact Input)

用户指定确切的输入数量,系统计算输出数量。

// swapExactTokensForTokens
const amountIn = ethers.utils.parseEther("1.0"); // Exact input 1 TokenA
const amountOutMin = ethers.utils.parseEther("0.95"); // Minimum 0.95 TokenB
const path = [tokenA_address, tokenB_address];
const fees = [100]; // 1% fee rate
const to = userAddress;
const referrer = ethers.constants.AddressZero;
const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // Expires in 20 minutes

const tx = await router.swapExactTokensForTokens(
    amountIn,
    amountOutMin,
    path,
    fees,
    to,
    referrer,
    deadline
);

精确输出交换 (Exact Output)

用户指定确切的输出数量,系统计算所需输入数量。

// swapTokensForExactTokens
const amountOut = ethers.utils.parseEther("1.0"); // Exact output 1 TokenB
const amountInMax = ethers.utils.parseEther("1.1"); // Maximum pay 1.1 TokenA
const path = [tokenA_address, tokenB_address];
const fees = [100]; // 1% fee rate

const tx = await router.swapTokensForExactTokens(
    amountOut,
    amountInMax,
    path,
    fees,
    to,
    referrer,
    deadline
);

JU代币交换

用JU购买代币

// swapExactETHForTokens - Use exact amount of JU to buy tokens
const amountOutMin = ethers.utils.parseEther("95"); // Minimum 95 tokens
const path = [WJU_address, token_address]; // Path must start with WJU
const fees = [100]; // 1% fee rate

const tx = await router.swapExactETHForTokens(
    amountOutMin,
    path,
    fees,
    to,
    referrer,
    deadline,
    { value: ethers.utils.parseEther("1.0") } // Send 1 JU
);

用JU购买精确数量代币

// swapETHForExactTokens - Use JU to buy exact amount of tokens
const amountOut = ethers.utils.parseEther("100"); // Exact output 100 tokens
const path = [WJU_address, token_address];
const fees = [100];

// First calculate how much JU is needed
const amounts = await router.getAmountsIn(amountOut, path, fees);
const juNeeded = amounts[0];

const tx = await router.swapETHForExactTokens(
    amountOut,
    path,
    fees,
    to,
    referrer,
    deadline,
    { value: juNeeded.mul(110).div(100) } // Send 10% extra to prevent price changes
);

卖出代币换JU

// swapExactTokensForETH - Sell exact amount of tokens for JU
const amountIn = ethers.utils.parseEther("100"); // Sell 100 tokens
const amountOutMin = ethers.utils.parseEther("0.9"); // Minimum 0.9 JU
const path = [token_address, WJU_address]; // Path must end with WJU
const fees = [100];

const tx = await router.swapExactTokensForETH(
    amountIn,
    amountOutMin,
    path,
    fees,
    to,
    referrer,
    deadline
);

卖出代币换精确数量JU

// swapTokensForExactETH - Sell tokens for exact amount of JU
const amountOut = ethers.utils.parseEther("1.0"); // Exact output 1 JU
const amountInMax = ethers.utils.parseEther("110"); // Maximum sell 110 tokens
const path = [token_address, WJU_address];
const fees = [100];

const tx = await router.swapTokensForExactETH(
    amountOut,
    amountInMax,
    path,
    fees,
    to,
    referrer,
    deadline
);

交换前准备

1. 代币授权

在进行代币交换前,需要授权Router合约使用您的代币:

const tokenContract = new ethers.Contract(tokenAddress, erc20ABI, signer);

// Check current allowance
const currentAllowance = await tokenContract.allowance(userAddress, routerAddress);
const requiredAmount = ethers.utils.parseEther("100");

if (currentAllowance.lt(requiredAmount)) {
    // Approve Router to use tokens
    const approveTx = await tokenContract.approve(routerAddress, requiredAmount);
    await approveTx.wait();
    console.log("Approval successful");
}

2. 价格查询

在交换前查询预期的输出数量:

// 查询精确输入的输出数量
const amountIn = ethers.utils.parseEther("1");
const path = [tokenA, tokenB];
const fees = [100];

const amounts = await router.getAmountsOut(amountIn, path, fees);
const expectedOutput = amounts[amounts.length - 1];
console.log("预期输出:", ethers.utils.formatEther(expectedOutput));

// 查询精确输出需要的输入数量
const amountOut = ethers.utils.parseEther("1");
const amountsIn = await router.getAmountsIn(amountOut, path, fees);
const requiredInput = amountsIn[0];
console.log("需要输入:", ethers.utils.formatEther(requiredInput));

3. 滑点设置

根据市场波动设置合理的滑点保护:

function calculateMinOutput(expectedOutput, slippagePercent) {
    const slippage = ethers.BigNumber.from(slippagePercent * 100); // 转换为基点
    const minOutput = expectedOutput.mul(10000 - slippage).div(10000);
    return minOutput;
}

// 设置1%滑点
const expectedOutput = ethers.utils.parseEther("99");
const minOutput = calculateMinOutput(expectedOutput, 1); // 1%滑点
console.log("最小输出:", ethers.utils.formatEther(minOutput));

完整交换示例

基础代币交换

async function swapTokens(
    tokenIn,
    tokenOut,
    amountIn,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    try {
        // 1. 检查和授权
        const tokenInContract = new ethers.Contract(tokenIn, erc20ABI, signer);
        const allowance = await tokenInContract.allowance(userAddress, ROUTER_ADDRESS);
        
        if (allowance.lt(amountIn)) {
            console.log("授权代币...");
            const approveTx = await tokenInContract.approve(ROUTER_ADDRESS, amountIn);
            await approveTx.wait();
        }
        
        // 2. 查询价格
        const path = [tokenIn, tokenOut];
        const fees = [100]; // 假设使用1%费率
        const amounts = await router.getAmountsOut(amountIn, path, fees);
        const expectedOutput = amounts[1];
        
        // 3. 计算最小输出(滑点保护)
        const minOutput = expectedOutput.mul(10000 - slippagePercent * 100).div(10000);
        
        // 4. 执行交换
        const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
        const tx = await router.swapExactTokensForTokens(
            amountIn,
            minOutput,
            path,
            fees,
            userAddress,
            ethers.constants.AddressZero, // 无推荐人
            deadline
        );
        
        console.log("交换交易已发送:", tx.hash);
        const receipt = await tx.wait();
        console.log("交换成功,区块:", receipt.blockNumber);
        
        return receipt;
    } catch (error) {
        console.error("交换失败:", error.message);
        throw error;
    }
}

// 使用示例
await swapTokens(
    "0xTokenA_Address",
    "0xTokenB_Address", 
    ethers.utils.parseEther("10"), // 交换10个TokenA
    1, // 1%滑点
    await signer.getAddress(),
    signer
);

JU代币交换示例

async function swapJUForTokens(
    tokenOut,
    juAmount,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    try {
        // 1. 查询价格
        const path = [WJU_ADDRESS, tokenOut];
        const fees = [100];
        const amounts = await router.getAmountsOut(juAmount, path, fees);
        const expectedOutput = amounts[1];
        
        // 2. 计算最小输出
        const minOutput = expectedOutput.mul(10000 - slippagePercent * 100).div(10000);
        
        // 3. 执行交换
        const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
        const tx = await router.swapExactETHForTokens(
            minOutput,
            path,
            fees,
            userAddress,
            ethers.constants.AddressZero,
            deadline,
            { value: juAmount }
        );
        
        console.log("JU交换交易已发送:", tx.hash);
        const receipt = await tx.wait();
        console.log("交换成功");
        
        return receipt;
    } catch (error) {
        console.error("JU交换失败:", error.message);
        throw error;
    }
}

高级功能

推荐人系统

使用推荐人可以获得费用优惠:

// 设置推荐人
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, signer);
const referrerAddress = "0xReferrer_Address";

// 检查是否已有推荐人
const currentReferrer = await factory.referrer(userAddress);
if (currentReferrer === ethers.constants.AddressZero) {
    const setReferrerTx = await factory.setReferrer(referrerAddress);
    await setReferrerTx.wait();
}

// 在交换时使用推荐人
const tx = await router.swapExactTokensForTokens(
    amountIn,
    amountOutMin,
    path,
    fees,
    to,
    referrerAddress, // 使用推荐人
    deadline
);

批量交换

async function batchSwap(swapParams, signer) {
    const results = [];
    
    for (const params of swapParams) {
        try {
            const result = await swapTokens(
                params.tokenIn,
                params.tokenOut,
                params.amountIn,
                params.slippage,
                params.userAddress,
                signer
            );
            results.push({ success: true, result });
        } catch (error) {
            results.push({ success: false, error: error.message });
        }
    }
    
    return results;
}

错误处理

常见错误及解决方案

function handleSwapError(error) {
    const errorMessage = error.message || error.toString();
    
    if (errorMessage.includes("INSUFFICIENT_OUTPUT_AMOUNT")) {
        return {
            type: "SLIPPAGE_TOO_HIGH",
            message: "滑点过大,请增加滑点容忍度或稍后重试",
            solution: "增加滑点设置或等待市场稳定"
        };
    }
    
    if (errorMessage.includes("EXPIRED")) {
        return {
            type: "TRANSACTION_EXPIRED", 
            message: "交易已过期",
            solution: "重新发起交易"
        };
    }
    
    if (errorMessage.includes("INSUFFICIENT_ALLOWANCE")) {
        return {
            type: "INSUFFICIENT_ALLOWANCE",
            message: "代币授权不足",
            solution: "增加代币授权额度"
        };
    }
    
    if (errorMessage.includes("INSUFFICIENT_BALANCE")) {
        return {
            type: "INSUFFICIENT_BALANCE",
            message: "余额不足",
            solution: "检查账户余额"
        };
    }
    
    return {
        type: "UNKNOWN_ERROR",
        message: "未知错误: " + errorMessage,
        solution: "请联系技术支持"
    };
}

// 使用示例
try {
    await swapTokens(...);
} catch (error) {
    const errorInfo = handleSwapError(error);
    console.error("交换失败:", errorInfo.message);
    console.log("解决方案:", errorInfo.solution);
}

最佳实践

1. 交换前检查

async function preSwapChecks(tokenIn, tokenOut, amountIn, userAddress) {
    const checks = {
        tokenBalance: false,
        tokenAllowance: false,
        pairExists: false,
        sufficientLiquidity: false
    };
    
    // 检查代币余额
    const tokenContract = new ethers.Contract(tokenIn, erc20ABI, provider);
    const balance = await tokenContract.balanceOf(userAddress);
    checks.tokenBalance = balance.gte(amountIn);
    
    // 检查授权额度
    const allowance = await tokenContract.allowance(userAddress, ROUTER_ADDRESS);
    checks.tokenAllowance = allowance.gte(amountIn);
    
    // 检查交易对是否存在
    const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
    const pairAddress = await factory.getPair(tokenIn, tokenOut, 100);
    checks.pairExists = pairAddress !== ethers.constants.AddressZero;
    
    if (checks.pairExists) {
        // 检查流动性
        const pair = new ethers.Contract(pairAddress, pairABI, provider);
        const reserves = await pair.getReserves();
        checks.sufficientLiquidity = reserves.reserve0.gt(0) && reserves.reserve1.gt(0);
    }
    
    return checks;
}

2. 价格影响计算

function calculatePriceImpact(amountIn, reserveIn, reserveOut) {
    // 理论价格(无价格影响)
    const theoreticalPrice = reserveOut.mul(ethers.utils.parseEther("1")).div(reserveIn);
    
    // 实际输出数量
    const actualOutput = getAmountOut(amountIn, reserveIn, reserveOut, 100);
    
    // 实际价格
    const actualPrice = actualOutput.mul(ethers.utils.parseEther("1")).div(amountIn);
    
    // 价格影响 = (理论价格 - 实际价格) / 理论价格
    const priceImpact = theoreticalPrice.sub(actualPrice).mul(10000).div(theoreticalPrice);
    
    return priceImpact; // 返回基点(100 = 1%)
}

3. 动态滑点设置

function calculateDynamicSlippage(priceImpact, baseSlippage = 50) {
    // 基础滑点50基点(0.5%)
    // 根据价格影响动态调整
    if (priceImpact.lt(100)) { // 小于1%
        return baseSlippage;
    } else if (priceImpact.lt(300)) { // 1-3%
        return baseSlippage + 50; // 增加0.5%
    } else { // 大于3%
        return baseSlippage + 100; // 增加1%
    }
}

监控和分析

交换事件监听

// 监听交换事件
const pair = new ethers.Contract(pairAddress, pairABI, provider);

pair.on("Swap", (sender, amount0In, amount1In, amount0Out, amount1Out, to) => {
    console.log("交换事件:", {
        sender,
        amount0In: ethers.utils.formatEther(amount0In),
        amount1In: ethers.utils.formatEther(amount1In),
        amount0Out: ethers.utils.formatEther(amount0Out),
        amount1Out: ethers.utils.formatEther(amount1Out),
        to
    });
});

交换历史查询

async function getSwapHistory(userAddress, fromBlock = 0) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
    
    // 查询用户的交换历史
    const filter = {
        address: ROUTER_ADDRESS,
        fromBlock: fromBlock,
        toBlock: 'latest'
    };
    
    const logs = await provider.getLogs(filter);
    const swapEvents = logs.filter(log => 
        log.topics[0] === ethers.utils.id("Swap(address,uint256,uint256,uint256,uint256,address)")
    );
    
    return swapEvents.map(event => {
        const decoded = router.interface.parseLog(event);
        return {
            blockNumber: event.blockNumber,
            transactionHash: event.transactionHash,
            ...decoded.args
        };
    });
}

Last updated