6 Commits

Author SHA1 Message Date
lzx
8201356476 integration wordpress 2025-11-18 17:26:07 +08:00
lzx
74d9a114c0 add log-system, bug fixed 2025-11-18 11:10:16 +08:00
lzx
ac22db02f3 modify some msg-struct and table-struct 2025-11-14 17:43:25 +08:00
lzx
245c9c94cb bug fix 2025-11-13 17:59:13 +08:00
lzx
00389efb75 update 2025-11-13 17:08:38 +08:00
lzx
8d7da5d345 merge transactions and update 2025-10-31 13:46:58 +08:00
42 changed files with 6446 additions and 2060 deletions

577
README.md
View File

@@ -74,118 +74,246 @@ M2Pool Payment System v2 是一个基于以太坊区块链的**分布式支付
↓ ↑ ↓ ↑
┌─────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────┐
│ RabbitMQ │ │ RabbitMQ │
│ ┌─────────┐ ┌──────────┐ ┌────────┐ │ ┌─────────┐ ┌──────────┐ ┌────────┐ ┌────────┐
│ │ topup │ │ withdraw │ │ pay │ 请求队列 │ │ topup │ │ withdraw │ │ pay │ │ remove │ 请求队列 │
│ └─────────┘ └──────────┘ └────────┘ │ └─────────┘ └──────────┘ └────────┘ └────────┘
│ ┌─────────┐ ┌──────────┐ ┌────────┐ │ ┌─────────┐ ┌──────────┐ ┌────────┐ ┌────────┐
│ │topup_ │ │withdraw_ │ │ pay_ │ 响应队列 │ │topup_ │ │withdraw_ │ │ pay_ │ │remove_ │ 响应队列 │
│ │ resp │ │ resp │ │ resp │ │ │ resp │ │ resp │ │ resp │ │ resp
│ └─────────┘ └──────────┘ └────────┘ │ └─────────┘ └──────────┘ └────────┘ └────────┘
└────────────┬────────────────────────────┬───────────────────┘ └────────────┬────────────────────────────┬───────────────────┘
│ │ │ │
↓ ↑ ↓ ↑
┌─────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────┐
│ M2Pool Payment System v2 │ │ M2Pool Payment System v2 │
│ │ │ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ ┌──────────────┐ ┌──────────────┐
│ │ RabbitMQ │ │ Blockchain │ │ Database │ │ Server │ │ ListenServer │
│ │ Consumer │─>│ Manager─>│ (MySQL) │ │ (路由层) │────────>│ (消息层)
│ └──────────────┘ └──────┬───────┘ └──────────────┘ │ └──────────────┘ └──────┬───────┘
│ WebSocket + RPC ┌────────────┴────────────┐
└────────────────────────────┼─────────────────────────────────┘ │ ↓ ↓ │
┌──────────────────┐ ┌──────────────────┐
Blockchain Database │ │
│ │ Manager │ │ (MySQL/SQLite) │ │
│ │ (ETH Node) │ │ │ │
│ └────────┬─────────┘ └──────────────────┘ │
│ │ │
│ │ WebSocket + RPC │
└───────────────────┼─────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────┐
│ 以太坊区块链网络 │ │ 以太坊区块链网络 │
│ │ │ │
│ ┌──────────────┐ ┌──────────────┐ │ │ ┌──────────────┐ ┌──────────────┐ │
│ │ 新区块 │ │ USDT Transfer │ │ │ │ 新区块 │ │ ETH/USDT
│ │ (NewHead) │ │ 事件 │ │ │ (NewHead) │ │ Transfer事件 │
│ └──────────────┘ └──────────────┘ │ │ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────┘
``` ```
### 消息流转架构
```
业务系统 → RabbitMQ → Server → ListenServer → Blockchain Node
↑ ↓
│ (记录到数据库)
│ │
│ (监听链上事件)
│ │
│ ←── 返回响应 ───
│ │
└─── 发送响应 ───────────┘
```
#### 核心组件说明
1. **Server (server.go)**: 服务入口和消息路由
- 接收 RabbitMQ 消息
- 验证消息签名
- 路由消息到 ListenServer
2. **ListenServer (listen/)**: 消息中间层
- 管理消息状态TopupMsgs, WithdrawMsgs, PayMsgs, RemoveMsgs
- 与区块链节点双向通信
- 持久化消息到 SQLite
- 转发响应到 RabbitMQ
3. **Blockchain Node (blockchain/eth/)**: 区块链交互层
- 监听链上事件ETH 和 USDT 转账)
- 执行转账交易
- 管理待确认交易UnConfirmTxs
- 确认交易状态
### 核心模块 ### 核心模块
#### 1. Blockchain Manager (`internal/blockchain/`) #### 1. Server (`internal/server.go`)
- **blockchain.go**:统一的区块链接口定义 - 服务入口和启动管理
- **eth/eth.go**:以太坊节点实现 - 消息签名验证
- 监听 USDT Transfer 事件(实时检测充值) - 消息路由到 ListenServer
- 监听新区块产生(触发交易确认) - RabbitMQ 消息处理回调
- 管理待确认交易池UnConfirmTxs - 响应消息转发到 RabbitMQ
- 执行 ERC20 转账(提现/支付)
- 自动重连机制
- 地址统一小写处理
#### 2. Message Queue (`internal/queue/`) #### 2. ListenServer (`internal/listen/`)
- **listen.go**:消息中间层,连接 RabbitMQ 和区块链节点
- **RmqMsgIn()**:处理来自 RabbitMQ 的消息
- 充值请求 → 写入数据库 → 添加到 TopupMsgs → 转发到区块链节点
- 提现请求 → 写入数据库 → 添加到 WithdrawMsgs → 转发到区块链节点
- 支付请求 → 写入数据库 → 添加到 PayMsgs → 转发到区块链节点
- 移除监听 → 写入数据库 → 添加到 RemoveMsgs → 转发到区块链节点
- **NetMsgIn()**:处理来自区块链节点的响应
- 充值响应 → 更新数据库 → 转发到 RabbitMQ
- 提现响应 → 更新数据库 → 转发到 RabbitMQ
- 支付响应 → 更新数据库 → 转发到 RabbitMQ
- 消息状态管理TopupMsgs, WithdrawMsgs, PayMsgs, RemoveMsgs
- 消息查找(根据 address 或 queue_id
#### 3. Blockchain Manager (`internal/blockchain/`)
- **blockchain.go**:统一的区块链接口定义和路由
- **eth/eth.go**:以太坊节点实现
- **Listen()**:监听链上事件
- 监听 ETH Transfer 事件
- 监听 USDT Transfer 事件
- 监听新区块产生(用于交易确认)
- **ListenMsg()**:监听来自 ListenServer 的消息
- 充值请求 → 添加到钱包监听
- 提现请求 → 执行转账
- 支付请求 → 执行转账
- 移除监听 → 移除钱包监听
- **Transfer()**:执行转账交易
- 管理待确认交易池UnConfirmTxs
- 交易确认逻辑
#### 4. Message Queue (`internal/queue/`)
- **rabbitmq.go**RabbitMQ 消息队列服务 - **rabbitmq.go**RabbitMQ 消息队列服务
- 消费 3 个请求队列(充值/提现/支付) - 消费 4 个请求队列(充值/提现/支付/移除监听
- 发布 3 个响应队列(交易确认结果) - 发布 4 个响应队列(交易确认结果)
- 自动重连和错误重试 - 自动重连和错误重试
- 消息持久化 - 消息持久化
#### 3. Database (`internal/db/`) #### 5. Database (`internal/db/`)
- **db.go**MySQL 数据库连接池 - **mysql.go**MySQL 数据库连接池
- 存储钱包私钥(加密) - 存储钱包私钥(加密)
- 连接池管理 - 连接池管理
- 事务支持 - 事务支持
- **sqlite.go**SQLite 本地存储
- 存储消息请求和响应记录
- 存储未确认交易信息
- 提供事务支持
#### 4. Message (`internal/msg/`) #### 6. Message (`internal/msg/`)
- **msg.go**:消息结构定义 - **msg.go**:消息结构定义
- 请求消息TopupMsg_req, WithdrawMsg_req, PayMsg_req - 请求消息TopupMsg_req, WithdrawMsg_req, PayMsg_req, RemoveListenMsg_req
- 响应消息TopupMsg_resp, WithdrawMsg_resp, PayMsg_resp - 响应消息TopupMsg_resp, WithdrawMsg_resp, PayMsg_resp, RemoveListenMsg_resp
- 配置结构Config, RMQConfig, ETHConfig - **config.go**配置结构
- Config, RmqConfig, ETHConfig, MysqlConfig
#### 5. Utils (`internal/utils/`) #### 7. Utils (`internal/utils/`)
- **utils.go**:工具函数 - **utils.go**:工具函数
- 数值转换BigInt ↔ Float64 - 数值转换BigInt ↔ Float64
- 哈希计算 - 数组/切片操作
- 加密解密 - 字符串处理
#### 6. Crypto (`internal/crypto/`) #### 8. Crypto (`internal/crypto/`)
- **crypto.go**:加密工具 - **crypto.go**:加密工具
- SHA256 哈希 - SHA256 哈希
- 签名验证 - 签名验证
#### 7. Server (`internal/server.go`) #### 9. Logger (`internal/logger/`)
- 服务启动和管理 - **transaction_logger.go**:交易日志记录
- 消息路由和处理 - 记录所有交易操作的详细日志
- 优雅关闭 - 记录 RMQ 与 Listen 之间的所有通信消息
- 按地址分文件存储,自动日志轮转和压缩
##### 日志分类
**① 交易日志(按地址分文件)**
- 充值日志:按 `address` 命名文件
- 提现日志:按 `fromAddress` 命名文件
- 支付日志:按 `fromAddress` 命名文件
- ETH 节点日志:`ethnode.log`
**② RMQ ↔ Listen 通信日志(按地址分文件)**
- 充值请求/响应:按 `address` 命名文件
- 提现请求/响应:按 `fromAddress` 命名文件
- 支付请求/响应:按 `fromAddress` 命名文件
- 移除监听请求/响应:按 `address` 命名文件
##### 日志格式
**RMQ ↔ Listen 通信日志格式**`[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount`
- **TopupReq**: `[TopupReq]: 2006-01-02 15:04:05--0xabc...-ETH-USDT-0`
- **WithdrawReq**: `[WithdrawReq]: 2006-01-02 15:04:05-0x123...-0xabc...-ETH-USDT-100.500000`
- **PayReq**: `[PayReq]: 2006-01-02 15:04:05-0x123...-0xabc...-ETH-USDT-50.250000`
- **TopupResp**: `[TopupResp]: 2006-01-02 15:04:05-0xdef...-0xabc...-ETH-USDT-200.000000`
- **WithdrawResp**: `[WithdrawResp]: 2006-01-02 15:04:05-0x123...-0xabc...-ETH-USDT-100.500000`
- **PayResp**: `[PayResp]: 2006-01-02 15:04:05-0x123...-0xabc...-ETH-USDT-50.250000`
##### 日志轮转机制
- 单文件超过 **1MB** 自动压缩为 `.gz` 格式
- 后台异步压缩,不影响性能
- 压缩后的文件名格式:`{address}_{timestamp}.log.gz`
- 自动创建新的日志文件继续写入
##### 日志存储位置
所有日志文件存储在 `logs/` 目录下:
```
logs/
├── 0x123...abc.log # 地址 0x123...abc 的日志
├── 0x456...def.log # 地址 0x456...def 的日志
├── 0x123...abc_20240102_120000.log.gz # 压缩的历史日志
├── ethnode.log # ETH 节点日志
└── ...
```
### 项目结构 ### 项目结构
``` ```
m2pool-payment-v2/ m2pool-payment-v2/
├── cmd/ # 主程序入口 ├── cmd/ # 主程序入口
│ └── main.go # 程序入口,解析命令行参数 │ └── main.go # 程序入口,解析命令行参数 (-key)
├── internal/ # 内部包(不对外暴露) ├── internal/ # 内部包(不对外暴露)
│ ├── server.go # 服务启动和管理 │ ├── server.go # 服务启动和管理、消息路由
│ ├── blockchain/ # 区块链交互模块 │ ├── blockchain/ # 区块链交互模块
│ │ ├── blockchain.go # 统一的区块链接口定义 │ │ ├── blockchain.go # 统一的区块链接口定义和路由
│ │ ── eth/ # 以太坊实现 │ │ ── eth/ # 以太坊实现
│ │ ── eth.go # USDT 监听、转账、确认 │ │ ── eth.go # ETH节点接口定义
│ │ └── tron/ # TRON 实现(待开发 │ │ └── eth_prv.go # ETH节点实现监听、转账、确认
│ ├── crypto/ # 加密工具 │ ├── listen/ # 消息中间层
│ │ ── crypto.go # SHA256、签名验证 │ │ ── listen.go # ListenServer 定义和消息查找
│ │ └── listen_prv.go # ListenServer 实现(消息处理)
│ ├── queue/ # 消息队列
│ │ └── rabbitmq.go # RabbitMQ 客户端封装
│ ├── db/ # 数据库 │ ├── db/ # 数据库
│ │ ├── db.go # MySQL 连接池管理 │ │ ├── mysql.go # MySQL 连接池管理
│ │ └── sqlite.go # SQLite 本地存储 │ │ └── sqlite.go # SQLite 本地存储
│ ├── msg/ # 消息定义 │ ├── msg/ # 消息定义
│ │ ── msg.go # 请求/响应结构体定义 │ │ ── msg.go # 请求/响应结构体定义
├── queue/ # 消息队列 │ └── config.go # 配置结构定义
│ ├── rabbitmq.go # RabbitMQ 客户端封装 │ ├── crypto/ # 加密工具
│ │ └── README.md # RabbitMQ 使用文档 │ │ └── crypto.go # SHA256、签名验证
│ ├── logger/ # 日志记录 │ ├── logger/ # 日志记录
│ │ └── transaction_logger.go # 交易日志 │ │ └── transaction_logger.go # 交易日志
│ ├── constant/ # 常量定义
│ │ └── constant.go # 状态码等常量
│ └── utils/ # 工具函数 │ └── utils/ # 工具函数
│ └── utils.go # 类型转换、格式化 │ └── utils.go # 类型转换、格式化
├── public/ # 公共资源 ├── public/ # 公共资源
│ ├── SQLite3.sql # SQLite 表结构 │ ├── eth.sql # ETH钱包表结构MySQL
── migration.sql # 数据库迁移脚本 ── msg.sql # 消息表结构SQLite
│ └── eth_mysql.sql # MySQL 完整建表脚本
├── bin/ # 构建和启动脚本
│ ├── build.sh # 编译脚本
│ └── start.sh # 启动脚本
├── test/ # 测试和示例 ├── test/ # 测试和示例
│ ├── test.go # 测试程序(独立运行) │ ├── test.go # 测试程序(独立运行)
│ └── config.json # 配置文件 │ └── config.json # 配置文件示例
├── 流程.txt # 业务流程说明文档
├── go.mod # Go 模块定义 ├── go.mod # Go 模块定义
├── go.sum # 依赖版本锁定 ├── go.sum # 依赖版本锁定
└── README.md # 项目文档(本文件) └── README.md # 项目文档(本文件)
@@ -256,57 +384,155 @@ e.WsClient.SubscribeNewHead(e.Ctx, headers)
### 1. 充值功能 💰 ### 1. 充值功能 💰
**完整流程:**
``` ```
用户转账 → 实时检测 → 待确认通知 → 区块确认 → 最终通知 业务系统 → RabbitMQ → Server → ListenServer → 写数据库 → 添加到TopupMsgs
Blockchain Node → 添加到钱包 → 记录到钱包数据库
新区块产生 → 监听链上事件 → 对比交易
消息中的to = 区块交易中的to(充值)
返回消息 → ListenServer → 修改数据库
记录到UnConfirmTxs → 返回RabbitMQ → 发送待确认消息(status=2)
新区块产生 → 读取UnConfirmTxs → 对比交易高度
符合确认条件(当前高度 >= 交易高度 + 确认高度)
修改钱包数据 → 返回消息 → ListenServer → 修改数据库
返回RabbitMQ → 发送最终确认消息(status=1/0)
``` ```
**特点:** **特点:**
- ✅ 实时检测到账 - ✅ 实时检测到账
- ✅ 发送**两次**通知:待确认 + 最终确认 - ✅ 发送**两次**通知:待确认(status=2) + 最终确认(status=1/0)
- ✅ 支持多币种(当前支持 USDT - ✅ 支持 ETH 和 USDT
- ✅ 自动地址监听管理 - ✅ 自动地址监听管理
- ✅ 钱包余额自动更新
**消息流:** **消息流:**
1. 业务系统发送充值请求 → RabbitMQ 1. **业务系统发送充值请求** → RabbitMQ `topup.queue`
2. 系统添加地址监听 2. **Server 验证签名** → 转发到 ListenServer
3. 用户转账 → 立即通知status=2 待确认) 3. **ListenServer** → 写入 SQLite 数据库 → 添加到 TopupMsgs → 转发到 Blockchain Node
4. 等待 20 个区块 → 最终通知status=1 成功 / 0 失败) 4. **Blockchain Node** → 添加地址到钱包监听 → 写入 MySQL 钱包数据库
5. **监听链上事件** → 检测到充值交易 → 匹配地址 → 返回 TopupMsg_respstatus=2→ 记录到 UnConfirmTxs
6. **ListenServer** → 更新数据库 → 发送到 RabbitMQ `topup_resp.queue`**第一次通知**
7. **新区块确认** → 达到确认高度 → 更新钱包余额 → 返回 TopupMsg_respstatus=1/0
8. **ListenServer** → 更新数据库 → 发送到 RabbitMQ `topup_resp.queue`**第二次通知**
### 2. 提现功能 💸 ### 2. 提现功能 💸
**完整流程:**
``` ```
提现请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果 业务系统 → RabbitMQ → Server → ListenServer → 写数据库 → 添加到WithdrawMsgs
Blockchain Node → 记录到数据库
开始转账 → 校验余额 → 发送交易
转账结果返回 → ListenServer → 修改相关状态
新区块产生 → 监听链上事件 → 对比交易
消息中的from、to、amount = 区块交易中的from、to、amount
返回消息 → ListenServer → 修改数据库状态
记录到UnConfirmTxs → 返回RabbitMQ → 发送消息(status=2)
新区块产生 → 读取UnConfirmTxs → 对比交易高度
符合确认条件(当前高度 >= 交易高度 + 确认高度)
修改钱包数据 → 返回消息 → ListenServer → 修改数据库
返回RabbitMQ → 发送最终确认消息(status=1/0)
``` ```
**特点:** **特点:**
- ✅ 自动余额检查 - ✅ 自动余额检查
- ✅ 余额不足时使用归集钱包 - ✅ 余额不足时使用归集钱包
- ✅ 发送**次**通知:最终确认 - ✅ 发送**次**通知:待确认(status=2) + 最终确认(status=1/0)
- ✅ Gas 费用检查 - ✅ Gas 费用检查
- ✅ 支持 ETH 和 USDT
**消息流:** **消息流:**
1. 业务系统发送提现请求 → RabbitMQ 1. **业务系统发送提现请求** → RabbitMQ `withdraw.queue`
2. 系统验证余额并发送交易 2. **Server 验证签名** → 转发到 ListenServer
3. 等待 20 个区块确认 3. **ListenServer** → 写入 SQLite 数据库 → 添加到 WithdrawMsgs → 转发到 Blockchain Node
4. 返回结果status=1 成功 / 0 失败 4. **Blockchain Node** → 验证余额 → 发送交易 → 记录到数据库 → 返回 WithdrawMsg_respstatus=2
5. **ListenServer** → 更新数据库 → 发送到 RabbitMQ `withdraw_resp.queue`**第一次通知**
6. **监听链上事件** → 检测到提现交易 → 匹配交易 → 记录到 UnConfirmTxs
7. **新区块确认** → 达到确认高度 → 更新钱包余额 → 返回 WithdrawMsg_respstatus=1/0
8. **ListenServer** → 更新数据库 → 发送到 RabbitMQ `withdraw_resp.queue`**第二次通知**
### 3. 支付功能 💳 ### 3. 支付功能 💳
**完整流程:**
``` ```
支付请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果 业务系统 → RabbitMQ → Server → ListenServer → 写数据库 → 添加到PayMsgs
Blockchain Node → 记录到数据库
开始转账 → 校验余额 → 发送交易
转账结果返回 → ListenServer → 修改相关状态
新区块产生 → 监听链上事件 → 对比交易
消息中的from、to、amount = 区块交易中的from、to、amount
返回消息 → ListenServer → 修改数据库状态
记录到UnConfirmTxs → 返回RabbitMQ → 发送消息(status=2)
新区块产生 → 读取UnConfirmTxs → 对比交易高度
符合确认条件(当前高度 >= 交易高度 + 确认高度)
修改钱包数据 → 返回消息 → ListenServer → 修改数据库
返回RabbitMQ → 发送最终确认消息(status=1/0)
``` ```
**特点:** **特点:**
-订单关联 -支持批量支付(一次请求多笔转账)
- ✅ 自动余额检查 - ✅ 自动余额检查
- ✅ 发送**次**通知:最终确认 - ✅ 发送**次**通知:待确认(status=2) + 最终确认(status=1/0)
-支持商户收款 -Gas 费用检查
- ✅ 支持 ETH 和 USDT
**消息流:** **消息流:**
1. 业务系统发送支付请求含订单ID→ RabbitMQ 1. **业务系统发送支付请求** → RabbitMQ `pay.queue`(可包含多笔交易)
2. 系统验证余额并发送交易 2. **Server 验证签名** → 转发到 ListenServer
3. 等待 20 个区块确认 3. **ListenServer** → 写入 SQLite 数据库 → 添加到 PayMsgs → 转发到 Blockchain Node
4. 返回结果status=1 成功 / 0 失败 4. **Blockchain Node** → 验证余额 → 逐个发送交易 → 记录到数据库 → 返回 PayDatastatus=2
5. **ListenServer** → 更新数据库 → 发送到 RabbitMQ `pay_resp.queue`**第一次通知**
6. **监听链上事件** → 检测到支付交易 → 匹配交易 → 记录到 UnConfirmTxs
7. **新区块确认** → 达到确认高度 → 更新钱包余额 → 返回 PayDatastatus=1/0
8. **ListenServer** → 更新数据库 → 发送到 RabbitMQ `pay_resp.queue`**第二次通知**
### 4. 移除监听功能 🗑️
**完整流程:**
```
业务系统 → RabbitMQ → Server → ListenServer → 写数据库 → 添加到RemoveMsgs
Blockchain Node → 移除钱包监听
返回消息 → ListenServer → 修改数据库
返回RabbitMQ → 发送响应消息(status=1/0)
```
**特点:**
- ✅ 支持移除充值监听
- ✅ 支持移除提现/支付监听
- ✅ 实时生效
--- ---
@@ -355,16 +581,25 @@ cd test
cp config.json config.json.backup cp config.json config.json.backup
# 编辑 config.json填入实际配置 # 编辑 config.json填入实际配置
# 5. 编译主程序 # 5. 准备配置文件
cd .. cp bin/config.json ./config.json
# 编辑 config.json填入实际配置
# 6. 初始化数据库
# MySQL: 执行 public/eth_mysql.sql
# SQLite: 程序会自动创建(如果不存在)
# 7. 编译主程序
go build -o m2pool-payment cmd/main.go go build -o m2pool-payment cmd/main.go
# 6. 运行(指定通信密钥) # 8. 运行(指定通信密钥)
./m2pool-payment -key=your_secret_key ./m2pool-payment -key=your_secret_key
# 或者运行测试程序 # 或者使用启动脚本
cd test cd bin
go run test.go chmod +x build.sh start.sh
./build.sh
./start.sh your_secret_key
``` ```
### 配置文件示例 ### 配置文件示例
@@ -373,6 +608,7 @@ go run test.go
```json ```json
{ {
"net": ["ETH"],
"rmq_config": { "rmq_config": {
"sub_addr": "amqp://m2pool:m2pool@localhost:5672", "sub_addr": "amqp://m2pool:m2pool@localhost:5672",
"pay": { "pay": {
@@ -390,6 +626,11 @@ go run test.go
"exchange": "pay.exchange", "exchange": "pay.exchange",
"routing": ["pay.withdraw.routing.key"] "routing": ["pay.withdraw.routing.key"]
}, },
"remove": {
"queue": "pay.remove.queue",
"exchange": "pay.exchange",
"routing": ["pay.remove.routing.key"]
},
"pay_resp": { "pay_resp": {
"queue": "pay.auto.return.queue", "queue": "pay.auto.return.queue",
"exchange": "pay.exchange", "exchange": "pay.exchange",
@@ -404,22 +645,33 @@ go run test.go
"queue": "pay.withdraw.return.queue", "queue": "pay.withdraw.return.queue",
"exchange": "pay.exchange", "exchange": "pay.exchange",
"routing": ["pay.withdraw.return.routing.key"] "routing": ["pay.withdraw.return.routing.key"]
},
"remove_resp": {
"queue": "pay.remove.return.queue",
"exchange": "pay.exchange",
"routing": ["pay.remove.return.routing.key"]
} }
}, },
"eth_config": { "msg_config": {
"rpcUrl": "http://localhost:8545", "sqlite_path": "./msg.db"
"wsUrl": "ws://localhost:8546", },
"confirmHeight": 20, "mysql_config": {
"dbConfig": { "wallet": {
"user": "root", "user": "root",
"password": "your_password", "password": "your_password",
"host": "127.0.0.1", "host": "127.0.0.1",
"port": 3306, "port": 3306,
"database": "payment", "database": "payment",
"maxOpenConns": 20, "maxOpenConns": 20,
"maxIdleCoons": 20, "maxIdleConns": 10,
"connMaxLife": 120 "connMaxLife": 120000000000
} }
},
"eth_config": {
"rpc_url": "http://localhost:8545",
"ws_url": "ws://localhost:8546",
"confirm_height": 20,
"sqlite_path": "./eth.db"
} }
} }
``` ```
@@ -580,19 +832,23 @@ go run test.go
## 常见问题 ## 常见问题
### Q1: 为什么充值会收到两次通知? ### Q1: 为什么所有功能都会收到两次通知?
**A:** 这是设计特性! **A:** 这是设计特性!
- **第一次**status=2检测到交易提醒用户"正在确认" - **第一次**status=2检测到交易/开始处理,提醒用户"正在确认"
- **第二次**status=1/0交易确认通知最终结果 - **第二次**status=1/0交易确认通知最终结果
业务系统应该: 业务系统应该:
- status=2显示进度**不增加余额** - status=2显示进度**不修改余额或订单状态**
- status=1增加余额 - status=1增加/减少余额,更新订单状态
- status=0交易失败不修改余额提示错误
### Q2: 提现/支付为什么只有一次通知? ### Q2: 为什么需要两次通知?
**A:** 因为是系统主动发起的交易,用户已经知道在处理中,不需要额外的待确认通知。 **A:**
1. **实时反馈**:用户能立即知道交易已被检测到
2. **状态追踪**:业务系统可以追踪交易从待确认到最终确认的完整流程
3. **用户体验**:提供更好的用户交互体验
### Q3: 如何处理交易失败? ### Q3: 如何处理交易失败?
@@ -965,38 +1221,121 @@ type Metrics struct {
--- ---
## 业务流程详解
根据 `流程.txt` 文档,系统的完整业务流程如下:
### 充值流程
1. **RabbitMQ → ListenServer**
- 接收充值请求
- 写入 SQLite 数据库(`topup_req_msg` 表)
- 添加到 TopupMsgs 内存映射
2. **ListenServer → Blockchain Node**
- 转发充值请求到区块链节点
- 区块链节点接收后:
- 添加地址到钱包监听
- 记录到 MySQL 钱包数据库(`ETH_wallets` 表)
3. **Blockchain Node → ListenServer监听阶段**
- 新区块产生时,监听链上交易
- 对比消息中的 `to` 地址和区块交易中的 `to` 地址
- 如果匹配(充值检测):
- 返回 TopupMsg_respstatus=2 待确认)
- 记录到 UnConfirmTxs待确认交易池
- ListenServer 接收后:
- 修改数据库状态
- 返回 RabbitMQ → 发送第一次通知status=2
4. **Blockchain Node → ListenServer确认阶段**
- 新区块产生时,读取 UnConfirmTxs 数据
- 对比每个交易的高度:`当前高度 >= 交易高度 + 确认高度`
- 符合确认条件后:
- 修改钱包数据(更新余额)
- 返回 TopupMsg_respstatus=1 成功 / 0 失败)
- ListenServer 接收后:
- 修改数据库状态
- 返回 RabbitMQ → 发送第二次通知status=1/0
### 提现/支付流程
1. **RabbitMQ → ListenServer**
- 接收提现/支付请求
- 写入 SQLite 数据库(`withdraw_req_msg``pay_req_msg` 表)
- 添加到 WithdrawMsgs 或 PayMsgs 内存映射
2. **ListenServer → Blockchain Node**
- 转发提现/支付请求到区块链节点
- 区块链节点接收后:
- 记录到数据库
- 开始转账流程:
- 校验余额
- 发送交易
- 返回响应status=2 待确认)
- ListenServer 接收后:
- 修改相关状态
- 返回 RabbitMQ → 发送第一次通知status=2
3. **Blockchain Node → ListenServer监听阶段**
- 新区块产生时,监听链上交易
- 对比消息中的 `from``to``amount` 和区块交易中的对应字段
- 如果匹配(提现/支付检测):
- 返回响应status=2 待确认)
- 记录到 UnConfirmTxs
- ListenServer 接收后:
- 修改数据库状态
- 返回 RabbitMQ → 发送第一次通知status=2
4. **Blockchain Node → ListenServer确认阶段**
- 新区块产生时,读取 UnConfirmTxs 数据
- 对比每个交易的高度:`当前高度 >= 交易高度 + 确认高度`
- 符合确认条件后:
- 修改钱包数据(更新余额)
- 返回响应status=1 成功 / 0 失败)
- ListenServer 接收后:
- 修改数据库状态
- 返回 RabbitMQ → 发送第二次通知status=1/0
## 重要修复说明 ## 重要修复说明
### 🔧 已修复的问题 ### 🔧 已修复的问题
#### 1. QueueId 重复问题 #### 1. 服务启动问题
**问题**两笔不同的交易出现相同的 QueueId **问题**部分关键服务未启动
**原因**:数据库表主键设计错误,使用 `(from_addr, to_addr)` 作为主键
**修复** **修复**
- 修改数据库表结构,将主键改为 `queueId` - 添加 `rmqServer.Start()` 启动 RabbitMQ 服务
- 创建数据库迁移脚本 `public/migration.sql` - 添加 `messageServer.RmqMsgIn()` 启动消息接收监听
- 修复 SQL 插入语句的参数数量不匹配问题 - 添加 `messageServer.NetMsgIn()` 启动网络消息监听
- 添加 `blockChainServer.Listen()` 启动区块链监听
- 添加 `blockChainServer.ListenMsg()` 启动区块链消息监听
#### 2. 重复发送响应问题 #### 2. 错误处理优化
**问题**提现和支付会发送两次响应 **问题**使用 `log.Fatalf` 导致程序非正常退出
**原因**:转账失败时,先发送失败响应,然后仍然进入链上确认流程
**修复** **修复**
- 在转账失败时添加 `return` 语句 - 将非关键错误的 `log.Fatalf` 改为 `log.Printf`
- 确保转账失败时不进入链上确认流程 - 添加错误恢复机制,确保程序稳定运行
- 只有转账成功才会进入链上监听和确认
### 📊 修复后的消息发送次数 #### 3. 变量和字段名称修复
| 场景 | 消息处理阶段 | 链上确认阶段 | 总发送次数 | **问题**:部分变量和字段名称存在拼写错误
|------|-------------|-------------|-----------|
| **转账失败** | 发送失败响应 | 不进入已return | **1次** | **修复**
| **转账成功** | 不发送响应 | 发送成功响应 | **1次** | - 修复 `Heihgt``Height`
- 修复数据库字段名称不匹配问题
- 修复 mutex 解锁错误
#### 4. 逻辑错误修复
**问题**:交易确认高度判断逻辑错误
**修复**
- 修复确认高度判断条件:`now_height >= tx.Height+e.ConfirmHeight`
- 修复变量重复声明问题
--- ---

