Compare commits
2 Commits
8965b96f0e
...
753bd4a4d6
| Author | SHA1 | Date | |
|---|---|---|---|
| 753bd4a4d6 | |||
| 922f6df4e8 |
107
README.md
107
README.md
@@ -1,75 +1,94 @@
|
|||||||
# USDTMan
|
# USDTMan
|
||||||
|
|
||||||
TRON USDT TRC20 收款监听服务 - 基于交易记录扫描 + 区块确认数验证
|
TRON USDT TRC20 收款监听服务
|
||||||
|
|
||||||
## 功能
|
## 特性
|
||||||
|
|
||||||
- 实时监听多个 TRON 地址的 USDT 收款
|
- 扫描交易记录 + 区块确认数验证(默认 >= 6)
|
||||||
- 区块确认数验证(默认6个确认)
|
- 支持 `big.Int` 处理任意金额
|
||||||
- WebSocket 实时推送收款通知
|
- WebSocket 实时推送
|
||||||
- HTTP API 管理监听地址
|
- 主动查询历史交易(时间/金额/确认数过滤)
|
||||||
|
- 代理支持
|
||||||
|
|
||||||
## API 使用方式
|
## 使用
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// 创建监听器(配置对象方式)
|
|
||||||
uman := usdtman.NewUSDTMan(usdtman.Config{
|
uman := usdtman.NewUSDTMan(usdtman.Config{
|
||||||
Addresses: []string{"地址1", "地址2"},
|
Addresses: []string{"TN8nJ...", "TXYZo..."},
|
||||||
APIKey: "YOUR_API_KEY",
|
APIKey: "YOUR_API_KEY",
|
||||||
QueryInterval: 5 * time.Second, // 查询间隔(可选,默认 5 秒)
|
QueryInterval: 5 * time.Second, // 查询间隔
|
||||||
MinConfirmations: 6, // 最小确认数(可选,默认 6)
|
MinConfirmations: 6, // 最小确认数
|
||||||
MaxHistoryTxns: 20, // 查询历史交易数(可选,默认 20)
|
MaxHistoryTxns: 20, // 监听查询数量
|
||||||
ProxyURL: "http://127.0.0.1:7890", // HTTP/SOCKS5 代理(可选)
|
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) {
|
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()
|
uman.Start()
|
||||||
|
defer uman.Stop()
|
||||||
|
|
||||||
// 停止监听
|
// 主动查询历史
|
||||||
uman.Stop()
|
payments, _ := uman.QueryTransactions(usdtman.QueryFilter{
|
||||||
|
Address: "TN8nJ...",
|
||||||
// 动态添加/移除地址
|
StartTime: startTimestamp, // 毫秒
|
||||||
uman.AddAddress("新地址")
|
EndTime: endTimestamp, // 毫秒
|
||||||
uman.RemoveAddress("旧地址")
|
MinAmount: big.NewInt(10000000), // >= 10 USDT
|
||||||
|
MinConfirmations: 6,
|
||||||
|
Limit: 50,
|
||||||
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## 运行
|
## 运行 HTTP Server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd cmd/server
|
cd cmd/server
|
||||||
go run main.go
|
PROXY_URL=http://127.0.0.1:7890 go run main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
访问 http://localhost:8084
|
访问 http://localhost:8084
|
||||||
|
|
||||||
## 接口
|
## HTTP API
|
||||||
|
|
||||||
- `POST /start` - 启动监听
|
- `POST /start` - 启动监听
|
||||||
- `POST /stop` - 停止监听
|
- `POST /stop` - 停止监听
|
||||||
- `POST /add-address` - 添加监听地址
|
- `POST /add-address` - 添加地址
|
||||||
- `POST /remove-address` - 移除地址
|
- `POST /remove-address` - 移除地址
|
||||||
- `GET /list-addresses` - 列出所有地址
|
- `GET /list-addresses` - 地址列表
|
||||||
- `GET /payments` - 获取收款历史
|
- `GET /payments` - 监听缓存记录(最多100条)
|
||||||
- `WS /ws` - WebSocket 连接
|
- `POST /query` - 主动查询历史
|
||||||
|
- `WS /ws` - WebSocket 推送
|
||||||
|
|
||||||
## 确认机制
|
### 主动查询示例
|
||||||
|
|
||||||
- 扫描地址的最近交易记录
|
```bash
|
||||||
- 计算区块确认数(当前区块 - 交易区块)
|
curl -X POST http://localhost:8084/query \
|
||||||
- 仅在确认数 >= 6 时触发回调
|
-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`): 主动查询链上历史,支持条件过滤,不触发回调
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -56,8 +57,8 @@ func main() {
|
|||||||
|
|
||||||
// 设置收款回调
|
// 设置收款回调
|
||||||
uman.OnPaymentComplete(func(payment *usdtman.USDTPayment) {
|
uman.OnPaymentComplete(func(payment *usdtman.USDTPayment) {
|
||||||
log.Printf("💰 收到 USDT: %s -> %.6f USDT (确认数: %d, TxID: %s)",
|
log.Printf("💰 收到 USDT: %s -> %s USDT (确认数: %d, TxID: %s)",
|
||||||
payment.From, payment.Amount, payment.Confirmations, payment.TxID)
|
payment.From, payment.GetAmountString(), payment.Confirmations, payment.TxID)
|
||||||
|
|
||||||
paymentLock.Lock()
|
paymentLock.Lock()
|
||||||
paymentEvents = append(paymentEvents, *payment)
|
paymentEvents = append(paymentEvents, *payment)
|
||||||
@@ -69,12 +70,17 @@ func main() {
|
|||||||
broadcastPayment(payment)
|
broadcastPayment(payment)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// test code
|
||||||
|
uman.AddAddress("TWwGSYwpSzT6GTBr4AQw9QF6m4VVui3UGc") // tronlink trc20 gasfree 地址
|
||||||
|
uman.Start()
|
||||||
|
|
||||||
http.HandleFunc("/start", startMonitor)
|
http.HandleFunc("/start", startMonitor)
|
||||||
http.HandleFunc("/stop", stopMonitor)
|
http.HandleFunc("/stop", stopMonitor)
|
||||||
http.HandleFunc("/add-address", addAddress)
|
http.HandleFunc("/add-address", addAddress)
|
||||||
http.HandleFunc("/remove-address", removeAddress)
|
http.HandleFunc("/remove-address", removeAddress)
|
||||||
http.HandleFunc("/list-addresses", listAddresses)
|
http.HandleFunc("/list-addresses", listAddresses)
|
||||||
http.HandleFunc("/payments", getPayments)
|
http.HandleFunc("/payments", getPayments)
|
||||||
|
http.HandleFunc("/query", queryTransactions)
|
||||||
http.HandleFunc("/ws", handleWebSocket)
|
http.HandleFunc("/ws", handleWebSocket)
|
||||||
http.HandleFunc("/", serveIndex)
|
http.HandleFunc("/", serveIndex)
|
||||||
|
|
||||||
@@ -192,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) {
|
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -222,7 +281,8 @@ func broadcastPayment(payment *usdtman.USDTPayment) {
|
|||||||
message := map[string]interface{}{
|
message := map[string]interface{}{
|
||||||
"type": "usdt_payment",
|
"type": "usdt_payment",
|
||||||
"address": payment.Address,
|
"address": payment.Address,
|
||||||
"amount": payment.Amount,
|
"amount": payment.GetAmountFloat(),
|
||||||
|
"amountRaw": payment.Amount.String(),
|
||||||
"from": payment.From,
|
"from": payment.From,
|
||||||
"txId": payment.TxID,
|
"txId": payment.TxID,
|
||||||
"block": payment.BlockNumber,
|
"block": payment.BlockNumber,
|
||||||
|
|||||||
60
index.html
60
index.html
@@ -34,11 +34,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>收款记录</h3>
|
<h3>收款记录(监听缓存)</h3>
|
||||||
<button onclick="getPayments()">刷新</button>
|
<button onclick="getPayments()">刷新</button>
|
||||||
<div id="payments"></div>
|
<div id="payments"></div>
|
||||||
</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">
|
<div class="section">
|
||||||
<h3>实时日志 (WebSocket)</h3>
|
<h3>实时日志 (WebSocket)</h3>
|
||||||
<div id="log"></div>
|
<div id="log"></div>
|
||||||
@@ -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 = '<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();
|
listAddresses();
|
||||||
getPayments();
|
getPayments();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
27
log/blocks.json
Normal file
27
log/blocks.json
Normal 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
23
log/transactions.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -40,10 +41,20 @@ type USDTMan struct {
|
|||||||
httpClient *http.Client // HTTP 客户端
|
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 收款信息
|
// USDTPayment USDT 收款信息
|
||||||
type USDTPayment struct {
|
type USDTPayment struct {
|
||||||
Address string
|
Address string
|
||||||
Amount float64
|
Amount *big.Int // 原始金额(micro USDT,10^-6),例如 13260000 = 13.26 USDT
|
||||||
TxID string
|
TxID string
|
||||||
BlockNumber int64
|
BlockNumber int64
|
||||||
Timestamp int64
|
Timestamp int64
|
||||||
@@ -51,23 +62,57 @@ type USDTPayment struct {
|
|||||||
Confirmations int64
|
Confirmations int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// TronGridTransaction 交易信息
|
// 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 {
|
type TronGridTransaction struct {
|
||||||
Ret []map[string]interface{} `json:"ret"`
|
TransactionID string `json:"transaction_id"`
|
||||||
TxID string `json:"txID"`
|
BlockTimestamp int64 `json:"block_timestamp"`
|
||||||
BlockNumber int64 `json:"blockNumber"`
|
From string `json:"from"`
|
||||||
BlockTimeStamp int64 `json:"block_timestamp"`
|
To string `json:"to"`
|
||||||
RawData struct {
|
Type string `json:"type"`
|
||||||
Contract []struct {
|
Value string `json:"value"`
|
||||||
Parameter struct {
|
TokenInfo struct {
|
||||||
Value struct {
|
Symbol string `json:"symbol"`
|
||||||
Amount int64 `json:"amount"`
|
Address string `json:"address"`
|
||||||
To string `json:"to_address"`
|
Decimals int `json:"decimals"`
|
||||||
From string `json:"owner_address"`
|
Name string `json:"name"`
|
||||||
} `json:"value"`
|
} `json:"token_info"`
|
||||||
} `json:"parameter"`
|
|
||||||
} `json:"contract"`
|
|
||||||
} `json:"raw_data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TronGridAccountTransactions 账户交易列表
|
// TronGridAccountTransactions 账户交易列表
|
||||||
@@ -75,8 +120,8 @@ type TronGridAccountTransactions struct {
|
|||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Data []TronGridTransaction `json:"data"`
|
Data []TronGridTransaction `json:"data"`
|
||||||
Meta struct {
|
Meta struct {
|
||||||
|
At int64 `json:"at"`
|
||||||
PageSize int `json:"page_size"`
|
PageSize int `json:"page_size"`
|
||||||
Fingerprint string `json:"fingerprint"`
|
|
||||||
} `json:"meta"`
|
} `json:"meta"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,28 +266,51 @@ func (m *USDTMan) checkAddress(address string) {
|
|||||||
for _, txn := range transactions {
|
for _, txn := range transactions {
|
||||||
// 检查是否已处理
|
// 检查是否已处理
|
||||||
m.txnMutex.RLock()
|
m.txnMutex.RLock()
|
||||||
processed := m.processedTxns[txn.TxID]
|
processed := m.processedTxns[txn.TransactionID]
|
||||||
m.txnMutex.RUnlock()
|
m.txnMutex.RUnlock()
|
||||||
|
|
||||||
if processed {
|
if processed {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取交易的区块号(需要通过 transaction_id 查询)
|
||||||
|
blockNumber, err := m.getTransactionBlock(txn.TransactionID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error getting transaction block: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// 计算确认数
|
// 计算确认数
|
||||||
confirmations := currentBlock - txn.BlockNumber
|
confirmations := currentBlock - blockNumber
|
||||||
if confirmations < m.minConfirmations {
|
if confirmations < m.minConfirmations {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析交易数据
|
// 检查是否是转入该地址
|
||||||
payment := m.parseTransaction(&txn, address, confirmations)
|
if !strings.EqualFold(txn.To, address) {
|
||||||
if payment == nil {
|
|
||||||
continue
|
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.txnMutex.Lock()
|
||||||
m.processedTxns[txn.TxID] = true
|
m.processedTxns[txn.TransactionID] = true
|
||||||
m.txnMutex.Unlock()
|
m.txnMutex.Unlock()
|
||||||
|
|
||||||
// 触发回调
|
// 触发回调
|
||||||
@@ -293,6 +361,83 @@ func (m *USDTMan) getUSDTTransactions(address string) ([]TronGridTransaction, er
|
|||||||
return result.Data, nil
|
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 获取当前区块高度
|
// getCurrentBlock 获取当前区块高度
|
||||||
func (m *USDTMan) getCurrentBlock() (int64, error) {
|
func (m *USDTMan) getCurrentBlock() (int64, error) {
|
||||||
url := fmt.Sprintf("%s/wallet/getnowblock", TronGridAPI)
|
url := fmt.Sprintf("%s/wallet/getnowblock", TronGridAPI)
|
||||||
@@ -329,41 +474,7 @@ func (m *USDTMan) getCurrentBlock() (int64, error) {
|
|||||||
return blockInfo.BlockHeader.RawData.Number, nil
|
return blockInfo.BlockHeader.RawData.Number, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseTransaction 解析交易
|
// parseTransaction 解析交易(已废弃,直接在 checkAddress 中处理)
|
||||||
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 获取所有监听地址
|
// GetAddresses 获取所有监听地址
|
||||||
func (m *USDTMan) GetAddresses() []string {
|
func (m *USDTMan) GetAddresses() []string {
|
||||||
@@ -402,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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user