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