diff --git a/README.md b/README.md index fdfedce..31b6363 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,94 @@ # USDTMan -TRON USDT TRC20 收款监听服务 - 基于交易记录扫描 + 区块确认数验证 +TRON USDT TRC20 收款监听服务 -## 功能 +## 特性 -- 实时监听多个 TRON 地址的 USDT 收款 -- 区块确认数验证(默认6个确认) -- WebSocket 实时推送收款通知 -- HTTP API 管理监听地址 +- 扫描交易记录 + 区块确认数验证(默认 >= 6) +- 支持 `big.Int` 处理任意金额 +- WebSocket 实时推送 +- 主动查询历史交易(时间/金额/确认数过滤) +- 代理支持 -## API 使用方式 +## 使用 ```go -// 创建监听器(配置对象方式) uman := usdtman.NewUSDTMan(usdtman.Config{ - Addresses: []string{"地址1", "地址2"}, + Addresses: []string{"TN8nJ...", "TXYZo..."}, APIKey: "YOUR_API_KEY", - QueryInterval: 5 * time.Second, // 查询间隔(可选,默认 5 秒) - MinConfirmations: 6, // 最小确认数(可选,默认 6) - MaxHistoryTxns: 20, // 查询历史交易数(可选,默认 20) - ProxyURL: "http://127.0.0.1:7890", // HTTP/SOCKS5 代理(可选) + QueryInterval: 5 * time.Second, // 查询间隔 + MinConfirmations: 6, // 最小确认数 + MaxHistoryTxns: 20, // 监听查询数量 + ProxyURL: "http://127.0.0.1:7890", // 可选 }) -// 或者使用自定义 Transport -uman := usdtman.NewUSDTMan(usdtman.Config{ - Addresses: []string{"地址1"}, - APIKey: "YOUR_API_KEY", - Transport: &http.Transport{ - Proxy: http.ProxyURL(proxyURL), - // 其他自定义配置... - }, -}) - -// 设置收款回调 uman.OnPaymentComplete(func(payment *usdtman.USDTPayment) { - fmt.Printf("收到 %.6f USDT,确认数: %d\n", payment.Amount, payment.Confirmations) + fmt.Printf("收到 %s USDT (确认: %d)\n", + payment.GetAmountString(), payment.Confirmations) }) -// 启动监听 uman.Start() +defer uman.Stop() -// 停止监听 -uman.Stop() - -// 动态添加/移除地址 -uman.AddAddress("新地址") -uman.RemoveAddress("旧地址") +// 主动查询历史 +payments, _ := uman.QueryTransactions(usdtman.QueryFilter{ + Address: "TN8nJ...", + StartTime: startTimestamp, // 毫秒 + EndTime: endTimestamp, // 毫秒 + MinAmount: big.NewInt(10000000), // >= 10 USDT + MinConfirmations: 6, + Limit: 50, +}) ``` -## 运行 +## 运行 HTTP Server ```bash cd cmd/server -go run main.go +PROXY_URL=http://127.0.0.1:7890 go run main.go ``` 访问 http://localhost:8084 -## 接口 +## HTTP API - `POST /start` - 启动监听 -- `POST /stop` - 停止监听 -- `POST /add-address` - 添加监听地址 +- `POST /stop` - 停止监听 +- `POST /add-address` - 添加地址 - `POST /remove-address` - 移除地址 -- `GET /list-addresses` - 列出所有地址 -- `GET /payments` - 获取收款历史 -- `WS /ws` - WebSocket 连接 +- `GET /list-addresses` - 地址列表 +- `GET /payments` - 监听缓存记录(最多100条) +- `POST /query` - 主动查询历史 +- `WS /ws` - WebSocket 推送 -## 确认机制 +### 主动查询示例 -- 扫描地址的最近交易记录 -- 计算区块确认数(当前区块 - 交易区块) -- 仅在确认数 >= 6 时触发回调 -- 自动去重,避免重复处理 +```bash +curl -X POST http://localhost:8084/query \ + -H "Content-Type: application/json" \ + -d '{ + "address": "TN8nJ...", + "startTime": 1770000000000, + "endTime": 1770100000000, + "minAmount": "10000000", + "minConfirmations": 6, + "limit": 50 + }' +``` + +## 配置项 + +| 参数 | 类型 | 默认 | 说明 | +|------|------|------|------| +| `Addresses` | []string | [] | 监听地址 | +| `APIKey` | string | - | TronGrid API Key | +| `QueryInterval` | time.Duration | 5s | 查询间隔 | +| `MinConfirmations` | int64 | 6 | 最小确认数 | +| `MaxHistoryTxns` | int | 20 | 监听查询数量 | +| `ProxyURL` | string | - | HTTP/SOCKS5 代理 | +| `Transport` | http.RoundTripper | nil | 自定义 Transport | + +## 监听 vs 查询 + +- **监听** (`/payments`): 自动轮询最近交易,达到确认数后触发回调,缓存最多 100 条 +- **查询** (`/query`): 主动查询链上历史,支持条件过滤,不触发回调 diff --git a/cmd/server/main.go b/cmd/server/main.go index 9884517..bfb9fb6 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "log" + "math/big" "net/http" "os" "sync" @@ -79,6 +80,7 @@ func main() { http.HandleFunc("/remove-address", removeAddress) http.HandleFunc("/list-addresses", listAddresses) http.HandleFunc("/payments", getPayments) + http.HandleFunc("/query", queryTransactions) http.HandleFunc("/ws", handleWebSocket) http.HandleFunc("/", serveIndex) @@ -196,6 +198,59 @@ func getPayments(w http.ResponseWriter, r *http.Request) { }) } +func queryTransactions(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"` + StartTime int64 `json:"startTime"` // 毫秒时间戳 + EndTime int64 `json:"endTime"` // 毫秒时间戳 + MinAmount string `json:"minAmount"` // 字符串格式的金额 + MinConfirmations int64 `json:"minConfirmations"` // 最小确认数 + Limit int `json:"limit"` // 查询数量 + } + + 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 + } + + filter := usdtman.QueryFilter{ + Address: req.Address, + StartTime: req.StartTime, + EndTime: req.EndTime, + MinConfirmations: req.MinConfirmations, + Limit: req.Limit, + } + + // 解析最小金额 + if req.MinAmount != "" { + minAmount := new(big.Int) + if _, ok := minAmount.SetString(req.MinAmount, 10); ok { + filter.MinAmount = minAmount + } + } + + payments, err := uman.QueryTransactions(filter) + if err != nil { + jsonResponse(w, false, fmt.Sprintf("查询失败: %v", err), nil) + return + } + + jsonResponse(w, true, "success", map[string]interface{}{ + "payments": payments, + "count": len(payments), + }) +} + func handleWebSocket(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { diff --git a/index.html b/index.html index 2e7c358..5fa40af 100644 --- a/index.html +++ b/index.html @@ -34,11 +34,23 @@
-

