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" ) const TRC20_USDT_ID = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" func Trc20CallBack(address string, wg *sync.WaitGroup) { defer wg.Done() defer func() { if err := recover(); err != nil { log.Sugar.Error(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) 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) 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) } } 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) 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 } 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) } } func evmChainLogLabel(chainNetwork string) string { switch chainNetwork { case mdb.NetworkEthereum: return "ETH" case mdb.NetworkBsc: return "BSC" case mdb.NetworkPolygon: return "POLYGON" case mdb.NetworkPlasma: return "PLASMA" default: return "EVM" } } // 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 { log.Sugar.Errorf("[%s-WS] TryProcessEvmERC20Transfer panic: %v", evmChainLogLabel(chainNetwork), err) } }() var usdt, usdc common.Address var polygonUsdcE common.Address switch chainNetwork { case mdb.NetworkEthereum: usdt = common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7") usdc = common.HexToAddress("0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") case mdb.NetworkBsc: usdt = common.HexToAddress("0x55d398326f99059fF775485246999027B3197955") usdc = common.HexToAddress("0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d") case mdb.NetworkPolygon: usdt = common.HexToAddress("0xc2132D05D31c914a87C6611C10748AEb04B58e8F") usdc = common.HexToAddress("0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359") polygonUsdcE = common.HexToAddress("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174") case mdb.NetworkPlasma: // USDT0(官方),6 decimals;链上暂无与 ETH 同级的 Circle USDC 部署,仅匹配 USDT 订单 usdt = common.HexToAddress("0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb") default: return } var tokenSym string switch { case contract == usdt: tokenSym = "USDT" case contract == usdc || (polygonUsdcE != (common.Address{}) && contract == polygonUsdcE): tokenSym = "USDC" default: net := evmChainLogLabel(chainNetwork) log.Sugar.Warnf("[%s-WS] skip unsupported contract %s", net, contract.Hex()) return } net := evmChainLogLabel(chainNetwork) walletAddr := strings.ToLower(toAddr.Hex()) if rawValue == nil || rawValue.Sign() <= 0 { log.Sugar.Infof("[%s-%s][%s] skip non-positive or nil amount", net, tokenSym, 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("[%s-%s][%s] skip non-positive amount %.2f", net, tokenSym, walletAddr, amount) return } log.Sugar.Debugf("[%s-%s][%s] processing transfer hash=%s amount=%.2f", net, tokenSym, walletAddr, txHash, amount) tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(chainNetwork, walletAddr, tokenSym, amount) if err != nil { log.Sugar.Warnf("[%s-%s][%s] lock lookup: %v", net, tokenSym, walletAddr, err) return } if tradeID == "" { log.Sugar.Warnf("[%s-%s][%s] skip unmatched tx hash=%s amount=%.2f", net, tokenSym, walletAddr, txHash, amount) return } order, err := data.GetOrderInfoByTradeId(tradeID) if err != nil { log.Sugar.Warnf("[%s-%s][%s] load order: %v", net, tokenSym, walletAddr, err) return } if strings.ToLower(strings.TrimSpace(order.Network)) != chainNetwork { log.Sugar.Warnf("[%s-%s][%s] skip trade_id=%s network=%q", net, tokenSym, walletAddr, tradeID, order.Network) return } if strings.ToUpper(strings.TrimSpace(order.Token)) != tokenSym { log.Sugar.Warnf("[%s-%s][%s] skip trade_id=%s token mismatch order=%s", net, tokenSym, walletAddr, tradeID, order.Token) return } req := &request.OrderProcessingRequest{ ReceiveAddress: walletAddr, Token: tokenSym, Network: chainNetwork, 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("[%s-%s][%s] skip resolved trade_id=%s hash=%s err=%v", net, tokenSym, walletAddr, tradeID, txHash, err) return } log.Sugar.Errorf("[%s-%s][%s] OrderProcessing: %v", net, tokenSym, walletAddr, err) return } sendPaymentNotification(order) log.Sugar.Infof("[%s-%s][%s] payment processed trade_id=%s hash=%s", net, tokenSym, walletAddr, tradeID, txHash) } func sendPaymentNotification(order *mdb.Orders) { msg := fmt.Sprintf( "🎉 收款成功通知\n\n"+ "💰 金额信息\n"+ "├ 订单金额:%.2f %s\n"+ "└ 实际到账:%.2f %s\n\n"+ "📋 订单信息\n"+ "├ 交易号:%s\n"+ "├ 订单号:%s\n"+ "├ 网络:%s\n"+ "└ 钱包地址:%s\n\n"+ "⏰ 时间信息\n"+ "├ 创建时间:%s\n"+ "└ 支付时间:%s", order.Amount, strings.ToUpper(order.Currency), order.ActualAmount, strings.ToUpper(order.Token), order.TradeId, order.OrderId, networkDisplay(order.Network), order.ReceiveAddress, order.CreatedAt.ToDateTimeString(), carbon.Now().ToDateTimeString(), ) telegram.SendToBot(msg) } func networkDisplay(n string) string { switch strings.ToLower(strings.TrimSpace(n)) { case mdb.NetworkTron: return "Tron" case mdb.NetworkSolana: return "Solana" case mdb.NetworkEthereum: return "Ethereum" case mdb.NetworkBsc: return "BSC" case mdb.NetworkPolygon: return "Polygon" case mdb.NetworkPlasma: return "Plasma" default: if n == "" { return "Tron" } return strings.ToUpper(n) } }