diff --git a/CLI-USAGE.md b/CLI-USAGE.md new file mode 100644 index 0000000..330ea77 --- /dev/null +++ b/CLI-USAGE.md @@ -0,0 +1,190 @@ +# MiniSwap CLI 工具使用指南 + +这个 CLI 工具让你可以通过命令行操作和测试 MiniSwapAMM 合约的所有功能,无需前端界面。 + +## 前置条件 + +1. 确保已经部署了合约: +```bash +npm run deploy +``` + +2. 确保本地网络正在运行: +```bash +npm run node +``` + +## 快速开始 + +### 方法1: 交互式 CLI 工具 + +启动交互式命令行界面: +```bash +npm run cli +``` + +这将启动一个交互式界面,你可以输入命令来操作合约。 + +### 方法2: 测试网络连接 + +如果遇到连接问题,可以先运行连接测试: +```bash +npm run test-connection +``` + +这会测试网络连接和合约加载是否正常。 + +### 方法3: 运行完整测试场景 + +运行预设的完整测试场景: +```bash +npm run test-scenario +``` + +这会自动执行一系列操作,演示 AMM 的所有功能。 + +## CLI 命令详解 + +### 基本信息查询 + +- `accounts` - 显示所有可用账户 +- `switch ` - 切换到指定账户(例如:`switch 1`) +- `balances` - 查看当前账户的所有代币余额 +- `reserves` - 查看 AMM 池的储备量和价格信息 + +### 代币操作 + +- `mint [amount]` - 铸造测试代币(默认 1000 个) + - 例如:`mint 500` 铸造 500 个代币 +- `approve [amount]` - 授权代币给 AMM 合约(默认 10000 个) + - 例如:`approve 5000` 授权 5000 个代币 + +### 流动性操作 + +- `add [slippage]` - 添加流动性 + - 例如:`add 1000 2000 1` 添加 1000 TKA + 2000 TKB,允许 1% 滑点 +- `remove [slippage]` - 移除流动性 + - 例如:`remove 500 1` 移除 500 个 LP 代币,允许 1% 滑点 + +### 代币交换 + +- `swapAB [slippage]` - 将 TokenA 交换为 TokenB + - 例如:`swapAB 100 1` 用 100 TKA 交换 TKB,允许 1% 滑点 +- `swapBA [slippage]` - 将 TokenB 交换为 TokenA + - 例如:`swapBA 50 1` 用 50 TKB 交换 TKA,允许 1% 滑点 + +### 价格计算 + +- `calc ` - 计算交换价格 + - `calc 100 a2b` - 计算 100 TKA 能换多少 TKB + - `calc 50 b2a` - 计算 50 TKB 能换多少 TKA + +### 其他命令 + +- `help` - 显示帮助信息 +- `exit` - 退出程序 + +## 使用示例 + +以下是一个完整的操作流程示例: + +```bash +# 启动 CLI +npm run cli + +# 查看当前账户余额 +> balances + +# 铸造一些测试代币 +> mint 5000 + +# 授权代币给 AMM 合约 +> approve 10000 + +# 添加初始流动性(1000 TKA + 2000 TKB) +> add 1000 2000 + +# 查看池子状态 +> reserves + +# 切换到另一个账户 +> switch 1 + +# 为新账户铸造代币 +> mint 3000 +> approve 5000 + +# 进行代币交换 +> swapAB 100 1 + +# 查看交换后的余额 +> balances +> reserves + +# 计算交换价格 +> calc 50 a2b + +# 退出 +> exit +``` + +## 滑点保护 + +所有涉及交换和流动性操作的命令都支持滑点保护参数: +- 滑点参数以百分比形式输入(例如:1 表示 1%) +- 如果不指定,默认使用 1% 的滑点保护 +- 滑点保护可以防止因价格变动导致的不利交易 + +## 注意事项 + +1. **网络连接**:确保本地 Hardhat 网络正在运行 +2. **代币授权**:在添加流动性或交换代币前,必须先授权代币给 AMM 合约 +3. **账户管理**:可以使用 `switch` 命令在不同账户间切换,模拟多用户操作 +4. **交易确认**:每个交易都会等待区块确认,可能需要几秒钟 +5. **错误处理**:如果操作失败,CLI 会显示详细的错误信息 + +## 故障排除 + +### 常见错误及解决方法 + +1. **"could not decode result data" 错误** + - 原因:CLI 工具连接的网络与合约部署的网络不一致 + - 解决:确保先运行 `npm run node`,然后运行 `npm run deploy`,最后运行 `npm run cli` + +2. **"合约地址文件不存在" 错误** + - 解决:运行 `npm run deploy` 重新部署合约 + +3. **"网络连接失败" 错误** + - 解决:确保本地网络正在运行 `npm run node` + +4. **"合约验证失败" 错误** + - 原因:合约可能没有正确部署或网络不匹配 + - 解决:重新部署合约 `npm run deploy` + +5. **其他问题** + - **代币不足**:使用 `mint` 命令铸造更多代币 + - **未授权代币**:使用 `approve` 命令授权代币 + - **滑点过大**:增加滑点保护参数或减少交换数量 + +### 完整重置流程 + +如果遇到无法解决的问题,可以完全重置: + +```bash +# 1. 停止当前网络 (Ctrl+C) +# 2. 重新启动网络 +npm run node + +# 3. 重新部署合约(新终端) +npm run deploy + +# 4. 启动 CLI +npm run cli +``` + +## 高级功能 + +- **批量测试**:使用 `npm run test-scenario` 运行完整的测试场景 +- **多账户操作**:使用 `switch` 命令模拟多用户环境 +- **价格影响分析**:通过连续小额交换观察价格变化 +- **流动性管理**:添加和移除流动性来观察对价格的影响 \ No newline at end of file diff --git a/contract-addresses.json b/contract-addresses.json index efa73c4..39d2350 100644 --- a/contract-addresses.json +++ b/contract-addresses.json @@ -1,6 +1,6 @@ { - "TokenA": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", - "TokenB": "0x0165878A594ca255338adfa4d48449f69242Eb8F", - "MiniSwapAMM": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "TokenA": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "TokenB": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "MiniSwapAMM": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", "deployer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" } \ No newline at end of file diff --git a/contracts/MiniSwapAMM.sol b/contracts/MiniSwapAMM.sol index ee064bb..481e5cf 100644 --- a/contracts/MiniSwapAMM.sol +++ b/contracts/MiniSwapAMM.sol @@ -116,8 +116,7 @@ contract MiniSwapAMM is ERC20, ReentrancyGuard { * @param amountBOutMin 最小输出的 tokenB 数量 * @return amountBOut 实际输出的 tokenB 数量 */ - function swapAForB(uint256 amountAIn, uint256 amountBOutMin) - external nonReentrant returns (uint256 amountBOut) { + function swapAForB(uint256 amountAIn, uint256 amountBOutMin) external nonReentrant returns (uint256 amountBOut) { require(amountAIn > 0, "MiniSwapAMM: INSUFFICIENT_INPUT_AMOUNT"); amountBOut = getAmountOut(amountAIn, reserveA, reserveB); diff --git a/package.json b/package.json index 7b6a8df..a275651 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ "deploy": "hardhat run scripts/deploy.js --network localhost", "node": "hardhat node", "dev": "cd frontend && npm start", - "build": "cd frontend && npm run build" + "build": "cd frontend && npm run build", + "cli": "hardhat run scripts/cli.js --network localhost", + "test-scenario": "hardhat run scripts/test-scenario.js --network localhost", + "test-connection": "hardhat run scripts/quick-test.js --network localhost" }, "keywords": ["defi", "dex", "amm", "ethereum", "solidity", "react"], "author": "", diff --git a/scripts/cli.js b/scripts/cli.js new file mode 100644 index 0000000..e596c7d --- /dev/null +++ b/scripts/cli.js @@ -0,0 +1,483 @@ +#!/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 - 切换到指定账户'); + log('yellow', ' balances - 查看当前账户余额'); + log('yellow', ' reserves - 查看 AMM 储备量'); + log('yellow', ' mint [amount] - 铸造代币 (默认 1000)'); + log('yellow', ' approve [amount] - 授权代币 (默认 10000)'); + log('yellow', ' add [slippage] - 添加流动性'); + log('yellow', ' remove [slippage] - 移除流动性'); + log('yellow', ' swapAB [slippage] - 交换 A->B'); + log('yellow', ' swapBA [slippage] - 交换 B->A'); + log('yellow', ' calc - 计算交换价格'); + 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 [slippage]'); + } else { + await this.addLiquidity(args[1], args[2], args[3] || "1"); + } + break; + case 'remove': + if (args.length < 2) { + log('red', '❌ 用法: remove [slippage]'); + } else { + await this.removeLiquidity(args[1], args[2] || "1"); + } + break; + case 'swapAB': + if (args.length < 2) { + log('red', '❌ 用法: swapAB [slippage]'); + } else { + await this.swapAForB(args[1], args[2] || "1"); + } + break; + case 'swapBA': + if (args.length < 2) { + log('red', '❌ 用法: swapBA [slippage]'); + } else { + await this.swapBForA(args[1], args[2] || "1"); + } + break; + case 'calc': + if (args.length < 3) { + log('red', '❌ 用法: calc '); + } 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; \ No newline at end of file diff --git a/scripts/quick-test.js b/scripts/quick-test.js new file mode 100644 index 0000000..4d091a2 --- /dev/null +++ b/scripts/quick-test.js @@ -0,0 +1,97 @@ +const { ethers } = require("hardhat"); +const fs = require("fs"); + +// 颜色输出 +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', + white: '\x1b[37m' +}; + +function log(color, message) { + console.log(`${colors[color]}${message}${colors.reset}`); +} + +async function main() { + log('cyan', '🔍 快速网络和合约连接测试'); + + try { + // 测试网络连接 + log('blue', '\n1. 测试网络连接...'); + const signers = await ethers.getSigners(); + const signer = signers[0]; + const network = await signer.provider.getNetwork(); + + log('green', `✅ 网络连接成功: ${network.name || 'localhost'} (Chain ID: ${network.chainId})`); + log('white', `账户地址: ${signer.address}`); + + const balance = await signer.provider.getBalance(signer.address); + log('white', `ETH 余额: ${ethers.formatEther(balance)}`); + + // 测试合约地址文件 + log('blue', '\n2. 检查合约地址文件...'); + if (!fs.existsSync("contract-addresses.json")) { + log('red', '❌ 合约地址文件不存在,请运行 npm run deploy'); + return; + } + + const addresses = JSON.parse(fs.readFileSync("contract-addresses.json", "utf8")); + log('green', '✅ 合约地址文件存在'); + log('white', `TokenA: ${addresses.TokenA}`); + log('white', `TokenB: ${addresses.TokenB}`); + log('white', `MiniSwapAMM: ${addresses.MiniSwapAMM}`); + + // 测试合约连接 + log('blue', '\n3. 测试合约连接...'); + + const TokenA = await ethers.getContractFactory("TokenA"); + const TokenB = await ethers.getContractFactory("TokenB"); + const MiniSwapAMM = await ethers.getContractFactory("MiniSwapAMM"); + + const tokenA = TokenA.attach(addresses.TokenA); + const tokenB = TokenB.attach(addresses.TokenB); + const amm = MiniSwapAMM.attach(addresses.MiniSwapAMM); + + // 测试基本调用 + const nameA = await tokenA.name(); + const nameB = await tokenB.name(); + const [reserveA, reserveB] = await amm.getReserves(); + + log('green', '✅ 合约连接成功'); + log('white', `TokenA 名称: ${nameA}`); + log('white', `TokenB 名称: ${nameB}`); + log('white', `AMM 储备: ${ethers.formatEther(reserveA)} TKA, ${ethers.formatEther(reserveB)} TKB`); + + // 测试余额查询 + log('blue', '\n4. 测试代币余额查询...'); + const balanceA = await tokenA.balanceOf(signer.address); + const balanceB = await tokenB.balanceOf(signer.address); + const lpBalance = await amm.balanceOf(signer.address); + + log('green', '✅ 余额查询成功'); + log('white', `TokenA 余额: ${ethers.formatEther(balanceA)} TKA`); + log('white', `TokenB 余额: ${ethers.formatEther(balanceB)} TKB`); + log('white', `LP Token 余额: ${ethers.formatEther(lpBalance)} MSLP`); + + log('cyan', '\n🎉 所有测试通过!CLI 工具应该可以正常工作了'); + log('yellow', '现在可以运行: npm run cli'); + + } catch (error) { + log('red', `❌ 测试失败: ${error.message}`); + log('yellow', '\n请检查:'); + log('white', '1. 本地网络是否正在运行: npm run node'); + log('white', '2. 合约是否已部署: npm run deploy'); + log('white', '3. 网络配置是否正确'); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); \ No newline at end of file diff --git a/scripts/test-scenario.js b/scripts/test-scenario.js new file mode 100644 index 0000000..2bd5ad4 --- /dev/null +++ b/scripts/test-scenario.js @@ -0,0 +1,231 @@ +const { ethers } = require("hardhat"); +const fs = require("fs"); + +// 颜色输出 +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', + white: '\x1b[37m' +}; + +function log(color, message) { + console.log(`${colors[color]}${message}${colors.reset}`); +} + +async function main() { + log('cyan', '🚀 开始 MiniSwap AMM 完整测试场景'); + + // 加载合约地址 + const addresses = JSON.parse(fs.readFileSync("contract-addresses.json", "utf8")); + + // 获取签名者 + const signers = await ethers.getSigners(); + const [deployer, user1, user2] = signers; + + // 连接到合约 + const TokenA = await ethers.getContractFactory("TokenA"); + const TokenB = await ethers.getContractFactory("TokenB"); + const MiniSwapAMM = await ethers.getContractFactory("MiniSwapAMM"); + + const tokenA = TokenA.attach(addresses.TokenA); + const tokenB = TokenB.attach(addresses.TokenB); + const amm = MiniSwapAMM.attach(addresses.MiniSwapAMM); + + log('blue', '📄 合约连接成功'); + + // 步骤 1: 显示初始状态 + log('yellow', '\n=== 步骤 1: 显示初始状态 ==='); + await showBalances('部署者', deployer, tokenA, tokenB, amm); + await showBalances('用户1', user1, tokenA, tokenB, amm); + await showReserves(amm); + + // 步骤 2: 为用户铸造代币 + log('yellow', '\n=== 步骤 2: 为用户铸造代币 ==='); + const mintAmount = ethers.parseEther("5000"); + + await tokenA.connect(user1).faucet(mintAmount); + await tokenB.connect(user1).faucet(mintAmount); + await tokenA.connect(user2).faucet(mintAmount); + await tokenB.connect(user2).faucet(mintAmount); + + log('green', '✅ 为用户1和用户2各铸造了 5000 TKA 和 5000 TKB'); + + // 步骤 3: 授权代币 + log('yellow', '\n=== 步骤 3: 授权代币给 AMM 合约 ==='); + const approveAmount = ethers.parseEther("10000"); + const ammAddress = await amm.getAddress(); + + await tokenA.connect(user1).approve(ammAddress, approveAmount); + await tokenB.connect(user1).approve(ammAddress, approveAmount); + await tokenA.connect(user2).approve(ammAddress, approveAmount); + await tokenB.connect(user2).approve(ammAddress, approveAmount); + + log('green', '✅ 用户1和用户2已授权代币给 AMM 合约'); + + // 步骤 4: 用户1添加初始流动性 + log('yellow', '\n=== 步骤 4: 用户1添加初始流动性 ==='); + const liquidityA = ethers.parseEther("1000"); + const liquidityB = ethers.parseEther("2000"); // 1:2 比例 + + const tx1 = await amm.connect(user1).addLiquidity( + liquidityA, + liquidityB, + liquidityA, + liquidityB + ); + await tx1.wait(); + + log('green', '✅ 用户1添加了 1000 TKA + 2000 TKB 的流动性'); + + await showBalances('用户1', user1, tokenA, tokenB, amm); + await showReserves(amm); + + // 步骤 5: 用户2进行交换 A -> B + log('yellow', '\n=== 步骤 5: 用户2进行交换 (A -> B) ==='); + const swapAmount = ethers.parseEther("100"); + + // 计算预期输出 + const [reserveA, reserveB] = await amm.getReserves(); + const expectedOut = await amm.getAmountOut(swapAmount, reserveA, reserveB); + + log('blue', `输入: 100 TKA`); + log('blue', `预期输出: ${ethers.formatEther(expectedOut)} TKB`); + + const tx2 = await amm.connect(user2).swapAForB(swapAmount, 0); + await tx2.wait(); + + log('green', '✅ 用户2完成交换'); + + await showBalances('用户2', user2, tokenA, tokenB, amm); + await showReserves(amm); + + // 步骤 6: 用户2进行反向交换 B -> A + log('yellow', '\n=== 步骤 6: 用户2进行反向交换 (B -> A) ==='); + const swapBackAmount = ethers.parseEther("50"); + + // 重新获取储备量 + const [newReserveA, newReserveB] = await amm.getReserves(); + const expectedOutBack = await amm.getAmountOut(swapBackAmount, newReserveB, newReserveA); + + log('blue', `输入: 50 TKB`); + log('blue', `预期输出: ${ethers.formatEther(expectedOutBack)} TKA`); + + const tx3 = await amm.connect(user2).swapBForA(swapBackAmount, 0); + await tx3.wait(); + + log('green', '✅ 用户2完成反向交换'); + + await showBalances('用户2', user2, tokenA, tokenB, amm); + await showReserves(amm); + + // 步骤 7: 用户1添加更多流动性 + log('yellow', '\n=== 步骤 7: 用户1添加更多流动性 ==='); + + // 根据当前比例添加流动性 + const [currentReserveA, currentReserveB] = await amm.getReserves(); + const addAmountA = ethers.parseEther("200"); + const addAmountB = (addAmountA * currentReserveB) / currentReserveA; + + log('blue', `添加 ${ethers.formatEther(addAmountA)} TKA + ${ethers.formatEther(addAmountB)} TKB`); + + const tx4 = await amm.connect(user1).addLiquidity( + addAmountA, + addAmountB, + addAmountA * 99n / 100n, // 1% 滑点 + addAmountB * 99n / 100n + ); + await tx4.wait(); + + log('green', '✅ 用户1添加了更多流动性'); + + await showBalances('用户1', user1, tokenA, tokenB, amm); + await showReserves(amm); + + // 步骤 8: 用户1移除部分流动性 + log('yellow', '\n=== 步骤 8: 用户1移除部分流动性 ==='); + + const lpBalance = await amm.balanceOf(user1.address); + const removeAmount = lpBalance / 4n; // 移除 1/4 的流动性 + + log('blue', `移除 ${ethers.formatEther(removeAmount)} LP 代币`); + + const tx5 = await amm.connect(user1).removeLiquidity( + removeAmount, + 0, // 简化起见,最小值设为0 + 0 + ); + await tx5.wait(); + + log('green', '✅ 用户1移除了部分流动性'); + + await showBalances('用户1', user1, tokenA, tokenB, amm); + await showReserves(amm); + + // 步骤 9: 多次小额交换测试价格影响 + log('yellow', '\n=== 步骤 9: 多次小额交换测试价格影响 ==='); + + for (let i = 1; i <= 3; i++) { + const smallSwap = ethers.parseEther("10"); + const tx = await amm.connect(user2).swapAForB(smallSwap, 0); + await tx.wait(); + + const [rA, rB] = await amm.getReserves(); + const price = Number(ethers.formatEther(rB)) / Number(ethers.formatEther(rA)); + log('blue', `第${i}次交换后,价格 (TKB/TKA): ${price.toFixed(6)}`); + } + + // 最终状态 + log('yellow', '\n=== 最终状态 ==='); + await showBalances('用户1', user1, tokenA, tokenB, amm); + await showBalances('用户2', user2, tokenA, tokenB, amm); + await showReserves(amm); + + log('cyan', '\n🎉 测试场景完成!'); + + // 显示费用收益 + log('yellow', '\n=== 交易费用分析 ==='); + const finalReserves = await amm.getReserves(); + log('white', '由于每次交换都有 0.3% 的手续费'); + log('white', '这些费用会留在流动性池中,增加流动性提供者的收益'); + log('white', `最终储备量已经包含了累积的交易费用`); +} + +async function showBalances(name, signer, tokenA, tokenB, amm) { + const address = signer.address; + const ethBalance = await signer.provider.getBalance(address); + const tokenABalance = await tokenA.balanceOf(address); + const tokenBBalance = await tokenB.balanceOf(address); + const lpBalance = await amm.balanceOf(address); + + log('cyan', `\n--- ${name} 余额 (${address.slice(0,8)}...) ---`); + log('white', `ETH: ${ethers.formatEther(ethBalance).slice(0,8)}`); + log('white', `TKA: ${ethers.formatEther(tokenABalance)}`); + log('white', `TKB: ${ethers.formatEther(tokenBBalance)}`); + log('white', `LP: ${ethers.formatEther(lpBalance)}`); +} + +async function showReserves(amm) { + const [reserveA, reserveB] = await amm.getReserves(); + const totalSupply = await amm.totalSupply(); + + log('cyan', '\n--- AMM 储备量 ---'); + log('white', `TKA 储备: ${ethers.formatEther(reserveA)}`); + log('white', `TKB 储备: ${ethers.formatEther(reserveB)}`); + log('white', `LP 总量: ${ethers.formatEther(totalSupply)}`); + + if (reserveA > 0 && reserveB > 0) { + const price = Number(ethers.formatEther(reserveB)) / Number(ethers.formatEther(reserveA)); + log('white', `价格 (TKB/TKA): ${price.toFixed(6)}`); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); \ No newline at end of file