usdtman
This commit is contained in:
31
README.md
31
README.md
@@ -1,13 +1,31 @@
|
|||||||
# USDTMan
|
# USDTMan
|
||||||
|
|
||||||
TRON USDT TRC20 收款监听服务
|
TRON USDT TRC20 收款监听服务 - 基于交易记录扫描 + 区块确认数验证
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
- 实时监听多个 TRON 地址的 USDT 收款
|
- 实时监听多个 TRON 地址的 USDT 收款
|
||||||
|
- 区块确认数验证(默认6个确认)
|
||||||
- WebSocket 实时推送收款通知
|
- WebSocket 实时推送收款通知
|
||||||
- HTTP API 管理监听地址
|
- HTTP API 管理监听地址
|
||||||
- 测试页面
|
|
||||||
|
## API 使用方式
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 创建监听器
|
||||||
|
uman := usdtman.NewUSDTMan([]string{"地址1", "地址2"}, "API_KEY")
|
||||||
|
|
||||||
|
// 设置收款回调
|
||||||
|
uman.OnPaymentComplete(func(payment *usdtman.USDTPayment) {
|
||||||
|
fmt.Printf("收到 %.6f USDT,确认数: %d\n", payment.Amount, payment.Confirmations)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 启动监听
|
||||||
|
uman.Start()
|
||||||
|
|
||||||
|
// 停止监听
|
||||||
|
uman.Stop()
|
||||||
|
```
|
||||||
|
|
||||||
## 运行
|
## 运行
|
||||||
|
|
||||||
@@ -18,7 +36,7 @@ go run main.go
|
|||||||
|
|
||||||
访问 http://localhost:8084
|
访问 http://localhost:8084
|
||||||
|
|
||||||
## API
|
## 接口
|
||||||
|
|
||||||
- `POST /start` - 启动监听
|
- `POST /start` - 启动监听
|
||||||
- `POST /stop` - 停止监听
|
- `POST /stop` - 停止监听
|
||||||
@@ -27,3 +45,10 @@ go run main.go
|
|||||||
- `GET /list-addresses` - 列出所有地址
|
- `GET /list-addresses` - 列出所有地址
|
||||||
- `GET /payments` - 获取收款历史
|
- `GET /payments` - 获取收款历史
|
||||||
- `WS /ws` - WebSocket 连接
|
- `WS /ws` - WebSocket 连接
|
||||||
|
|
||||||
|
## 确认机制
|
||||||
|
|
||||||
|
- 扫描地址的最近交易记录
|
||||||
|
- 计算区块确认数(当前区块 - 交易区块)
|
||||||
|
- 仅在确认数 >= 6 时触发回调
|
||||||
|
- 自动去重,避免重复处理
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tronMonitor *usdtman.TronUSDTMonitor
|
uman *usdtman.USDTMan
|
||||||
paymentEvents []usdtman.USDTPayment
|
paymentEvents []usdtman.USDTPayment
|
||||||
paymentLock sync.RWMutex
|
paymentLock sync.RWMutex
|
||||||
upgrader = websocket.Upgrader{
|
upgrader = websocket.Upgrader{
|
||||||
@@ -34,10 +34,13 @@ func main() {
|
|||||||
apiKey = "da1e77dc-b35b-4458-846a-5a551b9df4b2"
|
apiKey = "da1e77dc-b35b-4458-846a-5a551b9df4b2"
|
||||||
}
|
}
|
||||||
|
|
||||||
tronMonitor = usdtman.NewTronUSDTMonitor(apiKey)
|
// 初始化 USDTMan (无初始地址)
|
||||||
tronMonitor.SetPaymentCallback(func(payment *usdtman.USDTPayment) {
|
uman = usdtman.NewUSDTMan([]string{}, apiKey)
|
||||||
log.Printf("💰 收到 USDT: %s -> %.6f USDT (TxID: %s)",
|
|
||||||
payment.From, payment.Amount, payment.TxID)
|
// 设置收款回调
|
||||||
|
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()
|
paymentLock.Lock()
|
||||||
paymentEvents = append(paymentEvents, *payment)
|
paymentEvents = append(paymentEvents, *payment)
|
||||||
@@ -77,7 +80,7 @@ func startMonitor(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tronMonitor.Start(); err != nil {
|
if err := uman.Start(); err != nil {
|
||||||
jsonResponse(w, false, fmt.Sprintf("启动失败: %v", err), nil)
|
jsonResponse(w, false, fmt.Sprintf("启动失败: %v", err), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -91,7 +94,7 @@ func stopMonitor(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tronMonitor.Stop()
|
uman.Stop()
|
||||||
jsonResponse(w, true, "监听已停止", nil)
|
jsonResponse(w, true, "监听已停止", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +118,7 @@ func addAddress(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tronMonitor.AddAddress(req.Address)
|
uman.AddAddress(req.Address)
|
||||||
jsonResponse(w, true, "地址已添加", map[string]interface{}{
|
jsonResponse(w, true, "地址已添加", map[string]interface{}{
|
||||||
"address": req.Address,
|
"address": req.Address,
|
||||||
})
|
})
|
||||||
@@ -136,7 +139,7 @@ func removeAddress(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tronMonitor.RemoveAddress(req.Address)
|
uman.RemoveAddress(req.Address)
|
||||||
jsonResponse(w, true, "地址已移除", map[string]interface{}{
|
jsonResponse(w, true, "地址已移除", map[string]interface{}{
|
||||||
"address": req.Address,
|
"address": req.Address,
|
||||||
})
|
})
|
||||||
@@ -148,7 +151,7 @@ func listAddresses(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
addresses := tronMonitor.GetAllAddresses()
|
addresses := uman.GetAddresses()
|
||||||
jsonResponse(w, true, "success", map[string]interface{}{
|
jsonResponse(w, true, "success", map[string]interface{}{
|
||||||
"addresses": addresses,
|
"addresses": addresses,
|
||||||
"count": len(addresses),
|
"count": len(addresses),
|
||||||
@@ -207,6 +210,7 @@ func broadcastPayment(payment *usdtman.USDTPayment) {
|
|||||||
"txId": payment.TxID,
|
"txId": payment.TxID,
|
||||||
"block": payment.BlockNumber,
|
"block": payment.BlockNumber,
|
||||||
"time": payment.Timestamp,
|
"time": payment.Timestamp,
|
||||||
|
"confirmations": payment.Confirmations,
|
||||||
}
|
}
|
||||||
|
|
||||||
clientsLock.RLock()
|
clientsLock.RLock()
|
||||||
@@ -218,138 +222,5 @@ func broadcastPayment(payment *usdtman.USDTPayment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func serveIndex(w http.ResponseWriter, r *http.Request) {
|
func serveIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
http.ServeFile(w, r, "index.html")
|
||||||
w.Write([]byte(indexHTML))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
132
index.html
Normal 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>
|
||||||
@@ -11,22 +11,18 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TronUSDTMonitor 监听 USDT 收款
|
// USDTMan USDT 监听管理器
|
||||||
type TronUSDTMonitor struct {
|
type USDTMan struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
addresses map[string]*AddressInfo // 地址映射
|
addresses []string
|
||||||
apiKey string
|
apiKey string
|
||||||
running bool
|
running bool
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
onPaymentReceived func(*USDTPayment)
|
onPaymentComplete func(*USDTPayment)
|
||||||
}
|
minConfirmations int64
|
||||||
|
processedTxns map[string]bool // 已处理的交易
|
||||||
// AddressInfo 地址信息
|
txnMutex sync.RWMutex
|
||||||
type AddressInfo struct {
|
|
||||||
Address string
|
|
||||||
LastBlock int64
|
|
||||||
TotalReceived float64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// USDTPayment USDT 收款信息
|
// USDTPayment USDT 收款信息
|
||||||
@@ -37,69 +33,74 @@ type USDTPayment struct {
|
|||||||
BlockNumber int64
|
BlockNumber int64
|
||||||
Timestamp int64
|
Timestamp int64
|
||||||
From string
|
From string
|
||||||
|
Confirmations int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// TronGridEvent TronGrid 事件响应
|
// TronGridTransaction 交易信息
|
||||||
type TronGridEvent struct {
|
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"`
|
Success bool `json:"success"`
|
||||||
Data []struct {
|
Data []TronGridTransaction `json:"data"`
|
||||||
TransactionID string `json:"transaction_id"`
|
|
||||||
BlockNumber int64 `json:"block_number"`
|
|
||||||
BlockTimestamp int64 `json:"block_timestamp"`
|
|
||||||
ContractAddress string `json:"contract_address"`
|
|
||||||
EventName string `json:"event_name"`
|
|
||||||
Result map[string]interface{} `json:"result"`
|
|
||||||
} `json:"data"`
|
|
||||||
Meta struct {
|
Meta struct {
|
||||||
PageSize int `json:"page_size"`
|
PageSize int `json:"page_size"`
|
||||||
|
Fingerprint string `json:"fingerprint"`
|
||||||
} `json:"meta"`
|
} `json:"meta"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TronGridBlockInfo 区块信息
|
||||||
|
type TronGridBlockInfo struct {
|
||||||
|
BlockHeader struct {
|
||||||
|
RawData struct {
|
||||||
|
Number int64 `json:"number"`
|
||||||
|
} `json:"raw_data"`
|
||||||
|
} `json:"block_header"`
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TronGridAPI = "https://api.trongrid.io"
|
TronGridAPI = "https://api.trongrid.io"
|
||||||
USDTContractAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" // USDT TRC20
|
USDTContractAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" // USDT TRC20
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewTronUSDTMonitor 创建监听器
|
// NewUSDTMan 创建监听管理器
|
||||||
func NewTronUSDTMonitor(apiKey string) *TronUSDTMonitor {
|
func NewUSDTMan(addresses []string, apiKey string) *USDTMan {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &TronUSDTMonitor{
|
return &USDTMan{
|
||||||
addresses: make(map[string]*AddressInfo),
|
addresses: addresses,
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
|
minConfirmations: 6,
|
||||||
|
processedTxns: make(map[string]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddAddress 添加监听地址
|
// OnPaymentComplete 设置收款回调
|
||||||
func (m *TronUSDTMonitor) AddAddress(address string) {
|
func (m *USDTMan) OnPaymentComplete(callback func(*USDTPayment)) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
m.onPaymentComplete = callback
|
||||||
if _, exists := m.addresses[address]; !exists {
|
|
||||||
m.addresses[address] = &AddressInfo{
|
|
||||||
Address: address,
|
|
||||||
LastBlock: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveAddress 移除监听地址
|
|
||||||
func (m *TronUSDTMonitor) RemoveAddress(address string) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
delete(m.addresses, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPaymentCallback 设置收款回调
|
|
||||||
func (m *TronUSDTMonitor) SetPaymentCallback(callback func(*USDTPayment)) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
m.onPaymentReceived = callback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start 开始监听
|
// Start 开始监听
|
||||||
func (m *TronUSDTMonitor) Start() error {
|
func (m *USDTMan) Start() error {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
if m.running {
|
if m.running {
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
@@ -113,7 +114,7 @@ func (m *TronUSDTMonitor) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop 停止监听
|
// Stop 停止监听
|
||||||
func (m *TronUSDTMonitor) Stop() {
|
func (m *USDTMan) Stop() {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
if !m.running {
|
if !m.running {
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
@@ -126,8 +127,8 @@ func (m *TronUSDTMonitor) Stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// monitorLoop 监听循环
|
// monitorLoop 监听循环
|
||||||
func (m *TronUSDTMonitor) monitorLoop() {
|
func (m *USDTMan) monitorLoop() {
|
||||||
ticker := time.NewTicker(3 * time.Second)
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -141,64 +142,61 @@ func (m *TronUSDTMonitor) monitorLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkAllAddresses 检查所有地址
|
// checkAllAddresses 检查所有地址
|
||||||
func (m *TronUSDTMonitor) checkAllAddresses() {
|
func (m *USDTMan) checkAllAddresses() {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
addresses := make([]*AddressInfo, 0, len(m.addresses))
|
addresses := make([]string, len(m.addresses))
|
||||||
for _, info := range m.addresses {
|
copy(addresses, m.addresses)
|
||||||
addresses = append(addresses, info)
|
|
||||||
}
|
|
||||||
m.mu.RUnlock()
|
m.mu.RUnlock()
|
||||||
|
|
||||||
for _, addrInfo := range addresses {
|
for _, address := range addresses {
|
||||||
m.checkAddress(addrInfo)
|
m.checkAddress(address)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkAddress 检查单个地址
|
// checkAddress 检查单个地址的交易
|
||||||
func (m *TronUSDTMonitor) checkAddress(addrInfo *AddressInfo) {
|
func (m *USDTMan) checkAddress(address string) {
|
||||||
events, err := m.getUSDTEvents(addrInfo.Address, addrInfo.LastBlock)
|
transactions, err := m.getUSDTTransactions(address)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, event := range events {
|
currentBlock, err := m.getCurrentBlock()
|
||||||
if event.EventName != "Transfer" {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
to, ok := event.Result["to"].(string)
|
// 计算确认数
|
||||||
if !ok || !strings.EqualFold(to, addrInfo.Address) {
|
confirmations := currentBlock - txn.BlockNumber
|
||||||
|
if confirmations < m.minConfirmations {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
from, _ := event.Result["from"].(string)
|
// 解析交易数据
|
||||||
|
payment := m.parseTransaction(&txn, address, confirmations)
|
||||||
valueStr, ok := event.Result["value"].(string)
|
if payment == nil {
|
||||||
if !ok {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
amount := parseUSDTAmount(valueStr)
|
// 标记为已处理
|
||||||
if amount <= 0 {
|
m.txnMutex.Lock()
|
||||||
continue
|
m.processedTxns[txn.TxID] = true
|
||||||
}
|
m.txnMutex.Unlock()
|
||||||
|
|
||||||
payment := &USDTPayment{
|
|
||||||
Address: addrInfo.Address,
|
|
||||||
Amount: amount,
|
|
||||||
TxID: event.TransactionID,
|
|
||||||
BlockNumber: event.BlockNumber,
|
|
||||||
Timestamp: event.BlockTimestamp,
|
|
||||||
From: from,
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.BlockNumber > addrInfo.LastBlock {
|
|
||||||
addrInfo.LastBlock = event.BlockNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 触发回调
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
callback := m.onPaymentReceived
|
callback := m.onPaymentComplete
|
||||||
m.mu.RUnlock()
|
m.mu.RUnlock()
|
||||||
|
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
@@ -207,16 +205,10 @@ func (m *TronUSDTMonitor) checkAddress(addrInfo *AddressInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUSDTEvents 获取 USDT 转账事件
|
// getUSDTTransactions 获取地址的 USDT 交易
|
||||||
func (m *TronUSDTMonitor) getUSDTEvents(address string, sinceBlock int64) ([]struct {
|
func (m *USDTMan) getUSDTTransactions(address string) ([]TronGridTransaction, error) {
|
||||||
TransactionID string
|
url := fmt.Sprintf("%s/v1/accounts/%s/transactions/trc20?limit=20&contract_address=%s&only_to=true",
|
||||||
BlockNumber int64
|
TronGridAPI, address, USDTContractAddress)
|
||||||
BlockTimestamp int64
|
|
||||||
EventName string
|
|
||||||
Result map[string]interface{}
|
|
||||||
}, error) {
|
|
||||||
url := fmt.Sprintf("%s/v1/contracts/%s/events?event_name=Transfer&only_to=true&min_block_timestamp=%d&limit=200",
|
|
||||||
TronGridAPI, USDTContractAddress, sinceBlock)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -243,72 +235,121 @@ func (m *TronUSDTMonitor) getUSDTEvents(address string, sinceBlock int64) ([]str
|
|||||||
return nil, fmt.Errorf("TronGrid API error: %d %s", resp.StatusCode, string(body))
|
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 {
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !result.Success {
|
return result.Data, nil
|
||||||
return nil, fmt.Errorf("TronGrid API returned success=false")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var filtered []struct {
|
// getCurrentBlock 获取当前区块高度
|
||||||
TransactionID string
|
func (m *USDTMan) getCurrentBlock() (int64, error) {
|
||||||
BlockNumber int64
|
url := fmt.Sprintf("%s/wallet/getnowblock", TronGridAPI)
|
||||||
BlockTimestamp int64
|
|
||||||
EventName string
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
Result map[string]interface{}
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, event := range result.Data {
|
if m.apiKey != "" {
|
||||||
to, ok := event.Result["to"].(string)
|
req.Header.Set("TRON-PRO-API-KEY", m.apiKey)
|
||||||
if ok && strings.EqualFold(to, address) {
|
}
|
||||||
filtered = append(filtered, struct {
|
|
||||||
TransactionID string
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
BlockNumber int64
|
resp, err := client.Do(req)
|
||||||
BlockTimestamp int64
|
if err != nil {
|
||||||
EventName string
|
return 0, err
|
||||||
Result map[string]interface{}
|
}
|
||||||
}{
|
defer resp.Body.Close()
|
||||||
TransactionID: event.TransactionID,
|
|
||||||
BlockNumber: event.BlockNumber,
|
body, err := io.ReadAll(resp.Body)
|
||||||
BlockTimestamp: event.BlockTimestamp,
|
if err != nil {
|
||||||
EventName: event.EventName,
|
return 0, err
|
||||||
Result: event.Result,
|
}
|
||||||
})
|
|
||||||
|
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 解析交易
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered, nil
|
// GetAddresses 获取所有监听地址
|
||||||
}
|
func (m *USDTMan) GetAddresses() []string {
|
||||||
|
|
||||||
// parseUSDTAmount 解析 USDT 金额(6位小数)
|
|
||||||
func parseUSDTAmount(valueStr string) float64 {
|
|
||||||
valueStr = strings.TrimPrefix(valueStr, "0x")
|
|
||||||
|
|
||||||
var value int64
|
|
||||||
fmt.Sscanf(valueStr, "%d", &value)
|
|
||||||
|
|
||||||
return float64(value) / 1000000.0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAddressInfo 获取地址信息
|
|
||||||
func (m *TronUSDTMonitor) GetAddressInfo(address string) (*AddressInfo, bool) {
|
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
info, exists := m.addresses[address]
|
addresses := make([]string, len(m.addresses))
|
||||||
return info, exists
|
copy(addresses, m.addresses)
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllAddresses 获取所有监听地址
|
|
||||||
func (m *TronUSDTMonitor) GetAllAddresses() []string {
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
|
|
||||||
addresses := make([]string, 0, len(m.addresses))
|
|
||||||
for addr := range m.addresses {
|
|
||||||
addresses = append(addresses, addr)
|
|
||||||
}
|
|
||||||
return addresses
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user