1
go.mod
View File

@@ -39,6 +39,7 @@ require (
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.4.2 // indirect
github.com/holiman/uint256 v1.3.2 // indirect github.com/holiman/uint256 v1.3.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/rabbitmq/amqp091-go v1.10.0 github.com/rabbitmq/amqp091-go v1.10.0
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect

4
go.sum
View File

@@ -117,8 +117,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= 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 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=

View File

@@ -0,0 +1,128 @@
# 安装指南
## 前置要求
1. WordPress 5.0 或更高版本
2. WooCommerce 3.0 或更高版本
3. PHP 7.4 或更高版本
4. M2Pool 支付系统后端Go 服务)
## 安装步骤
### 1. 安装 WordPress 插件
1.`integration/wordpress` 文件夹复制到 WordPress 的插件目录:
```
wp-content/plugins/m2pool-eth-payment/
```
2. 在 WordPress 后台激活插件:
- 进入 `插件 > 已安装的插件`
- 找到 "M2Pool ETH Payment Gateway"
- 点击 "启用"
### 2. 配置插件
1. 进入 `设置 > M2Pool ETH 支付`
2. 配置以下选项:
- **API 地址**: 支付系统的 API 地址(如果使用 API Bridge则为 `http://localhost:8080`
- **API 密钥**: 与后端系统的 `msgKey` 对应的密钥
- **接收地址**: 用于接收支付的以太坊地址
- **监听间隔**: 检查支付状态的间隔时间(建议 30-60 秒)
3. 保存设置
### 3. 配置 WooCommerce 支付网关
1. 进入 `WooCommerce > 设置 > 支付`
2. 找到 "M2Pool ETH 支付"
3. 点击 "管理"
4. 启用支付方式并配置:
- **标题**: 客户看到的支付方式名称
- **描述**: 支付方式说明
- **发送地址**: 发送支付的地址(可选)
- **接收地址**: 接收支付的地址
- **链名称**: 选择 ETH
- **代币符号**: 选择 ETH 或 USDT
5. 保存更改
### 4. 设置 API Bridge可选
如果您的后端系统只支持 RabbitMQ需要运行 API Bridge 服务:
1. 进入 `integration/wordpress/api-bridge` 目录
2. 安装依赖:
```bash
go mod download
```
3. 编译:
```bash
go build -o api-bridge main.go
```
4. 运行:
```bash
./api-bridge
```
5. 在 WordPress 插件设置中,将 API 地址设置为 `http://localhost:8080`
## 测试支付
1. 在 WooCommerce 创建一个测试订单
2. 选择 "ETH 支付" 作为支付方式
3. 完成订单后,您会看到支付说明页面
4. 向显示的地址支付指定金额的 ETH
5. 系统会自动检测支付并更新订单状态
## Webhook 配置(推荐)
为了实时接收支付状态更新,建议配置 Webhook
1. 在 WordPress 插件设置页面找到 Webhook URL
2. 将此 URL 配置到您的支付系统中
3. 当支付状态更新时,系统会自动通知 WordPress
Webhook URL 格式:
```
https://your-site.com/wp-json/m2pool-eth/v1/webhook
```
## 故障排除
### 支付状态不更新
1. 检查 API 地址是否正确
2. 检查 API 密钥是否匹配
3. 查看 WordPress 错误日志
4. 确认后端服务正常运行
### 无法创建支付请求
1. 检查 API Bridge 是否运行(如果使用)
2. 检查 RabbitMQ 连接是否正常
3. 查看后端服务日志
### 订单状态不更新
1. 检查定时任务是否运行:
- 进入 `工具 > 计划任务`
- 查找 `m2pool_eth_check_payments`
2. 手动触发支付检查(通过 AJAX
3. 检查数据库表 `wp_m2pool_eth_payments` 中的数据
## 数据库表
插件会自动创建以下数据库表:
- `wp_m2pool_eth_payments`: 存储支付记录
您可以通过 phpMyAdmin 或 WordPress 数据库工具查看和管理这些表。
## 支持
如有问题,请查看:
- README.md - 详细文档
- WordPress 错误日志
- 后端服务日志

View File

@@ -0,0 +1,202 @@
# M2Pool ETH Payment Gateway for WordPress
WordPress 支付网关插件,支持以太坊 (ETH) 交易的支付、监听和返回支付结果。
## 功能特性
- ✅ 集成 WooCommerce 支付网关
- ✅ 支持 ETH 和 USDT 支付
- ✅ 自动监听支付状态
- ✅ 支持 Webhook 回调
- ✅ 支付状态实时更新
- ✅ 完整的订单管理
## 安装要求
- WordPress 5.0+
- WooCommerce 3.0+
- PHP 7.4+
## 安装步骤
1. 将插件文件夹上传到 `/wp-content/plugins/` 目录
2. 在 WordPress 后台激活插件
3. 进入 `设置 > M2Pool ETH 支付` 配置 API 地址和密钥
4.`WooCommerce > 设置 > 支付` 中启用并配置支付网关
## 配置说明
### 基本设置
1. **API 地址**: 支付系统的 API 地址(例如: `http://localhost:8080`
2. **API 密钥**: 用于签名验证的密钥(与后端系统的 `msgKey` 对应)
3. **接收地址**: 用于接收支付的以太坊地址
4. **监听间隔**: 检查支付状态的间隔时间(秒)
### Webhook 配置
Webhook URL: `https://your-site.com/wp-json/m2pool-eth/v1/webhook`
将此 URL 配置到您的支付系统中,以便接收支付状态更新。
## API 接口说明
插件需要与后端支付系统通信。如果后端系统只支持 RabbitMQ您需要创建一个中间 API 服务。
### 需要的 API 接口
#### 1. 创建支付请求
```
POST /api/payment/create
```
请求体:
```json
{
"queue_id": "wp_123_1234567890",
"chain": "ETH",
"symbol": "ETH",
"from_address": "0x...",
"to_address": "0x...",
"amount": 0.1,
"fee": 0,
"timestamp": 1234567890,
"sign": "signature_hash"
}
```
响应:
```json
{
"success": true,
"queue_id": "wp_123_1234567890"
}
```
#### 2. 查询支付状态
```
GET /api/payment/status/{queue_id}
```
响应:
```json
{
"queue_id": "wp_123_1234567890",
"status": 1,
"tx_hash": "0x...",
"block_height": 12345,
"amount": 0.1
}
```
状态码说明:
- `0`: 待支付
- `1`: 支付成功
- `2`: 待确认
- `3`: 支付失败
## 中间 API 服务
如果您的后端系统只支持 RabbitMQ可以创建一个简单的 HTTP API 服务来桥接 WordPress 和 RabbitMQ。
示例代码Go:
```go
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/streadway/amqp"
)
func createPaymentHandler(w http.ResponseWriter, r *http.Request) {
var req PaymentRequest
json.NewDecoder(r.Body).Decode(&req)
// 发送到 RabbitMQ
conn, _ := amqp.Dial("amqp://guest:guest@localhost:5672/")
ch, _ := conn.Channel()
body, _ := json.Marshal(req)
ch.Publish("pay.exchange", "pay.auto.routing.key", false, false, amqp.Publishing{
ContentType: "application/json",
Body: body,
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
func main() {
http.HandleFunc("/api/payment/create", createPaymentHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
```
## 数据库结构
插件会自动创建以下数据库表:
- `wp_m2pool_eth_payments`: 存储支付记录
表结构:
- `id`: 主键
- `order_id`: WooCommerce 订单 ID
- `queue_id`: 支付队列 ID
- `from_address`: 发送地址
- `to_address`: 接收地址
- `amount`: 支付金额
- `fee`: 手续费
- `chain`: 链名称
- `symbol`: 代币符号
- `tx_hash`: 交易哈希
- `block_height`: 区块高度
- `status`: 支付状态
- `created_at`: 创建时间
- `updated_at`: 更新时间
## 使用流程
1. 客户选择 ETH 支付方式
2. 系统生成支付地址和金额
3. 客户向指定地址支付
4. 系统自动监听支付状态
5. 支付确认后自动更新订单状态
## 开发说明
### 文件结构
```
m2pool-eth-payment/
├── m2pool-eth-payment.php # 主插件文件
├── includes/
│ ├── class-m2pool-eth-gateway.php # 支付网关类
│ ├── class-m2pool-eth-api.php # API 客户端
│ └── class-m2pool-eth-listener.php # 支付监听器
├── templates/
│ ├── settings.php # 设置页面模板
│ └── payment-instructions.php # 支付说明模板
└── README.md
```
### 扩展开发
要添加新的功能,可以:
1. 扩展 `M2Pool_ETH_Gateway` 类添加新的支付方式
2. 扩展 `M2Pool_ETH_API` 类添加新的 API 接口
3. 修改 `M2Pool_ETH_Listener` 类自定义监听逻辑
## 许可证
MIT License
## 支持
如有问题,请提交 Issue 或联系开发团队。

View File

@@ -0,0 +1,78 @@
# M2Pool API Bridge
这是一个中间 API 服务,用于连接 WordPress 插件和基于 RabbitMQ 的支付系统。
## 功能
- 接收 WordPress 插件的 HTTP 请求
- 将请求转换为 RabbitMQ 消息
- 监听 RabbitMQ 响应并返回给 WordPress
## 安装
```bash
cd api-bridge
go mod download
go build -o api-bridge main.go
```
## 配置
修改 `main.go` 中的 RabbitMQ 配置:
```go
rmqConfig = RabbitMQConfig{
URL: "amqp://m2pool:m2pool@localhost:5672",
Exchange: "pay.exchange",
Queues: map[string]string{
"pay": "pay.auto.routing.key",
"topup": "pay.recharge.routing.key",
"status": "pay.auto.return.routing.key",
},
}
```
## 运行
```bash
./api-bridge
```
服务将在 `http://localhost:8080` 启动。
## API 接口
### 创建支付请求
```
POST /api/payment/create
```
### 查询支付状态
```
GET /api/payment/status/{queue_id}
```
### 创建充值请求
```
POST /api/topup/create
```
### 查询充值状态
```
GET /api/topup/status/{queue_id}
```
## 注意事项
当前实现是基础版本,实际使用中需要:
1. 实现状态缓存Redis 或数据库)
2. 监听 RabbitMQ 响应队列
3. 实现签名验证
4. 添加错误处理和日志记录
5. 添加认证和授权

View File

@@ -0,0 +1,8 @@
module m2pool-api-bridge
go 1.21
require (
github.com/gorilla/mux v1.8.1
github.com/streadway/amqp v1.1.0
)

View File

@@ -0,0 +1,4 @@
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM=
github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg=

View File

@@ -0,0 +1,241 @@
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/streadway/amqp"
)
// PaymentRequest 支付请求
type PaymentRequest struct {
QueueID string `json:"queue_id"`
Chain string `json:"chain"`
Symbol string `json:"symbol"`
FromAddress string `json:"from_address"`
ToAddress string `json:"to_address"`
Amount float64 `json:"amount"`
Fee float64 `json:"fee"`
Timestamp uint64 `json:"timestamp"`
Sign string `json:"sign"`
}
// TopupRequest 充值请求
type TopupRequest struct {
QueueID string `json:"queue_id"`
Chain string `json:"chain"`
Symbol string `json:"symbol"`
Address string `json:"address"`
Timestamp uint64 `json:"timestamp"`
Sign string `json:"sign"`
}
// APIResponse API 响应
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Message string `json:"message,omitempty"`
}
// RabbitMQConfig RabbitMQ 配置
type RabbitMQConfig struct {
URL string
Exchange string
Queues map[string]string
}
var rmqConfig RabbitMQConfig
var rmqConn *amqp.Connection
var rmqCh *amqp.Channel
func main() {
// 初始化 RabbitMQ 配置
rmqConfig = RabbitMQConfig{
URL: "amqp://m2pool:m2pool@localhost:5672",
Exchange: "pay.exchange",
Queues: map[string]string{
"pay": "pay.auto.routing.key",
"topup": "pay.recharge.routing.key",
"status": "pay.auto.return.routing.key",
},
}
// 连接 RabbitMQ
var err error
rmqConn, err = amqp.Dial(rmqConfig.URL)
if err != nil {
log.Fatalf("Failed to connect to RabbitMQ: %v", err)
}
defer rmqConn.Close()
rmqCh, err = rmqConn.Channel()
if err != nil {
log.Fatalf("Failed to open channel: %v", err)
}
defer rmqCh.Close()
// 设置路由
r := mux.NewRouter()
// 支付相关接口
r.HandleFunc("/api/payment/create", createPaymentHandler).Methods("POST")
r.HandleFunc("/api/payment/status/{queue_id}", getPaymentStatusHandler).Methods("GET")
// 充值相关接口
r.HandleFunc("/api/topup/create", createTopupHandler).Methods("POST")
r.HandleFunc("/api/topup/status/{queue_id}", getTopupStatusHandler).Methods("GET")
// 健康检查
r.HandleFunc("/health", healthCheckHandler).Methods("GET")
log.Println("API Bridge Server starting on :8080")
log.Fatal(http.ListenAndServe(":8888", r))
}
// createPaymentHandler 创建支付请求
func createPaymentHandler(w http.ResponseWriter, r *http.Request) {
var req PaymentRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, APIResponse{
Success: false,
Message: "Invalid request body",
})
return
}
// 发送到 RabbitMQ
body, err := json.Marshal(req)
if err != nil {
respondJSON(w, http.StatusInternalServerError, APIResponse{
Success: false,
Message: "Failed to marshal request",
})
return
}
err = rmqCh.Publish(
rmqConfig.Exchange,
rmqConfig.Queues["pay"],
false,
false,
amqp.Publishing{
ContentType: "application/json",
Body: body,
},
)
if err != nil {
log.Printf("Failed to publish payment request: %v", err)
respondJSON(w, http.StatusInternalServerError, APIResponse{
Success: false,
Message: "Failed to send payment request",
})
return
}
respondJSON(w, http.StatusOK, APIResponse{
Success: true,
Data: map[string]string{
"queue_id": req.QueueID,
},
})
}
// getPaymentStatusHandler 查询支付状态
func getPaymentStatusHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
queueID := vars["queue_id"]
// 这里应该从数据库或缓存中查询状态
// 为了演示,我们返回一个模拟响应
// 实际实现中,应该监听 RabbitMQ 的响应队列并存储状态
respondJSON(w, http.StatusOK, APIResponse{
Success: true,
Data: map[string]interface{}{
"queue_id": queueID,
"status": 0, // 0=待支付, 1=成功, 2=待确认, 3=失败
},
})
}
// createTopupHandler 创建充值请求
func createTopupHandler(w http.ResponseWriter, r *http.Request) {
var req TopupRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, APIResponse{
Success: false,
Message: "Invalid request body",
})
return
}
// 发送到 RabbitMQ
body, err := json.Marshal(req)
if err != nil {
respondJSON(w, http.StatusInternalServerError, APIResponse{
Success: false,
Message: "Failed to marshal request",
})
return
}
err = rmqCh.Publish(
rmqConfig.Exchange,
rmqConfig.Queues["topup"],
false,
false,
amqp.Publishing{
ContentType: "application/json",
Body: body,
},
)
if err != nil {
log.Printf("Failed to publish topup request: %v", err)
respondJSON(w, http.StatusInternalServerError, APIResponse{
Success: false,
Message: "Failed to send topup request",
})
return
}
respondJSON(w, http.StatusOK, APIResponse{
Success: true,
Data: map[string]string{
"queue_id": req.QueueID,
},
})
}
// getTopupStatusHandler 查询充值状态
func getTopupStatusHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
queueID := vars["queue_id"]
// 这里应该从数据库或缓存中查询状态
respondJSON(w, http.StatusOK, APIResponse{
Success: true,
Data: map[string]interface{}{
"queue_id": queueID,
"status": 0,
},
})
}
// healthCheckHandler 健康检查
func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, APIResponse{
Success: true,
Message: "API Bridge is running",
})
}
// respondJSON 返回 JSON 响应
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}

View File

@@ -0,0 +1,160 @@
<?php
/**
* M2Pool ETH API 客户端
*/
if (!defined('ABSPATH')) {
exit;
}
class M2Pool_ETH_API {
/**
* API 基础 URL
*/
private $api_url;
/**
* API 密钥
*/
private $api_key;
/**
* 构造函数
*/
public function __construct() {
$this->api_url = get_option('m2pool_eth_api_url', 'http://localhost:8080');
$this->api_key = get_option('m2pool_eth_api_key', '');
}
/**
* 生成签名
*/
private function generate_sign($timestamp) {
if (empty($this->api_key)) {
return '';
}
return hash('sha256', dechex($timestamp) . $this->api_key);
}
/**
* 发送 HTTP 请求
*/
private function request($endpoint, $method = 'GET', $data = array()) {
$url = rtrim($this->api_url, '/') . '/' . ltrim($endpoint, '/');
$args = array(
'method' => $method,
'timeout' => 30,
'headers' => array(
'Content-Type' => 'application/json',
),
);
if (!empty($data)) {
$args['body'] = json_encode($data);
}
$response = wp_remote_request($url, $args);
if (is_wp_error($response)) {
return $response;
}
$body = wp_remote_retrieve_body($response);
$code = wp_remote_retrieve_response_code($response);
if ($code >= 200 && $code < 300) {
$data = json_decode($body, true);
return $data;
} else {
return new WP_Error('api_error', sprintf(__('API 错误: %s', 'm2pool-eth-payment'), $body), array('status' => $code));
}
}
/**
* 创建支付请求
*
* 注意:这个方法假设有一个 HTTP API 接口。
* 如果后端系统只支持 RabbitMQ需要创建一个中间 API 服务。
*/
public function create_payment($queue_id, $from_address, $to_address, $amount, $chain = 'ETH', $symbol = 'ETH') {
$timestamp = time();
$sign = $this->generate_sign($timestamp);
$data = array(
'queue_id' => $queue_id,
'chain' => $chain,
'symbol' => $symbol,
'from_address' => $from_address,
'to_address' => $to_address,
'amount' => $amount,
'fee' => 0,
'timestamp' => $timestamp,
'sign' => $sign,
);
// 发送到支付 API
// 注意:这里需要根据实际的后端 API 接口调整
// 如果后端只支持 RabbitMQ需要创建一个中间 API 服务来处理
$result = $this->request('/api/payment/create', 'POST', $data);
if (is_wp_error($result)) {
return $result;
}
return $result;
}
/**
* 查询支付状态
*/
public function get_payment_status($queue_id) {
$result = $this->request('/api/payment/status/' . $queue_id, 'GET');
if (is_wp_error($result)) {
return $result;
}
return $result;
}
/**
* 创建充值监听请求
*/
public function create_topup($queue_id, $address, $chain = 'ETH', $symbol = 'ETH') {
$timestamp = time();
$sign = $this->generate_sign($timestamp);
$data = array(
'queue_id' => $queue_id,
'chain' => $chain,
'symbol' => $symbol,
'address' => $address,
'timestamp' => $timestamp,
'sign' => $sign,
);
$result = $this->request('/api/topup/create', 'POST', $data);
if (is_wp_error($result)) {
return $result;
}
return $result;
}
/**
* 查询充值状态
*/
public function get_topup_status($queue_id) {
$result = $this->request('/api/topup/status/' . $queue_id, 'GET');
if (is_wp_error($result)) {
return $result;
}
return $result;
}
}

View File

@@ -0,0 +1,320 @@
<?php
/**
* M2Pool ETH 支付网关类
*/
if (!defined('ABSPATH')) {
exit;
}
class M2Pool_ETH_Gateway extends WC_Payment_Gateway {
/**
* 构造函数
*/
public function __construct() {
$this->id = 'm2pool_eth';
$this->icon = apply_filters('woocommerce_m2pool_eth_icon', '');
$this->has_fields = false;
$this->method_title = __('M2Pool ETH 支付', 'm2pool-eth-payment');
$this->method_description = __('接受以太坊 (ETH) 支付', 'm2pool-eth-payment');
$this->supports = array(
'products',
);
// 加载设置
$this->init_form_fields();
$this->init_settings();
// 定义用户可见的设置
$this->title = $this->get_option('title', __('ETH 支付', 'm2pool-eth-payment'));
$this->description = $this->get_option('description', __('使用以太坊进行支付', 'm2pool-eth-payment'));
$this->enabled = $this->get_option('enabled', 'no');
// 保存设置
add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options'));
// 支付完成页面
add_action('woocommerce_thankyou_' . $this->id, array($this, 'thankyou_page'));
// 订单详情页面
add_action('woocommerce_order_details_after_order_table', array($this, 'order_details'), 10, 1);
// 确保在结账页面显示
add_filter('woocommerce_available_payment_gateways', array($this, 'ensure_available'), 100);
}
/**
* 确保支付网关可用(强制显示)
*/
public function ensure_available($available_gateways) {
// 如果已启用且配置正确,确保在列表中
if ($this->enabled === 'yes') {
$to_address = $this->get_option('to_address', get_option('m2pool_eth_to_address', ''));
if (!empty($to_address) && $this->is_available()) {
// 确保网关在可用列表中
if (!isset($available_gateways[$this->id])) {
$available_gateways[$this->id] = $this;
}
}
}
return $available_gateways;
}
/**
* 检查支付网关是否可用
*/
public function is_available() {
// 检查是否启用
if ($this->enabled !== 'yes') {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('M2Pool ETH Gateway: Not enabled');
}
return false;
}
// 检查是否有接收地址
$to_address = $this->get_option('to_address', get_option('m2pool_eth_to_address', ''));
if (empty($to_address)) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('M2Pool ETH Gateway: No receive address configured');
}
return false;
}
// 检查是否有订单(在结账页面)
if (is_checkout() || is_checkout_pay_page()) {
// 在结账页面,确保有订单
$order_id = absint(get_query_var('order-pay'));
if ($order_id > 0) {
$order = wc_get_order($order_id);
if (!$order || $order->get_total() <= 0) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('M2Pool ETH Gateway: Invalid order or zero amount');
}
return false;
}
}
}
// 调用父类方法
$parent_available = parent::is_available();
// 如果父类返回 false记录原因用于调试
if (!$parent_available && defined('WP_DEBUG') && WP_DEBUG) {
$order = WC()->cart;
$total = $order ? $order->get_total() : 0;
error_log(sprintf(
'M2Pool ETH Gateway: Parent is_available() returned false. Cart total: %s, Enabled: %s, Address: %s',
$total,
$this->enabled,
!empty($to_address) ? 'set' : 'empty'
));
}
return $parent_available;
}
/**
* 初始化表单字段
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __('启用/禁用', 'm2pool-eth-payment'),
'type' => 'checkbox',
'label' => __('启用 M2Pool ETH 支付', 'm2pool-eth-payment'),
'default' => 'no',
),
'title' => array(
'title' => __('标题', 'm2pool-eth-payment'),
'type' => 'text',
'description' => __('客户在结账时看到的支付方式标题', 'm2pool-eth-payment'),
'default' => __('ETH 支付', 'm2pool-eth-payment'),
'desc_tip' => true,
),
'description' => array(
'title' => __('描述', 'm2pool-eth-payment'),
'type' => 'textarea',
'description' => __('客户在结账时看到的支付方式描述', 'm2pool-eth-payment'),
'default' => __('使用以太坊 (ETH) 进行支付', 'm2pool-eth-payment'),
'desc_tip' => true,
),
'from_address' => array(
'title' => __('发送地址', 'm2pool-eth-payment'),
'type' => 'text',
'description' => __('用于接收支付的以太坊地址', 'm2pool-eth-payment'),
'default' => get_option('m2pool_eth_from_address', ''),
'desc_tip' => true,
),
'to_address' => array(
'title' => __('接收地址', 'm2pool-eth-payment'),
'type' => 'text',
'description' => __('用于接收支付的以太坊地址', 'm2pool-eth-payment'),
'default' => get_option('m2pool_eth_to_address', ''),
'desc_tip' => true,
),
'chain' => array(
'title' => __('链名称', 'm2pool-eth-payment'),
'type' => 'select',
'options' => array(
'ETH' => 'Ethereum',
),
'default' => 'ETH',
),
'symbol' => array(
'title' => __('代币符号', 'm2pool-eth-payment'),
'type' => 'select',
'options' => array(
'ETH' => 'ETH',
'USDT' => 'USDT',
),
'default' => 'ETH',
),
);
}
/**
* 处理支付
*/
public function process_payment($order_id) {
$order = wc_get_order($order_id);
if (!$order) {
wc_add_notice(__('订单不存在', 'm2pool-eth-payment'), 'error');
return;
}
// 获取 API 客户端
$api = new M2Pool_ETH_API();
// 准备支付请求
$amount = floatval($order->get_total());
$from_address = $this->get_option('from_address', get_option('m2pool_eth_from_address', ''));
$to_address = $this->get_option('to_address', get_option('m2pool_eth_to_address', ''));
$chain = $this->get_option('chain', 'ETH');
$symbol = $this->get_option('symbol', 'ETH');
if (empty($to_address)) {
wc_add_notice(__('支付网关配置错误:接收地址未设置', 'm2pool-eth-payment'), 'error');
return;
}
// 生成队列 ID
$queue_id = 'wp_' . $order_id . '_' . time();
// 发送支付请求
$result = $api->create_payment($queue_id, $from_address, $to_address, $amount, $chain, $symbol);
if (is_wp_error($result)) {
wc_add_notice($result->get_error_message(), 'error');
return;
}
// 保存支付信息到数据库
global $wpdb;
$table_name = $wpdb->prefix . 'm2pool_eth_payments';
$wpdb->insert(
$table_name,
array(
'order_id' => $order_id,
'queue_id' => $queue_id,
'from_address' => $from_address,
'to_address' => $to_address,
'amount' => $amount,
'fee' => isset($result['fee']) ? $result['fee'] : 0,
'chain' => $chain,
'symbol' => $symbol,
'status' => 0, // 待支付
),
array('%d', '%s', '%s', '%s', '%f', '%f', '%s', '%s', '%d')
);
// 更新订单元数据
$order->update_meta_data('_m2pool_queue_id', $queue_id);
$order->update_meta_data('_m2pool_payment_address', $to_address);
$order->update_meta_data('_m2pool_payment_amount', $amount);
$order->update_meta_data('_m2pool_payment_chain', $chain);
$order->update_meta_data('_m2pool_payment_symbol', $symbol);
$order->save();
// 将订单状态设置为待支付
$order->update_status('pending', __('等待 ETH 支付', 'm2pool-eth-payment'));
// 启动支付监听
$listener = new M2Pool_ETH_Listener();
$listener->start_listening($order_id);
// 返回成功
return array(
'result' => 'success',
'redirect' => $this->get_return_url($order),
);
}
/**
* 感谢页面
*/
public function thankyou_page($order_id) {
$order = wc_get_order($order_id);
if (!$order || $order->get_payment_method() !== $this->id) {
return;
}
$payment_address = $order->get_meta('_m2pool_payment_address');
$payment_amount = $order->get_meta('_m2pool_payment_amount');
$payment_symbol = $order->get_meta('_m2pool_payment_symbol');
$queue_id = $order->get_meta('_m2pool_queue_id');
if (!$payment_address || !$payment_amount) {
return;
}
include M2POOL_ETH_PLUGIN_DIR . 'templates/payment-instructions.php';
}
/**
* 订单详情页面
*/
public function order_details($order) {
if ($order->get_payment_method() !== $this->id) {
return;
}
$payment_address = $order->get_meta('_m2pool_payment_address');
$payment_amount = $order->get_meta('_m2pool_payment_amount');
$payment_symbol = $order->get_meta('_m2pool_payment_symbol');
$tx_hash = $order->get_meta('_m2pool_tx_hash');
if (!$payment_address || !$payment_amount) {
return;
}
?>
<section class="woocommerce-order-details m2pool-eth-payment-details">
<h2 class="woocommerce-order-details__title"><?php echo esc_html__('ETH 支付信息', 'm2pool-eth-payment'); ?></h2>
<table class="woocommerce-table woocommerce-table--order-details shop_table order_details">
<tbody>
<tr>
<th><?php echo esc_html__('支付地址', 'm2pool-eth-payment'); ?>:</th>
<td><code><?php echo esc_html($payment_address); ?></code></td>
</tr>
<tr>
<th><?php echo esc_html__('支付金额', 'm2pool-eth-payment'); ?>:</th>
<td><?php echo esc_html($payment_amount); ?> <?php echo esc_html($payment_symbol); ?></td>
</tr>
<?php if ($tx_hash): ?>
<tr>
<th><?php echo esc_html__('交易哈希', 'm2pool-eth-payment'); ?>:</th>
<td><a href="https://etherscan.io/tx/<?php echo esc_attr($tx_hash); ?>" target="_blank"><?php echo esc_html($tx_hash); ?></a></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</section>
<?php
}
}

