From b1d3e07c368fa6b145c5fcc80f5bd00746c2353f Mon Sep 17 00:00:00 2001 From: lzx <393768033@qq.com> Date: Mon, 27 Oct 2025 16:27:33 +0800 Subject: [PATCH] update --- README.html | 1523 +++++++++++++++++ README.md | 1266 +++++--------- bin/config.json | 17 + go.mod | 12 +- go.sum | 49 +- internal/blockchain/blockchain.go | 14 +- internal/blockchain/eth/batch_transfer.go | 213 +++ .../blockchain/eth/batch_transfer_example.md | 101 ++ internal/blockchain/eth/eth.go | 211 ++- internal/db/sqlite.go | 120 ++ internal/logger/transaction_logger.go | 28 +- internal/msg/msg.go | 36 +- internal/server.go | 269 ++- public/SQLite3.sql | 71 + 14 files changed, 2982 insertions(+), 948 deletions(-) create mode 100644 README.html create mode 100644 internal/blockchain/eth/batch_transfer.go create mode 100644 internal/blockchain/eth/batch_transfer_example.md create mode 100644 internal/db/sqlite.go create mode 100644 public/SQLite3.sql diff --git a/README.html b/README.html new file mode 100644 index 0000000..a85ee1f --- /dev/null +++ b/README.html @@ -0,0 +1,1523 @@ + + + + + M2Pool Payment System v2 + + + + + + + + + + + + + +

M2Pool Payment System v2

+
+

基于以太坊区块链的分布式支付系统

+

Go Version +Ethereum +RabbitMQ +MySQL +License

+

支持 充值提现支付 三大核心功能,实时监听链上交易,自动确认到账。

+

基于 Go 1.24 + Ethereum + RabbitMQ + MySQL 技术栈构建的企业级支付解决方案。

+

快速开始项目架构功能特性性能测试常见问题

+
+
+

📋 目录

+ +
+

项目简介

+

M2Pool Payment System v2 是一个基于以太坊区块链的分布式支付解决方案,提供完整的数字货币充值、提现、支付功能。

+

核心能力

+ +

技术栈

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
组件技术版本
语言Go1.24+
区块链Ethereumgo-ethereum v1.16.4
消息队列RabbitMQamqp091-go v1.10.0
数据库MySQL8.0+
网络协议WebSocket + RPC-
+
+

项目架构

+

系统架构图

+
┌─────────────────────────────────────────────────────────────┐
+│                      业务系统                                │
+│            (Web/App/API Server)                             │
+└────────────┬────────────────────────────┬───────────────────┘
+             │                            │
+             │ 请求                        │ 响应
+             ↓                            ↑
+┌─────────────────────────────────────────────────────────────┐
+│                      RabbitMQ                                │
+│  ┌─────────┐  ┌──────────┐  ┌────────┐                     │
+│  │ topup   │  │ withdraw │  │  pay   │  请求队列            │
+│  └─────────┘  └──────────┘  └────────┘                     │
+│  ┌─────────┐  ┌──────────┐  ┌────────┐                     │
+│  │topup_   │  │withdraw_ │  │ pay_   │  响应队列            │
+│  │  resp   │  │   resp   │  │  resp  │                     │
+│  └─────────┘  └──────────┘  └────────┘                     │
+└────────────┬────────────────────────────┬───────────────────┘
+             │                            │
+             ↓                            ↑
+┌─────────────────────────────────────────────────────────────┐
+│              M2Pool Payment System v2                        │
+│                                                              │
+│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
+│  │ RabbitMQ     │  │ Blockchain   │  │  Database    │      │
+│  │ Consumer     │─>│   Manager    │─>│   (MySQL)    │      │
+│  └──────────────┘  └──────┬───────┘  └──────────────┘      │
+│                            │                                 │
+│                            │ WebSocket + RPC                 │
+└────────────────────────────┼─────────────────────────────────┘
+                             │
+                             ↓
+┌─────────────────────────────────────────────────────────────┐
+│                   以太坊区块链网络                            │
+│                                                              │
+│  ┌──────────────┐         ┌──────────────┐                 │
+│  │   新区块      │         │ USDT Transfer │                │
+│  │  (NewHead)   │         │    事件       │                │
+│  └──────────────┘         └──────────────┘                 │
+└─────────────────────────────────────────────────────────────┘
+
+

核心模块

+

1. Blockchain Manager (internal/blockchain/)

+ +

2. Message Queue (internal/queue/)

+ +

3. Database (internal/db/)

+ +

4. Message (internal/msg/)

+ +

5. Utils (internal/utils/)

+ +

6. Crypto (internal/crypto/)

+ +

7. Server (internal/server.go)

+ +

项目结构

+
m2pool-payment-v2/
+├── cmd/                      # 主程序入口
+│   └── main.go              # 程序入口,解析命令行参数
+├── internal/                 # 内部包(不对外暴露)
+│   ├── server.go            # 服务启动和管理
+│   ├── blockchain/          # 区块链交互模块
+│   │   ├── blockchain.go    # 统一的区块链接口定义
+│   │   ├── eth/             # 以太坊实现
+│   │   │   └── eth.go       # USDT 监听、转账、确认
+│   │   └── tron/            # TRON 实现(待开发)
+│   ├── crypto/              # 加密工具
+│   │   └── crypto.go        # SHA256、签名验证
+│   ├── db/                  # 数据库
+│   │   ├── db.go            # MySQL 连接池管理
+│   │   └── sqlite.go        # SQLite 本地存储
+│   ├── msg/                 # 消息定义
+│   │   └── msg.go           # 请求/响应结构体定义
+│   ├── queue/               # 消息队列
+│   │   ├── rabbitmq.go      # RabbitMQ 客户端封装
+│   │   └── README.md        # RabbitMQ 使用文档
+│   ├── logger/              # 日志记录
+│   │   └── transaction_logger.go  # 交易日志
+│   └── utils/               # 工具函数
+│       └── utils.go         # 类型转换、格式化
+├── public/                  # 公共资源
+│   ├── SQLite3.sql          # SQLite 表结构
+│   └── migration.sql        # 数据库迁移脚本
+├── test/                    # 测试和示例
+│   ├── test.go             # 测试程序(独立运行)
+│   └── config.json         # 配置文件
+├── go.mod                   # Go 模块定义
+├── go.sum                   # 依赖版本锁定
+└── README.md               # 项目文档(本文件)
+
+

核心特性详解

+

1. 双重监听机制 🎯

+

系统同时监听两种链上事件:

+

① USDT Transfer 事件监听

+
// 检测 USDT 转账,用于充值检测和交易确认触发
+e.WsClient.SubscribeFilterLogs(query, e.USDT.LogsChan)
+
+

② 新区块头监听

+
// 每个新区块触发交易确认检查,确保及时确认
+e.WsClient.SubscribeNewHead(e.Ctx, headers)
+
+

2. 智能交易确认 ⚡

+

事件驱动 + 区块驱动

+ +

3. 地址统一规范 🔡

+

所有以太坊地址统一转换为小写

+ +

避免大小写不一致导致的匹配失败。

+

4. 并发安全设计 🔒

+ +

5. 余额智能管理 💰

+

自动归集钱包切换:

+
用户钱包余额 < 转账金额
+    ↓
+自动使用归集钱包
+    ↓
+确保交易成功
+
+

6. Gas 费用检查 ⛽

+

转账前自动检查:

+ +
+

功能特性

+

1. 充值功能 💰

+
用户转账 → 实时检测 → 待确认通知 → 区块确认 → 最终通知
+
+

特点:

+ +

消息流:

+
    +
  1. 业务系统发送充值请求 → RabbitMQ
  2. +
  3. 系统添加地址监听
  4. +
  5. 用户转账 → 立即通知(status=2 待确认)
  6. +
  7. 等待 20 个区块 → 最终通知(status=1 成功 / 0 失败)
  8. +
+

2. 提现功能 💸

+
提现请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果
+
+

特点:

+ +

消息流:

+
    +
  1. 业务系统发送提现请求 → RabbitMQ
  2. +
  3. 系统验证余额并发送交易
  4. +
  5. 等待 20 个区块确认
  6. +
  7. 返回结果(status=1 成功 / 0 失败)
  8. +
+

3. 支付功能 💳

+
支付请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果
+
+

特点:

+ +

消息流:

+
    +
  1. 业务系统发送支付请求(含订单ID)→ RabbitMQ
  2. +
  3. 系统验证余额并发送交易
  4. +
  5. 等待 20 个区块确认
  6. +
  7. 返回结果(status=1 成功 / 0 失败)
  8. +
+
+

快速开始

+

前置条件

+ +

安装步骤

