精确数量交换
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