Compare commits

...

5 Commits

Author SHA1 Message Date
753bd4a4d6 增加主动查询 api 2026-02-03 15:05:06 +08:00
922f6df4e8 fix bugs 2026-02-03 13:07:29 +08:00
8965b96f0e fix update 2026-02-03 00:10:43 +08:00
79c1c710a2 usdtman 2026-02-03 00:01:41 +08:00
3d3e814879 first blood 2026-02-02 23:51:17 +08:00
8 changed files with 1240 additions and 2 deletions

View File

@@ -1,3 +1,94 @@
# usdtman
# USDTMan
usdtman for ipay server
TRON USDT TRC20 收款监听服务
## 特性
- 扫描交易记录 + 区块确认数验证(默认 >= 6
- 支持 `big.Int` 处理任意金额
- WebSocket 实时推送
- 主动查询历史交易(时间/金额/确认数过滤)
- 代理支持
## 使用
```go
uman := usdtman.NewUSDTMan(usdtman.Config{
Addresses: []string{"TN8nJ...", "TXYZo..."},
APIKey: "YOUR_API_KEY",
QueryInterval: 5 * time.Second, // 查询间隔
MinConfirmations: 6, // 最小确认数
MaxHistoryTxns: 20, // 监听查询数量
ProxyURL: "http://127.0.0.1:7890", // 可选
})
uman.OnPaymentComplete(func(payment *usdtman.USDTPayment) {
fmt.Printf("收到 %s USDT (确认: %d)\n",
payment.GetAmountString(), payment.Confirmations)
})
uman.Start()
defer uman.Stop()
// 主动查询历史
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
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 /remove-address` - 移除地址
- `GET /list-addresses` - 地址列表
- `GET /payments` - 监听缓存记录最多100条
- `POST /query` - 主动查询历史
- `WS /ws` - WebSocket 推送
### 主动查询示例
```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`): 主动查询链上历史,支持条件过滤,不触发回调

303
cmd/server/main.go Normal file
View File

@@ -0,0 +1,303 @@
package main
import (
"encoding/json"
"fmt"
"log"
"math/big"
"net/http"
"os"
"sync"
"time"
"usdtman"
"github.com/gorilla/websocket"
)
var (
uman *usdtman.USDTMan
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"
}
// 初始化 USDTMan
proxyURL := os.Getenv("PROXY_URL") // 例如: http://127.0.0.1:7890
config := usdtman.Config{
Addresses: []string{}, // 初始无地址,可通过 API 添加
APIKey: apiKey,
QueryInterval: 5 * time.Second, // 每 5 秒查询一次
MinConfirmations: 6, // 6 个确认
MaxHistoryTxns: 20, // 每次查询最多 20 条历史交易
}
// 如果设置了代理
if proxyURL != "" {
config.ProxyURL = proxyURL
log.Printf("✅ 使用代理: %s", proxyURL)
}
uman = usdtman.NewUSDTMan(config)
// 设置收款回调
uman.OnPaymentComplete(func(payment *usdtman.USDTPayment) {
log.Printf("💰 收到 USDT: %s -> %s USDT (确认数: %d, TxID: %s)",
payment.From, payment.GetAmountString(), payment.Confirmations, payment.TxID)
paymentLock.Lock()
paymentEvents = append(paymentEvents, *payment)
if len(paymentEvents) > 100 {
paymentEvents = paymentEvents[len(paymentEvents)-100:]
}
paymentLock.Unlock()
broadcastPayment(payment)
})
// test code
uman.AddAddress("TWwGSYwpSzT6GTBr4AQw9QF6m4VVui3UGc") // tronlink trc20 gasfree 地址
uman.Start()
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("/query", queryTransactions)
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 := uman.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
}
uman.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
}
uman.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
}
uman.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 := uman.GetAddresses()
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 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 {
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.GetAmountFloat(),
"amountRaw": payment.Amount.String(),
"from": payment.From,
"txId": payment.TxID,
"block": payment.BlockNumber,
"time": payment.Timestamp,
"confirmations": payment.Confirmations,
}
clientsLock.RLock()
defer clientsLock.RUnlock()
for conn := range clients {
conn.WriteJSON(message)
}
}
func serveIndex(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "index.html")
}

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module usdtman
go 1.23.0
require github.com/gorilla/websocket v1.5.3

2
go.sum Normal file
View File

@@ -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=

190
index.html Normal file
View File

