- 新增 CLI 工具,支持通过命令行与 MiniSwapAMM 合约交互 - 创建 CLI 使用指南文档,详细说明命令和操作流程 - 更新合约地址,确保与新部署的合约匹配 - 添加快速网络连接测试脚本,验证合约连接和网络状态 - 实现完整测试场景脚本,模拟用户操作和流动性管理
483 lines
16 KiB
JavaScript
483 lines
16 KiB
JavaScript
#!/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;
|