feat: add Solana payment scanning with USDT/USDC support and wallet management API

- Rewrite Solana scanner: stateless scan with processed-signature cache, query wallet + USDT ATA + USDC ATA addresses to cover all SPL transfers
  - Add network field to transaction_lock (4-tuple unique index) and order flow
  - Add wallet management endpoints (add/list/get/status/delete) with token auth
  - Add network info to Telegram payment notification
  - Add USDC hint to test script GMPay prompt
  - Add API docs and Solana scanning docs
This commit is contained in:
line-6000
2026-04-11 00:10:54 +08:00
parent a7963b335b
commit 3f071e6c01
21 changed files with 1579 additions and 233 deletions
+3 -2
View File
@@ -23,6 +23,7 @@ const sqliteBusyRetryAttempts = 3
type expirableOrder struct {
ID uint64 `gorm:"column:id"`
TradeId string `gorm:"column:trade_id"`
Network string `gorm:"column:network"`
ReceiveAddress string `gorm:"column:receive_address"`
Token string `gorm:"column:token"`
ActualAmount float64 `gorm:"column:actual_amount"`
@@ -65,7 +66,7 @@ func processExpiredOrders() {
var orders []expirableOrder
err := withSQLiteBusyRetry(func() error {
return dao.Mdb.Model(&mdb.Orders{}).
Select("id", "trade_id", "receive_address", "token", "actual_amount").
Select("id", "trade_id", "network", "receive_address", "token", "actual_amount").
Where("status = ?", mdb.StatusWaitPay).
Where("created_at <= ?", expirationCutoff).
Order("id asc").
@@ -89,7 +90,7 @@ func processExpiredOrders() {
if !expired {
continue
}
if err = data.UnLockTransaction(order.ReceiveAddress, order.Token, order.ActualAmount); err != nil {
if err = data.UnLockTransaction(order.Network, order.ReceiveAddress, order.Token, order.ActualAmount); err != nil {
log.Sugar.Warnf("[mq] release expired transaction lock failed, trade_id=%s, err=%v", order.TradeId, err)
}
}
+6 -4
View File
@@ -27,6 +27,7 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
ActualAmount: 1,
ReceiveAddress: "wallet_1",
Token: "USDT",
Network: "tron",
Status: mdb.StatusWaitPay,
NotifyUrl: "https://merchant.example/callback",
}
@@ -36,7 +37,7 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
if err := dao.Mdb.Model(order).UpdateColumn("created_at", time.Now().Add(-20*time.Minute)).Error; err != nil {
t.Fatalf("age expired order: %v", err)
}
if err := data.LockTransaction(order.ReceiveAddress, order.Token, order.TradeId, order.ActualAmount, time.Hour); err != nil {
if err := data.LockTransaction("tron", order.ReceiveAddress, order.Token, order.TradeId, order.ActualAmount, time.Hour); err != nil {
t.Fatalf("lock expired order: %v", err)
}
@@ -48,13 +49,14 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
ActualAmount: 1.01,
ReceiveAddress: "wallet_1",
Token: "USDT",
Network: "tron",
Status: mdb.StatusWaitPay,
NotifyUrl: "https://merchant.example/callback",
}
if err := dao.Mdb.Create(recentOrder).Error; err != nil {
t.Fatalf("create recent order: %v", err)
}
if err := data.LockTransaction(recentOrder.ReceiveAddress, recentOrder.Token, recentOrder.TradeId, recentOrder.ActualAmount, time.Hour); err != nil {
if err := data.LockTransaction("tron", recentOrder.ReceiveAddress, recentOrder.Token, recentOrder.TradeId, recentOrder.ActualAmount, time.Hour); err != nil {
t.Fatalf("lock recent order: %v", err)
}
@@ -67,7 +69,7 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
if expired.Status != mdb.StatusExpired {
t.Fatalf("expired order status = %d, want %d", expired.Status, mdb.StatusExpired)
}
lockTradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(order.ReceiveAddress, order.Token, order.ActualAmount)
lockTradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken("tron", order.ReceiveAddress, order.Token, order.ActualAmount)
if err != nil {
t.Fatalf("expired order lock lookup: %v", err)
}
@@ -82,7 +84,7 @@ func TestProcessExpiredOrdersExpiresWaitingOrdersAndReleasesLocks(t *testing.T)
if recent.Status != mdb.StatusWaitPay {
t.Fatalf("recent order status = %d, want %d", recent.Status, mdb.StatusWaitPay)
}
lockTradeID, err = data.GetTradeIdByWalletAddressAndAmountAndToken(recentOrder.ReceiveAddress, recentOrder.Token, recentOrder.ActualAmount)
lockTradeID, err = data.GetTradeIdByWalletAddressAndAmountAndToken("tron", recentOrder.ReceiveAddress, recentOrder.Token, recentOrder.ActualAmount)
if err != nil {
t.Fatalf("recent order lock lookup: %v", err)
}