Files
epusdt/src/model/service/task_service_test.go
T
line-6000 9b397bd45e feat: support TON and USDT Jetton payments
- add TON chain, token, RPC, address normalization, and order matching support
- add liteclient scanner with masterchain/shard catch-up and Jetton validation
- add TON manual payment verification and focused tests
2026-06-11 14:47:46 +08:00

226 lines
6.9 KiB
Go

package service
import (
"math/big"
"strings"
"testing"
"time"
"github.com/GMWalletApp/epusdt/internal/testutil"
"github.com/GMWalletApp/epusdt/model/dao"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/notify"
"github.com/ethereum/go-ethereum/common"
"gorm.io/gorm/clause"
)
func upsertTaskServiceTestChainToken(t *testing.T, token mdb.ChainToken) {
t.Helper()
if err := dao.Mdb.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "network"}, {Name: "symbol"}},
DoUpdates: clause.AssignmentColumns([]string{
"contract_address",
"decimals",
"enabled",
"min_amount",
}),
}).Create(&token).Error; err != nil {
t.Fatalf("seed chain token: %v", err)
}
}
func TestSendPaymentNotificationUsesLatestOrderUpdatedAt(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
const channelType = "test-pay-success-time"
got := make(chan string, 1)
notify.RegisterSender(channelType, func(config, text string) error {
got <- text
return nil
})
if err := dao.Mdb.Create(&mdb.NotificationChannel{
Type: channelType,
Name: "test",
Config: "{}",
Events: `{"pay_success":true}`,
Enabled: true,
}).Error; err != nil {
t.Fatalf("seed notification channel: %v", err)
}
order := &mdb.Orders{
TradeId: "T202604270001",
OrderId: "ORD202604270001",
Amount: 100,
Currency: "cny",
ActualAmount: 14.28,
Token: "USDT",
Network: mdb.NetworkTron,
ReceiveAddress: "TTestAddress",
Status: mdb.StatusWaitPay,
}
if err := dao.Mdb.Create(order).Error; err != nil {
t.Fatalf("seed order: %v", err)
}
const createdAt = "2026-04-27 09:00:00"
const staleUpdatedAt = "2026-04-27 09:01:00"
const paidAt = "2026-04-27 10:20:30"
if err := dao.Mdb.Exec("UPDATE orders SET created_at = ?, updated_at = ? WHERE trade_id = ?", createdAt, staleUpdatedAt, order.TradeId).Error; err != nil {
t.Fatalf("set initial timestamps: %v", err)
}
var staleOrderModel mdb.Orders
if err := dao.Mdb.Where("trade_id = ?", order.TradeId).Take(&staleOrderModel).Error; err != nil {
t.Fatalf("load stale order: %v", err)
}
if err := dao.Mdb.Exec("UPDATE orders SET status = ?, updated_at = ? WHERE trade_id = ?", mdb.StatusPaySuccess, paidAt, order.TradeId).Error; err != nil {
t.Fatalf("set paid timestamp: %v", err)
}
sendPaymentNotification(&staleOrderModel)
select {
case text := <-got:
if !strings.Contains(text, "支付时间:"+paidAt) {
t.Fatalf("notification payment time = %q, want %s", text, paidAt)
}
if strings.Contains(text, "支付时间:"+staleUpdatedAt) {
t.Fatalf("notification used stale payment time: %q", text)
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for notification")
}
}
func TestTryProcessEvmERC20TransferUsesChainTokenContract(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
const (
tradeID = "T202605110001"
orderID = "ORD202605110001"
tokenSym = "FOO"
amount = 12.34
)
contract := common.HexToAddress("0x1111111111111111111111111111111111111111")
receiveAddress := common.HexToAddress("0x2222222222222222222222222222222222222222")
if err := dao.Mdb.Create(&mdb.ChainToken{
Network: mdb.NetworkEthereum,
Symbol: tokenSym,
ContractAddress: contract.Hex(),
Decimals: 8,
Enabled: true,
}).Error; err != nil {
t.Fatalf("seed chain token: %v", err)
}
order := &mdb.Orders{
TradeId: tradeID,
OrderId: orderID,
Amount: 100,
Currency: "CNY",
ActualAmount: amount,
Token: tokenSym,
Network: mdb.NetworkEthereum,
ReceiveAddress: strings.ToLower(receiveAddress.Hex()),
Status: mdb.StatusWaitPay,
}
if err := dao.Mdb.Create(order).Error; err != nil {
t.Fatalf("seed order: %v", err)
}
if err := data.LockTransaction(mdb.NetworkEthereum, order.ReceiveAddress, order.Token, order.TradeId, order.ActualAmount, time.Hour); err != nil {
t.Fatalf("lock transaction: %v", err)
}
rawValue := big.NewInt(1_234_000_000) // 12.34 with 8 decimals
TryProcessEvmERC20Transfer(mdb.NetworkEthereum, contract, receiveAddress, rawValue, "0xfoo-hash", time.Now().UnixMilli())
paid, err := data.GetOrderInfoByTradeId(tradeID)
if err != nil {
t.Fatalf("load paid order: %v", err)
}
if paid.Status != mdb.StatusPaySuccess {
t.Fatalf("order status = %d, want %d", paid.Status, mdb.StatusPaySuccess)
}
if paid.BlockTransactionId != "0xfoo-hash" {
t.Fatalf("block transaction id = %q, want %q", paid.BlockTransactionId, "0xfoo-hash")
}
lockTradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkEthereum, receiveAddress.Hex(), tokenSym, amount)
if err != nil {
t.Fatalf("lookup released lock: %v", err)
}
if lockTradeID != "" {
t.Fatalf("runtime lock still exists for trade_id=%q", lockTradeID)
}
}
func TestTryProcessEvmERC20TransferSkipsTransfersBeforeOrderCreation(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
const (
tradeID = "T202605110002"
orderID = "ORD202605110002"
tokenSym = "USDT"
amount = 25.5
)
contract := common.HexToAddress("0x3333333333333333333333333333333333333333")
receiveAddress := common.HexToAddress("0x4444444444444444444444444444444444444444")
upsertTaskServiceTestChainToken(t, mdb.ChainToken{
Network: mdb.NetworkEthereum,
Symbol: tokenSym,
ContractAddress: contract.Hex(),
Decimals: 6,
Enabled: true,
})
order := &mdb.Orders{
TradeId: tradeID,
OrderId: orderID,
Amount: 100,
Currency: "CNY",
ActualAmount: amount,
Token: tokenSym,
Network: mdb.NetworkEthereum,
ReceiveAddress: strings.ToLower(receiveAddress.Hex()),
Status: mdb.StatusWaitPay,
}
if err := dao.Mdb.Create(order).Error; err != nil {
t.Fatalf("seed order: %v", err)
}
if err := data.LockTransaction(mdb.NetworkEthereum, order.ReceiveAddress, order.Token, order.TradeId, order.ActualAmount, time.Hour); err != nil {
t.Fatalf("lock transaction: %v", err)
}
rawValue := big.NewInt(25_500_000) // 25.5 with 6 decimals
oldBlockTsMs := order.CreatedAt.TimestampMilli() - 1
TryProcessEvmERC20Transfer(mdb.NetworkEthereum, contract, receiveAddress, rawValue, "0xold-hash", oldBlockTsMs)
got, err := data.GetOrderInfoByTradeId(tradeID)
if err != nil {
t.Fatalf("load order: %v", err)
}
if got.Status != mdb.StatusWaitPay {
t.Fatalf("order status = %d, want %d", got.Status, mdb.StatusWaitPay)
}
if got.BlockTransactionId != "" {
t.Fatalf("block transaction id = %q, want empty", got.BlockTransactionId)
}
lockTradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkEthereum, receiveAddress.Hex(), tokenSym, amount)
if err != nil {
t.Fatalf("lookup retained lock: %v", err)
}
if lockTradeID != tradeID {
t.Fatalf("runtime lock trade_id = %q, want %q", lockTradeID, tradeID)
}
}