代币交换
代币交换是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