315 lines
6.8 KiB
Go
315 lines
6.8 KiB
Go
package usdtman
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// TronUSDTMonitor 监听 USDT 收款
|
||
type TronUSDTMonitor struct {
|
||
mu sync.RWMutex
|
||
addresses map[string]*AddressInfo // 地址映射
|
||
apiKey string
|
||
running bool
|
||
ctx context.Context
|
||
cancel context.CancelFunc
|
||
onPaymentReceived func(*USDTPayment)
|
||
}
|
||
|
||
// AddressInfo 地址信息
|
||
type AddressInfo struct {
|
||
Address string
|
||
LastBlock int64
|
||
TotalReceived float64
|
||
}
|
||
|
||
// USDTPayment USDT 收款信息
|
||
type USDTPayment struct {
|
||
Address string
|
||
Amount float64
|
||
TxID string
|
||
BlockNumber int64
|
||
Timestamp int64
|
||
From string
|
||
}
|
||
|
||
// TronGridEvent TronGrid 事件响应
|
||
type TronGridEvent 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"`
|
||
Meta struct {
|
||
PageSize int `json:"page_size"`
|
||
} `json:"meta"`
|
||
}
|
||
|
||
const (
|
||
TronGridAPI = "https://api.trongrid.io"
|
||
USDTContractAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" // USDT TRC20
|
||
)
|
||
|
||
// NewTronUSDTMonitor 创建监听器
|
||
func NewTronUSDTMonitor(apiKey string) *TronUSDTMonitor {
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
return &TronUSDTMonitor{
|
||
addresses: make(map[string]*AddressInfo),
|
||
apiKey: apiKey,
|
||
ctx: ctx,
|
||
cancel: cancel,
|
||
}
|
||
}
|
||
|
||
// AddAddress 添加监听地址
|
||
func (m *TronUSDTMonitor) AddAddress(address string) {
|
||
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
|
||
}
|
||
|
||
// Start 开始监听
|
||
func (m *TronUSDTMonitor) 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 *TronUSDTMonitor) Stop() {
|
||
m.mu.Lock()
|
||
if !m.running {
|
||
m.mu.Unlock()
|
||
return
|
||
}
|
||
m.running = false
|
||
m.mu.Unlock()
|
||
|
||
m.cancel()
|
||
}
|
||
|
||
// monitorLoop 监听循环
|
||
func (m *TronUSDTMonitor) monitorLoop() {
|
||
ticker := time.NewTicker(3 * time.Second)
|
||
defer ticker.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-m.ctx.Done():
|
||
return
|
||
case <-ticker.C:
|
||
m.checkAllAddresses()
|
||
}
|
||
}
|
||
}
|
||
|
||
// checkAllAddresses 检查所有地址
|
||
func (m *TronUSDTMonitor) checkAllAddresses() {
|
||
m.mu.RLock()
|
||
addresses := make([]*AddressInfo, 0, len(m.addresses))
|
||
for _, info := range m.addresses {
|
||
addresses = append(addresses, info)
|
||
}
|
||
m.mu.RUnlock()
|
||
|
||
for _, addrInfo := range addresses {
|
||
m.checkAddress(addrInfo)
|
||
}
|
||
}
|
||
|
||
// checkAddress 检查单个地址
|
||
func (m *TronUSDTMonitor) checkAddress(addrInfo *AddressInfo) {
|
||
events, err := m.getUSDTEvents(addrInfo.Address, addrInfo.LastBlock)
|
||
if err != nil {
|
||
fmt.Printf("Error checking address %s: %v\n", addrInfo.Address, err)
|
||
return
|
||
}
|
||
|
||
for _, event := range events {
|
||
if event.EventName != "Transfer" {
|
||
continue
|
||
}
|
||
|
||
to, ok := event.Result["to"].(string)
|
||
if !ok || !strings.EqualFold(to, addrInfo.Address) {
|
||
continue
|
||
}
|
||
|
||
from, _ := event.Result["from"].(string)
|
||
|
||
valueStr, ok := event.Result["value"].(string)
|
||
if !ok {
|
||
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.mu.RLock()
|
||
callback := m.onPaymentReceived
|
||
m.mu.RUnlock()
|
||
|
||
if callback != nil {
|
||
callback(payment)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
|
||
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)
|
||
}
|
||
|
||
client := &http.Client{Timeout: 10 * time.Second}
|
||
resp, err := client.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 TronGridEvent
|
||
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
|
||
}
|
||
|
||
// 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()
|
||
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)
|
||
}
|
||
return addresses
|
||
}
|