多跳交换
多跳交换允许用户在没有直接交易对的代币之间进行交换,通过中间代币作为桥梁完成交易。JAMM DEX支持灵活的多跳路径,每一跳可以使用不同的费率等级,为用户提供最优的交换路径。
多跳交换原理
基本概念
多跳交换将一次复杂的交换分解为多个简单的交换:
TokenA → TokenB → TokenC
每一跳都是独立的交换操作:
第一跳: TokenA → TokenB
第二跳: 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