@@ -0,0 +1,190 @@
<!DOCTYPE html>
<html>
<head>
<title>USDTMan - USDT Monitor</title>
<style>
body { font-family: Arial; max-width: 1000px; margin: 20px auto; padding: 20px; }
h1 { color: #333; }
.section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
button { padding: 8px 15px; margin: 5px; cursor: pointer; }
input { padding: 8px; margin: 5px; width: 400px; }
.success { color: green; }
.error { color: red; }
#log { max-height: 300px; overflow-y: auto; background: #f5f5f5; padding: 10px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
</style>
</head>
<body>
<h1>🚀 USDTMan - USDT Monitor</h1>
<div class="section">
<h3>监听控制</h3>
<button onclick="startMonitor()">启动监听</button>
<button onclick="stopMonitor()">停止监听</button>
<div id="status"></div>
</div>
<div class="section">
<h3>地址管理</h3>
<input type="text" id="address" placeholder="输入 TRON 地址">
<button onclick="addAddress()">添加地址</button>
<button onclick="listAddresses()">刷新列表</button>
<div id="addresses"></div>
</div>
<div class="section">
<h3>收款记录(监听缓存)</h3>
<button onclick="getPayments()">刷新</button>
<div id="payments"></div>
</div>
<div class="section">
<h3>主动查询交易记录</h3>
<input type="text" id="queryAddress" placeholder="TRON 地址">
<input type="datetime-local" id="startTime" placeholder="开始时间">
<input type="datetime-local" id="endTime" placeholder="结束时间">
<input type="number" id="minAmount" placeholder="最小金额 (USDT)" step="0.01">
<input type="number" id="minConfirmations" placeholder="最小确认数" value="6">
<input type="number" id="queryLimit" placeholder="查询数量" value="50">
<button onclick="queryTransactions()">查询</button>
<div id="queryResults"></div>
</div>
<div class="section">
<h3>实时日志 (WebSocket)</h3>
<div id="log"></div>
</div>
<script>
const ws = new WebSocket('ws://' + location.host + '/ws');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'usdt_payment') {
addLog('💰 收到 USDT: ' + data.from.substring(0, 10) + '... -> ' + data.amount.toFixed(6) + ' USDT (确认数: ' + data.confirmations + ')');
getPayments();
}
};
function addLog(msg) {
const log = document.getElementById('log');
const time = new Date().toLocaleTimeString();
log.innerHTML = '[' + time + '] ' + msg + '<br>' + log.innerHTML;
}
async function startMonitor() {
const res = await fetch('/start', { method: 'POST' });
const data = await res.json();
document.getElementById('status').innerHTML = '<span class="success">' + data.message + '</span>';
}
async function stopMonitor() {
const res = await fetch('/stop', { method: 'POST' });
const data = await res.json();
document.getElementById('status').innerHTML = '<span class="error">' + data.message + '</span>';
}
async function addAddress() {
const address = document.getElementById('address').value;
if (!address) return alert('请输入地址');
const res = await fetch('/add-address', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address })
});
const data = await res.json();
alert(data.message);
listAddresses();
}
async function listAddresses() {
const res = await fetch('/list-addresses');
const data = await res.json();
const html = data.data.addresses.map(addr =>
'<div>' + addr + ' <button onclick="removeAddress(\'' + addr + '\')">移除</button></div>'
).join('');
document.getElementById('addresses').innerHTML = html || '暂无地址';
}
async function removeAddress(addr) {
const res = await fetch('/remove-address', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address: addr })
});
listAddresses();
}
async function getPayments() {
const res = await fetch('/payments');
const data = await res.json();
if (data.data.payments && data.data.payments.length > 0) {
const html = '<table><tr><th>时间</th><th>地址</th><th>金额</th><th>来源</th><th>确认数</th><th>TxID</th></tr>' +
data.data.payments.map(p =>
'<tr><td>' + new Date(p.Timestamp).toLocaleString() + '</td>' +
'<td>' + p.Address.substring(0, 10) + '...</td>' +
'<td>' + p.Amount.toFixed(6) + ' USDT</td>' +
'<td>' + p.From.substring(0, 10) + '...</td>' +
'<td>' + p.Confirmations + '</td>' +
'<td><a href="https://tronscan.org/#/transaction/' + p.TxID + '" target="_blank">' +
p.TxID.substring(0, 10) + '...</a></td></tr>'
).join('') + '</table>';
document.getElementById('payments').innerHTML = html;
} else {
document.getElementById('payments').innerHTML = '暂无记录';
}
}
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 = '<div class="success">找到 ' + data.data.count + ' 条记录</div>' +
'<table><tr><th>时间</th><th>金额</th><th>来源</th><th>确认数</th><th>TxID</th></tr>' +
data.data.payments.map(p =>
'<tr><td>' + new Date(p.Timestamp).toLocaleString() + '</td>' +
'<td>' + p.Amount.toFixed(6) + ' USDT</td>' +
'<td>' + p.From.substring(0, 10) + '...</td>' +
'<td>' + p.Confirmations + '</td>' +
'<td><a href="https://tronscan.org/#/transaction/' + p.TxID + '" target="_blank">' +
p.TxID.substring(0, 10) + '...</a></td></tr>'
).join('') + '</table>';
document.getElementById('queryResults').innerHTML = html;
} else {
document.getElementById('queryResults').innerHTML = '<div class="error">' + (data.message || '未找到记录') + '</div>';
}
}
listAddresses();
getPayments();
</script>
</body>
</html>

