feat: 添加 MiniSwap CLI 工具及相关文档
- 新增 CLI 工具,支持通过命令行与 MiniSwapAMM 合约交互 - 创建 CLI 使用指南文档,详细说明命令和操作流程 - 更新合约地址,确保与新部署的合约匹配 - 添加快速网络连接测试脚本,验证合约连接和网络状态 - 实现完整测试场景脚本,模拟用户操作和流动性管理
This commit is contained in:
190
CLI-USAGE.md
Normal file
190
CLI-USAGE.md
Normal file
@ -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 <index>` - 切换到指定账户(例如:`switch 1`)
|
||||||
|
- `balances` - 查看当前账户的所有代币余额
|
||||||
|
- `reserves` - 查看 AMM 池的储备量和价格信息
|
||||||
|
|
||||||
|
### 代币操作
|
||||||
|
|
||||||
|
- `mint [amount]` - 铸造测试代币(默认 1000 个)
|
||||||
|
- 例如:`mint 500` 铸造 500 个代币
|
||||||
|
- `approve [amount]` - 授权代币给 AMM 合约(默认 10000 个)
|
||||||
|
- 例如:`approve 5000` 授权 5000 个代币
|
||||||
|
|
||||||
|
### 流动性操作
|
||||||
|
|
||||||
|
- `add <amountA> <amountB> [slippage]` - 添加流动性
|
||||||
|
- 例如:`add 1000 2000 1` 添加 1000 TKA + 2000 TKB,允许 1% 滑点
|
||||||
|
- `remove <liquidity> [slippage]` - 移除流动性
|
||||||
|
- 例如:`remove 500 1` 移除 500 个 LP 代币,允许 1% 滑点
|
||||||
|
|
||||||
|
### 代币交换
|
||||||
|
|
||||||
|
- `swapAB <amount> [slippage]` - 将 TokenA 交换为 TokenB
|
||||||
|
- 例如:`swapAB 100 1` 用 100 TKA 交换 TKB,允许 1% 滑点
|
||||||
|
- `swapBA <amount> [slippage]` - 将 TokenB 交换为 TokenA
|
||||||
|
- 例如:`swapBA 50 1` 用 50 TKB 交换 TKA,允许 1% 滑点
|
||||||
|
|
||||||
|
### 价格计算
|
||||||
|
|
||||||
|
- `calc <amount> <direction>` - 计算交换价格
|
||||||
|
- `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` 命令模拟多用户环境
|
||||||
|
- **价格影响分析**:通过连续小额交换观察价格变化
|
||||||
|
- **流动性管理**:添加和移除流动性来观察对价格的影响
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"TokenA": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707",
|
"TokenA": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
|
||||||
"TokenB": "0x0165878A594ca255338adfa4d48449f69242Eb8F",
|
"TokenB": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
|
||||||
"MiniSwapAMM": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
|
"MiniSwapAMM": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0",
|
||||||
"deployer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
|
"deployer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
|
||||||
}
|
}
|
||||||
@ -116,8 +116,7 @@ contract MiniSwapAMM is ERC20, ReentrancyGuard {
|
|||||||
* @param amountBOutMin 最小输出的 tokenB 数量
|
* @param amountBOutMin 最小输出的 tokenB 数量
|
||||||
* @return amountBOut 实际输出的 tokenB 数量
|
* @return amountBOut 实际输出的 tokenB 数量
|
||||||
*/
|
*/
|
||||||
function swapAForB(uint256 amountAIn, uint256 amountBOutMin)
|
function swapAForB(uint256 amountAIn, uint256 amountBOutMin) external nonReentrant returns (uint256 amountBOut) {
|
||||||
external nonReentrant returns (uint256 amountBOut) {
|
|
||||||
require(amountAIn > 0, "MiniSwapAMM: INSUFFICIENT_INPUT_AMOUNT");
|
require(amountAIn > 0, "MiniSwapAMM: INSUFFICIENT_INPUT_AMOUNT");
|
||||||
|
|
||||||
amountBOut = getAmountOut(amountAIn, reserveA, reserveB);
|
amountBOut = getAmountOut(amountAIn, reserveA, reserveB);
|
||||||
|
|||||||
@ -9,7 +9,10 @@
|
|||||||
"deploy": "hardhat run scripts/deploy.js --network localhost",
|
"deploy": "hardhat run scripts/deploy.js --network localhost",
|
||||||
"node": "hardhat node",
|
"node": "hardhat node",
|
||||||
"dev": "cd frontend && npm start",
|
"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"],
|
"keywords": ["defi", "dex", "amm", "ethereum", "solidity", "react"],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
483
scripts/cli.js
Normal file
483
scripts/cli.js
Normal file
@ -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 <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;
|
||||||
97
scripts/quick-test.js
Normal file
97
scripts/quick-test.js
Normal file
@ -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);
|
||||||
|
});
|
||||||
231
scripts/test-scenario.js
Normal file
231
scripts/test-scenario.js
Normal file
@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user