+
# 1. 克隆项目
+git clone <repository-url>
+cd m2pool-payment-v2
+
+# 2. 安装依赖
+go mod download
+
+# 3. 创建数据库
+mysql -u root -p
+
+
CREATE DATABASE payment CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+USE payment;
+
+-- 创建钱包余额表
+CREATE TABLE `eth_balance` (
+  `id` INT AUTO_INCREMENT PRIMARY KEY,
+  `address` VARCHAR(42) NOT NULL UNIQUE,
+  `private_key` VARCHAR(255) NOT NULL COMMENT '加密后的私钥',
+  `balance` DECIMAL(20, 8) DEFAULT 0,
+  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  INDEX `idx_address` (`address`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ETH钱包表';
+
+
# 4. 配置文件
+cd test
+cp config.json config.json.backup
+# 编辑 config.json,填入实际配置
+
+# 5. 编译主程序
+cd ..
+go build -o m2pool-payment cmd/main.go
+
+# 6. 运行(指定通信密钥)
+./m2pool-payment -key=your_secret_key
+
+# 或者运行测试程序
+cd test
+go run test.go
+
+

配置文件示例

+

创建 test/config.json

+
{
+    "rmq_config": {
+        "sub_addr": "amqp://m2pool:m2pool@localhost:5672",
+        "pay": {
+            "queue": "pay.auto.queue",
+            "exchange": "pay.exchange",
+            "routing": ["pay.auto.routing.key"]
+        },
+        "topup": {
+            "queue": "pay.recharge.queue",
+            "exchange": "pay.exchange",
+            "routing": ["pay.recharge.routing.key"]
+        },
+        "withdraw": {
+            "queue": "pay.withdraw.queue",
+            "exchange": "pay.exchange",
+            "routing": ["pay.withdraw.routing.key"]
+        },
+        "pay_resp": {
+            "queue": "pay.auto.return.queue",
+            "exchange": "pay.exchange",
+            "routing": ["pay.auto.return.routing.key"]
+        },
+        "topup_resp": {
+            "queue": "pay.recharge.return.queue",
+            "exchange": "pay.exchange",
+            "routing": ["pay.recharge.return.routing.key"]
+        },
+        "withdraw_resp": {
+            "queue": "pay.withdraw.return.queue",
+            "exchange": "pay.exchange",
+            "routing": ["pay.withdraw.return.routing.key"]
+        }
+    },
+    "eth_config": {
+        "rpcUrl": "http://localhost:8545",
+        "wsUrl": "ws://localhost:8546",
+        "confirmHeight": 20,
+        "dbConfig": {
+            "user": "root",
+            "password": "your_password",
+            "host": "127.0.0.1",
+            "port": 3306,
+            "database": "payment",
+            "maxOpenConns": 20,
+            "maxIdleCoons": 20,
+            "connMaxLife": 120
+        }
+    }
+}
+
+
+

配置说明

+

配置项说明

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
配置项说明默认值必填
rmq_config.sub_addrRabbitMQ 连接地址-
eth_config.rpcUrl以太坊 RPC 地址-
eth_config.wsUrl以太坊 WebSocket 地址-
eth_config.confirmHeight确认区块数20
dbConfig.user数据库用户名root
dbConfig.password数据库密码-
dbConfig.database数据库名称payment
+
+

使用示例

+

1. 充值流程

+

步骤 1:业务系统发送充值请求

+

发送到 RabbitMQ 队列:pay.recharge.queue

+
{
+    "chain": "ETH",
+    "symbol": "USDT",
+    "address": "0x4e5b2e1dc63f6b91cb6cd759936495434c7e972f",
+    "timestamp": 1758610297,
+    "sign": "signature_hash"
+}
+
+

步骤 2:用户转账

+

用户向指定地址转账 USDT

+

步骤 3:接收通知

+

从 RabbitMQ 队列:pay.recharge.return.queue 接收两次消息:

+

第一次(待确认):

+
{
+    "address": "0x4e5b2e1dc63f6b91cb6cd759936495434c7e972f",
+    "status": 2,
+    "chain": "ETH",
+    "symbol": "USDT",
+    "amount": 100.5,
+    "tx_hash": "0xabc..."
+}
+
+

第二次(最终确认):

+
{
+    "address": "0x4e5b2e1dc63f6b91cb6cd759936495434c7e972f",
+    "status": 1,
+    "chain": "ETH",
+    "symbol": "USDT",
+    "amount": 100.5,
+    "tx_hash": "0xabc..."
+}
+
+

2. 提现流程

+

步骤 1:业务系统发送提现请求

+

发送到 RabbitMQ 队列:pay.withdraw.queue

+
{
+    "queue_id": "withdraw_123",
+    "from_address": "0x1111...",
+    "to_address": "0x2222...",
+    "amount": 50.0,
+    "chain": "ETH",
+    "symbol": "USDT",
+    "timestamp": 1758610297,
+    "sign": "signature_hash"
+}
+
+

步骤 2:系统处理

+ +

步骤 3:接收通知

+

从 RabbitMQ 队列:pay.withdraw.return.queue 接收一次消息:

+
{
+    "queue_id": "withdraw_123",
+    "status": 1,
+    "amount": 50.0,
+    "chain": "ETH",
+    "symbol": "USDT",
+    "tx_hash": "0xdef..."
+}
+
+

3. 支付流程

+

步骤 1:业务系统发送支付请求

+

发送到 RabbitMQ 队列:pay.auto.queue

+
{
+    "queue_id": "pay_456",
+    "from_address": "0x1111...",
+    "to_address": "0x3333...",
+    "amount": 200.0,
+    "chain": "ETH",
+    "symbol": "USDT",
+    "order_id": "order_789",
+    "timestamp": 1758610297,
+    "sign": "signature_hash"
+}
+
+

步骤 2:系统处理

+ +

步骤 3:接收通知

+

从 RabbitMQ 队列:pay.auto.return.queue 接收一次消息:

+
{
+    "queue_id": "pay_456",
+    "status": 1,
+    "amount": 200.0,
+    "chain": "ETH",
+    "symbol": "USDT",
+    "order_id": "order_789",
+    "tx_hash": "0xghi..."
+}
+
+
+

常见问题

+

Q1: 为什么充值会收到两次通知?

+

A: 这是设计特性!

+ +

业务系统应该:

+ +

Q2: 提现/支付为什么只有一次通知?

+

A: 因为是系统主动发起的交易,用户已经知道在处理中,不需要额外的待确认通知。

+

Q3: 如何处理交易失败?

+

A: 系统会返回 status=0 的消息,业务系统应该:

+ +

Q4: 确认需要多长时间?

+

A: 配置为 20 个区块确认,以太坊约 12 秒/块:

+ +

Q5: 如何保证私钥安全?

+

A:

+
    +
  1. 私钥加密存储:数据库中存储加密后的私钥
  2. +
  3. 临时解密:仅在转账时临时解密,用完立即释放
  4. +
  5. 访问控制:数据库限制访问权限
  6. +
  7. 建议方案: + +
  8. +
+

⚠️ 重要:当前代码中的解密逻辑是占位代码,生产环境必须替换为真实的加密算法!

+

Q6: 余额不足时如何处理?

+

A: 系统会自动使用归集钱包转账。归集钱包应该:

+ +

Q7: 支持哪些网络?

+

A:

+ +

Q8: Gas 费用谁承担?

+

A:

+ +

建议:提现/支付时从用户金额中扣除 Gas 费

+
+

部署指南

+

Docker 部署(推荐)

+
# Dockerfile
+FROM golang:1.24-alpine AS builder
+
+WORKDIR /app
+COPY . .
+RUN go mod download
+RUN CGO_ENABLED=0 GOOS=linux go build -o m2pool-payment cmd/main.go
+
+FROM alpine:latest
+RUN apk --no-cache add ca-certificates
+WORKDIR /root/
+
+COPY --from=builder /app/m2pool-payment .
+COPY --from=builder /app/test/config.json .
+
+CMD ["./m2pool-payment"]
+
+
# docker-compose.yml
+version: '3.8'
+
+services:
+  payment:
+    build: .
+    depends_on:
+      - mysql
+      - rabbitmq
+    environment:
+      - CONFIG_PATH=/root/config.json
+    volumes:
+      - ./config.json:/root/config.json
+    restart: unless-stopped
+
+  mysql:
+    image: mysql:8.0
+    environment:
+      MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
+      MYSQL_DATABASE: payment
+    ports:
+      - "3306:3306"
+    volumes:
+      - mysql_data:/var/lib/mysql
+
+  rabbitmq:
+    image: rabbitmq:3-management
+    environment:
+      RABBITMQ_DEFAULT_USER: m2pool
+      RABBITMQ_DEFAULT_PASS: m2pool
+    ports:
+      - "5672:5672"
+      - "15672:15672"
+    volumes:
+      - rabbitmq_data:/var/lib/rabbitmq
+
+volumes:
+  mysql_data:
+  rabbitmq_data:
+
+
# 启动
+docker-compose up -d
+
+# 查看日志
+docker-compose logs -f payment
+
+# 停止
+docker-compose down
+
+

系统服务部署

+
# 1. 创建系统用户
+sudo useradd -r -s /bin/false m2pool
+
+# 2. 创建服务文件
+sudo nano /etc/systemd/system/m2pool-payment.service
+
+
[Unit]
+Description=M2Pool Payment System
+After=network.target mysql.service rabbitmq-server.service
+
+[Service]
+Type=simple
+User=m2pool
+WorkingDirectory=/opt/m2pool-payment
+ExecStart=/opt/m2pool-payment/m2pool-payment
+Restart=on-failure
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+
+
# 3. 启动服务
+sudo systemctl daemon-reload
+sudo systemctl enable m2pool-payment
+sudo systemctl start m2pool-payment
+
+# 4. 查看状态
+sudo systemctl status m2pool-payment
+
+# 5. 查看日志
+sudo journalctl -u m2pool-payment -f
+
+
+

性能测试

+

压测环境配置

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
配置项规格说明
CPU4核 2.4GHzIntel/AMD x64
内存8GB DDR4系统 + 缓存
网络100Mbps公网带宽
数据库MySQL 8.0本地部署
消息队列RabbitMQ 3.x本地部署
区块链节点Infura/Alchemy云端服务
+

预估压测结果

+

1. 充值功能压测

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
并发用户数TPS响应时间成功率说明
1050-80< 2秒99.9%轻量负载,性能优秀
50200-3002-5秒99.5%中等负载,性能良好
100300-5005-10秒99.0%高负载,性能稳定
200400-60010-20秒98.5%极限负载,性能下降
500+500-800> 20秒< 98%超负载,不建议
+

特点:

+ +

2. 提现功能压测

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
并发用户数TPS响应时间成功率说明
520-303-8秒99.8%轻量负载,性能优秀
2050-808-15秒99.0%中等负载,性能良好
5080-12015-30秒98.5%高负载,性能稳定
100100-15030-60秒97.0%极限负载,性能下降
200+120-200> 60秒< 95%超负载,不建议
+

特点:

+ +

3. 支付功能压测

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
并发用户数TPS响应时间成功率说明
520-303-8秒99.8%轻量负载,性能优秀
2050-808-15秒99.0%中等负载,性能良好
5080-12015-30秒98.5%高负载,性能稳定
100100-15030-60秒97.0%极限负载,性能下降
200+120-200> 60秒< 95%超负载,不建议
+

特点:

+ +

系统资源占用

+

内存使用情况

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
并发数基础内存峰值内存说明
10100MB150MB轻量运行
50200MB350MB中等负载
100300MB500MB高负载
200500MB800MB极限负载
+

CPU使用情况

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
并发数基础CPU峰值CPU说明
105%15%轻量运行
5015%35%中等负载
10025%50%高负载
20040%70%极限负载
+

网络带宽使用

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
并发数上行带宽下行带宽说明
101Mbps2Mbps轻量运行
503Mbps5Mbps中等负载
1005Mbps8Mbps高负载
2008Mbps12Mbps极限负载
+

性能瓶颈分析

+

1. 主要瓶颈

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
瓶颈类型影响程度解决方案
区块链确认时间🔴 高使用Layer2或侧链
数据库查询🟡 中添加索引,使用缓存
私钥解密🟡 中优化加密算法
网络延迟🟡 中使用CDN,就近部署
Gas费用波动🟡 中动态Gas价格调整
+

2. 优化建议

+

短期优化(1-2周):

+ +

中期优化(1-2月):

+ +

长期优化(3-6月):

+ +

压测工具推荐

+

1. 消息队列压测

+
# 使用 RabbitMQ 压测工具
+docker run --rm -it \
+  -e RABBITMQ_HOST=localhost \
+  -e RABBITMQ_PORT=5672 \
+  -e RABBITMQ_USER=m2pool \
+  -e RABBITMQ_PASS=m2pool \
+  rabbitmq-perf-test:latest \
+  --rate 100 --time 60 --queue pay.withdraw.queue
+
+

2. 数据库压测

+
# 使用 sysbench 压测 MySQL
+sysbench mysql \
+  --mysql-host=localhost \
+  --mysql-port=3306 \
+  --mysql-user=root \
+  --mysql-password=password \
+  --mysql-db=payment \
+  --tables=1 \
+  --table-size=10000 \
+  --threads=10 \
+  --time=60 \
+  run
+
+

3. 系统监控

+
# 监控系统资源
+htop                    # CPU和内存监控
+iotop                   # 磁盘IO监控
+nethogs                 # 网络带宽监控
+
+

生产环境建议

+

1. 推荐配置

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
环境CPU内存并发数说明
开发环境2核4GB< 10功能测试
测试环境4核8GB< 50压力测试
生产环境8核16GB< 100稳定运行
高可用环境16核32GB< 200负载均衡
+

2. 监控指标

+
// 关键监控指标
+type Metrics struct {
+    TotalTransactions   int64   // 总交易数
+    PendingTransactions int     // 待确认交易数
+    FailedTransactions  int64   // 失败交易数
+    ChannelUsage        int     // Channel使用率
+    LastBlockHeight     uint64  // 最新区块高度
+    MemoryUsage         int64   // 内存使用量
+    CPUUsage            float64 // CPU使用率
+}
+
+

3. 告警阈值

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
指标警告阈值严重阈值说明
待确认交易数> 100> 500交易积压
Channel使用率> 80%> 95%消息积压
交易失败率> 5%> 10%系统异常
内存使用率> 80%> 95%内存不足
CPU使用率> 70%> 90%CPU过载
+
+

重要修复说明

+

🔧 已修复的问题

+

1. QueueId 重复问题

+

问题:两笔不同的交易出现相同的 QueueId

+

原因:数据库表主键设计错误,使用 (from_addr, to_addr) 作为主键

+

修复

+ +

2. 重复发送响应问题

+

问题:提现和支付会发送两次响应

+

原因:转账失败时,先发送失败响应,然后仍然进入链上确认流程

+

修复

+ +

📊 修复后的消息发送次数

+ + + + + + + + + + + + + + + + + + + + + + + +
场景消息处理阶段链上确认阶段总发送次数
转账失败发送失败响应不进入(已return)1次
转账成功不发送响应发送成功响应1次
+
+

贡献指南

+

欢迎贡献代码!请遵循以下步骤:

+

提交流程

+
    +
  1. Fork 本项目
  2. +
  3. 创建功能分支 (git checkout -b feature/AmazingFeature)
  4. +
  5. 提交更改 (git commit -m 'feat: Add some AmazingFeature')
  6. +
  7. 推送到分支 (git push origin feature/AmazingFeature)
  8. +
  9. 开启 Pull Request
  10. +
+

代码规范

+ +

Commit 规范

+
# 新功能
+feat: 添加 BTC 网络支持
+
+# Bug 修复
+fix: 修复充值消息重复发送问题
+
+# 文档更新
+docs: 更新 API 文档
+
+# 性能优化
+perf: 优化交易确认性能
+
+# 代码重构
+refactor: 重构数据库连接池
+
+# 测试
+test: 添加单元测试
+
+
+

许可证

+

MIT License

+

Copyright (c) 2025 M2Pool Team

+

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+

The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.

+

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.

+
+
+

⭐ 如果这个项目对你有帮助,请给一个 Star!⭐

+

Made with ❤️ by M2Pool Team

+
+ + + + \ No newline at end of file diff --git a/README.md b/README.md index 61b53c2..ca4123a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ 基于 Go 1.24 + Ethereum + RabbitMQ + MySQL 技术栈构建的企业级支付解决方案。 -[快速开始](#快速开始) • [功能特性](#功能特性) • [架构设计](#架构设计) • [配置说明](#配置说明) +[快速开始](#快速开始) • [项目架构](#项目架构) • [功能特性](#功能特性) • [性能测试](#性能测试) • [常见问题](#常见问题) @@ -23,18 +23,15 @@ ## 📋 目录 - [项目简介](#项目简介) +- [项目架构](#项目架构) - [功能特性](#功能特性) -- [架构设计](#架构设计) - [快速开始](#快速开始) - [配置说明](#配置说明) - [使用示例](#使用示例) -- [API 文档](#api-文档) -- [开发指南](#开发指南) -- [部署指南](#部署指南) -- [性能指标](#性能指标) - [常见问题](#常见问题) +- [部署指南](#部署指南) +- [性能测试](#性能测试) - [贡献指南](#贡献指南) -- [许可证](#许可证) --- @@ -47,7 +44,7 @@ M2Pool Payment System v2 是一个基于以太坊区块链的**分布式支付 - 🔍 **实时监听**:订阅链上事件,实时检测 USDT 转账 - ⚡ **快速确认**:20 个区块确认,约 4-5 分钟到账 - 🔒 **安全可靠**:私钥加密存储,签名验证机制 -- 📊 **高并发**:支持少量并发(50-200 TPS) +- 📊 **高并发**:支持中等并发(50-200 TPS) - 🔄 **自动重连**:WebSocket 断开自动重连 - 📨 **消息队列**:基于 RabbitMQ 的异步通信 @@ -63,69 +60,7 @@ M2Pool Payment System v2 是一个基于以太坊区块链的**分布式支付 --- -## 功能特性 - -### 1. 充值功能 💰 - -``` -用户转账 → 实时检测 → 待确认通知 → 区块确认 → 最终通知 -``` - -**特点:** -- ✅ 实时检测到账 -- ✅ 发送**两次**通知:待确认 + 最终确认 -- ✅ 支持多币种(当前支持 USDT) -- ✅ 自动地址监听管理 - -**消息流:** -1. 业务系统发送充值请求 → RabbitMQ -2. 系统添加地址监听 -3. 用户转账 → 立即通知(status=2 待确认) -4. 等待 20 个区块 → 最终通知(status=1 成功 / 0 失败) - ---- - -### 2. 提现功能 💸 - -``` -提现请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果 -``` - -**特点:** -- ✅ 自动余额检查 -- ✅ 余额不足时使用归集钱包 -- ✅ 发送**一次**通知:最终确认 -- ✅ Gas 费用检查 - -**消息流:** -1. 业务系统发送提现请求 → RabbitMQ -2. 系统验证余额并发送交易 -3. 等待 20 个区块确认 -4. 返回结果(status=1 成功 / 0 失败) - ---- - -### 3. 支付功能 💳 - -``` -支付请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果 -``` - -**特点:** -- ✅ 订单关联 -- ✅ 自动余额检查 -- ✅ 发送**一次**通知:最终确认 -- ✅ 支持商户收款 - -**消息流:** -1. 业务系统发送支付请求(含订单ID)→ RabbitMQ -2. 系统验证余额并发送交易 -3. 等待 20 个区块确认 -4. 返回结果(status=1 成功 / 0 失败) - ---- - -## 架构设计 +## 项目架构 ### 系统架构图 @@ -218,6 +153,161 @@ M2Pool Payment System v2 是一个基于以太坊区块链的**分布式支付 - 消息路由和处理 - 优雅关闭 +### 项目结构 + +``` +m2pool-payment-v2/ +├── cmd/ # 主程序入口 +│ └── main.go # 程序入口,解析命令行参数 +├── internal/ # 内部包(不对外暴露) +│ ├── server.go # 服务启动和管理 +│ ├── blockchain/ # 区块链交互模块 +│ │ ├── blockchain.go # 统一的区块链接口定义 +│ │ ├── eth/ # 以太坊实现 +│ │ │ └── eth.go # USDT 监听、转账、确认 +│ │ └── tron/ # TRON 实现(待开发) +│ ├── crypto/ # 加密工具 +│ │ └── crypto.go # SHA256、签名验证 +│ ├── db/ # 数据库 +│ │ ├── db.go # MySQL 连接池管理 +│ │ └── sqlite.go # SQLite 本地存储 +│ ├── msg/ # 消息定义 +│ │ └── msg.go # 请求/响应结构体定义 +│ ├── queue/ # 消息队列 +│ │ ├── rabbitmq.go # RabbitMQ 客户端封装 +│ │ └── README.md # RabbitMQ 使用文档 +│ ├── logger/ # 日志记录 +│ │ └── transaction_logger.go # 交易日志 +│ └── utils/ # 工具函数 +│ └── utils.go # 类型转换、格式化 +├── public/ # 公共资源 +│ ├── SQLite3.sql # SQLite 表结构 +│ └── migration.sql # 数据库迁移脚本 +├── test/ # 测试和示例 +│ ├── test.go # 测试程序(独立运行) +│ └── config.json # 配置文件 +├── go.mod # Go 模块定义 +├── go.sum # 依赖版本锁定 +└── README.md # 项目文档(本文件) +``` + +### 核心特性详解 + +#### 1. 双重监听机制 🎯 + +系统同时监听两种链上事件: + +**① USDT Transfer 事件监听** +```go +// 检测 USDT 转账,用于充值检测和交易确认触发 +e.WsClient.SubscribeFilterLogs(query, e.USDT.LogsChan) +``` + +**② 新区块头监听** +```go +// 每个新区块触发交易确认检查,确保及时确认 +e.WsClient.SubscribeNewHead(e.Ctx, headers) +``` + +#### 2. 智能交易确认 ⚡ + +**事件驱动 + 区块驱动**: +- Transfer 事件到达时立即检查 +- 每个新区块产生时也检查 +- **确保交易在第 20 个区块后立即确认** + +#### 3. 地址统一规范 🔡 + +所有以太坊地址**统一转换为小写**: +- 存储时转换 +- 比较时转换 +- 查询时转换 + +避免大小写不一致导致的匹配失败。 + +#### 4. 并发安全设计 🔒 + +- `sync.Map` 用于高并发地址监听 +- `sync.Mutex` 保护共享数据结构 +- Channel 缓冲区防止阻塞 +- Goroutine panic 恢复机制 + +#### 5. 余额智能管理 💰 + +**自动归集钱包切换:** +``` +用户钱包余额 < 转账金额 + ↓ +自动使用归集钱包 + ↓ +确保交易成功 +``` + +#### 6. Gas 费用检查 ⛽ + +转账前自动检查: +- USDT 余额是否足够 +- ETH 余额是否足够支付 Gas +- 预估 Gas 价格 + +--- + +## 功能特性 + +### 1. 充值功能 💰 + +``` +用户转账 → 实时检测 → 待确认通知 → 区块确认 → 最终通知 +``` + +**特点:** +- ✅ 实时检测到账 +- ✅ 发送**两次**通知:待确认 + 最终确认 +- ✅ 支持多币种(当前支持 USDT) +- ✅ 自动地址监听管理 + +**消息流:** +1. 业务系统发送充值请求 → RabbitMQ +2. 系统添加地址监听 +3. 用户转账 → 立即通知(status=2 待确认) +4. 等待 20 个区块 → 最终通知(status=1 成功 / 0 失败) + +### 2. 提现功能 💸 + +``` +提现请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果 +``` + +**特点:** +- ✅ 自动余额检查 +- ✅ 余额不足时使用归集钱包 +- ✅ 发送**一次**通知:最终确认 +- ✅ Gas 费用检查 + +**消息流:** +1. 业务系统发送提现请求 → RabbitMQ +2. 系统验证余额并发送交易 +3. 等待 20 个区块确认 +4. 返回结果(status=1 成功 / 0 失败) + +### 3. 支付功能 💳 + +``` +支付请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果 +``` + +**特点:** +- ✅ 订单关联 +- ✅ 自动余额检查 +- ✅ 发送**一次**通知:最终确认 +- ✅ 支持商户收款 + +**消息流:** +1. 业务系统发送支付请求(含订单ID)→ RabbitMQ +2. 系统验证余额并发送交易 +3. 等待 20 个区块确认 +4. 返回结果(status=1 成功 / 0 失败) + --- ## 快速开始 @@ -284,37 +374,7 @@ go run test.go ```json { "rmq_config": { - "sub_addr": "amqp://m2pool:m2pool@localhost:5672" - // ... 其他配置见下文 - }, - "eth_config": { - "rpcUrl": "http://localhost:8545", - "wsUrl": "ws://localhost:8546", - "confirmHeight": 20, - "dbConfig": { - "user": "root", - "password": "your_password", - "host": "127.0.0.1", - "port": 3306, - "database": "payment", - "maxOpenConns": 20, - "maxIdleCoons": 20, - "connMaxLife": 120 - } - } -} -``` - ---- - -## 配置说明 - -### 配置文件结构 (config.json) - -```json -{ - "rmq_config": { - "sub_addr": "amqp://username:password@localhost:5672", + "sub_addr": "amqp://m2pool:m2pool@localhost:5672", "pay": { "queue": "pay.auto.queue", "exchange": "pay.exchange", @@ -364,6 +424,10 @@ go run test.go } ``` +--- + +## 配置说明 + ### 配置项说明 | 配置项 | 说明 | 默认值 | 必填 | @@ -428,8 +492,6 @@ go run test.go } ``` ---- - ### 2. 提现流程 **步骤 1:业务系统发送提现请求** @@ -471,8 +533,6 @@ go run test.go } ``` ---- - ### 3. 支付流程 **步骤 1:业务系统发送支付请求** @@ -518,271 +578,72 @@ go run test.go --- -## API 文档 +## 常见问题 -### 状态码说明 +### Q1: 为什么充值会收到两次通知? -| 状态码 | 常量名 | 说明 | 适用场景 | -|--------|--------|------|---------| -| `0` | STATUS_FAILED | 交易失败 | 交易被回退或执行失败 | -| `1` | STATUS_SUCCESS | 交易成功 | 交易成功并已确认 | -| `2` | STATUS_PENDING | 待确认 | 交易已检测到,等待区块确认 | -| `3` | STATUS_VERIFY_FAILED | 验证失败 | 签名验证失败 | +**A:** 这是设计特性! +- **第一次**(status=2):检测到交易,提醒用户"正在确认" +- **第二次**(status=1/0):交易确认,通知最终结果 -### 消息结构 +业务系统应该: +- status=2:显示进度,**不增加余额** +- status=1:增加余额 -#### 充值请求 (TopupMsg_req) +### Q2: 提现/支付为什么只有一次通知? -| 字段 | 类型 | 说明 | 必填 | -|------|------|------|------| -| chain | string | 链名称 (ETH) | ✅ | -| symbol | string | 币种 (USDT) | ✅ | -| address | string | 充值地址 | ✅ | -| timestamp | uint64 | 时间戳 | ✅ | -| sign | string | 签名 | ✅ | +**A:** 因为是系统主动发起的交易,用户已经知道在处理中,不需要额外的待确认通知。 -#### 充值响应 (TopupMsg_resp) +### Q3: 如何处理交易失败? -| 字段 | 类型 | 说明 | -|------|------|------| -| address | string | 充值地址 | -| status | int | 状态码 (0/1/2/3) | -| chain | string | 链名称 | -| symbol | string | 币种 | -| amount | float64 | 金额 | -| tx_hash | string | 交易哈希 | +**A:** 系统会返回 status=0 的消息,业务系统应该: +- 充值失败:不增加余额,提示用户联系客服 +- 提现失败:退回用户余额 +- 支付失败:恢复订单状态,退回余额 -#### 提现请求 (WithdrawMsg_req) +### Q4: 确认需要多长时间? -| 字段 | 类型 | 说明 | 必填 | -|------|------|------|------| -| queue_id | string | 队列ID | ✅ | -| from_address | string | 转出地址 | ✅ | -| to_address | string | 转入地址 | ✅ | -| amount | float64 | 金额 | ✅ | -| chain | string | 链名称 | ✅ | -| symbol | string | 币种 | ✅ | -| timestamp | uint64 | 时间戳 | ✅ | -| sign | string | 签名 | ✅ | +**A:** 配置为 20 个区块确认,以太坊约 12 秒/块: +- 理论时间:20 × 12 = 240 秒(4 分钟) +- 实际时间:4-5 分钟(包括网络延迟) -#### 提现响应 (WithdrawMsg_resp) +### Q5: 如何保证私钥安全? -| 字段 | 类型 | 说明 | -|------|------|------| -| queue_id | string | 队列ID | -| status | int | 状态码 | -| amount | float64 | 金额 | -| chain | string | 链名称 | -| symbol | string | 币种 | -| tx_hash | string | 交易哈希 | +**A:** +1. **私钥加密存储**:数据库中存储加密后的私钥 +2. **临时解密**:仅在转账时临时解密,用完立即释放 +3. **访问控制**:数据库限制访问权限 +4. **建议方案**: + - 使用 AES-256 加密私钥 + - 使用 HSM(硬件安全模块) + - 使用云服务商的 KMS(密钥管理服务) + - 使用环境变量传递解密密钥 -#### 支付请求 (PayMsg_req) +⚠️ **重要**:当前代码中的解密逻辑是**占位代码**,生产环境必须替换为真实的加密算法! -| 字段 | 类型 | 说明 | 必填 | -|------|------|------|------| -| queue_id | string | 队列ID | ✅ | -| from_address | string | 付款地址 | ✅ | -| to_address | string | 收款地址(商户) | ✅ | -| amount | float64 | 金额 | ✅ | -| chain | string | 链名称 | ✅ | -| symbol | string | 币种 | ✅ | -| order_id | string | 订单ID | ✅ | -| timestamp | uint64 | 时间戳 | ✅ | -| sign | string | 签名 | ✅ | +### Q6: 余额不足时如何处理? -#### 支付响应 (PayMsg_resp) +**A:** 系统会自动使用**归集钱包**转账。归集钱包应该: +- 保持足够的余额 +- 定期从各个钱包归集资金 +- 设置余额告警 -| 字段 | 类型 | 说明 | -|------|------|------| -| queue_id | string | 队列ID | -| status | int | 状态码 | -| amount | float64 | 金额 | -| chain | string | 链名称 | -| symbol | string | 币种 | -| order_id | string | 订单ID | -| tx_hash | string | 交易哈希 | +### Q7: 支持哪些网络? ---- +**A:** +- ✅ 以太坊主网(Mainnet) +- ✅ 以太坊测试网(Goerli, Sepolia) +- ✅ 私有链 +- ⚠️ 需要修改 USDT 合约地址 -## 开发指南 +### Q8: Gas 费用谁承担? -### 项目结构 +**A:** +- **充值**:用户承担(用户自己发送交易) +- **提现**:平台承担(系统发送交易) +- **支付**:平台承担(系统发送交易) -``` -m2pool-payment-v2/ -├── cmd/ # 主程序入口 -│ └── main.go # 程序入口,解析命令行参数 -├── internal/ # 内部包(不对外暴露) -│ ├── server.go # 服务启动和管理 -│ ├── blockchain/ # 区块链交互模块 -│ │ ├── blockchain.go # 统一的区块链接口定义 -│ │ ├── eth/ # 以太坊实现 -│ │ │ └── eth.go # USDT 监听、转账、确认 -│ │ └── tron/ # TRON 实现(待开发) -│ ├── crypto/ # 加密工具 -│ │ └── crypto.go # SHA256、签名验证 -│ ├── db/ # 数据库 -│ │ └── db.go # MySQL 连接池管理 -│ ├── msg/ # 消息定义 -│ │ └── msg.go # 请求/响应结构体定义 -│ ├── queue/ # 消息队列 -│ │ ├── rabbitmq.go # RabbitMQ 客户端封装 -│ │ └── README.md # RabbitMQ 使用文档 -│ └── utils/ # 工具函数 -│ └── utils.go # 类型转换、格式化 -├── test/ # 测试和示例 -│ ├── test.go # 测试程序(独立运行) -│ └── config.json # 配置文件 -├── go.mod # Go 模块定义 -├── go.sum # 依赖版本锁定 -└── README.md # 项目文档(本文件) -``` - -### 代码统计 - -| 模块 | 文件 | 代码行数 | 说明 | -|------|------|---------|------| -| **eth** | eth.go | ~700 | 以太坊核心逻辑 | -| **queue** | rabbitmq.go | ~350 | RabbitMQ 封装 | -| **server** | server.go | ~300 | 服务管理 | -| **msg** | msg.go | ~130 | 消息定义 | -| **blockchain** | blockchain.go | ~70 | 接口定义 | -| **其他** | - | ~200 | 工具、数据库等 | -| **总计** | - | **~1750** | - | - -### 开发环境设置 - -#### 方式一:Docker 快速启动(推荐) - -```bash -# 1. 启动 MySQL -docker run -d \ - --name m2pool-mysql \ - -p 3306:3306 \ - -e MYSQL_ROOT_PASSWORD=Lzx2021@! \ - -e MYSQL_DATABASE=payment \ - -v mysql_data:/var/lib/mysql \ - mysql:8.0 - -# 2. 启动 RabbitMQ -docker run -d \ - --name m2pool-rabbitmq \ - -p 5672:5672 \ - -p 15672:15672 \ - -e RABBITMQ_DEFAULT_USER=m2pool \ - -e RABBITMQ_DEFAULT_PASS=m2pool \ - -v rabbitmq_data:/var/lib/rabbitmq \ - rabbitmq:3-management - -# 3. 访问 RabbitMQ 管理界面 -# http://localhost:15672 -# 用户名: m2pool -# 密码: m2pool -``` - -#### 方式二:本地安装 - -```bash -# Ubuntu/Debian -sudo apt update -sudo apt install golang-1.24 mysql-server rabbitmq-server - -# CentOS/RHEL -sudo yum install golang mysql-server rabbitmq-server - -# macOS -brew install go mysql rabbitmq -``` - -#### 以太坊节点选择 - -**选项 1:使用公共节点服务(推荐)** -```bash -# Infura(免费层每天 100,000 请求) -RPC: https://mainnet.infura.io/v3/YOUR_API_KEY -WS: wss://mainnet.infura.io/ws/v3/YOUR_API_KEY - -# Alchemy(免费层每秒 25 请求) -RPC: https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY -WS: wss://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY -``` - -**选项 2:自建节点** -```bash -# 使用 Geth -geth --http --http.addr 0.0.0.0 --http.port 8545 \ - --ws --ws.addr 0.0.0.0 --ws.port 8546 \ - --http.api eth,net,web3 --ws.api eth,net,web3 - -# 或使用 Erigon(更轻量) -erigon --http --ws -``` - -**选项 3:测试网络** -```bash -# Goerli 测试网(免费) -RPC: https://goerli.infura.io/v3/YOUR_API_KEY -WS: wss://goerli.infura.io/ws/v3/YOUR_API_KEY - -# Sepolia 测试网(推荐) -RPC: https://sepolia.infura.io/v3/YOUR_API_KEY -WS: wss://sepolia.infura.io/ws/v3/YOUR_API_KEY -``` - -### 数据库表结构 - -```sql --- 钱包余额表(必须) -CREATE TABLE `eth_balance` ( - `id` INT AUTO_INCREMENT PRIMARY KEY, - `address` VARCHAR(42) NOT NULL UNIQUE COMMENT '钱包地址(小写)', - `private_key` VARCHAR(255) NOT NULL COMMENT '加密后的私钥', - `balance` DECIMAL(20, 8) DEFAULT 0 COMMENT 'USDT余额', - `eth_balance` DECIMAL(20, 18) DEFAULT 0 COMMENT 'ETH余额(用于Gas)', - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - INDEX `idx_address` (`address`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ETH钱包表'; - --- 插入归集钱包示例 -INSERT INTO `eth_balance` (`address`, `private_key`, `balance`, `eth_balance`) -VALUES ('归集钱包', 'encrypted_private_key_here', 10000.00, 1.0); - --- 交易记录表(可选,用于审计) -CREATE TABLE `transactions` ( - `id` BIGINT AUTO_INCREMENT PRIMARY KEY, - `tx_hash` VARCHAR(66) NOT NULL UNIQUE COMMENT '交易哈希', - `from_address` VARCHAR(42) NOT NULL COMMENT '发送地址', - `to_address` VARCHAR(42) NOT NULL COMMENT '接收地址', - `amount` DECIMAL(20, 8) NOT NULL COMMENT '金额', - `symbol` VARCHAR(10) NOT NULL COMMENT '币种', - `chain` VARCHAR(10) NOT NULL COMMENT '链名称', - `tx_type` TINYINT NOT NULL COMMENT '0=充值,1=提现,2=支付', - `status` TINYINT NOT NULL COMMENT '0=失败,1=成功,2=待确认', - `block_height` BIGINT COMMENT '区块高度', - `queue_id` VARCHAR(50) COMMENT '队列ID', - `order_id` VARCHAR(50) COMMENT '订单ID', - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `confirmed_at` TIMESTAMP NULL COMMENT '确认时间', - INDEX `idx_tx_hash` (`tx_hash`), - INDEX `idx_from` (`from_address`), - INDEX `idx_to` (`to_address`), - INDEX `idx_status` (`status`), - INDEX `idx_order_id` (`order_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易记录表'; -``` - -### 重要说明 - -⚠️ **私钥安全**: -- 数据库中存储的是**加密后的私钥** -- 需要实现真实的加密/解密逻辑 -- 当前代码中的解密逻辑是占位代码,需要替换为实际的加密算法(如 AES-256) - -⚠️ **归集钱包**: -- 必须在数据库中配置归集钱包 -- 地址字段填写 `"归集钱包"` 字符串 -- 保持足够的 USDT 和 ETH 余额 +建议:提现/支付时从用户金额中扣除 Gas 费 --- @@ -904,567 +765,238 @@ sudo journalctl -u m2pool-payment -f --- -## 性能指标 +## 性能测试 -### 处理能力 +### 压测环境配置 -| 指标 | 数值 | 说明 | -|------|------|------| -| **充值检测 TPS** | 500-1000 | 每秒处理交易数 | -| **提现/支付 TPS** | 200-500 | 包含数据库查询 | -| **消息发送 TPS** | 5000-10000 | RabbitMQ 发送速率 | -| **并发地址监听** | 10000+ | 同时监听的地址数量 | +| 配置项 | 规格 | 说明 | +|--------|------|------| +| **CPU** | 4核 2.4GHz | Intel/AMD x64 | +| **内存** | 8GB DDR4 | 系统 + 缓存 | +| **网络** | 100Mbps | 公网带宽 | +| **数据库** | MySQL 8.0 | 本地部署 | +| **消息队列** | RabbitMQ 3.x | 本地部署 | +| **区块链节点** | Infura/Alchemy | 云端服务 | -### 响应时间 +### 预估压测结果 -| 操作 | 响应时间 | 说明 | -|------|---------|------| -| **充值待确认通知** | < 3 秒 | 检测到交易后 | -| **充值最终确认** | 4-5 分钟 | 20 个区块 | -| **提现执行** | < 5 秒 | 发送交易 | -| **提现最终确认** | 4-5 分钟 | 20 个区块 | +#### 1. 充值功能压测 -### 资源占用(4核8G环境) +| 并发用户数 | TPS | 响应时间 | 成功率 | 说明 | +|-----------|-----|---------|--------|------| +| **10** | 50-80 | < 2秒 | 99.9% | 轻量负载,性能优秀 | +| **50** | 200-300 | 2-5秒 | 99.5% | 中等负载,性能良好 | +| **100** | 300-500 | 5-10秒 | 99.0% | 高负载,性能稳定 | +| **200** | 400-600 | 10-20秒 | 98.5% | 极限负载,性能下降 | +| **500+** | 500-800 | > 20秒 | < 98% | 超负载,不建议 | -| 资源 | 使用量 | 峰值 | -|------|--------|------| -| **CPU** | 5-15% | 30% | -| **内存** | 100-300 MB | 500 MB | -| **网络带宽** | 1-5 MB/s | 10 MB/s | -| **数据库连接** | 5-10 | 20 | +**特点:** +- ✅ 充值检测性能优秀(纯监听,无链上操作) +- ✅ 内存占用低(仅地址监听) +- ✅ 网络带宽消耗小 ---- +#### 2. 提现功能压测 -## 常见问题 +| 并发用户数 | TPS | 响应时间 | 成功率 | 说明 | +|-----------|-----|---------|--------|------| +| **5** | 20-30 | 3-8秒 | 99.8% | 轻量负载,性能优秀 | +| **20** | 50-80 | 8-15秒 | 99.0% | 中等负载,性能良好 | +| **50** | 80-120 | 15-30秒 | 98.5% | 高负载,性能稳定 | +| **100** | 100-150 | 30-60秒 | 97.0% | 极限负载,性能下降 | +| **200+** | 120-200 | > 60秒 | < 95% | 超负载,不建议 | -### Q1: 为什么充值会收到两次通知? +**特点:** +- ⚠️ 受链上交易确认时间影响(4-5分钟) +- ⚠️ 数据库查询和私钥解密开销 +- ⚠️ Gas费用波动影响成功率 -**A:** 这是设计特性! -- **第一次**(status=2):检测到交易,提醒用户"正在确认" -- **第二次**(status=1/0):交易确认,通知最终结果 +#### 3. 支付功能压测 -业务系统应该: -- status=2:显示进度,**不增加余额** -- status=1:增加余额 +| 并发用户数 | TPS | 响应时间 | 成功率 | 说明 | +|-----------|-----|---------|--------|------| +| **5** | 20-30 | 3-8秒 | 99.8% | 轻量负载,性能优秀 | +| **20** | 50-80 | 8-15秒 | 99.0% | 中等负载,性能良好 | +| **50** | 80-120 | 15-30秒 | 98.5% | 高负载,性能稳定 | +| **100** | 100-150 | 30-60秒 | 97.0% | 极限负载,性能下降 | +| **200+** | 120-200 | > 60秒 | < 95% | 超负载,不建议 | -### Q2: 提现/支付为什么只有一次通知? +**特点:** +- ⚠️ 与提现功能性能相近 +- ⚠️ 订单ID处理增加少量开销 +- ⚠️ 商户地址验证开销 -**A:** 因为是系统主动发起的交易,用户已经知道在处理中,不需要额外的待确认通知。 +### 系统资源占用 -### Q3: 如何处理交易失败? +#### 内存使用情况 -**A:** 系统会返回 status=0 的消息,业务系统应该: -- 充值失败:不增加余额,提示用户联系客服 -- 提现失败:退回用户余额 -- 支付失败:恢复订单状态,退回余额 +| 并发数 | 基础内存 | 峰值内存 | 说明 | +|--------|----------|----------|------| +| **10** | 100MB | 150MB | 轻量运行 | +| **50** | 200MB | 350MB | 中等负载 | +| **100** | 300MB | 500MB | 高负载 | +| **200** | 500MB | 800MB | 极限负载 | -### Q4: 确认需要多长时间? +#### CPU使用情况 -**A:** 配置为 20 个区块确认,以太坊约 12 秒/块: -- 理论时间:20 × 12 = 240 秒(4 分钟) -- 实际时间:4-5 分钟(包括网络延迟) +| 并发数 | 基础CPU | 峰值CPU | 说明 | +|--------|---------|---------|------| +| **10** | 5% | 15% | 轻量运行 | +| **50** | 15% | 35% | 中等负载 | +| **100** | 25% | 50% | 高负载 | +| **200** | 40% | 70% | 极限负载 | -### Q5: 如何保证私钥安全? +#### 网络带宽使用 -**A:** -1. **私钥加密存储**:数据库中存储加密后的私钥 -2. **临时解密**:仅在转账时临时解密,用完立即释放 -3. **访问控制**:数据库限制访问权限 -4. **建议方案**: - - 使用 AES-256 加密私钥 - - 使用 HSM(硬件安全模块) - - 使用云服务商的 KMS(密钥管理服务) - - 使用环境变量传递解密密钥 +| 并发数 | 上行带宽 | 下行带宽 | 说明 | +|--------|----------|----------|------| +| **10** | 1Mbps | 2Mbps | 轻量运行 | +| **50** | 3Mbps | 5Mbps | 中等负载 | +| **100** | 5Mbps | 8Mbps | 高负载 | +| **200** | 8Mbps | 12Mbps | 极限负载 | -⚠️ **重要**:当前代码中的解密逻辑(`eth.go` 第 467 行)是**占位代码**,生产环境必须替换为真实的加密算法! +### 性能瓶颈分析 -```go -// ❌ 当前代码(占位) -privateKey := encryptedKey + address + "" +#### 1. 主要瓶颈 -// ✅ 应该改为(示例) -privateKey := AES256Decrypt(encryptedKey, decodeKey) +| 瓶颈类型 | 影响程度 | 解决方案 | +|----------|----------|----------| +| **区块链确认时间** | 🔴 高 | 使用Layer2或侧链 | +| **数据库查询** | 🟡 中 | 添加索引,使用缓存 | +| **私钥解密** | 🟡 中 | 优化加密算法 | +| **网络延迟** | 🟡 中 | 使用CDN,就近部署 | +| **Gas费用波动** | 🟡 中 | 动态Gas价格调整 | + +#### 2. 优化建议 + +**短期优化(1-2周):** +- ✅ 添加数据库索引 +- ✅ 优化SQL查询语句 +- ✅ 增加Channel缓冲区大小 +- ✅ 使用连接池复用 + +**中期优化(1-2月):** +- 🔄 实现Redis缓存 +- 🔄 优化私钥加密算法 +- 🔄 添加负载均衡 +- 🔄 实现读写分离 + +**长期优化(3-6月):** +- 🚀 支持Layer2网络 +- 🚀 实现微服务架构 +- 🚀 添加水平扩展 +- 🚀 使用分布式数据库 + +### 压测工具推荐 + +#### 1. 消息队列压测 + +```bash +# 使用 RabbitMQ 压测工具 +docker run --rm -it \ + -e RABBITMQ_HOST=localhost \ + -e RABBITMQ_PORT=5672 \ + -e RABBITMQ_USER=m2pool \ + -e RABBITMQ_PASS=m2pool \ + rabbitmq-perf-test:latest \ + --rate 100 --time 60 --queue pay.withdraw.queue ``` -### Q6: 余额不足时如何处理? +#### 2. 数据库压测 -**A:** 系统会自动使用**归集钱包**转账。归集钱包应该: -- 保持足够的余额 -- 定期从各个钱包归集资金 -- 设置余额告警 - -### Q7: 支持哪些网络? - -**A:** -- ✅ 以太坊主网(Mainnet) -- ✅ 以太坊测试网(Goerli, Sepolia) -- ✅ 私有链 -- ⚠️ 需要修改 USDT 合约地址 - -### Q8: Gas 费用谁承担? - -**A:** -- **充值**:用户承担(用户自己发送交易) -- **提现**:平台承担(系统发送交易) -- **支付**:平台承担(系统发送交易) - -建议:提现/支付时从用户金额中扣除 Gas 费 - -### Q9: 如何监控系统状态? - -**A:** 建议监控: -- 待确认交易数量:`len(UnConfirmTxs)` -- Channel 使用率:`len(chainEventCh)/cap(chainEventCh)` -- RabbitMQ 连接状态 -- WebSocket 连接状态 -- 数据库连接池状态 - -### Q10: 如何处理重复消息? - -**A:** RabbitMQ 可能重复投递消息,业务系统应该: -1. 使用 `tx_hash` 作为唯一标识 -2. 实现幂等性处理 -3. 数据库添加唯一索引 - -### Q11: 支持的 USDT 合约地址是什么? - -**A:** 当前配置的合约地址: -- **以太坊主网**:`0xdAC17F958D2ee523a2206206994597C13D831ec7` -- **测试网**:需要部署测试 ERC20 合约 -- **私有链**:需要部署自己的 ERC20 合约 - -修改合约地址位置:`internal/blockchain/eth/eth.go` 第 99 行 - -### Q12: 新区块监听的作用是什么? - -**A:** 新区块监听确保交易及时确认: -- **问题**:如果只依赖 USDT Transfer 事件,在长时间无 USDT 转账时,待确认交易无法被确认 -- **解决**:监听新区块产生,每个新区块都检查待确认交易 -- **效果**:交易在达到第 20 个区块后,下一个区块就会被确认 - -### Q13: Channel 缓冲区设置多大? - -**A:** 当前设置为 1000: -```go -chainEventCh := make(chan any, 1000) +```bash +# 使用 sysbench 压测 MySQL +sysbench mysql \ + --mysql-host=localhost \ + --mysql-port=3306 \ + --mysql-user=root \ + --mysql-password=password \ + --mysql-db=payment \ + --tables=1 \ + --table-size=10000 \ + --threads=10 \ + --time=60 \ + run ``` -- **轻量负载**(<50 TPS):100 足够 -- **中等负载**(50-200 TPS):500-1000 推荐 -- **高负载**(>200 TPS):2000+ 或优化架构 +#### 3. 系统监控 -监控 Channel 使用率,避免满载丢消息。 +```bash +# 监控系统资源 +htop # CPU和内存监控 +iotop # 磁盘IO监控 +nethogs # 网络带宽监控 +``` ---- +### 生产环境建议 -## 安全建议 +#### 1. 推荐配置 -### 🔒 安全检查清单 +| 环境 | CPU | 内存 | 并发数 | 说明 | +|------|-----|------|--------|------| +| **开发环境** | 2核 | 4GB | < 10 | 功能测试 | +| **测试环境** | 4核 | 8GB | < 50 | 压力测试 | +| **生产环境** | 8核 | 16GB | < 100 | 稳定运行 | +| **高可用环境** | 16核 | 32GB | < 200 | 负载均衡 | -- [ ] 私钥加密存储 -- [ ] 使用 HTTPS/WSS 连接 -- [ ] 签名验证所有请求 -- [ ] 限制 API 访问频率 -- [ ] 定期备份数据库 -- [ ] 监控异常交易 -- [ ] 设置余额告警 -- [ ] 使用防火墙限制访问 -- [ ] 定期更新依赖包 -- [ ] 日志脱敏处理 - -### ⚠️ 重要提示 - -1. **私钥管理** - - ❌ 不要在代码中硬编码私钥 - - ❌ 不要在日志中打印私钥 - - ✅ 使用环境变量或密钥管理服务 - -2. **网络安全** - - ✅ 使用 VPN 或专线连接区块链节点 - - ✅ RabbitMQ 启用 TLS - - ✅ MySQL 限制远程访问 - -3. **资金安全** - - ✅ 设置单笔交易限额 - - ✅ 异常交易人工审核 - - ✅ 多签钱包(建议) - - ✅ 冷热钱包分离 - ---- - -## 监控告警 - -### 推荐监控指标 +#### 2. 监控指标 ```go -// 添加监控指标 +// 关键监控指标 type Metrics struct { - TotalTransactions int64 // 总交易数 - PendingTransactions int // 待确认交易数 - FailedTransactions int64 // 失败交易数 - ChannelUsage int // Channel使用率 - LastBlockHeight uint64 // 最新区块高度 -} - -// 定期上报 -go func() { - ticker := time.NewTicker(1 * time.Minute) - for range ticker.C { - log.Printf("📊 待确认交易: %d", len(e.UnConfirmTxs)) - log.Printf("📊 监听地址数: %d", countAddresses()) - log.Printf("📊 Channel使用率: %d%%", len(ch)*100/cap(ch)) - } -}() -``` - -### 告警规则建议 - -| 指标 | 阈值 | 告警级别 | -|------|------|---------| -| 待确认交易数 | > 100 | ⚠️ 警告 | -| 待确认交易数 | > 500 | 🔴 严重 | -| Channel 使用率 | > 80% | ⚠️ 警告 | -| Channel 使用率 | > 95% | 🔴 严重 | -| 交易失败率 | > 5% | ⚠️ 警告 | -| WebSocket 断线 | 重连 > 3次/小时 | ⚠️ 警告 | - ---- - -## 故障排查 - -### 问题:充值检测不到 - -**可能原因:** -1. 地址未加入监听列表 -2. WebSocket 连接断开 -3. 合约地址配置错误 -4. 网络 ID 不匹配 - -**排查步骤:** -```bash -# 检查日志 -grep "新增钱包监听消息" logs/payment.log - -# 检查订阅状态 -grep "订阅成功" logs/payment.log - -# 测试节点连接 -curl -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ - http://localhost:8545 -``` - -### 问题:提现/支付失败 - -**可能原因:** -1. 余额不足 -2. Gas 费不足 -3. 私钥错误 -4. nonce 冲突 - -**排查步骤:** -```bash -# 检查错误日志 -grep "转账失败" logs/payment.log - -# 检查余额 -grep "余额" logs/payment.log - -# 检查私钥 -grep "查询私钥" logs/payment.log -``` - -### 问题:消息未返回 - -**可能原因:** -1. RabbitMQ 连接断开 -2. Channel 阻塞 -3. 交易未确认 - -**排查步骤:** -```bash -# 检查 RabbitMQ 连接 -rabbitmqctl list_connections - -# 检查队列状态 -rabbitmqctl list_queues - -# 检查待确认交易 -# 添加 HTTP 接口查询 UnConfirmTxs -``` - ---- - -## 注意事项 - -### ⚠️ 生产环境部署前必须修改 - -#### 1. 私钥加密实现(重要!) - -**位置**:`internal/blockchain/eth/eth.go` 第 467 行 - -**当前代码**(占位): -```go -privateKey := encryptedKey + address + "" -``` - -**必须改为**(示例): -```go -// 使用 AES-256-GCM 加密 -import "crypto/aes" -import "crypto/cipher" - -func (e *ETHNode) decodePrivatekey(address string) string { - // 从数据库查询加密密钥 - encryptedKey := queryFromDB(address) - - // 使用 AES 解密 - privateKey := AESDecrypt(encryptedKey, e.decodeKey) - - return privateKey + TotalTransactions int64 // 总交易数 + PendingTransactions int // 待确认交易数 + FailedTransactions int64 // 失败交易数 + ChannelUsage int // Channel使用率 + LastBlockHeight uint64 // 最新区块高度 + MemoryUsage int64 // 内存使用量 + CPUUsage float64 // CPU使用率 } ``` -#### 2. USDT 合约地址验证 +#### 3. 告警阈值 -确认你的以太坊网络与合约地址匹配: - -| 网络 | Chain ID | USDT 合约地址 | -|------|----------|--------------| -| 主网 | 1 | `0xdAC17F958D2ee523a2206206994597C13D831ec7` ✅ | -| Goerli | 5 | 需要部署测试合约 | -| Sepolia | 11155111 | 需要部署测试合约 | -| 私有链 | 自定义 | 需要部署自己的合约 | - -**修改位置**:`internal/blockchain/eth/eth.go` 第 99 行 - -#### 3. 消息签名密钥 - -**当前密钥**:`9f3c7a12`(测试用) - -**生产环境**: -```bash -# 生成强密钥 -openssl rand -hex 32 - -# 启动时传入 -./m2pool-payment -key=your_production_secret_key -``` - -#### 4. 数据库密码安全 - -- ❌ 不要在代码中硬编码密码 -- ✅ 使用环境变量 -- ✅ 使用配置管理工具(如 Vault) +| 指标 | 警告阈值 | 严重阈值 | 说明 | +|------|----------|----------|------| +| **待确认交易数** | > 100 | > 500 | 交易积压 | +| **Channel使用率** | > 80% | > 95% | 消息积压 | +| **交易失败率** | > 5% | > 10% | 系统异常 | +| **内存使用率** | > 80% | > 95% | 内存不足 | +| **CPU使用率** | > 70% | > 90% | CPU过载 | --- -## 贡献指南 +## 重要修复说明 -欢迎贡献代码!请遵循以下步骤: +### 🔧 已修复的问题 -1. Fork 本项目 -2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) -3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) -4. 推送到分支 (`git push origin feature/AmazingFeature`) -5. 开启 Pull Request +#### 1. QueueId 重复问题 -### 代码规范 +**问题**:两笔不同的交易出现相同的 QueueId -- ✅ 使用 `gofmt` 格式化代码 -- ✅ 遵循 Go 命名规范 -- ✅ 添加必要的注释 -- ✅ 所有地址统一小写处理 -- ✅ 使用状态码常量(不要硬编码数字) -- ✅ 添加错误处理和日志 -- ✅ 更新相关文档 +**原因**:数据库表主键设计错误,使用 `(from_addr, to_addr)` 作为主键 -### 提交规范 +**修复**: +- 修改数据库表结构,将主键改为 `queueId` +- 创建数据库迁移脚本 `public/migration.sql` +- 修复 SQL 插入语句的参数数量不匹配问题 -```bash -# 功能 -feat: 添加 BTC 网络支持 +#### 2. 重复发送响应问题 -# 修复 -fix: 修复充值消息重复发送问题 +**问题**:提现和支付会发送两次响应 -# 文档 -docs: 更新 API 文档 +**原因**:转账失败时,先发送失败响应,然后仍然进入链上确认流程 -# 性能 -perf: 优化交易确认性能 +**修复**: +- 在转账失败时添加 `return` 语句 +- 确保转账失败时不进入链上确认流程 +- 只有转账成功才会进入链上监听和确认 -# 重构 -refactor: 重构数据库连接池 -``` +### 📊 修复后的消息发送次数 ---- - -## 命令行参数 - -```bash -# 启动程序 -./m2pool-payment -key=your_secret_key - -# 参数说明 --key string - 通信密钥,用于消息签名验证 (默认: "m2pool") -``` - -### 签名验证算法 - -```go -// 生成签名 -hash := SHA256(hex(timestamp) + secret_key) -sign := hex.EncodeToString(hash) - -// 示例 -timestamp = 1758610297 -secret_key = "9f3c7a12" -hash = SHA256("696a2d6929" + "9f3c7a12") -sign = "219b3b3935f3d56db7eacd32aae84fa06df95806373d6fc4ed6e9b35ffb17f2d" -``` - ---- - -## 运行日志示例 - -### 启动日志 -``` -======================================== -🚀 M2Pool Payment System Starting... -======================================== -✅ 配置加载成功: RPC=http://10.168.3.236:18545, WS=ws://10.168.3.236:18546 -✅ 区块链服务初始化完成 -✅ RabbitMQ服务初始化完成: amqp://m2pool:m2pool@localhost:5672 -✅ RabbitMQ 监听启动完成 -======================================== -🎉 所有服务启动完成! -======================================== -🔍 ETH 开始监听 USDT Transfer 事件... -🔍 开始监听新区块... -✅ 订阅成功 -✅ 新区块订阅成功 -``` - -### 充值日志 -``` -📥 [RMQ] 收到充值请求: Chain=ETH, Symbol=USDT, Address=0x123... -📨 [链上] 充值待确认: Address=0x123..., Amount=100.50, TxHash=0xabc... -📤 [RMQ] 发送充值响应: Address=0x123..., Status=2, TxHash=0xabc... -✅ [链上] 充值确认: Address=0x123..., Amount=100.50, TxHash=0xabc..., Status=1 -📤 [RMQ] 发送充值响应: Address=0x123..., Status=1, TxHash=0xabc... -``` - -### 提现日志 -``` -📥 [RMQ] 收到提现请求: QueueId=w123, From=0x111..., To=0x222..., Amount=50.00 USDT -✅ [链上] 提现确认: QueueId=w123, Amount=50.00, TxHash=0xdef..., Status=1 -📤 [RMQ] 发送提现响应: QueueId=w123, Status=1, TxHash=0xdef... -``` - ---- - -## 路线图 - -### ✅ v2.0 (已完成) -- [x] 以太坊 USDT 支持 -- [x] 充值/提现/支付功能 -- [x] RabbitMQ 集成 -- [x] 双重监听机制 -- [x] 自动交易确认 -- [x] 地址统一规范 -- [x] Gas 费用检查 -- [x] Panic 恢复机制 - -### 🚧 v2.1 (开发中) -- [ ] 支持更多 ERC20 代币(USDC, DAI) -- [ ] 交易记录持久化 -- [ ] HTTP API 接口 -- [ ] 私钥真实加密实现 -- [ ] 性能优化(读写锁、缓存) - -### 📋 v2.2 (计划中) -- [ ] 支持 TRON 网络(TRC20-USDT) -- [ ] 支持 BTC 网络 -- [ ] 多签钱包支持 -- [ ] 管理后台界面 -- [ ] 实时监控面板 -- [ ] 告警系统 - -### 🔮 v3.0 (规划中) -- [ ] 微服务架构拆分 -- [ ] 水平扩展支持 -- [ ] 分布式事务 -- [ ] 高可用集群部署 -- [ ] Kubernetes 支持 - ---- - -## 核心特性详解 - -### 1. 双重监听机制 🎯 - -系统同时监听两种链上事件: - -**① USDT Transfer 事件监听** -```go -// 检测 USDT 转账,用于充值检测和交易确认触发 -e.WsClient.SubscribeFilterLogs(query, e.USDT.LogsChan) -``` - -**② 新区块头监听** -```go -// 每个新区块触发交易确认检查,确保及时确认 -e.WsClient.SubscribeNewHead(e.Ctx, headers) -``` - -### 2. 智能交易确认 ⚡ - -**事件驱动 + 区块驱动**: -- Transfer 事件到达时立即检查 -- 每个新区块产生时也检查 -- **确保交易在第 20 个区块后立即确认** - -### 3. 地址统一规范 🔡 - -所有以太坊地址**统一转换为小写**: -- 存储时转换 -- 比较时转换 -- 查询时转换 - -避免大小写不一致导致的匹配失败。 - -### 4. 并发安全设计 🔒 - -- `sync.Map` 用于高并发地址监听 -- `sync.Mutex` 保护共享数据结构 -- Channel 缓冲区防止阻塞 -- Goroutine panic 恢复机制 - -### 5. 余额智能管理 💰 - -**自动归集钱包切换:** -``` -用户钱包余额 < 转账金额 - ↓ -自动使用归集钱包 - ↓ -确保交易成功 -``` - -### 6. Gas 费用检查 ⛽ - -转账前自动检查: -- USDT 余额是否足够 -- ETH 余额是否足够支付 Gas -- 预估 Gas 价格 - ---- - -## 相关文档 - -- [RabbitMQ 使用说明](internal/queue/README.md) +| 场景 | 消息处理阶段 | 链上确认阶段 | 总发送次数 | +|------|-------------|-------------|-----------| +| **转账失败** | 发送失败响应 | 不进入(已return) | **1次** | +| **转账成功** | 不发送响应 | 发送成功响应 | **1次** | --- @@ -1514,16 +1046,6 @@ test: 添加单元测试 --- -## 技术支持 - -如有问题,欢迎通过以下方式联系: - -- 📧 Email: support@example.com -- 💬 Issues: [GitHub Issues](https://github.com/your-repo/issues) -- 📖 文档: [项目 Wiki](https://github.com/your-repo/wiki) - ---- - ## 许可证 MIT License @@ -1556,6 +1078,4 @@ SOFTWARE. Made with ❤️ by M2Pool Team - - - + \ No newline at end of file diff --git a/bin/config.json b/bin/config.json index c9b55ce..2b8b565 100644 --- a/bin/config.json +++ b/bin/config.json @@ -1,4 +1,7 @@ { + "sqlite3": { + "msg_path": "./msg.db" + }, "rmq_config": { "sub_addr": "amqp://m2pool:m2pool@localhost:5672", "pay": { @@ -22,6 +25,13 @@ "pay.withdraw.routing.key" ] }, + "remove": { + "queue": "pay.remove.queue", + "exchange": "pay.exchange", + "routing": [ + "pay.remove.routing.key" + ] + }, "pay_resp": { "queue": "pay.auto.return.queue", "exchange": "pay.exchange", @@ -42,6 +52,13 @@ "routing": [ "pay.withdraw.return.routing.key" ] + }, + "remove_resp": { + "queue": "pay.remove.return.queue", + "exchange": "pay.exchange", + "routing": [ + "pay.remove.return.routing.key" + ] } }, "eth_config": { diff --git a/go.mod b/go.mod index 95ef2b6..dd39cdc 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,17 @@ require ( require ( github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect google.golang.org/protobuf v1.36.6 // indirect + modernc.org/libc v1.66.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) require ( @@ -36,6 +45,7 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect golang.org/x/crypto v0.36.0 // indirect - golang.org/x/sync v0.12.0 // indirect + golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.36.0 // indirect + modernc.org/sqlite v1.39.1 ) diff --git a/go.sum b/go.sum index ceafd51..8dac093 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,8 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/ethereum/c-kzg-4844/v2 v2.1.3 h1:DQ21UU0VSsuGy8+pcMJHDS0CV1bKmJmxsJYK8l3MiLU= @@ -75,6 +77,10 @@ github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXi github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= @@ -115,6 +121,8 @@ github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxd github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= @@ -141,6 +149,8 @@ github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJf github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -169,12 +179,15 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= -golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= @@ -183,6 +196,8 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= @@ -191,3 +206,29 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4= +modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/blockchain/blockchain.go b/internal/blockchain/blockchain.go index 99f6f52..7dbe6ff 100644 --- a/internal/blockchain/blockchain.go +++ b/internal/blockchain/blockchain.go @@ -6,8 +6,8 @@ import ( ) type IChainServer interface { - AddAddress(address string, msg any) - RemoveAddress(address string) + AddAddress(address string, msg any) error + RemoveAddress(address string) error Listen(symbol string, ch chan any) Transfer(symbol string, msg any) error Stop() @@ -30,21 +30,23 @@ func (b *BlockChainServer) RegisterChain(name string, chain IChainServer) { b.chains[name] = chain } -func (b *BlockChainServer) AddAddress(chain, address string, msg any) { +func (b *BlockChainServer) AddAddress(chain, address string, msg any) error { if srv, ok := b.chains[chain]; ok { srv.AddAddress(address, msg) fmt.Printf("✅ 添加监听地址: chain=%s, address=%s\n", chain, address) + return nil } else { - fmt.Printf("⚠️ 链未注册: %s\n", chain) + return fmt.Errorf("⚠️ 链未注册: %s\n", chain) } } -func (b *BlockChainServer) RemoveAddress(chain, address string) { +func (b *BlockChainServer) RemoveAddress(chain, address string) error { if srv, ok := b.chains[chain]; ok { srv.RemoveAddress(address) fmt.Printf("🗑️ 移除监听地址: chain=%s, address=%s\n", chain, address) + return nil } else { - fmt.Printf("⚠️ 链未注册: %s\n", chain) + return fmt.Errorf("⚠️ 链未注册: %s\n", chain) } } diff --git a/internal/blockchain/eth/batch_transfer.go b/internal/blockchain/eth/batch_transfer.go new file mode 100644 index 0000000..f82d5e1 --- /dev/null +++ b/internal/blockchain/eth/batch_transfer.go @@ -0,0 +1,213 @@ +package eth + +import ( + "crypto/ecdsa" + "fmt" + "log" + "m2pool-payment/internal/utils" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" +) + +// BatchTransferItem 批量转账单项 +type BatchTransferItem struct { + ToAddress string // 接收地址 + Amount float64 // 转账金额 +} + +// BatchTransferResult 批量转账结果 +type BatchTransferResult struct { + TxHash string // 交易哈希 + Success bool // 是否成功 + TotalAmount float64 // 总转账金额 + Count int // 转账笔数 +} + +// usdt_batch_transfer 批量转账ERC20-USDT +// from: 发送地址 +// items: 批量转账列表 +// returns: 交易哈希和错误信息 +func (e *ETHNode) USDTBatchTransfer(from string, items []BatchTransferItem) (*BatchTransferResult, error) { + if len(items) == 0 { + return nil, fmt.Errorf("批量转账列表不能为空") + } + + // 统一转换为小写 + from = strings.ToLower(from) + + // 计算总金额 + var totalAmount float64 + for _, item := range items { + if item.Amount <= 0 { + return nil, fmt.Errorf("转账金额必须大于0") + } + totalAmount += item.Amount + } + + // 1. 校验钱包USDT余额 + balance, err := e.getUSDTBalance(from) + log.Printf("🔄 批量转账 - 检测钱包=%s,余额=%.2f USDT", from, balance) + if err != nil { + return nil, fmt.Errorf("获取余额失败: %w", err) + } + + if balance < totalAmount { + return nil, fmt.Errorf("余额不足: 余额=%.2f USDT < 需要=%.2f USDT", balance, totalAmount) + } + + // 2. 通过from地址前往数据库查找出对应加密后的私钥,并解密真实的私钥 + originalKey := e.decodePrivatekey(from) + if originalKey == "" { + return nil, fmt.Errorf("无法获取私钥") + } + + privateKey, err := crypto.HexToECDSA(originalKey) + if err != nil { + return nil, fmt.Errorf("解析私钥失败: %w", err) + } + + // 3. 获得nonce + nonce, err := e.RpcClient.PendingNonceAt(e.Ctx, common.HexToAddress(from)) + if err != nil { + return nil, fmt.Errorf("获取nonce失败: %w", err) + } + + // 4. 构造批量转账数据 + // 使用 transfer(address[], uint256[]) 或多次transfer调用 + // 这里使用多次transfer调用的方式,因为标准ERC20没有批量转账方法 + + // 方法1: 构造多次transfer调用(适合少量转账) + if len(items) <= 3 { + return e.batchTransferMultipleCalls(from, privateKey, nonce, items) + } + + // 方法2: 使用合约批量转账(需要部署代理合约,这里简化处理) + // 注意:这里实现的是多次transaction方式 + return e.batchTransferSeparateTransactions(from, privateKey, nonce, items) +} + +// batchTransferMultipleCalls 使用一个交易多次调用transfer(需要gas优化) +func (e *ETHNode) batchTransferMultipleCalls(from string, privateKey *ecdsa.PrivateKey, nonce uint64, items []BatchTransferItem) (*BatchTransferResult, error) { + // 注意:标准ERC20不支持批量transfer,这里需要自定义合约 + // 或者使用多次独立交易 + log.Printf("⚠️ 标准ERC20不支持批量transfer,改用多次独立交易") + + // 回退到多次独立交易 + return e.batchTransferSeparateTransactions(from, privateKey, nonce, items) +} + +// batchTransferSeparateTransactions 执行多次独立的transfer交易 +func (e *ETHNode) batchTransferSeparateTransactions(from string, privateKey *ecdsa.PrivateKey, nonce uint64, items []BatchTransferItem) (*BatchTransferResult, error) { + var totalAmount float64 + var txHashes []string + var allSuccess bool = true + + for i, item := range items { + // 构造单个transfer交易 + amountBigInt := utils.Float64ToBigIntUSDT(item.Amount) + data, err := e.USDT.ABI.Pack("transfer", common.HexToAddress(strings.ToLower(item.ToAddress)), amountBigInt) + if err != nil { + log.Printf("❌ 批量转账第%d笔打包失败: %v", i+1, err) + allSuccess = false + continue + } + + // 获取gas limit + gasLimit, err := e.getGasLimit() + if err != nil { + log.Printf("❌ 批量转账第%d笔获取gasLimit失败: %v", i+1, err) + allSuccess = false + continue + } + + // 获取gas费用 + maxFeePerGas, maxPriorityFeePerGas, err := e.getEIP1559GasFees() + + var txHash string + if err != nil { + // 回退到传统gas price + gasPrice, err := e.getSuggestGasPrice() + if err != nil { + log.Printf("❌ 批量转账第%d笔获取gasPrice失败: %v", i+1, err) + allSuccess = false + continue + } + + tx := types.NewTransaction(nonce+uint64(i), e.USDT.Address, big.NewInt(0), gasLimit, gasPrice, data) + signedTx, err := types.SignTx(tx, types.NewEIP155Signer(e.NetId), privateKey) + if err != nil { + log.Printf("❌ 批量转账第%d笔签名失败: %v", i+1, err) + allSuccess = false + continue + } + + txHash = signedTx.Hash().Hex() + err = e.RpcClient.SendTransaction(e.Ctx, signedTx) + if err != nil { + log.Printf("❌ 批量转账第%d笔发送失败: %v", i+1, err) + allSuccess = false + continue + } + } else { + // 使用EIP-1559交易 + ethBalance, err := e.getETHBlance(from) + if err != nil { + log.Printf("❌ 批量转账第%d笔获取ETH余额失败: %v", i+1, err) + allSuccess = false + continue + } + + maxGasCost := new(big.Int).Mul(new(big.Int).SetUint64(gasLimit), maxFeePerGas) + if ethBalance.Cmp(maxGasCost) == -1 { + log.Printf("❌ 批量转账第%d笔ETH余额不足", i+1) + allSuccess = false + continue + } + + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: e.NetId, + Nonce: nonce + uint64(i), + GasTipCap: maxPriorityFeePerGas, + GasFeeCap: maxFeePerGas, + Gas: gasLimit, + To: &e.USDT.Address, + Value: big.NewInt(0), + Data: data, + }) + + signedTx, err := types.SignTx(tx, types.NewLondonSigner(e.NetId), privateKey) + if err != nil { + log.Printf("❌ 批量转账第%d笔签名失败: %v", i+1, err) + allSuccess = false + continue + } + + txHash = signedTx.Hash().Hex() + err = e.RpcClient.SendTransaction(e.Ctx, signedTx) + if err != nil { + log.Printf("❌ 批量转账第%d笔发送失败: %v", i+1, err) + allSuccess = false + continue + } + } + + txHashes = append(txHashes, txHash) + totalAmount += item.Amount + log.Printf("✅ 批量转账第%d笔已提交: %s, 金额=%.2f USDT, 收款地址=%s", + i+1, txHash, item.Amount, strings.ToLower(item.ToAddress)) + } + + log.Printf("📊 批量转账完成: 总计%d笔, 成功%d笔, 总金额=%.2f USDT", + len(items), len(txHashes), totalAmount) + + return &BatchTransferResult{ + TxHash: strings.Join(txHashes, ","), + Success: allSuccess && len(txHashes) == len(items), + TotalAmount: totalAmount, + Count: len(txHashes), + }, nil +} diff --git a/internal/blockchain/eth/batch_transfer_example.md b/internal/blockchain/eth/batch_transfer_example.md new file mode 100644 index 0000000..49fc196 --- /dev/null +++ b/internal/blockchain/eth/batch_transfer_example.md @@ -0,0 +1,101 @@ +# ERC20-USDT 批量转账功能 + +## 功能说明 + +该文件 `batch_transfer.go` 提供了 ERC20-USDT 的批量转账功能,支持从同一个发送地址向多个不同的接收地址转账。 + +## 主要功能 + +### 1. 批量转账类型 + +```go +type BatchTransferItem struct { + ToAddress string // 接收地址 + Amount float64 // 转账金额 +} + +type BatchTransferResult struct { + TxHash string // 交易哈希(多个用逗号分隔) + Success bool // 是否成功 + TotalAmount float64 // 总转账金额 + Count int // 转账笔数 +} +``` + +### 2. 使用方法 + +```go +// 1. 准备批量转账列表 +items := []eth.BatchTransferItem{ + {ToAddress: "0xRecipient1", Amount: 100.0}, + {ToAddress: "0xRecipient2", Amount: 200.0}, + {ToAddress: "0xRecipient3", Amount: 50.0}, +} + +// 2. 调用批量转账 +fromAddress := "0xYourAddress" +result, err := ethNode.USDTBatchTransfer(fromAddress, items) +if err != nil { + log.Fatalf("批量转账失败: %v", err) +} + +// 3. 处理结果 +fmt.Printf("批量转账完成: %d笔, 总金额: %.2f USDT", result.Count, result.TotalAmount) +fmt.Printf("交易哈希: %s", result.TxHash) +``` + +## 工作原理 + +由于标准 ERC20 合约不支持批量转账,本实现采用以下策略: + +1. **多次独立交易**:对每笔转账创建一个独立的 ERC20 `transfer` 交易 +2. **Nonce 管理**:自动管理 nonce,确保交易按顺序广播 +3. **Gas 费用**:支持 EIP-1559 动态费用和传统 gas price +4. **错误处理**:单笔失败不影响其他交易,返回成功和失败的详细统计 + +## 注意事项 + +### 1. Gas 费用 + +- 每笔转账需要独立的 gas 费用(约 65,000 gas) +- 批量转账 10 笔需要约 650,000 gas +- 确保发送地址有足够的 ETH 作为 gas 费用 + +### 2. 余额检查 + +- 函数会自动检查 USDT 余额是否足够 +- 如果余额不足,会返回错误并终止转账 + +### 3. 部分成功 + +- 如果某些转账失败,函数会继续执行其他转账 +- 返回结果中包含成功笔数和详细交易哈希 + +### 4. 网络拥堵 + +- 在高网络拥堵时,某些交易可能被推迟 +- 建议监控所有交易状态 + +## 性能优化建议 + +如果需要更高效的批量转账,考虑: + +1. **部署批量转账代理合约**:实现一个合约方法 `batchTransfer(address[] to, uint256[] amounts)` +2. **使用多签钱包**:减少私钥管理风险 +3. **Gas 优化**:使用更低的 gas price 分批发送 + +## 示例输出 + +``` +🔄 批量转账 - 检测钱包=0x...,余额=1000.00 USDT +✅ 批量转账第1笔已提交: 0xabc123..., 金额=100.00 USDT, 收款地址=0x... +✅ 批量转账第2笔已提交: 0xdef456..., 金额=200.00 USDT, 收款地址=0x... +✅ 批量转账第3笔已提交: 0x789ghi..., 金额=50.00 USDT, 收款地址=0x... +📊 批量转账完成: 总计3笔, 成功3笔, 总金额=350.00 USDT +``` + +## 限制 + +- 标准 ERC20 不支持真正的批量转账(单笔交易) +- 需要确保发送地址有足够的 ETH 作为 gas 费用 +- 交易按顺序发送,可能在高负载时较慢 diff --git a/internal/blockchain/eth/eth.go b/internal/blockchain/eth/eth.go index c367c53..82c5884 100644 --- a/internal/blockchain/eth/eth.go +++ b/internal/blockchain/eth/eth.go @@ -125,27 +125,25 @@ func NewETHNode(cfg message.ETHConfig, decodeKey string) (*ETHNode, error) { } // ============================ 抽象接口 ============================ -func (e *ETHNode) AddAddress(address string, rmq_msg any) { +func (e *ETHNode) AddAddress(address string, rmq_msg any) error { // 统一转换为小写 address = strings.ToLower(address) log.Printf("新增钱包监听消息:%v", rmq_msg) e.ListenAddresses.Store(address, true) e.mu.Lock() - if len(e.RmqMsgs[address]) == 0 { - e.RmqMsgs[address] = []any{rmq_msg} - } else { - e.RmqMsgs[address] = append(e.RmqMsgs[address], rmq_msg) - } + e.RmqMsgs[address] = append(e.RmqMsgs[address], rmq_msg) e.mu.Unlock() + return nil } -func (e *ETHNode) RemoveAddress(address string) { +func (e *ETHNode) RemoveAddress(address string) error { // 统一转换为小写 address = strings.ToLower(address) e.ListenAddresses.Delete(address) e.mu.Lock() delete(e.RmqMsgs, address) e.mu.Unlock() + return nil } func (e *ETHNode) Listen(symbol string, ch chan any) { @@ -174,6 +172,10 @@ func (e *ETHNode) Transfer(symbol string, msg any) error { return nil } +func (e *ETHNode) Stop() { + e.Cancel() +} + // ============================ rpc节点方法 ============================ func (e *ETHNode) getETHBlance(address string) (*big.Int, error) { @@ -221,12 +223,11 @@ func (e *ETHNode) getUSDTBalance(address string) (float64, error) { return bal, nil } -func (e *ETHNode) getBlockHeight() (uint64, error) { - header, err := e.RpcClient.HeaderByNumber(e.Ctx, nil) - if err != nil { - return 0, fmt.Errorf("failed to get latest block header: %w", err) - } - return header.Number.Uint64(), nil +func (e *ETHNode) getGasLimit() (uint64, error) { + // 对于ERC20转账,使用固定的gas limit + // 通常ERC20 transfer需要约65,000-100,000 gas + // 这里设置为80,000,足够覆盖大部分情况 + return 80000, nil } func (e *ETHNode) getSuggestGasPrice() (*big.Int, error) { @@ -235,9 +236,56 @@ func (e *ETHNode) getSuggestGasPrice() (*big.Int, error) { if err != nil { return nil, fmt.Errorf("get suggest-gasprice error:%v", err) } + + // 设置gas price上限,避免在网络拥堵时费用过高 + // 这里设置为20 Gwei (20 * 10^9 wei) + maxGasPrice := new(big.Int).SetUint64(20000000000) // 20 Gwei + + if gasPrice.Cmp(maxGasPrice) > 0 { + log.Printf("⚠️ 建议gas price过高 (%v Gwei),使用上限 20 Gwei", new(big.Int).Div(gasPrice, big.NewInt(1000000000))) + return maxGasPrice, nil + } + + log.Printf("✅ 使用建议gas price: %v Gwei", new(big.Int).Div(gasPrice, big.NewInt(1000000000))) return gasPrice, nil } +// getEIP1559GasFees 获取EIP-1559的gas费用参数 +func (e *ETHNode) getEIP1559GasFees() (*big.Int, *big.Int, error) { + ctx := context.Background() + + // 获取基础费用 + latestBlock, err := e.RpcClient.BlockByNumber(ctx, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to get latest block: %w", err) + } + + baseFee := latestBlock.BaseFee() + if baseFee == nil { + return nil, nil, fmt.Errorf("base fee not available") + } + + // 设置优先级费用(tip),这里设置为2 Gwei + maxPriorityFeePerGas := new(big.Int).SetUint64(2000000000) // 2 Gwei + + // 计算最大费用 = 基础费用 + 优先级费用 + maxFeePerGas := new(big.Int).Add(baseFee, maxPriorityFeePerGas) + + // 设置最大费用上限为30 Gwei + maxFeeLimit := new(big.Int).SetUint64(30000000000) // 30 Gwei + if maxFeePerGas.Cmp(maxFeeLimit) > 0 { + log.Printf("⚠️ 计算的最大费用过高 (%v Gwei),使用上限 30 Gwei", new(big.Int).Div(maxFeePerGas, big.NewInt(1000000000))) + maxFeePerGas = maxFeeLimit + } + + log.Printf("✅ EIP-1559 Gas费用: BaseFee=%v Gwei, MaxPriorityFee=%v Gwei, MaxFee=%v Gwei", + new(big.Int).Div(baseFee, big.NewInt(1000000000)), + new(big.Int).Div(maxPriorityFeePerGas, big.NewInt(1000000000)), + new(big.Int).Div(maxFeePerGas, big.NewInt(1000000000))) + + return maxFeePerGas, maxPriorityFeePerGas, nil +} + // ============================ 业务方法 ============================ func (e *ETHNode) listen_usdt(ch chan any) error { fmt.Println("🔍 ETH 开始监听 USDT Transfer 事件...") @@ -257,6 +305,7 @@ func (e *ETHNode) listen_usdt(ch chan any) error { fmt.Println("✅ 订阅成功") // 处理事件 for { + select { case err := <-sub.Err(): fmt.Println("⚠️ 订阅异常,准备重连:", err) @@ -578,7 +627,6 @@ func (e *ETHNode) decodePrivatekey(address string) string { } // 使用key解密 privateKey := encryptedKey // 实际使用时替换成具体的解密代码 - // fmt.Println(privateKey) return privateKey } @@ -624,7 +672,6 @@ func (e *ETHNode) usdt_transfer(msg any) error { if originalKey == "" { return fmt.Errorf("failed to query privatekey") } - fmt.Println(originalKey) privateKey, err := crypto.HexToECDSA(originalKey) if err != nil { return fmt.Errorf("failed to parse private key: %w", err) @@ -640,41 +687,125 @@ func (e *ETHNode) usdt_transfer(msg any) error { if err != nil { return fmt.Errorf("failed to pack transfer data: %w", err) } - gasPrice, err := e.getSuggestGasPrice() // 获得当前建议gasPrice + gasLimit, err := e.getGasLimit() // 获得gasLimit if err != nil { - return fmt.Errorf("get suggest-gasprice error:%v", err) + return fmt.Errorf("get gas limit error:%v", err) } - eth_balance, err := e.getETHBlance(final_from) // 获得钱包eth余额 + + // 获取EIP-1559 gas费用参数 + maxFeePerGas, maxPriorityFeePerGas, err := e.getEIP1559GasFees() + if err != nil { + log.Printf("⚠️ 获取EIP-1559费用失败,回退到传统gas price: %v", err) + // 回退到传统gas price + gasPrice, err := e.getSuggestGasPrice() + if err != nil { + return fmt.Errorf("get suggest-gasprice error:%v", err) + } + + eth_balance, err := e.getETHBlance(final_from) + if err != nil { + return fmt.Errorf("%w", err) + } + + gasLimit_b := new(big.Int).SetUint64(gasLimit) + gas := new(big.Int).Mul(gasLimit_b, gasPrice) + + // 计算gas费用(以ETH为单位) + gasInETH := new(big.Float).SetInt(gas) + gasInETH.Quo(gasInETH, new(big.Float).SetInt64(1000000000000000000)) + + log.Printf("💰 传统Gas费用预估: Limit=%d, Price=%v Gwei, 总费用=%.6f ETH", + gasLimit, + new(big.Int).Div(gasPrice, big.NewInt(1000000000)), + gasInETH) + + // 判断钱包eth是否支持本次交易gas费用 + if eth_balance.Cmp(gas) == -1 { + ethBalanceInETH := new(big.Float).SetInt(eth_balance) + ethBalanceInETH.Quo(ethBalanceInETH, new(big.Float).SetInt64(1000000000000000000)) + return fmt.Errorf("❌ 地址 %s ETH余额不足: %.6f ETH < %.6f ETH (gas费用)", + final_from, ethBalanceInETH, gasInETH) + } + + // 构造传统交易 + tx := types.NewTransaction( + nonce, + e.USDT.Address, + big.NewInt(0), + gasLimit, + gasPrice, + data, + ) + + // 签名并发送传统交易 + signedTx, err := types.SignTx(tx, types.NewEIP155Signer(e.NetId), privateKey) + if err != nil { + return fmt.Errorf("failed to sign transaction: %w", err) + } + + txHash := signedTx.Hash().Hex() + err = e.RpcClient.SendTransaction(e.Ctx, signedTx) + if err != nil { + return fmt.Errorf("failed to send transaction: %w", err) + } + + log.Printf("✅ 传统交易已提交至mempool:%s,金额:%.2f USDT, 手续费:%.6f ETH", txHash, amount, gasInETH) + return nil + } + + // 使用EIP-1559交易 + eth_balance, err := e.getETHBlance(final_from) if err != nil { return fmt.Errorf("%w", err) } - var gasLimit uint64 = 100000 - gasLimit_b := new(big.Int).SetUint64(gasLimit) - gas := new(big.Int).Mul(gasLimit_b, gasPrice) + + // 计算最大可能的gas费用 + maxGasCost := new(big.Int).Mul(new(big.Int).SetUint64(gasLimit), maxFeePerGas) + + // 计算gas费用(以ETH为单位) + maxGasCostInETH := new(big.Float).SetInt(maxGasCost) + maxGasCostInETH.Quo(maxGasCostInETH, new(big.Float).SetInt64(1000000000000000000)) + + log.Printf("💰 EIP-1559 Gas费用预估: Limit=%d, MaxFee=%v Gwei, MaxPriorityFee=%v Gwei, 最大费用=%.6f ETH", + gasLimit, + new(big.Int).Div(maxFeePerGas, big.NewInt(1000000000)), + new(big.Int).Div(maxPriorityFeePerGas, big.NewInt(1000000000)), + maxGasCostInETH) + // 判断钱包eth是否支持本次交易gas费用 - if eth_balance.Cmp(gas) == -1 { - return fmt.Errorf("address=%s balance less than gas=%v(wei)", final_from, eth_balance) + if eth_balance.Cmp(maxGasCost) == -1 { + ethBalanceInETH := new(big.Float).SetInt(eth_balance) + ethBalanceInETH.Quo(ethBalanceInETH, new(big.Float).SetInt64(1000000000000000000)) + return fmt.Errorf("❌ 地址 %s ETH余额不足: %.6f ETH < %.6f ETH (最大gas费用)", + final_from, ethBalanceInETH, maxGasCostInETH) } - // 构造发送到 USDT 合约地址的交易 - tx := types.NewTransaction( - nonce, - e.USDT.Address, // 发送到USDT合约地址 - big.NewInt(0), // value为0(ERC20转账不需要ETH) - gasLimit, // GasLimit设置为100000(ERC20转账需要更多gas) - gasPrice, // GasPrice: 20 Gwei - data, // 附加数据:transfer方法调用 - ) - // 6, 签名交易并获得txHash - signedTx, err := types.SignTx(tx, types.NewEIP155Signer(e.NetId), privateKey) - // txHash := signedTx.Hash().Hex() // 通过签名信息解析出交易hash + + // 构造EIP-1559交易 + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: e.NetId, + Nonce: nonce, + GasTipCap: maxPriorityFeePerGas, + GasFeeCap: maxFeePerGas, + Gas: gasLimit, + To: &e.USDT.Address, + Value: big.NewInt(0), + Data: data, + }) + // 6, 签名EIP-1559交易并获得txHash + signedTx, err := types.SignTx(tx, types.NewLondonSigner(e.NetId), privateKey) if err != nil { - return fmt.Errorf("failed to sign transaction: %w", err) + return fmt.Errorf("failed to sign EIP-1559 transaction: %w", err) } - // 7, 发送交易 + + txHash := signedTx.Hash().Hex() + + // 7, 发送EIP-1559交易 err = e.RpcClient.SendTransaction(e.Ctx, signedTx) if err != nil { - return fmt.Errorf("failed to send transaction: %w", err) + return fmt.Errorf("failed to send EIP-1559 transaction: %w", err) } + + log.Printf("✅ EIP-1559交易已提交至mempool:%s,金额:%.2f USDT, 最大手续费:%.6f ETH", txHash, amount, maxGasCostInETH) // // 8, 构造交易消息 // tx_msg := message.Tx_msg{ // TxType: tx_type, @@ -692,7 +823,3 @@ func (e *ETHNode) usdt_transfer(msg any) error { // e.UnConfirmTxs[txHash] = tx_msg return nil } - -func (e *ETHNode) Stop() { - e.Cancel() -} diff --git a/internal/db/sqlite.go b/internal/db/sqlite.go new file mode 100644 index 0000000..4aa42e0 --- /dev/null +++ b/internal/db/sqlite.go @@ -0,0 +1,120 @@ +package db + +import ( + "database/sql" + "fmt" + + _ "modernc.org/sqlite" // 导入驱动 +) + +type SQLite struct { + DB *sql.DB +} + +// 初始化连接 +func NewSQLite(path string) (*SQLite, error) { + db, err := sql.Open("sqlite", path) + if err != nil { + return nil, fmt.Errorf("open sqlite failed: %v", err) + } + return &SQLite{DB: db}, nil +} + +// 关闭数据库 +func (s *SQLite) Close() { + if s.DB != nil { + s.DB.Close() + } +} + +// 通用新建表 +func (s *SQLite) Exec_(sql string) error { + _, err := s.DB.Exec(sql) + if err != nil { + return fmt.Errorf("Exec DB error: %w", err) + } + return nil +} + +// 通用查询方法(返回[]map[string]any) +func (s *SQLite) Query_(query string, args ...any) ([]map[string]any, error) { + rows, err := s.DB.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("query error: %v", err) + } + defer rows.Close() + + columns, err := rows.Columns() + if err != nil { + return nil, err + } + + var results []map[string]any + for rows.Next() { + // 创建一个与列数相同的 slice 用于 Scan + values := make([]any, len(columns)) + valuePtrs := make([]any, len(columns)) + for i := range values { + valuePtrs[i] = &values[i] + } + + if err := rows.Scan(valuePtrs...); err != nil { + return nil, err + } + + // 将行转换为 map + rowMap := make(map[string]any) + for i, col := range columns { + val := values[i] + if b, ok := val.([]byte); ok { + rowMap[col] = string(b) + } else { + rowMap[col] = val + } + } + results = append(results, rowMap) + } + return results, nil +} + +// 插入数据(支持事务和批量) +func (s *SQLite) Insert(sqlStr string, args ...[]any) error { + tx, err := s.DB.Begin() + if err != nil { + return err + } + stmt, err := tx.Prepare(sqlStr) + if err != nil { + return err + } + defer stmt.Close() + + for _, a := range args { + _, err = stmt.Exec(a...) + if err != nil { + tx.Rollback() + return fmt.Errorf("exec insert error: %v", err) + } + } + return tx.Commit() +} + +// 更新数据 +func (s *SQLite) Update(sqlStr string, args ...any) (int64, error) { + res, err := s.DB.Exec(sqlStr, args...) + if err != nil { + return 0, fmt.Errorf("update error: %v", err) + } + rows, _ := res.RowsAffected() + return rows, nil +} + +// 删除数据 +func (s *SQLite) Delete(sqlStr string, args ...any) (int64, error) { + res, err := s.DB.Exec(sqlStr, args...) + if err != nil { + return 0, fmt.Errorf("delete error: %v", err) + } + rows, _ := res.RowsAffected() + return rows, nil +} diff --git a/internal/logger/transaction_logger.go b/internal/logger/transaction_logger.go index 74300bf..885d6fc 100644 --- a/internal/logger/transaction_logger.go +++ b/internal/logger/transaction_logger.go @@ -188,20 +188,20 @@ func compressFile(filePath string) error { } // LogTopup 记录充值消息 -func LogTopup(address string, status string, amount float64, txHash string, blockHeight uint64) { +func LogTopup(toAddress string, status string, amount float64, txHash string, blockHeight uint64) { if txLogger == nil { return } - lf, err := txLogger.getOrCreateLogFile(address) + lf, err := txLogger.getOrCreateLogFile(toAddress) if err != nil { fmt.Printf("⚠️ 获取日志文件失败: %v\n", err) return } timestamp := time.Now().Format("2006-01-02 15:04:05") - content := fmt.Sprintf("%s [topup]-[%s] | 金额: %.6f | 交易哈希: %s | 区块高度: %d | 地址: %s", - timestamp, status, amount, txHash, blockHeight, address) + content := fmt.Sprintf("%s [topup]-[%s] | 金额: %.6f | 交易哈希: %s | 区块高度: %d | ToAddress: %s", + timestamp, status, amount, txHash, blockHeight, toAddress) if err := lf.write(content); err != nil { fmt.Printf("⚠️ 写入日志失败: %v\n", err) @@ -209,21 +209,21 @@ func LogTopup(address string, status string, amount float64, txHash string, bloc } // LogWithdraw 记录提现消息 -func LogWithdraw(queueId string, status string, amount float64, from string, to string, txHash string, blockHeight uint64) { +func LogWithdraw(toAddress string, status string, amount float64, fromAddress string, txHash string, blockHeight uint64) { if txLogger == nil { return } - // 使用 queueId 作为文件名 - lf, err := txLogger.getOrCreateLogFile(queueId) + // 使用 toAddress 作为文件名 + lf, err := txLogger.getOrCreateLogFile(toAddress) if err != nil { fmt.Printf("⚠️ 获取日志文件失败: %v\n", err) return } timestamp := time.Now().Format("2006-01-02 15:04:05") - content := fmt.Sprintf("%s [withdraw]-[%s] | 金额: %.6f | From: %s | To: %s | 交易哈希: %s | 区块高度: %d", - timestamp, status, amount, from, to, txHash, blockHeight) + content := fmt.Sprintf("%s [withdraw]-[%s] | 金额: %.6f | FromAddress: %s | ToAddress: %s | 交易哈希: %s | 区块高度: %d", + timestamp, status, amount, fromAddress, toAddress, txHash, blockHeight) if err := lf.write(content); err != nil { fmt.Printf("⚠️ 写入日志失败: %v\n", err) @@ -231,21 +231,21 @@ func LogWithdraw(queueId string, status string, amount float64, from string, to } // LogPay 记录支付消息 -func LogPay(orderId string, queueId string, status string, amount float64, from string, to string, txHash string, blockHeight uint64) { +func LogPay(toAddress string, status string, amount float64, fromAddress string, txHash string, blockHeight uint64, orderId string, queueId string) { if txLogger == nil { return } - // 使用 orderId 作为文件名 - lf, err := txLogger.getOrCreateLogFile(orderId) + // 使用 toAddress 作为文件名 + lf, err := txLogger.getOrCreateLogFile(toAddress) if err != nil { fmt.Printf("⚠️ 获取日志文件失败: %v\n", err) return } timestamp := time.Now().Format("2006-01-02 15:04:05") - content := fmt.Sprintf("%s [pay]-[%s] | 订单ID: %s | 队列ID: %s | 金额: %.6f | From: %s | To: %s | 交易哈希: %s | 区块高度: %d", - timestamp, status, orderId, queueId, amount, from, to, txHash, blockHeight) + content := fmt.Sprintf("%s [pay]-[%s] | 金额: %.6f | FromAddress: %s | ToAddress: %s | 交易哈希: %s | 区块高度: %d | OrderId: %s | QueueId: %s", + timestamp, status, amount, fromAddress, toAddress, txHash, blockHeight, orderId, queueId) if err := lf.write(content); err != nil { fmt.Printf("⚠️ 写入日志失败: %v\n", err) diff --git a/internal/msg/msg.go b/internal/msg/msg.go index 8d8add0..c70a373 100644 --- a/internal/msg/msg.go +++ b/internal/msg/msg.go @@ -4,19 +4,26 @@ import "time" // 配置文件结构 type Config struct { + SQLite3 SQLite3 `json:"sqlite3"` RMQConfig RMQConfig `json:"rmq_config"` ETHConfig ETHConfig `json:"eth_config"` TRONConfig TRONConfig `json:"tron_config"` } +type SQLite3 struct { + MsgPath string `json:"msg_path"` +} + type RMQConfig struct { SubAddr string `json:"sub_addr"` // 监听地址 PayConfig QueueConfig `json:"pay"` // 支付 TopUpConfig QueueConfig `json:"topup"` // 充值 WithdrawConfig QueueConfig `json:"withdraw"` // 提现 + RemoveConfig QueueConfig `json:"remove"` // 移除监听 PayRespConfig QueueConfig `json:"pay_resp"` // 支付回复 TopUpRespConfig QueueConfig `json:"topup_resp"` // 充值回复 WithdrawRespConfig QueueConfig `json:"withdraw_resp"` // 提现回复 + RemoveRespConfig QueueConfig `json:"remove_resp"` // 移除监听回复 } type QueueConfig struct { @@ -50,7 +57,7 @@ type DbConfig struct { ConnMaxLife time.Duration `json:"connMaxLife"` // 连接最大存活时间 } -// ======================================================================= +// =============================== type0 =============================== // 接收的充值消息 type TopupMsg_req struct { Chain string `json:"chain"` // 链名称 @@ -71,6 +78,7 @@ type TopupMsg_resp struct { BlockHeight uint64 `json:"block_height"` // 区块高度 } +// =============================== type1 =============================== // 接收的提现消息 type WithdrawMsg_req struct { QueueId string `json:"queue_id"` @@ -86,16 +94,17 @@ type WithdrawMsg_req struct { // 返回提现结果消息 type WithdrawMsg_resp struct { QueueId string `json:"queue_id"` - Status int `json:"status"` - Amount float64 `json:"amount"` Chain string `json:"chain"` // 链名称 Symbol string `json:"symbol"` // 币种 + Status int `json:"status"` + Amount float64 `json:"amount"` TxHash string `json:"tx_hash"` FromAddress string `json:"from_address"` // 来源地址 ToAddress string `json:"to_address"` // 目标地址 BlockHeight uint64 `json:"block_height"` // 区块高度 } +// =============================== type2 =============================== // 接收到的支付消息 type PayMsg_req struct { QueueId string `json:"queue_id"` @@ -123,6 +132,27 @@ type PayMsg_resp struct { BlockHeight uint64 `json:"block_height"` // 区块高度 } +// =============================== type3 =============================== +// 接收到的删除监听地址消息 +type RemoveListenMsg_req struct { + MsgType int `json:"msg_type"` + Chain string `json:"chain"` + Symbol string `json:"symbol"` + Address string `json:"address"` + Timestamp uint64 `json:"timestamp"` + Sign string `json:"sign"` +} + +// 返回收到的删除监听地址消息 +type RemoveListenMsg_resp struct { + MsgType int `json:"msg_type"` + Chain string `json:"chain"` + Symbol string `json:"symbol"` + Address string `json:"address"` + Status int `json:"status"` // 0失败 1成功 +} + +// ===================================================================== // 节点通用消息结构 type Tx_msg struct { TxType int `json:"tx_type"` // 转账类型:0充值,1提现,2支付 diff --git a/internal/server.go b/internal/server.go index 90be55c..8375464 100644 --- a/internal/server.go +++ b/internal/server.go @@ -8,6 +8,7 @@ import ( "m2pool-payment/internal/blockchain" "m2pool-payment/internal/blockchain/eth" "m2pool-payment/internal/crypto" + "m2pool-payment/internal/db" "m2pool-payment/internal/logger" message "m2pool-payment/internal/msg" rmq "m2pool-payment/internal/queue" @@ -15,6 +16,7 @@ import ( "os/signal" "strings" "syscall" + "time" ) const MSG_KEY string = "9f3c7a12" @@ -32,6 +34,7 @@ type ServerCtx struct { Config message.Config blockChainServer *blockchain.BlockChainServer rmqServer *rmq.RabbitMQServer + sqlitedb db.SQLite } var s_ctx ServerCtx @@ -76,6 +79,113 @@ func initBlockChainServer() { log.Println("✅ 区块链服务初始化完成") } +func loadSQLiteData() { + err1 := loadTopupReqMsg() + if err1 != nil { + log.Fatalf("load topup msg err:%v", err1) + } + err2 := loadWithdrawReqMsg() + if err2 != nil { + log.Fatalf("load withdraw msg err:%v", err2) + } + err3 := loadPayReqMsg() + if err3 != nil { + log.Fatalf("load pay msg err:%v", err3) + } +} + +func loadTopupReqMsg() error { + sql := `SELECT chain, symbol, timestamp, to_addr FROM msg_topup_req;` + rows, err := s_ctx.sqlitedb.DB.Query(sql) + if err != nil { + return fmt.Errorf("query history topup-msg error: %w", err) + } + defer rows.Close() + + var topupReq_msg message.TopupMsg_req + hasData := false + for rows.Next() { + hasData = true + if err := rows.Scan(&topupReq_msg.Chain, &topupReq_msg.Symbol, &topupReq_msg.Timestamp, &topupReq_msg.Address); err != nil { + return err + } + s_ctx.blockChainServer.AddAddress(topupReq_msg.Chain, topupReq_msg.Address, topupReq_msg) + } + + if !hasData { + log.Println("Msg_topup_req`s msg has not data, doesn`t need to load.") + return nil + } + + // 在遍历完所有数据后检查是否发生了错误 + if err := rows.Err(); err != nil { + log.Printf("Error encountered while iterating over rows: %v", err) + } + return nil +} + +func loadWithdrawReqMsg() error { + sql := `SELECT queueId, chain, symbol, timestamp, from_addr, to_addr, amount FROM msg_withdraw_req;` + rows, err := s_ctx.sqlitedb.DB.Query(sql) + if err != nil { + return fmt.Errorf("query history withdraw-msg error: %w", err) + } + defer rows.Close() + + var withdrawReq_msg message.WithdrawMsg_req + hasData := false + for rows.Next() { + hasData = true + // var chain, symbol, to_addr string + // var timestamp uint64 + if err := rows.Scan(&withdrawReq_msg.QueueId, &withdrawReq_msg.Chain, &withdrawReq_msg.Symbol, &withdrawReq_msg.Timestamp, &withdrawReq_msg.FromAddress, &withdrawReq_msg.ToAddress, &withdrawReq_msg.Amount); err != nil { + return err + } + s_ctx.blockChainServer.AddAddress(withdrawReq_msg.Chain, withdrawReq_msg.ToAddress, withdrawReq_msg) + } + + if !hasData { + log.Println("Msg_withdraw_req`s msg has not data, doesn`t need to load.") + return nil + } + + // 在遍历完所有数据后检查是否发生了错误 + if err := rows.Err(); err != nil { + log.Printf("Error encountered while iterating over rows: %v", err) + } + return nil +} + +func loadPayReqMsg() error { + sql := `SELECT queueId, chain, symbol, timestamp, from_addr, to_addr, amount, orderId FROM msg_pay_req;` + rows, err := s_ctx.sqlitedb.DB.Query(sql) + if err != nil { + return fmt.Errorf("query history pay-msg error: %w", err) + } + defer rows.Close() + + var payReq_msg message.PayMsg_req + hasData := false + for rows.Next() { + hasData = true + if err := rows.Scan(&payReq_msg.QueueId, &payReq_msg.Chain, &payReq_msg.Symbol, &payReq_msg.Timestamp, &payReq_msg.FromAddress, &payReq_msg.ToAddress, &payReq_msg.Amount, &payReq_msg.OrderId); err != nil { + return err + } + s_ctx.blockChainServer.AddAddress(payReq_msg.Chain, payReq_msg.ToAddress, payReq_msg) + } + + if !hasData { + log.Println("Msg_pay_req`s msg has not data, doesn`t need to load.") + return nil + } + + // 在遍历完所有数据后检查是否发生了错误 + if err := rows.Err(); err != nil { + log.Printf("Error encountered while iterating over rows: %v", err) + } + return nil +} + func initRmqServer() { // 初始化rmq服务 rmq_server, err := rmq.NewRabbitMQServer(s_ctx.Config.RMQConfig) @@ -88,6 +198,21 @@ func initRmqServer() { log.Printf("✅ RabbitMQ服务初始化完成: %s", s_ctx.Config.RMQConfig.SubAddr) } +func initSQLite(sqlite3_file string) { + // 初始化sqlite3数据库 + sqlite3, err := db.NewSQLite(sqlite3_file) + if err != nil { + log.Fatalf("connect sqlite3 error:%v", err) + return + } + sqlByte, err := os.ReadFile("../public/SQLite3.sql") + if err != nil { + log.Fatalf("open sql file error: %s", "../public/SQLite3.sql") + } + sqlite3.Exec_(string(sqlByte)) + s_ctx.sqlitedb = *sqlite3 +} + func handleTopupMsg() { s_ctx.rmqServer.OnTopupMsg = func(msg message.TopupMsg_req) { msg.Address = strings.ToLower(msg.Address) @@ -109,7 +234,32 @@ func handleTopupMsg() { } // 添加监听地址 - s_ctx.blockChainServer.AddAddress(msg.Chain, msg.Address, msg) + // go func() { + err := s_ctx.blockChainServer.AddAddress(msg.Chain, msg.Address, msg) + if err != nil { + log.Printf("❌ 添加监听地址失败: %v", err) + // 发送失败响应 + err = s_ctx.rmqServer.PublishTopupResp(message.TopupMsg_resp{ + Address: msg.Address, + Status: STATUS_FAILED, + Chain: msg.Chain, + Symbol: msg.Symbol, + Amount: 0, + TxHash: "", + }) + if err != nil { + log.Printf("❌ 发布充值失败响应失败: %v", err) + } + return + } + // }() + // 将新增数据写入sqlite + insert_sql := `INSERT OR REPLACE INTO msg_topup_req (chain, symbol, timestamp, to_addr) VALUES (?, ?, ?, ?)` + data := []any{msg.Chain, msg.Symbol, msg.Timestamp, msg.Address} + err = s_ctx.sqlitedb.Insert(insert_sql, data) + if err != nil { + log.Printf("❌ 插入 msg_req 失败: %v, data: %+v", err, data) + } } } @@ -147,6 +297,30 @@ func handleWithdrawMsg() { Symbol: msg.Symbol, TxHash: "", }) + return // 转账失败时直接返回,不进入链上确认流程 + } + // go func() { + err = s_ctx.blockChainServer.AddAddress(msg.Chain, msg.ToAddress, msg) + if err != nil { + log.Printf("❌ 添加监听地址失败: %v", err) + // 发送失败响应 + s_ctx.rmqServer.PublishWithdrawResp(message.WithdrawMsg_resp{ + QueueId: msg.QueueId, + Status: STATUS_FAILED, + Amount: msg.Amount, + Chain: msg.Chain, + Symbol: msg.Symbol, + TxHash: "", + }) + return + } + // }() + // 将新增数据写入sqlite + insert_sql := `INSERT OR REPLACE INTO msg_withdraw_req (queueId, chain, symbol, timestamp, from_addr, to_addr, amount) VALUES (?, ?, ?, ?, ?, ?, ?)` + data := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.Timestamp, msg.FromAddress, msg.ToAddress, msg.Amount} + err = s_ctx.sqlitedb.Insert(insert_sql, data) + if err != nil { + log.Printf("❌ 插入 withdraw_req 失败: %v, data: %+v", err, data) } } } @@ -187,6 +361,31 @@ func handlePayMsg() { OrderId: msg.OrderId, TxHash: "", }) + return // 转账失败时直接返回,不进入链上确认流程 + } + // go func() { + err = s_ctx.blockChainServer.AddAddress(msg.Chain, msg.ToAddress, msg) + if err != nil { + log.Printf("❌ 添加监听地址失败: %v", err) + // 发送失败响应 + s_ctx.rmqServer.PublishPayResp(message.PayMsg_resp{ + QueueId: msg.QueueId, + Status: STATUS_FAILED, + Amount: msg.Amount, + Chain: msg.Chain, + Symbol: msg.Symbol, + OrderId: msg.OrderId, + TxHash: "", + }) + return + } + // }() + // 将新增数据写入sqlite + insert_sql := `INSERT OR REPLACE INTO msg_pay_req (queueId, chain, symbol, timestamp, from_addr, to_addr, amount, orderId) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + data := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.Timestamp, msg.FromAddress, msg.ToAddress, msg.Amount, msg.OrderId} + err = s_ctx.sqlitedb.Insert(insert_sql, data) + if err != nil { + log.Printf("❌ 插入 pay_req 失败: %v, data: %+v", err, data) } } } @@ -233,32 +432,87 @@ func handleChainEvent(chainEventCh chan any) { err := s_ctx.rmqServer.PublishTopupResp(msg) if err != nil { log.Printf("❌ 发送充值响应失败: %v", err) + return } + go func() { + // 插入响应数据 + sql := `INSERT INTO msg_topup_resp (chain, symbol, timestamp, to_addr, amount, height, txHash, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + data := []any{msg.Chain, msg.Symbol, time.Now().Unix(), msg.Address, msg.Amount, msg.BlockHeight, msg.TxHash, msg.Status} + err := s_ctx.sqlitedb.Insert(sql, data) + if err != nil { + log.Printf("❌ 插入 topup_resp 失败: %v", err) + return + } + }() case message.WithdrawMsg_resp: // 提现确认 log.Printf("✅ [链上] 提现确认: QueueId=%s, Amount=%.2f, TxHash=%s, Status=%d", msg.QueueId, msg.Amount, msg.TxHash, msg.Status) // 记录交易日志 - logger.LogWithdraw(msg.QueueId, "确认", msg.Amount, msg.FromAddress, - msg.ToAddress, msg.TxHash, msg.BlockHeight) + logger.LogWithdraw(msg.ToAddress, "确认", msg.Amount, msg.FromAddress, msg.TxHash, msg.BlockHeight) err := s_ctx.rmqServer.PublishWithdrawResp(msg) if err != nil { log.Printf("❌ 发送提现响应失败: %v", err) + return } + go func() { + // 插入响应数据 + sql := `INSERT INTO msg_withdraw_resp (queueId, chain, symbol, timestamp, from_addr, to_addr, amount, height, txHash, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + data := []any{msg.QueueId, msg.Chain, msg.Symbol, time.Now().Unix(), msg.FromAddress, msg.ToAddress, msg.Amount, msg.BlockHeight, msg.TxHash, msg.Status} + err := s_ctx.sqlitedb.Insert(sql, data) + if err != nil { + log.Printf("❌ 插入 withdraw_resp 失败: %v", err) + return + } + + // 删除对应数据 + del_sql := `DELETE FROM msg_withdraw_req WHERE queueId = ?;` + count, err := s_ctx.sqlitedb.Delete(del_sql, msg.QueueId) + if err != nil { + log.Printf("❌ 清理 withdraw_req 失败: %v, queueId=%s", err, msg.QueueId) + } else if count == 0 { + log.Printf("⚠️ 未找到要删除的 withdraw_req 记录: queueId=%s", msg.QueueId) + } else { + log.Printf("✅ 清理 withdraw_req 成功: 删除了 %d 条记录, queueId=%s", count, msg.QueueId) + } + }() + case message.PayMsg_resp: // 支付确认 log.Printf("✅ [链上] 支付确认: QueueId=%s, OrderId=%s, Amount=%.2f, TxHash=%s, Status=%d", msg.QueueId, msg.OrderId, msg.Amount, msg.TxHash, msg.Status) // 记录交易日志 - logger.LogPay(msg.OrderId, msg.QueueId, "确认", msg.Amount, msg.FromAddress, - msg.ToAddress, msg.TxHash, msg.BlockHeight) + logger.LogPay(msg.ToAddress, "确认", msg.Amount, msg.FromAddress, msg.TxHash, msg.BlockHeight, msg.OrderId, msg.QueueId) err := s_ctx.rmqServer.PublishPayResp(msg) if err != nil { log.Printf("❌ 发送支付响应失败: %v", err) + return } + go func() { + // 插入响应数据 + sql := `INSERT INTO msg_pay_resp (queueId, chain, symbol, timestamp, from_addr, to_addr, amount, height, txHash, status, orderId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + data := []any{msg.QueueId, msg.Chain, msg.Symbol, time.Now().Unix(), msg.FromAddress, msg.ToAddress, msg.Amount, msg.BlockHeight, msg.TxHash, msg.Status, msg.OrderId} + err := s_ctx.sqlitedb.Insert(sql, data) + if err != nil { + log.Printf("❌ 插入 pay_resp 失败: %v", err) + return + } + + // 删除对应数据 + del_sql := `DELETE FROM msg_pay_req WHERE queueId = ?;` + count, err := s_ctx.sqlitedb.Delete(del_sql, msg.QueueId) + if err != nil { + log.Printf("❌ 清理 pay_req 失败: %v, queueId=%s", err, msg.QueueId) + } else if count == 0 { + log.Printf("⚠️ 未找到要删除的 pay_req 记录: queueId=%s", msg.QueueId) + } else { + log.Printf("✅ 清理 pay_req 成功: 删除了 %d 条记录, queueId=%s", count, msg.QueueId) + } + }() + default: log.Printf("⚠️ 未知消息类型: %T", event) } @@ -283,6 +537,11 @@ func Start(msgKey string) { // ================== 初始化区块链节点 ================== initBlockChainServer() + // ================== 初始化SQLite3 ================== + initSQLite(s_ctx.Config.SQLite3.MsgPath) + // 读取历史信息 + loadSQLiteData() + // ================== 初始化 RabbitMQ 服务 ================== initRmqServer() diff --git a/public/SQLite3.sql b/public/SQLite3.sql new file mode 100644 index 0000000..9cff7b2 --- /dev/null +++ b/public/SQLite3.sql @@ -0,0 +1,71 @@ +CREATE TABLE IF NOT EXISTS msg_topup_req ( + chain TEXT, + symbol TEXT, + timestamp INTEGER, + to_addr TEXT, + PRIMARY KEY(to_addr) +); + +CREATE TABLE IF NOT EXISTS msg_withdraw_req ( + queueId TEXT, + chain TEXT, + symbol TEXT, + timestamp INTEGER, + from_addr TEXT, + to_addr TEXT, + amount NUMERIC, + PRIMARY KEY(queueId) +); + +CREATE TABLE IF NOT EXISTS msg_pay_req ( + queueId TEXT, + chain TEXT, + symbol TEXT, + timestamp INTEGER, + from_addr TEXT, + to_addr TEXT, + amount NUMERIC, + orderId TEXT, + PRIMARY KEY(queueId) +); + +CREATE TABLE IF NOT EXISTS msg_topup_resp ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chain TEXT, + symbol TEXT, + timestamp INTEGER, + to_addr TEXT, + amount NUMERIC, + height INTEGER, + TxHash TEXT, + status INTEGER +); + +CREATE TABLE IF NOT EXISTS msg_withdraw_resp ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + queueId TEXT, + chain TEXT, + symbol TEXT, + timestamp INTEGER, + from_addr TEXT, + to_addr TEXT, + amount NUMERIC, + height INTEGER, + txHash TEXT, + status INTEGER +); + +CREATE TABLE IF NOT EXISTS msg_pay_resp ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + queueId TEXT, + chain TEXT, + symbol TEXT, + timestamp INTEGER, + from_addr TEXT, + to_addr TEXT, + amount NUMERIC, + height INTEGER, + txHash TEXT, + orderId TEXT, + status INTEGER +); \ No newline at end of file