From 3d3e8148790660ce94dd4b146b0a7950e9b211f3 Mon Sep 17 00:00:00 2001 From: TQCasey <494294315@qq.com> Date: Mon, 2 Feb 2026 23:51:17 +0800 Subject: [PATCH] first blood --- README.md | 30 +++- cmd/server/main.go | 355 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 + go.sum | 2 + tron_usdt_monitor.go | 314 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 704 insertions(+), 2 deletions(-) create mode 100644 cmd/server/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 tron_usdt_monitor.go diff --git a/README.md b/README.md index 46d1bcd..2bc45ce 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,29 @@ -# usdtman +# USDTMan -usdtman for ipay server \ No newline at end of file +TRON USDT TRC20 收款监听服务 + +## 功能 + +- 实时监听多个 TRON 地址的 USDT 收款 +- WebSocket 实时推送收款通知 +- HTTP API 管理监听地址 +- 测试页面 + +## 运行 + +```bash +cd cmd/server +go run main.go +``` + +访问 http://localhost:8084 + +## API + +- `POST /start` - 启动监听 +- `POST /stop` - 停止监听 +- `POST /add-address` - 添加监听地址 +- `POST /remove-address` - 移除地址 +- `GET /list-addresses` - 列出所有地址 +- `GET /payments` - 获取收款历史 +- `WS /ws` - WebSocket 连接 diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..b7ec572 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,355 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "sync" + "usdtman" + + "github.com/gorilla/websocket" +) + +var ( + tronMonitor *usdtman.TronUSDTMonitor + paymentEvents []usdtman.USDTPayment + paymentLock sync.RWMutex + upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, + } + clients = make(map[*websocket.Conn]bool) + clientsLock sync.RWMutex +) + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = "8084" + } + + apiKey := os.Getenv("TRON_API_KEY") + if apiKey == "" { + 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) + + paymentLock.Lock() + paymentEvents = append(paymentEvents, *payment) + if len(paymentEvents) > 100 { + paymentEvents = paymentEvents[len(paymentEvents)-100:] + } + paymentLock.Unlock() + + broadcastPayment(payment) + }) + + http.HandleFunc("/start", startMonitor) + http.HandleFunc("/stop", stopMonitor) + http.HandleFunc("/add-address", addAddress) + http.HandleFunc("/remove-address", removeAddress) + http.HandleFunc("/list-addresses", listAddresses) + http.HandleFunc("/payments", getPayments) + http.HandleFunc("/ws", handleWebSocket) + http.HandleFunc("/", serveIndex) + + log.Printf("🚀 USDTMan Server running on :%s", port) + log.Fatal(http.ListenAndServe(":"+port, nil)) +} + +func jsonResponse(w http.ResponseWriter, success bool, message string, data interface{}) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": success, + "message": message, + "data": data, + }) +} + +func startMonitor(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + jsonResponse(w, false, "Method not allowed", nil) + return + } + + if err := tronMonitor.Start(); err != nil { + jsonResponse(w, false, fmt.Sprintf("启动失败: %v", err), nil) + return + } + + jsonResponse(w, true, "监听已启动", nil) +} + +func stopMonitor(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + jsonResponse(w, false, "Method not allowed", nil) + return + } + + tronMonitor.Stop() + jsonResponse(w, true, "监听已停止", nil) +} + +func addAddress(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + jsonResponse(w, false, "Method not allowed", nil) + return + } + + var req struct { + Address string `json:"address"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonResponse(w, false, "invalid request", nil) + return + } + + if req.Address == "" { + jsonResponse(w, false, "address is required", nil) + return + } + + tronMonitor.AddAddress(req.Address) + jsonResponse(w, true, "地址已添加", map[string]interface{}{ + "address": req.Address, + }) +} + +func removeAddress(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + jsonResponse(w, false, "Method not allowed", nil) + return + } + + var req struct { + Address string `json:"address"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonResponse(w, false, "invalid request", nil) + return + } + + tronMonitor.RemoveAddress(req.Address) + jsonResponse(w, true, "地址已移除", map[string]interface{}{ + "address": req.Address, + }) +} + +func listAddresses(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + jsonResponse(w, false, "Method not allowed", nil) + return + } + + addresses := tronMonitor.GetAllAddresses() + jsonResponse(w, true, "success", map[string]interface{}{ + "addresses": addresses, + "count": len(addresses), + }) +} + +func getPayments(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + jsonResponse(w, false, "Method not allowed", nil) + return + } + + paymentLock.RLock() + events := make([]usdtman.USDTPayment, len(paymentEvents)) + copy(events, paymentEvents) + paymentLock.RUnlock() + + jsonResponse(w, true, "success", map[string]interface{}{ + "payments": events, + "count": len(events), + }) +} + +func handleWebSocket(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println("WebSocket upgrade error:", err) + return + } + + clientsLock.Lock() + clients[conn] = true + clientsLock.Unlock() + + defer func() { + clientsLock.Lock() + delete(clients, conn) + clientsLock.Unlock() + conn.Close() + }() + + for { + _, _, err := conn.ReadMessage() + if err != nil { + break + } + } +} + +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, + } + + clientsLock.RLock() + defer clientsLock.RUnlock() + + for conn := range clients { + conn.WriteJSON(message) + } +} + +func serveIndex(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(indexHTML)) +} + +const indexHTML = ` + + + USDTMan - USDT Monitor + + + +

