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 ( import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"errors"
"strconv" "strconv"
"strings" "strings"
"github.com/GMWalletApp/epusdt/model/data" "github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4" "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 { func (c *BaseAdminController) CreateApiKey(ctx echo.Context) error {
req := new(CreateApiKeyRequest) req := new(CreateApiKeyRequest)
if err := ctx.Bind(req); err != nil { 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 { if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
@@ -150,11 +150,11 @@ func isUniqueViolation(err error) bool {
func (c *BaseAdminController) UpdateApiKey(ctx echo.Context) error { func (c *BaseAdminController) UpdateApiKey(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
req := new(UpdateApiKeyRequest) req := new(UpdateApiKeyRequest)
if err := ctx.Bind(req); err != nil { if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
fields := map[string]interface{}{} fields := map[string]interface{}{}
if req.Name != nil { if req.Name != nil {
@@ -187,11 +187,11 @@ func (c *BaseAdminController) UpdateApiKey(ctx echo.Context) error {
func (c *BaseAdminController) ChangeApiKeyStatus(ctx echo.Context) error { func (c *BaseAdminController) ChangeApiKeyStatus(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
req := new(ChangeApiKeyStatusRequest) req := new(ChangeApiKeyStatusRequest)
if err := ctx.Bind(req); err != nil { 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 { if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err) 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 { func (c *BaseAdminController) RotateApiKeySecret(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
secret := randomHex(32) secret := randomHex(32)
if err := data.UpdateApiKeyFields(id, map[string]interface{}{"secret_key": secret}); err != nil { 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 { func (c *BaseAdminController) DeleteApiKey(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
if err := data.DeleteApiKeyByID(id); err != nil { if err := data.DeleteApiKeyByID(id); err != nil {
return c.FailJson(ctx, err) 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 { func (c *BaseAdminController) GetApiKeyStats(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
row, err := data.GetApiKeyByID(id) row, err := data.GetApiKeyByID(id)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
if row.ID == 0 { 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{}{ return c.SucJson(ctx, map[string]interface{}{
"call_count": row.CallCount, "call_count": row.CallCount,
@@ -291,14 +291,14 @@ func (c *BaseAdminController) GetApiKeyStats(ctx echo.Context) error {
func (c *BaseAdminController) GetApiKeySecret(ctx echo.Context) error { func (c *BaseAdminController) GetApiKeySecret(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
row, err := data.GetApiKeyByID(id) row, err := data.GetApiKeyByID(id)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
if row.ID == 0 { 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}) 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/data"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/util/constant"
appjwt "github.com/GMWalletApp/epusdt/util/jwt" appjwt "github.com/GMWalletApp/epusdt/util/jwt"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@@ -53,7 +54,7 @@ type InitialPasswordResponse struct {
func (c *BaseAdminController) Login(ctx echo.Context) error { func (c *BaseAdminController) Login(ctx echo.Context) error {
req := new(LoginRequest) req := new(LoginRequest)
if err := ctx.Bind(req); err != nil { 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 { if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
@@ -63,10 +64,10 @@ func (c *BaseAdminController) Login(ctx echo.Context) error {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
if user.ID == 0 || !data.VerifyPassword(user.PasswordHash, req.Password) { 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 { 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) token, err := appjwt.Sign(user.ID, user.Username)
if err != nil { if err != nil {
@@ -94,6 +95,9 @@ func (c *BaseAdminController) Login(ctx echo.Context) error {
func (c *BaseAdminController) GetInitialPassword(ctx echo.Context) error { func (c *BaseAdminController) GetInitialPassword(ctx echo.Context) error {
password, err := data.ConsumeInitialAdminPassword() password, err := data.ConsumeInitialAdminPassword()
if err != nil { 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.FailJson(ctx, err)
} }
return c.SucJson(ctx, InitialPasswordResponse{ 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 { func (c *BaseAdminController) Me(ctx echo.Context) error {
uid := currentAdminUserID(ctx) uid := currentAdminUserID(ctx)
if uid == 0 { if uid == 0 {
return c.FailJson(ctx, errors.New("unauthorized")) return c.FailJson(ctx, constant.AdminUnauthorizedErr)
} }
user, err := data.GetAdminUserByID(uid) user, err := data.GetAdminUserByID(uid)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
if user.ID == 0 { 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 // Warn the frontend when the operator hasn't changed the seeded
// initial password. // initial password.
@@ -176,11 +180,11 @@ func (c *BaseAdminController) Me(ctx echo.Context) error {
func (c *BaseAdminController) ChangePassword(ctx echo.Context) error { func (c *BaseAdminController) ChangePassword(ctx echo.Context) error {
uid := currentAdminUserID(ctx) uid := currentAdminUserID(ctx)
if uid == 0 { if uid == 0 {
return c.FailJson(ctx, errors.New("unauthorized")) return c.FailJson(ctx, constant.AdminUnauthorizedErr)
} }
req := new(ChangePasswordRequest) req := new(ChangePasswordRequest)
if err := ctx.Bind(req); err != nil { 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 { if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
@@ -190,7 +194,7 @@ func (c *BaseAdminController) ChangePassword(ctx echo.Context) error {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
if user.ID == 0 || !data.VerifyPassword(user.PasswordHash, req.OldPassword) { 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 { if err := data.UpdateAdminUserPassword(uid, req.NewPassword); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
+2 -1
View File
@@ -2,6 +2,7 @@ package admin
import ( import (
"github.com/GMWalletApp/epusdt/model/data" "github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@@ -49,7 +50,7 @@ func (c *BaseAdminController) UpdateChain(ctx echo.Context) error {
network := ctx.Param("network") network := ctx.Param("network")
req := new(UpdateChainRequest) req := new(UpdateChainRequest)
if err := ctx.Bind(req); err != nil { if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
fields := map[string]interface{}{} fields := map[string]interface{}{}
if req.Enabled != nil { if req.Enabled != nil {
@@ -6,6 +6,7 @@ import (
"github.com/GMWalletApp/epusdt/model/data" "github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4" "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 { func (c *BaseAdminController) CreateChainToken(ctx echo.Context) error {
req := new(CreateChainTokenRequest) req := new(CreateChainTokenRequest)
if err := ctx.Bind(req); err != nil { 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 { if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err) 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 { func (c *BaseAdminController) UpdateChainToken(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
req := new(UpdateChainTokenRequest) req := new(UpdateChainTokenRequest)
if err := ctx.Bind(req); err != nil { if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
fields := map[string]interface{}{} fields := map[string]interface{}{}
if req.ContractAddress != nil { if req.ContractAddress != nil {
@@ -147,11 +148,11 @@ func (c *BaseAdminController) UpdateChainToken(ctx echo.Context) error {
func (c *BaseAdminController) ChangeChainTokenStatus(ctx echo.Context) error { func (c *BaseAdminController) ChangeChainTokenStatus(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
req := new(ChangeChainTokenStatusRequest) req := new(ChangeChainTokenStatusRequest)
if err := ctx.Bind(req); err != nil { 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 { if err := data.UpdateChainTokenFields(id, map[string]interface{}{"enabled": req.Enabled}); err != nil {
return c.FailJson(ctx, err) 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 { func (c *BaseAdminController) DeleteChainToken(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
if err := data.DeleteChainTokenByID(id); err != nil { if err := data.DeleteChainTokenByID(id); err != nil {
return c.FailJson(ctx, err) 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. // BatchImportResult is one row in the batch import response.
type BatchImportResult struct { type BatchImportResult struct {
Address string `json:"address" example:"TTestTronAddress001"` Address string `json:"address" example:"TTestTronAddress001"`
OK bool `json:"ok" example:"true"` OK bool `json:"ok" example:"true"`
Error string `json:"error,omitempty" example:""` 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. // SettingsUpsertResult is one row in the settings upsert response.
type SettingsUpsertResult struct { type SettingsUpsertResult struct {
Key string `json:"key" example:"rate.api_url"` Key string `json:"key" example:"rate.api_url"`
OK bool `json:"ok" example:"true"` OK bool `json:"ok" example:"true"`
Error string `json:"error,omitempty" example:""` 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. // Overview returns the top-card numbers for the dashboard landing page.
+13 -13
View File
@@ -2,7 +2,6 @@ package admin
import ( import (
"encoding/json" "encoding/json"
"errors"
"strconv" "strconv"
"strings" "strings"
@@ -10,6 +9,7 @@ import (
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/notify" "github.com/GMWalletApp/epusdt/notify"
"github.com/GMWalletApp/epusdt/telegram" "github.com/GMWalletApp/epusdt/telegram"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4" "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 { func (c *BaseAdminController) CreateNotificationChannel(ctx echo.Context) error {
req := new(CreateNotificationChannelRequest) req := new(CreateNotificationChannelRequest)
if err := ctx.Bind(req); err != nil { 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)) req.Type = strings.ToLower(strings.TrimSpace(req.Type))
if err := c.ValidateStruct(ctx, req); err != nil { 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 { func (c *BaseAdminController) UpdateNotificationChannel(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
existing, err := data.GetNotificationChannelByID(id) existing, err := data.GetNotificationChannelByID(id)
if err != nil { if err != nil {
@@ -130,7 +130,7 @@ func (c *BaseAdminController) UpdateNotificationChannel(ctx echo.Context) error
req := new(UpdateNotificationChannelRequest) req := new(UpdateNotificationChannelRequest)
if err := ctx.Bind(req); err != nil { if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
fields := map[string]interface{}{} fields := map[string]interface{}{}
if req.Name != nil { if req.Name != nil {
@@ -175,7 +175,7 @@ func (c *BaseAdminController) UpdateNotificationChannel(ctx echo.Context) error
func (c *BaseAdminController) ChangeNotificationChannelStatus(ctx echo.Context) error { func (c *BaseAdminController) ChangeNotificationChannelStatus(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
existing, err := data.GetNotificationChannelByID(id) existing, err := data.GetNotificationChannelByID(id)
if err != nil { if err != nil {
@@ -185,7 +185,7 @@ func (c *BaseAdminController) ChangeNotificationChannelStatus(ctx echo.Context)
req := new(ChangeChannelStatusRequest) req := new(ChangeChannelStatusRequest)
if err := ctx.Bind(req); err != nil { 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 { if err := data.UpdateNotificationChannelFields(id, map[string]interface{}{"enabled": req.Enabled}); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
@@ -209,7 +209,7 @@ func (c *BaseAdminController) ChangeNotificationChannelStatus(ctx echo.Context)
func (c *BaseAdminController) DeleteNotificationChannel(ctx echo.Context) error { func (c *BaseAdminController) DeleteNotificationChannel(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
existing, err := data.GetNotificationChannelByID(id) existing, err := data.GetNotificationChannelByID(id)
if err != nil { 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. // + chat_id; webhook/email kept loose until those senders exist.
func validateChannelConfig(channelType string, cfg map[string]interface{}) error { func validateChannelConfig(channelType string, cfg map[string]interface{}) error {
if cfg == nil { if cfg == nil {
return errors.New("config required") return constant.NotificationConfigErr
} }
if channelType == mdb.NotificationTypeTelegram { if channelType == mdb.NotificationTypeTelegram {
configJSON, _ := json.Marshal(cfg) configJSON, _ := json.Marshal(cfg)
if _, err := notify.ParseTelegramConfig(string(configJSON)); err != nil { if _, err := notify.ParseTelegramConfig(string(configJSON)); err != nil {
return err return constant.NotificationConfigErr
} }
} }
return nil return nil
@@ -260,10 +260,10 @@ func normalizeChannelEvents(raw interface{}) (map[string]bool, error) {
case "1", "true", "on", "yes": case "1", "true", "on", "yes":
return true, nil return true, nil
default: default:
return false, errors.New("invalid boolean string") return false, constant.NotificationEventsErr
} }
default: 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 { for _, v := range t {
s, ok := v.(string) s, ok := v.(string)
if !ok { if !ok {
return nil, errors.New("events array must contain strings") return nil, constant.NotificationEventsErr
} }
s = trimKey(s) s = trimKey(s)
if s == "" { if s == "" {
@@ -309,6 +309,6 @@ func normalizeChannelEvents(raw interface{}) (map[string]bool, error) {
} }
return out, nil return out, nil
default: 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 ( import (
"encoding/csv" "encoding/csv"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
@@ -13,6 +12,7 @@ import (
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/model/request" "github.com/GMWalletApp/epusdt/model/request"
"github.com/GMWalletApp/epusdt/model/service" "github.com/GMWalletApp/epusdt/model/service"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/log" "github.com/GMWalletApp/epusdt/util/log"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@@ -88,7 +88,7 @@ func (c *BaseAdminController) GetOrder(ctx echo.Context) error {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
if order.ID == 0 { if order.ID == 0 {
return c.FailJson(ctx, errors.New("order not found")) return c.FailJson(ctx, constant.OrderNotExists)
} }
return c.SucJson(ctx, order) return c.SucJson(ctx, order)
} }
@@ -110,17 +110,17 @@ func (c *BaseAdminController) CloseOrder(ctx echo.Context) error {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
if order.ID == 0 { if order.ID == 0 {
return c.FailJson(ctx, errors.New("order not found")) return c.FailJson(ctx, constant.OrderNotExists)
} }
if order.Status != mdb.StatusWaitPay { 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) ok, err := data.CloseOrderManually(tradeID)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
if !ok { 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. // Release the transaction lock so the amount slot becomes reusable.
_ = data.UnLockTransaction(order.Network, order.ReceiveAddress, order.Token, order.ActualAmount) _ = 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) req := new(request.ManualPaymentRequest)
if err := ctx.Bind(req); err != nil { 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) 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) req.BlockTransactionId = strings.TrimSpace(req.BlockTransactionId)
if err := c.ValidateStruct(ctx, req); err != nil { 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) return c.FailJson(ctx, err)
} }
if order.ID == 0 { 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) 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) return c.FailJson(ctx, err)
} }
if order.Status != mdb.StatusPaySuccess { 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) 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) return c.FailJson(ctx, err)
} }
if strings.TrimSpace(order.NotifyUrl) == "" { 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) 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) return c.FailJson(ctx, err)
} }
@@ -204,7 +204,7 @@ func (c *BaseAdminController) ResendCallback(ctx echo.Context) error {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
if !ok { 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) 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) return c.FailJson(ctx, err)
} }
@@ -389,7 +389,7 @@ func (c *BaseAdminController) GetOrderWithSub(ctx echo.Context) error {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
if order.ID == 0 { if order.ID == 0 {
return c.FailJson(ctx, errors.New("order not found")) return c.FailJson(ctx, constant.OrderNotExists)
} }
subOrders, err := data.GetAllSubOrders(tradeID) subOrders, err := data.GetAllSubOrders(tradeID)
if err != nil { if err != nil {
+82 -13
View File
@@ -1,25 +1,28 @@
package admin package admin
import ( import (
"errors" "net/url"
"strconv" "strconv"
"strings" "strings"
"github.com/GMWalletApp/epusdt/model/data" "github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/task" "github.com/GMWalletApp/epusdt/task"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
// CreateRpcNodeRequest is the payload for creating an RPC node. // CreateRpcNodeRequest is the payload for creating an RPC node.
type CreateRpcNodeRequest struct { type CreateRpcNodeRequest struct {
Network string `json:"network" validate:"required" example:"tron"` 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长连接 // 连接类型 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"` Weight int `json:"weight" example:"1"`
ApiKey string `json:"api_key" example:""` ApiKey string `json:"api_key" example:""`
Enabled *bool `json:"enabled" example:"true"` 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. // UpdateRpcNodeRequest is the payload for updating an RPC node.
@@ -28,6 +31,7 @@ type UpdateRpcNodeRequest struct {
Weight *int `json:"weight" example:"1"` Weight *int `json:"weight" example:"1"`
ApiKey *string `json:"api_key" example:"your-api-key"` ApiKey *string `json:"api_key" example:"your-api-key"`
Enabled *bool `json:"enabled" example:"true"` 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. // 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 { func (c *BaseAdminController) CreateRpcNode(ctx echo.Context) error {
req := new(CreateRpcNodeRequest) req := new(CreateRpcNodeRequest)
if err := ctx.Bind(req); err != nil { 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 { if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
@@ -77,13 +81,23 @@ func (c *BaseAdminController) CreateRpcNode(ctx echo.Context) error {
if req.Enabled != nil { if req.Enabled != nil {
enabled = *req.Enabled 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{ row := &mdb.RpcNode{
Network: strings.ToLower(strings.TrimSpace(req.Network)), Network: strings.ToLower(strings.TrimSpace(req.Network)),
Url: strings.TrimSpace(req.Url), Url: nodeURL,
Type: strings.ToLower(strings.TrimSpace(req.Type)), Type: nodeType,
Weight: weight, Weight: weight,
ApiKey: req.ApiKey, ApiKey: req.ApiKey,
Enabled: enabled, Enabled: enabled,
Purpose: purpose,
Status: mdb.RpcNodeStatusUnknown, Status: mdb.RpcNodeStatusUnknown,
LastLatencyMs: -1, LastLatencyMs: -1,
} }
@@ -93,7 +107,7 @@ func (c *BaseAdminController) CreateRpcNode(ctx echo.Context) error {
return c.SucJson(ctx, row) return c.SucJson(ctx, row)
} }
// UpdateRpcNode patches url/weight/api_key/enabled. // UpdateRpcNode patches url/weight/api_key/enabled/purpose.
// @Summary Update RPC node // @Summary Update RPC node
// @Description Patch RPC node fields // @Description Patch RPC node fields
// @Tags Admin RPC Nodes // @Tags Admin RPC Nodes
@@ -108,15 +122,26 @@ func (c *BaseAdminController) CreateRpcNode(ctx echo.Context) error {
func (c *BaseAdminController) UpdateRpcNode(ctx echo.Context) error { func (c *BaseAdminController) UpdateRpcNode(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
req := new(UpdateRpcNodeRequest) req := new(UpdateRpcNodeRequest)
if err := ctx.Bind(req); err != nil { if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
fields := map[string]interface{}{} fields := map[string]interface{}{}
if req.Url != nil { 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 { if req.Weight != nil {
fields["weight"] = *req.Weight fields["weight"] = *req.Weight
@@ -127,6 +152,13 @@ func (c *BaseAdminController) UpdateRpcNode(ctx echo.Context) error {
if req.Enabled != nil { if req.Enabled != nil {
fields["enabled"] = *req.Enabled 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 { if err := data.UpdateRpcNodeFields(id, fields); err != nil {
return c.FailJson(ctx, err) 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 { func (c *BaseAdminController) DeleteRpcNode(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
if err := data.DeleteRpcNodeByID(id); err != nil { if err := data.DeleteRpcNodeByID(id); err != nil {
return c.FailJson(ctx, err) 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 { func (c *BaseAdminController) HealthCheckRpcNode(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
row, err := data.GetRpcNodeByID(id) row, err := data.GetRpcNodeByID(id)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
if row.ID == 0 { 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) status, latency := task.ProbeNode(row.Url)
if err := data.UpdateRpcNodeHealth(id, status, latency); err != nil { 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, "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/data"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/telegram" "github.com/GMWalletApp/epusdt/telegram"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/security" "github.com/GMWalletApp/epusdt/util/security"
"github.com/labstack/echo/v4" "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 // independently so a malformed row in the middle doesn't drop earlier
// ones. Errors are returned per-key so the UI can surface them. // ones. Errors are returned per-key so the UI can surface them.
// @Summary Upsert settings // @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 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 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. // @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 { func (c *BaseAdminController) UpsertSettings(ctx echo.Context) error {
req := new(SettingsUpsertRequest) req := new(SettingsUpsertRequest)
if err := ctx.Bind(req); err != nil { 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 { if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
type result struct { type result struct {
Key string `json:"key"` Key string `json:"key"`
OK bool `json:"ok"` OK bool `json:"ok"`
Error string `json:"error,omitempty"` ErrorCode int `json:"error_code,omitempty"`
Error string `json:"error,omitempty"`
} }
out := make([]result, 0, len(req.Items)) 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 { for _, item := range req.Items {
key := strings.TrimSpace(item.Key) key := strings.TrimSpace(item.Key)
if key == "" { if key == "" {
out = append(out, result{Key: item.Key, OK: false, Error: "key required"}) out = append(out, errorResult(item.Key, constant.SettingItemErr))
continue continue
} }
value, err := normalizeSettingValue(item.Value) value, err := normalizeSettingValue(item.Value)
if err != nil { if err != nil {
out = append(out, result{Key: key, OK: false, Error: err.Error()}) out = append(out, errorResult(key, constant.SettingItemErr))
continue continue
} }
value, err = normalizeAndValidateSettingItem(item.Group, key, value) value, err = normalizeAndValidateSettingItem(item.Group, key, value)
if err != nil { if err != nil {
out = append(out, result{Key: key, OK: false, Error: err.Error()}) out = append(out, errorResult(key, constant.SettingItemErr))
continue continue
} }
if key == mdb.SettingKeyAmountPrecision { if key == mdb.SettingKeyAmountPrecision {
@@ -144,7 +150,7 @@ func (c *BaseAdminController) UpsertSettings(ctx echo.Context) error {
item.Type = mdb.SettingTypeJSON item.Type = mdb.SettingTypeJSON
} }
if err := data.SetSetting(item.Group, key, value, item.Type); err != nil { 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 continue
} }
out = append(out, result{Key: key, OK: true}) out = append(out, result{Key: key, OK: true})
+17 -15
View File
@@ -1,13 +1,13 @@
package admin package admin
import ( import (
"errors"
"strconv" "strconv"
"strings" "strings"
"github.com/GMWalletApp/epusdt/model/dao" "github.com/GMWalletApp/epusdt/model/dao"
"github.com/GMWalletApp/epusdt/model/data" "github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4" "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 { func (c *BaseAdminController) AdminAddWallet(ctx echo.Context) error {
req := new(AdminAddWalletRequest) req := new(AdminAddWalletRequest)
if err := ctx.Bind(req); err != nil { 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 { if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err) 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 { func (c *BaseAdminController) AdminGetWallet(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
wallet, err := data.GetWalletAddressById(id) wallet, err := data.GetWalletAddressById(id)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
if wallet.ID == 0 { if wallet.ID == 0 {
return c.FailJson(ctx, errors.New("wallet not found")) return c.FailJson(ctx, constant.WalletNotFoundErr)
} }
counts, _ := data.CountOrdersByAddress() counts, _ := data.CountOrdersByAddress()
return c.SucJson(ctx, WalletListItem{WalletAddress: *wallet, OrderCount: counts[wallet.Address]}) 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 { func (c *BaseAdminController) AdminUpdateWallet(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
req := new(AdminUpdateWalletRequest) req := new(AdminUpdateWalletRequest)
if err := ctx.Bind(req); err != nil { if err := ctx.Bind(req); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
fields := map[string]interface{}{} fields := map[string]interface{}{}
if req.Remark != nil { if req.Remark != nil {
@@ -202,11 +202,11 @@ func (c *BaseAdminController) AdminUpdateWallet(ctx echo.Context) error {
func (c *BaseAdminController) AdminChangeWalletStatus(ctx echo.Context) error { func (c *BaseAdminController) AdminChangeWalletStatus(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
req := new(AdminChangeWalletStatusRequest) req := new(AdminChangeWalletStatusRequest)
if err := ctx.Bind(req); err != nil { 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 { if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err) 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 { func (c *BaseAdminController) AdminDeleteWallet(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.ParamsMarshalErr)
} }
if err := data.DeleteWalletAddressById(id); err != nil { if err := data.DeleteWalletAddressById(id); err != nil {
return c.FailJson(ctx, err) 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 // them all as observation wallets. Per-row failures are collected and
// returned so the UI can surface which addresses already existed. // returned so the UI can surface which addresses already existed.
// @Summary Batch import wallets // @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 // @Tags Admin Wallets
// @Security AdminJWT // @Security AdminJWT
// @Accept json // @Accept json
@@ -254,21 +254,23 @@ func (c *BaseAdminController) AdminDeleteWallet(ctx echo.Context) error {
func (c *BaseAdminController) AdminBatchImportWallets(ctx echo.Context) error { func (c *BaseAdminController) AdminBatchImportWallets(ctx echo.Context) error {
req := new(AdminBatchImportRequest) req := new(AdminBatchImportRequest)
if err := ctx.Bind(req); err != nil { 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 { if err := c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
type result struct { type result struct {
Address string `json:"address"` Address string `json:"address"`
OK bool `json:"ok"` OK bool `json:"ok"`
Error string `json:"error,omitempty"` ErrorCode int `json:"error_code,omitempty"`
Error string `json:"error,omitempty"`
} }
out := make([]result, 0, len(req.Addresses)) out := make([]result, 0, len(req.Addresses))
for _, addr := range req.Addresses { for _, addr := range req.Addresses {
row, err := data.AddWalletAddressWithNetwork(req.Network, addr) row, err := data.AddWalletAddressWithNetwork(req.Network, addr)
if err != nil { 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 continue
} }
_ = dao.Mdb.Model(&mdb.WalletAddress{}).Where("id = ?", row.ID). _ = dao.Mdb.Model(&mdb.WalletAddress{}).Where("id = ?", row.ID).
+2 -2
View File
@@ -1,7 +1,7 @@
package controller package controller
import ( import (
"errors" "github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/http" "github.com/GMWalletApp/epusdt/util/http"
"github.com/gookit/validate" "github.com/gookit/validate"
"github.com/gookit/validate/locales/zhcn" "github.com/gookit/validate/locales/zhcn"
@@ -46,6 +46,6 @@ func (c *BaseController) ValidateStruct(ctx echo.Context, i interface{}, scene .
if v.Validate() { if v.Validate() {
return nil return nil
} else { } 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 name formData string false "Order name"
// @Param payment_type formData string false "Payment type" // @Param payment_type formData string false "Payment type"
// @Success 200 {object} response.ApiResponse{data=response.CreateTransactionResponse} // @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] // @Router /payments/gmpay/v1/order/create-transaction [post]
func (c *BaseCommController) CreateTransaction(ctx echo.Context) (err error) { func (c *BaseCommController) CreateTransaction(ctx echo.Context) (err error) {
req := new(request.CreateTransactionRequest) req := new(request.CreateTransactionRequest)
@@ -68,7 +68,7 @@ func (c *BaseCommController) CreateTransaction(ctx echo.Context) (err error) {
// @Produce json // @Produce json
// @Param request body request.SwitchNetworkRequest true "Switch network payload" // @Param request body request.SwitchNetworkRequest true "Switch network payload"
// @Success 200 {object} response.ApiResponse{data=response.CheckoutCounterResponse} // @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] // @Router /pay/switch-network [post]
func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) { func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
req := new(request.SwitchNetworkRequest) req := new(request.SwitchNetworkRequest)
@@ -85,7 +85,7 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
jsonBytes, err := json.MarshalIndent(resp, "", " ") jsonBytes, err := json.MarshalIndent(resp, "", " ")
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, constant.SystemErr)
} }
log.Sugar.Debugf("switch network response: \n%s", string(jsonBytes)) 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 formData string true "MD5 signature"
// @Param sign_type formData string false "Signature type (MD5)" // @Param sign_type formData string false "Signature type (MD5)"
// @Success 302 "Redirect to checkout counter" // @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 [post]
// @Router /payments/epay/v1/order/create-transaction/submit.php [get] // @Router /payments/epay/v1/order/create-transaction/submit.php [get]
func (c *BaseCommController) CreateTransactionAndRedirect(ctx echo.Context) (err error) { 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 trade_id path string true "Trade ID"
// @Param request body request.ManualPaymentRequest true "Transaction hash payload" // @Param request body request.ManualPaymentRequest true "Transaction hash payload"
// @Success 200 {object} response.ApiResponse{data=response.ManualPaymentResponse} // @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] // @Router /pay/submit-tx-hash/{trade_id} [post]
func (c *BaseCommController) SubmitTxHash(ctx echo.Context) (err error) { func (c *BaseCommController) SubmitTxHash(ctx echo.Context) (err error) {
tradeId := ctx.Param("trade_id") tradeId := ctx.Param("trade_id")
@@ -73,7 +73,7 @@ func (c *BaseCommController) SubmitTxHash(ctx echo.Context) (err error) {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
resp, err := service.SubmitManualPayment(tradeId, req.BlockTransactionId) resp, err := service.SubmitCashierManualPayment(tradeId, req.BlockTransactionId)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
+25 -8
View File
@@ -46,6 +46,7 @@ func MdbTableInit() {
} }
seedChains() seedChains()
backfillRpcNodePurpose()
seedRpcNodes() seedRpcNodes()
seedChainTokens() seedChainTokens()
seedDefaultSettings() seedDefaultSettings()
@@ -106,14 +107,7 @@ func seedChainTokens() {
// seedRpcNodes inserts default RPC endpoints for each chain. Checks per // seedRpcNodes inserts default RPC endpoints for each chain. Checks per
// (network, url) so re-runs are idempotent and admin edits persist. // (network, url) so re-runs are idempotent and admin edits persist.
func seedRpcNodes() { func seedRpcNodes() {
defaults := []mdb.RpcNode{ defaults := defaultRpcNodes()
{Network: mdb.NetworkTron, Url: "https://api.trongrid.io", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkEthereum, Url: "wss://ethereum.publicnode.com", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkSolana, Url: "https://api.mainnet-beta.solana.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkBsc, Url: "wss://bsc.drpc.org", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkPolygon, Url: "wss://polygon-bor-rpc.publicnode.com", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkPlasma, Url: "wss://rpc.plasma.to", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Status: mdb.RpcNodeStatusUnknown},
}
for _, d := range defaults { for _, d := range defaults {
var count int64 var count int64
if err := Mdb.Model(&mdb.RpcNode{}). if err := Mdb.Model(&mdb.RpcNode{}).
@@ -131,6 +125,29 @@ func seedRpcNodes() {
} }
} }
func defaultRpcNodes() []mdb.RpcNode {
defaults := []mdb.RpcNode{
{Network: mdb.NetworkTron, Url: "https://api.trongrid.io", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkEthereum, Url: "wss://ethereum.publicnode.com", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkSolana, Url: "https://api.mainnet-beta.solana.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkBsc, Url: "wss://bsc.drpc.org", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkPolygon, Url: "wss://polygon-bor-rpc.publicnode.com", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkPlasma, Url: "wss://rpc.plasma.to", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkEthereum, Url: "https://rpc.epusdt.com/ethereum", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkBsc, Url: "https://rpc.epusdt.com/binance", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkPolygon, Url: "https://rpc.epusdt.com/polygon", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusUnknown},
}
return defaults
}
func backfillRpcNodePurpose() {
if err := Mdb.Model(&mdb.RpcNode{}).
Where("purpose = '' OR purpose IS NULL").
Update("purpose", mdb.RpcNodePurposeGeneral).Error; err != nil {
color.Red.Printf("[store_db] backfill rpc_nodes purpose err=%s\n", err)
}
}
// seedDefaultSettings inserts built-in default settings. // seedDefaultSettings inserts built-in default settings.
// Uses ON CONFLICT DO NOTHING so admin edits persist across restarts. // Uses ON CONFLICT DO NOTHING so admin edits persist across restarts.
func seedDefaultSettings() { func seedDefaultSettings() {
+43
View File
@@ -0,0 +1,43 @@
package dao
import (
"testing"
"github.com/GMWalletApp/epusdt/model/mdb"
)
func TestDefaultRpcNodesIncludesManualVerifyEpusdtEvmNodes(t *testing.T) {
want := map[string]string{
mdb.NetworkEthereum: "https://rpc.epusdt.com/ethereum",
mdb.NetworkBsc: "https://rpc.epusdt.com/binance",
mdb.NetworkPolygon: "https://rpc.epusdt.com/polygon",
}
got := make(map[string]mdb.RpcNode)
for _, node := range defaultRpcNodes() {
if node.Purpose != mdb.RpcNodePurposeManualVerify {
continue
}
if _, ok := want[node.Network]; ok {
got[node.Network] = node
}
}
for network, url := range want {
node, ok := got[network]
if !ok {
t.Fatalf("missing manual_verify seed rpc node for %s", network)
}
if node.Url != url {
t.Fatalf("%s manual_verify seed url = %q, want %q", network, node.Url, url)
}
if node.Type != mdb.RpcNodeTypeHttp {
t.Fatalf("%s manual_verify seed type = %q, want %q", network, node.Type, mdb.RpcNodeTypeHttp)
}
if !node.Enabled {
t.Fatalf("%s manual_verify seed enabled = false, want true", network)
}
if node.Status != mdb.RpcNodeStatusUnknown {
t.Fatalf("%s manual_verify seed status = %q, want %q", network, node.Status, mdb.RpcNodeStatusUnknown)
}
}
}
+253 -1
View File
@@ -1,14 +1,44 @@
package data package data
import ( import (
"fmt"
"math/rand" "math/rand"
"net/url"
"sort"
"strings" "strings"
"sync"
"time"
"github.com/GMWalletApp/epusdt/model/dao" "github.com/GMWalletApp/epusdt/model/dao"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/dromara/carbon/v2" "github.com/dromara/carbon/v2"
) )
const (
RpcFailoverThreshold = 3
RpcFailoverCooldown = 60 * time.Second
)
var gRpcFailover = struct {
sync.Mutex
failures map[uint64]int
cooldownUntil map[uint64]time.Time
}{
failures: make(map[uint64]int),
cooldownUntil: make(map[uint64]time.Time),
}
func NormalizeRpcNodePurpose(purpose string) string {
switch strings.ToLower(strings.TrimSpace(purpose)) {
case mdb.RpcNodePurposeManualVerify:
return mdb.RpcNodePurposeManualVerify
case mdb.RpcNodePurposeBoth:
return mdb.RpcNodePurposeBoth
default:
return mdb.RpcNodePurposeGeneral
}
}
// ListRpcNodes returns rows optionally filtered by network. // ListRpcNodes returns rows optionally filtered by network.
func ListRpcNodes(network string) ([]mdb.RpcNode, error) { func ListRpcNodes(network string) ([]mdb.RpcNode, error) {
var rows []mdb.RpcNode var rows []mdb.RpcNode
@@ -20,6 +50,84 @@ func ListRpcNodes(network string) ([]mdb.RpcNode, error) {
return rows, err return rows, err
} }
// ListRpcNodesForHealth returns nodes that should be probed by the periodic
// health job. Manual verification nodes are intentionally excluded because
// they may be paid/high-limit endpoints that should only be used on demand.
func ListRpcNodesForHealth() ([]mdb.RpcNode, error) {
var rows []mdb.RpcNode
err := dao.Mdb.Model(&mdb.RpcNode{}).
Where("(purpose IN ? OR purpose = '' OR purpose IS NULL)", []string{mdb.RpcNodePurposeGeneral, mdb.RpcNodePurposeBoth}).
Order("id ASC").
Find(&rows).Error
return rows, err
}
// ListGeneralRpcCandidates returns enabled non-manual nodes for ordinary
// scanners/listeners. Down nodes are skipped; ok nodes are ordered before
// unknown/bootstrap nodes.
func ListGeneralRpcCandidates(network, nodeType string) ([]mdb.RpcNode, error) {
var rows []mdb.RpcNode
err := dao.Mdb.Model(&mdb.RpcNode{}).
Where("network = ?", strings.ToLower(strings.TrimSpace(network))).
Where("type = ?", strings.ToLower(strings.TrimSpace(nodeType))).
Where("enabled = ?", true).
Where("(purpose IN ? OR purpose = '' OR purpose IS NULL)", []string{mdb.RpcNodePurposeGeneral, mdb.RpcNodePurposeBoth}).
Where("(status IN ? OR status = '' OR status IS NULL)", []string{mdb.RpcNodeStatusOk, mdb.RpcNodeStatusUnknown}).
Find(&rows).Error
if err != nil {
return nil, err
}
buckets := make([][]mdb.RpcNode, 2)
for _, row := range rows {
row.Purpose = NormalizeRpcNodePurpose(row.Purpose)
if row.Status == mdb.RpcNodeStatusOk {
buckets[0] = append(buckets[0], row)
} else {
buckets[1] = append(buckets[1], row)
}
}
out := make([]mdb.RpcNode, 0, len(rows))
for _, bucket := range buckets {
sortRpcNodes(bucket)
out = append(out, bucket...)
}
return out, nil
}
func SelectGeneralRpcNode(network, nodeType string, excludeIDs ...uint64) (*mdb.RpcNode, error) {
rows, err := ListGeneralRpcCandidates(network, nodeType)
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
excluded := make(map[uint64]struct{}, len(excludeIDs))
for _, id := range excludeIDs {
if id > 0 {
excluded[id] = struct{}{}
}
}
for i := range rows {
if _, ok := excluded[rows[i].ID]; ok {
continue
}
if IsRpcNodeCoolingDown(rows[i].ID) {
continue
}
return &rows[i], nil
}
if len(excluded) == 0 {
for i := range rows {
return &rows[i], nil
}
}
return nil, nil
}
// GetRpcNodeByID fetches one row. // GetRpcNodeByID fetches one row.
func GetRpcNodeByID(id uint64) (*mdb.RpcNode, error) { func GetRpcNodeByID(id uint64) (*mdb.RpcNode, error) {
row := new(mdb.RpcNode) row := new(mdb.RpcNode)
@@ -29,6 +137,7 @@ func GetRpcNodeByID(id uint64) (*mdb.RpcNode, error) {
// CreateRpcNode inserts a row. // CreateRpcNode inserts a row.
func CreateRpcNode(row *mdb.RpcNode) error { func CreateRpcNode(row *mdb.RpcNode) error {
row.Purpose = NormalizeRpcNodePurpose(row.Purpose)
return dao.Mdb.Create(row).Error return dao.Mdb.Create(row).Error
} }
@@ -37,6 +146,9 @@ func UpdateRpcNodeFields(id uint64, fields map[string]interface{}) error {
if len(fields) == 0 { if len(fields) == 0 {
return nil return nil
} }
if purpose, ok := fields["purpose"].(string); ok {
fields["purpose"] = NormalizeRpcNodePurpose(purpose)
}
return dao.Mdb.Model(&mdb.RpcNode{}).Where("id = ?", id).Updates(fields).Error return dao.Mdb.Model(&mdb.RpcNode{}).Where("id = ?", id).Updates(fields).Error
} }
@@ -48,13 +160,15 @@ func DeleteRpcNodeByID(id uint64) error {
// SelectRpcNode picks a healthy RPC endpoint for a (network, type) pair. // SelectRpcNode picks a healthy RPC endpoint for a (network, type) pair.
// Strategy: weighted random among rows where enabled=true AND status=ok. // Strategy: weighted random among rows where enabled=true AND status=ok.
// Falls back to enabled rows with status=unknown when no health check has // Falls back to enabled rows with status=unknown when no health check has
// run yet. Explicitly down rows are not selected. // run yet. Explicitly down rows are not selected. Manual verification nodes
// are excluded so paid/manual-only RPCs are never used by scanners/listeners.
func SelectRpcNode(network, nodeType string) (*mdb.RpcNode, error) { func SelectRpcNode(network, nodeType string) (*mdb.RpcNode, error) {
var rows []mdb.RpcNode var rows []mdb.RpcNode
err := dao.Mdb.Model(&mdb.RpcNode{}). err := dao.Mdb.Model(&mdb.RpcNode{}).
Where("network = ?", strings.ToLower(network)). Where("network = ?", strings.ToLower(network)).
Where("type = ?", strings.ToLower(nodeType)). Where("type = ?", strings.ToLower(nodeType)).
Where("enabled = ?", true). Where("enabled = ?", true).
Where("(purpose IN ? OR purpose = '' OR purpose IS NULL)", []string{mdb.RpcNodePurposeGeneral, mdb.RpcNodePurposeBoth}).
Find(&rows).Error Find(&rows).Error
if err != nil { if err != nil {
return nil, err return nil, err
@@ -82,6 +196,144 @@ func SelectRpcNode(network, nodeType string) (*mdb.RpcNode, error) {
return pickWeighted(candidates), nil return pickWeighted(candidates), nil
} }
// ListManualPaymentRpcCandidates returns enabled RPC nodes for manual payment
// verification. Ordinary RPCs are tried first to avoid paid endpoint usage;
// manual_verify nodes are used only as a fallback.
func ListManualPaymentRpcCandidates(network, nodeType string) ([]mdb.RpcNode, error) {
var rows []mdb.RpcNode
err := dao.Mdb.Model(&mdb.RpcNode{}).
Where("network = ?", strings.ToLower(strings.TrimSpace(network))).
Where("type = ?", strings.ToLower(strings.TrimSpace(nodeType))).
Where("enabled = ?", true).
Where("(status IN ? OR status = '' OR status IS NULL)", []string{mdb.RpcNodeStatusOk, mdb.RpcNodeStatusUnknown}).
Find(&rows).Error
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
buckets := make([][]mdb.RpcNode, 4)
for _, row := range rows {
row.Purpose = NormalizeRpcNodePurpose(row.Purpose)
switch row.Purpose {
case mdb.RpcNodePurposeGeneral, mdb.RpcNodePurposeBoth:
if row.Status == mdb.RpcNodeStatusOk {
buckets[0] = append(buckets[0], row)
} else {
buckets[1] = append(buckets[1], row)
}
case mdb.RpcNodePurposeManualVerify:
if row.Status == mdb.RpcNodeStatusOk {
buckets[2] = append(buckets[2], row)
} else {
buckets[3] = append(buckets[3], row)
}
}
}
out := make([]mdb.RpcNode, 0, len(rows))
for _, bucket := range buckets {
sortRpcNodes(bucket)
out = append(out, bucket...)
}
return out, nil
}
func sortRpcNodes(rows []mdb.RpcNode) {
sort.SliceStable(rows, func(i, j int) bool {
if rows[i].Weight != rows[j].Weight {
return rows[i].Weight > rows[j].Weight
}
return rows[i].ID < rows[j].ID
})
}
func RecordRpcNodeSuccess(nodeID uint64) {
if nodeID == 0 {
return
}
gRpcFailover.Lock()
defer gRpcFailover.Unlock()
delete(gRpcFailover.failures, nodeID)
delete(gRpcFailover.cooldownUntil, nodeID)
}
func RecordRpcNodeFailure(nodeID uint64) (int, bool) {
if nodeID == 0 {
return 0, false
}
now := time.Now()
gRpcFailover.Lock()
defer gRpcFailover.Unlock()
if until, ok := gRpcFailover.cooldownUntil[nodeID]; ok {
if now.Before(until) {
return gRpcFailover.failures[nodeID], true
}
delete(gRpcFailover.cooldownUntil, nodeID)
delete(gRpcFailover.failures, nodeID)
}
gRpcFailover.failures[nodeID]++
failures := gRpcFailover.failures[nodeID]
if failures >= RpcFailoverThreshold {
gRpcFailover.failures[nodeID] = 0
gRpcFailover.cooldownUntil[nodeID] = now.Add(RpcFailoverCooldown)
return failures, true
}
return failures, false
}
func IsRpcNodeCoolingDown(nodeID uint64) bool {
if nodeID == 0 {
return false
}
now := time.Now()
gRpcFailover.Lock()
defer gRpcFailover.Unlock()
until, ok := gRpcFailover.cooldownUntil[nodeID]
if !ok {
return false
}
if now.Before(until) {
return true
}
delete(gRpcFailover.cooldownUntil, nodeID)
delete(gRpcFailover.failures, nodeID)
return false
}
func RpcNodeLogLabel(node mdb.RpcNode) string {
return fmt.Sprintf("id=%d network=%s type=%s endpoint=%s", node.ID, node.Network, node.Type, safeRpcEndpoint(node.Url))
}
func safeRpcEndpoint(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
parsed, err := url.Parse(raw)
if err == nil && parsed.Host != "" {
if parsed.Scheme != "" {
return parsed.Scheme + "://" + parsed.Host
}
return parsed.Host
}
cut := strings.IndexAny(raw, "/?")
if cut >= 0 {
return raw[:cut]
}
return raw
}
func ResetRpcFailoverForTest() {
gRpcFailover.Lock()
defer gRpcFailover.Unlock()
gRpcFailover.failures = make(map[uint64]int)
gRpcFailover.cooldownUntil = make(map[uint64]time.Time)
}
// pickWeighted chooses one row using the Weight column. Weights < 1 are // pickWeighted chooses one row using the Weight column. Weights < 1 are
// coerced to 1 so an admin-zeroed row still has a chance (use Enabled=false // coerced to 1 so an admin-zeroed row still has a chance (use Enabled=false
// to truly disable). // to truly disable).
+304
View File
@@ -1,6 +1,7 @@
package data package data
import ( import (
"strings"
"testing" "testing"
"github.com/GMWalletApp/epusdt/internal/testutil" "github.com/GMWalletApp/epusdt/internal/testutil"
@@ -99,3 +100,306 @@ func TestSelectRpcNodeIgnoresDownRows(t *testing.T) {
t.Fatalf("SelectRpcNode() = %#v, want nil", got) t.Fatalf("SelectRpcNode() = %#v, want nil", got)
} }
} }
func TestSelectRpcNodeExcludesManualVerifyRows(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := dao.Mdb.Create(&mdb.RpcNode{
Network: mdb.NetworkTron,
Url: "https://manual.example.com",
Type: mdb.RpcNodeTypeHttp,
Weight: 100,
Enabled: true,
Purpose: mdb.RpcNodePurposeManualVerify,
Status: mdb.RpcNodeStatusOk,
}).Error; err != nil {
t.Fatalf("seed manual rpc_node: %v", err)
}
if err := dao.Mdb.Create(&mdb.RpcNode{
Network: mdb.NetworkTron,
Url: "https://general.example.com",
Type: mdb.RpcNodeTypeHttp,
Weight: 1,
Enabled: true,
Purpose: mdb.RpcNodePurposeGeneral,
Status: mdb.RpcNodeStatusUnknown,
}).Error; err != nil {
t.Fatalf("seed general rpc_node: %v", err)
}
got, err := SelectRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
if err != nil {
t.Fatalf("SelectRpcNode(): %v", err)
}
if got == nil || got.Url != "https://general.example.com" {
t.Fatalf("SelectRpcNode() = %#v, want general row", got)
}
}
func TestSelectRpcNodePurposeFilterAppliesToAnyNodeType(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
const nodeType = "grpc"
if err := dao.Mdb.Create(&mdb.RpcNode{
Network: mdb.NetworkTron,
Url: "grpc://manual.example.com",
Type: nodeType,
Weight: 100,
Enabled: true,
Purpose: mdb.RpcNodePurposeManualVerify,
Status: mdb.RpcNodeStatusOk,
}).Error; err != nil {
t.Fatalf("seed manual rpc_node: %v", err)
}
if err := dao.Mdb.Create(&mdb.RpcNode{
Network: mdb.NetworkTron,
Url: "grpc://general.example.com",
Type: nodeType,
Weight: 1,
Enabled: true,
Purpose: mdb.RpcNodePurposeGeneral,
Status: mdb.RpcNodeStatusOk,
}).Error; err != nil {
t.Fatalf("seed general rpc_node: %v", err)
}
got, err := SelectRpcNode(mdb.NetworkTron, nodeType)
if err != nil {
t.Fatalf("SelectRpcNode(): %v", err)
}
if got == nil || got.Url != "grpc://general.example.com" {
t.Fatalf("SelectRpcNode() = %#v, want general grpc row", got)
}
}
func TestSelectRpcNodeManualVerifyOnlyReturnsNil(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := dao.Mdb.Create(&mdb.RpcNode{
Network: mdb.NetworkTron,
Url: "https://manual-only.example.com",
Type: mdb.RpcNodeTypeHttp,
Weight: 1,
Enabled: true,
Purpose: mdb.RpcNodePurposeManualVerify,
Status: mdb.RpcNodeStatusOk,
}).Error; err != nil {
t.Fatalf("seed manual rpc_node: %v", err)
}
got, err := SelectRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
if err != nil {
t.Fatalf("SelectRpcNode(): %v", err)
}
if got != nil {
t.Fatalf("SelectRpcNode() = %#v, want nil", got)
}
}
func TestListManualPaymentRpcCandidatesGeneralBeforeManualVerify(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
rows := []mdb.RpcNode{
{Network: mdb.NetworkTron, Url: "https://manual-ok.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkTron, Url: "https://general-unknown.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkTron, Url: "https://general-ok.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkTron, Url: "https://manual-unknown.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkTron, Url: "https://down.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusDown},
}
if err := dao.Mdb.Create(&rows).Error; err != nil {
t.Fatalf("seed rpc_nodes: %v", err)
}
got, err := ListManualPaymentRpcCandidates(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
if err != nil {
t.Fatalf("ListManualPaymentRpcCandidates(): %v", err)
}
var urls []string
for _, row := range got {
urls = append(urls, row.Url)
}
want := []string{
"https://general-ok.example.com",
"https://general-unknown.example.com",
"https://manual-ok.example.com",
"https://manual-unknown.example.com",
}
if len(urls) != len(want) {
t.Fatalf("candidate urls = %#v, want %#v", urls, want)
}
for i := range want {
if urls[i] != want[i] {
t.Fatalf("candidate urls = %#v, want %#v", urls, want)
}
}
}
func TestListManualPaymentRpcCandidatesAllowsManualVerifyFallbackOnly(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
rows := []mdb.RpcNode{
{Network: mdb.NetworkTron, Url: "https://manual-ok.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkTron, Url: "https://manual-down.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusDown},
}
if err := dao.Mdb.Create(&rows).Error; err != nil {
t.Fatalf("seed rpc_nodes: %v", err)
}
got, err := ListManualPaymentRpcCandidates(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
if err != nil {
t.Fatalf("ListManualPaymentRpcCandidates(): %v", err)
}
if len(got) != 1 || got[0].Url != "https://manual-ok.example.com" {
t.Fatalf("candidate rows = %#v, want only manual ok fallback", got)
}
}
func TestSelectGeneralRpcNodeSkipsCoolingNode(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
ResetRpcFailoverForTest()
t.Cleanup(ResetRpcFailoverForTest)
rows := []mdb.RpcNode{
{Network: mdb.NetworkTron, Url: "https://primary.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkTron, Url: "https://backup.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
}
if err := dao.Mdb.Create(&rows).Error; err != nil {
t.Fatalf("seed rpc_nodes: %v", err)
}
first, err := SelectGeneralRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
if err != nil {
t.Fatalf("SelectGeneralRpcNode(): %v", err)
}
if first == nil || first.Url != "https://primary.example.com" {
t.Fatalf("first node = %#v, want primary", first)
}
for i := 0; i < RpcFailoverThreshold; i++ {
RecordRpcNodeFailure(first.ID)
}
got, err := SelectGeneralRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
if err != nil {
t.Fatalf("SelectGeneralRpcNode(): %v", err)
}
if got == nil || got.Url != "https://backup.example.com" {
t.Fatalf("node after cooldown = %#v, want backup", got)
}
}
func TestSelectGeneralRpcNodeFallsBackWhenAllNodesCooling(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
ResetRpcFailoverForTest()
t.Cleanup(ResetRpcFailoverForTest)
rows := []mdb.RpcNode{
{Network: mdb.NetworkTron, Url: "https://primary.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkTron, Url: "https://backup.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
}
if err := dao.Mdb.Create(&rows).Error; err != nil {
t.Fatalf("seed rpc_nodes: %v", err)
}
for i := range rows {
for n := 0; n < RpcFailoverThreshold; n++ {
RecordRpcNodeFailure(rows[i].ID)
}
}
got, err := SelectGeneralRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
if err != nil {
t.Fatalf("SelectGeneralRpcNode(): %v", err)
}
if got == nil || got.Url != "https://primary.example.com" {
t.Fatalf("SelectGeneralRpcNode() = %#v, want primary when every node is cooling", got)
}
}
func TestSelectGeneralRpcNodeReturnsNilWhenExcludedAlternatesCooling(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
ResetRpcFailoverForTest()
t.Cleanup(ResetRpcFailoverForTest)
rows := []mdb.RpcNode{
{Network: mdb.NetworkTron, Url: "https://primary.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkTron, Url: "https://backup.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
}
if err := dao.Mdb.Create(&rows).Error; err != nil {
t.Fatalf("seed rpc_nodes: %v", err)
}
for i := 0; i < RpcFailoverThreshold; i++ {
RecordRpcNodeFailure(rows[1].ID)
}
got, err := SelectGeneralRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp, rows[0].ID)
if err != nil {
t.Fatalf("SelectGeneralRpcNode(): %v", err)
}
if got != nil {
t.Fatalf("SelectGeneralRpcNode() = %#v, want nil when only alternate is cooling", got)
}
}
func TestRecordRpcNodeSuccessClearsCooldown(t *testing.T) {
ResetRpcFailoverForTest()
t.Cleanup(ResetRpcFailoverForTest)
const nodeID uint64 = 42
for i := 0; i < RpcFailoverThreshold; i++ {
RecordRpcNodeFailure(nodeID)
}
if !IsRpcNodeCoolingDown(nodeID) {
t.Fatal("node should be cooling after threshold")
}
RecordRpcNodeSuccess(nodeID)
if IsRpcNodeCoolingDown(nodeID) {
t.Fatal("node should not be cooling after success")
}
failures, cooling := RecordRpcNodeFailure(nodeID)
if cooling || failures != 1 {
t.Fatalf("failure state after success = failures=%d cooling=%v, want failures=1 cooling=false", failures, cooling)
}
}
func TestRpcNodeLogLabelHidesPathAndQuery(t *testing.T) {
label := RpcNodeLogLabel(mdb.RpcNode{
BaseModel: mdb.BaseModel{ID: 7},
Network: mdb.NetworkEthereum,
Type: mdb.RpcNodeTypeWs,
Url: "wss://rpc.example.com/v3/secret-key?token=secret",
})
if strings.Contains(label, "secret") || strings.Contains(label, "/v3/") {
t.Fatalf("RpcNodeLogLabel() leaked sensitive URL parts: %s", label)
}
if !strings.Contains(label, "wss://rpc.example.com") {
t.Fatalf("RpcNodeLogLabel() = %s, want host", label)
}
}
func TestListRpcNodesForHealthExcludesManualVerify(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
rows := []mdb.RpcNode{
{Network: mdb.NetworkTron, Url: "https://general.example.com", Type: mdb.RpcNodeTypeHttp, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkTron, Url: "https://manual.example.com", Type: mdb.RpcNodeTypeHttp, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusUnknown},
}
if err := dao.Mdb.Create(&rows).Error; err != nil {
t.Fatalf("seed rpc_nodes: %v", err)
}
got, err := ListRpcNodesForHealth()
if err != nil {
t.Fatalf("ListRpcNodesForHealth(): %v", err)
}
if len(got) != 1 || got[0].Url != "https://general.example.com" {
t.Fatalf("ListRpcNodesForHealth() = %#v, want only general row", got)
}
}
+17 -10
View File
@@ -13,19 +13,26 @@ const (
RpcNodeStatusDown = "down" RpcNodeStatusDown = "down"
) )
// RpcNode records one RPC endpoint for a network. Clients select a node const (
// by weight among rows where Enabled=true AND Status=ok. A periodic RpcNodePurposeGeneral = "general"
// task/rpc_health_job writes Status/LastLatencyMs/LastCheckedAt. RpcNodePurposeManualVerify = "manual_verify"
RpcNodePurposeBoth = "both"
)
// RpcNode records one RPC endpoint for a network. Runtime selection filters by
// Type, Status and Purpose so paid manual-verification nodes stay isolated.
type RpcNode struct { type RpcNode struct {
Network string `gorm:"column:network;size:32;index:rpc_nodes_network_status_index,priority:1" json:"network" example:"tron"` Network string `gorm:"column:network;size:32;index:rpc_nodes_network_status_index,priority:1" json:"network" example:"tron"`
Url string `gorm:"column:url;size:512" json:"url" example:"https://api.trongrid.io"` Url string `gorm:"column:url;size:512" json:"url" example:"https://api.trongrid.io"`
// 连接类型 http=HTTP请求 ws=WebSocket长连接 // 连接类型 http=HTTP请求 ws=WebSocket长连接
Type string `gorm:"column:type;size:16" json:"type" enums:"http,ws" example:"http"` Type string `gorm:"column:type;size:16" json:"type" enums:"http,ws" example:"http"`
Weight int `gorm:"column:weight;default:1" json:"weight" example:"1"` Weight int `gorm:"column:weight;default:1" json:"weight" example:"1"`
ApiKey string `gorm:"column:api_key;size:255" json:"api_key" example:"your-api-key"` ApiKey string `gorm:"column:api_key;size:255" json:"api_key" example:"your-api-key"`
Enabled bool `gorm:"column:enabled;default:true" json:"enabled" example:"true"` Enabled bool `gorm:"column:enabled;default:true" json:"enabled" example:"true"`
// 用途 general=通用 manual_verify=补单专用 both=通用+补单
Purpose string `gorm:"column:purpose;size:32;default:general;index:rpc_nodes_purpose_index" json:"purpose" enums:"general,manual_verify,both" example:"general"`
// 健康状态 unknown=未知 ok=正常 down=异常 // 健康状态 unknown=未知 ok=正常 down=异常
Status string `gorm:"column:status;size:16;default:unknown;index:rpc_nodes_network_status_index,priority:2" json:"status" enums:"unknown,ok,down" example:"ok"` Status string `gorm:"column:status;size:16;default:unknown;index:rpc_nodes_network_status_index,priority:2" json:"status" enums:"unknown,ok,down" example:"ok"`
LastLatencyMs int `gorm:"column:last_latency_ms;default:-1" json:"last_latency_ms" example:"120"` LastLatencyMs int `gorm:"column:last_latency_ms;default:-1" json:"last_latency_ms" example:"120"`
LastCheckedAt carbon.Time `gorm:"column:last_checked_at" json:"last_checked_at" example:"2026-04-16 12:00:00"` LastCheckedAt carbon.Time `gorm:"column:last_checked_at" json:"last_checked_at" example:"2026-04-16 12:00:00"`
BaseModel BaseModel
+7 -4
View File
@@ -4,8 +4,11 @@ package response
// Used for swagger documentation only — the actual response is built // Used for swagger documentation only — the actual response is built
// by util/http.Resp. // by util/http.Resp.
type ApiResponse struct { type ApiResponse struct {
StatusCode int `json:"status_code" example:"200"` // Stable errno used by the frontend for i18n. Success is 200; failures use codes from util/constant.Errno.
Message string `json:"message" example:"success"` StatusCode int `json:"status_code" example:"200"`
Data interface{} `json:"data"` // Default server-side errno text. Frontend should prefer status_code for display translation.
RequestID string `json:"request_id" example:"req-123456"` Message string `json:"message" example:"success"`
Data interface{} `json:"data"`
// Request id echoed from X-Request-ID when present.
RequestID string `json:"request_id" example:"req-123456"`
} }
+165 -62
View File
@@ -54,7 +54,9 @@ func SetManualOrderPaymentValidatorForTest(fn func(*mdb.Orders, string) (string,
if fn == nil { if fn == nil {
manualOrderPaymentValidator = validateManualOrderPaymentDefault manualOrderPaymentValidator = validateManualOrderPaymentDefault
} else { } else {
manualOrderPaymentValidator = fn manualOrderPaymentValidator = func(order *mdb.Orders, blockTransactionID string) (string, error) {
return fn(order, blockTransactionID)
}
} }
manualOrderPaymentValidatorMu.Unlock() manualOrderPaymentValidatorMu.Unlock()
return func() { return func() {
@@ -282,25 +284,25 @@ func validateManualEvmPaymentWithClient(ctx context.Context, client evmChainRead
func dialManualEvmClients(ctx context.Context, network string) ([]manualEvmClient, error) { func dialManualEvmClients(ctx context.Context, network string) ([]manualEvmClient, error) {
var clients []manualEvmClient var clients []manualEvmClient
var connectErrors []string var connectErrors []string
for _, nodeType := range []string{mdb.RpcNodeTypeWs, mdb.RpcNodeTypeHttp} {
node, err := data.SelectRpcNode(network, nodeType) nodes, err := listManualEvmRpcCandidates(network)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if node == nil || node.ID == 0 || strings.TrimSpace(node.Url) == "" { for _, node := range nodes {
if strings.TrimSpace(node.Url) == "" {
continue continue
} }
rpcURL := strings.TrimSpace(node.Url) client, err := dialManualEvmClient(ctx, node)
client, err := ethclient.DialContext(ctx, rpcURL)
if err == nil { if err == nil {
clients = append(clients, manualEvmClient{ clients = append(clients, manualEvmClient{
label: fmt.Sprintf("%s %s", nodeType, rpcURL), label: manualRpcNodeLabel(node),
reader: client, reader: client,
close: client.Close, close: client.Close,
}) })
continue continue
} }
connectErrors = append(connectErrors, fmt.Sprintf("%s %s: %v", nodeType, rpcURL, err)) connectErrors = append(connectErrors, fmt.Sprintf("%s: %v", manualRpcNodeLabel(node), err))
} }
if len(clients) > 0 { if len(clients) > 0 {
return clients, nil return clients, nil
@@ -311,6 +313,49 @@ func dialManualEvmClients(ctx context.Context, network string) ([]manualEvmClien
return nil, fmt.Errorf("no enabled %s WS/HTTP RPC node configured", network) return nil, fmt.Errorf("no enabled %s WS/HTTP RPC node configured", network)
} }
func listManualEvmRpcCandidates(network string) ([]mdb.RpcNode, error) {
buckets := make([][]mdb.RpcNode, 4)
for _, nodeType := range []string{mdb.RpcNodeTypeHttp, mdb.RpcNodeTypeWs} {
nodes, err := data.ListManualPaymentRpcCandidates(network, nodeType)
if err != nil {
return nil, err
}
for _, node := range nodes {
node.Purpose = data.NormalizeRpcNodePurpose(node.Purpose)
switch node.Purpose {
case mdb.RpcNodePurposeGeneral, mdb.RpcNodePurposeBoth:
if node.Status == mdb.RpcNodeStatusOk {
buckets[0] = append(buckets[0], node)
} else {
buckets[1] = append(buckets[1], node)
}
case mdb.RpcNodePurposeManualVerify:
if node.Status == mdb.RpcNodeStatusOk {
buckets[2] = append(buckets[2], node)
} else {
buckets[3] = append(buckets[3], node)
}
}
}
}
out := make([]mdb.RpcNode, 0)
for _, bucket := range buckets {
out = append(out, bucket...)
}
return out, nil
}
func dialManualEvmClient(ctx context.Context, node mdb.RpcNode) (*ethclient.Client, error) {
rpcURL := strings.TrimSpace(node.Url)
return ethclient.DialContext(ctx, rpcURL)
}
func manualRpcNodeLabel(node mdb.RpcNode) string {
purpose := data.NormalizeRpcNodePurpose(node.Purpose)
return fmt.Sprintf("%s purpose=%s", data.RpcNodeLogLabel(node), purpose)
}
func ensureEvmTransactionNotBeforeOrder(blockTime uint64, order *mdb.Orders) error { func ensureEvmTransactionNotBeforeOrder(blockTime uint64, order *mdb.Orders) error {
if order == nil { if order == nil {
return fmt.Errorf("order not found") return fmt.Errorf("order not found")
@@ -398,55 +443,77 @@ func validateManualTronPayment(order *mdb.Orders, txID string) (string, error) {
return "", err return "", err
} }
baseURL, apiKey, err := ResolveTronNode() nodes, err := data.ListManualPaymentRpcCandidates(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
if err != nil { if err != nil {
return "", err return "", err
} }
if len(nodes) == 0 {
var tx manualTronTransaction return "", fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkTron, mdb.RpcNodeTypeHttp)
if err = tronPostJSON(baseURL, apiKey, "/wallet/gettransactionbyid", map[string]interface{}{"value": normalizedTxID}, &tx); err != nil {
return "", fmt.Errorf("fetch tron transaction: %w", err)
}
if strings.TrimSpace(tx.TxID) == "" {
return "", fmt.Errorf("transaction not found")
}
if strings.TrimSpace(tx.TxID) != "" && !strings.EqualFold(strings.TrimSpace(tx.TxID), normalizedTxID) {
return "", fmt.Errorf("transaction id mismatch")
}
if len(tx.Ret) > 0 && tx.Ret[0].ContractRet != "" && tx.Ret[0].ContractRet != "SUCCESS" {
return "", fmt.Errorf("transaction is not successful: %s", tx.Ret[0].ContractRet)
} }
var info manualTronTxInfo var verifyErrors []string
if err = tronPostJSON(baseURL, apiKey, "/wallet/gettransactioninfobyid", map[string]interface{}{"value": normalizedTxID}, &info); err != nil { for _, node := range nodes {
return "", fmt.Errorf("fetch tron transaction info: %w", err) if strings.TrimSpace(node.Url) == "" {
} continue
if info.BlockNumber <= 0 { }
return "", fmt.Errorf("transaction is not confirmed") if err = validateManualTronPaymentWithNode(order, normalizedTxID, node); err != nil {
} verifyErrors = append(verifyErrors, fmt.Sprintf("%s: %v", manualRpcNodeLabel(node), err))
if info.Receipt.Result != "" && info.Receipt.Result != "SUCCESS" { continue
return "", fmt.Errorf("transaction is not successful: %s", info.Receipt.Result)
}
if err = ensureTronConfirmations(baseURL, apiKey, order.Network, info.BlockNumber); err != nil {
return "", err
}
if info.BlockTimeStamp <= 0 {
return "", fmt.Errorf("transaction block timestamp missing")
}
if info.BlockTimeStamp < order.CreatedAt.TimestampMilli() {
return "", fmt.Errorf("transaction predates the order")
}
if strings.EqualFold(strings.TrimSpace(order.Token), "TRX") {
if err = validateManualTronNativeTransfer(order, &tx); err != nil {
return "", err
} }
return normalizedTxID, nil return normalizedTxID, nil
} }
if err = validateManualTronTRC20Transfer(order, &tx, &info); err != nil { if len(verifyErrors) > 0 {
return "", err return "", fmt.Errorf("manual TRON verification failed: %s", strings.Join(verifyErrors, "; "))
} }
return normalizedTxID, nil return "", fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkTron, mdb.RpcNodeTypeHttp)
}
func validateManualTronPaymentWithNode(order *mdb.Orders, normalizedTxID string, node mdb.RpcNode) error {
baseURL := strings.TrimSpace(node.Url)
var tx manualTronTransaction
if err := tronPostJSON(baseURL, node.ApiKey, "/wallet/gettransactionbyid", map[string]interface{}{"value": normalizedTxID}, &tx); err != nil {
return fmt.Errorf("fetch tron transaction: %w", err)
}
if strings.TrimSpace(tx.TxID) == "" {
return fmt.Errorf("transaction not found")
}
if strings.TrimSpace(tx.TxID) != "" && !strings.EqualFold(strings.TrimSpace(tx.TxID), normalizedTxID) {
return fmt.Errorf("transaction id mismatch")
}
if len(tx.Ret) > 0 && tx.Ret[0].ContractRet != "" && tx.Ret[0].ContractRet != "SUCCESS" {
return fmt.Errorf("transaction is not successful: %s", tx.Ret[0].ContractRet)
}
var info manualTronTxInfo
if err := tronPostJSON(baseURL, node.ApiKey, "/wallet/gettransactioninfobyid", map[string]interface{}{"value": normalizedTxID}, &info); err != nil {
return fmt.Errorf("fetch tron transaction info: %w", err)
}
if info.BlockNumber <= 0 {
return fmt.Errorf("transaction is not confirmed")
}
if info.Receipt.Result != "" && info.Receipt.Result != "SUCCESS" {
return fmt.Errorf("transaction is not successful: %s", info.Receipt.Result)
}
if err := ensureTronConfirmations(baseURL, node.ApiKey, order.Network, info.BlockNumber); err != nil {
return err
}
if info.BlockTimeStamp <= 0 {
return fmt.Errorf("transaction block timestamp missing")
}
if info.BlockTimeStamp < order.CreatedAt.TimestampMilli() {
return fmt.Errorf("transaction predates the order")
}
if strings.EqualFold(strings.TrimSpace(order.Token), "TRX") {
if err := validateManualTronNativeTransfer(order, &tx); err != nil {
return err
}
return nil
}
if err := validateManualTronTRC20Transfer(order, &tx, &info); err != nil {
return err
}
return nil
} }
func validateManualTronNativeTransfer(order *mdb.Orders, tx *manualTronTransaction) error { func validateManualTronNativeTransfer(order *mdb.Orders, tx *manualTronTransaction) error {
@@ -571,20 +638,48 @@ func tronPostJSON(baseURL, apiKey, path string, body interface{}, out interface{
func validateManualSolanaPayment(order *mdb.Orders, sig string) (string, error) { func validateManualSolanaPayment(order *mdb.Orders, sig string) (string, error) {
sig = strings.TrimSpace(sig) sig = strings.TrimSpace(sig)
txData, err := SolGetTransaction(sig) nodes, err := data.ListManualPaymentRpcCandidates(mdb.NetworkSolana, mdb.RpcNodeTypeHttp)
if err != nil { if err != nil {
return "", fmt.Errorf("fetch solana transaction: %w", err) return "", err
}
if len(nodes) == 0 {
return "", fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkSolana, mdb.RpcNodeTypeHttp)
}
var verifyErrors []string
for _, node := range nodes {
rpcURL := strings.TrimSpace(node.Url)
if rpcURL == "" {
continue
}
if err = validateManualSolanaPaymentWithRPC(order, sig, node); err != nil {
verifyErrors = append(verifyErrors, fmt.Sprintf("%s: %v", manualRpcNodeLabel(node), err))
continue
}
return sig, nil
}
if len(verifyErrors) > 0 {
return "", fmt.Errorf("manual Solana verification failed: %s", strings.Join(verifyErrors, "; "))
}
return "", fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkSolana, mdb.RpcNodeTypeHttp)
}
func validateManualSolanaPaymentWithRPC(order *mdb.Orders, sig string, node mdb.RpcNode) error {
rpcURL := strings.TrimSpace(node.Url)
txData, err := solGetTransactionWithURL(rpcURL, sig)
if err != nil {
return fmt.Errorf("fetch solana transaction: %w", err)
} }
if !gjson.GetBytes(txData, "result").Exists() || gjson.GetBytes(txData, "result").Type == gjson.Null { if !gjson.GetBytes(txData, "result").Exists() || gjson.GetBytes(txData, "result").Type == gjson.Null {
return "", fmt.Errorf("transaction not found") return fmt.Errorf("transaction not found")
} }
if err = ensureSolanaConfirmations(order.Network, sig); err != nil { if err = ensureSolanaConfirmationsWithRPC(order.Network, sig, rpcURL); err != nil {
return "", err return err
} }
tokens, err := data.ListEnabledChainTokensByNetwork(mdb.NetworkSolana) tokens, err := data.ListEnabledChainTokensByNetwork(mdb.NetworkSolana)
if err != nil { if err != nil {
return "", err return err
} }
mintTokens := make(map[string]*mdb.ChainToken, len(tokens)) mintTokens := make(map[string]*mdb.ChainToken, len(tokens))
var nativeSolToken *mdb.ChainToken var nativeSolToken *mdb.ChainToken
@@ -615,17 +710,17 @@ func validateManualSolanaPayment(order *mdb.Orders, sig string) (string, error)
continue continue
} }
if err = ensureSolanaTransferNotBeforeOrder(transferInfo.BlockTime, order); err != nil { if err = ensureSolanaTransferNotBeforeOrder(transferInfo.BlockTime, order); err != nil {
return "", err return err
} }
if amountMatchesFloat(order.ActualAmount, amount) { if amountMatchesFloat(order.ActualAmount, amount) {
return sig, nil return nil
} }
amountMismatch = true amountMismatch = true
} }
if amountMismatch { if amountMismatch {
return "", fmt.Errorf("transaction amount mismatch") return fmt.Errorf("transaction amount mismatch")
} }
return "", fmt.Errorf("matching solana transfer to order address not found") return fmt.Errorf("matching solana transfer to order address not found")
} }
func ensureSolanaTransferNotBeforeOrder(blockTime int64, order *mdb.Orders) error { func ensureSolanaTransferNotBeforeOrder(blockTime int64, order *mdb.Orders) error {
@@ -642,6 +737,14 @@ func ensureSolanaTransferNotBeforeOrder(blockTime int64, order *mdb.Orders) erro
} }
func ensureSolanaConfirmations(network, sig string) error { func ensureSolanaConfirmations(network, sig string) error {
rpcURL, err := resolveSolanaRpcURL()
if err != nil {
return err
}
return ensureSolanaConfirmationsWithRPC(network, sig, rpcURL)
}
func ensureSolanaConfirmationsWithRPC(network, sig string, rpcURL string) error {
chain, err := data.GetChainByNetwork(network) chain, err := data.GetChainByNetwork(network)
if err != nil { if err != nil {
return err return err
@@ -654,7 +757,7 @@ func ensureSolanaConfirmations(network, sig string) error {
return nil return nil
} }
body, err := SolRetryClient("getSignatureStatuses", []interface{}{ body, err := solRetryClientWithURL(rpcURL, "getSignatureStatuses", []interface{}{
[]string{sig}, []string{sig},
map[string]interface{}{"searchTransactionHistory": true}, map[string]interface{}{"searchTransactionHistory": true},
}) })
@@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"sync/atomic"
"testing" "testing"
"time" "time"
@@ -117,6 +118,154 @@ func TestManualVerifyDialEvmClientsIncludesHTTPNode(t *testing.T) {
} }
} }
func TestManualVerifyEvmCandidatesPreferGeneralAcrossNodeTypes(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
rows := []mdb.RpcNode{
{Network: mdb.NetworkEthereum, Type: mdb.RpcNodeTypeHttp, Url: "http://manual.example.com", Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkEthereum, Type: mdb.RpcNodeTypeWs, Url: "ws://general.example.com", Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
}
if err := dao.Mdb.Create(&rows).Error; err != nil {
t.Fatalf("seed rpc_nodes: %v", err)
}
got, err := listManualEvmRpcCandidates(mdb.NetworkEthereum)
if err != nil {
t.Fatalf("listManualEvmRpcCandidates(): %v", err)
}
urls := make([]string, 0, len(got))
for _, node := range got {
urls = append(urls, node.Url)
}
want := []string{"ws://general.example.com", "http://manual.example.com"}
if len(urls) != len(want) {
t.Fatalf("candidate urls = %#v, want %#v", urls, want)
}
for i := range want {
if urls[i] != want[i] {
t.Fatalf("candidate urls = %#v, want %#v", urls, want)
}
}
}
func TestManualRpcNodeLabelHidesSensitiveURLParts(t *testing.T) {
label := manualRpcNodeLabel(mdb.RpcNode{
BaseModel: mdb.BaseModel{ID: 9},
Network: mdb.NetworkEthereum,
Type: mdb.RpcNodeTypeHttp,
Url: "https://rpc.example.com/v3/secret-key?token=secret",
Purpose: mdb.RpcNodePurposeManualVerify,
})
if strings.Contains(label, "secret") || strings.Contains(label, "/v3/") {
t.Fatalf("manualRpcNodeLabel() leaked sensitive URL parts: %s", label)
}
if !strings.Contains(label, "endpoint=https://rpc.example.com") {
t.Fatalf("manualRpcNodeLabel() = %s, want sanitized endpoint", label)
}
if !strings.Contains(label, "purpose=manual_verify") {
t.Fatalf("manualRpcNodeLabel() = %s, want purpose", label)
}
}
func TestManualVerifyDialEvmClientDoesNotForwardUserIPHeader(t *testing.T) {
var headerCalls int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("X-USER-IP"); got != "" {
t.Errorf("X-USER-IP header = %q, want empty", got)
http.Error(w, "unexpected user ip header", http.StatusBadRequest)
return
}
atomic.AddInt32(&headerCalls, 1)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"error": map[string]interface{}{
"code": -32000,
"message": "not found",
},
})
}))
defer server.Close()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
client, err := dialManualEvmClient(ctx, mdb.RpcNode{
Network: mdb.NetworkEthereum,
Type: mdb.RpcNodeTypeHttp,
Url: server.URL,
Purpose: mdb.RpcNodePurposeManualVerify,
})
if err != nil {
t.Fatalf("dialManualEvmClient(): %v", err)
}
defer client.Close()
_, _ = client.TransactionReceipt(ctx, common.HexToHash("0x"+strings.Repeat("a", 64)))
if got := atomic.LoadInt32(&headerCalls); got == 0 {
t.Fatal("manual verify EVM request did not reach test RPC")
}
}
func TestManualVerifyDialEvmClientDoesNotForwardUserIPForGeneralNode(t *testing.T) {
var calls int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("X-USER-IP"); got != "" {
t.Errorf("X-USER-IP header = %q, want empty", got)
http.Error(w, "unexpected user ip header", http.StatusBadRequest)
return
}
atomic.AddInt32(&calls, 1)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"error": map[string]interface{}{
"code": -32000,
"message": "not found",
},
})
}))
defer server.Close()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
client, err := dialManualEvmClient(ctx, mdb.RpcNode{
Network: mdb.NetworkEthereum,
Type: mdb.RpcNodeTypeHttp,
Url: server.URL,
Purpose: mdb.RpcNodePurposeGeneral,
})
if err != nil {
t.Fatalf("dialManualEvmClient(): %v", err)
}
defer client.Close()
_, _ = client.TransactionReceipt(ctx, common.HexToHash("0x"+strings.Repeat("a", 64)))
if got := atomic.LoadInt32(&calls); got == 0 {
t.Fatal("general EVM request did not reach test RPC")
}
}
func TestTronPostJSONDoesNotForwardUserIPWithoutHeaders(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("X-USER-IP"); got != "" {
t.Errorf("X-USER-IP header = %q, want empty", got)
http.Error(w, "unexpected user ip header", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"ok":true}`))
}))
defer server.Close()
var out map[string]interface{}
if err := tronPostJSON(server.URL, "", "/wallet/getnowblock", map[string]interface{}{}, &out); err != nil {
t.Fatalf("tronPostJSON(): %v", err)
}
}
func TestManualVerifyEvmRejectsTransactionBeforeOrder(t *testing.T) { func TestManualVerifyEvmRejectsTransactionBeforeOrder(t *testing.T) {
order := &mdb.Orders{BaseModel: mdb.BaseModel{CreatedAt: *carbon.NewTime(carbon.CreateFromTimestampMilli(time.Now().UnixMilli()))}} order := &mdb.Orders{BaseModel: mdb.BaseModel{CreatedAt: *carbon.NewTime(carbon.CreateFromTimestampMilli(time.Now().UnixMilli()))}}
txTime := uint64(time.Now().Add(-time.Hour).Unix()) txTime := uint64(time.Now().Add(-time.Hour).Unix())
@@ -271,7 +420,12 @@ func TestManualVerifyTronPaymentHTTPFlow(t *testing.T) {
rawAmount := big.NewInt(1230000) rawAmount := big.NewInt(1230000)
blockTimeMs := time.Now().Add(time.Minute).UnixMilli() blockTimeMs := time.Now().Add(time.Minute).UnixMilli()
requestCalls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("X-USER-IP"); got != "" {
t.Errorf("X-USER-IP header = %q, want empty", got)
}
requestCalls++
var req map[string]string var req map[string]string
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Errorf("decode tron request: %v", err) t.Errorf("decode tron request: %v", err)
@@ -333,6 +487,7 @@ func TestManualVerifyTronPaymentHTTPFlow(t *testing.T) {
Url: server.URL, Url: server.URL,
Enabled: true, Enabled: true,
Status: mdb.RpcNodeStatusOk, Status: mdb.RpcNodeStatusOk,
Purpose: mdb.RpcNodePurposeManualVerify,
}).Error; err != nil { }).Error; err != nil {
t.Fatalf("create tron rpc node: %v", err) t.Fatalf("create tron rpc node: %v", err)
} }
@@ -366,6 +521,9 @@ func TestManualVerifyTronPaymentHTTPFlow(t *testing.T) {
if got != txID { if got != txID {
t.Fatalf("canonical tx id = %q, want %q", got, txID) t.Fatalf("canonical tx id = %q, want %q", got, txID)
} }
if requestCalls != 3 {
t.Fatalf("manual verify request calls = %d, want 3", requestCalls)
}
} }
func TestManualVerifySolanaRejectsMissingBlockTime(t *testing.T) { func TestManualVerifySolanaRejectsMissingBlockTime(t *testing.T) {
+44 -7
View File
@@ -60,7 +60,7 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
network := strings.ToLower(strings.TrimSpace(req.Network)) network := strings.ToLower(strings.TrimSpace(req.Network))
notifyURL := strings.TrimSpace(req.NotifyUrl) notifyURL := strings.TrimSpace(req.NotifyUrl)
if err := security.ValidatePublicHTTPURL(notifyURL); err != nil { if err := security.ValidatePublicHTTPURL(notifyURL); err != nil {
return nil, err return nil, constant.NotifyURLErr
} }
gCreateTransactionLock.Lock() gCreateTransactionLock.Lock()
@@ -351,16 +351,43 @@ func SubmitManualPayment(tradeId, blockTransactionId string) (*response.ManualPa
if err != nil { if err != nil {
return nil, err return nil, err
} }
if order.Status != mdb.StatusWaitPay { return submitManualPaymentForOrder(order, blockTransactionId)
return nil, constant.OrderNotWaitPay }
// SubmitCashierManualPayment is the public cashier variant. It rejects hashes
// already stored on any order before touching RPC so repeated/public probes do
// not spend RPC quota. Admin mark-paid intentionally keeps using
// SubmitManualPayment and the existing verification path.
func SubmitCashierManualPayment(tradeId, blockTransactionId string) (*response.ManualPaymentResponse, error) {
tradeId = strings.TrimSpace(tradeId)
blockTransactionId = strings.TrimSpace(blockTransactionId)
order, err := GetOrderInfoByTradeId(tradeId)
if err != nil {
return nil, err
} }
if !isOnChainOrder(order.PayProvider) { if err = validateManualPaymentOrder(order); err != nil {
return nil, errors.New("order is not an on-chain payment order") return nil, err
}
if err = ensureManualBlockTransactionUnused(order, blockTransactionId); err != nil {
return nil, err
}
return submitManualPaymentForOrder(order, blockTransactionId)
}
func submitManualPaymentForOrder(order *mdb.Orders, blockTransactionId string) (*response.ManualPaymentResponse, error) {
blockTransactionId = strings.TrimSpace(blockTransactionId)
if err := validateManualPaymentOrder(order); err != nil {
return nil, err
} }
verifiedBlockTransactionID, err := ValidateManualOrderPayment(order, blockTransactionId) verifiedBlockTransactionID, err := ValidateManualOrderPayment(order, blockTransactionId)
if err != nil { if err != nil {
return nil, err var rspErr *constant.RspError
if errors.As(err, &rspErr) {
return nil, err
}
return nil, constant.ManualPaymentVerifyErr
} }
if err = OrderProcessing(&request.OrderProcessingRequest{ if err = OrderProcessing(&request.OrderProcessingRequest{
ReceiveAddress: order.ReceiveAddress, ReceiveAddress: order.ReceiveAddress,
@@ -385,6 +412,16 @@ func SubmitManualPayment(tradeId, blockTransactionId string) (*response.ManualPa
}, nil }, nil
} }
func validateManualPaymentOrder(order *mdb.Orders) error {
if order.Status != mdb.StatusWaitPay {
return constant.OrderNotWaitPay
}
if !isOnChainOrder(order.PayProvider) {
return constant.ManualPaymentProviderErr
}
return nil
}
func isOnChainOrder(payProvider string) bool { func isOnChainOrder(payProvider string) bool {
payProvider = strings.TrimSpace(payProvider) payProvider = strings.TrimSpace(payProvider)
return payProvider == "" || payProvider == mdb.PaymentProviderOnChain return payProvider == "" || payProvider == mdb.PaymentProviderOnChain
@@ -645,7 +682,7 @@ func switchToOkPay(parent *mdb.Orders, token string) (*response.CheckoutCounterR
if err != nil { if err != nil {
_ = data.MarkProviderOrderFailed(subTradeID, mdb.PaymentProviderOkPay) _ = data.MarkProviderOrderFailed(subTradeID, mdb.PaymentProviderOkPay)
_ = data.ExpireOrderByTradeID(subTradeID) _ = data.ExpireOrderByTradeID(subTradeID)
return nil, err return nil, constant.PaymentProviderCreateErr
} }
if err = data.UpdateProviderOrderCreated(subTradeID, mdb.PaymentProviderOkPay, okpayOrder.ProviderOrderID, okpayOrder.PayURL); err != nil { if err = data.UpdateProviderOrderCreated(subTradeID, mdb.PaymentProviderOkPay, okpayOrder.ProviderOrderID, okpayOrder.PayURL); err != nil {
_ = data.MarkProviderOrderFailed(subTradeID, mdb.PaymentProviderOkPay) _ = data.MarkProviderOrderFailed(subTradeID, mdb.PaymentProviderOkPay)
+2 -2
View File
@@ -80,8 +80,8 @@ func TestCreateTransactionRejectsPrivateNotifyURL(t *testing.T) {
req := newCreateTransactionRequest("order_private_notify_url", 1) req := newCreateTransactionRequest("order_private_notify_url", 1)
req.NotifyUrl = "http://127.0.0.1/notify" req.NotifyUrl = "http://127.0.0.1/notify"
if _, err := CreateTransaction(req, nil); err == nil { if _, err := CreateTransaction(req, nil); err != constant.NotifyURLErr {
t.Fatal("CreateTransaction returned nil error for private notify_url") t.Fatalf("CreateTransaction error = %v, want %v", err, constant.NotifyURLErr)
} }
} }
+110 -18
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
"net/http"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@@ -28,6 +29,12 @@ var gProcessedSignatures sync.Map // sig -> unix timestamp
var processSolanaOrder = OrderProcessing var processSolanaOrder = OrderProcessing
var (
solRPCRetryCount = 5
solRPCRetryWait = 2 * time.Second
solRPCRetryMaxWait = 10 * time.Second
)
type TransferInfo struct { type TransferInfo struct {
Source string // Source address (for SOL) or source ATA (for SPL tokens) Source string // Source address (for SOL) or source ATA (for SPL tokens)
Destination string // Destination address (for SOL) or destination ATA (for SPL tokens) Destination string // Destination address (for SOL) or destination ATA (for SPL tokens)
@@ -293,26 +300,74 @@ type solSignatureResult struct {
} }
func resolveSolanaRpcURL() (string, error) { func resolveSolanaRpcURL() (string, error) {
node, err := data.SelectRpcNode(mdb.NetworkSolana, mdb.RpcNodeTypeHttp) node, err := resolveSolanaRpcNode()
if err != nil { if err != nil {
return "", err return "", err
} }
rpcURL := strings.TrimSpace(node.Url)
return rpcURL, nil
}
func resolveSolanaRpcNode(excludeIDs ...uint64) (*mdb.RpcNode, error) {
node, err := data.SelectGeneralRpcNode(mdb.NetworkSolana, mdb.RpcNodeTypeHttp, excludeIDs...)
if err != nil {
return nil, err
}
if node == nil || node.ID == 0 { if node == nil || node.ID == 0 {
return "", fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkSolana, mdb.RpcNodeTypeHttp) return nil, fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkSolana, mdb.RpcNodeTypeHttp)
} }
rpcURL := strings.TrimSpace(node.Url) rpcURL := strings.TrimSpace(node.Url)
if rpcURL == "" { if rpcURL == "" {
return "", fmt.Errorf("rpc_nodes id=%d has empty url", node.ID) return nil, fmt.Errorf("rpc_nodes id=%d has empty url", node.ID)
} }
return rpcURL, nil node.Url = rpcURL
return node, nil
} }
// SolRetryClient 发送 Solana JSON-RPC 请求,自动重试 // SolRetryClient 发送 Solana JSON-RPC 请求,自动重试
func SolRetryClient(method string, params []interface{}) ([]byte, error) { func SolRetryClient(method string, params []interface{}) ([]byte, error) {
tried := make([]uint64, 0, 3)
var lastErr error
for attempts := 0; attempts < 3; attempts++ {
node, err := resolveSolanaRpcNode(tried...)
if err != nil {
if lastErr != nil {
return nil, lastErr
}
return nil, err
}
if len(tried) > 0 {
log.Sugar.Warnf("[SOL] trying alternate RPC node method=%s node=%s", method, data.RpcNodeLogLabel(*node))
}
body, err := solRetryClientWithURL(node.Url, method, params)
if err == nil {
data.RecordRpcNodeSuccess(node.ID)
return body, nil
}
lastErr = err
failures, cooling := data.RecordRpcNodeFailure(node.ID)
nodeLabel := data.RpcNodeLogLabel(*node)
if !cooling {
log.Sugar.Warnf("[SOL] RPC node failed method=%s node=%s failures=%d/%d", method, nodeLabel, failures, data.RpcFailoverThreshold)
return nil, err
}
log.Sugar.Warnf("[SOL] RPC node reached fail threshold method=%s node=%s, trying another node", method, nodeLabel)
tried = append(tried, node.ID)
}
return nil, lastErr
}
func solRetryClientWithURL(rpcUrl string, method string, params []interface{}) ([]byte, error) {
return solRetryClientWithURLHeaders(rpcUrl, method, params, nil)
}
func solRetryClientWithURLHeaders(rpcUrl string, method string, params []interface{}, headers http.Header) ([]byte, error) {
client := resty.New() client := resty.New()
client.SetRetryCount(5) client.SetRetryCount(solRPCRetryCount)
client.SetRetryWaitTime(2 * time.Second) client.SetRetryWaitTime(solRPCRetryWait)
client.SetRetryMaxWaitTime(10 * time.Second) client.SetRetryMaxWaitTime(solRPCRetryMaxWait)
client.AddRetryCondition(func(r *resty.Response, err error) bool { client.AddRetryCondition(func(r *resty.Response, err error) bool {
if err != nil { if err != nil {
return true return true
@@ -323,25 +378,35 @@ func SolRetryClient(method string, params []interface{}) ([]byte, error) {
return false return false
}) })
rpcUrl, err := resolveSolanaRpcURL() req := client.R().SetHeader("Content-Type", "application/json")
if err != nil { for key, values := range headers {
return nil, err for _, value := range values {
req.SetHeader(key, value)
}
} }
resp, err := client.R(). resp, err := req.SetBody(map[string]interface{}{
SetHeader("Content-Type", "application/json"). "jsonrpc": "2.0",
SetBody(map[string]interface{}{ "id": 1,
"jsonrpc": "2.0", "method": method,
"id": 1, "params": params,
"method": method, }).
"params": params,
}).
Post(rpcUrl) Post(rpcUrl)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if resp.StatusCode() >= http.StatusBadRequest {
return nil, fmt.Errorf("solana rpc HTTP %d", resp.StatusCode())
}
respBody := resp.Body() respBody := resp.Body()
if errValue := gjson.GetBytes(respBody, "error"); errValue.Exists() && errValue.Type != gjson.Null {
code := errValue.Get("code").String()
if code == "" {
code = "unknown"
}
return nil, fmt.Errorf("solana rpc error code=%s", code)
}
return respBody, nil return respBody, nil
} }
@@ -401,6 +466,33 @@ func SolGetTransaction(sig string) ([]byte, error) {
return txData, nil return txData, nil
} }
func solGetTransactionWithURL(rpcURL string, sig string) ([]byte, error) {
return solGetTransactionWithURLHeaders(rpcURL, sig, nil)
}
func solGetTransactionWithURLHeaders(rpcURL string, sig string, headers http.Header) ([]byte, error) {
txData, err := solRetryClientWithURLHeaders(rpcURL, "getTransaction", []interface{}{
sig,
map[string]interface{}{
"encoding": "jsonParsed",
"commitment": "confirmed",
"maxSupportedTransactionVersion": 0, // suport
},
}, headers)
if err != nil {
log.Sugar.Errorf("SolRetryClient failed: %v", err)
return nil, err
}
errField := gjson.GetBytes(txData, "result.meta.err")
if errField.Exists() && errField.Type != gjson.Null {
log.Sugar.Warnf("Transaction failed: %v", errField.String())
return nil, fmt.Errorf("transaction failed: %s", errField.String())
}
return txData, nil
}
// isTransferToAddress 判断转账目标是否为指定钱包地址 // isTransferToAddress 判断转账目标是否为指定钱包地址
func isTransferToAddress(transfer *TransferInfo, targetAddress string) bool { func isTransferToAddress(transfer *TransferInfo, targetAddress string) bool {
// Native SOL transfer - check destination directly // Native SOL transfer - check destination directly
+258
View File
@@ -28,6 +28,7 @@ func setupSolanaRPCNode(t *testing.T, url string) func() {
Type: mdb.RpcNodeTypeHttp, Type: mdb.RpcNodeTypeHttp,
Weight: 1, Weight: 1,
Enabled: true, Enabled: true,
Purpose: mdb.RpcNodePurposeGeneral,
Status: mdb.RpcNodeStatusOk, Status: mdb.RpcNodeStatusOk,
} }
if err := dao.Mdb.Create(node).Error; err != nil { if err := dao.Mdb.Create(node).Error; err != nil {
@@ -59,6 +60,263 @@ func TestResolveSolanaRpcURLWithRow(t *testing.T) {
} }
} }
func TestResolveSolanaRpcURLIgnoresManualVerifyOnly(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := dao.Mdb.Create(&mdb.RpcNode{
Network: mdb.NetworkSolana,
Url: "https://paid-solana.example.com",
Type: mdb.RpcNodeTypeHttp,
Weight: 100,
Enabled: true,
Purpose: mdb.RpcNodePurposeManualVerify,
Status: mdb.RpcNodeStatusOk,
}).Error; err != nil {
t.Fatalf("seed manual rpc_node: %v", err)
}
if got, err := resolveSolanaRpcURL(); err == nil {
t.Fatalf("resolveSolanaRpcURL() = %q, nil; want error", got)
}
}
func TestResolveSolanaRpcURLUsesGeneralWhenManualVerifyExists(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
rows := []mdb.RpcNode{
{Network: mdb.NetworkSolana, Url: "https://paid-solana.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkSolana, Url: " https://general-solana.example.com ", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
}
if err := dao.Mdb.Create(&rows).Error; err != nil {
t.Fatalf("seed rpc_nodes: %v", err)
}
got, err := resolveSolanaRpcURL()
if err != nil {
t.Fatalf("resolveSolanaRpcURL(): %v", err)
}
if got != "https://general-solana.example.com" {
t.Fatalf("resolveSolanaRpcURL() = %q, want general node", got)
}
}
func TestSolRetryClientSwitchesAfterFailureThreshold(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
data.ResetRpcFailoverForTest()
t.Cleanup(data.ResetRpcFailoverForTest)
oldRetryCount := solRPCRetryCount
oldRetryWait := solRPCRetryWait
oldRetryMaxWait := solRPCRetryMaxWait
solRPCRetryCount = 0
solRPCRetryWait = time.Millisecond
solRPCRetryMaxWait = time.Millisecond
t.Cleanup(func() {
solRPCRetryCount = oldRetryCount
solRPCRetryWait = oldRetryWait
solRPCRetryMaxWait = oldRetryMaxWait
})
var primaryCalls int
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
primaryCalls++
http.Error(w, "temporary", http.StatusBadGateway)
}))
defer primary.Close()
var backupCalls int
backup := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
backupCalls++
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": "ok",
})
}))
defer backup.Close()
rows := []mdb.RpcNode{
{Network: mdb.NetworkSolana, Url: primary.URL, Type: mdb.RpcNodeTypeHttp, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkSolana, Url: backup.URL, Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
}
if err := dao.Mdb.Create(&rows).Error; err != nil {
t.Fatalf("seed rpc_nodes: %v", err)
}
for i := 0; i < data.RpcFailoverThreshold-1; i++ {
if _, err := SolRetryClient("getHealth", nil); err == nil {
t.Fatalf("SolRetryClient attempt %d unexpectedly succeeded", i+1)
}
}
if backupCalls != 0 {
t.Fatalf("backup calls before threshold = %d, want 0", backupCalls)
}
body, err := SolRetryClient("getHealth", nil)
if err != nil {
t.Fatalf("SolRetryClient after threshold: %v", err)
}
if gjson.GetBytes(body, "result").String() != "ok" {
t.Fatalf("response body = %s, want result ok", string(body))
}
if backupCalls == 0 {
t.Fatal("backup RPC was not called after threshold")
}
if primaryCalls == 0 {
t.Fatal("primary RPC was not called")
}
}
func TestSolRetryClientCountsJSONRPCErrorAsNodeFailure(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
data.ResetRpcFailoverForTest()
t.Cleanup(data.ResetRpcFailoverForTest)
oldRetryCount := solRPCRetryCount
oldRetryWait := solRPCRetryWait
oldRetryMaxWait := solRPCRetryMaxWait
solRPCRetryCount = 0
solRPCRetryWait = time.Millisecond
solRPCRetryMaxWait = time.Millisecond
t.Cleanup(func() {
solRPCRetryCount = oldRetryCount
solRPCRetryWait = oldRetryWait
solRPCRetryMaxWait = oldRetryMaxWait
})
var primaryCalls int
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
primaryCalls++
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"error": map[string]interface{}{
"code": -32005,
},
})
}))
defer primary.Close()
var backupCalls int
backup := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
backupCalls++
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": "ok",
})
}))
defer backup.Close()
rows := []mdb.RpcNode{
{Network: mdb.NetworkSolana, Url: primary.URL, Type: mdb.RpcNodeTypeHttp, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkSolana, Url: backup.URL, Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
}
if err := dao.Mdb.Create(&rows).Error; err != nil {
t.Fatalf("seed rpc_nodes: %v", err)
}
for i := 0; i < data.RpcFailoverThreshold-1; i++ {
if _, err := SolRetryClient("getHealth", nil); err == nil {
t.Fatalf("SolRetryClient attempt %d unexpectedly succeeded", i+1)
}
}
body, err := SolRetryClient("getHealth", nil)
if err != nil {
t.Fatalf("SolRetryClient after JSON-RPC error threshold: %v", err)
}
if gjson.GetBytes(body, "result").String() != "ok" {
t.Fatalf("response body = %s, want result ok", string(body))
}
if primaryCalls < data.RpcFailoverThreshold {
t.Fatalf("primary calls = %d, want at least threshold", primaryCalls)
}
if backupCalls == 0 {
t.Fatal("backup RPC was not called after JSON-RPC error threshold")
}
}
func TestSolRetryClientWithURLHeadersForwardsCustomHeaders(t *testing.T) {
oldRetryCount := solRPCRetryCount
oldRetryWait := solRPCRetryWait
oldRetryMaxWait := solRPCRetryMaxWait
solRPCRetryCount = 0
solRPCRetryWait = time.Millisecond
solRPCRetryMaxWait = time.Millisecond
t.Cleanup(func() {
solRPCRetryCount = oldRetryCount
solRPCRetryWait = oldRetryWait
solRPCRetryMaxWait = oldRetryMaxWait
})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("X-Test-Header"); got != "test-value" {
t.Errorf("X-Test-Header = %q, want test-value", got)
http.Error(w, "bad custom header", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": "ok",
})
}))
defer server.Close()
headers := http.Header{}
headers.Set("X-Test-Header", "test-value")
body, err := solRetryClientWithURLHeaders(server.URL, "getHealth", nil, headers)
if err != nil {
t.Fatalf("solRetryClientWithURLHeaders(): %v", err)
}
if gjson.GetBytes(body, "result").String() != "ok" {
t.Fatalf("response body = %s, want result ok", string(body))
}
}
func TestSolRetryClientWithURLDoesNotForwardUserIPByDefault(t *testing.T) {
oldRetryCount := solRPCRetryCount
oldRetryWait := solRPCRetryWait
oldRetryMaxWait := solRPCRetryMaxWait
solRPCRetryCount = 0
solRPCRetryWait = time.Millisecond
solRPCRetryMaxWait = time.Millisecond
t.Cleanup(func() {
solRPCRetryCount = oldRetryCount
solRPCRetryWait = oldRetryWait
solRPCRetryMaxWait = oldRetryMaxWait
})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("X-USER-IP"); got != "" {
t.Errorf("X-USER-IP header = %q, want empty", got)
http.Error(w, "unexpected user ip header", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": "ok",
})
}))
defer server.Close()
body, err := solRetryClientWithURL(server.URL, "getHealth", nil)
if err != nil {
t.Fatalf("solRetryClientWithURL(): %v", err)
}
if gjson.GetBytes(body, "result").String() != "ok" {
t.Fatalf("response body = %s, want result ok", string(body))
}
}
func TestSolCallBackDoesNotCacheSignatureAfterRetryableOrderProcessingError(t *testing.T) { func TestSolCallBackDoesNotCacheSignatureAfterRetryableOrderProcessingError(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t) cleanup := testutil.SetupTestDatabases(t)
defer cleanup() defer cleanup()
+14 -4
View File
@@ -18,18 +18,28 @@ import (
) )
func resolveTronNode() (string, string, error) { func resolveTronNode() (string, string, error) {
node, err := data.SelectRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp) node, err := ResolveTronRpcNode()
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
rpcURL := strings.TrimRight(strings.TrimSpace(node.Url), "/")
return rpcURL, node.ApiKey, nil
}
func ResolveTronRpcNode(excludeIDs ...uint64) (*mdb.RpcNode, error) {
node, err := data.SelectGeneralRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp, excludeIDs...)
if err != nil {
return nil, err
}
if node == nil || node.ID == 0 { if node == nil || node.ID == 0 {
return "", "", fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkTron, mdb.RpcNodeTypeHttp) return nil, fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkTron, mdb.RpcNodeTypeHttp)
} }
rpcURL := strings.TrimRight(strings.TrimSpace(node.Url), "/") rpcURL := strings.TrimRight(strings.TrimSpace(node.Url), "/")
if rpcURL == "" { if rpcURL == "" {
return "", "", fmt.Errorf("rpc_nodes id=%d has empty url", node.ID) return nil, fmt.Errorf("rpc_nodes id=%d has empty url", node.ID)
} }
return rpcURL, node.ApiKey, nil node.Url = rpcURL
return node, nil
} }
func ResolveTronNode() (string, string, error) { func ResolveTronNode() (string, string, error) {
+43
View File
@@ -51,6 +51,49 @@ func TestResolveTronNode_WithRow(t *testing.T) {
} }
} }
func TestResolveTronNodeIgnoresManualVerifyOnly(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := dao.Mdb.Create(&mdb.RpcNode{
Network: mdb.NetworkTron,
Url: "https://paid-tron.example.com",
Type: mdb.RpcNodeTypeHttp,
ApiKey: "paid-key",
Weight: 100,
Enabled: true,
Purpose: mdb.RpcNodePurposeManualVerify,
Status: mdb.RpcNodeStatusOk,
}).Error; err != nil {
t.Fatalf("seed manual rpc_node: %v", err)
}
if gotURL, gotKey, err := resolveTronNode(); err == nil {
t.Fatalf("resolveTronNode() = (%q, %q, nil), want error", gotURL, gotKey)
}
}
func TestResolveTronNodeUsesGeneralWhenManualVerifyExists(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
rows := []mdb.RpcNode{
{Network: mdb.NetworkTron, Url: "https://paid-tron.example.com", Type: mdb.RpcNodeTypeHttp, ApiKey: "paid-key", Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkTron, Url: "https://general-tron.example.com", Type: mdb.RpcNodeTypeHttp, ApiKey: "general-key", Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
}
if err := dao.Mdb.Create(&rows).Error; err != nil {
t.Fatalf("seed rpc_nodes: %v", err)
}
gotURL, gotKey, err := resolveTronNode()
if err != nil {
t.Fatalf("resolveTronNode(): %v", err)
}
if gotURL != "https://general-tron.example.com" || gotKey != "general-key" {
t.Fatalf("resolveTronNode() = (%q, %q), want general node", gotURL, gotKey)
}
}
// TestResolveTronNode_DisabledRow verifies that a disabled row is ignored. // TestResolveTronNode_DisabledRow verifies that a disabled row is ignored.
func TestResolveTronNode_DisabledRow(t *testing.T) { func TestResolveTronNode_DisabledRow(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t) cleanup := testutil.SetupTestDatabases(t)
+123 -7
View File
@@ -15,6 +15,7 @@ import (
"github.com/GMWalletApp/epusdt/model/data" "github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/model/service" "github.com/GMWalletApp/epusdt/model/service"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@@ -339,8 +340,15 @@ func TestAdminInitPasswordFlow(t *testing.T) {
if recFetch2.Code != http.StatusBadRequest { if recFetch2.Code != http.StatusBadRequest {
t.Fatalf("second fetch should fail with 400, got %d body=%s", recFetch2.Code, recFetch2.Body.String()) t.Fatalf("second fetch should fail with 400, got %d body=%s", recFetch2.Code, recFetch2.Body.String())
} }
if !strings.Contains(strings.ToLower(recFetch2.Body.String()), "already fetched") { var respFetch2 map[string]interface{}
t.Fatalf("expected already fetched error, got: %s", recFetch2.Body.String()) if err := json.Unmarshal(recFetch2.Body.Bytes(), &respFetch2); err != nil {
t.Fatalf("unmarshal second fetch response: %v", err)
}
if got := int(respFetch2["status_code"].(float64)); got != 10040 {
t.Fatalf("second fetch status_code = %d, want 10040; response=%v", got, respFetch2)
}
if got, _ := respFetch2["message"].(string); got != constant.Errno[10040] {
t.Fatalf("second fetch message = %q, want %q; response=%v", got, constant.Errno[10040], respFetch2)
} }
token := adminLogin(t, e, testAdminUsername, initPassword) token := adminLogin(t, e, testAdminUsername, initPassword)
@@ -523,14 +531,25 @@ func TestAdminRpcNodes_CRUD(t *testing.T) {
if nodeID == nil { if nodeID == nil {
t.Fatal("CreateRpcNode missing id") t.Fatal("CreateRpcNode missing id")
} }
if got, _ := dataObj["purpose"].(string); got != mdb.RpcNodePurposeGeneral {
t.Fatalf("created purpose = %q, want %q", got, mdb.RpcNodePurposeGeneral)
}
nodeIDStr := fmt.Sprintf("%.0f", nodeID.(float64)) nodeIDStr := fmt.Sprintf("%.0f", nodeID.(float64))
// Update. // Update.
rec = doPatchAdmin(e, "/admin/api/v1/rpc-nodes/"+nodeIDStr, map[string]interface{}{ rec = doPatchAdmin(e, "/admin/api/v1/rpc-nodes/"+nodeIDStr, map[string]interface{}{
"url": "https://eth-mainnet2.example.com", "url": "https://eth-mainnet2.example.com",
"purpose": mdb.RpcNodePurposeManualVerify,
}, token) }, token)
t.Logf("UpdateRpcNode: %s", rec.Body.String()) t.Logf("UpdateRpcNode: %s", rec.Body.String())
assertOK(t, rec) assertOK(t, rec)
updatedNode, err := data.GetRpcNodeByID(uint64(nodeID.(float64)))
if err != nil {
t.Fatalf("reload rpc node: %v", err)
}
if updatedNode.Purpose != mdb.RpcNodePurposeManualVerify {
t.Fatalf("updated purpose = %q, want %q", updatedNode.Purpose, mdb.RpcNodePurposeManualVerify)
}
// Health check — network likely unreachable in test, but route must not 404/500. // Health check — network likely unreachable in test, but route must not 404/500.
rec = doPostAdmin(e, "/admin/api/v1/rpc-nodes/"+nodeIDStr+"/health-check", nil, token) rec = doPostAdmin(e, "/admin/api/v1/rpc-nodes/"+nodeIDStr+"/health-check", nil, token)
@@ -545,6 +564,95 @@ func TestAdminRpcNodes_CRUD(t *testing.T) {
assertOK(t, rec) assertOK(t, rec)
} }
func TestAdminRpcNodes_RejectsURLTypeMismatch(t *testing.T) {
e, token := setupAdminTestEnv(t)
rec := doPostAdmin(e, "/admin/api/v1/rpc-nodes", map[string]interface{}{
"network": "ethereum",
"url": "wss://eth-mainnet.example.com",
"type": mdb.RpcNodeTypeHttp,
}, token)
t.Logf("CreateRpcNode(http with wss): status=%d body=%s", rec.Code, rec.Body.String())
if rec.Code == http.StatusOK {
t.Fatalf("expected create mismatch to fail, got 200: %s", rec.Body.String())
}
assertErrorCode(t, rec, 10022)
rec = doPostAdmin(e, "/admin/api/v1/rpc-nodes", map[string]interface{}{
"network": "ethereum",
"url": "https://eth-mainnet.example.com",
"type": mdb.RpcNodeTypeWs,
}, token)
t.Logf("CreateRpcNode(ws with https): status=%d body=%s", rec.Code, rec.Body.String())
if rec.Code == http.StatusOK {
t.Fatalf("expected create mismatch to fail, got 200: %s", rec.Body.String())
}
assertErrorCode(t, rec, 10023)
rec = doPostAdmin(e, "/admin/api/v1/rpc-nodes", map[string]interface{}{
"network": "ethereum",
"url": "https://eth-mainnet.example.com",
"type": mdb.RpcNodeTypeHttp,
"purpose": "invalid-purpose",
}, token)
t.Logf("CreateRpcNode(invalid purpose): status=%d body=%s", rec.Code, rec.Body.String())
if rec.Code == http.StatusOK {
t.Fatalf("expected invalid purpose to fail, got 200: %s", rec.Body.String())
}
assertErrorCode(t, rec, 10020)
rec = doPostAdmin(e, "/admin/api/v1/rpc-nodes", map[string]interface{}{
"network": "ethereum",
"url": "https://eth-mainnet.example.com",
"type": mdb.RpcNodeTypeHttp,
}, token)
resp := assertOK(t, rec)
dataObj, _ := resp["data"].(map[string]interface{})
nodeID := uint64(dataObj["id"].(float64))
nodeIDStr := fmt.Sprintf("%d", nodeID)
rec = doPatchAdmin(e, "/admin/api/v1/rpc-nodes/"+nodeIDStr, map[string]interface{}{
"url": "wss://eth-mainnet.example.com",
}, token)
t.Logf("UpdateRpcNode(http with wss): status=%d body=%s", rec.Code, rec.Body.String())
if rec.Code == http.StatusOK {
t.Fatalf("expected update mismatch to fail, got 200: %s", rec.Body.String())
}
assertErrorCode(t, rec, 10022)
node, err := data.GetRpcNodeByID(nodeID)
if err != nil {
t.Fatalf("reload rpc node: %v", err)
}
if node.Url != "https://eth-mainnet.example.com" {
t.Fatalf("node url changed after rejected update: %q", node.Url)
}
}
func TestAdminPathIDParseErrorUsesParamsErrno(t *testing.T) {
e, token := setupAdminTestEnv(t)
rec := doGetAdmin(e, "/admin/api/v1/api-keys/not-a-number/stats", token)
assertErrorCode(t, rec, 10009)
}
func assertErrorCode(t *testing.T, rec *httptest.ResponseRecorder, wantCode int) {
t.Helper()
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected HTTP 400, got %d: %s", rec.Code, rec.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal error response: %v", err)
}
if got := int(resp["status_code"].(float64)); got != wantCode {
t.Fatalf("status_code = %d, want %d; response=%v", got, wantCode, resp)
}
if got, _ := resp["message"].(string); got != constant.Errno[wantCode] {
t.Fatalf("message = %q, want %q; response=%v", got, constant.Errno[wantCode], resp)
}
}
// ─── Wallets ───────────────────────────────────────────────────────────────── // ─── Wallets ─────────────────────────────────────────────────────────────────
// TestAdminWallets_CRUD verifies wallet management routes. // TestAdminWallets_CRUD verifies wallet management routes.
@@ -693,9 +801,14 @@ func TestAdminOrders_MarkPaidSuccessAfterVerification(t *testing.T) {
}) })
defer restore() defer restore()
rec := doPostAdmin(e, "/admin/api/v1/orders/"+order.TradeId+"/mark-paid", map[string]interface{}{ jsonBytes, _ := json.Marshal(map[string]interface{}{
"block_transaction_id": "block-admin-ok", "block_transaction_id": "block-admin-ok",
}, token) })
req := httptest.NewRequest(http.MethodPost, "/admin/api/v1/orders/"+order.TradeId+"/mark-paid", strings.NewReader(string(jsonBytes)))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.Header.Set(echo.HeaderAuthorization, "Bearer "+token)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
t.Logf("MarkOrderPaid success: status=%d body=%s", rec.Code, rec.Body.String()) t.Logf("MarkOrderPaid success: status=%d body=%s", rec.Code, rec.Body.String())
assertOK(t, rec) assertOK(t, rec)
if !verified { if !verified {
@@ -1211,8 +1324,11 @@ func TestAdminSettings_RejectsPrivateRateAPIURL(t *testing.T) {
if result["ok"] != false { if result["ok"] != false {
t.Fatalf("private rate.api_url result = %v, want ok=false", result) t.Fatalf("private rate.api_url result = %v, want ok=false", result)
} }
if got, _ := result["error"].(string); !strings.Contains(got, "rate.api_url invalid") { if got := int(result["error_code"].(float64)); got != 10043 {
t.Fatalf("private rate.api_url error = %q", got) t.Fatalf("private rate.api_url error_code = %d, want 10043; result=%v", got, result)
}
if got, _ := result["error"].(string); got != constant.Errno[10043] {
t.Fatalf("private rate.api_url error = %q, want %q", got, constant.Errno[10043])
} }
} }
+3 -3
View File
@@ -75,7 +75,7 @@ func RegisterRoute(e *echo.Echo) {
formParams, err := ctx.FormParams() formParams, err := ctx.FormParams()
if err != nil && ctx.Request().Method == http.MethodPost { if err != nil && ctx.Request().Method == http.MethodPost {
return comm.Ctrl.FailJson(ctx, fmt.Errorf("invalid epay form params: %w", err)) return comm.Ctrl.FailJson(ctx, constant.ParamsMarshalErr)
} }
if err == nil { if err == nil {
copyParams(formParams) copyParams(formParams)
@@ -132,7 +132,7 @@ func RegisterRoute(e *echo.Echo) {
amountFloat, err := strconv.ParseFloat(money, 64) amountFloat, err := strconv.ParseFloat(money, 64)
if err != nil { if err != nil {
return comm.Ctrl.FailJson(ctx, fmt.Errorf("invalid money value: %s", money)) return comm.Ctrl.FailJson(ctx, constant.PayAmountErr)
} }
body := map[string]interface{}{ body := map[string]interface{}{
@@ -154,7 +154,7 @@ func RegisterRoute(e *echo.Echo) {
jsonBytes, err := json.Marshal(body) jsonBytes, err := json.Marshal(body)
if err != nil { if err != nil {
return comm.Ctrl.FailJson(ctx, err) return comm.Ctrl.FailJson(ctx, constant.SystemErr)
} }
ctx.Request().Body = io.NopCloser(bytes.NewBuffer(jsonBytes)) ctx.Request().Body = io.NopCloser(bytes.NewBuffer(jsonBytes))
+160 -3
View File
@@ -18,6 +18,7 @@ import (
"github.com/GMWalletApp/epusdt/model/data" "github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/model/service" "github.com/GMWalletApp/epusdt/model/service"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/http_client" "github.com/GMWalletApp/epusdt/util/http_client"
"github.com/GMWalletApp/epusdt/util/log" "github.com/GMWalletApp/epusdt/util/log"
"github.com/GMWalletApp/epusdt/util/sign" "github.com/GMWalletApp/epusdt/util/sign"
@@ -286,6 +287,39 @@ func TestCreateOrderGmpayRejectsPrivateNotifyURL(t *testing.T) {
if rec.Code != http.StatusBadRequest { if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String()) t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
} }
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal private notify response: %v", err)
}
if got := int(resp["status_code"].(float64)); got != 10041 {
t.Fatalf("status_code = %d, want 10041; response=%v", got, resp)
}
if got, _ := resp["message"].(string); got != constant.Errno[10041] {
t.Fatalf("message = %q, want %q", got, constant.Errno[10041])
}
}
func TestPublicJSONBindErrorUsesParamsErrno(t *testing.T) {
e := setupTestEnv(t)
req := httptest.NewRequest(http.MethodPost, "/pay/submit-tx-hash/bad-json-order", strings.NewReader("{"))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal bind error response: %v", err)
}
if got := int(resp["status_code"].(float64)); got != 10009 {
t.Fatalf("status_code = %d, want 10009; response=%v", got, resp)
}
if got, _ := resp["message"].(string); got != constant.Errno[10009] {
t.Fatalf("message = %q, want %q", got, constant.Errno[10009])
}
} }
// TestCreateOrderGmpayV1SolNative tests creating an order for native SOL token. // TestCreateOrderGmpayV1SolNative tests creating an order for native SOL token.
@@ -1101,9 +1135,13 @@ func TestSubmitTxHash_SuccessUpdatesCheckStatus(t *testing.T) {
}) })
defer restore() defer restore()
rec := doPost(e, "/pay/submit-tx-hash/"+tradeID, map[string]interface{}{ jsonBytes, _ := json.Marshal(map[string]interface{}{
"block_transaction_id": " user-submitted-hash ", "block_transaction_id": " user-submitted-hash ",
}) })
req := httptest.NewRequest(http.MethodPost, "/pay/submit-tx-hash/"+tradeID, strings.NewReader(string(jsonBytes)))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
t.Logf("SubmitTxHash(success): status=%d body=%s", rec.Code, rec.Body.String()) t.Logf("SubmitTxHash(success): status=%d body=%s", rec.Code, rec.Body.String())
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
@@ -1127,9 +1165,9 @@ func TestSubmitTxHash_SuccessUpdatesCheckStatus(t *testing.T) {
t.Fatalf("submit response status = %d, want %d", got, mdb.StatusPaySuccess) t.Fatalf("submit response status = %d, want %d", got, mdb.StatusPaySuccess)
} }
req := httptest.NewRequest(http.MethodGet, "/pay/check-status/"+tradeID, nil) statusReq := httptest.NewRequest(http.MethodGet, "/pay/check-status/"+tradeID, nil)
statusRec := httptest.NewRecorder() statusRec := httptest.NewRecorder()
e.ServeHTTP(statusRec, req) e.ServeHTTP(statusRec, statusReq)
if statusRec.Code != http.StatusOK { if statusRec.Code != http.StatusOK {
t.Fatalf("check-status expected 200, got %d: %s", statusRec.Code, statusRec.Body.String()) t.Fatalf("check-status expected 200, got %d: %s", statusRec.Code, statusRec.Body.String())
} }
@@ -1146,6 +1184,86 @@ func TestSubmitTxHash_SuccessUpdatesCheckStatus(t *testing.T) {
} }
} }
func TestSubmitTxHash_VerificationFailureAllowsRetrySameHash(t *testing.T) {
e := setupTestEnv(t)
tradeID := createCheckoutCounterRespTestOrder(t, e, "submit-tx-hash-retry-001")
calls := 0
restore := service.SetManualOrderPaymentValidatorForTest(func(order *mdb.Orders, blockID string) (string, error) {
calls++
if order.TradeId != tradeID {
t.Fatalf("validator trade_id = %s, want %s", order.TradeId, tradeID)
}
if blockID != "retry-hash" {
t.Fatalf("validator block id = %s, want retry-hash", blockID)
}
if calls == 1 {
return "", fmt.Errorf("rpc verification failed")
}
return "canonical-retry-hash", nil
})
defer restore()
rec := doPost(e, "/pay/submit-tx-hash/"+tradeID, map[string]interface{}{
"block_transaction_id": "retry-hash",
})
t.Logf("SubmitTxHash(verification failure): status=%d body=%s", rec.Code, rec.Body.String())
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
}
var failResp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &failResp); err != nil {
t.Fatalf("unmarshal failure response: %v", err)
}
if got := int(failResp["status_code"].(float64)); got != 10038 {
t.Fatalf("failure status_code = %d, want 10038", got)
}
if got, _ := failResp["message"].(string); got != constant.Errno[10038] {
t.Fatalf("failure message = %q, want %q", got, constant.Errno[10038])
}
reloaded, err := data.GetOrderInfoByTradeId(tradeID)
if err != nil {
t.Fatalf("reload order after failure: %v", err)
}
if reloaded.Status != mdb.StatusWaitPay || reloaded.BlockTransactionId != "" {
t.Fatalf("order changed after failed submit: status=%d block=%q", reloaded.Status, reloaded.BlockTransactionId)
}
statusReq := httptest.NewRequest(http.MethodGet, "/pay/check-status/"+tradeID, nil)
statusRec := httptest.NewRecorder()
e.ServeHTTP(statusRec, statusReq)
if statusRec.Code != http.StatusOK {
t.Fatalf("check-status expected 200, got %d: %s", statusRec.Code, statusRec.Body.String())
}
var statusResp map[string]interface{}
if err := json.Unmarshal(statusRec.Body.Bytes(), &statusResp); err != nil {
t.Fatalf("unmarshal check-status response: %v", err)
}
statusData := statusResp["data"].(map[string]interface{})
if got := int(statusData["status"].(float64)); got != mdb.StatusWaitPay {
t.Fatalf("check-status status after failed submit = %d, want %d", got, mdb.StatusWaitPay)
}
retryRec := doPost(e, "/pay/submit-tx-hash/"+tradeID, map[string]interface{}{
"block_transaction_id": "retry-hash",
})
t.Logf("SubmitTxHash(retry same hash): status=%d body=%s", retryRec.Code, retryRec.Body.String())
if retryRec.Code != http.StatusOK {
t.Fatalf("expected retry success, got %d: %s", retryRec.Code, retryRec.Body.String())
}
if calls != 2 {
t.Fatalf("validator calls = %d, want 2", calls)
}
reloaded, err = data.GetOrderInfoByTradeId(tradeID)
if err != nil {
t.Fatalf("reload order after retry: %v", err)
}
if reloaded.Status != mdb.StatusPaySuccess || reloaded.BlockTransactionId != "canonical-retry-hash" {
t.Fatalf("order after retry: status=%d block=%q", reloaded.Status, reloaded.BlockTransactionId)
}
}
func TestSubmitTxHash_RejectsOkPayOrder(t *testing.T) { func TestSubmitTxHash_RejectsOkPayOrder(t *testing.T) {
e := setupTestEnv(t) e := setupTestEnv(t)
order := &mdb.Orders{ order := &mdb.Orders{
@@ -1192,6 +1310,45 @@ func TestSubmitTxHash_RejectsOkPayOrder(t *testing.T) {
} }
} }
func TestSubmitTxHash_RejectsExistingHashBeforeRpc(t *testing.T) {
e := setupTestEnv(t)
existing := &mdb.Orders{
TradeId: "trade-submit-tx-hash-existing",
OrderId: "order-submit-tx-hash-existing",
BlockTransactionId: "existing-hash",
Amount: 10,
Currency: "CNY",
ActualAmount: 1.23,
ReceiveAddress: "TTestTronAddress001",
Token: "USDT",
Network: mdb.NetworkTron,
Status: mdb.StatusPaySuccess,
PayProvider: mdb.PaymentProviderOnChain,
}
if err := dao.Mdb.Create(existing).Error; err != nil {
t.Fatalf("create existing paid order: %v", err)
}
tradeID := createCheckoutCounterRespTestOrder(t, e, "submit-tx-hash-existing-001")
called := false
restore := service.SetManualOrderPaymentValidatorForTest(func(*mdb.Orders, string) (string, error) {
called = true
return "existing-hash", nil
})
defer restore()
rec := doPost(e, "/pay/submit-tx-hash/"+tradeID, map[string]interface{}{
"block_transaction_id": "existing-hash",
})
t.Logf("SubmitTxHash(existing hash): status=%d body=%s", rec.Code, rec.Body.String())
if rec.Code == http.StatusOK {
t.Fatalf("expected failure, got 200: %s", rec.Body.String())
}
if called {
t.Fatal("verifier should not run when hash already exists")
}
}
// TestCheckoutCounter_NotFound verifies that /pay/checkout-counter/:trade_id // TestCheckoutCounter_NotFound verifies that /pay/checkout-counter/:trade_id
// with an unknown trade_id does not return an empty 404 (which would mean the // with an unknown trade_id does not return an empty 404 (which would mean the
// route is not registered). When the static HTML template is present the // route is not registered). When the static HTML template is present the
+3 -3
View File
@@ -66,18 +66,18 @@ func runBscListener(contracts []common.Address) {
} }
}() }()
wsURL, ok := resolveChainWsURL(mdb.NetworkBsc, "[BSC-WS]") wsNode, ok := resolveChainWsNode(mdb.NetworkBsc, "[BSC-WS]")
if !ok { if !ok {
return return
} }
log.Sugar.Infof("[BSC-WS] connecting to %s watching %d contract(s)", wsURL, len(contracts)) log.Sugar.Infof("[BSC-WS] connecting using WSS node %s watching %d contract(s)", data.RpcNodeLogLabel(wsNode), len(contracts))
query := ethereum.FilterQuery{ query := ethereum.FilterQuery{
Addresses: contracts, Addresses: contracts,
Topics: [][]common.Hash{}, Topics: [][]common.Hash{},
} }
runEvmWsLogListener(ctx, "[BSC-WS]", wsURL, query, func(client *ethclient.Client, vLog types.Log) { runEvmWsLogListener(ctx, "[BSC-WS]", wsNode, query, func(client *ethclient.Client, vLog types.Log) {
if len(vLog.Topics) < 3 { if len(vLog.Topics) < 3 {
return return
} }
+13 -4
View File
@@ -93,19 +93,28 @@ func loadChainTokenContracts(network, logPrefix string) []common.Address {
// given network. If no enabled node is configured, the caller skips the // given network. If no enabled node is configured, the caller skips the
// current listener run so admin-side disabled/deleted rows are respected. // current listener run so admin-side disabled/deleted rows are respected.
func resolveChainWsURL(network, logPrefix string) (string, bool) { func resolveChainWsURL(network, logPrefix string) (string, bool) {
node, err := data.SelectRpcNode(network, mdb.RpcNodeTypeWs) node, ok := resolveChainWsNode(network, logPrefix)
if !ok {
return "", false
}
return strings.TrimSpace(node.Url), true
}
func resolveChainWsNode(network, logPrefix string, excludeIDs ...uint64) (mdb.RpcNode, bool) {
node, err := data.SelectGeneralRpcNode(network, mdb.RpcNodeTypeWs, excludeIDs...)
if err == nil && node != nil && node.ID > 0 { if err == nil && node != nil && node.ID > 0 {
rpcURL := strings.TrimSpace(node.Url) rpcURL := strings.TrimSpace(node.Url)
if rpcURL != "" { if rpcURL != "" {
return rpcURL, true node.Url = rpcURL
return *node, true
} }
log.Sugar.Errorf("%s rpc_nodes id=%d has empty url", logPrefix, node.ID) log.Sugar.Errorf("%s rpc_nodes id=%d has empty url", logPrefix, node.ID)
return "", false return mdb.RpcNode{}, false
} }
if err != nil { if err != nil {
log.Sugar.Errorf("%s resolve rpc_nodes err=%v", logPrefix, err) log.Sugar.Errorf("%s resolve rpc_nodes err=%v", logPrefix, err)
} else { } else {
log.Sugar.Warnf("%s no enabled %s WS RPC node configured in rpc_nodes", logPrefix, network) log.Sugar.Warnf("%s no enabled %s WS RPC node configured in rpc_nodes", logPrefix, network)
} }
return "", false return mdb.RpcNode{}, false
} }
+74
View File
@@ -5,6 +5,7 @@ import (
"github.com/GMWalletApp/epusdt/internal/testutil" "github.com/GMWalletApp/epusdt/internal/testutil"
"github.com/GMWalletApp/epusdt/model/dao" "github.com/GMWalletApp/epusdt/model/dao"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
) )
@@ -42,6 +43,79 @@ func TestResolveChainWsURLWithRow(t *testing.T) {
} }
} }
func TestResolveChainWsURLIgnoresManualVerifyOnly(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := dao.Mdb.Create(&mdb.RpcNode{
Network: mdb.NetworkEthereum,
Url: "wss://paid-ethereum.example.com",
Type: mdb.RpcNodeTypeWs,
Weight: 100,
Enabled: true,
Purpose: mdb.RpcNodePurposeManualVerify,
Status: mdb.RpcNodeStatusOk,
}).Error; err != nil {
t.Fatalf("seed manual rpc_node: %v", err)
}
if got, ok := resolveChainWsURL(mdb.NetworkEthereum, "[TEST]"); ok {
t.Fatalf("resolveChainWsURL() = (%q, true), want false", got)
}
}
func TestResolveChainWsURLUsesGeneralWhenManualVerifyExists(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
rows := []mdb.RpcNode{
{Network: mdb.NetworkEthereum, Url: "wss://paid-ethereum.example.com", Type: mdb.RpcNodeTypeWs, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkEthereum, Url: " wss://general-ethereum.example.com ", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
}
if err := dao.Mdb.Create(&rows).Error; err != nil {
t.Fatalf("seed rpc_nodes: %v", err)
}
got, ok := resolveChainWsURL(mdb.NetworkEthereum, "[TEST]")
if !ok {
t.Fatalf("resolveChainWsURL() ok=false, want true")
}
if got != "wss://general-ethereum.example.com" {
t.Fatalf("resolveChainWsURL() = %q, want general node", got)
}
}
func TestResolveChainWsNodeSkipsCoolingNode(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
data.ResetRpcFailoverForTest()
t.Cleanup(data.ResetRpcFailoverForTest)
rows := []mdb.RpcNode{
{Network: mdb.NetworkEthereum, Url: "wss://primary-ethereum.example.com", Type: mdb.RpcNodeTypeWs, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkEthereum, Url: "wss://backup-ethereum.example.com", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
}
if err := dao.Mdb.Create(&rows).Error; err != nil {
t.Fatalf("seed rpc_nodes: %v", err)
}
primary, ok := resolveChainWsNode(mdb.NetworkEthereum, "[TEST]")
if !ok || primary.Url != "wss://primary-ethereum.example.com" {
t.Fatalf("primary node = %#v ok=%v, want primary", primary, ok)
}
for i := 0; i < data.RpcFailoverThreshold; i++ {
data.RecordRpcNodeFailure(primary.ID)
}
got, ok := resolveChainWsNode(mdb.NetworkEthereum, "[TEST]")
if !ok {
t.Fatalf("resolveChainWsNode() ok=false, want true")
}
if got.Url != "wss://backup-ethereum.example.com" {
t.Fatalf("resolveChainWsNode() = %#v, want backup", got)
}
}
func TestResolveChainWsURLDisabledRow(t *testing.T) { func TestResolveChainWsURLDisabledRow(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t) cleanup := testutil.SetupTestDatabases(t)
defer cleanup() defer cleanup()
+3 -3
View File
@@ -71,18 +71,18 @@ func runEthereumListener(contracts []common.Address) {
} }
}() }()
wsURL, ok := resolveChainWsURL(mdb.NetworkEthereum, "[ETH-WS]") wsNode, ok := resolveChainWsNode(mdb.NetworkEthereum, "[ETH-WS]")
if !ok { if !ok {
return return
} }
log.Sugar.Infof("[ETH-WS] connecting to %s watching %d contract(s)", wsURL, len(contracts)) log.Sugar.Infof("[ETH-WS] connecting using WSS node %s watching %d contract(s)", data.RpcNodeLogLabel(wsNode), len(contracts))
query := ethereum.FilterQuery{ query := ethereum.FilterQuery{
Addresses: contracts, Addresses: contracts,
Topics: [][]common.Hash{}, Topics: [][]common.Hash{},
} }
runEvmWsLogListener(ctx, "[ETH-WS]", wsURL, query, func(client *ethclient.Client, vLog types.Log) { runEvmWsLogListener(ctx, "[ETH-WS]", wsNode, query, func(client *ethclient.Client, vLog types.Log) {
if len(vLog.Topics) < 3 { if len(vLog.Topics) < 3 {
return return
} }
+44 -14
View File
@@ -2,8 +2,11 @@ package task
import ( import (
"context" "context"
"fmt"
"time" "time"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/util/log" "github.com/GMWalletApp/epusdt/util/log"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
@@ -11,18 +14,21 @@ import (
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
) )
// runEvmWsLogListener connects to wsURL, subscribes to Transfer logs, // runEvmWsLogListener connects to node.Url, subscribes to Transfer logs,
// and dispatches each log to handleLog. It retries on transient errors // and dispatches each log to handleLog. It retries on transient errors
// with exponential backoff. The ctx lets the caller trigger a clean // with exponential backoff until the node reaches the failure threshold.
// exit — e.g. when admin disables the chain, the caller cancels the // The ctx lets the caller trigger a clean exit — e.g. when admin disables
// context and the function returns instead of reconnecting forever. // the chain, the caller cancels the context and the function returns.
func runEvmWsLogListener(ctx context.Context, logPrefix, wsURL string, query ethereum.FilterQuery, handleLog func(*ethclient.Client, types.Log)) { func runEvmWsLogListener(ctx context.Context, logPrefix string, node mdb.RpcNode, query ethereum.FilterQuery, handleLog func(*ethclient.Client, types.Log)) {
const ( const (
minBackoff = 2 * time.Second minBackoff = 2 * time.Second
maxBackoff = 60 * time.Second maxBackoff = 60 * time.Second
rejoinWait = 3 * time.Second rejoinWait = 3 * time.Second
stableResetAfter = 60 * time.Second
) )
failWait := minBackoff failWait := minBackoff
wsURL := node.Url
nodeLabel := data.RpcNodeLogLabel(node)
for { for {
if ctx.Err() != nil { if ctx.Err() != nil {
@@ -32,6 +38,9 @@ func runEvmWsLogListener(ctx context.Context, logPrefix, wsURL string, query eth
client, err := ethclient.Dial(wsURL) client, err := ethclient.Dial(wsURL)
if err != nil { if err != nil {
log.Sugar.Warnf("%s dial: %v, retry in %s", logPrefix, err, failWait) log.Sugar.Warnf("%s dial: %v, retry in %s", logPrefix, err, failWait)
if recordEvmWsNodeFailure(logPrefix, node, "dial") {
return
}
if !sleepOrDone(ctx, failWait) { if !sleepOrDone(ctx, failWait) {
return return
} }
@@ -44,6 +53,9 @@ func runEvmWsLogListener(ctx context.Context, logPrefix, wsURL string, query eth
if err != nil { if err != nil {
client.Close() client.Close()
log.Sugar.Warnf("%s subscribe: %v, retry in %s", logPrefix, err, failWait) log.Sugar.Warnf("%s subscribe: %v, retry in %s", logPrefix, err, failWait)
if recordEvmWsNodeFailure(logPrefix, node, "subscribe") {
return
}
if !sleepOrDone(ctx, failWait) { if !sleepOrDone(ctx, failWait) {
return return
} }
@@ -52,20 +64,26 @@ func runEvmWsLogListener(ctx context.Context, logPrefix, wsURL string, query eth
} }
failWait = minBackoff failWait = minBackoff
log.Sugar.Infof("%s connected, subscribed to Transfer logs", logPrefix) log.Sugar.Infof("%s connected, subscribed to Transfer logs using WSS node %s", logPrefix, nodeLabel)
recvLoop(ctx, client, sub, logsCh, logPrefix, handleLog) connectedAt := time.Now()
recvErr := recvLoop(ctx, client, sub, logsCh, logPrefix, handleLog)
if ctx.Err() != nil { if ctx.Err() != nil {
return return
} }
if time.Since(connectedAt) >= stableResetAfter {
data.RecordRpcNodeSuccess(node.ID)
} else if recvErr != nil && recordEvmWsNodeFailure(logPrefix, node, recvErr.Error()) {
return
}
if !sleepOrDone(ctx, rejoinWait) { if !sleepOrDone(ctx, rejoinWait) {
return return
} }
} }
} }
func recvLoop(ctx context.Context, client *ethclient.Client, sub ethereum.Subscription, logsCh <-chan types.Log, logPrefix string, handleLog func(*ethclient.Client, types.Log)) { func recvLoop(ctx context.Context, client *ethclient.Client, sub ethereum.Subscription, logsCh <-chan types.Log, logPrefix string, handleLog func(*ethclient.Client, types.Log)) error {
defer func() { defer func() {
sub.Unsubscribe() sub.Unsubscribe()
client.Close() client.Close()
@@ -75,24 +93,36 @@ func recvLoop(ctx context.Context, client *ethclient.Client, sub ethereum.Subscr
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.Sugar.Infof("%s context cancelled, stopping", logPrefix) log.Sugar.Infof("%s context cancelled, stopping", logPrefix)
return return nil
case err := <-sub.Err(): case err := <-sub.Err():
if err != nil { if err != nil {
log.Sugar.Warnf("%s subscription error: %v, reconnecting", logPrefix, err) log.Sugar.Warnf("%s subscription error: %v, reconnecting", logPrefix, err)
return fmt.Errorf("subscription error")
} else { } else {
log.Sugar.Warnf("%s subscription closed, reconnecting", logPrefix) log.Sugar.Warnf("%s subscription closed, reconnecting", logPrefix)
return fmt.Errorf("subscription closed")
} }
return
case vLog, ok := <-logsCh: case vLog, ok := <-logsCh:
if !ok { if !ok {
log.Sugar.Warnf("%s log channel closed, reconnecting", logPrefix) log.Sugar.Warnf("%s log channel closed, reconnecting", logPrefix)
return return fmt.Errorf("log channel closed")
} }
handleLog(client, vLog) handleLog(client, vLog)
} }
} }
} }
func recordEvmWsNodeFailure(logPrefix string, node mdb.RpcNode, reason string) bool {
failures, cooling := data.RecordRpcNodeFailure(node.ID)
nodeLabel := data.RpcNodeLogLabel(node)
if !cooling {
log.Sugar.Warnf("%s WSS node failed (%s), node=%s failures=%d/%d", logPrefix, reason, nodeLabel, failures, data.RpcFailoverThreshold)
return false
}
log.Sugar.Warnf("%s WSS node reached fail threshold (%s), node=%s, resolving another node", logPrefix, reason, nodeLabel)
return true
}
// sleepOrDone waits for d or for ctx cancellation, whichever comes // sleepOrDone waits for d or for ctx cancellation, whichever comes
// first. Returns true if the sleep completed normally, false if ctx // first. Returns true if the sleep completed normally, false if ctx
// was cancelled (caller should exit). // was cancelled (caller should exit).
+3 -3
View File
@@ -65,18 +65,18 @@ func runPlasmaListener(contracts []common.Address) {
} }
}() }()
wsURL, ok := resolveChainWsURL(mdb.NetworkPlasma, "[PLASMA-WS]") wsNode, ok := resolveChainWsNode(mdb.NetworkPlasma, "[PLASMA-WS]")
if !ok { if !ok {
return return
} }
log.Sugar.Infof("[PLASMA-WS] connecting to %s watching %d contract(s)", wsURL, len(contracts)) log.Sugar.Infof("[PLASMA-WS] connecting using WSS node %s watching %d contract(s)", data.RpcNodeLogLabel(wsNode), len(contracts))
query := ethereum.FilterQuery{ query := ethereum.FilterQuery{
Addresses: contracts, Addresses: contracts,
Topics: [][]common.Hash{}, Topics: [][]common.Hash{},
} }
runEvmWsLogListener(ctx, "[PLASMA-WS]", wsURL, query, func(client *ethclient.Client, vLog types.Log) { runEvmWsLogListener(ctx, "[PLASMA-WS]", wsNode, query, func(client *ethclient.Client, vLog types.Log) {
if len(vLog.Topics) < 3 { if len(vLog.Topics) < 3 {
return return
} }
+3 -3
View File
@@ -65,18 +65,18 @@ func runPolygonListener(contracts []common.Address) {
} }
}() }()
wsURL, ok := resolveChainWsURL(mdb.NetworkPolygon, "[POLYGON-WS]") wsNode, ok := resolveChainWsNode(mdb.NetworkPolygon, "[POLYGON-WS]")
if !ok { if !ok {
return return
} }
log.Sugar.Infof("[POLYGON-WS] connecting to %s watching %d contract(s)", wsURL, len(contracts)) log.Sugar.Infof("[POLYGON-WS] connecting using WSS node %s watching %d contract(s)", data.RpcNodeLogLabel(wsNode), len(contracts))
query := ethereum.FilterQuery{ query := ethereum.FilterQuery{
Addresses: contracts, Addresses: contracts,
Topics: [][]common.Hash{}, Topics: [][]common.Hash{},
} }
runEvmWsLogListener(ctx, "[POLYGON-WS]", wsURL, query, func(client *ethclient.Client, vLog types.Log) { runEvmWsLogListener(ctx, "[POLYGON-WS]", wsNode, query, func(client *ethclient.Client, vLog types.Log) {
if len(vLog.Topics) < 3 { if len(vLog.Topics) < 3 {
return return
} }
+41 -5
View File
@@ -337,6 +337,8 @@ func processBlock(block *Block, tokenMap map[string]mdb.ChainToken) {
type Scanner struct { type Scanner struct {
baseURL string baseURL string
apiKey string apiKey string
nodeID uint64
nodeLabel string
lastBlock int64 lastBlock int64
// 统计 // 统计
totalBlocks int64 totalBlocks int64
@@ -349,16 +351,16 @@ func NewScanner() *Scanner {
} }
func (s *Scanner) Init() error { func (s *Scanner) Init() error {
baseURL, apiKey, err := service.ResolveTronNode() node, err := service.ResolveTronRpcNode()
if err != nil { if err != nil {
return fmt.Errorf("resolve tron node: %w", err) return fmt.Errorf("resolve tron node: %w", err)
} }
s.baseURL = baseURL s.useRpcNode(node)
s.apiKey = apiKey
log.Sugar.Infof("[TRON-BLOCK] node=%s", s.baseURL) log.Sugar.Infof("[TRON-BLOCK] using RPC node %s", s.nodeLabel)
block, err := GetNowBlock(s.baseURL, s.apiKey) block, err := GetNowBlock(s.baseURL, s.apiKey)
if err != nil { if err != nil {
s.recordRpcFailure("initial get latest block")
return fmt.Errorf("获取初始块失败: %w", err) return fmt.Errorf("获取初始块失败: %w", err)
} }
s.lastBlock = block.BlockHeader.RawData.Number s.lastBlock = block.BlockHeader.RawData.Number
@@ -366,6 +368,13 @@ func (s *Scanner) Init() error {
return nil return nil
} }
func (s *Scanner) useRpcNode(node *mdb.RpcNode) {
s.baseURL = strings.TrimRight(strings.TrimSpace(node.Url), "/")
s.apiKey = node.ApiKey
s.nodeID = node.ID
s.nodeLabel = data.RpcNodeLogLabel(*node)
}
func (s *Scanner) Run() { func (s *Scanner) Run() {
log.Sugar.Info("[TRON-BLOCK] start scanning (TRC20 + TRX)") log.Sugar.Info("[TRON-BLOCK] start scanning (TRC20 + TRX)")
ticker := time.NewTicker(PollInterval) ticker := time.NewTicker(PollInterval)
@@ -388,14 +397,17 @@ func (s *Scanner) poll() {
latest, err := GetNowBlock(s.baseURL, s.apiKey) latest, err := GetNowBlock(s.baseURL, s.apiKey)
if err != nil { if err != nil {
log.Sugar.Warnf("[TRON-BLOCK] get latest block: %v", err) log.Sugar.Warnf("[TRON-BLOCK] get latest block: %v", err)
s.recordRpcFailure("get latest block")
return return
} }
latestNum := latest.BlockHeader.RawData.Number latestNum := latest.BlockHeader.RawData.Number
if latestNum <= s.lastBlock { if latestNum <= s.lastBlock {
data.RecordRpcNodeSuccess(s.nodeID)
return return
} }
tokenMap := loadTronTRC20TokenMap() tokenMap := loadTronTRC20TokenMap()
hadBlockFetchError := false
for num := s.lastBlock + 1; num <= latestNum; num++ { for num := s.lastBlock + 1; num <= latestNum; num++ {
var block *Block var block *Block
if num == latestNum { if num == latestNum {
@@ -404,7 +416,8 @@ func (s *Scanner) poll() {
block, err = GetBlockByNum(s.baseURL, s.apiKey, num) block, err = GetBlockByNum(s.baseURL, s.apiKey, num)
if err != nil { if err != nil {
log.Sugar.Warnf("[TRON-BLOCK] get block %d: %v", num, err) log.Sugar.Warnf("[TRON-BLOCK] get block %d: %v", num, err)
continue hadBlockFetchError = true
break
} }
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
} }
@@ -420,6 +433,29 @@ func (s *Scanner) poll() {
} }
} }
} }
if hadBlockFetchError {
s.recordRpcFailure("get historical block")
return
}
data.RecordRpcNodeSuccess(s.nodeID)
}
func (s *Scanner) recordRpcFailure(reason string) {
failures, cooling := data.RecordRpcNodeFailure(s.nodeID)
if !cooling {
log.Sugar.Warnf("[TRON-BLOCK] RPC node failed (%s), node=%s failures=%d/%d", reason, s.nodeLabel, failures, data.RpcFailoverThreshold)
return
}
log.Sugar.Warnf("[TRON-BLOCK] RPC node reached fail threshold (%s), node=%s, trying another node", reason, s.nodeLabel)
node, err := service.ResolveTronRpcNode(s.nodeID)
if err != nil {
log.Sugar.Warnf("[TRON-BLOCK] no alternate RPC node available after failure, keep current node=%s: %v", s.nodeLabel, err)
return
}
oldLabel := s.nodeLabel
s.useRpcNode(node)
log.Sugar.Warnf("[TRON-BLOCK] switched RPC node from %s to %s", oldLabel, s.nodeLabel)
} }
func StartTronBlockScannerListener() { func StartTronBlockScannerListener() {
+102 -13
View File
@@ -1,23 +1,112 @@
package task package task
import "testing" import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
func TestTronAddressToHexConvertsBase58Contract(t *testing.T) { "github.com/GMWalletApp/epusdt/internal/testutil"
got, err := TronAddressToHex("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t") "github.com/GMWalletApp/epusdt/model/dao"
if err != nil { "github.com/GMWalletApp/epusdt/model/data"
t.Fatalf("TronAddressToHex(): %v", err) "github.com/GMWalletApp/epusdt/model/mdb"
)
func TestTronScannerSwitchesNodeAfterFailureThreshold(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
data.ResetRpcFailoverForTest()
t.Cleanup(data.ResetRpcFailoverForTest)
primary := &mdb.RpcNode{
Network: mdb.NetworkTron,
Url: "https://primary-tron.example.com",
Type: mdb.RpcNodeTypeHttp,
Weight: 100,
Enabled: true,
Purpose: mdb.RpcNodePurposeGeneral,
Status: mdb.RpcNodeStatusOk,
}
backup := &mdb.RpcNode{
Network: mdb.NetworkTron,
Url: "https://backup-tron.example.com",
Type: mdb.RpcNodeTypeHttp,
Weight: 1,
Enabled: true,
Purpose: mdb.RpcNodePurposeGeneral,
Status: mdb.RpcNodeStatusOk,
}
if err := dao.Mdb.Create(primary).Error; err != nil {
t.Fatalf("seed primary rpc_node: %v", err)
}
if err := dao.Mdb.Create(backup).Error; err != nil {
t.Fatalf("seed backup rpc_node: %v", err)
} }
const want = "41a614f803b6fd780986a42c78ec9c7f77e6ded13c" scanner := NewScanner()
if got != want { scanner.useRpcNode(primary)
t.Fatalf("TronAddressToHex() = %q, want %q", got, want) for i := 0; i < data.RpcFailoverThreshold-1; i++ {
scanner.recordRpcFailure("test")
if scanner.nodeID != primary.ID {
t.Fatalf("node switched before threshold to id=%d", scanner.nodeID)
}
}
scanner.recordRpcFailure("test")
if scanner.nodeID != backup.ID {
t.Fatalf("scanner node id = %d, want backup id=%d", scanner.nodeID, backup.ID)
}
if scanner.baseURL != backup.Url {
t.Fatalf("scanner baseURL = %q, want %q", scanner.baseURL, backup.Url)
} }
} }
func TestNormalizeTronHexAddress(t *testing.T) { func TestTronScannerStopsOnHistoricalBlockFetchError(t *testing.T) {
got := normalizeTronHexAddress("0X41A614F803B6FD780986A42C78EC9C7F77E6DED13C") cleanup := testutil.SetupTestDatabases(t)
const want = "41a614f803b6fd780986a42c78ec9c7f77e6ded13c" defer cleanup()
if got != want { data.ResetRpcFailoverForTest()
t.Fatalf("normalizeTronHexAddress() = %q, want %q", got, want) t.Cleanup(data.ResetRpcFailoverForTest)
node := &mdb.RpcNode{
Network: mdb.NetworkTron,
Type: mdb.RpcNodeTypeHttp,
Weight: 1,
Enabled: true,
Purpose: mdb.RpcNodePurposeGeneral,
Status: mdb.RpcNodeStatusOk,
}
if err := dao.Mdb.Create(node).Error; err != nil {
t.Fatalf("seed rpc_node: %v", err)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/wallet/getnowblock":
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"block_header": map[string]interface{}{
"raw_data": map[string]interface{}{
"number": 3,
"timestamp": int64(1000),
},
},
"transactions": []interface{}{},
})
case "/wallet/getblockbynum":
http.Error(w, "temporary", http.StatusBadGateway)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
node.Url = server.URL
scanner := NewScanner()
scanner.useRpcNode(node)
scanner.lastBlock = 1
scanner.poll()
if scanner.lastBlock != 1 {
t.Fatalf("lastBlock = %d, want 1 so failed block is retried", scanner.lastBlock)
} }
} }
+4 -4
View File
@@ -14,9 +14,9 @@ import (
const rpcProbeTimeout = 5 * time.Second const rpcProbeTimeout = 5 * time.Second
// RpcHealthJob periodically probes every enabled rpc_nodes row and // RpcHealthJob periodically probes enabled general/both rpc_nodes rows.
// writes status/last_latency_ms. Results drive SelectRpcNode weighted // Manual verification nodes are left for on-demand admin checks so paid
// picking at runtime. // endpoints are not consumed by the scheduler.
type RpcHealthJob struct{} type RpcHealthJob struct{}
var gRpcHealthJobLock sync.Mutex var gRpcHealthJobLock sync.Mutex
@@ -25,7 +25,7 @@ func (r RpcHealthJob) Run() {
gRpcHealthJobLock.Lock() gRpcHealthJobLock.Lock()
defer gRpcHealthJobLock.Unlock() defer gRpcHealthJobLock.Unlock()
nodes, err := data.ListRpcNodes("") nodes, err := data.ListRpcNodesForHealth()
if err != nil { if err != nil {
log.Sugar.Errorf("[rpc-health] list nodes err=%v", err) log.Sugar.Errorf("[rpc-health] list nodes err=%v", err)
return return
+58
View File
@@ -1,5 +1,7 @@
package constant package constant
import "errors"
var Errno = map[int]string{ var Errno = map[int]string{
400: "system error", 400: "system error",
401: "signature verification failed", 401: "signature verification failed",
@@ -21,6 +23,30 @@ var Errno = map[int]string{
10017: "payment provider is not enabled", 10017: "payment provider is not enabled",
10018: "payment provider configuration is incomplete", 10018: "payment provider configuration is incomplete",
10019: "payment provider does not support this token or network", 10019: "payment provider does not support this token or network",
10020: "invalid rpc node purpose",
10021: "invalid rpc node url",
10022: "rpc node http url required",
10023: "rpc node websocket url required",
10024: "invalid rpc node type",
10025: "invalid username or password",
10026: "admin user disabled",
10027: "admin unauthorized",
10028: "admin user not found",
10029: "old password incorrect",
10030: "wallet not found",
10031: "api key not found",
10032: "rpc node not found",
10033: "order callback not applicable",
10034: "order notify url empty",
10035: "resend callback failed",
10036: "invalid notification config",
10037: "invalid notification events",
10038: "manual payment verification failed",
10039: "manual payment only supports on-chain orders",
10040: "initial admin password unavailable, check from logs",
10041: "invalid notify url",
10042: "payment provider order creation failed",
10043: "invalid setting item",
} }
var ( var (
@@ -44,6 +70,30 @@ var (
PaymentProviderNotEnabled = Err(10017) PaymentProviderNotEnabled = Err(10017)
PaymentProviderConfigErr = Err(10018) PaymentProviderConfigErr = Err(10018)
PaymentProviderNotSupport = Err(10019) PaymentProviderNotSupport = Err(10019)
RpcNodePurposeErr = Err(10020)
RpcNodeURLErr = Err(10021)
RpcNodeHTTPURLErr = Err(10022)
RpcNodeWebSocketURLErr = Err(10023)
RpcNodeTypeErr = Err(10024)
AdminInvalidCredentialErr = Err(10025)
AdminUserDisabledErr = Err(10026)
AdminUnauthorizedErr = Err(10027)
AdminUserNotFoundErr = Err(10028)
AdminOldPasswordErr = Err(10029)
WalletNotFoundErr = Err(10030)
ApiKeyNotFoundErr = Err(10031)
RpcNodeNotFoundErr = Err(10032)
OrderCallbackNotApplicable = Err(10033)
OrderNotifyURLEmptyErr = Err(10034)
OrderResendCallbackErr = Err(10035)
NotificationConfigErr = Err(10036)
NotificationEventsErr = Err(10037)
ManualPaymentVerifyErr = Err(10038)
ManualPaymentProviderErr = Err(10039)
InitialAdminPasswordErr = Err(10040)
NotifyURLErr = Err(10041)
PaymentProviderCreateErr = Err(10042)
SettingItemErr = Err(10043)
) )
type RspError struct { type RspError struct {
@@ -67,6 +117,14 @@ func (re *RspError) Render() (code int, msg string) {
return re.Code, re.Msg return re.Code, re.Msg
} }
func ResolveErrno(err error) (code int, msg string) {
var rspErr *RspError
if errors.As(err, &rspErr) {
return rspErr.Render()
}
return SystemErr.(*RspError).Render()
}
// HttpStatus maps a RspError code to the HTTP status the handler // HttpStatus maps a RspError code to the HTTP status the handler
// should use on the wire. Small codes (< 1000) are treated as real // should use on the wire. Small codes (< 1000) are treated as real
// HTTP status codes (e.g. 400 system error, 401 signature failure) so // HTTP status codes (e.g. 400 system error, 401 signature failure) so
+7 -7
View File
@@ -1,6 +1,7 @@
package http package http
import ( import (
"errors"
"github.com/GMWalletApp/epusdt/util/constant" "github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/page" "github.com/GMWalletApp/epusdt/util/page"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@@ -65,13 +66,12 @@ func (r *Resp) SucJsonPage(e echo.Context, data interface{}, pagination page.Pag
func (r *Resp) FailJson(e echo.Context, err error) error { func (r *Resp) FailJson(e echo.Context, err error) error {
rr := new(Response) rr := new(Response)
httpStatus := http.StatusBadRequest httpStatus := http.StatusBadRequest
switch t := err.(type) { var rspErr *constant.RspError
case *constant.RspError: if errors.As(err, &rspErr) {
rr.StatusCode, rr.Message = t.Render() rr.StatusCode, rr.Message = rspErr.Render()
httpStatus = t.HttpStatus() httpStatus = rspErr.HttpStatus()
default: } else {
rr.StatusCode = http.StatusBadRequest rr.StatusCode, rr.Message = constant.ResolveErrno(err)
rr.Message = err.Error()
} }
rr.RequestID = e.Request().Header.Get(echo.HeaderXRequestID) rr.RequestID = e.Request().Header.Get(echo.HeaderXRequestID)
return r.Json(e, httpStatus, &rr) return r.Json(e, httpStatus, &rr)
+61
View File
@@ -0,0 +1,61 @@
package http
import (
"encoding/json"
"errors"
"fmt"
nethttp "net/http"
"net/http/httptest"
"testing"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4"
)
func TestFailJsonHidesPlainErrorsBehindSystemErrno(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(nethttp.MethodGet, "/", nil)
rec := httptest.NewRecorder()
ctx := e.NewContext(req, rec)
if err := new(Resp).FailJson(ctx, errors.New("database connection details")); err != nil {
t.Fatalf("FailJson returned error: %v", err)
}
if rec.Code != nethttp.StatusBadRequest {
t.Fatalf("http status = %d, want 400", rec.Code)
}
var body map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
if got := int(body["status_code"].(float64)); got != 400 {
t.Fatalf("status_code = %d, want 400; body=%v", got, body)
}
if got, _ := body["message"].(string); got != constant.Errno[400] {
t.Fatalf("message = %q, want %q", got, constant.Errno[400])
}
}
func TestFailJsonPreservesWrappedRspErrorErrno(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(nethttp.MethodGet, "/", nil)
rec := httptest.NewRecorder()
ctx := e.NewContext(req, rec)
err := fmt.Errorf("wrap: %w", constant.OrderNotExists)
if callErr := new(Resp).FailJson(ctx, err); callErr != nil {
t.Fatalf("FailJson returned error: %v", callErr)
}
var body map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
if got := int(body["status_code"].(float64)); got != 10008 {
t.Fatalf("status_code = %d, want 10008; body=%v", got, body)
}
if got, _ := body["message"].(string); got != constant.Errno[10008] {
t.Fatalf("message = %q, want %q", got, constant.Errno[10008])
}
}
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./unauthorized-error-Dobf70xk.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./unauthorized-error-D6E2I2cs.js";var t=e;export{t as component};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./forbidden-C53ESqXn.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./forbidden-Cuuo0D_A.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./not-found-error-Cbs2xbU5.js";var t=e;export{t as component};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./not-found-error-CrcHrNtf.js";var t=e;export{t as component};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./general-error-B3tm7Ven.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./general-error-D1_cvFwU.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./maintenance-error-CG6hQ3sq.js";var t=e;export{t as component};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./maintenance-error-aweE6mOQ.js";var t=e;export{t as component};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Af as n}from"./messages-a04pwL1_.js";import{o as r,s as i,t as a}from"./useRouter-DtTm7XkK.js";import{t as o}from"./useStore-CupyIEEP.js";import{f as s}from"./ClientOnly-CsiZapiS.js";import"./redirect-DMNGvjzO.js";import{a as c,i as l,r as u,s as d,t as f}from"./Match-CHiu67uZ.js";import{n as p}from"./matchContext-BFCdcHzo.js";import{t as m}from"./atom-DQpaPCwp.js";function h(e){if(typeof document<`u`&&document.querySelector){let t=e.stores.location.get(),n=t.state.__hashScrollIntoViewOptions??!0;if(n&&t.hash!==``){let e=document.getElementById(t.hash);e&&e.scrollIntoView(n)}}}var g=e(t(),1);function _(){let e=a(),t=g.useRef({router:e,mounted:!1}),[n,c]=g.useState(!1),l=o(e.stores.isLoading,e=>e),u=o(e.stores.hasPending,e=>e),f=i(l),p=l||n||u,_=i(p),v=l||u,y=i(v);return e.startTransition=e=>{c(!0),g.startTransition(()=>{e(),c(!1)})},g.useEffect(()=>{let t=e.history.subscribe(e.load),n=e.buildLocation({to:e.latestLocation.pathname,search:!0,params:!0,hash:!0,state:!0,_includeValidateSearch:!0});return s(e.latestLocation.publicHref)!==s(n.publicHref)&&e.commitLocation({...n,replace:!0}),()=>{t()}},[e,e.history]),r(()=>{typeof window<`u`&&e.ssr||t.current.router===e&&t.current.mounted||(t.current={router:e,mounted:!0},(async()=>{try{await e.load()}catch(e){console.error(e)}})())},[e]),r(()=>{f&&!l&&e.emit({type:`onLoad`,...d(e.stores.location.get(),e.stores.resolvedLocation.get())})},[f,e,l]),r(()=>{y&&!v&&e.emit({type:`onBeforeRouteMount`,...d(e.stores.location.get(),e.stores.resolvedLocation.get())})},[v,y,e]),r(()=>{if(_&&!p){let t=d(e.stores.location.get(),e.stores.resolvedLocation.get());e.emit({type:`onResolved`,...t}),m(()=>{e.stores.status.set(`idle`),e.stores.resolvedLocation.set(e.stores.location.get())}),t.hrefChanged&&h(e)}},[p,_,e]),null}var v=n();function y(){let e=a(),t=e.routesById.__root__.options.pendingComponent??e.options.defaultPendingComponent,n=t?(0,v.jsx)(t,{}):null,r=(0,v.jsxs)(typeof document<`u`&&e.ssr?u:g.Suspense,{fallback:n,children:[(0,v.jsx)(_,{}),(0,v.jsx)(b,{})]});return e.options.InnerWrap?(0,v.jsx)(e.options.InnerWrap,{children:r}):r}function b(){let e=a(),t=o(e.stores.firstId,e=>e),n=o(e.stores.loadedAt,e=>e),r=t?(0,v.jsx)(f,{matchId:t}):null;return(0,v.jsx)(p.Provider,{value:t,children:e.options.disableGlobalCatchBoundary?r:(0,v.jsx)(l,{getResetKey:()=>n,errorComponent:c,onCatch:void 0,children:r})})}function x(){let e=a();return o(e.stores.matchRouteDeps,e=>e),g.useCallback(t=>{let{pending:n,caseSensitive:r,fuzzy:i,includeSearch:a,...o}=t;return e.matchRoute(o,{pending:n,caseSensitive:r,fuzzy:i,includeSearch:a})},[e])}export{x as n,y as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{dp as n}from"./messages-Bbvu5aaF.js";import{o as r,s as i,t as a}from"./useRouter-DtTm7XkK.js";import{t as o}from"./useStore-CupyIEEP.js";import{f as s}from"./ClientOnly-39EveoQU.js";import"./redirect-DMNGvjzO.js";import{a as c,i as l,r as u,s as d,t as f}from"./Match-D2hlI1L9.js";import{n as p}from"./matchContext-BFCdcHzo.js";import{t as m}from"./atom-DQpaPCwp.js";function h(e){if(typeof document<`u`&&document.querySelector){let t=e.stores.location.get(),n=t.state.__hashScrollIntoViewOptions??!0;if(n&&t.hash!==``){let e=document.getElementById(t.hash);e&&e.scrollIntoView(n)}}}var g=e(t(),1);function _(){let e=a(),t=g.useRef({router:e,mounted:!1}),[n,c]=g.useState(!1),l=o(e.stores.isLoading,e=>e),u=o(e.stores.hasPending,e=>e),f=i(l),p=l||n||u,_=i(p),v=l||u,y=i(v);return e.startTransition=e=>{c(!0),g.startTransition(()=>{e(),c(!1)})},g.useEffect(()=>{let t=e.history.subscribe(e.load),n=e.buildLocation({to:e.latestLocation.pathname,search:!0,params:!0,hash:!0,state:!0,_includeValidateSearch:!0});return s(e.latestLocation.publicHref)!==s(n.publicHref)&&e.commitLocation({...n,replace:!0}),()=>{t()}},[e,e.history]),r(()=>{typeof window<`u`&&e.ssr||t.current.router===e&&t.current.mounted||(t.current={router:e,mounted:!0},(async()=>{try{await e.load()}catch(e){console.error(e)}})())},[e]),r(()=>{f&&!l&&e.emit({type:`onLoad`,...d(e.stores.location.get(),e.stores.resolvedLocation.get())})},[f,e,l]),r(()=>{y&&!v&&e.emit({type:`onBeforeRouteMount`,...d(e.stores.location.get(),e.stores.resolvedLocation.get())})},[v,y,e]),r(()=>{if(_&&!p){let t=d(e.stores.location.get(),e.stores.resolvedLocation.get());e.emit({type:`onResolved`,...t}),m(()=>{e.stores.status.set(`idle`),e.stores.resolvedLocation.set(e.stores.location.get())}),t.hrefChanged&&h(e)}},[p,_,e]),null}var v=n();function y(){let e=a(),t=e.routesById.__root__.options.pendingComponent??e.options.defaultPendingComponent,n=t?(0,v.jsx)(t,{}):null,r=(0,v.jsxs)(typeof document<`u`&&e.ssr?u:g.Suspense,{fallback:n,children:[(0,v.jsx)(_,{}),(0,v.jsx)(b,{})]});return e.options.InnerWrap?(0,v.jsx)(e.options.InnerWrap,{children:r}):r}function b(){let e=a(),t=o(e.stores.firstId,e=>e),n=o(e.stores.loadedAt,e=>e),r=t?(0,v.jsx)(f,{matchId:t}):null;return(0,v.jsx)(p.Provider,{value:t,children:e.options.disableGlobalCatchBoundary?r:(0,v.jsx)(l,{getResetKey:()=>n,errorComponent:c,onCatch:void 0,children:r})})}function x(){let e=a();return o(e.stores.matchRouteDeps,e=>e),g.useCallback(t=>{let{pending:n,caseSensitive:r,fuzzy:i,includeSearch:a,...o}=t;return e.matchRoute(o,{pending:n,caseSensitive:r,fuzzy:i,includeSearch:a})},[e])}export{x as n,y as t};
+1
View File
@@ -0,0 +1 @@
import{dp as e}from"./messages-Bbvu5aaF.js";import{m as t}from"./search-provider-CMLTXja2.js";import{n,t as r}from"./theme-switch-g0D_A-ng.js";import{t as i}from"./general-error-B3tm7Ven.js";import{t as a}from"./not-found-error-CrcHrNtf.js";import{t as o}from"./_error-CBKE93YH.js";import{t as s}from"./unauthorized-error-Dobf70xk.js";import{t as c}from"./forbidden-C53ESqXn.js";import{t as l}from"./maintenance-error-aweE6mOQ.js";import{n as u,r as d,t as f}from"./header-Bfa1tMiw.js";var p=e();function m(){let{error:e}=o.useParams(),m={unauthorized:s,forbidden:c,"not-found":a,"internal-server-error":i,"maintenance-error":l}[e]||a;return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(f,{className:`border-b`,fixed:!0,children:[(0,p.jsx)(u,{}),(0,p.jsxs)(`div`,{className:`ms-auto flex items-center space-x-4`,children:[(0,p.jsx)(n,{}),(0,p.jsx)(r,{}),(0,p.jsx)(t,{}),(0,p.jsx)(d,{})]})]}),(0,p.jsx)(`div`,{className:`flex-1 [&>div]:h-full`,children:(0,p.jsx)(m,{})})]})}export{m as component};
-1
View File
@@ -1 +0,0 @@
import{Af as e}from"./messages-a04pwL1_.js";import{m as t}from"./search-provider-CK5PTj-3.js";import{n,t as r}from"./theme-switch-Calzw4i8.js";import{t as i}from"./general-error-D1_cvFwU.js";import{t as a}from"./not-found-error-Cbs2xbU5.js";import{t as o}from"./_error-Bz322TH4.js";import{t as s}from"./unauthorized-error-D6E2I2cs.js";import{t as c}from"./forbidden-Cuuo0D_A.js";import{t as l}from"./maintenance-error-CG6hQ3sq.js";import{n as u,r as d,t as f}from"./header-IIm-5sfl.js";var p=e();function m(){let{error:e}=o.useParams(),m={unauthorized:s,forbidden:c,"not-found":a,"internal-server-error":i,"maintenance-error":l}[e]||a;return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(f,{className:`border-b`,fixed:!0,children:[(0,p.jsx)(u,{}),(0,p.jsxs)(`div`,{className:`ms-auto flex items-center space-x-4`,children:[(0,p.jsx)(n,{}),(0,p.jsx)(r,{}),(0,p.jsx)(t,{}),(0,p.jsx)(d,{})]})]}),(0,p.jsx)(`div`,{className:`flex-1 [&>div]:h-full`,children:(0,p.jsx)(m,{})})]})}export{m as component};
-2
View File
@@ -1,2 +0,0 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_error-BqMJbzwU.js","assets/search-provider-CK5PTj-3.js","assets/chunk-DECur_0Z.js","assets/dist-CRowTfGU.js","assets/dist-CDVNvXxU.js","assets/messages-a04pwL1_.js","assets/react-CO2uhaBc.js","assets/confirm-dialog-D7ACP12b.js","assets/button-CYi3aIkB.js","assets/clsx-D9aGYY3V.js","assets/dist-DlgHyFeS.js","assets/dist-DXs3DLXA.js","assets/dist-JWsv7m_D.js","assets/dist-zN1ah_GY.js","assets/es2015-BA51vX-r.js","assets/dist-Db-AFHTl.js","assets/dist-Dw4KnGRx.js","assets/dist-CgDchsPc.js","assets/dist-CkoYnY3E.js","assets/dist-luTOV918.js","assets/dist-BixeH3nX.js","assets/dist-CRIkojQ7.js","assets/separator-WgrtiSAc.js","assets/tooltip-CQmL0SjD.js","assets/dist-ctTJ4wXQ.js","assets/dist-8HA91BP-.js","assets/auth-store-D-hE9_JG.js","assets/dist-UPs4wRk9.js","assets/with-selector-DBfssCrY.js","assets/cookies-BzZOQYPw.js","assets/useRouter-DtTm7XkK.js","assets/useNavigate-7u4DX0Ho.js","assets/useStore-CupyIEEP.js","assets/command-BhAZ2WFL.js","assets/createLucideIcon-Br0Bd5k2.js","assets/dialog-ChTTLrcb.js","assets/skeleton-DD6-GM15.js","assets/wallet-DpVSAZti.js","assets/sun-BwqrnykR.js","assets/shield-check-CiWq1V4z.js","assets/logo-l7kBPv3Y.js","assets/input-CBgRT74-.js","assets/font-provider-CDmc3dQm.js","assets/theme-provider-ClDDiEyS.js","assets/theme-switch-Calzw4i8.js","assets/dropdown-menu-C3ZAaLkh.js","assets/check-DldThLle.js","assets/header-IIm-5sfl.js","assets/link-N06pvHXW.js","assets/ClientOnly-CsiZapiS.js","assets/forbidden-Cuuo0D_A.js","assets/general-error-D1_cvFwU.js","assets/maintenance-error-CG6hQ3sq.js","assets/not-found-error-Cbs2xbU5.js","assets/unauthorized-error-D6E2I2cs.js"])))=>i.map(i=>d[i]);
import{n as e,t}from"./lazyRouteComponent-BHUIfFWA.js";import{t as n}from"./preload-helper-vQKTmgnp.js";var r=e(`/_authenticated/errors/$error`)({component:t(()=>n(()=>import(`./_error-BqMJbzwU.js`),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54])),`component`)});export{r as t};
+2
View File
@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_error-BUB-_p3M.js","assets/search-provider-CMLTXja2.js","assets/chunk-DECur_0Z.js","assets/dist-CRowTfGU.js","assets/dist-FGEIDBp-.js","assets/messages-Bbvu5aaF.js","assets/react-CO2uhaBc.js","assets/confirm-dialog-CIKVlzfB.js","assets/button-DlGFBuNZ.js","assets/clsx-D9aGYY3V.js","assets/dist-B_ALJuOq.js","assets/dist-Cy-xiyt7.js","assets/dist--ZhT0Dzg.js","assets/dist-BkAVwTGu.js","assets/es2015-b-3Ydtpy.js","assets/dist-BaRXEuYn.js","assets/dist-DI4dxjh-.js","assets/dist-BHsKknyh.js","assets/dist-DRkG7ylN.js","assets/dist-CLy3Z8cG.js","assets/dist-BixeH3nX.js","assets/dist-BYCFrz1s.js","assets/separator-CRhLMH2U.js","assets/tooltip-CXiEyuQR.js","assets/dist-l3nGCB93.js","assets/dist-mVZdxKLu.js","assets/auth-store-BnVIXJIp.js","assets/dist-0D92g85D.js","assets/with-selector-DBfssCrY.js","assets/cookies-BzZOQYPw.js","assets/useRouter-DtTm7XkK.js","assets/useNavigate-7u4DX0Ho.js","assets/useStore-CupyIEEP.js","assets/command-CAasmd2P.js","assets/createLucideIcon-Br0Bd5k2.js","assets/dialog-DCa0Wx6n.js","assets/skeleton-CPJRjth3.js","assets/wallet-GeyQ8E8l.js","assets/sun-BwqrnykR.js","assets/shield-check-CiWq1V4z.js","assets/logo-DB0DDsg6.js","assets/input-CJ09jXcS.js","assets/font-provider-CUqnfmoa.js","assets/theme-provider-l0tGtGY9.js","assets/theme-switch-g0D_A-ng.js","assets/dropdown-menu-Ci0ZDJlU.js","assets/check-DldThLle.js","assets/header-Bfa1tMiw.js","assets/link-BwMKq7lq.js","assets/ClientOnly-39EveoQU.js","assets/forbidden-C53ESqXn.js","assets/general-error-B3tm7Ven.js","assets/maintenance-error-aweE6mOQ.js","assets/not-found-error-CrcHrNtf.js","assets/unauthorized-error-Dobf70xk.js"])))=>i.map(i=>d[i]);
import{n as e,t}from"./lazyRouteComponent-D7-crJIH.js";import{t as n}from"./preload-helper-vQKTmgnp.js";var r=e(`/_authenticated/errors/$error`)({component:t(()=>n(()=>import(`./_error-BUB-_p3M.js`),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54])),`component`)});export{r as t};
File diff suppressed because one or more lines are too long
-2
View File
@@ -1,2 +0,0 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_trade_id-B85-gvyN.js","assets/chunk-DECur_0Z.js","assets/button-CYi3aIkB.js","assets/clsx-D9aGYY3V.js","assets/messages-a04pwL1_.js","assets/react-CO2uhaBc.js","assets/select-hdWA6aFg.js","assets/dist-ctTJ4wXQ.js","assets/dist-CDVNvXxU.js","assets/dist-JWsv7m_D.js","assets/dist-zN1ah_GY.js","assets/dist-CRIkojQ7.js","assets/dist-CRowTfGU.js","assets/dist-luTOV918.js","assets/dist-Dw4KnGRx.js","assets/dist-DXs3DLXA.js","assets/es2015-BA51vX-r.js","assets/dist-BixeH3nX.js","assets/dist-8HA91BP-.js","assets/createLucideIcon-Br0Bd5k2.js","assets/check-DldThLle.js","assets/chevron-down-BPPKEraq.js","assets/auth-store-D-hE9_JG.js","assets/dist-UPs4wRk9.js","assets/with-selector-DBfssCrY.js","assets/cookies-BzZOQYPw.js","assets/useNavigate-7u4DX0Ho.js","assets/useRouter-DtTm7XkK.js","assets/copy-to-clipboard--eZunBzP.js","assets/arrow-left-Ck1MERx0.js","assets/radio-group-CexQxA-X.js","assets/dist-CgDchsPc.js","assets/dist-Db-AFHTl.js","assets/dist-CkoYnY3E.js","assets/loader-circle-DRj7MiMc.js","assets/send-g-CrrC4k.js","assets/triangle-alert-BEkslGAT.js","assets/dialog-ChTTLrcb.js","assets/dist-DlgHyFeS.js","assets/labels-BDaRvEke.js","assets/input-CBgRT74-.js","assets/checkout-model-BfGeSuFW.js"])))=>i.map(i=>d[i]);
import{n as e,t}from"./lazyRouteComponent-BHUIfFWA.js";import{t as n}from"./preload-helper-vQKTmgnp.js";var r=e(`/cashier/$trade_id/`)({component:t(()=>n(()=>import(`./_trade_id-B85-gvyN.js`),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41])),`component`)});export{r as t};
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_trade_id-D2lai0y2.js","assets/chunk-DECur_0Z.js","assets/button-DlGFBuNZ.js","assets/clsx-D9aGYY3V.js","assets/messages-Bbvu5aaF.js","assets/react-CO2uhaBc.js","assets/select-BRRg0L32.js","assets/dist-l3nGCB93.js","assets/dist-FGEIDBp-.js","assets/dist--ZhT0Dzg.js","assets/dist-BkAVwTGu.js","assets/dist-BYCFrz1s.js","assets/dist-CRowTfGU.js","assets/dist-CLy3Z8cG.js","assets/dist-DI4dxjh-.js","assets/dist-Cy-xiyt7.js","assets/es2015-b-3Ydtpy.js","assets/dist-BixeH3nX.js","assets/dist-mVZdxKLu.js","assets/createLucideIcon-Br0Bd5k2.js","assets/check-DldThLle.js","assets/chevron-down-BPPKEraq.js","assets/auth-store-BnVIXJIp.js","assets/dist-0D92g85D.js","assets/with-selector-DBfssCrY.js","assets/cookies-BzZOQYPw.js","assets/useNavigate-7u4DX0Ho.js","assets/useRouter-DtTm7XkK.js","assets/copy-to-clipboard--eZunBzP.js","assets/arrow-left-Ck1MERx0.js","assets/radio-group-DIzxqq_2.js","assets/dist-BHsKknyh.js","assets/dist-BaRXEuYn.js","assets/dist-DRkG7ylN.js","assets/loader-circle-DRj7MiMc.js","assets/send-g-CrrC4k.js","assets/triangle-alert-BEkslGAT.js","assets/dialog-DCa0Wx6n.js","assets/dist-B_ALJuOq.js","assets/labels-DcMMAJnk.js","assets/input-CJ09jXcS.js","assets/checkout-model-BfGeSuFW.js"])))=>i.map(i=>d[i]);
import{n as e,t}from"./lazyRouteComponent-D7-crJIH.js";import{t as n}from"./preload-helper-vQKTmgnp.js";var r=e(`/cashier/$trade_id/`)({component:t(()=>n(()=>import(`./_trade_id-D2lai0y2.js`),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41])),`component`)});export{r as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{Af as e,_n as t,gn as n,hn as r,mn as i,pn as a,vf as o,vn as s,yf as c,yn as l}from"./messages-a04pwL1_.js";import{i as u,n as d,t as f}from"./button-CYi3aIkB.js";import{n as p,r as m}from"./font-provider-CDmc3dQm.js";import{n as h}from"./theme-provider-ClDDiEyS.js";import{t as g}from"./chevron-down-BPPKEraq.js";import{n as _,t as v}from"./radio-group-CexQxA-X.js";import{r as y,s as b}from"./zod-CQemhjci.js";import{a as x,c as S,i as C,l as w,n as T,o as E,r as D,s as O,t as k}from"./form-Bwi_c-SI.js";import{t as A}from"./page-header-yGXiG9iK.js";import{t as j}from"./show-submitted-data-D7TduAhD.js";var M=e(),N=b({theme:y([`light`,`dark`]),font:y(m)});function P(){let{font:e,setFont:s}=p(),{theme:l,setTheme:y}=h(),b={theme:l,font:e},A=w({resolver:S(N),defaultValues:b});function P(t){t.font!==e&&s(t.font),t.theme!==l&&y(t.theme),j(t)}return(0,M.jsx)(k,{...A,children:(0,M.jsxs)(`form`,{className:`space-y-8`,onSubmit:A.handleSubmit(P),children:[(0,M.jsx)(C,{control:A.control,name:`font`,render:({field:e})=>(0,M.jsxs)(x,{children:[(0,M.jsx)(E,{children:t()}),(0,M.jsxs)(`div`,{className:`relative w-max`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(`select`,{className:u(d({variant:`outline`}),`w-50 appearance-none font-normal capitalize`,`dark:bg-background dark:hover:bg-background`),...e,children:m.map(e=>(0,M.jsx)(`option`,{value:e,children:e},e))})}),(0,M.jsx)(g,{className:`absolute inset-e-3 top-2.5 h-4 w-4 opacity-50`})]}),(0,M.jsx)(D,{className:`font-manrope`,children:n()}),(0,M.jsx)(O,{})]})}),(0,M.jsx)(C,{control:A.control,name:`theme`,render:({field:e})=>(0,M.jsxs)(x,{children:[(0,M.jsx)(E,{children:r()}),(0,M.jsx)(D,{children:i()}),(0,M.jsx)(O,{}),(0,M.jsxs)(v,{className:`grid max-w-md grid-cols-2 gap-8 pt-2`,defaultValue:e.value,onValueChange:e.onChange,children:[(0,M.jsx)(x,{children:(0,M.jsxs)(E,{className:`[&:has([data-state=checked])>div]:border-primary`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(_,{className:`sr-only`,value:`light`})}),(0,M.jsx)(`div`,{className:`items-center rounded-md border-2 border-muted p-1 hover:border-accent`,children:(0,M.jsxs)(`div`,{className:`space-y-2 rounded-sm bg-[#ecedef] p-2`,children:[(0,M.jsxs)(`div`,{className:`space-y-2 rounded-md bg-white p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-2 w-20 rounded-lg bg-[#ecedef]`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-[#ecedef]`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-white p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-[#ecedef]`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-[#ecedef]`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-white p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-[#ecedef]`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-[#ecedef]`})]})]})}),(0,M.jsx)(`span`,{className:`block w-full p-2 text-center font-normal`,children:c()})]})}),(0,M.jsx)(x,{children:(0,M.jsxs)(E,{className:`[&:has([data-state=checked])>div]:border-primary`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(_,{className:`sr-only`,value:`dark`})}),(0,M.jsx)(`div`,{className:`items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground`,children:(0,M.jsxs)(`div`,{className:`space-y-2 rounded-sm bg-slate-950 p-2`,children:[(0,M.jsxs)(`div`,{className:`space-y-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-2 w-20 rounded-lg bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]})]})}),(0,M.jsx)(`span`,{className:`block w-full p-2 text-center font-normal`,children:o()})]})})]})]})}),(0,M.jsx)(f,{type:`submit`,children:a()})]})})}function F(){return(0,M.jsx)(A,{description:s(),title:l(),variant:`section`,children:(0,M.jsx)(P,{})})}var I=F;export{I as component}; import{$f as e,_n as t,bn as n,dp as r,ep as i,gn as a,hn as o,mn as s,vn as c,yn as l}from"./messages-Bbvu5aaF.js";import{i as u,n as d,t as f}from"./button-DlGFBuNZ.js";import{n as p,r as m}from"./font-provider-CUqnfmoa.js";import{n as h}from"./theme-provider-l0tGtGY9.js";import{t as g}from"./chevron-down-BPPKEraq.js";import{n as _,t as v}from"./radio-group-DIzxqq_2.js";import{r as y,s as b}from"./zod-CQemhjci.js";import{a as x,c as S,i as C,l as w,n as T,o as E,r as D,s as O,t as k}from"./form-CwBNjUHP.js";import{t as A}from"./page-header-v0-NzBJQ.js";import{t as j}from"./show-submitted-data-C_xyg4eH.js";var M=r(),N=b({theme:y([`light`,`dark`]),font:y(m)});function P(){let{font:n,setFont:r}=p(),{theme:l,setTheme:y}=h(),b={theme:l,font:n},A=w({resolver:S(N),defaultValues:b});function P(e){e.font!==n&&r(e.font),e.theme!==l&&y(e.theme),j(e)}return(0,M.jsx)(k,{...A,children:(0,M.jsxs)(`form`,{className:`space-y-8`,onSubmit:A.handleSubmit(P),children:[(0,M.jsx)(C,{control:A.control,name:`font`,render:({field:e})=>(0,M.jsxs)(x,{children:[(0,M.jsx)(E,{children:c()}),(0,M.jsxs)(`div`,{className:`relative w-max`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(`select`,{className:u(d({variant:`outline`}),`w-50 appearance-none font-normal capitalize`,`dark:bg-background dark:hover:bg-background`),...e,children:m.map(e=>(0,M.jsx)(`option`,{value:e,children:e},e))})}),(0,M.jsx)(g,{className:`absolute inset-e-3 top-2.5 h-4 w-4 opacity-50`})]}),(0,M.jsx)(D,{className:`font-manrope`,children:t()}),(0,M.jsx)(O,{})]})}),(0,M.jsx)(C,{control:A.control,name:`theme`,render:({field:t})=>(0,M.jsxs)(x,{children:[(0,M.jsx)(E,{children:a()}),(0,M.jsx)(D,{children:o()}),(0,M.jsx)(O,{}),(0,M.jsxs)(v,{className:`grid max-w-md grid-cols-2 gap-8 pt-2`,defaultValue:t.value,onValueChange:t.onChange,children:[(0,M.jsx)(x,{children:(0,M.jsxs)(E,{className:`[&:has([data-state=checked])>div]:border-primary`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(_,{className:`sr-only`,value:`light`})}),(0,M.jsx)(`div`,{className:`items-center rounded-md border-2 border-muted p-1 hover:border-accent`,children:(0,M.jsxs)(`div`,{className:`space-y-2 rounded-sm bg-[#ecedef] p-2`,children:[(0,M.jsxs)(`div`,{className:`space-y-2 rounded-md bg-white p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-2 w-20 rounded-lg bg-[#ecedef]`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-[#ecedef]`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-white p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-[#ecedef]`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-[#ecedef]`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-white p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-[#ecedef]`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-[#ecedef]`})]})]})}),(0,M.jsx)(`span`,{className:`block w-full p-2 text-center font-normal`,children:i()})]})}),(0,M.jsx)(x,{children:(0,M.jsxs)(E,{className:`[&:has([data-state=checked])>div]:border-primary`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(_,{className:`sr-only`,value:`dark`})}),(0,M.jsx)(`div`,{className:`items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground`,children:(0,M.jsxs)(`div`,{className:`space-y-2 rounded-sm bg-slate-950 p-2`,children:[(0,M.jsxs)(`div`,{className:`space-y-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-2 w-20 rounded-lg bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]})]})}),(0,M.jsx)(`span`,{className:`block w-full p-2 text-center font-normal`,children:e()})]})})]})]})}),(0,M.jsx)(f,{type:`submit`,children:s()})]})})}function F(){return(0,M.jsx)(A,{description:l(),title:n(),variant:`section`,children:(0,M.jsx)(P,{})})}var I=F;export{I as component};
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Af as n}from"./messages-a04pwL1_.js";var r=e(t(),1),i=n(),a=(0,r.createContext)({isTyping:!1,passwordLength:0,setIsTyping:()=>void 0,setPasswordLength:()=>void 0,setShowPassword:()=>void 0,showPassword:!1});function o({children:e}){let[t,n]=(0,r.useState)(!1),[o,s]=(0,r.useState)(!1),[c,l]=(0,r.useState)(0);return(0,i.jsx)(a.Provider,{value:{isTyping:t,passwordLength:c,setIsTyping:n,setPasswordLength:l,setShowPassword:s,showPassword:o},children:e})}function s(){return(0,r.useContext)(a)}export{s as n,o as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{dp as n}from"./messages-Bbvu5aaF.js";var r=e(t(),1),i=n(),a=(0,r.createContext)({isTyping:!1,passwordLength:0,setIsTyping:()=>void 0,setPasswordLength:()=>void 0,setShowPassword:()=>void 0,showPassword:!1});function o({children:e}){let[t,n]=(0,r.useState)(!1),[o,s]=(0,r.useState)(!1),[c,l]=(0,r.useState)(0);return(0,i.jsx)(a.Provider,{value:{isTyping:t,passwordLength:c,setIsTyping:n,setPasswordLength:l,setShowPassword:s,showPassword:o},children:e})}function s(){return(0,r.useContext)(a)}export{s as n,o as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{Af as e}from"./messages-a04pwL1_.js";import{i as t,o as n,r}from"./button-CYi3aIkB.js";var i=e(),a=r(`inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border border-transparent px-2 py-0.5 font-medium text-xs transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3`,{variants:{variant:{default:`bg-primary text-primary-foreground [a&]:hover:bg-primary/90`,secondary:`bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90`,destructive:`bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90`,outline:`border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground`,ghost:`[a&]:hover:bg-accent [a&]:hover:text-accent-foreground`,link:`text-primary underline-offset-4 [a&]:hover:underline`}},defaultVariants:{variant:`default`}});function o({className:e,variant:r=`default`,asChild:o=!1,...s}){return(0,i.jsx)(o?n:`span`,{className:t(a({variant:r}),e),"data-slot":`badge`,"data-variant":r,...s})}export{o as t}; import{dp as e}from"./messages-Bbvu5aaF.js";import{i as t,o as n,r}from"./button-DlGFBuNZ.js";var i=e(),a=r(`inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border border-transparent px-2 py-0.5 font-medium text-xs transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3`,{variants:{variant:{default:`bg-primary text-primary-foreground [a&]:hover:bg-primary/90`,secondary:`bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90`,destructive:`bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90`,outline:`border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground`,ghost:`[a&]:hover:bg-accent [a&]:hover:text-accent-foreground`,link:`text-primary underline-offset-4 [a&]:hover:underline`}},defaultVariants:{variant:`default`}});function o({className:e,variant:r=`default`,asChild:o=!1,...s}){return(0,i.jsx)(o?n:`span`,{className:t(a({variant:r}),e),"data-slot":`badge`,"data-variant":r,...s})}export{o as t};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{Af as e}from"./messages-a04pwL1_.js";import{i as t}from"./button-CYi3aIkB.js";var n=e();function r({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm`,e),"data-slot":`card`,...r})}function i({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6`,e),"data-slot":`card-header`,...r})}function a({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`font-semibold leading-none`,e),"data-slot":`card-title`,...r})}function o({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`text-muted-foreground text-sm`,e),"data-slot":`card-description`,...r})}function s({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`px-6`,e),"data-slot":`card-content`,...r})}export{a,i,s as n,o as r,r as t}; import{dp as e}from"./messages-Bbvu5aaF.js";import{i as t}from"./button-DlGFBuNZ.js";var n=e();function r({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm`,e),"data-slot":`card`,...r})}function i({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6`,e),"data-slot":`card-header`,...r})}function a({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`font-semibold leading-none`,e),"data-slot":`card-title`,...r})}function o({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`text-muted-foreground text-sm`,e),"data-slot":`card-description`,...r})}function s({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`px-6`,e),"data-slot":`card-content`,...r})}export{a,i,s as n,o as r,r as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{a as t,d as n,u as r}from"./auth-store-D-hE9_JG.js";import{t as i}from"./react-CO2uhaBc.js";import{t as a}from"./createLucideIcon-Br0Bd5k2.js";var o=a(`pen-line`,[[`path`,{d:`M13 21h8`,key:`1jsn5i`}],[`path`,{d:`M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z`,key:`1a8usu`}]]),s=e(i(),1),c=new r({chains:[],loading:!1});function l(e){c.setState(t=>({...t,chains:e}))}function u(e){c.setState(t=>({...t,loading:e}))}function d(){let e=n(c,e=>e.chains),r=n(c,e=>e.loading),i=t.useQuery(`get`,`/admin/api/v1/chains`);return(0,s.useEffect)(()=>{u(i.isLoading)},[i.isLoading]),(0,s.useEffect)(()=>{i.data?.data&&l(i.data.data)},[i.data?.data]),{chains:e,loading:r,refetch:i.refetch}}export{o as n,d as t}; import{r as e}from"./chunk-DECur_0Z.js";import{a as t,d as n,f as r}from"./auth-store-BnVIXJIp.js";import{t as i}from"./react-CO2uhaBc.js";import{t as a}from"./createLucideIcon-Br0Bd5k2.js";var o=a(`pen-line`,[[`path`,{d:`M13 21h8`,key:`1jsn5i`}],[`path`,{d:`M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z`,key:`1a8usu`}]]),s=e(i(),1),c=new n({chains:[],loading:!1});function l(e){c.setState(t=>({...t,chains:e}))}function u(e){c.setState(t=>({...t,loading:e}))}function d(){let e=r(c,e=>e.chains),n=r(c,e=>e.loading),i=t.useQuery(`get`,`/admin/api/v1/chains`);return(0,s.useEffect)(()=>{u(i.isLoading)},[i.isLoading]),(0,s.useEffect)(()=>{i.data?.data&&l(i.data.data)},[i.data?.data]),{chains:e,loading:n,refetch:i.refetch}}export{o as n,d as t};
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Af as n}from"./messages-a04pwL1_.js";import{t as r}from"./dist-zN1ah_GY.js";import{i,u as a}from"./button-CYi3aIkB.js";import{a as o,r as s,t as c}from"./dist-CDVNvXxU.js";import{t as l}from"./dist-Db-AFHTl.js";import{t as u}from"./dist-BixeH3nX.js";import{t as d}from"./dist-CRIkojQ7.js";import{t as f}from"./check-DldThLle.js";var p=e(t(),1),m=n(),h=`Checkbox`,[g,_]=o(h),[v,y]=g(h);function b(e){let{__scopeCheckbox:t,checked:n,children:r,defaultChecked:i,disabled:a,form:o,name:s,onCheckedChange:l,required:u,value:d=`on`,internal_do_not_use_render:f}=e,[g,_]=c({prop:n,defaultProp:i??!1,onChange:l,caller:h}),[y,b]=p.useState(null),[x,S]=p.useState(null),C=p.useRef(!1),w=y?!!o||!!y.closest(`form`):!0,T={checked:g,disabled:a,setChecked:_,control:y,setControl:b,name:s,form:o,value:d,hasConsumerStoppedPropagationRef:C,required:u,defaultChecked:k(i)?!1:i,isFormControl:w,bubbleInput:x,setBubbleInput:S};return(0,m.jsx)(v,{scope:t,...T,children:O(f)?f(T):r})}var x=`CheckboxTrigger`,S=p.forwardRef(({__scopeCheckbox:e,onKeyDown:t,onClick:n,...i},o)=>{let{control:c,value:l,disabled:u,checked:d,required:f,setControl:h,setChecked:g,hasConsumerStoppedPropagationRef:_,isFormControl:v,bubbleInput:b}=y(x,e),S=a(o,h),C=p.useRef(d);return p.useEffect(()=>{let e=c?.form;if(e){let t=()=>g(C.current);return e.addEventListener(`reset`,t),()=>e.removeEventListener(`reset`,t)}},[c,g]),(0,m.jsx)(r.button,{type:`button`,role:`checkbox`,"aria-checked":k(d)?`mixed`:d,"aria-required":f,"data-state":A(d),"data-disabled":u?``:void 0,disabled:u,value:l,...i,ref:S,onKeyDown:s(t,e=>{e.key===`Enter`&&e.preventDefault()}),onClick:s(n,e=>{g(e=>k(e)?!0:!e),b&&v&&(_.current=e.isPropagationStopped(),_.current||e.stopPropagation())})})});S.displayName=x;var C=p.forwardRef((e,t)=>{let{__scopeCheckbox:n,name:r,checked:i,defaultChecked:a,required:o,disabled:s,value:c,onCheckedChange:l,form:u,...d}=e;return(0,m.jsx)(b,{__scopeCheckbox:n,checked:i,defaultChecked:a,disabled:s,required:o,onCheckedChange:l,name:r,form:u,value:c,internal_do_not_use_render:({isFormControl:e})=>(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(S,{...d,ref:t,__scopeCheckbox:n}),e&&(0,m.jsx)(D,{__scopeCheckbox:n})]})})});C.displayName=h;var w=`CheckboxIndicator`,T=p.forwardRef((e,t)=>{let{__scopeCheckbox:n,forceMount:i,...a}=e,o=y(w,n);return(0,m.jsx)(l,{present:i||k(o.checked)||o.checked===!0,children:(0,m.jsx)(r.span,{"data-state":A(o.checked),"data-disabled":o.disabled?``:void 0,...a,ref:t,style:{pointerEvents:`none`,...e.style}})})});T.displayName=w;var E=`CheckboxBubbleInput`,D=p.forwardRef(({__scopeCheckbox:e,...t},n)=>{let{control:i,hasConsumerStoppedPropagationRef:o,checked:s,defaultChecked:c,required:l,disabled:f,name:h,value:g,form:_,bubbleInput:v,setBubbleInput:b}=y(E,e),x=a(n,b),S=u(s),C=d(i);p.useEffect(()=>{let e=v;if(!e)return;let t=window.HTMLInputElement.prototype,n=Object.getOwnPropertyDescriptor(t,`checked`).set,r=!o.current;if(S!==s&&n){let t=new Event(`click`,{bubbles:r});e.indeterminate=k(s),n.call(e,k(s)?!1:s),e.dispatchEvent(t)}},[v,S,s,o]);let w=p.useRef(k(s)?!1:s);return(0,m.jsx)(r.input,{type:`checkbox`,"aria-hidden":!0,defaultChecked:c??w.current,required:l,disabled:f,name:h,value:g,form:_,...t,tabIndex:-1,ref:x,style:{...t.style,...C,position:`absolute`,pointerEvents:`none`,opacity:0,margin:0,transform:`translateX(-100%)`}})});D.displayName=E;function O(e){return typeof e==`function`}function k(e){return e===`indeterminate`}function A(e){return k(e)?`indeterminate`:e?`checked`:`unchecked`}function j({className:e,...t}){return(0,m.jsx)(C,{className:i(`peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs outline-none transition-shadow focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:data-[state=checked]:bg-primary dark:aria-invalid:ring-destructive/40`,e),"data-slot":`checkbox`,...t,children:(0,m.jsx)(T,{className:`grid place-content-center text-current transition-none`,"data-slot":`checkbox-indicator`,children:(0,m.jsx)(f,{className:`size-3.5`})})})}export{j as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{dp as n}from"./messages-Bbvu5aaF.js";import{t as r}from"./dist-BkAVwTGu.js";import{i,u as a}from"./button-DlGFBuNZ.js";import{a as o,r as s,t as c}from"./dist-FGEIDBp-.js";import{t as l}from"./dist-BaRXEuYn.js";import{t as u}from"./dist-BixeH3nX.js";import{t as d}from"./dist-BYCFrz1s.js";import{t as f}from"./check-DldThLle.js";var p=e(t(),1),m=n(),h=`Checkbox`,[g,_]=o(h),[v,y]=g(h);function b(e){let{__scopeCheckbox:t,checked:n,children:r,defaultChecked:i,disabled:a,form:o,name:s,onCheckedChange:l,required:u,value:d=`on`,internal_do_not_use_render:f}=e,[g,_]=c({prop:n,defaultProp:i??!1,onChange:l,caller:h}),[y,b]=p.useState(null),[x,S]=p.useState(null),C=p.useRef(!1),w=y?!!o||!!y.closest(`form`):!0,T={checked:g,disabled:a,setChecked:_,control:y,setControl:b,name:s,form:o,value:d,hasConsumerStoppedPropagationRef:C,required:u,defaultChecked:k(i)?!1:i,isFormControl:w,bubbleInput:x,setBubbleInput:S};return(0,m.jsx)(v,{scope:t,...T,children:O(f)?f(T):r})}var x=`CheckboxTrigger`,S=p.forwardRef(({__scopeCheckbox:e,onKeyDown:t,onClick:n,...i},o)=>{let{control:c,value:l,disabled:u,checked:d,required:f,setControl:h,setChecked:g,hasConsumerStoppedPropagationRef:_,isFormControl:v,bubbleInput:b}=y(x,e),S=a(o,h),C=p.useRef(d);return p.useEffect(()=>{let e=c?.form;if(e){let t=()=>g(C.current);return e.addEventListener(`reset`,t),()=>e.removeEventListener(`reset`,t)}},[c,g]),(0,m.jsx)(r.button,{type:`button`,role:`checkbox`,"aria-checked":k(d)?`mixed`:d,"aria-required":f,"data-state":A(d),"data-disabled":u?``:void 0,disabled:u,value:l,...i,ref:S,onKeyDown:s(t,e=>{e.key===`Enter`&&e.preventDefault()}),onClick:s(n,e=>{g(e=>k(e)?!0:!e),b&&v&&(_.current=e.isPropagationStopped(),_.current||e.stopPropagation())})})});S.displayName=x;var C=p.forwardRef((e,t)=>{let{__scopeCheckbox:n,name:r,checked:i,defaultChecked:a,required:o,disabled:s,value:c,onCheckedChange:l,form:u,...d}=e;return(0,m.jsx)(b,{__scopeCheckbox:n,checked:i,defaultChecked:a,disabled:s,required:o,onCheckedChange:l,name:r,form:u,value:c,internal_do_not_use_render:({isFormControl:e})=>(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(S,{...d,ref:t,__scopeCheckbox:n}),e&&(0,m.jsx)(D,{__scopeCheckbox:n})]})})});C.displayName=h;var w=`CheckboxIndicator`,T=p.forwardRef((e,t)=>{let{__scopeCheckbox:n,forceMount:i,...a}=e,o=y(w,n);return(0,m.jsx)(l,{present:i||k(o.checked)||o.checked===!0,children:(0,m.jsx)(r.span,{"data-state":A(o.checked),"data-disabled":o.disabled?``:void 0,...a,ref:t,style:{pointerEvents:`none`,...e.style}})})});T.displayName=w;var E=`CheckboxBubbleInput`,D=p.forwardRef(({__scopeCheckbox:e,...t},n)=>{let{control:i,hasConsumerStoppedPropagationRef:o,checked:s,defaultChecked:c,required:l,disabled:f,name:h,value:g,form:_,bubbleInput:v,setBubbleInput:b}=y(E,e),x=a(n,b),S=u(s),C=d(i);p.useEffect(()=>{let e=v;if(!e)return;let t=window.HTMLInputElement.prototype,n=Object.getOwnPropertyDescriptor(t,`checked`).set,r=!o.current;if(S!==s&&n){let t=new Event(`click`,{bubbles:r});e.indeterminate=k(s),n.call(e,k(s)?!1:s),e.dispatchEvent(t)}},[v,S,s,o]);let w=p.useRef(k(s)?!1:s);return(0,m.jsx)(r.input,{type:`checkbox`,"aria-hidden":!0,defaultChecked:c??w.current,required:l,disabled:f,name:h,value:g,form:_,...t,tabIndex:-1,ref:x,style:{...t.style,...C,position:`absolute`,pointerEvents:`none`,opacity:0,margin:0,transform:`translateX(-100%)`}})});D.displayName=E;function O(e){return typeof e==`function`}function k(e){return e===`indeterminate`}function A(e){return k(e)?`indeterminate`:e?`checked`:`unchecked`}function j({className:e,...t}){return(0,m.jsx)(C,{className:i(`peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs outline-none transition-shadow focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:data-[state=checked]:bg-primary dark:aria-invalid:ring-destructive/40`,e),"data-slot":`checkbox`,...t,children:(0,m.jsx)(T,{className:`grid place-content-center text-current transition-none`,"data-slot":`checkbox-indicator`,children:(0,m.jsx)(f,{className:`size-3.5`})})})}export{j as t};
@@ -0,0 +1 @@
import{Jf as e,Kf as t,dp as n,qf as r}from"./messages-Bbvu5aaF.js";import{t as i}from"./telescope-Eo9PEeOA.js";import{i as a,o,r as s,s as c,t as l}from"./dialog-DCa0Wx6n.js";var u=n();function d({open:n,onOpenChange:d}){return(0,u.jsx)(l,{onOpenChange:d,open:n,children:(0,u.jsx)(s,{className:`sm:max-w-sm`,children:(0,u.jsxs)(o,{className:`items-center text-center`,children:[(0,u.jsx)(i,{className:`mb-2 size-12 text-muted-foreground`}),(0,u.jsx)(c,{children:e()}),(0,u.jsxs)(a,{children:[r(),(0,u.jsx)(`br`,{}),(0,u.jsx)(`p`,{className:`mt-2 text-center`,children:t()})]})]})})})}export{d as t};
@@ -1 +0,0 @@
import{Af as e,df as t,ff as n,pf as r}from"./messages-a04pwL1_.js";import{t as i}from"./telescope-Eo9PEeOA.js";import{i as a,o,r as s,s as c,t as l}from"./dialog-ChTTLrcb.js";var u=e();function d({open:e,onOpenChange:d}){return(0,u.jsx)(l,{onOpenChange:d,open:e,children:(0,u.jsx)(s,{className:`sm:max-w-sm`,children:(0,u.jsxs)(o,{className:`items-center text-center`,children:[(0,u.jsx)(i,{className:`mb-2 size-12 text-muted-foreground`}),(0,u.jsx)(c,{children:r()}),(0,u.jsxs)(a,{children:[n(),(0,u.jsx)(`br`,{}),(0,u.jsx)(`p`,{className:`mt-2 text-center`,children:t()})]})]})})})}export{d as t};
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Af as n,Tf as r,wf as i}from"./messages-a04pwL1_.js";import{c as a,i as o,t as s,u as c}from"./button-CYi3aIkB.js";import{a as l,r as u}from"./dist-CDVNvXxU.js";import{a as d,c as f,i as p,l as m,n as h,o as g,r as _,s as ee,t as v,u as y}from"./dist-DlgHyFeS.js";var b=e(t(),1),x=n(),S=`AlertDialog`,[C,te]=l(S,[y]),w=y(),T=e=>{let{__scopeAlertDialog:t,...n}=e,r=w(t);return(0,x.jsx)(g,{...r,...n,modal:!0})};T.displayName=S;var E=`AlertDialogTrigger`,D=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(f,{...i,...r,ref:t})});D.displayName=E;var O=`AlertDialogPortal`,k=e=>{let{__scopeAlertDialog:t,...n}=e,r=w(t);return(0,x.jsx)(d,{...r,...n})};k.displayName=O;var A=`AlertDialogOverlay`,j=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(p,{...i,...r,ref:t})});j.displayName=A;var M=`AlertDialogContent`,[N,P]=C(M),F=a(`AlertDialogContent`),I=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,children:r,...i}=e,a=w(n),o=b.useRef(null),s=c(t,o),l=b.useRef(null);return(0,x.jsx)(m,{contentName:M,titleName:L,docsSlug:`alert-dialog`,children:(0,x.jsx)(N,{scope:n,cancelRef:l,children:(0,x.jsxs)(h,{role:`alertdialog`,...a,...i,ref:s,onOpenAutoFocus:u(i.onOpenAutoFocus,e=>{e.preventDefault(),l.current?.focus({preventScroll:!0})}),onPointerDownOutside:e=>e.preventDefault(),onInteractOutside:e=>e.preventDefault(),children:[(0,x.jsx)(F,{children:r}),(0,x.jsx)(G,{contentRef:o})]})})})});I.displayName=M;var L=`AlertDialogTitle`,R=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(ee,{...i,...r,ref:t})});R.displayName=L;var z=`AlertDialogDescription`,B=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(_,{...i,...r,ref:t})});B.displayName=z;var V=`AlertDialogAction`,H=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(v,{...i,...r,ref:t})});H.displayName=V;var U=`AlertDialogCancel`,W=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,{cancelRef:i}=P(U,n),a=w(n),o=c(t,i);return(0,x.jsx)(v,{...a,...r,ref:o})});W.displayName=U;var G=({contentRef:e})=>{let t=`\`${M}\` requires a description for the component to be accessible for screen reader users. import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{ap as n,dp as r,op as i}from"./messages-Bbvu5aaF.js";import{c as a,i as o,t as s,u as c}from"./button-DlGFBuNZ.js";import{a as l,r as u}from"./dist-FGEIDBp-.js";import{a as d,c as f,i as p,l as m,n as h,o as g,r as _,s as ee,t as v,u as y}from"./dist-B_ALJuOq.js";var b=e(t(),1),x=r(),S=`AlertDialog`,[C,te]=l(S,[y]),w=y(),T=e=>{let{__scopeAlertDialog:t,...n}=e,r=w(t);return(0,x.jsx)(g,{...r,...n,modal:!0})};T.displayName=S;var E=`AlertDialogTrigger`,D=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(f,{...i,...r,ref:t})});D.displayName=E;var O=`AlertDialogPortal`,k=e=>{let{__scopeAlertDialog:t,...n}=e,r=w(t);return(0,x.jsx)(d,{...r,...n})};k.displayName=O;var A=`AlertDialogOverlay`,j=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(p,{...i,...r,ref:t})});j.displayName=A;var M=`AlertDialogContent`,[N,P]=C(M),F=a(`AlertDialogContent`),I=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,children:r,...i}=e,a=w(n),o=b.useRef(null),s=c(t,o),l=b.useRef(null);return(0,x.jsx)(m,{contentName:M,titleName:L,docsSlug:`alert-dialog`,children:(0,x.jsx)(N,{scope:n,cancelRef:l,children:(0,x.jsxs)(h,{role:`alertdialog`,...a,...i,ref:s,onOpenAutoFocus:u(i.onOpenAutoFocus,e=>{e.preventDefault(),l.current?.focus({preventScroll:!0})}),onPointerDownOutside:e=>e.preventDefault(),onInteractOutside:e=>e.preventDefault(),children:[(0,x.jsx)(F,{children:r}),(0,x.jsx)(G,{contentRef:o})]})})})});I.displayName=M;var L=`AlertDialogTitle`,R=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(ee,{...i,...r,ref:t})});R.displayName=L;var z=`AlertDialogDescription`,B=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(_,{...i,...r,ref:t})});B.displayName=z;var V=`AlertDialogAction`,H=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(v,{...i,...r,ref:t})});H.displayName=V;var U=`AlertDialogCancel`,W=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,{cancelRef:i}=P(U,n),a=w(n),o=c(t,i);return(0,x.jsx)(v,{...a,...r,ref:o})});W.displayName=U;var G=({contentRef:e})=>{let t=`\`${M}\` requires a description for the component to be accessible for screen reader users.
You can add a description to the \`${M}\` by passing a \`${z}\` component as a child, which also benefits sighted users by adding visible context to the dialog. You can add a description to the \`${M}\` by passing a \`${z}\` component as a child, which also benefits sighted users by adding visible context to the dialog.
Alternatively, you can use your own component as a description by assigning it an \`id\` and passing the same value to the \`aria-describedby\` prop in \`${M}\`. If the description is confusing or duplicative for sighted users, you can use the \`@radix-ui/react-visually-hidden\` primitive as a wrapper around your description component. Alternatively, you can use your own component as a description by assigning it an \`id\` and passing the same value to the \`aria-describedby\` prop in \`${M}\`. If the description is confusing or duplicative for sighted users, you can use the \`@radix-ui/react-visually-hidden\` primitive as a wrapper around your description component.
For more information, see https://radix-ui.com/primitives/docs/components/alert-dialog`;return b.useEffect(()=>{document.getElementById(e.current?.getAttribute(`aria-describedby`))||console.warn(t)},[t,e]),null},K=T,q=k,J=j,Y=I,X=W,Z=R,Q=B;function ne({...e}){return(0,x.jsx)(K,{"data-slot":`alert-dialog`,...e})}function re({...e}){return(0,x.jsx)(q,{"data-slot":`alert-dialog-portal`,...e})}function ie({className:e,...t}){return(0,x.jsx)(J,{className:o(`data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in`,e),"data-slot":`alert-dialog-overlay`,...t})}function ae({className:e,size:t=`default`,...n}){return(0,x.jsxs)(re,{children:[(0,x.jsx)(ie,{}),(0,x.jsx)(Y,{className:o(`group/alert-dialog-content data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[state=closed]:animate-out data-[state=open]:animate-in data-[size=default]:sm:max-w-lg`,e),"data-size":t,"data-slot":`alert-dialog-content`,...n})]})}function oe({className:e,...t}){return(0,x.jsx)(`div`,{className:o(`grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]`,e),"data-slot":`alert-dialog-header`,...t})}function $({className:e,...t}){return(0,x.jsx)(`div`,{className:o(`flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end`,e),"data-slot":`alert-dialog-footer`,...t})}function se({className:e,...t}){return(0,x.jsx)(Z,{className:o(`font-semibold text-lg sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2`,e),"data-slot":`alert-dialog-title`,...t})}function ce({className:e,...t}){return(0,x.jsx)(Q,{className:o(`text-muted-foreground text-sm`,e),"data-slot":`alert-dialog-description`,...t})}function le({className:e,variant:t=`outline`,size:n=`default`,...r}){return(0,x.jsx)(s,{asChild:!0,size:n,variant:t,children:(0,x.jsx)(X,{className:o(e),"data-slot":`alert-dialog-cancel`,...r})})}function ue(e){let{title:t,desc:n,children:a,className:c,confirmText:l,cancelBtnText:u,destructive:d,isLoading:f,disabled:p=!1,form:m,handleConfirm:h,...g}=e;return(0,x.jsx)(ne,{...g,children:(0,x.jsxs)(ae,{className:o(c&&c),children:[(0,x.jsxs)(oe,{className:`text-start`,children:[(0,x.jsx)(se,{children:t}),(0,x.jsx)(ce,{asChild:!0,children:(0,x.jsx)(`div`,{children:n})})]}),a,(0,x.jsxs)($,{children:[(0,x.jsx)(le,{disabled:f,children:u??r()}),(0,x.jsx)(s,{disabled:p||f,form:m,onClick:h,type:m?`submit`:`button`,variant:d?`destructive`:`default`,children:l??i()})]})]})})}export{ue as t}; For more information, see https://radix-ui.com/primitives/docs/components/alert-dialog`;return b.useEffect(()=>{document.getElementById(e.current?.getAttribute(`aria-describedby`))||console.warn(t)},[t,e]),null},K=T,q=k,J=j,Y=I,X=W,Z=R,Q=B;function ne({...e}){return(0,x.jsx)(K,{"data-slot":`alert-dialog`,...e})}function re({...e}){return(0,x.jsx)(q,{"data-slot":`alert-dialog-portal`,...e})}function ie({className:e,...t}){return(0,x.jsx)(J,{className:o(`data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in`,e),"data-slot":`alert-dialog-overlay`,...t})}function ae({className:e,size:t=`default`,...n}){return(0,x.jsxs)(re,{children:[(0,x.jsx)(ie,{}),(0,x.jsx)(Y,{className:o(`group/alert-dialog-content data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[state=closed]:animate-out data-[state=open]:animate-in data-[size=default]:sm:max-w-lg`,e),"data-size":t,"data-slot":`alert-dialog-content`,...n})]})}function oe({className:e,...t}){return(0,x.jsx)(`div`,{className:o(`grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]`,e),"data-slot":`alert-dialog-header`,...t})}function $({className:e,...t}){return(0,x.jsx)(`div`,{className:o(`flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end`,e),"data-slot":`alert-dialog-footer`,...t})}function se({className:e,...t}){return(0,x.jsx)(Z,{className:o(`font-semibold text-lg sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2`,e),"data-slot":`alert-dialog-title`,...t})}function ce({className:e,...t}){return(0,x.jsx)(Q,{className:o(`text-muted-foreground text-sm`,e),"data-slot":`alert-dialog-description`,...t})}function le({className:e,variant:t=`outline`,size:n=`default`,...r}){return(0,x.jsx)(s,{asChild:!0,size:n,variant:t,children:(0,x.jsx)(X,{className:o(e),"data-slot":`alert-dialog-cancel`,...r})})}function ue(e){let{title:t,desc:r,children:a,className:c,confirmText:l,cancelBtnText:u,destructive:d,isLoading:f,disabled:p=!1,form:m,handleConfirm:h,...g}=e;return(0,x.jsx)(ne,{...g,children:(0,x.jsxs)(ae,{className:o(c&&c),children:[(0,x.jsxs)(oe,{className:`text-start`,children:[(0,x.jsx)(se,{children:t}),(0,x.jsx)(ce,{asChild:!0,children:(0,x.jsx)(`div`,{children:r})})]}),a,(0,x.jsxs)($,{children:[(0,x.jsx)(le,{disabled:f,children:u??i()}),(0,x.jsx)(s,{disabled:p||f,form:m,onClick:h,type:m?`submit`:`button`,variant:d?`destructive`:`default`,children:l??n()})]})]})})}export{ue as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{Af as e}from"./messages-a04pwL1_.js";import{i as t,t as n}from"./button-CYi3aIkB.js";import{a as r,c as i,i as a,n as o,o as s,r as c,s as l,t as u}from"./dist-DlgHyFeS.js";import{t as d}from"./createLucideIcon-Br0Bd5k2.js";var f=d(`x`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),p=e();function m({...e}){return(0,p.jsx)(s,{"data-slot":`dialog`,...e})}function h({...e}){return(0,p.jsx)(i,{"data-slot":`dialog-trigger`,...e})}function g({...e}){return(0,p.jsx)(r,{"data-slot":`dialog-portal`,...e})}function _({...e}){return(0,p.jsx)(u,{"data-slot":`dialog-close`,...e})}function v({className:e,...n}){return(0,p.jsx)(a,{className:t(`data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in`,e),"data-slot":`dialog-overlay`,...n})}function y({className:e,children:n,showCloseButton:r=!0,...i}){return(0,p.jsxs)(g,{"data-slot":`dialog-portal`,children:[(0,p.jsx)(v,{}),(0,p.jsxs)(o,{className:t(`data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg outline-none duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:max-w-lg`,e),"data-slot":`dialog-content`,...i,children:[n,r&&(0,p.jsxs)(u,{className:`absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0`,"data-slot":`dialog-close`,children:[(0,p.jsx)(f,{}),(0,p.jsx)(`span`,{className:`sr-only`,children:`Close`})]})]})]})}function b({className:e,...n}){return(0,p.jsx)(`div`,{className:t(`flex flex-col gap-2 text-center sm:text-left`,e),"data-slot":`dialog-header`,...n})}function x({className:e,showCloseButton:r=!1,children:i,...a}){return(0,p.jsxs)(`div`,{className:t(`flex flex-col-reverse gap-2 sm:flex-row sm:justify-end`,e),"data-slot":`dialog-footer`,...a,children:[i,r&&(0,p.jsx)(u,{asChild:!0,children:(0,p.jsx)(n,{variant:`outline`,children:`Close`})})]})}function S({className:e,...n}){return(0,p.jsx)(l,{className:t(`font-semibold text-lg leading-none`,e),"data-slot":`dialog-title`,...n})}function C({className:e,...n}){return(0,p.jsx)(c,{className:t(`text-muted-foreground text-sm`,e),"data-slot":`dialog-description`,...n})}export{x as a,h as c,C as i,f as l,_ as n,b as o,y as r,S as s,m as t}; import{dp as e}from"./messages-Bbvu5aaF.js";import{i as t,t as n}from"./button-DlGFBuNZ.js";import{a as r,c as i,i as a,n as o,o as s,r as c,s as l,t as u}from"./dist-B_ALJuOq.js";import{t as d}from"./createLucideIcon-Br0Bd5k2.js";var f=d(`x`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),p=e();function m({...e}){return(0,p.jsx)(s,{"data-slot":`dialog`,...e})}function h({...e}){return(0,p.jsx)(i,{"data-slot":`dialog-trigger`,...e})}function g({...e}){return(0,p.jsx)(r,{"data-slot":`dialog-portal`,...e})}function _({...e}){return(0,p.jsx)(u,{"data-slot":`dialog-close`,...e})}function v({className:e,...n}){return(0,p.jsx)(a,{className:t(`data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in`,e),"data-slot":`dialog-overlay`,...n})}function y({className:e,children:n,showCloseButton:r=!0,...i}){return(0,p.jsxs)(g,{"data-slot":`dialog-portal`,children:[(0,p.jsx)(v,{}),(0,p.jsxs)(o,{className:t(`data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg outline-none duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:max-w-lg`,e),"data-slot":`dialog-content`,...i,children:[n,r&&(0,p.jsxs)(u,{className:`absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0`,"data-slot":`dialog-close`,children:[(0,p.jsx)(f,{}),(0,p.jsx)(`span`,{className:`sr-only`,children:`Close`})]})]})]})}function b({className:e,...n}){return(0,p.jsx)(`div`,{className:t(`flex flex-col gap-2 text-center sm:text-left`,e),"data-slot":`dialog-header`,...n})}function x({className:e,showCloseButton:r=!1,children:i,...a}){return(0,p.jsxs)(`div`,{className:t(`flex flex-col-reverse gap-2 sm:flex-row sm:justify-end`,e),"data-slot":`dialog-footer`,...a,children:[i,r&&(0,p.jsx)(u,{asChild:!0,children:(0,p.jsx)(n,{variant:`outline`,children:`Close`})})]})}function S({className:e,...n}){return(0,p.jsx)(l,{className:t(`font-semibold text-lg leading-none`,e),"data-slot":`dialog-title`,...n})}function C({className:e,...n}){return(0,p.jsx)(c,{className:t(`text-muted-foreground text-sm`,e),"data-slot":`dialog-description`,...n})}export{x as a,h as c,C as i,f as l,_ as n,b as o,y as r,S as s,m as t};
-1
View File
@@ -1 +0,0 @@
import{Af as e,an as t,cn as n,dn as r,fn as i,in as a,ln as o,nn as s,on as c,rn as l,sn as u,tn as d,un as f}from"./messages-a04pwL1_.js";import{t as p}from"./button-CYi3aIkB.js";import{t as m}from"./checkbox-CjsMMPUy.js";import{c as h,i as g,s as _}from"./zod-CQemhjci.js";import{a as v,c as y,i as b,l as x,n as S,o as C,r as w,s as T,t as E}from"./form-Bwi_c-SI.js";import{t as D}from"./page-header-yGXiG9iK.js";import{t as O}from"./show-submitted-data-D7TduAhD.js";var k=e(),A=[{id:`recents`,label:f()},{id:`home`,label:o()},{id:`applications`,label:n()},{id:`desktop`,label:u()},{id:`downloads`,label:c()},{id:`documents`,label:t()}],j=_({items:g(h()).refine(e=>e.some(e=>e),{message:a()})}),M={items:[`recents`,`home`]};function N(){let e=x({resolver:y(j),defaultValues:M});return(0,k.jsx)(E,{...e,children:(0,k.jsxs)(`form`,{className:`space-y-8`,onSubmit:e.handleSubmit(e=>O(e)),children:[(0,k.jsx)(b,{control:e.control,name:`items`,render:()=>(0,k.jsxs)(v,{children:[(0,k.jsxs)(`div`,{className:`mb-4`,children:[(0,k.jsx)(C,{className:`text-base`,children:l()}),(0,k.jsx)(w,{children:s()})]}),A.map(t=>(0,k.jsx)(b,{control:e.control,name:`items`,render:({field:e})=>(0,k.jsxs)(v,{className:`flex flex-row items-start`,children:[(0,k.jsx)(S,{children:(0,k.jsx)(m,{checked:e.value?.includes(t.id),onCheckedChange:n=>n?e.onChange([...e.value,t.id]):e.onChange(e.value?.filter(e=>e!==t.id))})}),(0,k.jsx)(C,{className:`font-normal`,children:t.label})]},t.id)},t.id)),(0,k.jsx)(T,{})]})}),(0,k.jsx)(p,{type:`submit`,children:d()})]})})}function P(){return(0,k.jsx)(D,{description:r(),title:i(),variant:`section`,children:(0,k.jsx)(N,{})})}var F=P;export{F as component};
+1
View File
@@ -0,0 +1 @@
import{an as e,cn as t,dn as n,dp as r,fn as i,in as a,ln as o,nn as s,on as c,pn as l,rn as u,sn as d,un as f}from"./messages-Bbvu5aaF.js";import{t as p}from"./button-DlGFBuNZ.js";import{t as m}from"./checkbox-3W7oo_7n.js";import{c as h,i as g,s as _}from"./zod-CQemhjci.js";import{a as v,c as y,i as b,l as x,n as S,o as C,r as w,s as T,t as E}from"./form-CwBNjUHP.js";import{t as D}from"./page-header-v0-NzBJQ.js";import{t as O}from"./show-submitted-data-C_xyg4eH.js";var k=r(),A=[{id:`recents`,label:n()},{id:`home`,label:f()},{id:`applications`,label:o()},{id:`desktop`,label:t()},{id:`downloads`,label:d()},{id:`documents`,label:c()}],j=_({items:g(h()).refine(e=>e.some(e=>e),{message:e()})}),M={items:[`recents`,`home`]};function N(){let e=x({resolver:y(j),defaultValues:M});return(0,k.jsx)(E,{...e,children:(0,k.jsxs)(`form`,{className:`space-y-8`,onSubmit:e.handleSubmit(e=>O(e)),children:[(0,k.jsx)(b,{control:e.control,name:`items`,render:()=>(0,k.jsxs)(v,{children:[(0,k.jsxs)(`div`,{className:`mb-4`,children:[(0,k.jsx)(C,{className:`text-base`,children:a()}),(0,k.jsx)(w,{children:u()})]}),A.map(t=>(0,k.jsx)(b,{control:e.control,name:`items`,render:({field:e})=>(0,k.jsxs)(v,{className:`flex flex-row items-start`,children:[(0,k.jsx)(S,{children:(0,k.jsx)(m,{checked:e.value?.includes(t.id),onCheckedChange:n=>n?e.onChange([...e.value,t.id]):e.onChange(e.value?.filter(e=>e!==t.id))})}),(0,k.jsx)(C,{className:`font-normal`,children:t.label})]},t.id)},t.id)),(0,k.jsx)(T,{})]})}),(0,k.jsx)(p,{type:`submit`,children:s()})]})})}function P(){return(0,k.jsx)(D,{description:i(),title:l(),variant:`section`,children:(0,k.jsx)(N,{})})}var F=P;export{F as component};
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{n}from"./dist-CDVNvXxU.js";var r=e(t(),1),i=r.useId||(()=>void 0),a=0;function o(e){let[t,o]=r.useState(i());return n(()=>{e||o(e=>e??String(a++))},[e]),e||(t?`radix-${t}`:``)}function s(e){let t=r.useRef(e);return r.useEffect(()=>{t.current=e}),r.useMemo(()=>(...e)=>t.current?.(...e),[])}export{o as n,s as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{n}from"./dist-FGEIDBp-.js";var r=e(t(),1),i=r.useId||(()=>void 0),a=0;function o(e){let[t,o]=r.useState(i());return n(()=>{e||o(e=>e??String(a++))},[e]),e||(t?`radix-${t}`:``)}function s(e){let t=r.useRef(e);return r.useEffect(()=>{t.current=e}),r.useMemo(()=>(...e)=>t.current?.(...e),[])}export{o as n,s as t};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Af as n}from"./messages-a04pwL1_.js";import{t as r}from"./dist-zN1ah_GY.js";import{u as i}from"./button-CYi3aIkB.js";import{a,r as o,t as s}from"./dist-CDVNvXxU.js";import{t as c}from"./dist-Db-AFHTl.js";import{n as l}from"./dist-Dw4KnGRx.js";import{t as u}from"./dist-BixeH3nX.js";import{t as d}from"./dist-CRIkojQ7.js";import{n as f,r as p,t as m}from"./dist-CkoYnY3E.js";var h=e(t(),1),g=n(),_=`Radio`,[v,y]=a(_),[b,x]=v(_),S=h.forwardRef((e,t)=>{let{__scopeRadio:n,name:a,checked:s=!1,required:c,disabled:l,value:u=`on`,onCheck:d,form:f,...p}=e,[m,_]=h.useState(null),v=i(t,e=>_(e)),y=h.useRef(!1),x=m?f||!!m.closest(`form`):!0;return(0,g.jsxs)(b,{scope:n,checked:s,disabled:l,children:[(0,g.jsx)(r.button,{type:`button`,role:`radio`,"aria-checked":s,"data-state":D(s),"data-disabled":l?``:void 0,disabled:l,value:u,...p,ref:v,onClick:o(e.onClick,e=>{s||d?.(),x&&(y.current=e.isPropagationStopped(),y.current||e.stopPropagation())})}),x&&(0,g.jsx)(E,{control:m,bubbles:!y.current,name:a,value:u,checked:s,required:c,disabled:l,form:f,style:{transform:`translateX(-100%)`}})]})});S.displayName=_;var C=`RadioIndicator`,w=h.forwardRef((e,t)=>{let{__scopeRadio:n,forceMount:i,...a}=e,o=x(C,n);return(0,g.jsx)(c,{present:i||o.checked,children:(0,g.jsx)(r.span,{"data-state":D(o.checked),"data-disabled":o.disabled?``:void 0,...a,ref:t})})});w.displayName=C;var T=`RadioBubbleInput`,E=h.forwardRef(({__scopeRadio:e,control:t,checked:n,bubbles:a=!0,...o},s)=>{let c=h.useRef(null),l=i(c,s),f=u(n),p=d(t);return h.useEffect(()=>{let e=c.current;if(!e)return;let t=window.HTMLInputElement.prototype,r=Object.getOwnPropertyDescriptor(t,`checked`).set;if(f!==n&&r){let t=new Event(`click`,{bubbles:a});r.call(e,n),e.dispatchEvent(t)}},[f,n,a]),(0,g.jsx)(r.input,{type:`radio`,"aria-hidden":!0,defaultChecked:n,...o,tabIndex:-1,ref:l,style:{...o.style,...p,position:`absolute`,pointerEvents:`none`,opacity:0,margin:0}})});E.displayName=T;function D(e){return e?`checked`:`unchecked`}var O=[`ArrowUp`,`ArrowDown`,`ArrowLeft`,`ArrowRight`],k=`RadioGroup`,[A,j]=a(k,[p,y]),M=p(),N=y(),[P,F]=A(k),I=h.forwardRef((e,t)=>{let{__scopeRadioGroup:n,name:i,defaultValue:a,value:o,required:c=!1,disabled:u=!1,orientation:d,dir:p,loop:m=!0,onValueChange:h,..._}=e,v=M(n),y=l(p),[b,x]=s({prop:o,defaultProp:a??null,onChange:h,caller:k});return(0,g.jsx)(P,{scope:n,name:i,required:c,disabled:u,value:b,onValueChange:x,children:(0,g.jsx)(f,{asChild:!0,...v,orientation:d,dir:y,loop:m,children:(0,g.jsx)(r.div,{role:`radiogroup`,"aria-required":c,"aria-orientation":d,"data-disabled":u?``:void 0,dir:y,..._,ref:t})})})});I.displayName=k;var L=`RadioGroupItem`,R=h.forwardRef((e,t)=>{let{__scopeRadioGroup:n,disabled:r,...a}=e,s=F(L,n),c=s.disabled||r,l=M(n),u=N(n),d=h.useRef(null),f=i(t,d),p=s.value===a.value,_=h.useRef(!1);return h.useEffect(()=>{let e=e=>{O.includes(e.key)&&(_.current=!0)},t=()=>_.current=!1;return document.addEventListener(`keydown`,e),document.addEventListener(`keyup`,t),()=>{document.removeEventListener(`keydown`,e),document.removeEventListener(`keyup`,t)}},[]),(0,g.jsx)(m,{asChild:!0,...l,focusable:!c,active:p,children:(0,g.jsx)(S,{disabled:c,required:s.required,checked:p,...u,...a,name:s.name,ref:f,onCheck:()=>s.onValueChange(a.value),onKeyDown:o(e=>{e.key===`Enter`&&e.preventDefault()}),onFocus:o(a.onFocus,()=>{_.current&&d.current?.click()})})})});R.displayName=L;var z=`RadioGroupIndicator`,B=h.forwardRef((e,t)=>{let{__scopeRadioGroup:n,...r}=e;return(0,g.jsx)(w,{...N(n),...r,ref:t})});B.displayName=z;var V=I,H=R,U=B;export{H as n,V as r,U as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{dp as n}from"./messages-Bbvu5aaF.js";import{t as r}from"./dist-BkAVwTGu.js";import{u as i}from"./button-DlGFBuNZ.js";import{a,r as o,t as s}from"./dist-FGEIDBp-.js";import{t as c}from"./dist-BaRXEuYn.js";import{n as l}from"./dist-DI4dxjh-.js";import{t as u}from"./dist-BixeH3nX.js";import{t as d}from"./dist-BYCFrz1s.js";import{n as f,r as p,t as m}from"./dist-DRkG7ylN.js";var h=e(t(),1),g=n(),_=`Radio`,[v,y]=a(_),[b,x]=v(_),S=h.forwardRef((e,t)=>{let{__scopeRadio:n,name:a,checked:s=!1,required:c,disabled:l,value:u=`on`,onCheck:d,form:f,...p}=e,[m,_]=h.useState(null),v=i(t,e=>_(e)),y=h.useRef(!1),x=m?f||!!m.closest(`form`):!0;return(0,g.jsxs)(b,{scope:n,checked:s,disabled:l,children:[(0,g.jsx)(r.button,{type:`button`,role:`radio`,"aria-checked":s,"data-state":D(s),"data-disabled":l?``:void 0,disabled:l,value:u,...p,ref:v,onClick:o(e.onClick,e=>{s||d?.(),x&&(y.current=e.isPropagationStopped(),y.current||e.stopPropagation())})}),x&&(0,g.jsx)(E,{control:m,bubbles:!y.current,name:a,value:u,checked:s,required:c,disabled:l,form:f,style:{transform:`translateX(-100%)`}})]})});S.displayName=_;var C=`RadioIndicator`,w=h.forwardRef((e,t)=>{let{__scopeRadio:n,forceMount:i,...a}=e,o=x(C,n);return(0,g.jsx)(c,{present:i||o.checked,children:(0,g.jsx)(r.span,{"data-state":D(o.checked),"data-disabled":o.disabled?``:void 0,...a,ref:t})})});w.displayName=C;var T=`RadioBubbleInput`,E=h.forwardRef(({__scopeRadio:e,control:t,checked:n,bubbles:a=!0,...o},s)=>{let c=h.useRef(null),l=i(c,s),f=u(n),p=d(t);return h.useEffect(()=>{let e=c.current;if(!e)return;let t=window.HTMLInputElement.prototype,r=Object.getOwnPropertyDescriptor(t,`checked`).set;if(f!==n&&r){let t=new Event(`click`,{bubbles:a});r.call(e,n),e.dispatchEvent(t)}},[f,n,a]),(0,g.jsx)(r.input,{type:`radio`,"aria-hidden":!0,defaultChecked:n,...o,tabIndex:-1,ref:l,style:{...o.style,...p,position:`absolute`,pointerEvents:`none`,opacity:0,margin:0}})});E.displayName=T;function D(e){return e?`checked`:`unchecked`}var O=[`ArrowUp`,`ArrowDown`,`ArrowLeft`,`ArrowRight`],k=`RadioGroup`,[A,j]=a(k,[p,y]),M=p(),N=y(),[P,F]=A(k),I=h.forwardRef((e,t)=>{let{__scopeRadioGroup:n,name:i,defaultValue:a,value:o,required:c=!1,disabled:u=!1,orientation:d,dir:p,loop:m=!0,onValueChange:h,..._}=e,v=M(n),y=l(p),[b,x]=s({prop:o,defaultProp:a??null,onChange:h,caller:k});return(0,g.jsx)(P,{scope:n,name:i,required:c,disabled:u,value:b,onValueChange:x,children:(0,g.jsx)(f,{asChild:!0,...v,orientation:d,dir:y,loop:m,children:(0,g.jsx)(r.div,{role:`radiogroup`,"aria-required":c,"aria-orientation":d,"data-disabled":u?``:void 0,dir:y,..._,ref:t})})})});I.displayName=k;var L=`RadioGroupItem`,R=h.forwardRef((e,t)=>{let{__scopeRadioGroup:n,disabled:r,...a}=e,s=F(L,n),c=s.disabled||r,l=M(n),u=N(n),d=h.useRef(null),f=i(t,d),p=s.value===a.value,_=h.useRef(!1);return h.useEffect(()=>{let e=e=>{O.includes(e.key)&&(_.current=!0)},t=()=>_.current=!1;return document.addEventListener(`keydown`,e),document.addEventListener(`keyup`,t),()=>{document.removeEventListener(`keydown`,e),document.removeEventListener(`keyup`,t)}},[]),(0,g.jsx)(m,{asChild:!0,...l,focusable:!c,active:p,children:(0,g.jsx)(S,{disabled:c,required:s.required,checked:p,...u,...a,name:s.name,ref:f,onCheck:()=>s.onValueChange(a.value),onKeyDown:o(e=>{e.key===`Enter`&&e.preventDefault()}),onFocus:o(a.onFocus,()=>{_.current&&d.current?.click()})})})});R.displayName=L;var z=`RadioGroupIndicator`,B=h.forwardRef((e,t)=>{let{__scopeRadioGroup:n,...r}=e;return(0,g.jsx)(w,{...N(n),...r,ref:t})});B.displayName=z;var V=I,H=R,U=B;export{H as n,V as r,U as t};
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{n}from"./dist-CDVNvXxU.js";var r=e(t(),1);function i(e){let[t,i]=r.useState(void 0);return n(()=>{if(e){i({width:e.offsetWidth,height:e.offsetHeight});let t=new ResizeObserver(t=>{if(!Array.isArray(t)||!t.length)return;let n=t[0],r,a;if(`borderBoxSize`in n){let e=n.borderBoxSize,t=Array.isArray(e)?e[0]:e;r=t.inlineSize,a=t.blockSize}else r=e.offsetWidth,a=e.offsetHeight;i({width:r,height:a})});return t.observe(e,{box:`border-box`}),()=>t.unobserve(e)}else i(void 0)},[e]),t}export{i as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{n}from"./dist-FGEIDBp-.js";var r=e(t(),1);function i(e){let[t,i]=r.useState(void 0);return n(()=>{if(e){i({width:e.offsetWidth,height:e.offsetHeight});let t=new ResizeObserver(t=>{if(!Array.isArray(t)||!t.length)return;let n=t[0],r,a;if(`borderBoxSize`in n){let e=n.borderBoxSize,t=Array.isArray(e)?e[0]:e;r=t.inlineSize,a=t.blockSize}else r=e.offsetWidth,a=e.offsetHeight;i({width:r,height:a})});return t.observe(e,{box:`border-box`}),()=>t.unobserve(e)}else i(void 0)},[e]),t}export{i as t};
@@ -1,4 +1,4 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Af as n}from"./messages-a04pwL1_.js";import{t as r}from"./dist-zN1ah_GY.js";import{s as i,u as a}from"./button-CYi3aIkB.js";import{a as o,i as s,r as c,t as l}from"./dist-CDVNvXxU.js";import{t as u}from"./dist-Db-AFHTl.js";import{n as d}from"./dist-JWsv7m_D.js";import{n as f,t as p}from"./dist-DXs3DLXA.js";import{i as m,n as h,r as g,t as _}from"./es2015-BA51vX-r.js";var v=e(t(),1),y=n(),b=`Dialog`,[x,S]=o(b),[C,w]=x(b),T=e=>{let{__scopeDialog:t,children:n,open:r,defaultOpen:i,onOpenChange:a,modal:o=!0}=e,s=v.useRef(null),c=v.useRef(null),[u,f]=l({prop:r,defaultProp:i??!1,onChange:a,caller:b});return(0,y.jsx)(C,{scope:t,triggerRef:s,contentRef:c,contentId:d(),titleId:d(),descriptionId:d(),open:u,onOpenChange:f,onOpenToggle:v.useCallback(()=>f(e=>!e),[f]),modal:o,children:n})};T.displayName=b;var E=`DialogTrigger`,D=v.forwardRef((e,t)=>{let{__scopeDialog:n,...i}=e,o=w(E,n),s=a(t,o.triggerRef);return(0,y.jsx)(r.button,{type:`button`,"aria-haspopup":`dialog`,"aria-expanded":o.open,"aria-controls":o.contentId,"data-state":q(o.open),...i,ref:s,onClick:c(e.onClick,o.onOpenToggle)})});D.displayName=E;var O=`DialogPortal`,[k,A]=x(O,{forceMount:void 0}),j=e=>{let{__scopeDialog:t,forceMount:n,children:r,container:i}=e,a=w(O,t);return(0,y.jsx)(k,{scope:t,forceMount:n,children:v.Children.map(r,e=>(0,y.jsx)(u,{present:n||a.open,children:(0,y.jsx)(p,{asChild:!0,container:i,children:e})}))})};j.displayName=O;var M=`DialogOverlay`,N=v.forwardRef((e,t)=>{let n=A(M,e.__scopeDialog),{forceMount:r=n.forceMount,...i}=e,a=w(M,e.__scopeDialog);return a.modal?(0,y.jsx)(u,{present:r||a.open,children:(0,y.jsx)(F,{...i,ref:t})}):null});N.displayName=M;var P=i(`DialogOverlay.RemoveScroll`),F=v.forwardRef((e,t)=>{let{__scopeDialog:n,...i}=e,a=w(M,n);return(0,y.jsx)(h,{as:P,allowPinchZoom:!0,shards:[a.contentRef],children:(0,y.jsx)(r.div,{"data-state":q(a.open),...i,ref:t,style:{pointerEvents:`auto`,...i.style}})})}),I=`DialogContent`,L=v.forwardRef((e,t)=>{let n=A(I,e.__scopeDialog),{forceMount:r=n.forceMount,...i}=e,a=w(I,e.__scopeDialog);return(0,y.jsx)(u,{present:r||a.open,children:a.modal?(0,y.jsx)(R,{...i,ref:t}):(0,y.jsx)(z,{...i,ref:t})})});L.displayName=I;var R=v.forwardRef((e,t)=>{let n=w(I,e.__scopeDialog),r=v.useRef(null),i=a(t,n.contentRef,r);return v.useEffect(()=>{let e=r.current;if(e)return _(e)},[]),(0,y.jsx)(B,{...e,ref:i,trapFocus:n.open,disableOutsidePointerEvents:!0,onCloseAutoFocus:c(e.onCloseAutoFocus,e=>{e.preventDefault(),n.triggerRef.current?.focus()}),onPointerDownOutside:c(e.onPointerDownOutside,e=>{let t=e.detail.originalEvent,n=t.button===0&&t.ctrlKey===!0;(t.button===2||n)&&e.preventDefault()}),onFocusOutside:c(e.onFocusOutside,e=>e.preventDefault())})}),z=v.forwardRef((e,t)=>{let n=w(I,e.__scopeDialog),r=v.useRef(!1),i=v.useRef(!1);return(0,y.jsx)(B,{...e,ref:t,trapFocus:!1,disableOutsidePointerEvents:!1,onCloseAutoFocus:t=>{e.onCloseAutoFocus?.(t),t.defaultPrevented||(r.current||n.triggerRef.current?.focus(),t.preventDefault()),r.current=!1,i.current=!1},onInteractOutside:t=>{e.onInteractOutside?.(t),t.defaultPrevented||(r.current=!0,t.detail.originalEvent.type===`pointerdown`&&(i.current=!0));let a=t.target;n.triggerRef.current?.contains(a)&&t.preventDefault(),t.detail.originalEvent.type===`focusin`&&i.current&&t.preventDefault()}})}),B=v.forwardRef((e,t)=>{let{__scopeDialog:n,trapFocus:r,onOpenAutoFocus:i,onCloseAutoFocus:o,...s}=e,c=w(I,n),l=v.useRef(null),u=a(t,l);return g(),(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(m,{asChild:!0,loop:!0,trapped:r,onMountAutoFocus:i,onUnmountAutoFocus:o,children:(0,y.jsx)(f,{role:`dialog`,id:c.contentId,"aria-describedby":c.descriptionId,"aria-labelledby":c.titleId,"data-state":q(c.open),...s,ref:u,onDismiss:()=>c.onOpenChange(!1)})}),(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(Z,{titleId:c.titleId}),(0,y.jsx)($,{contentRef:l,descriptionId:c.descriptionId})]})]})}),V=`DialogTitle`,H=v.forwardRef((e,t)=>{let{__scopeDialog:n,...i}=e,a=w(V,n);return(0,y.jsx)(r.h2,{id:a.titleId,...i,ref:t})});H.displayName=V;var U=`DialogDescription`,W=v.forwardRef((e,t)=>{let{__scopeDialog:n,...i}=e,a=w(U,n);return(0,y.jsx)(r.p,{id:a.descriptionId,...i,ref:t})});W.displayName=U;var G=`DialogClose`,K=v.forwardRef((e,t)=>{let{__scopeDialog:n,...i}=e,a=w(G,n);return(0,y.jsx)(r.button,{type:`button`,...i,ref:t,onClick:c(e.onClick,()=>a.onOpenChange(!1))})});K.displayName=G;function q(e){return e?`open`:`closed`}var J=`DialogTitleWarning`,[Y,X]=s(J,{contentName:I,titleName:V,docsSlug:`dialog`}),Z=({titleId:e})=>{let t=X(J),n=`\`${t.contentName}\` requires a \`${t.titleName}\` for the component to be accessible for screen reader users. import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{dp as n}from"./messages-Bbvu5aaF.js";import{t as r}from"./dist-BkAVwTGu.js";import{s as i,u as a}from"./button-DlGFBuNZ.js";import{a as o,i as s,r as c,t as l}from"./dist-FGEIDBp-.js";import{t as u}from"./dist-BaRXEuYn.js";import{n as d}from"./dist--ZhT0Dzg.js";import{n as f,t as p}from"./dist-Cy-xiyt7.js";import{i as m,n as h,r as g,t as _}from"./es2015-b-3Ydtpy.js";var v=e(t(),1),y=n(),b=`Dialog`,[x,S]=o(b),[C,w]=x(b),T=e=>{let{__scopeDialog:t,children:n,open:r,defaultOpen:i,onOpenChange:a,modal:o=!0}=e,s=v.useRef(null),c=v.useRef(null),[u,f]=l({prop:r,defaultProp:i??!1,onChange:a,caller:b});return(0,y.jsx)(C,{scope:t,triggerRef:s,contentRef:c,contentId:d(),titleId:d(),descriptionId:d(),open:u,onOpenChange:f,onOpenToggle:v.useCallback(()=>f(e=>!e),[f]),modal:o,children:n})};T.displayName=b;var E=`DialogTrigger`,D=v.forwardRef((e,t)=>{let{__scopeDialog:n,...i}=e,o=w(E,n),s=a(t,o.triggerRef);return(0,y.jsx)(r.button,{type:`button`,"aria-haspopup":`dialog`,"aria-expanded":o.open,"aria-controls":o.contentId,"data-state":q(o.open),...i,ref:s,onClick:c(e.onClick,o.onOpenToggle)})});D.displayName=E;var O=`DialogPortal`,[k,A]=x(O,{forceMount:void 0}),j=e=>{let{__scopeDialog:t,forceMount:n,children:r,container:i}=e,a=w(O,t);return(0,y.jsx)(k,{scope:t,forceMount:n,children:v.Children.map(r,e=>(0,y.jsx)(u,{present:n||a.open,children:(0,y.jsx)(p,{asChild:!0,container:i,children:e})}))})};j.displayName=O;var M=`DialogOverlay`,N=v.forwardRef((e,t)=>{let n=A(M,e.__scopeDialog),{forceMount:r=n.forceMount,...i}=e,a=w(M,e.__scopeDialog);return a.modal?(0,y.jsx)(u,{present:r||a.open,children:(0,y.jsx)(F,{...i,ref:t})}):null});N.displayName=M;var P=i(`DialogOverlay.RemoveScroll`),F=v.forwardRef((e,t)=>{let{__scopeDialog:n,...i}=e,a=w(M,n);return(0,y.jsx)(h,{as:P,allowPinchZoom:!0,shards:[a.contentRef],children:(0,y.jsx)(r.div,{"data-state":q(a.open),...i,ref:t,style:{pointerEvents:`auto`,...i.style}})})}),I=`DialogContent`,L=v.forwardRef((e,t)=>{let n=A(I,e.__scopeDialog),{forceMount:r=n.forceMount,...i}=e,a=w(I,e.__scopeDialog);return(0,y.jsx)(u,{present:r||a.open,children:a.modal?(0,y.jsx)(R,{...i,ref:t}):(0,y.jsx)(z,{...i,ref:t})})});L.displayName=I;var R=v.forwardRef((e,t)=>{let n=w(I,e.__scopeDialog),r=v.useRef(null),i=a(t,n.contentRef,r);return v.useEffect(()=>{let e=r.current;if(e)return _(e)},[]),(0,y.jsx)(B,{...e,ref:i,trapFocus:n.open,disableOutsidePointerEvents:!0,onCloseAutoFocus:c(e.onCloseAutoFocus,e=>{e.preventDefault(),n.triggerRef.current?.focus()}),onPointerDownOutside:c(e.onPointerDownOutside,e=>{let t=e.detail.originalEvent,n=t.button===0&&t.ctrlKey===!0;(t.button===2||n)&&e.preventDefault()}),onFocusOutside:c(e.onFocusOutside,e=>e.preventDefault())})}),z=v.forwardRef((e,t)=>{let n=w(I,e.__scopeDialog),r=v.useRef(!1),i=v.useRef(!1);return(0,y.jsx)(B,{...e,ref:t,trapFocus:!1,disableOutsidePointerEvents:!1,onCloseAutoFocus:t=>{e.onCloseAutoFocus?.(t),t.defaultPrevented||(r.current||n.triggerRef.current?.focus(),t.preventDefault()),r.current=!1,i.current=!1},onInteractOutside:t=>{e.onInteractOutside?.(t),t.defaultPrevented||(r.current=!0,t.detail.originalEvent.type===`pointerdown`&&(i.current=!0));let a=t.target;n.triggerRef.current?.contains(a)&&t.preventDefault(),t.detail.originalEvent.type===`focusin`&&i.current&&t.preventDefault()}})}),B=v.forwardRef((e,t)=>{let{__scopeDialog:n,trapFocus:r,onOpenAutoFocus:i,onCloseAutoFocus:o,...s}=e,c=w(I,n),l=v.useRef(null),u=a(t,l);return g(),(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(m,{asChild:!0,loop:!0,trapped:r,onMountAutoFocus:i,onUnmountAutoFocus:o,children:(0,y.jsx)(f,{role:`dialog`,id:c.contentId,"aria-describedby":c.descriptionId,"aria-labelledby":c.titleId,"data-state":q(c.open),...s,ref:u,onDismiss:()=>c.onOpenChange(!1)})}),(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(Z,{titleId:c.titleId}),(0,y.jsx)($,{contentRef:l,descriptionId:c.descriptionId})]})]})}),V=`DialogTitle`,H=v.forwardRef((e,t)=>{let{__scopeDialog:n,...i}=e,a=w(V,n);return(0,y.jsx)(r.h2,{id:a.titleId,...i,ref:t})});H.displayName=V;var U=`DialogDescription`,W=v.forwardRef((e,t)=>{let{__scopeDialog:n,...i}=e,a=w(U,n);return(0,y.jsx)(r.p,{id:a.descriptionId,...i,ref:t})});W.displayName=U;var G=`DialogClose`,K=v.forwardRef((e,t)=>{let{__scopeDialog:n,...i}=e,a=w(G,n);return(0,y.jsx)(r.button,{type:`button`,...i,ref:t,onClick:c(e.onClick,()=>a.onOpenChange(!1))})});K.displayName=G;function q(e){return e?`open`:`closed`}var J=`DialogTitleWarning`,[Y,X]=s(J,{contentName:I,titleName:V,docsSlug:`dialog`}),Z=({titleId:e})=>{let t=X(J),n=`\`${t.contentName}\` requires a \`${t.titleName}\` for the component to be accessible for screen reader users.
If you want to hide the \`${t.titleName}\`, you can wrap it with our VisuallyHidden component. If you want to hide the \`${t.titleName}\`, you can wrap it with our VisuallyHidden component.
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{u as n}from"./button-CYi3aIkB.js";import{n as r}from"./dist-CDVNvXxU.js";var i=e(t(),1);function a(e,t){return i.useReducer((e,n)=>t[e][n]??e,e)}var o=e=>{let{present:t,children:r}=e,a=s(t),o=typeof r==`function`?r({present:a.isPresent}):i.Children.only(r),c=n(a.ref,l(o));return typeof r==`function`||a.isPresent?i.cloneElement(o,{ref:c}):null};o.displayName=`Presence`;function s(e){let[t,n]=i.useState(),o=i.useRef(null),s=i.useRef(e),l=i.useRef(`none`),[u,d]=a(e?`mounted`:`unmounted`,{mounted:{UNMOUNT:`unmounted`,ANIMATION_OUT:`unmountSuspended`},unmountSuspended:{MOUNT:`mounted`,ANIMATION_END:`unmounted`},unmounted:{MOUNT:`mounted`}});return i.useEffect(()=>{let e=c(o.current);l.current=u===`mounted`?e:`none`},[u]),r(()=>{let t=o.current,n=s.current;if(n!==e){let r=l.current,i=c(t);e?d(`MOUNT`):i===`none`||t?.display===`none`?d(`UNMOUNT`):d(n&&r!==i?`ANIMATION_OUT`:`UNMOUNT`),s.current=e}},[e,d]),r(()=>{if(t){let e,n=t.ownerDocument.defaultView??window,r=r=>{let i=c(o.current).includes(CSS.escape(r.animationName));if(r.target===t&&i&&(d(`ANIMATION_END`),!s.current)){let r=t.style.animationFillMode;t.style.animationFillMode=`forwards`,e=n.setTimeout(()=>{t.style.animationFillMode===`forwards`&&(t.style.animationFillMode=r)})}},i=e=>{e.target===t&&(l.current=c(o.current))};return t.addEventListener(`animationstart`,i),t.addEventListener(`animationcancel`,r),t.addEventListener(`animationend`,r),()=>{n.clearTimeout(e),t.removeEventListener(`animationstart`,i),t.removeEventListener(`animationcancel`,r),t.removeEventListener(`animationend`,r)}}else d(`ANIMATION_END`)},[t,d]),{isPresent:[`mounted`,`unmountSuspended`].includes(u),ref:i.useCallback(e=>{o.current=e?getComputedStyle(e):null,n(e)},[])}}function c(e){return e?.animationName||`none`}function l(e){let t=Object.getOwnPropertyDescriptor(e.props,`ref`)?.get,n=t&&`isReactWarning`in t&&t.isReactWarning;return n?e.ref:(t=Object.getOwnPropertyDescriptor(e,`ref`)?.get,n=t&&`isReactWarning`in t&&t.isReactWarning,n?e.props.ref:e.props.ref||e.ref)}export{o as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{u as n}from"./button-DlGFBuNZ.js";import{n as r}from"./dist-FGEIDBp-.js";var i=e(t(),1);function a(e,t){return i.useReducer((e,n)=>t[e][n]??e,e)}var o=e=>{let{present:t,children:r}=e,a=s(t),o=typeof r==`function`?r({present:a.isPresent}):i.Children.only(r),c=n(a.ref,l(o));return typeof r==`function`||a.isPresent?i.cloneElement(o,{ref:c}):null};o.displayName=`Presence`;function s(e){let[t,n]=i.useState(),o=i.useRef(null),s=i.useRef(e),l=i.useRef(`none`),[u,d]=a(e?`mounted`:`unmounted`,{mounted:{UNMOUNT:`unmounted`,ANIMATION_OUT:`unmountSuspended`},unmountSuspended:{MOUNT:`mounted`,ANIMATION_END:`unmounted`},unmounted:{MOUNT:`mounted`}});return i.useEffect(()=>{let e=c(o.current);l.current=u===`mounted`?e:`none`},[u]),r(()=>{let t=o.current,n=s.current;if(n!==e){let r=l.current,i=c(t);e?d(`MOUNT`):i===`none`||t?.display===`none`?d(`UNMOUNT`):d(n&&r!==i?`ANIMATION_OUT`:`UNMOUNT`),s.current=e}},[e,d]),r(()=>{if(t){let e,n=t.ownerDocument.defaultView??window,r=r=>{let i=c(o.current).includes(CSS.escape(r.animationName));if(r.target===t&&i&&(d(`ANIMATION_END`),!s.current)){let r=t.style.animationFillMode;t.style.animationFillMode=`forwards`,e=n.setTimeout(()=>{t.style.animationFillMode===`forwards`&&(t.style.animationFillMode=r)})}},i=e=>{e.target===t&&(l.current=c(o.current))};return t.addEventListener(`animationstart`,i),t.addEventListener(`animationcancel`,r),t.addEventListener(`animationend`,r),()=>{n.clearTimeout(e),t.removeEventListener(`animationstart`,i),t.removeEventListener(`animationcancel`,r),t.removeEventListener(`animationend`,r)}}else d(`ANIMATION_END`)},[t,d]),{isPresent:[`mounted`,`unmountSuspended`].includes(u),ref:i.useCallback(e=>{o.current=e?getComputedStyle(e):null,n(e)},[])}}function c(e){return e?.animationName||`none`}function l(e){let t=Object.getOwnPropertyDescriptor(e.props,`ref`)?.get,n=t&&`isReactWarning`in t&&t.isReactWarning;return n?e.ref:(t=Object.getOwnPropertyDescriptor(e,`ref`)?.get,n=t&&`isReactWarning`in t&&t.isReactWarning,n?e.props.ref:e.props.ref||e.ref)}export{o as t};
@@ -1 +1 @@
import{r as e,t}from"./chunk-DECur_0Z.js";import{t as n}from"./react-CO2uhaBc.js";import{Af as r}from"./messages-a04pwL1_.js";import{s as i}from"./button-CYi3aIkB.js";var a=t((e=>{var t=n();function r(e){var t=`https://react.dev/errors/`+e;if(1<arguments.length){t+=`?args[]=`+encodeURIComponent(arguments[1]);for(var n=2;n<arguments.length;n++)t+=`&args[]=`+encodeURIComponent(arguments[n])}return`Minified React error #`+e+`; visit `+t+` for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`}function i(){}var a={d:{f:i,r:function(){throw Error(r(522))},D:i,C:i,L:i,m:i,X:i,S:i,M:i},p:0,findDOMNode:null},o=Symbol.for(`react.portal`);function s(e,t,n){var r=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:o,key:r==null?null:``+r,children:e,containerInfo:t,implementation:n}}var c=t.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function l(e,t){if(e===`font`)return``;if(typeof t==`string`)return t===`use-credentials`?t:``}e.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=a,e.createPortal=function(e,t){var n=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!t||t.nodeType!==1&&t.nodeType!==9&&t.nodeType!==11)throw Error(r(299));return s(e,t,null,n)},e.flushSync=function(e){var t=c.T,n=a.p;try{if(c.T=null,a.p=2,e)return e()}finally{c.T=t,a.p=n,a.d.f()}},e.preconnect=function(e,t){typeof e==`string`&&(t?(t=t.crossOrigin,t=typeof t==`string`?t===`use-credentials`?t:``:void 0):t=null,a.d.C(e,t))},e.prefetchDNS=function(e){typeof e==`string`&&a.d.D(e)},e.preinit=function(e,t){if(typeof e==`string`&&t&&typeof t.as==`string`){var n=t.as,r=l(n,t.crossOrigin),i=typeof t.integrity==`string`?t.integrity:void 0,o=typeof t.fetchPriority==`string`?t.fetchPriority:void 0;n===`style`?a.d.S(e,typeof t.precedence==`string`?t.precedence:void 0,{crossOrigin:r,integrity:i,fetchPriority:o}):n===`script`&&a.d.X(e,{crossOrigin:r,integrity:i,fetchPriority:o,nonce:typeof t.nonce==`string`?t.nonce:void 0})}},e.preinitModule=function(e,t){if(typeof e==`string`)if(typeof t==`object`&&t){if(t.as==null||t.as===`script`){var n=l(t.as,t.crossOrigin);a.d.M(e,{crossOrigin:n,integrity:typeof t.integrity==`string`?t.integrity:void 0,nonce:typeof t.nonce==`string`?t.nonce:void 0})}}else t??a.d.M(e)},e.preload=function(e,t){if(typeof e==`string`&&typeof t==`object`&&t&&typeof t.as==`string`){var n=t.as,r=l(n,t.crossOrigin);a.d.L(e,n,{crossOrigin:r,integrity:typeof t.integrity==`string`?t.integrity:void 0,nonce:typeof t.nonce==`string`?t.nonce:void 0,type:typeof t.type==`string`?t.type:void 0,fetchPriority:typeof t.fetchPriority==`string`?t.fetchPriority:void 0,referrerPolicy:typeof t.referrerPolicy==`string`?t.referrerPolicy:void 0,imageSrcSet:typeof t.imageSrcSet==`string`?t.imageSrcSet:void 0,imageSizes:typeof t.imageSizes==`string`?t.imageSizes:void 0,media:typeof t.media==`string`?t.media:void 0})}},e.preloadModule=function(e,t){if(typeof e==`string`)if(t){var n=l(t.as,t.crossOrigin);a.d.m(e,{as:typeof t.as==`string`&&t.as!==`script`?t.as:void 0,crossOrigin:n,integrity:typeof t.integrity==`string`?t.integrity:void 0})}else a.d.m(e)},e.requestFormReset=function(e){a.d.r(e)},e.unstable_batchedUpdates=function(e,t){return e(t)},e.useFormState=function(e,t,n){return c.H.useFormState(e,t,n)},e.useFormStatus=function(){return c.H.useHostTransitionStatus()},e.version=`19.2.5`})),o=t(((e,t)=>{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=a()})),s=e(n(),1),c=e(o(),1),l=r(),u=[`a`,`button`,`div`,`form`,`h2`,`h3`,`img`,`input`,`label`,`li`,`nav`,`ol`,`p`,`select`,`span`,`svg`,`ul`].reduce((e,t)=>{let n=i(`Primitive.${t}`),r=s.forwardRef((e,r)=>{let{asChild:i,...a}=e,o=i?n:t;return typeof window<`u`&&(window[Symbol.for(`radix-ui`)]=!0),(0,l.jsx)(o,{...a,ref:r})});return r.displayName=`Primitive.${t}`,{...e,[t]:r}},{});function d(e,t){e&&c.flushSync(()=>e.dispatchEvent(t))}export{d as n,o as r,u as t}; import{r as e,t}from"./chunk-DECur_0Z.js";import{t as n}from"./react-CO2uhaBc.js";import{dp as r}from"./messages-Bbvu5aaF.js";import{s as i}from"./button-DlGFBuNZ.js";var a=t((e=>{var t=n();function r(e){var t=`https://react.dev/errors/`+e;if(1<arguments.length){t+=`?args[]=`+encodeURIComponent(arguments[1]);for(var n=2;n<arguments.length;n++)t+=`&args[]=`+encodeURIComponent(arguments[n])}return`Minified React error #`+e+`; visit `+t+` for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`}function i(){}var a={d:{f:i,r:function(){throw Error(r(522))},D:i,C:i,L:i,m:i,X:i,S:i,M:i},p:0,findDOMNode:null},o=Symbol.for(`react.portal`);function s(e,t,n){var r=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:o,key:r==null?null:``+r,children:e,containerInfo:t,implementation:n}}var c=t.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function l(e,t){if(e===`font`)return``;if(typeof t==`string`)return t===`use-credentials`?t:``}e.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=a,e.createPortal=function(e,t){var n=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!t||t.nodeType!==1&&t.nodeType!==9&&t.nodeType!==11)throw Error(r(299));return s(e,t,null,n)},e.flushSync=function(e){var t=c.T,n=a.p;try{if(c.T=null,a.p=2,e)return e()}finally{c.T=t,a.p=n,a.d.f()}},e.preconnect=function(e,t){typeof e==`string`&&(t?(t=t.crossOrigin,t=typeof t==`string`?t===`use-credentials`?t:``:void 0):t=null,a.d.C(e,t))},e.prefetchDNS=function(e){typeof e==`string`&&a.d.D(e)},e.preinit=function(e,t){if(typeof e==`string`&&t&&typeof t.as==`string`){var n=t.as,r=l(n,t.crossOrigin),i=typeof t.integrity==`string`?t.integrity:void 0,o=typeof t.fetchPriority==`string`?t.fetchPriority:void 0;n===`style`?a.d.S(e,typeof t.precedence==`string`?t.precedence:void 0,{crossOrigin:r,integrity:i,fetchPriority:o}):n===`script`&&a.d.X(e,{crossOrigin:r,integrity:i,fetchPriority:o,nonce:typeof t.nonce==`string`?t.nonce:void 0})}},e.preinitModule=function(e,t){if(typeof e==`string`)if(typeof t==`object`&&t){if(t.as==null||t.as===`script`){var n=l(t.as,t.crossOrigin);a.d.M(e,{crossOrigin:n,integrity:typeof t.integrity==`string`?t.integrity:void 0,nonce:typeof t.nonce==`string`?t.nonce:void 0})}}else t??a.d.M(e)},e.preload=function(e,t){if(typeof e==`string`&&typeof t==`object`&&t&&typeof t.as==`string`){var n=t.as,r=l(n,t.crossOrigin);a.d.L(e,n,{crossOrigin:r,integrity:typeof t.integrity==`string`?t.integrity:void 0,nonce:typeof t.nonce==`string`?t.nonce:void 0,type:typeof t.type==`string`?t.type:void 0,fetchPriority:typeof t.fetchPriority==`string`?t.fetchPriority:void 0,referrerPolicy:typeof t.referrerPolicy==`string`?t.referrerPolicy:void 0,imageSrcSet:typeof t.imageSrcSet==`string`?t.imageSrcSet:void 0,imageSizes:typeof t.imageSizes==`string`?t.imageSizes:void 0,media:typeof t.media==`string`?t.media:void 0})}},e.preloadModule=function(e,t){if(typeof e==`string`)if(t){var n=l(t.as,t.crossOrigin);a.d.m(e,{as:typeof t.as==`string`&&t.as!==`script`?t.as:void 0,crossOrigin:n,integrity:typeof t.integrity==`string`?t.integrity:void 0})}else a.d.m(e)},e.requestFormReset=function(e){a.d.r(e)},e.unstable_batchedUpdates=function(e,t){return e(t)},e.useFormState=function(e,t,n){return c.H.useFormState(e,t,n)},e.useFormStatus=function(){return c.H.useHostTransitionStatus()},e.version=`19.2.5`})),o=t(((e,t)=>{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=a()})),s=e(n(),1),c=e(o(),1),l=r(),u=[`a`,`button`,`div`,`form`,`h2`,`h3`,`img`,`input`,`label`,`li`,`nav`,`ol`,`p`,`select`,`span`,`svg`,`ul`].reduce((e,t)=>{let n=i(`Primitive.${t}`),r=s.forwardRef((e,r)=>{let{asChild:i,...a}=e,o=i?n:t;return typeof window<`u`&&(window[Symbol.for(`radix-ui`)]=!0),(0,l.jsx)(o,{...a,ref:r})});return r.displayName=`Primitive.${t}`,{...e,[t]:r}},{});function d(e,t){e&&c.flushSync(()=>e.dispatchEvent(t))}export{d as n,o as r,u as t};
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Af as n}from"./messages-a04pwL1_.js";import{s as r,u as i}from"./button-CYi3aIkB.js";import{a}from"./dist-CDVNvXxU.js";var o=e(t(),1),s=n();function c(e){let t=e+`CollectionProvider`,[n,c]=a(t),[l,u]=n(t,{collectionRef:{current:null},itemMap:new Map}),d=e=>{let{scope:t,children:n}=e,r=o.useRef(null),i=o.useRef(new Map).current;return(0,s.jsx)(l,{scope:t,itemMap:i,collectionRef:r,children:n})};d.displayName=t;let f=e+`CollectionSlot`,p=r(f),m=o.forwardRef((e,t)=>{let{scope:n,children:r}=e;return(0,s.jsx)(p,{ref:i(t,u(f,n).collectionRef),children:r})});m.displayName=f;let h=e+`CollectionItemSlot`,g=`data-radix-collection-item`,_=r(h),v=o.forwardRef((e,t)=>{let{scope:n,children:r,...a}=e,c=o.useRef(null),l=i(t,c),d=u(h,n);return o.useEffect(()=>(d.itemMap.set(c,{ref:c,...a}),()=>void d.itemMap.delete(c))),(0,s.jsx)(_,{[g]:``,ref:l,children:r})});v.displayName=h;function y(t){let n=u(e+`CollectionConsumer`,t);return o.useCallback(()=>{let e=n.collectionRef.current;if(!e)return[];let t=Array.from(e.querySelectorAll(`[${g}]`));return Array.from(n.itemMap.values()).sort((e,n)=>t.indexOf(e.ref.current)-t.indexOf(n.ref.current))},[n.collectionRef,n.itemMap])}return[{Provider:d,Slot:m,ItemSlot:v},y,c]}export{c as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{dp as n}from"./messages-Bbvu5aaF.js";import{s as r,u as i}from"./button-DlGFBuNZ.js";import{a}from"./dist-FGEIDBp-.js";var o=e(t(),1),s=n();function c(e){let t=e+`CollectionProvider`,[n,c]=a(t),[l,u]=n(t,{collectionRef:{current:null},itemMap:new Map}),d=e=>{let{scope:t,children:n}=e,r=o.useRef(null),i=o.useRef(new Map).current;return(0,s.jsx)(l,{scope:t,itemMap:i,collectionRef:r,children:n})};d.displayName=t;let f=e+`CollectionSlot`,p=r(f),m=o.forwardRef((e,t)=>{let{scope:n,children:r}=e;return(0,s.jsx)(p,{ref:i(t,u(f,n).collectionRef),children:r})});m.displayName=f;let h=e+`CollectionItemSlot`,g=`data-radix-collection-item`,_=r(h),v=o.forwardRef((e,t)=>{let{scope:n,children:r,...a}=e,c=o.useRef(null),l=i(t,c),d=u(h,n);return o.useEffect(()=>(d.itemMap.set(c,{ref:c,...a}),()=>void d.itemMap.delete(c))),(0,s.jsx)(_,{[g]:``,ref:l,children:r})});v.displayName=h;function y(t){let n=u(e+`CollectionConsumer`,t);return o.useCallback(()=>{let e=n.collectionRef.current;if(!e)return[];let t=Array.from(e.querySelectorAll(`[${g}]`));return Array.from(n.itemMap.values()).sort((e,n)=>t.indexOf(e.ref.current)-t.indexOf(n.ref.current))},[n.collectionRef,n.itemMap])}return[{Provider:d,Slot:m,ItemSlot:v},y,c]}export{c as t};
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Af as n}from"./messages-a04pwL1_.js";import{n as r,r as i,t as a}from"./dist-zN1ah_GY.js";import{u as o}from"./button-CYi3aIkB.js";import{n as s,r as c}from"./dist-CDVNvXxU.js";import{t as l}from"./dist-JWsv7m_D.js";var u=e(t(),1);function d(e,t=globalThis?.document){let n=l(e);u.useEffect(()=>{let e=e=>{e.key===`Escape`&&n(e)};return t.addEventListener(`keydown`,e,{capture:!0}),()=>t.removeEventListener(`keydown`,e,{capture:!0})},[n,t])}var f=n(),p=`DismissableLayer`,m=`dismissableLayer.update`,h=`dismissableLayer.pointerDownOutside`,g=`dismissableLayer.focusOutside`,_,v=u.createContext({layers:new Set,layersWithOutsidePointerEventsDisabled:new Set,branches:new Set}),y=u.forwardRef((e,t)=>{let{disableOutsidePointerEvents:n=!1,onEscapeKeyDown:r,onPointerDownOutside:i,onFocusOutside:s,onInteractOutside:l,onDismiss:p,...h}=e,g=u.useContext(v),[y,b]=u.useState(null),x=y?.ownerDocument??globalThis?.document,[,T]=u.useState({}),E=o(t,e=>b(e)),D=Array.from(g.layers),[O]=[...g.layersWithOutsidePointerEventsDisabled].slice(-1),k=D.indexOf(O),A=y?D.indexOf(y):-1,j=g.layersWithOutsidePointerEventsDisabled.size>0,M=A>=k,N=S(e=>{let t=e.target,n=[...g.branches].some(e=>e.contains(t));!M||n||(i?.(e),l?.(e),e.defaultPrevented||p?.())},x),P=C(e=>{let t=e.target;[...g.branches].some(e=>e.contains(t))||(s?.(e),l?.(e),e.defaultPrevented||p?.())},x);return d(e=>{A===g.layers.size-1&&(r?.(e),!e.defaultPrevented&&p&&(e.preventDefault(),p()))},x),u.useEffect(()=>{if(y)return n&&(g.layersWithOutsidePointerEventsDisabled.size===0&&(_=x.body.style.pointerEvents,x.body.style.pointerEvents=`none`),g.layersWithOutsidePointerEventsDisabled.add(y)),g.layers.add(y),w(),()=>{n&&g.layersWithOutsidePointerEventsDisabled.size===1&&(x.body.style.pointerEvents=_)}},[y,x,n,g]),u.useEffect(()=>()=>{y&&(g.layers.delete(y),g.layersWithOutsidePointerEventsDisabled.delete(y),w())},[y,g]),u.useEffect(()=>{let e=()=>T({});return document.addEventListener(m,e),()=>document.removeEventListener(m,e)},[]),(0,f.jsx)(a.div,{...h,ref:E,style:{pointerEvents:j?M?`auto`:`none`:void 0,...e.style},onFocusCapture:c(e.onFocusCapture,P.onFocusCapture),onBlurCapture:c(e.onBlurCapture,P.onBlurCapture),onPointerDownCapture:c(e.onPointerDownCapture,N.onPointerDownCapture)})});y.displayName=p;var b=`DismissableLayerBranch`,x=u.forwardRef((e,t)=>{let n=u.useContext(v),r=u.useRef(null),i=o(t,r);return u.useEffect(()=>{let e=r.current;if(e)return n.branches.add(e),()=>{n.branches.delete(e)}},[n.branches]),(0,f.jsx)(a.div,{...e,ref:i})});x.displayName=b;function S(e,t=globalThis?.document){let n=l(e),r=u.useRef(!1),i=u.useRef(()=>{});return u.useEffect(()=>{let e=e=>{if(e.target&&!r.current){let r=function(){T(h,n,a,{discrete:!0})},a={originalEvent:e};e.pointerType===`touch`?(t.removeEventListener(`click`,i.current),i.current=r,t.addEventListener(`click`,i.current,{once:!0})):r()}else t.removeEventListener(`click`,i.current);r.current=!1},a=window.setTimeout(()=>{t.addEventListener(`pointerdown`,e)},0);return()=>{window.clearTimeout(a),t.removeEventListener(`pointerdown`,e),t.removeEventListener(`click`,i.current)}},[t,n]),{onPointerDownCapture:()=>r.current=!0}}function C(e,t=globalThis?.document){let n=l(e),r=u.useRef(!1);return u.useEffect(()=>{let e=e=>{e.target&&!r.current&&T(g,n,{originalEvent:e},{discrete:!1})};return t.addEventListener(`focusin`,e),()=>t.removeEventListener(`focusin`,e)},[t,n]),{onFocusCapture:()=>r.current=!0,onBlurCapture:()=>r.current=!1}}function w(){let e=new CustomEvent(m);document.dispatchEvent(e)}function T(e,t,n,{discrete:i}){let a=n.originalEvent.target,o=new CustomEvent(e,{bubbles:!1,cancelable:!0,detail:n});t&&a.addEventListener(e,t,{once:!0}),i?r(a,o):a.dispatchEvent(o)}var E=e(i(),1),D=`Portal`,O=u.forwardRef((e,t)=>{let{container:n,...r}=e,[i,o]=u.useState(!1);s(()=>o(!0),[]);let c=n||i&&globalThis?.document?.body;return c?E.createPortal((0,f.jsx)(a.div,{...r,ref:t}),c):null});O.displayName=D;export{y as n,O as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{dp as n}from"./messages-Bbvu5aaF.js";import{n as r,r as i,t as a}from"./dist-BkAVwTGu.js";import{u as o}from"./button-DlGFBuNZ.js";import{n as s,r as c}from"./dist-FGEIDBp-.js";import{t as l}from"./dist--ZhT0Dzg.js";var u=e(t(),1);function d(e,t=globalThis?.document){let n=l(e);u.useEffect(()=>{let e=e=>{e.key===`Escape`&&n(e)};return t.addEventListener(`keydown`,e,{capture:!0}),()=>t.removeEventListener(`keydown`,e,{capture:!0})},[n,t])}var f=n(),p=`DismissableLayer`,m=`dismissableLayer.update`,h=`dismissableLayer.pointerDownOutside`,g=`dismissableLayer.focusOutside`,_,v=u.createContext({layers:new Set,layersWithOutsidePointerEventsDisabled:new Set,branches:new Set}),y=u.forwardRef((e,t)=>{let{disableOutsidePointerEvents:n=!1,onEscapeKeyDown:r,onPointerDownOutside:i,onFocusOutside:s,onInteractOutside:l,onDismiss:p,...h}=e,g=u.useContext(v),[y,b]=u.useState(null),x=y?.ownerDocument??globalThis?.document,[,T]=u.useState({}),E=o(t,e=>b(e)),D=Array.from(g.layers),[O]=[...g.layersWithOutsidePointerEventsDisabled].slice(-1),k=D.indexOf(O),A=y?D.indexOf(y):-1,j=g.layersWithOutsidePointerEventsDisabled.size>0,M=A>=k,N=S(e=>{let t=e.target,n=[...g.branches].some(e=>e.contains(t));!M||n||(i?.(e),l?.(e),e.defaultPrevented||p?.())},x),P=C(e=>{let t=e.target;[...g.branches].some(e=>e.contains(t))||(s?.(e),l?.(e),e.defaultPrevented||p?.())},x);return d(e=>{A===g.layers.size-1&&(r?.(e),!e.defaultPrevented&&p&&(e.preventDefault(),p()))},x),u.useEffect(()=>{if(y)return n&&(g.layersWithOutsidePointerEventsDisabled.size===0&&(_=x.body.style.pointerEvents,x.body.style.pointerEvents=`none`),g.layersWithOutsidePointerEventsDisabled.add(y)),g.layers.add(y),w(),()=>{n&&g.layersWithOutsidePointerEventsDisabled.size===1&&(x.body.style.pointerEvents=_)}},[y,x,n,g]),u.useEffect(()=>()=>{y&&(g.layers.delete(y),g.layersWithOutsidePointerEventsDisabled.delete(y),w())},[y,g]),u.useEffect(()=>{let e=()=>T({});return document.addEventListener(m,e),()=>document.removeEventListener(m,e)},[]),(0,f.jsx)(a.div,{...h,ref:E,style:{pointerEvents:j?M?`auto`:`none`:void 0,...e.style},onFocusCapture:c(e.onFocusCapture,P.onFocusCapture),onBlurCapture:c(e.onBlurCapture,P.onBlurCapture),onPointerDownCapture:c(e.onPointerDownCapture,N.onPointerDownCapture)})});y.displayName=p;var b=`DismissableLayerBranch`,x=u.forwardRef((e,t)=>{let n=u.useContext(v),r=u.useRef(null),i=o(t,r);return u.useEffect(()=>{let e=r.current;if(e)return n.branches.add(e),()=>{n.branches.delete(e)}},[n.branches]),(0,f.jsx)(a.div,{...e,ref:i})});x.displayName=b;function S(e,t=globalThis?.document){let n=l(e),r=u.useRef(!1),i=u.useRef(()=>{});return u.useEffect(()=>{let e=e=>{if(e.target&&!r.current){let r=function(){T(h,n,a,{discrete:!0})},a={originalEvent:e};e.pointerType===`touch`?(t.removeEventListener(`click`,i.current),i.current=r,t.addEventListener(`click`,i.current,{once:!0})):r()}else t.removeEventListener(`click`,i.current);r.current=!1},a=window.setTimeout(()=>{t.addEventListener(`pointerdown`,e)},0);return()=>{window.clearTimeout(a),t.removeEventListener(`pointerdown`,e),t.removeEventListener(`click`,i.current)}},[t,n]),{onPointerDownCapture:()=>r.current=!0}}function C(e,t=globalThis?.document){let n=l(e),r=u.useRef(!1);return u.useEffect(()=>{let e=e=>{e.target&&!r.current&&T(g,n,{originalEvent:e},{discrete:!1})};return t.addEventListener(`focusin`,e),()=>t.removeEventListener(`focusin`,e)},[t,n]),{onFocusCapture:()=>r.current=!0,onBlurCapture:()=>r.current=!1}}function w(){let e=new CustomEvent(m);document.dispatchEvent(e)}function T(e,t,n,{discrete:i}){let a=n.originalEvent.target,o=new CustomEvent(e,{bubbles:!1,cancelable:!0,detail:n});t&&a.addEventListener(e,t,{once:!0}),i?r(a,o):a.dispatchEvent(o)}var E=e(i(),1),D=`Portal`,O=u.forwardRef((e,t)=>{let{container:n,...r}=e,[i,o]=u.useState(!1);s(()=>o(!0),[]);let c=n||i&&globalThis?.document?.body;return c?E.createPortal((0,f.jsx)(a.div,{...r,ref:t}),c):null});O.displayName=D;export{y as n,O as t};
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Af as n}from"./messages-a04pwL1_.js";var r=e(t(),1),i=n(),a=r.createContext(void 0),o=e=>{let{dir:t,children:n}=e;return(0,i.jsx)(a.Provider,{value:t,children:n})};function s(e){let t=r.useContext(a);return e||t||`ltr`}export{s as n,o as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{dp as n}from"./messages-Bbvu5aaF.js";var r=e(t(),1),i=n(),a=r.createContext(void 0),o=e=>{let{dir:t,children:n}=e;return(0,i.jsx)(a.Provider,{value:t,children:n})};function s(e){let t=r.useContext(a);return e||t||`ltr`}export{s as n,o as t};
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Af as n}from"./messages-a04pwL1_.js";import{t as r}from"./dist-zN1ah_GY.js";import{u as i}from"./button-CYi3aIkB.js";import{a,r as o,t as s}from"./dist-CDVNvXxU.js";import{t as c}from"./dist-luTOV918.js";import{n as l,t as u}from"./dist-JWsv7m_D.js";import{n as d}from"./dist-Dw4KnGRx.js";var f=e(t(),1),p=n(),m=`rovingFocusGroup.onEntryFocus`,h={bubbles:!1,cancelable:!0},g=`RovingFocusGroup`,[_,v,y]=c(g),[b,x]=a(g,[y]),[S,C]=b(g),w=f.forwardRef((e,t)=>(0,p.jsx)(_.Provider,{scope:e.__scopeRovingFocusGroup,children:(0,p.jsx)(_.Slot,{scope:e.__scopeRovingFocusGroup,children:(0,p.jsx)(T,{...e,ref:t})})}));w.displayName=g;var T=f.forwardRef((e,t)=>{let{__scopeRovingFocusGroup:n,orientation:a,loop:c=!1,dir:l,currentTabStopId:_,defaultCurrentTabStopId:y,onCurrentTabStopIdChange:b,onEntryFocus:x,preventScrollOnEntryFocus:C=!1,...w}=e,T=f.useRef(null),E=i(t,T),D=d(l),[O,k]=s({prop:_,defaultProp:y??null,onChange:b,caller:g}),[A,M]=f.useState(!1),N=u(x),P=v(n),F=f.useRef(!1),[I,L]=f.useState(0);return f.useEffect(()=>{let e=T.current;if(e)return e.addEventListener(m,N),()=>e.removeEventListener(m,N)},[N]),(0,p.jsx)(S,{scope:n,orientation:a,dir:D,loop:c,currentTabStopId:O,onItemFocus:f.useCallback(e=>k(e),[k]),onItemShiftTab:f.useCallback(()=>M(!0),[]),onFocusableItemAdd:f.useCallback(()=>L(e=>e+1),[]),onFocusableItemRemove:f.useCallback(()=>L(e=>e-1),[]),children:(0,p.jsx)(r.div,{tabIndex:A||I===0?-1:0,"data-orientation":a,...w,ref:E,style:{outline:`none`,...e.style},onMouseDown:o(e.onMouseDown,()=>{F.current=!0}),onFocus:o(e.onFocus,e=>{let t=!F.current;if(e.target===e.currentTarget&&t&&!A){let t=new CustomEvent(m,h);if(e.currentTarget.dispatchEvent(t),!t.defaultPrevented){let e=P().filter(e=>e.focusable);j([e.find(e=>e.active),e.find(e=>e.id===O),...e].filter(Boolean).map(e=>e.ref.current),C)}}F.current=!1}),onBlur:o(e.onBlur,()=>M(!1))})})}),E=`RovingFocusGroupItem`,D=f.forwardRef((e,t)=>{let{__scopeRovingFocusGroup:n,focusable:i=!0,active:a=!1,tabStopId:s,children:c,...u}=e,d=l(),m=s||d,h=C(E,n),g=h.currentTabStopId===m,y=v(n),{onFocusableItemAdd:b,onFocusableItemRemove:x,currentTabStopId:S}=h;return f.useEffect(()=>{if(i)return b(),()=>x()},[i,b,x]),(0,p.jsx)(_.ItemSlot,{scope:n,id:m,focusable:i,active:a,children:(0,p.jsx)(r.span,{tabIndex:g?0:-1,"data-orientation":h.orientation,...u,ref:t,onMouseDown:o(e.onMouseDown,e=>{i?h.onItemFocus(m):e.preventDefault()}),onFocus:o(e.onFocus,()=>h.onItemFocus(m)),onKeyDown:o(e.onKeyDown,e=>{if(e.key===`Tab`&&e.shiftKey){h.onItemShiftTab();return}if(e.target!==e.currentTarget)return;let t=A(e,h.orientation,h.dir);if(t!==void 0){if(e.metaKey||e.ctrlKey||e.altKey||e.shiftKey)return;e.preventDefault();let n=y().filter(e=>e.focusable).map(e=>e.ref.current);if(t===`last`)n.reverse();else if(t===`prev`||t===`next`){t===`prev`&&n.reverse();let r=n.indexOf(e.currentTarget);n=h.loop?M(n,r+1):n.slice(r+1)}setTimeout(()=>j(n))}}),children:typeof c==`function`?c({isCurrentTabStop:g,hasTabStop:S!=null}):c})})});D.displayName=E;var O={ArrowLeft:`prev`,ArrowUp:`prev`,ArrowRight:`next`,ArrowDown:`next`,PageUp:`first`,Home:`first`,PageDown:`last`,End:`last`};function k(e,t){return t===`rtl`?e===`ArrowLeft`?`ArrowRight`:e===`ArrowRight`?`ArrowLeft`:e:e}function A(e,t,n){let r=k(e.key,n);if(!(t===`vertical`&&[`ArrowLeft`,`ArrowRight`].includes(r))&&!(t===`horizontal`&&[`ArrowUp`,`ArrowDown`].includes(r)))return O[r]}function j(e,t=!1){let n=document.activeElement;for(let r of e)if(r===n||(r.focus({preventScroll:t}),document.activeElement!==n))return}function M(e,t){return e.map((n,r)=>e[(t+r)%e.length])}var N=w,P=D;export{N as n,x as r,P as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{dp as n}from"./messages-Bbvu5aaF.js";import{t as r}from"./dist-BkAVwTGu.js";import{u as i}from"./button-DlGFBuNZ.js";import{a,r as o,t as s}from"./dist-FGEIDBp-.js";import{t as c}from"./dist-CLy3Z8cG.js";import{n as l,t as u}from"./dist--ZhT0Dzg.js";import{n as d}from"./dist-DI4dxjh-.js";var f=e(t(),1),p=n(),m=`rovingFocusGroup.onEntryFocus`,h={bubbles:!1,cancelable:!0},g=`RovingFocusGroup`,[_,v,y]=c(g),[b,x]=a(g,[y]),[S,C]=b(g),w=f.forwardRef((e,t)=>(0,p.jsx)(_.Provider,{scope:e.__scopeRovingFocusGroup,children:(0,p.jsx)(_.Slot,{scope:e.__scopeRovingFocusGroup,children:(0,p.jsx)(T,{...e,ref:t})})}));w.displayName=g;var T=f.forwardRef((e,t)=>{let{__scopeRovingFocusGroup:n,orientation:a,loop:c=!1,dir:l,currentTabStopId:_,defaultCurrentTabStopId:y,onCurrentTabStopIdChange:b,onEntryFocus:x,preventScrollOnEntryFocus:C=!1,...w}=e,T=f.useRef(null),E=i(t,T),D=d(l),[O,k]=s({prop:_,defaultProp:y??null,onChange:b,caller:g}),[A,M]=f.useState(!1),N=u(x),P=v(n),F=f.useRef(!1),[I,L]=f.useState(0);return f.useEffect(()=>{let e=T.current;if(e)return e.addEventListener(m,N),()=>e.removeEventListener(m,N)},[N]),(0,p.jsx)(S,{scope:n,orientation:a,dir:D,loop:c,currentTabStopId:O,onItemFocus:f.useCallback(e=>k(e),[k]),onItemShiftTab:f.useCallback(()=>M(!0),[]),onFocusableItemAdd:f.useCallback(()=>L(e=>e+1),[]),onFocusableItemRemove:f.useCallback(()=>L(e=>e-1),[]),children:(0,p.jsx)(r.div,{tabIndex:A||I===0?-1:0,"data-orientation":a,...w,ref:E,style:{outline:`none`,...e.style},onMouseDown:o(e.onMouseDown,()=>{F.current=!0}),onFocus:o(e.onFocus,e=>{let t=!F.current;if(e.target===e.currentTarget&&t&&!A){let t=new CustomEvent(m,h);if(e.currentTarget.dispatchEvent(t),!t.defaultPrevented){let e=P().filter(e=>e.focusable);j([e.find(e=>e.active),e.find(e=>e.id===O),...e].filter(Boolean).map(e=>e.ref.current),C)}}F.current=!1}),onBlur:o(e.onBlur,()=>M(!1))})})}),E=`RovingFocusGroupItem`,D=f.forwardRef((e,t)=>{let{__scopeRovingFocusGroup:n,focusable:i=!0,active:a=!1,tabStopId:s,children:c,...u}=e,d=l(),m=s||d,h=C(E,n),g=h.currentTabStopId===m,y=v(n),{onFocusableItemAdd:b,onFocusableItemRemove:x,currentTabStopId:S}=h;return f.useEffect(()=>{if(i)return b(),()=>x()},[i,b,x]),(0,p.jsx)(_.ItemSlot,{scope:n,id:m,focusable:i,active:a,children:(0,p.jsx)(r.span,{tabIndex:g?0:-1,"data-orientation":h.orientation,...u,ref:t,onMouseDown:o(e.onMouseDown,e=>{i?h.onItemFocus(m):e.preventDefault()}),onFocus:o(e.onFocus,()=>h.onItemFocus(m)),onKeyDown:o(e.onKeyDown,e=>{if(e.key===`Tab`&&e.shiftKey){h.onItemShiftTab();return}if(e.target!==e.currentTarget)return;let t=A(e,h.orientation,h.dir);if(t!==void 0){if(e.metaKey||e.ctrlKey||e.altKey||e.shiftKey)return;e.preventDefault();let n=y().filter(e=>e.focusable).map(e=>e.ref.current);if(t===`last`)n.reverse();else if(t===`prev`||t===`next`){t===`prev`&&n.reverse();let r=n.indexOf(e.currentTarget);n=h.loop?M(n,r+1):n.slice(r+1)}setTimeout(()=>j(n))}}),children:typeof c==`function`?c({isCurrentTabStop:g,hasTabStop:S!=null}):c})})});D.displayName=E;var O={ArrowLeft:`prev`,ArrowUp:`prev`,ArrowRight:`next`,ArrowDown:`next`,PageUp:`first`,Home:`first`,PageDown:`last`,End:`last`};function k(e,t){return t===`rtl`?e===`ArrowLeft`?`ArrowRight`:e===`ArrowRight`?`ArrowLeft`:e:e}function A(e,t,n){let r=k(e.key,n);if(!(t===`vertical`&&[`ArrowLeft`,`ArrowRight`].includes(r))&&!(t===`horizontal`&&[`ArrowUp`,`ArrowDown`].includes(r)))return O[r]}function j(e,t=!1){let n=document.activeElement;for(let r of e)if(r===n||(r.focus({preventScroll:t}),document.activeElement!==n))return}function M(e,t){return e.map((n,r)=>e[(t+r)%e.length])}var N=w,P=D;export{N as n,x as r,P as t};
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Af as n}from"./messages-a04pwL1_.js";var r=e(t(),1),i=n();function a(e,t){let n=r.createContext(t),a=e=>{let{children:t,...a}=e,o=r.useMemo(()=>a,Object.values(a));return(0,i.jsx)(n.Provider,{value:o,children:t})};a.displayName=e+`Provider`;function o(i){let a=r.useContext(n);if(a)return a;if(t!==void 0)return t;throw Error(`\`${i}\` must be used within \`${e}\``)}return[a,o]}function o(e,t=[]){let n=[];function a(t,a){let o=r.createContext(a),s=n.length;n=[...n,a];let c=t=>{let{scope:n,children:a,...c}=t,l=n?.[e]?.[s]||o,u=r.useMemo(()=>c,Object.values(c));return(0,i.jsx)(l.Provider,{value:u,children:a})};c.displayName=t+`Provider`;function l(n,i){let c=i?.[e]?.[s]||o,l=r.useContext(c);if(l)return l;if(a!==void 0)return a;throw Error(`\`${n}\` must be used within \`${t}\``)}return[c,l]}let o=()=>{let t=n.map(e=>r.createContext(e));return function(n){let i=n?.[e]||t;return r.useMemo(()=>({[`__scope${e}`]:{...n,[e]:i}}),[n,i])}};return o.scopeName=e,[a,s(o,...t)]}function s(...e){let t=e[0];if(e.length===1)return t;let n=()=>{let n=e.map(e=>({useScope:e(),scopeName:e.scopeName}));return function(e){let i=n.reduce((t,{useScope:n,scopeName:r})=>{let i=n(e)[`__scope${r}`];return{...t,...i}},{});return r.useMemo(()=>({[`__scope${t.scopeName}`]:i}),[i])}};return n.scopeName=t.scopeName,n}typeof window<`u`&&window.document&&window.document.createElement;function c(e,t,{checkForDefaultPrevented:n=!0}={}){return function(r){if(e?.(r),n===!1||!r.defaultPrevented)return t?.(r)}}var l=globalThis?.document?r.useLayoutEffect:()=>{},u=r.useInsertionEffect||l;function d({prop:e,defaultProp:t,onChange:n=()=>{},caller:i}){let[a,o,s]=f({defaultProp:t,onChange:n}),c=e!==void 0,l=c?e:a;{let t=r.useRef(e!==void 0);r.useEffect(()=>{let e=t.current;e!==c&&console.warn(`${i} is changing from ${e?`controlled`:`uncontrolled`} to ${c?`controlled`:`uncontrolled`}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`),t.current=c},[c,i])}return[l,r.useCallback(t=>{if(c){let n=p(t)?t(e):t;n!==e&&s.current?.(n)}else o(t)},[c,e,o,s])]}function f({defaultProp:e,onChange:t}){let[n,i]=r.useState(e),a=r.useRef(n),o=r.useRef(t);return u(()=>{o.current=t},[t]),r.useEffect(()=>{a.current!==n&&(o.current?.(n),a.current=n)},[n,a]),[n,i,o]}function p(e){return typeof e==`function`}export{o as a,a as i,l as n,c as r,d as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{dp as n}from"./messages-Bbvu5aaF.js";var r=e(t(),1),i=n();function a(e,t){let n=r.createContext(t),a=e=>{let{children:t,...a}=e,o=r.useMemo(()=>a,Object.values(a));return(0,i.jsx)(n.Provider,{value:o,children:t})};a.displayName=e+`Provider`;function o(i){let a=r.useContext(n);if(a)return a;if(t!==void 0)return t;throw Error(`\`${i}\` must be used within \`${e}\``)}return[a,o]}function o(e,t=[]){let n=[];function a(t,a){let o=r.createContext(a),s=n.length;n=[...n,a];let c=t=>{let{scope:n,children:a,...c}=t,l=n?.[e]?.[s]||o,u=r.useMemo(()=>c,Object.values(c));return(0,i.jsx)(l.Provider,{value:u,children:a})};c.displayName=t+`Provider`;function l(n,i){let c=i?.[e]?.[s]||o,l=r.useContext(c);if(l)return l;if(a!==void 0)return a;throw Error(`\`${n}\` must be used within \`${t}\``)}return[c,l]}let o=()=>{let t=n.map(e=>r.createContext(e));return function(n){let i=n?.[e]||t;return r.useMemo(()=>({[`__scope${e}`]:{...n,[e]:i}}),[n,i])}};return o.scopeName=e,[a,s(o,...t)]}function s(...e){let t=e[0];if(e.length===1)return t;let n=()=>{let n=e.map(e=>({useScope:e(),scopeName:e.scopeName}));return function(e){let i=n.reduce((t,{useScope:n,scopeName:r})=>{let i=n(e)[`__scope${r}`];return{...t,...i}},{});return r.useMemo(()=>({[`__scope${t.scopeName}`]:i}),[i])}};return n.scopeName=t.scopeName,n}typeof window<`u`&&window.document&&window.document.createElement;function c(e,t,{checkForDefaultPrevented:n=!0}={}){return function(r){if(e?.(r),n===!1||!r.defaultPrevented)return t?.(r)}}var l=globalThis?.document?r.useLayoutEffect:()=>{},u=r.useInsertionEffect||l;function d({prop:e,defaultProp:t,onChange:n=()=>{},caller:i}){let[a,o,s]=f({defaultProp:t,onChange:n}),c=e!==void 0,l=c?e:a;{let t=r.useRef(e!==void 0);r.useEffect(()=>{let e=t.current;e!==c&&console.warn(`${i} is changing from ${e?`controlled`:`uncontrolled`} to ${c?`controlled`:`uncontrolled`}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`),t.current=c},[c,i])}return[l,r.useCallback(t=>{if(c){let n=p(t)?t(e):t;n!==e&&s.current?.(n)}else o(t)},[c,e,o,s])]}function f({defaultProp:e,onChange:t}){let[n,i]=r.useState(e),a=r.useRef(n),o=r.useRef(t);return u(()=>{o.current=t},[t]),r.useEffect(()=>{a.current!==n&&(o.current?.(n),a.current=n)},[n,a]),[n,i,o]}function p(e){return typeof e==`function`}export{o as a,a as i,l as n,c as r,d as t};
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More