first commit
This commit is contained in:
75
internal/blockchain/blockchain.go
Normal file
75
internal/blockchain/blockchain.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package blockchain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type IChainServer interface {
|
||||
AddAddress(address string, msg any)
|
||||
RemoveAddress(address string)
|
||||
Listen(symbol string, ch chan any)
|
||||
Transfer(symbol string, msg any) error
|
||||
Stop()
|
||||
}
|
||||
|
||||
type BlockChainServer struct {
|
||||
mu sync.Mutex
|
||||
chains map[string]IChainServer // "ETH", "TRON", "BTC"
|
||||
}
|
||||
|
||||
func NewBlockChainServer() *BlockChainServer {
|
||||
return &BlockChainServer{
|
||||
chains: make(map[string]IChainServer),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BlockChainServer) RegisterChain(name string, chain IChainServer) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.chains[name] = chain
|
||||
}
|
||||
|
||||
func (b *BlockChainServer) AddAddress(chain, address string, msg any) {
|
||||
if srv, ok := b.chains[chain]; ok {
|
||||
srv.AddAddress(address, msg)
|
||||
fmt.Printf("✅ 添加监听地址: chain=%s, address=%s\n", chain, address)
|
||||
} else {
|
||||
fmt.Printf("⚠️ 链未注册: %s\n", chain)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BlockChainServer) RemoveAddress(chain, address string) {
|
||||
if srv, ok := b.chains[chain]; ok {
|
||||
srv.RemoveAddress(address)
|
||||
fmt.Printf("🗑️ 移除监听地址: chain=%s, address=%s\n", chain, address)
|
||||
} else {
|
||||
fmt.Printf("⚠️ 链未注册: %s\n", chain)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BlockChainServer) Listen(chain, symbol string, ch chan any) error {
|
||||
if srv, ok := b.chains[chain]; ok {
|
||||
go func() {
|
||||
srv.Listen(symbol, ch)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("链未注册: %s", chain)
|
||||
}
|
||||
|
||||
func (b *BlockChainServer) Transfer(chain, symbol string, msg any) error {
|
||||
if srv, ok := b.chains[chain]; ok {
|
||||
fmt.Printf("💸 %s-%s发起转账: %+v\n", chain, symbol, msg)
|
||||
return srv.Transfer(symbol, msg)
|
||||
}
|
||||
return fmt.Errorf("链未注册: %s", chain)
|
||||
}
|
||||
|
||||
func (b *BlockChainServer) Stop(chain string) {
|
||||
if srv, ok := b.chains[chain]; ok {
|
||||
fmt.Printf("💸 关闭服务: %+v\n", chain)
|
||||
srv.Stop()
|
||||
}
|
||||
fmt.Printf("链未注册: %s", chain)
|
||||
}
|
||||
690
internal/blockchain/eth/eth.go
Normal file
690
internal/blockchain/eth/eth.go
Normal file
@@ -0,0 +1,690 @@
|
||||
package eth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"m2pool-payment/internal/db"
|
||||
message "m2pool-payment/internal/msg"
|
||||
"m2pool-payment/internal/utils"
|
||||
"math/big"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"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 {
|
||||
decodeKey string // 私钥解密密钥,从程序启动命令行获得
|
||||
NetId *big.Int // 网络ID,主网为1,其他ID可通过rpc.NetworkID方法获取
|
||||
Config message.ETHConfig // 配置文件
|
||||
WsClient *ethclient.Client
|
||||
RpcClient *ethclient.Client
|
||||
Db db.MySQLPool
|
||||
mu sync.Mutex
|
||||
ListenAddresses sync.Map // key:"钱包地址", value:bool
|
||||
UnConfirmTxs map[string]message.Tx_msg // 待交易地址池,key:"交易hash", value: message.Tx
|
||||
USDT *USDT // ETH相关
|
||||
RmqMsgs map[string][]any // 根据地址查找出该地址涉及的消息,消息需要断言(topupreq_msg, payreq_msg, withdrawreq_msg)
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type USDT struct {
|
||||
Address common.Address // USDT合约地址
|
||||
ABI abi.ABI // USDT ABI
|
||||
TransferSig common.Hash // USDT函数签名
|
||||
LogsChan chan types.Log
|
||||
}
|
||||
|
||||
func NewETHNode(cfg message.ETHConfig, decodeKey string) (*ETHNode, error) {
|
||||
// 连入ETH节点的ws
|
||||
ws_client, err := ethclient.Dial(cfg.WsURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Ethereum node: %w", err)
|
||||
}
|
||||
// 连入ETH节点的rpc
|
||||
rpc_client, err := ethclient.Dial(cfg.RpcURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Ethereum node rpc: %w", err)
|
||||
}
|
||||
// 创建可取消的 context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
// 获得net_id
|
||||
netId, err := rpc_client.NetworkID(ctx)
|
||||
if err != nil {
|
||||
cancel()
|
||||
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) // 初始化合约日志通道
|
||||
|
||||
// 初始化数据库
|
||||
dbConn, err := db.NewMySQLPool(cfg.DbConfig)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("mysql connect error: %w", err)
|
||||
}
|
||||
|
||||
return ÐNode{
|
||||
decodeKey: decodeKey,
|
||||
NetId: netId,
|
||||
Config: cfg,
|
||||
WsClient: ws_client,
|
||||
RpcClient: rpc_client,
|
||||
Db: *dbConn,
|
||||
ListenAddresses: sync.Map{},
|
||||
UnConfirmTxs: make(map[string]message.Tx_msg),
|
||||
USDT: usdt,
|
||||
RmqMsgs: make(map[string][]any),
|
||||
Ctx: ctx,
|
||||
Cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ============================ 抽象接口 ============================
|
||||
func (e *ETHNode) AddAddress(address string, rmq_msg any) {
|
||||
// 统一转换为小写
|
||||
address = strings.ToLower(address)
|
||||
log.Printf("新增钱包监听消息:%v", rmq_msg)
|
||||
e.ListenAddresses.Store(address, true)
|
||||
e.mu.Lock()
|
||||
if len(e.RmqMsgs[address]) == 0 {
|
||||
e.RmqMsgs[address] = []any{rmq_msg}
|
||||
} else {
|
||||
e.RmqMsgs[address] = append(e.RmqMsgs[address], rmq_msg)
|
||||
}
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
func (e *ETHNode) RemoveAddress(address string) {
|
||||
// 统一转换为小写
|
||||
address = strings.ToLower(address)
|
||||
e.ListenAddresses.Delete(address)
|
||||
e.mu.Lock()
|
||||
delete(e.RmqMsgs, address)
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
func (e *ETHNode) Listen(symbol string, ch chan any) {
|
||||
// 启动新区块监听(用于触发交易确认检查)
|
||||
go e.listenNewBlocks("USDT", ch)
|
||||
switch symbol {
|
||||
case "USDT":
|
||||
// 启动 USDT Transfer 事件监听
|
||||
err := e.listen_usdt(ch)
|
||||
if err != nil {
|
||||
log.Fatal("Listen USDT Transactions Error:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ETHNode) Transfer(symbol string, msg any) error {
|
||||
switch symbol {
|
||||
case "USDT":
|
||||
err := e.usdt_transfer(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s transfer ERROR: %w", symbol, err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported symbol: %s", symbol)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================ rpc节点方法 ============================
|
||||
|
||||
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) getBlockHeight() (uint64, error) {
|
||||
header, err := e.RpcClient.HeaderByNumber(e.Ctx, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get latest block header: %w", err)
|
||||
}
|
||||
return header.Number.Uint64(), nil
|
||||
}
|
||||
|
||||
func (e *ETHNode) 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)
|
||||
}
|
||||
return gasPrice, 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,
|
||||
}
|
||||
// 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,
|
||||
}
|
||||
// 充值消息不删除,可能会有多笔充值到同一地址
|
||||
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,
|
||||
}
|
||||
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,
|
||||
}
|
||||
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 // 实际使用时替换成具体的解密代码
|
||||
// fmt.Println(privateKey)
|
||||
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) // 存入该笔msg(AddAddress内部会转小写)
|
||||
// 统一转换为小写
|
||||
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) // 存入该笔msg(AddAddress内部会转小写)
|
||||
// 统一转换为小写
|
||||
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")
|
||||
}
|
||||
fmt.Println(originalKey)
|
||||
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)
|
||||
}
|
||||
gasPrice, err := e.getSuggestGasPrice() // 获得当前建议gasPrice
|
||||
if err != nil {
|
||||
return fmt.Errorf("get suggest-gasprice error:%v", err)
|
||||
}
|
||||
eth_balance, err := e.getETHBlance(final_from) // 获得钱包eth余额
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
var gasLimit uint64 = 100000
|
||||
gasLimit_b := new(big.Int).SetUint64(gasLimit)
|
||||
gas := new(big.Int).Mul(gasLimit_b, gasPrice)
|
||||
// 判断钱包eth是否支持本次交易gas费用
|
||||
if eth_balance.Cmp(gas) == -1 {
|
||||
return fmt.Errorf("address=%s balance less than gas=%v(wei)", final_from, eth_balance)
|
||||
}
|
||||
// 构造发送到 USDT 合约地址的交易
|
||||
tx := types.NewTransaction(
|
||||
nonce,
|
||||
e.USDT.Address, // 发送到USDT合约地址
|
||||
big.NewInt(0), // value为0(ERC20转账不需要ETH)
|
||||
gasLimit, // GasLimit设置为100000(ERC20转账需要更多gas)
|
||||
gasPrice, // GasPrice: 20 Gwei
|
||||
data, // 附加数据:transfer方法调用
|
||||
)
|
||||
// 6, 签名交易并获得txHash
|
||||
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(e.NetId), privateKey)
|
||||
txHash := signedTx.Hash().Hex() // 通过签名信息解析出交易hash
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign transaction: %w", err)
|
||||
}
|
||||
// 7, 发送交易
|
||||
err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send transaction: %w", err)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
func (e *ETHNode) Stop() {
|
||||
e.Cancel()
|
||||
}
|
||||
12
internal/crypto/crypto.go
Normal file
12
internal/crypto/crypto.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func Sha256Hash(key string) []byte {
|
||||
bytes, _ := hex.DecodeString(key)
|
||||
sha256_hash := sha256.Sum256(bytes)
|
||||
return sha256_hash[:]
|
||||
}
|
||||
70
internal/db/db.go
Normal file
70
internal/db/db.go
Normal file
@@ -0,0 +1,70 @@
|
||||
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()
|
||||
}
|
||||
125
internal/msg/msg.go
Normal file
125
internal/msg/msg.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package msg
|
||||
|
||||
import "time"
|
||||
|
||||
// 配置文件结构
|
||||
type Config struct {
|
||||
RMQConfig RMQConfig `json:"rmq_config"`
|
||||
ETHConfig ETHConfig `json:"eth_config"`
|
||||
}
|
||||
|
||||
type RMQConfig struct {
|
||||
SubAddr string `json:"sub_addr"` // 监听地址
|
||||
PayConfig QueueConfig `json:"pay"` // 支付
|
||||
TopUpConfig QueueConfig `json:"topup"` // 充值
|
||||
WithdrawConfig QueueConfig `json:"withdraw"` // 提现
|
||||
PayRespConfig QueueConfig `json:"pay_resp"` // 支付回复
|
||||
TopUpRespConfig QueueConfig `json:"topup_resp"` // 充值回复
|
||||
WithdrawRespConfig QueueConfig `json:"withdraw_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"`
|
||||
}
|
||||
|
||||
// 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"` // 连接最大存活时间
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// 接收的充值消息
|
||||
type TopupMsg_req struct {
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
Address string `json:"address"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
// 返回充值结果消息
|
||||
type TopupMsg_resp struct {
|
||||
Address string `json:"address"`
|
||||
Status int `json:"status"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
Amount float64 `json:"amount"`
|
||||
TxHash string `json:"tx_hash"`
|
||||
}
|
||||
|
||||
// 接收的提现消息
|
||||
type WithdrawMsg_req struct {
|
||||
QueueId string `json:"queue_id"`
|
||||
FromAddress string `json:"from_address"` // 我们提供的地址
|
||||
ToAddress string `json:"to_address"` // 用户要提现到的地址
|
||||
Amount float64 `json:"amount"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
// 返回提现结果消息
|
||||
type WithdrawMsg_resp struct {
|
||||
QueueId string `json:"queue_id"`
|
||||
Status int `json:"status"`
|
||||
Amount float64 `json:"amount"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
TxHash string `json:"tx_hash"`
|
||||
}
|
||||
|
||||
// 接收到的支付消息
|
||||
type PayMsg_req struct {
|
||||
QueueId string `json:"queue_id"`
|
||||
FromAddress string `json:"from_address"` // 我们提供的地址
|
||||
ToAddress string `json:"to_address"` // 卖家地址
|
||||
Amount float64 `json:"amount"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
OrderId string `json:"order_id"` // 订单号
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
// 返回支付结果消息
|
||||
type PayMsg_resp struct {
|
||||
QueueId string `json:"queue_id"`
|
||||
Status int `json:"status"`
|
||||
Amount float64 `json:"amount"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
OrderId string `json:"order_id"` // 订单号
|
||||
TxHash string `json:"tx_hash"`
|
||||
}
|
||||
|
||||
// 节点通用消息结构
|
||||
type Tx_msg struct {
|
||||
TxType int `json:"tx_type"` // 转账类型:0充值,1提现,2支付
|
||||
Tx Tx `json:"tx"`
|
||||
}
|
||||
type Tx struct {
|
||||
From string `json:"from"` // 充值/提现/支付的来源地址
|
||||
To string `json:"to"` // 充值/提现/支付的目标地址
|
||||
Height uint64 `json:"height"` // 区块高度
|
||||
TxHash string `json:"tx_hash"` // 交易哈希
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
Value float64 `json:"value"` // 数量,单位是币
|
||||
Status int `json:"status"` // 交易状态,1成功,0失败, 2待确认
|
||||
}
|
||||
363
internal/queue/README.md
Normal file
363
internal/queue/README.md
Normal file
@@ -0,0 +1,363 @@
|
||||
# RabbitMQ 服务使用说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
这个 RabbitMQ 服务实现了区块链支付系统的消息队列功能,包括:
|
||||
|
||||
### 消费队列(监听)
|
||||
1. **充值队列 (topup)** - 监听用户充值请求
|
||||
2. **提现队列 (withdraw)** - 监听用户提现请求
|
||||
3. **支付队列 (pay)** - 监听用户支付请求
|
||||
|
||||
### 发布队列(响应)
|
||||
1. **充值响应队列 (topup_resp)** - 发送充值确认结果
|
||||
2. **提现响应队列 (withdraw_resp)** - 发送提现确认结果
|
||||
3. **支付响应队列 (pay_resp)** - 发送支付确认结果
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
go get github.com/rabbitmq/amqp091-go
|
||||
```
|
||||
|
||||
### 2. 配置文件示例 (config.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"rmq_config": {
|
||||
"sub_addr": "amqp://username:password@localhost:5672",
|
||||
"pay": {
|
||||
"queue": "pay.auto.queue",
|
||||
"exchange": "pay.exchange",
|
||||
"routing": ["pay.auto.routing.key"]
|
||||
},
|
||||
"topup": {
|
||||
"queue": "pay.recharge.queue",
|
||||
"exchange": "pay.exchange",
|
||||
"routing": ["pay.recharge.routing.key"]
|
||||
},
|
||||
"withdraw": {
|
||||
"queue": "pay.withdraw.queue",
|
||||
"exchange": "pay.exchange",
|
||||
"routing": ["pay.withdraw.routing.key"]
|
||||
},
|
||||
"pay_resp": {
|
||||
"queue": "pay.auto.return.queue",
|
||||
"exchange": "pay.exchange",
|
||||
"routing": ["pay.auto.return.routing.key"]
|
||||
},
|
||||
"topup_resp": {
|
||||
"queue": "pay.recharge.return.queue",
|
||||
"exchange": "pay.exchange",
|
||||
"routing": ["pay.recharge.return.routing.key"]
|
||||
},
|
||||
"withdraw_resp": {
|
||||
"queue": "pay.withdraw.return.queue",
|
||||
"exchange": "pay.exchange",
|
||||
"routing": ["pay.withdraw.return.routing.key"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用示例
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
message "m2pool-payment/internal/msg"
|
||||
rmq "m2pool-payment/internal/queue"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
config := loadConfig() // 你的配置加载逻辑
|
||||
|
||||
// 创建 RabbitMQ 服务
|
||||
rmqServer, err := rmq.NewRabbitMQServer(config.RMQConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("创建 RabbitMQ 服务失败: %v", err)
|
||||
}
|
||||
defer rmqServer.Close()
|
||||
|
||||
// 设置消息处理回调
|
||||
rmqServer.OnTopupMsg = func(msg message.TopupMsg_req) {
|
||||
log.Printf("收到充值请求: %+v", msg)
|
||||
// 处理充值逻辑...
|
||||
// 添加地址监听等
|
||||
}
|
||||
|
||||
rmqServer.OnWithdrawMsg = func(msg message.WithdrawMsg_req) {
|
||||
log.Printf("收到提现请求: %+v", msg)
|
||||
// 处理提现逻辑...
|
||||
// 调用区块链转账
|
||||
}
|
||||
|
||||
rmqServer.OnPayMsg = func(msg message.PayMsg_req) {
|
||||
log.Printf("收到支付请求: %+v", msg)
|
||||
// 处理支付逻辑...
|
||||
// 调用区块链转账
|
||||
}
|
||||
|
||||
// 启动监听
|
||||
if err := rmqServer.Start(); err != nil {
|
||||
log.Fatalf("启动监听失败: %v", err)
|
||||
}
|
||||
|
||||
// 发送响应消息示例
|
||||
rmqServer.PublishTopupResp(message.TopupMsg_resp{
|
||||
Address: "0x123...",
|
||||
Status: 1,
|
||||
Chain: "ETH",
|
||||
Symbol: "USDT",
|
||||
Amount: 100.0,
|
||||
TxHash: "0xabc...",
|
||||
})
|
||||
|
||||
// 保持运行
|
||||
select {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 说明
|
||||
|
||||
### 创建服务
|
||||
|
||||
```go
|
||||
func NewRabbitMQServer(config message.RMQConfig) (*RabbitMQServer, error)
|
||||
```
|
||||
|
||||
创建一个新的 RabbitMQ 服务实例。
|
||||
|
||||
**参数:**
|
||||
- `config`: RabbitMQ 配置
|
||||
|
||||
**返回:**
|
||||
- `*RabbitMQServer`: 服务实例
|
||||
- `error`: 错误信息
|
||||
|
||||
---
|
||||
|
||||
### 启动监听
|
||||
|
||||
```go
|
||||
func (r *RabbitMQServer) Start() error
|
||||
```
|
||||
|
||||
启动所有队列的监听(topup, withdraw, pay)。
|
||||
|
||||
---
|
||||
|
||||
### 消息回调设置
|
||||
|
||||
```go
|
||||
// 充值请求回调
|
||||
rmqServer.OnTopupMsg = func(msg message.TopupMsg_req) {
|
||||
// 处理逻辑
|
||||
}
|
||||
|
||||
// 提现请求回调
|
||||
rmqServer.OnWithdrawMsg = func(msg message.WithdrawMsg_req) {
|
||||
// 处理逻辑
|
||||
}
|
||||
|
||||
// 支付请求回调
|
||||
rmqServer.OnPayMsg = func(msg message.PayMsg_req) {
|
||||
// 处理逻辑
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 发布响应消息
|
||||
|
||||
```go
|
||||
// 发布充值响应
|
||||
func (r *RabbitMQServer) PublishTopupResp(resp message.TopupMsg_resp) error
|
||||
|
||||
// 发布提现响应
|
||||
func (r *RabbitMQServer) PublishWithdrawResp(resp message.WithdrawMsg_resp) error
|
||||
|
||||
// 发布支付响应
|
||||
func (r *RabbitMQServer) PublishPayResp(resp message.PayMsg_resp) error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 关闭服务
|
||||
|
||||
```go
|
||||
func (r *RabbitMQServer) Close() error
|
||||
```
|
||||
|
||||
优雅关闭 RabbitMQ 连接和所有监听。
|
||||
|
||||
---
|
||||
|
||||
## 消息格式
|
||||
|
||||
### 充值请求 (TopupMsg_req)
|
||||
```json
|
||||
{
|
||||
"chain": "ETH",
|
||||
"symbol": "USDT",
|
||||
"address": "0x123...",
|
||||
"timestamp": 1234567890,
|
||||
"sign": "signature_string"
|
||||
}
|
||||
```
|
||||
|
||||
### 充值响应 (TopupMsg_resp)
|
||||
```json
|
||||
{
|
||||
"address": "0x123...",
|
||||
"status": 1,
|
||||
"chain": "ETH",
|
||||
"symbol": "USDT",
|
||||
"amount": 100.5,
|
||||
"tx_hash": "0xabc..."
|
||||
}
|
||||
```
|
||||
|
||||
### 提现请求 (WithdrawMsg_req)
|
||||
```json
|
||||
{
|
||||
"queue_id": "queue_123",
|
||||
"from_address": "0x123...",
|
||||
"to_address": "0x456...",
|
||||
"amount": 50.0,
|
||||
"chain": "ETH",
|
||||
"symbol": "USDT",
|
||||
"timestamp": 1234567890,
|
||||
"sign": "signature_string"
|
||||
}
|
||||
```
|
||||
|
||||
### 提现响应 (WithdrawMsg_resp)
|
||||
```json
|
||||
{
|
||||
"queue_id": "queue_123",
|
||||
"status": 1,
|
||||
"amount": 50.0,
|
||||
"chain": "ETH",
|
||||
"symbol": "USDT",
|
||||
"tx_hash": "0xdef..."
|
||||
}
|
||||
```
|
||||
|
||||
### 支付请求 (PayMsg_req)
|
||||
```json
|
||||
{
|
||||
"queue_id": "queue_456",
|
||||
"from_address": "0x123...",
|
||||
"to_address": "0x789...",
|
||||
"amount": 200.0,
|
||||
"chain": "ETH",
|
||||
"symbol": "USDT",
|
||||
"order_id": "order_789",
|
||||
"timestamp": 1234567890,
|
||||
"sign": "signature_string"
|
||||
}
|
||||
```
|
||||
|
||||
### 支付响应 (PayMsg_resp)
|
||||
```json
|
||||
{
|
||||
"queue_id": "queue_456",
|
||||
"status": 1,
|
||||
"amount": 200.0,
|
||||
"chain": "ETH",
|
||||
"symbol": "USDT",
|
||||
"order_id": "order_789",
|
||||
"tx_hash": "0xghi..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 状态码说明
|
||||
|
||||
- `status = 0` - 失败
|
||||
- `status = 1` - 成功
|
||||
- `status = 2` - 待确认(仅用于链上交易)
|
||||
|
||||
---
|
||||
|
||||
## 特性
|
||||
|
||||
✅ **自动重连** - 连接断开时自动重连
|
||||
✅ **消息持久化** - 消息不会丢失
|
||||
✅ **手动确认** - 处理成功后才确认消息
|
||||
✅ **并发安全** - 支持多 goroutine 并发发布
|
||||
✅ **优雅关闭** - 支持优雅关闭所有连接
|
||||
✅ **错误处理** - 完善的错误日志和重试机制
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **连接地址格式**: `amqp://username:password@host:port/`
|
||||
2. **消息确认**: 只有处理成功的消息才会被确认,失败的消息会重新入队
|
||||
3. **并发安全**: 发布消息时使用了互斥锁,可以安全地从多个 goroutine 调用
|
||||
4. **重连机制**: 连接断开时会自动重连,间隔 3 秒
|
||||
5. **消息持久化**: 消息和队列都是持久化的,重启后不会丢失
|
||||
|
||||
---
|
||||
|
||||
## 工作流程
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
||||
│ 业务系统 │ ------> │ RabbitMQ │ ------> │ 支付系统 │
|
||||
│ │ 充值/ │ (消息队列) │ 消费 │ (本系统) │
|
||||
│ │ 提现/ │ │ │ │
|
||||
│ │ 支付 │ │ │ │
|
||||
└─────────────┘ └──────────────┘ └─────────────┘
|
||||
|
|
||||
| 监听链上
|
||||
v
|
||||
┌─────────────┐
|
||||
│ 区块链节点 │
|
||||
│ (ETH) │
|
||||
└─────────────┘
|
||||
|
|
||||
| 交易确认
|
||||
v
|
||||
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
||||
│ 业务系统 │ <------ │ RabbitMQ │ <------ │ 支付系统 │
|
||||
│ │ 响应 │ (响应队列) │ 发布 │ (本系统) │
|
||||
└─────────────┘ └──────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 连接失败
|
||||
- 检查 RabbitMQ 服务是否运行
|
||||
- 检查用户名密码是否正确
|
||||
- 检查网络连接和端口(默认 5672)
|
||||
|
||||
### 消息未被消费
|
||||
- 检查队列绑定是否正确
|
||||
- 检查 routing key 是否匹配
|
||||
- 查看 RabbitMQ 管理界面的队列状态
|
||||
|
||||
### 消息重复消费
|
||||
- 确保消息处理成功后调用了 Ack
|
||||
- 检查是否有多个消费者实例
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
344
internal/queue/rabbitmq.go
Normal file
344
internal/queue/rabbitmq.go
Normal file
@@ -0,0 +1,344 @@
|
||||
package rmq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
message "m2pool-payment/internal/msg"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
// RabbitMQServer RabbitMQ 服务
|
||||
type RabbitMQServer struct {
|
||||
config message.RMQConfig
|
||||
conn *amqp.Connection
|
||||
channel *amqp.Channel
|
||||
mu sync.Mutex
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
// 消息处理回调函数
|
||||
OnTopupMsg func(message.TopupMsg_req) // 充值请求回调
|
||||
OnWithdrawMsg func(message.WithdrawMsg_req) // 提现请求回调
|
||||
OnPayMsg func(message.PayMsg_req) // 支付请求回调
|
||||
}
|
||||
|
||||
// NewRabbitMQServer 创建 RabbitMQ 服务
|
||||
func NewRabbitMQServer(config message.RMQConfig) (*RabbitMQServer, error) {
|
||||
// 创建连接
|
||||
conn, err := amqp.Dial(config.SubAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to RabbitMQ: %w", err)
|
||||
}
|
||||
|
||||
// 创建通道
|
||||
channel, err := conn.Channel()
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to open a channel: %w", err)
|
||||
}
|
||||
|
||||
// 创建可取消的 context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
server := &RabbitMQServer{
|
||||
config: config,
|
||||
conn: conn,
|
||||
channel: channel,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
// 初始化队列和交换机
|
||||
if err := server.setupQueuesAndExchanges(); err != nil {
|
||||
server.Close()
|
||||
return nil, fmt.Errorf("failed to setup queues: %w", err)
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// setupQueuesAndExchanges 设置队列和交换机
|
||||
func (r *RabbitMQServer) setupQueuesAndExchanges() error {
|
||||
configs := []message.QueueConfig{
|
||||
r.config.PayConfig,
|
||||
r.config.TopUpConfig,
|
||||
r.config.WithdrawConfig,
|
||||
r.config.PayRespConfig,
|
||||
r.config.TopUpRespConfig,
|
||||
r.config.WithdrawRespConfig,
|
||||
}
|
||||
|
||||
for _, cfg := range configs {
|
||||
// 声明交换机
|
||||
err := r.channel.ExchangeDeclare(
|
||||
cfg.ExchangeName, // 交换机名称
|
||||
"direct", // 类型:direct(与现有交换机类型一致)
|
||||
true, // durable
|
||||
false, // auto-deleted
|
||||
false, // internal
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to declare exchange %s: %w", cfg.ExchangeName, err)
|
||||
}
|
||||
|
||||
// 声明队列
|
||||
_, err = r.channel.QueueDeclare(
|
||||
cfg.QueueName, // 队列名称
|
||||
true, // durable
|
||||
false, // delete when unused
|
||||
false, // exclusive
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to declare queue %s: %w", cfg.QueueName, err)
|
||||
}
|
||||
|
||||
// 绑定队列到交换机
|
||||
for _, routingKey := range cfg.Routing {
|
||||
err = r.channel.QueueBind(
|
||||
cfg.QueueName, // 队列名称
|
||||
routingKey, // routing key
|
||||
cfg.ExchangeName, // 交换机名称
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to bind queue %s to exchange %s with key %s: %w",
|
||||
cfg.QueueName, cfg.ExchangeName, routingKey, err)
|
||||
}
|
||||
}
|
||||
|
||||
// log.Printf("✅ 队列配置成功: Queue=%s, Exchange=%s, RoutingKeys=%v",
|
||||
// cfg.QueueName, cfg.ExchangeName, cfg.Routing)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start 启动监听所有队列
|
||||
func (r *RabbitMQServer) Start() error {
|
||||
// 启动充值消息监听
|
||||
go r.consumeTopup()
|
||||
// 启动提现消息监听
|
||||
go r.consumeWithdraw()
|
||||
// 启动支付消息监听
|
||||
go r.consumePay()
|
||||
|
||||
// log.Println("🚀 RabbitMQ 服务启动成功,开始监听消息...")
|
||||
return nil
|
||||
}
|
||||
|
||||
// consumeTopup 消费充值消息
|
||||
func (r *RabbitMQServer) consumeTopup() {
|
||||
r.consumeQueue(
|
||||
r.config.TopUpConfig.QueueName,
|
||||
"topup",
|
||||
func(body []byte) error {
|
||||
var msg message.TopupMsg_req
|
||||
if err := json.Unmarshal(body, &msg); err != nil {
|
||||
return fmt.Errorf("failed to parse topup message: %w", err)
|
||||
}
|
||||
log.Printf("📥 [RMQ] 收到充值请求: Chain=%s, Symbol=%s, Address=%s",
|
||||
msg.Chain, msg.Symbol, msg.Address)
|
||||
|
||||
if r.OnTopupMsg != nil {
|
||||
r.OnTopupMsg(msg)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// consumeWithdraw 消费提现消息
|
||||
func (r *RabbitMQServer) consumeWithdraw() {
|
||||
r.consumeQueue(
|
||||
r.config.WithdrawConfig.QueueName,
|
||||
"withdraw",
|
||||
func(body []byte) error {
|
||||
var msg message.WithdrawMsg_req
|
||||
if err := json.Unmarshal(body, &msg); err != nil {
|
||||
return fmt.Errorf("failed to parse withdraw message: %w", err)
|
||||
}
|
||||
log.Printf("📥 [RMQ] 收到提现请求: QueueId=%s, From=%s, To=%s, Amount=%.2f %s",
|
||||
msg.QueueId, msg.FromAddress, msg.ToAddress, msg.Amount, msg.Symbol)
|
||||
|
||||
if r.OnWithdrawMsg != nil {
|
||||
r.OnWithdrawMsg(msg)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// consumePay 消费支付消息
|
||||
func (r *RabbitMQServer) consumePay() {
|
||||
r.consumeQueue(
|
||||
r.config.PayConfig.QueueName,
|
||||
"pay",
|
||||
func(body []byte) error {
|
||||
var msg message.PayMsg_req
|
||||
if err := json.Unmarshal(body, &msg); err != nil {
|
||||
return fmt.Errorf("failed to parse pay message: %w", err)
|
||||
}
|
||||
log.Printf("📥 [RMQ] 收到支付请求: QueueId=%s, OrderId=%s, From=%s, To=%s, Amount=%.2f %s",
|
||||
msg.QueueId, msg.OrderId, msg.FromAddress, msg.ToAddress, msg.Amount, msg.Symbol)
|
||||
|
||||
if r.OnPayMsg != nil {
|
||||
r.OnPayMsg(msg)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// consumeQueue 通用队列消费方法
|
||||
func (r *RabbitMQServer) consumeQueue(queueName, msgType string, handler func([]byte) error) {
|
||||
for {
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
// log.Printf("🛑 停止监听队列: %s", queueName)
|
||||
return
|
||||
default:
|
||||
msgs, err := r.channel.Consume(
|
||||
queueName, // 队列名称
|
||||
"", // consumer tag
|
||||
false, // auto-ack (设置为false,手动确认)
|
||||
false, // exclusive
|
||||
false, // no-local
|
||||
false, // no-wait
|
||||
nil, // args
|
||||
)
|
||||
if err != nil {
|
||||
// log.Printf("❌ 消费队列 %s 失败: %v, 3秒后重试...", queueName, err)
|
||||
time.Sleep(3 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// log.Printf("✅ 开始监听队列: %s (%s)", queueName, msgType)
|
||||
|
||||
for msg := range msgs {
|
||||
err := handler(msg.Body)
|
||||
if err != nil {
|
||||
// log.Printf("⚠️ 处理 %s 消息失败: %v", msgType, err)
|
||||
// 消息处理失败,不确认,让消息重新入队
|
||||
msg.Nack(false, true)
|
||||
} else {
|
||||
// 消息处理成功,确认消息
|
||||
msg.Ack(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 channel 关闭,等待后重连
|
||||
// log.Printf("⚠️ 队列 %s 连接断开,3秒后重连...", queueName)
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PublishTopupResp 发布充值响应
|
||||
func (r *RabbitMQServer) PublishTopupResp(resp message.TopupMsg_resp) error {
|
||||
return r.publishMessage(
|
||||
r.config.TopUpRespConfig,
|
||||
resp,
|
||||
fmt.Sprintf("充值响应: Address=%s, Status=%d, TxHash=%s",
|
||||
resp.Address, resp.Status, resp.TxHash),
|
||||
)
|
||||
}
|
||||
|
||||
// PublishWithdrawResp 发布提现响应
|
||||
func (r *RabbitMQServer) PublishWithdrawResp(resp message.WithdrawMsg_resp) error {
|
||||
return r.publishMessage(
|
||||
r.config.WithdrawRespConfig,
|
||||
resp,
|
||||
fmt.Sprintf("提现响应: QueueId=%s, Status=%d, TxHash=%s",
|
||||
resp.QueueId, resp.Status, resp.TxHash),
|
||||
)
|
||||
}
|
||||
|
||||
// PublishPayResp 发布支付响应
|
||||
func (r *RabbitMQServer) PublishPayResp(resp message.PayMsg_resp) error {
|
||||
return r.publishMessage(
|
||||
r.config.PayRespConfig,
|
||||
resp,
|
||||
fmt.Sprintf("支付响应: QueueId=%s, OrderId=%s, Status=%d, TxHash=%s",
|
||||
resp.QueueId, resp.OrderId, resp.Status, resp.TxHash),
|
||||
)
|
||||
}
|
||||
|
||||
// publishMessage 通用消息发布方法
|
||||
func (r *RabbitMQServer) publishMessage(config message.QueueConfig, msg interface{}, logMsg string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// 序列化消息
|
||||
body, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal message: %w", err)
|
||||
}
|
||||
|
||||
// 使用第一个 routing key(如果有多个)
|
||||
routingKey := ""
|
||||
if len(config.Routing) > 0 {
|
||||
routingKey = config.Routing[0]
|
||||
}
|
||||
|
||||
// 发布消息
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = r.channel.PublishWithContext(
|
||||
ctx,
|
||||
config.ExchangeName, // 交换机
|
||||
routingKey, // routing key
|
||||
false, // mandatory
|
||||
false, // immediate
|
||||
amqp.Publishing{
|
||||
ContentType: "application/json",
|
||||
Body: body,
|
||||
DeliveryMode: amqp.Persistent, // 持久化消息
|
||||
Timestamp: time.Now(),
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to publish message: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("📤 [RMQ] 发送%s", logMsg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭连接
|
||||
func (r *RabbitMQServer) Close() error {
|
||||
// log.Println("🛑 正在关闭 RabbitMQ 服务...")
|
||||
|
||||
r.cancel() // 取消所有 goroutine
|
||||
|
||||
if r.channel != nil {
|
||||
if err := r.channel.Close(); err != nil {
|
||||
// log.Printf("⚠️ 关闭 channel 失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if r.conn != nil {
|
||||
if err := r.conn.Close(); err != nil {
|
||||
// log.Printf("⚠️ 关闭连接失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// log.Println("✅ RabbitMQ 服务已关闭")
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsConnected 检查连接状态
|
||||
func (r *RabbitMQServer) IsConnected() bool {
|
||||
return r.conn != nil && !r.conn.IsClosed()
|
||||
}
|
||||
296
internal/server.go
Normal file
296
internal/server.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"m2pool-payment/internal/blockchain"
|
||||
"m2pool-payment/internal/blockchain/eth"
|
||||
"m2pool-payment/internal/crypto"
|
||||
message "m2pool-payment/internal/msg"
|
||||
rmq "m2pool-payment/internal/queue"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const MSG_KEY string = "9f3c7a12"
|
||||
|
||||
// 状态码常量
|
||||
const (
|
||||
STATUS_FAILED = 0 // 失败
|
||||
STATUS_SUCCESS = 1 // 成功
|
||||
STATUS_PENDING = 2 // 待确认
|
||||
STATUS_VERIFY_FAILED = 3 // 验证失败
|
||||
)
|
||||
|
||||
type ServerCtx struct {
|
||||
Config message.Config
|
||||
blockChainServer *blockchain.BlockChainServer
|
||||
rmqServer *rmq.RabbitMQServer
|
||||
}
|
||||
|
||||
var s_ctx ServerCtx
|
||||
|
||||
// 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() {
|
||||
file, err := os.ReadFile("config.json")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("读取配置文件失败: %v", err))
|
||||
}
|
||||
|
||||
err = json.Unmarshal(file, &s_ctx.Config)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("解析配置文件失败: %v", err))
|
||||
}
|
||||
|
||||
log.Printf("✅ 配置加载成功: RPC=%s, WS=%s",
|
||||
s_ctx.Config.ETHConfig.RpcURL, s_ctx.Config.ETHConfig.WsURL)
|
||||
}
|
||||
|
||||
func initBlockChainServer() {
|
||||
// 初始化节点服务
|
||||
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)
|
||||
// 将所有注册的blockChainServer绑定至server
|
||||
s_ctx.blockChainServer = node_server
|
||||
|
||||
log.Println("✅ 区块链服务初始化完成")
|
||||
}
|
||||
|
||||
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 handleTopupMsg() {
|
||||
s_ctx.rmqServer.OnTopupMsg = func(msg message.TopupMsg_req) {
|
||||
msg.Address = strings.ToLower(msg.Address)
|
||||
|
||||
// 验证签名
|
||||
if !verifyMessage(msg.Timestamp, msg.Sign) {
|
||||
err := s_ctx.rmqServer.PublishTopupResp(message.TopupMsg_resp{
|
||||
Address: msg.Address,
|
||||
Status: STATUS_VERIFY_FAILED,
|
||||
Chain: msg.Chain,
|
||||
Symbol: msg.Symbol,
|
||||
Amount: 0,
|
||||
TxHash: "",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("❌ 发布充值失败响应失败: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 添加监听地址
|
||||
s_ctx.blockChainServer.AddAddress(msg.Chain, msg.Address, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func handleWithdrawMsg() {
|
||||
s_ctx.rmqServer.OnWithdrawMsg = func(msg message.WithdrawMsg_req) {
|
||||
msg.FromAddress = strings.ToLower(msg.FromAddress)
|
||||
msg.ToAddress = strings.ToLower(msg.ToAddress)
|
||||
|
||||
// 验证签名
|
||||
if !verifyMessage(msg.Timestamp, msg.Sign) {
|
||||
err := s_ctx.rmqServer.PublishWithdrawResp(message.WithdrawMsg_resp{
|
||||
QueueId: msg.QueueId,
|
||||
Status: STATUS_VERIFY_FAILED,
|
||||
Chain: msg.Chain,
|
||||
Symbol: msg.Symbol,
|
||||
Amount: 0,
|
||||
TxHash: "",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("❌ 发布提现失败响应失败: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 执行转账
|
||||
err := s_ctx.blockChainServer.Transfer(msg.Chain, msg.Symbol, 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: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handlePayMsg() {
|
||||
s_ctx.rmqServer.OnPayMsg = func(msg message.PayMsg_req) {
|
||||
msg.FromAddress = strings.ToLower(msg.FromAddress)
|
||||
msg.ToAddress = strings.ToLower(msg.ToAddress)
|
||||
|
||||
// 验证签名
|
||||
if !verifyMessage(msg.Timestamp, msg.Sign) {
|
||||
err := s_ctx.rmqServer.PublishPayResp(message.PayMsg_resp{
|
||||
QueueId: msg.QueueId,
|
||||
Status: STATUS_VERIFY_FAILED,
|
||||
Amount: msg.Amount,
|
||||
Chain: msg.Chain,
|
||||
Symbol: msg.Symbol,
|
||||
OrderId: msg.OrderId,
|
||||
TxHash: "",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("❌ 发布支付失败响应失败: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 执行转账
|
||||
err := s_ctx.blockChainServer.Transfer(msg.Chain, msg.Symbol, 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: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initRmqListen() {
|
||||
// ================== 设置 RabbitMQ 消息处理回调 ==================
|
||||
// 先设置所有回调(同步执行,避免竞态)
|
||||
handleTopupMsg()
|
||||
handleWithdrawMsg()
|
||||
handlePayMsg()
|
||||
|
||||
// 回调设置完成后,再启动 RabbitMQ 监听
|
||||
if err := s_ctx.rmqServer.Start(); err != nil {
|
||||
log.Fatalf("启动 RabbitMQ 监听失败: %v", err)
|
||||
}
|
||||
log.Println("✅ RabbitMQ 监听启动完成")
|
||||
}
|
||||
|
||||
func handleChainEvent(chainEventCh chan any) {
|
||||
for event := range chainEventCh {
|
||||
// 添加 panic 恢复,防止单个消息处理错误导致整个 goroutine 退出
|
||||
func() {
|
||||
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)
|
||||
} else {
|
||||
log.Printf("✅ [链上] 充值确认: Address=%s, Amount=%.2f, TxHash=%s, Status=%d",
|
||||
msg.Address, msg.Amount, msg.TxHash, msg.Status)
|
||||
}
|
||||
err := s_ctx.rmqServer.PublishTopupResp(msg)
|
||||
if err != nil {
|
||||
log.Printf("❌ 发送充值响应失败: %v", err)
|
||||
}
|
||||
|
||||
case message.WithdrawMsg_resp:
|
||||
// 提现确认
|
||||
log.Printf("✅ [链上] 提现确认: QueueId=%s, Amount=%.2f, TxHash=%s, Status=%d",
|
||||
msg.QueueId, msg.Amount, msg.TxHash, msg.Status)
|
||||
err := s_ctx.rmqServer.PublishWithdrawResp(msg)
|
||||
if err != nil {
|
||||
log.Printf("❌ 发送提现响应失败: %v", err)
|
||||
}
|
||||
|
||||
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)
|
||||
err := s_ctx.rmqServer.PublishPayResp(msg)
|
||||
if err != nil {
|
||||
log.Printf("❌ 发送支付响应失败: %v", err)
|
||||
}
|
||||
|
||||
default:
|
||||
log.Printf("⚠️ 未知消息类型: %T", event)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func Start() {
|
||||
log.Println("========================================")
|
||||
log.Println("🚀 M2Pool Payment System Starting...")
|
||||
log.Println("========================================")
|
||||
|
||||
// 加载配置
|
||||
loadConfig()
|
||||
|
||||
// ================== 初始化区块链节点 ==================
|
||||
initBlockChainServer()
|
||||
|
||||
// ================== 初始化 RabbitMQ 服务 ==================
|
||||
initRmqServer()
|
||||
|
||||
// ================== 启动链上事件监听通道 ==================
|
||||
chainEventCh := make(chan any, 1000) // 增加缓冲区,避免高并发丢消息
|
||||
go s_ctx.blockChainServer.Listen("ETH", "USDT", chainEventCh)
|
||||
|
||||
// ================== 启动 RabbitMQ 监听 ==================
|
||||
initRmqListen()
|
||||
|
||||
// ================== 处理链上确认事件 ==================
|
||||
go handleChainEvent(chainEventCh)
|
||||
|
||||
log.Println("========================================")
|
||||
log.Println("🎉 所有服务启动完成!")
|
||||
log.Println("========================================")
|
||||
// ================== 等待退出信号 ==================
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigCh
|
||||
|
||||
// 优雅关闭
|
||||
log.Println("========================================")
|
||||
log.Println("🛑 收到退出信号,正在关闭服务...")
|
||||
log.Println("========================================")
|
||||
|
||||
s_ctx.blockChainServer.Stop("ETH")
|
||||
s_ctx.rmqServer.Close()
|
||||
|
||||
log.Println("👋 服务已全部关闭")
|
||||
}
|
||||
40
internal/utils/utils.go
Normal file
40
internal/utils/utils.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func BigIntUSDTToFloat64(value *big.Int) float64 {
|
||||
f := new(big.Float).SetInt(value)
|
||||
scale := new(big.Float).SetFloat64(1e6) // USDT 精度 6 位
|
||||
f.Quo(f, scale)
|
||||
result, _ := f.Float64()
|
||||
return result
|
||||
}
|
||||
|
||||
// USDT 一般精度是 6 位小数
|
||||
const USDTDecimals = 6
|
||||
|
||||
// Float64ToBigIntUSDT 将 float64 金额转换成 *big.Int
|
||||
func Float64ToBigIntUSDT(amount float64) *big.Int {
|
||||
// 乘上精度系数
|
||||
scale := math.Pow10(USDTDecimals)
|
||||
bigAmount := new(big.Int)
|
||||
bigAmount.SetInt64(int64(amount * scale))
|
||||
return bigAmount
|
||||
}
|
||||
|
||||
func Slice_delete(arr []any, index int) []any {
|
||||
if index < 0 || index >= len(arr) {
|
||||
// 处理越界
|
||||
log.Fatalf("slice arr error: index=%d, arr length=%d", index, len(arr))
|
||||
return nil
|
||||
}
|
||||
if index >= 0 && index < len(arr) {
|
||||
copy(arr[index:], arr[index+1:]) // 后面的元素往前移动
|
||||
arr = arr[:len(arr)-1] // 去掉最后一个多余元素
|
||||
}
|
||||
return arr
|
||||
}
|
||||
Reference in New Issue
Block a user