🚀 USDTMan - USDT Monitor

+ +
+

监听控制

+ + +
+
+ +
+

地址管理

+ + + +
+
+ +
+

收款记录

+ +
+
+ +
+

实时日志 (WebSocket)

+
+
+ + + +` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cd749ad --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module usdtman + +go 1.23.0 + +require github.com/gorilla/websocket v1.5.3 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..25a9fc4 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/tron_usdt_monitor.go b/tron_usdt_monitor.go new file mode 100644 index 0000000..c292b65 --- /dev/null +++ b/tron_usdt_monitor.go @@ -0,0 +1,314 @@ +package usdtman + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "sync" + "time" +) + +// TronUSDTMonitor 监听 USDT 收款 +type TronUSDTMonitor struct { + mu sync.RWMutex + addresses map[string]*AddressInfo // 地址映射 + apiKey string + running bool + ctx context.Context + cancel context.CancelFunc + onPaymentReceived func(*USDTPayment) +} + +// AddressInfo 地址信息 +type AddressInfo struct { + Address string + LastBlock int64 + TotalReceived float64 +} + +// USDTPayment USDT 收款信息 +type USDTPayment struct { + Address string + Amount float64 + TxID string + BlockNumber int64 + Timestamp int64 + From string +} + +// 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"` + } `json:"meta"` +} + +const ( + TronGridAPI = "https://api.trongrid.io" + USDTContractAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" // USDT TRC20 +) + +// NewTronUSDTMonitor 创建监听器 +func NewTronUSDTMonitor(apiKey string) *TronUSDTMonitor { + ctx, cancel := context.WithCancel(context.Background()) + return &TronUSDTMonitor{ + addresses: make(map[string]*AddressInfo), + apiKey: apiKey, + ctx: ctx, + cancel: cancel, + } +} + +// AddAddress 添加监听地址 +func (m *TronUSDTMonitor) AddAddress(address string) { + 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 +} + +// Start 开始监听 +func (m *TronUSDTMonitor) Start() error { + m.mu.Lock() + if m.running { + m.mu.Unlock() + return fmt.Errorf("monitor already running") + } + m.running = true + m.mu.Unlock() + + go m.monitorLoop() + return nil +} + +// Stop 停止监听 +func (m *TronUSDTMonitor) Stop() { + m.mu.Lock() + if !m.running { + m.mu.Unlock() + return + } + m.running = false + m.mu.Unlock() + + m.cancel() +} + +// monitorLoop 监听循环 +func (m *TronUSDTMonitor) monitorLoop() { + ticker := time.NewTicker(3 * time.Second) + defer ticker.Stop() + + for { + select { + case <-m.ctx.Done(): + return + case <-ticker.C: + m.checkAllAddresses() + } + } +} + +// checkAllAddresses 检查所有地址 +func (m *TronUSDTMonitor) checkAllAddresses() { + m.mu.RLock() + addresses := make([]*AddressInfo, 0, len(m.addresses)) + for _, info := range m.addresses { + addresses = append(addresses, info) + } + m.mu.RUnlock() + + for _, addrInfo := range addresses { + m.checkAddress(addrInfo) + } +} + +// checkAddress 检查单个地址 +func (m *TronUSDTMonitor) checkAddress(addrInfo *AddressInfo) { + events, err := m.getUSDTEvents(addrInfo.Address, addrInfo.LastBlock) + if err != nil { + fmt.Printf("Error checking address %s: %v\n", addrInfo.Address, err) + return + } + + for _, event := range events { + if event.EventName != "Transfer" { + continue + } + + to, ok := event.Result["to"].(string) + if !ok || !strings.EqualFold(to, addrInfo.Address) { + continue + } + + from, _ := event.Result["from"].(string) + + valueStr, ok := event.Result["value"].(string) + if !ok { + 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.mu.RLock() + callback := m.onPaymentReceived + m.mu.RUnlock() + + if callback != nil { + callback(payment) + } + } +} + +// 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) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + if m.apiKey != "" { + req.Header.Set("TRON-PRO-API-KEY", m.apiKey) + } + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("TronGrid API error: %d %s", resp.StatusCode, string(body)) + } + + var result TronGridEvent + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + if !result.Success { + return nil, fmt.Errorf("TronGrid API returned success=false") + } + + var filtered []struct { + TransactionID string + BlockNumber int64 + BlockTimestamp int64 + EventName string + Result map[string]interface{} + } + + 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, + }) + } + } + + return filtered, nil +} + +// parseUSDTAmount 解析 USDT 金额(6位小数) +func parseUSDTAmount(valueStr string) float64 { + valueStr = strings.TrimPrefix(valueStr, "0x") + + 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) + } + return addresses +}