From 79c1c710a2e1c35fe16cb76b547a048458416017 Mon Sep 17 00:00:00 2001 From: TQCasey <494294315@qq.com> Date: Tue, 3 Feb 2026 00:01:41 +0800 Subject: [PATCH] usdtman --- README.md | 31 +++- cmd/server/main.go | 173 +++----------------- index.html | 132 ++++++++++++++++ tron_usdt_monitor.go | 369 ++++++++++++++++++++++++------------------- 4 files changed, 387 insertions(+), 318 deletions(-) create mode 100644 index.html diff --git a/README.md b/README.md index 2bc45ce..e192c4e 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,31 @@ # USDTMan -TRON USDT TRC20 收款监听服务 +TRON USDT TRC20 收款监听服务 - 基于交易记录扫描 + 区块确认数验证 ## 功能 - 实时监听多个 TRON 地址的 USDT 收款 +- 区块确认数验证(默认6个确认) - WebSocket 实时推送收款通知 - HTTP API 管理监听地址 -- 测试页面 + +## API 使用方式 + +```go +// 创建监听器 +uman := usdtman.NewUSDTMan([]string{"地址1", "地址2"}, "API_KEY") + +// 设置收款回调 +uman.OnPaymentComplete(func(payment *usdtman.USDTPayment) { + fmt.Printf("收到 %.6f USDT,确认数: %d\n", payment.Amount, payment.Confirmations) +}) + +// 启动监听 +uman.Start() + +// 停止监听 +uman.Stop() +``` ## 运行 @@ -18,7 +36,7 @@ go run main.go 访问 http://localhost:8084 -## API +## 接口 - `POST /start` - 启动监听 - `POST /stop` - 停止监听 @@ -27,3 +45,10 @@ go run main.go - `GET /list-addresses` - 列出所有地址 - `GET /payments` - 获取收款历史 - `WS /ws` - WebSocket 连接 + +## 确认机制 + +- 扫描地址的最近交易记录 +- 计算区块确认数(当前区块 - 交易区块) +- 仅在确认数 >= 6 时触发回调 +- 自动去重,避免重复处理 diff --git a/cmd/server/main.go b/cmd/server/main.go index b7ec572..e5f48e7 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -13,7 +13,7 @@ import ( ) var ( - tronMonitor *usdtman.TronUSDTMonitor + uman *usdtman.USDTMan paymentEvents []usdtman.USDTPayment paymentLock sync.RWMutex upgrader = websocket.Upgrader{ @@ -34,10 +34,13 @@ func main() { apiKey = "da1e77dc-b35b-4458-846a-5a551b9df4b2" } - tronMonitor = usdtman.NewTronUSDTMonitor(apiKey) - tronMonitor.SetPaymentCallback(func(payment *usdtman.USDTPayment) { - log.Printf("💰 收到 USDT: %s -> %.6f USDT (TxID: %s)", - payment.From, payment.Amount, payment.TxID) + // 初始化 USDTMan (无初始地址) + uman = usdtman.NewUSDTMan([]string{}, apiKey) + + // 设置收款回调 + uman.OnPaymentComplete(func(payment *usdtman.USDTPayment) { + log.Printf("💰 收到 USDT: %s -> %.6f USDT (确认数: %d, TxID: %s)", + payment.From, payment.Amount, payment.Confirmations, payment.TxID) paymentLock.Lock() paymentEvents = append(paymentEvents, *payment) @@ -77,7 +80,7 @@ func startMonitor(w http.ResponseWriter, r *http.Request) { return } - if err := tronMonitor.Start(); err != nil { + if err := uman.Start(); err != nil { jsonResponse(w, false, fmt.Sprintf("启动失败: %v", err), nil) return } @@ -91,7 +94,7 @@ func stopMonitor(w http.ResponseWriter, r *http.Request) { return } - tronMonitor.Stop() + uman.Stop() jsonResponse(w, true, "监听已停止", nil) } @@ -115,7 +118,7 @@ func addAddress(w http.ResponseWriter, r *http.Request) { return } - tronMonitor.AddAddress(req.Address) + uman.AddAddress(req.Address) jsonResponse(w, true, "地址已添加", map[string]interface{}{ "address": req.Address, }) @@ -136,7 +139,7 @@ func removeAddress(w http.ResponseWriter, r *http.Request) { return } - tronMonitor.RemoveAddress(req.Address) + uman.RemoveAddress(req.Address) jsonResponse(w, true, "地址已移除", map[string]interface{}{ "address": req.Address, }) @@ -148,7 +151,7 @@ func listAddresses(w http.ResponseWriter, r *http.Request) { return } - addresses := tronMonitor.GetAllAddresses() + addresses := uman.GetAddresses() jsonResponse(w, true, "success", map[string]interface{}{ "addresses": addresses, "count": len(addresses), @@ -200,13 +203,14 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) { func broadcastPayment(payment *usdtman.USDTPayment) { message := map[string]interface{}{ - "type": "usdt_payment", - "address": payment.Address, - "amount": payment.Amount, - "from": payment.From, - "txId": payment.TxID, - "block": payment.BlockNumber, - "time": payment.Timestamp, + "type": "usdt_payment", + "address": payment.Address, + "amount": payment.Amount, + "from": payment.From, + "txId": payment.TxID, + "block": payment.BlockNumber, + "time": payment.Timestamp, + "confirmations": payment.Confirmations, } clientsLock.RLock() @@ -218,138 +222,5 @@ func broadcastPayment(payment *usdtman.USDTPayment) { } func serveIndex(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(indexHTML)) + http.ServeFile(w, r, "index.html") } - -const indexHTML = ` - - - USDTMan - USDT Monitor - - - -

