new trc
This commit is contained in:
+100
-222
@@ -1,261 +1,140 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
|
||||||
tron "github.com/assimon/luuu/crypto"
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/assimon/luuu/model/data"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/assimon/luuu/model/mdb"
|
||||||
"github.com/assimon/luuu/model/request"
|
"github.com/assimon/luuu/model/request"
|
||||||
"github.com/assimon/luuu/telegram"
|
"github.com/assimon/luuu/telegram"
|
||||||
"github.com/assimon/luuu/util/constant"
|
"github.com/assimon/luuu/util/constant"
|
||||||
"github.com/assimon/luuu/util/http_client"
|
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/assimon/luuu/util/log"
|
||||||
"github.com/assimon/luuu/util/math"
|
"github.com/assimon/luuu/util/math"
|
||||||
"github.com/dromara/carbon/v2"
|
"github.com/dromara/carbon/v2"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/gookit/goutil/stdutil"
|
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"github.com/tidwall/gjson"
|
"math/big"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const TRC20_USDT_ID = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
|
func TryProcessTronTRC20Transfer(toAddr string, rawValue *big.Int, txHash string, blockTsMs int64) {
|
||||||
|
|
||||||
func Trc20CallBack(address string, wg *sync.WaitGroup) {
|
|
||||||
defer wg.Done()
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
log.Sugar.Error(err)
|
log.Sugar.Errorf("[TRC20][%s] TryProcessTronTRC20Transfer panic: %v", toAddr, err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var innerWg sync.WaitGroup
|
addr := strings.TrimSpace(toAddr)
|
||||||
innerWg.Add(2)
|
if addr == "" || rawValue == nil || rawValue.Sign() <= 0 {
|
||||||
go checkTrxTransfers(address, &innerWg)
|
|
||||||
go checkTrc20Transfers(address, &innerWg)
|
|
||||||
innerWg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkTrxTransfers(address string, wg *sync.WaitGroup) {
|
|
||||||
defer wg.Done()
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
log.Sugar.Errorf("[TRX][%s] panic recovered: %v", address, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
client := http_client.GetHttpClient()
|
|
||||||
startTime := carbon.Now().AddHours(-24).TimestampMilli()
|
|
||||||
endTime := carbon.Now().TimestampMilli()
|
|
||||||
url := fmt.Sprintf("https://api.trongrid.io/v1/accounts/%s/transactions", address)
|
|
||||||
|
|
||||||
resp, err := client.R().SetQueryParams(map[string]string{
|
|
||||||
"order_by": "block_timestamp,desc",
|
|
||||||
"limit": "100",
|
|
||||||
"only_to": "true",
|
|
||||||
"min_timestamp": stdutil.ToString(startTime),
|
|
||||||
"max_timestamp": stdutil.ToString(endTime),
|
|
||||||
}).SetHeader("TRON-PRO-API-KEY", config.TRON_GRID_API_KEY).Get(url)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if resp.StatusCode() != http.StatusOK {
|
|
||||||
panic(fmt.Sprintf("TRX API returned status %d", resp.StatusCode()))
|
|
||||||
}
|
|
||||||
|
|
||||||
success := gjson.GetBytes(resp.Body(), "success").Bool()
|
|
||||||
if !success {
|
|
||||||
panic("TRX API response indicates failure")
|
|
||||||
}
|
|
||||||
|
|
||||||
transfers := gjson.GetBytes(resp.Body(), "data").Array()
|
|
||||||
if len(transfers) == 0 {
|
|
||||||
log.Sugar.Debugf("[TRX][%s] no transfer records found", address)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Sugar.Debugf("[TRX][%s] fetched %d transfer records", address, len(transfers))
|
|
||||||
|
|
||||||
for i, transfer := range transfers {
|
decimalQuant := decimal.NewFromBigInt(rawValue, 0)
|
||||||
if transfer.Get("raw_data.contract.0.type").String() != "TransferContract" {
|
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1_000_000)).InexactFloat64(), 2)
|
||||||
continue
|
if amount <= 0 {
|
||||||
}
|
return
|
||||||
if transfer.Get("ret.0.contractRet").String() != "SUCCESS" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
toAddressHex := transfer.Get("raw_data.contract.0.parameter.value.to_address").String()
|
|
||||||
toBytes, err := hex.DecodeString(toAddressHex)
|
|
||||||
if err != nil {
|
|
||||||
log.Sugar.Errorf("[TRX][%s] decode address failed on tx #%d: %v", address, i, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if tron.EncodeCheck(toBytes) != address {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
rawAmount := transfer.Get("raw_data.contract.0.parameter.value.amount").String()
|
|
||||||
decimalQuant, err := decimal.NewFromString(rawAmount)
|
|
||||||
if err != nil {
|
|
||||||
log.Sugar.Errorf("[TRX][%s] parse amount failed on tx #%d: %v", address, i, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1000000)).InexactFloat64(), 2)
|
|
||||||
if amount <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
txID := transfer.Get("txID").String()
|
|
||||||
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, address, "TRX", amount)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if tradeID == "" {
|
|
||||||
log.Sugar.Debugf("[TRX][%s] skip unmatched tx hash=%s amount=%.2f", address, txID, amount)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Sugar.Infof("[TRX][%s] matched trade_id=%s hash=%s amount=%.2f", address, tradeID, txID, amount)
|
|
||||||
|
|
||||||
order, err := data.GetOrderInfoByTradeId(tradeID)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
blockTimestamp := transfer.Get("block_timestamp").Int()
|
|
||||||
createTime := order.CreatedAt.TimestampMilli()
|
|
||||||
if blockTimestamp < createTime {
|
|
||||||
log.Sugar.Warnf("[TRX][%s] skip tx %s because block time %d is before order create time %d", address, txID, blockTimestamp, createTime)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &request.OrderProcessingRequest{
|
|
||||||
ReceiveAddress: address,
|
|
||||||
Token: "TRX",
|
|
||||||
Network: mdb.NetworkTron,
|
|
||||||
TradeId: tradeID,
|
|
||||||
Amount: amount,
|
|
||||||
BlockTransactionId: txID,
|
|
||||||
}
|
|
||||||
err = OrderProcessing(req)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
|
|
||||||
log.Sugar.Infof("[TRX][%s] skip resolved transfer trade_id=%s hash=%s err=%v", address, tradeID, txID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPaymentNotification(order)
|
|
||||||
log.Sugar.Infof("[TRX][%s] payment processed trade_id=%s hash=%s", address, tradeID, txID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, addr, "USDT", amount)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Warnf("[TRC20][%s] lock lookup: %v", addr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tradeID == "" {
|
||||||
|
log.Sugar.Debugf("[TRC20][%s] skip unmatched tx hash=%s amount=%.2f", addr, txHash, amount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Warnf("[TRC20][%s] load order: %v", addr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if blockTsMs > 0 && blockTsMs < order.CreatedAt.TimestampMilli() {
|
||||||
|
log.Sugar.Warnf("[TRC20][%s] skip tx %s because block time %d is before order create time %d", addr, txHash, blockTsMs, order.CreatedAt.TimestampMilli())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &request.OrderProcessingRequest{
|
||||||
|
ReceiveAddress: addr,
|
||||||
|
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("[TRC20][%s] skip resolved transfer trade_id=%s hash=%s err=%v", addr, tradeID, txHash, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Sugar.Errorf("[TRC20][%s] OrderProcessing trade_id=%s hash=%s: %v", addr, tradeID, txHash, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPaymentNotification(order)
|
||||||
|
log.Sugar.Infof("[TRC20][%s] payment processed trade_id=%s hash=%s", addr, tradeID, txHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkTrc20Transfers(address string, wg *sync.WaitGroup) {
|
func TryProcessTronTRXTransfer(toAddr string, rawSun int64, txHash string, blockTsMs int64) {
|
||||||
defer wg.Done()
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
log.Sugar.Errorf("[TRC20][%s] panic recovered: %v", address, err)
|
log.Sugar.Errorf("[TRX][%s] TryProcessTronTRXTransfer panic: %v", toAddr, err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
client := http_client.GetHttpClient()
|
addr := strings.TrimSpace(toAddr)
|
||||||
startTime := carbon.Now().AddHours(-24).TimestampMilli()
|
if addr == "" || rawSun <= 0 {
|
||||||
endTime := carbon.Now().TimestampMilli()
|
|
||||||
url := fmt.Sprintf("https://api.trongrid.io/v1/accounts/%s/transactions/trc20", address)
|
|
||||||
|
|
||||||
resp, err := client.R().SetQueryParams(map[string]string{
|
|
||||||
"order_by": "block_timestamp,desc",
|
|
||||||
"limit": "100",
|
|
||||||
"only_to": "true",
|
|
||||||
"min_timestamp": stdutil.ToString(startTime),
|
|
||||||
"max_timestamp": stdutil.ToString(endTime),
|
|
||||||
}).SetHeader("TRON-PRO-API-KEY", config.TRON_GRID_API_KEY).Get(url)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if resp.StatusCode() != http.StatusOK {
|
|
||||||
panic(fmt.Sprintf("TRC20 API returned status %d", resp.StatusCode()))
|
|
||||||
}
|
|
||||||
|
|
||||||
success := gjson.GetBytes(resp.Body(), "success").Bool()
|
|
||||||
if !success {
|
|
||||||
panic("TRC20 API response indicates failure")
|
|
||||||
}
|
|
||||||
|
|
||||||
transfers := gjson.GetBytes(resp.Body(), "data").Array()
|
|
||||||
if len(transfers) == 0 {
|
|
||||||
log.Sugar.Debugf("[TRC20][%s] no transfer records found", address)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Sugar.Debugf("[TRC20][%s] fetched %d transfer records", address, len(transfers))
|
|
||||||
|
|
||||||
for i, transfer := range transfers {
|
decimalQuant := decimal.NewFromInt(rawSun)
|
||||||
if transfer.Get("token_info.address").String() != TRC20_USDT_ID {
|
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1_000_000)).InexactFloat64(), 2)
|
||||||
continue
|
if amount <= 0 {
|
||||||
}
|
return
|
||||||
if transfer.Get("to").String() != address {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
valueStr := transfer.Get("value").String()
|
|
||||||
decimalQuant, err := decimal.NewFromString(valueStr)
|
|
||||||
if err != nil {
|
|
||||||
log.Sugar.Errorf("[TRC20][%s] parse value failed on tx #%d: %v", address, i, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tokenDecimals := transfer.Get("token_info.decimals").Int()
|
|
||||||
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.New(1, int32(tokenDecimals))).InexactFloat64(), 2)
|
|
||||||
if amount <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
txID := transfer.Get("transaction_id").String()
|
|
||||||
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, address, "USDT", amount)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if tradeID == "" {
|
|
||||||
log.Sugar.Debugf("[TRC20][%s] skip unmatched tx hash=%s amount=%.2f", address, txID, amount)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Sugar.Infof("[TRC20][%s] matched trade_id=%s hash=%s amount=%.2f", address, tradeID, txID, amount)
|
|
||||||
|
|
||||||
order, err := data.GetOrderInfoByTradeId(tradeID)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
blockTimestamp := transfer.Get("block_timestamp").Int()
|
|
||||||
createTime := order.CreatedAt.TimestampMilli()
|
|
||||||
if blockTimestamp < createTime {
|
|
||||||
log.Sugar.Warnf("[TRC20][%s] skip tx %s because block time %d is before order create time %d", address, txID, blockTimestamp, createTime)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &request.OrderProcessingRequest{
|
|
||||||
ReceiveAddress: address,
|
|
||||||
Token: "USDT",
|
|
||||||
Network: mdb.NetworkTron,
|
|
||||||
TradeId: tradeID,
|
|
||||||
Amount: amount,
|
|
||||||
BlockTransactionId: txID,
|
|
||||||
}
|
|
||||||
err = OrderProcessing(req)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
|
|
||||||
log.Sugar.Infof("[TRC20][%s] skip resolved transfer trade_id=%s hash=%s err=%v", address, tradeID, txID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPaymentNotification(order)
|
|
||||||
log.Sugar.Infof("[TRC20][%s] payment processed trade_id=%s hash=%s", address, tradeID, txID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, addr, "TRX", amount)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Warnf("[TRX][%s] lock lookup: %v", addr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tradeID == "" {
|
||||||
|
log.Sugar.Debugf("[TRX][%s] skip unmatched tx hash=%s amount=%.2f", addr, txHash, amount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Warnf("[TRX][%s] load order: %v", addr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if blockTsMs > 0 && blockTsMs < order.CreatedAt.TimestampMilli() {
|
||||||
|
log.Sugar.Warnf("[TRX][%s] skip tx %s because block time %d is before order create time %d", addr, txHash, blockTsMs, order.CreatedAt.TimestampMilli())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &request.OrderProcessingRequest{
|
||||||
|
ReceiveAddress: addr,
|
||||||
|
Token: "TRX",
|
||||||
|
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("[TRX][%s] skip resolved transfer trade_id=%s hash=%s err=%v", addr, tradeID, txHash, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Sugar.Errorf("[TRX][%s] OrderProcessing trade_id=%s hash=%s: %v", addr, tradeID, txHash, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPaymentNotification(order)
|
||||||
|
log.Sugar.Infof("[TRX][%s] payment processed trade_id=%s hash=%s", addr, tradeID, txHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
func evmChainLogLabel(chainNetwork string) string {
|
func evmChainLogLabel(chainNetwork string) string {
|
||||||
@@ -273,7 +152,6 @@ func evmChainLogLabel(chainNetwork string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TryProcessEvmERC20Transfer 处理各 EVM 链上 USDT/USDC(及 Polygon USDC.e)的 Transfer 入账(合约与 network 需一致)。
|
|
||||||
func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, toAddr common.Address, rawValue *big.Int, txHash string, blockTsMs int64) {
|
func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, toAddr common.Address, rawValue *big.Int, txHash string, blockTsMs int64) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
|
|||||||
+2
-8
@@ -11,17 +11,11 @@ func Start() {
|
|||||||
go StartBscWebSocketListener()
|
go StartBscWebSocketListener()
|
||||||
go StartPolygonWebSocketListener()
|
go StartPolygonWebSocketListener()
|
||||||
go StartPlasmaWebSocketListener()
|
go StartPlasmaWebSocketListener()
|
||||||
|
go StartTronBlockScannerListener()
|
||||||
|
|
||||||
c := cron.New()
|
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钱包监听
|
// solana钱包监听
|
||||||
_, err = c.AddJob("@every 5s", ListenSolJob{})
|
_, err := c.AddJob("@every 5s", ListenSolJob{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Sugar.Errorf("[task] Failed to add ListenSolJob: %v", err)
|
log.Sugar.Errorf("[task] Failed to add ListenSolJob: %v", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,372 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/assimon/luuu/config"
|
||||||
|
tron "github.com/assimon/luuu/crypto"
|
||||||
|
"github.com/assimon/luuu/model/service"
|
||||||
|
"github.com/assimon/luuu/util/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TronAPIBase = "https://api.trongrid.io"
|
||||||
|
|
||||||
|
// USDT 合约地址 (TRC20 主网)
|
||||||
|
USDTContractHex = "41a614f803b6fd780986a42c78ec9c7f77e6ded13c" // Base58: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
|
||||||
|
// transfer(address,uint256) 方法签名前4字节
|
||||||
|
TransferMethodID = "a9059cbb"
|
||||||
|
|
||||||
|
PollInterval = 3 * time.Second
|
||||||
|
RequestTimeout = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func HexToTronAddress(hexAddr string) (string, error) {
|
||||||
|
hexAddr = strings.TrimPrefix(hexAddr, "0x")
|
||||||
|
hexAddr = strings.TrimPrefix(hexAddr, "0X")
|
||||||
|
|
||||||
|
raw, err := hex.DecodeString(hexAddr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保有 0x41 前缀(TRON 主网地址前缀)
|
||||||
|
if len(raw) == 20 {
|
||||||
|
raw = append([]byte{0x41}, raw...)
|
||||||
|
}
|
||||||
|
if len(raw) != 21 {
|
||||||
|
return "", fmt.Errorf("地址长度非法: %d bytes", len(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tron.EncodeCheck(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockHeader struct {
|
||||||
|
RawData struct {
|
||||||
|
Number int64 `json:"number"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
WitnessAddress string `json:"witness_address"`
|
||||||
|
ParentHash string `json:"parentHash"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
} `json:"raw_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TriggerSmartContractValue struct {
|
||||||
|
OwnerAddress string `json:"owner_address"`
|
||||||
|
ContractAddress string `json:"contract_address"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
CallValue int64 `json:"call_value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContractParam struct {
|
||||||
|
TypeURL string `json:"type_url"`
|
||||||
|
Value json.RawMessage `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Transaction struct {
|
||||||
|
TxID string `json:"txID"`
|
||||||
|
RawData struct {
|
||||||
|
Contract []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Parameter ContractParam `json:"parameter"`
|
||||||
|
} `json:"contract"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
FeeLimit int64 `json:"fee_limit"`
|
||||||
|
} `json:"raw_data"`
|
||||||
|
Ret []struct {
|
||||||
|
ContractRet string `json:"contractRet"`
|
||||||
|
} `json:"ret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Block struct {
|
||||||
|
BlockID string `json:"blockID"`
|
||||||
|
BlockHeader BlockHeader `json:"block_header"`
|
||||||
|
Transactions []Transaction `json:"transactions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type USDTTransfer struct {
|
||||||
|
TxID string
|
||||||
|
From string
|
||||||
|
To string
|
||||||
|
Raw *big.Int // 原始数值(6 decimals)
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TRXTransfer struct {
|
||||||
|
TxID string
|
||||||
|
From string
|
||||||
|
To string
|
||||||
|
RawSun int64 // 单位: SUN
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransferContractValue struct {
|
||||||
|
OwnerAddress string `json:"owner_address"`
|
||||||
|
ToAddress string `json:"to_address"`
|
||||||
|
Amount int64 `json:"amount"` // 单位: SUN
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpClient = &http.Client{Timeout: RequestTimeout}
|
||||||
|
|
||||||
|
func doPost(url string, body interface{}) ([]byte, error) {
|
||||||
|
data, _ := json.Marshal(body)
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
if k := strings.TrimSpace(config.TRON_GRID_API_KEY); k != "" {
|
||||||
|
req.Header.Set("TRON-PRO-API-KEY", k)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(b))
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNowBlock() (*Block, error) {
|
||||||
|
b, err := doPost(TronAPIBase+"/wallet/getnowblock", map[string]interface{}{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var block Block
|
||||||
|
return &block, json.Unmarshal(b, &block)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBlockByNum(num int64) (*Block, error) {
|
||||||
|
b, err := doPost(TronAPIBase+"/wallet/getblockbynum", map[string]interface{}{"num": num})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var block Block
|
||||||
|
return &block, json.Unmarshal(b, &block)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUSDTTransfer(tx Transaction) *USDTTransfer {
|
||||||
|
if len(tx.RawData.Contract) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c := tx.RawData.Contract[0]
|
||||||
|
if c.Type != "TriggerSmartContract" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var val TriggerSmartContractValue
|
||||||
|
if err := json.Unmarshal(c.Parameter.Value, &val); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是 USDT 合约
|
||||||
|
contractHex := strings.ToLower(strings.TrimPrefix(val.ContractAddress, "0x"))
|
||||||
|
if contractHex != USDTContractHex {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 data 字段
|
||||||
|
// 格式: [4字节方法ID][32字节 to 地址][32字节 amount]
|
||||||
|
data := strings.TrimPrefix(strings.ToLower(val.Data), "0x")
|
||||||
|
if len(data) < 8+64+64 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if data[:8] != TransferMethodID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// to 地址:后 40 个十六进制字符(20 字节)
|
||||||
|
toHex := data[8+24 : 8+64] // 跳过前12字节填充,取后20字节
|
||||||
|
toAddr, err := HexToTronAddress(toHex)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// amount:后 32 字节大整数
|
||||||
|
amountHex := data[8+64 : 8+64+64]
|
||||||
|
amountBytes, err := hex.DecodeString(amountHex)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
amountBig := new(big.Int).SetBytes(amountBytes)
|
||||||
|
|
||||||
|
// from 地址
|
||||||
|
fromAddr, err := HexToTronAddress(val.OwnerAddress)
|
||||||
|
if err != nil {
|
||||||
|
fromAddr = val.OwnerAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
// 交易状态
|
||||||
|
status := "SUCCESS"
|
||||||
|
if len(tx.Ret) > 0 && tx.Ret[0].ContractRet != "" && tx.Ret[0].ContractRet != "SUCCESS" {
|
||||||
|
status = tx.Ret[0].ContractRet
|
||||||
|
}
|
||||||
|
|
||||||
|
return &USDTTransfer{
|
||||||
|
TxID: tx.TxID,
|
||||||
|
From: fromAddr,
|
||||||
|
To: toAddr,
|
||||||
|
Raw: amountBig,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTRXTransfer(tx Transaction) *TRXTransfer {
|
||||||
|
if len(tx.RawData.Contract) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c := tx.RawData.Contract[0]
|
||||||
|
if c.Type != "TransferContract" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var val TransferContractValue
|
||||||
|
if err := json.Unmarshal(c.Parameter.Value, &val); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if val.Amount <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fromAddr, err := HexToTronAddress(val.OwnerAddress)
|
||||||
|
if err != nil {
|
||||||
|
fromAddr = val.OwnerAddress
|
||||||
|
}
|
||||||
|
toAddr, err := HexToTronAddress(val.ToAddress)
|
||||||
|
if err != nil {
|
||||||
|
toAddr = val.ToAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
status := "SUCCESS"
|
||||||
|
if len(tx.Ret) > 0 && tx.Ret[0].ContractRet != "" && tx.Ret[0].ContractRet != "SUCCESS" {
|
||||||
|
status = tx.Ret[0].ContractRet
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TRXTransfer{
|
||||||
|
TxID: tx.TxID,
|
||||||
|
From: fromAddr,
|
||||||
|
To: toAddr,
|
||||||
|
RawSun: val.Amount,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processBlock(block *Block) {
|
||||||
|
blockTsMs := block.BlockHeader.RawData.Timestamp
|
||||||
|
for _, tx := range block.Transactions {
|
||||||
|
if t := parseUSDTTransfer(tx); t != nil {
|
||||||
|
if t.Status != "SUCCESS" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
service.TryProcessTronTRC20Transfer(t.To, t.Raw, t.TxID, blockTsMs)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t := parseTRXTransfer(tx); t != nil {
|
||||||
|
if t.Status != "SUCCESS" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
service.TryProcessTronTRXTransfer(t.To, t.RawSun, t.TxID, blockTsMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Scanner struct {
|
||||||
|
lastBlock int64
|
||||||
|
// 统计
|
||||||
|
totalBlocks int64
|
||||||
|
totalUSDTTxs int64
|
||||||
|
totalTRXTxs int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScanner() *Scanner {
|
||||||
|
return &Scanner{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) Init() error {
|
||||||
|
log.Sugar.Infof("[TRON-BLOCK] node=%s", TronAPIBase)
|
||||||
|
block, err := GetNowBlock()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取初始块失败: %w", err)
|
||||||
|
}
|
||||||
|
s.lastBlock = block.BlockHeader.RawData.Number
|
||||||
|
log.Sugar.Infof("[TRON-BLOCK] start block=%d", s.lastBlock)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) Run() {
|
||||||
|
log.Sugar.Info("[TRON-BLOCK] start scanning (USDT TRC20 + TRX)")
|
||||||
|
ticker := time.NewTicker(PollInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
statTicker := time.NewTicker(60 * time.Second)
|
||||||
|
defer statTicker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-statTicker.C:
|
||||||
|
log.Sugar.Infof("[TRON-BLOCK] stats blocks=%d usdt=%d trx=%d", s.totalBlocks, s.totalUSDTTxs, s.totalTRXTxs)
|
||||||
|
case <-ticker.C:
|
||||||
|
s.poll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) poll() {
|
||||||
|
latest, err := GetNowBlock()
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Warnf("[TRON-BLOCK] get latest block: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
latestNum := latest.BlockHeader.RawData.Number
|
||||||
|
if latestNum <= s.lastBlock {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for num := s.lastBlock + 1; num <= latestNum; num++ {
|
||||||
|
var block *Block
|
||||||
|
if num == latestNum {
|
||||||
|
block = latest
|
||||||
|
} else {
|
||||||
|
block, err = GetBlockByNum(num)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Warnf("[TRON-BLOCK] get block %d: %v", num, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
processBlock(block)
|
||||||
|
s.lastBlock = num
|
||||||
|
s.totalBlocks++
|
||||||
|
|
||||||
|
for _, tx := range block.Transactions {
|
||||||
|
if parseUSDTTransfer(tx) != nil {
|
||||||
|
s.totalUSDTTxs++
|
||||||
|
} else if parseTRXTransfer(tx) != nil {
|
||||||
|
s.totalTRXTxs++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartTronBlockScannerListener() {
|
||||||
|
scanner := NewScanner()
|
||||||
|
if err := scanner.Init(); err != nil {
|
||||||
|
log.Sugar.Errorf("[TRON-BLOCK] init: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scanner.Run()
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package task
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
|
||||||
"github.com/assimon/luuu/model/mdb"
|
|
||||||
"github.com/assimon/luuu/model/service"
|
|
||||||
"github.com/assimon/luuu/util/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ListenTrc20Job struct{}
|
|
||||||
|
|
||||||
var gListenTrc20JobLock sync.Mutex
|
|
||||||
|
|
||||||
func (r ListenTrc20Job) Run() {
|
|
||||||
gListenTrc20JobLock.Lock()
|
|
||||||
defer gListenTrc20JobLock.Unlock()
|
|
||||||
log.Sugar.Debug("[ListenTrc20Job] Job triggered")
|
|
||||||
walletAddress, err := data.GetAvailableWalletAddressByNetwork(mdb.NetworkTron)
|
|
||||||
if err != nil {
|
|
||||||
log.Sugar.Errorf("[ListenTrc20Job] Failed to get wallet addresses: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(walletAddress) <= 0 {
|
|
||||||
log.Sugar.Debug("[ListenTrc20Job] No available wallet addresses")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go service.Trc20CallBack(address.Address, &wg)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
log.Sugar.Debug("[ListenTrc20Job] Job completed")
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user