View File

@@ -0,0 +1,255 @@
<?php
/**
* M2Pool ETH 支付监听器
*/
if (!defined('ABSPATH')) {
exit;
}
class M2Pool_ETH_Listener {
/**
* API 客户端
*/
private $api;
/**
* 构造函数
*/
public function __construct() {
$this->api = new M2Pool_ETH_API();
}
/**
* 开始监听订单支付
*/
public function start_listening($order_id) {
// 创建定时任务检查支付状态
if (!wp_next_scheduled('m2pool_eth_check_payments')) {
$interval = get_option('m2pool_eth_listen_interval', 30);
wp_schedule_event(time(), 'm2pool_eth_interval', 'm2pool_eth_check_payments');
}
// 添加自定义间隔
add_filter('cron_schedules', array($this, 'add_cron_interval'));
}
/**
* 添加自定义 Cron 间隔
*/
public function add_cron_interval($schedules) {
$interval = get_option('m2pool_eth_listen_interval', 30);
$schedules['m2pool_eth_interval'] = array(
'interval' => $interval,
'display' => sprintf(__('每 %d 秒', 'm2pool-eth-payment'), $interval),
);
return $schedules;
}
/**
* 检查支付状态
*/
public function check_payment_status($order_id) {
$order = wc_get_order($order_id);
if (!$order) {
return array(
'success' => false,
'message' => __('订单不存在', 'm2pool-eth-payment'),
);
}
$queue_id = $order->get_meta('_m2pool_queue_id');
if (empty($queue_id)) {
return array(
'success' => false,
'message' => __('未找到支付队列 ID', 'm2pool-eth-payment'),
);
}
// 从数据库获取支付记录
global $wpdb;
$table_name = $wpdb->prefix . 'm2pool_eth_payments';
$payment = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE order_id = %d AND queue_id = %s",
$order_id,
$queue_id
));
if (!$payment) {
return array(
'success' => false,
'message' => __('未找到支付记录', 'm2pool-eth-payment'),
);
}
// 如果已经成功,直接返回
if ($payment->status == 1) {
return array(
'success' => true,
'status' => 'completed',
'message' => __('支付已完成', 'm2pool-eth-payment'),
);
}
// 查询 API 获取最新状态
$result = $this->api->get_payment_status($queue_id);
if (is_wp_error($result)) {
return array(
'success' => false,
'message' => $result->get_error_message(),
);
}
// 更新支付状态
$this->update_payment_status($order_id, $result);
return array(
'success' => true,
'status' => $this->get_status_text($result['status']),
'data' => $result,
);
}
/**
* 处理 Webhook
*/
public function process_webhook($data) {
if (!isset($data['queue_id'])) {
return false;
}
$queue_id = $data['queue_id'];
// 从数据库查找订单
global $wpdb;
$table_name = $wpdb->prefix . 'm2pool_eth_payments';
$payment = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE queue_id = %s",
$queue_id
));
if (!$payment) {
return false;
}
// 更新支付状态
$this->update_payment_status($payment->order_id, $data);
return true;
}
/**
* 更新支付状态
*/
private function update_payment_status($order_id, $data) {
$order = wc_get_order($order_id);
if (!$order) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'm2pool_eth_payments';
$status = isset($data['status']) ? intval($data['status']) : 0;
$tx_hash = isset($data['tx_hash']) ? $data['tx_hash'] : null;
$block_height = isset($data['block_height']) ? $data['block_height'] : null;
$amount = isset($data['amount']) ? floatval($data['amount']) : null;
// 更新数据库
$update_data = array(
'status' => $status,
);
if ($tx_hash) {
$update_data['tx_hash'] = $tx_hash;
}
if ($block_height) {
$update_data['block_height'] = $block_height;
}
if ($amount !== null) {
$update_data['amount'] = $amount;
}
$wpdb->update(
$table_name,
$update_data,
array('order_id' => $order_id),
array('%d', '%s', '%d', '%f'),
array('%d')
);
// 更新订单状态
if ($status == 1) {
// 支付成功
$order->update_meta_data('_m2pool_tx_hash', $tx_hash);
$order->payment_complete();
$order->add_order_note(__('ETH 支付成功', 'm2pool-eth-payment'));
} elseif ($status == 2) {
// 待确认
$order->update_status('on-hold', __('ETH 支付待确认', 'm2pool-eth-payment'));
if ($tx_hash) {
$order->update_meta_data('_m2pool_tx_hash', $tx_hash);
$order->add_order_note(sprintf(__('ETH 支付待确认,交易哈希: %s', 'm2pool-eth-payment'), $tx_hash));
}
} elseif ($status == 0 || $status == 3) {
// 支付失败
$order->update_status('failed', __('ETH 支付失败', 'm2pool-eth-payment'));
$order->add_order_note(__('ETH 支付失败', 'm2pool-eth-payment'));
}
$order->save();
}
/**
* 获取状态文本
*/
private function get_status_text($status) {
$statuses = array(
0 => 'pending',
1 => 'completed',
2 => 'processing',
3 => 'failed',
);
return isset($statuses[$status]) ? $statuses[$status] : 'unknown';
}
/**
* 批量检查所有待支付订单
*/
public function check_all_pending_payments() {
global $wpdb;
$table_name = $wpdb->prefix . 'm2pool_eth_payments';
// 获取所有待支付和待确认的订单
$payments = $wpdb->get_results(
"SELECT * FROM $table_name WHERE status IN (0, 2) ORDER BY created_at ASC LIMIT 50"
);
foreach ($payments as $payment) {
$this->check_payment_status($payment->order_id);
// 避免请求过快
sleep(1);
}
}
}
// 注册定时任务处理函数
add_action('m2pool_eth_check_payments', 'm2pool_eth_check_all_payments');
// 定时任务处理函数
function m2pool_eth_check_all_payments() {
$listener = new M2Pool_ETH_Listener();
$listener->check_all_pending_payments();
}

View File

@@ -0,0 +1,275 @@
<?php
/**
* Plugin Name: M2Pool ETH Payment Gateway
* Plugin URI: https://github.com/m2pool/payment-gateway
* Description: 支持 ETH 交易的 WordPress 支付网关,支持支付、监听和返回支付结果
* Version: 1.0.0
* Author: M2Pool
* Author URI: https://m2pool.com
* Text Domain: m2pool-eth-payment
* Domain Path: /languages
* Requires at least: 5.0
* Requires PHP: 7.4
* WC requires at least: 3.0
* WC tested up to: 8.0
*/
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
// 定义插件常量
define('M2POOL_ETH_VERSION', '1.0.0');
define('M2POOL_ETH_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('M2POOL_ETH_PLUGIN_URL', plugin_dir_url(__FILE__));
define('M2POOL_ETH_PLUGIN_FILE', __FILE__);
/**
* 主插件类
*/
class M2Pool_ETH_Payment {
/**
* 单例实例
*/
private static $instance = null;
/**
* 获取单例实例
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 构造函数
*/
private function __construct() {
$this->init_hooks();
}
/**
* 初始化钩子
*/
private function init_hooks() {
// 激活插件时创建数据库表
register_activation_hook(__FILE__, array($this, 'activate'));
// 停用插件时清理
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
// 在 WooCommerce 加载后初始化
add_action('plugins_loaded', array($this, 'init'), 11);
// 添加管理菜单
add_action('admin_menu', array($this, 'add_admin_menu'));
// 注册 AJAX 处理
add_action('wp_ajax_m2pool_check_payment', array($this, 'ajax_check_payment'));
add_action('wp_ajax_nopriv_m2pool_check_payment', array($this, 'ajax_check_payment'));
// 注册 Webhook 端点
add_action('rest_api_init', array($this, 'register_webhook_routes'));
// 加载文本域
add_action('init', array($this, 'load_textdomain'));
}
/**
* 初始化插件
*/
public function init() {
// 检查 WooCommerce 是否激活
if (!class_exists('WooCommerce')) {
add_action('admin_notices', array($this, 'woocommerce_missing_notice'));
return;
}
// 加载支付网关类
require_once M2POOL_ETH_PLUGIN_DIR . 'includes/class-m2pool-eth-gateway.php';
require_once M2POOL_ETH_PLUGIN_DIR . 'includes/class-m2pool-eth-api.php';
require_once M2POOL_ETH_PLUGIN_DIR . 'includes/class-m2pool-eth-listener.php';
// 注册支付网关(必须在类加载后)
add_filter('woocommerce_payment_gateways', array($this, 'add_gateway'));
}
/**
* WooCommerce 缺失通知
*/
public function woocommerce_missing_notice() {
?>
<div class="error">
<p><?php echo esc_html__('M2Pool ETH Payment Gateway 需要 WooCommerce 插件才能工作。', 'm2pool-eth-payment'); ?></p>
</div>
<?php
}
/**
* 添加支付网关
*/
public function add_gateway($gateways) {
$gateways[] = 'M2Pool_ETH_Gateway';
return $gateways;
}
/**
* 激活插件
*/
public function activate() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_name = $wpdb->prefix . 'm2pool_eth_payments';
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
order_id bigint(20) UNSIGNED NOT NULL,
queue_id varchar(255) NOT NULL,
from_address varchar(255) NOT NULL,
to_address varchar(255) NOT NULL,
amount decimal(20,8) NOT NULL,
fee decimal(20,8) DEFAULT 0,
chain varchar(50) DEFAULT 'ETH',
symbol varchar(50) DEFAULT 'ETH',
tx_hash varchar(255) DEFAULT NULL,
block_height bigint(20) UNSIGNED DEFAULT NULL,
status int(11) DEFAULT 0 COMMENT '0=待支付,1=成功,2=待确认,3=失败',
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY queue_id (queue_id),
KEY order_id (order_id),
KEY tx_hash (tx_hash),
KEY status (status)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
// 设置默认选项
if (!get_option('m2pool_eth_api_url')) {
update_option('m2pool_eth_api_url', 'http://localhost:8080');
}
if (!get_option('m2pool_eth_api_key')) {
update_option('m2pool_eth_api_key', '');
}
if (!get_option('m2pool_eth_listen_interval')) {
update_option('m2pool_eth_listen_interval', 30); // 30秒轮询一次
}
}
/**
* 停用插件
*/
public function deactivate() {
// 清理定时任务
wp_clear_scheduled_hook('m2pool_eth_check_payments');
}
/**
* 添加管理菜单
*/
public function add_admin_menu() {
add_options_page(
__('M2Pool ETH 支付设置', 'm2pool-eth-payment'),
__('M2Pool ETH 支付', 'm2pool-eth-payment'),
'manage_options',
'm2pool-eth-settings',
array($this, 'settings_page')
);
}
/**
* 设置页面
*/
public function settings_page() {
if (isset($_POST['m2pool_eth_save_settings'])) {
check_admin_referer('m2pool_eth_settings');
update_option('m2pool_eth_api_url', sanitize_text_field($_POST['api_url']));
update_option('m2pool_eth_api_key', sanitize_text_field($_POST['api_key']));
update_option('m2pool_eth_listen_interval', intval($_POST['listen_interval']));
update_option('m2pool_eth_from_address', sanitize_text_field($_POST['from_address']));
update_option('m2pool_eth_to_address', sanitize_text_field($_POST['to_address']));
echo '<div class="notice notice-success"><p>' . esc_html__('设置已保存', 'm2pool-eth-payment') . '</p></div>';
}
$api_url = get_option('m2pool_eth_api_url', 'http://localhost:8080');
$api_key = get_option('m2pool_eth_api_key', '');
$listen_interval = get_option('m2pool_eth_listen_interval', 30);
$from_address = get_option('m2pool_eth_from_address', '');
$to_address = get_option('m2pool_eth_to_address', '');
include M2POOL_ETH_PLUGIN_DIR . 'templates/settings.php';
}
/**
* AJAX 检查支付状态
*/
public function ajax_check_payment() {
check_ajax_referer('m2pool_eth_check', 'nonce');
$order_id = intval($_POST['order_id']);
$order = wc_get_order($order_id);
if (!$order) {
wp_send_json_error(array('message' => __('订单不存在', 'm2pool-eth-payment')));
}
$listener = new M2Pool_ETH_Listener();
$result = $listener->check_payment_status($order_id);
if ($result['success']) {
wp_send_json_success($result);
} else {
wp_send_json_error($result);
}
}
/**
* 注册 Webhook 路由
*/
public function register_webhook_routes() {
register_rest_route('m2pool-eth/v1', '/webhook', array(
'methods' => 'POST',
'callback' => array($this, 'handle_webhook'),
'permission_callback' => '__return_true',
));
}
/**
* 处理 Webhook
*/
public function handle_webhook($request) {
$data = $request->get_json_params();
if (!isset($data['queue_id'])) {
return new WP_Error('invalid_data', __('无效的数据', 'm2pool-eth-payment'), array('status' => 400));
}
$listener = new M2Pool_ETH_Listener();
$result = $listener->process_webhook($data);
if ($result) {
return new WP_REST_Response(array('success' => true), 200);
} else {
return new WP_Error('processing_failed', __('处理失败', 'm2pool-eth-payment'), array('status' => 500));
}
}
/**
* 加载文本域
*/
public function load_textdomain() {
load_plugin_textdomain('m2pool-eth-payment', false, dirname(plugin_basename(__FILE__)) . '/languages');
}
}
// 初始化插件
M2Pool_ETH_Payment::get_instance();

View File

@@ -0,0 +1,136 @@
<?php
/**
* 支付说明模板
*/
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="m2pool-eth-payment-instructions">
<h3><?php echo esc_html__('ETH 支付说明', 'm2pool-eth-payment'); ?></h3>
<div class="payment-info">
<p><strong><?php echo esc_html__('请向以下地址支付:', 'm2pool-eth-payment'); ?></strong></p>
<div class="payment-address">
<code id="payment-address-code"><?php echo esc_html($payment_address); ?></code>
<button type="button" class="button copy-address" data-address="<?php echo esc_attr($payment_address); ?>">
<?php echo esc_html__('复制地址', 'm2pool-eth-payment'); ?>
</button>
</div>
<p><strong><?php echo esc_html__('支付金额:', 'm2pool-eth-payment'); ?></strong></p>
<p class="payment-amount"><?php echo esc_html($payment_amount); ?> <?php echo esc_html($payment_symbol); ?></p>
<div class="payment-status" id="payment-status">
<p><?php echo esc_html__('等待支付中...', 'm2pool-eth-payment'); ?></p>
</div>
</div>
<div class="payment-notes">
<p><strong><?php echo esc_html__('注意事项:', 'm2pool-eth-payment'); ?></strong></p>
<ul>
<li><?php echo esc_html__('请确保支付金额准确', 'm2pool-eth-payment'); ?></li>
<li><?php echo esc_html__('支付完成后,系统会自动确认(通常需要几分钟)', 'm2pool-eth-payment'); ?></li>
<li><?php echo esc_html__('请勿向此地址发送其他代币', 'm2pool-eth-payment'); ?></li>
</ul>
</div>
</div>
<script type="text/javascript">
jQuery(document).ready(function($) {
// 复制地址功能
$('.copy-address').on('click', function() {
var address = $(this).data('address');
var $temp = $('<input>');
$('body').append($temp);
$temp.val(address).select();
document.execCommand('copy');
$temp.remove();
$(this).text('<?php echo esc_js(__('已复制', 'm2pool-eth-payment')); ?>');
setTimeout(function() {
$('.copy-address').text('<?php echo esc_js(__('复制地址', 'm2pool-eth-payment')); ?>');
}, 2000);
});
// 定期检查支付状态
var orderId = <?php echo esc_js($order_id); ?>;
var checkInterval = setInterval(function() {
$.ajax({
url: '<?php echo esc_url(admin_url('admin-ajax.php')); ?>',
type: 'POST',
data: {
action: 'm2pool_check_payment',
order_id: orderId,
nonce: '<?php echo esc_js(wp_create_nonce('m2pool_eth_check')); ?>'
},
success: function(response) {
if (response.success && response.data.status === 'completed') {
clearInterval(checkInterval);
$('#payment-status').html('<p style="color: green;"><?php echo esc_js(__('支付成功!页面将自动刷新...', 'm2pool-eth-payment')); ?></p>');
setTimeout(function() {
location.reload();
}, 2000);
} else if (response.success && response.data.status === 'processing') {
$('#payment-status').html('<p style="color: orange;"><?php echo esc_js(__('支付确认中...', 'm2pool-eth-payment')); ?></p>');
} else if (response.success && response.data.status === 'failed') {
clearInterval(checkInterval);
$('#payment-status').html('<p style="color: red;"><?php echo esc_js(__('支付失败', 'm2pool-eth-payment')); ?></p>');
}
}
});
}, 10000); // 每10秒检查一次
// 30秒后停止检查避免无限检查
setTimeout(function() {
clearInterval(checkInterval);
}, 300000); // 5分钟
});
</script>
<style>
.m2pool-eth-payment-instructions {
margin: 20px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
background: #f9f9f9;
}
.payment-address {
margin: 10px 0;
display: flex;
align-items: center;
gap: 10px;
}
.payment-address code {
flex: 1;
padding: 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
word-break: break-all;
}
.payment-amount {
font-size: 1.2em;
font-weight: bold;
color: #0073aa;
margin: 10px 0;
}
.payment-status {
margin: 15px 0;
padding: 10px;
background: #fff;
border-radius: 4px;
}
.payment-notes ul {
margin-left: 20px;
}
</style>

View File

@@ -0,0 +1,81 @@
<?php
/**
* 设置页面模板
*/
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="wrap">
<h1><?php echo esc_html__('M2Pool ETH 支付设置', 'm2pool-eth-payment'); ?></h1>
<form method="post" action="">
<?php wp_nonce_field('m2pool_eth_settings'); ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="api_url"><?php echo esc_html__('API 地址', 'm2pool-eth-payment'); ?></label>
</th>
<td>
<input type="url" id="api_url" name="api_url" value="<?php echo esc_attr($api_url); ?>" class="regular-text" />
<p class="description"><?php echo esc_html__('支付系统的 API 地址,例如: http://localhost:8080', 'm2pool-eth-payment'); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="api_key"><?php echo esc_html__('API 密钥', 'm2pool-eth-payment'); ?></label>
</th>
<td>
<input type="text" id="api_key" name="api_key" value="<?php echo esc_attr($api_key); ?>" class="regular-text" />
<p class="description"><?php echo esc_html__('用于签名验证的 API 密钥', 'm2pool-eth-payment'); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="from_address"><?php echo esc_html__('发送地址', 'm2pool-eth-payment'); ?></label>
</th>
<td>
<input type="text" id="from_address" name="from_address" value="<?php echo esc_attr($from_address); ?>" class="regular-text" />
<p class="description"><?php echo esc_html__('用于发送支付的以太坊地址(可选)', 'm2pool-eth-payment'); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="to_address"><?php echo esc_html__('接收地址', 'm2pool-eth-payment'); ?></label>
</th>
<td>
<input type="text" id="to_address" name="to_address" value="<?php echo esc_attr($to_address); ?>" class="regular-text" />
<p class="description"><?php echo esc_html__('用于接收支付的以太坊地址', 'm2pool-eth-payment'); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="listen_interval"><?php echo esc_html__('监听间隔(秒)', 'm2pool-eth-payment'); ?></label>
</th>
<td>
<input type="number" id="listen_interval" name="listen_interval" value="<?php echo esc_attr($listen_interval); ?>" min="10" max="300" />
<p class="description"><?php echo esc_html__('检查支付状态的间隔时间(秒),建议 30-60 秒', 'm2pool-eth-payment'); ?></p>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="m2pool_eth_save_settings" class="button button-primary" value="<?php echo esc_attr__('保存设置', 'm2pool-eth-payment'); ?>" />
</p>
</form>
<hr />
<h2><?php echo esc_html__('Webhook 设置', 'm2pool-eth-payment'); ?></h2>
<p><?php echo esc_html__('Webhook URL用于接收支付状态通知:', 'm2pool-eth-payment'); ?></p>
<code><?php echo esc_url(rest_url('m2pool-eth/v1/webhook')); ?></code>
<p class="description"><?php echo esc_html__('将此 URL 配置到您的支付系统中,以便接收支付状态更新', 'm2pool-eth-payment'); ?></p>
</div>

View File

@@ -1,21 +1,22 @@
// 区块链网络抽象接口统一实现
package blockchain package blockchain
import ( import (
"fmt" "fmt"
"log"
"sync" "sync"
) )
type IChainServer interface { type IChainServer interface {
AddAddress(address string, msg any) error Listen() error
RemoveAddress(address string) error ListenMsg()
Listen(symbol string, ch chan any) Transfer(from, to, symbol string, amount, fee float64) error
Transfer(symbol string, msg any) error
Stop() Stop()
} }
type BlockChainServer struct { type BlockChainServer struct {
mu sync.Mutex mu sync.Mutex
chains map[string]IChainServer // "ETH", "TRON", "BTC" chains map[string]IChainServer
} }
func NewBlockChainServer() *BlockChainServer { func NewBlockChainServer() *BlockChainServer {
@@ -30,40 +31,41 @@ func (b *BlockChainServer) RegisterChain(name string, chain IChainServer) {
b.chains[name] = chain b.chains[name] = chain
} }
func (b *BlockChainServer) AddAddress(chain, address string, msg any) error { func (b *BlockChainServer) Listen(chain string, ch chan 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 {
return fmt.Errorf("⚠️ 链未注册: %s\n", chain)
}
}
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 {
return fmt.Errorf("⚠️ 链未注册: %s\n", chain)
}
}
func (b *BlockChainServer) Listen(chain, symbol string, ch chan any) error {
if srv, ok := b.chains[chain]; ok { if srv, ok := b.chains[chain]; ok {
go func() { go func() {
srv.Listen(symbol, ch) err := srv.Listen()
if err != nil {
log.Printf("Start Listen error: %v", err)
return
}
}() }()
return nil return nil
} }
return fmt.Errorf("链未注册: %s", chain) return fmt.Errorf("链未注册: %s", chain)
} }
func (b *BlockChainServer) Transfer(chain, symbol string, msg any) error { func (b *BlockChainServer) ListenMsg(chain string) error {
if srv, ok := b.chains[chain]; ok { if srv, ok := b.chains[chain]; ok {
fmt.Printf("💸 %s-%s发起转账: %+v\n", chain, symbol, msg) go func() {
return srv.Transfer(symbol, msg) srv.ListenMsg()
}()
return nil
}
return fmt.Errorf("链未注册: %s", chain)
}
func (b *BlockChainServer) Transfer(chain, from, to, symbol string, amount, fee float64) error {
if srv, ok := b.chains[chain]; ok {
fmt.Printf("💸 发起转账: Chain(%s)-Symbol(%s), From(%s), To(%s), Amount(%f)\n", chain, symbol, from, to, amount)
go func() {
err := srv.Transfer(from, to, symbol, amount, fee)
if err != nil {
log.Printf("Transfer Error: %v\n Transfer has Exited", err)
return
}
}()
return nil
} }
return fmt.Errorf("链未注册: %s", chain) return fmt.Errorf("链未注册: %s", chain)
} }
@@ -72,6 +74,7 @@ func (b *BlockChainServer) Stop(chain string) {
if srv, ok := b.chains[chain]; ok { if srv, ok := b.chains[chain]; ok {
fmt.Printf("💸 关闭服务: %+v\n", chain) fmt.Printf("💸 关闭服务: %+v\n", chain)
srv.Stop() srv.Stop()
return
} }
fmt.Printf("链未注册: %s", chain) fmt.Printf("链未注册: %s", chain)
} }

View File

@@ -1,213 +0,0 @@
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
}

View File

@@ -1,101 +0,0 @@
# 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 费用
- 交易按顺序发送,可能在高负载时较慢

View File

