Files
mini-swap/scripts/cli.js
NoBey 7cddf1d987 feat: 添加 MiniSwap CLI 工具及相关文档
- 新增 CLI 工具,支持通过命令行与 MiniSwapAMM 合约交互
- 创建 CLI 使用指南文档,详细说明命令和操作流程
- 更新合约地址,确保与新部署的合约匹配
- 添加快速网络连接测试脚本,验证合约连接和网络状态
- 实现完整测试场景脚本,模拟用户操作和流动性管理
2025-07-12 02:28:36 +08:00

483 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
const { ethers } = require("hardhat");
const fs = require("fs");
const path = require("path");
// 颜色输出工具
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m'
};
function log(color, message) {
console.log(`${colors[color]}${message}${colors.reset}`);
}
class MiniSwapCLI {
constructor() {
this.contracts = {};
this.signer = null;
this.signers = [];
}
async init() {
try {
// 连接到网络
await this.connectToNetwork();
// 加载合约
await this.loadContracts();
log('green', '✅ CLI 工具初始化成功!');
log('cyan', `当前账户: ${this.signer.address}`);
} catch (error) {
log('red', `❌ 初始化失败: ${error.message}`);
process.exit(1);
}
}
async connectToNetwork() {
try {
this.signers = await ethers.getSigners();
this.signer = this.signers[0];
// 获取网络信息
const network = await this.signer.provider.getNetwork();
log('blue', `连接到网络: ${network.name || 'localhost'} (Chain ID: ${network.chainId})`);
log('blue', `找到 ${this.signers.length} 个账户`);
// 验证网络连接
const balance = await this.signer.provider.getBalance(this.signer.address);
if (balance === 0n) {
log('yellow', '⚠️ 警告: 当前账户余额为 0 ETH');
}
} catch (error) {
throw new Error(`网络连接失败: ${error.message}`);
}
}
async loadContracts() {
const addressesPath = path.join(__dirname, '..', 'contract-addresses.json');
if (!fs.existsSync(addressesPath)) {
throw new Error('合约地址文件不存在,请先运行 npm run deploy');
}
const addresses = JSON.parse(fs.readFileSync(addressesPath, 'utf8'));
log('blue', '正在加载合约...');
log('white', `TokenA: ${addresses.TokenA}`);
log('white', `TokenB: ${addresses.TokenB}`);
log('white', `MiniSwapAMM: ${addresses.MiniSwapAMM}`);
// 加载合约
const TokenA = await ethers.getContractFactory("TokenA");
const TokenB = await ethers.getContractFactory("TokenB");
const MiniSwapAMM = await ethers.getContractFactory("MiniSwapAMM");
this.contracts.tokenA = TokenA.attach(addresses.TokenA);
this.contracts.tokenB = TokenB.attach(addresses.TokenB);
this.contracts.amm = MiniSwapAMM.attach(addresses.MiniSwapAMM);
// 验证合约连接
try {
await this.contracts.tokenA.name();
await this.contracts.tokenB.name();
await this.contracts.amm.getReserves();
log('green', '✅ 合约加载并验证成功');
} catch (error) {
throw new Error(`合约验证失败,请确保合约已正确部署到当前网络: ${error.message}`);
}
}
// 切换账户
async switchAccount(index) {
if (index < 0 || index >= this.signers.length) {
log('red', '❌ 无效的账户索引');
return;
}
this.signer = this.signers[index];
// 重新连接合约到新的签名者
this.contracts.tokenA = this.contracts.tokenA.connect(this.signer);
this.contracts.tokenB = this.contracts.tokenB.connect(this.signer);
this.contracts.amm = this.contracts.amm.connect(this.signer);
log('green', `✅ 切换到账户 ${index}: ${this.signer.address}`);
}
// 查看所有账户
async showAccounts() {
log('cyan', '\n=== 账户列表 ===');
for (let i = 0; i < this.signers.length; i++) {
const current = this.signers[i].address === this.signer.address ? ' (当前)' : '';
log('white', `[${i}] ${this.signers[i].address}${current}`);
}
}
// 查看余额
async showBalances() {
try {
const address = this.signer.address;
// 获取 ETH 余额
const ethBalance = await this.signer.provider.getBalance(address);
// 获取代币余额
const tokenABalance = await this.contracts.tokenA.balanceOf(address);
const tokenBBalance = await this.contracts.tokenB.balanceOf(address);
const lpBalance = await this.contracts.amm.balanceOf(address);
log('cyan', '\n=== 账户余额 ===');
log('white', `地址: ${address}`);
log('white', `ETH: ${ethers.formatEther(ethBalance)}`);
log('white', `TokenA: ${ethers.formatEther(tokenABalance)} TKA`);
log('white', `TokenB: ${ethers.formatEther(tokenBBalance)} TKB`);
log('white', `LP Token: ${ethers.formatEther(lpBalance)} MSLP`);
} catch (error) {
log('red', `❌ 查询余额失败: ${error.message}`);
}
}
// 查看 AMM 储备量
async showReserves() {
try {
const [reserveA, reserveB] = await this.contracts.amm.getReserves();
const totalSupply = await this.contracts.amm.totalSupply();
log('cyan', '\n=== AMM 储备量 ===');
log('white', `TokenA 储备: ${ethers.formatEther(reserveA)} TKA`);
log('white', `TokenB 储备: ${ethers.formatEther(reserveB)} TKB`);
log('white', `LP Token 总供应: ${ethers.formatEther(totalSupply)} MSLP`);
if (reserveA > 0 && reserveB > 0) {
const priceAtoB = Number(ethers.formatEther(reserveB)) / Number(ethers.formatEther(reserveA));
const priceBtoA = Number(ethers.formatEther(reserveA)) / Number(ethers.formatEther(reserveB));
log('white', `价格 (A->B): ${priceAtoB.toFixed(6)} TKB/TKA`);
log('white', `价格 (B->A): ${priceBtoA.toFixed(6)} TKA/TKB`);
}
} catch (error) {
log('red', `❌ 查询储备量失败: ${error.message}`);
}
}
// 铸造代币
async mintTokens(amount = "1000") {
try {
log('yellow', '🔄 铸造代币中...');
const amountWei = ethers.parseEther(amount);
const txA = await this.contracts.tokenA.faucet(amountWei);
const txB = await this.contracts.tokenB.faucet(amountWei);
await txA.wait();
await txB.wait();
log('green', `✅ 成功铸造 ${amount} TKA 和 ${amount} TKB`);
} catch (error) {
log('red', `❌ 铸造代币失败: ${error.message}`);
}
}
// 授权代币
async approveTokens(amount = "10000") {
try {
log('yellow', '🔄 授权代币中...');
const amountWei = ethers.parseEther(amount);
const ammAddress = await this.contracts.amm.getAddress();
const txA = await this.contracts.tokenA.approve(ammAddress, amountWei);
const txB = await this.contracts.tokenB.approve(ammAddress, amountWei);
await txA.wait();
await txB.wait();
log('green', `✅ 成功授权 ${amount} TKA 和 ${amount} TKB 给 AMM 合约`);
} catch (error) {
log('red', `❌ 授权代币失败: ${error.message}`);
}
}
// 添加流动性
async addLiquidity(amountA, amountB, slippage = "1") {
try {
log('yellow', '🔄 添加流动性中...');
const amountAWei = ethers.parseEther(amountA);
const amountBWei = ethers.parseEther(amountB);
// 计算滑点保护
const slippagePercent = parseFloat(slippage) / 100;
const amountAMin = amountAWei * BigInt(Math.floor((1 - slippagePercent) * 1000)) / 1000n;
const amountBMin = amountBWei * BigInt(Math.floor((1 - slippagePercent) * 1000)) / 1000n;
const tx = await this.contracts.amm.addLiquidity(
amountAWei,
amountBWei,
amountAMin,
amountBMin
);
const receipt = await tx.wait();
log('green', `✅ 流动性添加成功!`);
log('blue', `交易哈希: ${receipt.hash}`);
} catch (error) {
log('red', `❌ 添加流动性失败: ${error.message}`);
}
}
// 移除流动性
async removeLiquidity(liquidity, slippage = "1") {
try {
log('yellow', '🔄 移除流动性中...');
const liquidityWei = ethers.parseEther(liquidity);
// 计算最小输出
const [reserveA, reserveB] = await this.contracts.amm.getReserves();
const totalSupply = await this.contracts.amm.totalSupply();
const amountA = liquidityWei * reserveA / totalSupply;
const amountB = liquidityWei * reserveB / totalSupply;
const slippagePercent = parseFloat(slippage) / 100;
const amountAMin = amountA * BigInt(Math.floor((1 - slippagePercent) * 1000)) / 1000n;
const amountBMin = amountB * BigInt(Math.floor((1 - slippagePercent) * 1000)) / 1000n;
const tx = await this.contracts.amm.removeLiquidity(
liquidityWei,
amountAMin,
amountBMin
);
const receipt = await tx.wait();
log('green', `✅ 流动性移除成功!`);
log('blue', `交易哈希: ${receipt.hash}`);
} catch (error) {
log('red', `❌ 移除流动性失败: ${error.message}`);
}
}
// 交换 A -> B
async swapAForB(amountIn, slippage = "1") {
try {
log('yellow', '🔄 交换 TokenA -> TokenB 中...');
const amountInWei = ethers.parseEther(amountIn);
const [reserveA, reserveB] = await this.contracts.amm.getReserves();
const amountOut = await this.contracts.amm.getAmountOut(amountInWei, reserveA, reserveB);
const slippagePercent = parseFloat(slippage) / 100;
const amountOutMin = amountOut * BigInt(Math.floor((1 - slippagePercent) * 1000)) / 1000n;
log('blue', `预期输出: ${ethers.formatEther(amountOut)} TKB`);
log('blue', `最小输出: ${ethers.formatEther(amountOutMin)} TKB`);
const tx = await this.contracts.amm.swapAForB(amountInWei, amountOutMin);
const receipt = await tx.wait();
log('green', `✅ 交换成功!`);
log('blue', `交易哈希: ${receipt.hash}`);
} catch (error) {
log('red', `❌ 交换失败: ${error.message}`);
}
}
// 交换 B -> A
async swapBForA(amountIn, slippage = "1") {
try {
log('yellow', '🔄 交换 TokenB -> TokenA 中...');
const amountInWei = ethers.parseEther(amountIn);
const [reserveA, reserveB] = await this.contracts.amm.getReserves();
const amountOut = await this.contracts.amm.getAmountOut(amountInWei, reserveB, reserveA);
const slippagePercent = parseFloat(slippage) / 100;
const amountOutMin = amountOut * BigInt(Math.floor((1 - slippagePercent) * 1000)) / 1000n;
log('blue', `预期输出: ${ethers.formatEther(amountOut)} TKA`);
log('blue', `最小输出: ${ethers.formatEther(amountOutMin)} TKA`);
const tx = await this.contracts.amm.swapBForA(amountInWei, amountOutMin);
const receipt = await tx.wait();
log('green', `✅ 交换成功!`);
log('blue', `交易哈希: ${receipt.hash}`);
} catch (error) {
log('red', `❌ 交换失败: ${error.message}`);
}
}
// 计算交换价格
async calculateSwap(amountIn, direction = "a2b") {
try {
const amountInWei = ethers.parseEther(amountIn);
const [reserveA, reserveB] = await this.contracts.amm.getReserves();
let amountOut;
if (direction === "a2b") {
amountOut = await this.contracts.amm.getAmountOut(amountInWei, reserveA, reserveB);
log('cyan', '\n=== 交换计算 (A -> B) ===');
log('white', `输入: ${amountIn} TKA`);
log('white', `输出: ${ethers.formatEther(amountOut)} TKB`);
} else {
amountOut = await this.contracts.amm.getAmountOut(amountInWei, reserveB, reserveA);
log('cyan', '\n=== 交换计算 (B -> A) ===');
log('white', `输入: ${amountIn} TKB`);
log('white', `输出: ${ethers.formatEther(amountOut)} TKA`);
}
const rate = Number(ethers.formatEther(amountOut)) / Number(amountIn);
log('white', `汇率: ${rate.toFixed(6)}`);
} catch (error) {
log('red', `❌ 计算失败: ${error.message}`);
}
}
// 显示帮助信息
showHelp() {
log('cyan', '\n=== MiniSwap CLI 工具 ===');
log('white', '可用命令:');
log('yellow', ' accounts - 显示所有账户');
log('yellow', ' switch <index> - 切换到指定账户');
log('yellow', ' balances - 查看当前账户余额');
log('yellow', ' reserves - 查看 AMM 储备量');
log('yellow', ' mint [amount] - 铸造代币 (默认 1000)');
log('yellow', ' approve [amount] - 授权代币 (默认 10000)');
log('yellow', ' add <amountA> <amountB> [slippage] - 添加流动性');
log('yellow', ' remove <liquidity> [slippage] - 移除流动性');
log('yellow', ' swapAB <amount> [slippage] - 交换 A->B');
log('yellow', ' swapBA <amount> [slippage] - 交换 B->A');
log('yellow', ' calc <amount> <a2b|b2a> - 计算交换价格');
log('yellow', ' help - 显示帮助信息');
log('yellow', ' exit - 退出程序');
log('white', '\n滑点参数单位为百分比例如 1 表示 1%');
}
// 主循环
async run() {
await this.init();
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
this.showHelp();
const prompt = () => {
rl.question('\n> ', async (input) => {
const args = input.trim().split(' ');
const command = args[0];
try {
switch (command) {
case 'accounts':
await this.showAccounts();
break;
case 'switch':
await this.switchAccount(parseInt(args[1]));
break;
case 'balances':
await this.showBalances();
break;
case 'reserves':
await this.showReserves();
break;
case 'mint':
await this.mintTokens(args[1] || "1000");
break;
case 'approve':
await this.approveTokens(args[1] || "10000");
break;
case 'add':
if (args.length < 3) {
log('red', '❌ 用法: add <amountA> <amountB> [slippage]');
} else {
await this.addLiquidity(args[1], args[2], args[3] || "1");
}
break;
case 'remove':
if (args.length < 2) {
log('red', '❌ 用法: remove <liquidity> [slippage]');
} else {
await this.removeLiquidity(args[1], args[2] || "1");
}
break;
case 'swapAB':
if (args.length < 2) {
log('red', '❌ 用法: swapAB <amount> [slippage]');
} else {
await this.swapAForB(args[1], args[2] || "1");
}
break;
case 'swapBA':
if (args.length < 2) {
log('red', '❌ 用法: swapBA <amount> [slippage]');
} else {
await this.swapBForA(args[1], args[2] || "1");
}
break;
case 'calc':
if (args.length < 3) {
log('red', '❌ 用法: calc <amount> <a2b|b2a>');
} else {
await this.calculateSwap(args[1], args[2]);
}
break;
case 'help':
this.showHelp();
break;
case 'exit':
log('green', '👋 再见!');
rl.close();
return;
case '':
break;
default:
log('red', `❌ 未知命令: ${command}`);
log('yellow', '输入 "help" 查看可用命令');
}
} catch (error) {
log('red', `❌ 执行命令时出错: ${error.message}`);
}
prompt();
});
};
prompt();
}
}
// 如果直接运行此文件
if (require.main === module) {
const cli = new MiniSwapCLI();
cli.run().catch(console.error);
}
module.exports = MiniSwapCLI;