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
-
-
-
-
-
地址管理
-
-
-
-
-
-
-
-
-
-
-
-
-`
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
+
+
+
+
+
地址管理
+
+
+
+
+
+
+
+
+
+
+
+
+
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
}