Merge branch 'dev' into dev-runtime

This commit is contained in:
alphago9
2026-03-30 20:14:29 +08:00
27 changed files with 532 additions and 318 deletions
+38 -27
View File
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"math/rand"
"strings"
"sync"
"time"
@@ -35,14 +36,20 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
gCreateTransactionLock.Lock()
defer gCreateTransactionLock.Unlock()
token := strings.ToUpper(strings.TrimSpace(req.Token))
currency := strings.ToUpper(strings.TrimSpace(req.Currency))
payAmount := math.MustParsePrecFloat64(req.Amount, 2)
rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(currency))
if rate <= 0 {
return nil, constant.RateAmountErr
}
decimalPayAmount := decimal.NewFromFloat(payAmount)
decimalRate := decimal.NewFromFloat(config.GetUsdtRate())
decimalUsdt := decimalPayAmount.Div(decimalRate)
decimalTokenAmount := decimalPayAmount.Mul(decimal.NewFromFloat(rate))
if decimalPayAmount.Cmp(decimal.NewFromFloat(CnyMinimumPaymentAmount)) == -1 {
return nil, constant.PayAmountErr
}
if decimalUsdt.Cmp(decimal.NewFromFloat(UsdtMinimumPaymentAmount)) == -1 {
if decimalTokenAmount.Cmp(decimal.NewFromFloat(UsdtMinimumPaymentAmount)) == -1 {
return nil, constant.PayAmountErr
}
@@ -62,35 +69,37 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
return nil, constant.NotAvailableWalletAddress
}
tradeId := GenerateCode()
amount := math.MustParsePrecFloat64(decimalUsdt.InexactFloat64(), 2)
availableToken, availableAmount, err := ReserveAvailableWalletAndAmount(tradeId, amount, walletAddress)
tradeID := GenerateCode()
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
availableAddress, availableAmount, err := ReserveAvailableWalletAndAmount(tradeID, token, amount, walletAddress)
if err != nil {
return nil, err
}
if availableToken == "" {
if availableAddress == "" {
return nil, constant.NotAvailableAmountErr
}
tx := dao.Mdb.Begin()
order := &mdb.Orders{
TradeId: tradeId,
OrderId: req.OrderId,
Amount: req.Amount,
ActualAmount: availableAmount,
Token: availableToken,
Status: mdb.StatusWaitPay,
NotifyUrl: req.NotifyUrl,
RedirectUrl: req.RedirectUrl,
TradeId: tradeID,
OrderId: req.OrderId,
Amount: req.Amount,
Currency: currency,
ActualAmount: availableAmount,
ReceiveAddress: availableAddress,
Token: token,
Status: mdb.StatusWaitPay,
NotifyUrl: req.NotifyUrl,
RedirectUrl: req.RedirectUrl,
}
if err = data.CreateOrderWithTransaction(tx, order); err != nil {
tx.Rollback()
_ = data.UnLockTransaction(availableToken, availableAmount)
_ = data.UnLockTransactionByTradeId(tradeID)
return nil, err
}
if err = tx.Commit().Error; err != nil {
tx.Rollback()
_ = data.UnLockTransaction(availableToken, availableAmount)
_ = data.UnLockTransactionByTradeId(tradeID)
return nil, err
}
@@ -99,7 +108,9 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
TradeId: order.TradeId,
OrderId: order.OrderId,
Amount: order.Amount,
Currency: order.Currency,
ActualAmount: order.ActualAmount,
ReceiveAddress: order.ReceiveAddress,
Token: order.Token,
ExpirationTime: expirationTime,
PaymentUrl: fmt.Sprintf("%s/pay/checkout-counter/%s", config.GetAppUri(), order.TradeId),
@@ -137,22 +148,22 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
return err
}
if err = data.UnLockTransaction(req.Token, req.Amount); err != nil {
if err = data.UnLockTransaction(req.ReceiveAddress, req.Token, req.Amount); err != nil {
log.Sugar.Warnf("[order] unlock transaction after pay success failed, trade_id=%s, err=%v", req.TradeId, err)
}
return nil
}
// ReserveAvailableWalletAndAmount finds and locks a token+amount pair.
func ReserveAvailableWalletAndAmount(tradeId string, amount float64, walletAddress []mdb.WalletAddress) (string, float64, error) {
availableToken := ""
// ReserveAvailableWalletAndAmount finds and locks an address+token+amount pair.
func ReserveAvailableWalletAndAmount(tradeID string, token string, amount float64, walletAddress []mdb.WalletAddress) (string, float64, error) {
availableAddress := ""
availableAmount := amount
tryLockWalletFunc := func(targetAmount float64) (string, error) {
for _, address := range walletAddress {
err := data.LockTransaction(address.Token, tradeId, targetAmount, config.GetOrderExpirationTimeDuration())
err := data.LockTransaction(address.Address, token, tradeID, targetAmount, config.GetOrderExpirationTimeDuration())
if err == nil {
return address.Token, nil
return address.Address, nil
}
if errors.Is(err, data.ErrTransactionLocked) {
continue
@@ -163,20 +174,20 @@ func ReserveAvailableWalletAndAmount(tradeId string, amount float64, walletAddre
}
for i := 0; i < IncrementalMaximumNumber; i++ {
token, err := tryLockWalletFunc(availableAmount)
address, err := tryLockWalletFunc(availableAmount)
if err != nil {
return "", 0, err
}
if token == "" {
if address == "" {
decimalOldAmount := decimal.NewFromFloat(availableAmount)
decimalIncr := decimal.NewFromFloat(UsdtAmountPerIncrement)
availableAmount = decimalOldAmount.Add(decimalIncr).InexactFloat64()
continue
}
availableToken = token
availableAddress = address
break
}
return availableToken, availableAmount, nil
return availableAddress, availableAmount, nil
}
// GenerateCode creates a unique trade id.
+34 -47
View File
@@ -13,6 +13,16 @@ import (
"github.com/assimon/luuu/util/constant"
)
func newCreateTransactionRequest(orderID string, amount float64) *request.CreateTransactionRequest {
return &request.CreateTransactionRequest{
OrderId: orderID,
Currency: "CNY",
Token: "USDT",
Amount: amount,
NotifyUrl: "https://merchant.example/callback",
}
}
func TestCreateTransactionAssignsIncrementedAmountsAndLocks(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
@@ -21,22 +31,11 @@ func TestCreateTransactionAssignsIncrementedAmountsAndLocks(t *testing.T) {
t.Fatalf("add wallet: %v", err)
}
req1 := &request.CreateTransactionRequest{
OrderId: "order_1",
Amount: 1,
NotifyUrl: "https://merchant.example/callback",
}
req2 := &request.CreateTransactionRequest{
OrderId: "order_2",
Amount: 1,
NotifyUrl: "https://merchant.example/callback",
}
resp1, err := CreateTransaction(req1)
resp1, err := CreateTransaction(newCreateTransactionRequest("order_1", 1))
if err != nil {
t.Fatalf("create first transaction: %v", err)
}
resp2, err := CreateTransaction(req2)
resp2, err := CreateTransaction(newCreateTransactionRequest("order_2", 1))
if err != nil {
t.Fatalf("create second transaction: %v", err)
}
@@ -47,11 +46,14 @@ func TestCreateTransactionAssignsIncrementedAmountsAndLocks(t *testing.T) {
if got := fmt.Sprintf("%.2f", resp2.ActualAmount); got != "1.01" {
t.Fatalf("second actual amount = %s, want 1.01", got)
}
if resp1.Token != "wallet_1" || resp2.Token != "wallet_1" {
t.Fatalf("unexpected wallet tokens: %s, %s", resp1.Token, resp2.Token)
if resp1.ReceiveAddress != "wallet_1" || resp2.ReceiveAddress != "wallet_1" {
t.Fatalf("unexpected receive addresses: %s, %s", resp1.ReceiveAddress, resp2.ReceiveAddress)
}
if resp1.Token != "USDT" || resp2.Token != "USDT" {
t.Fatalf("unexpected tokens: %s, %s", resp1.Token, resp2.Token)
}
tradeID1, err := data.GetTradeIdByWalletAddressAndAmount(resp1.Token, resp1.ActualAmount)
tradeID1, err := data.GetTradeIdByWalletAddressAndAmountAndToken(resp1.ReceiveAddress, resp1.Token, resp1.ActualAmount)
if err != nil {
t.Fatalf("get first runtime lock: %v", err)
}
@@ -59,7 +61,7 @@ func TestCreateTransactionAssignsIncrementedAmountsAndLocks(t *testing.T) {
t.Fatalf("first runtime lock = %s, want %s", tradeID1, resp1.TradeId)
}
tradeID2, err := data.GetTradeIdByWalletAddressAndAmount(resp2.Token, resp2.ActualAmount)
tradeID2, err := data.GetTradeIdByWalletAddressAndAmountAndToken(resp2.ReceiveAddress, resp2.Token, resp2.ActualAmount)
if err != nil {
t.Fatalf("get second runtime lock: %v", err)
}
@@ -76,16 +78,13 @@ func TestOrderProcessingMarksPaidAndReleasesLock(t *testing.T) {
t.Fatalf("add wallet: %v", err)
}
resp, err := CreateTransaction(&request.CreateTransactionRequest{
OrderId: "order_1",
Amount: 1,
NotifyUrl: "https://merchant.example/callback",
})
resp, err := CreateTransaction(newCreateTransactionRequest("order_1", 1))
if err != nil {
t.Fatalf("create transaction: %v", err)
}
err = OrderProcessing(&request.OrderProcessingRequest{
ReceiveAddress: resp.ReceiveAddress,
Token: resp.Token,
TradeId: resp.TradeId,
Amount: resp.ActualAmount,
@@ -109,7 +108,7 @@ func TestOrderProcessingMarksPaidAndReleasesLock(t *testing.T) {
t.Fatalf("block transaction id = %s, want block_1", order.BlockTransactionId)
}
tradeID, err := data.GetTradeIdByWalletAddressAndAmount(resp.Token, resp.ActualAmount)
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(resp.ReceiveAddress, resp.Token, resp.ActualAmount)
if err != nil {
t.Fatalf("get runtime lock after processing: %v", err)
}
@@ -126,16 +125,13 @@ func TestOrderProcessingRejectsDuplicateBlockForSameOrder(t *testing.T) {
t.Fatalf("add wallet: %v", err)
}
resp, err := CreateTransaction(&request.CreateTransactionRequest{
OrderId: "order_1",
Amount: 1,
NotifyUrl: "https://merchant.example/callback",
})
resp, err := CreateTransaction(newCreateTransactionRequest("order_1", 1))
if err != nil {
t.Fatalf("create transaction: %v", err)
}
req := &request.OrderProcessingRequest{
ReceiveAddress: resp.ReceiveAddress,
Token: resp.Token,
TradeId: resp.TradeId,
Amount: resp.ActualAmount,
@@ -170,11 +166,7 @@ func TestOrderProcessingDoesNotReviveExpiredOrder(t *testing.T) {
t.Fatalf("add wallet: %v", err)
}
resp, err := CreateTransaction(&request.CreateTransactionRequest{
OrderId: "order_1",
Amount: 1,
NotifyUrl: "https://merchant.example/callback",
})
resp, err := CreateTransaction(newCreateTransactionRequest("order_1", 1))
if err != nil {
t.Fatalf("create transaction: %v", err)
}
@@ -186,6 +178,7 @@ func TestOrderProcessingDoesNotReviveExpiredOrder(t *testing.T) {
}
err = OrderProcessing(&request.OrderProcessingRequest{
ReceiveAddress: resp.ReceiveAddress,
Token: resp.Token,
TradeId: resp.TradeId,
Amount: resp.ActualAmount,
@@ -218,19 +211,11 @@ func TestOrderProcessingOnlyOneOrderClaimsABlockTransaction(t *testing.T) {
t.Fatalf("add wallet: %v", err)
}
resp1, err := CreateTransaction(&request.CreateTransactionRequest{
OrderId: "order_1",
Amount: 1,
NotifyUrl: "https://merchant.example/callback",
})
resp1, err := CreateTransaction(newCreateTransactionRequest("order_1", 1))
if err != nil {
t.Fatalf("create first transaction: %v", err)
}
resp2, err := CreateTransaction(&request.CreateTransactionRequest{
OrderId: "order_2",
Amount: 2,
NotifyUrl: "https://merchant.example/callback",
})
resp2, err := CreateTransaction(newCreateTransactionRequest("order_2", 2))
if err != nil {
t.Fatalf("create second transaction: %v", err)
}
@@ -239,24 +224,26 @@ func TestOrderProcessingOnlyOneOrderClaimsABlockTransaction(t *testing.T) {
errs := make(chan error, 2)
var wg sync.WaitGroup
for _, tc := range []struct {
address string
token string
tradeID string
amount float64
}{
{token: resp1.Token, tradeID: resp1.TradeId, amount: resp1.ActualAmount},
{token: resp2.Token, tradeID: resp2.TradeId, amount: resp2.ActualAmount},
{address: resp1.ReceiveAddress, token: resp1.Token, tradeID: resp1.TradeId, amount: resp1.ActualAmount},
{address: resp2.ReceiveAddress, token: resp2.Token, tradeID: resp2.TradeId, amount: resp2.ActualAmount},
} {
wg.Add(1)
go func(token, tradeID string, amount float64) {
go func(address, token, tradeID string, amount float64) {
defer wg.Done()
<-start
errs <- OrderProcessing(&request.OrderProcessingRequest{
ReceiveAddress: address,
Token: token,
TradeId: tradeID,
Amount: amount,
BlockTransactionId: "shared_block",
})
}(tc.token, tc.tradeID, tc.amount)
}(tc.address, tc.token, tc.tradeID, tc.amount)
}
close(start)
+4 -2
View File
@@ -9,18 +9,20 @@ import (
"github.com/assimon/luuu/model/response"
)
// GetCheckoutCounterByTradeId 获取收银台详情,通过订单
// GetCheckoutCounterByTradeId returns checkout info for a pending order.
func GetCheckoutCounterByTradeId(tradeId string) (*response.CheckoutCounterResponse, error) {
orderInfo, err := data.GetOrderInfoByTradeId(tradeId)
if err != nil {
return nil, err
}
if orderInfo.ID <= 0 || orderInfo.Status != mdb.StatusWaitPay {
return nil, errors.New("不存在待支付订单或已过期!")
return nil, errors.New("pending order does not exist or has expired")
}
resp := &response.CheckoutCounterResponse{
TradeId: orderInfo.TradeId,
ActualAmount: orderInfo.ActualAmount,
ReceiveAddress: orderInfo.ReceiveAddress,
Token: orderInfo.Token,
ExpirationTime: orderInfo.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
RedirectUrl: orderInfo.RedirectUrl,
+187 -92
View File
@@ -1,67 +1,32 @@
package service
import (
"encoding/hex"
"errors"
"fmt"
"net/http"
"strings"
"sync"
tron "github.com/assimon/luuu/crypto"
"github.com/assimon/luuu/config"
"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/json"
"github.com/assimon/luuu/util/log"
"github.com/assimon/luuu/util/math"
"github.com/dromara/carbon/v2"
"github.com/gookit/goutil/stdutil"
"github.com/shopspring/decimal"
"github.com/tidwall/gjson"
)
const UsdtTrc20ApiUri = "https://apilist.tronscanapi.com/api/transfer/trc20"
const TRC20_USDT_ID = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
type UsdtTrc20Resp struct {
PageSize int `json:"page_size"`
Code int `json:"code"`
Data []Data `json:"data"`
}
type TokenInfo struct {
TokenID string `json:"tokenId"`
TokenAbbr string `json:"tokenAbbr"`
TokenName string `json:"tokenName"`
TokenDecimal int `json:"tokenDecimal"`
TokenCanShow int `json:"tokenCanShow"`
TokenType string `json:"tokenType"`
TokenLogo string `json:"tokenLogo"`
TokenLevel string `json:"tokenLevel"`
IssuerAddr string `json:"issuerAddr"`
Vip bool `json:"vip"`
}
type Data struct {
Amount string `json:"amount"`
ApprovalAmount string `json:"approval_amount"`
BlockTimestamp int64 `json:"block_timestamp"`
Block int `json:"block"`
From string `json:"from"`
To string `json:"to"`
Hash string `json:"hash"`
Confirmed int `json:"confirmed"`
ContractType string `json:"contract_type"`
ContracTType int `json:"contractType"`
Revert int `json:"revert"`
ContractRet string `json:"contract_ret"`
EventType string `json:"event_type"`
IssueAddress string `json:"issue_address"`
Decimals int `json:"decimals"`
TokenName string `json:"token_name"`
ID string `json:"id"`
Direction int `json:"direction"`
}
// Trc20CallBack polls transfers for one wallet and matches them to active orders.
func Trc20CallBack(token string, wg *sync.WaitGroup) {
func Trc20CallBack(address string, wg *sync.WaitGroup) {
defer wg.Done()
defer func() {
if err := recover(); err != nil {
@@ -69,88 +34,218 @@ func Trc20CallBack(token string, wg *sync.WaitGroup) {
}
}()
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{
"sort": "-timestamp",
"limit": "50",
"start": "0",
"direction": "2",
"db_version": "1",
"trc20Id": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"address": token,
"start_timestamp": stdutil.ToString(startTime),
"end_timestamp": stdutil.ToString(endTime),
}).Get(UsdtTrc20ApiUri)
"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(err)
panic(fmt.Sprintf("TRX API returned status %d", resp.StatusCode()))
}
var trc20Resp UsdtTrc20Resp
if err = json.Cjson.Unmarshal(resp.Body(), &trc20Resp); err != nil {
panic(err)
}
if trc20Resp.PageSize <= 0 {
return
success := gjson.GetBytes(resp.Body(), "success").Bool()
if !success {
panic("TRX API response indicates failure")
}
for _, transfer := range trc20Resp.Data {
if transfer.To != token || transfer.ContractRet != "SUCCESS" {
for i, transfer := range gjson.GetBytes(resp.Body(), "data").Array() {
if transfer.Get("raw_data.contract.0.type").String() != "TransferContract" {
continue
}
if transfer.Get("ret.0.contractRet").String() != "SUCCESS" {
continue
}
decimalQuant, err := decimal.NewFromString(transfer.Amount)
toAddressHex := transfer.Get("raw_data.contract.0.parameter.value.to_address").String()
toBytes, err := hex.DecodeString(toAddressHex)
if err != nil {
panic(err)
log.Sugar.Errorf("[TRX][%s] decode address failed on tx #%d: %v", address, i, err)
continue
}
decimalDivisor := decimal.NewFromFloat(1000000)
amount := decimalQuant.Div(decimalDivisor).InexactFloat64()
tradeId, err := data.GetTradeIdByWalletAddressAndAmount(token, amount)
if err != nil {
panic(err)
}
if tradeId == "" {
if tron.EncodeCheck(toBytes) != address {
continue
}
order, err := data.GetOrderInfoByTradeId(tradeId)
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(address, "TRX", amount)
if err != nil {
panic(err)
}
if tradeID == "" {
continue
}
order, err := data.GetOrderInfoByTradeId(tradeID)
if err != nil {
panic(err)
}
blockTimestamp := transfer.Get("block_timestamp").Int()
createTime := order.CreatedAt.TimestampMilli()
if transfer.BlockTimestamp < createTime {
panic("orders cannot actually be matched")
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{
Token: token,
TradeId: tradeId,
ReceiveAddress: address,
Token: "TRX",
TradeId: tradeID,
Amount: amount,
BlockTransactionId: transfer.Hash,
BlockTransactionId: txID,
}
if err = OrderProcessing(req); err != nil {
err = OrderProcessing(req)
if err != nil {
if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
log.Sugar.Infof("[task] skip already resolved transfer, trade_id=%s, block_transaction_id=%s, err=%v", tradeId, transfer.Hash, err)
log.Sugar.Infof("[TRX][%s] skip resolved transfer trade_id=%s hash=%s err=%v", address, tradeID, txID, err)
continue
}
panic(err)
}
msgTpl := `
<b>馃摙馃摙鏈夋柊鐨勪氦鏄撴敮浠樻垚鍔燂紒</b>
<pre>浜ゆ槗鍙凤細%s</pre>
<pre>璁㈠崟鍙凤細%s</pre>
<pre>璇锋眰鏀粯閲戦锛?f cny</pre>
<pre>瀹為檯鏀粯閲戦锛?f usdt</pre>
<pre>閽卞寘鍦板潃锛?s</pre>
<pre>璁㈠崟鍒涘缓鏃堕棿锛?s</pre>
<pre>鏀粯鎴愬姛鏃堕棿锛?s</pre>
`
msg := fmt.Sprintf(msgTpl, order.TradeId, order.OrderId, order.Amount, order.ActualAmount, order.Token, order.CreatedAt.ToDateTimeString(), carbon.Now().ToDateTimeString())
telegram.SendToBot(msg)
sendPaymentNotification(order)
}
}
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")
}
for i, transfer := range gjson.GetBytes(resp.Body(), "data").Array() {
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(address, "USDT", amount)
if err != nil {
panic(err)
}
if tradeID == "" {
continue
}
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",
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)
}
}
func sendPaymentNotification(order *mdb.Orders) {
msg := fmt.Sprintf(
"Payment received\nTrade ID: %s\nOrder ID: %s\nOrder Amount: %.2f %s\nReceived Amount: %.2f %s\nAddress: %s\nCreated At: %s\nPaid At: %s",
order.TradeId,
order.OrderId,
order.Amount,
strings.ToUpper(order.Currency),
order.ActualAmount,
strings.ToUpper(order.Token),
order.ReceiveAddress,
order.CreatedAt.ToDateTimeString(),
carbon.Now().ToDateTimeString(),
)
telegram.SendToBot(msg)
}