多跳交换

多跳交换允许用户在没有直接交易对的代币之间进行交换,通过中间代币作为桥梁完成交易。JAMM DEX支持灵活的多跳路径,每一跳可以使用不同的费率等级,为用户提供最优的交换路径。

多跳交换原理

基本概念

多跳交换将一次复杂的交换分解为多个简单的交换:

TokenA → TokenB → TokenC

每一跳都是独立的交换操作:

  1. 第一跳: TokenA → TokenB

  2. 第二跳: TokenB → TokenC

路径和费率

在JAMM DEX中,多跳交换需要指定:

  • 路径数组: [TokenA, TokenB, TokenC]

  • 费率数组: [fee1, fee2]

注意:费率数组的长度总是比路径数组少1。

多跳交换实现

精确输入多跳交换

async function multiHopSwapExactInput(
    amountIn,
    amountOutMin,
    path,
    fees,
    to,
    referrer,
    deadline,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    // 验证路径和费率数组长度
    if (path.length - fees.length !== 1) {
        throw new Error("路径和费率数组长度不匹配");
    }
    
    // 执行多跳交换
    const tx = await router.swapExactTokensForTokens(
        amountIn,
        amountOutMin,
        path,
        fees,
        to,
        referrer,
        deadline
    );
    
    return tx;
}

// 使用示例: USDC → WJU → TokenX
const path = [USDC_ADDRESS, WJU_ADDRESS, TOKENX_ADDRESS];
const fees = [50, 100]; // USDC/WJU使用0.5%费率,WJU/TokenX使用1%费率
const amountIn = ethers.utils.parseUnits("100", 6); // 100 USDC
const amountOutMin = ethers.utils.parseEther("0.95"); // 最少0.95个TokenX

await multiHopSwapExactInput(
    amountIn,
    amountOutMin,
    path,
    fees,
    userAddress,
    ethers.constants.AddressZero,
    deadline,
    signer
);

精确输出多跳交换

async function multiHopSwapExactOutput(
    amountOut,
    amountInMax,
    path,
    fees,
    to,
    referrer,
    deadline,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    const tx = await router.swapTokensForExactTokens(
        amountOut,
        amountInMax,
        path,
        fees,
        to,
        referrer,
        deadline
    );
    
    return tx;
}

// 使用示例: 获得精确100个TokenX
const amountOut = ethers.utils.parseEther("100"); // 精确100个TokenX
const amountInMax = ethers.utils.parseUnits("110", 6); // 最多支付110 USDC

await multiHopSwapExactOutput(
    amountOut,
    amountInMax,
    path,
    fees,
    userAddress,
    ethers.constants.AddressZero,
    deadline,
    signer
);

路径优化

最优路径查找

class PathFinder {
    constructor(factoryAddress, provider) {
        this.factory = new ethers.Contract(factoryAddress, factoryABI, provider);
        this.provider = provider;
        this.commonBases = [WJU_ADDRESS, USDC_ADDRESS, USDT_ADDRESS]; // 常用中间代币
    }
    
    async findBestPath(tokenIn, tokenOut, amountIn) {
        const paths = [];
        
        // 1. 直接路径
        const directPath = await this.checkDirectPath(tokenIn, tokenOut, amountIn);
        if (directPath) {
            paths.push(directPath);
        }
        
        // 2. 通过常用基础代币的路径
        for (const base of this.commonBases) {
            if (base !== tokenIn && base !== tokenOut) {
                const path = await this.checkTwoHopPath(tokenIn, base, tokenOut, amountIn);
                if (path) {
                    paths.push(path);
                }
            }
        }
        
        // 3. 选择最优路径(输出最大)
        return paths.reduce((best, current) => 
            current.outputAmount.gt(best.outputAmount) ? current : best
        );
    }
    
