Compare commits

...

2 Commits

Author SHA1 Message Date
8965b96f0e fix update 2026-02-03 00:10:43 +08:00
79c1c710a2 usdtman 2026-02-03 00:01:41 +08:00
4 changed files with 472 additions and 316 deletions

View File

@@ -1,13 +1,52 @@
# USDTMan
TRON USDT TRC20 收款监听服务
TRON USDT TRC20 收款监听服务 - 基于交易记录扫描 + 区块确认数验证
## 功能
- 实时监听多个 TRON 地址的 USDT 收款
- 区块确认数验证默认6个确认
- WebSocket 实时推送收款通知
- HTTP API 管理监听地址
- 测试页面
## API 使用方式
```go
// 创建监听器(配置对象方式)
uman := usdtman.NewUSDTMan(usdtman.Config{
Addresses: []string{"地址1", "地址2"},
APIKey: "YOUR_API_KEY",
QueryInterval: 5 * time.Second, // 查询间隔(可选,默认 5 秒)
MinConfirmations: 6, // 最小确认数(可选,默认 6
MaxHistoryTxns: 20, // 查询历史交易数(可选,默认 20
ProxyURL: "http://127.0.0.1:7890", // HTTP/SOCKS5 代理(可选)
})
// 或者使用自定义 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)
})
// 启动监听
uman.Start()
// 停止监听
uman.Stop()
// 动态添加/移除地址
uman.AddAddress("新地址")
uman.RemoveAddress("旧地址")
```
## 运行
@@ -18,7 +57,7 @@ go run main.go
访问 http://localhost:8084
## API
## 接口
- `POST /start` - 启动监听
- `POST /stop` - 停止监听
@@ -27,3 +66,10 @@ go run main.go
- `GET /list-addresses` - 列出所有地址
- `GET /payments` - 获取收款历史
- `WS /ws` - WebSocket 连接
## 确认机制
- 扫描地址的最近交易记录
- 计算区块确认数(当前区块 - 交易区块)
- 仅在确认数 >= 6 时触发回调
- 自动去重,避免重复处理

View File

