new trc
This commit is contained in:
@@ -1,261 +1,140 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"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/mdb"
|
||||
"github.com/assimon/luuu/model/request"
|
||||
"github.com/assimon/luuu/telegram"
|
||||
"github.com/assimon/luuu/util/constant"
|
||||
"github.com/assimon/luuu/util/http_client"
|
||||
"github.com/assimon/luuu/util/log"
|
||||
"github.com/assimon/luuu/util/math"
|
||||
"github.com/dromara/carbon/v2"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/gookit/goutil/stdutil"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tidwall/gjson"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const TRC20_USDT_ID = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
|
||||
|
||||
func Trc20CallBack(address string, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
func TryProcessTronTRC20Transfer(toAddr string, rawValue *big.Int, txHash string, blockTsMs int64) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Sugar.Error(err)
|
||||
log.Sugar.Errorf("[TRC20][%s] TryProcessTronTRC20Transfer panic: %v", toAddr, err)
|
||||
}
|
||||
}()
|
||||
|
||||
var innerWg sync.WaitGroup
|
||||
innerWg.Add(2)
|
||||
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)
|
||||
addr := strings.TrimSpace(toAddr)
|
||||
if addr == "" || rawValue == nil || rawValue.Sign() <= 0 {
|
||||
return
|
||||
}
|
||||
log.Sugar.Debugf("[TRX][%s] fetched %d transfer records", address, len(transfers))
|
||||
|
||||
for i, transfer := range transfers {
|
||||
if transfer.Get("raw_data.contract.0.type").String() != "TransferContract" {
|
||||
continue
|
||||
}
|
||||
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)
|
||||
decimalQuant := decimal.NewFromBigInt(rawValue, 0)
|
||||
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1_000_000)).InexactFloat64(), 2)
|
||||
if amount <= 0 {
|
||||
continue
|
||||
return
|
||||
}
|
||||
|
||||
txID := transfer.Get("txID").String()
|
||||
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, address, "TRX", amount)
|
||||
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, addr, "USDT", amount)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Sugar.Warnf("[TRC20][%s] lock lookup: %v", addr, err)
|
||||
return
|
||||
}
|
||||
if tradeID == "" {
|
||||
log.Sugar.Debugf("[TRX][%s] skip unmatched tx hash=%s amount=%.2f", address, txID, amount)
|
||||
continue
|
||||
log.Sugar.Debugf("[TRC20][%s] skip unmatched tx hash=%s amount=%.2f", addr, txHash, amount)
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func checkTrc20Transfers(address string, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Sugar.Errorf("[TRC20][%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/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)
|
||||
log.Sugar.Warnf("[TRC20][%s] load order: %v", addr, err)
|
||||
return
|
||||
}
|
||||
log.Sugar.Debugf("[TRC20][%s] fetched %d transfer records", address, len(transfers))
|
||||
|
||||
for i, transfer := range transfers {
|
||||
if transfer.Get("token_info.address").String() != TRC20_USDT_ID {
|
||||
continue
|
||||
}
|
||||
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
|
||||
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: address,
|
||||
ReceiveAddress: addr,
|
||||
Token: "USDT",
|
||||
Network: mdb.NetworkTron,
|
||||
TradeId: tradeID,
|
||||
Amount: amount,
|
||||
BlockTransactionId: txID,
|
||||
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", address, tradeID, txID, err)
|
||||
continue
|
||||
log.Sugar.Infof("[TRC20][%s] skip resolved transfer trade_id=%s hash=%s err=%v", addr, tradeID, txHash, err)
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
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", address, tradeID, txID)
|
||||
log.Sugar.Infof("[TRC20][%s] payment processed trade_id=%s hash=%s", addr, tradeID, txHash)
|
||||
}
|
||||
|
||||
func TryProcessTronTRXTransfer(toAddr string, rawSun int64, txHash string, blockTsMs int64) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Sugar.Errorf("[TRX][%s] TryProcessTronTRXTransfer panic: %v", toAddr, err)
|
||||
}
|
||||
}()
|
||||
|
||||
addr := strings.TrimSpace(toAddr)
|
||||
if addr == "" || rawSun <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
decimalQuant := decimal.NewFromInt(rawSun)
|
||||
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1_000_000)).InexactFloat64(), 2)
|
||||
if amount <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -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) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
|
||||
+2
-8
@@ -11,17 +11,11 @@ func Start() {
|
||||
go StartBscWebSocketListener()
|
||||
go StartPolygonWebSocketListener()
|
||||
go StartPlasmaWebSocketListener()
|
||||
go StartTronBlockScannerListener()
|
||||
|
||||
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
|
||||
|
||||
@@ -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