feat: get support chain api

This commit is contained in:
Ginta
2026-04-14 19:03:39 +08:00
19 changed files with 1701 additions and 879 deletions
+203
View File
@@ -95,6 +95,8 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
Status: mdb.StatusWaitPay,
NotifyUrl: req.NotifyUrl,
RedirectUrl: req.RedirectUrl,
Name: req.Name,
PaymentType: req.PaymentType,
}
if err = data.CreateOrderWithTransaction(tx, order); err != nil {
tx.Rollback()
@@ -155,6 +157,69 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
if err = data.UnLockTransaction(req.Network, 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)
}
// Load order to check parent-child relationship
order, err := data.GetOrderInfoByTradeId(req.TradeId)
if err != nil {
return nil
}
// Parent order paid directly: expire all sub-orders and release their locks
if order.ParentTradeId == "" {
subs, subErr := data.GetActiveSubOrders(order.TradeId)
if subErr != nil {
log.Sugar.Errorf("[order] get sub-orders for parent failed, trade_id=%s, err=%v", order.TradeId, subErr)
return nil
}
for _, sub := range subs {
if err = data.ExpireOrderByTradeId(sub.TradeId); err != nil {
log.Sugar.Warnf("[order] expire sub-order failed, trade_id=%s, err=%v", sub.TradeId, err)
}
if err = data.UnLockTransaction(sub.Network, sub.ReceiveAddress, sub.Token, sub.ActualAmount); err != nil {
log.Sugar.Warnf("[order] unlock sub-order transaction failed, trade_id=%s, err=%v", sub.TradeId, err)
}
}
return nil
}
// Sub-order should not trigger its own callback (notify_url is empty).
// OrderSuccessWithTransaction unconditionally sets callback_confirm=No,
// reset it here to prevent the callback worker from retrying an empty URL.
if err = data.ResetCallbackConfirmOk(order.TradeId); err != nil {
log.Sugar.Warnf("[order] reset sub-order callback_confirm failed, trade_id=%s, err=%v", order.TradeId, err)
}
parent, err := data.GetOrderInfoByTradeId(order.ParentTradeId)
if err != nil {
log.Sugar.Errorf("[order] load parent order failed, parent_trade_id=%s, err=%v", order.ParentTradeId, err)
return nil
}
// Mark parent as paid with sub-order's payment details
if _, err = data.MarkParentOrderSuccess(parent.TradeId, order); err != nil {
log.Sugar.Errorf("[order] mark parent success failed, parent_trade_id=%s, err=%v", parent.TradeId, err)
}
// Release parent's own wallet lock
if err = data.UnLockTransaction(parent.Network, parent.ReceiveAddress, parent.Token, parent.ActualAmount); err != nil {
log.Sugar.Warnf("[order] unlock parent transaction failed, parent_trade_id=%s, err=%v", parent.TradeId, err)
}
// Expire sibling sub-orders and release their locks
siblings, err := data.GetSiblingSubOrders(parent.TradeId, order.TradeId)
if err != nil {
log.Sugar.Errorf("[order] get sibling sub-orders failed, parent_trade_id=%s, err=%v", parent.TradeId, err)
return nil
}
for _, sib := range siblings {
if err = data.ExpireOrderByTradeId(sib.TradeId); err != nil {
log.Sugar.Warnf("[order] expire sibling failed, trade_id=%s, err=%v", sib.TradeId, err)
}
if err = data.UnLockTransaction(sib.Network, sib.ReceiveAddress, sib.Token, sib.ActualAmount); err != nil {
log.Sugar.Warnf("[order] unlock sibling transaction failed, trade_id=%s, err=%v", sib.TradeId, err)
}
}
return nil
}
@@ -212,3 +277,141 @@ func GetOrderInfoByTradeId(tradeId string) (*mdb.Orders, error) {
}
return order, nil
}
const MaxSubOrders = 2
// SwitchNetwork creates or returns an existing sub-order for a different token+network.
func SwitchNetwork(req *request.SwitchNetworkRequest) (*response.CheckoutCounterResponse, error) {
gCreateTransactionLock.Lock()
defer gCreateTransactionLock.Unlock()
token := strings.ToUpper(strings.TrimSpace(req.Token))
network := strings.ToLower(strings.TrimSpace(req.Network))
// 1. Load parent order
parent, err := data.GetOrderInfoByTradeId(req.TradeId)
if err != nil {
return nil, err
}
if parent.ID <= 0 {
return nil, constant.OrderNotExists
}
if parent.ParentTradeId != "" {
return nil, constant.CannotSwitchSubOrder
}
if parent.Status != mdb.StatusWaitPay {
return nil, constant.OrderNotWaitPay
}
// 2. Same token+network as parent → mark selected and return
if strings.EqualFold(parent.Token, token) && strings.EqualFold(parent.Network, network) {
_ = data.MarkOrderSelected(parent.TradeId)
parent.IsSelected = true
return buildCheckoutResponse(parent), nil
}
// 3. Existing active sub-order for this token+network → return it
existing, err := data.GetSubOrderByTokenNetwork(parent.TradeId, token, network)
if err != nil {
return nil, err
}
if existing.ID > 0 {
_ = data.MarkOrderSelected(parent.TradeId)
_ = data.MarkOrderSelected(existing.TradeId)
_ = data.RefreshOrderExpiration(parent.TradeId)
existing.IsSelected = true
return buildCheckoutResponse(existing), nil
}
// 4. Check sub-order limit
count, err := data.CountActiveSubOrders(parent.TradeId)
if err != nil {
return nil, err
}
if count >= MaxSubOrders {
return nil, constant.SubOrderLimitExceeded
}
// 5. Calculate amount for the new network
rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(parent.Currency))
if rate <= 0 {
return nil, constant.RateAmountErr
}
decimalPayAmount := decimal.NewFromFloat(parent.Amount)
decimalTokenAmount := decimalPayAmount.Mul(decimal.NewFromFloat(rate))
if decimalTokenAmount.Cmp(decimal.NewFromFloat(UsdtMinimumPaymentAmount)) == -1 {
return nil, constant.PayAmountErr
}
// 6. Find and lock wallet
walletAddress, err := data.GetAvailableWalletAddressByNetwork(network)
if err != nil {
return nil, err
}
if len(walletAddress) <= 0 {
return nil, constant.NotAvailableWalletAddress
}
subTradeID := GenerateCode()
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
availableAddress, availableAmount, err := ReserveAvailableWalletAndAmount(subTradeID, network, token, amount, walletAddress)
if err != nil {
return nil, err
}
if availableAddress == "" {
return nil, constant.NotAvailableAmountErr
}
// 7. Create sub-order
tx := dao.Mdb.Begin()
subOrder := &mdb.Orders{
TradeId: subTradeID,
OrderId: subTradeID, // sub-order uses its own trade_id as order_id (unique constraint)
ParentTradeId: parent.TradeId,
Amount: parent.Amount,
Currency: parent.Currency,
ActualAmount: availableAmount,
ReceiveAddress: availableAddress,
Token: token,
Network: network,
Status: mdb.StatusWaitPay,
IsSelected: true,
NotifyUrl: "",
RedirectUrl: parent.RedirectUrl,
Name: parent.Name,
CallBackConfirm: mdb.CallBackConfirmOk, // don't trigger callback on sub-order
PaymentType: parent.PaymentType,
}
if err = data.CreateOrderWithTransaction(tx, subOrder); err != nil {
tx.Rollback()
_ = data.UnLockTransactionByTradeId(subTradeID)
return nil, err
}
if err = tx.Commit().Error; err != nil {
tx.Rollback()
_ = data.UnLockTransactionByTradeId(subTradeID)
return nil, err
}
// Mark parent as selected and refresh its expiration to match the sub-order
_ = data.MarkOrderSelected(parent.TradeId)
_ = data.RefreshOrderExpiration(parent.TradeId)
return buildCheckoutResponse(subOrder), nil
}
func buildCheckoutResponse(order *mdb.Orders) *response.CheckoutCounterResponse {
return &response.CheckoutCounterResponse{
TradeId: order.TradeId,
Amount: order.Amount,
ActualAmount: order.ActualAmount,
Token: order.Token,
Currency: order.Currency,
ReceiveAddress: order.ReceiveAddress,
Network: order.Network,
ExpirationTime: order.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
RedirectUrl: order.RedirectUrl,
CreatedAt: order.CreatedAt.TimestampMilli(),
IsSelected: order.IsSelected,
}
}
+1
View File
@@ -32,6 +32,7 @@ func GetCheckoutCounterByTradeId(tradeId string) (*response.CheckoutCounterRespo
ExpirationTime: orderInfo.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
RedirectUrl: orderInfo.RedirectUrl,
CreatedAt: orderInfo.CreatedAt.TimestampMilli(),
IsSelected: orderInfo.IsSelected,
}
return resp, nil
}