@@ -7,13 +7,14 @@ import (
"net/http"
"os"
"sync"
"time"
"usdtman"
"github.com/gorilla/websocket"
)
var (
tronMonitor *usdtman.TronUSDTMonitor
uman *usdtman.USDTMan
paymentEvents []usdtman.USDTPayment
paymentLock sync.RWMutex
upgrader = websocket.Upgrader{
@@ -34,10 +35,29 @@ func main() {
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)
// 初始化 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 -> %.6f USDT (确认数: %d, TxID: %s)",
payment.From, payment.Amount, payment.Confirmations, payment.TxID)
paymentLock.Lock()
paymentEvents = append(paymentEvents, *payment)
@@ -77,7 +97,7 @@ func startMonitor(w http.ResponseWriter, r *http.Request) {
return
}
if err := tronMonitor.Start(); err != nil {
if err := uman.Start(); err != nil {
jsonResponse(w, false, fmt.Sprintf("启动失败: %v", err), nil)
return
}
@@ -91,7 +111,7 @@ func stopMonitor(w http.ResponseWriter, r *http.Request) {
return
}
tronMonitor.Stop()
uman.Stop()
jsonResponse(w, true, "监听已停止", nil)
}
@@ -115,7 +135,7 @@ func addAddress(w http.ResponseWriter, r *http.Request) {
return
}
tronMonitor.AddAddress(req.Address)
uman.AddAddress(req.Address)
jsonResponse(w, true, "地址已添加", map[string]interface{}{
"address": req.Address,
})
@@ -136,7 +156,7 @@ func removeAddress(w http.ResponseWriter, r *http.Request) {
return
}
tronMonitor.RemoveAddress(req.Address)
uman.RemoveAddress(req.Address)
jsonResponse(w, true, "地址已移除", map[string]interface{}{
"address": req.Address,
})
@@ -148,7 +168,7 @@ func listAddresses(w http.ResponseWriter, r *http.Request) {
return
}
addresses := tronMonitor.GetAllAddresses()
addresses := uman.GetAddresses()
jsonResponse(w, true, "success", map[string]interface{}{
"addresses": addresses,
"count": len(addresses),
@@ -207,6 +227,7 @@ func broadcastPayment(payment *usdtman.USDTPayment) {
"txId": payment.TxID,
"block": payment.BlockNumber,
"time": payment.Timestamp,
"confirmations": payment.Confirmations,
}
clientsLock.RLock()
@@ -218,138 +239,5 @@ func broadcastPayment(payment *usdtman.USDTPayment) {
}
func serveIndex(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(indexHTML))
http.ServeFile(w, r, "index.html")
}
const indexHTML = `<!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>实时日志 (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 + ' -> ' + data.amount.toFixed(6) + ' USDT (TxID: ' + data.txId + ')');
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>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><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 = '暂无记录';
}
}
listAddresses();
getPayments();
</script>
</body>
</html>`

132
index.html Normal file
View File

@@ -0,0 +1,132 @@
<!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>实时日志 (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 = '暂无记录';
}
}
listAddresses();
getPayments();
</script>
</body>
</html>

View File

@@ -6,27 +6,38 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
// TronUSDTMonitor 监听 USDT 收款
type TronUSDTMonitor struct {
// 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 map[string]*AddressInfo // 地址映射
addresses []string
apiKey string
queryInterval time.Duration
running bool
ctx context.Context
cancel context.CancelFunc
onPaymentReceived func(*USDTPayment)
}
// AddressInfo 地址信息
type AddressInfo struct {
Address string
LastBlock int64
TotalReceived float64
onPaymentComplete func(*USDTPayment)
minConfirmations int64
maxHistoryTxns int
processedTxns map[string]bool // 已处理的交易
txnMutex sync.RWMutex
httpClient *http.Client // HTTP 客户端
}
// USDTPayment USDT 收款信息
@@ -37,69 +48,110 @@ type USDTPayment struct {
BlockNumber int64
Timestamp int64
From string
Confirmations int64
}
// TronGridEvent TronGrid 事件响应
type TronGridEvent struct {
// TronGridTransaction 交易信息
type TronGridTransaction struct {
Ret []map[string]interface{} `json:"ret"`
TxID string `json:"txID"`
BlockNumber int64 `json:"blockNumber"`
BlockTimeStamp int64 `json:"block_timestamp"`
RawData struct {
Contract []struct {
Parameter struct {
Value struct {
Amount int64 `json:"amount"`
To string `json:"to_address"`
From string `json:"owner_address"`
} `json:"value"`
} `json:"parameter"`
} `json:"contract"`
} `json:"raw_data"`
}
// TronGridAccountTransactions 账户交易列表
type TronGridAccountTransactions 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"`
Data []TronGridTransaction `json:"data"`
Meta struct {
PageSize int `json:"page_size"`
Fingerprint string `json:"fingerprint"`
} `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
)
// NewTronUSDTMonitor 创建监听器
func NewTronUSDTMonitor(apiKey string) *TronUSDTMonitor {
// NewUSDTMan 创建监听管理
func NewUSDTMan(config Config) *USDTMan {
ctx, cancel := context.WithCancel(context.Background())
return &TronUSDTMonitor{
addresses: make(map[string]*AddressInfo),
apiKey: apiKey,
// 设置默认值
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),
}
}
// AddAddress 添加监听地址
func (m *TronUSDTMonitor) AddAddress(address string) {
// OnPaymentComplete 设置收款回调
func (m *USDTMan) OnPaymentComplete(callback func(*USDTPayment)) {
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
m.onPaymentComplete = callback
}
// Start 开始监听
func (m *TronUSDTMonitor) Start() error {
func (m *USDTMan) Start() error {
m.mu.Lock()
if m.running {
m.mu.Unlock()
@@ -113,7 +165,7 @@ func (m *TronUSDTMonitor) Start() error {
}
// Stop 停止监听
func (m *TronUSDTMonitor) Stop() {
func (m *USDTMan) Stop() {
m.mu.Lock()
if !m.running {
m.mu.Unlock()
@@ -126,8 +178,8 @@ func (m *TronUSDTMonitor) Stop() {
}
// monitorLoop 监听循环
func (m *TronUSDTMonitor) monitorLoop() {
ticker := time.NewTicker(3 * time.Second)
func (m *USDTMan) monitorLoop() {
ticker := time.NewTicker(m.queryInterval)
defer ticker.Stop()
for {
@@ -141,64 +193,61 @@ func (m *TronUSDTMonitor) monitorLoop() {
}
// checkAllAddresses 检查所有地址
func (m *TronUSDTMonitor) checkAllAddresses() {
func (m *USDTMan) checkAllAddresses() {
m.mu.RLock()
addresses := make([]*AddressInfo, 0, len(m.addresses))
for _, info := range m.addresses {
addresses = append(addresses, info)
}
addresses := make([]string, len(m.addresses))
copy(addresses, m.addresses)
m.mu.RUnlock()
for _, addrInfo := range addresses {
m.checkAddress(addrInfo)
for _, address := range addresses {
m.checkAddress(address)
}
}
// checkAddress 检查单个地址
func (m *TronUSDTMonitor) checkAddress(addrInfo *AddressInfo) {
events, err := m.getUSDTEvents(addrInfo.Address, addrInfo.LastBlock)
// checkAddress 检查单个地址的交易
func (m *USDTMan) checkAddress(address string) {
transactions, err := m.getUSDTTransactions(address)
if err != nil {
fmt.Printf("Error checking address %s: %v\n", addrInfo.Address, err)
fmt.Printf("Error checking address %s: %v\n", address, err)
return
}
for _, event := range events {
if event.EventName != "Transfer" {
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.TxID]
m.txnMutex.RUnlock()
if processed {
continue
}
to, ok := event.Result["to"].(string)
if !ok || !strings.EqualFold(to, addrInfo.Address) {
// 计算确认数
confirmations := currentBlock - txn.BlockNumber
if confirmations < m.minConfirmations {
continue
}
from, _ := event.Result["from"].(string)
valueStr, ok := event.Result["value"].(string)
if !ok {
// 解析交易数据
payment := m.parseTransaction(&txn, address, confirmations)
if payment == nil {
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.txnMutex.Lock()
m.processedTxns[txn.TxID] = true
m.txnMutex.Unlock()
// 触发回调
m.mu.RLock()
callback := m.onPaymentReceived
callback := m.onPaymentComplete
m.mu.RUnlock()
if callback != nil {
@@ -207,16 +256,10 @@ func (m *TronUSDTMonitor) checkAddress(addrInfo *AddressInfo) {
}
}
// 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)
// 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 {
@@ -227,8 +270,7 @@ func (m *TronUSDTMonitor) getUSDTEvents(address string, sinceBlock int64) ([]str
req.Header.Set("TRON-PRO-API-KEY", m.apiKey)
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
resp, err := m.httpClient.Do(req)
if err != nil {
return nil, err
}
@@ -243,72 +285,120 @@ func (m *TronUSDTMonitor) getUSDTEvents(address string, sinceBlock int64) ([]str
return nil, fmt.Errorf("TronGrid API error: %d %s", resp.StatusCode, string(body))
}
var result TronGridEvent
var result TronGridAccountTransactions
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
return result.Data, nil
}
// parseUSDTAmount 解析 USDT 金额6位小数
func parseUSDTAmount(valueStr string) float64 {
valueStr = strings.TrimPrefix(valueStr, "0x")
// getCurrentBlock 获取当前区块高度
func (m *USDTMan) getCurrentBlock() (int64, error) {
url := fmt.Sprintf("%s/wallet/getnowblock", TronGridAPI)
var value int64
fmt.Sscanf(valueStr, "%d", &value)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return 0, err
}
return float64(value) / 1000000.0
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
}
// GetAddressInfo 获取地址信息
func (m *TronUSDTMonitor) GetAddressInfo(address string) (*AddressInfo, bool) {
// parseTransaction 解析交易
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 获取所有监听地址
func (m *USDTMan) GetAddresses() []string {
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)
}
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
}
}
}