feat: isolate manual verification RPC usage

- add rpc node purpose support for general/manual_verify/both
- keep scan, WSS and health checks away from manual_verify nodes
- fallback manual hash verification from general RPC to manual_verify RPC
- add public cashier hash submission path and errno responses
- update tests for RPC selection, failover and manual payment flows
This commit is contained in:
line-6000
2026-05-27 19:36:56 +08:00
parent 13c81ee560
commit c69b0d6c0c
185 changed files with 2525 additions and 483 deletions
+12 -12
View File
@@ -3,12 +3,12 @@ package admin
import (
"crypto/rand"
"encoding/hex"
"errors"
"strconv"
"strings"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4"
)
@@ -79,7 +79,7 @@ func (c *BaseAdminController) ListApiKeys(ctx echo.Context) error {
func (c *BaseAdminController) CreateApiKey(ctx echo.Context) error {
req := new(CreateApiKeyRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err)
@@ -150,11 +150,11 @@ func isUniqueViolation(err error) bool {
func (c *BaseAdminController) UpdateApiKey(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
req := new(UpdateApiKeyRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
fields := map[string]interface{}{}
if req.Name != nil {
@@ -187,11 +187,11 @@ func (c *BaseAdminController) UpdateApiKey(ctx echo.Context) error {
func (c *BaseAdminController) ChangeApiKeyStatus(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
req := new(ChangeApiKeyStatusRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err)
@@ -215,7 +215,7 @@ func (c *BaseAdminController) ChangeApiKeyStatus(ctx echo.Context) error {
func (c *BaseAdminController) RotateApiKeySecret(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
secret := randomHex(32)
if err := data.UpdateApiKeyFields(id, map[string]interface{}{"secret_key": secret}); err != nil {
@@ -239,7 +239,7 @@ func (c *BaseAdminController) RotateApiKeySecret(ctx echo.Context) error {
func (c *BaseAdminController) DeleteApiKey(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := data.DeleteApiKeyByID(id); err != nil {
return c.FailJson(ctx, err)
@@ -262,14 +262,14 @@ func (c *BaseAdminController) DeleteApiKey(ctx echo.Context) error {
func (c *BaseAdminController) GetApiKeyStats(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
row, err := data.GetApiKeyByID(id)
if err != nil {
return c.FailJson(ctx, err)
}
if row.ID == 0 {
return c.FailJson(ctx, errors.New("api key not found"))
return c.FailJson(ctx, constant.ApiKeyNotFoundErr)
}
return c.SucJson(ctx, map[string]interface{}{
"call_count": row.CallCount,
@@ -291,14 +291,14 @@ func (c *BaseAdminController) GetApiKeyStats(ctx echo.Context) error {
func (c *BaseAdminController) GetApiKeySecret(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
row, err := data.GetApiKeyByID(id)
if err != nil {
return c.FailJson(ctx, err)
}
if row.ID == 0 {
return c.FailJson(ctx, errors.New("api key not found"))
return c.FailJson(ctx, constant.ApiKeyNotFoundErr)
}
return c.SucJson(ctx, map[string]string{"secret_key": row.SecretKey})
}
+12 -8
View File
@@ -5,6 +5,7 @@ import (
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/util/constant"
appjwt "github.com/GMWalletApp/epusdt/util/jwt"
"github.com/labstack/echo/v4"
)
@@ -53,7 +54,7 @@ type InitialPasswordResponse struct {
func (c *BaseAdminController) Login(ctx echo.Context) error {
req := new(LoginRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err)
@@ -63,10 +64,10 @@ func (c *BaseAdminController) Login(ctx echo.Context) error {
return c.FailJson(ctx, err)
}
if user.ID == 0 || !data.VerifyPassword(user.PasswordHash, req.Password) {
return c.FailJson(ctx, errors.New("invalid username or password"))
return c.FailJson(ctx, constant.AdminInvalidCredentialErr)
}
if user.Status != mdb.AdminUserStatusEnable {
return c.FailJson(ctx, errors.New("user disabled"))
return c.FailJson(ctx, constant.AdminUserDisabledErr)
}
token, err := appjwt.Sign(user.ID, user.Username)
if err != nil {
@@ -94,6 +95,9 @@ func (c *BaseAdminController) Login(ctx echo.Context) error {
func (c *BaseAdminController) GetInitialPassword(ctx echo.Context) error {
password, err := data.ConsumeInitialAdminPassword()
if err != nil {
if errors.Is(err, data.ErrInitAdminPasswordAlreadyFetched) || errors.Is(err, data.ErrInitAdminPasswordUnavailable) {
return c.FailJson(ctx, constant.InitialAdminPasswordErr)
}
return c.FailJson(ctx, err)
}
return c.SucJson(ctx, InitialPasswordResponse{
@@ -144,14 +148,14 @@ func (c *BaseAdminController) Logout(ctx echo.Context) error {
func (c *BaseAdminController) Me(ctx echo.Context) error {
uid := currentAdminUserID(ctx)
if uid == 0 {
return c.FailJson(ctx, errors.New("unauthorized"))
return c.FailJson(ctx, constant.AdminUnauthorizedErr)
}
user, err := data.GetAdminUserByID(uid)
if err != nil {
return c.FailJson(ctx, err)
}
if user.ID == 0 {
return c.FailJson(ctx, errors.New("user not found"))
return c.FailJson(ctx, constant.AdminUserNotFoundErr)
}
// Warn the frontend when the operator hasn't changed the seeded
// initial password.
@@ -176,11 +180,11 @@ func (c *BaseAdminController) Me(ctx echo.Context) error {
func (c *BaseAdminController) ChangePassword(ctx echo.Context) error {
uid := currentAdminUserID(ctx)
if uid == 0 {
return c.FailJson(ctx, errors.New("unauthorized"))
return c.FailJson(ctx, constant.AdminUnauthorizedErr)
}
req := new(ChangePasswordRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err)
@@ -190,7 +194,7 @@ func (c *BaseAdminController) ChangePassword(ctx echo.Context) error {
return c.FailJson(ctx, err)
}
if user.ID == 0 || !data.VerifyPassword(user.PasswordHash, req.OldPassword) {
return c.FailJson(ctx, errors.New("old password incorrect"))
return c.FailJson(ctx, constant.AdminOldPasswordErr)
}
if err := data.UpdateAdminUserPassword(uid, req.NewPassword); err != nil {
return c.FailJson(ctx, err)
+2 -1
View File
@@ -2,6 +2,7 @@ package admin
import (
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4"
)
@@ -49,7 +50,7 @@ func (c *BaseAdminController) UpdateChain(ctx echo.Context) error {
network := ctx.Param("network")
req := new(UpdateChainRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
fields := map[string]interface{}{}
if req.Enabled != nil {
@@ -6,6 +6,7 @@ import (
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4"
)
@@ -65,7 +66,7 @@ func (c *BaseAdminController) ListChainTokens(ctx echo.Context) error {
func (c *BaseAdminController) CreateChainToken(ctx echo.Context) error {
req := new(CreateChainTokenRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err)
@@ -107,11 +108,11 @@ func (c *BaseAdminController) CreateChainToken(ctx echo.Context) error {
func (c *BaseAdminController) UpdateChainToken(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
req := new(UpdateChainTokenRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
fields := map[string]interface{}{}
if req.ContractAddress != nil {
@@ -147,11 +148,11 @@ func (c *BaseAdminController) UpdateChainToken(ctx echo.Context) error {
func (c *BaseAdminController) ChangeChainTokenStatus(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
req := new(ChangeChainTokenStatusRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := data.UpdateChainTokenFields(id, map[string]interface{}{"enabled": req.Enabled}); err != nil {
return c.FailJson(ctx, err)
@@ -172,7 +173,7 @@ func (c *BaseAdminController) ChangeChainTokenStatus(ctx echo.Context) error {
func (c *BaseAdminController) DeleteChainToken(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := data.DeleteChainTokenByID(id); err != nil {
return c.FailJson(ctx, err)
+8 -6
View File
@@ -49,16 +49,18 @@ type RotateSecretResponse struct {
// BatchImportResult is one row in the batch import response.
type BatchImportResult struct {
Address string `json:"address" example:"TTestTronAddress001"`
OK bool `json:"ok" example:"true"`
Error string `json:"error,omitempty" example:""`
Address string `json:"address" example:"TTestTronAddress001"`
OK bool `json:"ok" example:"true"`
ErrorCode int `json:"error_code,omitempty" example:"10001"`
Error string `json:"error,omitempty" example:"wallet address already exists"`
}
// SettingsUpsertResult is one row in the settings upsert response.
type SettingsUpsertResult struct {
Key string `json:"key" example:"rate.api_url"`
OK bool `json:"ok" example:"true"`
Error string `json:"error,omitempty" example:""`
Key string `json:"key" example:"rate.api_url"`
OK bool `json:"ok" example:"true"`
ErrorCode int `json:"error_code,omitempty" example:"10043"`
Error string `json:"error,omitempty" example:"invalid setting item"`
}
// Overview returns the top-card numbers for the dashboard landing page.
+13 -13
View File
@@ -2,7 +2,6 @@ package admin
import (
"encoding/json"
"errors"
"strconv"
"strings"
@@ -10,6 +9,7 @@ import (
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/notify"
"github.com/GMWalletApp/epusdt/telegram"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4"
)
@@ -69,7 +69,7 @@ func (c *BaseAdminController) ListNotificationChannels(ctx echo.Context) error {
func (c *BaseAdminController) CreateNotificationChannel(ctx echo.Context) error {
req := new(CreateNotificationChannelRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
req.Type = strings.ToLower(strings.TrimSpace(req.Type))
if err := c.ValidateStruct(ctx, req); err != nil {
@@ -120,7 +120,7 @@ func (c *BaseAdminController) CreateNotificationChannel(ctx echo.Context) error
func (c *BaseAdminController) UpdateNotificationChannel(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
existing, err := data.GetNotificationChannelByID(id)
if err != nil {
@@ -130,7 +130,7 @@ func (c *BaseAdminController) UpdateNotificationChannel(ctx echo.Context) error
req := new(UpdateNotificationChannelRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
fields := map[string]interface{}{}
if req.Name != nil {
@@ -175,7 +175,7 @@ func (c *BaseAdminController) UpdateNotificationChannel(ctx echo.Context) error
func (c *BaseAdminController) ChangeNotificationChannelStatus(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
existing, err := data.GetNotificationChannelByID(id)
if err != nil {
@@ -185,7 +185,7 @@ func (c *BaseAdminController) ChangeNotificationChannelStatus(ctx echo.Context)
req := new(ChangeChannelStatusRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := data.UpdateNotificationChannelFields(id, map[string]interface{}{"enabled": req.Enabled}); err != nil {
return c.FailJson(ctx, err)
@@ -209,7 +209,7 @@ func (c *BaseAdminController) ChangeNotificationChannelStatus(ctx echo.Context)
func (c *BaseAdminController) DeleteNotificationChannel(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
existing, err := data.GetNotificationChannelByID(id)
if err != nil {
@@ -230,12 +230,12 @@ func (c *BaseAdminController) DeleteNotificationChannel(ctx echo.Context) error
// + chat_id; webhook/email kept loose until those senders exist.
func validateChannelConfig(channelType string, cfg map[string]interface{}) error {
if cfg == nil {
return errors.New("config required")
return constant.NotificationConfigErr
}
if channelType == mdb.NotificationTypeTelegram {
configJSON, _ := json.Marshal(cfg)
if _, err := notify.ParseTelegramConfig(string(configJSON)); err != nil {
return err
return constant.NotificationConfigErr
}
}
return nil
@@ -260,10 +260,10 @@ func normalizeChannelEvents(raw interface{}) (map[string]bool, error) {
case "1", "true", "on", "yes":
return true, nil
default:
return false, errors.New("invalid boolean string")
return false, constant.NotificationEventsErr
}
default:
return false, errors.New("invalid event value type")
return false, constant.NotificationEventsErr
}
}
@@ -299,7 +299,7 @@ func normalizeChannelEvents(raw interface{}) (map[string]bool, error) {
for _, v := range t {
s, ok := v.(string)
if !ok {
return nil, errors.New("events array must contain strings")
return nil, constant.NotificationEventsErr
}
s = trimKey(s)
if s == "" {
@@ -309,6 +309,6 @@ func normalizeChannelEvents(raw interface{}) (map[string]bool, error) {
}
return out, nil
default:
return nil, errors.New("events must be object or string array")
return nil, constant.NotificationEventsErr
}
}
+11 -11
View File
@@ -2,7 +2,6 @@ package admin
import (
"encoding/csv"
"errors"
"fmt"
"net/http"
"strconv"
@@ -13,6 +12,7 @@ import (
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/model/request"
"github.com/GMWalletApp/epusdt/model/service"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/log"
"github.com/labstack/echo/v4"
)
@@ -88,7 +88,7 @@ func (c *BaseAdminController) GetOrder(ctx echo.Context) error {
return c.FailJson(ctx, err)
}
if order.ID == 0 {
return c.FailJson(ctx, errors.New("order not found"))
return c.FailJson(ctx, constant.OrderNotExists)
}
return c.SucJson(ctx, order)
}
@@ -110,17 +110,17 @@ func (c *BaseAdminController) CloseOrder(ctx echo.Context) error {
return c.FailJson(ctx, err)
}
if order.ID == 0 {
return c.FailJson(ctx, errors.New("order not found"))
return c.FailJson(ctx, constant.OrderNotExists)
}
if order.Status != mdb.StatusWaitPay {
return c.FailJson(ctx, errors.New("order is not waiting payment"))
return c.FailJson(ctx, constant.OrderNotWaitPay)
}
ok, err := data.CloseOrderManually(tradeID)
if err != nil {
return c.FailJson(ctx, err)
}
if !ok {
return c.FailJson(ctx, errors.New("close failed (concurrent state change)"))
return c.FailJson(ctx, constant.OrderStatusConflict)
}
// Release the transaction lock so the amount slot becomes reusable.
_ = data.UnLockTransaction(order.Network, order.ReceiveAddress, order.Token, order.ActualAmount)
@@ -147,7 +147,7 @@ func (c *BaseAdminController) MarkOrderPaid(ctx echo.Context) error {
req := new(request.ManualPaymentRequest)
if err := ctx.Bind(req); err != nil {
log.Sugar.Warnf("[admin-order] mark-paid bind failed admin_user_id=%d trade_id=%s err=%v", adminUserID, tradeID, err)
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
req.BlockTransactionId = strings.TrimSpace(req.BlockTransactionId)
if err := c.ValidateStruct(ctx, req); err != nil {
@@ -184,17 +184,17 @@ func (c *BaseAdminController) ResendCallback(ctx echo.Context) error {
return c.FailJson(ctx, err)
}
if order.ID == 0 {
err = errors.New("order not found")
err = constant.OrderNotExists
log.Sugar.Warnf("[admin-order] resend-callback rejected admin_user_id=%d trade_id=%s err=%v", adminUserID, tradeID, err)
return c.FailJson(ctx, err)
}
if order.Status != mdb.StatusPaySuccess {
err = errors.New("order is not paid (callback not applicable)")
err = constant.OrderCallbackNotApplicable
log.Sugar.Warnf("[admin-order] resend-callback rejected admin_user_id=%d trade_id=%s status=%d err=%v", adminUserID, tradeID, order.Status, err)
return c.FailJson(ctx, err)
}
if strings.TrimSpace(order.NotifyUrl) == "" {
err = errors.New("order has empty notify_url")
err = constant.OrderNotifyURLEmptyErr
log.Sugar.Warnf("[admin-order] resend-callback rejected admin_user_id=%d trade_id=%s err=%v", adminUserID, tradeID, err)
return c.FailJson(ctx, err)
}
@@ -204,7 +204,7 @@ func (c *BaseAdminController) ResendCallback(ctx echo.Context) error {
return c.FailJson(ctx, err)
}
if !ok {
err = errors.New("resend callback failed (concurrent state change)")
err = constant.OrderResendCallbackErr
log.Sugar.Warnf("[admin-order] resend-callback rejected admin_user_id=%d trade_id=%s err=%v", adminUserID, tradeID, err)
return c.FailJson(ctx, err)
}
@@ -389,7 +389,7 @@ func (c *BaseAdminController) GetOrderWithSub(ctx echo.Context) error {
return c.FailJson(ctx, err)
}
if order.ID == 0 {
return c.FailJson(ctx, errors.New("order not found"))
return c.FailJson(ctx, constant.OrderNotExists)
}
subOrders, err := data.GetAllSubOrders(tradeID)
if err != nil {
+82 -13
View File
@@ -1,25 +1,28 @@
package admin
import (
"errors"
"net/url"
"strconv"
"strings"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/task"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4"
)
// CreateRpcNodeRequest is the payload for creating an RPC node.
type CreateRpcNodeRequest struct {
Network string `json:"network" validate:"required" example:"tron"`
Url string `json:"url" validate:"required" example:"https://api.trongrid.io"`
Url string `json:"url" validate:"required" example:"https://api.trongrid.io"`
// 连接类型 http=HTTP请求 ws=WebSocket长连接
Type string `json:"type" validate:"required|in:http,ws" enums:"http,ws" example:"http"`
Type string `json:"type" validate:"required|in:http,ws" enums:"http,ws" example:"http"`
Weight int `json:"weight" example:"1"`
ApiKey string `json:"api_key" example:""`
Enabled *bool `json:"enabled" example:"true"`
// 用途 general=通用 manual_verify=补单专用 both=通用+补单
Purpose string `json:"purpose" enums:"general,manual_verify,both" example:"general"`
}
// UpdateRpcNodeRequest is the payload for updating an RPC node.
@@ -28,6 +31,7 @@ type UpdateRpcNodeRequest struct {
Weight *int `json:"weight" example:"1"`
ApiKey *string `json:"api_key" example:"your-api-key"`
Enabled *bool `json:"enabled" example:"true"`
Purpose *string `json:"purpose" enums:"general,manual_verify,both" example:"manual_verify"`
}
// ListRpcNodes returns rows optionally filtered by network.
@@ -64,7 +68,7 @@ func (c *BaseAdminController) ListRpcNodes(ctx echo.Context) error {
func (c *BaseAdminController) CreateRpcNode(ctx echo.Context) error {
req := new(CreateRpcNodeRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err)
@@ -77,13 +81,23 @@ func (c *BaseAdminController) CreateRpcNode(ctx echo.Context) error {
if req.Enabled != nil {
enabled = *req.Enabled
}
purpose, err := rpcNodePurposeFromRequest(req.Purpose)
if err != nil {
return c.FailJson(ctx, err)
}
nodeURL := strings.TrimSpace(req.Url)
nodeType := strings.ToLower(strings.TrimSpace(req.Type))
if err := validateRpcNodeURLForType(nodeURL, nodeType); err != nil {
return c.FailJson(ctx, err)
}
row := &mdb.RpcNode{
Network: strings.ToLower(strings.TrimSpace(req.Network)),
Url: strings.TrimSpace(req.Url),
Type: strings.ToLower(strings.TrimSpace(req.Type)),
Url: nodeURL,
Type: nodeType,
Weight: weight,
ApiKey: req.ApiKey,
Enabled: enabled,
Purpose: purpose,
Status: mdb.RpcNodeStatusUnknown,
LastLatencyMs: -1,
}
@@ -93,7 +107,7 @@ func (c *BaseAdminController) CreateRpcNode(ctx echo.Context) error {
return c.SucJson(ctx, row)
}
// UpdateRpcNode patches url/weight/api_key/enabled.
// UpdateRpcNode patches url/weight/api_key/enabled/purpose.
// @Summary Update RPC node
// @Description Patch RPC node fields
// @Tags Admin RPC Nodes
@@ -108,15 +122,26 @@ func (c *BaseAdminController) CreateRpcNode(ctx echo.Context) error {
func (c *BaseAdminController) UpdateRpcNode(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
req := new(UpdateRpcNodeRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
fields := map[string]interface{}{}
if req.Url != nil {
fields["url"] = *req.Url
row, err := data.GetRpcNodeByID(id)
if err != nil {
return c.FailJson(ctx, err)
}
if row.ID == 0 {
return c.FailJson(ctx, constant.RpcNodeNotFoundErr)
}
nodeURL := strings.TrimSpace(*req.Url)
if err := validateRpcNodeURLForType(nodeURL, row.Type); err != nil {
return c.FailJson(ctx, err)
}
fields["url"] = nodeURL
}
if req.Weight != nil {
fields["weight"] = *req.Weight
@@ -127,6 +152,13 @@ func (c *BaseAdminController) UpdateRpcNode(ctx echo.Context) error {
if req.Enabled != nil {
fields["enabled"] = *req.Enabled
}
if req.Purpose != nil {
purpose, err := rpcNodePurposeFromRequest(*req.Purpose)
if err != nil {
return c.FailJson(ctx, err)
}
fields["purpose"] = purpose
}
if err := data.UpdateRpcNodeFields(id, fields); err != nil {
return c.FailJson(ctx, err)
}
@@ -146,7 +178,7 @@ func (c *BaseAdminController) UpdateRpcNode(ctx echo.Context) error {
func (c *BaseAdminController) DeleteRpcNode(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := data.DeleteRpcNodeByID(id); err != nil {
return c.FailJson(ctx, err)
@@ -170,14 +202,14 @@ func (c *BaseAdminController) DeleteRpcNode(ctx echo.Context) error {
func (c *BaseAdminController) HealthCheckRpcNode(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
row, err := data.GetRpcNodeByID(id)
if err != nil {
return c.FailJson(ctx, err)
}
if row.ID == 0 {
return c.FailJson(ctx, errors.New("node not found"))
return c.FailJson(ctx, constant.RpcNodeNotFoundErr)
}
status, latency := task.ProbeNode(row.Url)
if err := data.UpdateRpcNodeHealth(id, status, latency); err != nil {
@@ -188,3 +220,40 @@ func (c *BaseAdminController) HealthCheckRpcNode(ctx echo.Context) error {
"last_latency_ms": latency,
})
}
func rpcNodePurposeFromRequest(raw string) (string, error) {
purpose := strings.ToLower(strings.TrimSpace(raw))
if purpose == "" {
return mdb.RpcNodePurposeGeneral, nil
}
switch purpose {
case mdb.RpcNodePurposeGeneral, mdb.RpcNodePurposeManualVerify, mdb.RpcNodePurposeBoth:
return purpose, nil
default:
return "", constant.RpcNodePurposeErr
}
}
func validateRpcNodeURLForType(rawURL string, nodeType string) error {
rawURL = strings.TrimSpace(rawURL)
nodeType = strings.ToLower(strings.TrimSpace(nodeType))
parsed, err := url.Parse(rawURL)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return constant.RpcNodeURLErr
}
scheme := strings.ToLower(parsed.Scheme)
switch nodeType {
case mdb.RpcNodeTypeHttp:
if scheme == "http" || scheme == "https" {
return nil
}
return constant.RpcNodeHTTPURLErr
case mdb.RpcNodeTypeWs:
if scheme == "ws" || scheme == "wss" {
return nil
}
return constant.RpcNodeWebSocketURLErr
default:
return constant.RpcNodeTypeErr
}
}
+15 -9
View File
@@ -10,6 +10,7 @@ import (
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/telegram"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/security"
"github.com/labstack/echo/v4"
)
@@ -90,7 +91,7 @@ func (c *BaseAdminController) ListSettings(ctx echo.Context) error {
// independently so a malformed row in the middle doesn't drop earlier
// ones. Errors are returned per-key so the UI can surface them.
// @Summary Upsert settings
// @Description Batch insert/update settings. Returns per-key status.
// @Description Batch insert/update settings. Returns per-key status; failed items include error_code for frontend i18n.
// @Description Supported groups: brand, rate, system, epay, okpay.
// @Description epay group keys: epay.default_token (e.g. "usdt"), epay.default_currency (e.g. "cny"), epay.default_network (e.g. "tron").
// @Description okpay group keys: okpay.enabled, okpay.shop_id, okpay.shop_token, okpay.api_url, okpay.callback_url, okpay.return_url, okpay.timeout_seconds, okpay.allow_tokens.
@@ -108,31 +109,36 @@ func (c *BaseAdminController) ListSettings(ctx echo.Context) error {
func (c *BaseAdminController) UpsertSettings(ctx echo.Context) error {
req := new(SettingsUpsertRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err)
}
type result struct {
Key string `json:"key"`
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
Key string `json:"key"`
OK bool `json:"ok"`
ErrorCode int `json:"error_code,omitempty"`
Error string `json:"error,omitempty"`
}
out := make([]result, 0, len(req.Items))
errorResult := func(key string, err error) result {
code, msg := constant.ResolveErrno(err)
return result{Key: key, OK: false, ErrorCode: code, Error: msg}
}
for _, item := range req.Items {
key := strings.TrimSpace(item.Key)
if key == "" {
out = append(out, result{Key: item.Key, OK: false, Error: "key required"})
out = append(out, errorResult(item.Key, constant.SettingItemErr))
continue
}
value, err := normalizeSettingValue(item.Value)
if err != nil {
out = append(out, result{Key: key, OK: false, Error: err.Error()})
out = append(out, errorResult(key, constant.SettingItemErr))
continue
}
value, err = normalizeAndValidateSettingItem(item.Group, key, value)
if err != nil {
out = append(out, result{Key: key, OK: false, Error: err.Error()})
out = append(out, errorResult(key, constant.SettingItemErr))
continue
}
if key == mdb.SettingKeyAmountPrecision {
@@ -144,7 +150,7 @@ func (c *BaseAdminController) UpsertSettings(ctx echo.Context) error {
item.Type = mdb.SettingTypeJSON
}
if err := data.SetSetting(item.Group, key, value, item.Type); err != nil {
out = append(out, result{Key: key, OK: false, Error: err.Error()})
out = append(out, errorResult(key, err))
continue
}
out = append(out, result{Key: key, OK: true})
+17 -15
View File
@@ -1,13 +1,13 @@
package admin
import (
"errors"
"strconv"
"strings"
"github.com/GMWalletApp/epusdt/model/dao"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4"
)
@@ -105,7 +105,7 @@ func (c *BaseAdminController) AdminListWallets(ctx echo.Context) error {
func (c *BaseAdminController) AdminAddWallet(ctx echo.Context) error {
req := new(AdminAddWalletRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err)
@@ -140,14 +140,14 @@ func (c *BaseAdminController) AdminAddWallet(ctx echo.Context) error {
func (c *BaseAdminController) AdminGetWallet(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
wallet, err := data.GetWalletAddressById(id)
if err != nil {
return c.FailJson(ctx, err)
}
if wallet.ID == 0 {
return c.FailJson(ctx, errors.New("wallet not found"))
return c.FailJson(ctx, constant.WalletNotFoundErr)
}
counts, _ := data.CountOrdersByAddress()
return c.SucJson(ctx, WalletListItem{WalletAddress: *wallet, OrderCount: counts[wallet.Address]})
@@ -168,11 +168,11 @@ func (c *BaseAdminController) AdminGetWallet(ctx echo.Context) error {
func (c *BaseAdminController) AdminUpdateWallet(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
req := new(AdminUpdateWalletRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
fields := map[string]interface{}{}
if req.Remark != nil {
@@ -202,11 +202,11 @@ func (c *BaseAdminController) AdminUpdateWallet(ctx echo.Context) error {
func (c *BaseAdminController) AdminChangeWalletStatus(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
req := new(AdminChangeWalletStatusRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err)
@@ -230,7 +230,7 @@ func (c *BaseAdminController) AdminChangeWalletStatus(ctx echo.Context) error {
func (c *BaseAdminController) AdminDeleteWallet(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := data.DeleteWalletAddressById(id); err != nil {
return c.FailJson(ctx, err)
@@ -242,7 +242,7 @@ func (c *BaseAdminController) AdminDeleteWallet(ctx echo.Context) error {
// them all as observation wallets. Per-row failures are collected and
// returned so the UI can surface which addresses already existed.
// @Summary Batch import wallets
// @Description Import multiple wallet addresses at once. Per-row status is returned.
// @Description Import multiple wallet addresses at once. Per-row status is returned; failed rows include error_code for frontend i18n.
// @Tags Admin Wallets
// @Security AdminJWT
// @Accept json
@@ -254,21 +254,23 @@ func (c *BaseAdminController) AdminDeleteWallet(ctx echo.Context) error {
func (c *BaseAdminController) AdminBatchImportWallets(ctx echo.Context) error {
req := new(AdminBatchImportRequest)
if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err)
}
type result struct {
Address string `json:"address"`
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
Address string `json:"address"`
OK bool `json:"ok"`
ErrorCode int `json:"error_code,omitempty"`
Error string `json:"error,omitempty"`
}
out := make([]result, 0, len(req.Addresses))
for _, addr := range req.Addresses {
row, err := data.AddWalletAddressWithNetwork(req.Network, addr)
if err != nil {
out = append(out, result{Address: addr, OK: false, Error: err.Error()})
code, msg := constant.ResolveErrno(err)
out = append(out, result{Address: addr, OK: false, ErrorCode: code, Error: msg})
continue
}
_ = dao.Mdb.Model(&mdb.WalletAddress{}).Where("id = ?", row.ID).
+2 -2
View File
@@ -1,7 +1,7 @@
package controller
import (
"errors"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/http"
"github.com/gookit/validate"
"github.com/gookit/validate/locales/zhcn"
@@ -46,6 +46,6 @@ func (c *BaseController) ValidateStruct(ctx echo.Context, i interface{}, scene .
if v.Validate() {
return nil
} else {
return errors.New(v.Errors.One())
return constant.ParamsMarshalErr
}
}
+4 -4
View File
@@ -41,7 +41,7 @@ func apiKeyFromContext(ctx echo.Context) *mdb.ApiKey {
// @Param name formData string false "Order name"
// @Param payment_type formData string false "Payment type"
// @Success 200 {object} response.ApiResponse{data=response.CreateTransactionResponse}
// @Failure 400 {object} response.ApiResponse
// @Failure 400 {object} response.ApiResponse "Stable errno in status_code: 10009 invalid params, 10041 invalid notify_url, 10004 invalid amount, 10014 chain disabled, 10003 no wallet, 10005 no amount channel"
// @Router /payments/gmpay/v1/order/create-transaction [post]
func (c *BaseCommController) CreateTransaction(ctx echo.Context) (err error) {
req := new(request.CreateTransactionRequest)
@@ -68,7 +68,7 @@ func (c *BaseCommController) CreateTransaction(ctx echo.Context) (err error) {
// @Produce json
// @Param request body request.SwitchNetworkRequest true "Switch network payload"
// @Success 200 {object} response.ApiResponse{data=response.CheckoutCounterResponse}
// @Failure 400 {object} response.ApiResponse
// @Failure 400 {object} response.ApiResponse "Stable errno in status_code: 10008 order not found, 10012 cannot switch sub-order, 10013 not waiting payment, 10017/10018/10019 provider errors, 10042 provider order creation failed"
// @Router /pay/switch-network [post]
func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
req := new(request.SwitchNetworkRequest)
@@ -85,7 +85,7 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
jsonBytes, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return c.FailJson(ctx, err)
return c.FailJson(ctx, constant.SystemErr)
}
log.Sugar.Debugf("switch network response: \n%s", string(jsonBytes))
@@ -121,7 +121,7 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
// @Param sign formData string true "MD5 signature"
// @Param sign_type formData string false "Signature type (MD5)"
// @Success 302 "Redirect to checkout counter"
// @Failure 400 {object} response.ApiResponse
// @Failure 400 {object} response.ApiResponse "Stable errno in status_code: 10009 invalid params, 10041 invalid notify_url, 10004 invalid amount, 10014 chain disabled, 10003 no wallet, 10005 no amount channel"
// @Router /payments/epay/v1/order/create-transaction/submit.php [post]
// @Router /payments/epay/v1/order/create-transaction/submit.php [get]
func (c *BaseCommController) CreateTransactionAndRedirect(ctx echo.Context) (err error) {
+2 -2
View File
@@ -60,7 +60,7 @@ func (c *BaseCommController) CheckStatus(ctx echo.Context) (err error) {
// @Param trade_id path string true "Trade ID"
// @Param request body request.ManualPaymentRequest true "Transaction hash payload"
// @Success 200 {object} response.ApiResponse{data=response.ManualPaymentResponse}
// @Failure 400 {object} response.ApiResponse "Order not found, order not waiting payment, unsupported provider order, invalid hash, or other request error"
// @Failure 400 {object} response.ApiResponse "Stable errno in status_code: 10008 order not found, 10009 invalid params, 10013 not waiting payment, 10038 verification failed, 10039 unsupported provider, 10007 hash already processed"
// @Router /pay/submit-tx-hash/{trade_id} [post]
func (c *BaseCommController) SubmitTxHash(ctx echo.Context) (err error) {
tradeId := ctx.Param("trade_id")
@@ -73,7 +73,7 @@ func (c *BaseCommController) SubmitTxHash(ctx echo.Context) (err error) {
return c.FailJson(ctx, err)
}
resp, err := service.SubmitManualPayment(tradeId, req.BlockTransactionId)
resp, err := service.SubmitCashierManualPayment(tradeId, req.BlockTransactionId)
if err != nil {
return c.FailJson(ctx, err)
}