27
log/blocks.json Normal file
View File

@@ -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"
}
]
}

23
log/transactions.json Normal file
View File

@@ -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
}
}

597
tron_usdt_monitor.go Normal file
View File

@@ -0,0 +1,597 @@
package usdtman
import (
"context"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
// Config USDTMan 配置
type Config struct {
Addresses []string // 监听地址列表
APIKey string // TronGrid API Key
QueryInterval time.Duration // 查询间隔(秒),默认 5 秒
MinConfirmations int64 // 最小确认数,默认 6
MaxHistoryTxns int // 每次查询的最大历史交易数,默认 20
ProxyURL string // HTTP/SOCKS5 代理地址,例如 "http://127.0.0.1:7890" 或 "socks5://127.0.0.1:1080"
Transport http.RoundTripper // 自定义 Transport优先级高于 ProxyURL
}
// USDTMan USDT 监听管理器
type USDTMan struct {
mu sync.RWMutex
addresses []string
apiKey string
queryInterval time.Duration
running bool
ctx context.Context
cancel context.CancelFunc
onPaymentComplete func(*USDTPayment)
minConfirmations int64
maxHistoryTxns int
processedTxns map[string]bool // 已处理的交易
txnMutex sync.RWMutex
httpClient *http.Client // HTTP 客户端
}
// QueryFilter 查询过滤条件
type QueryFilter struct {
Address string // 地址(必填)
StartTime int64 // 开始时间戳毫秒0 表示不限制
EndTime int64 // 结束时间戳毫秒0 表示不限制
MinAmount *big.Int // 最小金额micro USDTnil 表示不限制
MinConfirmations int64 // 最小确认数0 表示不限制
Limit int // 查询数量限制0 使用默认值
}
// USDTPayment USDT 收款信息
type USDTPayment struct {
Address string
Amount *big.Int // 原始金额micro USDT10^-6例如 13260000 = 13.26 USDT
TxID string
BlockNumber int64
Timestamp int64
From string
Confirmations int64
}
// GetAmountFloat 获取浮点数金额USDT
func (p *USDTPayment) GetAmountFloat() float64 {
divisor := new(big.Float).SetInt64(1000000)
amount := new(big.Float).SetInt(p.Amount)
result := new(big.Float).Quo(amount, divisor)
f, _ := result.Float64()
return f
}
// GetAmountString 获取字符串金额USDT保留6位小数
func (p *USDTPayment) GetAmountString() string {
return fmt.Sprintf("%.6f", p.GetAmountFloat())
}
// MarshalJSON 自定义 JSON 序列化
func (p *USDTPayment) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Address string `json:"Address"`
Amount float64 `json:"Amount"`
AmountRaw string `json:"AmountRaw"`
TxID string `json:"TxID"`
BlockNumber int64 `json:"BlockNumber"`
Timestamp int64 `json:"Timestamp"`
From string `json:"From"`
Confirmations int64 `json:"Confirmations"`
}{
Address: p.Address,
Amount: p.GetAmountFloat(),
AmountRaw: p.Amount.String(),
TxID: p.TxID,
BlockNumber: p.BlockNumber,
Timestamp: p.Timestamp,
From: p.From,
Confirmations: p.Confirmations,
})
}
// TronGridTransaction TRC20 交易信息
type TronGridTransaction struct {
TransactionID string `json:"transaction_id"`
BlockTimestamp int64 `json:"block_timestamp"`
From string `json:"from"`
To string `json:"to"`
Type string `json:"type"`
Value string `json:"value"`
TokenInfo struct {
Symbol string `json:"symbol"`
Address string `json:"address"`
Decimals int `json:"decimals"`
Name string `json:"name"`
} `json:"token_info"`
}
// TronGridAccountTransactions 账户交易列表
type TronGridAccountTransactions struct {
Success bool `json:"success"`
Data []TronGridTransaction `json:"data"`
Meta struct {
At int64 `json:"at"`
PageSize int `json:"page_size"`
} `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
)
// NewUSDTMan 创建监听管理器
func NewUSDTMan(config Config) *USDTMan {
ctx, cancel := context.WithCancel(context.Background())
// 设置默认值
if config.QueryInterval == 0 {
config.QueryInterval = 5 * time.Second
}
if config.MinConfirmations == 0 {
config.MinConfirmations = 6
}
if config.MaxHistoryTxns == 0 {
config.MaxHistoryTxns = 20
}
// 创建 HTTP 客户端
httpClient := &http.Client{
Timeout: 10 * time.Second,
}
// 配置代理
if config.Transport != nil {
// 使用自定义 Transport
httpClient.Transport = config.Transport
} else if config.ProxyURL != "" {
// 使用代理 URL
proxyURL, err := url.Parse(config.ProxyURL)
if err == nil {
httpClient.Transport = &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
} else {
fmt.Printf("⚠️ 代理 URL 解析失败: %v使用直连\n", err)
}
}
return &USDTMan{
addresses: config.Addresses,
apiKey: config.APIKey,
queryInterval: config.QueryInterval,
minConfirmations: config.MinConfirmations,
maxHistoryTxns: config.MaxHistoryTxns,
httpClient: httpClient,
ctx: ctx,
cancel: cancel,
processedTxns: make(map[string]bool),
}
}
// OnPaymentComplete 设置收款回调
func (m *USDTMan) OnPaymentComplete(callback func(*USDTPayment)) {
m.mu.Lock()
defer m.mu.Unlock()
m.onPaymentComplete = callback
}
// Start 开始监听
func (m *USDTMan) 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 *USDTMan) Stop() {
m.mu.Lock()
if !m.running {
m.mu.Unlock()
return
}
m.running = false
m.mu.Unlock()
m.cancel()
}
// monitorLoop 监听循环
func (m *USDTMan) monitorLoop() {
ticker := time.NewTicker(m.queryInterval)
defer ticker.Stop()
for {
select {
case <-m.ctx.Done():
return
case <-ticker.C:
m.checkAllAddresses()
}
}
}
// checkAllAddresses 检查所有地址
func (m *USDTMan) checkAllAddresses() {
m.mu.RLock()
addresses := make([]string, len(m.addresses))
copy(addresses, m.addresses)
m.mu.RUnlock()
for _, address := range addresses {
m.checkAddress(address)
}
}
// checkAddress 检查单个地址的交易
func (m *USDTMan) checkAddress(address string) {
transactions, err := m.getUSDTTransactions(address)
if err != nil {
fmt.Printf("Error checking address %s: %v\n", address, err)
return
}
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.TransactionID]
m.txnMutex.RUnlock()
if processed {
continue
}
// 获取交易的区块号(需要通过 transaction_id 查询)
blockNumber, err := m.getTransactionBlock(txn.TransactionID)
if err != nil {
fmt.Printf("Error getting transaction block: %v\n", err)
continue
}
// 计算确认数
confirmations := currentBlock - blockNumber
if confirmations < m.minConfirmations {
continue
}
// 检查是否是转入该地址
if !strings.EqualFold(txn.To, address) {
continue
}
// 解析金额(使用 big.Int
amount := new(big.Int)
amount, ok := amount.SetString(txn.Value, 10)
if !ok || amount.Sign() <= 0 {
continue
}
payment := &USDTPayment{
Address: address,
Amount: amount,
TxID: txn.TransactionID,
BlockNumber: blockNumber,
Timestamp: txn.BlockTimestamp,
From: txn.From,
Confirmations: confirmations,
}
// 标记为已处理
m.txnMutex.Lock()
m.processedTxns[txn.TransactionID] = true
m.txnMutex.Unlock()
// 触发回调
m.mu.RLock()
callback := m.onPaymentComplete
m.mu.RUnlock()
if callback != nil {
callback(payment)
}
}
}
// getUSDTTransactions 获取地址的 USDT 交易
func (m *USDTMan) getUSDTTransactions(address string) ([]TronGridTransaction, error) {
url := fmt.Sprintf("%s/v1/accounts/%s/transactions/trc20?limit=%d&contract_address=%s&only_to=true",
TronGridAPI, address, m.maxHistoryTxns, 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
}
// 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)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return 0, err
}
if m.apiKey != "" {
req.Header.Set("TRON-PRO-API-KEY", m.apiKey)
}
resp, err := m.httpClient.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 result struct {
BlockNumber int64 `json:"blockNumber"`
}
fmt.Println("body", string(body))
if err := json.Unmarshal(body, &result); err != nil {
return 0, err
}
return result.BlockNumber, 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
}
if m.apiKey != "" {
req.Header.Set("TRON-PRO-API-KEY", m.apiKey)
}
resp, err := m.httpClient.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 解析交易(已废弃,直接在 checkAddress 中处理)
// 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
}
}
m.addresses = append(m.addresses, address)
}
// RemoveAddress 移除监听地址
func (m *USDTMan) RemoveAddress(address string) {
m.mu.Lock()
defer m.mu.Unlock()
for i, addr := range m.addresses {
if addr == address {
m.addresses = append(m.addresses[:i], m.addresses[i+1:]...)
return
}
}
}
// 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
}