@@ -5,821 +5,227 @@ import (
"fmt" "fmt"
"log" "log"
"m2pool-payment/internal/db" "m2pool-payment/internal/db"
"m2pool-payment/internal/listen"
message "m2pool-payment/internal/msg" message "m2pool-payment/internal/msg"
"m2pool-payment/internal/utils"
"math/big" "math/big"
"strings"
"sync" "sync"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
) )
const erc20ABI = `
[
{
"constant": true,
"inputs": [{"name": "_owner", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "balance", "type": "uint256"}],
"type": "function"
},
{
"constant": false,
"inputs": [
{"name": "_to", "type": "address"},
{"name": "_value", "type": "uint256"}
],
"name": "transfer",
"outputs": [{"name": "", "type": "bool"}],
"type": "function"
},
{
"anonymous": false,
"inputs": [
{"indexed": true, "name": "from", "type": "address"},
{"indexed": true, "name": "to", "type": "address"},
{"indexed": false,"name": "value","type": "uint256"}
],
"name": "Transfer",
"type": "event"
}
]
`
type ETHNode struct { type ETHNode struct {
decodeKey string // 私钥解密密钥,从程序启动命令行获得 decodeKey string // 消息解密密钥,用于解密RMQ消息的Sign
NetId *big.Int // 网络ID主网为1其他ID可通过rpc.NetworkID方法获取 NetID *big.Int
Config message.ETHConfig // 配置文件 Config message.ETHConfig
WsClient *ethclient.Client ConfirmHeight uint64
RpcClient *ethclient.Client WsClient *ethclient.Client
Db db.MySQLPool RpcClient *ethclient.Client
mu sync.Mutex MysqlDB *db.MySQLPool
ListenAddresses sync.Map // key:"钱包地址", value:bool SqliteDB *db.SQLite
UnConfirmTxs map[string]message.Tx_msg // 待交易地址池key:"交易hash", value: message.Tx USDT *USDT
USDT *USDT // ETH相关 NetInfo *NetInfo // 网络通用状态(当前高度、gas费用等)
RmqMsgs map[string][]any // 根据地址查找出该地址涉及的消息,消息需要断言(topupreq_msg, payreq_msg, withdrawreq_msg) // Messages map[string]any // {"queue_id": message.Topup{}, "queue_id": message.Withdraw{}, ...}
Ctx context.Context MessageServer *listen.ListenServer
Cancel context.CancelFunc UnConfirmedTxs *UnConfirmedTxs
Wallets map[string]*Wallets // {"address" : &Wallets{}, ...}
LogsChan chan *types.Header
Ctx context.Context
Cancel context.CancelFunc
mu sync.Mutex
} }
type USDT struct { type USDT struct {
Address common.Address // USDT合约地址 Address common.Address // USDT合约地址
ABI abi.ABI // USDT ABI ListeningAddresses map[string]any // 监听的USDT转账消息
TransferSig common.Hash // USDT函数签名 ABI abi.ABI // USDT ABI
LogsChan chan types.Log TransferSig common.Hash // USDT函数签名
LogsChan chan types.Log
} }
func NewETHNode(cfg message.ETHConfig, decodeKey string) (*ETHNode, error) { type UnConfirmedTxs struct {
mu sync.Mutex
Transactions map[string]message.Transaction // {"queue_id": message.Transactions{}, ...}
}
type NetInfo struct {
mu sync.Mutex
Height uint64
GasLimit uint64
GasTipCap *big.Int
GasFeeCap *big.Int
GasPrice *big.Int // 老版本转账使用的gas
}
type Wallets struct {
address string
queueId string
pk string
eth_balance *eth_balance
usdt_balance *usdt_balance
timestamp uint64
sign string
status int
mu sync.Mutex
}
type eth_balance struct {
symbol string // 默认ETH
used_gas *big.Int // 1 ETH = 1e18 WEI
balance *big.Int // 实际拥有的ETH余额
successed_tx_hash []string
failed_tx_hash []string
}
type usdt_balance struct {
symbol string
freeze_num *big.Int // 1 USDT = 1e6 WEI
balance *big.Int // 实际拥有的USDT余额
successed_tx_hash []string
failed_tx_hash []string
}
func NewETHNode(cfg message.Config, decodeKey string, l *listen.ListenServer) (*ETHNode, error) {
// 连入ETH节点的ws // 连入ETH节点的ws
ws_client, err := ethclient.Dial(cfg.WsURL) ws_client, err := ethclient.Dial(cfg.ETHConfig.WsUrl)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to connect to Ethereum node: %w", err) return nil, fmt.Errorf("failed to connect to Ethereum node: %w", err)
} }
// 连入ETH节点的rpc // 连入ETH节点的rpc
rpc_client, err := ethclient.Dial(cfg.RpcURL) rpc_client, err := ethclient.Dial(cfg.ETHConfig.RpcUrl)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to connect to Ethereum node rpc: %w", err) return nil, fmt.Errorf("failed to connect to Ethereum node rpc: %w", err)
} }
// 创建可取消的 context // 创建可取消的 context
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
// 获得net_id // 获得net_id
netId, err := rpc_client.NetworkID(ctx) netId, err := rpc_client.NetworkID(ctx)
if err != nil { if err != nil {
cancel() cancel()
return nil, fmt.Errorf("failed to connect to get node net_id: %w", err) return nil, fmt.Errorf("failed to connect to get node net_id: %w", err)
} }
// 构造USDT合约相关
usdt := &USDT{}
usdt.Address = common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7") // 解析合约地址
usdt.ABI = func() abi.ABI { a, _ := abi.JSON(strings.NewReader(erc20ABI)); return a }() // 解析合约ABI
usdt.TransferSig = crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)")) // 解析合约transfer函数签名
usdt.LogsChan = make(chan types.Log, 1000) // 初始化合约日志通道
// 初始化数据库 // 初始化MySQL数据库
dbConn, err := db.NewMySQLPool(cfg.DbConfig) dbConn, err := db.NewMySQLPool(cfg.MysqlConfig["wallet"])
if err != nil { if err != nil {
cancel() cancel()
return nil, fmt.Errorf("mysql connect error: %w", err) return nil, fmt.Errorf("mysql connect error: %w", err)
} }
return &ETHNode{ // 初始化SQLite3
decodeKey: decodeKey, sqlite, err := db.NewSQLite(cfg.ETHConfig.SqlitePath)
NetId: netId, if err != nil {
Config: cfg, cancel()
WsClient: ws_client, return nil, fmt.Errorf("sqlite3 connect error: %w", err)
RpcClient: rpc_client, }
Db: *dbConn, logchan := make(chan *types.Header)
ListenAddresses: sync.Map{}, // 初始化USDT
UnConfirmTxs: make(map[string]message.Tx_msg), usdt := init_USDT()
USDT: usdt, ethnode := &ETHNode{
RmqMsgs: make(map[string][]any), decodeKey: decodeKey,
Ctx: ctx, NetID: netId,
Cancel: cancel, Config: cfg.ETHConfig,
}, nil ConfirmHeight: cfg.ETHConfig.ConfirmHeight,
WsClient: ws_client,
RpcClient: rpc_client,
USDT: usdt,
MysqlDB: dbConn,
SqliteDB: sqlite,
NetInfo: &NetInfo{},
UnConfirmedTxs: &UnConfirmedTxs{
Transactions: make(map[string]message.Transaction),
},
Wallets: make(map[string]*Wallets),
MessageServer: l,
LogsChan: logchan,
Ctx: ctx,
Cancel: cancel,
}
// 初始化表
// err = ethnode.MysqlDB.ExecuteSQLFile("../public/eth_mysql.sql")
// if err != nil {
// log.Fatalf("ETH-Chain初始化数据库表失败%v", err)
// }
// 更新网络公共数据和加载钱包
height, err := ethnode.getHeight()
if err != nil {
return nil, fmt.Errorf("failed to get blockHeight: %v", err)
}
// 更新网络公共数据
ethnode.updateNetInfo(height)
// 加载钱包
if err := ethnode.loadWallets(); err != nil {
return nil, fmt.Errorf("[inital eth wallets]: %v", err)
}
// 加载未确认交易
if err := ethnode.loadUnConfirmedTxs(); err != nil {
return nil, fmt.Errorf("load unconfirmtxs error: %v", err)
}
log.Println("✅ ETH节点已启动")
// ethnode.handleHistoryETHEvent(23795552)
return ethnode, nil
} }
// ============================ 抽象接口 ============================ // 转账
func (e *ETHNode) AddAddress(address string, rmq_msg any) error { func (e *ETHNode) Transfer(from, to, symbol string, amount, fee float64) error {
// 统一转换为小写 // 执行转账
address = strings.ToLower(address) err := e.handleTx(symbol, from, to, amount)
log.Printf("新增钱包监听消息:%v", rmq_msg) if err != nil {
e.ListenAddresses.Store(address, true) return fmt.Errorf("%s-tranfer error: %v", symbol, err)
e.mu.Lock() }
e.RmqMsgs[address] = append(e.RmqMsgs[address], rmq_msg)
e.mu.Unlock()
return nil return nil
} }
func (e *ETHNode) RemoveAddress(address string) error { // 监听消息
// 统一转换为小写 func (e *ETHNode) ListenMsg() {
address = strings.ToLower(address) log.Printf("✅ 开始监听msg...")
e.ListenAddresses.Delete(address) if e.MessageServer.ChToChainServer["ETH"] == nil {
e.mu.Lock() log.Printf("ETH消息通道还未建立")
delete(e.RmqMsgs, address) return
e.mu.Unlock() }
return nil
}
func (e *ETHNode) Listen(symbol string, ch chan any) { for msg := range e.MessageServer.ChToChainServer["ETH"] {
// 启动新区块监听(用于触发交易确认检查) switch v := msg.(type) {
go e.listenNewBlocks("USDT", ch) case message.TopupMsg_req:
switch symbol { go e.handleListen_Topup_req(v)
case "USDT": case message.WithdrawMsg_req:
// 启动 USDT Transfer 事件监听 go e.handleListen_Withdraw_req(v)
err := e.listen_usdt(ch) case message.PayMsg_req:
if err != nil { go e.handleListen_Pay_req(v)
log.Fatal("Listen USDT Transactions Error:", err) case message.RemoveListenMsg_req:
go e.handleListen_Remove_req(v)
default:
log.Printf("ListenMsg error: %v", msg)
} }
} }
} }
func (e *ETHNode) Transfer(symbol string, msg any) error { // 监听区块链数据
switch symbol { func (e *ETHNode) Listen() error {
case "USDT": log.Println("✅ 开始监听 ETH 和 USDT 转账事件...")
err := e.usdt_transfer(msg) go func() {
err := e.listenETHTransactions()
if err != nil { if err != nil {
return fmt.Errorf("%s transfer ERROR: %w", symbol, err) log.Printf("Listen ETH Transactions Error: %v", err)
} }
default: }()
return fmt.Errorf("unsupported symbol: %s", symbol)
} go func() {
err := e.listenUSDTTransactions()
if err != nil {
log.Printf("Listen USDT Transactions Error: %v", err)
}
}()
return nil return nil
} }
func (e *ETHNode) Stop() { func (e *ETHNode) Stop() {
e.Cancel() if e.Cancel != nil {
} e.Cancel()
}
// ============================ rpc节点方法 ============================ log.Println("🛑 停止监听...")
func (e *ETHNode) getETHBlance(address string) (*big.Int, error) {
account := common.HexToAddress(address)
ctx := context.Background()
balance, err := e.RpcClient.BalanceAt(ctx, account, nil) // nil表示最新高度
if err != nil {
return nil, fmt.Errorf("failed to get eth balance:%w", err)
}
// fBalance := new(big.Float).SetInt(balance)
// ethValue := new(big.Float).Quo(fBalance, big.NewFloat(1e18)) // 转 ETH
// value, _ := ethValue.Float64() // 转 float64
return balance, nil
}
func (e *ETHNode) getUSDTBalance(address string) (float64, error) {
// 统一转换为小写common.HexToAddress会自动处理但为了一致性显式转换
address = strings.ToLower(address)
contractAddress := e.USDT.Address
accountAddress := common.HexToAddress(address)
data, err := e.USDT.ABI.Pack("balanceOf", accountAddress)
if err != nil {
return 0, fmt.Errorf("failed to pack balanceOf data: %w", err)
}
msg := ethereum.CallMsg{
To: &contractAddress,
Data: data,
}
// 使用 CallContract 方法查询合约余额
res, err := e.RpcClient.CallContract(e.Ctx, msg, nil)
if err != nil {
return 0, fmt.Errorf("failed to get contract balance: %w", err)
}
// 解析返回的字节为 *big.Int
outputs, err := e.USDT.ABI.Unpack("balanceOf", res)
if err != nil || len(outputs) == 0 {
return 0, fmt.Errorf("failed to unpack balanceOf result: %w", err)
}
balance, ok := outputs[0].(*big.Int)
if !ok {
return 0, fmt.Errorf("unexpected type for balanceOf result")
}
bal := utils.BigIntUSDTToFloat64(balance)
return bal, 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) {
ctx := context.Background()
gasPrice, err := e.RpcClient.SuggestGasPrice(ctx)
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 事件...")
// 过滤掉非USDT数据
query := ethereum.FilterQuery{
Addresses: []common.Address{e.USDT.Address},
}
// 负责重连
for {
// 订阅日志
sub, err := e.WsClient.SubscribeFilterLogs(e.Ctx, query, e.USDT.LogsChan)
if err != nil {
fmt.Println("❌ 订阅失败, 5秒后重试:", err)
time.Sleep(5 * time.Second)
continue
}
fmt.Println("✅ 订阅成功")
// 处理事件
for {
select {
case err := <-sub.Err():
fmt.Println("⚠️ 订阅异常,准备重连:", err)
sub.Unsubscribe() // 清理旧订阅
time.Sleep(3 * time.Second)
goto reconnect // 跳出内层循环,回到外层重新订阅
case vLog := <-e.USDT.LogsChan:
e.handleUSDTEvent(vLog, ch) // 事件解析 + 分类传递链消息的通道是vLog而非ch且一次只传递一笔交易
case <-e.Ctx.Done():
fmt.Println("🛑 收到停止信号,退出监听")
sub.Unsubscribe()
return e.Ctx.Err()
}
}
reconnect:
}
}
func (e *ETHNode) handleUSDTEvent(vLog types.Log, ch chan any) {
from := common.HexToAddress(vLog.Topics[1].Hex())
to := common.HexToAddress(vLog.Topics[2].Hex())
height := vLog.BlockNumber
fromAddr := strings.ToLower(from.Hex())
toAddr := strings.ToLower(to.Hex())
var transferEvent struct{ Value *big.Int }
if err := e.USDT.ABI.UnpackIntoInterface(&transferEvent, "Transfer", vLog.Data); err != nil {
fmt.Println("ABI 解析错误:", err)
return
}
// 先验证toAddr是否在监听列表里面
_, ok := e.ListenAddresses.Load(toAddr)
if !ok {
return
}
tx_hash := vLog.TxHash.Hex()
// tx, tx_ok := e.UnConfirmTxs[tx_hash]
// if tx_ok {
// // 【支付/提现】待确认交易中存在该交易Hash说明是我们主动发起的交易
// // 直接走确认流程,不发送待确认消息
// // log.Printf("🔍 检测到已发起的交易: TxHash=%s, Type=%d", tx_hash, tx.TxType)
// e.confirm("USDT", height, tx, ch)
// } else {
// 【充值】待确认交易中不存在该交易hash说明是外部转账
// 添加至待确认交易中,并立即发送待确认消息
// 1,先根据to查询RmqMsgs再根据存在的rmq_msg中的相关数据存入待确认交易
value, rmq_msg_ok := e.RmqMsgs[toAddr]
var tx_type int
if rmq_msg_ok {
for _, v := range value {
_, ok := v.(message.TopupMsg_req)
if ok {
tx_type = 0
}
_, ok1 := v.(message.WithdrawMsg_req)
if ok1 {
tx_type = 1
}
_, ok2 := v.(message.PayMsg_req)
if ok2 {
tx_type = 2
}
}
}
e.UnConfirmTxs[tx_hash] = message.Tx_msg{
TxType: tx_type,
Tx: message.Tx{
From: fromAddr,
To: toAddr,
Height: height,
TxHash: tx_hash,
Symbol: "USDT",
Value: utils.BigIntUSDTToFloat64(transferEvent.Value),
Status: 2, // 待确认状态
},
}
// log.Printf("📝 待确认交易新增: TxHash=%s, Height=%d, To=%s, Type=%d", tx_hash, height, toAddr, tx_type)
// 🔔 【仅充值】立即发送待确认状态的消息(支付/提现不发送待确认消息)
if tx_type == 0 && rmq_msg_ok {
for _, v := range value {
d1, ok := v.(message.TopupMsg_req)
if ok && strings.ToLower(d1.Address) == toAddr {
pendingMsg := message.TopupMsg_resp{
Address: toAddr,
Status: 2, // 待确认状态
Chain: d1.Chain,
Symbol: d1.Symbol,
Amount: utils.BigIntUSDTToFloat64(transferEvent.Value),
TxHash: tx_hash,
BlockHeight: height,
}
// log.Printf("📤 发送待确认充值消息: TxHash=%s, Address=%s, Amount=%.2f",
// tx_hash, toAddr, pendingMsg.Amount)
// 异步发送,避免阻塞事件处理
go func(msg message.TopupMsg_resp) {
select {
case ch <- msg:
log.Printf("✅ 待确认充值消息已发送")
default:
log.Printf("⚠️ 通道阻塞,待确认消息发送失败")
}
}(pendingMsg)
break
}
}
// }
}
}
// listenNewBlocks 监听新区块产生,触发交易确认检查
func (e *ETHNode) listenNewBlocks(symbol string, ch chan any) {
fmt.Println("🔍 开始监听新区块...")
headers := make(chan *types.Header, 10)
// 负责重连
for {
// 订阅新区块头
sub, err := e.WsClient.SubscribeNewHead(e.Ctx, headers)
if err != nil {
fmt.Println("❌ 订阅新区块失败, 5秒后重试:", err)
time.Sleep(5 * time.Second)
continue
}
fmt.Println("✅ 新区块订阅成功")
// 处理新区块
for {
select {
case err := <-sub.Err():
fmt.Println("⚠️ 新区块订阅异常,准备重连:", err)
sub.Unsubscribe()
time.Sleep(3 * time.Second)
goto reconnect
case header := <-headers:
// 每当有新区块,检查待确认交易
currentHeight := header.Number.Uint64()
// log.Printf("🆕 新区块: %d", currentHeight)
// 检查是否有待确认交易
e.mu.Lock()
hasPendingTx := len(e.UnConfirmTxs) > 0
e.mu.Unlock()
if hasPendingTx {
e.checkAndConfirmTransactions(symbol, currentHeight, ch)
}
case <-e.Ctx.Done():
fmt.Println("🛑 停止新区块监听")
sub.Unsubscribe()
return
}
}
reconnect:
}
}
// checkAndConfirmTransactions 检查并确认达到确认高度的交易
func (e *ETHNode) checkAndConfirmTransactions(symbol string, currentHeight uint64, ch chan any) {
e.mu.Lock()
needConfirmList := []message.Tx_msg{}
for _, tx := range e.UnConfirmTxs {
// 检查是否达到确认高度
if currentHeight >= tx.Tx.Height+e.Config.ConfirmHeight {
log.Printf("当前高度=%d, 交易高度=%d", currentHeight, tx.Tx.Height)
// log.Printf("✅ 交易达到确认高度: TxHash=%s, 当前高度=%d, 交易高度=%d, 需确认=%d块",
// txHash, currentHeight, tx.Tx.Height, e.Config.ConfirmHeight)
needConfirmList = append(needConfirmList, tx)
}
}
e.mu.Unlock()
// 批量触发确认(在锁外执行,避免长时间持锁)
for _, tx := range needConfirmList {
e.confirm(symbol, currentHeight, tx, ch)
}
}
func (e *ETHNode) confirm(symbol string, height uint64, tx message.Tx_msg, ch chan any) {
switch symbol {
case "USDT":
e.confirm_usdt(tx, height, ch)
}
}
func (e *ETHNode) confirm_usdt(tx message.Tx_msg, height uint64, ch chan any) {
// 先从 UnConfirmTxs 中读取
e.mu.Lock()
unConfirmTx, ok := e.UnConfirmTxs[tx.Tx.TxHash]
e.mu.Unlock()
if !ok {
return
}
if height < unConfirmTx.Tx.Height {
return
}
// 超过确认高度,查询交易数据
txHash := common.HexToHash(tx.Tx.TxHash)
receipt, err := e.RpcClient.TransactionReceipt(e.Ctx, txHash)
var status int
if err != nil {
log.Println("⚠️ 查询交易收据失败 TxHash=", txHash, err)
status = 0
} else if receipt.Status == types.ReceiptStatusSuccessful {
status = 1
} else {
status = 0
}
tx.Tx.Status = status
// 通过通道发送,异步写避免阻塞
go func(tx message.Tx_msg) {
e.mu.Lock()
var result_msg any
var matchIndex = -1 // 记录匹配的索引
rmq_msg := e.RmqMsgs[tx.Tx.To]
for i, v := range rmq_msg {
// 处理充值
d1, ok := v.(message.TopupMsg_req)
if ok {
// 统一转小写比较
if strings.ToLower(d1.Address) == tx.Tx.To {
result_msg = message.TopupMsg_resp{
Address: tx.Tx.To,
Status: tx.Tx.Status,
Chain: d1.Chain,
Symbol: d1.Symbol,
Amount: tx.Tx.Value,
TxHash: tx.Tx.TxHash,
BlockHeight: tx.Tx.Height,
}
// 充值消息不删除,可能会有多笔充值到同一地址
break
}
}
// 处理提现
d2, ok1 := v.(message.WithdrawMsg_req)
if ok1 {
// 统一转小写比较
if strings.ToLower(d2.FromAddress) == tx.Tx.From &&
strings.ToLower(d2.ToAddress) == tx.Tx.To &&
d2.Amount == tx.Tx.Value {
result_msg = message.WithdrawMsg_resp{
QueueId: d2.QueueId,
Status: tx.Tx.Status,
Amount: tx.Tx.Value,
Chain: d2.Chain,
Symbol: d2.Symbol,
TxHash: tx.Tx.TxHash,
FromAddress: tx.Tx.From,
ToAddress: tx.Tx.To,
BlockHeight: tx.Tx.Height,
}
matchIndex = i // 记录索引,稍后删除
break
}
}
// 处理支付
d3, ok2 := v.(message.PayMsg_req)
if ok2 {
// 统一转小写比较
if strings.ToLower(d3.FromAddress) == tx.Tx.From &&
strings.ToLower(d3.ToAddress) == tx.Tx.To &&
d3.Amount == tx.Tx.Value {
result_msg = message.PayMsg_resp{
QueueId: d3.QueueId,
Status: tx.Tx.Status,
Amount: tx.Tx.Value,
Chain: d3.Chain,
Symbol: d3.Symbol,
OrderId: d3.OrderId,
TxHash: tx.Tx.TxHash,
FromAddress: tx.Tx.From,
ToAddress: tx.Tx.To,
BlockHeight: tx.Tx.Height,
}
matchIndex = i // 记录索引,稍后删除
break
}
}
}
// 循环结束后,统一删除匹配的消息(提现和支付需要删除)
if matchIndex >= 0 {
e.RmqMsgs[tx.Tx.To] = utils.Slice_delete(e.RmqMsgs[tx.Tx.To], matchIndex)
}
e.mu.Unlock()
select {
case ch <- result_msg:
default:
fmt.Println("⚠️ confirm通道阻塞消息丢失:", txHash)
}
}(tx)
// 删除已确认交易
e.mu.Lock()
delete(e.UnConfirmTxs, tx.Tx.TxHash)
e.mu.Unlock()
}
func (e *ETHNode) decodePrivatekey(address string) string {
// 统一转换为小写
address = strings.ToLower(address)
// 查询加密后的私钥
querySql := "SELECT `private_key` FROM eth_balance WHERE address = ? LIMIT 1;"
log.Println("查询私钥的钱包地址:", address)
var encryptedKey string
err := e.Db.QueryRow(querySql, address).Scan(&encryptedKey)
if err != nil {
log.Println("❌ 查询私钥失败:", err)
return ""
}
// 使用key解密
privateKey := encryptedKey // 实际使用时替换成具体的解密代码
return privateKey
}
func (e *ETHNode) usdt_transfer(msg any) error {
var user_from, final_from, to string
var amount float64
// var tx_type int
// now_height, err := e.getBlockHeight()
// if err != nil {
// return fmt.Errorf("get lastest height error: %v", err)
// }
// ---------------------------------------------------------------------------------------------
// 断言,确定本次转账是哪个类型
// 支付操作
v, ok := msg.(message.PayMsg_req)
if ok {
e.AddAddress(v.ToAddress, v) // 存入该笔msgAddAddress内部会转小写
// 统一转换为小写
user_from, final_from, to, amount = strings.ToLower(v.FromAddress), strings.ToLower(v.FromAddress), strings.ToLower(v.ToAddress), v.Amount
// tx_type = 2
}
// 提现操作
k, ok1 := msg.(message.WithdrawMsg_req)
if ok1 {
e.AddAddress(k.ToAddress, k) // 存入该笔msgAddAddress内部会转小写
// 统一转换为小写
user_from, final_from, to, amount = strings.ToLower(k.FromAddress), strings.ToLower(k.FromAddress), strings.ToLower(k.ToAddress), k.Amount
// tx_type = 1
}
// ---------------------------------------------------------------------------------------------
// 1,校验钱包余额
balance, err := e.getUSDTBalance(user_from)
log.Printf("检测Transfer钱包=%s余额=%f", user_from, balance)
if err != nil {
return fmt.Errorf("failed to get balance: %w", err)
}
// 2,钱包余额不足,调用归集钱包转账
if balance < amount {
final_from = "归集钱包"
}
// 3,通过from地址前往数据库查找出对应加密后的私钥并解密真实的私钥
originalKey := e.decodePrivatekey(final_from)
if originalKey == "" {
return fmt.Errorf("failed to query privatekey")
}
privateKey, err := crypto.HexToECDSA(originalKey)
if err != nil {
return fmt.Errorf("failed to parse private key: %w", err)
}
// 4, 获得nonce
nonce, err := e.RpcClient.PendingNonceAt(e.Ctx, common.HexToAddress(final_from))
if err != nil {
return fmt.Errorf("failed to get nonce: %w", err)
}
// 5, 构造交易ERC20 transfer 调用)
amountBigInt := utils.Float64ToBigIntUSDT(amount)
data, err := e.USDT.ABI.Pack("transfer", common.HexToAddress(to), amountBigInt) // 打包 transfer(address,uint256) 方法调用
if err != nil {
return fmt.Errorf("failed to pack transfer data: %w", err)
}
gasLimit, err := e.getGasLimit() // 获得gasLimit
if err != nil {
return fmt.Errorf("get gas limit error:%v", err)
}
// 获取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)
}
// 计算最大可能的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(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)
}
// 构造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 EIP-1559 transaction: %w", err)
}
txHash := signedTx.Hash().Hex()
// 7, 发送EIP-1559交易
err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
if err != nil {
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,
// Tx: message.Tx{
// From: final_from,
// To: to,
// Height: now_height,
// TxHash: txHash,
// Symbol: "USDT",
// Value: amount,
// Status: 2,
// },
// }
// // 9, 将构造的交易消息存入待确认交易中
// e.UnConfirmTxs[txHash] = tx_msg
return nil
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
package tron
// 区块
// {
// "blockID":,
// "block_header": map,
// "transactions": map,
//}
type TRONNode struct {
decodeKey string
ConfirmHeight uint64
}

View File

@@ -0,0 +1 @@
package tron

View File

@@ -0,0 +1,44 @@
package constant
const (
STATUS_FAILED = 0 // 失败
STATUS_SUCCESS = 1 // 成功
STATUS_PENDING = 2 // 待确认
STATUS_VERIFY_FAILED = 3 // 验证失败
STATUS_BALANCE_NOT_ENOUGH = 4 // 钱包余额不足
STATUS_UNTRANSFER = 5 // 未支付
STATUS_END = 6 // 完成
STATUS_ERROR = 7
)
const ETH_ERC20_USDT_CONTRACT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
const ETH_ERC20_ABI = `
[
{
"constant": true,
"inputs": [{"name": "_owner", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "balance", "type": "uint256"}],
"type": "function"
},
{
"constant": false,
"inputs": [
{"name": "_to", "type": "address"},
{"name": "_value", "type": "uint256"}
],
"name": "transfer",
"outputs": [{"name": "", "type": "bool"}],
"type": "function"
},
{
"anonymous": false,
"inputs": [
{"indexed": true, "name": "from", "type": "address"},
{"indexed": true, "name": "to", "type": "address"},
{"indexed": false,"name": "value","type": "uint256"}
],
"name": "Transfer",
"type": "event"
}
]
`

View File

@@ -1,70 +0,0 @@
package db
import (
"database/sql"
"fmt"
message "m2pool-payment/internal/msg"
_ "github.com/go-sql-driver/mysql"
)
type MySQLPool struct {
db *sql.DB
}
// NewMySQLPool 初始化连接池
func NewMySQLPool(cfg message.DbConfig) (*MySQLPool, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
// 设置连接池参数
db.SetMaxOpenConns(cfg.MaxOpenConns)
db.SetMaxIdleConns(cfg.MaxIdleConns)
db.SetConnMaxLifetime(cfg.ConnMaxLife)
// 测试连接
if err := db.Ping(); err != nil {
return nil, err
}
return &MySQLPool{db: db}, nil
}
// Exec 执行 INSERT/UPDATE/DELETE
func (p *MySQLPool) Exec(query string, args ...any) (sql.Result, error) {
return p.db.Exec(query, args...)
}
// Query 查询多行
func (p *MySQLPool) Query(query string, args ...any) (*sql.Rows, error) {
return p.db.Query(query, args...)
}
// QueryRow 查询单行
func (p *MySQLPool) QueryRow(query string, args ...any) *sql.Row {
return p.db.QueryRow(query, args...)
}
// Transaction 执行事务
func (p *MySQLPool) Transaction(fn func(tx *sql.Tx) error) error {
tx, err := p.db.Begin()
if err != nil {
return err
}
if err := fn(tx); err != nil {
_ = tx.Rollback()
return err
}
return tx.Commit()
}
// Close 关闭连接池
func (p *MySQLPool) Close() error {
return p.db.Close()
}

218
internal/db/mysql.go Normal file
View File

@@ -0,0 +1,218 @@
package db
import (
"bufio"
"database/sql"
"fmt"
"io/ioutil"
"log"
message "m2pool-payment/internal/msg"
"strings"
_ "github.com/go-sql-driver/mysql"
)
type MySQLPool struct {
db *sql.DB
}
// NewMySQLPool 初始化连接池
func NewMySQLPool(cfg message.MysqlConfig) (*MySQLPool, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
// 设置连接池参数
db.SetMaxOpenConns(cfg.MaxOpenConns)
db.SetMaxIdleConns(cfg.MaxIdleConns)
db.SetConnMaxLifetime(cfg.ConnMaxLife)
// 测试连接
if err := db.Ping(); err != nil {
return nil, err
}
return &MySQLPool{db: db}, nil
}
// splitSQLStatements 将 SQL 内容按分号分割
func splitSQLStatements(sqlContent string) []string {
// 使用 bufio 扫描文件内容
var statements []string
scanner := bufio.NewScanner(strings.NewReader(sqlContent))
var queryBuilder strings.Builder
for scanner.Scan() {
line := scanner.Text()
// 处理每一行的 SQL 语句
if strings.TrimSpace(line) == "" {
continue
}
// 如果行中包含分号,说明是完整的 SQL 语句
queryBuilder.WriteString(line)
if strings.HasSuffix(line, ";") {
statements = append(statements, queryBuilder.String())
queryBuilder.Reset() // 清空构建器以便准备下一条 SQL 语句
} else {
queryBuilder.WriteString("\n")
}
}
// 处理可能的扫描错误
if err := scanner.Err(); err != nil {
log.Fatalf("error reading .sql file: %v\n", err)
}
return statements
}
func (p *MySQLPool) ExecuteSQLFile(filePath string) error {
// 读取 SQL 文件内容
sqlContent, err := ioutil.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read SQL file: %v", err)
}
// 将文件内容按分号 (;) 分割成多条 SQL 语句
queries := splitSQLStatements(string(sqlContent))
// 执行每一条 SQL 语句
for _, query := range queries {
// 跳过空行或注释
if strings.TrimSpace(query) == "" || strings.HasPrefix(strings.TrimSpace(query), "--") {
continue
}
// 执行 SQL 语句
_, err := p.db.Exec(query)
if err != nil {
log.Printf("error executing query: %v\n", err)
} else {
// fmt.Println("Executed query:", query)
}
}
return nil
}
// Exec 执行 INSERT/UPDATE/DELETE
func (p *MySQLPool) Exec(query string, args ...any) (sql.Result, error) {
return p.db.Exec(query, args...)
}
// Query 查询多行
func (p *MySQLPool) Query(query string, args ...any) (*sql.Rows, error) {
return p.db.Query(query, args...)
}
// QueryRow 查询单行
func (p *MySQLPool) QueryRow(query string, args ...any) *sql.Row {
return p.db.QueryRow(query, args...)
}
// Transaction 执行事务
func (p *MySQLPool) Transaction(fn func(tx *sql.Tx) error) error {
tx, err := p.db.Begin()
if err != nil {
return err
}
if err := fn(tx); err != nil {
_ = tx.Rollback()
return err
}
return tx.Commit()
}
// Insert 执行通用插入操作
func (p *MySQLPool) Insert(query string, values [][]any) (sql.Result, error) {
// 预处理查询
stmt, err := p.db.Prepare(query)
if err != nil {
return nil, fmt.Errorf("failed to prepare statement: %v", err)
}
defer stmt.Close()
// 执行批量插入
var result sql.Result
for _, row := range values {
result, err = stmt.Exec(row...)
if err != nil {
return nil, fmt.Errorf("failed to execute insert: %v", err)
}
}
return result, nil
}
// Delete 执行通用删除操作
// Update 执行通用更新操作
func (p *MySQLPool) Update(query string, values []any) (sql.Result, error) {
// 预处理查询
stmt, err := p.db.Prepare(query)
if err != nil {
return nil, fmt.Errorf("failed to prepare statement: %v", err)
}
defer stmt.Close()
// 执行更新操作
result, err := stmt.Exec(values...)
if err != nil {
return nil, fmt.Errorf("failed to execute update: %v", err)
}
return result, nil
}
// ExecuteTransactions 执行多条增删改操作,确保事务的原子性
func (p *MySQLPool) ExecuteTransactions(str_sqls []string, params [][]any) error {
// 检查 SQL 和参数的数量是否匹配
if len(str_sqls) != len(params) {
return fmt.Errorf("sql length != params length")
}
// 开始事务
tx, err := p.db.Begin()
if err != nil {
return fmt.Errorf("failed to begin transaction: %v", err)
}
// 确保在函数结束时提交或回滚事务
defer func() {
if err != nil {
// 发生错误时回滚事务
if rollbackErr := tx.Rollback(); rollbackErr != nil {
err = fmt.Errorf("failed to rollback transaction: %v", rollbackErr)
}
} else {
// 如果没有错误,提交事务
if commitErr := tx.Commit(); commitErr != nil {
err = fmt.Errorf("failed to commit transaction: %v", commitErr)
}
}
}()
// 执行每个 SQL 语句
for i, sql_str := range str_sqls {
// 使用事务对象 tx 来执行 SQL
_, err := tx.Exec(sql_str, params[i]...)
if err != nil {
// 如果执行失败,立即返回并且触发回滚
return fmt.Errorf("failed to execute SQL at index %d: %v", i, err)
}
}
// 如果所有 SQL 执行成功,则返回 nil
return nil
}
// Close 关闭连接池
func (p *MySQLPool) Close() error {
return p.db.Close()
}

