This commit is contained in:
Ginta
2026-04-15 21:21:21 +08:00
parent 097c716714
commit 7d0217b618
8 changed files with 184 additions and 39 deletions
+74
View File
@@ -374,6 +374,80 @@ func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, to
log.Sugar.Infof("[%s-%s][%s] payment processed trade_id=%s hash=%s", net, tokenSym, walletAddr, tradeID, txHash)
}
// TryProcessTronTRC20Transfer 处理 Tron 链上 TRC20-USDT 的 Transfer 入账。
func TryProcessTronTRC20Transfer(toAddr string, rawValue *big.Int, txHash string, blockTsMs int64) {
defer func() {
if err := recover(); err != nil {
log.Sugar.Errorf("[TRON-WS] TryProcessTronTRC20Transfer panic: %v", err)
}
}()
walletAddr := strings.TrimSpace(toAddr)
if walletAddr == "" {
return
}
if rawValue == nil || rawValue.Sign() <= 0 {
log.Sugar.Infof("[TRON-USDT][%s] skip non-positive or nil amount", walletAddr)
return
}
decimalQuant := decimal.NewFromBigInt(rawValue, 0)
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1_000_000)).InexactFloat64(), 2)
if amount <= 0 {
log.Sugar.Warnf("[TRON-USDT][%s] skip non-positive amount %.2f", walletAddr, amount)
return
}
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, walletAddr, "USDT", amount)
if err != nil {
log.Sugar.Warnf("[TRON-USDT][%s] lock lookup: %v", walletAddr, err)
return
}
if tradeID == "" {
log.Sugar.Debugf("[TRON-USDT][%s] skip unmatched tx hash=%s amount=%.2f", walletAddr, txHash, amount)
return
}
order, err := data.GetOrderInfoByTradeId(tradeID)
if err != nil {
log.Sugar.Warnf("[TRON-USDT][%s] load order: %v", walletAddr, err)
return
}
if blockTsMs > 0 && blockTsMs < order.CreatedAt.TimestampMilli() {
log.Sugar.Warnf("[TRON-USDT][%s] skip tx %s because block time %d is before order create time %d", walletAddr, txHash, blockTsMs, order.CreatedAt.TimestampMilli())
return
}
if strings.ToLower(strings.TrimSpace(order.Network)) != mdb.NetworkTron {
log.Sugar.Warnf("[TRON-USDT][%s] skip trade_id=%s network=%q", walletAddr, tradeID, order.Network)
return
}
if strings.ToUpper(strings.TrimSpace(order.Token)) != "USDT" {
log.Sugar.Warnf("[TRON-USDT][%s] skip trade_id=%s token mismatch order=%s", walletAddr, tradeID, order.Token)
return
}
req := &request.OrderProcessingRequest{
ReceiveAddress: walletAddr,
Token: "USDT",
Network: mdb.NetworkTron,
TradeId: tradeID,
Amount: amount,
BlockTransactionId: txHash,
}
err = OrderProcessing(req)
if err != nil {
if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
log.Sugar.Infof("[TRON-USDT][%s] skip resolved trade_id=%s hash=%s err=%v", walletAddr, tradeID, txHash, err)
return
}
log.Sugar.Errorf("[TRON-USDT][%s] OrderProcessing: %v", walletAddr, err)
return
}
sendPaymentNotification(order)
log.Sugar.Infof("[TRON-USDT][%s] payment processed trade_id=%s hash=%s", walletAddr, tradeID, txHash)
}
func sendPaymentNotification(order *mdb.Orders) {
msg := fmt.Sprintf(
"🎉 <b>收款成功通知</b>\n\n"+
+2 -8
View File
@@ -11,17 +11,11 @@ func Start() {
go StartBscWebSocketListener()
go StartPolygonWebSocketListener()
go StartPlasmaWebSocketListener()
go StartTronTrc20WebSocketListener()
c := cron.New()
// trc20钱包监听
_, err := c.AddJob("@every 5s", ListenTrc20Job{})
if err != nil {
log.Sugar.Errorf("[task] Failed to add ListenTrc20Job: %v", err)
return
}
log.Sugar.Info("[task] ListenTrc20Job scheduled successfully (@every 5s)")
// solana钱包监听
_, err = c.AddJob("@every 5s", ListenSolJob{})
_, err := c.AddJob("@every 5s", ListenSolJob{})
if err != nil {
log.Sugar.Errorf("[task] Failed to add ListenSolJob: %v", err)
return
+1 -1
View File
@@ -58,7 +58,7 @@ func StartBscWebSocketListener() {
Topics: [][]common.Hash{},
}
runEvmWsLogListener("[BSC-WS]", wsURL, query, func(client *ethclient.Client, vLog types.Log) {
runWsLogListener("[BSC-WS]", wsURL, query, func(client *ethclient.Client, vLog types.Log) {
if len(vLog.Topics) < 3 {
return
}
+1 -1
View File
@@ -62,7 +62,7 @@ func StartEthereumWebSocketListener() {
Topics: [][]common.Hash{},
}
runEvmWsLogListener("[ETH-WS]", wsURL, query, func(client *ethclient.Client, vLog types.Log) {
runWsLogListener("[ETH-WS]", wsURL, query, func(client *ethclient.Client, vLog types.Log) {
if len(vLog.Topics) < 3 {
return
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/ethereum/go-ethereum/ethclient"
)
func runEvmWsLogListener(logPrefix, wsURL string, query ethereum.FilterQuery, handleLog func(*ethclient.Client, types.Log)) {
func runWsLogListener(logPrefix, wsURL string, query ethereum.FilterQuery, handleLog func(*ethclient.Client, types.Log)) {
const (
minBackoff = 2 * time.Second
maxBackoff = 60 * time.Second
+1 -1
View File
@@ -54,7 +54,7 @@ func StartPlasmaWebSocketListener() {
Topics: [][]common.Hash{},
}
runEvmWsLogListener("[PLASMA-WS]", wsURL, query, func(client *ethclient.Client, vLog types.Log) {
runWsLogListener("[PLASMA-WS]", wsURL, query, func(client *ethclient.Client, vLog types.Log) {
if len(vLog.Topics) < 3 {
return
}
+1 -1
View File
@@ -60,7 +60,7 @@ func StartPolygonWebSocketListener() {
Topics: [][]common.Hash{},
}
runEvmWsLogListener("[POLYGON-WS]", wsURL, query, func(client *ethclient.Client, vLog types.Log) {
runWsLogListener("[POLYGON-WS]", wsURL, query, func(client *ethclient.Client, vLog types.Log) {
if len(vLog.Topics) < 3 {
return
}
+97 -20
View File
@@ -1,39 +1,116 @@
package task
import (
"sync"
"context"
"math/big"
"strings"
"sync/atomic"
"time"
tron "github.com/assimon/luuu/crypto"
"github.com/assimon/luuu/model/data"
"github.com/assimon/luuu/model/mdb"
"github.com/assimon/luuu/model/service"
"github.com/assimon/luuu/util/log"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
type ListenTrc20Job struct{}
const tronUsdtContractBase58 = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
var gListenTrc20JobLock sync.Mutex
// TronGrid 的 JSON-RPC 使用 20-byte hex 地址(去掉 Tron 主网 0x41 前缀)。
var tronUsdtTrc20Contract = common.HexToAddress("0xa614f803b6fd780986a42c78ec9c7f77e6ded13c")
func (r ListenTrc20Job) Run() {
gListenTrc20JobLock.Lock()
defer gListenTrc20JobLock.Unlock()
log.Sugar.Debug("[ListenTrc20Job] Job triggered")
walletAddress, err := data.GetAvailableWalletAddressByNetwork(mdb.NetworkTron)
type tronRecipientSnapshot struct {
addrs map[string]struct{}
}
var tronWatchedRecipients atomic.Pointer[tronRecipientSnapshot]
func StartTronTrc20WebSocketListener() {
log.Sugar.Infof("[TRON-WS] listening contract base58=%s hex=%s", tronUsdtContractBase58, tronUsdtTrc20Contract.Hex())
wallets, err := data.GetAvailableWalletAddressByNetwork(mdb.NetworkTron)
if err != nil {
log.Sugar.Errorf("[ListenTrc20Job] Failed to get wallet addresses: %v", err)
log.Sugar.Errorf("[TRON-WS] Failed to get wallet addresses: %v", err)
return
}
if len(walletAddress) <= 0 {
log.Sugar.Debug("[ListenTrc20Job] No available wallet addresses")
return
storeTronRecipientsFromWallets(wallets)
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
w, err := data.GetAvailableWalletAddressByNetwork(mdb.NetworkTron)
if err != nil {
log.Sugar.Warnf("[TRON-WS] refresh wallet addresses: %v", err)
continue
}
log.Sugar.Infof("[ListenTrc20Job] Found %d wallet addresses to monitor", len(walletAddress))
var wg sync.WaitGroup
for _, address := range walletAddress {
log.Sugar.Infof("[ListenTrc20Job] Listening to address: %s", address.Address)
storeTronRecipientsFromWallets(w)
}
}()
wg.Add(1)
go service.Trc20CallBack(address.Address, &wg)
wsURL := "wss://api.trongrid.io/jsonrpc"
query := ethereum.FilterQuery{
Addresses: []common.Address{tronUsdtTrc20Contract},
Topics: [][]common.Hash{},
}
wg.Wait()
log.Sugar.Debug("[ListenTrc20Job] Job completed")
runWsLogListener("[TRON-WS]", wsURL, query, func(client *ethclient.Client, vLog types.Log) {
if len(vLog.Topics) < 3 {
return
}
if vLog.Topics[0].String() != transferEventHash.String() {
return
}
toAddr := tronHexToBase58(common.HexToAddress(vLog.Topics[2].Hex()))
//if !isWatchedTronRecipient(toAddr) {
// return
//}
amount := new(big.Int).SetBytes(vLog.Data)
if amount.Sign() <= 0 {
return
}
log.Sugar.Infof("[TRON-WS] Detected TRC20 transfer to %s amount=%s tx=%s", toAddr, amount.String(), vLog.TxHash.Hex())
var blockTsMs int64
header, err := client.HeaderByNumber(context.Background(), big.NewInt(int64(vLog.BlockNumber)))
if err != nil {
log.Sugar.Warnf("[TRON-WS] HeaderByNumber block=%d: %v, using local time", vLog.BlockNumber, err)
blockTsMs = time.Now().UnixMilli()
} else {
blockTsMs = int64(header.Time) * 1000
}
service.TryProcessTronTRC20Transfer(toAddr, amount, vLog.TxHash.Hex(), blockTsMs)
})
}
func storeTronRecipientsFromWallets(wallets []mdb.WalletAddress) int {
m := make(map[string]struct{})
for _, w := range wallets {
addr := strings.TrimSpace(w.Address)
if addr == "" {
continue
}
m[addr] = struct{}{}
}
tronWatchedRecipients.Store(&tronRecipientSnapshot{addrs: m})
return len(m)
}
func isWatchedTronRecipient(to string) bool {
snap := tronWatchedRecipients.Load()
if snap == nil || len(snap.addrs) == 0 {
return false
}
_, ok := snap.addrs[strings.TrimSpace(to)]
return ok
}
func tronHexToBase58(addr common.Address) string {
raw := append([]byte{tron.PrefixMainnet}, addr.Bytes()...)
return tron.EncodeCheck(raw)
}