🚀 USDTMan - USDT Monitor

- -
-

监听控制

- - -
-
- -
-

地址管理

- - - -
-
- -
-

收款记录

- -
-
- -
-

实时日志 (WebSocket)

-
-
- - - -` diff --git a/index.html b/index.html new file mode 100644 index 0000000..7fa1a1f --- /dev/null +++ b/index.html @@ -0,0 +1,132 @@ + + + + USDTMan - USDT Monitor + + + +

🚀 USDTMan - USDT Monitor

+ +
+

监听控制

+ + +
+
+ +
+

地址管理

+ + + +
+
+ +
+

收款记录

+ +
+
+ +
+

实时日志 (WebSocket)

+
+
+ + + + diff --git a/tron_usdt_monitor.go b/tron_usdt_monitor.go index c292b65..af4ee16 100644 --- a/tron_usdt_monitor.go +++ b/tron_usdt_monitor.go @@ -11,95 +11,96 @@ import ( "time" ) -// TronUSDTMonitor 监听 USDT 收款 -type TronUSDTMonitor struct { +// USDTMan USDT 监听管理器 +type USDTMan struct { mu sync.RWMutex - addresses map[string]*AddressInfo // 地址映射 + addresses []string apiKey string running bool ctx context.Context cancel context.CancelFunc - onPaymentReceived func(*USDTPayment) -} - -// AddressInfo 地址信息 -type AddressInfo struct { - Address string - LastBlock int64 - TotalReceived float64 + onPaymentComplete func(*USDTPayment) + minConfirmations int64 + processedTxns map[string]bool // 已处理的交易 + txnMutex sync.RWMutex } // USDTPayment USDT 收款信息 type USDTPayment struct { - Address string - Amount float64 - TxID string - BlockNumber int64 - Timestamp int64 - From string + Address string + Amount float64 + TxID string + BlockNumber int64 + Timestamp int64 + From string + Confirmations int64 } -// TronGridEvent TronGrid 事件响应 -type TronGridEvent struct { - Success bool `json:"success"` - Data []struct { - TransactionID string `json:"transaction_id"` - BlockNumber int64 `json:"block_number"` - BlockTimestamp int64 `json:"block_timestamp"` - ContractAddress string `json:"contract_address"` - EventName string `json:"event_name"` - Result map[string]interface{} `json:"result"` - } `json:"data"` - Meta struct { - PageSize int `json:"page_size"` +// TronGridTransaction 交易信息 +type TronGridTransaction struct { + Ret []map[string]interface{} `json:"ret"` + TxID string `json:"txID"` + BlockNumber int64 `json:"blockNumber"` + BlockTimeStamp int64 `json:"block_timestamp"` + RawData struct { + Contract []struct { + Parameter struct { + Value struct { + Amount int64 `json:"amount"` + To string `json:"to_address"` + From string `json:"owner_address"` + } `json:"value"` + } `json:"parameter"` + } `json:"contract"` + } `json:"raw_data"` +} + +// TronGridAccountTransactions 账户交易列表 +type TronGridAccountTransactions struct { + Success bool `json:"success"` + Data []TronGridTransaction `json:"data"` + Meta struct { + PageSize int `json:"page_size"` + Fingerprint string `json:"fingerprint"` } `json:"meta"` } +// TronGridBlockInfo 区块信息 +type TronGridBlockInfo struct { + BlockHeader struct { + RawData struct { + Number int64 `json:"number"` + } `json:"raw_data"` + } `json:"block_header"` +} + const ( TronGridAPI = "https://api.trongrid.io" USDTContractAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" // USDT TRC20 ) -// NewTronUSDTMonitor 创建监听器 -func NewTronUSDTMonitor(apiKey string) *TronUSDTMonitor { +// NewUSDTMan 创建监听管理器 +func NewUSDTMan(addresses []string, apiKey string) *USDTMan { ctx, cancel := context.WithCancel(context.Background()) - return &TronUSDTMonitor{ - addresses: make(map[string]*AddressInfo), - apiKey: apiKey, - ctx: ctx, - cancel: cancel, + return &USDTMan{ + addresses: addresses, + apiKey: apiKey, + ctx: ctx, + cancel: cancel, + minConfirmations: 6, + processedTxns: make(map[string]bool), } } -// AddAddress 添加监听地址 -func (m *TronUSDTMonitor) AddAddress(address string) { +// OnPaymentComplete 设置收款回调 +func (m *USDTMan) OnPaymentComplete(callback func(*USDTPayment)) { m.mu.Lock() defer m.mu.Unlock() - - if _, exists := m.addresses[address]; !exists { - m.addresses[address] = &AddressInfo{ - Address: address, - LastBlock: 0, - } - } -} - -// RemoveAddress 移除监听地址 -func (m *TronUSDTMonitor) RemoveAddress(address string) { - m.mu.Lock() - defer m.mu.Unlock() - delete(m.addresses, address) -} - -// SetPaymentCallback 设置收款回调 -func (m *TronUSDTMonitor) SetPaymentCallback(callback func(*USDTPayment)) { - m.mu.Lock() - defer m.mu.Unlock() - m.onPaymentReceived = callback + m.onPaymentComplete = callback } // Start 开始监听 -func (m *TronUSDTMonitor) Start() error { +func (m *USDTMan) Start() error { m.mu.Lock() if m.running { m.mu.Unlock() @@ -113,7 +114,7 @@ func (m *TronUSDTMonitor) Start() error { } // Stop 停止监听 -func (m *TronUSDTMonitor) Stop() { +func (m *USDTMan) Stop() { m.mu.Lock() if !m.running { m.mu.Unlock() @@ -126,8 +127,8 @@ func (m *TronUSDTMonitor) Stop() { } // monitorLoop 监听循环 -func (m *TronUSDTMonitor) monitorLoop() { - ticker := time.NewTicker(3 * time.Second) +func (m *USDTMan) monitorLoop() { + ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for { @@ -141,64 +142,61 @@ func (m *TronUSDTMonitor) monitorLoop() { } // checkAllAddresses 检查所有地址 -func (m *TronUSDTMonitor) checkAllAddresses() { +func (m *USDTMan) checkAllAddresses() { m.mu.RLock() - addresses := make([]*AddressInfo, 0, len(m.addresses)) - for _, info := range m.addresses { - addresses = append(addresses, info) - } + addresses := make([]string, len(m.addresses)) + copy(addresses, m.addresses) m.mu.RUnlock() - for _, addrInfo := range addresses { - m.checkAddress(addrInfo) + for _, address := range addresses { + m.checkAddress(address) } } -// checkAddress 检查单个地址 -func (m *TronUSDTMonitor) checkAddress(addrInfo *AddressInfo) { - events, err := m.getUSDTEvents(addrInfo.Address, addrInfo.LastBlock) +// checkAddress 检查单个地址的交易 +func (m *USDTMan) checkAddress(address string) { + transactions, err := m.getUSDTTransactions(address) if err != nil { - fmt.Printf("Error checking address %s: %v\n", addrInfo.Address, err) + fmt.Printf("Error checking address %s: %v\n", address, err) return } - for _, event := range events { - if event.EventName != "Transfer" { + currentBlock, err := m.getCurrentBlock() + if err != nil { + fmt.Printf("Error getting current block: %v\n", err) + return + } + + for _, txn := range transactions { + // 检查是否已处理 + m.txnMutex.RLock() + processed := m.processedTxns[txn.TxID] + m.txnMutex.RUnlock() + + if processed { continue } - to, ok := event.Result["to"].(string) - if !ok || !strings.EqualFold(to, addrInfo.Address) { + // 计算确认数 + confirmations := currentBlock - txn.BlockNumber + if confirmations < m.minConfirmations { continue } - from, _ := event.Result["from"].(string) - - valueStr, ok := event.Result["value"].(string) - if !ok { + // 解析交易数据 + payment := m.parseTransaction(&txn, address, confirmations) + if payment == nil { continue } - amount := parseUSDTAmount(valueStr) - if amount <= 0 { - continue - } - - payment := &USDTPayment{ - Address: addrInfo.Address, - Amount: amount, - TxID: event.TransactionID, - BlockNumber: event.BlockNumber, - Timestamp: event.BlockTimestamp, - From: from, - } - - if event.BlockNumber > addrInfo.LastBlock { - addrInfo.LastBlock = event.BlockNumber - } + // 标记为已处理 + m.txnMutex.Lock() + m.processedTxns[txn.TxID] = true + m.txnMutex.Unlock() + // 触发回调 m.mu.RLock() - callback := m.onPaymentReceived + callback := m.onPaymentComplete m.mu.RUnlock() if callback != nil { @@ -207,16 +205,10 @@ func (m *TronUSDTMonitor) checkAddress(addrInfo *AddressInfo) { } } -// getUSDTEvents 获取 USDT 转账事件 -func (m *TronUSDTMonitor) getUSDTEvents(address string, sinceBlock int64) ([]struct { - TransactionID string - BlockNumber int64 - BlockTimestamp int64 - EventName string - Result map[string]interface{} -}, error) { - url := fmt.Sprintf("%s/v1/contracts/%s/events?event_name=Transfer&only_to=true&min_block_timestamp=%d&limit=200", - TronGridAPI, USDTContractAddress, sinceBlock) +// getUSDTTransactions 获取地址的 USDT 交易 +func (m *USDTMan) getUSDTTransactions(address string) ([]TronGridTransaction, error) { + url := fmt.Sprintf("%s/v1/accounts/%s/transactions/trc20?limit=20&contract_address=%s&only_to=true", + TronGridAPI, address, USDTContractAddress) req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -243,72 +235,121 @@ func (m *TronUSDTMonitor) getUSDTEvents(address string, sinceBlock int64) ([]str return nil, fmt.Errorf("TronGrid API error: %d %s", resp.StatusCode, string(body)) } - var result TronGridEvent + var result TronGridAccountTransactions if err := json.Unmarshal(body, &result); err != nil { return nil, err } - if !result.Success { - return nil, fmt.Errorf("TronGrid API returned success=false") + return result.Data, nil +} + +// getCurrentBlock 获取当前区块高度 +func (m *USDTMan) getCurrentBlock() (int64, error) { + url := fmt.Sprintf("%s/wallet/getnowblock", TronGridAPI) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return 0, err } - var filtered []struct { - TransactionID string - BlockNumber int64 - BlockTimestamp int64 - EventName string - Result map[string]interface{} + if m.apiKey != "" { + req.Header.Set("TRON-PRO-API-KEY", m.apiKey) } - for _, event := range result.Data { - to, ok := event.Result["to"].(string) - if ok && strings.EqualFold(to, address) { - filtered = append(filtered, struct { - TransactionID string - BlockNumber int64 - BlockTimestamp int64 - EventName string - Result map[string]interface{} - }{ - TransactionID: event.TransactionID, - BlockNumber: event.BlockNumber, - BlockTimestamp: event.BlockTimestamp, - EventName: event.EventName, - Result: event.Result, - }) + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, err + } + + if resp.StatusCode != 200 { + return 0, fmt.Errorf("TronGrid API error: %d", resp.StatusCode) + } + + var blockInfo TronGridBlockInfo + if err := json.Unmarshal(body, &blockInfo); err != nil { + return 0, err + } + + return blockInfo.BlockHeader.RawData.Number, nil +} + +// parseTransaction 解析交易 +func (m *USDTMan) parseTransaction(txn *TronGridTransaction, targetAddress string, confirmations int64) *USDTPayment { + if len(txn.Ret) == 0 || txn.Ret[0]["contractRet"] != "SUCCESS" { + return nil + } + + if len(txn.RawData.Contract) == 0 { + return nil + } + + contract := txn.RawData.Contract[0] + value := contract.Parameter.Value + + // 转换地址格式 + to := value.To + from := value.From + + // 检查是否是目标地址 + if !strings.EqualFold(to, targetAddress) { + return nil + } + + // USDT 是 6 位小数 + amount := float64(value.Amount) / 1000000.0 + + return &USDTPayment{ + Address: targetAddress, + Amount: amount, + TxID: txn.TxID, + BlockNumber: txn.BlockNumber, + Timestamp: txn.BlockTimeStamp, + From: from, + Confirmations: confirmations, + } +} + +// GetAddresses 获取所有监听地址 +func (m *USDTMan) GetAddresses() []string { + m.mu.RLock() + defer m.mu.RUnlock() + + addresses := make([]string, len(m.addresses)) + copy(addresses, m.addresses) + return addresses +} + +// AddAddress 添加监听地址 +func (m *USDTMan) AddAddress(address string) { + m.mu.Lock() + defer m.mu.Unlock() + + // 检查是否已存在 + for _, addr := range m.addresses { + if addr == address { + return } } - return filtered, nil + m.addresses = append(m.addresses, address) } -// parseUSDTAmount 解析 USDT 金额(6位小数) -func parseUSDTAmount(valueStr string) float64 { - valueStr = strings.TrimPrefix(valueStr, "0x") +// RemoveAddress 移除监听地址 +func (m *USDTMan) RemoveAddress(address string) { + m.mu.Lock() + defer m.mu.Unlock() - var value int64 - fmt.Sscanf(valueStr, "%d", &value) - - return float64(value) / 1000000.0 -} - -// GetAddressInfo 获取地址信息 -func (m *TronUSDTMonitor) GetAddressInfo(address string) (*AddressInfo, bool) { - m.mu.RLock() - defer m.mu.RUnlock() - - info, exists := m.addresses[address] - return info, exists -} - -// GetAllAddresses 获取所有监听地址 -func (m *TronUSDTMonitor) GetAllAddresses() []string { - m.mu.RLock() - defer m.mu.RUnlock() - - addresses := make([]string, 0, len(m.addresses)) - for addr := range m.addresses { - addresses = append(addresses, addr) + for i, addr := range m.addresses { + if addr == address { + m.addresses = append(m.addresses[:i], m.addresses[i+1:]...) + return + } } - return addresses }