    async checkDirectPath(tokenIn, tokenOut, amountIn) {
        const fees = [50, 100, 200, 300]; // 检查所有费率等级
        let bestOutput = ethers.BigNumber.from(0);
        let bestFee = 0;
        
        for (const fee of fees) {
            try {
                const pairAddress = await this.factory.getPair(tokenIn, tokenOut, fee);
                if (pairAddress === ethers.constants.AddressZero) continue;
                
                const pair = new ethers.Contract(pairAddress, pairABI, this.provider);
                const reserves = await pair.getReserves();
                
                if (reserves.reserve0.gt(0) && reserves.reserve1.gt(0)) {
                    const output = this.calculateOutput(amountIn, reserves, fee, tokenIn, tokenOut);
                    if (output.gt(bestOutput)) {
                        bestOutput = output;
                        bestFee = fee;
                    }
                }
            } catch (error) {
                continue;
            }
        }
        
        if (bestOutput.gt(0)) {
            return {
                path: [tokenIn, tokenOut],
                fees: [bestFee],
                outputAmount: bestOutput,
                hops: 1
            };
        }
        
        return null;
    }
    
    async checkTwoHopPath(tokenIn, intermediate, tokenOut, amountIn) {
        // 检查第一跳: tokenIn → intermediate
        const firstHop = await this.checkDirectPath(tokenIn, intermediate, amountIn);
        if (!firstHop) return null;
        
        // 检查第二跳: intermediate → tokenOut
        const secondHop = await this.checkDirectPath(intermediate, tokenOut, firstHop.outputAmount);
        if (!secondHop) return null;
        
        return {
            path: [tokenIn, intermediate, tokenOut],
            fees: [firstHop.fees[0], secondHop.fees[0]],
            outputAmount: secondHop.outputAmount,
            hops: 2
        };
    }
    
    calculateOutput(amountIn, reserves, fee, tokenIn, tokenOut) {
        // 简化的输出计算(实际应使用JAMMLibrary的逻辑)
        const [reserveIn, reserveOut] = tokenIn < tokenOut 
            ? [reserves.reserve0, reserves.reserve1]
            : [reserves.reserve1, reserves.reserve0];
            
        const amountInWithFee = amountIn.mul(10000 - fee);
        const numerator = amountInWithFee.mul(reserveOut);
        const denominator = reserveIn.mul(10000).add(amountInWithFee);
        
        return numerator.div(denominator);
    }
}

// 使用示例
const pathFinder = new PathFinder(FACTORY_ADDRESS, provider);
const bestPath = await pathFinder.findBestPath(
    TOKEN_A_ADDRESS,
    TOKEN_B_ADDRESS,
    ethers.utils.parseEther("100")
);

console.log("最优路径:", bestPath);

价格比较

async function compareMultiHopPrices(tokenIn, tokenOut, amountIn) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
    const results = [];
    
    // 路径1: 直接交换
    try {
        const directPath = [tokenIn, tokenOut];
        const directFees = [100]; // 1%费率
        const directAmounts = await router.getAmountsOut(amountIn, directPath, directFees);
        results.push({
            path: directPath,
            fees: directFees,
            output: directAmounts[directAmounts.length - 1],
            description: "直接交换"
        });
    } catch (error) {
        console.log("直接路径不可用");
    }
    
    // 路径2: 通过WJU
    try {
        const wjuPath = [tokenIn, WJU_ADDRESS, tokenOut];
        const wjuFees = [100, 100]; // 都使用1%费率
        const wjuAmounts = await router.getAmountsOut(amountIn, wjuPath, wjuFees);
        results.push({
            path: wjuPath,
            fees: wjuFees,
            output: wjuAmounts[wjuAmounts.length - 1],
            description: "通过WJU"
        });
    } catch (error) {
        console.log("WJU路径不可用");
    }
    
    // 路径3: 通过USDC
    try {
        const usdcPath = [tokenIn, USDC_ADDRESS, tokenOut];
        const usdcFees = [50, 50]; // 都使用0.5%费率
        const usdcAmounts = await router.getAmountsOut(amountIn, usdcPath, usdcFees);
        results.push({
            path: usdcPath,
            fees: usdcFees,
            output: usdcAmounts[usdcAmounts.length - 1],
            description: "通过USDC"
        });
    } catch (error) {
        console.log("USDC路径不可用");
    }
    
    // 按输出数量排序
    results.sort((a, b) => b.output.gt(a.output) ? 1 : -1);
    
    return results;
}

JU代币多跳交换

JU → 代币 → 代币

async function swapJUMultiHop(
    tokenIntermediate,
    tokenOut,
    juAmount,
    amountOutMin,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    const path = [WJU_ADDRESS, tokenIntermediate, tokenOut];
    const fees = [100, 100]; // 可根据实际情况调整
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    const tx = await router.swapExactETHForTokens(
        amountOutMin,
        path,
        fees,
        await signer.getAddress(),
        ethers.constants.AddressZero,
        deadline,
        { value: juAmount }
    );
    
    return tx;
}

