feat: add configurable payment amount precision

- add system.amount_precision setting with 2-6 validation
This commit is contained in:
line-6000
2026-05-11 17:11:43 +08:00
parent 1c805ed061
commit 8d2ddbd045
178 changed files with 532 additions and 273 deletions
+77 -30
View File
@@ -23,9 +23,14 @@ type PendingCallbackOrder struct {
UpdatedAt carbon.Time `gorm:"column:updated_at"`
}
func normalizeAmount(amount float64, precision int) (int64, string) {
precision = NormalizeAmountPrecision(precision)
value := decimal.NewFromFloat(amount).Round(int32(precision))
return value.Shift(int32(precision)).IntPart(), value.StringFixed(int32(precision))
}
func normalizeLockAmount(amount float64) (int64, string) {
value := decimal.NewFromFloat(amount).Round(2)
return value.Shift(2).IntPart(), value.StringFixed(2)
return normalizeAmount(amount, GetAmountPrecision())
}
func normalizeLockNetwork(network string) string {
@@ -53,6 +58,30 @@ func applyLockAddressFilter(tx *gorm.DB, network, address string) *gorm.DB {
return tx.Where("address = ?", address)
}
func activeLocksForAddress(tx *gorm.DB, network, address, token string, now time.Time) *gorm.DB {
query := tx.Model(&mdb.TransactionLock{}).
Where("network = ?", normalizeLockNetwork(network)).
Where("token = ?", normalizeLockToken(token)).
Where("expires_at > ?", now)
return applyLockAddressFilter(query, network, address)
}
func lockMatchesAmount(lock mdb.TransactionLock, amount float64) bool {
precision := NormalizeAmountPrecision(lock.AmountPrecision)
scaledAmount, _ := normalizeAmount(amount, precision)
return lock.AmountScaled == scaledAmount
}
func lockAmountDecimal(lock mdb.TransactionLock) decimal.Decimal {
if strings.TrimSpace(lock.AmountText) != "" {
if value, err := decimal.NewFromString(lock.AmountText); err == nil {
return value
}
}
precision := NormalizeAmountPrecision(lock.AmountPrecision)
return decimal.NewFromInt(lock.AmountScaled).Shift(int32(-precision))
}
// GetOrderInfoByOrderId fetches an order by merchant order id.
func GetOrderInfoByOrderId(orderId string) (*mdb.Orders, error) {
order := new(mdb.Orders)
@@ -278,45 +307,43 @@ func ExpireOrderByTradeId(tradeId string) error {
func GetTradeIdByWalletAddressAndAmountAndToken(network string, address string, token string, amount float64) (string, error) {
network = normalizeLockNetwork(network)
address = normalizeLockAddress(network, address)
scaledAmount, _ := normalizeLockAmount(amount)
var lock mdb.TransactionLock
query := dao.RuntimeDB.Model(&mdb.TransactionLock{}).
Where("network = ?", network).
Where("token = ?", normalizeLockToken(token)).
Where("amount_scaled = ?", scaledAmount).
Where("expires_at > ?", time.Now())
query = applyLockAddressFilter(query, network, address)
err := query.Limit(1).Find(&lock).Error
var locks []mdb.TransactionLock
err := activeLocksForAddress(dao.RuntimeDB, network, address, token, time.Now()).
Order("created_at ASC").
Find(&locks).Error
if err != nil {
return "", err
}
if lock.ID <= 0 {
return "", nil
for _, lock := range locks {
if lockMatchesAmount(lock, amount) {
return lock.TradeId, nil
}
}
return lock.TradeId, nil
return "", nil
}
// 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 {
network = normalizeLockNetwork(network)
address = normalizeLockAddress(network, address)
scaledAmount, amountText := normalizeLockAmount(amount)
precision := GetAmountPrecision()
scaledAmount, amountText := normalizeAmount(amount, precision)
normalizedToken := normalizeLockToken(token)
now := time.Now()
lock := &mdb.TransactionLock{
Network: network,
Address: address,
Token: normalizedToken,
AmountScaled: scaledAmount,
AmountText: amountText,
TradeId: tradeID,
ExpiresAt: now.Add(expirationTime),
Network: network,
Address: address,
Token: normalizedToken,
AmountScaled: scaledAmount,
AmountText: amountText,
AmountPrecision: precision,
TradeId: tradeID,
ExpiresAt: now.Add(expirationTime),
}
return dao.RuntimeDB.Transaction(func(tx *gorm.DB) error {
expiredQuery := tx.Where("network = ?", network).
Where("token = ?", normalizedToken).
Where("amount_scaled = ?", scaledAmount).
Where("expires_at <= ?", now)
expiredQuery = applyLockAddressFilter(expiredQuery, network, address)
if err := expiredQuery.Delete(&mdb.TransactionLock{}).Error; err != nil {
@@ -326,6 +353,18 @@ func LockTransaction(network, address, token, tradeID string, amount float64, ex
return err
}
var existing []mdb.TransactionLock
if err := activeLocksForAddress(tx, network, address, normalizedToken, now).
Find(&existing).Error; err != nil {
return err
}
candidateAmount := lockAmountDecimal(*lock)
for _, item := range existing {
if lockAmountDecimal(item).Equal(candidateAmount) {
return ErrTransactionLocked
}
}
result := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(lock)
if result.Error != nil {
return result.Error
@@ -341,13 +380,21 @@ func LockTransaction(network, address, token, tradeID string, amount float64, ex
func UnLockTransaction(network string, address string, token string, amount float64) error {
network = normalizeLockNetwork(network)
address = normalizeLockAddress(network, address)
scaledAmount, _ := normalizeLockAmount(amount)
query := dao.RuntimeDB.
Where("network = ?", network).
Where("token = ?", normalizeLockToken(token)).
Where("amount_scaled = ?", scaledAmount)
query = applyLockAddressFilter(query, network, address)
return query.Delete(&mdb.TransactionLock{}).Error
var locks []mdb.TransactionLock
if err := activeLocksForAddress(dao.RuntimeDB, network, address, token, time.Now()).
Find(&locks).Error; err != nil {
return err
}
ids := make([]uint64, 0, len(locks))
for _, lock := range locks {
if lockMatchesAmount(lock, amount) {
ids = append(ids, lock.ID)
}
}
if len(ids) == 0 {
return nil
}
return dao.RuntimeDB.Where("id IN ?", ids).Delete(&mdb.TransactionLock{}).Error
}
func UnLockTransactionByTradeId(tradeID string) error {