4 Commits

Author SHA1 Message Date
line-6000 04c2b9b0b6 Merge pull request #87 from GMWalletApp/dev 2026-06-12 00:50:10 +08:00
line-6000 9ac9894dba feat: add runtime log level settings 2026-06-12 00:49:18 +08:00
line-6000 9b397bd45e feat: support TON and USDT Jetton payments
- add TON chain, token, RPC, address normalization, and order matching support
- add liteclient scanner with masterchain/shard catch-up and Jetton validation
- add TON manual payment verification and focused tests
2026-06-11 14:47:46 +08:00
line-6000 225cae2c24 Merge pull request #86 from GMWalletApp/dev
feat: add in-memory RPC runtime stats SSE endpoint
2026-06-10 00:19:38 +08:00
179 changed files with 3617 additions and 229 deletions
+8
View File
@@ -6,6 +6,7 @@ import (
"github.com/GMWalletApp/epusdt/config" "github.com/GMWalletApp/epusdt/config"
"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/mq" "github.com/GMWalletApp/epusdt/mq"
"github.com/GMWalletApp/epusdt/task" "github.com/GMWalletApp/epusdt/task"
"github.com/GMWalletApp/epusdt/telegram" "github.com/GMWalletApp/epusdt/telegram"
@@ -21,6 +22,13 @@ func InitApp() {
config.Init() config.Init()
log.Init() log.Init()
dao.Init() dao.Init()
logLevel := data.GetSettingString(mdb.SettingKeySystemLogLevel, mdb.SettingDefaultSystemLogLevel)
if err := log.SetLevel(logLevel); err != nil {
color.Red.Printf("[bootstrap] apply log level setting %q err=%s; fallback=%s\n", logLevel, err, mdb.SettingDefaultSystemLogLevel)
if fallbackErr := log.SetLevel(mdb.SettingDefaultSystemLogLevel); fallbackErr != nil {
color.Red.Printf("[bootstrap] apply fallback log level err=%s\n", fallbackErr)
}
}
// Wire settings-table lookups into the config package so // Wire settings-table lookups into the config package so
// GetRateApiUrl / GetUsdtRate can consult admin-configured values. // GetRateApiUrl / GetUsdtRate can consult admin-configured values.
config.SettingsGetString = func(key string) string { config.SettingsGetString = func(key string) string {
+7 -1
View File
@@ -352,7 +352,13 @@ func GetRateForCoin(coin string, base string) float64 {
if coin == "usdt" && base == "usd" { if coin == "usdt" && base == "usd" {
return 1 return 1
} }
return getRateForCoinFromAPI(coin, base) if rate := getRateForCoinFromAPI(coin, base); rate > 0 {
return rate
}
if coin == "ton" {
return getRateForCoinFromAPI("toncoin", base)
}
return 0
} }
func getForcedRateForCoin(coin string, base string) float64 { func getForcedRateForCoin(coin string, base string) float64 {
+29
View File
@@ -329,6 +329,35 @@ func TestGetRateForCoinUsesAPIWhenForcedPairMissing(t *testing.T) {
} }
} }
func TestGetRateForCoinUsesToncoinAPIFallbackForTon(t *testing.T) {
viper.Reset()
t.Cleanup(viper.Reset)
t.Setenv("API_RATE_URL", "")
installMockHTTPClient(t, func(r *http.Request) (*http.Response, error) {
if r.URL.Path != "/cny.json" {
t.Fatalf("rate api path = %s, want /cny.json", r.URL.Path)
}
return &http.Response{
StatusCode: http.StatusOK,
Status: "200 OK",
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"cny":{"toncoin":1.82}}`)),
Request: r,
}, nil
})
installSettingsGetter(t, map[string]string{
"rate.forced_rate_list": `{"cny":{"ton":0}}`,
"rate.api_url": "https://rate.example.test",
})
rate := GetRateForCoin("ton", "cny")
if math.Abs(rate-1.82) > 1e-9 {
t.Fatalf("GetRateForCoin(ton, cny) = %v, want 1.82", rate)
}
}
func TestGetUsdtRateReturnsZeroWhenAPIUnavailableWithoutAdminOverride(t *testing.T) { func TestGetUsdtRateReturnsZeroWhenAPIUnavailableWithoutAdminOverride(t *testing.T) {
viper.Reset() viper.Reset()
t.Cleanup(viper.Reset) t.Cleanup(viper.Reset)
+2 -3
View File
@@ -14,8 +14,7 @@ type UpdateChainRequest struct {
DisplayName *string `json:"display_name" example:"Tron"` DisplayName *string `json:"display_name" example:"Tron"`
} }
// ListChains returns the chains table (tron/ethereum/solana seeded; new // ListChains returns the chains table for all seeded supported networks.
// networks like bsc/polygon inserted manually once supported).
// @Summary List chains // @Summary List chains
// @Description Returns all supported blockchain networks // @Description Returns all supported blockchain networks
// @Tags Admin Chains // @Tags Admin Chains
@@ -41,7 +40,7 @@ func (c *BaseAdminController) ListChains(ctx echo.Context) error {
// @Security AdminJWT // @Security AdminJWT
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param network path string true "Network name (e.g. tron, ethereum, solana)" // @Param network path string true "Network name (e.g. ton, tron, ethereum, solana)"
// @Param request body admin.UpdateChainRequest true "Chain settings" // @Param request body admin.UpdateChainRequest true "Chain settings"
// @Success 200 {object} response.ApiResponse // @Success 200 {object} response.ApiResponse
// @Failure 400 {object} response.ApiResponse // @Failure 400 {object} response.ApiResponse
+1
View File
@@ -132,6 +132,7 @@ func (c *BaseAdminController) CloseOrder(ctx echo.Context) error {
// callback + notification fire normally. // callback + notification fire normally.
// @Summary Mark order paid // @Summary Mark order paid
// @Description Manually mark a waiting order as paid (operator补单) // @Description Manually mark a waiting order as paid (operator补单)
// @Description TON accepts canonical ton:<receive_raw>:<lt>:<hash>, lt:hash, or a unique recent hash-only reference for the order receive address.
// @Tags Admin Orders // @Tags Admin Orders
// @Security AdminJWT // @Security AdminJWT
// @Accept json // @Accept json
+9 -5
View File
@@ -16,8 +16,8 @@ import (
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长连接 lite=TON liteserver配置
Type string `json:"type" validate:"required|in:http,ws" enums:"http,ws" example:"http"` Type string `json:"type" validate:"required|in:http,ws,lite" enums:"http,ws,lite" 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"`
@@ -187,9 +187,8 @@ func (c *BaseAdminController) DeleteRpcNode(ctx echo.Context) error {
} }
// HealthCheckRpcNode performs an on-demand probe and writes the result. // HealthCheckRpcNode performs an on-demand probe and writes the result.
// For HTTP endpoints this is a GET to the URL with a short timeout; for // For HTTP/WS/lite endpoints this is a TCP-level check against the configured
// WS we just attempt a TCP-level check via the same HTTP client (the // URL host. TON lite rows usually point to a global.config.json HTTPS URL.
// handshake URL resolves the host identically).
// @Summary Health check RPC node // @Summary Health check RPC node
// @Description Perform an on-demand health probe on an RPC node // @Description Perform an on-demand health probe on an RPC node
// @Tags Admin RPC Nodes // @Tags Admin RPC Nodes
@@ -253,6 +252,11 @@ func validateRpcNodeURLForType(rawURL string, nodeType string) error {
return nil return nil
} }
return constant.RpcNodeWebSocketURLErr return constant.RpcNodeWebSocketURLErr
case mdb.RpcNodeTypeLite:
if scheme == "http" || scheme == "https" {
return nil
}
return constant.RpcNodeHTTPURLErr
default: default:
return constant.RpcNodeTypeErr return constant.RpcNodeTypeErr
} }
+32 -6
View File
@@ -11,6 +11,7 @@ import (
"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/constant"
appLog "github.com/GMWalletApp/epusdt/util/log"
"github.com/GMWalletApp/epusdt/util/security" "github.com/GMWalletApp/epusdt/util/security"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@@ -19,7 +20,7 @@ import (
// Supported groups and keys: // Supported groups and keys:
// //
// - group=rate: // - group=rate:
// rate.forced_rate_list (json) — override rate map, e.g. {"cny":{"usdt":0.14635}}; base/coin keys are normalized to lowercase // rate.forced_rate_list (json) — override rate map, e.g. {"cny":{"usdt":0.14635,"ton":0.5}}; base/coin keys are normalized to lowercase
// rate.api_url (string) — external rate API URL used when no positive forced rate exists // rate.api_url (string) — external rate API URL used when no positive forced rate exists
// rate.adjust_percent (float) — rate adjustment percentage // rate.adjust_percent (float) — rate adjustment percentage
// rate.okx_c2c_enabled (bool) — use OKX C2C rate feed // rate.okx_c2c_enabled (bool) — use OKX C2C rate feed
@@ -27,7 +28,7 @@ import (
// - group=epay: // - group=epay:
// epay.default_token (string) — token for EPAY orders, e.g. "usdt" (default) // epay.default_token (string) — token for EPAY orders, e.g. "usdt" (default)
// epay.default_currency (string) — fiat currency for EPAY orders, e.g. "cny" (default) // epay.default_currency (string) — fiat currency for EPAY orders, e.g. "cny" (default)
// epay.default_network (string) — blockchain network for EPAY orders, e.g. "tron" (default) // epay.default_network (string) — blockchain network for EPAY orders, e.g. "tron" or "ton" (default "tron")
// //
// - group=okpay: // - group=okpay:
// okpay.enabled (bool) — enable OkPay as a switch-network payment option // okpay.enabled (bool) — enable OkPay as a switch-network payment option
@@ -54,6 +55,7 @@ import (
// - group=system: // - group=system:
// system.order_expiration_time (int) — order expiry in minutes // system.order_expiration_time (int) — order expiry in minutes
// system.amount_precision (int) — payment amount precision, 2-6 decimals (default 2) // system.amount_precision (int) — payment amount precision, 2-6 decimals (default 2)
// system.log_level (string) — runtime log level: debug, info, warn, error (default error)
type SettingUpsertItem struct { type SettingUpsertItem struct {
Group string `json:"group" enums:"brand,rate,system,epay,okpay" example:"epay"` Group string `json:"group" enums:"brand,rate,system,epay,okpay" example:"epay"`
Key string `json:"key" example:"epay.default_network"` Key string `json:"key" example:"epay.default_network"`
@@ -93,11 +95,11 @@ func (c *BaseAdminController) ListSettings(ctx echo.Context) error {
// @Summary Upsert settings // @Summary Upsert settings
// @Description Batch insert/update settings. Returns per-key status; failed items include error_code for frontend i18n. // @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" or "ton"), epay.default_currency (e.g. "cny"), epay.default_network (e.g. "tron" or "ton").
// @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.
// @Description rate group keys: rate.forced_rate_list (JSON map, e.g. {"cny":{"usdt":0.14635}}; base/coin keys are normalized to lowercase), rate.api_url, rate.adjust_percent, rate.okx_c2c_enabled. // @Description rate group keys: rate.forced_rate_list (JSON map, e.g. {"cny":{"usdt":0.14635,"ton":0.5}}; base/coin keys are normalized to lowercase), rate.api_url, rate.adjust_percent, rate.okx_c2c_enabled.
// @Description brand group keys: brand.checkout_name, brand.logo_url, brand.site_title, brand.success_copy, brand.support_url, brand.background_color, brand.background_image_url. Legacy aliases brand.site_name, brand.page_title and brand.pay_success_text are also supported. // @Description brand group keys: brand.checkout_name, brand.logo_url, brand.site_title, brand.success_copy, brand.support_url, brand.background_color, brand.background_image_url. Legacy aliases brand.site_name, brand.page_title and brand.pay_success_text are also supported.
// @Description system group keys: system.order_expiration_time, system.amount_precision (int, 2-6, default 2). // @Description system group keys: system.order_expiration_time, system.amount_precision (int, 2-6, default 2), system.log_level (debug|info|warn|error, default error).
// @Tags Admin Settings // @Tags Admin Settings
// @Security AdminJWT // @Security AdminJWT
// @Accept json // @Accept json
@@ -149,10 +151,20 @@ func (c *BaseAdminController) UpsertSettings(ctx echo.Context) error {
item.Group = mdb.SettingGroupRate item.Group = mdb.SettingGroupRate
item.Type = mdb.SettingTypeJSON item.Type = mdb.SettingTypeJSON
} }
if key == mdb.SettingKeySystemLogLevel {
item.Group = mdb.SettingGroupSystem
item.Type = mdb.SettingTypeString
}
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, errorResult(key, err)) out = append(out, errorResult(key, err))
continue continue
} }
if key == mdb.SettingKeySystemLogLevel {
if err := appLog.SetLevel(value); err != nil {
out = append(out, errorResult(key, err))
continue
}
}
out = append(out, result{Key: key, OK: true}) out = append(out, result{Key: key, OK: true})
} }
@@ -218,6 +230,15 @@ func normalizeAndValidateSettingItem(group, key, value string) (string, error) {
if err := security.ValidatePublicHTTPURL(value); err != nil { if err := security.ValidatePublicHTTPURL(value); err != nil {
return value, fmt.Errorf("%s invalid: %w", key, err) return value, fmt.Errorf("%s invalid: %w", key, err)
} }
case mdb.SettingKeySystemLogLevel:
if strings.ToLower(strings.TrimSpace(group)) != mdb.SettingGroupSystem {
return value, fmt.Errorf("%s must use group %s", key, mdb.SettingGroupSystem)
}
normalized, err := appLog.NormalizeLevel(value)
if err != nil {
return value, err
}
return normalized, nil
} }
return value, nil return value, nil
} }
@@ -274,7 +295,7 @@ func normalizeForcedRateListSetting(group, key, value string) (string, error) {
// DeleteSetting removes one row. The next read of that key will fall // DeleteSetting removes one row. The next read of that key will fall
// back to the hardcoded default (see settings_data.GetSetting*). // back to the hardcoded default (see settings_data.GetSetting*).
// @Summary Delete setting // @Summary Delete setting
// @Description Remove a setting by key (falls back to default) // @Description Remove a setting by key (falls back to default). Deleting system.log_level immediately resets runtime logging to error.
// @Tags Admin Settings // @Tags Admin Settings
// @Security AdminJWT // @Security AdminJWT
// @Produce json // @Produce json
@@ -290,5 +311,10 @@ func (c *BaseAdminController) DeleteSetting(ctx echo.Context) error {
if err := data.DeleteSetting(key); err != nil { if err := data.DeleteSetting(key); err != nil {
return c.FailJson(ctx, err) return c.FailJson(ctx, err)
} }
if key == mdb.SettingKeySystemLogLevel {
if err := appLog.SetLevel(mdb.SettingDefaultSystemLogLevel); err != nil {
return c.FailJson(ctx, err)
}
}
return c.SucJson(ctx, nil) return c.SucJson(ctx, nil)
} }
+6 -6
View File
@@ -32,8 +32,8 @@ func apiKeyFromContext(ctx echo.Context) *mdb.ApiKey {
// @Param request body request.CreateTransactionRequest false "Transaction payload (JSON)" // @Param request body request.CreateTransactionRequest false "Transaction payload (JSON)"
// @Param order_id formData string false "Merchant order ID" // @Param order_id formData string false "Merchant order ID"
// @Param currency formData string false "Fiat currency (e.g. cny)" // @Param currency formData string false "Fiat currency (e.g. cny)"
// @Param token formData string false "Crypto token (e.g. usdt)" // @Param token formData string false "Crypto token (e.g. TON, USDT)"
// @Param network formData string false "Network (e.g. tron)" // @Param network formData string false "Network (e.g. ton, tron)"
// @Param amount formData number false "Amount" // @Param amount formData number false "Amount"
// @Param notify_url formData string false "Callback URL" // @Param notify_url formData string false "Callback URL"
// @Param signature formData string false "MD5 signature" // @Param signature formData string false "MD5 signature"
@@ -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 "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" // @Failure 400 {object} response.ApiResponse "Stable errno in status_code: 10009 invalid params, 10041 invalid notify_url, 10004 invalid amount, 10014 chain disabled, 10016 unsupported asset, 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)
@@ -61,14 +61,14 @@ func (c *BaseCommController) CreateTransaction(ctx echo.Context) (err error) {
// SwitchNetwork 切换支付网络,创建或返回子订单 // SwitchNetwork 切换支付网络,创建或返回子订单
// @Summary Switch payment network // @Summary Switch payment network
// @Description Switch to a different payment target, creating or returning a sub-order. // @Description Switch to a different payment target, creating or returning a sub-order.
// @Description Normal values such as tron/solana/ethereum create on-chain child orders. // @Description Normal values such as ton/tron/solana/ethereum create on-chain child orders.
// @Description The special value okpay creates or reuses an OkPay-hosted child order and returns its payment_url. // @Description The special value okpay creates or reuses an OkPay-hosted child order and returns its payment_url.
// @Tags Payment // @Tags Payment
// @Accept json // @Accept json
// @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 "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" // @Failure 400 {object} response.ApiResponse "Stable errno in status_code: 10008 order not found, 10012 cannot switch sub-order, 10013 not waiting payment, 10016 unsupported asset, 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)
@@ -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 "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" // @Failure 400 {object} response.ApiResponse "Stable errno in status_code: 10009 invalid params, 10041 invalid notify_url, 10004 invalid amount, 10014 chain disabled, 10016 unsupported asset, 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) {
+1
View File
@@ -54,6 +54,7 @@ func (c *BaseCommController) CheckStatus(ctx echo.Context) (err error) {
// SubmitTxHash 手动提交交易 hash 补单 // SubmitTxHash 手动提交交易 hash 补单
// @Summary Submit transaction hash // @Summary Submit transaction hash
// @Description Submit an on-chain transaction hash from the cashier. The hash is verified against the order and, when valid, the same manual payment processing path used by admin mark-paid is executed. OkPay/provider orders are not supported. // @Description Submit an on-chain transaction hash from the cashier. The hash is verified against the order and, when valid, the same manual payment processing path used by admin mark-paid is executed. OkPay/provider orders are not supported.
// @Description TON accepts canonical ton:<receive_raw>:<lt>:<hash>, lt:hash, or a unique recent hash-only reference for the order receive address.
// @Tags Payment // @Tags Payment
// @Accept json // @Accept json
// @Produce json // @Produce json
+1
View File
@@ -23,6 +23,7 @@ require (
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1 github.com/spf13/viper v1.20.1
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0
github.com/xssnick/tonutils-go v1.16.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/crypto v0.48.0 golang.org/x/crypto v0.48.0
gopkg.in/telebot.v3 v3.0.0 gopkg.in/telebot.v3 v3.0.0
+2
View File
@@ -348,6 +348,8 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHg
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xssnick/tonutils-go v1.16.0 h1:MLYVZiiMB0Q+XudLo7WyjOli4gMuenNmJpBqETqa5DE=
github.com/xssnick/tonutils-go v1.16.0/go.mod h1:CNSM+L0FYufOFhprg8otc9Y/ecFfLrWqFMq00Gh9yk4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
+10 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
"go.uber.org/zap" "go.uber.org/zap"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
"gorm.io/gorm/schema" "gorm.io/gorm/schema"
) )
@@ -65,11 +66,19 @@ func SetupTestDatabases(t testing.TB) func() {
// Seed all standard chains as enabled so IsChainEnabled checks pass. // Seed all standard chains as enabled so IsChainEnabled checks pass.
for _, network := range []string{ for _, network := range []string{
mdb.NetworkTron, mdb.NetworkSolana, mdb.NetworkEthereum, mdb.NetworkTron, mdb.NetworkSolana, mdb.NetworkEthereum,
mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma, mdb.NetworkTon,
} { } {
mainDB.Create(&mdb.Chain{Network: network, Enabled: true}) mainDB.Create(&mdb.Chain{Network: network, Enabled: true})
} }
mainDB.Clauses(clause.OnConflict{DoNothing: true}).Create(&[]mdb.ChainToken{
{Network: mdb.NetworkTron, Symbol: "USDT", ContractAddress: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", Decimals: 6, Enabled: true},
{Network: mdb.NetworkEthereum, Symbol: "USDT", ContractAddress: "0xdAC17F958D2ee523a2206206994597C13D831ec7", Decimals: 6, Enabled: true},
{Network: mdb.NetworkBsc, Symbol: "USDT", ContractAddress: "0x55d398326f99059fF775485246999027B3197955", Decimals: 18, Enabled: true},
{Network: mdb.NetworkTon, Symbol: "TON", ContractAddress: "", Decimals: 9, Enabled: true},
{Network: mdb.NetworkTon, Symbol: "USDT", ContractAddress: "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", Decimals: 6, Enabled: true},
})
// Seed two universal api_keys rows. Both usable for EPAY/GMPAY // Seed two universal api_keys rows. Both usable for EPAY/GMPAY
// flows; the numeric PID 1001 row lets legacy tests that submit // flows; the numeric PID 1001 row lets legacy tests that submit
// `pid=1001` still match. // `pid=1001` still match.
+6
View File
@@ -64,6 +64,7 @@ func seedChains() {
{Network: mdb.NetworkBsc, DisplayName: "BSC", Enabled: true, MinConfirmations: 3, ScanIntervalSec: 5}, {Network: mdb.NetworkBsc, DisplayName: "BSC", Enabled: true, MinConfirmations: 3, ScanIntervalSec: 5},
{Network: mdb.NetworkPolygon, DisplayName: "Polygon", Enabled: true, MinConfirmations: 3, ScanIntervalSec: 5}, {Network: mdb.NetworkPolygon, DisplayName: "Polygon", Enabled: true, MinConfirmations: 3, ScanIntervalSec: 5},
{Network: mdb.NetworkPlasma, DisplayName: "Plasma", Enabled: true, MinConfirmations: 1, ScanIntervalSec: 5}, {Network: mdb.NetworkPlasma, DisplayName: "Plasma", Enabled: true, MinConfirmations: 1, ScanIntervalSec: 5},
{Network: mdb.NetworkTon, DisplayName: "TON", Enabled: true, MinConfirmations: 1, ScanIntervalSec: 5},
} }
if err := Mdb.Clauses(clause.OnConflict{DoNothing: true}).Create(&defaults).Error; err != nil { if err := Mdb.Clauses(clause.OnConflict{DoNothing: true}).Create(&defaults).Error; err != nil {
color.Red.Printf("[store_db] seed chains err=%s\n", err) color.Red.Printf("[store_db] seed chains err=%s\n", err)
@@ -98,6 +99,9 @@ func seedChainTokens() {
{Network: mdb.NetworkPolygon, Symbol: "USDC.e", ContractAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", Decimals: 6, Enabled: true}, {Network: mdb.NetworkPolygon, Symbol: "USDC.e", ContractAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", Decimals: 6, Enabled: true},
// Plasma // Plasma
{Network: mdb.NetworkPlasma, Symbol: "USDT", ContractAddress: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", Decimals: 6, Enabled: true}, {Network: mdb.NetworkPlasma, Symbol: "USDT", ContractAddress: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", Decimals: 6, Enabled: true},
// TON
{Network: mdb.NetworkTon, Symbol: "TON", ContractAddress: "", Decimals: 9, Enabled: true},
{Network: mdb.NetworkTon, Symbol: "USDT", ContractAddress: "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", Decimals: 6, Enabled: true},
} }
if err := Mdb.Clauses(clause.OnConflict{DoNothing: true}).Create(&defaults).Error; err != nil { if err := Mdb.Clauses(clause.OnConflict{DoNothing: true}).Create(&defaults).Error; err != nil {
color.Red.Printf("[store_db] seed chain_tokens err=%s\n", err) color.Red.Printf("[store_db] seed chain_tokens err=%s\n", err)
@@ -133,6 +137,7 @@ func defaultRpcNodes() []mdb.RpcNode {
{Network: mdb.NetworkBsc, Url: "wss://bsc.drpc.org", Type: mdb.RpcNodeTypeWs, 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.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.NetworkPlasma, Url: "wss://rpc.plasma.to", Type: mdb.RpcNodeTypeWs, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusUnknown},
{Network: mdb.NetworkTon, Url: "https://ton-blockchain.github.io/global.config.json", Type: mdb.RpcNodeTypeLite, 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.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.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}, {Network: mdb.NetworkPolygon, Url: "https://rpc.epusdt.com/polygon", Type: mdb.RpcNodeTypeHttp, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusUnknown},
@@ -157,6 +162,7 @@ func seedDefaultSettings() {
} }
defaults := []mdb.Setting{ defaults := []mdb.Setting{
{Group: mdb.SettingGroupSystem, Key: mdb.SettingKeyAmountPrecision, Value: "2", Type: mdb.SettingTypeInt}, {Group: mdb.SettingGroupSystem, Key: mdb.SettingKeyAmountPrecision, Value: "2", Type: mdb.SettingTypeInt},
{Group: mdb.SettingGroupSystem, Key: mdb.SettingKeySystemLogLevel, Value: mdb.SettingDefaultSystemLogLevel, Type: mdb.SettingTypeString},
{Group: mdb.SettingGroupEpay, Key: mdb.SettingKeyEpayDefaultToken, Value: "usdt", Type: mdb.SettingTypeString}, {Group: mdb.SettingGroupEpay, Key: mdb.SettingKeyEpayDefaultToken, Value: "usdt", Type: mdb.SettingTypeString},
{Group: mdb.SettingGroupEpay, Key: mdb.SettingKeyEpayDefaultCurrency, Value: "cny", Type: mdb.SettingTypeString}, {Group: mdb.SettingGroupEpay, Key: mdb.SettingKeyEpayDefaultCurrency, Value: "cny", Type: mdb.SettingTypeString},
{Group: mdb.SettingGroupEpay, Key: mdb.SettingKeyEpayDefaultNetwork, Value: "tron", Type: mdb.SettingTypeString}, {Group: mdb.SettingGroupEpay, Key: mdb.SettingKeyEpayDefaultNetwork, Value: "tron", Type: mdb.SettingTypeString},
+96
View File
@@ -1,9 +1,13 @@
package dao package dao
import ( import (
"path/filepath"
"testing" "testing"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/libtnb/sqlite"
"github.com/spf13/viper"
"gorm.io/gorm"
) )
func TestDefaultRpcNodesIncludesManualVerifyEpusdtEvmNodes(t *testing.T) { func TestDefaultRpcNodesIncludesManualVerifyEpusdtEvmNodes(t *testing.T) {
@@ -41,3 +45,95 @@ func TestDefaultRpcNodesIncludesManualVerifyEpusdtEvmNodes(t *testing.T) {
} }
} }
} }
func TestDefaultRpcNodesIncludesTonLiteGeneralNode(t *testing.T) {
var got *mdb.RpcNode
nodes := defaultRpcNodes()
for i := range nodes {
node := nodes[i]
if node.Network == mdb.NetworkTon && node.Type == mdb.RpcNodeTypeLite {
got = &node
break
}
}
if got == nil {
t.Fatal("missing TON lite seed rpc node")
}
if got.Url != "https://ton-blockchain.github.io/global.config.json" {
t.Fatalf("TON lite seed url = %q", got.Url)
}
if got.Purpose != mdb.RpcNodePurposeGeneral {
t.Fatalf("TON lite seed purpose = %q, want %q", got.Purpose, mdb.RpcNodePurposeGeneral)
}
if !got.Enabled {
t.Fatal("TON lite seed enabled = false, want true")
}
if got.Status != mdb.RpcNodeStatusUnknown {
t.Fatalf("TON lite seed status = %q, want %q", got.Status, mdb.RpcNodeStatusUnknown)
}
}
func TestSeedDefaultSettingsIncludesSystemLogLevel(t *testing.T) {
db := setupSeedSettingsTestDB(t)
Mdb = db
seedDefaultSettings()
var row mdb.Setting
if err := Mdb.Where("`key` = ?", mdb.SettingKeySystemLogLevel).Take(&row).Error; err != nil {
t.Fatalf("load system.log_level seed: %v", err)
}
if row.Group != mdb.SettingGroupSystem {
t.Fatalf("system.log_level group = %q, want %q", row.Group, mdb.SettingGroupSystem)
}
if row.Value != mdb.SettingDefaultSystemLogLevel {
t.Fatalf("system.log_level value = %q, want %q", row.Value, mdb.SettingDefaultSystemLogLevel)
}
if row.Type != mdb.SettingTypeString {
t.Fatalf("system.log_level type = %q, want %q", row.Type, mdb.SettingTypeString)
}
}
func TestSeedDefaultSettingsDoesNotOverwriteSystemLogLevel(t *testing.T) {
db := setupSeedSettingsTestDB(t)
Mdb = db
if err := Mdb.Create(&mdb.Setting{
Group: mdb.SettingGroupSystem,
Key: mdb.SettingKeySystemLogLevel,
Value: "debug",
Type: mdb.SettingTypeString,
}).Error; err != nil {
t.Fatalf("precreate system.log_level: %v", err)
}
seedDefaultSettings()
var row mdb.Setting
if err := Mdb.Where("`key` = ?", mdb.SettingKeySystemLogLevel).Take(&row).Error; err != nil {
t.Fatalf("load system.log_level seed: %v", err)
}
if row.Value != "debug" {
t.Fatalf("system.log_level value = %q, want existing debug", row.Value)
}
}
func setupSeedSettingsTestDB(t *testing.T) *gorm.DB {
t.Helper()
oldDB := Mdb
viper.Reset()
viper.Set("app_uri", "https://example.com")
t.Cleanup(func() {
Mdb = oldDB
viper.Reset()
})
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "seed-settings.db")), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(&mdb.Setting{}); err != nil {
t.Fatalf("migrate settings: %v", err)
}
return db
}
+8 -1
View File
@@ -8,6 +8,7 @@ import (
"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/GMWalletApp/epusdt/model/request" "github.com/GMWalletApp/epusdt/model/request"
addressutil "github.com/GMWalletApp/epusdt/util/address"
"github.com/dromara/carbon/v2" "github.com/dromara/carbon/v2"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"gorm.io/gorm" "gorm.io/gorm"
@@ -39,9 +40,15 @@ func normalizeLockNetwork(network string) string {
func normalizeLockAddress(network, address string) string { func normalizeLockAddress(network, address string) string {
address = strings.TrimSpace(address) address = strings.TrimSpace(address)
if isEVMNetwork(normalizeLockNetwork(network)) { network = normalizeLockNetwork(network)
if isEVMNetwork(network) {
return strings.ToLower(address) return strings.ToLower(address)
} }
if network == mdb.NetworkTon {
if raw, err := addressutil.TonRawAddressKey(address); err == nil {
return raw
}
}
return address return address
} }
+28
View File
@@ -8,6 +8,7 @@ import (
"github.com/GMWalletApp/epusdt/internal/testutil" "github.com/GMWalletApp/epusdt/internal/testutil"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/xssnick/tonutils-go/address"
) )
func TestEvmTransactionLockAddressIsCaseInsensitive(t *testing.T) { func TestEvmTransactionLockAddressIsCaseInsensitive(t *testing.T) {
@@ -156,3 +157,30 @@ func TestNonEvmTransactionLockAddressRemainsCaseSensitive(t *testing.T) {
t.Fatalf("tron address lookup should remain case-sensitive, got trade id %q", gotTradeID) t.Fatalf("tron address lookup should remain case-sensitive, got trade id %q", gotTradeID)
} }
} }
func TestTonTransactionLockAddressUsesRawKey(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
bounceable := "EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I"
addr := address.MustParseAddr(bounceable)
nonBounce := addr.Bounce(false).String()
if err := LockTransaction(mdb.NetworkTon, bounceable, "TON", "trade-ton", 1.23, time.Hour); err != nil {
t.Fatalf("lock ton transaction: %v", err)
}
gotTradeID, err := GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTon, nonBounce, "TON", 1.23)
if err != nil {
t.Fatalf("lookup ton lock: %v", err)
}
if gotTradeID != "trade-ton" {
t.Fatalf("ton lock lookup = %q, want trade-ton", gotTradeID)
}
gotTradeID, err = GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTon, addr.StringRaw(), "TON", 1.23)
if err != nil {
t.Fatalf("lookup ton raw lock: %v", err)
}
if gotTradeID != "trade-ton" {
t.Fatalf("ton raw lock lookup = %q, want trade-ton", gotTradeID)
}
}
+33 -3
View File
@@ -5,6 +5,7 @@ import (
"github.com/GMWalletApp/epusdt/model/dao" "github.com/GMWalletApp/epusdt/model/dao"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
addressutil "github.com/GMWalletApp/epusdt/util/address"
"github.com/GMWalletApp/epusdt/util/constant" "github.com/GMWalletApp/epusdt/util/constant"
) )
@@ -34,10 +35,23 @@ func normalizeWalletAddressByNetwork(network, address string) string {
return address return address
} }
func normalizeWalletAddressByNetworkE(network, address string) (string, error) {
network = normalizeWalletNetwork(network)
address = strings.TrimSpace(address)
if network == mdb.NetworkTon {
return addressutil.NormalizeTonAddress(address)
}
return normalizeWalletAddressByNetwork(network, address), nil
}
// AddWalletAddressWithNetwork 创建指定网络的钱包地址 // AddWalletAddressWithNetwork 创建指定网络的钱包地址
func AddWalletAddressWithNetwork(network, address string) (*mdb.WalletAddress, error) { func AddWalletAddressWithNetwork(network, address string) (*mdb.WalletAddress, error) {
network = normalizeWalletNetwork(network) network = normalizeWalletNetwork(network)
address = normalizeWalletAddressByNetwork(network, address) var err error
address, err = normalizeWalletAddressByNetworkE(network, address)
if err != nil {
return nil, err
}
exist, err := GetWalletAddressByNetworkAndAddress(network, address) exist, err := GetWalletAddressByNetworkAndAddress(network, address)
if err != nil { if err != nil {
@@ -75,9 +89,13 @@ func AddWalletAddressWithNetwork(network, address string) (*mdb.WalletAddress, e
// GetWalletAddressByNetworkAndAddress 通过网络和地址查询 // GetWalletAddressByNetworkAndAddress 通过网络和地址查询
func GetWalletAddressByNetworkAndAddress(network, address string) (*mdb.WalletAddress, error) { func GetWalletAddressByNetworkAndAddress(network, address string) (*mdb.WalletAddress, error) {
network = normalizeWalletNetwork(network) network = normalizeWalletNetwork(network)
address = normalizeWalletAddressByNetwork(network, address) var err error
address, err = normalizeWalletAddressByNetworkE(network, address)
if err != nil {
return nil, err
}
walletAddress := new(mdb.WalletAddress) walletAddress := new(mdb.WalletAddress)
err := dao.Mdb.Model(walletAddress). err = dao.Mdb.Model(walletAddress).
Where("network = ?", network). Where("network = ?", network).
Where("address = ?", address). Where("address = ?", address).
Limit(1).Find(walletAddress).Error Limit(1).Find(walletAddress).Error
@@ -126,6 +144,12 @@ func GetAvailableWalletAddressByNetwork(network string) ([]mdb.WalletAddress, er
for i := range list { for i := range list {
list[i].Address = strings.ToLower(strings.TrimSpace(list[i].Address)) list[i].Address = strings.ToLower(strings.TrimSpace(list[i].Address))
} }
} else if network == mdb.NetworkTon {
for i := range list {
if normalized, err := addressutil.NormalizeTonAddress(list[i].Address); err == nil {
list[i].Address = normalized
}
}
} }
return list, err return list, err
} }
@@ -149,6 +173,12 @@ func GetAllWalletAddressByNetwork(network string) ([]mdb.WalletAddress, error) {
for i := range list { for i := range list {
list[i].Address = strings.ToLower(strings.TrimSpace(list[i].Address)) list[i].Address = strings.ToLower(strings.TrimSpace(list[i].Address))
} }
} else if network == mdb.NetworkTon {
for i := range list {
if normalized, err := addressutil.NormalizeTonAddress(list[i].Address); err == nil {
list[i].Address = normalized
}
}
} }
return list, err return list, err
} }
@@ -7,6 +7,8 @@ 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/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/xssnick/tonutils-go/address"
) )
func TestAddWalletAddressWithNetworkNormalizesEvmAddressToLowercase(t *testing.T) { func TestAddWalletAddressWithNetworkNormalizesEvmAddressToLowercase(t *testing.T) {
@@ -81,3 +83,23 @@ func TestAddWalletAddressWithNetworkKeepsOriginalCaseForNonEvm(t *testing.T) {
t.Fatalf("solana wallet address = %q, want %q", solRow.Address, solAddress) t.Fatalf("solana wallet address = %q, want %q", solRow.Address, solAddress)
} }
} }
func TestAddWalletAddressWithNetworkNormalizesTonAddressVariants(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
bounceable := "EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I"
addr := address.MustParseAddr(bounceable)
expected := addr.Bounce(false).String()
row, err := AddWalletAddressWithNetwork(mdb.NetworkTon, addr.StringRaw())
if err != nil {
t.Fatalf("add raw ton wallet: %v", err)
}
if row.Address != expected {
t.Fatalf("stored TON address = %q, want %q", row.Address, expected)
}
if _, err = AddWalletAddressWithNetwork(mdb.NetworkTon, bounceable); err != constant.WalletAddressAlreadyExists {
t.Fatalf("add equivalent ton wallet error = %v, want already exists", err)
}
}
+3 -2
View File
@@ -5,6 +5,7 @@ import "github.com/dromara/carbon/v2"
const ( const (
RpcNodeTypeHttp = "http" RpcNodeTypeHttp = "http"
RpcNodeTypeWs = "ws" RpcNodeTypeWs = "ws"
RpcNodeTypeLite = "lite"
) )
const ( const (
@@ -24,8 +25,8 @@ const (
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长连接 lite=TON liteserver配置
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,lite" 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"`
+9 -4
View File
@@ -4,10 +4,10 @@ package mdb
// Groups: brand, rate, system, epay, okpay. Merchant credentials (pid + // Groups: brand, rate, system, epay, okpay. Merchant credentials (pid +
// secret_key) live in the api_keys table; notification configs live in // secret_key) live in the api_keys table; notification configs live in
// NotificationChannel. // NotificationChannel.
// Default rows for the epay/okpay groups are seeded on first startup (see // Default rows for the system/epay/okpay groups are seeded on first startup
// model/dao/mdb_table_init.go seedDefaultSettings). All other groups start // (see model/dao/mdb_table_init.go seedDefaultSettings). Other groups start
// empty and fall back to hardcoded defaults until an admin sets them. // empty and fall back to hardcoded defaults until an admin sets them. The
// Only exception: system.jwt_secret is auto-generated on first startup. // system.jwt_secret key is auto-generated on first startup.
const ( const (
SettingGroupBrand = "brand" SettingGroupBrand = "brand"
SettingGroupRate = "rate" SettingGroupRate = "rate"
@@ -31,6 +31,7 @@ const (
SettingKeyInitAdminPasswordChanged = "system.init_admin_password_changed" SettingKeyInitAdminPasswordChanged = "system.init_admin_password_changed"
SettingKeyOrderExpiration = "system.order_expiration_time" SettingKeyOrderExpiration = "system.order_expiration_time"
SettingKeyAmountPrecision = "system.amount_precision" SettingKeyAmountPrecision = "system.amount_precision"
SettingKeySystemLogLevel = "system.log_level"
SettingKeyBrandCheckoutName = "brand.checkout_name" SettingKeyBrandCheckoutName = "brand.checkout_name"
SettingKeyBrandSiteName = "brand.site_name" SettingKeyBrandSiteName = "brand.site_name"
SettingKeyBrandLogoUrl = "brand.logo_url" SettingKeyBrandLogoUrl = "brand.logo_url"
@@ -62,6 +63,10 @@ const (
SettingKeyOkPayAllowTokens = "okpay.allow_tokens" SettingKeyOkPayAllowTokens = "okpay.allow_tokens"
) )
const (
SettingDefaultSystemLogLevel = "error"
)
type Setting struct { type Setting struct {
Group string `gorm:"column:group;size:32;index:settings_group_index" json:"group" enums:"brand,rate,system,epay,okpay" example:"rate"` Group string `gorm:"column:group;size:32;index:settings_group_index" json:"group" enums:"brand,rate,system,epay,okpay" example:"rate"`
Key string `gorm:"column:key;uniqueIndex:settings_key_uindex;size:128" json:"key" example:"rate.forced_rate_list"` Key string `gorm:"column:key;uniqueIndex:settings_key_uindex;size:128" json:"key" example:"rate.forced_rate_list"`
+1
View File
@@ -12,6 +12,7 @@ const (
NetworkBsc = "binance" NetworkBsc = "binance"
NetworkPolygon = "polygon" NetworkPolygon = "polygon"
NetworkPlasma = "plasma" NetworkPlasma = "plasma"
NetworkTon = "ton"
) )
const ( const (
+2 -2
View File
@@ -6,8 +6,8 @@ import "github.com/gookit/validate"
type CreateTransactionRequest struct { type CreateTransactionRequest struct {
OrderId string `json:"order_id" form:"order_id" validate:"required|maxLen:32" example:"ORD20260416001"` OrderId string `json:"order_id" form:"order_id" validate:"required|maxLen:32" example:"ORD20260416001"`
Currency string `json:"currency" form:"currency" validate:"required" example:"cny"` // 法币 如:cny Currency string `json:"currency" form:"currency" validate:"required" example:"cny"` // 法币 如:cny
Token string `json:"token" form:"token" validate:"required" example:"usdt"` // 币种 如:usdt Token string `json:"token" form:"token" validate:"required" example:"usdt"` // 币种 如:usdt、ton
Network string `json:"network" form:"network" validate:"required" example:"tron"` // 网络 如:tron Network string `json:"network" form:"network" validate:"required" example:"tron"` // 网络 如:ton、tron
Amount float64 `json:"amount" form:"amount" validate:"required|isFloat|gt:0.01" example:"100.00"` Amount float64 `json:"amount" form:"amount" validate:"required|isFloat|gt:0.01" example:"100.00"`
NotifyUrl string `json:"notify_url" form:"notify_url" validate:"required" example:"https://example.com/notify"` NotifyUrl string `json:"notify_url" form:"notify_url" validate:"required" example:"https://example.com/notify"`
Signature string `json:"signature" form:"signature" validate:"required" example:"a1b2c3d4e5f6..."` Signature string `json:"signature" form:"signature" validate:"required" example:"a1b2c3d4e5f6..."`
+246
View File
@@ -16,6 +16,7 @@ import (
tron "github.com/GMWalletApp/epusdt/crypto" tron "github.com/GMWalletApp/epusdt/crypto"
"github.com/GMWalletApp/epusdt/model/data" "github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
addressutil "github.com/GMWalletApp/epusdt/util/address"
"github.com/GMWalletApp/epusdt/util/constant" "github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/math" "github.com/GMWalletApp/epusdt/util/math"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@@ -23,6 +24,10 @@ import (
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/ton/jetton"
) )
const manualVerifyRequestTimeout = 15 * time.Second const manualVerifyRequestTimeout = 15 * time.Second
@@ -82,6 +87,8 @@ func validateManualOrderPaymentDefault(order *mdb.Orders, blockTransactionID str
canonicalTxID, err = validateManualTronPayment(order, txID) canonicalTxID, err = validateManualTronPayment(order, txID)
case mdb.NetworkSolana: case mdb.NetworkSolana:
canonicalTxID, err = validateManualSolanaPayment(order, txID) canonicalTxID, err = validateManualSolanaPayment(order, txID)
case mdb.NetworkTon:
canonicalTxID, err = validateManualTonPayment(order, txID)
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma: case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma:
canonicalTxID, err = validateManualEvmPayment(order, txID) canonicalTxID, err = validateManualEvmPayment(order, txID)
default: default:
@@ -636,6 +643,245 @@ func tronPostJSON(baseURL, apiKey, path string, body interface{}, out interface{
return json.Unmarshal(raw, out) return json.Unmarshal(raw, out)
} }
type manualTonTxRef struct {
ReceiveRaw string
LT uint64
HashHex string
HasLT bool
}
func validateManualTonPayment(order *mdb.Orders, txID string) (string, error) {
ref, err := parseManualTonTxRef(txID)
if err != nil {
return "", err
}
orderAddr, err := addressutil.ParseTonMainnetAddress(order.ReceiveAddress)
if err != nil {
return "", fmt.Errorf("invalid order receive address: %w", err)
}
if ref.ReceiveRaw != "" && ref.ReceiveRaw != orderAddr.Raw {
return "", fmt.Errorf("transaction recipient mismatch")
}
nodes, err := data.ListManualPaymentRpcCandidates(mdb.NetworkTon, mdb.RpcNodeTypeLite)
if err != nil {
return "", err
}
if len(nodes) == 0 {
return "", fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkTon, mdb.RpcNodeTypeLite)
}
var verifyErrors []string
for _, node := range nodes {
api, closeFn, err := dialManualTonClient(node)
if err != nil {
verifyErrors = append(verifyErrors, fmt.Sprintf("%s: connect: %v", manualRpcNodeLabel(node), err))
continue
}
canonicalID, err := validateManualTonPaymentWithAPI(context.Background(), api, order, orderAddr.Address, ref)
closeFn()
if err != nil {
verifyErrors = append(verifyErrors, fmt.Sprintf("%s: %v", manualRpcNodeLabel(node), err))
continue
}
return canonicalID, nil
}
if len(verifyErrors) > 0 {
return "", fmt.Errorf("manual TON verification failed: %s", strings.Join(verifyErrors, "; "))
}
return "", fmt.Errorf("no enabled %s %s RPC node configured in rpc_nodes", mdb.NetworkTon, mdb.RpcNodeTypeLite)
}
func parseManualTonTxRef(input string) (manualTonTxRef, error) {
raw := strings.TrimSpace(input)
if raw == "" {
return manualTonTxRef{}, fmt.Errorf("block_transaction_id is required")
}
if strings.HasPrefix(strings.ToLower(raw), "ton:") {
body := raw[4:]
last := strings.LastIndex(body, ":")
if last <= 0 || last == len(body)-1 {
return manualTonTxRef{}, fmt.Errorf("invalid ton canonical transaction id")
}
hashHex, err := normalizeTonTxHashHex(body[last+1:])
if err != nil {
return manualTonTxRef{}, err
}
left := body[:last]
mid := strings.LastIndex(left, ":")
if mid <= 0 || mid == len(left)-1 {
return manualTonTxRef{}, fmt.Errorf("invalid ton canonical transaction id")
}
lt, err := parseUint64Strict(left[mid+1:])
if err != nil {
return manualTonTxRef{}, fmt.Errorf("invalid ton transaction lt")
}
receiveRaw, err := addressutil.TonRawAddressKey(left[:mid])
if err != nil {
return manualTonTxRef{}, fmt.Errorf("invalid ton canonical recipient: %w", err)
}
return manualTonTxRef{ReceiveRaw: receiveRaw, LT: lt, HashHex: hashHex, HasLT: true}, nil
}
if idx := strings.Index(raw, ":"); idx > 0 {
lt, err := parseUint64Strict(raw[:idx])
if err != nil {
return manualTonTxRef{}, fmt.Errorf("invalid ton transaction lt")
}
hashHex, err := normalizeTonTxHashHex(raw[idx+1:])
if err != nil {
return manualTonTxRef{}, err
}
return manualTonTxRef{LT: lt, HashHex: hashHex, HasLT: true}, nil
}
hashHex, err := normalizeTonTxHashHex(raw)
if err != nil {
return manualTonTxRef{}, err
}
return manualTonTxRef{HashHex: hashHex}, nil
}
func normalizeTonTxHashHex(raw string) (string, error) {
hashHex := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(raw, "0x"), "0X")))
if len(hashHex) != 64 {
return "", fmt.Errorf("invalid ton transaction hash length")
}
if _, err := hex.DecodeString(hashHex); err != nil {
return "", fmt.Errorf("invalid ton transaction hash")
}
return hashHex, nil
}
func parseUint64Strict(raw string) (uint64, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return 0, fmt.Errorf("empty integer")
}
var out uint64
for _, ch := range raw {
if ch < '0' || ch > '9' {
return 0, fmt.Errorf("invalid integer")
}
out = out*10 + uint64(ch-'0')
}
return out, nil
}
func dialManualTonClient(node mdb.RpcNode) (ton.APIClientWrapped, func(), error) {
ctx, cancel := context.WithTimeout(context.Background(), manualVerifyRequestTimeout)
defer cancel()
return ConnectTonLiteAPI(ctx, node.Url, manualVerifyRequestTimeout, 3)
}
func validateManualTonPaymentWithAPI(ctx context.Context, api ton.APIClientWrapped, order *mdb.Orders, receive *address.Address, ref manualTonTxRef) (string, error) {
master, err := api.CurrentMasterchainInfo(ctx)
if err != nil {
return "", fmt.Errorf("fetch current masterchain: %w", err)
}
txs, err := listManualTonCandidateTransactions(ctx, api, master, receive, ref)
if err != nil {
return "", fmt.Errorf("list ton account transactions: %w", err)
}
if len(txs) == 0 {
return "", fmt.Errorf("transaction not found")
}
state, err := manualTonTokenState(ctx, api, master, receive)
if err != nil {
return "", err
}
return validateManualTonPaymentCandidates(order, receive, ref, txs, state)
}
func validateManualTonPaymentCandidates(order *mdb.Orders, receive *address.Address, ref manualTonTxRef, txs []*tlb.Transaction, state *manualTonState) (string, error) {
if state == nil {
state = &manualTonState{jettonWallets: make(map[string]mdb.ChainToken)}
}
var matched []*TonObservedTransfer
for _, tx := range txs {
if tx == nil {
continue
}
hashHex := hex.EncodeToString(tx.Hash)
if !strings.EqualFold(hashHex, ref.HashHex) {
continue
}
if ref.HasLT && tx.LT != ref.LT {
continue
}
transfer, err := ParseTonInboundTransfer(tx, receive, state.nativeToken, state.jettonWallets)
if err != nil || transfer == nil {
continue
}
if err = EnsureTonTransferMatchesOrder(order, transfer); err != nil {
return "", err
}
matched = append(matched, transfer)
}
if len(matched) == 0 {
return "", fmt.Errorf("matching ton transfer to order address not found")
}
if len(matched) > 1 && !ref.HasLT {
return "", fmt.Errorf("ton transaction hash matched multiple recent transactions; submit canonical id or lt:hash")
}
receiveRaw := addressutil.TonRawAddressObjectKey(receive)
return TonCanonicalBlockTransactionID(receiveRaw, matched[0].LT, matched[0].TxHashHex), nil
}
func listManualTonCandidateTransactions(ctx context.Context, api ton.APIClientWrapped, master *ton.BlockIDExt, receive *address.Address, ref manualTonTxRef) ([]*tlb.Transaction, error) {
waiter := api.WaitForBlock(master.SeqNo)
if ref.HasLT {
hashBytes, err := hex.DecodeString(ref.HashHex)
if err != nil {
return nil, err
}
return waiter.ListTransactions(ctx, receive, 1, ref.LT, hashBytes)
}
account, err := waiter.GetAccount(ctx, master, receive)
if err != nil {
return nil, fmt.Errorf("fetch ton account state: %w", err)
}
if account == nil || account.LastTxLT == 0 || len(account.LastTxHash) == 0 {
return nil, fmt.Errorf("transaction not found")
}
return waiter.ListTransactions(ctx, receive, 100, account.LastTxLT, account.LastTxHash)
}
type manualTonState struct {
nativeToken *mdb.ChainToken
jettonWallets map[string]mdb.ChainToken
}
func manualTonTokenState(ctx context.Context, api ton.APIClientWrapped, master *ton.BlockIDExt, receive *address.Address) (*manualTonState, error) {
tokens, err := data.ListEnabledChainTokensByNetwork(mdb.NetworkTon)
if err != nil {
return nil, err
}
state := &manualTonState{jettonWallets: make(map[string]mdb.ChainToken)}
for i := range tokens {
sym := strings.ToUpper(strings.TrimSpace(tokens[i].Symbol))
contract := strings.TrimSpace(tokens[i].ContractAddress)
if sym == TonNativeSymbol && contract == "" {
token := tokens[i]
state.nativeToken = &token
continue
}
if contract == "" {
continue
}
masterAddr, err := addressutil.ParseTonMainnetAddress(contract)
if err != nil {
continue
}
wallet, err := jetton.NewJettonMasterClient(api, masterAddr.Address).GetJettonWalletAtBlock(ctx, receive, master)
if err != nil {
continue
}
state.jettonWallets[addressutil.TonRawAddressObjectKey(wallet.Address())] = tokens[i]
}
return state, nil
}
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)
nodes, err := data.ListManualPaymentRpcCandidates(mdb.NetworkSolana, mdb.RpcNodeTypeHttp) nodes, err := data.ListManualPaymentRpcCandidates(mdb.NetworkSolana, mdb.RpcNodeTypeHttp)
+237 -8
View File
@@ -1,7 +1,9 @@
package service package service
import ( import (
"bytes"
"context" "context"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -16,11 +18,33 @@ 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/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
addressutil "github.com/GMWalletApp/epusdt/util/address"
"github.com/dromara/carbon/v2" "github.com/dromara/carbon/v2"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/ton/jetton"
"github.com/xssnick/tonutils-go/tvm/cell"
"gorm.io/gorm/clause"
) )
func upsertTestChainToken(t *testing.T, token mdb.ChainToken) {
t.Helper()
if err := dao.Mdb.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "network"}, {Name: "symbol"}},
DoUpdates: clause.AssignmentColumns([]string{
"contract_address",
"decimals",
"enabled",
"min_amount",
}),
}).Create(&token).Error; err != nil {
t.Fatalf("upsert token: %v", err)
}
}
func TestManualVerifyEvmHashAcceptsOptional0x(t *testing.T) { func TestManualVerifyEvmHashAcceptsOptional0x(t *testing.T) {
hash := strings.Repeat("a", 64) hash := strings.Repeat("a", 64)
if !isEvmHash(hash) { if !isEvmHash(hash) {
@@ -89,6 +113,215 @@ func TestManualVerifyNormalizeTronTxIDAcceptsOptional0x(t *testing.T) {
} }
} }
func TestParseManualTonTxRefAcceptsCanonicalLTAndHashOnly(t *testing.T) {
receiveRaw := "0:ba295e33b3c4c9b5265aa4ead1166a92931ce9abea120a8c5e91044a1257f89c"
hash := strings.Repeat("a", 64)
ref, err := parseManualTonTxRef("ton:" + receiveRaw + ":123:" + strings.ToUpper(hash))
if err != nil {
t.Fatalf("parse canonical TON tx ref: %v", err)
}
if ref.ReceiveRaw != receiveRaw || ref.LT != 123 || ref.HashHex != hash || !ref.HasLT {
t.Fatalf("canonical ref = %#v", ref)
}
ref, err = parseManualTonTxRef("456:0X" + strings.ToUpper(hash))
if err != nil {
t.Fatalf("parse lt:hash TON tx ref: %v", err)
}
if ref.ReceiveRaw != "" || ref.LT != 456 || ref.HashHex != hash || !ref.HasLT {
t.Fatalf("lt:hash ref = %#v", ref)
}
ref, err = parseManualTonTxRef("0X" + strings.ToUpper(hash))
if err != nil {
t.Fatalf("parse hash-only TON tx ref: %v", err)
}
if ref.ReceiveRaw != "" || ref.LT != 0 || ref.HashHex != hash || ref.HasLT {
t.Fatalf("hash-only ref = %#v", ref)
}
}
type fakeManualTonAPI struct {
ton.APIClientWrapped
master *ton.BlockIDExt
account *tlb.Account
txs []*tlb.Transaction
getAccountCalls int
listCalls []manualTonListCall
}
type manualTonListCall struct {
limit uint32
lt uint64
hashHex string
}
func (f *fakeManualTonAPI) CurrentMasterchainInfo(context.Context) (*ton.BlockIDExt, error) {
return f.master, nil
}
func (f *fakeManualTonAPI) WaitForBlock(uint32) ton.APIClientWrapped {
return f
}
func (f *fakeManualTonAPI) GetAccount(context.Context, *ton.BlockIDExt, *address.Address) (*tlb.Account, error) {
f.getAccountCalls++
if f.account == nil {
return &tlb.Account{}, nil
}
return f.account, nil
}
func (f *fakeManualTonAPI) ListTransactions(_ context.Context, _ *address.Address, limit uint32, lt uint64, txHash []byte) ([]*tlb.Transaction, error) {
f.listCalls = append(f.listCalls, manualTonListCall{
limit: limit,
lt: lt,
hashHex: hex.EncodeToString(txHash),
})
return f.txs, nil
}
func TestValidateManualTonPaymentWithAPINativeTON(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := dao.Mdb.Model(&mdb.ChainToken{}).
Where("network = ? AND symbol = ?", mdb.NetworkTon, "USDT").
Update("enabled", false).Error; err != nil {
t.Fatalf("disable TON USDT token: %v", err)
}
receive := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
sender := address.NewAddress(0, 0, bytes.Repeat([]byte{0x66}, 32)).Bounce(false).Testnet(false)
tx := tonTestInboundTx(t, sender, receive, tlb.MustFromTON("1.23"), nil)
order := &mdb.Orders{
BaseModel: mdb.BaseModel{ID: 1, CreatedAt: *carbon.NewTime(carbon.CreateFromTimestampMilli(time.Now().Add(-time.Minute).UnixMilli()))},
Network: mdb.NetworkTon,
Token: "TON",
ActualAmount: 1.23,
ReceiveAddress: receive.Bounce(false).String(),
}
api := &fakeManualTonAPI{
master: &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: -0x8000000000000000, SeqNo: 9},
txs: []*tlb.Transaction{tx},
}
ref := manualTonTxRef{LT: tx.LT, HashHex: hex.EncodeToString(tx.Hash), HasLT: true}
got, err := validateManualTonPaymentWithAPI(context.Background(), api, order, receive, ref)
if err != nil {
t.Fatalf("validateManualTonPaymentWithAPI(): %v", err)
}
if api.getAccountCalls != 0 {
t.Fatalf("exact TON ref fetched account state %d times, want 0", api.getAccountCalls)
}
if len(api.listCalls) != 1 {
t.Fatalf("ListTransactions calls = %#v, want one exact lookup", api.listCalls)
}
if call := api.listCalls[0]; call.limit != 1 || call.lt != tx.LT || call.hashHex != hex.EncodeToString(tx.Hash) {
t.Fatalf("ListTransactions exact call = %#v, want limit=1 lt/hash from ref", call)
}
want := TonCanonicalBlockTransactionID(receive.StringRaw(), tx.LT, hex.EncodeToString(tx.Hash))
if got != want {
t.Fatalf("canonical id = %q, want %q", got, want)
}
}
func TestValidateManualTonPaymentCandidatesUSDTJetton(t *testing.T) {
receive := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
sender := address.NewAddress(0, 0, bytes.Repeat([]byte{0x68}, 32)).Bounce(false).Testnet(false)
jettonWallet := address.NewAddress(0, 0, bytes.Repeat([]byte{0x69}, 32)).Bounce(false).Testnet(false)
body, err := tlb.ToCell(jetton.TransferNotification{
QueryID: 7,
Amount: tlb.MustFromNano(big.NewInt(1_000_000), 6),
Sender: sender,
ForwardPayload: cell.BeginCell().EndCell(),
})
if err != nil {
t.Fatalf("build jetton notification body: %v", err)
}
tx := tonTestInboundTx(t, jettonWallet, receive, tlb.FromNanoTONU(1), body)
order := &mdb.Orders{
BaseModel: mdb.BaseModel{ID: 1, CreatedAt: *carbon.NewTime(carbon.CreateFromTimestampMilli(time.Now().Add(-time.Minute).UnixMilli()))},
Network: mdb.NetworkTon,
Token: "USDT",
ActualAmount: 1,
ReceiveAddress: receive.Bounce(false).String(),
}
state := &manualTonState{
jettonWallets: map[string]mdb.ChainToken{
addressutil.TonRawAddressObjectKey(jettonWallet): {
BaseModel: mdb.BaseModel{ID: 2},
Network: mdb.NetworkTon,
Symbol: "USDT",
Decimals: 6,
Enabled: true,
},
},
}
got, err := validateManualTonPaymentCandidates(order, receive, manualTonTxRef{
LT: tx.LT,
HashHex: hex.EncodeToString(tx.Hash),
HasLT: true,
}, []*tlb.Transaction{tx}, state)
if err != nil {
t.Fatalf("validateManualTonPaymentCandidates(): %v", err)
}
want := TonCanonicalBlockTransactionID(receive.StringRaw(), tx.LT, hex.EncodeToString(tx.Hash))
if got != want {
t.Fatalf("canonical id = %q, want %q", got, want)
}
}
func TestValidateManualTonPaymentWithAPIRejectsAmbiguousHashOnly(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := dao.Mdb.Model(&mdb.ChainToken{}).
Where("network = ? AND symbol = ?", mdb.NetworkTon, "USDT").
Update("enabled", false).Error; err != nil {
t.Fatalf("disable TON USDT token: %v", err)
}
receive := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
sender := address.NewAddress(0, 0, bytes.Repeat([]byte{0x67}, 32)).Bounce(false).Testnet(false)
tx1 := tonTestInboundTx(t, sender, receive, tlb.MustFromTON("1.23"), nil)
tx2 := tonTestInboundTx(t, sender, receive, tlb.MustFromTON("1.23"), nil)
tx2.LT = tx1.LT + 1
order := &mdb.Orders{
BaseModel: mdb.BaseModel{ID: 1, CreatedAt: *carbon.NewTime(carbon.CreateFromTimestampMilli(time.Now().Add(-time.Minute).UnixMilli()))},
Network: mdb.NetworkTon,
Token: "TON",
ActualAmount: 1.23,
ReceiveAddress: receive.Bounce(false).String(),
}
api := &fakeManualTonAPI{
master: &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: -0x8000000000000000, SeqNo: 9},
account: &tlb.Account{
LastTxLT: tx2.LT,
LastTxHash: tx2.Hash,
},
txs: []*tlb.Transaction{tx1, tx2},
}
_, err := validateManualTonPaymentWithAPI(context.Background(), api, order, receive, manualTonTxRef{
HashHex: hex.EncodeToString(tx1.Hash),
})
if err == nil || !strings.Contains(err.Error(), "matched multiple") {
t.Fatalf("validate ambiguous hash-only error = %v, want multiple-match error", err)
}
if api.getAccountCalls != 1 {
t.Fatalf("hash-only TON ref fetched account state %d times, want 1", api.getAccountCalls)
}
if len(api.listCalls) != 1 {
t.Fatalf("ListTransactions calls = %#v, want one recent lookup", api.listCalls)
}
if call := api.listCalls[0]; call.limit != 100 || call.lt != tx2.LT || call.hashHex != hex.EncodeToString(tx2.Hash) {
t.Fatalf("ListTransactions recent call = %#v, want account last tx cursor", call)
}
}
func TestManualVerifyDialEvmClientsIncludesHTTPNode(t *testing.T) { func TestManualVerifyDialEvmClientsIncludesHTTPNode(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t) cleanup := testutil.SetupTestDatabases(t)
defer cleanup() defer cleanup()
@@ -369,15 +602,13 @@ func TestManualVerifyTronTRC20UsesTransferEvent(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("recipient address: %v", err) t.Fatalf("recipient address: %v", err)
} }
if err = dao.Mdb.Create(&mdb.ChainToken{ upsertTestChainToken(t, mdb.ChainToken{
Network: mdb.NetworkTron, Network: mdb.NetworkTron,
Symbol: "USDT", Symbol: "USDT",
ContractAddress: contractAddress, ContractAddress: contractAddress,
Decimals: 6, Decimals: 6,
Enabled: true, Enabled: true,
}).Error; err != nil { })
t.Fatalf("create token: %v", err)
}
rawAmount := big.NewInt(1230000) rawAmount := big.NewInt(1230000)
tx := manualTronTransactionFromCallData(t, contractHex, recipientHex, rawAmount) tx := manualTronTransactionFromCallData(t, contractHex, recipientHex, rawAmount)
@@ -491,15 +722,13 @@ func TestManualVerifyTronPaymentHTTPFlow(t *testing.T) {
}).Error; err != nil { }).Error; err != nil {
t.Fatalf("create tron rpc node: %v", err) t.Fatalf("create tron rpc node: %v", err)
} }
if err = dao.Mdb.Create(&mdb.ChainToken{ upsertTestChainToken(t, mdb.ChainToken{
Network: mdb.NetworkTron, Network: mdb.NetworkTron,
Symbol: "USDT", Symbol: "USDT",
ContractAddress: contractAddress, ContractAddress: contractAddress,
Decimals: 6, Decimals: 6,
Enabled: true, Enabled: true,
}).Error; err != nil { })
t.Fatalf("create token: %v", err)
}
order := &mdb.Orders{ order := &mdb.Orders{
TradeId: "manual-tron-http-flow", TradeId: "manual-tron-http-flow",
OrderId: "manual-tron-http-flow", OrderId: "manual-tron-http-flow",
+44 -17
View File
@@ -14,6 +14,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/response" "github.com/GMWalletApp/epusdt/model/response"
addressutil "github.com/GMWalletApp/epusdt/util/address"
"github.com/GMWalletApp/epusdt/util/constant" "github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/log" "github.com/GMWalletApp/epusdt/util/log"
"github.com/GMWalletApp/epusdt/util/math" "github.com/GMWalletApp/epusdt/util/math"
@@ -48,11 +49,27 @@ func normalizeOrderAddressByNetwork(network, address string) string {
switch network { switch network {
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma: case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma:
return strings.ToLower(address) return strings.ToLower(address)
case mdb.NetworkTon:
if normalized, err := addressutil.NormalizeTonAddress(address); err == nil {
return normalized
}
return address
default: default:
return address return address
} }
} }
func ensureEnabledOrderAsset(network, token string) (*mdb.ChainToken, error) {
tokenRow, err := data.GetEnabledChainTokenBySymbol(network, token)
if err != nil {
return nil, err
}
if tokenRow == nil || tokenRow.ID == 0 {
return nil, constant.SupportedAssetNotFound
}
return tokenRow, nil
}
// CreateTransaction creates a new payment order. // CreateTransaction creates a new payment order.
func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey) (*response.CreateTransactionResponse, error) { func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey) (*response.CreateTransactionResponse, error) {
token := strings.ToUpper(strings.TrimSpace(req.Token)) token := strings.ToUpper(strings.TrimSpace(req.Token))
@@ -68,6 +85,21 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
amountPrecision := data.GetAmountPrecision() amountPrecision := data.GetAmountPrecision()
payAmount := math.MustParsePrecFloat64(req.Amount, amountPrecision) payAmount := math.MustParsePrecFloat64(req.Amount, amountPrecision)
exist, err := data.GetOrderInfoByOrderId(req.OrderId)
if err != nil {
return nil, err
}
if exist.ID > 0 {
return nil, constant.OrderAlreadyExists
}
if !data.IsChainEnabled(network) {
return nil, constant.ChainNotEnabled
}
if _, err = ensureEnabledOrderAsset(network, token); err != nil {
return nil, err
}
rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(currency)) rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(currency))
if rate <= 0 { if rate <= 0 {
return nil, constant.RateAmountErr return nil, constant.RateAmountErr
@@ -82,17 +114,6 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
return nil, constant.PayAmountErr return nil, constant.PayAmountErr
} }
exist, err := data.GetOrderInfoByOrderId(req.OrderId)
if err != nil {
return nil, err
}
if exist.ID > 0 {
return nil, constant.OrderAlreadyExists
}
if !data.IsChainEnabled(network) {
return nil, constant.ChainNotEnabled
}
walletAddress, err := data.GetAvailableWalletAddressByNetwork(network) walletAddress, err := data.GetAvailableWalletAddressByNetwork(network)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -485,7 +506,16 @@ func SwitchNetwork(req *request.SwitchNetworkRequest) (*response.CheckoutCounter
return nil, constant.SubOrderLimitExceeded return nil, constant.SubOrderLimitExceeded
} }
// 5. Calculate amount for the new network // 5. Validate the target chain and asset before any rate lookup so
// disabled/unknown tokens return the stable unsupported-asset error.
if !data.IsChainEnabled(network) {
return nil, constant.ChainNotEnabled
}
if _, err = ensureEnabledOrderAsset(network, token); err != nil {
return nil, err
}
// 6. Calculate amount for the new network
rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(parent.Currency)) rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(parent.Currency))
if rate <= 0 { if rate <= 0 {
return nil, constant.RateAmountErr return nil, constant.RateAmountErr
@@ -496,10 +526,7 @@ func SwitchNetwork(req *request.SwitchNetworkRequest) (*response.CheckoutCounter
return nil, constant.PayAmountErr return nil, constant.PayAmountErr
} }
// 6. Find and lock wallet // 7. Find and lock wallet
if !data.IsChainEnabled(network) {
return nil, constant.ChainNotEnabled
}
walletAddress, err := data.GetAvailableWalletAddressByNetwork(network) walletAddress, err := data.GetAvailableWalletAddressByNetwork(network)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -518,7 +545,7 @@ func SwitchNetwork(req *request.SwitchNetworkRequest) (*response.CheckoutCounter
return nil, constant.NotAvailableAmountErr return nil, constant.NotAvailableAmountErr
} }
// 7. Create sub-order // 8. Create sub-order
tx := dao.Mdb.Begin() tx := dao.Mdb.Begin()
subOrder := &mdb.Orders{ subOrder := &mdb.Orders{
TradeId: subTradeID, TradeId: subTradeID,
+420
View File
@@ -4,6 +4,7 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io" "io"
"math/big"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
@@ -18,6 +19,7 @@ import (
"github.com/GMWalletApp/epusdt/util/constant" "github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/http_client" "github.com/GMWalletApp/epusdt/util/http_client"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/xssnick/tonutils-go/address"
) )
func newCreateTransactionRequest(orderID string, amount float64) *request.CreateTransactionRequest { func newCreateTransactionRequest(orderID string, amount float64) *request.CreateTransactionRequest {
@@ -281,6 +283,424 @@ func TestCreateTransactionNormalizesEvmReceiveAddressToLowercase(t *testing.T) {
} }
} }
func TestCreateTransactionCreatesTonOrderAndRawLock(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := data.SetSetting("rate", "rate.forced_rate_list", `{"cny":{"ton":0.5}}`, "json"); err != nil {
t.Fatalf("set forced rate: %v", err)
}
addr := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
wallet, err := data.AddWalletAddressWithNetwork(mdb.NetworkTon, addr.StringRaw())
if err != nil {
t.Fatalf("add ton wallet: %v", err)
}
req := newCreateTransactionRequest("order_ton_native_1", 10)
req.Network = mdb.NetworkTon
req.Token = "TON"
resp, err := CreateTransaction(req, nil)
if err != nil {
t.Fatalf("create TON transaction: %v", err)
}
if resp.ReceiveAddress != wallet.Address {
t.Fatalf("receive address = %q, want %q", resp.ReceiveAddress, wallet.Address)
}
if resp.Token != "TON" {
t.Fatalf("token = %q, want TON", resp.Token)
}
if got := fmt.Sprintf("%.2f", resp.ActualAmount); got != "5.00" {
t.Fatalf("actual amount = %s, want 5.00", got)
}
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTon, addr.StringRaw(), "TON", resp.ActualAmount)
if err != nil {
t.Fatalf("lookup ton runtime lock: %v", err)
}
if tradeID != resp.TradeId {
t.Fatalf("runtime lock trade_id = %q, want %q", tradeID, resp.TradeId)
}
}
func TestCreateTransactionCreatesTonOrderWithConfiguredPrecision(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := data.SetSetting(mdb.SettingGroupSystem, mdb.SettingKeyAmountPrecision, "4", mdb.SettingTypeInt); err != nil {
t.Fatalf("set amount precision: %v", err)
}
if err := data.SetSetting("rate", "rate.forced_rate_list", `{"cny":{"ton":0.12345}}`, "json"); err != nil {
t.Fatalf("set forced rate: %v", err)
}
addr := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
if _, err := data.AddWalletAddressWithNetwork(mdb.NetworkTon, addr.StringRaw()); err != nil {
t.Fatalf("add ton wallet: %v", err)
}
req := newCreateTransactionRequest("order_ton_precision_1", 10)
req.Network = mdb.NetworkTon
req.Token = "TON"
resp, err := CreateTransaction(req, nil)
if err != nil {
t.Fatalf("create TON transaction: %v", err)
}
if got := fmt.Sprintf("%.4f", resp.ActualAmount); got != "1.2345" {
t.Fatalf("actual amount = %s, want 1.2345", got)
}
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTon, addr.Bounce(true).String(), "TON", 1.2345)
if err != nil {
t.Fatalf("lookup ton runtime lock: %v", err)
}
if tradeID != resp.TradeId {
t.Fatalf("runtime lock trade_id = %q, want %q", tradeID, resp.TradeId)
}
}
func TestCreateTransactionRejectsUnsupportedTonToken(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
req := newCreateTransactionRequest("order_ton_gram_1", 10)
req.Network = mdb.NetworkTon
req.Token = "GRAM"
_, err := CreateTransaction(req, nil)
if err != constant.SupportedAssetNotFound {
t.Fatalf("create unsupported TON token error = %v, want %v", err, constant.SupportedAssetNotFound)
}
}
func TestCreateTransactionRejectsDisabledTonTokenBeforeRate(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := dao.Mdb.Model(&mdb.ChainToken{}).
Where("network = ? AND symbol = ?", mdb.NetworkTon, "TON").
Update("enabled", false).Error; err != nil {
t.Fatalf("disable TON token: %v", err)
}
req := newCreateTransactionRequest("order_ton_disabled_1", 10)
req.Network = mdb.NetworkTon
req.Token = "TON"
_, err := CreateTransaction(req, nil)
if err != constant.SupportedAssetNotFound {
t.Fatalf("create disabled TON token error = %v, want %v", err, constant.SupportedAssetNotFound)
}
}
func TestCreateTransactionSupportedTonTokenWithoutRateReturnsRateError(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := data.SetSetting("rate", "rate.forced_rate_list", `{"cny":{"ton":0}}`, "json"); err != nil {
t.Fatalf("set forced rate: %v", err)
}
if err := data.SetSetting("rate", "rate.api_url", "", "string"); err != nil {
t.Fatalf("clear rate api url: %v", err)
}
req := newCreateTransactionRequest("order_ton_missing_rate_1", 10)
req.Network = mdb.NetworkTon
req.Token = "TON"
_, err := CreateTransaction(req, nil)
if err != constant.RateAmountErr {
t.Fatalf("create TON without rate error = %v, want %v", err, constant.RateAmountErr)
}
}
func TestSwitchNetworkCreatesTonSubOrderAndRawLock(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := data.SetSetting("rate", "rate.forced_rate_list", `{"cny":{"usdt":0.1,"ton":0.5}}`, "json"); err != nil {
t.Fatalf("set forced rate: %v", err)
}
if _, err := data.AddWalletAddress("TTestTronAddress001"); err != nil {
t.Fatalf("add tron wallet: %v", err)
}
addr := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
wallet, err := data.AddWalletAddressWithNetwork(mdb.NetworkTon, addr.StringRaw())
if err != nil {
t.Fatalf("add ton wallet: %v", err)
}
parentReq := newCreateTransactionRequest("order_switch_ton_1", 10)
parentReq.Network = mdb.NetworkTron
parentResp, err := CreateTransaction(parentReq, nil)
if err != nil {
t.Fatalf("create parent transaction: %v", err)
}
subResp, err := SwitchNetwork(&request.SwitchNetworkRequest{
TradeId: parentResp.TradeId,
Token: "TON",
Network: mdb.NetworkTon,
})
if err != nil {
t.Fatalf("switch to TON: %v", err)
}
if subResp.Network != mdb.NetworkTon || subResp.Token != "TON" {
t.Fatalf("sub order network/token = %s/%s, want ton/TON", subResp.Network, subResp.Token)
}
if subResp.ReceiveAddress != wallet.Address {
t.Fatalf("sub order receive address = %q, want %q", subResp.ReceiveAddress, wallet.Address)
}
if got := fmt.Sprintf("%.2f", subResp.ActualAmount); got != "5.00" {
t.Fatalf("sub order actual amount = %s, want 5.00", got)
}
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTon, addr.StringRaw(), "TON", subResp.ActualAmount)
if err != nil {
t.Fatalf("lookup TON sub-order lock: %v", err)
}
if tradeID != subResp.TradeId {
t.Fatalf("TON sub-order lock trade_id = %q, want %q", tradeID, subResp.TradeId)
}
}
func TestSwitchNetworkRejectsUnsupportedTonToken(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := data.SetSetting("rate", "rate.forced_rate_list", `{"cny":{"usdt":0.1}}`, "json"); err != nil {
t.Fatalf("set forced rate: %v", err)
}
if _, err := data.AddWalletAddress("TTestTronAddress002"); err != nil {
t.Fatalf("add tron wallet: %v", err)
}
parentResp, err := CreateTransaction(newCreateTransactionRequest("order_switch_gram_1", 10), nil)
if err != nil {
t.Fatalf("create parent transaction: %v", err)
}
_, err = SwitchNetwork(&request.SwitchNetworkRequest{
TradeId: parentResp.TradeId,
Token: "GRAM",
Network: mdb.NetworkTon,
})
if err != constant.SupportedAssetNotFound {
t.Fatalf("switch unsupported TON token error = %v, want %v", err, constant.SupportedAssetNotFound)
}
}
func TestTryProcessTonTransferMarksOrderPaidAndReleasesRawLock(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := data.SetSetting("rate", "rate.forced_rate_list", `{"cny":{"ton":0.5}}`, "json"); err != nil {
t.Fatalf("set forced rate: %v", err)
}
addr := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
if _, err := data.AddWalletAddressWithNetwork(mdb.NetworkTon, addr.StringRaw()); err != nil {
t.Fatalf("add ton wallet: %v", err)
}
req := newCreateTransactionRequest("order_ton_process_1", 10)
req.Network = mdb.NetworkTon
req.Token = "TON"
resp, err := CreateTransaction(req, nil)
if err != nil {
t.Fatalf("create TON transaction: %v", err)
}
transfer := &TonObservedTransfer{
ReceiveAddress: resp.ReceiveAddress,
Token: mdb.ChainToken{BaseModel: mdb.BaseModel{ID: 1}, Network: mdb.NetworkTon, Symbol: "TON", Decimals: 9, Enabled: true},
RawAmount: big.NewInt(5_000_000_000),
Amount: resp.ActualAmount,
BlockTimeMs: time.Now().Add(time.Second).UnixMilli(),
LT: 100,
TxHashHex: strings.Repeat("1", 64),
BlockID: TonCanonicalBlockTransactionID(addr.StringRaw(), 100, strings.Repeat("1", 64)),
}
TryProcessTonTransfer(transfer)
order, err := data.GetOrderInfoByTradeId(resp.TradeId)
if err != nil {
t.Fatalf("reload order: %v", err)
}
if order.Status != mdb.StatusPaySuccess {
t.Fatalf("order status = %d, want %d", order.Status, mdb.StatusPaySuccess)
}
if order.BlockTransactionId != transfer.BlockID {
t.Fatalf("block transaction id = %q, want %q", order.BlockTransactionId, transfer.BlockID)
}
lock, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTon, addr.StringRaw(), "TON", resp.ActualAmount)
if err != nil {
t.Fatalf("lookup ton lock: %v", err)
}
if lock != "" {
t.Fatalf("TON lock still exists after payment: %s", lock)
}
}
func TestTryProcessTonJettonTransferMarksUSDTOrderPaid(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := data.SetSetting("rate", "rate.forced_rate_list", `{"cny":{"usdt":0.1}}`, "json"); err != nil {
t.Fatalf("set forced rate: %v", err)
}
addr := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
if _, err := data.AddWalletAddressWithNetwork(mdb.NetworkTon, addr.StringRaw()); err != nil {
t.Fatalf("add ton wallet: %v", err)
}
req := newCreateTransactionRequest("order_ton_usdt_process_1", 10)
req.Network = mdb.NetworkTon
req.Token = "USDT"
resp, err := CreateTransaction(req, nil)
if err != nil {
t.Fatalf("create TON USDT transaction: %v", err)
}
transfer := &TonObservedTransfer{
ReceiveAddress: resp.ReceiveAddress,
Token: mdb.ChainToken{BaseModel: mdb.BaseModel{ID: 2}, Network: mdb.NetworkTon, Symbol: "USDT", Decimals: 6, Enabled: true},
RawAmount: big.NewInt(1_000_000),
Amount: resp.ActualAmount,
BlockTimeMs: time.Now().Add(time.Second).UnixMilli(),
LT: 110,
TxHashHex: strings.Repeat("4", 64),
BlockID: TonCanonicalBlockTransactionID(addr.StringRaw(), 110, strings.Repeat("4", 64)),
}
TryProcessTonTransfer(transfer)
order, err := data.GetOrderInfoByTradeId(resp.TradeId)
if err != nil {
t.Fatalf("reload order: %v", err)
}
if order.Status != mdb.StatusPaySuccess {
t.Fatalf("order status = %d, want %d", order.Status, mdb.StatusPaySuccess)
}
if order.BlockTransactionId != transfer.BlockID {
t.Fatalf("block transaction id = %q, want %q", order.BlockTransactionId, transfer.BlockID)
}
lock, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTon, addr.StringRaw(), "USDT", resp.ActualAmount)
if err != nil {
t.Fatalf("lookup TON USDT lock: %v", err)
}
if lock != "" {
t.Fatalf("TON USDT lock still exists after payment: %s", lock)
}
}
func TestTryProcessTonTransferSkipsTransfersBeforeOrderCreation(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := data.SetSetting("rate", "rate.forced_rate_list", `{"cny":{"ton":0.5}}`, "json"); err != nil {
t.Fatalf("set forced rate: %v", err)
}
addr := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
if _, err := data.AddWalletAddressWithNetwork(mdb.NetworkTon, addr.StringRaw()); err != nil {
t.Fatalf("add ton wallet: %v", err)
}
req := newCreateTransactionRequest("order_ton_old_transfer_1", 10)
req.Network = mdb.NetworkTon
req.Token = "TON"
resp, err := CreateTransaction(req, nil)
if err != nil {
t.Fatalf("create TON transaction: %v", err)
}
order, err := data.GetOrderInfoByTradeId(resp.TradeId)
if err != nil {
t.Fatalf("load order: %v", err)
}
TryProcessTonTransfer(&TonObservedTransfer{
ReceiveAddress: resp.ReceiveAddress,
Token: mdb.ChainToken{BaseModel: mdb.BaseModel{ID: 1}, Network: mdb.NetworkTon, Symbol: "TON", Decimals: 9, Enabled: true},
RawAmount: big.NewInt(5_000_000_000),
Amount: resp.ActualAmount,
BlockTimeMs: order.CreatedAt.TimestampMilli() - 1,
LT: 101,
TxHashHex: strings.Repeat("2", 64),
BlockID: TonCanonicalBlockTransactionID(addr.StringRaw(), 101, strings.Repeat("2", 64)),
})
order, err = data.GetOrderInfoByTradeId(resp.TradeId)
if err != nil {
t.Fatalf("reload order: %v", err)
}
if order.Status != mdb.StatusWaitPay {
t.Fatalf("order status = %d, want wait-pay", order.Status)
}
if order.BlockTransactionId != "" {
t.Fatalf("old transfer set block transaction id = %q", order.BlockTransactionId)
}
}
func TestTryProcessTonSubOrderPaysParentAndReleasesLocks(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := data.SetSetting("rate", "rate.forced_rate_list", `{"cny":{"usdt":0.1,"ton":0.5}}`, "json"); err != nil {
t.Fatalf("set forced rate: %v", err)
}
if _, err := data.AddWalletAddress("TTestTronAddressForTonSub"); err != nil {
t.Fatalf("add tron wallet: %v", err)
}
addr := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
if _, err := data.AddWalletAddressWithNetwork(mdb.NetworkTon, addr.StringRaw()); err != nil {
t.Fatalf("add ton wallet: %v", err)
}
parentReq := newCreateTransactionRequest("order_ton_sub_parent_1", 10)
parentReq.Network = mdb.NetworkTron
parentResp, err := CreateTransaction(parentReq, nil)
if err != nil {
t.Fatalf("create parent order: %v", err)
}
subResp, err := SwitchNetwork(&request.SwitchNetworkRequest{
TradeId: parentResp.TradeId,
Token: "TON",
Network: mdb.NetworkTon,
})
if err != nil {
t.Fatalf("switch to TON: %v", err)
}
TryProcessTonTransfer(&TonObservedTransfer{
ReceiveAddress: subResp.ReceiveAddress,
Token: mdb.ChainToken{BaseModel: mdb.BaseModel{ID: 1}, Network: mdb.NetworkTon, Symbol: "TON", Decimals: 9, Enabled: true},
RawAmount: big.NewInt(5_000_000_000),
Amount: subResp.ActualAmount,
BlockTimeMs: time.Now().Add(time.Second).UnixMilli(),
LT: 102,
TxHashHex: strings.Repeat("3", 64),
BlockID: TonCanonicalBlockTransactionID(addr.StringRaw(), 102, strings.Repeat("3", 64)),
})
parent, err := data.GetOrderInfoByTradeId(parentResp.TradeId)
if err != nil {
t.Fatalf("reload parent order: %v", err)
}
if parent.Status != mdb.StatusPaySuccess {
t.Fatalf("parent status = %d, want paid", parent.Status)
}
sub, err := data.GetOrderInfoByTradeId(subResp.TradeId)
if err != nil {
t.Fatalf("reload sub order: %v", err)
}
if sub.Status != mdb.StatusPaySuccess {
t.Fatalf("sub status = %d, want paid", sub.Status)
}
parentLock, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, parentResp.ReceiveAddress, parentResp.Token, parentResp.ActualAmount)
if err != nil {
t.Fatalf("lookup parent lock: %v", err)
}
if parentLock != "" {
t.Fatalf("parent lock still exists: %s", parentLock)
}
subLock, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTon, addr.StringRaw(), "TON", subResp.ActualAmount)
if err != nil {
t.Fatalf("lookup sub lock: %v", err)
}
if subLock != "" {
t.Fatalf("TON sub-order lock still exists: %s", subLock)
}
}
func TestOrderProcessingMarksPaidAndReleasesLock(t *testing.T) { func TestOrderProcessingMarksPaidAndReleasesLock(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t) cleanup := testutil.SetupTestDatabases(t)
defer cleanup() defer cleanup()
+18 -4
View File
@@ -12,8 +12,24 @@ 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/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"gorm.io/gorm/clause"
) )
func upsertTaskServiceTestChainToken(t *testing.T, token mdb.ChainToken) {
t.Helper()
if err := dao.Mdb.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "network"}, {Name: "symbol"}},
DoUpdates: clause.AssignmentColumns([]string{
"contract_address",
"decimals",
"enabled",
"min_amount",
}),
}).Create(&token).Error; err != nil {
t.Fatalf("seed chain token: %v", err)
}
}
func TestSendPaymentNotificationUsesLatestOrderUpdatedAt(t *testing.T) { func TestSendPaymentNotificationUsesLatestOrderUpdatedAt(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t) cleanup := testutil.SetupTestDatabases(t)
defer cleanup() defer cleanup()
@@ -158,15 +174,13 @@ func TestTryProcessEvmERC20TransferSkipsTransfersBeforeOrderCreation(t *testing.
contract := common.HexToAddress("0x3333333333333333333333333333333333333333") contract := common.HexToAddress("0x3333333333333333333333333333333333333333")
receiveAddress := common.HexToAddress("0x4444444444444444444444444444444444444444") receiveAddress := common.HexToAddress("0x4444444444444444444444444444444444444444")
if err := dao.Mdb.Create(&mdb.ChainToken{ upsertTaskServiceTestChainToken(t, mdb.ChainToken{
Network: mdb.NetworkEthereum, Network: mdb.NetworkEthereum,
Symbol: tokenSym, Symbol: tokenSym,
ContractAddress: contract.Hex(), ContractAddress: contract.Hex(),
Decimals: 6, Decimals: 6,
Enabled: true, Enabled: true,
}).Error; err != nil { })
t.Fatalf("seed chain token: %v", err)
}
order := &mdb.Orders{ order := &mdb.Orders{
TradeId: tradeID, TradeId: tradeID,
+37
View File
@@ -0,0 +1,37 @@
package service
import (
"context"
"fmt"
"strings"
"time"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/ton"
)
// ConnectTonLiteAPI builds a tonutils-go API client from a liteserver global
// config URL. Scanner and manual verification use this same connection setup.
func ConnectTonLiteAPI(ctx context.Context, configURL string, requestTimeout time.Duration, reconnectLimit int) (ton.APIClientWrapped, func(), error) {
configURL = strings.TrimSpace(configURL)
if configURL == "" {
return nil, func() {}, fmt.Errorf("ton lite config url is empty")
}
if reconnectLimit <= 0 {
reconnectLimit = 3
}
cfg, err := liteclient.GetConfigFromUrl(ctx, configURL)
if err != nil {
return nil, func() {}, err
}
pool := liteclient.NewConnectionPool()
pool.SetOnDisconnect(pool.DefaultReconnect(3*time.Second, reconnectLimit))
if err = pool.AddConnectionsFromConfig(ctx, cfg); err != nil {
pool.Stop()
return nil, func() {}, err
}
base := ton.NewAPIClient(pool, ton.ProofCheckPolicyFast)
base.SetTrustedBlockFromConfig(cfg)
api := base.WithRetry(2).WithTimeout(requestTimeout).WithLSInfoInErrors()
return api, pool.Stop, nil
}
+262
View File
@@ -0,0 +1,262 @@
package service
import (
"encoding/hex"
"errors"
"fmt"
"math/big"
"strings"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/model/request"
addressutil "github.com/GMWalletApp/epusdt/util/address"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/log"
"github.com/GMWalletApp/epusdt/util/math"
"github.com/shopspring/decimal"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/ton/jetton"
"github.com/xssnick/tonutils-go/tvm/cell"
)
const (
TonNativeSymbol = "TON"
TonTransferNotificationOpcode = 0x7362d09c
)
type TonObservedTransfer struct {
ReceiveAddress string
Token mdb.ChainToken
RawAmount *big.Int
Amount float64
BlockTimeMs int64
LT uint64
TxHashHex string
BlockID string
SenderAddress string
JettonWallet string
}
func TonCanonicalBlockTransactionID(receiveRaw string, lt uint64, txHashHex string) string {
return fmt.Sprintf("ton:%s:%d:%s", strings.ToLower(strings.TrimSpace(receiveRaw)), lt, strings.ToLower(strings.TrimSpace(txHashHex)))
}
func ParseTonInboundTransfer(tx *tlb.Transaction, receive *address.Address, nativeToken *mdb.ChainToken, jettonWalletTokens map[string]mdb.ChainToken) (*TonObservedTransfer, error) {
if tx == nil || receive == nil || tx.IO.In == nil || tx.IO.In.MsgType != tlb.MsgTypeInternal {
return nil, nil
}
if isTonTransactionBounced(tx) {
return nil, nil
}
in := tx.IO.In.AsInternal()
if in == nil || in.DstAddr == nil || !in.DstAddr.Equals(receive) {
return nil, nil
}
receiveRaw := addressutil.TonRawAddressObjectKey(receive)
txHashHex := hex.EncodeToString(tx.Hash)
blockID := TonCanonicalBlockTransactionID(receiveRaw, tx.LT, txHashHex)
blockTimeMs := int64(tx.Now) * 1000
if in.SrcAddr != nil {
srcRaw := addressutil.TonRawAddressObjectKey(in.SrcAddr)
if token, ok := jettonWalletTokens[srcRaw]; ok {
transfer, err := parseTonJettonTransferNotification(in)
if err != nil {
return nil, nil
}
amount := tonRawAmountToFloat(transfer.Amount.Nano(), token.Decimals)
if amount <= 0 {
return nil, nil
}
if token.MinAmount > 0 && amount < token.MinAmount {
return nil, nil
}
return &TonObservedTransfer{
ReceiveAddress: addressutil.NormalizeTonAddressObject(receive),
Token: token,
RawAmount: transfer.Amount.Nano(),
Amount: amount,
BlockTimeMs: blockTimeMs,
LT: tx.LT,
TxHashHex: txHashHex,
BlockID: blockID,
SenderAddress: addressutil.NormalizeTonAddressObject(transfer.Sender),
JettonWallet: addressutil.NormalizeTonAddressObject(in.SrcAddr),
}, nil
}
}
if tonPayloadIsTransferNotification(in.Body) {
return nil, nil
}
if nativeToken == nil || nativeToken.ID == 0 {
return nil, nil
}
rawNano := in.Amount.Nano()
if rawNano == nil || rawNano.Sign() <= 0 {
return nil, nil
}
amount := tonRawAmountToFloat(rawNano, nativeToken.Decimals)
if amount <= 0 {
return nil, nil
}
if nativeToken.MinAmount > 0 && amount < nativeToken.MinAmount {
return nil, nil
}
return &TonObservedTransfer{
ReceiveAddress: addressutil.NormalizeTonAddressObject(receive),
Token: *nativeToken,
RawAmount: rawNano,
Amount: amount,
BlockTimeMs: blockTimeMs,
LT: tx.LT,
TxHashHex: txHashHex,
BlockID: blockID,
}, nil
}
func TryProcessTonTransfer(transfer *TonObservedTransfer) {
if transfer == nil {
return
}
tokenSym := strings.ToUpper(strings.TrimSpace(transfer.Token.Symbol))
defer func() {
if err := recover(); err != nil {
log.Sugar.Errorf("[TON-%s][%s] TryProcessTonTransfer panic: %v", tokenSym, transfer.ReceiveAddress, err)
}
}()
receive := strings.TrimSpace(transfer.ReceiveAddress)
if tokenSym == "" || receive == "" || transfer.Amount <= 0 || transfer.BlockID == "" {
return
}
log.Sugar.Infof("[TON-%s][%s] observed transfer tx=%s lt=%d amount=%.6f", tokenSym, receive, transfer.BlockID, transfer.LT, transfer.Amount)
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTon, receive, tokenSym, transfer.Amount)
if err != nil {
log.Sugar.Warnf("[TON-%s][%s] lock lookup: %v", tokenSym, receive, err)
return
}
if tradeID == "" {
log.Sugar.Infof("[TON-%s][%s] skip unmatched transfer tx=%s amount=%.6f", tokenSym, receive, transfer.BlockID, transfer.Amount)
return
}
order, err := data.GetOrderInfoByTradeId(tradeID)
if err != nil {
log.Sugar.Warnf("[TON-%s][%s] load order: %v", tokenSym, receive, err)
return
}
if strings.ToLower(strings.TrimSpace(order.Network)) != mdb.NetworkTon {
log.Sugar.Warnf("[TON-%s][%s] skip trade_id=%s network=%q", tokenSym, receive, tradeID, order.Network)
return
}
if strings.ToUpper(strings.TrimSpace(order.Token)) != tokenSym {
log.Sugar.Warnf("[TON-%s][%s] skip trade_id=%s token mismatch order=%s", tokenSym, receive, tradeID, order.Token)
return
}
if !tonOrderAddressMatches(order.ReceiveAddress, receive) {
log.Sugar.Warnf("[TON-%s][%s] skip trade_id=%s receive address mismatch order=%s", tokenSym, receive, tradeID, order.ReceiveAddress)
return
}
if transfer.BlockTimeMs > 0 && transfer.BlockTimeMs < order.CreatedAt.TimestampMilli() {
log.Sugar.Warnf("[TON-%s][%s] skip tx %s because block time %d is before order create time %d", tokenSym, receive, transfer.BlockID, transfer.BlockTimeMs, order.CreatedAt.TimestampMilli())
return
}
req := &request.OrderProcessingRequest{
ReceiveAddress: order.ReceiveAddress,
Token: tokenSym,
Network: mdb.NetworkTon,
TradeId: tradeID,
Amount: transfer.Amount,
BlockTransactionId: transfer.BlockID,
}
err = OrderProcessing(req)
if err != nil {
if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
log.Sugar.Infof("[TON-%s][%s] skip resolved transfer trade_id=%s tx=%s err=%v", tokenSym, receive, tradeID, transfer.BlockID, err)
return
}
log.Sugar.Errorf("[TON-%s][%s] OrderProcessing trade_id=%s tx=%s: %v", tokenSym, receive, tradeID, transfer.BlockID, err)
return
}
sendPaymentNotification(order)
log.Sugar.Infof("[TON-%s][%s] payment processed trade_id=%s tx=%s", tokenSym, receive, tradeID, transfer.BlockID)
}
func tonOrderAddressMatches(a, b string) bool {
ar, errA := addressutil.TonRawAddressKey(a)
br, errB := addressutil.TonRawAddressKey(b)
return errA == nil && errB == nil && ar == br
}
func tonRawAmountToFloat(rawAmount *big.Int, decimals int) float64 {
if rawAmount == nil || rawAmount.Sign() <= 0 {
return 0
}
if decimals < 0 {
decimals = 0
}
amount := decimal.NewFromBigInt(rawAmount, -int32(decimals))
return math.MustParsePrecFloat64(amount.InexactFloat64(), data.MaxAmountPrecision)
}
func parseTonJettonTransferNotification(in *tlb.InternalMessage) (*jetton.TransferNotification, error) {
if in == nil || in.Body == nil {
return nil, fmt.Errorf("missing jetton transfer notification body")
}
var transfer jetton.TransferNotification
if err := tlb.LoadFromCell(&transfer, in.Body.BeginParse()); err != nil {
return nil, err
}
return &transfer, nil
}
func tonPayloadIsTransferNotification(body *cell.Cell) bool {
if body == nil {
return false
}
op, err := body.BeginParse().LoadUInt(32)
return err == nil && op == TonTransferNotificationOpcode
}
func isTonTransactionBounced(tx *tlb.Transaction) bool {
if tx == nil {
return false
}
if dsc, ok := tx.Description.(tlb.TransactionDescriptionOrdinary); ok && dsc.BouncePhase != nil {
if _, ok = dsc.BouncePhase.Phase.(tlb.BouncePhaseOk); ok {
return true
}
}
return false
}
func EnsureTonTransferMatchesOrder(order *mdb.Orders, transfer *TonObservedTransfer) error {
if order == nil || order.ID == 0 {
return fmt.Errorf("order not found")
}
if transfer == nil {
return fmt.Errorf("matching ton transfer to order address not found")
}
if strings.ToLower(strings.TrimSpace(order.Network)) != mdb.NetworkTon {
return fmt.Errorf("order network mismatch")
}
if !strings.EqualFold(order.Token, transfer.Token.Symbol) {
return fmt.Errorf("transaction token mismatch")
}
if !tonOrderAddressMatches(order.ReceiveAddress, transfer.ReceiveAddress) {
return fmt.Errorf("transaction recipient mismatch")
}
if transfer.BlockTimeMs <= 0 || transfer.BlockTimeMs < order.CreatedAt.TimestampMilli() {
return fmt.Errorf("transaction predates the order")
}
if !amountMatchesRaw(order.ActualAmount, transfer.RawAmount, transfer.Token.Decimals) {
return fmt.Errorf("transaction amount mismatch")
}
return nil
}
+173
View File
@@ -0,0 +1,173 @@
package service
import (
"bytes"
"math/big"
"strings"
"testing"
"time"
"github.com/GMWalletApp/epusdt/model/mdb"
addressutil "github.com/GMWalletApp/epusdt/util/address"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/ton/jetton"
"github.com/xssnick/tonutils-go/tvm/cell"
)
func tonTestInboundTx(t *testing.T, src, dst *address.Address, amount tlb.Coins, body *cell.Cell) *tlb.Transaction {
t.Helper()
return &tlb.Transaction{
LT: 123,
Now: uint32(time.Now().Unix()),
Hash: bytes.Repeat([]byte{0x11}, 32),
IO: struct {
In *tlb.Message `tlb:"maybe ^"`
Out *tlb.MessagesList `tlb:"maybe ^"`
}{
In: &tlb.Message{
MsgType: tlb.MsgTypeInternal,
Msg: &tlb.InternalMessage{
SrcAddr: src,
DstAddr: dst,
Amount: amount,
Body: body,
},
},
},
}
}
func TestParseTonInboundTransferNativeTON(t *testing.T) {
receive := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
sender := address.NewAddress(0, 0, bytes.Repeat([]byte{0x22}, 32)).Bounce(false).Testnet(false)
native := &mdb.ChainToken{BaseModel: mdb.BaseModel{ID: 1}, Network: mdb.NetworkTon, Symbol: "TON", Decimals: 9, Enabled: true}
tx := tonTestInboundTx(t, sender, receive, tlb.MustFromTON("1.23"), cell.BeginCell().EndCell())
transfer, err := ParseTonInboundTransfer(tx, receive, native, nil)
if err != nil {
t.Fatalf("ParseTonInboundTransfer(): %v", err)
}
if transfer == nil {
t.Fatal("expected native TON transfer")
}
if transfer.Token.Symbol != "TON" {
t.Fatalf("token = %q, want TON", transfer.Token.Symbol)
}
if transfer.Amount != 1.23 {
t.Fatalf("amount = %v, want 1.23", transfer.Amount)
}
if transfer.ReceiveAddress != addressutil.NormalizeTonAddressObject(receive) {
t.Fatalf("receive address = %q", transfer.ReceiveAddress)
}
if !strings.HasPrefix(transfer.BlockID, "ton:"+addressutil.TonRawAddressObjectKey(receive)+":123:") {
t.Fatalf("block id = %q, want canonical TON prefix", transfer.BlockID)
}
}
func TestParseTonInboundTransferJettonNotification(t *testing.T) {
receive := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
sender := address.NewAddress(0, 0, bytes.Repeat([]byte{0x33}, 32)).Bounce(false).Testnet(false)
jettonWallet := address.NewAddress(0, 0, bytes.Repeat([]byte{0x44}, 32)).Bounce(false).Testnet(false)
native := &mdb.ChainToken{BaseModel: mdb.BaseModel{ID: 1}, Network: mdb.NetworkTon, Symbol: "TON", Decimals: 9, Enabled: true}
usdt := mdb.ChainToken{BaseModel: mdb.BaseModel{ID: 2}, Network: mdb.NetworkTon, Symbol: "USDT", Decimals: 6, Enabled: true}
body, err := tlb.ToCell(jetton.TransferNotification{
QueryID: 7,
Amount: tlb.MustFromNano(big.NewInt(1230000), 6),
Sender: sender,
ForwardPayload: cell.BeginCell().EndCell(),
})
if err != nil {
t.Fatalf("build jetton notification body: %v", err)
}
tx := tonTestInboundTx(t, jettonWallet, receive, tlb.FromNanoTONU(1), body)
transfer, err := ParseTonInboundTransfer(tx, receive, native, map[string]mdb.ChainToken{
addressutil.TonRawAddressObjectKey(jettonWallet): usdt,
})
if err != nil {
t.Fatalf("ParseTonInboundTransfer(): %v", err)
}
if transfer == nil {
t.Fatal("expected USDT jetton transfer")
}
if transfer.Token.Symbol != "USDT" {
t.Fatalf("token = %q, want USDT", transfer.Token.Symbol)
}
if transfer.Amount != 1.23 {
t.Fatalf("amount = %v, want 1.23", transfer.Amount)
}
if transfer.SenderAddress != addressutil.NormalizeTonAddressObject(sender) {
t.Fatalf("sender address = %q", transfer.SenderAddress)
}
if transfer.JettonWallet != addressutil.NormalizeTonAddressObject(jettonWallet) {
t.Fatalf("jetton wallet = %q", transfer.JettonWallet)
}
}
func TestParseTonInboundTransferJettonNotificationWithNoGasComputeSkip(t *testing.T) {
receive := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
sender := address.NewAddress(0, 0, bytes.Repeat([]byte{0x33}, 32)).Bounce(false).Testnet(false)
jettonWallet := address.NewAddress(0, 0, bytes.Repeat([]byte{0x44}, 32)).Bounce(false).Testnet(false)
usdt := mdb.ChainToken{BaseModel: mdb.BaseModel{ID: 2}, Network: mdb.NetworkTon, Symbol: "USDT", Decimals: 6, Enabled: true}
body, err := tlb.ToCell(jetton.TransferNotification{
QueryID: 7,
Amount: tlb.MustFromNano(big.NewInt(150000), 6),
Sender: sender,
ForwardPayload: cell.BeginCell().EndCell(),
})
if err != nil {
t.Fatalf("build jetton notification body: %v", err)
}
tx := tonTestInboundTx(t, jettonWallet, receive, tlb.FromNanoTONU(1), body)
tx.Description = tlb.TransactionDescriptionOrdinary{
ComputePhase: tlb.ComputePhase{Phase: tlb.ComputePhaseSkipped{
Reason: tlb.ComputeSkipReason{Type: tlb.ComputeSkipReasonNoGas},
}},
Aborted: true,
}
transfer, err := ParseTonInboundTransfer(tx, receive, nil, map[string]mdb.ChainToken{
addressutil.TonRawAddressObjectKey(jettonWallet): usdt,
})
if err != nil {
t.Fatalf("ParseTonInboundTransfer(): %v", err)
}
if transfer == nil {
t.Fatal("expected no-gas jetton notification to be accepted")
}
if transfer.Token.Symbol != "USDT" {
t.Fatalf("token = %q, want USDT", transfer.Token.Symbol)
}
if transfer.Amount != 0.15 {
t.Fatalf("amount = %v, want 0.15", transfer.Amount)
}
}
func TestParseTonInboundTransferDoesNotCountUntrustedJettonNotificationAsTON(t *testing.T) {
receive := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
sender := address.NewAddress(0, 0, bytes.Repeat([]byte{0x33}, 32)).Bounce(false).Testnet(false)
untrustedJettonWallet := address.NewAddress(0, 0, bytes.Repeat([]byte{0x55}, 32)).Bounce(false).Testnet(false)
native := &mdb.ChainToken{BaseModel: mdb.BaseModel{ID: 1}, Network: mdb.NetworkTon, Symbol: "TON", Decimals: 9, Enabled: true}
body, err := tlb.ToCell(jetton.TransferNotification{
QueryID: 7,
Amount: tlb.MustFromNano(big.NewInt(1230000), 6),
Sender: sender,
ForwardPayload: cell.BeginCell().EndCell(),
})
if err != nil {
t.Fatalf("build jetton notification body: %v", err)
}
tx := tonTestInboundTx(t, untrustedJettonWallet, receive, tlb.FromNanoTONU(1), body)
transfer, err := ParseTonInboundTransfer(tx, receive, native, nil)
if err != nil {
t.Fatalf("ParseTonInboundTransfer(): %v", err)
}
if transfer != nil {
t.Fatalf("untrusted jetton notification produced transfer: %#v", transfer)
}
}
+85
View File
@@ -18,6 +18,7 @@ import (
"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/constant"
appLog "github.com/GMWalletApp/epusdt/util/log"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@@ -1441,6 +1442,90 @@ func TestAdminSettings_AmountPrecisionValidationAndListing(t *testing.T) {
} }
} }
func TestAdminSettings_LogLevelValidationRuntimeUpdateAndReset(t *testing.T) {
e, token := setupAdminTestEnv(t)
oldLevel := appLog.CurrentLevel()
t.Cleanup(func() {
_ = appLog.SetLevel(oldLevel)
})
if err := appLog.SetLevel(mdb.SettingDefaultSystemLogLevel); err != nil {
t.Fatalf("reset log level: %v", err)
}
rec := doPutAdmin(e, "/admin/api/v1/settings", map[string]interface{}{
"items": []map[string]interface{}{
{"group": "system", "key": mdb.SettingKeySystemLogLevel, "value": " DEBUG ", "type": "string"},
},
}, token)
resp := assertOK(t, rec)
results, ok := resp["data"].([]interface{})
if !ok || len(results) != 1 {
t.Fatalf("expected one result, got %T %v", resp["data"], resp["data"])
}
result, _ := results[0].(map[string]interface{})
if result["ok"] != true {
t.Fatalf("system.log_level upsert result = %v", result)
}
if got := appLog.CurrentLevel(); got != "debug" {
t.Fatalf("runtime log level = %q, want debug", got)
}
if got := data.GetSettingString(mdb.SettingKeySystemLogLevel, ""); got != "debug" {
t.Fatalf("stored system.log_level = %q, want debug", got)
}
rec = doPutAdmin(e, "/admin/api/v1/settings", map[string]interface{}{
"items": []map[string]interface{}{
{"group": "system", "key": mdb.SettingKeySystemLogLevel, "value": "fatal", "type": "string"},
{"group": "rate", "key": mdb.SettingKeySystemLogLevel, "value": "info", "type": "string"},
{"group": "system", "key": mdb.SettingKeySystemLogLevel, "value": "", "type": "string"},
},
}, token)
resp = assertOK(t, rec)
results, ok = resp["data"].([]interface{})
if !ok || len(results) != 3 {
t.Fatalf("expected three results, got %T %v", resp["data"], resp["data"])
}
for _, item := range results {
result, _ := item.(map[string]interface{})
if result["ok"] != false {
t.Fatalf("invalid system.log_level result = %v, want ok=false", result)
}
}
if got := appLog.CurrentLevel(); got != "debug" {
t.Fatalf("invalid upsert changed runtime log level to %q", got)
}
rec = doDeleteAdmin(e, "/admin/api/v1/settings/"+mdb.SettingKeySystemLogLevel, token)
assertOK(t, rec)
if got := appLog.CurrentLevel(); got != mdb.SettingDefaultSystemLogLevel {
t.Fatalf("runtime log level after delete = %q, want %q", got, mdb.SettingDefaultSystemLogLevel)
}
if got := data.GetSettingString(mdb.SettingKeySystemLogLevel, mdb.SettingDefaultSystemLogLevel); got != mdb.SettingDefaultSystemLogLevel {
t.Fatalf("deleted system.log_level fallback = %q, want %q", got, mdb.SettingDefaultSystemLogLevel)
}
rec = doPutAdmin(e, "/admin/api/v1/settings", map[string]interface{}{
"items": []map[string]interface{}{
{"group": "system", "key": mdb.SettingKeySystemLogLevel, "value": "warn", "type": "string"},
},
}, token)
assertOK(t, rec)
if got := appLog.CurrentLevel(); got != "warn" {
t.Fatalf("runtime log level after restore = %q, want warn", got)
}
var restored mdb.Setting
if err := dao.Mdb.Unscoped().Where("`key` = ?", mdb.SettingKeySystemLogLevel).Take(&restored).Error; err != nil {
t.Fatalf("load restored system.log_level unscoped: %v", err)
}
if restored.DeletedAt.Valid {
t.Fatalf("restored system.log_level still has deleted_at=%v", restored.DeletedAt)
}
if restored.Value != "warn" {
t.Fatalf("restored system.log_level value = %q, want warn", restored.Value)
}
}
// TestAdminSettings_DeleteNonExistent verifies deleting a non-existent setting. // TestAdminSettings_DeleteNonExistent verifies deleting a non-existent setting.
func TestAdminSettings_DeleteNonExistent(t *testing.T) { func TestAdminSettings_DeleteNonExistent(t *testing.T) {
e, token := setupAdminTestEnv(t) e, token := setupAdminTestEnv(t)
+7 -1
View File
@@ -26,6 +26,7 @@ import (
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/spf13/viper" "github.com/spf13/viper"
"go.uber.org/zap"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
@@ -46,8 +47,13 @@ func setupTestEnv(t *testing.T) *echo.Echo {
viper.Set("log_save_path", tmpDir) viper.Set("log_save_path", tmpDir)
viper.Set("sqlite_database_filename", tmpDir+"/test.db") viper.Set("sqlite_database_filename", tmpDir+"/test.db")
viper.Set("runtime_sqlite_filename", tmpDir+"/runtime.db") viper.Set("runtime_sqlite_filename", tmpDir+"/runtime.db")
config.LogLevel = "info"
config.LogSavePath = tmpDir
log.Init() log.Sugar = zap.NewNop().Sugar()
if err := log.SetLevel(config.LogLevel); err != nil {
t.Fatalf("set test log level: %v", err)
}
// init config paths // init config paths
os.Setenv("EPUSDT_CONFIG", tmpDir) os.Setenv("EPUSDT_CONFIG", tmpDir)
+1
View File
@@ -14,6 +14,7 @@ func Start() {
go StartPolygonWebSocketListener() go StartPolygonWebSocketListener()
go StartPlasmaWebSocketListener() go StartPlasmaWebSocketListener()
go StartTronBlockScannerListener() go StartTronBlockScannerListener()
go StartTonBlockScannerListener()
c := cron.New() c := cron.New()
+574
View File
@@ -0,0 +1,574 @@
package task
import (
"context"
"encoding/hex"
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/model/service"
addressutil "github.com/GMWalletApp/epusdt/util/address"
"github.com/GMWalletApp/epusdt/util/log"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/ton/jetton"
)
const (
tonMasterShard = int64(-0x8000000000000000)
tonScannerConnectTimeout = 20 * time.Second
tonScannerRequestTimeout = 15 * time.Second
tonScannerRetryDelay = 3 * time.Second
tonCatchupYield = 250 * time.Millisecond
tonMaxMasterBlocksTick = uint32(50)
tonBlockTxPageSize = uint32(256)
)
type tonWatchState struct {
wallets map[string]*address.Address
nativeToken *mdb.ChainToken
jettonTokens []mdb.ChainToken
}
func (s *tonWatchState) hasWalletInWorkchain(workchain int32) bool {
if s == nil {
return false
}
for _, wallet := range s.wallets {
if wallet != nil && wallet.Workchain() == workchain {
return true
}
}
return false
}
type tonRuntimeCursor struct {
initialized bool
lastMasterSeqNo uint32
shardSeqNo map[string]uint32
}
func (c *tonRuntimeCursor) ensureShardSeqNo() {
if c.shardSeqNo == nil {
c.shardSeqNo = make(map[string]uint32)
}
}
type tonJettonWalletCache struct {
sync.Mutex
walletByOwnerToken map[string]string
}
func newTonJettonWalletCache() *tonJettonWalletCache {
return &tonJettonWalletCache{walletByOwnerToken: make(map[string]string)}
}
// StartTonBlockScannerListener runs a forward-only TON masterchain scanner.
func StartTonBlockScannerListener() {
cache := newTonJettonWalletCache()
cursor := &tonRuntimeCursor{}
for {
chain, interval := tonChainConfig()
if chain == nil || !chain.Enabled {
sleepOrDone(context.Background(), interval)
continue
}
node, ok := resolveTonLiteNode()
if !ok {
sleepOrDone(context.Background(), interval)
continue
}
log.Sugar.Infof("[TON] connecting using lite node %s", data.RpcNodeLogLabel(node))
api, closeFn, err := connectTonLiteAPI(node)
if err != nil {
log.Sugar.Warnf("[TON] connect lite node %s failed: %v", data.RpcNodeLogLabel(node), err)
recordTonLiteNodeFailure(node, "connect")
sleepOrDone(context.Background(), tonScannerRetryDelay)
continue
}
log.Sugar.Infof("[TON] connected using lite node %s", data.RpcNodeLogLabel(node))
err = runTonScanner(context.Background(), api, cache, cursor)
closeFn()
if err != nil {
log.Sugar.Warnf("[TON] scanner stopped: %v", err)
recordTonLiteNodeFailure(node, err.Error())
sleepOrDone(context.Background(), tonScannerRetryDelay)
continue
}
}
}
func runTonScanner(ctx context.Context, api ton.APIClientWrapped, cache *tonJettonWalletCache, cursor *tonRuntimeCursor) error {
if cursor == nil {
cursor = &tonRuntimeCursor{}
}
watchSignature := ""
for {
chain, interval := tonChainConfig()
if chain == nil || !chain.Enabled {
return nil
}
state, err := loadTonWatchState()
if err != nil {
return err
}
if len(state.wallets) == 0 || (state.nativeToken == nil && len(state.jettonTokens) == 0) {
log.Sugar.Debug("[TON] no enabled wallets or tokens, idling")
if !sleepOrDone(ctx, interval) {
return nil
}
continue
}
if sig := tonWatchStateSignature(state); sig != watchSignature {
watchSignature = sig
log.Sugar.Infof(
"[TON] watching wallets=%d native_ton=%t jetton_tokens=%d wallet_workchains=%s",
len(state.wallets),
state.nativeToken != nil,
len(state.jettonTokens),
tonWalletWorkchainSummary(state),
)
}
head, err := api.CurrentMasterchainInfo(ctx)
if err != nil {
data.RecordRpcFailure(mdb.NetworkTon)
return fmt.Errorf("current masterchain info: %w", err)
}
confirmedHeadSeqNo := tonConfirmedMasterSeqNo(head.SeqNo, chain.MinConfirmations)
data.RecordRpcSuccess(mdb.NetworkTon)
data.RecordRpcBlockHeight(mdb.NetworkTon, int64(head.SeqNo))
if !cursor.initialized {
if err = initializeTonRuntimeCursor(ctx, api, head, confirmedHeadSeqNo, cursor); err != nil {
data.RecordRpcFailure(mdb.NetworkTon)
return err
}
log.Sugar.Infof(
"[TON] current masterchain head=%d confirmed=%d root=%s file=%s",
head.SeqNo,
confirmedHeadSeqNo,
shortTonHash(head.RootHash),
shortTonHash(head.FileHash),
)
log.Sugar.Infof("[TON] initialized runtime cursor at masterchain seqno=%d head=%d", cursor.lastMasterSeqNo, head.SeqNo)
if !sleepOrDone(ctx, interval) {
return nil
}
continue
}
if cursor.lastMasterSeqNo >= confirmedHeadSeqNo {
if !sleepOrDone(ctx, interval) {
return nil
}
continue
}
target := cursor.lastMasterSeqNo + tonMaxMasterBlocksTick
if target > confirmedHeadSeqNo {
target = confirmedHeadSeqNo
}
log.Sugar.Infof("[TON] processing masterchain blocks from=%d to=%d head=%d confirmed=%d", cursor.lastMasterSeqNo+1, target, head.SeqNo, confirmedHeadSeqNo)
for seq := cursor.lastMasterSeqNo + 1; seq <= target; seq++ {
master, err := api.WaitForBlock(seq).LookupBlock(ctx, address.MasterchainID, tonMasterShard, seq)
if err != nil {
data.RecordRpcFailure(mdb.NetworkTon)
return fmt.Errorf("lookup masterchain seqno=%d: %w", seq, err)
}
if err = processTonMasterBlockWithCursor(ctx, api, master, state, cache, cursor); err != nil {
data.RecordRpcFailure(mdb.NetworkTon)
return fmt.Errorf("process masterchain seqno=%d: %w", seq, err)
}
cursor.lastMasterSeqNo = master.SeqNo
data.RecordRpcSuccess(mdb.NetworkTon)
data.RecordRpcBlockHeight(mdb.NetworkTon, int64(seq))
}
if target < confirmedHeadSeqNo {
if !sleepOrDone(ctx, tonCatchupYield) {
return nil
}
}
}
}
func tonConfirmedMasterSeqNo(headSeqNo uint32, minConfirmations int) uint32 {
if minConfirmations <= 1 {
return headSeqNo
}
conf := uint32(minConfirmations)
if headSeqNo < conf-1 {
return 0
}
return headSeqNo - conf + 1
}
func initializeTonRuntimeCursor(ctx context.Context, api ton.APIClientWrapped, head *ton.BlockIDExt, confirmedHeadSeqNo uint32, cursor *tonRuntimeCursor) error {
if cursor == nil {
return nil
}
if head == nil {
return fmt.Errorf("current masterchain head is nil")
}
initMaster := head
if confirmedHeadSeqNo > 0 && confirmedHeadSeqNo != head.SeqNo {
var err error
initMaster, err = api.WaitForBlock(confirmedHeadSeqNo).LookupBlock(ctx, address.MasterchainID, tonMasterShard, confirmedHeadSeqNo)
if err != nil {
return fmt.Errorf("lookup initial confirmed masterchain seqno=%d: %w", confirmedHeadSeqNo, err)
}
}
if confirmedHeadSeqNo > 0 {
if err := initializeTonShardCursors(ctx, api, initMaster, cursor); err != nil {
return fmt.Errorf("initialize shard cursors masterchain seqno=%d: %w", initMaster.SeqNo, err)
}
}
cursor.initialized = true
cursor.lastMasterSeqNo = confirmedHeadSeqNo
return nil
}
func processTonMasterBlock(ctx context.Context, api ton.APIClientWrapped, master *ton.BlockIDExt, state *tonWatchState, cache *tonJettonWalletCache) error {
return processTonMasterBlockWithCursor(ctx, api, master, state, cache, nil)
}
func processTonMasterBlockWithCursor(ctx context.Context, api ton.APIClientWrapped, master *ton.BlockIDExt, state *tonWatchState, cache *tonJettonWalletCache, cursor *tonRuntimeCursor) error {
if state.hasWalletInWorkchain(address.MasterchainID) {
if err := processTonShardBlock(ctx, api, master, master, state, cache); err != nil {
return fmt.Errorf("process masterchain transactions: %w", err)
}
}
shards, err := api.WaitForBlock(master.SeqNo).GetBlockShardsInfo(ctx, master)
if err != nil {
return err
}
log.Sugar.Debugf(
"[TON] masterchain seqno=%d root=%s file=%s shard_blocks=%d workchains=%s",
master.SeqNo,
shortTonHash(master.RootHash),
shortTonHash(master.FileHash),
len(shards),
tonShardWorkchainSummary(shards),
)
if cursor != nil {
return processTonShardBlocksWithCursor(ctx, api, master, shards, state, cache, cursor)
}
for _, shard := range shards {
if err = processTonShardBlock(ctx, api, master, shard, state, cache); err != nil {
return err
}
}
return nil
}
func initializeTonShardCursors(ctx context.Context, api ton.APIClientWrapped, master *ton.BlockIDExt, cursor *tonRuntimeCursor) error {
if cursor == nil || master == nil {
return nil
}
shards, err := api.WaitForBlock(master.SeqNo).GetBlockShardsInfo(ctx, master)
if err != nil {
return err
}
cursor.ensureShardSeqNo()
for _, shard := range shards {
if shard == nil {
continue
}
cursor.shardSeqNo[tonShardCursorKey(shard)] = shard.SeqNo
}
log.Sugar.Infof(
"[TON] initialized shard cursors masterchain seqno=%d shard_blocks=%d workchains=%s",
master.SeqNo,
len(shards),
tonShardWorkchainSummary(shards),
)
return nil
}
func processTonShardBlocksWithCursor(ctx context.Context, api ton.APIClientWrapped, master *ton.BlockIDExt, shards []*ton.BlockIDExt, state *tonWatchState, cache *tonJettonWalletCache, cursor *tonRuntimeCursor) error {
if cursor == nil {
return nil
}
cursor.ensureShardSeqNo()
for _, shard := range shards {
if shard == nil {
continue
}
key := tonShardCursorKey(shard)
lastSeqNo, ok := cursor.shardSeqNo[key]
if !state.hasWalletInWorkchain(shard.Workchain) {
cursor.shardSeqNo[key] = shard.SeqNo
continue
}
start := shard.SeqNo
if ok {
if shard.SeqNo <= lastSeqNo {
continue
}
start = lastSeqNo + 1
}
if shard.SeqNo > start {
log.Sugar.Infof(
"[TON] processing shard blocks master_seqno=%d workchain=%d shard=%x from=%d to=%d",
master.SeqNo,
shard.Workchain,
uint64(shard.Shard),
start,
shard.SeqNo,
)
}
for seq := start; seq <= shard.SeqNo; seq++ {
shardBlock := shard
if seq != shard.SeqNo {
var err error
shardBlock, err = api.WaitForBlock(master.SeqNo).LookupBlock(ctx, shard.Workchain, shard.Shard, seq)
if err != nil {
return fmt.Errorf("lookup shard block workchain=%d shard=%x seqno=%d: %w", shard.Workchain, uint64(shard.Shard), seq, err)
}
}
if err := processTonShardBlock(ctx, api, master, shardBlock, state, cache); err != nil {
return err
}
cursor.shardSeqNo[key] = seq
}
}
return nil
}
func processTonShardBlock(ctx context.Context, api ton.APIClientWrapped, master, shard *ton.BlockIDExt, state *tonWatchState, cache *tonJettonWalletCache) error {
log.Sugar.Debugf("[TON] scanning shard master_seqno=%d workchain=%d shard=%d shard_seqno=%d", master.SeqNo, shard.Workchain, shard.Shard, shard.SeqNo)
var after *ton.TransactionID3
for {
shorts, incomplete, err := api.WaitForBlock(master.SeqNo).GetBlockTransactionsV2(ctx, shard, tonBlockTxPageSize, after)
if err != nil {
return err
}
for i := range shorts {
account := addressutil.TonAddressFromShortAccount(shard.Workchain, shorts[i].Account)
if account == nil {
continue
}
receive, ok := state.wallets[addressutil.TonRawAddressObjectKey(account)]
if !ok {
continue
}
log.Sugar.Debugf("[TON] watched account tx candidate master_seqno=%d workchain=%d shard=%d lt=%d account=%s", master.SeqNo, shard.Workchain, shard.Shard, shorts[i].LT, addressutil.TonRawAddressObjectKey(account))
tx, err := api.WaitForBlock(master.SeqNo).GetTransaction(ctx, shard, account, shorts[i].LT)
if err != nil {
return err
}
if tx == nil {
return fmt.Errorf("missing full transaction workchain=%d shard=%d seqno=%d lt=%d", shard.Workchain, shard.Shard, shard.SeqNo, shorts[i].LT)
}
jettonWallets, err := cache.jettonWalletTokens(ctx, api, master, receive, state.jettonTokens)
if err != nil {
return err
}
transfer, err := service.ParseTonInboundTransfer(tx, receive, state.nativeToken, jettonWallets)
if err != nil {
log.Sugar.Warnf("[TON] parse inbound tx lt=%d hash=%s: %v", tx.LT, hex.EncodeToString(tx.Hash), err)
continue
}
if transfer == nil {
log.Sugar.Debugf(
"[TON] unsupported watched tx master_seqno=%d workchain=%d shard=%d lt=%d hash=%s account=%s",
master.SeqNo,
shard.Workchain,
shard.Shard,
tx.LT,
hex.EncodeToString(tx.Hash),
addressutil.TonRawAddressObjectKey(account),
)
continue
}
service.TryProcessTonTransfer(transfer)
}
if !incomplete {
return nil
}
if len(shorts) == 0 {
return fmt.Errorf("incomplete transaction page without cursor progress workchain=%d shard=%d seqno=%d", shard.Workchain, shard.Shard, shard.SeqNo)
}
after = shorts[len(shorts)-1].ID3()
}
}
func (c *tonJettonWalletCache) jettonWalletTokens(ctx context.Context, api ton.APIClientWrapped, master *ton.BlockIDExt, owner *address.Address, tokens []mdb.ChainToken) (map[string]mdb.ChainToken, error) {
out := make(map[string]mdb.ChainToken, len(tokens))
ownerRaw := addressutil.TonRawAddressObjectKey(owner)
for _, token := range tokens {
masterParsed, err := addressutil.ParseTonMainnetAddress(token.ContractAddress)
if err != nil {
return nil, fmt.Errorf("invalid jetton master symbol=%s address=%s: %w", token.Symbol, token.ContractAddress, err)
}
cacheKey := masterParsed.Raw + "|" + ownerRaw
c.Lock()
walletRaw, ok := c.walletByOwnerToken[cacheKey]
c.Unlock()
if !ok {
wallet, err := jetton.NewJettonMasterClient(api, masterParsed.Address).GetJettonWalletAtBlock(ctx, owner, master)
if err != nil {
return nil, fmt.Errorf("derive jetton wallet symbol=%s owner=%s: %w", token.Symbol, ownerRaw, err)
}
walletRaw = addressutil.TonRawAddressObjectKey(wallet.Address())
c.Lock()
c.walletByOwnerToken[cacheKey] = walletRaw
c.Unlock()
}
out[walletRaw] = token
}
return out, nil
}
func loadTonWatchState() (*tonWatchState, error) {
walletRows, err := data.GetAvailableWalletAddressByNetwork(mdb.NetworkTon)
if err != nil {
return nil, err
}
tokens, err := data.ListEnabledChainTokensByNetwork(mdb.NetworkTon)
if err != nil {
return nil, err
}
state := &tonWatchState{wallets: make(map[string]*address.Address)}
for _, row := range walletRows {
parsed, err := addressutil.ParseTonMainnetAddress(row.Address)
if err != nil {
log.Sugar.Warnf("[TON] skip invalid wallet address=%s err=%v", row.Address, err)
continue
}
state.wallets[parsed.Raw] = parsed.Address
}
for i := range tokens {
sym := strings.ToUpper(strings.TrimSpace(tokens[i].Symbol))
contract := strings.TrimSpace(tokens[i].ContractAddress)
switch {
case sym == service.TonNativeSymbol && contract == "":
token := tokens[i]
state.nativeToken = &token
case contract != "":
state.jettonTokens = append(state.jettonTokens, tokens[i])
}
}
return state, nil
}
func tonChainConfig() (*mdb.Chain, time.Duration) {
row, err := data.GetChainByNetwork(mdb.NetworkTon)
if err != nil || row.ID == 0 {
return nil, 5 * time.Second
}
interval := time.Duration(row.ScanIntervalSec) * time.Second
if interval <= 0 {
interval = 5 * time.Second
}
return row, interval
}
func tonWatchStateSignature(state *tonWatchState) string {
if state == nil {
return ""
}
return fmt.Sprintf("%d|%t|%d|%s", len(state.wallets), state.nativeToken != nil, len(state.jettonTokens), tonWalletWorkchainSummary(state))
}
func tonWalletWorkchainSummary(state *tonWatchState) string {
if state == nil || len(state.wallets) == 0 {
return "none"
}
counts := make(map[int32]int)
for _, wallet := range state.wallets {
if wallet == nil {
continue
}
counts[wallet.Workchain()]++
}
return formatTonWorkchainCounts(counts)
}
func tonShardWorkchainSummary(shards []*ton.BlockIDExt) string {
if len(shards) == 0 {
return "none"
}
counts := make(map[int32]int)
for _, shard := range shards {
if shard == nil {
continue
}
counts[shard.Workchain]++
}
return formatTonWorkchainCounts(counts)
}
func tonShardCursorKey(shard *ton.BlockIDExt) string {
if shard == nil {
return ""
}
return fmt.Sprintf("%d:%d", shard.Workchain, shard.Shard)
}
func formatTonWorkchainCounts(counts map[int32]int) string {
if len(counts) == 0 {
return "none"
}
workchains := make([]int, 0, len(counts))
for workchain := range counts {
workchains = append(workchains, int(workchain))
}
sort.Ints(workchains)
parts := make([]string, 0, len(workchains))
for _, workchain := range workchains {
parts = append(parts, fmt.Sprintf("%d:%d", workchain, counts[int32(workchain)]))
}
return strings.Join(parts, ",")
}
func shortTonHash(raw []byte) string {
hash := hex.EncodeToString(raw)
if len(hash) > 16 {
return hash[:16]
}
if hash == "" {
return "-"
}
return hash
}
func resolveTonLiteNode(excludeIDs ...uint64) (mdb.RpcNode, bool) {
node, err := data.SelectGeneralRpcNode(mdb.NetworkTon, mdb.RpcNodeTypeLite, excludeIDs...)
if err == nil && node != nil && node.ID > 0 {
node.Url = strings.TrimSpace(node.Url)
if node.Url != "" {
return *node, true
}
log.Sugar.Errorf("[TON] rpc_nodes id=%d has empty url", node.ID)
return mdb.RpcNode{}, false
}
if err != nil {
log.Sugar.Errorf("[TON] resolve lite rpc_nodes err=%v", err)
} else {
log.Sugar.Warn("[TON] no enabled lite RPC node configured in rpc_nodes")
}
return mdb.RpcNode{}, false
}
func connectTonLiteAPI(node mdb.RpcNode) (ton.APIClientWrapped, func(), error) {
ctx, cancel := context.WithTimeout(context.Background(), tonScannerConnectTimeout)
defer cancel()
return service.ConnectTonLiteAPI(ctx, node.Url, tonScannerRequestTimeout, 5)
}
func recordTonLiteNodeFailure(node mdb.RpcNode, reason string) {
data.RecordRpcFailure(mdb.NetworkTon)
failures, cooling := data.RecordRpcNodeFailure(node.ID)
nodeLabel := data.RpcNodeLogLabel(node)
if cooling {
log.Sugar.Warnf("[TON] lite node reached fail threshold (%s), node=%s failures=%d/%d", reason, nodeLabel, failures, data.RpcFailoverThreshold)
return
}
log.Sugar.Warnf("[TON] lite node failed (%s), node=%s failures=%d/%d", reason, nodeLabel, failures, data.RpcFailoverThreshold)
}
+717
View File
@@ -0,0 +1,717 @@
package task
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"math/big"
"testing"
"time"
"github.com/GMWalletApp/epusdt/internal/testutil"
"github.com/GMWalletApp/epusdt/model/dao"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/model/request"
"github.com/GMWalletApp/epusdt/model/service"
addressutil "github.com/GMWalletApp/epusdt/util/address"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/ton/jetton"
"github.com/xssnick/tonutils-go/tvm/cell"
)
type fakeTonShardAPI struct {
ton.APIClientWrapped
shorts []ton.TransactionShortInfo
shortsByShardSeq map[uint32][]ton.TransactionShortInfo
incomplete bool
shards []*ton.BlockIDExt
shardsErr error
lookupBlocks map[uint32]*ton.BlockIDExt
lookupSeqs []uint32
txByLT map[uint64]*tlb.Transaction
fetchedLTs []uint64
fetchedAddrs []string
fetchedShardSeqNos []uint32
}
type fakeTonScannerAPI struct {
ton.APIClientWrapped
head *ton.BlockIDExt
shards []*ton.BlockIDExt
shardsErr error
lookups []uint32
cancel context.CancelFunc
cancelAtLookupSeq uint32
}
func (f *fakeTonScannerAPI) CurrentMasterchainInfo(context.Context) (*ton.BlockIDExt, error) {
return f.head, nil
}
func (f *fakeTonScannerAPI) WaitForBlock(uint32) ton.APIClientWrapped {
return f
}
func (f *fakeTonScannerAPI) LookupBlock(_ context.Context, workchain int32, shard int64, seqno uint32) (*ton.BlockIDExt, error) {
f.lookups = append(f.lookups, seqno)
if f.cancel != nil && f.cancelAtLookupSeq == seqno {
f.cancel()
}
return &ton.BlockIDExt{
Workchain: workchain,
Shard: shard,
SeqNo: seqno,
RootHash: bytes.Repeat([]byte{byte(seqno)}, 32),
FileHash: bytes.Repeat([]byte{byte(seqno + 1)}, 32),
}, nil
}
func (f *fakeTonScannerAPI) GetBlockShardsInfo(context.Context, *ton.BlockIDExt) ([]*ton.BlockIDExt, error) {
if f.shardsErr != nil {
return nil, f.shardsErr
}
return f.shards, nil
}
func (f *fakeTonShardAPI) WaitForBlock(uint32) ton.APIClientWrapped {
return f
}
func (f *fakeTonShardAPI) GetBlockTransactionsV2(_ context.Context, block *ton.BlockIDExt, _ uint32, _ ...*ton.TransactionID3) ([]ton.TransactionShortInfo, bool, error) {
if block != nil {
f.fetchedShardSeqNos = append(f.fetchedShardSeqNos, block.SeqNo)
}
return f.blockShorts(block), f.incomplete, nil
}
func (f *fakeTonShardAPI) blockShorts(block *ton.BlockIDExt) []ton.TransactionShortInfo {
if block != nil && f.shortsByShardSeq != nil {
if shorts, ok := f.shortsByShardSeq[block.SeqNo]; ok {
return shorts
}
}
return f.shorts
}
func (f *fakeTonShardAPI) GetBlockShardsInfo(context.Context, *ton.BlockIDExt) ([]*ton.BlockIDExt, error) {
if f.shardsErr != nil {
return nil, f.shardsErr
}
return f.shards, nil
}
func (f *fakeTonShardAPI) LookupBlock(_ context.Context, workchain int32, shard int64, seqno uint32) (*ton.BlockIDExt, error) {
f.lookupSeqs = append(f.lookupSeqs, seqno)
if f.lookupBlocks != nil {
if block, ok := f.lookupBlocks[seqno]; ok {
return block, nil
}
}
return &ton.BlockIDExt{Workchain: workchain, Shard: shard, SeqNo: seqno}, nil
}
func (f *fakeTonShardAPI) GetTransaction(_ context.Context, _ *ton.BlockIDExt, addr *address.Address, lt uint64) (*tlb.Transaction, error) {
f.fetchedLTs = append(f.fetchedLTs, lt)
f.fetchedAddrs = append(f.fetchedAddrs, addressutil.TonRawAddressObjectKey(addr))
tx := f.txByLT[lt]
if tx == nil {
return nil, fmt.Errorf("missing tx lt=%d", lt)
}
return tx, nil
}
func seedTonScannerWallet(t *testing.T) {
t.Helper()
if _, err := data.AddWalletAddressWithNetwork(mdb.NetworkTon, "EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I"); err != nil {
t.Fatalf("seed TON wallet: %v", err)
}
}
func TestResolveTonLiteNodeSkipsCoolingPrimary(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
data.ResetRpcFailoverForTest()
t.Cleanup(data.ResetRpcFailoverForTest)
rows := []mdb.RpcNode{
{Network: mdb.NetworkTon, Url: " https://primary-ton.example.com/global.config.json ", Type: mdb.RpcNodeTypeLite, Weight: 100, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkTon, Url: "https://backup-ton.example.com/global.config.json", Type: mdb.RpcNodeTypeLite, Weight: 1, Enabled: true, Purpose: mdb.RpcNodePurposeGeneral, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkTon, Url: "https://manual-ton.example.com/global.config.json", Type: mdb.RpcNodeTypeLite, Weight: 1000, Enabled: true, Purpose: mdb.RpcNodePurposeManualVerify, Status: mdb.RpcNodeStatusOk},
{Network: mdb.NetworkTon, Url: "https://http-ton.example.com", Type: mdb.RpcNodeTypeHttp, Weight: 2000, 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 := resolveTonLiteNode()
if !ok {
t.Fatalf("resolveTonLiteNode() ok=false, want true")
}
if primary.Url != "https://primary-ton.example.com/global.config.json" {
t.Fatalf("primary node = %#v, want trimmed primary lite node", primary)
}
for i := 0; i < data.RpcFailoverThreshold; i++ {
data.RecordRpcNodeFailure(primary.ID)
}
got, ok := resolveTonLiteNode()
if !ok {
t.Fatalf("resolveTonLiteNode() after cooldown ok=false, want true")
}
if got.Url != "https://backup-ton.example.com/global.config.json" {
t.Fatalf("resolveTonLiteNode() after cooldown = %#v, want backup lite node", got)
}
}
func TestRunTonScannerInitializesCursorAtCurrentHead(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
seedTonScannerWallet(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()
api := &fakeTonScannerAPI{head: &ton.BlockIDExt{
Workchain: address.MasterchainID,
Shard: tonMasterShard,
SeqNo: 77,
RootHash: bytes.Repeat([]byte{0x77}, 32),
FileHash: bytes.Repeat([]byte{0x78}, 32),
}}
cursor := &tonRuntimeCursor{}
if err := runTonScanner(ctx, api, newTonJettonWalletCache(), cursor); err != nil {
t.Fatalf("runTonScanner(): %v", err)
}
if !cursor.initialized {
t.Fatal("cursor not initialized")
}
if cursor.lastMasterSeqNo != 77 {
t.Fatalf("cursor seqno = %d, want 77", cursor.lastMasterSeqNo)
}
if len(api.lookups) != 0 {
t.Fatalf("initial cursor should not backfill, lookups=%#v", api.lookups)
}
}
func TestRunTonScannerIdleStatesPreserveInitializedCursor(t *testing.T) {
tests := []struct {
name string
setup func(t *testing.T)
}{
{
name: "chain disabled",
setup: func(t *testing.T) {
t.Helper()
seedTonScannerWallet(t)
if err := data.UpdateChainFields(mdb.NetworkTon, map[string]interface{}{"enabled": false}); err != nil {
t.Fatalf("disable TON chain: %v", err)
}
},
},
{
name: "no enabled wallets",
setup: func(t *testing.T) {
t.Helper()
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
tt.setup(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()
api := &fakeTonScannerAPI{
head: &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: tonMasterShard, SeqNo: 11},
}
cursor := &tonRuntimeCursor{initialized: true, lastMasterSeqNo: 10}
if err := runTonScanner(ctx, api, newTonJettonWalletCache(), cursor); err != nil {
t.Fatalf("runTonScanner(): %v", err)
}
if !cursor.initialized || cursor.lastMasterSeqNo != 10 {
t.Fatalf("cursor changed during idle state: initialized=%v seqno=%d, want initialized=true seqno=10", cursor.initialized, cursor.lastMasterSeqNo)
}
})
}
}
func TestRunTonScannerDoesNotAdvanceCursorOnMasterProcessingFailure(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
seedTonScannerWallet(t)
api := &fakeTonScannerAPI{
head: &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: tonMasterShard, SeqNo: 11},
shardsErr: errors.New("shard fetch failed"),
}
cursor := &tonRuntimeCursor{initialized: true, lastMasterSeqNo: 10}
err := runTonScanner(context.Background(), api, newTonJettonWalletCache(), cursor)
if err == nil {
t.Fatal("expected scanner failure")
}
if cursor.lastMasterSeqNo != 10 {
t.Fatalf("cursor advanced after failed block: %d, want 10", cursor.lastMasterSeqNo)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
api = &fakeTonScannerAPI{
head: &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: tonMasterShard, SeqNo: 11},
shards: nil,
}
if err := runTonScanner(ctx, api, newTonJettonWalletCache(), cursor); err != nil {
t.Fatalf("runTonScanner retry: %v", err)
}
if len(api.lookups) != 1 || api.lookups[0] != 11 {
t.Fatalf("retry lookups = %#v, want [11]", api.lookups)
}
if cursor.lastMasterSeqNo != 11 {
t.Fatalf("cursor seqno after retry = %d, want 11", cursor.lastMasterSeqNo)
}
}
func TestRunTonScannerDoesNotMarkCursorInitializedOnInitialShardFailure(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
seedTonScannerWallet(t)
api := &fakeTonScannerAPI{
head: &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: tonMasterShard, SeqNo: 11},
shardsErr: errors.New("initial shard fetch failed"),
}
cursor := &tonRuntimeCursor{}
err := runTonScanner(context.Background(), api, newTonJettonWalletCache(), cursor)
if err == nil {
t.Fatal("expected scanner failure")
}
if cursor.initialized {
t.Fatal("cursor was marked initialized after failed initial shard cursor setup")
}
if cursor.lastMasterSeqNo != 0 {
t.Fatalf("cursor master seqno = %d, want 0", cursor.lastMasterSeqNo)
}
}
func TestRunTonScannerRespectsMinConfirmations(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
seedTonScannerWallet(t)
if err := data.UpdateChainFields(mdb.NetworkTon, map[string]interface{}{"min_confirmations": 3}); err != nil {
t.Fatalf("set TON min confirmations: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
api := &fakeTonScannerAPI{
head: &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: tonMasterShard, SeqNo: 12},
}
cursor := &tonRuntimeCursor{initialized: true, lastMasterSeqNo: 10}
if err := runTonScanner(ctx, api, newTonJettonWalletCache(), cursor); err != nil {
t.Fatalf("runTonScanner(): %v", err)
}
if len(api.lookups) != 0 {
t.Fatalf("scanner processed unconfirmed blocks, lookups=%#v", api.lookups)
}
if cursor.lastMasterSeqNo != 10 {
t.Fatalf("cursor seqno = %d, want 10", cursor.lastMasterSeqNo)
}
}
func TestRunTonScannerCatchupProcessesAtMostFiftyMasterBlocksPerTick(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
seedTonScannerWallet(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()
api := &fakeTonScannerAPI{
head: &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: tonMasterShard, SeqNo: 100},
shards: nil,
}
cursor := &tonRuntimeCursor{initialized: true, lastMasterSeqNo: 1}
if err := runTonScanner(ctx, api, newTonJettonWalletCache(), cursor); err != nil {
t.Fatalf("runTonScanner(): %v", err)
}
if len(api.lookups) != int(tonMaxMasterBlocksTick) {
t.Fatalf("lookup count = %d, want %d", len(api.lookups), tonMaxMasterBlocksTick)
}
if cursor.lastMasterSeqNo != 51 {
t.Fatalf("cursor seqno = %d, want 51", cursor.lastMasterSeqNo)
}
}
func TestRunTonScannerCatchupContinuesUntilConfirmedHead(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
seedTonScannerWallet(t)
ctx, cancel := context.WithCancel(context.Background())
api := &fakeTonScannerAPI{
head: &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: tonMasterShard, SeqNo: 60},
shards: nil,
cancel: cancel,
cancelAtLookupSeq: 60,
}
cursor := &tonRuntimeCursor{initialized: true, lastMasterSeqNo: 1}
if err := runTonScanner(ctx, api, newTonJettonWalletCache(), cursor); err != nil {
t.Fatalf("runTonScanner(): %v", err)
}
if len(api.lookups) != 59 {
t.Fatalf("lookup count = %d, want 59", len(api.lookups))
}
if api.lookups[0] != 2 || api.lookups[len(api.lookups)-1] != 60 {
t.Fatalf("lookups = %#v, want continuous 2..60", api.lookups)
}
for i, seq := range api.lookups {
want := uint32(i + 2)
if seq != want {
t.Fatalf("lookup[%d] = %d, want %d", i, seq, want)
}
}
if cursor.lastMasterSeqNo != 60 {
t.Fatalf("cursor seqno = %d, want 60", cursor.lastMasterSeqNo)
}
}
func TestProcessTonShardBlockFetchesOnlyWatchedWalletTransactions(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
receive := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
unwatched := address.NewAddress(0, byte(receive.Workchain()), bytes.Repeat([]byte{0x77}, 32)).Bounce(false).Testnet(false)
sender := address.NewAddress(0, byte(receive.Workchain()), bytes.Repeat([]byte{0x88}, 32)).Bounce(false).Testnet(false)
tx := &tlb.Transaction{
LT: 2,
Now: uint32(time.Now().Unix()),
Hash: bytes.Repeat([]byte{0x99}, 32),
IO: struct {
In *tlb.Message `tlb:"maybe ^"`
Out *tlb.MessagesList `tlb:"maybe ^"`
}{
In: &tlb.Message{
MsgType: tlb.MsgTypeInternal,
Msg: &tlb.InternalMessage{
SrcAddr: sender,
DstAddr: receive,
Amount: tlb.MustFromTON("1"),
Body: cell.BeginCell().EndCell(),
},
},
},
}
api := &fakeTonShardAPI{
shorts: []ton.TransactionShortInfo{
{Account: unwatched.Data(), LT: 1, Hash: bytes.Repeat([]byte{0x01}, 32)},
{Account: receive.Data(), LT: 2, Hash: bytes.Repeat([]byte{0x02}, 32)},
},
txByLT: map[uint64]*tlb.Transaction{2: tx},
}
state := &tonWatchState{
wallets: map[string]*address.Address{
addressutil.TonRawAddressObjectKey(receive): receive,
},
nativeToken: &mdb.ChainToken{BaseModel: mdb.BaseModel{ID: 1}, Network: mdb.NetworkTon, Symbol: service.TonNativeSymbol, Decimals: 9, Enabled: true},
}
master := &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: tonMasterShard, SeqNo: 10}
shard := &ton.BlockIDExt{Workchain: receive.Workchain(), Shard: 0, SeqNo: 9}
err := processTonShardBlock(context.Background(), api, master, shard, state, newTonJettonWalletCache())
if err != nil {
t.Fatalf("processTonShardBlock(): %v", err)
}
if len(api.fetchedLTs) != 1 || api.fetchedLTs[0] != 2 {
t.Fatalf("fetched LTs = %#v, want [2]", api.fetchedLTs)
}
if len(api.fetchedAddrs) != 1 || api.fetchedAddrs[0] != addressutil.TonRawAddressObjectKey(receive) {
t.Fatalf("fetched addresses = %#v, want watched receive address", api.fetchedAddrs)
}
}
func TestProcessTonMasterBlockWithCursorScansSkippedShardBlocks(t *testing.T) {
receive := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
sender := address.NewAddress(0, byte(receive.Workchain()), bytes.Repeat([]byte{0x88}, 32)).Bounce(false).Testnet(false)
txHash := bytes.Repeat([]byte{0x42}, 32)
tx := &tlb.Transaction{
LT: 88,
Now: uint32(time.Now().Unix()),
Hash: txHash,
IO: struct {
In *tlb.Message `tlb:"maybe ^"`
Out *tlb.MessagesList `tlb:"maybe ^"`
}{
In: &tlb.Message{
MsgType: tlb.MsgTypeInternal,
Msg: &tlb.InternalMessage{
SrcAddr: sender,
DstAddr: receive,
Amount: tlb.MustFromTON("1"),
Body: cell.BeginCell().EndCell(),
},
},
},
}
currentShard := &ton.BlockIDExt{Workchain: receive.Workchain(), Shard: tonMasterShard, SeqNo: 89}
skippedShard := &ton.BlockIDExt{Workchain: receive.Workchain(), Shard: tonMasterShard, SeqNo: 88}
api := &fakeTonShardAPI{
shards: []*ton.BlockIDExt{currentShard},
lookupBlocks: map[uint32]*ton.BlockIDExt{88: skippedShard},
shortsByShardSeq: map[uint32][]ton.TransactionShortInfo{
88: {{Account: receive.Data(), LT: tx.LT, Hash: txHash}},
89: nil,
},
txByLT: map[uint64]*tlb.Transaction{tx.LT: tx},
}
state := &tonWatchState{
wallets: map[string]*address.Address{
addressutil.TonRawAddressObjectKey(receive): receive,
},
}
cursorKey := tonShardCursorKey(currentShard)
cursor := &tonRuntimeCursor{
initialized: true,
lastMasterSeqNo: 10,
shardSeqNo: map[string]uint32{cursorKey: 87},
}
master := &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: tonMasterShard, SeqNo: 10}
err := processTonMasterBlockWithCursor(context.Background(), api, master, state, newTonJettonWalletCache(), cursor)
if err != nil {
t.Fatalf("processTonMasterBlockWithCursor(): %v", err)
}
if len(api.lookupSeqs) != 1 || api.lookupSeqs[0] != 88 {
t.Fatalf("lookup shard seqs = %#v, want [88]", api.lookupSeqs)
}
if len(api.fetchedShardSeqNos) != 2 || api.fetchedShardSeqNos[0] != 88 || api.fetchedShardSeqNos[1] != 89 {
t.Fatalf("fetched shard seqs = %#v, want [88 89]", api.fetchedShardSeqNos)
}
if len(api.fetchedLTs) != 1 || api.fetchedLTs[0] != tx.LT {
t.Fatalf("fetched LTs = %#v, want [%d]", api.fetchedLTs, tx.LT)
}
if cursor.shardSeqNo[cursorKey] != 89 {
t.Fatalf("shard cursor = %d, want 89", cursor.shardSeqNo[cursorKey])
}
}
func TestProcessTonShardBlockMatchesJettonUSDTOrder(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := data.SetSetting("rate", "rate.forced_rate_list", `{"cny":{"usdt":0.1}}`, "json"); err != nil {
t.Fatalf("set forced rate: %v", err)
}
receive := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
if _, err := data.AddWalletAddressWithNetwork(mdb.NetworkTon, receive.StringRaw()); err != nil {
t.Fatalf("add ton wallet: %v", err)
}
resp, err := service.CreateTransaction(&request.CreateTransactionRequest{
OrderId: "order_ton_usdt_shard_1",
Currency: "CNY",
Token: "USDT",
Network: mdb.NetworkTon,
Amount: 10,
NotifyUrl: "https://93.184.216.34/callback",
}, nil)
if err != nil {
t.Fatalf("create TON USDT order: %v", err)
}
sender := address.NewAddress(0, 0, bytes.Repeat([]byte{0x31}, 32)).Bounce(false).Testnet(false)
jettonWallet := address.NewAddress(0, byte(receive.Workchain()), bytes.Repeat([]byte{0x44}, 32)).Bounce(false).Testnet(false)
body, err := tlb.ToCell(jetton.TransferNotification{
QueryID: 7,
Amount: tlb.MustFromNano(big.NewInt(1_000_000), 6),
Sender: sender,
ForwardPayload: cell.BeginCell().EndCell(),
})
if err != nil {
t.Fatalf("build jetton notification body: %v", err)
}
tx := &tlb.Transaction{
LT: 22,
Now: uint32(time.Now().Add(time.Second).Unix()),
Hash: bytes.Repeat([]byte{0x42}, 32),
IO: struct {
In *tlb.Message `tlb:"maybe ^"`
Out *tlb.MessagesList `tlb:"maybe ^"`
}{
In: &tlb.Message{
MsgType: tlb.MsgTypeInternal,
Msg: &tlb.InternalMessage{
SrcAddr: jettonWallet,
DstAddr: receive,
Amount: tlb.FromNanoTONU(1),
Body: body,
},
},
},
}
usdtMaster, err := addressutil.ParseTonMainnetAddress("0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe")
if err != nil {
t.Fatalf("parse USDT jetton master: %v", err)
}
cache := newTonJettonWalletCache()
ownerRaw := addressutil.TonRawAddressObjectKey(receive)
cache.walletByOwnerToken[usdtMaster.Raw+"|"+ownerRaw] = addressutil.TonRawAddressObjectKey(jettonWallet)
api := &fakeTonShardAPI{
shorts: []ton.TransactionShortInfo{
{Account: receive.Data(), LT: tx.LT, Hash: tx.Hash},
},
txByLT: map[uint64]*tlb.Transaction{tx.LT: tx},
}
state := &tonWatchState{
wallets: map[string]*address.Address{
ownerRaw: receive,
},
jettonTokens: []mdb.ChainToken{
{BaseModel: mdb.BaseModel{ID: 2}, Network: mdb.NetworkTon, Symbol: "USDT", ContractAddress: usdtMaster.Raw, Decimals: 6, Enabled: true},
},
}
master := &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: tonMasterShard, SeqNo: 10}
shard := &ton.BlockIDExt{Workchain: receive.Workchain(), Shard: 0, SeqNo: 9}
if err := processTonShardBlock(context.Background(), api, master, shard, state, cache); err != nil {
t.Fatalf("processTonShardBlock(): %v", err)
}
order, err := data.GetOrderInfoByTradeId(resp.TradeId)
if err != nil {
t.Fatalf("reload order: %v", err)
}
if order.Status != mdb.StatusPaySuccess {
t.Fatalf("order status = %d, want %d", order.Status, mdb.StatusPaySuccess)
}
wantBlockID := service.TonCanonicalBlockTransactionID(receive.StringRaw(), tx.LT, hex.EncodeToString(tx.Hash))
if order.BlockTransactionId != wantBlockID {
t.Fatalf("block transaction id = %q, want %q", order.BlockTransactionId, wantBlockID)
}
lock, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTon, receive.StringRaw(), "USDT", resp.ActualAmount)
if err != nil {
t.Fatalf("lookup TON USDT lock: %v", err)
}
if lock != "" {
t.Fatalf("TON USDT lock still exists after shard processing: %s", lock)
}
}
func TestProcessTonMasterBlockScansMasterchainWalletTransactions(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
masterchainID := address.MasterchainID
masterchainByte := byte(int8(masterchainID))
receive := address.NewAddress(0, masterchainByte, bytes.Repeat([]byte{0x66}, 32)).Bounce(false).Testnet(false)
sender := address.NewAddress(0, masterchainByte, bytes.Repeat([]byte{0x88}, 32)).Bounce(false).Testnet(false)
tx := &tlb.Transaction{
LT: 2,
Now: uint32(time.Now().Unix()),
Hash: bytes.Repeat([]byte{0x99}, 32),
IO: struct {
In *tlb.Message `tlb:"maybe ^"`
Out *tlb.MessagesList `tlb:"maybe ^"`
}{
In: &tlb.Message{
MsgType: tlb.MsgTypeInternal,
Msg: &tlb.InternalMessage{
SrcAddr: sender,
DstAddr: receive,
Amount: tlb.MustFromTON("1"),
Body: cell.BeginCell().EndCell(),
},
},
},
}
api := &fakeTonShardAPI{
shorts: []ton.TransactionShortInfo{
{Account: receive.Data(), LT: 2, Hash: bytes.Repeat([]byte{0x02}, 32)},
},
txByLT: map[uint64]*tlb.Transaction{2: tx},
}
state := &tonWatchState{
wallets: map[string]*address.Address{
addressutil.TonRawAddressObjectKey(receive): receive,
},
nativeToken: &mdb.ChainToken{BaseModel: mdb.BaseModel{ID: 1}, Network: mdb.NetworkTon, Symbol: service.TonNativeSymbol, Decimals: 9, Enabled: true},
}
master := &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: tonMasterShard, SeqNo: 10}
if err := processTonMasterBlock(context.Background(), api, master, state, newTonJettonWalletCache()); err != nil {
t.Fatalf("processTonMasterBlock(): %v", err)
}
if len(api.fetchedLTs) != 1 || api.fetchedLTs[0] != 2 {
t.Fatalf("fetched LTs = %#v, want [2]", api.fetchedLTs)
}
if len(api.fetchedAddrs) != 1 || api.fetchedAddrs[0] != addressutil.TonRawAddressObjectKey(receive) {
t.Fatalf("fetched addresses = %#v, want masterchain receive address", api.fetchedAddrs)
}
}
func TestProcessTonShardBlockFailsOnIncompleteEmptyPage(t *testing.T) {
api := &fakeTonShardAPI{incomplete: true}
state := &tonWatchState{wallets: map[string]*address.Address{}}
master := &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: tonMasterShard, SeqNo: 10}
shard := &ton.BlockIDExt{Workchain: 0, Shard: 0, SeqNo: 9}
err := processTonShardBlock(context.Background(), api, master, shard, state, newTonJettonWalletCache())
if err == nil {
t.Fatal("expected incomplete empty page error")
}
}
func TestProcessTonShardBlockFailsWhenJettonWalletsCannotBeResolved(t *testing.T) {
receive := address.MustParseAddr("EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I")
sender := address.NewAddress(0, byte(receive.Workchain()), bytes.Repeat([]byte{0x88}, 32)).Bounce(false).Testnet(false)
tx := &tlb.Transaction{
LT: 2,
Now: uint32(time.Now().Unix()),
Hash: bytes.Repeat([]byte{0x99}, 32),
IO: struct {
In *tlb.Message `tlb:"maybe ^"`
Out *tlb.MessagesList `tlb:"maybe ^"`
}{
In: &tlb.Message{
MsgType: tlb.MsgTypeInternal,
Msg: &tlb.InternalMessage{
SrcAddr: sender,
DstAddr: receive,
Amount: tlb.MustFromTON("1"),
Body: cell.BeginCell().EndCell(),
},
},
},
}
api := &fakeTonShardAPI{
shorts: []ton.TransactionShortInfo{
{Account: receive.Data(), LT: 2, Hash: bytes.Repeat([]byte{0x02}, 32)},
},
txByLT: map[uint64]*tlb.Transaction{2: tx},
}
state := &tonWatchState{
wallets: map[string]*address.Address{
addressutil.TonRawAddressObjectKey(receive): receive,
},
jettonTokens: []mdb.ChainToken{
{BaseModel: mdb.BaseModel{ID: 1}, Network: mdb.NetworkTon, Symbol: "USDT", ContractAddress: "not-a-ton-address", Decimals: 6, Enabled: true},
},
}
master := &ton.BlockIDExt{Workchain: address.MasterchainID, Shard: tonMasterShard, SeqNo: 10}
shard := &ton.BlockIDExt{Workchain: receive.Workchain(), Shard: 0, SeqNo: 9}
err := processTonShardBlock(context.Background(), api, master, shard, state, newTonJettonWalletCache())
if err == nil {
t.Fatal("expected jetton wallet resolution failure")
}
if len(api.fetchedLTs) != 1 || api.fetchedLTs[0] != 2 {
t.Fatalf("fetched LTs = %#v, want [2]", api.fetchedLTs)
}
}
+9
View File
@@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
addressutil "github.com/GMWalletApp/epusdt/util/address"
"github.com/btcsuite/btcutil/base58" "github.com/btcsuite/btcutil/base58"
"github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go"
) )
@@ -53,6 +54,9 @@ func isValidAddressByNetwork(network, addr string) bool {
return isValidTronAddress(addr) return isValidTronAddress(addr)
case mdb.NetworkSolana: case mdb.NetworkSolana:
return isValidSolanaAddress(addr) return isValidSolanaAddress(addr)
case mdb.NetworkTon:
_, err := addressutil.ParseTonMainnetAddress(addr)
return err == nil
default: default:
// 其余 EVM 链统一使用 0x 地址校验 // 其余 EVM 链统一使用 0x 地址校验
return isValidEthereumAddress(addr) return isValidEthereumAddress(addr)
@@ -64,6 +68,11 @@ func normalizeWalletAddressByNetwork(network, addr string) string {
switch strings.ToLower(strings.TrimSpace(network)) { switch strings.ToLower(strings.TrimSpace(network)) {
case mdb.NetworkTron, mdb.NetworkSolana: case mdb.NetworkTron, mdb.NetworkSolana:
return addr return addr
case mdb.NetworkTon:
if normalized, err := addressutil.NormalizeTonAddress(addr); err == nil {
return normalized
}
return addr
default: default:
return strings.ToLower(addr) return strings.ToLower(addr)
} }
+135
View File
@@ -0,0 +1,135 @@
package addressutil
import (
"encoding/base64"
"encoding/binary"
"fmt"
"strings"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/crc16"
)
// TonParsedAddress carries the canonical display and raw forms used by epusdt.
type TonParsedAddress struct {
Address *address.Address
Display string
Raw string
}
// ParseTonMainnetAddress accepts raw and user-friendly TON addresses and rejects
// user-friendly testnet-only addresses. Raw addresses do not contain a testnet
// flag, so they are treated as mainnet addresses by definition.
func ParseTonMainnetAddress(input string) (*TonParsedAddress, error) {
raw := strings.TrimSpace(input)
if raw == "" {
return nil, fmt.Errorf("ton address is empty")
}
var (
addr *address.Address
err error
checkFlags bool
)
if strings.Contains(raw, ":") {
addr, err = address.ParseRawAddr(raw)
} else {
addr, err = parseUserFriendly(raw)
checkFlags = true
}
if err != nil {
return nil, err
}
if addr == nil || addr.Type() != address.StdAddress || addr.IsAddrNone() {
return nil, fmt.Errorf("unsupported ton address type")
}
if checkFlags && addr.IsTestnetOnly() {
return nil, fmt.Errorf("testnet-only ton address is not supported")
}
mainnet := addr.Bounce(false).Testnet(false)
return &TonParsedAddress{
Address: mainnet,
Display: mainnet.String(),
Raw: strings.ToLower(mainnet.StringRaw()),
}, nil
}
// NormalizeTonAddress returns the canonical user-friendly address stored in DB.
func NormalizeTonAddress(input string) (string, error) {
parsed, err := ParseTonMainnetAddress(input)
if err != nil {
return "", err
}
return parsed.Display, nil
}
// TonRawAddressKey returns the workchain:hash key used for comparisons and locks.
func TonRawAddressKey(input string) (string, error) {
parsed, err := ParseTonMainnetAddress(input)
if err != nil {
return "", err
}
return parsed.Raw, nil
}
// NormalizeTonAddressObject returns the canonical user-friendly form for a parsed
// address object.
func NormalizeTonAddressObject(addr *address.Address) string {
if addr == nil {
return ""
}
return addr.Bounce(false).Testnet(false).String()
}
// TonRawAddressObjectKey returns the raw workchain:hash key for a parsed address.
func TonRawAddressObjectKey(addr *address.Address) string {
if addr == nil {
return ""
}
return strings.ToLower(addr.Bounce(false).Testnet(false).StringRaw())
}
// TonAddressFromShortAccount builds a standard TON address from a shard
// transaction account id returned by liteserver block transaction lists.
func TonAddressFromShortAccount(workchain int32, account []byte) *address.Address {
if len(account) != 32 {
return nil
}
return address.NewAddress(0, byte(workchain), append([]byte(nil), account...)).Bounce(false).Testnet(false)
}
func parseUserFriendly(input string) (*address.Address, error) {
if addr, err := address.ParseAddr(input); err == nil {
return addr, nil
}
for _, enc := range []*base64.Encoding{
base64.RawStdEncoding,
base64.StdEncoding,
base64.URLEncoding,
} {
data, err := enc.DecodeString(input)
if err != nil {
continue
}
addr, err := addressFromFriendlyBytes(data)
if err == nil {
return addr, nil
}
}
return nil, fmt.Errorf("invalid ton address")
}
func addressFromFriendlyBytes(data []byte) (*address.Address, error) {
if len(data) != 36 {
return nil, fmt.Errorf("incorrect ton address data length")
}
want := binary.BigEndian.Uint16(data[34:])
if crc16.ChecksumXMODEM(data[:34]) != want {
return nil, fmt.Errorf("invalid ton address checksum")
}
if len(data[2:34]) != 32 {
return nil, fmt.Errorf("incorrect ton address hash length")
}
return address.NewAddress(data[0], data[1], append([]byte(nil), data[2:34]...)), nil
}
+56
View File
@@ -0,0 +1,56 @@
package addressutil
import (
"encoding/base64"
"strings"
"testing"
"github.com/xssnick/tonutils-go/address"
)
const testTonBounceable = "EQC6KV4zs8TJtSZapOrRFmqSkxzpq-oSCoxekQRKElf4nC1I"
func TestParseTonMainnetAddressNormalizesSupportedFormats(t *testing.T) {
base := address.MustParseAddr(testTonBounceable)
raw := base.StringRaw()
nonBounce := base.Bounce(false).String()
rawBytes, err := base64.RawURLEncoding.DecodeString(testTonBounceable)
if err != nil {
t.Fatalf("decode fixture: %v", err)
}
stdBase64 := base64.RawStdEncoding.EncodeToString(rawBytes)
for _, input := range []string{testTonBounceable, nonBounce, raw, stdBase64} {
parsed, err := ParseTonMainnetAddress(input)
if err != nil {
t.Fatalf("ParseTonMainnetAddress(%q): %v", input, err)
}
if parsed.Display != nonBounce {
t.Fatalf("display for %q = %q, want %q", input, parsed.Display, nonBounce)
}
if parsed.Raw != strings.ToLower(raw) {
t.Fatalf("raw for %q = %q, want %q", input, parsed.Raw, strings.ToLower(raw))
}
}
}
func TestParseTonMainnetAddressRejectsTestnetOnly(t *testing.T) {
testnet := address.MustParseAddr(testTonBounceable).Testnet(true).String()
if _, err := ParseTonMainnetAddress(testnet); err == nil {
t.Fatal("expected testnet-only address to be rejected")
}
}
func TestTonAddressFromShortAccount(t *testing.T) {
base := address.MustParseAddr(testTonBounceable)
got := TonAddressFromShortAccount(base.Workchain(), base.Data())
if got == nil {
t.Fatal("TonAddressFromShortAccount() returned nil")
}
if TonRawAddressObjectKey(got) != TonRawAddressObjectKey(base) {
t.Fatalf("raw address = %q, want %q", TonRawAddressObjectKey(got), TonRawAddressObjectKey(base))
}
if got = TonAddressFromShortAccount(base.Workchain(), base.Data()[:31]); got != nil {
t.Fatalf("TonAddressFromShortAccount() with short account = %#v, want nil", got)
}
}
+80 -11
View File
@@ -2,6 +2,9 @@ package log
import ( import (
"fmt" "fmt"
"strings"
"sync"
"github.com/GMWalletApp/epusdt/config" "github.com/GMWalletApp/epusdt/config"
"github.com/natefinch/lumberjack" "github.com/natefinch/lumberjack"
"github.com/spf13/viper" "github.com/spf13/viper"
@@ -13,11 +16,16 @@ import (
var Sugar *zap.SugaredLogger var Sugar *zap.SugaredLogger
var (
atomicLevel = zap.NewAtomicLevelAt(zapcore.InfoLevel)
levelMu sync.Mutex
)
func Init() { func Init() {
level := getLogLevel() atomicLevel.SetLevel(getLogLevel())
core := zapcore.NewTee( core := zapcore.NewTee(
zapcore.NewCore(getConsoleEncoder(), getConsoleWriter(), level), zapcore.NewCore(getConsoleEncoder(), getConsoleWriter(), atomicLevel),
zapcore.NewCore(getFileEncoder(), getFileWriter(), level), zapcore.NewCore(getFileEncoder(), getFileWriter(), atomicLevel),
) )
logger := zap.New(core, zap.AddCaller()) logger := zap.New(core, zap.AddCaller())
Sugar = logger.Sugar() Sugar = logger.Sugar()
@@ -56,14 +64,75 @@ func getConsoleWriter() zapcore.WriteSyncer {
} }
func getLogLevel() zapcore.Level { func getLogLevel() zapcore.Level {
switch config.LogLevel { level, _, err := parseLevel(config.LogLevel)
case "debug": if err != nil {
return zapcore.DebugLevel
case "warn":
return zapcore.WarnLevel
case "error":
return zapcore.ErrorLevel
default:
return zapcore.InfoLevel return zapcore.InfoLevel
} }
return level
}
func SetLevel(level string) error {
nextLevel, normalized, err := parseLevel(level)
if err != nil {
return err
}
levelMu.Lock()
defer levelMu.Unlock()
currentLevel := atomicLevel.Level()
current := levelToString(currentLevel)
if current == normalized {
return nil
}
message := fmt.Sprintf("[log] level changed: %s -> %s", current, normalized)
if Sugar != nil && currentLevel <= zapcore.WarnLevel {
Sugar.Warn(message)
}
atomicLevel.SetLevel(nextLevel)
if Sugar != nil && currentLevel > zapcore.WarnLevel {
Sugar.Warn(message)
}
return nil
}
func CurrentLevel() string {
return levelToString(atomicLevel.Level())
}
func NormalizeLevel(level string) (string, error) {
_, normalized, err := parseLevel(level)
return normalized, err
}
func parseLevel(level string) (zapcore.Level, string, error) {
normalized := strings.ToLower(strings.TrimSpace(level))
switch normalized {
case "debug":
return zapcore.DebugLevel, normalized, nil
case "info":
return zapcore.InfoLevel, normalized, nil
case "warn":
return zapcore.WarnLevel, normalized, nil
case "error":
return zapcore.ErrorLevel, normalized, nil
default:
return zapcore.InfoLevel, "", fmt.Errorf("unsupported log level %q", level)
}
}
func levelToString(level zapcore.Level) string {
switch level {
case zapcore.DebugLevel:
return "debug"
case zapcore.InfoLevel:
return "info"
case zapcore.WarnLevel:
return "warn"
case zapcore.ErrorLevel:
return "error"
default:
return level.String()
}
} }
+45
View File
@@ -0,0 +1,45 @@
package log
import (
"testing"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"
)
func TestSetLevelUpdatesAtomicLevelAndLogsChange(t *testing.T) {
oldSugar := Sugar
oldLevel := atomicLevel.Level()
t.Cleanup(func() {
Sugar = oldSugar
atomicLevel.SetLevel(oldLevel)
})
core, observed := observer.New(zapcore.WarnLevel)
Sugar = zap.New(core).Sugar()
atomicLevel.SetLevel(zapcore.ErrorLevel)
if err := SetLevel(" DEBUG "); err != nil {
t.Fatalf("SetLevel(DEBUG): %v", err)
}
if got := CurrentLevel(); got != "debug" {
t.Fatalf("CurrentLevel = %q, want debug", got)
}
logs := observed.FilterMessage("[log] level changed: error -> debug").All()
if len(logs) != 1 {
t.Fatalf("level change logs = %d, want 1; all=%v", len(logs), observed.All())
}
if err := SetLevel("debug"); err != nil {
t.Fatalf("SetLevel(debug no-op): %v", err)
}
logs = observed.FilterMessage("[log] level changed: error -> debug").All()
if len(logs) != 1 {
t.Fatalf("no-op level change emitted extra logs: %d", len(logs))
}
if err := SetLevel("fatal"); err == nil {
t.Fatal("SetLevel(fatal) succeeded, want error")
}
}
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./unauthorized-error-Btb6_KCA.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./unauthorized-error-Dv-DRB5e.js";var t=e;export{t as component};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./forbidden-2YkGwREa.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./forbidden-CHYUR1RB.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./not-found-error-AnL_Yh5s.js";var t=e;export{t as component};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./not-found-error-DAU8lBe-.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./general-error-CkWgD-Kh.js";var t=e;export{t as component};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./general-error-CJCCJkg5.js";var t=e;export{t as component};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./maintenance-error-D_5WJHLy.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./maintenance-error-CpldkShG.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{Pp as n}from"./messages-d1hRBHLP.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-CMPEj0pJ.js";import"./redirect-DMNGvjzO.js";import{a as c,i as l,r as u,s as d,t as f}from"./Match-9UShCljL.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{Kp as n}from"./messages-CokTNgkr.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-DpS_F-Xn.js";import"./redirect-DMNGvjzO.js";import{a as c,i as l,r as u,s as d,t as f}from"./Match-CjZe6DmC.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
@@ -1 +0,0 @@
import{Pp as e}from"./messages-d1hRBHLP.js";import{m as t}from"./search-provider-Zrg-SPXk.js";import{n,t as r}from"./theme-switch-DporjlJW.js";import{t as i}from"./general-error-CkWgD-Kh.js";import{t as a}from"./not-found-error-AnL_Yh5s.js";import{t as o}from"./_error-DqSF-qO2.js";import{t as s}from"./unauthorized-error-Dv-DRB5e.js";import{t as c}from"./forbidden-CHYUR1RB.js";import{t as l}from"./maintenance-error-CpldkShG.js";import{n as u,r as d,t as f}from"./header-BZlnQmen.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
@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_error-oy4G8XRq.js","assets/search-provider-BIQ51MIB.js","assets/chunk-DECur_0Z.js","assets/dist-CRowTfGU.js","assets/dist-CKhUg8lz.js","assets/messages-CokTNgkr.js","assets/react-CO2uhaBc.js","assets/confirm-dialog-D_xrov5z.js","assets/button-DdD9bP3z.js","assets/clsx-D9aGYY3V.js","assets/dist-BTFQXMl4.js","assets/dist-BQFHFtr-.js","assets/dist-r2DO4E6w.js","assets/dist-CrzsE82B.js","assets/es2015-Cc8TzKT6.js","assets/dist-BbSJdbb-.js","assets/dist-TZZx9SXO.js","assets/dist-C8gCQuQD.js","assets/dist-3039QLji.js","assets/dist-CQdg-Tnw.js","assets/dist-BixeH3nX.js","assets/dist-DIWGcwtg.js","assets/separator-BSf5wz9l.js","assets/tooltip-GAwM0SrD.js","assets/dist-5sRZ1J6O.js","assets/dist-_fvN-8F3.js","assets/auth-store-C6nFFVPp.js","assets/dist-CSdexNO_.js","assets/with-selector-DBfssCrY.js","assets/cookies-BzZOQYPw.js","assets/useRouter-DtTm7XkK.js","assets/useNavigate-7u4DX0Ho.js","assets/useStore-CupyIEEP.js","assets/command-Ci9WQkc-.js","assets/createLucideIcon-Br0Bd5k2.js","assets/dialog-DixYhGRE.js","assets/skeleton-C0TiN-ew.js","assets/wallet-BEOGa_nU.js","assets/sun-BwqrnykR.js","assets/shield-check-CiWq1V4z.js","assets/logo-BeMv8yVy.js","assets/input-BdArIRZ9.js","assets/font-provider-XOdRSAjV.js","assets/theme-provider-Bq1RKBft.js","assets/theme-switch-DEZuhZJw.js","assets/dropdown-menu-tLOgVJlP.js","assets/check-DldThLle.js","assets/header-DxK_xQxT.js","assets/link-CK5djrfn.js","assets/ClientOnly-DpS_F-Xn.js","assets/forbidden-2YkGwREa.js","assets/general-error-CJCCJkg5.js","assets/maintenance-error-D_5WJHLy.js","assets/not-found-error-DAU8lBe-.js","assets/unauthorized-error-Btb6_KCA.js"])))=>i.map(i=>d[i]);
import{n as e,t}from"./lazyRouteComponent-CE1uAfxe.js";import{t as n}from"./preload-helper-vQKTmgnp.js";var r=e(`/_authenticated/errors/$error`)({component:t(()=>n(()=>import(`./_error-oy4G8XRq.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
@@ -1,2 +0,0 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_error-D4dSyXw0.js","assets/search-provider-Zrg-SPXk.js","assets/chunk-DECur_0Z.js","assets/dist-CRowTfGU.js","assets/dist-CIovDv3v.js","assets/messages-d1hRBHLP.js","assets/react-CO2uhaBc.js","assets/confirm-dialog-CIBixfaW.js","assets/button-CbdgUDvS.js","assets/clsx-D9aGYY3V.js","assets/dist-BjDKUEMi.js","assets/dist-ByVfo7qC.js","assets/dist-qAA-axL4.js","assets/dist-CXfAGnWR.js","assets/es2015-Chgs9D7p.js","assets/dist-xVhEP_Vg.js","assets/dist-s_CiB-DN.js","assets/dist-BlTmpVme.js","assets/dist-D13PZ7es.js","assets/dist-DOx-EAQr.js","assets/dist-BixeH3nX.js","assets/dist-DuNl0ZwE.js","assets/separator-CE12xF6y.js","assets/tooltip-DuzUBeLr.js","assets/dist-BIXdBdVU.js","assets/dist-BGJ_1VEi.js","assets/auth-store-GibZbQPL.js","assets/dist-Bk35TVDN.js","assets/with-selector-DBfssCrY.js","assets/cookies-BzZOQYPw.js","assets/useRouter-DtTm7XkK.js","assets/useNavigate-7u4DX0Ho.js","assets/useStore-CupyIEEP.js","assets/command-DIyNKYlo.js","assets/createLucideIcon-Br0Bd5k2.js","assets/dialog-SgRLLGoQ.js","assets/skeleton-DwLAyCLM.js","assets/wallet-CkKQfl5z.js","assets/sun-BwqrnykR.js","assets/shield-check-CiWq1V4z.js","assets/logo-CGyEdT5T.js","assets/input-CusCjhBW.js","assets/font-provider-aBht_Njp.js","assets/theme-provider-BXElecjF.js","assets/theme-switch-DporjlJW.js","assets/dropdown-menu-DwTFINO1.js","assets/check-DldThLle.js","assets/header-BZlnQmen.js","assets/link-Br2JDLvw.js","assets/ClientOnly-CMPEj0pJ.js","assets/forbidden-CHYUR1RB.js","assets/general-error-CkWgD-Kh.js","assets/maintenance-error-CpldkShG.js","assets/not-found-error-AnL_Yh5s.js","assets/unauthorized-error-Dv-DRB5e.js"])))=>i.map(i=>d[i]);
import{n as e,t}from"./lazyRouteComponent-CFOfOLgA.js";import{t as n}from"./preload-helper-vQKTmgnp.js";var r=e(`/_authenticated/errors/$error`)({component:t(()=>n(()=>import(`./_error-D4dSyXw0.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};
+1
View File
@@ -0,0 +1 @@
import{Kp as e}from"./messages-CokTNgkr.js";import{m as t}from"./search-provider-BIQ51MIB.js";import{n,t as r}from"./theme-switch-DEZuhZJw.js";import{t as i}from"./general-error-CJCCJkg5.js";import{t as a}from"./not-found-error-DAU8lBe-.js";import{t as o}from"./_error-DWrFFiWw.js";import{t as s}from"./unauthorized-error-Btb6_KCA.js";import{t as c}from"./forbidden-2YkGwREa.js";import{t as l}from"./maintenance-error-D_5WJHLy.js";import{n as u,r as d,t as f}from"./header-DxK_xQxT.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
@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_trade_id-DCdjklsJ.js","assets/chunk-DECur_0Z.js","assets/button-DdD9bP3z.js","assets/clsx-D9aGYY3V.js","assets/messages-CokTNgkr.js","assets/react-CO2uhaBc.js","assets/select-CjMGKVW5.js","assets/dist-5sRZ1J6O.js","assets/dist-CKhUg8lz.js","assets/dist-r2DO4E6w.js","assets/dist-CrzsE82B.js","assets/dist-DIWGcwtg.js","assets/dist-CRowTfGU.js","assets/dist-CQdg-Tnw.js","assets/dist-TZZx9SXO.js","assets/dist-BQFHFtr-.js","assets/es2015-Cc8TzKT6.js","assets/dist-BixeH3nX.js","assets/dist-_fvN-8F3.js","assets/createLucideIcon-Br0Bd5k2.js","assets/check-DldThLle.js","assets/chevron-down-BPPKEraq.js","assets/auth-store-C6nFFVPp.js","assets/dist-CSdexNO_.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-DA6nlktd.js","assets/dist-C8gCQuQD.js","assets/dist-BbSJdbb-.js","assets/dist-3039QLji.js","assets/loader-circle-DRj7MiMc.js","assets/send-g-CrrC4k.js","assets/triangle-alert-BEkslGAT.js","assets/dialog-DixYhGRE.js","assets/dist-BTFQXMl4.js","assets/crypto-icon-BxZHTcEz.js","assets/labels-Baqaexjy.js","assets/input-BdArIRZ9.js","assets/checkout-model-qShof1IE.js"])))=>i.map(i=>d[i]);
import{n as e,t}from"./lazyRouteComponent-CE1uAfxe.js";import{t as n}from"./preload-helper-vQKTmgnp.js";var r=e(`/cashier/$trade_id/`)({component:t(()=>n(()=>import(`./_trade_id-DCdjklsJ.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])),`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-Ck9STVZb.js","assets/chunk-DECur_0Z.js","assets/button-CbdgUDvS.js","assets/clsx-D9aGYY3V.js","assets/messages-d1hRBHLP.js","assets/react-CO2uhaBc.js","assets/select-Cv_sRi68.js","assets/dist-BIXdBdVU.js","assets/dist-CIovDv3v.js","assets/dist-qAA-axL4.js","assets/dist-CXfAGnWR.js","assets/dist-DuNl0ZwE.js","assets/dist-CRowTfGU.js","assets/dist-DOx-EAQr.js","assets/dist-s_CiB-DN.js","assets/dist-ByVfo7qC.js","assets/es2015-Chgs9D7p.js","assets/dist-BixeH3nX.js","assets/dist-BGJ_1VEi.js","assets/createLucideIcon-Br0Bd5k2.js","assets/check-DldThLle.js","assets/chevron-down-BPPKEraq.js","assets/auth-store-GibZbQPL.js","assets/dist-Bk35TVDN.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-CX51BC9z.js","assets/dist-BlTmpVme.js","assets/dist-xVhEP_Vg.js","assets/dist-D13PZ7es.js","assets/loader-circle-DRj7MiMc.js","assets/send-g-CrrC4k.js","assets/triangle-alert-BEkslGAT.js","assets/dialog-SgRLLGoQ.js","assets/dist-BjDKUEMi.js","assets/crypto-icon-BwB-F_ID.js","assets/labels-CfnKbsNx.js","assets/input-CusCjhBW.js","assets/checkout-model-CY0q1fms.js"])))=>i.map(i=>d[i]);
import{n as e,t}from"./lazyRouteComponent-CFOfOLgA.js";import{t as n}from"./preload-helper-vQKTmgnp.js";var r=e(`/cashier/$trade_id/`)({component:t(()=>n(()=>import(`./_trade_id-Ck9STVZb.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])),`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{Cp as e,Pp as t,Sp as n,_n as r,bn as i,gn as a,hn as o,mn as s,vn as c,yn as l}from"./messages-d1hRBHLP.js";import{i as u,n as d,t as f}from"./button-CbdgUDvS.js";import{n as p,r as m}from"./font-provider-aBht_Njp.js";import{n as h}from"./theme-provider-BXElecjF.js";import{t as g}from"./chevron-down-BPPKEraq.js";import{n as _,t as v}from"./radio-group-CX51BC9z.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-Db7d571a.js";import{t as A}from"./page-header-DkeFH2Rh.js";import{t as j}from"./show-submitted-data-CfmGDLbU.js";var M=t(),N=b({theme:y([`light`,`dark`]),font:y(m)});function P(){let{font:t,setFont:i}=p(),{theme:l,setTheme:y}=h(),b={theme:l,font:t},A=w({resolver:S(N),defaultValues:b});function P(e){e.font!==t&&i(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:r()}),(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:e()})]})}),(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:n()})]})})]})]})}),(0,M.jsx)(f,{type:`submit`,children:s()})]})})}function F(){return(0,M.jsx)(A,{description:l(),title:i(),variant:`section`,children:(0,M.jsx)(P,{})})}var I=F;export{I as component}; import{Fp as e,Kp as t,Pp as n,_n as r,bn as i,gn as a,hn as o,mn as s,vn as c,yn as l}from"./messages-CokTNgkr.js";import{i as u,n as d,t as f}from"./button-DdD9bP3z.js";import{n as p,r as m}from"./font-provider-XOdRSAjV.js";import{n as h}from"./theme-provider-Bq1RKBft.js";import{t as g}from"./chevron-down-BPPKEraq.js";import{n as _,t as v}from"./radio-group-DA6nlktd.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-B-HnSRAy.js";import{t as A}from"./page-header-Bgrl_xms.js";import{t as j}from"./show-submitted-data-C7kK9HtD.js";var M=t(),N=b({theme:y([`light`,`dark`]),font:y(m)});function P(){let{font:t,setFont:i}=p(),{theme:l,setTheme:y}=h(),b={theme:l,font:t},A=w({resolver:S(N),defaultValues:b});function P(e){e.font!==t&&i(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:r()}),(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:e()})]})}),(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:n()})]})})]})]})}),(0,M.jsx)(f,{type:`submit`,children:s()})]})})}function F(){return(0,M.jsx)(A,{description:l(),title:i(),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{Pp as n}from"./messages-d1hRBHLP.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{Kp as n}from"./messages-CokTNgkr.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
@@ -1 +1 @@
import{Pp as e}from"./messages-d1hRBHLP.js";import{i as t,o as n,r}from"./button-CbdgUDvS.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{Kp as e}from"./messages-CokTNgkr.js";import{i as t,o as n,r}from"./button-DdD9bP3z.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{Pp as e}from"./messages-d1hRBHLP.js";import{i as t}from"./button-CbdgUDvS.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{Kp as e}from"./messages-CokTNgkr.js";import{i as t}from"./button-DdD9bP3z.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,f as r}from"./auth-store-GibZbQPL.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}; import{r as e}from"./chunk-DECur_0Z.js";import{a as t,d as n,f as r}from"./auth-store-C6nFFVPp.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{Pp as n}from"./messages-d1hRBHLP.js";import{t as r}from"./dist-CXfAGnWR.js";import{i,u as a}from"./button-CbdgUDvS.js";import{a as o,r as s,t as c}from"./dist-CIovDv3v.js";import{t as l}from"./dist-xVhEP_Vg.js";import{t as u}from"./dist-BixeH3nX.js";import{t as d}from"./dist-DuNl0ZwE.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{Kp as n}from"./messages-CokTNgkr.js";import{t as r}from"./dist-CrzsE82B.js";import{i,u as a}from"./button-DdD9bP3z.js";import{a as o,r as s,t as c}from"./dist-CKhUg8lz.js";import{t as l}from"./dist-BbSJdbb-.js";import{t as u}from"./dist-BixeH3nX.js";import{t as d}from"./dist-DIWGcwtg.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};
@@ -1 +0,0 @@
import{Pp as e,_p as t,gp as n,hp as r}from"./messages-d1hRBHLP.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-SgRLLGoQ.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:t()}),(0,u.jsxs)(a,{children:[n(),(0,u.jsx)(`br`,{}),(0,u.jsx)(`p`,{className:`mt-2 text-center`,children:r()})]})]})})})}export{d as t};
@@ -0,0 +1 @@
import{Dp as e,Kp as t,Op as n,kp as r}from"./messages-CokTNgkr.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-DixYhGRE.js";var u=t();function d({open:t,onOpenChange:d}){return(0,u.jsx)(l,{onOpenChange:d,open:t,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:e()})]})]})})})}export{d as t};
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Op as n,Pp as r,kp as i}from"./messages-d1hRBHLP.js";import{c as a,i as o,t as s,u as c}from"./button-CbdgUDvS.js";import{a as l,r as u}from"./dist-CIovDv3v.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-BjDKUEMi.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. import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Bp as n,Kp as r,Vp as i}from"./messages-CokTNgkr.js";import{c as a,i as o,t as s,u as c}from"./button-DdD9bP3z.js";import{a as l,r as u}from"./dist-CKhUg8lz.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-BTFQXMl4.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.
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Pp as n}from"./messages-d1hRBHLP.js";var r=e(t(),1);function i(e){let t=new Map,n=new Map;return{get(e){return t.get(e)},load(r){let i=t.get(r);if(i)return Promise.resolve(i);let a=n.get(r);if(a)return a;let o=e(r).then(e=>(t.set(r,e),e)).finally(()=>{n.delete(r)});return n.set(r,o),o}}}async function a(e,t,n){for(let n of e){let e=`${n.base}/${t}`;try{let t=await fetch(e,{signal:AbortSignal.timeout(3e3)});if(t.ok)return await t.json()}catch{}}return n}function o({cdnList:e,fallback:t,path:n}){return i(r=>a(e,n(r),t))}var s=`GMWalletApp/crypto-icons`,c=`latest`,l=[{base:`https://cdn.jsdmirror.com/gh/${s}@${c}`,name:`jsdmirror`},{base:`https://cdn.jsdelivr.net/gh/${s}@${c}`,name:`jsdelivr`},{base:`https://fastly.jsdelivr.net/gh/${s}@${c}`,name:`fastly.jsdelivr`},{base:`https://gcore.jsdelivr.net/gh/${s}@${c}`,name:`gcore.jsdelivr`}],u={exchange:`exchanges`,network:`networks`,token:`tokens`,wallet:`wallets`},d=o({cdnList:l,fallback:{},path:e=>`aliases/${u[e]}.json`}),f=o({cdnList:l,fallback:[],path:e=>`maps/${u[e]}.json`});function p(e){return String(e??``).trim().toLowerCase()}function m(e){return p(e).replace(/\s+/g,`-`)}function h(e){let t=e?.split(`:`).pop();return t?t.trim():``}function g(e){return[e.id,e.symbol,e.name,e.shortName,h(e.filePath)].map(e=>m(e??``)).filter(Boolean)}function _(e,t){let n=e.find(e=>g(e).includes(t));return n&&(h(n.filePath)||n.id||n.symbol)||null}async function v(e,t){let n=m(t);if(!n)return``;let[r,i]=await Promise.all([f.load(e),d.load(e)]),a=m(i[n]??``);return _(r,n)||(a?_(r,a):null)||i[n]||n}function y(e,t,n={}){let{variant:r=`branded`}=n,i=m(t);return i?`${l[0].base}/assets/${u[e]}/${r}/${i}.svg`:null}async function b(e,t,n={}){let{variant:r=`branded`}=n,i=await v(e,t),a=`assets/${u[e]}/${r}/${i}.svg`;for(let e of l){let t=`${e.base}/${a}`;try{if((await fetch(t,{method:`HEAD`,signal:AbortSignal.timeout(2500)})).ok)return{url:t}}catch{}}return{url:`${l[0].base}/${a}`}}function x(e,t){return t?.trim()||e||`--`}var S=n();function C({type:e,name:t,variant:n=`branded`,size:i=16,width:a,height:o,className:s,alt:c,style:l,onError:u,...d}){let f=y(e,t,{variant:n}),[p,m]=(0,r.useState)(null),[h,g]=(0,r.useState)(f),_=h??f,v=a??i,x=o??i;return(0,r.useEffect)(()=>{let r=!1;return m(null),g(y(e,t,{variant:n})),b(e,t,{variant:n}).then(e=>{r||g(e.url)}),()=>{r=!0}},[t,e,n]),!(_&&t)||p===_?null:(0,S.jsx)(`img`,{alt:c??``,className:s,height:x,onError:e=>{m(_),u?.(e)},src:_,style:{display:`inline-block`,...l},width:v,...d})}export{p as a,l as i,x as n,v as o,u as r,o as s,C as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Kp as n}from"./messages-CokTNgkr.js";var r=e(t(),1);function i(e){let t=new Map,n=new Map;return{get(e){return t.get(e)},load(r){let i=t.get(r);if(i)return Promise.resolve(i);let a=n.get(r);if(a)return a;let o=e(r).then(e=>(t.set(r,e),e)).finally(()=>{n.delete(r)});return n.set(r,o),o}}}async function a(e,t,n){for(let n of e){let e=`${n.base}/${t}`;try{let t=await fetch(e,{signal:AbortSignal.timeout(3e3)});if(t.ok)return await t.json()}catch{}}return n}function o({cdnList:e,fallback:t,path:n}){return i(r=>a(e,n(r),t))}var s=`GMWalletApp/crypto-icons`,c=`latest`,l=[{base:`https://cdn.jsdmirror.com/gh/${s}@${c}`,name:`jsdmirror`},{base:`https://cdn.jsdelivr.net/gh/${s}@${c}`,name:`jsdelivr`},{base:`https://fastly.jsdelivr.net/gh/${s}@${c}`,name:`fastly.jsdelivr`},{base:`https://gcore.jsdelivr.net/gh/${s}@${c}`,name:`gcore.jsdelivr`}],u={exchange:`exchanges`,network:`networks`,token:`tokens`,wallet:`wallets`},d=o({cdnList:l,fallback:{},path:e=>`aliases/${u[e]}.json`}),f=o({cdnList:l,fallback:[],path:e=>`maps/${u[e]}.json`});function p(e){return String(e??``).trim().toLowerCase()}function m(e){return p(e).replace(/\s+/g,`-`)}function h(e){let t=e?.split(`:`).pop();return t?t.trim():``}function g(e){return[e.id,e.symbol,e.name,e.shortName,h(e.filePath)].map(e=>m(e??``)).filter(Boolean)}function _(e,t){let n=e.find(e=>g(e).includes(t));return n&&(h(n.filePath)||n.id||n.symbol)||null}async function v(e,t){let n=m(t);if(!n)return``;let[r,i]=await Promise.all([f.load(e),d.load(e)]),a=m(i[n]??``);return _(r,n)||(a?_(r,a):null)||i[n]||n}function y(e,t,n={}){let{variant:r=`branded`}=n,i=m(t);return i?`${l[0].base}/assets/${u[e]}/${r}/${i}.svg`:null}async function b(e,t,n={}){let{variant:r=`branded`}=n,i=await v(e,t),a=`assets/${u[e]}/${r}/${i}.svg`;for(let e of l){let t=`${e.base}/${a}`;try{if((await fetch(t,{method:`HEAD`,signal:AbortSignal.timeout(2500)})).ok)return{url:t}}catch{}}return{url:`${l[0].base}/${a}`}}function x(e,t){return t?.trim()||e||`--`}var S=n();function C({type:e,name:t,variant:n=`branded`,size:i=16,width:a,height:o,className:s,alt:c,style:l,onError:u,...d}){let f=y(e,t,{variant:n}),[p,m]=(0,r.useState)(null),[h,g]=(0,r.useState)(f),_=h??f,v=a??i,x=o??i;return(0,r.useEffect)(()=>{let r=!1;return m(null),g(y(e,t,{variant:n})),b(e,t,{variant:n}).then(e=>{r||g(e.url)}),()=>{r=!0}},[t,e,n]),!(_&&t)||p===_?null:(0,S.jsx)(`img`,{alt:c??``,className:s,height:x,onError:e=>{m(_),u?.(e)},src:_,style:{display:`inline-block`,...l},width:v,...d})}export{p as a,l as i,x as n,v as o,u as r,o as s,C 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{Pp as e}from"./messages-d1hRBHLP.js";import{i as t,t as n}from"./button-CbdgUDvS.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-BjDKUEMi.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{Kp as e}from"./messages-CokTNgkr.js";import{i as t,t as n}from"./button-DdD9bP3z.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-BTFQXMl4.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 +1 @@
import{Pp 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,pn as l,rn as u,sn as d,un as f}from"./messages-d1hRBHLP.js";import{t as p}from"./button-CbdgUDvS.js";import{t as m}from"./checkbox-_PDlkNfe.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-Db7d571a.js";import{t as D}from"./page-header-DkeFH2Rh.js";import{t as O}from"./show-submitted-data-CfmGDLbU.js";var k=e(),A=[{id:`recents`,label:r()},{id:`home`,label:f()},{id:`applications`,label:o()},{id:`desktop`,label:n()},{id:`downloads`,label:d()},{id:`documents`,label:c()}],j=_({items:g(h()).refine(e=>e.some(e=>e),{message:t()})}),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}; import{Kp 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,pn as l,rn as u,sn as d,un as f}from"./messages-CokTNgkr.js";import{t as p}from"./button-DdD9bP3z.js";import{t as m}from"./checkbox-DKTsjLgh.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-B-HnSRAy.js";import{t as D}from"./page-header-Bgrl_xms.js";import{t as O}from"./show-submitted-data-C7kK9HtD.js";var k=e(),A=[{id:`recents`,label:r()},{id:`home`,label:f()},{id:`applications`,label:o()},{id:`desktop`,label:n()},{id:`downloads`,label:d()},{id:`documents`,label:c()}],j=_({items:g(h()).refine(e=>e.some(e=>e),{message:t()})}),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{Pp as n}from"./messages-d1hRBHLP.js";import{t as r}from"./dist-CXfAGnWR.js";import{u as i}from"./button-CbdgUDvS.js";import{a,r as o,t as s}from"./dist-CIovDv3v.js";import{t as c}from"./dist-DOx-EAQr.js";import{n as l,t as u}from"./dist-qAA-axL4.js";import{n as d}from"./dist-s_CiB-DN.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{Kp as n}from"./messages-CokTNgkr.js";import{t as r}from"./dist-CrzsE82B.js";import{u as i}from"./button-DdD9bP3z.js";import{a,r as o,t as s}from"./dist-CKhUg8lz.js";import{t as c}from"./dist-CQdg-Tnw.js";import{n as l,t as u}from"./dist-r2DO4E6w.js";import{n as d}from"./dist-TZZx9SXO.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};
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{Pp as n}from"./messages-d1hRBHLP.js";import{n as r,r as i,t as a}from"./dist-CXfAGnWR.js";import{u as o}from"./button-CbdgUDvS.js";import{n as s,r as c}from"./dist-CIovDv3v.js";import{t as l}from"./dist-qAA-axL4.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{Kp as n}from"./messages-CokTNgkr.js";import{n as r,r as i,t as a}from"./dist-CrzsE82B.js";import{u as o}from"./button-DdD9bP3z.js";import{n as s,r as c}from"./dist-CKhUg8lz.js";import{t as l}from"./dist-r2DO4E6w.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,4 +1,4 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Pp as n}from"./messages-d1hRBHLP.js";import{t as r}from"./dist-CXfAGnWR.js";import{s as i,u as a}from"./button-CbdgUDvS.js";import{a as o,i as s,r as c,t as l}from"./dist-CIovDv3v.js";import{t as u}from"./dist-xVhEP_Vg.js";import{n as d}from"./dist-qAA-axL4.js";import{n as f,t as p}from"./dist-ByVfo7qC.js";import{i as m,n as h,r as g,t as _}from"./es2015-Chgs9D7p.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{Kp as n}from"./messages-CokTNgkr.js";import{t as r}from"./dist-CrzsE82B.js";import{s as i,u as a}from"./button-DdD9bP3z.js";import{a as o,i as s,r as c,t as l}from"./dist-CKhUg8lz.js";import{t as u}from"./dist-BbSJdbb-.js";import{n as d}from"./dist-r2DO4E6w.js";import{n as f,t as p}from"./dist-BQFHFtr-.js";import{i as m,n as h,r as g,t as _}from"./es2015-Cc8TzKT6.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-CbdgUDvS.js";import{n as r}from"./dist-CIovDv3v.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-DdD9bP3z.js";import{n as r}from"./dist-CKhUg8lz.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}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Pp as n}from"./messages-d1hRBHLP.js";import{t as r}from"./dist-CXfAGnWR.js";import{u as i}from"./button-CbdgUDvS.js";import{a,r as o,t as s}from"./dist-CIovDv3v.js";import{t as c}from"./dist-xVhEP_Vg.js";import{n as l}from"./dist-s_CiB-DN.js";import{t as u}from"./dist-BixeH3nX.js";import{t as d}from"./dist-DuNl0ZwE.js";import{n as f,r as p,t as m}from"./dist-D13PZ7es.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{Kp as n}from"./messages-CokTNgkr.js";import{t as r}from"./dist-CrzsE82B.js";import{u as i}from"./button-DdD9bP3z.js";import{a,r as o,t as s}from"./dist-CKhUg8lz.js";import{t as c}from"./dist-BbSJdbb-.js";import{n as l}from"./dist-TZZx9SXO.js";import{t as u}from"./dist-BixeH3nX.js";import{t as d}from"./dist-DIWGcwtg.js";import{n as f,r as p,t as m}from"./dist-3039QLji.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{Pp as n}from"./messages-d1hRBHLP.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{Kp as n}from"./messages-CokTNgkr.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};
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Pp as n}from"./messages-d1hRBHLP.js";import{s as r,u as i}from"./button-CbdgUDvS.js";import{a}from"./dist-CIovDv3v.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{Kp as n}from"./messages-CokTNgkr.js";import{s as r,u as i}from"./button-DdD9bP3z.js";import{a}from"./dist-CKhUg8lz.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};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r as e,t}from"./chunk-DECur_0Z.js";import{t as n}from"./react-CO2uhaBc.js";import{Pp as r}from"./messages-d1hRBHLP.js";import{s as i}from"./button-CbdgUDvS.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{Kp as r}from"./messages-CokTNgkr.js";import{s as i}from"./button-DdD9bP3z.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{n}from"./dist-CIovDv3v.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-CKhUg8lz.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 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Pp as n}from"./messages-d1hRBHLP.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{Kp as n}from"./messages-CokTNgkr.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{Pp as n}from"./messages-d1hRBHLP.js";import{t as r}from"./dist-CXfAGnWR.js";var i=e(t(),1),a=n(),o=Object.freeze({position:`absolute`,border:0,width:1,height:1,padding:0,margin:-1,overflow:`hidden`,clip:`rect(0, 0, 0, 0)`,whiteSpace:`nowrap`,wordWrap:`normal`}),s=`VisuallyHidden`,c=i.forwardRef((e,t)=>(0,a.jsx)(r.span,{...e,ref:t,style:{...o,...e.style}}));c.displayName=s;var l=c;export{o as n,l as t}; import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{Kp as n}from"./messages-CokTNgkr.js";import{t as r}from"./dist-CrzsE82B.js";var i=e(t(),1),a=n(),o=Object.freeze({position:`absolute`,border:0,width:1,height:1,padding:0,margin:-1,overflow:`hidden`,clip:`rect(0, 0, 0, 0)`,whiteSpace:`nowrap`,wordWrap:`normal`}),s=`VisuallyHidden`,c=i.forwardRef((e,t)=>(0,a.jsx)(r.span,{...e,ref:t,style:{...o,...e.style}}));c.displayName=s;var l=c;export{o as n,l as t};
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{t}from"./react-CO2uhaBc.js";import{n}from"./dist-CIovDv3v.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-CKhUg8lz.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
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{Pp as e,lt as t}from"./messages-d1hRBHLP.js";import{i as n}from"./button-CbdgUDvS.js";import{t as r}from"./createLucideIcon-Br0Bd5k2.js";var i=r(`inbox`,[[`polyline`,{points:`22 12 16 12 14 15 10 15 8 12 2 12`,key:`o97t9d`}],[`path`,{d:`M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z`,key:`oot6mr`}]]),a=r(`refresh-cw`,[[`path`,{d:`M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8`,key:`v9h5vc`}],[`path`,{d:`M21 3v5h-5`,key:`1q7to0`}],[`path`,{d:`M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16`,key:`3uifl3`}],[`path`,{d:`M8 16H3v5`,key:`1cv678`}]]),o=r(`sliders-horizontal`,[[`path`,{d:`M10 5H3`,key:`1qgfaw`}],[`path`,{d:`M12 19H3`,key:`yhmn1j`}],[`path`,{d:`M14 3v4`,key:`1sua03`}],[`path`,{d:`M16 17v4`,key:`1q0r14`}],[`path`,{d:`M21 12h-9`,key:`1o4lsq`}],[`path`,{d:`M21 19h-5`,key:`1rlt1p`}],[`path`,{d:`M21 5h-7`,key:`1oszz2`}],[`path`,{d:`M8 10v4`,key:`tgpxqk`}],[`path`,{d:`M8 12H3`,key:`a7s4jb`}]]),s=e();function c({className:e,description:r=t(),icon:a,title:o}){return(0,s.jsxs)(`div`,{className:n(`flex flex-col items-center justify-center gap-3 py-16 text-center`,e),children:[(0,s.jsx)(`div`,{className:`flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground`,children:a??(0,s.jsx)(i,{className:`size-6`})}),o&&(0,s.jsx)(`p`,{className:`font-medium text-sm`,children:o}),(0,s.jsx)(`p`,{className:`text-muted-foreground text-sm`,children:r})]})}export{o as n,a as r,c as t}; import{Kp as e,lt as t}from"./messages-CokTNgkr.js";import{i as n}from"./button-DdD9bP3z.js";import{t as r}from"./createLucideIcon-Br0Bd5k2.js";var i=r(`inbox`,[[`polyline`,{points:`22 12 16 12 14 15 10 15 8 12 2 12`,key:`o97t9d`}],[`path`,{d:`M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z`,key:`oot6mr`}]]),a=r(`refresh-cw`,[[`path`,{d:`M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8`,key:`v9h5vc`}],[`path`,{d:`M21 3v5h-5`,key:`1q7to0`}],[`path`,{d:`M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16`,key:`3uifl3`}],[`path`,{d:`M8 16H3v5`,key:`1cv678`}]]),o=r(`sliders-horizontal`,[[`path`,{d:`M10 5H3`,key:`1qgfaw`}],[`path`,{d:`M12 19H3`,key:`yhmn1j`}],[`path`,{d:`M14 3v4`,key:`1sua03`}],[`path`,{d:`M16 17v4`,key:`1q0r14`}],[`path`,{d:`M21 12h-9`,key:`1o4lsq`}],[`path`,{d:`M21 19h-5`,key:`1rlt1p`}],[`path`,{d:`M21 5h-7`,key:`1oszz2`}],[`path`,{d:`M8 10v4`,key:`tgpxqk`}],[`path`,{d:`M8 12H3`,key:`a7s4jb`}]]),s=e();function c({className:e,description:r=t(),icon:a,title:o}){return(0,s.jsxs)(`div`,{className:n(`flex flex-col items-center justify-center gap-3 py-16 text-center`,e),children:[(0,s.jsx)(`div`,{className:`flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground`,children:a??(0,s.jsx)(i,{className:`size-6`})}),o&&(0,s.jsx)(`p`,{className:`font-medium text-sm`,children:o}),(0,s.jsx)(`p`,{className:`text-muted-foreground text-sm`,children:r})]})}export{o as n,a as r,c as t};
@@ -1 +1 @@
import{r as e}from"./chunk-DECur_0Z.js";import{a as t}from"./auth-store-GibZbQPL.js";import{t as n}from"./react-CO2uhaBc.js";import{Mn as r,Nn as i,Pp as a,cf as o,df as s,ff as c,jn as l,lf as u,of as d,sf as f,uf as p}from"./messages-d1hRBHLP.js";import{t as m}from"./button-CbdgUDvS.js";import{a as h,i as g,n as _,r as v,t as y}from"./select-Cv_sRi68.js";import{c as b,s as x}from"./zod-CQemhjci.js";import{a as S,c as C,i as w,l as T,n as E,o as D,s as O,t as k}from"./form-Db7d571a.js";import{t as A}from"./input-CusCjhBW.js";import{t as j}from"./page-header-DkeFH2Rh.js";import{n as M,t as N}from"./labels-CfnKbsNx.js";import{a as P,i as F,n as I,t as L}from"./lib-paj-DJMX.js";var R=e(n(),1),z=a(),B=x({defaultToken:b().trim().min(1,o()),defaultCurrency:b().trim().min(1,f()),defaultNetwork:b().trim().min(1,d())});function V(){let e=F(`epay`),n=t.useQuery(`get`,`/admin/api/v1/config`),i=P(),a=(0,R.useRef)(``),o=T({resolver:C(B),defaultValues:{defaultToken:`usdt`,defaultCurrency:`CNY`,defaultNetwork:`TRON`}}),c=(0,R.useMemo)(()=>n.data?.data?.supported_assets??[],[n.data?.data?.supported_assets]),d=(0,R.useMemo)(()=>H(c),[c]),f=o.watch(`defaultNetwork`),b=(0,R.useMemo)(()=>U(c,f),[c,f]);(0,R.useEffect)(()=>{let t=e.data?.data;if(!(t&&!n.isLoading))return;let r=`${e.dataUpdatedAt}:${n.dataUpdatedAt}`;if(a.current===r)return;a.current=r;let i=L(t,`epay.default_network`).trim(),s=d.find(e=>q(e.network,i))?.network||d[0]?.network||i||`TRON`,l=U(c,s),u=L(t,`epay.default_token`).trim(),f=l.find(e=>q(e.value,u))?.value||(u&&l.length===0?u.toLowerCase():l[0]?.value||`usdt`);o.reset({defaultToken:f,defaultCurrency:L(t,`epay.default_currency`,`CNY`).trim().toUpperCase(),defaultNetwork:s})},[n.dataUpdatedAt,n.isLoading,o,d,e.dataUpdatedAt,e.data,c]);async function x(t){await I(i.mutateAsync,[{group:`epay`,key:`epay.default_token`,type:`string`,value:t.defaultToken.toLowerCase()},{group:`epay`,key:`epay.default_currency`,type:`string`,value:t.defaultCurrency.toLowerCase()},{group:`epay`,key:`epay.default_network`,type:`string`,value:t.defaultNetwork.toLowerCase()}],r()),await e.refetch()}return(0,z.jsx)(k,{...o,children:(0,z.jsxs)(`form`,{className:`space-y-6`,onSubmit:o.handleSubmit(x),children:[(0,z.jsxs)(`div`,{className:`grid gap-6 md:grid-cols-2`,children:[(0,z.jsx)(w,{control:o.control,name:`defaultNetwork`,render:({field:e})=>{let t=G(d,e.value),r=t?.network||e.value;return(0,z.jsxs)(S,{children:[(0,z.jsx)(D,{children:u()}),(0,z.jsxs)(y,{disabled:n.isLoading||d.length===0,onValueChange:t=>{e.onChange(t);let n=U(c,t)[0]?.value;o.setValue(`defaultToken`,n??``,{shouldDirty:!0,shouldValidate:!0})},value:e.value,children:[(0,z.jsx)(E,{children:(0,z.jsxs)(g,{className:`w-full`,children:[(0,z.jsx)(`span`,{className:`sr-only`,children:(0,z.jsx)(h,{})}),(0,z.jsx)(`span`,{className:`pointer-events-none`,children:(0,z.jsx)(N,{displayName:t?.displayName,network:r||`TRON`})})]})}),(0,z.jsx)(_,{children:d.map(e=>(0,z.jsx)(v,{textValue:W(e),value:e.network,children:(0,z.jsx)(`span`,{className:`pointer-events-none`,children:(0,z.jsx)(N,{displayName:e.displayName,network:e.network})})},e.network))})]}),(0,z.jsx)(O,{})]})}}),(0,z.jsx)(w,{control:o.control,name:`defaultToken`,render:({field:e})=>{let t=K(b,e.value);return(0,z.jsxs)(S,{children:[(0,z.jsx)(D,{children:s()}),(0,z.jsxs)(y,{disabled:n.isLoading||b.length===0,onValueChange:e.onChange,value:e.value,children:[(0,z.jsx)(E,{children:(0,z.jsxs)(g,{className:`w-full`,children:[(0,z.jsx)(`span`,{className:`sr-only`,children:(0,z.jsx)(h,{})}),(0,z.jsx)(`span`,{className:`pointer-events-none`,children:(0,z.jsx)(M,{token:t||`USDT`})})]})}),(0,z.jsx)(_,{children:b.map(e=>(0,z.jsx)(v,{textValue:e.displayName,value:e.value,children:(0,z.jsx)(`span`,{className:`pointer-events-none`,children:(0,z.jsx)(M,{token:e.displayName})})},e.value))})]}),(0,z.jsx)(O,{})]})}})]}),(0,z.jsx)(w,{control:o.control,name:`defaultCurrency`,render:({field:e})=>(0,z.jsxs)(S,{children:[(0,z.jsx)(D,{children:p()}),(0,z.jsx)(E,{children:(0,z.jsx)(A,{placeholder:`CNY`,...e})}),(0,z.jsx)(O,{})]})}),(0,z.jsx)(m,{disabled:i.isPending||e.isLoading,type:`submit`,children:l()})]})})}function H(e){let t=new Map;for(let n of e){if(!n.network)continue;let e=n.network.toLowerCase();t.has(e)||t.set(e,{displayName:n.display_name,network:n.network})}return Array.from(t.values())}function U(e,t){let n=new Map;for(let r of e)if(q(r.network,t))for(let e of r.tokens??[]){if(!e)continue;let t=e.toLowerCase();n.has(t)||n.set(t,{displayName:e.toUpperCase(),value:t})}return Array.from(n.values())}function W(e){return e.displayName?.trim()||e.network}function G(e,t){return e.find(e=>q(e.network,t))}function K(e,t){return t?e.find(e=>q(e.value,t))?.displayName||t.toUpperCase():``}function q(e,t){return String(e??``).toLowerCase()===String(t??``).toLowerCase()}function J(){return(0,z.jsx)(j,{description:i(),title:c(),variant:`section`,children:(0,z.jsx)(V,{})})}var Y=J;export{Y as component}; import{r as e}from"./chunk-DECur_0Z.js";import{a as t}from"./auth-store-C6nFFVPp.js";import{t as n}from"./react-CO2uhaBc.js";import{Cf as r,Kp as i,Mn as a,Nn as o,Sf as s,bf as c,jn as l,vf as u,wf as d,xf as f,yf as p}from"./messages-CokTNgkr.js";import{t as m}from"./button-DdD9bP3z.js";import{a as h,i as g,n as _,r as v,t as y}from"./select-CjMGKVW5.js";import{c as b,s as x}from"./zod-CQemhjci.js";import{a as S,c as C,i as w,l as T,n as E,o as D,s as O,t as k}from"./form-B-HnSRAy.js";import{t as A}from"./input-BdArIRZ9.js";import{t as j}from"./page-header-Bgrl_xms.js";import{n as M,t as N}from"./labels-Baqaexjy.js";import{a as P,i as F,n as I,t as L}from"./lib-CDQ-O113.js";var R=e(n(),1),z=i(),B=x({defaultToken:b().trim().min(1,c()),defaultCurrency:b().trim().min(1,p()),defaultNetwork:b().trim().min(1,u())});function V(){let e=F(`epay`),n=t.useQuery(`get`,`/admin/api/v1/config`),i=P(),o=(0,R.useRef)(``),c=T({resolver:C(B),defaultValues:{defaultToken:`usdt`,defaultCurrency:`CNY`,defaultNetwork:`TRON`}}),u=(0,R.useMemo)(()=>n.data?.data?.supported_assets??[],[n.data?.data?.supported_assets]),d=(0,R.useMemo)(()=>H(u),[u]),p=c.watch(`defaultNetwork`),b=(0,R.useMemo)(()=>U(u,p),[u,p]);(0,R.useEffect)(()=>{let t=e.data?.data;if(!(t&&!n.isLoading))return;let r=`${e.dataUpdatedAt}:${n.dataUpdatedAt}`;if(o.current===r)return;o.current=r;let i=L(t,`epay.default_network`).trim(),a=d.find(e=>q(e.network,i))?.network||d[0]?.network||i||`TRON`,s=U(u,a),l=L(t,`epay.default_token`).trim(),f=s.find(e=>q(e.value,l))?.value||(l&&s.length===0?l.toLowerCase():s[0]?.value||`usdt`);c.reset({defaultToken:f,defaultCurrency:L(t,`epay.default_currency`,`CNY`).trim().toUpperCase(),defaultNetwork:a})},[n.dataUpdatedAt,n.isLoading,c,d,e.dataUpdatedAt,e.data,u]);async function x(t){await I(i.mutateAsync,[{group:`epay`,key:`epay.default_token`,type:`string`,value:t.defaultToken.toLowerCase()},{group:`epay`,key:`epay.default_currency`,type:`string`,value:t.defaultCurrency.toLowerCase()},{group:`epay`,key:`epay.default_network`,type:`string`,value:t.defaultNetwork.toLowerCase()}],a()),await e.refetch()}return(0,z.jsx)(k,{...c,children:(0,z.jsxs)(`form`,{className:`space-y-6`,onSubmit:c.handleSubmit(x),children:[(0,z.jsxs)(`div`,{className:`grid gap-6 md:grid-cols-2`,children:[(0,z.jsx)(w,{control:c.control,name:`defaultNetwork`,render:({field:e})=>{let t=G(d,e.value),r=t?.network||e.value;return(0,z.jsxs)(S,{children:[(0,z.jsx)(D,{children:f()}),(0,z.jsxs)(y,{disabled:n.isLoading||d.length===0,onValueChange:t=>{e.onChange(t);let n=U(u,t)[0]?.value;c.setValue(`defaultToken`,n??``,{shouldDirty:!0,shouldValidate:!0})},value:e.value,children:[(0,z.jsx)(E,{children:(0,z.jsxs)(g,{className:`w-full`,children:[(0,z.jsx)(`span`,{className:`sr-only`,children:(0,z.jsx)(h,{})}),(0,z.jsx)(`span`,{className:`pointer-events-none`,children:(0,z.jsx)(N,{displayName:t?.displayName,network:r||`TRON`})})]})}),(0,z.jsx)(_,{children:d.map(e=>(0,z.jsx)(v,{textValue:W(e),value:e.network,children:(0,z.jsx)(`span`,{className:`pointer-events-none`,children:(0,z.jsx)(N,{displayName:e.displayName,network:e.network})})},e.network))})]}),(0,z.jsx)(O,{})]})}}),(0,z.jsx)(w,{control:c.control,name:`defaultToken`,render:({field:e})=>{let t=K(b,e.value);return(0,z.jsxs)(S,{children:[(0,z.jsx)(D,{children:r()}),(0,z.jsxs)(y,{disabled:n.isLoading||b.length===0,onValueChange:e.onChange,value:e.value,children:[(0,z.jsx)(E,{children:(0,z.jsxs)(g,{className:`w-full`,children:[(0,z.jsx)(`span`,{className:`sr-only`,children:(0,z.jsx)(h,{})}),(0,z.jsx)(`span`,{className:`pointer-events-none`,children:(0,z.jsx)(M,{token:t||`USDT`})})]})}),(0,z.jsx)(_,{children:b.map(e=>(0,z.jsx)(v,{textValue:e.displayName,value:e.value,children:(0,z.jsx)(`span`,{className:`pointer-events-none`,children:(0,z.jsx)(M,{token:e.displayName})})},e.value))})]}),(0,z.jsx)(O,{})]})}})]}),(0,z.jsx)(w,{control:c.control,name:`defaultCurrency`,render:({field:e})=>(0,z.jsxs)(S,{children:[(0,z.jsx)(D,{children:s()}),(0,z.jsx)(E,{children:(0,z.jsx)(A,{placeholder:`CNY`,...e})}),(0,z.jsx)(O,{})]})}),(0,z.jsx)(m,{disabled:i.isPending||e.isLoading,type:`submit`,children:l()})]})})}function H(e){let t=new Map;for(let n of e){if(!n.network)continue;let e=n.network.toLowerCase();t.has(e)||t.set(e,{displayName:n.display_name,network:n.network})}return Array.from(t.values())}function U(e,t){let n=new Map;for(let r of e)if(q(r.network,t))for(let e of r.tokens??[]){if(!e)continue;let t=e.toLowerCase();n.has(t)||n.set(t,{displayName:e.toUpperCase(),value:t})}return Array.from(n.values())}function W(e){return e.displayName?.trim()||e.network}function G(e,t){return e.find(e=>q(e.network,t))}function K(e,t){return t?e.find(e=>q(e.value,t))?.displayName||t.toUpperCase():``}function q(e,t){return String(e??``).toLowerCase()===String(t??``).toLowerCase()}function J(){return(0,z.jsx)(j,{description:o(),title:d(),variant:`section`,children:(0,z.jsx)(V,{})})}var Y=J;export{Y as component};

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