View File

@@ -9,15 +9,20 @@ import (
type SQLite struct { type SQLite struct {
DB *sql.DB DB *sql.DB
// Ch chan any
} }
// 初始化连接 // 初始化连接
func NewSQLite(path string) (*SQLite, error) { func NewSQLite(path string) (*SQLite, error) {
db, err := sql.Open("sqlite", path) db, err := sql.Open("sqlite", path)
// ch := make(chan any, 1000)
if err != nil { if err != nil {
return nil, fmt.Errorf("open sqlite failed: %v", err) return nil, fmt.Errorf("open sqlite failed: %v", err)
} }
return &SQLite{DB: db}, nil return &SQLite{
DB: db,
// Ch: ch,
}, nil
} }
// 关闭数据库 // 关闭数据库
@@ -27,8 +32,8 @@ func (s *SQLite) Close() {
} }
} }
// 通用新建表 // 新建表
func (s *SQLite) Exec_(sql string) error { func (s *SQLite) CreateTable(sql string) error {
_, err := s.DB.Exec(sql) _, err := s.DB.Exec(sql)
if err != nil { if err != nil {
return fmt.Errorf("Exec DB error: %w", err) return fmt.Errorf("Exec DB error: %w", err)
@@ -36,6 +41,48 @@ func (s *SQLite) Exec_(sql string) error {
return nil return 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) 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
}
// 更新数据
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
}
// 通用查询方法(返回[]map[string]any // 通用查询方法(返回[]map[string]any
func (s *SQLite) Query_(query string, args ...any) ([]map[string]any, error) { func (s *SQLite) Query_(query string, args ...any) ([]map[string]any, error) {
rows, err := s.DB.Query(query, args...) rows, err := s.DB.Query(query, args...)
@@ -77,44 +124,44 @@ func (s *SQLite) Query_(query string, args ...any) ([]map[string]any, error) {
return results, nil return results, nil
} }
// 插入数据(支持事务和批量) // 事务增删改(多条增删改串行,只要有一条增删改失败则全部回滚并返回错误)
func (s *SQLite) Insert(sqlStr string, args ...[]any) error { func (s *SQLite) ExecuteTransactions(str_sqls []string, params [][]any) error {
// 检查 SQL 和参数的数量是否匹配
if len(str_sqls) != len(params) {
return fmt.Errorf("sql length != params length")
}
// 开始事务
tx, err := s.DB.Begin() tx, err := s.DB.Begin()
if err != nil { if err != nil {
return err return fmt.Errorf("failed to begin transaction: %v", err)
} }
stmt, err := tx.Prepare(sqlStr)
if err != nil {
return err
}
defer stmt.Close()
for _, a := range args { // 确保在函数结束时提交或回滚事务
_, err = stmt.Exec(a...) defer func() {
if err != nil { if err != nil {
tx.Rollback() // 发生错误时回滚事务
return fmt.Errorf("exec insert error: %v", err) if rollbackErr := tx.Rollback(); rollbackErr != nil {
err = fmt.Errorf("failed to rollback transaction: %v", rollbackErr)
}
} else {
// 如果没有错误,提交事务
if commitErr := tx.Commit(); commitErr != nil {
err = fmt.Errorf("failed to commit transaction: %v", commitErr)
}
}
}()
// 执行每个 SQL 语句
for i, sql_str := range str_sqls {
// 使用事务对象 tx 来执行 SQL
_, err := tx.Exec(sql_str, params[i]...)
if err != nil {
// 如果执行失败,立即返回并且触发回滚
return fmt.Errorf("failed to execute SQL: %v", err)
} }
} }
return tx.Commit()
}
// 更新数据 // 如果所有 SQL 执行成功,则返回 nil
func (s *SQLite) Update(sqlStr string, args ...any) (int64, error) { return nil
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
} }

336
internal/listen/listen.go Normal file
View File

@@ -0,0 +1,336 @@
package listen
import (
"fmt"
"log"
"m2pool-payment/internal/db"
message "m2pool-payment/internal/msg"
"m2pool-payment/internal/utils"
"sync"
)
type ListenServer struct {
Config message.Config
MysqlDB *db.MySQLPool
SqliteDB *db.SQLite
TopupMsgs map[string]*TopupMsgs // {"ETH": TopupMsgs{"queue_id": message.Topupmsg_req{}, ...}}
WithdrawMsgs map[string]*WithdrawMsgs
PayMsgs map[string]*PayMsgs
RemoveMsgs map[string]*RemoveMsgs
ChToChainServer map[string]chan any
ChFromChainServer map[string]chan any
ChFromRmqServer chan any
ChToRmqServer chan any
mu sync.Mutex
}
type TopupMsgs struct {
Msgs map[string]message.TopupMsg_req // {"queue_id": message.Topupmsg_req{}, ...}
mu sync.RWMutex // 读写锁
}
type WithdrawMsgs struct {
Msgs map[string]message.WithdrawMsg_req
mu sync.RWMutex // 读写锁
}
type PayMsgs struct {
Msgs map[string]message.PayMsg_req
mu sync.RWMutex // 读写锁
}
type RemoveMsgs struct {
Msgs map[string]message.RemoveListenMsg_req
mu sync.RWMutex // 读写锁
}
func NewListenServer(cfg message.Config) *ListenServer {
// 初始化MySQL数据库
dbConn, err := db.NewMySQLPool(cfg.MysqlConfig["wallet"])
if err != nil {
log.Printf("mysql connect error: %v", err)
return nil
}
// 初始化SQLite3
sqlite, err := db.NewSQLite(cfg.MsgConfig.SqlitePath)
if err != nil {
log.Printf("sqlite3 connect error: %v", err)
return nil
}
nets := cfg.Net
topup_msgs := make(map[string]*TopupMsgs)
withdraw_msgs := make(map[string]*WithdrawMsgs)
pay_msgs := make(map[string]*PayMsgs)
remove_msgs := make(map[string]*RemoveMsgs)
for _, net := range nets {
topup_msgs[net] = &TopupMsgs{
Msgs: make(map[string]message.TopupMsg_req),
}
withdraw_msgs[net] = &WithdrawMsgs{
Msgs: make(map[string]message.WithdrawMsg_req),
}
pay_msgs[net] = &PayMsgs{
Msgs: make(map[string]message.PayMsg_req),
}
remove_msgs[net] = &RemoveMsgs{
Msgs: make(map[string]message.RemoveListenMsg_req),
}
}
ch := make(map[string]chan any)
chin := make(map[string]chan any)
rmq_ch_in := make(chan any, 1000)
rmq_ch_out := make(chan any, 1000)
for _, net := range cfg.Net {
ch[net] = make(chan any, 1000)
chin[net] = make(chan any, 1000)
}
var l = &ListenServer{
Config: cfg,
MysqlDB: dbConn,
SqliteDB: sqlite,
TopupMsgs: topup_msgs,
WithdrawMsgs: withdraw_msgs,
PayMsgs: pay_msgs,
RemoveMsgs: remove_msgs,
ChToChainServer: ch,
ChFromChainServer: chin,
ChFromRmqServer: rmq_ch_in,
ChToRmqServer: rmq_ch_out,
}
// err = l.MysqlDB.ExecuteSQLFile("../public/msg_mysql.sql")
// if err != nil {
// log.Fatalf("Listen-message初始化数据库表失败%v", err)
// }
l.loadMsg()
log.Println("✅ 消息监听处理已启动")
return l
}
// rmq -> listen -> chain server
func (l *ListenServer) RmqMsgIn() {
for msg := range l.ChFromRmqServer {
switch v := msg.(type) {
case message.TopupMsg_req:
if !utils.Contains(l.Config.Net, v.Chain) {
log.Printf("%s has not support", v.Chain)
continue
}
go l.handleRmqTopup_req(v)
case message.WithdrawMsg_req:
if !utils.Contains(l.Config.Net, v.Chain) {
log.Printf("%s has not support", v.Chain)
continue
}
go l.handleRmqWithdraw_req(v)
case message.PayMsg_req:
if !utils.Contains(l.Config.Net, v.Chain) {
log.Printf("%s has not support", v.Chain)
continue
}
go l.handleRmqPay_req(v)
case message.RemoveListenMsg_req:
if !utils.Contains(l.Config.Net, v.Chain) {
log.Printf("%s has not support", v.Chain)
continue
}
go l.handleRmqRemove_req(v)
default:
log.Printf("ListenMsg error: %v", msg)
}
}
}
// chain server -> listen -> rmq
func (l *ListenServer) NetMsgIn() {
for _, ch := range l.ChFromChainServer {
for msg := range ch {
switch v := msg.(type) {
// 接收到区块链节点返回的更新req状态消息
case message.UpdateReqState:
go l.handleUpdateReqState(v)
// 接收到区块链节点服务返回的resp消息除了修改相关状态和数据库还需通过rmq_out通道返回出去
case message.TopupMsg_resp:
go l.handleChainTopup_resp(v)
case message.WithdrawMsg_resp:
go l.handleChainWithdraw_resp(v)
case message.PayMsg_resp:
go l.handleChainPay_resp(v)
case message.RemoveListenMsg_resp:
go l.handleChainRemove_resp(v)
default:
log.Printf("MsgType not found: %v", v)
}
}
}
}
// 根据 address 查找对应的消息
func (l *ListenServer) FindMsgWithAddress(chain, address string) (any, error) {
// 检查链是否支持
if !utils.Contains(l.Config.Net, chain) {
return nil, fmt.Errorf("%s has not support", chain)
}
// 查找 TopupMsgs
l.TopupMsgs[chain].mu.RLock()
for _, topup_msg := range l.TopupMsgs[chain].Msgs {
if address == topup_msg.Address {
l.TopupMsgs[chain].mu.RUnlock()
return topup_msg, nil
}
}
l.TopupMsgs[chain].mu.RUnlock()
// 查找 WithdrawMsgs
l.WithdrawMsgs[chain].mu.RLock()
for _, withdraw_msg := range l.WithdrawMsgs[chain].Msgs {
if address == withdraw_msg.FromAddress {
l.WithdrawMsgs[chain].mu.RUnlock()
return withdraw_msg, nil
}
}
l.WithdrawMsgs[chain].mu.RUnlock()
// 查找 PayMsgs
l.PayMsgs[chain].mu.RLock()
for _, pay_msg := range l.PayMsgs[chain].Msgs {
if address == pay_msg.FromAddress {
l.PayMsgs[chain].mu.RUnlock()
return pay_msg, nil
}
}
l.PayMsgs[chain].mu.RUnlock()
// 如果没有找到匹配项,返回错误
return nil, fmt.Errorf("Address(%s) not found in any message", address)
}
// 根据address查找Topup消息
func (l *ListenServer) FindTopupMsgWithToaddress(chain, toAddress string) (message.TopupMsg_req, error) {
// 检查链是否支持
if !utils.Contains(l.Config.Net, chain) {
return message.TopupMsg_req{}, fmt.Errorf("%s has not support", chain)
}
l.TopupMsgs[chain].mu.RLock()
for _, topup_msg := range l.TopupMsgs[chain].Msgs {
if toAddress == topup_msg.Address {
l.TopupMsgs[chain].mu.RUnlock()
return topup_msg, nil
}
}
l.TopupMsgs[chain].mu.RUnlock()
return message.TopupMsg_req{}, fmt.Errorf("Address(%s) not found in topup message", toAddress)
}
// 根据address查找Topup消息
func (l *ListenServer) FindWithdrawMsgWithToaddress(chain, fromAddress string) (message.WithdrawMsg_req, error) {
// 检查链是否支持
if !utils.Contains(l.Config.Net, chain) {
return message.WithdrawMsg_req{}, fmt.Errorf("%s has not support", chain)
}
l.WithdrawMsgs[chain].mu.RLock()
for _, withdraw_msg := range l.WithdrawMsgs[chain].Msgs {
if fromAddress == withdraw_msg.FromAddress {
l.WithdrawMsgs[chain].mu.RUnlock()
return withdraw_msg, nil
}
}
l.WithdrawMsgs[chain].mu.RUnlock()
return message.WithdrawMsg_req{}, fmt.Errorf("Address(%s) not found in withdraw message", fromAddress)
}
// 根据address查找Topup消息
func (l *ListenServer) FindPayMsgWithToaddress(chain, fromAddress string) (message.PayMsg_req, error) {
// 检查链是否支持
if !utils.Contains(l.Config.Net, chain) {
return message.PayMsg_req{}, fmt.Errorf("%s has not support", chain)
}
l.PayMsgs[chain].mu.RLock()
for _, pay_msg := range l.PayMsgs[chain].Msgs {
if fromAddress == pay_msg.FromAddress {
l.PayMsgs[chain].mu.RUnlock()
return pay_msg, nil
}
}
l.PayMsgs[chain].mu.RUnlock()
return message.PayMsg_req{}, fmt.Errorf("Address(%s) not found in pay message", fromAddress)
}
// 根据queue_id查找到对应消息
func (l *ListenServer) FindMsgWithQueueID(chain, queue_id string, txType int) (any, error) {
if !utils.Contains(l.Config.Net, chain) {
return nil, fmt.Errorf("%s has not support", chain)
}
switch txType {
case 0:
l.TopupMsgs[chain].mu.RLock()
defer l.TopupMsgs[chain].mu.RUnlock()
if msg, ok := l.TopupMsgs[chain].Msgs[queue_id]; ok {
return msg, nil
} else {
return nil, fmt.Errorf("QueueID(%s) not found in txType(%d)", queue_id, txType)
}
case 1:
l.WithdrawMsgs[chain].mu.RLock()
defer l.WithdrawMsgs[chain].mu.RUnlock()
if msg, ok := l.WithdrawMsgs[chain].Msgs[queue_id]; ok {
return msg, nil
} else {
return nil, fmt.Errorf("QueueID(%s) not found in txType(%d)", queue_id, txType)
}
case 2:
l.PayMsgs[chain].mu.RLock()
defer l.PayMsgs[chain].mu.RUnlock()
if msg, ok := l.PayMsgs[chain].Msgs[queue_id]; ok {
return msg, nil
} else {
return nil, fmt.Errorf("QueueID(%s) not found in txType(%d)", queue_id, txType)
}
default:
return nil, fmt.Errorf("This txType(%d) don`t supporttxType(0,1,2)", txType)
}
}
func (l *ListenServer) ReadTopupMsgs(chain string) (map[string]message.TopupMsg_req, error) {
l.TopupMsgs[chain].mu.RLock()
defer l.TopupMsgs[chain].mu.RUnlock()
var topup_msgs = make(map[string]message.TopupMsg_req)
if !utils.Contains(l.Config.Net, chain) {
return topup_msgs, fmt.Errorf("%s has not support", chain)
}
topup_msgs = l.TopupMsgs[chain].Msgs
return topup_msgs, nil
}
func (l *ListenServer) ReadWithdrawMsgs(chain string) (map[string]message.WithdrawMsg_req, error) {
l.WithdrawMsgs[chain].mu.RLock()
defer l.WithdrawMsgs[chain].mu.RUnlock()
var withdraw_msgs = make(map[string]message.WithdrawMsg_req)
if !utils.Contains(l.Config.Net, chain) {
return withdraw_msgs, fmt.Errorf("%s has not support", chain)
}
withdraw_msgs = l.WithdrawMsgs[chain].Msgs
return withdraw_msgs, nil
}
func (l *ListenServer) ReadPayMsgs(chain string) (map[string]message.PayMsg_req, error) {
l.PayMsgs[chain].mu.RLock()
defer l.PayMsgs[chain].mu.RUnlock()
var pay_msgs = make(map[string]message.PayMsg_req)
if !utils.Contains(l.Config.Net, chain) {
return pay_msgs, fmt.Errorf("%s has not support", chain)
}
pay_msgs = l.PayMsgs[chain].Msgs
return pay_msgs, nil
}

View File

@@ -0,0 +1,547 @@
package listen
import (
"fmt"
"log"
"m2pool-payment/internal/constant"
message "m2pool-payment/internal/msg"
"strconv"
"time"
)
// 加载所有消息
func (l *ListenServer) loadMsg() {
topup_sql := "SELECT queue_id, chain, symbol, address, timestamp, sign, status FROM topup_req_msg WHERE status = ?" // 1正在监听
withdraw_sql := "SELECT queue_id, chain, symbol, from_addr, to_addr, amount, fee, timestamp, sign, status FROM withdraw_req_msg WHERE status = ? OR status = ?" // 2待确认5待支付
pay_sql := "SELECT queue_id, chain, symbol, from_addr, to_addr, amount, fee, timestamp, sign, status FROM withdraw_req_msg WHERE status = ? OR status = ?" // 2待确认5待支付
remove_sql := "SELECT queue_id, msg_type, chain, symbol, address, timestamp, sign, status FROM remove_req_msg WHERE status = ?" // 2待执行
topup_params := []any{1}
withdraw_params := []any{2, 5}
pay_params := []any{2, 5}
remove_params := []any{2}
// 处理充值消息
go func() {
rows, err := l.MysqlDB.Query(topup_sql, topup_params...)
if err != nil {
log.Fatalf("load topup-msg error: %v", err)
return
}
defer rows.Close() // 确保在函数结束时关闭 rows
// 遍历查询结果
for rows.Next() {
var queueID, chain, symbol, address, sign string
var timestamp int64
var status int
// 扫描每行数据到相应的变量
err := rows.Scan(&queueID, &chain, &symbol, &address, &timestamp, &sign, &status)
if err != nil {
log.Printf("failed to scan topup row: %v", err)
continue
}
// 处理数据
l.TopupMsgs[chain].mu.RLock()
l.TopupMsgs[chain].Msgs[queueID] = message.TopupMsg_req{
QueueId: queueID,
Chain: chain,
Symbol: symbol,
Address: address,
Timestamp: uint64(timestamp),
Sign: sign,
Status: status,
}
l.TopupMsgs[chain].mu.RUnlock()
}
if err := rows.Err(); err != nil {
log.Printf("error occurred during rows iteration: %v", err)
}
log.Printf("充值历史消息load完毕")
}()
// 处理提现消息
go func() {
rows, err := l.MysqlDB.Query(withdraw_sql, withdraw_params...)
if err != nil {
log.Fatalf("load withdraw-msg error: %v", err)
return
}
defer rows.Close()
// 遍历查询结果
for rows.Next() {
var queueID, chain, symbol, fromAddr, toAddr, amountStr, feeStr, sign string
var timestamp int64
var status int
// 扫描每行数据
err := rows.Scan(&queueID, &chain, &symbol, &fromAddr, &toAddr, &amountStr, &feeStr, &timestamp, &sign, &status)
if err != nil {
log.Printf("failed to scan withdraw row: %v", err)
continue
}
// 将 amount 和 fee 转换为浮动数字
amount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
log.Printf("failed to parse amount: %v", err)
continue
}
fee, err := strconv.ParseFloat(feeStr, 64)
if err != nil {
log.Printf("failed to parse fee: %v", err)
continue
}
// 处理数据
l.WithdrawMsgs[chain].mu.RLock()
l.WithdrawMsgs[chain].Msgs[queueID] = message.WithdrawMsg_req{
QueueId: queueID,
Chain: chain,
Symbol: symbol,
FromAddress: fromAddr,
ToAddress: toAddr,
Amount: amount,
Fee: fee,
Timestamp: uint64(timestamp),
Sign: sign,
Status: status,
}
l.WithdrawMsgs[chain].mu.RUnlock()
}
if err := rows.Err(); err != nil {
log.Printf("error occurred during rows iteration: %v", err)
}
log.Printf("提现历史消息load完毕")
}()
// 处理支付消息
go func() {
rows, err := l.MysqlDB.Query(pay_sql, pay_params...)
if err != nil {
log.Fatalf("load pay-msg error: %v", err)
return
}
defer rows.Close()
// 遍历查询结果
for rows.Next() {
var queueID, chain, symbol, fromAddr, toAddr, amountStr, feeStr, sign string
var timestamp int64
var status int
// 扫描每行数据
err := rows.Scan(&queueID, &chain, &symbol, &fromAddr, &toAddr, &amountStr, &feeStr, &timestamp, &sign, &status)
if err != nil {
log.Printf("failed to scan pay row: %v", err)
continue
}
// 将 amount 和 fee 转换为浮动数字
amount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
log.Printf("failed to parse amount: %v", err)
continue
}
fee, err := strconv.ParseFloat(feeStr, 64)
if err != nil {
log.Printf("failed to parse fee: %v", err)
continue
}
// 处理数据
l.PayMsgs[chain].mu.RLock()
l.PayMsgs[chain].Msgs[queueID] = message.PayMsg_req{
QueueId: queueID,
Chain: chain,
Symbol: symbol,
FromAddress: fromAddr,
ToAddress: toAddr,
Amount: amount,
Fee: fee,
Timestamp: uint64(timestamp),
Sign: sign,
Status: status,
}
l.PayMsgs[chain].mu.RUnlock()
}
if err := rows.Err(); err != nil {
log.Printf("error occurred during rows iteration: %v", err)
}
log.Printf("支付历史消息load完毕")
}()
// 处理移除消息
go func() {
rows, err := l.MysqlDB.Query(remove_sql, remove_params...)
if err != nil {
log.Fatalf("load remove-msg error: %v", err)
return
}
defer rows.Close()
// 遍历查询结果
for rows.Next() {
var queueID, chain, symbol, address, sign string
var timestamp int64
var status, msgType int
// 扫描每行数据
err := rows.Scan(&queueID, &msgType, &chain, &symbol, &address, &timestamp, &sign, &status)
if err != nil {
log.Printf("failed to scan remove row: %v", err)
continue
}
// 处理数据
l.RemoveMsgs[chain].mu.RLock()
l.RemoveMsgs[chain].Msgs[queueID] = message.RemoveListenMsg_req{
QueueId: queueID,
MsgType: msgType,
Chain: chain,
Symbol: symbol,
Address: address,
Timestamp: uint64(timestamp),
Sign: sign,
Status: status,
}
l.RemoveMsgs[chain].mu.RUnlock()
}
if err := rows.Err(); err != nil {
log.Printf("error occurred during rows iteration: %v", err)
}
log.Printf("移除监听历史消息load完毕")
}()
}
// 充值消息
func (l *ListenServer) handleRmqTopup_req(msg message.TopupMsg_req) {
// 添加到TopupMsgs
l.TopupMsgs[msg.Chain].mu.RLock()
l.TopupMsgs[msg.Chain].Msgs[msg.QueueId] = msg
l.TopupMsgs[msg.Chain].mu.RUnlock()
// 写数据库
sql := "INSERT INTO topup_req_msg (queue_id, chain, symbol, address, timestamp, sign) VALUES (?,?,?,?,?,?)"
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.Address, msg.Timestamp, msg.Sign}
_, err := l.MysqlDB.Insert(sql, [][]any{params})
if err != nil {
log.Printf("Insert Topup_req msg error: %v", err)
go l.asyncSendMsgToRmq(message.TopupMsg_resp{
QueueId: msg.QueueId,
Chain: msg.Chain,
Symbol: msg.Symbol,
Address: msg.Address,
Status: constant.STATUS_ERROR,
}, 3, 5*time.Second)
return
}
// 传给对应的node server
go l.asyncSendMsgToChain(msg, msg.Chain, 3, 5*time.Second)
log.Printf("Insert Topup_req msg success: QueueId(%s)", msg.QueueId)
}
// 提现消息
func (l *ListenServer) handleRmqWithdraw_req(msg message.WithdrawMsg_req) {
// 添加到WithdrawMsgs
l.WithdrawMsgs[msg.Chain].mu.RLock()
l.WithdrawMsgs[msg.Chain].Msgs[msg.QueueId] = msg
l.WithdrawMsgs[msg.Chain].mu.RUnlock()
// 写数据库
sql := "INSERT INTO withdraw_req_msg (queue_id, from_addr, to_addr, amount, fee, chain, symbol, timestamp, sign) VALUES (?,?,?,?,?,?,?,?,?)"
params := []any{msg.QueueId, msg.FromAddress, msg.ToAddress, fmt.Sprintf("%f", msg.Amount), fmt.Sprintf("%f", msg.Fee), msg.Chain, msg.Symbol, msg.Timestamp, msg.Sign}
_, err := l.MysqlDB.Insert(sql, [][]any{params})
if err != nil {
log.Printf("Insert Withdraw_req msg error: %v", err)
go l.asyncSendMsgToRmq(message.WithdrawMsg_resp{
QueueId: msg.QueueId,
Chain: msg.Chain,
Symbol: msg.Symbol,
FromAddress: msg.FromAddress,
ToAddress: msg.ToAddress,
Status: constant.STATUS_ERROR,
}, 3, 5*time.Second)
return
}
// 传给对应的node server
go l.asyncSendMsgToChain(msg, msg.Chain, 3, 5*time.Second)
log.Printf("Insert Withdraw_req msg success: QueueId(%s)", msg.QueueId)
}
// 支付消息
func (l *ListenServer) handleRmqPay_req(msg message.PayMsg_req) {
// 添加到WithdrawMsgs
l.PayMsgs[msg.Chain].mu.RLock()
l.PayMsgs[msg.Chain].Msgs[msg.QueueId] = msg
l.PayMsgs[msg.Chain].mu.RUnlock()
// 写数据库
sql := "INSERT INTO pay_req_msg (queue_id, from_addr, to_addr, amount, fee, chain, symbol, timestamp, sign) VALUES (?,?,?,?,?,?,?,?,?)"
params := []any{msg.QueueId, msg.FromAddress, msg.ToAddress, fmt.Sprintf("%f", msg.Amount), fmt.Sprintf("%f", msg.Fee), msg.Chain, msg.Symbol, msg.Timestamp, msg.Sign}
_, err := l.MysqlDB.Insert(sql, [][]any{params})
if err != nil {
log.Printf("Insert PayMsg_req msg error: %v", err)
go l.asyncSendMsgToRmq(message.PayMsg_resp{
QueueId: msg.QueueId,
Chain: msg.Chain,
Symbol: msg.Symbol,
FromAddress: msg.FromAddress,
ToAddress: msg.ToAddress,
Status: constant.STATUS_ERROR,
}, 3, 5*time.Second)
return
}
// 传给对应的node server
go l.asyncSendMsgToChain(msg, msg.Chain, 3, 5*time.Second)
log.Printf("Insert PayMsg_req msg success: QueueId(%s)", msg.QueueId)
}
// 移除监听消息
func (l *ListenServer) handleRmqRemove_req(msg message.RemoveListenMsg_req) {
l.RemoveMsgs[msg.Chain].mu.RLock()
l.RemoveMsgs[msg.Chain].Msgs[msg.QueueId] = msg
l.RemoveMsgs[msg.Chain].mu.RUnlock()
sql := "INSERT INTO remove_req_msg (queue_id, msg_type, chain, symbol, address, timestamp, sign) VALUES (?,?,?,?,?,?,?)"
params := []any{msg.QueueId, msg.MsgType, msg.Chain, msg.Symbol, msg.Address, msg.Timestamp, msg.Sign}
_, err := l.MysqlDB.Insert(sql, [][]any{params})
if err != nil {
log.Printf("Insert Remove_req msg error: %v", err)
go l.asyncSendMsgToRmq(message.RemoveListenMsg_resp{
QueueId: msg.QueueId,
MsgType: msg.MsgType,
Chain: msg.Chain,
Symbol: msg.Symbol,
Address: msg.Address,
Status: constant.STATUS_ERROR,
}, 3, 5*time.Second)
}
go l.asyncSendMsgToChain(msg, msg.Chain, 3, 5*time.Second)
log.Printf("Insert Remove_req msg success: QueueId(%s)", msg.QueueId)
}
// 更新状态响应
func (l *ListenServer) handleUpdateReqState(msg message.UpdateReqState) {
typeMap := map[int]string{
0: "topup_req_msg",
1: "withdraw_req_msg",
2: "pay_req_msg",
}
str := "UPDATE " + typeMap[msg.MsgType] + " SET status = ? WHERE queue_id = ?"
params := []any{msg.Status, msg.QueueId}
go func() {
_, err := l.MysqlDB.Update(str, params)
if err != nil {
// 更详细的错误日志,包括 QueueId 和 Status
log.Printf("Failed to update update_req_msg for queue_id %s: %v", msg.QueueId, err)
}
// else if count != 1 {
// // 如果更新的行数不是 1日志中记录详细信息
// log.Printf("Unexpected update count for queue_id %s: expected 1, got %d", msg.QueueId, count)
// }
}()
}
// 充值响应
func (l *ListenServer) handleChainTopup_resp(msg message.TopupMsg_resp) {
switch msg.Status {
case constant.STATUS_SUCCESS, constant.STATUS_FAILED:
// 修改数据库
str := "UPDATE topup_resp_msg SET status = ? WHERE tx_hash = ?"
params := []any{msg.Status, msg.TxHash}
_, err := l.MysqlDB.Update(str, params)
if err != nil {
// 更详细的错误日志,包括 QueueId 和 Status
log.Printf("Failed to update topup_resp_msg for queue_id %s: %v", msg.QueueId, err)
}
// else if count != 1 {
// // 如果更新的行数不是 1日志中记录详细信息
// log.Printf("Unexpected update count for queue_id %s: expected 1, got %d", msg.QueueId, count)
// }
case constant.STATUS_PENDING:
str := "INSERT INTO topup_resp_msg (queue_id, chain, symbol, from_addr, to_addr, amount, tx_hash, height, status) VALUES (?,?,?,?,?,?,?,?,?)"
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.Address, msg.Amount, msg.TxHash, msg.BlockHeight, msg.Status}
_, err := l.MysqlDB.Insert(str, [][]any{params})
if err != nil {
log.Printf("Insert Topup_resp msg error: %v", err)
}
default:
// 插入数据库
str := "INSERT INTO topup_resp_msg (queue_id, chain, symbol, to_addr, status) VALUES (?,?,?,?,?,?,?,?,?)"
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.Address, msg.Status}
_, err := l.MysqlDB.Insert(str, [][]any{params})
if err != nil {
log.Printf("Insert Topup_resp msg error: %v", err)
}
}
go l.asyncSendMsgToRmq(msg, 3, 5*time.Second)
}
// 提现响应
func (l *ListenServer) handleChainWithdraw_resp(msg message.WithdrawMsg_resp) {
switch msg.Status {
// pending状态表示转账已完成且在区块链中被找到等待后续确认+记录到数据库
case constant.STATUS_PENDING:
go func() {
str := "INSERT INTO withdraw_resp_msg (queue_id, chain, symbol, from_addr, to_addr, tx_hash, amount, fee, height, status) VALUES (?,?,?,?,?,?,?,?,?,?)"
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.ToAddress, msg.TxHash, msg.Amount, msg.Fee, msg.BlockHeight, msg.Status}
_, err := l.MysqlDB.Insert(str, [][]any{params})
if err != nil {
log.Println(err)
}
}()
case constant.STATUS_SUCCESS, constant.STATUS_FAILED:
l.WithdrawMsgs[msg.Chain].mu.Lock()
delete(l.WithdrawMsgs[msg.Chain].Msgs, msg.QueueId)
l.WithdrawMsgs[msg.Chain].mu.Unlock()
go func() {
str := "UPDATE withdraw_resp_msg SET status = ? WHERE tx_hash = ?"
params := []any{msg.Status, msg.TxHash}
_, err := l.MysqlDB.Update(str, params)
if err != nil {
// 更详细的错误日志,包括 QueueId 和 Status
log.Printf("Failed to update withdraw_resp_msg for queue_id %s: %v", msg.QueueId, err)
}
// else if count != 1 {
// // 如果更新的行数不是 1日志中记录详细信息
// log.Printf("Unexpected update count for queue_id %s: expected 1, got %d", msg.QueueId, count)
// }
}()
go l.asyncSendMsgToRmq(msg, 3, 5*time.Second)
default:
l.WithdrawMsgs[msg.Chain].mu.Lock()
delete(l.WithdrawMsgs[msg.Chain].Msgs, msg.QueueId)
l.WithdrawMsgs[msg.Chain].mu.Unlock()
go func() {
str := "INSERT INTO withdraw_resp_msg (queue_id, chain, symbol, from_addr, to_addr, amount, fee, status) VALUES (?,?,?,?,?,?,?,?)"
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.ToAddress, fmt.Sprintf("%f", msg.Amount), fmt.Sprintf("%f", msg.Fee), msg.Status}
_, err := l.MysqlDB.Insert(str, [][]any{params})
if err != nil {
log.Println(err)
}
}()
go l.asyncSendMsgToRmq(msg, 3, 5*time.Second)
}
}
// 支付响应
func (l *ListenServer) handleChainPay_resp(msg message.PayMsg_resp) {
switch msg.Status {
// pending状态表示转账已完成且在区块链中被找到等待后续确认+记录到数据库
case constant.STATUS_PENDING:
go func() {
str := "INSERT INTO pay_resp_msg (queue_id, chain, symbol, from_addr, to_addr, tx_hash, amount, fee, height, status) VALUES (?,?,?,?,?,?,?,?,?,?)"
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.ToAddress, msg.TxHash, msg.Amount, msg.Fee, msg.BlockHeight, msg.Status}
_, err := l.MysqlDB.Insert(str, [][]any{params})
if err != nil {
log.Println(err)
}
}()
case constant.STATUS_SUCCESS, constant.STATUS_FAILED:
l.WithdrawMsgs[msg.Chain].mu.Lock()
delete(l.WithdrawMsgs[msg.Chain].Msgs, msg.QueueId)
l.WithdrawMsgs[msg.Chain].mu.Unlock()
go func() {
str := "UPDATE pay_resp_msg SET status = ? WHERE tx_hash = ?"
params := []any{msg.Status, msg.TxHash}
_, err := l.MysqlDB.Update(str, params)
if err != nil {
// 更详细的错误日志,包括 QueueId 和 Status
log.Printf("Failed to update pay_resp_msg for queue_id %s: %v", msg.QueueId, err)
}
// else if count != 1 {
// // 如果更新的行数不是 1日志中记录详细信息
// log.Printf("Unexpected update count for queue_id %s: expected 1, got %d", msg.QueueId, count)
// }
}()
go l.asyncSendMsgToRmq(msg, 3, 5*time.Second)
default:
l.WithdrawMsgs[msg.Chain].mu.Lock()
delete(l.WithdrawMsgs[msg.Chain].Msgs, msg.QueueId)
l.WithdrawMsgs[msg.Chain].mu.Unlock()
go func() {
str := "INSERT INTO pay_resp_msg (queue_id, chain, symbol, from_addr, to_addr, amount, fee, status) VALUES (?,?,?,?,?,?,?,?)"
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.ToAddress, fmt.Sprintf("%f", msg.Amount), fmt.Sprintf("%f", msg.Fee), msg.Status}
_, err := l.MysqlDB.Insert(str, [][]any{params})
if err != nil {
log.Println(err)
}
}()
go l.asyncSendMsgToRmq(msg, 3, 5*time.Second)
}
}
// 移除监听响应
func (l *ListenServer) handleChainRemove_resp(msg message.RemoveListenMsg_resp) {
// 如果状态为成功,移除该 QueueId
if msg.Status == constant.STATUS_SUCCESS {
l.RemoveMsgs[msg.Chain].mu.Lock()
delete(l.RemoveMsgs[msg.Chain].Msgs, msg.QueueId)
l.RemoveMsgs[msg.Chain].mu.Unlock()
}
// 使用 goroutine 异步更新数据库
go func() {
// 更新数据库
str := "UPDATE remove_resp_msg SET status = ? WHERE queue_id = ?"
params := []any{msg.Status, msg.QueueId}
_, err := l.MysqlDB.Update(str, params)
if err != nil {
// 更详细的错误日志,包括 QueueId 和 Status
log.Printf("Failed to update remove_resp_msg for queue_id %s: %v", msg.QueueId, err)
}
// else if count != 1 {
// // 如果更新的行数不是 1日志中记录详细信息
// log.Printf("Unexpected update count for queue_id %s: expected 1, got %d", msg.QueueId, count)
// }
}()
// 异步发送消息到 RMQ
go l.asyncSendMsgToRmq(msg, 3, 5*time.Second)
}
// 异步发送通道消息Listen -> Chain server
func (l *ListenServer) asyncSendMsgToChain(msg any, chain string, retries int, timeout time.Duration) {
for retries > 0 {
select {
case l.ChToChainServer[chain] <- msg: // 如果通道没有满,就发送消息
// log.Printf("Sent message to rmq_ch_out: %v", msg)
return
case <-time.After(timeout): // 超时控制
log.Printf("Timeout sending message to rmq_ch_out: %v", msg)
retries--
if retries == 0 {
log.Printf("Max retries reached, giving up on sending message: %v", msg)
return
}
// 在超时后进行重试
log.Printf("Retrying sending message: %v, retries left: %d", msg, retries)
}
}
}
// 异步发送通道消息Listen -> Rmq
func (l *ListenServer) asyncSendMsgToRmq(msg any, retries int, timeout time.Duration) {
for retries > 0 {
select {
case l.ChToRmqServer <- msg: // 如果通道没有满,就发送消息
// log.Printf("Sent message to rmq_ch_out: %v", msg)
return
case <-time.After(timeout): // 超时控制
log.Printf("Timeout sending message to rmq_ch_out: %v", msg)
retries--
if retries == 0 {
log.Printf("Max retries reached, giving up on sending message: %v", msg)
return
}
// 在超时后进行重试
log.Printf("Retrying sending message: %v, retries left: %d", msg, retries)
}
}
}