// 使用示例: JU → USDC → TokenX
await swapJUMultiHop(
    USDC_ADDRESS,
    TOKENX_ADDRESS,
    ethers.utils.parseEther("1"), // 1 JU
    ethers.utils.parseEther("95"), // 最少95个TokenX
    signer
);

代币 → 代币 → JU

async function swapMultiHopToJU(
    tokenIn,
    tokenIntermediate,
    amountIn,
    amountOutMin,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    const path = [tokenIn, tokenIntermediate, WJU_ADDRESS];
    const fees = [100, 100];
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    const tx = await router.swapExactTokensForETH(
        amountIn,
        amountOutMin,
        path,
        fees,
        await signer.getAddress(),
        ethers.constants.AddressZero,
        deadline
    );
    
    return tx;
}

高级多跳策略

分割交换

将大额交换分割为多个小额交换以减少价格影响:

async function splitMultiHopSwap(
    amountIn,
    path,
    fees,
    splits,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    const splitAmount = amountIn.div(splits);
    const results = [];
    
    for (let i = 0; i < splits; i++) {
        try {
            // 每次交换前重新计算最小输出
            const amounts = await router.getAmountsOut(splitAmount, path, fees);
            const minOutput = amounts[amounts.length - 1].mul(95).div(100); // 5%滑点
            
            const tx = await router.swapExactTokensForTokens(
                splitAmount,
                minOutput,
                path,
                fees,
                await signer.getAddress(),
                ethers.constants.AddressZero,
                Math.floor(Date.now() / 1000) + 60 * 20
            );
            
            results.push(tx);
            
            // 等待一段时间再进行下一次交换
            if (i < splits - 1) {
                await new Promise(resolve => setTimeout(resolve, 5000));
            }
        } catch (error) {
            console.error(`第${i + 1}次分割交换失败:`, error.message);
        }
    }
    
    return results;
}

动态路径调整

根据实时流动性调整交换路径:

class DynamicRouter {
    constructor(routerAddress, factoryAddress, provider) {
        this.router = new ethers.Contract(routerAddress, routerABI, provider);
        this.factory = new ethers.Contract(factoryAddress, factoryABI, provider);
        this.provider = provider;
    }
    
    async executeOptimalSwap(tokenIn, tokenOut, amountIn, maxSlippage = 300) {
        // 1. 查找所有可能的路径
        const paths = await this.findAllPaths(tokenIn, tokenOut);
        
        // 2. 计算每条路径的输出
        const pathResults = await Promise.all(
            paths.map(path => this.calculatePathOutput(path, amountIn))
        );
        
        // 3. 选择最优路径
        const bestPath = pathResults.reduce((best, current) => 
            current.output.gt(best.output) ? current : best
        );
        
        // 4. 检查滑点
        const priceImpact = this.calculatePriceImpact(bestPath, amountIn);
        if (priceImpact.gt(maxSlippage)) {
            throw new Error(`价格影响过大: ${priceImpact.toString()}基点`);
        }
        
        // 5. 执行交换
        const minOutput = bestPath.output.mul(10000 - maxSlippage).div(10000);
        return await this.router.swapExactTokensForTokens(
            amountIn,
            minOutput,
            bestPath.path,
            bestPath.fees,
            await this.provider.getSigner().getAddress(),
            ethers.constants.AddressZero,
            Math.floor(Date.now() / 1000) + 60 * 20
        );
    }
    
    async findAllPaths(tokenIn, tokenOut, maxHops = 3) {
        // 实现路径查找逻辑
        // 返回所有可能的路径组合
    }
    
    async calculatePathOutput(pathInfo, amountIn) {
        try {
            const amounts = await this.router.getAmountsOut(
                amountIn, 
                pathInfo.path, 
                pathInfo.fees
            );
            return {
                ...pathInfo,
                output: amounts[amounts.length - 1]
            };
        } catch (error) {
            return {
                ...pathInfo,
                output: ethers.BigNumber.from(0)
            };
        }
    }
    
    calculatePriceImpact(pathInfo, amountIn) {
        // 计算价格影响
        // 返回基点数值
    }
}

监控和分析

多跳交换事件监听