收款记录

+

收款记录(监听缓存)

+
+

主动查询交易记录

+ + + + + + + +
+
+

实时日志 (WebSocket)

@@ -113,7 +125,7 @@ data.data.payments.map(p => '' + new Date(p.Timestamp).toLocaleString() + '' + '' + p.Address.substring(0, 10) + '...' + - '' + (p.Amount / 1000000).toFixed(6) + ' USDT' + + '' + p.Amount.toFixed(6) + ' USDT' + '' + p.From.substring(0, 10) + '...' + '' + p.Confirmations + '' + '' + @@ -125,6 +137,52 @@ } } + async function queryTransactions() { + const address = document.getElementById('queryAddress').value.trim(); + if (!address) { + alert('请输入地址'); + return; + } + + const startTime = document.getElementById('startTime').value; + const endTime = document.getElementById('endTime').value; + const minAmount = document.getElementById('minAmount').value; + const minConfirmations = document.getElementById('minConfirmations').value; + const limit = document.getElementById('queryLimit').value; + + const body = { + address: address, + startTime: startTime ? new Date(startTime).getTime() : 0, + endTime: endTime ? new Date(endTime).getTime() : 0, + minAmount: minAmount ? (parseFloat(minAmount) * 1000000).toString() : '', + minConfirmations: parseInt(minConfirmations) || 0, + limit: parseInt(limit) || 50, + }; + + const res = await fetch('/query', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + + const data = await res.json(); + if (data.success && data.data.payments && data.data.payments.length > 0) { + const html = '
找到 ' + data.data.count + ' 条记录
' + + '' + + data.data.payments.map(p => + '' + + '' + + '' + + '' + + '' + ).join('') + '
时间金额来源确认数TxID
' + new Date(p.Timestamp).toLocaleString() + '' + p.Amount.toFixed(6) + ' USDT' + p.From.substring(0, 10) + '...' + p.Confirmations + '' + + p.TxID.substring(0, 10) + '...
'; + document.getElementById('queryResults').innerHTML = html; + } else { + document.getElementById('queryResults').innerHTML = '
' + (data.message || '未找到记录') + '
'; + } + } + listAddresses(); getPayments(); diff --git a/log/blocks.json b/log/blocks.json new file mode 100644 index 0000000..717e4ad --- /dev/null +++ b/log/blocks.json @@ -0,0 +1,27 @@ +{ + "id": "2bf9c73deb56166ad38906e732a898646cfc3db15e0daab0e33a6c4e3bf0509e", + "blockNumber": 79809798, + "blockTimeStamp": 1770093843000, + "contractResult": [ + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "contract_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", + "receipt": { + "energy_usage": 130285, + "energy_usage_total": 130285, + "net_usage": 345, + "result": "SUCCESS", + "energy_penalty_total": 100635 + }, + "log": [ + { + "address": "a614f803b6fd780986a42c78ec9c7f77e6ded13c", + "topics": [ + "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0000000000000000000000006158fe96b950111b8f1f31249fedda484e941312", + "000000000000000000000000e5fcae42ed4ecf7e23e4a561aded72c69aeeabc0" + ], + "data": "0000000000000000000000000000000000000000000000000000000000ca54e0" + } + ] +} \ No newline at end of file diff --git a/log/transactions.json b/log/transactions.json new file mode 100644 index 0000000..ed5c66c --- /dev/null +++ b/log/transactions.json @@ -0,0 +1,23 @@ +{ + "data": [ + { + "transaction_id": "2bf9c73deb56166ad38906e732a898646cfc3db15e0daab0e33a6c4e3bf0509e", + "token_info": { + "symbol": "USDT", + "address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + "decimals": 6, + "name": "Tether USD" + }, + "block_timestamp": 1770093843000, + "from": "TJqwA7SoZnERE4zW5uDEiPkbz4B66h9TFj", + "to": "TWwGSYwpSzT6GTBr4AQw9QF6m4VVui3UGc", + "type": "Transfer", + "value": "13260000" + } + ], + "success": true, + "meta": { + "at": 1770094054104, + "page_size": 1 + } +} \ No newline at end of file diff --git a/tron_usdt_monitor.go b/tron_usdt_monitor.go index 58dcc2f..ae79708 100644 --- a/tron_usdt_monitor.go +++ b/tron_usdt_monitor.go @@ -41,6 +41,16 @@ type USDTMan struct { httpClient *http.Client // HTTP 客户端 } +// QueryFilter 查询过滤条件 +type QueryFilter struct { + Address string // 地址(必填) + StartTime int64 // 开始时间戳(毫秒),0 表示不限制 + EndTime int64 // 结束时间戳(毫秒),0 表示不限制 + MinAmount *big.Int // 最小金额(micro USDT),nil 表示不限制 + MinConfirmations int64 // 最小确认数,0 表示不限制 + Limit int // 查询数量限制,0 使用默认值 +} + // USDTPayment USDT 收款信息 type USDTPayment struct { Address string @@ -351,6 +361,43 @@ func (m *USDTMan) getUSDTTransactions(address string) ([]TronGridTransaction, er return result.Data, nil } +// getUSDTTransactionsWithLimit 获取指定数量的交易 +func (m *USDTMan) getUSDTTransactionsWithLimit(address string, limit int) ([]TronGridTransaction, error) { + url := fmt.Sprintf("%s/v1/accounts/%s/transactions/trc20?limit=%d&contract_address=%s&only_to=true", + TronGridAPI, address, limit, USDTContractAddress) + + 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) + } + + resp, err := m.httpClient.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 TronGridAccountTransactions + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + return result.Data, nil +} + // getTransactionBlock 获取交易的区块号 func (m *USDTMan) getTransactionBlock(txID string) (int64, error) { url := fmt.Sprintf("%s/wallet/gettransactioninfobyid?value=%s", TronGridAPI, txID) @@ -466,3 +513,85 @@ func (m *USDTMan) RemoveAddress(address string) { } } } + +// QueryTransactions 主动查询交易记录(带过滤条件) +func (m *USDTMan) QueryTransactions(filter QueryFilter) ([]*USDTPayment, error) { + if filter.Address == "" { + return nil, fmt.Errorf("address is required") + } + + // 设置默认 limit + limit := filter.Limit + if limit == 0 { + limit = m.maxHistoryTxns + } + + // 获取交易列表 + transactions, err := m.getUSDTTransactionsWithLimit(filter.Address, limit) + if err != nil { + return nil, err + } + + // 获取当前区块(用于计算确认数) + currentBlock, err := m.getCurrentBlock() + if err != nil { + return nil, err + } + + var results []*USDTPayment + + for _, txn := range transactions { + // 时间过滤 + if filter.StartTime > 0 && txn.BlockTimestamp < filter.StartTime { + continue + } + if filter.EndTime > 0 && txn.BlockTimestamp > filter.EndTime { + continue + } + + // 只处理转入该地址的交易 + if !strings.EqualFold(txn.To, filter.Address) { + continue + } + + // 解析金额 + amount := new(big.Int) + amount, ok := amount.SetString(txn.Value, 10) + if !ok || amount.Sign() <= 0 { + continue + } + + // 金额过滤 + if filter.MinAmount != nil && amount.Cmp(filter.MinAmount) < 0 { + continue + } + + // 获取区块号 + blockNumber, err := m.getTransactionBlock(txn.TransactionID) + if err != nil { + continue + } + + // 计算确认数 + confirmations := currentBlock - blockNumber + + // 确认数过滤 + if filter.MinConfirmations > 0 && confirmations < filter.MinConfirmations { + continue + } + + payment := &USDTPayment{ + Address: filter.Address, + Amount: amount, + TxID: txn.TransactionID, + BlockNumber: blockNumber, + Timestamp: txn.BlockTimestamp, + From: txn.From, + Confirmations: confirmations, + } + + results = append(results, payment) + } + + return results, nil +}