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
+11 -7
View File
@@ -108,11 +108,12 @@ func UpdateOrderIsExpirationById(id uint64, expirationCutoff time.Time) (bool, e
return result.RowsAffected > 0, result.Error
}
// GetTradeIdByWalletAddressAndAmountAndToken resolves the reserved trade id by address, token and amount.
func GetTradeIdByWalletAddressAndAmountAndToken(address string, token string, amount float64) (string, error) {
// GetTradeIdByWalletAddressAndAmountAndToken resolves the reserved trade id by network, address, token and amount.
func GetTradeIdByWalletAddressAndAmountAndToken(network string, address string, token string, amount float64) (string, error) {
scaledAmount, _ := normalizeLockAmount(amount)
var lock mdb.TransactionLock
err := dao.RuntimeDB.Model(&mdb.TransactionLock{}).
Where("network = ?", network).
Where("address = ?", address).
Where("token = ?", normalizeLockToken(token)).
Where("amount_scaled = ?", scaledAmount).
@@ -128,12 +129,13 @@ func GetTradeIdByWalletAddressAndAmountAndToken(address string, token string, am
return lock.TradeId, nil
}
// LockTransaction reserves an address+token+amount pair in sqlite until expiration.
func LockTransaction(address, token, tradeID string, amount float64, expirationTime time.Duration) error {
// LockTransaction reserves a network+address+token+amount pair in sqlite until expiration.
func LockTransaction(network, address, token, tradeID string, amount float64, expirationTime time.Duration) error {
scaledAmount, amountText := normalizeLockAmount(amount)
normalizedToken := normalizeLockToken(token)
now := time.Now()
lock := &mdb.TransactionLock{
Network: network,
Address: address,
Token: normalizedToken,
AmountScaled: scaledAmount,
@@ -143,7 +145,8 @@ func LockTransaction(address, token, tradeID string, amount float64, expirationT
}
return dao.RuntimeDB.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("address = ?", address).
if err := tx.Where("network = ?", network).
Where("address = ?", address).
Where("token = ?", normalizedToken).
Where("amount_scaled = ?", scaledAmount).
Where("expires_at <= ?", now).
@@ -165,10 +168,11 @@ func LockTransaction(address, token, tradeID string, amount float64, expirationT
})
}
// UnLockTransaction releases the reservation for address+token+amount.
func UnLockTransaction(address string, token string, amount float64) error {
// UnLockTransaction releases the reservation for network+address+token+amount.
func UnLockTransaction(network string, address string, token string, amount float64) error {
scaledAmount, _ := normalizeLockAmount(amount)
return dao.RuntimeDB.
Where("network = ?", network).
Where("address = ?", address).
Where("token = ?", normalizeLockToken(token)).
Where("amount_scaled = ?", scaledAmount).
+40 -3
View File
@@ -1,14 +1,23 @@
package data
import (
"strings"
"github.com/assimon/luuu/model/dao"
"github.com/assimon/luuu/model/mdb"
"github.com/assimon/luuu/util/constant"
)
// AddWalletAddress 创建钱包
// AddWalletAddress 创建钱包 (默认 tron 网络,用于 Telegram 添加)
func AddWalletAddress(address string) (*mdb.WalletAddress, error) {
exist, err := GetWalletAddressByToken(address)
return AddWalletAddressWithNetwork(mdb.NetworkTron, address)
}
// AddWalletAddressWithNetwork 创建指定网络的钱包地址
func AddWalletAddressWithNetwork(network, address string) (*mdb.WalletAddress, error) {
network = strings.ToLower(strings.TrimSpace(network))
address = strings.TrimSpace(address)
exist, err := GetWalletAddressByNetworkAndAddress(network, address)
if err != nil {
return nil, err
}
@@ -16,6 +25,7 @@ func AddWalletAddress(address string) (*mdb.WalletAddress, error) {
return nil, constant.WalletAddressAlreadyExists
}
walletAddress := &mdb.WalletAddress{
Network: network,
Address: address,
Status: mdb.TokenStatusEnable,
}
@@ -23,7 +33,17 @@ func AddWalletAddress(address string) (*mdb.WalletAddress, error) {
return walletAddress, err
}
// GetWalletAddressByToken 通过钱包地址获取address
// GetWalletAddressByNetworkAndAddress 通过网络和地址查询
func GetWalletAddressByNetworkAndAddress(network, address string) (*mdb.WalletAddress, error) {
walletAddress := new(mdb.WalletAddress)
err := dao.Mdb.Model(walletAddress).
Where("network = ?", network).
Where("address = ?", address).
Limit(1).Find(walletAddress).Error
return walletAddress, err
}
// GetWalletAddressByToken 通过钱包地址获取address (兼容旧接口)
func GetWalletAddressByToken(address string) (*mdb.WalletAddress, error) {
walletAddress := new(mdb.WalletAddress)
err := dao.Mdb.Model(walletAddress).Limit(1).Find(walletAddress, "address = ?", address).Error
@@ -50,6 +70,16 @@ func GetAvailableWalletAddress() ([]mdb.WalletAddress, error) {
return WalletAddressList, err
}
// GetAvailableWalletAddressByNetwork 获得指定网络的所有可用钱包地址
func GetAvailableWalletAddressByNetwork(network string) ([]mdb.WalletAddress, error) {
var list []mdb.WalletAddress
err := dao.Mdb.Model(list).
Where("status = ?", mdb.TokenStatusEnable).
Where("network = ?", network).
Find(&list).Error
return list, err
}
// GetAllWalletAddress 获得所有钱包地址
func GetAllWalletAddress() ([]mdb.WalletAddress, error) {
var WalletAddressList []mdb.WalletAddress
@@ -57,6 +87,13 @@ func GetAllWalletAddress() ([]mdb.WalletAddress, error) {
return WalletAddressList, err
}
// GetAllWalletAddressByNetwork 获得指定网络的所有钱包地址
func GetAllWalletAddressByNetwork(network string) ([]mdb.WalletAddress, error) {
var list []mdb.WalletAddress
err := dao.Mdb.Model(list).Where("network = ?", network).Find(&list).Error
return list, err
}
// ChangeWalletAddressStatus 启用禁用钱包
func ChangeWalletAddressStatus(id uint64, status int) error {
err := dao.Mdb.Model(&mdb.WalletAddress{}).Where("id = ?", id).Update("status", status).Error