async function monitorMultiHopSwaps(userAddress) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
    
    // 监听用户的多跳交换
    const filter = router.filters.SwapExactTokensForTokens(null, null, null, null, userAddress);
    
    router.on(filter, async (sender, amountIn, amountOutMin, path, fees, to, referrer, deadline, event) => {
        if (path.length > 2) {
            console.log("多跳交换检测到:", {
                sender,
                path: path.map(addr => addr.slice(0, 6) + '...'),
                fees,
                hops: path.length - 1,
                transactionHash: event.transactionHash
            });
            
            // 获取实际输出数量
            const receipt = await event.getTransactionReceipt();
            // 解析日志获取实际交换数据
        }
    });
}

路径效率分析

async function analyzePathEfficiency(tokenIn, tokenOut, amountIn) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
    
    const paths = [
        { path: [tokenIn, tokenOut], fees: [100], name: "直接" },
        { path: [tokenIn, WJU_ADDRESS, tokenOut], fees: [100, 100], name: "通过WJU" },
        { path: [tokenIn, USDC_ADDRESS, tokenOut], fees: [50, 50], name: "通过USDC" }
    ];
    
    const analysis = [];
    
    for (const pathInfo of paths) {
        try {
            const amounts = await router.getAmountsOut(amountIn, pathInfo.path, pathInfo.fees);
            const output = amounts[amounts.length - 1];
            const totalFee = pathInfo.fees.reduce((sum, fee) => sum + fee, 0);
            
            analysis.push({
                name: pathInfo.name,
                path: pathInfo.path,
                output: output,
                totalFee: totalFee,
                efficiency: output.mul(10000).div(amountIn), // 输出/输入比率
                hops: pathInfo.path.length - 1
            });
        } catch (error) {
            analysis.push({
                name: pathInfo.name,
                error: error.message
            });
        }
    }
    
    // 按效率排序
    analysis.sort((a, b) => {
        if (!a.efficiency || !b.efficiency) return 0;
        return b.efficiency.gt(a.efficiency) ? 1 : -1;
    });
    
    return analysis;
}

最佳实践

1. 路径选择策略

const pathSelectionStrategy = {
    // 小额交换: 优先考虑费用最低
    smallAmount: (paths) => paths.sort((a, b) => a.totalFee - b.totalFee)[0],
    
    // 大额交换: 优先考虑流动性最深
    largeAmount: (paths) => paths.sort((a, b) => b.liquidity.gt(a.liquidity) ? 1 : -1)[0],
    
    // 平衡策略: 综合考虑输出和费用
    balanced: (paths) => paths.sort((a, b) => {
        const scoreA = a.output.mul(10000).div(a.totalFee + 1);
        const scoreB = b.output.mul(10000).div(b.totalFee + 1);
        return scoreB.gt(scoreA) ? 1 : -1;
    })[0]
};

2. 滑点管理

function calculateDynamicSlippage(pathLength, marketVolatility) {
    let baseSlippage = 50; // 0.5%基础滑点
    
    // 根据跳数增加滑点
    const hopPenalty = (pathLength - 1) * 25; // 每跳增加0.25%
    
    // 根据市场波动调整
    const volatilityAdjustment = marketVolatility * 100;
    
    return Math.min(baseSlippage + hopPenalty + volatilityAdjustment, 500); // 最大5%
}

3. Gas优化

async function optimizeMultiHopGas(paths, gasPrice) {
    const gasEstimates = await Promise.all(
        paths.map(async (path) => {
            try {
                const gasEstimate = await router.estimateGas.swapExactTokensForTokens(
                    ethers.utils.parseEther("1"),
                    0,
                    path.path,
                    path.fees,
                    userAddress,
                    ethers.constants.AddressZero,
                    Math.floor(Date.now() / 1000) + 60 * 20
                );
                
                return {
                    ...path,
                    gasEstimate,
                    gasCost: gasEstimate.mul(gasPrice)
                };
            } catch (error) {
                return { ...path, gasEstimate: null };
            }
        })
    );
    
    // 考虑Gas成本的最优路径
    return gasEstimates.filter(p => p.gasEstimate).sort((a, b) => {
        const netA = a.output.sub(a.gasCost);
        const netB = b.output.sub(b.gasCost);
        return netB.gt(netA) ? 1 : -1;
    })[0];
}

Last updated