View File

@@ -209,20 +209,20 @@ func LogTopup(toAddress string, status string, amount float64, txHash string, bl
} }
// LogWithdraw 记录提现消息 // LogWithdraw 记录提现消息
func LogWithdraw(toAddress string, status string, amount float64, fromAddress string, txHash string, blockHeight uint64) { func LogWithdraw(fromAddress string, status string, amount float64, toAddress string, txHash string, blockHeight uint64) {
if txLogger == nil { if txLogger == nil {
return return
} }
// 使用 toAddress 作为文件名 // 使用 toAddress 作为文件名
lf, err := txLogger.getOrCreateLogFile(toAddress) lf, err := txLogger.getOrCreateLogFile(fromAddress)
if err != nil { if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err) fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return return
} }
timestamp := time.Now().Format("2006-01-02 15:04:05") timestamp := time.Now().Format("2006-01-02 15:04:05")
content := fmt.Sprintf("%s [withdraw]-[%s] | 金额: %.6f | FromAddress: %s | ToAddress: %s | 交易哈希: %s | 区块高度: %d", content := fmt.Sprintf("%s [提现]-[%s] | 金额: %.6f | FromAddress: %s | ToAddress: %s | 交易哈希: %s | 区块高度: %d",
timestamp, status, amount, fromAddress, toAddress, txHash, blockHeight) timestamp, status, amount, fromAddress, toAddress, txHash, blockHeight)
if err := lf.write(content); err != nil { if err := lf.write(content); err != nil {
@@ -231,21 +231,235 @@ func LogWithdraw(toAddress string, status string, amount float64, fromAddress st
} }
// LogPay 记录支付消息 // LogPay 记录支付消息
func LogPay(toAddress string, status string, amount float64, fromAddress string, txHash string, blockHeight uint64, orderId string, queueId string) { func LogPay(status string, fromAddress string, queueId string) {
if txLogger == nil { if txLogger == nil {
return return
} }
// 使用 toAddress 作为文件名 // 使用 toAddress 作为文件名
lf, err := txLogger.getOrCreateLogFile(toAddress) lf, err := txLogger.getOrCreateLogFile(fromAddress)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
// t, err := json.Marshal(transactions)
// if err != nil {
// fmt.Println("Error marshalling to JSON:", err)
// return
// }
timestamp := time.Now().Format("2006-01-02 15:04:05")
content := fmt.Sprintf("%s [pay]-[%s] | FromAddress: %s | QueueId: %s",
timestamp, status, fromAddress, queueId)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
}
}
// 记录当前监听的钱包和所有消息
func LogETHNode(msg string) {
if txLogger == nil {
return
}
lf, err := txLogger.getOrCreateLogFile("ethnode")
if err != nil { if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err) fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return return
} }
timestamp := time.Now().Format("2006-01-02 15:04:05") timestamp := time.Now().Format("2006-01-02 15:04:05")
content := fmt.Sprintf("%s [pay]-[%s] | 金额: %.6f | FromAddress: %s | ToAddress: %s | 交易哈希: %s | 区块高度: %d | OrderId: %s | QueueId: %s", content := fmt.Sprintf("[ETH-NODE:%s]: %s", timestamp, msg)
timestamp, status, amount, fromAddress, toAddress, txHash, blockHeight, orderId, queueId)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
}
}
// =============================== RMQ <-> Listen 通信日志 ===============================
// LogRmqToListenTopupReq 记录 RMQ -> Listen 充值请求
// 使用 address 作为文件名
func LogRmqToListenTopupReq(queueId, chain, symbol, address string, timestamp uint64) {
if txLogger == nil {
return
}
lf, err := txLogger.getOrCreateLogFile(address)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
t := time.Now().Format("2006-01-02 15:04:05")
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
// TopupReq 没有 fromAddress使用 "-" 代替toAddress 是 addressamount 为 0
content := fmt.Sprintf("[TopupReq]: %s--%s-%s-%s-0",
t, address, chain, symbol)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
}
}
// LogRmqToListenWithdrawReq 记录 RMQ -> Listen 提现请求
// 使用 fromAddress 作为文件名
func LogRmqToListenWithdrawReq(queueId, chain, symbol, fromAddress, toAddress string, amount, fee float64, timestamp uint64) {
if txLogger == nil {
return
}
lf, err := txLogger.getOrCreateLogFile(fromAddress)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
t := time.Now().Format("2006-01-02 15:04:05")
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
content := fmt.Sprintf("[WithdrawReq]: %s-%s-%s-%s-%s-%.6f",
t, fromAddress, toAddress, chain, symbol, amount)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
}
}
// LogRmqToListenPayReq 记录 RMQ -> Listen 支付请求
// 使用 fromAddress 作为文件名
func LogRmqToListenPayReq(queueId, chain, symbol, fromAddress, toAddress string, amount, fee float64, timestamp uint64) {
if txLogger == nil {
return
}
lf, err := txLogger.getOrCreateLogFile(fromAddress)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
t := time.Now().Format("2006-01-02 15:04:05")
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
content := fmt.Sprintf("[PayReq]: %s-%s-%s-%s-%s-%.6f",
t, fromAddress, toAddress, chain, symbol, amount)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
}
}
// LogRmqToListenRemoveReq 记录 RMQ -> Listen 移除监听请求
// 使用 address 作为文件名
func LogRmqToListenRemoveReq(queueId string, msgType int, chain, symbol, address string, timestamp uint64) {
if txLogger == nil {
return
}
lf, err := txLogger.getOrCreateLogFile(address)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
t := time.Now().Format("2006-01-02 15:04:05")
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
// RemoveReq 没有 fromAddresstoAddress 是 addressamount 为 0
content := fmt.Sprintf("[RemoveReq]: %s--%s-%s-%s-0",
t, address, chain, symbol)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
}
}
// LogListenToRmqTopupResp 记录 Listen -> RMQ 充值响应
// 使用 address 作为文件名
func LogListenToRmqTopupResp(queueId, chain, symbol, address, fromAddress, txHash string, amount float64, blockHeight uint64, status int) {
if txLogger == nil {
return
}
lf, err := txLogger.getOrCreateLogFile(address)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
t := time.Now().Format("2006-01-02 15:04:05")
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
// TopupResp 中 address 是目标地址toAddressfromAddress 是来源地址
content := fmt.Sprintf("[TopupResp]: %s-%s-%s-%s-%s-%.6f",
t, fromAddress, address, chain, symbol, amount)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
}
}
// LogListenToRmqWithdrawResp 记录 Listen -> RMQ 提现响应
// 使用 fromAddress 作为文件名
func LogListenToRmqWithdrawResp(queueId, chain, symbol, fromAddress, toAddress, txHash string, amount, fee float64, blockHeight uint64, status int) {
if txLogger == nil {
return
}
lf, err := txLogger.getOrCreateLogFile(fromAddress)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
t := time.Now().Format("2006-01-02 15:04:05")
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
content := fmt.Sprintf("[WithdrawResp]: %s-%s-%s-%s-%s-%.6f",
t, fromAddress, toAddress, chain, symbol, amount)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
}
}
// LogListenToRmqPayResp 记录 Listen -> RMQ 支付响应
// 使用 fromAddress 作为文件名
func LogListenToRmqPayResp(queueId, chain, symbol, fromAddress, toAddress, txHash string, amount, fee float64, blockHeight uint64, status int) {
if txLogger == nil {
return
}
lf, err := txLogger.getOrCreateLogFile(fromAddress)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
t := time.Now().Format("2006-01-02 15:04:05")
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
content := fmt.Sprintf("[PayResp]: %s-%s-%s-%s-%s-%.6f",
t, fromAddress, toAddress, chain, symbol, amount)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
}
}
// LogListenToRmqRemoveResp 记录 Listen -> RMQ 移除监听响应
// 使用 address 作为文件名
func LogListenToRmqRemoveResp(queueId string, msgType int, chain, symbol, address string, status int) {
if txLogger == nil {
return
}
lf, err := txLogger.getOrCreateLogFile(address)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
t := time.Now().Format("2006-01-02 15:04:05")
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
// RemoveResp 没有 fromAddresstoAddress 是 addressamount 为 0
content := fmt.Sprintf("[RemoveResp]: %s--%s-%s-%s-0",
t, address, chain, symbol)
if err := lf.write(content); err != nil { if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err) fmt.Printf("⚠️ 写入日志失败: %v\n", err)

53
internal/msg/config.go Normal file
View File

@@ -0,0 +1,53 @@
package msg
import "time"
// 配置文件结构
type Config struct {
Net []string `json:"net"`
RmqConfig RmqConfig `json:"rmq_config"`
MsgConfig MsgConfig `json:"msg_config"`
MysqlConfig map[string]MysqlConfig `json:"mysql_config"` // {dbname: MysqlConfig{}, ...}
ETHConfig ETHConfig `json:"eth_config"`
}
type RmqConfig struct {
SubAddr string `json:"sub_addr"`
Pay Queue `json:"pay"`
Topup Queue `json:"topup"`
Withdraw Queue `json:"withdraw"`
Remove Queue `json:"remove"`
PayResp Queue `json:"pay_resp"`
TopupResp Queue `json:"topup_resp"`
WithdrawResp Queue `json:"withdraw_resp"`
RemoveResp Queue `json:"remove_resp"`
}
type Queue struct {
Queue string `json:"queue"`
Exchange string `json:"exchange"`
Routing []string `json:"routing"`
}
type MsgConfig struct {
SqlitePath string `json:"sqlite_path"` // sqlite3数据库文件路径
}
type MysqlConfig struct {
User string `json:"user"`
Password string `json:"password"`
Host string `json:"host"`
Port int `json:"port"`
Database string `json:"database"`
MaxOpenConns int `json:"maxOpenConns"`
MaxIdleConns int `json:"maxIdleConns"`
ConnMaxLife time.Duration `json:"connMaxLife"`
}
type ETHConfig struct {
RpcUrl string `json:"rpc_url"`
WsUrl string `json:"ws_url"`
ConfirmHeight uint64 `json:"confirm_height"`
SqlitePath string `json:"sqlite_path"` // sqlite3数据库文件路径
SupportTokens []string `json:"support_tokens"` // 支持的代币列表,如["USDT", "DAI"]
}

View File

@@ -1,150 +1,138 @@
package msg package msg
import "time" import "math/big"
// 配置文件结构
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 {
QueueName string `json:"queue"`
ExchangeName string `json:"exchange"`
Routing []string `json:"routing"`
}
type ETHConfig struct {
RpcURL string `json:"rpcUrl"` // rpc连接地址
WsURL string `json:"wsUrl"` // websocket连接地址
ConfirmHeight uint64 `json:"confirmHeight"` // 确认所需区块数
DbConfig DbConfig `json:"dbConfig"`
}
type TRONConfig struct {
RpcUrl string `json:"rpcUrl"`
ConfirmHeight uint64 `json:"confirmHeight"`
DbConfig DbConfig `json:"dbConfig"`
}
// Config 数据库配置
type DbConfig struct {
User string `json:"user"`
Password string `json:"password"`
Host string `json:"host"`
Port int `json:"port"`
Database string `json:"database"`
MaxOpenConns int `json:"maxOpenConns"` // 最大打开连接数
MaxIdleConns int `json:"maxIdleCoons"` // 最大空闲连接数
ConnMaxLife time.Duration `json:"connMaxLife"` // 连接最大存活时间
}
// =============================== type0 =============================== // =============================== type0 ===============================
// 接收的充值消息 // 接收的充值消息
type TopupMsg_req struct { type TopupMsg_req struct {
QueueId string `json:"queue_id"`
Chain string `json:"chain"` // 链名称 Chain string `json:"chain"` // 链名称
Symbol string `json:"symbol"` // 币种 Symbol string `json:"symbol"` // 币种
Address string `json:"address"` Address string `json:"address"`
Timestamp uint64 `json:"timestamp"` Timestamp uint64 `json:"timestamp"`
Sign string `json:"sign"` Sign string `json:"sign"`
Status int `json:"status,omitempty"` // 1监听中2停止监听
} }
// 返回充值结果消息 // 返回充值结果消息
type TopupMsg_resp struct { type TopupMsg_resp struct {
Address string `json:"address"` QueueId string `json:"queue_id"`
Status int `json:"status"` Chain string `json:"chain"` // 链名称
Chain string `json:"chain"` // 链名称 Symbol string `json:"symbol"` // 币种
Symbol string `json:"symbol"` // 币种 FromAddress string `json:"from_address,omitempty"` // 充值来源地址
Amount float64 `json:"amount"` Address string `json:"address"` // 充值目标地址
TxHash string `json:"tx_hash"` TxHash string `json:"tx_hash,omitempty"`
BlockHeight uint64 `json:"block_height"` // 区块高度 Amount float64 `json:"amount,omitempty"`
BlockHeight uint64 `json:"block_height,omitempty"` // 区块高度
Status int `json:"status"` // 0失败1成功2待定3sign校验失败
} }
// =============================== type1 =============================== // =============================== type1 ===============================
// 接收的提现消息 // 接收的提现消息
type WithdrawMsg_req struct { type WithdrawMsg_req struct {
QueueId string `json:"queue_id"` QueueId string `json:"queue_id"`
Chain string `json:"chain"` // 链名称
Symbol string `json:"symbol"` // 币种
FromAddress string `json:"from_address"` // 我们提供的地址 FromAddress string `json:"from_address"` // 我们提供的地址
ToAddress string `json:"to_address"` // 用户要提现到的地址 ToAddress string `json:"to_address"` // 用户要提现到的地址
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
Chain string `json:"chain"` // 链名称 Fee float64 `json:"fee"`
Symbol string `json:"symbol"` // 币种
Timestamp uint64 `json:"timestamp"` Timestamp uint64 `json:"timestamp"`
Sign string `json:"sign"` Sign string `json:"sign"`
Status int `json:"status,omitempty"`
} }
// 返回提现结果消息 // 返回提现结果消息
type WithdrawMsg_resp struct { type WithdrawMsg_resp struct {
QueueId string `json:"queue_id"` QueueId string `json:"queue_id"`
Chain string `json:"chain"` // 链名称 Chain string `json:"chain"` // 链名称
Symbol string `json:"symbol"` // 币种 Symbol string `json:"symbol"` // 币种
Status int `json:"status"`
Amount float64 `json:"amount"`
TxHash string `json:"tx_hash"`
FromAddress string `json:"from_address"` // 来源地址 FromAddress string `json:"from_address"` // 来源地址
ToAddress string `json:"to_address"` // 目标地址 ToAddress string `json:"to_address"` // 目标地址
BlockHeight uint64 `json:"block_height"` // 区块高度 TxHash string `json:"tx_hash,omitempty"`
Amount float64 `json:"amount,omitempty"`
Fee float64 `json:"fee,omitempty"`
BlockHeight uint64 `json:"block_height,omitempty"` // 区块高度
Status int `json:"status"` // 0失败1成功3sign校验失败
} }
// =============================== type2 =============================== // =============================== type2 ===============================
// 接收到的支付消息
type PayMsg_req struct { type PayMsg_req struct {
QueueId string `json:"queue_id"` QueueId string `json:"queue_id"`
Chain string `json:"chain"` // 链名称
Symbol string `json:"symbol"` // 币种
FromAddress string `json:"from_address"` // 我们提供的地址 FromAddress string `json:"from_address"` // 我们提供的地址
ToAddress string `json:"to_address"` // 卖家地址 ToAddress string `json:"to_address"` // 用户要提现到的地址
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
Chain string `json:"chain"` // 链名称 Fee float64 `json:"fee"`
Symbol string `json:"symbol"` // 币种
OrderId string `json:"order_id"` // 订单号
Timestamp uint64 `json:"timestamp"` Timestamp uint64 `json:"timestamp"`
Sign string `json:"sign"` Sign string `json:"sign"`
Status int `json:"status,omitempty"`
} }
// 返回支付结果消息
type PayMsg_resp struct { type PayMsg_resp struct {
QueueId string `json:"queue_id"` QueueId string `json:"queue_id"`
Status int `json:"status"` Chain string `json:"chain"` // 链名称
Amount float64 `json:"amount"` Symbol string `json:"symbol"` // 币种
Chain string `json:"chain"` // 链名称
Symbol string `json:"symbol"` // 币种
OrderId string `json:"order_id"` // 订单号
TxHash string `json:"tx_hash"`
FromAddress string `json:"from_address"` // 来源地址 FromAddress string `json:"from_address"` // 来源地址
ToAddress string `json:"to_address"` // 目标地址 ToAddress string `json:"to_address"` // 目标地址
BlockHeight uint64 `json:"block_height"` // 区块高度 TxHash string `json:"tx_hash,omitempty"`
Amount float64 `json:"amount,omitempty"`
Fee float64 `json:"fee,omitempty"`
BlockHeight uint64 `json:"block_height,omitempty"` // 区块高度
Status int `json:"status"` // 0失败1成功3sign校验失败
} }
// type PayMsg_req struct {
// QueueId string `json:"queue_id"`
// Chain string `json:"chain"`
// Symbol string `json:"symbol"`
// FromAddress string `json:"from_address"`
// TotalAmount float64 `json:"total_amount"`
// TotalFee float64 `json:"total_fee"`
// Timestamp uint64 `json:"timestamp"`
// Sign string `json:"sign"`
// Transactions map[string]PayData `json:"transactions"` // {"to_address": PayData_req{}, ...}
// Status int `json:"status,omitempty"`
// }
// type PayMsg_resp struct {
// QueueId string `json:"queue_id"`
// Chain string `json:"chain"`
// Symbol string `json:"symbol"`
// FromAddress string `json:"from_address"`
// PayStatus int `json:"pay_status"` // 1至少有一笔转账成功3sign校验失败4钱包余额不足
// Transactions map[string]PayData `json:"transactions"` // {"to_address": PayData_resp{}, ...}
// }
// type PayData struct {
// QueueId string `json:"queue_id,omitempty"`
// Chain string `json:"chain,omitempty"`
// Symbol string `json:"symbol,omitempty"`
// TxHash string `json:"tx_hash,omitempty"`
// ToAddress string `json:"to_address"`
// Amount float64 `json:"amount"`
// Fee float64 `json:"fee"`
// BlockHeight uint64 `json:"block_height,omitempty"`
// Status int `json:"status,omitempty"` // 0失败1成功, 2待确认
// }
// =============================== type3 =============================== // =============================== type3 ===============================
// 接收到的删除监听地址消息 // 接收到的删除监听地址消息
type RemoveListenMsg_req struct { type RemoveListenMsg_req struct {
QueueId string `json:"queue_id"`
MsgType int `json:"msg_type"` MsgType int `json:"msg_type"`
Chain string `json:"chain"` Chain string `json:"chain"`
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
Address string `json:"address"` Address string `json:"address"`
Timestamp uint64 `json:"timestamp"` Timestamp uint64 `json:"timestamp"`
Sign string `json:"sign"` Sign string `json:"sign"`
Status int `json:"status,omitempty"`
} }
// 返回收到的删除监听地址消息 // 返回收到的删除监听地址消息
type RemoveListenMsg_resp struct { type RemoveListenMsg_resp struct {
QueueId string `json:"queue_id"`
MsgType int `json:"msg_type"` MsgType int `json:"msg_type"`
Chain string `json:"chain"` Chain string `json:"chain"`
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
@@ -152,18 +140,49 @@ type RemoveListenMsg_resp struct {
Status int `json:"status"` // 0失败 1成功 Status int `json:"status"` // 0失败 1成功
} }
// ===================================================================== // =============================== ChainServer -> ListenServer ===============================
// 节点通用消息结构 // 节点服务响应
type Tx_msg struct { type ChainServer_resp struct {
TxType int `json:"tx_type"` // 转账类型0充值1提现2支付 QueueId string
Tx Tx `json:"tx"` MsgType int
Chain string
Symbol string
Status int // 遵循constant模块定义
} }
type Tx struct {
From string `json:"from"` // 充值/提现/支付的来源地址 // =============================== 其他 ===============================
To string `json:"to"` // 充值/提现/支付的目标地址 type Transaction struct {
Height uint64 `json:"height"` // 区块高度 QueueId string `json:"queue_id,omitempty"` // 交易对应的QueueId
TxHash string `json:"tx_hash"` // 交易哈希 // 交易对应的消息类型0充值 1提现 2支付
Symbol string `json:"symbol"` // 币种 // 充值tx.to_address = msg.Address
Value float64 `json:"value"` // 数量,单位是币 // 提现/支付tx.to_address = msg.ToAddress && tx.from_address = msg.FromAddress && tx.value = msg.Amount
Status int `json:"status"` // 交易状态1成功0失败, 2待确认 // 同时还要通过msg确认属于哪种类型即上述3个条件是否都存在于某个msg钟
TxType int `json:"tx_type,omitempty"`
Chain string `json:"chain"`
Symbol string `json:"symbol"`
From string `json:"from"`
To string `json:"to"`
TxHash string `json:"tx_hash,omitempty"`
Height uint64 `json:"height,omitempty"`
Amount *big.Int `json:"amount"`
GasUsed *big.Int `json:"gas_used,omitempty"` // 转账实际产生的eth消耗
FreezeFee *big.Int `json:"freeze_fee,omitempty"` // erc20转账冻结的usdt,只有提现才会修改
Status int `json:"status"` // 交易状态1成功0失败, 2待确认
}
type TransferResult struct {
QueueId string
MsgType int // 1提现2支付
Chain string
Symbol string
FromAddress string
ToAddress string
Amount float64
Status int
}
type UpdateReqState struct {
QueueId string
MsgType int
Status int
} }

View File

@@ -1,46 +0,0 @@
# RabbitMQ 服务模块
本模块提供 RabbitMQ 消息队列的封装,用于接收业务系统的请求和发送交易确认响应。
## 快速使用
```go
// 创建 RabbitMQ 服务
rmqServer, err := rmq.NewRabbitMQServer(config.RMQConfig)
if err != nil {
log.Fatal(err)
}
defer rmqServer.Close()
// 设置消息回调
rmqServer.OnTopupMsg = func(msg message.TopupMsg_req) {
// 处理充值请求
}
// 启动监听
rmqServer.Start()
// 发送响应
rmqServer.PublishTopupResp(response)
```
## 消息队列结构
### 请求队列(消费)
- `topup` - 充值请求
- `withdraw` - 提现请求
- `pay` - 支付请求
### 响应队列(发布)
- `topup_resp` - 充值响应
- `withdraw_resp` - 提现响应
- `pay_resp` - 支付响应
## 特性
**自动重连** - 连接断开时自动重连
**消息持久化** - 消息不会丢失
**手动确认** - 处理成功后才确认消息
**并发安全** - 支持多 goroutine 并发发布
更多详情请参考 [主 README](../../README.md)。

View File

@@ -14,7 +14,7 @@ import (
// RabbitMQServer RabbitMQ 服务 // RabbitMQServer RabbitMQ 服务
type RabbitMQServer struct { type RabbitMQServer struct {
config message.RMQConfig config message.RmqConfig
conn *amqp.Connection conn *amqp.Connection
channel *amqp.Channel channel *amqp.Channel
mu sync.Mutex mu sync.Mutex
@@ -22,13 +22,14 @@ type RabbitMQServer struct {
cancel context.CancelFunc cancel context.CancelFunc
// 消息处理回调函数 // 消息处理回调函数
OnTopupMsg func(message.TopupMsg_req) // 充值请求回调 OnTopupMsg func(message.TopupMsg_req) // 充值请求回调
OnWithdrawMsg func(message.WithdrawMsg_req) // 提现请求回调 OnWithdrawMsg func(message.WithdrawMsg_req) // 提现请求回调
OnPayMsg func(message.PayMsg_req) // 支付请求回调 OnPayMsg func(message.PayMsg_req) // 支付请求回调
OnRemoveMsg func(message.RemoveListenMsg_req) // 删除充值监听回调
} }
// NewRabbitMQServer 创建 RabbitMQ 服务 // NewRabbitMQServer 创建 RabbitMQ 服务
func NewRabbitMQServer(config message.RMQConfig) (*RabbitMQServer, error) { func NewRabbitMQServer(config message.RmqConfig) (*RabbitMQServer, error) {
// 创建连接 // 创建连接
conn, err := amqp.Dial(config.SubAddr) conn, err := amqp.Dial(config.SubAddr)
if err != nil { if err != nil {
@@ -58,61 +59,63 @@ func NewRabbitMQServer(config message.RMQConfig) (*RabbitMQServer, error) {
server.Close() server.Close()
return nil, fmt.Errorf("failed to setup queues: %w", err) return nil, fmt.Errorf("failed to setup queues: %w", err)
} }
log.Println("✅ RabbitMQ队列已启动")
return server, nil return server, nil
} }
// setupQueuesAndExchanges 设置队列和交换机 // setupQueuesAndExchanges 设置队列和交换机
func (r *RabbitMQServer) setupQueuesAndExchanges() error { func (r *RabbitMQServer) setupQueuesAndExchanges() error {
configs := []message.QueueConfig{ configs := []message.Queue{
r.config.PayConfig, r.config.Pay,
r.config.TopUpConfig, r.config.Topup,
r.config.WithdrawConfig, r.config.Withdraw,
r.config.PayRespConfig, r.config.Remove,
r.config.TopUpRespConfig, r.config.PayResp,
r.config.WithdrawRespConfig, r.config.TopupResp,
r.config.WithdrawResp,
r.config.RemoveResp,
} }
for _, cfg := range configs { for _, cfg := range configs {
// 声明交换机 // 声明交换机
err := r.channel.ExchangeDeclare( err := r.channel.ExchangeDeclare(
cfg.ExchangeName, // 交换机名称 cfg.Exchange, // 交换机名称
"direct", // 类型direct与现有交换机类型一致 "direct", // 类型direct与现有交换机类型一致
true, // durable true, // durable
false, // auto-deleted false, // auto-deleted
false, // internal false, // internal
false, // no-wait false, // no-wait
nil, // arguments nil, // arguments
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to declare exchange %s: %w", cfg.ExchangeName, err) return fmt.Errorf("failed to declare exchange %s: %w", cfg.Exchange, err)
} }
// 声明队列 // 声明队列
_, err = r.channel.QueueDeclare( _, err = r.channel.QueueDeclare(
cfg.QueueName, // 队列名称 cfg.Exchange, // 队列名称
true, // durable true, // durable
false, // delete when unused false, // delete when unused
false, // exclusive false, // exclusive
false, // no-wait false, // no-wait
nil, // arguments nil, // arguments
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to declare queue %s: %w", cfg.QueueName, err) return fmt.Errorf("failed to declare queue %s: %w", cfg.Exchange, err)
} }
// 绑定队列到交换机 // 绑定队列到交换机
for _, routingKey := range cfg.Routing { for _, routingKey := range cfg.Routing {
err = r.channel.QueueBind( err = r.channel.QueueBind(
cfg.QueueName, // 队列名称 cfg.Queue, // 队列名称
routingKey, // routing key routingKey, // routing key
cfg.ExchangeName, // 交换机名称 cfg.Exchange, // 交换机名称
false, // no-wait false, // no-wait
nil, // arguments nil, // arguments
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to bind queue %s to exchange %s with key %s: %w", return fmt.Errorf("failed to bind queue %s to exchange %s with key %s: %w",
cfg.QueueName, cfg.ExchangeName, routingKey, err) cfg.Queue, cfg.Exchange, routingKey, err)
} }
} }
@@ -131,6 +134,8 @@ func (r *RabbitMQServer) Start() error {
go r.consumeWithdraw() go r.consumeWithdraw()
// 启动支付消息监听 // 启动支付消息监听
go r.consumePay() go r.consumePay()
// 启动删除充值监听
go r.consumeRemove()
// log.Println("🚀 RabbitMQ 服务启动成功,开始监听消息...") // log.Println("🚀 RabbitMQ 服务启动成功,开始监听消息...")
return nil return nil
@@ -139,8 +144,7 @@ func (r *RabbitMQServer) Start() error {
// consumeTopup 消费充值消息 // consumeTopup 消费充值消息
func (r *RabbitMQServer) consumeTopup() { func (r *RabbitMQServer) consumeTopup() {
r.consumeQueue( r.consumeQueue(
r.config.TopUpConfig.QueueName, r.config.Topup.Queue,
"topup",
func(body []byte) error { func(body []byte) error {
var msg message.TopupMsg_req var msg message.TopupMsg_req
if err := json.Unmarshal(body, &msg); err != nil { if err := json.Unmarshal(body, &msg); err != nil {
@@ -160,8 +164,7 @@ func (r *RabbitMQServer) consumeTopup() {
// consumeWithdraw 消费提现消息 // consumeWithdraw 消费提现消息
func (r *RabbitMQServer) consumeWithdraw() { func (r *RabbitMQServer) consumeWithdraw() {
r.consumeQueue( r.consumeQueue(
r.config.WithdrawConfig.QueueName, r.config.Withdraw.Queue,
"withdraw",
func(body []byte) error { func(body []byte) error {
var msg message.WithdrawMsg_req var msg message.WithdrawMsg_req
if err := json.Unmarshal(body, &msg); err != nil { if err := json.Unmarshal(body, &msg); err != nil {
@@ -181,15 +184,14 @@ func (r *RabbitMQServer) consumeWithdraw() {
// consumePay 消费支付消息 // consumePay 消费支付消息
func (r *RabbitMQServer) consumePay() { func (r *RabbitMQServer) consumePay() {
r.consumeQueue( r.consumeQueue(
r.config.PayConfig.QueueName, r.config.Pay.Queue,
"pay",
func(body []byte) error { func(body []byte) error {
var msg message.PayMsg_req var msg message.PayMsg_req
if err := json.Unmarshal(body, &msg); err != nil { if err := json.Unmarshal(body, &msg); err != nil {
return fmt.Errorf("failed to parse pay message: %w", err) return fmt.Errorf("failed to parse pay message: %w", err)
} }
log.Printf("📥 [RMQ] 收到支付请求: QueueId=%s, OrderId=%s, From=%s, To=%s, Amount=%.2f %s", log.Printf("📥 [RMQ] 收到支付请求: QueueId=%s, From=%s, To=%s, Chain=%s, Symbol=%s, Amount=%f",
msg.QueueId, msg.OrderId, msg.FromAddress, msg.ToAddress, msg.Amount, msg.Symbol) msg.QueueId, msg.FromAddress, msg.ToAddress, msg.Chain, msg.Symbol, msg.Amount)
if r.OnPayMsg != nil { if r.OnPayMsg != nil {
r.OnPayMsg(msg) r.OnPayMsg(msg)
@@ -199,8 +201,27 @@ func (r *RabbitMQServer) consumePay() {
) )
} }
// consumeRemove 消费删除充值监听消息
func (r *RabbitMQServer) consumeRemove() {
r.consumeQueue(
r.config.Remove.Queue,
func(body []byte) error {
var msg message.RemoveListenMsg_req
if err := json.Unmarshal(body, &msg); err != nil {
return fmt.Errorf("failed to parse remove message: %w", err)
}
log.Printf("📥 [RMQ] 收到删除充值监听: Chain=%s, Symbol=%s, Address=%s", msg.Chain, msg.Symbol, msg.Address)
if r.OnRemoveMsg != nil {
r.OnRemoveMsg(msg)
}
return nil
},
)
}
// consumeQueue 通用队列消费方法 // consumeQueue 通用队列消费方法
func (r *RabbitMQServer) consumeQueue(queueName, msgType string, handler func([]byte) error) { func (r *RabbitMQServer) consumeQueue(queueName string, handler func([]byte) error) {
for { for {
select { select {
case <-r.ctx.Done(): case <-r.ctx.Done():
@@ -246,7 +267,7 @@ func (r *RabbitMQServer) consumeQueue(queueName, msgType string, handler func([]
// PublishTopupResp 发布充值响应 // PublishTopupResp 发布充值响应
func (r *RabbitMQServer) PublishTopupResp(resp message.TopupMsg_resp) error { func (r *RabbitMQServer) PublishTopupResp(resp message.TopupMsg_resp) error {
return r.publishMessage( return r.publishMessage(
r.config.TopUpRespConfig, r.config.TopupResp,
resp, resp,
fmt.Sprintf("充值响应: Address=%s, Status=%d, TxHash=%s", fmt.Sprintf("充值响应: Address=%s, Status=%d, TxHash=%s",
resp.Address, resp.Status, resp.TxHash), resp.Address, resp.Status, resp.TxHash),
@@ -256,7 +277,7 @@ func (r *RabbitMQServer) PublishTopupResp(resp message.TopupMsg_resp) error {
// PublishWithdrawResp 发布提现响应 // PublishWithdrawResp 发布提现响应
func (r *RabbitMQServer) PublishWithdrawResp(resp message.WithdrawMsg_resp) error { func (r *RabbitMQServer) PublishWithdrawResp(resp message.WithdrawMsg_resp) error {
return r.publishMessage( return r.publishMessage(
r.config.WithdrawRespConfig, r.config.WithdrawResp,
resp, resp,
fmt.Sprintf("提现响应: QueueId=%s, Status=%d, TxHash=%s", fmt.Sprintf("提现响应: QueueId=%s, Status=%d, TxHash=%s",
resp.QueueId, resp.Status, resp.TxHash), resp.QueueId, resp.Status, resp.TxHash),
@@ -266,15 +287,23 @@ func (r *RabbitMQServer) PublishWithdrawResp(resp message.WithdrawMsg_resp) erro
// PublishPayResp 发布支付响应 // PublishPayResp 发布支付响应
func (r *RabbitMQServer) PublishPayResp(resp message.PayMsg_resp) error { func (r *RabbitMQServer) PublishPayResp(resp message.PayMsg_resp) error {
return r.publishMessage( return r.publishMessage(
r.config.PayRespConfig, r.config.PayResp,
resp, resp,
fmt.Sprintf("支付响应: QueueId=%s, OrderId=%s, Status=%d, TxHash=%s", "支付响应",
resp.QueueId, resp.OrderId, resp.Status, resp.TxHash), )
}
// PublishRemoveResp 发布删除充值监听响应
func (r *RabbitMQServer) PublishRemoveResp(resp message.RemoveListenMsg_resp) error {
return r.publishMessage(
r.config.RemoveResp,
resp,
fmt.Sprintf("删除充值监听响应: Address=%s, Status=%d", resp.Address, resp.Status),
) )
} }
// publishMessage 通用消息发布方法 // publishMessage 通用消息发布方法
func (r *RabbitMQServer) publishMessage(config message.QueueConfig, msg interface{}, logMsg string) error { func (r *RabbitMQServer) publishMessage(config message.Queue, msg interface{}, logMsg string) error {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
@@ -296,10 +325,10 @@ func (r *RabbitMQServer) publishMessage(config message.QueueConfig, msg interfac
err = r.channel.PublishWithContext( err = r.channel.PublishWithContext(
ctx, ctx,
config.ExchangeName, // 交换机 config.Exchange, // 交换机
routingKey, // routing key routingKey, // routing key
false, // mandatory false, // mandatory
false, // immediate false, // immediate
amqp.Publishing{ amqp.Publishing{
ContentType: "application/json", ContentType: "application/json",
Body: body, Body: body,

View File

@@ -7,8 +7,9 @@ import (
"log" "log"
"m2pool-payment/internal/blockchain" "m2pool-payment/internal/blockchain"
"m2pool-payment/internal/blockchain/eth" "m2pool-payment/internal/blockchain/eth"
"m2pool-payment/internal/constant"
"m2pool-payment/internal/crypto" "m2pool-payment/internal/crypto"
"m2pool-payment/internal/db" "m2pool-payment/internal/listen"
"m2pool-payment/internal/logger" "m2pool-payment/internal/logger"
message "m2pool-payment/internal/msg" message "m2pool-payment/internal/msg"
rmq "m2pool-payment/internal/queue" rmq "m2pool-payment/internal/queue"
@@ -16,267 +17,95 @@ import (
"os/signal" "os/signal"
"strings" "strings"
"syscall" "syscall"
"time"
)
const MSG_KEY string = "9f3c7a12"
// 状态码常量
const (
STATUS_FAILED = 0 // 失败
STATUS_SUCCESS = 1 // 成功
STATUS_PENDING = 2 // 待确认
STATUS_VERIFY_FAILED = 3 // 验证失败
) )
type ServerCtx struct { type ServerCtx struct {
msgKey string msgKey string // 解密msg-sign的密钥启动命令参数传入
Config message.Config Config message.Config
blockChainServer *blockchain.BlockChainServer blockChainServer *blockchain.BlockChainServer
rmqServer *rmq.RabbitMQServer rmqServer *rmq.RabbitMQServer
sqlitedb db.SQLite messageServer *listen.ListenServer
} }
var s_ctx ServerCtx func loadConfig() message.Config {
// verifyMessage 验证消息签名
func verifyMessage(timestamp uint64, sign string) bool {
hash_byte := crypto.Sha256Hash(fmt.Sprintf("%x", timestamp) + MSG_KEY)
hash := hex.EncodeToString(hash_byte)
return hash == sign
}
func loadConfig(msgKey string) {
file, err := os.ReadFile("config.json") file, err := os.ReadFile("config.json")
if err != nil { if err != nil {
panic(fmt.Sprintf("读取配置文件失败: %v", err)) panic(fmt.Sprintf("读取配置文件失败: %v", err))
} }
var result message.Config
err = json.Unmarshal(file, &s_ctx.Config) err = json.Unmarshal(file, &result)
if err != nil { if err != nil {
panic(fmt.Sprintf("解析配置文件失败: %v", err)) panic(fmt.Sprintf("解析配置文件失败: %v", err))
} }
log.Printf("✅ 配置加载成功: RPC=%s, WS=%s", return result
s_ctx.Config.ETHConfig.RpcURL, s_ctx.Config.ETHConfig.WsURL)
s_ctx.msgKey = msgKey
} }
func initBlockChainServer() { func NewServer(msgKey string) *ServerCtx {
// 初始化节点服务 cfg := loadConfig()
l := listen.NewListenServer(cfg)
eth_node, err := eth.NewETHNode(cfg, msgKey, l)
if err != nil {
panic(err)
}
node_server := blockchain.NewBlockChainServer() node_server := blockchain.NewBlockChainServer()
// 初始化ETH节点
eth_node, err := eth.NewETHNode(s_ctx.Config.ETHConfig, "m2pool")
if err != nil {
log.Fatalf("ETH-Node Start error: %v", err)
}
// 注册ETH节点
node_server.RegisterChain("ETH", eth_node) node_server.RegisterChain("ETH", eth_node)
// 将所有注册的blockChainServer绑定至server rmq_server, err := rmq.NewRabbitMQServer(cfg.RmqConfig)
s_ctx.blockChainServer = node_server
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 { if err != nil {
return fmt.Errorf("query history topup-msg error: %w", err) panic(err)
} }
defer rows.Close() return &ServerCtx{
msgKey: msgKey,
var topupReq_msg message.TopupMsg_req Config: cfg,
hasData := false blockChainServer: node_server,
for rows.Next() { rmqServer: rmq_server,
hasData = true messageServer: l,
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 { // verifyMessage 验证消息签名
sql := `SELECT queueId, chain, symbol, timestamp, from_addr, to_addr, amount FROM msg_withdraw_req;` func (s *ServerCtx) verifyMessage(timestamp uint64, sign string) bool {
rows, err := s_ctx.sqlitedb.DB.Query(sql) hash_byte := crypto.Sha256Hash(fmt.Sprintf("%x", timestamp) + s.msgKey)
if err != nil { hash := hex.EncodeToString(hash_byte)
return fmt.Errorf("query history withdraw-msg error: %w", err) return hash == sign
}
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 { func (s *ServerCtx) handleTopupMsg() {
sql := `SELECT queueId, chain, symbol, timestamp, from_addr, to_addr, amount, orderId FROM msg_pay_req;` s.rmqServer.OnTopupMsg = func(msg message.TopupMsg_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)
if err != nil {
log.Fatalf("RabbitMQ Server Start error: %v", err)
}
// 将rmq服务绑定至server
s_ctx.rmqServer = rmq_server
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) msg.Address = strings.ToLower(msg.Address)
// 验证签名 // 验证签名
if !verifyMessage(msg.Timestamp, msg.Sign) { if !s.verifyMessage(msg.Timestamp, msg.Sign) {
err := s_ctx.rmqServer.PublishTopupResp(message.TopupMsg_resp{ err := s.rmqServer.PublishTopupResp(message.TopupMsg_resp{
QueueId: msg.QueueId,
Address: msg.Address, Address: msg.Address,
Status: STATUS_VERIFY_FAILED, Status: constant.STATUS_VERIFY_FAILED,
Chain: msg.Chain, Chain: msg.Chain,
Symbol: msg.Symbol, Symbol: msg.Symbol,
Amount: 0,
TxHash: "",
}) })
if err != nil { if err != nil {
log.Printf("❌ 发布充值失败响应失败: %v", err) log.Printf("❌ 发布充值失败响应失败: %v", err)
} }
return return
} }
// 记录 RMQ -> Listen 充值请求
// 添加监听地址 logger.LogRmqToListenTopupReq(msg.QueueId, msg.Chain, msg.Symbol, msg.Address, msg.Timestamp)
// go func() { s.messageServer.ChFromRmqServer <- msg
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)
}
} }
} }
func handleWithdrawMsg() { func (s *ServerCtx) handleWithdrawMsg() {
s_ctx.rmqServer.OnWithdrawMsg = func(msg message.WithdrawMsg_req) { s.rmqServer.OnWithdrawMsg = func(msg message.WithdrawMsg_req) {
msg.FromAddress = strings.ToLower(msg.FromAddress) msg.FromAddress = strings.ToLower(msg.FromAddress)
msg.ToAddress = strings.ToLower(msg.ToAddress) msg.ToAddress = strings.ToLower(msg.ToAddress)
// 验证签名 // 验证签名
if !verifyMessage(msg.Timestamp, msg.Sign) { if !s.verifyMessage(msg.Timestamp, msg.Sign) {
err := s_ctx.rmqServer.PublishWithdrawResp(message.WithdrawMsg_resp{ err := s.rmqServer.PublishWithdrawResp(message.WithdrawMsg_resp{
QueueId: msg.QueueId, QueueId: msg.QueueId,
Status: STATUS_VERIFY_FAILED, Chain: msg.Chain,
Chain: msg.Chain, Symbol: msg.Symbol,
Symbol: msg.Symbol, Status: constant.STATUS_VERIFY_FAILED,
Amount: 0, FromAddress: msg.FromAddress,
TxHash: "", ToAddress: msg.ToAddress,
}) })
if err != nil { if err != nil {
log.Printf("❌ 发布提现失败响应失败: %v", err) log.Printf("❌ 发布提现失败响应失败: %v", err)
@@ -284,62 +113,23 @@ func handleWithdrawMsg() {
return return
} }
// 执行转账 // 记录 RMQ -> Listen 提现请求
err := s_ctx.blockChainServer.Transfer(msg.Chain, msg.Symbol, msg) logger.LogRmqToListenWithdrawReq(msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.ToAddress, msg.Amount, msg.Fee, msg.Timestamp)
if err != nil { s.messageServer.ChFromRmqServer <- msg
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 // 转账失败时直接返回,不进入链上确认流程
}
// 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)
}
} }
} }
func handlePayMsg() { func (s *ServerCtx) handlePayMsg() {
s_ctx.rmqServer.OnPayMsg = func(msg message.PayMsg_req) { s.rmqServer.OnPayMsg = func(msg message.PayMsg_req) {
msg.FromAddress = strings.ToLower(msg.FromAddress) msg.FromAddress = strings.ToLower(msg.FromAddress)
msg.ToAddress = strings.ToLower(msg.ToAddress)
// 验证签名 // 验证签名
if !verifyMessage(msg.Timestamp, msg.Sign) { if !s.verifyMessage(msg.Timestamp, msg.Sign) {
err := s_ctx.rmqServer.PublishPayResp(message.PayMsg_resp{ err := s.rmqServer.PublishPayResp(message.PayMsg_resp{
QueueId: msg.QueueId, QueueId: msg.QueueId,
Status: STATUS_VERIFY_FAILED, FromAddress: msg.FromAddress,
Amount: msg.Amount, Status: constant.STATUS_VERIFY_FAILED,
Chain: msg.Chain,
Symbol: msg.Symbol,
OrderId: msg.OrderId,
TxHash: "",
}) })
if err != nil { if err != nil {
log.Printf("❌ 发布支付失败响应失败: %v", err) log.Printf("❌ 发布支付失败响应失败: %v", err)
@@ -347,176 +137,97 @@ func handlePayMsg() {
return return
} }
// 执行转账 // 记录 RMQ -> Listen 支付请求
err := s_ctx.blockChainServer.Transfer(msg.Chain, msg.Symbol, msg) logger.LogRmqToListenPayReq(msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.ToAddress, msg.Amount, msg.Fee, msg.Timestamp)
if err != nil { s.messageServer.ChFromRmqServer <- msg
log.Printf("❌ 支付转账失败: %v", err) }
// 发送失败响应 }
s_ctx.rmqServer.PublishPayResp(message.PayMsg_resp{
func (s *ServerCtx) handleRemoveMsg() {
s.rmqServer.OnRemoveMsg = func(msg message.RemoveListenMsg_req) {
msg.Address = strings.ToLower(msg.Address)
// 验证签名
if !s.verifyMessage(msg.Timestamp, msg.Sign) {
err := s.rmqServer.PublishRemoveResp(message.RemoveListenMsg_resp{
QueueId: msg.QueueId, QueueId: msg.QueueId,
Status: STATUS_FAILED, MsgType: msg.MsgType,
Amount: msg.Amount,
Chain: msg.Chain, Chain: msg.Chain,
Symbol: msg.Symbol, Symbol: msg.Symbol,
OrderId: msg.OrderId, Address: msg.Address,
TxHash: "", Status: constant.STATUS_VERIFY_FAILED,
})
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: "",
}) })
if err != nil {
log.Printf("❌ 发布移除监听失败响应失败: %v", err)
}
return return
} }
// }()
// 将新增数据写入sqlite // 记录 RMQ -> Listen 移除监听请求
insert_sql := `INSERT OR REPLACE INTO msg_pay_req (queueId, chain, symbol, timestamp, from_addr, to_addr, amount, orderId) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` logger.LogRmqToListenRemoveReq(msg.QueueId, msg.MsgType, msg.Chain, msg.Symbol, msg.Address, msg.Timestamp)
data := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.Timestamp, msg.FromAddress, msg.ToAddress, msg.Amount, msg.OrderId} s.messageServer.ChFromRmqServer <- msg
err = s_ctx.sqlitedb.Insert(insert_sql, data) }
if err != nil { }
log.Printf("❌ 插入 pay_req 失败: %v, data: %+v", err, data)
func (s *ServerCtx) handleRespMsg() {
for msg := range s.messageServer.ChToRmqServer {
switch v := msg.(type) {
case message.TopupMsg_resp:
log.Printf("📨[充值响应]QueueID=%s, Address=%s, Chain=%s, Symbol=%s, TxHash=%s, Status=%d, Amount=%f", v.QueueId, v.Address, v.Chain, v.Symbol, v.TxHash, v.Status, v.Amount)
// 记录 Listen -> RMQ 充值响应
logger.LogListenToRmqTopupResp(v.QueueId, v.Chain, v.Symbol, v.Address, v.FromAddress, v.TxHash, v.Amount, v.BlockHeight, v.Status)
err := s.rmqServer.PublishTopupResp(v)
if err != nil {
log.Printf("❌ 发送充值响应失败: %v", err)
return
}
case message.WithdrawMsg_resp:
log.Printf("📨[提现响应]QueueID=%s, Chain=%s, Symbol=%s, FromAddress=%s, ToAddress=%s, TxHash=%s, Status=%d, Amount=%f", v.QueueId, v.Chain, v.Symbol, v.FromAddress, v.ToAddress, v.TxHash, v.Status, v.Amount)
// 记录 Listen -> RMQ 提现响应
logger.LogListenToRmqWithdrawResp(v.QueueId, v.Chain, v.Symbol, v.FromAddress, v.ToAddress, v.TxHash, v.Amount, v.Fee, v.BlockHeight, v.Status)
err := s.rmqServer.PublishWithdrawResp(v)
if err != nil {
log.Printf("❌ 发送提现响应失败: %v", err)
return
}
case message.PayMsg_resp:
log.Printf("📨[提现响应]QueueID=%s, Chain=%s, Symbol=%s, FromAddress=%s, Status=%d", v.QueueId, v.Chain, v.Symbol, v.FromAddress, v.Status)
// 记录 Listen -> RMQ 支付响应
logger.LogListenToRmqPayResp(v.QueueId, v.Chain, v.Symbol, v.FromAddress, v.ToAddress, v.TxHash, v.Amount, v.Fee, v.BlockHeight, v.Status)
err := s.rmqServer.PublishPayResp(v)
if err != nil {
log.Printf("❌ 发送支付响应失败: %v", err)
return
}
case message.RemoveListenMsg_resp:
log.Printf("📨[充值响应]QueueID=%s, Address=%s, Chain=%s, Symbol=%s,Status=%d", v.QueueId, v.Address, v.Chain, v.Symbol, v.Status)
// 记录 Listen -> RMQ 移除监听响应
logger.LogListenToRmqRemoveResp(v.QueueId, v.MsgType, v.Chain, v.Symbol, v.Address, v.Status)
err := s.rmqServer.PublishRemoveResp(v)
if err != nil {
log.Printf("❌ 发送移除监听响应失败: %v", err)
return
}
default:
log.Printf("❌ 错误响应结构: %v", v)
return
} }
} }
} }
func initRmqListen() { func (s *ServerCtx) initDB() {
// ================== 设置 RabbitMQ 消息处理回调 ================== // msgDB_path := s.Config.MsgConfig.SqlitePath
// 先设置所有回调(同步执行,避免竞态) // ethDB_path := s.Config.ETHConfig.SqlitePath
handleTopupMsg() msg_sql_script_path := "../public/msg.sql"
handleWithdrawMsg() msg_sql_byte, err := os.ReadFile(msg_sql_script_path)
handlePayMsg() if err != nil {
log.Fatalf("open msg-sql file error: %v", err)
// 回调设置完成后,再启动 RabbitMQ 监听 return
if err := s_ctx.rmqServer.Start(); err != nil {
log.Fatalf("启动 RabbitMQ 监听失败: %v", err)
} }
log.Println("✅ RabbitMQ 监听启动完成")
}
func handleChainEvent(chainEventCh chan any) { err = s.messageServer.SqliteDB.CreateTable(string(msg_sql_byte))
for event := range chainEventCh { if err != nil {
// 添加 panic 恢复,防止单个消息处理错误导致整个 goroutine 退出 log.Fatalf("exec msg-sql error: %v", err)
func() { return
defer func() {
if r := recover(); r != nil {
log.Printf("❌ 处理链上事件 panic: %v, event: %+v", r, event)
}
}()
// 根据消息类型发送不同的响应
switch msg := event.(type) {
case message.TopupMsg_resp:
// 充值确认
if msg.Status == STATUS_PENDING {
log.Printf("📨 [链上] 充值待确认: Address=%s, Amount=%.2f, TxHash=%s",
msg.Address, msg.Amount, msg.TxHash)
// 记录交易日志:待确认
logger.LogTopup(msg.Address, "待确认", msg.Amount, msg.TxHash, msg.BlockHeight)
} else {
log.Printf("✅ [链上] 充值确认: Address=%s, Amount=%.2f, TxHash=%s, Status=%d",
msg.Address, msg.Amount, msg.TxHash, msg.Status)
// 记录交易日志:已确认
logger.LogTopup(msg.Address, "确认", msg.Amount, msg.TxHash, msg.BlockHeight)
}
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.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.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)
}
}()
} }
} }
@@ -525,35 +236,37 @@ func Start(msgKey string) {
log.Println("🚀 M2Pool Payment System Starting...") log.Println("🚀 M2Pool Payment System Starting...")
log.Println("========================================") log.Println("========================================")
// 加载配置 server := NewServer(msgKey)
loadConfig(msgKey) server.initDB()
// 启动消息处理
go server.handleTopupMsg()
go server.handleWithdrawMsg()
go server.handlePayMsg()
go server.handleRemoveMsg()
go server.handleRespMsg()
// 初始化交易日志系统 // 启动 RabbitMQ 服务
if err := logger.InitTransactionLogger("logs"); err != nil { if err := server.rmqServer.Start(); err != nil {
log.Fatalf("❌ 初始化交易日志系统失败: %v", err) log.Fatalf("❌ 启动 RabbitMQ 服务失败: %v", err)
} }
log.Println("✅ 交易日志系统初始化完成") log.Println("✅ RabbitMQ 服务启动成功")
// ================== 初始化区块链节点 ================== // 启动消息服务器监听
initBlockChainServer() go server.messageServer.RmqMsgIn()
go server.messageServer.NetMsgIn()
log.Println("✅ 消息服务器监听启动成功")
// ================== 初始化SQLite3 ================== // 启动区块链监听
initSQLite(s_ctx.Config.SQLite3.MsgPath) if err := server.blockChainServer.Listen("ETH", nil); err != nil {
// 读取历史信息 log.Fatalf("❌ 启动区块链监听失败: %v", err)
loadSQLiteData() }
log.Println("✅ 区块链监听启动成功")
// ================== 初始化 RabbitMQ 服务 ================== // 启动区块链消息监听
initRmqServer() if err := server.blockChainServer.ListenMsg("ETH"); err != nil {
log.Fatalf("❌ 启动区块链消息监听失败: %v", err)
// ================== 启动链上事件监听通道 ================== }
chainEventCh := make(chan any, 1000) // 增加缓冲区,避免高并发丢消息 log.Println("✅ 区块链消息监听启动成功")
go s_ctx.blockChainServer.Listen("ETH", "USDT", chainEventCh)
// ================== 启动 RabbitMQ 监听 ==================
initRmqListen()
// ================== 处理链上确认事件 ==================
go handleChainEvent(chainEventCh)
log.Println("========================================") log.Println("========================================")
log.Println("🎉 所有服务启动完成!") log.Println("🎉 所有服务启动完成!")
@@ -568,8 +281,8 @@ func Start(msgKey string) {
log.Println("🛑 收到退出信号,正在关闭服务...") log.Println("🛑 收到退出信号,正在关闭服务...")
log.Println("========================================") log.Println("========================================")
s_ctx.blockChainServer.Stop("ETH") server.blockChainServer.Stop("ETH")
s_ctx.rmqServer.Close() server.rmqServer.Close()
logger.CloseTransactionLogger() logger.CloseTransactionLogger()
log.Println("👋 服务已全部关闭") log.Println("👋 服务已全部关闭")

View File

@@ -6,6 +6,14 @@ import (
"math/big" "math/big"
) )
func BigIntETHToFloat64(value *big.Int) float64 {
f := new(big.Float).SetInt(value)
scale := new(big.Float).SetFloat64(1e18) // USDT 精度 6 位
f.Quo(f, scale)
result, _ := f.Float64()
return result
}
func BigIntUSDTToFloat64(value *big.Int) float64 { func BigIntUSDTToFloat64(value *big.Int) float64 {
f := new(big.Float).SetInt(value) f := new(big.Float).SetInt(value)
scale := new(big.Float).SetFloat64(1e6) // USDT 精度 6 位 scale := new(big.Float).SetFloat64(1e6) // USDT 精度 6 位
@@ -26,6 +34,16 @@ func Float64ToBigIntUSDT(amount float64) *big.Int {
return bigAmount return bigAmount
} }
const ETHDecimals = 18
func Float64ToBigIntETH(amount float64) *big.Int {
// 乘上精度系数
scale := math.Pow10(ETHDecimals)
bigAmount := new(big.Int)
bigAmount.SetInt64(int64(amount * scale))
return bigAmount
}
func Slice_delete(arr []any, index int) []any { func Slice_delete(arr []any, index int) []any {
if index < 0 || index >= len(arr) { if index < 0 || index >= len(arr) {
// 处理越界 // 处理越界
@@ -38,3 +56,61 @@ func Slice_delete(arr []any, index int) []any {
} }
return arr return arr
} }
// 函数:检查一个切片是否包含某个字符串
func Contains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
func Float64ToBigInt(symbol string, amount float64) *big.Int {
var scale float64
switch symbol {
case "ETH":
scale = math.Pow10(ETHDecimals)
case "USDT":
scale = math.Pow10(USDTDecimals)
default:
log.Printf("don`t support symbol: %s", symbol)
return nil
}
// 检查 amount 是否为负数
if amount < 0 {
log.Printf("Warning: negative amount for symbol %s: %f", symbol, amount)
amount = 0 // 可以选择将负数设置为 0 或返回错误
}
// 将 amount 转换为 big.Float 来避免溢出
bigAmount := new(big.Float)
bigAmount.SetFloat64(amount)
// 乘以 scale小数位数避免精度丢失
bigAmount = bigAmount.Mul(bigAmount, big.NewFloat(scale))
// 将 big.Float 转换为 big.Int取整
intAmount, _ := bigAmount.Int(nil)
return intAmount
}
func BigIntToFloat64(symbol string, amount *big.Int) float64 {
var scale *big.Float
f := new(big.Float).SetInt(amount)
switch symbol {
case "ETH":
scale = new(big.Float).SetFloat64(1e18)
case "USDT":
scale = new(big.Float).SetFloat64(1e6)
default:
log.Printf("don`t support symbol: %s", symbol)
return 0
}
f.Quo(f, scale)
result, _ := f.Float64()
return result
}

View File

@@ -1,71 +0,0 @@
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
);

45
public/eth.sql Normal file
View File

@@ -0,0 +1,45 @@
CREATE TABLE IF NOT EXISTS ETH_wallets (
address TEXT PRIMARY KEY NOT NULL,
queue_id TEXT NOT NULL,
timestamp INTEGER NOT NULL,
sign TEXT NOT NULL,
status INTEGER DEFAULT 0 -- 0未在监听 1正在监听
);
CREATE TABLE IF NOT EXISTS ETH_balances (
address TEXT NOT NULL,
symbol TEXT DEFAULT "ETH",
used_gas TEXT DEFAULT "0",
balance TEXT DEFAULT "0",
success_tx_hash TEXT DEFAULT NULL, --使,
failed_tx_hash TEXT DEFAULT NULL, --使,
PRIMARY KEY (address), -- 钱包地址唯一
FOREIGN KEY (address) REFERENCES eth_wallets (address) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS USDT_balances (
address TEXT NOT NULL,
symbol TEXT DEFAULT "USDT",
freeze_num TEXT DEFAULT "0",
balance TEXT DEFAULT "0",
success_tx_hash TEXT DEFAULT NULL, --使,
failed_tx_hash TEXT DEFAULT NULL, --使,
PRIMARY KEY (address), -- 钱包地址唯一
FOREIGN KEY (address) REFERENCES eth_wallets (address) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS eth_unconfirmed_tx (
queue_id TEXT NOT NULL, -- 关联的msg queue_id
tx_type INTEGER NOT NULL, --012
chain TEXT DEFALUT "ETH",
symbol TEXT,
from_addr TEXT,
to_addr TEXT,
tx_hash TEXT,
height INTEGER,
amount TEXT,
status INTEGER DEFAULT 2, --012
PRIMARY KEY (tx_hash),
FOREIGN KEY (queue_id) REFERENCES eth_wallets (queue_id) ON DELETE CASCADE
);

45
public/eth_mysql.sql Normal file
View File

@@ -0,0 +1,45 @@
CREATE TABLE IF NOT EXISTS ETH_wallets (
address VARCHAR(255) PRIMARY KEY NOT NULL,
queue_id VARCHAR(255) NOT NULL,
timestamp BIGINT NOT NULL,
sign VARCHAR(255) NOT NULL,
status TINYINT DEFAULT 0
);
CREATE INDEX idx_queue_id ON ETH_wallets (queue_id);
CREATE TABLE IF NOT EXISTS ETH_balances (
address VARCHAR(255) NOT NULL,
symbol VARCHAR(32) DEFAULT "ETH",
used_gas DECIMAL(40,16) DEFAULT 0,
balance DECIMAL(40,16) DEFAULT 0,
success_tx_hash TEXT DEFAULT NULL,
failed_tx_hash TEXT DEFAULT NULL,
PRIMARY KEY (address),
FOREIGN KEY (address) REFERENCES ETH_wallets (address) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS USDT_balances (
address VARCHAR(255) NOT NULL,
symbol VARCHAR(32) DEFAULT "USDT",
freeze_num DECIMAL(40,16) DEFAULT 0,
balance DECIMAL(40,16) DEFAULT 0,
success_tx_hash TEXT DEFAULT NULL,
failed_tx_hash TEXT DEFAULT NULL,
PRIMARY KEY (address),
FOREIGN KEY (address) REFERENCES ETH_wallets (address) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS ETH_unconfirmed_tx (
queue_id VARCHAR(255) NOT NULL,
tx_type TINYINT NOT NULL,
chain VARCHAR(32) DEFAULT "ETH",
symbol VARCHAR(32),
from_addr VARCHAR(255),
to_addr VARCHAR(255),
tx_hash VARCHAR(255),
height BIGINT,
amount DECIMAL(40,16),
status TINYINT DEFAULT 2,
FOREIGN KEY (queue_id) REFERENCES ETH_wallets (queue_id) ON DELETE CASCADE
);

3
public/infura Normal file
View File

@@ -0,0 +1,3 @@
API-KEY=431dbd08d89c406fb9f2b5ee812f5faf
HTTPS=https://mainnet.infura.io/v3/431dbd08d89c406fb9f2b5ee812f5faf
mnemonic=income miracle spin apple live cigar trim social age push eye chase

113
public/msg.sql Normal file
View File

@@ -0,0 +1,113 @@
CREATE TABLE IF NOT EXISTS topup_req_msg (
queue_id TEXT NOT NULL,
chain TEXT NOT NULL,
symbol TEXT NOT NULL,
address TEXT NOT NULL,
timestamp INTEGER,
sign TEXT,
status INTEGER DEFAULT 0, -- 0未在监听1正在监听
PRIMARY KEY (queue_id)
);
CREATE TABLE IF NOT EXISTS topup_resp_msg (
queue_id TEXT NOT NULL,
chain TEXT NOT NULL,
symbol TEXT NOT NULL,
from_addr TEXT DEFAULT NULL,
to_addr TEXT NOT NULL,
amount TEXT NOT NULL, -- 改为 TEXT 类型
tx_hash TEXT DEFAULT NULL,
height INTEGER DEFAULT NULL,
status INTEGER DEFAULT 5,
FOREIGN KEY (queue_id) REFERENCES topup_req_msg(queue_id)
);
CREATE TABLE IF NOT EXISTS withdraw_req_msg (
queue_id TEXT NOT NULL,
chain TEXT NOT NULL,
symbol TEXT NOT NULL,
from_addr TEXT NOT NULL,
to_addr TEXT NOT NULL,
amount TEXT NOT NULL, -- 改为 TEXT 类型
fee TEXT NOT NULL, -- 改为 TEXT 类型
timestamp INTEGER,
sign TEXT,
status INTEGER DEFAULT 5,
PRIMARY KEY (queue_id)
);
CREATE TABLE IF NOT EXISTS withdraw_resp_msg (
queue_id TEXT NOT NULL,
chain TEXT NOT NULL,
symbol TEXT NOT NULL,
from_addr TEXT NOT NULL,
to_addr TEXT NOT NULL,
tx_hash TEXT DEFAULT NULL,
amount TEXT DEFAULT NULL, -- 改为 TEXT 类型
fee TEXT DEFAULT NULL, -- 改为 TEXT 类型
height INTEGER DEFAULT NULL,
status INTEGER DEFAULT 5,
FOREIGN KEY (queue_id) REFERENCES withdraw_req_msg(queue_id)
);
CREATE TABLE IF NOT EXISTS pay_req_msg (
queue_id TEXT NOT NULL,
chain TEXT NOT NULL,
symbol TEXT NOT NULL,
from_addr TEXT NOT NULL,
to_addr TEXT NOT NULL,
amount TEXT NOT NULL, -- 改为 TEXT 类型
fee TEXT NOT NULL, -- 改为 TEXT 类型
timestamp INTEGER,
sign TEXT,
status INTEGER DEFAULT 5,
PRIMARY KEY (queue_id)
);
CREATE TABLE IF NOT EXISTS pay_resp_msg (
queue_id TEXT NOT NULL,
chain TEXT NOT NULL,
symbol TEXT NOT NULL,
from_addr TEXT NOT NULL,
to_addr TEXT NOT NULL,
tx_hash TEXT DEFAULT NULL,
amount TEXT DEFAULT NULL, -- 改为 TEXT 类型
fee TEXT DEFAULT NULL, -- 改为 TEXT 类型
height INTEGER DEFAULT NULL,
status INTEGER DEFAULT 5,
FOREIGN KEY (queue_id) REFERENCES pay_req_msg(queue_id)
);
-- pay_msg的交易
-- CREATE TABLE IF NOT EXISTS pay_msg_tx (
-- queue_id TEXT NOT NULL,
-- tx_hash TEXT DEFAULT NULL,
-- to_addr TEXT NOT NULL,
-- amount TEXT DEFAULT NULL, -- 改为 TEXT 类型
-- fee TEXT, -- 改为 TEXT 类型
-- height INTEGER DEFAULT 0,
-- status INTEGER DEFAULT 5,
-- FOREIGN KEY (queue_id) REFERENCES pay_req_msg(queue_id)
-- );
CREATE TABLE IF NOT EXISTS remove_req_msg(
queue_id TEXT NOT NULL,
msg_type INTEGER NOT NULL,
chain TEXT NOT NULL,
symbol TEXT NOT NULL,
address TEXT NOT NULL,
timestamp INTEGER,
sign TEXT,
status INTEGER DEFAULT 2,
PRIMARY KEY (queue_id)
);
CREATE TABLE IF NOT EXISTS remove_resp_msg(
queue_id TEXT NOT NULL,
msg_type INTEGER NOT NULL,
chain TEXT NOT NULL,
symbol TEXT NOT NULL,
address TEXT NOT NULL,
status INTEGER DEFAULT 2,
FOREIGN KEY (queue_id) REFERENCES remove_req_msg(queue_id)
);

101
public/msg_mysql.sql Normal file
View File

@@ -0,0 +1,101 @@
CREATE TABLE IF NOT EXISTS topup_req_msg (
queue_id VARCHAR(255) NOT NULL,
chain VARCHAR(32) NOT NULL,
symbol VARCHAR(32) NOT NULL,
address VARCHAR(255) NOT NULL,
timestamp BIGINT,
sign VARCHAR(255),
status TINYINT DEFAULT 1,
PRIMARY KEY (queue_id)
);
CREATE TABLE IF NOT EXISTS topup_resp_msg (
queue_id VARCHAR(255) NOT NULL,
chain VARCHAR(32) NOT NULL,
symbol VARCHAR(32) NOT NULL,
from_addr VARCHAR(255) NOT NULL,
to_addr VARCHAR(255) NOT NULL,
amount DECIMAL(30,16) NOT NULL,
tx_hash VARCHAR(255) DEFAULT NULL,
height BIGINT DEFAULT NULL,
status TINYINT DEFAULT 5,
FOREIGN KEY (queue_id) REFERENCES topup_req_msg(queue_id)
);
CREATE TABLE IF NOT EXISTS withdraw_req_msg (
queue_id VARCHAR(255) NOT NULL,
chain VARCHAR(32) NOT NULL,
symbol VARCHAR(32) NOT NULL,
from_addr VARCHAR(255) NOT NULL,
to_addr VARCHAR(255) NOT NULL,
amount DECIMAL(30,16) NOT NULL,
fee DECIMAL(30,16) NOT NULL,
timestamp BIGINT,
sign VARCHAR(255),
status TINYINT DEFAULT 5,
PRIMARY KEY (queue_id)
);
CREATE TABLE IF NOT EXISTS withdraw_resp_msg (
queue_id VARCHAR(255) NOT NULL,
chain VARCHAR(32) NOT NULL,
symbol VARCHAR(32) NOT NULL,
from_addr VARCHAR(255) NOT NULL,
to_addr VARCHAR(255) NOT NULL,
tx_hash VARCHAR(255) DEFAULT NULL,
amount DECIMAL(30,16) DEFAULT NULL,
fee DECIMAL(30,16) DEFAULT NULL,
height BIGINT DEFAULT NULL,
status TINYINT DEFAULT 5,
FOREIGN KEY (queue_id) REFERENCES withdraw_req_msg(queue_id)
);
CREATE TABLE IF NOT EXISTS pay_req_msg (
queue_id VARCHAR(255) NOT NULL,
chain VARCHAR(32) NOT NULL,
symbol VARCHAR(32) NOT NULL,
from_addr VARCHAR(255) NOT NULL,
to_addr VARCHAR(255) NOT NULL,
amount DECIMAL(30,16) NOT NULL,
fee DECIMAL(30,16) NOT NULL,
timestamp BIGINT,
sign VARCHAR(255),
status TINYINT DEFAULT 5,
PRIMARY KEY (queue_id)
);
CREATE TABLE IF NOT EXISTS pay_resp_msg (
queue_id VARCHAR(255) NOT NULL,
chain VARCHAR(32) NOT NULL,
symbol VARCHAR(32) NOT NULL,
from_addr VARCHAR(255) NOT NULL,
to_addr VARCHAR(255) NOT NULL,
tx_hash VARCHAR(255) DEFAULT NULL,
amount DECIMAL(30,16) DEFAULT NULL,
fee DECIMAL(30,16) DEFAULT NULL,
height BIGINT DEFAULT NULL,
status TINYINT DEFAULT 5,
FOREIGN KEY (queue_id) REFERENCES pay_req_msg(queue_id)
);
CREATE TABLE IF NOT EXISTS remove_req_msg(
queue_id VARCHAR(255) NOT NULL,
msg_type TINYINT NOT NULL,
chain VARCHAR(32) NOT NULL,
symbol VARCHAR(32) NOT NULL,
address VARCHAR(255) NOT NULL,
timestamp BIGINT,
sign VARCHAR(255),
status TINYINT DEFAULT 2,
PRIMARY KEY (queue_id)
);
CREATE TABLE IF NOT EXISTS remove_resp_msg(
queue_id VARCHAR(255) NOT NULL,
msg_type TINYINT NOT NULL,
chain VARCHAR(32) NOT NULL,
symbol VARCHAR(32) NOT NULL,
address VARCHAR(255) NOT NULL,
status TINYINT DEFAULT 2,
FOREIGN KEY (queue_id) REFERENCES remove_req_msg(queue_id)
);

22
流程.txt Normal file
View File

@@ -0,0 +1,22 @@
充值rmq -> listen -> 写数据库
-> 添加到TopupMsgs
-> 传给对应的node server -> node server接收 -> 添加到钱包
-> 记录到钱包数据库
提现/支付rmq -> listen -> 写数据库
-> 添加到WithdrawMsgs
-> 传给对应的node server -> node server接收 -> 记录到数据库
-> 开始转账 -> 校验余额 -> 转账 -> 转账结果返回 -> listen -> 修改相关状态
node server listen -> 新区块产生时读取当前listen server的消息 -> 对比消息和区块中的交易 -> 消息中的to = 区块交易中的to(充值) -> 返回消息 -> listen -> 修改数据库
-> 记录到unconfirmtxs -> 返回rmq -> 发送消息
-> 消息中的from、to、amount = 区块交易中的from、to、amount(提现/充值) -> 返回消息 -> listen -> 修改数据库状态
node server confirm -> 新区块产生同时会读取当前unconfirmtxs数据 -> 对比每个交易的高度 -> 符合确认条件 -> 修改钱包数据
-> 返回消息 -> listen -> 修改相关数据 -> 返回rmq -> 发出消息
确认后的状态修改余额增减、hash录入等