20 Commits

Author SHA1 Message Date
line-6000 ff157c33d9 Remove database config for PostgreSQL and MySQL
Removed PostgreSQL and MySQL configuration settings from the example environment file.
2026-05-09 21:46:44 +08:00
line-6000 51ad344b36 Merge pull request #62 from GMWalletApp/pay 2026-05-09 21:28:42 +08:00
mcmc 3623a543bb feat: expose site config in public/admin config responses 2026-05-09 19:01:53 +08:00
mcmc 377176a0f2 feat: integrate okpay switch-network flow and notify handling 2026-05-09 18:06:27 +08:00
line-6000 a4022df7b4 Change logo source
Updated logo source URL in README.
2026-05-07 17:27:39 +08:00
line-6000 32aa1b66f5 Clean up Dockerfile by removing static file copies
Remove redundant COPY commands for static files.
2026-05-02 01:51:40 +08:00
Astra3456 53e66bad35 Update disclaimer for clarity and completeness
Clarify the disclaimer regarding the use of the project and its risks.
2026-04-29 17:15:56 +10:00
line-6000 ff68279b0f Merge pull request #59 from GMWalletApp/dev
feat(pay): switch checkout counter to SPA redirect + JSON API
2026-04-29 00:36:12 +08:00
line-6000 9c533a7ff8 feat(pay): switch checkout counter to SPA redirect + JSON API
- redirect `/pay/checkout-counter/:trade_id` to `/cashier/:trade_id`
- add `/pay/checkout-counter-resp/:trade_id` for checkout JSON payload
- remove embedded `static` release and keep `www` as the web bundle source
- regenerate PWA/www assets and move chain icons to `/www/images/chains`
2026-04-29 00:34:58 +08:00
line-6000 785a162f88 Merge pull request #58 from GMWalletApp/hot_fix_0427
Hot fix 0427
2026-04-28 19:14:58 +08:00
Ginta 06933451c5 actions 2026-04-28 16:28:01 +08:00
line-6000 d17d212c24 change tron scan logic 2026-04-28 15:28:00 +08:00
Ginta 51a2accd9f 🐛 fix link 2026-04-27 15:57:26 +08:00
Ginta 96ea7481bc Revert "Revert "new trc (#54)""
This reverts commit 74c3a3a08c.
2026-04-27 15:52:50 +08:00
Ginta de9c45a944 static 2026-04-27 15:52:12 +08:00
Ginta 9e885d8b2f order notify 2026-04-27 15:43:35 +08:00
Ginta 74c3a3a08c Revert "new trc (#54)"
This reverts commit 0a19854602.
2026-04-27 00:04:05 +08:00
Ginta 0a19854602 new trc (#54)
* new trct
2026-04-26 23:32:43 +08:00
line-6000 67263da0f1 Merge pull request #51 from GMWalletApp/dev 2026-04-25 18:49:41 +08:00
line-6000 95879e33d4 refactor(routes):
- keep install-mode root redirect to /install and add tests covering install root redirect and install page serving
- change root backend handler from Any("/") to POST("/") so GET / can consistently serve the SPA after install

Co-authored-by: Copilot <copilot@github.com>
2026-04-25 18:48:28 +08:00
236 changed files with 2930 additions and 2310 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
images: ${{ env.DOCKERHUB_NAMESPACE }}/${{ env.IMAGE_NAME }} images: ${{ env.DOCKERHUB_NAMESPACE }}/${{ env.IMAGE_NAME }}
tags: | tags: |
type=raw,value=alpine type=raw,value=alpine
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest
type=ref,event=branch,suffix=-alpine type=ref,event=branch,suffix=-alpine
type=ref,event=tag type=ref,event=tag
type=sha,prefix=sha-,suffix=-alpine type=sha,prefix=sha-,suffix=-alpine
-2
View File
@@ -17,8 +17,6 @@ RUN apk --no-cache add ca-certificates tzdata
ARG API_RATE_URL="" ARG API_RATE_URL=""
WORKDIR /app WORKDIR /app
COPY --from=builder /app/src/static /app/static
COPY --from=builder /app/src/static /static
COPY --from=builder /app/src/.env.example /app/.env COPY --from=builder /app/src/.env.example /app/.env
RUN if [ -n "$API_RATE_URL" ]; then \ RUN if [ -n "$API_RATE_URL" ]; then \
sed -i "s|^api_rate_url=.*$|api_rate_url=${API_RATE_URL}|" /app/.env; \ sed -i "s|^api_rate_url=.*$|api_rate_url=${API_RATE_URL}|" /app/.env; \
+2 -2
View File
@@ -1,7 +1,7 @@
# Epusdt — Easy Payment USDT # Epusdt — Easy Payment USDT
<p align="center"> <p align="center">
<img src="wiki/img/usdtlogo.png" alt="Epusdt Logo - Multi-chain Crypto Payment Gateway" width="120"> <img src="https://gmwallet.app/favicon.png" alt="Epusdt Logo - Multi-chain Crypto Payment Gateway" width="120">
</p> </p>
<p align="center"> <p align="center">
@@ -155,7 +155,7 @@ Epusdt 遵守 [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html) 开源协议。
## ⚠️ 免责声明 ## ⚠️ 免责声明
本项目仅供学习与技术交流使用,用户在使用过程中需自行遵守所在地法律法规。由于涉及加密资产及资金安全,用户应自行审查相关代码与风险。加密资产属于高风险新兴资产(包括稳定币),其价值可能波动甚至归零,GMwallet 不对任何资产或使用结果作出保证。本内容不构成任何投资、税务、法律或金融建议,仅供教育用途,相关决策请咨询专业人士。 本项目仅供学习与技术交流使用,用户在使用过程中需自行遵守所在地法律法规。由于涉及加密资产及资金安全,用户应自行审查相关代码与风险。加密资产属于高风险新兴资产(包括稳定币),其价值可能波动甚至归零,GM Wallet 不对任何资产或使用结果作出保证。本内容不构成任何投资、税务、法律或金融建议,所有代码均按原样提供,仅供教育用途,相关决策请咨询专业人士。
--- ---
-21
View File
@@ -20,27 +20,6 @@ db_type=sqlite
sqlite_database_filename= sqlite_database_filename=
sqlite_table_prefix= sqlite_table_prefix=
# postgres config
postgres_host=127.0.0.1
postgres_port=3306
postgres_user=mysql_user
postgres_passwd=mysql_password
postgres_database=database_name
postgres_table_prefix=
postgres_max_idle_conns=10
postgres_max_open_conns=100
postgres_max_life_time=6
# mysql config
mysql_host=127.0.0.1
mysql_port=3306
mysql_user=mysql_user
mysql_passwd=mysql_password
mysql_database=database_name
mysql_table_prefix=
mysql_max_idle_conns=10
mysql_max_open_conns=100
mysql_max_life_time=6
# sqlite runtime store config # sqlite runtime store config
runtime_sqlite_filename=epusdt-runtime.db runtime_sqlite_filename=epusdt-runtime.db
+91
View File
@@ -3,6 +3,7 @@ package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"log" "log"
"net/url" "net/url"
"os" "os"
@@ -62,6 +63,9 @@ func Init() {
LogLevel = normalizeLogLevel(viper.GetString("log_level")) LogLevel = normalizeLogLevel(viper.GetString("log_level"))
StaticPath = normalizeStaticURLPath(viper.GetString("static_path")) StaticPath = normalizeStaticURLPath(viper.GetString("static_path"))
StaticFilePath = filepath.Join(configRootPath, strings.TrimPrefix(StaticPath, "/")) StaticFilePath = filepath.Join(configRootPath, strings.TrimPrefix(StaticPath, "/"))
if err = ensureConfiguredStaticFiles(); err != nil {
panic(err)
}
RuntimePath = resolvePathFromBase(configRootPath, viper.GetString("runtime_root_path"), filepath.Join(configRootPath, "runtime")) RuntimePath = resolvePathFromBase(configRootPath, viper.GetString("runtime_root_path"), filepath.Join(configRootPath, "runtime"))
LogSavePath = resolvePathFromBase(RuntimePath, viper.GetString("log_save_path"), filepath.Join(RuntimePath, "logs")) LogSavePath = resolvePathFromBase(RuntimePath, viper.GetString("log_save_path"), filepath.Join(RuntimePath, "logs"))
mustMkdir(RuntimePath) mustMkdir(RuntimePath)
@@ -82,6 +86,93 @@ func mustMkdir(path string) {
} }
} }
func ensureConfiguredStaticFiles() error {
if strings.TrimSpace(StaticFilePath) == "" {
return nil
}
exePath, err := os.Executable()
if err != nil {
return err
}
exePath, err = filepath.EvalSymlinks(exePath)
if err != nil {
return err
}
srcDir := filepath.Join(filepath.Dir(exePath), "static")
srcInfo, err := os.Stat(srcDir)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
if !srcInfo.IsDir() {
return nil
}
srcAbs, err := filepath.Abs(srcDir)
if err != nil {
return err
}
dstAbs, err := filepath.Abs(StaticFilePath)
if err != nil {
return err
}
if srcAbs == dstAbs {
return nil
}
return copyMissingStaticFiles(srcAbs, dstAbs)
}
func copyMissingStaticFiles(srcDir, dstDir string) error {
return filepath.WalkDir(srcDir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(srcDir, path)
if err != nil {
return err
}
dstPath := filepath.Join(dstDir, rel)
if d.IsDir() {
return os.MkdirAll(dstPath, 0o755)
}
if _, err = os.Stat(dstPath); err == nil {
return nil
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
if err = os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
return err
}
return copyFile(path, dstPath)
})
}
func copyFile(srcPath, dstPath string) error {
in, err := os.Open(srcPath)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(dstPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
if err != nil {
if errors.Is(err, os.ErrExist) {
return nil
}
return err
}
if _, err = io.Copy(out, in); err != nil {
_ = out.Close()
return err
}
return out.Close()
}
func normalizeLogLevel(level string) string { func normalizeLogLevel(level string) string {
switch strings.ToLower(strings.TrimSpace(level)) { switch strings.ToLower(strings.TrimSpace(level)) {
case "debug", "info", "warn", "error": case "debug", "info", "warn", "error":
+54
View File
@@ -177,6 +177,60 @@ func TestResolveConfigFilePathPrefersExplicitOverEnv(t *testing.T) {
} }
} }
func TestCopyMissingStaticFilesCopiesAssetsWithoutOverwriting(t *testing.T) {
root := t.TempDir()
src := filepath.Join(root, "app-static")
dst := filepath.Join(root, "data-static")
if err := os.MkdirAll(filepath.Join(src, "images"), 0o755); err != nil {
t.Fatalf("mkdir source: %v", err)
}
if err := os.WriteFile(filepath.Join(src, "index.html"), []byte("source-index"), 0o644); err != nil {
t.Fatalf("write source index: %v", err)
}
if err := os.WriteFile(filepath.Join(src, "payment.js"), []byte("source-payment"), 0o644); err != nil {
t.Fatalf("write source payment: %v", err)
}
if err := os.WriteFile(filepath.Join(src, "images", "logo.png"), []byte("source-logo"), 0o644); err != nil {
t.Fatalf("write source logo: %v", err)
}
if err := os.MkdirAll(dst, 0o755); err != nil {
t.Fatalf("mkdir destination: %v", err)
}
if err := os.WriteFile(filepath.Join(dst, "index.html"), []byte("custom-index"), 0o644); err != nil {
t.Fatalf("write existing destination index: %v", err)
}
if err := copyMissingStaticFiles(src, dst); err != nil {
t.Fatalf("copy missing static files: %v", err)
}
index, err := os.ReadFile(filepath.Join(dst, "index.html"))
if err != nil {
t.Fatalf("read destination index: %v", err)
}
if string(index) != "custom-index" {
t.Fatalf("existing index was overwritten: %q", string(index))
}
payment, err := os.ReadFile(filepath.Join(dst, "payment.js"))
if err != nil {
t.Fatalf("read copied payment: %v", err)
}
if string(payment) != "source-payment" {
t.Fatalf("payment.js = %q, want source-payment", string(payment))
}
logo, err := os.ReadFile(filepath.Join(dst, "images", "logo.png"))
if err != nil {
t.Fatalf("read copied nested asset: %v", err)
}
if string(logo) != "source-logo" {
t.Fatalf("logo.png = %q, want source-logo", string(logo))
}
}
func TestGetUsdtRatePrefersPositiveAdminOverride(t *testing.T) { func TestGetUsdtRatePrefersPositiveAdminOverride(t *testing.T) {
viper.Reset() viper.Reset()
t.Cleanup(viper.Reset) t.Cleanup(viper.Reset)
+22 -8
View File
@@ -24,17 +24,30 @@ import (
// 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" (default)
// //
// - group=okpay:
// okpay.enabled (bool) — enable OkPay as a switch-network payment option
// okpay.shop_id (string) — OkPay merchant/shop identifier
// okpay.shop_token (string) — OkPay signing token
// okpay.api_url (string) — OkPay API base URL
// okpay.callback_url (string) — server callback URL used for OkPay notify
// okpay.return_url (string) — optional default browser return URL after payment
// okpay.timeout_seconds (int) — outbound OkPay API timeout in seconds
// okpay.allow_tokens (string) — comma-separated allowed tokens, e.g. "USDT,TRX"
//
// - group=brand: // - group=brand:
// brand.site_name (string) — site display name // brand.checkout_name (string) — cashier display name (preferred)
// brand.logo_url (string) — logo image URL // brand.logo_url (string) — logo image URL
// brand.page_title (string) — payment page title // brand.site_title (string) — payment page title / website title (preferred)
// brand.pay_success_text (string) — text shown on payment success // brand.success_copy (string) — text shown on payment success (preferred)
// brand.support_url (string) — support / help URL // brand.support_url (string) — support / help URL
// brand.site_name (string) — legacy cashier/site display name
// brand.page_title (string) — legacy payment page title
// brand.pay_success_text (string) — legacy success text
// //
// - group=system: // - group=system:
// system.order_expiration_time (int) — order expiry in minutes // system.order_expiration_time (int) — order expiry in minutes
type SettingUpsertItem struct { type SettingUpsertItem struct {
Group string `json:"group" enums:"brand,rate,system,epay" 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"`
Value string `json:"value" example:"tron"` Value string `json:"value" example:"tron"`
Type string `json:"type" enums:"string,int,bool,json" example:"string"` Type string `json:"type" enums:"string,int,bool,json" example:"string"`
@@ -48,12 +61,12 @@ type SettingsUpsertRequest struct {
// ListSettings returns all rows, optionally filtered by group. // ListSettings returns all rows, optionally filtered by group.
// @Summary List settings // @Summary List settings
// @Description Returns all settings, optionally filtered by group. // @Description Returns all settings, optionally filtered by group.
// @Description Available groups: brand, rate, system, epay. // @Description Available groups: brand, rate, system, epay, okpay.
// @Description See SettingUpsertItem for the full list of supported keys per group. // @Description See SettingUpsertItem for the full list of supported keys per group.
// @Tags Admin Settings // @Tags Admin Settings
// @Security AdminJWT // @Security AdminJWT
// @Produce json // @Produce json
// @Param group query string false "Group filter (brand|rate|system|epay)" // @Param group query string false "Group filter (brand|rate|system|epay|okpay)"
// @Success 200 {object} response.ApiResponse{data=[]mdb.Setting} // @Success 200 {object} response.ApiResponse{data=[]mdb.Setting}
// @Failure 400 {object} response.ApiResponse // @Failure 400 {object} response.ApiResponse
// @Router /admin/api/v1/settings [get] // @Router /admin/api/v1/settings [get]
@@ -71,10 +84,11 @@ func (c *BaseAdminController) ListSettings(ctx echo.Context) error {
// ones. Errors are returned per-key so the UI can surface them. // ones. Errors are returned per-key so the UI can surface them.
// @Summary Upsert settings // @Summary Upsert settings
// @Description Batch insert/update settings. Returns per-key status. // @Description Batch insert/update settings. Returns per-key status.
// @Description Supported groups: brand, rate, system, epay. // @Description Supported groups: brand, rate, system, epay, okpay.
// @Description epay group keys: epay.default_token (e.g. "usdt"), epay.default_currency (e.g. "cny"), epay.default_network (e.g. "tron"). // @Description epay group keys: epay.default_token (e.g. "usdt"), epay.default_currency (e.g. "cny"), epay.default_network (e.g. "tron").
// @Description okpay group keys: okpay.enabled, okpay.shop_id, okpay.shop_token, okpay.api_url, okpay.callback_url, okpay.return_url, okpay.timeout_seconds, okpay.allow_tokens.
// @Description rate group keys: rate.forced_usdt_rate (>0 overrides USDT/CNY; <=0 uses rate.api_url), rate.api_url, rate.adjust_percent, rate.okx_c2c_enabled. // @Description rate group keys: rate.forced_usdt_rate (>0 overrides USDT/CNY; <=0 uses rate.api_url), rate.api_url, rate.adjust_percent, rate.okx_c2c_enabled.
// @Description brand group keys: brand.site_name, brand.logo_url, brand.page_title, brand.pay_success_text, brand.support_url. // @Description brand group keys: brand.checkout_name, brand.logo_url, brand.site_title, brand.success_copy, brand.support_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. // @Description system group keys: system.order_expiration_time.
// @Tags Admin Settings // @Tags Admin Settings
// @Security AdminJWT // @Security AdminJWT
+154
View File
@@ -0,0 +1,154 @@
package comm
import (
"bytes"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/GMWalletApp/epusdt/model/service"
"github.com/GMWalletApp/epusdt/util/log"
"github.com/labstack/echo/v4"
)
// OkPayNotify receives the server-to-server OkPay payment callback.
// The handler accepts JSON or form-urlencoded payloads and returns plain-text
// acknowledgements expected by the upstream provider.
// @Summary OkPay notify callback
// @Description Receives OkPay deposit callbacks for hosted checkout child orders.
// @Description The callback is verified against the configured OkPay shop token and, on success, marks the matching child order as paid.
// @Tags Payment
// @Accept json
// @Accept x-www-form-urlencoded
// @Produce plain
// @Success 200 {string} string "success"
// @Failure 400 {string} string "fail"
// @Router /payments/okpay/v1/notify [post]
func (c *BaseCommController) OkPayNotify(ctx echo.Context) error {
req := ctx.Request()
rawBody, err := io.ReadAll(req.Body)
if err != nil {
log.Sugar.Warnf("[okpay] read notify body failed err=%v", err)
return ctx.String(http.StatusBadRequest, "fail")
}
req.Body = io.NopCloser(bytes.NewReader(rawBody))
form := make(map[string]string)
copyForm := func(values url.Values) {
for key, items := range values {
if len(items) == 0 {
continue
}
form[key] = items[0]
}
}
copyForm(req.URL.Query())
contentType := strings.ToLower(req.Header.Get(echo.HeaderContentType))
if strings.Contains(contentType, echo.MIMEApplicationJSON) && len(rawBody) > 0 {
if jsonForm, jsonErr := flattenOkPayJSONBody(rawBody); jsonErr == nil {
for key, value := range jsonForm {
form[key] = value
}
}
}
// Match the PHP example's $_POST semantics: let Echo/net/http parse both
// application/x-www-form-urlencoded and multipart/form-data.
if len(form) == 0 {
req.Body = io.NopCloser(bytes.NewReader(rawBody))
if parsedForm, formErr := ctx.FormParams(); formErr == nil {
copyForm(parsedForm)
}
}
// Final fallback for raw query-string style bodies.
if len(form) == 0 && len(rawBody) > 0 && looksLikeQueryPayload(string(rawBody)) {
if parsed, parseErr := url.ParseQuery(string(rawBody)); parseErr == nil && len(parsed) > 0 {
copyForm(parsed)
}
}
rawFormData := string(rawBody)
if rawFormData == "" {
rawFormData = req.URL.RawQuery
}
if len(form) == 0 {
log.Sugar.Warnf("[okpay] empty notify payload method=%s content_type=%s remote=%s raw_query=%q raw_body=%q", req.Method, req.Header.Get(echo.HeaderContentType), ctx.RealIP(), req.URL.RawQuery, rawFormData)
return ctx.String(http.StatusBadRequest, "fail")
}
if err = service.HandleOkPayNotify(form, rawFormData); err != nil {
log.Sugar.Warnf("[okpay] notify handle failed method=%s content_type=%s remote=%s form=%v raw_query=%q raw_body=%q err=%v", req.Method, req.Header.Get(echo.HeaderContentType), ctx.RealIP(), form, req.URL.RawQuery, rawFormData, err)
return ctx.String(http.StatusBadRequest, "fail")
}
return ctx.String(http.StatusOK, "success")
}
func flattenOkPayJSONBody(rawBody []byte) (map[string]string, error) {
decoder := json.NewDecoder(bytes.NewReader(rawBody))
decoder.UseNumber()
var payload map[string]interface{}
if err := decoder.Decode(&payload); err != nil {
return nil, err
}
flat := make(map[string]string)
for key, value := range payload {
if key == "data" {
dataMap, ok := value.(map[string]interface{})
if !ok {
continue
}
for dataKey, dataValue := range dataMap {
flat["data["+dataKey+"]"] = stringifyOkPayJSONValue(dataValue)
}
continue
}
flat[key] = stringifyOkPayJSONValue(value)
}
return flat, nil
}
func stringifyOkPayJSONValue(value interface{}) string {
switch v := value.(type) {
case nil:
return ""
case string:
return strings.TrimSpace(v)
case json.Number:
return v.String()
case float64:
if v == math.Trunc(v) {
return strconv.FormatInt(int64(v), 10)
}
return strconv.FormatFloat(v, 'f', -1, 64)
case bool:
if v {
return "true"
}
return "false"
default:
return strings.TrimSpace(fmt.Sprint(v))
}
}
func looksLikeQueryPayload(raw string) bool {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return false
}
if strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, "[") {
return false
}
return strings.Contains(trimmed, "=")
}
+4 -2
View File
@@ -60,12 +60,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 network, 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 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 // @Success 200 {object} response.ApiResponse{data=response.CheckoutCounterResponse}
// @Failure 400 {object} response.ApiResponse // @Failure 400 {object} response.ApiResponse
// @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) {
+6 -18
View File
@@ -1,11 +1,8 @@
package comm package comm
import ( import (
"html/template"
"net/http" "net/http"
"path/filepath"
"github.com/GMWalletApp/epusdt/config"
"github.com/GMWalletApp/epusdt/model/response" "github.com/GMWalletApp/epusdt/model/response"
"github.com/GMWalletApp/epusdt/model/service" "github.com/GMWalletApp/epusdt/model/service"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@@ -13,12 +10,12 @@ import (
// CheckoutCounter 收银台 // CheckoutCounter 收银台
// @Summary Checkout counter page // @Summary Checkout counter page
// @Description Render the payment checkout counter HTML page for a given trade // @Description Render the payment checkout counter JSON response for a given trade
// @Tags Payment // @Tags Payment
// @Produce html // @Produce json
// @Param trade_id path string true "Trade ID" // @Param trade_id path string true "Trade ID"
// @Success 200 {string} string "HTML page" // @Success 200 {object} response.CheckoutCounterResponse
// @Router /pay/checkout-counter/{trade_id} [get] // @Router /pay/checkout-counter-resp/{trade_id} [get]
func (c *BaseCommController) CheckoutCounter(ctx echo.Context) (err error) { func (c *BaseCommController) CheckoutCounter(ctx echo.Context) (err error) {
tradeId := ctx.Param("trade_id") tradeId := ctx.Param("trade_id")
resp, err := service.GetCheckoutCounterByTradeId(tradeId) resp, err := service.GetCheckoutCounterByTradeId(tradeId)
@@ -26,22 +23,13 @@ func (c *BaseCommController) CheckoutCounter(ctx echo.Context) (err error) {
if err == service.ErrOrder { if err == service.ErrOrder {
// Unknown trade id: render the page with empty payload // Unknown trade id: render the page with empty payload
// (client side shows a friendly "order not found" screen). // (client side shows a friendly "order not found" screen).
tmpl, tmplErr := template.ParseFiles(filepath.Join(config.StaticFilePath, "index.html"))
if tmplErr != nil {
return ctx.String(http.StatusInternalServerError, tmplErr.Error())
}
ctx.Response().Status = http.StatusNotFound
emptyResp := response.CheckoutCounterResponse{} emptyResp := response.CheckoutCounterResponse{}
return tmpl.Execute(ctx.Response(), emptyResp) return c.SucJson(ctx, emptyResp)
} }
return ctx.String(http.StatusInternalServerError, err.Error()) return ctx.String(http.StatusInternalServerError, err.Error())
} }
tmpl, err := template.ParseFiles(filepath.Join(config.StaticFilePath, "index.html"))
if err != nil {
return ctx.String(http.StatusInternalServerError, err.Error())
}
return tmpl.Execute(ctx.Response(), resp) return c.SucJson(ctx, resp)
} }
// CheckStatus 支付状态检测 // CheckStatus 支付状态检测
@@ -5,38 +5,24 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/GMWalletApp/epusdt/middleware"
"github.com/GMWalletApp/epusdt/model/data" "github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/model/response" "github.com/GMWalletApp/epusdt/model/response"
"github.com/GMWalletApp/epusdt/util/constant" "github.com/GMWalletApp/epusdt/util/constant"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
// GetSupportedAssets 对外公开可用链与 token 列表。数据源是管理后台 func buildSupportedAssets() ([]response.NetworkTokenSupport, error) {
// 的 chains + chain_tokens + wallet_address 三张表的实时查询,admin
// 启用/禁用链或代币后,这个接口在下次调用时就会反映新状态(无缓存)。
// 仅返回:链 enabled=true、代币 enabled=true、且该链上至少有一个可用
// 钱包地址的组合。
// @Summary List supported (network, tokens) groupings
// @Description Public endpoint, Auth required in /admin/api/v1/supported-assets
// @Tags Supported Assets
// @Produce json
// @Success 200 {object} response.ApiResponse{data=response.SupportedAssetsResponse}
// @Failure 400 {object} response.ApiResponse
// @Router /payments/gmpay/v1/supported-assets [get]
// @Router /admin/api/v1/supported-assets [get]
func (c *BaseCommController) GetSupportedAssets(ctx echo.Context) error {
chains, err := data.ListEnabledChains() chains, err := data.ListEnabledChains()
if err != nil { if err != nil {
return c.FailJson(ctx, err) return nil, err
} }
wallets, err := data.GetAvailableWalletAddress() wallets, err := data.GetAvailableWalletAddress()
if err != nil { if err != nil {
return c.FailJson(ctx, err) return nil, err
} }
// A chain only qualifies if it has at least one wallet available —
// without a recipient address, payments on that chain can't be
// processed even if the chain + token config says "enabled".
networkHasWallet := make(map[string]struct{}) networkHasWallet := make(map[string]struct{})
for _, w := range wallets { for _, w := range wallets {
networkHasWallet[strings.ToLower(w.Network)] = struct{}{} networkHasWallet[strings.ToLower(w.Network)] = struct{}{}
@@ -50,7 +36,7 @@ func (c *BaseCommController) GetSupportedAssets(ctx echo.Context) error {
} }
tokens, err := data.ListEnabledChainTokensByNetwork(network) tokens, err := data.ListEnabledChainTokensByNetwork(network)
if err != nil { if err != nil {
return c.FailJson(ctx, err) return nil, err
} }
if len(tokens) == 0 { if len(tokens) == 0 {
continue continue
@@ -73,18 +59,58 @@ func (c *BaseCommController) GetSupportedAssets(ctx echo.Context) error {
}) })
} }
return c.SucJson(ctx, response.SupportedAssetsResponse{Supports: supports}) return supports, nil
} }
// ListSupportedAssetRecords 查询支持项明细(无需鉴权)。 // GetPublicConfig returns payment/site config consumed by cashier/frontends.
// @Summary List supported asset records // The public route returns brand/site display config plus non-sensitive
// @Description Public endpoint. Returns enabled chain_tokens, optionally filtered by network. // epay/okpay knobs, while the authenticated admin route also includes OkPay
// @Tags Supported Assets // credentials and internal URLs.
// @Summary Get public payment config
// @Description Returns supported_assets plus site, epay and okpay config used by cashier/frontends.
// @Description The authenticated admin variant (/admin/api/v1/config) also includes OkPay shop credentials and internal callback settings.
// @Tags Payment Config
// @Produce json // @Produce json
// @Param network query string false "Network filter (e.g. tron, ethereum)" // @Success 200 {object} response.ApiResponse{data=response.PublicConfigResponse}
// @Success 200 {object} response.ApiResponse{data=[]mdb.ChainToken}
// @Failure 400 {object} response.ApiResponse // @Failure 400 {object} response.ApiResponse
// @Router /payments/gmpay/v1/supported-assets/records [get] // @Router /payments/gmpay/v1/config [get]
// @Router /admin/api/v1/config [get]
func (c *BaseCommController) GetPublicConfig(ctx echo.Context) error {
supports, err := buildSupportedAssets()
if err != nil {
return c.FailJson(ctx, err)
}
okpay := response.OkPayPublicConfig{
Enabled: data.GetOkPayEnabled(),
AllowTokens: data.GetOkPayAllowTokens(),
}
if ctx.Get(middleware.AdminUserIDKey) != nil {
okpay.ShopID = data.GetOkPayShopID()
okpay.ShopToken = data.GetOkPayShopToken()
okpay.APIURL = data.GetOkPayAPIURL()
okpay.CallbackURL = data.GetOkPayCallbackURL()
okpay.ReturnURL = data.GetOkPayReturnURL()
okpay.TimeoutSeconds = data.GetOkPayTimeoutSeconds()
}
return c.SucJson(ctx, response.PublicConfigResponse{
SupportedAssets: supports,
Site: response.SitePublicConfig{
CashierName: data.GetBrandCashierName(),
LogoURL: data.GetBrandLogoURL(),
WebsiteTitle: data.GetBrandWebsiteTitle(),
SupportLink: data.GetBrandSupportURL(),
},
Epay: response.EpayPublicConfig{
DefaultToken: data.GetSettingString(mdb.SettingKeyEpayDefaultToken, "usdt"),
DefaultCurrency: data.GetSettingString(mdb.SettingKeyEpayDefaultCurrency, "cny"),
DefaultNetwork: data.GetSettingString(mdb.SettingKeyEpayDefaultNetwork, "tron"),
},
OkPay: okpay,
})
}
// ListSupportedAssetRecords is currently not wired to a public route.
// Kept as an internal helper candidate for future admin/debug use.
func (c *BaseCommController) ListSupportedAssetRecords(ctx echo.Context) error { func (c *BaseCommController) ListSupportedAssetRecords(ctx echo.Context) error {
network := ctx.QueryParam("network") network := ctx.QueryParam("network")
list, err := data.ListEnabledChainTokens(network) list, err := data.ListEnabledChainTokens(network)
@@ -94,15 +120,8 @@ func (c *BaseCommController) ListSupportedAssetRecords(ctx echo.Context) error {
return c.SucJson(ctx, list) return c.SucJson(ctx, list)
} }
// GetSupportedAsset 查询单条支持项(无需鉴权)。 // GetSupportedAsset is currently not wired to a public route.
// @Summary Get a supported asset // Kept as an internal helper candidate for future admin/debug use.
// @Description Public endpoint. Returns one chain_token row by ID. Returns 404 if the token is disabled.
// @Tags Supported Assets
// @Produce json
// @Param id path int true "ChainToken ID"
// @Success 200 {object} response.ApiResponse{data=mdb.ChainToken}
// @Failure 400 {object} response.ApiResponse
// @Router /payments/gmpay/v1/supported-assets/{id} [get]
func (c *BaseCommController) GetSupportedAsset(ctx echo.Context) error { func (c *BaseCommController) GetSupportedAsset(ctx echo.Context) error {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64) id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil { if err != nil {
+55 -31
View File
@@ -79,6 +79,60 @@ type installHandler struct {
done chan struct{} done chan struct{}
} }
func installRootRedirectMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if c.Request().Method == http.MethodGet && c.Request().URL.Path == "/" {
return c.Redirect(http.StatusFound, "/install")
}
return next(c)
}
}
func resolveInstallWWWRoot() string {
// Resolve www/ relative to the executable so SPA routes work regardless
// of the working directory. main.go extracts www/ next to the binary.
wwwRoot := "./www"
if exePath, err := os.Executable(); err == nil {
if exePath, err = filepath.EvalSymlinks(exePath); err == nil {
wwwRoot = filepath.Join(filepath.Dir(exePath), "www")
}
}
return wwwRoot
}
func newInstallServer(envFilePath, wwwRoot string) (*echo.Echo, *installHandler) {
h := &installHandler{
envFilePath: envFilePath,
done: make(chan struct{}),
}
e := echo.New()
e.HideBanner = true
e.HidePort = true
// api routes for the install frontend
api := e.Group("/api")
api.GET("/install/defaults", h.GetDefaults)
api.POST("/install", h.Submit)
// Redirect browser visits on root to /install so first-run users land
// on the wizard directly. This must run before the static middleware,
// otherwise "/" is intercepted by the SPA index.html fallback.
e.Use(installRootRedirectMiddleware)
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Skipper: func(c echo.Context) bool {
return luluHttp.ShouldSkipSPAFallback(c.Request().URL.Path)
},
HTML5: true,
Index: "index.html",
Root: wwwRoot,
}))
return e, h
}
// GetDefaults returns default values for the install form. // GetDefaults returns default values for the install form.
// //
// @Summary Install — get default values // @Summary Install — get default values
@@ -172,37 +226,7 @@ func RunInstallServer(listenAddr, envFilePath string) {
listenAddr = DefaultInstallAddr listenAddr = DefaultInstallAddr
} }
h := &installHandler{ e, h := newInstallServer(envFilePath, resolveInstallWWWRoot())
envFilePath: envFilePath,
done: make(chan struct{}),
}
e := echo.New()
e.HideBanner = true
e.HidePort = true
// api routes for the install frontend
api := e.Group("/api")
api.GET("/install/defaults", h.GetDefaults)
api.POST("/install", h.Submit)
// Resolve www/ relative to the executable so SPA routes work regardless
// of the working directory. main.go extracts www/ next to the binary.
wwwRoot := "./www"
if exePath, err := os.Executable(); err == nil {
if exePath, err = filepath.EvalSymlinks(exePath); err == nil {
wwwRoot = filepath.Join(filepath.Dir(exePath), "www")
}
}
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Skipper: func(c echo.Context) bool {
return luluHttp.ShouldSkipSPAFallback(c.Request().URL.Path)
},
HTML5: true,
Index: "index.html",
Root: wwwRoot,
}))
// Build a human-readable URL for the console hint. // Build a human-readable URL for the console hint.
installHost := listenAddr installHost := listenAddr
+49
View File
@@ -95,6 +95,55 @@ func TestInstallAPIDefaults(t *testing.T) {
} }
} }
func TestInstallServerRootRedirectsToInstall(t *testing.T) {
dir := t.TempDir()
wwwRoot := filepath.Join(dir, "www")
if err := os.MkdirAll(wwwRoot, 0o755); err != nil {
t.Fatalf("mkdir www root: %v", err)
}
if err := os.WriteFile(filepath.Join(wwwRoot, "index.html"), []byte("install-ui"), 0o644); err != nil {
t.Fatalf("write index.html: %v", err)
}
e, _ := newInstallServer(filepath.Join(dir, ".env"), wwwRoot)
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("status = %d, want 302; body: %s", rec.Code, rec.Body.String())
}
if got := rec.Header().Get("Location"); got != "/install" {
t.Fatalf("Location = %q, want /install", got)
}
}
func TestInstallServerServesSPAOnInstallRoute(t *testing.T) {
dir := t.TempDir()
wwwRoot := filepath.Join(dir, "www")
if err := os.MkdirAll(wwwRoot, 0o755); err != nil {
t.Fatalf("mkdir www root: %v", err)
}
const wantBody = "install-ui"
if err := os.WriteFile(filepath.Join(wwwRoot, "index.html"), []byte(wantBody), 0o644); err != nil {
t.Fatalf("write index.html: %v", err)
}
e, _ := newInstallServer(filepath.Join(dir, ".env"), wwwRoot)
req := httptest.NewRequest(http.MethodGet, "/install", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body: %s", rec.Code, rec.Body.String())
}
if body := rec.Body.String(); body != wantBody {
t.Fatalf("body = %q, want %q", body, wantBody)
}
}
func TestInstallAPISubmit(t *testing.T) { func TestInstallAPISubmit(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
envPath := filepath.Join(dir, ".env") envPath := filepath.Join(dir, ".env")
+1
View File
@@ -37,6 +37,7 @@ func SetupTestDatabases(t testing.TB) func() {
mustMigrate(t, mainDB, mustMigrate(t, mainDB,
&mdb.Orders{}, &mdb.Orders{},
&mdb.ProviderOrder{},
&mdb.WalletAddress{}, &mdb.WalletAddress{},
&mdb.ApiKey{}, &mdb.ApiKey{},
&mdb.Setting{}, &mdb.Setting{},
-9
View File
@@ -12,9 +12,6 @@ import (
"github.com/gookit/color" "github.com/gookit/color"
) )
//go:embed all:static
var staticDir embed.FS
//go:embed all:www //go:embed all:www
var wwwDir embed.FS var wwwDir embed.FS
@@ -92,12 +89,6 @@ func releaseStatic(fs embed.FS, target string) (string, error) {
} }
func main() { func main() {
staticPath, err := releaseStatic(staticDir, "static")
if err != nil {
panic(err)
}
fmt.Println("static released to:", staticPath)
wwwwPath, err := releaseStatic(wwwDir, "www") wwwwPath, err := releaseStatic(wwwDir, "www")
if err != nil { if err != nil {
panic(err) panic(err)
+11
View File
@@ -6,6 +6,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/GMWalletApp/epusdt/config"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/gookit/color" "github.com/gookit/color"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
@@ -35,6 +36,7 @@ func MdbTableInit() {
{"Chain", &mdb.Chain{}}, {"Chain", &mdb.Chain{}},
{"ChainToken", &mdb.ChainToken{}}, {"ChainToken", &mdb.ChainToken{}},
{"RpcNode", &mdb.RpcNode{}}, {"RpcNode", &mdb.RpcNode{}},
{"ProviderOrder", &mdb.ProviderOrder{}},
} }
for _, m := range migrations { for _, m := range migrations {
if err := Mdb.AutoMigrate(m.model); err != nil { if err := Mdb.AutoMigrate(m.model); err != nil {
@@ -132,10 +134,19 @@ func seedRpcNodes() {
// seedDefaultSettings inserts built-in default settings. // seedDefaultSettings inserts built-in default settings.
// Uses ON CONFLICT DO NOTHING so admin edits persist across restarts. // Uses ON CONFLICT DO NOTHING so admin edits persist across restarts.
func seedDefaultSettings() { func seedDefaultSettings() {
okPayCallbackURL := strings.TrimRight(strings.TrimSpace(config.GetAppUri()), "/")
if okPayCallbackURL != "" {
okPayCallbackURL += "/payments/okpay/v1/notify"
}
defaults := []mdb.Setting{ defaults := []mdb.Setting{
{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},
{Group: mdb.SettingGroupOkPay, Key: mdb.SettingKeyOkPayEnabled, Value: "false", Type: mdb.SettingTypeBool},
{Group: mdb.SettingGroupOkPay, Key: mdb.SettingKeyOkPayAPIURL, Value: "https://api.okaypay.me/shop/", Type: mdb.SettingTypeString},
{Group: mdb.SettingGroupOkPay, Key: mdb.SettingKeyOkPayCallbackURL, Value: okPayCallbackURL, Type: mdb.SettingTypeString},
{Group: mdb.SettingGroupOkPay, Key: mdb.SettingKeyOkPayTimeoutSeconds, Value: "10", Type: mdb.SettingTypeInt},
{Group: mdb.SettingGroupOkPay, Key: mdb.SettingKeyOkPayAllowTokens, Value: "USDT,TRX", Type: mdb.SettingTypeString},
} }
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 default settings err=%s\n", err) color.Red.Printf("[store_db] seed default settings err=%s\n", err)
+12
View File
@@ -202,6 +202,18 @@ func MarkOrderSelected(tradeId string) error {
Update("is_selected", true).Error Update("is_selected", true).Error
} }
// ExpireOrderByTradeID marks a waiting order as expired. Used to retire failed
// child-order attempts that should not remain selectable/reusable.
func ExpireOrderByTradeID(tradeId string) error {
return dao.Mdb.Model(&mdb.Orders{}).
Where("trade_id = ?", tradeId).
Where("status = ?", mdb.StatusWaitPay).
Updates(map[string]interface{}{
"status": mdb.StatusExpired,
"is_selected": false,
}).Error
}
// RefreshOrderExpiration resets created_at to now so the expiration timer restarts. // RefreshOrderExpiration resets created_at to now so the expiration timer restarts.
// Called on the parent order when a sub-order is created or returned. // Called on the parent order when a sub-order is created or returned.
func RefreshOrderExpiration(tradeId string) error { func RefreshOrderExpiration(tradeId string) error {
+76
View File
@@ -0,0 +1,76 @@
package data
import (
"github.com/GMWalletApp/epusdt/model/dao"
"github.com/GMWalletApp/epusdt/model/mdb"
"gorm.io/gorm"
)
func GetProviderOrderByTradeIDAndProvider(tradeID string, provider string) (*mdb.ProviderOrder, error) {
row := new(mdb.ProviderOrder)
err := dao.Mdb.Model(row).
Where("trade_id = ?", tradeID).
Where("provider = ?", provider).
Limit(1).
Find(row).Error
return row, err
}
func CreateProviderOrderWithTransaction(tx *gorm.DB, row *mdb.ProviderOrder) error {
return tx.Model(row).Create(row).Error
}
func UpdateProviderOrderCreated(tradeID string, provider string, providerOrderID string, payURL string) error {
return dao.Mdb.Model(&mdb.ProviderOrder{}).
Where("trade_id = ?", tradeID).
Where("provider = ?", provider).
Updates(map[string]interface{}{
"provider_order_id": providerOrderID,
"pay_url": payURL,
"status": mdb.ProviderOrderStatusPending,
}).Error
}
func MarkProviderOrderFailed(tradeID string, provider string) error {
return dao.Mdb.Model(&mdb.ProviderOrder{}).
Where("trade_id = ?", tradeID).
Where("provider = ?", provider).
Update("status", mdb.ProviderOrderStatusFailed).Error
}
func SaveProviderOrderNotify(tradeID string, provider string, notifyRaw string) error {
return dao.Mdb.Model(&mdb.ProviderOrder{}).
Where("trade_id = ?", tradeID).
Where("provider = ?", provider).
Update("notify_raw", notifyRaw).Error
}
func MarkProviderOrderPaid(tradeID string, provider string, notifyRaw string) error {
return dao.Mdb.Model(&mdb.ProviderOrder{}).
Where("trade_id = ?", tradeID).
Where("provider = ?", provider).
Updates(map[string]interface{}{
"status": mdb.ProviderOrderStatusPaid,
"notify_raw": notifyRaw,
}).Error
}
func MarkProviderOrderExpired(tradeID string, provider string) error {
return dao.Mdb.Model(&mdb.ProviderOrder{}).
Where("trade_id = ?", tradeID).
Where("provider = ?", provider).
Where("status IN ?", []string{mdb.ProviderOrderStatusCreating, mdb.ProviderOrderStatusPending}).
Update("status", mdb.ProviderOrderStatusExpired).Error
}
func GetSubOrderByTokenPayProvider(parentTradeID string, token string, payProvider string) (*mdb.Orders, error) {
order := new(mdb.Orders)
err := dao.Mdb.Model(order).
Where("parent_trade_id = ?", parentTradeID).
Where("token = ?", token).
Where("pay_provider = ?", payProvider).
Where("status = ?", mdb.StatusWaitPay).
Limit(1).
Find(order).Error
return order, err
}
+84
View File
@@ -5,6 +5,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/GMWalletApp/epusdt/config"
"github.com/GMWalletApp/epusdt/model/dao" "github.com/GMWalletApp/epusdt/model/dao"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
@@ -157,6 +158,89 @@ var sensitiveSettingKeys = []string{
mdb.SettingKeyInitAdminPasswordChanged, mdb.SettingKeyInitAdminPasswordChanged,
} }
func getFirstNonEmptySetting(fallback string, keys ...string) string {
for _, key := range keys {
value := strings.TrimSpace(GetSettingString(key, ""))
if value != "" {
return value
}
}
return fallback
}
func GetBrandCashierName() string {
return getFirstNonEmptySetting("", mdb.SettingKeyBrandCheckoutName, mdb.SettingKeyBrandSiteName)
}
func GetBrandLogoURL() string {
return strings.TrimSpace(GetSettingString(mdb.SettingKeyBrandLogoUrl, ""))
}
func GetBrandWebsiteTitle() string {
return getFirstNonEmptySetting("", mdb.SettingKeyBrandSiteTitle, mdb.SettingKeyBrandPageTitle)
}
func GetBrandSupportURL() string {
return strings.TrimSpace(GetSettingString(mdb.SettingKeyBrandSupportUrl, ""))
}
// OkPay settings helpers keep the provider-specific defaults in one place so
// business logic does not need to repeat raw settings keys.
func GetOkPayEnabled() bool {
return GetSettingBool(mdb.SettingKeyOkPayEnabled, false)
}
func GetOkPayShopID() string {
return strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayShopID, ""))
}
func GetOkPayShopToken() string {
return strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayShopToken, ""))
}
func GetOkPayAPIURL() string {
return strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayAPIURL, "https://api.okaypay.me/shop/"))
}
func GetOkPayCallbackURL() string {
if configured := strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayCallbackURL, "")); configured != "" {
return configured
}
appURI := strings.TrimSpace(config.GetAppUri())
if appURI == "" {
return ""
}
return strings.TrimRight(appURI, "/") + "/payments/okpay/v1/notify"
}
func GetOkPayReturnURL() string {
return strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayReturnURL, ""))
}
func GetOkPayTimeoutSeconds() int {
return GetSettingInt(mdb.SettingKeyOkPayTimeoutSeconds, 10)
}
func GetOkPayAllowTokens() []string {
raw := GetSettingString(mdb.SettingKeyOkPayAllowTokens, "USDT,TRX")
if strings.TrimSpace(raw) == "" {
return []string{"USDT", "TRX"}
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
token := strings.ToUpper(strings.TrimSpace(part))
if token == "" {
continue
}
out = append(out, token)
}
if len(out) == 0 {
return []string{"USDT", "TRX"}
}
return out
}
// ListSettingsByGroup returns all rows for a given group (empty group = all), // ListSettingsByGroup returns all rows for a given group (empty group = all),
// excluding any keys in sensitiveSettingKeys. // excluding any keys in sensitiveSettingKeys.
func ListSettingsByGroup(group string) ([]mdb.Setting, error) { func ListSettingsByGroup(group string) ([]mdb.Setting, error) {
+24 -1
View File
@@ -12,6 +12,19 @@ const (
PaymentTypeEpay = "Epay" PaymentTypeEpay = "Epay"
) )
const (
// PaymentProviderOnChain means this concrete order record is settled by
// sending funds to a directly assigned wallet address on a supported chain.
PaymentProviderOnChain = "on_chain"
// PaymentProviderOkPay means this concrete order record is settled through
// the third-party OkayPay/OkPay hosted checkout flow.
//
// In the current design this is typically used by a switch-network-created
// child order, while the parent order keeps its original merchant-facing
// semantics and callback behavior.
PaymentProviderOkPay = "okpay"
)
type Orders struct { type Orders struct {
TradeId string `gorm:"column:trade_id;uniqueIndex:orders_trade_id_uindex" json:"trade_id" example:"T2026041612345678"` TradeId string `gorm:"column:trade_id;uniqueIndex:orders_trade_id_uindex" json:"trade_id" example:"T2026041612345678"`
OrderId string `gorm:"column:order_id;uniqueIndex:orders_order_id_uindex" json:"order_id" example:"ORD20260416001"` OrderId string `gorm:"column:order_id;uniqueIndex:orders_order_id_uindex" json:"order_id" example:"ORD20260416001"`
@@ -33,7 +46,17 @@ type Orders struct {
CallBackConfirm int `gorm:"column:callback_confirm;default:2" json:"callback_confirm" enums:"1,2" example:"2"` CallBackConfirm int `gorm:"column:callback_confirm;default:2" json:"callback_confirm" enums:"1,2" example:"2"`
IsSelected bool `gorm:"column:is_selected;default:false" json:"is_selected" example:"false"` IsSelected bool `gorm:"column:is_selected;default:false" json:"is_selected" example:"false"`
PaymentType string `gorm:"column:payment_type" json:"payment_type" example:"Epay"` PaymentType string `gorm:"column:payment_type" json:"payment_type" example:"Epay"`
ApiKeyID uint64 `gorm:"column:api_key_id;default:0;index:orders_api_key_id_index" json:"api_key_id" example:"1"` // PayProvider identifies how this specific order row is collected.
//
// Semantics:
// - parent orders and regular chain child orders use on_chain
// - third-party hosted checkout child orders use their provider name
// (for example okpay)
//
// Existing rows default to on_chain for backward compatibility so upgrades
// can rely on AutoMigrate without rewriting old orders.
PayProvider string `gorm:"column:pay_provider;size:32;default:on_chain;index:orders_pay_provider_index" json:"pay_provider" example:"on_chain"`
ApiKeyID uint64 `gorm:"column:api_key_id;default:0;index:orders_api_key_id_index" json:"api_key_id" example:"1"`
// PayBySubId holds the primary-key ID of the sub-order that settled this parent order. // PayBySubId holds the primary-key ID of the sub-order that settled this parent order.
// Zero when the parent order was paid directly (no sub-order involved). // Zero when the parent order was paid directly (no sub-order involved).
PayBySubId uint64 `gorm:"column:pay_by_sub_id;default:0" json:"pay_by_sub_id" example:"0"` PayBySubId uint64 `gorm:"column:pay_by_sub_id;default:0" json:"pay_by_sub_id" example:"0"`
+48
View File
@@ -0,0 +1,48 @@
package mdb
const (
ProviderOrderStatusCreating = "creating"
ProviderOrderStatusPending = "pending"
ProviderOrderStatusPaid = "paid"
ProviderOrderStatusFailed = "failed"
ProviderOrderStatusExpired = "expired"
)
// ProviderOrder stores provider-specific checkout data for one internal order
// row. For OkPay this is expected to bind to the concrete child order created
// by switch-network, not to the parent merchant order.
//
// Binding rules:
// - trade_id always points to orders.trade_id
// - the bound orders row should use the same pay_provider value
// - provider identifies which downstream checkout created the provider row
// - provider_order_id is the provider's own order number (for OkPay this is
// the returned order_id from /payLink)
// - pay_url is the hosted payment URL returned by the provider
//
// The main orders table remains the source of truth for merchant-facing state,
// while this table keeps provider-specific identifiers and callback payloads.
type ProviderOrder struct {
TradeId string `gorm:"column:trade_id;size:32;not null;uniqueIndex:provider_orders_trade_id_provider_uindex,priority:1;index:provider_orders_trade_id_index" json:"trade_id" example:"T2026041612345678"`
// Provider values should match the bound Orders.PayProvider value.
Provider string `gorm:"column:provider;size:32;not null;uniqueIndex:provider_orders_trade_id_provider_uindex,priority:2;index:provider_orders_provider_status_index,priority:1" json:"provider" example:"okpay"`
// ProviderOrderID is the downstream provider's own order identifier for the
// bound concrete order row.
ProviderOrderID string `gorm:"column:provider_order_id;size:128;not null;default:'';index:provider_orders_provider_order_id_index" json:"provider_order_id" example:"ac7b86615fdb137576ae35879f7ed844"`
// PayURL is the hosted checkout URL returned by the downstream provider for
// the bound concrete order row.
PayURL string `gorm:"column:pay_url;size:512;not null;default:''" json:"pay_url" example:"https://pay.example.com/checkout/abc123"`
// Amount/Coin mirror the exact payload submitted to the provider, which may
// be useful when validating callbacks and troubleshooting mismatches.
Amount float64 `gorm:"column:amount" json:"amount" example:"14.2857"`
Coin string `gorm:"column:coin;size:16;not null;default:''" json:"coin" example:"USDT"`
Status string `gorm:"column:status;size:32;not null;default:pending;index:provider_orders_provider_status_index,priority:2" json:"status" example:"pending"`
// NotifyRaw stores the original provider callback payload for auditing and
// replay/debug use. It is provider-specific and intentionally opaque here.
NotifyRaw string `gorm:"column:notify_raw;type:text" json:"notify_raw"`
BaseModel
}
func (p *ProviderOrder) TableName() string {
return "provider_orders"
}
+19 -4
View File
@@ -1,9 +1,10 @@
package mdb package mdb
// Setting stores non-credential runtime configuration as key/value pairs. // Setting stores non-credential runtime configuration as key/value pairs.
// Groups: brand, rate, system, epay. Merchant credentials (pid + secret_key) // Groups: brand, rate, system, epay, okpay. Merchant credentials (pid +
// live in the api_keys table; notification configs live in NotificationChannel. // secret_key) live in the api_keys table; notification configs live in
// Default rows for the epay group are seeded on first startup (see // NotificationChannel.
// Default rows for the epay/okpay groups are seeded on first startup (see
// model/dao/mdb_table_init.go seedDefaultSettings). All other groups start // model/dao/mdb_table_init.go seedDefaultSettings). All 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.
// Only exception: system.jwt_secret is auto-generated on first startup. // Only exception: system.jwt_secret is auto-generated on first startup.
@@ -12,6 +13,7 @@ const (
SettingGroupRate = "rate" SettingGroupRate = "rate"
SettingGroupSystem = "system" SettingGroupSystem = "system"
SettingGroupEpay = "epay" SettingGroupEpay = "epay"
SettingGroupOkPay = "okpay"
) )
const ( const (
@@ -28,9 +30,12 @@ const (
SettingKeyInitAdminPasswordFetched = "system.init_admin_password_fetched" SettingKeyInitAdminPasswordFetched = "system.init_admin_password_fetched"
SettingKeyInitAdminPasswordChanged = "system.init_admin_password_changed" SettingKeyInitAdminPasswordChanged = "system.init_admin_password_changed"
SettingKeyOrderExpiration = "system.order_expiration_time" SettingKeyOrderExpiration = "system.order_expiration_time"
SettingKeyBrandCheckoutName = "brand.checkout_name"
SettingKeyBrandSiteName = "brand.site_name" SettingKeyBrandSiteName = "brand.site_name"
SettingKeyBrandLogoUrl = "brand.logo_url" SettingKeyBrandLogoUrl = "brand.logo_url"
SettingKeyBrandSiteTitle = "brand.site_title"
SettingKeyBrandPageTitle = "brand.page_title" SettingKeyBrandPageTitle = "brand.page_title"
SettingKeyBrandSuccessCopy = "brand.success_copy"
SettingKeyBrandPaySuccess = "brand.pay_success_text" SettingKeyBrandPaySuccess = "brand.pay_success_text"
SettingKeyBrandSupportUrl = "brand.support_url" SettingKeyBrandSupportUrl = "brand.support_url"
SettingKeyRateForcedUsdt = "rate.forced_usdt_rate" SettingKeyRateForcedUsdt = "rate.forced_usdt_rate"
@@ -42,10 +47,20 @@ const (
SettingKeyEpayDefaultToken = "epay.default_token" SettingKeyEpayDefaultToken = "epay.default_token"
SettingKeyEpayDefaultCurrency = "epay.default_currency" SettingKeyEpayDefaultCurrency = "epay.default_currency"
SettingKeyEpayDefaultNetwork = "epay.default_network" SettingKeyEpayDefaultNetwork = "epay.default_network"
// OkPay hosted-checkout settings.
SettingKeyOkPayEnabled = "okpay.enabled"
SettingKeyOkPayShopID = "okpay.shop_id"
SettingKeyOkPayShopToken = "okpay.shop_token"
SettingKeyOkPayAPIURL = "okpay.api_url"
SettingKeyOkPayCallbackURL = "okpay.callback_url"
SettingKeyOkPayReturnURL = "okpay.return_url"
SettingKeyOkPayTimeoutSeconds = "okpay.timeout_seconds"
SettingKeyOkPayAllowTokens = "okpay.allow_tokens"
) )
type Setting struct { type Setting struct {
Group string `gorm:"column:group;size:32;index:settings_group_index" json:"group" enums:"brand,rate,system" 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_usdt_rate"` Key string `gorm:"column:key;uniqueIndex:settings_key_uindex;size:128" json:"key" example:"rate.forced_usdt_rate"`
Value string `gorm:"column:value;type:text" json:"value" example:"7.2"` Value string `gorm:"column:value;type:text" json:"value" example:"7.2"`
Type string `gorm:"column:type;size:16;default:string" json:"type" enums:"string,int,bool,json" example:"string"` Type string `gorm:"column:type;size:16;default:string" json:"type" enums:"string,int,bool,json" example:"string"`
+1 -1
View File
@@ -43,7 +43,7 @@ type OrderProcessingRequest struct {
type SwitchNetworkRequest struct { type SwitchNetworkRequest struct {
TradeId string `json:"trade_id" validate:"required" example:"T2026041612345678"` TradeId string `json:"trade_id" validate:"required" example:"T2026041612345678"`
Token string `json:"token" validate:"required" example:"USDT"` Token string `json:"token" validate:"required" example:"USDT"`
Network string `json:"network" validate:"required" example:"ethereum"` Network string `json:"network" validate:"required" example:"okpay,tron,solana,ethereum"`
} }
func (r SwitchNetworkRequest) Translates() map[string]string { func (r SwitchNetworkRequest) Translates() map[string]string {
+9 -8
View File
@@ -1,16 +1,17 @@
package response package response
type CheckoutCounterResponse struct { type CheckoutCounterResponse struct {
TradeId string `json:"trade_id" example:"T2026041612345678"` // epusdt订单号 TradeId string `json:"trade_id" example:"T2026041612345678"` // epusdt订单号
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,保留4位小数 法币金额 Amount float64 `json:"amount" example:"100.0000"` // 订单金额,保留4位小数 法币金额
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额,保留4位小数 加密货币金额 ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额,保留4位小数 加密货币金额
Token string `json:"token" example:"USDT"` // 所属币种 TRX USDT...... Token string `json:"token" example:"USDT"` // 所属币种 TRX USDT......
Currency string `json:"currency" example:"CNY"` // 法币币种 CNY USD ... Currency string `json:"currency" example:"CNY"` // 法币币种 CNY USD ...
ReceiveAddress string `json:"receive_address" example:"TTestTronAddress001"` // 收款钱包地址 ReceiveAddress string `json:"receive_address" example:"TTestTronAddress001"` // 收款钱包地址
Network string `json:"network" example:"tron"` // 网络 TRON ETH ... Network string `json:"network" example:"tron"` // 网络 TRON ETH ...
ExpirationTime int64 `json:"expiration_time" example:"1713264600"` // 过期时间 时间戳 ExpirationTime int64 `json:"expiration_time" example:"1713264600"` // 过期时间 时间戳
RedirectUrl string `json:"redirect_url" example:"https://example.com/success"` RedirectUrl string `json:"redirect_url" example:"https://example.com/success"`
CreatedAt int64 `json:"created_at" example:"1713264000"` // 订单创建时间 时间戳 PaymentUrl string `json:"payment_url" example:"https://pay.example.com/checkout/T2026041612345678"` // 支付链接;链上订单通常为本地收银台,OkPay 子订单为第三方 payLink
CreatedAt int64 `json:"created_at" example:"1713264000"` // 订单创建时间 时间戳
IsSelected bool `json:"is_selected" example:"false"` IsSelected bool `json:"is_selected" example:"false"`
} }
+29 -2
View File
@@ -5,6 +5,33 @@ type NetworkTokenSupport struct {
Tokens []string `json:"tokens" example:"USDT,USDC"` Tokens []string `json:"tokens" example:"USDT,USDC"`
} }
type SupportedAssetsResponse struct { type EpayPublicConfig struct {
Supports []NetworkTokenSupport `json:"supports"` DefaultToken string `json:"default_token" example:"usdt"`
DefaultCurrency string `json:"default_currency" example:"cny"`
DefaultNetwork string `json:"default_network" example:"tron"`
}
type SitePublicConfig struct {
CashierName string `json:"cashier_name" example:"Acme Cashier"`
LogoURL string `json:"logo_url" example:"https://cdn.example.com/logo.png"`
WebsiteTitle string `json:"website_title" example:"Acme Payments"`
SupportLink string `json:"support_link" example:"https://example.com/support"`
}
type OkPayPublicConfig struct {
Enabled bool `json:"enabled" example:"true"`
AllowTokens []string `json:"allow_tokens" example:"USDT,TRX"`
ShopID string `json:"shop_id,omitempty" example:"okpay-shop-test"`
ShopToken string `json:"shop_token,omitempty" example:"secret-token"`
APIURL string `json:"api_url,omitempty" example:"https://api.okaypay.me/shop/"`
CallbackURL string `json:"callback_url,omitempty" example:"https://pay.example.com/payments/okpay/v1/notify"`
ReturnURL string `json:"return_url,omitempty" example:"https://pay.example.com/success"`
TimeoutSeconds int `json:"timeout_seconds,omitempty" example:"10"`
}
type PublicConfigResponse struct {
SupportedAssets []NetworkTokenSupport `json:"supported_assets"`
Site SitePublicConfig `json:"site"`
Epay EpayPublicConfig `json:"epay"`
OkPay OkPayPublicConfig `json:"okpay"`
} }
+278
View File
@@ -0,0 +1,278 @@
package service
import (
"crypto/md5"
"encoding/hex"
"fmt"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/request"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/http_client"
"github.com/GMWalletApp/epusdt/util/log"
)
type okPayCreateDepositResponse struct {
Status string `json:"status"`
Code int `json:"code"`
Data struct {
OrderID string `json:"order_id"`
PayURL string `json:"pay_url"`
} `json:"data"`
Msg string `json:"msg"`
}
type okPayDepositOrder struct {
ProviderOrderID string
PayURL string
}
type okPayNotifyPayload struct {
ID string `json:"id"`
Status string `json:"status"`
Code string `json:"code"`
OrderID string `json:"order_id"`
UniqueID string `json:"unique_id"`
PayUserID string `json:"pay_user_id"`
Amount string `json:"amount"`
Coin string `json:"coin"`
PayStatus string `json:"pay_status"`
NotifyType string `json:"notify_type"`
Sign string `json:"sign"`
RawFormData string `json:"raw_form_data"`
}
func okPaySign(form map[string]string, shopID string, shopToken string) map[string]string {
values := url.Values{}
for key, value := range form {
if strings.TrimSpace(value) == "" {
continue
}
values.Set(key, value)
}
values.Set("id", shopID)
query, _ := url.QueryUnescape(values.Encode())
sum := md5.Sum([]byte(query + "&token=" + shopToken))
signed := make(map[string]string, len(values)+1)
for key, vals := range values {
if len(vals) == 0 {
continue
}
signed[key] = vals[0]
}
signed["sign"] = strings.ToUpper(hex.EncodeToString(sum[:]))
return signed
}
func createOkPayDepositOrder(uniqueID string, amount float64, coin string, returnURL string) (*okPayDepositOrder, error) {
shopID := data.GetOkPayShopID()
shopToken := data.GetOkPayShopToken()
apiURL := strings.TrimSpace(data.GetOkPayAPIURL())
callbackURL := data.GetOkPayCallbackURL()
if shopID == "" || shopToken == "" || apiURL == "" || callbackURL == "" {
return nil, fmt.Errorf("okpay config incomplete")
}
if returnURL == "" {
returnURL = data.GetOkPayReturnURL()
}
form := map[string]string{
"unique_id": uniqueID,
"name": uniqueID,
"amount": fmt.Sprintf("%.2f", amount),
"coin": strings.ToUpper(strings.TrimSpace(coin)),
"callback_url": callbackURL,
"return_url": returnURL,
}
form = okPaySign(form, shopID, shopToken)
base, err := url.Parse(apiURL)
if err != nil {
return nil, err
}
base.Path = path.Join(base.Path, "payLink")
client := http_client.GetHttpClient()
client.SetTimeout(time.Duration(data.GetOkPayTimeoutSeconds()) * time.Second)
var payload okPayCreateDepositResponse
resp, err := client.R().
SetFormData(form).
SetResult(&payload).
Post(base.String())
if err != nil {
log.Sugar.Warnf("[okpay] create order request failed unique_id=%s api_url=%s callback_url=%s return_url=%s err=%v", uniqueID, base.String(), callbackURL, returnURL, err)
return nil, err
}
if resp.StatusCode() >= 400 {
log.Sugar.Warnf("[okpay] create order http error unique_id=%s status=%s api_url=%s callback_url=%s return_url=%s body=%s", uniqueID, resp.Status(), base.String(), callbackURL, returnURL, resp.String())
return nil, fmt.Errorf("okpay http status: %s", resp.Status())
}
if payload.Data.OrderID == "" || payload.Data.PayURL == "" {
log.Sugar.Warnf("[okpay] create order rejected unique_id=%s api_url=%s callback_url=%s return_url=%s status=%s code=%d msg=%s", uniqueID, base.String(), callbackURL, returnURL, payload.Status, payload.Code, payload.Msg)
if payload.Msg != "" {
return nil, fmt.Errorf("okpay create order failed: %s (callback_url=%s)", payload.Msg, callbackURL)
}
return nil, fmt.Errorf("okpay create order failed (callback_url=%s)", callbackURL)
}
return &okPayDepositOrder{
ProviderOrderID: payload.Data.OrderID,
PayURL: payload.Data.PayURL,
}, nil
}
func verifyOkPayNotify(form map[string]string) bool {
shopID := data.GetOkPayShopID()
shopToken := data.GetOkPayShopToken()
signature := strings.TrimSpace(form["sign"])
if shopID == "" || shopToken == "" || signature == "" {
return false
}
if expected := okPayNotifySign(form, shopToken); expected != "" && strings.EqualFold(expected, signature) {
return true
}
unsigned := make(map[string]string, len(form))
for key, value := range form {
if strings.EqualFold(key, "sign") {
continue
}
unsigned[key] = value
}
signed := okPaySign(unsigned, shopID, shopToken)
return strings.EqualFold(strings.TrimSpace(signed["sign"]), signature)
}
func okPayNotifySign(form map[string]string, shopToken string) string {
orderedKeys := []string{
"code",
"data[order_id]",
"data[unique_id]",
"data[pay_user_id]",
"data[amount]",
"data[coin]",
"data[status]",
"data[type]",
"id",
"status",
}
pairs := make([]string, 0, len(orderedKeys))
for _, key := range orderedKeys {
value := strings.TrimSpace(form[key])
if value == "" {
continue
}
pairs = append(pairs, url.QueryEscape(key)+"="+url.QueryEscape(value))
}
if len(pairs) == 0 {
return ""
}
query, err := url.QueryUnescape(strings.Join(pairs, "&"))
if err != nil {
return ""
}
sum := md5.Sum([]byte(query + "&token=" + shopToken))
return strings.ToUpper(hex.EncodeToString(sum[:]))
}
func parseOkPayNotify(form map[string]string, rawFormData string) *okPayNotifyPayload {
return &okPayNotifyPayload{
ID: strings.TrimSpace(form["id"]),
Status: strings.TrimSpace(form["status"]),
Code: strings.TrimSpace(form["code"]),
OrderID: strings.TrimSpace(form["data[order_id]"]),
UniqueID: strings.TrimSpace(form["data[unique_id]"]),
PayUserID: strings.TrimSpace(form["data[pay_user_id]"]),
Amount: strings.TrimSpace(form["data[amount]"]),
Coin: strings.TrimSpace(form["data[coin]"]),
PayStatus: strings.TrimSpace(form["data[status]"]),
NotifyType: strings.TrimSpace(form["data[type]"]),
Sign: strings.TrimSpace(form["sign"]),
RawFormData: rawFormData,
}
}
func HandleOkPayNotify(form map[string]string, rawFormData string) error {
if !verifyOkPayNotify(form) {
return fmt.Errorf("invalid okpay notify sign")
}
payload := parseOkPayNotify(form, rawFormData)
if payload.ID == "" || payload.ID != data.GetOkPayShopID() {
return fmt.Errorf("okpay notify shop id mismatch: %s", payload.ID)
}
if payload.UniqueID == "" || payload.OrderID == "" {
return fmt.Errorf("missing okpay notify identifiers")
}
order, err := data.GetOrderInfoByTradeId(payload.UniqueID)
if err != nil {
return err
}
if order.ID <= 0 {
return fmt.Errorf("okpay notify order not found: %s", payload.UniqueID)
}
if order.PayProvider != "okpay" {
return fmt.Errorf("okpay notify order provider mismatch: %s", payload.UniqueID)
}
providerRow, err := data.GetProviderOrderByTradeIDAndProvider(payload.UniqueID, order.PayProvider)
if err != nil {
return err
}
if providerRow.ID <= 0 {
return fmt.Errorf("okpay provider order not found: %s", payload.UniqueID)
}
_ = data.SaveProviderOrderNotify(payload.UniqueID, order.PayProvider, payload.RawFormData)
if !strings.EqualFold(payload.Status, "success") || !strings.EqualFold(payload.NotifyType, "deposit") || payload.PayStatus != "1" {
log.Sugar.Infof("[okpay] notify ignored trade_id=%s status=%s pay_status=%s type=%s", payload.UniqueID, payload.Status, payload.PayStatus, payload.NotifyType)
return nil
}
if providerRow.ProviderOrderID != "" && providerRow.ProviderOrderID != payload.OrderID {
return fmt.Errorf("okpay provider order id mismatch: got=%s want=%s", payload.OrderID, providerRow.ProviderOrderID)
}
if !strings.EqualFold(strings.TrimSpace(order.Token), strings.TrimSpace(payload.Coin)) {
return fmt.Errorf("okpay coin mismatch: got=%s want=%s", payload.Coin, order.Token)
}
notifyAmount, err := strconv.ParseFloat(payload.Amount, 64)
if err != nil {
return fmt.Errorf("invalid okpay amount: %w", err)
}
if fmt.Sprintf("%.2f", notifyAmount) != fmt.Sprintf("%.2f", order.ActualAmount) {
return fmt.Errorf("okpay amount mismatch: got=%.2f want=%.2f", notifyAmount, order.ActualAmount)
}
err = OrderProcessing(&request.OrderProcessingRequest{
ReceiveAddress: order.ReceiveAddress,
Currency: order.Currency,
Token: order.Token,
Network: order.Network,
Amount: order.ActualAmount,
TradeId: order.TradeId,
BlockTransactionId: payload.OrderID,
})
if err != nil && err != constant.OrderBlockAlreadyProcess {
return err
}
if err = data.MarkProviderOrderPaid(payload.UniqueID, order.PayProvider, payload.RawFormData); err != nil {
return err
}
return nil
}
+149
View File
@@ -120,6 +120,7 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
RedirectUrl: req.RedirectUrl, RedirectUrl: req.RedirectUrl,
Name: req.Name, Name: req.Name,
PaymentType: req.PaymentType, PaymentType: req.PaymentType,
PayProvider: mdb.PaymentProviderOnChain,
ApiKeyID: apiKeyID(apiKey), ApiKeyID: apiKeyID(apiKey),
} }
if err = data.CreateOrderWithTransaction(tx, order); err != nil { if err = data.CreateOrderWithTransaction(tx, order); err != nil {
@@ -199,6 +200,11 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
if err = data.ExpireOrderByTradeId(sub.TradeId); err != nil { if err = data.ExpireOrderByTradeId(sub.TradeId); err != nil {
log.Sugar.Warnf("[order] expire sub-order failed, trade_id=%s, err=%v", sub.TradeId, err) log.Sugar.Warnf("[order] expire sub-order failed, trade_id=%s, err=%v", sub.TradeId, err)
} }
if sub.PayProvider != "" && sub.PayProvider != mdb.PaymentProviderOnChain {
if err = data.MarkProviderOrderExpired(sub.TradeId, sub.PayProvider); err != nil {
log.Sugar.Warnf("[order] expire provider order failed, trade_id=%s, provider=%s, err=%v", sub.TradeId, sub.PayProvider, err)
}
}
if err = data.UnLockTransaction(sub.Network, sub.ReceiveAddress, sub.Token, sub.ActualAmount); err != nil { if err = data.UnLockTransaction(sub.Network, sub.ReceiveAddress, sub.Token, sub.ActualAmount); err != nil {
log.Sugar.Warnf("[order] unlock sub-order transaction failed, trade_id=%s, err=%v", sub.TradeId, err) log.Sugar.Warnf("[order] unlock sub-order transaction failed, trade_id=%s, err=%v", sub.TradeId, err)
} }
@@ -257,6 +263,11 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
// Release sibling locks after their status transitions commit. // Release sibling locks after their status transitions commit.
for _, sib := range siblings { for _, sib := range siblings {
if sib.PayProvider != "" && sib.PayProvider != mdb.PaymentProviderOnChain {
if err = data.MarkProviderOrderExpired(sib.TradeId, sib.PayProvider); err != nil {
log.Sugar.Warnf("[order] expire sibling provider order failed, trade_id=%s, provider=%s, err=%v", sib.TradeId, sib.PayProvider, err)
}
}
if err = data.UnLockTransaction(sib.Network, sib.ReceiveAddress, sib.Token, sib.ActualAmount); err != nil { if err = data.UnLockTransaction(sib.Network, sib.ReceiveAddress, sib.Token, sib.ActualAmount); err != nil {
log.Sugar.Warnf("[order] unlock sibling transaction failed, trade_id=%s, err=%v", sib.TradeId, err) log.Sugar.Warnf("[order] unlock sibling transaction failed, trade_id=%s, err=%v", sib.TradeId, err)
} }
@@ -346,6 +357,10 @@ func SwitchNetwork(req *request.SwitchNetworkRequest) (*response.CheckoutCounter
return nil, constant.OrderNotWaitPay return nil, constant.OrderNotWaitPay
} }
if network == mdb.PaymentProviderOkPay {
return switchToOkPay(parent, token)
}
// 2. Same token+network as parent → mark selected and return // 2. Same token+network as parent → mark selected and return
if strings.EqualFold(parent.Token, token) && strings.EqualFold(parent.Network, network) { if strings.EqualFold(parent.Token, token) && strings.EqualFold(parent.Network, network) {
_ = data.MarkOrderSelected(parent.TradeId) _ = data.MarkOrderSelected(parent.TradeId)
@@ -427,6 +442,7 @@ func SwitchNetwork(req *request.SwitchNetworkRequest) (*response.CheckoutCounter
Name: parent.Name, Name: parent.Name,
CallBackConfirm: mdb.CallBackConfirmOk, // don't trigger callback on sub-order CallBackConfirm: mdb.CallBackConfirmOk, // don't trigger callback on sub-order
PaymentType: parent.PaymentType, PaymentType: parent.PaymentType,
PayProvider: mdb.PaymentProviderOnChain,
ApiKeyID: parent.ApiKeyID, // inherit from parent so resolveOrderApiKey never fails ApiKeyID: parent.ApiKeyID, // inherit from parent so resolveOrderApiKey never fails
} }
if err = data.CreateOrderWithTransaction(tx, subOrder); err != nil { if err = data.CreateOrderWithTransaction(tx, subOrder); err != nil {
@@ -458,7 +474,140 @@ func buildCheckoutResponse(order *mdb.Orders) *response.CheckoutCounterResponse
Network: order.Network, Network: order.Network,
ExpirationTime: order.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(), ExpirationTime: order.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
RedirectUrl: order.RedirectUrl, RedirectUrl: order.RedirectUrl,
PaymentUrl: fmt.Sprintf("%s/pay/checkout-counter/%s", config.GetAppUri(), order.TradeId),
CreatedAt: order.CreatedAt.TimestampMilli(), CreatedAt: order.CreatedAt.TimestampMilli(),
IsSelected: order.IsSelected, IsSelected: order.IsSelected,
} }
} }
func switchToOkPay(parent *mdb.Orders, token string) (*response.CheckoutCounterResponse, error) {
if !data.GetOkPayEnabled() {
return nil, constant.PaymentProviderNotEnabled
}
if data.GetOkPayShopID() == "" || data.GetOkPayShopToken() == "" || data.GetOkPayAPIURL() == "" || data.GetOkPayCallbackURL() == "" {
return nil, constant.PaymentProviderConfigErr
}
if !okPayTokenAllowed(token) {
return nil, constant.PaymentProviderNotSupport
}
existing, err := data.GetSubOrderByTokenPayProvider(parent.TradeId, token, mdb.PaymentProviderOkPay)
if err != nil {
return nil, err
}
if existing.ID > 0 {
providerRow, err := data.GetProviderOrderByTradeIDAndProvider(existing.TradeId, mdb.PaymentProviderOkPay)
if err != nil {
return nil, err
}
if providerRow.ID == 0 || strings.TrimSpace(providerRow.PayURL) == "" {
return nil, constant.SystemErr
}
_ = data.MarkOrderSelected(parent.TradeId)
_ = data.MarkOrderSelected(existing.TradeId)
_ = data.RefreshOrderExpiration(parent.TradeId)
existing.IsSelected = true
resp := buildCheckoutResponse(existing)
resp.PaymentUrl = providerRow.PayURL
return resp, nil
}
count, err := data.CountActiveSubOrders(parent.TradeId)
if err != nil {
return nil, err
}
if count >= MaxSubOrders {
return nil, constant.SubOrderLimitExceeded
}
rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(parent.Currency))
if rate <= 0 {
return nil, constant.RateAmountErr
}
decimalPayAmount := decimal.NewFromFloat(parent.Amount)
decimalTokenAmount := decimalPayAmount.Mul(decimal.NewFromFloat(rate))
if decimalTokenAmount.Cmp(decimal.NewFromFloat(UsdtMinimumPaymentAmount)) == -1 {
return nil, constant.PayAmountErr
}
subTradeID := GenerateCode()
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
returnURL := strings.TrimSpace(parent.RedirectUrl)
if returnURL == "" {
returnURL = data.GetOkPayReturnURL()
}
if returnURL == "" {
returnURL = fmt.Sprintf("%s/pay/checkout-counter/%s", config.GetAppUri(), parent.TradeId)
}
tx := dao.Mdb.Begin()
subOrder := &mdb.Orders{
TradeId: subTradeID,
OrderId: subTradeID,
ParentTradeId: parent.TradeId,
Amount: parent.Amount,
Currency: parent.Currency,
ActualAmount: amount,
ReceiveAddress: "OKPAY",
Token: token,
Network: mdb.NetworkTron,
Status: mdb.StatusWaitPay,
IsSelected: true,
NotifyUrl: "",
RedirectUrl: parent.RedirectUrl,
Name: parent.Name,
CallBackConfirm: mdb.CallBackConfirmOk,
PaymentType: parent.PaymentType,
PayProvider: mdb.PaymentProviderOkPay,
ApiKeyID: parent.ApiKeyID,
}
if err = data.CreateOrderWithTransaction(tx, subOrder); err != nil {
tx.Rollback()
return nil, err
}
providerRow := &mdb.ProviderOrder{
TradeId: subTradeID,
Provider: mdb.PaymentProviderOkPay,
ProviderOrderID: "",
PayURL: "",
Amount: amount,
Coin: token,
Status: mdb.ProviderOrderStatusCreating,
}
if err = data.CreateProviderOrderWithTransaction(tx, providerRow); err != nil {
tx.Rollback()
return nil, err
}
if err = tx.Commit().Error; err != nil {
tx.Rollback()
return nil, err
}
okpayOrder, err := createOkPayDepositOrder(subTradeID, amount, token, returnURL)
if err != nil {
_ = data.MarkProviderOrderFailed(subTradeID, mdb.PaymentProviderOkPay)
_ = data.ExpireOrderByTradeID(subTradeID)
return nil, err
}
if err = data.UpdateProviderOrderCreated(subTradeID, mdb.PaymentProviderOkPay, okpayOrder.ProviderOrderID, okpayOrder.PayURL); err != nil {
_ = data.MarkProviderOrderFailed(subTradeID, mdb.PaymentProviderOkPay)
_ = data.ExpireOrderByTradeID(subTradeID)
return nil, err
}
_ = data.MarkOrderSelected(parent.TradeId)
_ = data.RefreshOrderExpiration(parent.TradeId)
resp := buildCheckoutResponse(subOrder)
resp.PaymentUrl = okpayOrder.PayURL
return resp, nil
}
func okPayTokenAllowed(token string) bool {
for _, item := range data.GetOkPayAllowTokens() {
if strings.EqualFold(item, token) {
return true
}
}
return false
}
+75
View File
@@ -612,6 +612,81 @@ func TestOrderProcessingSubOrderPaidParentKeepsOwnFields(t *testing.T) {
} }
} }
func TestOrderProcessingParentDirectPayExpiresOkPayProviderOrder(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if _, err := data.AddWalletAddress("TTestTronAddress001"); err != nil {
t.Fatalf("add tron wallet: %v", err)
}
parentReq := newCreateTransactionRequest("order_parent_direct_okpay_expire", 1)
parentReq.Network = mdb.NetworkTron
parentResp, err := CreateTransaction(parentReq, nil)
if err != nil {
t.Fatalf("create parent order: %v", err)
}
subOrder := &mdb.Orders{
TradeId: "okpay_sub_parent_direct_expire",
OrderId: "okpay_sub_parent_direct_expire",
ParentTradeId: parentResp.TradeId,
Amount: parentResp.Amount,
Currency: parentResp.Currency,
ActualAmount: 0.15,
ReceiveAddress: "OKPAY",
Token: "USDT",
Network: mdb.NetworkTron,
Status: mdb.StatusWaitPay,
NotifyUrl: "",
CallBackConfirm: mdb.CallBackConfirmOk,
PayProvider: mdb.PaymentProviderOkPay,
}
if err := dao.Mdb.Create(subOrder).Error; err != nil {
t.Fatalf("create okpay sub-order: %v", err)
}
providerRow := &mdb.ProviderOrder{
TradeId: subOrder.TradeId,
Provider: mdb.PaymentProviderOkPay,
ProviderOrderID: "okp-parent-direct-expire",
PayURL: "https://t.me/ExampleWalletBot?start=shop_deposit--okpay-order-parent-direct-expire",
Amount: subOrder.ActualAmount,
Coin: subOrder.Token,
Status: mdb.ProviderOrderStatusPending,
}
if err := dao.Mdb.Create(providerRow).Error; err != nil {
t.Fatalf("create provider row: %v", err)
}
err = OrderProcessing(&request.OrderProcessingRequest{
ReceiveAddress: parentResp.ReceiveAddress,
Token: strings.ToUpper(parentResp.Token),
Network: mdb.NetworkTron,
TradeId: parentResp.TradeId,
Amount: parentResp.ActualAmount,
BlockTransactionId: "block_parent_direct_okpay_expire",
})
if err != nil {
t.Fatalf("order processing parent direct pay: %v", err)
}
expiredSub, err := data.GetOrderInfoByTradeId(subOrder.TradeId)
if err != nil {
t.Fatalf("reload sub-order: %v", err)
}
if expiredSub.Status != mdb.StatusExpired {
t.Fatalf("sub-order status = %d, want %d", expiredSub.Status, mdb.StatusExpired)
}
expiredProviderRow, err := data.GetProviderOrderByTradeIDAndProvider(subOrder.TradeId, mdb.PaymentProviderOkPay)
if err != nil {
t.Fatalf("reload provider row: %v", err)
}
if expiredProviderRow.Status != mdb.ProviderOrderStatusExpired {
t.Fatalf("provider row status = %q, want %q", expiredProviderRow.Status, mdb.ProviderOrderStatusExpired)
}
}
// TestOrderProcessingSubOrderExpiresSiblingsAndReleasesLocks verifies that when one // TestOrderProcessingSubOrderExpiresSiblingsAndReleasesLocks verifies that when one
// sub-order is paid all sibling sub-orders are expired and their runtime locks (as // sub-order is paid all sibling sub-orders are expired and their runtime locks (as
// well as the parent's lock) are released. // well as the parent's lock) are released.
+164 -309
View File
@@ -1,32 +1,22 @@
package service package service
import ( import (
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
"net/http"
"strings" "strings"
"sync"
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"
"github.com/GMWalletApp/epusdt/model/request" "github.com/GMWalletApp/epusdt/model/request"
"github.com/GMWalletApp/epusdt/notify" "github.com/GMWalletApp/epusdt/notify"
"github.com/GMWalletApp/epusdt/util/constant" "github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/http_client"
"github.com/GMWalletApp/epusdt/util/log" "github.com/GMWalletApp/epusdt/util/log"
"github.com/GMWalletApp/epusdt/util/math" "github.com/GMWalletApp/epusdt/util/math"
"github.com/dromara/carbon/v2"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/gookit/goutil/stdutil"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"github.com/tidwall/gjson"
) )
// resolveTronNode returns (baseURL, apiKey) for the TRON HTTP RPC node.
// It reads the first healthy (or any enabled) row from the rpc_nodes table.
func resolveTronNode() (string, string, error) { func resolveTronNode() (string, string, error) {
node, err := data.SelectRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp) node, err := data.SelectRpcNode(mdb.NetworkTron, mdb.RpcNodeTypeHttp)
if err != nil { if err != nil {
@@ -42,296 +32,128 @@ func resolveTronNode() (string, string, error) {
return rpcURL, node.ApiKey, nil return rpcURL, node.ApiKey, nil
} }
func Trc20CallBack(address string, wg *sync.WaitGroup) { func ResolveTronNode() (string, string, error) {
defer wg.Done() return resolveTronNode()
}
func TryProcessTronTRC20Transfer(toAddr string, rawValue *big.Int, txHash string, blockTsMs int64) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
log.Sugar.Error(err) log.Sugar.Errorf("[TRC20][%s] TryProcessTronTRC20Transfer panic: %v", toAddr, err)
} }
}() }()
var innerWg sync.WaitGroup addr := strings.TrimSpace(toAddr)
innerWg.Add(2) if addr == "" || rawValue == nil || rawValue.Sign() <= 0 {
go checkTrxTransfers(address, &innerWg) return
go checkTrc20Transfers(address, &innerWg) }
innerWg.Wait()
decimalQuant := decimal.NewFromBigInt(rawValue, 0)
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1_000_000)).InexactFloat64(), 2)
if amount <= 0 {
return
}
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, addr, "USDT", amount)
if err != nil {
log.Sugar.Warnf("[TRC20][%s] lock lookup: %v", addr, err)
return
}
if tradeID == "" {
log.Sugar.Debugf("[TRC20][%s] skip unmatched tx hash=%s amount=%.2f", addr, txHash, amount)
return
}
order, err := data.GetOrderInfoByTradeId(tradeID)
if err != nil {
log.Sugar.Warnf("[TRC20][%s] load order: %v", addr, err)
return
}
if blockTsMs > 0 && blockTsMs < order.CreatedAt.TimestampMilli() {
log.Sugar.Warnf("[TRC20][%s] skip tx %s because block time %d is before order create time %d", addr, txHash, blockTsMs, order.CreatedAt.TimestampMilli())
return
}
req := &request.OrderProcessingRequest{
ReceiveAddress: addr,
Token: "USDT",
Network: mdb.NetworkTron,
TradeId: tradeID,
Amount: amount,
BlockTransactionId: txHash,
}
err = OrderProcessing(req)
if err != nil {
if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
log.Sugar.Infof("[TRC20][%s] skip resolved transfer trade_id=%s hash=%s err=%v", addr, tradeID, txHash, err)
return
}
log.Sugar.Errorf("[TRC20][%s] OrderProcessing trade_id=%s hash=%s: %v", addr, tradeID, txHash, err)
return
}
sendPaymentNotification(order)
log.Sugar.Infof("[TRC20][%s] payment processed trade_id=%s hash=%s", addr, tradeID, txHash)
} }
func checkTrxTransfers(address string, wg *sync.WaitGroup) { func TryProcessTronTRXTransfer(toAddr string, rawSun int64, txHash string, blockTsMs int64) {
defer wg.Done() defer func() {
if err := recover(); err != nil {
log.Sugar.Errorf("[TRX][%s] TryProcessTronTRXTransfer panic: %v", toAddr, err)
}
}()
// Native TRX is gated by a chain_tokens row with empty addr := strings.TrimSpace(toAddr)
// contract_address and symbol=TRX. Admin can disable this row to if addr == "" || rawSun <= 0 {
// stop scanning native transfers without touching the chain toggle. return
trxCfg, err := data.GetEnabledChainTokenBySymbol(mdb.NetworkTron, "TRX") }
decimalQuant := decimal.NewFromInt(rawSun)
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1_000_000)).InexactFloat64(), 2)
if amount <= 0 {
return
}
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, addr, "TRX", amount)
if err != nil { if err != nil {
log.Sugar.Errorf("[TRX][%s] load chain_tokens err=%v", address, err) log.Sugar.Warnf("[TRX][%s] lock lookup: %v", addr, err)
return return
} }
if trxCfg == nil || trxCfg.ID == 0 { if tradeID == "" {
log.Sugar.Debugf("[TRX][%s] native TRX disabled in chain_tokens, skipping", address) log.Sugar.Debugf("[TRX][%s] skip unmatched tx hash=%s amount=%.2f", addr, txHash, amount)
return return
} }
trxDecimals := trxCfg.Decimals
if trxDecimals <= 0 {
trxDecimals = 6
}
client := http_client.GetHttpClient() order, err := data.GetOrderInfoByTradeId(tradeID)
startTime := carbon.Now().AddHours(-24).TimestampMilli()
endTime := carbon.Now().TimestampMilli()
tronBaseURL, tronAPIKey, err := resolveTronNode()
if err != nil { if err != nil {
log.Sugar.Errorf("[TRX][%s] resolve rpc_nodes err=%v", address, err) log.Sugar.Warnf("[TRX][%s] load order: %v", addr, err)
return
}
if blockTsMs > 0 && blockTsMs < order.CreatedAt.TimestampMilli() {
log.Sugar.Warnf("[TRX][%s] skip tx %s because block time %d is before order create time %d", addr, txHash, blockTsMs, order.CreatedAt.TimestampMilli())
return return
} }
url := fmt.Sprintf("%s/v1/accounts/%s/transactions", tronBaseURL, address)
resp, err := client.R().SetQueryParams(map[string]string{ req := &request.OrderProcessingRequest{
"order_by": "block_timestamp,desc", ReceiveAddress: addr,
"limit": "100", Token: "TRX",
"only_to": "true", Network: mdb.NetworkTron,
"min_timestamp": stdutil.ToString(startTime), TradeId: tradeID,
"max_timestamp": stdutil.ToString(endTime), Amount: amount,
}).SetHeader("TRON-PRO-API-KEY", tronAPIKey).Get(url) BlockTransactionId: txHash,
}
err = OrderProcessing(req)
if err != nil { if err != nil {
log.Sugar.Errorf("[TRX][%s] HTTP request failed: %v", address, err) if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
return log.Sugar.Infof("[TRX][%s] skip resolved transfer trade_id=%s hash=%s err=%v", addr, tradeID, txHash, err)
} return
if resp.StatusCode() != http.StatusOK { }
log.Sugar.Errorf("[TRX][%s] API returned status %d", address, resp.StatusCode()) log.Sugar.Errorf("[TRX][%s] OrderProcessing trade_id=%s hash=%s: %v", addr, tradeID, txHash, err)
return return
} }
success := gjson.GetBytes(resp.Body(), "success").Bool() sendPaymentNotification(order)
if !success { log.Sugar.Infof("[TRX][%s] payment processed trade_id=%s hash=%s", addr, tradeID, txHash)
log.Sugar.Errorf("[TRX][%s] API response indicates failure", address)
return
}
transfers := gjson.GetBytes(resp.Body(), "data").Array()
if len(transfers) == 0 {
log.Sugar.Debugf("[TRX][%s] no transfer records found", address)
return
}
log.Sugar.Debugf("[TRX][%s] fetched %d transfer records", address, len(transfers))
for i, transfer := range transfers {
if transfer.Get("raw_data.contract.0.type").String() != "TransferContract" {
continue
}
if transfer.Get("ret.0.contractRet").String() != "SUCCESS" {
continue
}
toAddressHex := transfer.Get("raw_data.contract.0.parameter.value.to_address").String()
toBytes, err := hex.DecodeString(toAddressHex)
if err != nil {
log.Sugar.Errorf("[TRX][%s] decode address failed on tx #%d: %v", address, i, err)
continue
}
if tron.EncodeCheck(toBytes) != address {
continue
}
rawAmount := transfer.Get("raw_data.contract.0.parameter.value.amount").String()
decimalQuant, err := decimal.NewFromString(rawAmount)
if err != nil {
log.Sugar.Errorf("[TRX][%s] parse amount failed on tx #%d: %v", address, i, err)
continue
}
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.New(1, int32(trxDecimals))).InexactFloat64(), 2)
if trxCfg.MinAmount > 0 && amount < trxCfg.MinAmount {
continue
}
if amount <= 0 {
continue
}
txID := transfer.Get("txID").String()
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, address, strings.ToUpper(strings.TrimSpace(trxCfg.Symbol)), amount)
if err != nil {
log.Sugar.Errorf("[TRX][%s] lookup trade_id failed hash=%s err=%v", address, txID, err)
continue
}
if tradeID == "" {
log.Sugar.Debugf("[TRX][%s] skip unmatched tx hash=%s amount=%.2f", address, txID, amount)
continue
}
log.Sugar.Infof("[TRX][%s] matched trade_id=%s hash=%s amount=%.2f", address, tradeID, txID, amount)
order, err := data.GetOrderInfoByTradeId(tradeID)
if err != nil {
log.Sugar.Errorf("[TRX][%s] get order failed trade_id=%s err=%v", address, tradeID, err)
continue
}
blockTimestamp := transfer.Get("block_timestamp").Int()
createTime := order.CreatedAt.TimestampMilli()
if blockTimestamp < createTime {
log.Sugar.Warnf("[TRX][%s] skip tx %s because block time %d is before order create time %d", address, txID, blockTimestamp, createTime)
continue
}
req := &request.OrderProcessingRequest{
ReceiveAddress: address,
Token: strings.ToUpper(strings.TrimSpace(trxCfg.Symbol)),
Network: mdb.NetworkTron,
TradeId: tradeID,
Amount: amount,
BlockTransactionId: txID,
}
err = OrderProcessing(req)
if err != nil {
if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
log.Sugar.Infof("[TRX][%s] skip resolved transfer trade_id=%s hash=%s err=%v", address, tradeID, txID, err)
continue
}
log.Sugar.Errorf("[TRX][%s] order processing failed trade_id=%s hash=%s err=%v", address, tradeID, txID, err)
continue
}
sendPaymentNotification(order)
log.Sugar.Infof("[TRX][%s] payment processed trade_id=%s hash=%s", address, tradeID, txID)
}
}
func checkTrc20Transfers(address string, wg *sync.WaitGroup) {
defer wg.Done()
// Build contract -> token map for the TRON network. If nothing is
// configured, skip — preserves the previous behavior of only watching
// admin-approved tokens.
tokens, err := data.ListEnabledChainTokensByNetwork(mdb.NetworkTron)
if err != nil {
log.Sugar.Errorf("[TRC20][%s] load chain_tokens err=%v", address, err)
return
}
if len(tokens) == 0 {
log.Sugar.Debugf("[TRC20][%s] no enabled chain_tokens, skipping", address)
return
}
contractTokens := make(map[string]*mdb.ChainToken, len(tokens))
for i := range tokens {
c := strings.TrimSpace(tokens[i].ContractAddress)
if c == "" {
continue
}
contractTokens[c] = &tokens[i]
}
client := http_client.GetHttpClient()
startTime := carbon.Now().AddHours(-24).TimestampMilli()
endTime := carbon.Now().TimestampMilli()
tronBaseURL, tronAPIKey, err := resolveTronNode()
if err != nil {
log.Sugar.Errorf("[TRC20][%s] resolve rpc_nodes err=%v", address, err)
return
}
url := fmt.Sprintf("%s/v1/accounts/%s/transactions/trc20", tronBaseURL, address)
resp, err := client.R().SetQueryParams(map[string]string{
"order_by": "block_timestamp,desc",
"limit": "100",
"only_to": "true",
"min_timestamp": stdutil.ToString(startTime),
"max_timestamp": stdutil.ToString(endTime),
}).SetHeader("TRON-PRO-API-KEY", tronAPIKey).Get(url)
if err != nil {
log.Sugar.Errorf("[TRC20][%s] HTTP request failed: %v", address, err)
return
}
if resp.StatusCode() != http.StatusOK {
log.Sugar.Errorf("[TRC20][%s] API returned status %d", address, resp.StatusCode())
return
}
success := gjson.GetBytes(resp.Body(), "success").Bool()
if !success {
log.Sugar.Errorf("[TRC20][%s] API response indicates failure", address)
return
}
transfers := gjson.GetBytes(resp.Body(), "data").Array()
if len(transfers) == 0 {
log.Sugar.Debugf("[TRC20][%s] no transfer records found", address)
return
}
log.Sugar.Debugf("[TRC20][%s] fetched %d transfer records", address, len(transfers))
for i, transfer := range transfers {
contractAddr := transfer.Get("token_info.address").String()
cfg, ok := contractTokens[contractAddr]
if !ok {
continue
}
if transfer.Get("to").String() != address {
continue
}
tokenSym := strings.ToUpper(strings.TrimSpace(cfg.Symbol))
valueStr := transfer.Get("value").String()
decimalQuant, err := decimal.NewFromString(valueStr)
if err != nil {
log.Sugar.Errorf("[TRC20][%s] parse value failed on tx #%d: %v", address, i, err)
continue
}
tokenDecimals := transfer.Get("token_info.decimals").Int()
if tokenDecimals <= 0 {
tokenDecimals = int64(cfg.Decimals)
}
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.New(1, int32(tokenDecimals))).InexactFloat64(), 2)
if cfg.MinAmount > 0 && amount < cfg.MinAmount {
continue
}
if amount <= 0 {
continue
}
txID := transfer.Get("transaction_id").String()
tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, address, tokenSym, amount)
if err != nil {
log.Sugar.Errorf("[TRC20][%s] lookup trade_id failed hash=%s err=%v", address, txID, err)
continue
}
if tradeID == "" {
log.Sugar.Debugf("[TRC20][%s] skip unmatched %s tx hash=%s amount=%.2f", address, tokenSym, txID, amount)
continue
}
log.Sugar.Infof("[TRC20][%s] matched %s trade_id=%s hash=%s amount=%.2f", address, tokenSym, tradeID, txID, amount)
order, err := data.GetOrderInfoByTradeId(tradeID)
if err != nil {
log.Sugar.Errorf("[TRC20][%s] get order failed trade_id=%s err=%v", address, tradeID, err)
continue
}
blockTimestamp := transfer.Get("block_timestamp").Int()
createTime := order.CreatedAt.TimestampMilli()
if blockTimestamp < createTime {
log.Sugar.Warnf("[TRC20][%s] skip tx %s because block time %d is before order create time %d", address, txID, blockTimestamp, createTime)
continue
}
req := &request.OrderProcessingRequest{
ReceiveAddress: address,
Token: tokenSym,
Network: mdb.NetworkTron,
TradeId: tradeID,
Amount: amount,
BlockTransactionId: txID,
}
err = OrderProcessing(req)
if err != nil {
if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
log.Sugar.Infof("[TRC20][%s] skip resolved transfer trade_id=%s hash=%s err=%v", address, tradeID, txID, err)
continue
}
log.Sugar.Errorf("[TRC20][%s] order processing failed trade_id=%s hash=%s err=%v", address, tradeID, txID, err)
continue
}
sendPaymentNotification(order)
log.Sugar.Infof("[TRC20][%s] payment processed trade_id=%s hash=%s", address, tradeID, txID)
}
} }
func evmChainLogLabel(chainNetwork string) string { func evmChainLogLabel(chainNetwork string) string {
@@ -349,9 +171,6 @@ func evmChainLogLabel(chainNetwork string) string {
} }
} }
// TryProcessEvmERC20Transfer 处理各 EVM 链上代币的 Transfer 入账。
// 代币识别、符号和 decimals 全部从 chain_tokens 表动态查询 —
// 管理后台新增 token 即可立即生效,无需代码改动。
func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, toAddr common.Address, rawValue *big.Int, txHash string, blockTsMs int64) { func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, toAddr common.Address, rawValue *big.Int, txHash string, blockTsMs int64) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
@@ -359,38 +178,67 @@ func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, to
} }
}() }()
net := evmChainLogLabel(chainNetwork) var usdt, usdc common.Address
token, err := data.GetEnabledChainTokenByContract(chainNetwork, contract.Hex()) var polygonUsdcE common.Address
if err != nil { switch chainNetwork {
log.Sugar.Warnf("[%s-WS] chain_tokens lookup err=%v contract=%s", net, err, contract.Hex()) case mdb.NetworkEthereum:
usdt = common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7")
usdc = common.HexToAddress("0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
case mdb.NetworkBsc:
usdt = common.HexToAddress("0x55d398326f99059fF775485246999027B3197955")
usdc = common.HexToAddress("0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d")
case mdb.NetworkPolygon:
usdt = common.HexToAddress("0xc2132D05D31c914a87C6611C10748AEb04B58e8F")
usdc = common.HexToAddress("0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359")
polygonUsdcE = common.HexToAddress("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174")
case mdb.NetworkPlasma:
// USDT0(官方),6 decimals;链上暂无与 ETH 同级的 Circle USDC 部署,仅匹配 USDT 订单
usdt = common.HexToAddress("0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb")
default:
return return
} }
if token == nil || token.ID == 0 {
log.Sugar.Debugf("[%s-WS] skip unconfigured contract %s", net, contract.Hex())
return
}
tokenSym := strings.ToUpper(strings.TrimSpace(token.Symbol))
if tokenSym == "" {
log.Sugar.Warnf("[%s-WS] chain_token id=%d has empty symbol", net, token.ID)
return
}
decimals := token.Decimals
if decimals <= 0 {
decimals = 6
}
var tokenSym string
switch {
case contract == usdt:
tokenSym = "USDT"
case contract == usdc || (polygonUsdcE != (common.Address{}) && contract == polygonUsdcE):
tokenSym = "USDC"
default:
net := evmChainLogLabel(chainNetwork)
log.Sugar.Warnf("[%s-WS] skip unsupported contract %s", net, contract.Hex())
return
}
net := evmChainLogLabel(chainNetwork)
walletAddr := strings.ToLower(toAddr.Hex()) walletAddr := strings.ToLower(toAddr.Hex())
if rawValue == nil || rawValue.Sign() <= 0 { if rawValue == nil || rawValue.Sign() <= 0 {
log.Sugar.Infof("[%s-%s][%s] skip non-positive or nil amount", net, tokenSym, walletAddr) log.Sugar.Infof("[%s-%s][%s] skip non-positive or nil amount", net, tokenSym, walletAddr)
return return
} }
divisor := decimal.New(1, int32(decimals))
decimalQuant := decimal.NewFromBigInt(rawValue, 0) chainTokens, err := data.ListChainTokens(chainNetwork)
amount := math.MustParsePrecFloat64(decimalQuant.Div(divisor).InexactFloat64(), 2) if err != nil {
if token.MinAmount > 0 && amount < token.MinAmount { log.Sugar.Warnf("[%s-%s] load chain tokens: %v", net, tokenSym, err)
log.Sugar.Debugf("[%s-%s][%s] skip amount %.2f below min_amount %.2f", net, tokenSym, walletAddr, amount, token.MinAmount)
return return
} }
var tokenConfig *mdb.ChainToken
for _, t := range chainTokens {
if strings.EqualFold(t.Symbol, tokenSym) {
tokenConfig = &t
break
}
}
if tokenConfig == nil || !tokenConfig.Enabled {
log.Sugar.Warnf("[%s-%s] token not enabled or configured in chain_tokens", net, tokenSym)
return
}
pow := decimal.New(1, int32(tokenConfig.Decimals))
log.Sugar.Warnf("tokenConfig.Decimals %d pow %s", tokenConfig.Decimals, pow.String())
decimalQuant := decimal.NewFromBigInt(rawValue, 0)
amount := math.MustParsePrecFloat64(decimalQuant.Div(pow).InexactFloat64(), 2)
if amount <= 0 { if amount <= 0 {
log.Sugar.Warnf("[%s-%s][%s] skip non-positive amount %.2f", net, tokenSym, walletAddr, amount) log.Sugar.Warnf("[%s-%s][%s] skip non-positive amount %.2f", net, tokenSym, walletAddr, amount)
return return
@@ -421,11 +269,6 @@ func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, to
log.Sugar.Warnf("[%s-%s][%s] skip trade_id=%s token mismatch order=%s", net, tokenSym, walletAddr, tradeID, order.Token) log.Sugar.Warnf("[%s-%s][%s] skip trade_id=%s token mismatch order=%s", net, tokenSym, walletAddr, tradeID, order.Token)
return return
} }
if blockTsMs > 0 && blockTsMs < order.CreatedAt.TimestampMilli() {
log.Sugar.Warnf("[%s-%s][%s] skip tx %s because block_time_ms=%d is before order created_ms=%d",
net, tokenSym, walletAddr, txHash, blockTsMs, order.CreatedAt.TimestampMilli())
return
}
req := &request.OrderProcessingRequest{ req := &request.OrderProcessingRequest{
ReceiveAddress: walletAddr, ReceiveAddress: walletAddr,
@@ -450,6 +293,18 @@ func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, to
} }
func sendPaymentNotification(order *mdb.Orders) { func sendPaymentNotification(order *mdb.Orders) {
if order == nil {
return
}
if strings.TrimSpace(order.TradeId) != "" {
latest, err := data.GetOrderInfoByTradeId(order.TradeId)
if err != nil {
log.Sugar.Warnf("[notify] reload order failed trade_id=%s err=%v", order.TradeId, err)
} else if latest != nil && latest.TradeId != "" {
order = latest
}
}
msg := fmt.Sprintf( msg := fmt.Sprintf(
"🎉 <b>收款成功通知</b>\n\n"+ "🎉 <b>收款成功通知</b>\n\n"+
"💰 <b>金额信息</b>\n"+ "💰 <b>金额信息</b>\n"+
@@ -472,7 +327,7 @@ func sendPaymentNotification(order *mdb.Orders) {
networkDisplay(order.Network), networkDisplay(order.Network),
order.ReceiveAddress, order.ReceiveAddress,
order.CreatedAt.ToDateTimeString(), order.CreatedAt.ToDateTimeString(),
carbon.Now().ToDateTimeString(), order.UpdatedAt.ToDateTimeString(),
) )
notify.Dispatch(mdb.NotifyEventPaySuccess, msg) notify.Dispatch(mdb.NotifyEventPaySuccess, msg)
} }
+79
View File
@@ -0,0 +1,79 @@
package service
import (
"strings"
"testing"
"time"
"github.com/GMWalletApp/epusdt/internal/testutil"
"github.com/GMWalletApp/epusdt/model/dao"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/notify"
)
func TestSendPaymentNotificationUsesLatestOrderUpdatedAt(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
const channelType = "test-pay-success-time"
got := make(chan string, 1)
notify.RegisterSender(channelType, func(config, text string) error {
got <- text
return nil
})
if err := dao.Mdb.Create(&mdb.NotificationChannel{
Type: channelType,
Name: "test",
Config: "{}",
Events: `{"pay_success":true}`,
Enabled: true,
}).Error; err != nil {
t.Fatalf("seed notification channel: %v", err)
}
order := &mdb.Orders{
TradeId: "T202604270001",
OrderId: "ORD202604270001",
Amount: 100,
Currency: "cny",
ActualAmount: 14.28,
Token: "USDT",
Network: mdb.NetworkTron,
ReceiveAddress: "TTestAddress",
Status: mdb.StatusWaitPay,
}
if err := dao.Mdb.Create(order).Error; err != nil {
t.Fatalf("seed order: %v", err)
}
const createdAt = "2026-04-27 09:00:00"
const staleUpdatedAt = "2026-04-27 09:01:00"
const paidAt = "2026-04-27 10:20:30"
if err := dao.Mdb.Exec("UPDATE orders SET created_at = ?, updated_at = ? WHERE trade_id = ?", createdAt, staleUpdatedAt, order.TradeId).Error; err != nil {
t.Fatalf("set initial timestamps: %v", err)
}
var staleOrderModel mdb.Orders
if err := dao.Mdb.Where("trade_id = ?", order.TradeId).Take(&staleOrderModel).Error; err != nil {
t.Fatalf("load stale order: %v", err)
}
if err := dao.Mdb.Exec("UPDATE orders SET status = ?, updated_at = ? WHERE trade_id = ?", mdb.StatusPaySuccess, paidAt, order.TradeId).Error; err != nil {
t.Fatalf("set paid timestamp: %v", err)
}
sendPaymentNotification(&staleOrderModel)
select {
case text := <-got:
if !strings.Contains(text, "支付时间:"+paidAt) {
t.Fatalf("notification payment time = %q, want %s", text, paidAt)
}
if strings.Contains(text, "支付时间:"+staleUpdatedAt) {
t.Fatalf("notification used stale payment time: %q", text)
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for notification")
}
}
+114 -1
View File
@@ -682,10 +682,31 @@ func TestAdminDashboard_AllRoutes(t *testing.T) {
func TestAdminSettings_ListAndUpsert(t *testing.T) { func TestAdminSettings_ListAndUpsert(t *testing.T) {
e, token := setupAdminTestEnv(t) e, token := setupAdminTestEnv(t)
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayShopToken, "token-visible-in-settings", mdb.SettingTypeString); err != nil {
t.Fatalf("seed okpay.shop_token: %v", err)
}
// List settings. // List settings.
rec := doGetAdmin(e, "/admin/api/v1/settings", token) rec := doGetAdmin(e, "/admin/api/v1/settings", token)
t.Logf("ListSettings: %s", rec.Body.String()) t.Logf("ListSettings: %s", rec.Body.String())
assertOK(t, rec) resp := assertOK(t, rec)
rows, ok := resp["data"].([]interface{})
if !ok {
t.Fatalf("expected settings array, got %T", resp["data"])
}
foundToken := false
for _, row := range rows {
item, _ := row.(map[string]interface{})
if item["key"] == mdb.SettingKeyOkPayShopToken {
foundToken = true
if item["value"] != "token-visible-in-settings" {
t.Fatalf("okpay.shop_token value = %v, want token-visible-in-settings", item["value"])
}
}
}
if !foundToken {
t.Fatalf("expected admin settings list to expose %s", mdb.SettingKeyOkPayShopToken)
}
// Upsert a setting. // Upsert a setting.
rec = doPutAdmin(e, "/admin/api/v1/settings", map[string]interface{}{ rec = doPutAdmin(e, "/admin/api/v1/settings", map[string]interface{}{
@@ -708,6 +729,98 @@ func TestAdminSettings_DeleteNonExistent(t *testing.T) {
} }
} }
func TestAdminConfig_ExposesOkPayCredentials(t *testing.T) {
e, token := setupAdminTestEnv(t)
if err := data.SetSetting(mdb.SettingGroupBrand, mdb.SettingKeyBrandCheckoutName, "admin cashier", mdb.SettingTypeString); err != nil {
t.Fatalf("seed brand.checkout_name: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayEnabled, "true", mdb.SettingTypeBool); err != nil {
t.Fatalf("seed okpay.enabled: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayShopID, "shop-admin-1", mdb.SettingTypeString); err != nil {
t.Fatalf("seed okpay.shop_id: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayShopToken, "token-admin-1", mdb.SettingTypeString); err != nil {
t.Fatalf("seed okpay.shop_token: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayCallbackURL, "https://example.com/notify", mdb.SettingTypeString); err != nil {
t.Fatalf("seed okpay.callback_url: %v", err)
}
rec := doGetAdmin(e, "/admin/api/v1/config", token)
resp := assertOK(t, rec)
dataObj, _ := resp["data"].(map[string]interface{})
site, ok := dataObj["site"].(map[string]interface{})
if !ok {
t.Fatalf("expected site object, got %T", dataObj["site"])
}
if site["cashier_name"] != "admin cashier" {
t.Fatalf("cashier_name = %v, want admin cashier", site["cashier_name"])
}
okpay, ok := dataObj["okpay"].(map[string]interface{})
if !ok {
t.Fatalf("expected okpay object, got %T", dataObj["okpay"])
}
if okpay["shop_id"] != "shop-admin-1" {
t.Fatalf("shop_id = %v, want shop-admin-1", okpay["shop_id"])
}
if okpay["shop_token"] != "token-admin-1" {
t.Fatalf("shop_token = %v, want token-admin-1", okpay["shop_token"])
}
if okpay["callback_url"] != "https://example.com/notify" {
t.Fatalf("callback_url = %v", okpay["callback_url"])
}
}
func TestAdminSettings_UpsertBrandReflectsInConfig(t *testing.T) {
e, token := setupAdminTestEnv(t)
updateBrand := func(cashier, logo, title, support string) {
rec := doPutAdmin(e, "/admin/api/v1/settings", map[string]interface{}{
"items": []map[string]interface{}{
{"group": mdb.SettingGroupBrand, "key": mdb.SettingKeyBrandCheckoutName, "value": cashier, "type": mdb.SettingTypeString},
{"group": mdb.SettingGroupBrand, "key": mdb.SettingKeyBrandLogoUrl, "value": logo, "type": mdb.SettingTypeString},
{"group": mdb.SettingGroupBrand, "key": mdb.SettingKeyBrandSiteTitle, "value": title, "type": mdb.SettingTypeString},
{"group": mdb.SettingGroupBrand, "key": mdb.SettingKeyBrandSupportUrl, "value": support, "type": mdb.SettingTypeString},
},
}, token)
assertOK(t, rec)
}
assertSite := func(path, wantCashier, wantLogo, wantTitle, wantSupport string) {
rec := doGet(e, path)
if strings.HasPrefix(path, "/admin/") {
rec = doGetAdmin(e, path, token)
}
resp := assertOK(t, rec)
dataObj, _ := resp["data"].(map[string]interface{})
site, ok := dataObj["site"].(map[string]interface{})
if !ok {
t.Fatalf("%s expected site object, got %T", path, dataObj["site"])
}
if site["cashier_name"] != wantCashier {
t.Fatalf("%s cashier_name=%v, want %s", path, site["cashier_name"], wantCashier)
}
if site["logo_url"] != wantLogo {
t.Fatalf("%s logo_url=%v, want %s", path, site["logo_url"], wantLogo)
}
if site["website_title"] != wantTitle {
t.Fatalf("%s website_title=%v, want %s", path, site["website_title"], wantTitle)
}
if site["support_link"] != wantSupport {
t.Fatalf("%s support_link=%v, want %s", path, site["support_link"], wantSupport)
}
}
updateBrand("cashier-v1", "https://cdn.example.com/v1.png", "title-v1", "https://example.com/help-v1")
assertSite("/payments/gmpay/v1/config", "cashier-v1", "https://cdn.example.com/v1.png", "title-v1", "https://example.com/help-v1")
assertSite("/admin/api/v1/config", "cashier-v1", "https://cdn.example.com/v1.png", "title-v1", "https://example.com/help-v1")
updateBrand("cashier-v2", "https://cdn.example.com/v2.png", "title-v2", "https://example.com/help-v2")
assertSite("/payments/gmpay/v1/config", "cashier-v2", "https://cdn.example.com/v2.png", "title-v2", "https://example.com/help-v2")
assertSite("/admin/api/v1/config", "cashier-v2", "https://cdn.example.com/v2.png", "title-v2", "https://example.com/help-v2")
}
// ─── Notification Channels ─────────────────────────────────────────────────── // ─── Notification Channels ───────────────────────────────────────────────────
// TestAdminNotificationChannels_CRUD verifies notification channel routes. // TestAdminNotificationChannels_CRUD verifies notification channel routes.
+16 -7
View File
@@ -22,12 +22,20 @@ import (
// RegisterRoute 路由注册 // RegisterRoute 路由注册
func RegisterRoute(e *echo.Echo) { func RegisterRoute(e *echo.Echo) {
e.Any("/", func(c echo.Context) error { e.POST("/", func(c echo.Context) error {
return c.String(http.StatusOK, "hello epusdt, https://github.com/GMwalletApp/epusdt") return c.String(http.StatusOK, "hello epusdt, https://github.com/GMwalletApp/epusdt")
}) })
payRoute := e.Group("/pay") payRoute := e.Group("/pay")
payRoute.GET("/checkout-counter/:trade_id", comm.Ctrl.CheckoutCounter) payRoute.GET("/checkout-counter/:trade_id", func(ctx echo.Context) error {
tradeId := ctx.Param("trade_id")
targetURL := fmt.Sprintf("/cashier/%s", tradeId)
return ctx.Redirect(http.StatusMovedPermanently, targetURL)
})
payRoute.GET("/checkout-counter-resp/:trade_id", comm.Ctrl.CheckoutCounter)
payRoute.GET("/check-status/:trade_id", comm.Ctrl.CheckStatus) payRoute.GET("/check-status/:trade_id", comm.Ctrl.CheckStatus)
payRoute.POST("/switch-network", comm.Ctrl.SwitchNetwork) payRoute.POST("/switch-network", comm.Ctrl.SwitchNetwork)
@@ -37,9 +45,11 @@ func RegisterRoute(e *echo.Echo) {
// gmpay v1 routes // gmpay v1 routes
gmpayV1 := paymentRoute.Group("/gmpay/v1") gmpayV1 := paymentRoute.Group("/gmpay/v1")
gmpayV1.POST("/order/create-transaction", comm.Ctrl.CreateTransaction, middleware.CheckApiSign()) gmpayV1.POST("/order/create-transaction", comm.Ctrl.CreateTransaction, middleware.CheckApiSign())
gmpayV1.GET("/supported-assets", comm.Ctrl.GetSupportedAssets) gmpayV1.GET("/config", comm.Ctrl.GetPublicConfig)
// gmpayV1.GET("/supported-assets/records", comm.Ctrl.ListSupportedAssetRecords)
// gmpayV1.GET("/supported-assets/:id", comm.Ctrl.GetSupportedAsset) // okpay v1 routes
okpayV1 := paymentRoute.Group("/okpay/v1")
okpayV1.POST("/notify", comm.Ctrl.OkPayNotify)
// epay v1 routes // epay v1 routes
// //
@@ -214,8 +224,7 @@ func registerAdminRoutes(e *echo.Echo) {
authed.POST("/notification-channels/:id/status", admin.Ctrl.ChangeNotificationChannelStatus) authed.POST("/notification-channels/:id/status", admin.Ctrl.ChangeNotificationChannelStatus)
authed.DELETE("/notification-channels/:id", admin.Ctrl.DeleteNotificationChannel) authed.DELETE("/notification-channels/:id", admin.Ctrl.DeleteNotificationChannel)
// gmpayV1.GET("/supported-assets", comm.Ctrl.GetSupportedAssets) authed.GET("/config", comm.Ctrl.GetPublicConfig) // wrap for admin console, same payload as public endpoint
authed.GET("/supported-assets", comm.Ctrl.GetSupportedAssets) // wrap for admin console, same handler as public endpoint
// Chains // Chains
authed.GET("/chains", admin.Ctrl.ListChains) authed.GET("/chains", admin.Ctrl.ListChains)
+632 -14
View File
@@ -1,8 +1,11 @@
package route package route
import ( import (
"crypto/md5"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
@@ -14,8 +17,10 @@ import (
"github.com/GMWalletApp/epusdt/model/dao" "github.com/GMWalletApp/epusdt/model/dao"
"github.com/GMWalletApp/epusdt/model/data" "github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/util/http_client"
"github.com/GMWalletApp/epusdt/util/log" "github.com/GMWalletApp/epusdt/util/log"
"github.com/GMWalletApp/epusdt/util/sign" "github.com/GMWalletApp/epusdt/util/sign"
"github.com/go-resty/resty/v2"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/spf13/viper" "github.com/spf13/viper"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
@@ -64,6 +69,7 @@ func setupTestEnv(t *testing.T) *echo.Echo {
&mdb.Chain{}, &mdb.Chain{},
&mdb.ChainToken{}, &mdb.ChainToken{},
&mdb.RpcNode{}, &mdb.RpcNode{},
&mdb.ProviderOrder{},
) )
// reset the settings cache so stale entries from a prior test don't leak // reset the settings cache so stale entries from a prior test don't leak
@@ -136,6 +142,47 @@ func signBody(body map[string]interface{}) map[string]interface{} {
return body return body
} }
func signOkPayNotifyPayload(shopToken string, body map[string]interface{}) string {
data, _ := body["data"].(map[string]interface{})
ordered := []struct {
key string
value string
}{
{"code", stringifyOkPayTestValue(body["code"])},
{"data[order_id]", stringifyOkPayTestValue(data["order_id"])},
{"data[unique_id]", stringifyOkPayTestValue(data["unique_id"])},
{"data[pay_user_id]", stringifyOkPayTestValue(data["pay_user_id"])},
{"data[amount]", stringifyOkPayTestValue(data["amount"])},
{"data[coin]", stringifyOkPayTestValue(data["coin"])},
{"data[status]", stringifyOkPayTestValue(data["status"])},
{"data[type]", stringifyOkPayTestValue(data["type"])},
{"id", stringifyOkPayTestValue(body["id"])},
{"status", stringifyOkPayTestValue(body["status"])},
}
pairs := make([]string, 0, len(ordered))
for _, item := range ordered {
if item.value == "" {
continue
}
pairs = append(pairs, url.QueryEscape(item.key)+"="+url.QueryEscape(item.value))
}
query, _ := url.QueryUnescape(strings.Join(pairs, "&"))
sum := md5.Sum([]byte(query + "&token=" + shopToken))
return strings.ToUpper(hex.EncodeToString(sum[:]))
}
func stringifyOkPayTestValue(value interface{}) string {
switch v := value.(type) {
case nil:
return ""
case string:
return strings.TrimSpace(v)
default:
return strings.TrimSpace(fmt.Sprint(v))
}
}
func doPost(e *echo.Echo, path string, body map[string]interface{}) *httptest.ResponseRecorder { func doPost(e *echo.Echo, path string, body map[string]interface{}) *httptest.ResponseRecorder {
jsonBytes, _ := json.Marshal(body) jsonBytes, _ := json.Marshal(body)
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(string(jsonBytes))) req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(string(jsonBytes)))
@@ -167,6 +214,21 @@ func doFormPost(e *echo.Echo, path string, values url.Values) *httptest.Response
return rec return rec
} }
func TestRootPostRoute(t *testing.T) {
e := setupTestEnv(t)
req := httptest.NewRequest(http.MethodPost, "/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
if body := rec.Body.String(); body != "hello epusdt, https://github.com/GMwalletApp/epusdt" {
t.Fatalf("unexpected body: %q", body)
}
}
// TestCreateOrderGmpayV1Solana tests the gmpay route with solana network. // TestCreateOrderGmpayV1Solana tests the gmpay route with solana network.
func TestCreateOrderGmpayV1Solana(t *testing.T) { func TestCreateOrderGmpayV1Solana(t *testing.T) {
e := setupTestEnv(t) e := setupTestEnv(t)
@@ -296,11 +358,11 @@ func TestCreateOrderGmpayV1FormData(t *testing.T) {
t.Logf("Form-data order created: trade_id=%v", data["trade_id"]) t.Logf("Form-data order created: trade_id=%v", data["trade_id"])
} }
// getSupportedNetworks is a helper that calls GET /payments/gmpay/v1/supported-assets // getSupportedNetworks is a helper that calls GET /payments/gmpay/v1/config
// and returns a map of network → []token for easy assertions. // and returns a map of network → []token for easy assertions.
func getSupportedNetworks(t *testing.T, e *echo.Echo) map[string][]string { func getSupportedNetworks(t *testing.T, e *echo.Echo) map[string][]string {
t.Helper() t.Helper()
req := httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/supported-assets", nil) req := httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/config", nil)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
e.ServeHTTP(rec, req) e.ServeHTTP(rec, req)
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
@@ -308,7 +370,7 @@ func getSupportedNetworks(t *testing.T, e *echo.Echo) map[string][]string {
} }
resp := parseResp(t, rec) resp := parseResp(t, rec)
rawData, _ := resp["data"].(map[string]interface{}) rawData, _ := resp["data"].(map[string]interface{})
rawSupports, _ := rawData["supports"].([]interface{}) rawSupports, _ := rawData["supported_assets"].([]interface{})
result := make(map[string][]string, len(rawSupports)) result := make(map[string][]string, len(rawSupports))
for _, item := range rawSupports { for _, item := range rawSupports {
row := item.(map[string]interface{}) row := item.(map[string]interface{})
@@ -325,7 +387,7 @@ func getSupportedNetworks(t *testing.T, e *echo.Echo) map[string][]string {
// TestGetSupportedAssets_ChainTokenToggle verifies that: // TestGetSupportedAssets_ChainTokenToggle verifies that:
// - disabling a chain_token removes that token (and possibly the whole network) // - disabling a chain_token removes that token (and possibly the whole network)
// from the /supported-assets response // from the /config supported_assets response
// - re-enabling it brings it back // - re-enabling it brings it back
func TestGetSupportedAssets_ChainTokenToggle(t *testing.T) { func TestGetSupportedAssets_ChainTokenToggle(t *testing.T) {
e := setupTestEnv(t) e := setupTestEnv(t)
@@ -333,7 +395,7 @@ func TestGetSupportedAssets_ChainTokenToggle(t *testing.T) {
// Baseline: tron should appear with USDT and TRX. // Baseline: tron should appear with USDT and TRX.
before := getSupportedNetworks(t, e) before := getSupportedNetworks(t, e)
if _, ok := before["tron"]; !ok { if _, ok := before["tron"]; !ok {
t.Fatal("expected tron in baseline supported-assets") t.Fatal("expected tron in baseline config.supported_assets")
} }
// Disable the tron USDT chain_token. // Disable the tron USDT chain_token.
@@ -357,7 +419,7 @@ func TestGetSupportedAssets_ChainTokenToggle(t *testing.T) {
afterAllDisabled := getSupportedNetworks(t, e) afterAllDisabled := getSupportedNetworks(t, e)
if _, ok := afterAllDisabled["tron"]; ok { if _, ok := afterAllDisabled["tron"]; ok {
t.Fatal("tron should disappear from supported-assets when all its chain_tokens are disabled") t.Fatal("tron should disappear from config.supported_assets when all its chain_tokens are disabled")
} }
t.Logf("After disabling all tron tokens: networks = %v", afterAllDisabled) t.Logf("After disabling all tron tokens: networks = %v", afterAllDisabled)
@@ -385,7 +447,7 @@ func TestGetSupportedAssets_ChainTokenToggle(t *testing.T) {
// TestGetSupportedAssets_WalletAddressToggle verifies that: // TestGetSupportedAssets_WalletAddressToggle verifies that:
// - disabling ALL wallet addresses for a network removes that network from // - disabling ALL wallet addresses for a network removes that network from
// the /supported-assets response (even if chain + tokens are still enabled) // the /config supported_assets response (even if chain + tokens are still enabled)
// - re-enabling any address brings the network back // - re-enabling any address brings the network back
func TestGetSupportedAssets_WalletAddressToggle(t *testing.T) { func TestGetSupportedAssets_WalletAddressToggle(t *testing.T) {
e := setupTestEnv(t) e := setupTestEnv(t)
@@ -393,7 +455,7 @@ func TestGetSupportedAssets_WalletAddressToggle(t *testing.T) {
// Baseline: solana should be present. // Baseline: solana should be present.
before := getSupportedNetworks(t, e) before := getSupportedNetworks(t, e)
if _, ok := before["solana"]; !ok { if _, ok := before["solana"]; !ok {
t.Fatal("expected solana in baseline supported-assets") t.Fatal("expected solana in baseline config.supported_assets")
} }
// Disable the only solana wallet address. // Disable the only solana wallet address.
@@ -403,7 +465,7 @@ func TestGetSupportedAssets_WalletAddressToggle(t *testing.T) {
after := getSupportedNetworks(t, e) after := getSupportedNetworks(t, e)
if _, ok := after["solana"]; ok { if _, ok := after["solana"]; ok {
t.Fatal("solana should disappear from supported-assets when all its wallet addresses are disabled") t.Fatal("solana should disappear from config.supported_assets when all its wallet addresses are disabled")
} }
t.Logf("After disabling solana wallets: networks = %v", after) t.Logf("After disabling solana wallets: networks = %v", after)
@@ -419,10 +481,22 @@ func TestGetSupportedAssets_WalletAddressToggle(t *testing.T) {
t.Logf("After re-enabling solana wallets: solana tokens = %v", restored["solana"]) t.Logf("After re-enabling solana wallets: solana tokens = %v", restored["solana"])
} }
func TestGetSupportedAssetsPublic(t *testing.T) { func TestGetPublicConfig(t *testing.T) {
e := setupTestEnv(t) e := setupTestEnv(t)
if err := data.SetSetting(mdb.SettingGroupBrand, mdb.SettingKeyBrandCheckoutName, "asd", mdb.SettingTypeString); err != nil {
t.Fatalf("seed brand.checkout_name: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupBrand, mdb.SettingKeyBrandLogoUrl, "https://cdn.example.com/logo.png", mdb.SettingTypeString); err != nil {
t.Fatalf("seed brand.logo_url: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupBrand, mdb.SettingKeyBrandSiteTitle, "asd title", mdb.SettingTypeString); err != nil {
t.Fatalf("seed brand.site_title: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupBrand, mdb.SettingKeyBrandSupportUrl, "https://example.com/support", mdb.SettingTypeString); err != nil {
t.Fatalf("seed brand.support_url: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/supported-assets", nil) req := httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/config", nil)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
e.ServeHTTP(rec, req) e.ServeHTTP(rec, req)
@@ -435,17 +509,57 @@ func TestGetSupportedAssetsPublic(t *testing.T) {
t.Fatalf("expected status_code=200, got %v", resp["status_code"]) t.Fatalf("expected status_code=200, got %v", resp["status_code"])
} }
data, ok := resp["data"].(map[string]interface{}) respData, ok := resp["data"].(map[string]interface{})
if !ok { if !ok {
t.Fatalf("expected object data, got %T", resp["data"]) t.Fatalf("expected object data, got %T", resp["data"])
} }
supports, ok := data["supports"].([]interface{}) supports, ok := respData["supported_assets"].([]interface{})
if !ok { if !ok {
t.Fatalf("expected supports array, got %T", data["supports"]) t.Fatalf("expected supported_assets array, got %T", respData["supported_assets"])
} }
if len(supports) < 2 { if len(supports) < 2 {
t.Fatalf("expected >= 2 network supports, got %d", len(supports)) t.Fatalf("expected >= 2 network supports, got %d", len(supports))
} }
site, ok := respData["site"].(map[string]interface{})
if !ok {
t.Fatalf("expected site object, got %T", respData["site"])
}
if site["cashier_name"] != "asd" {
t.Fatalf("site.cashier_name = %v, want asd", site["cashier_name"])
}
if site["logo_url"] != "https://cdn.example.com/logo.png" {
t.Fatalf("site.logo_url = %v", site["logo_url"])
}
if site["website_title"] != "asd title" {
t.Fatalf("site.website_title = %v, want asd title", site["website_title"])
}
if site["support_link"] != "https://example.com/support" {
t.Fatalf("site.support_link = %v", site["support_link"])
}
epay, ok := respData["epay"].(map[string]interface{})
if !ok {
t.Fatalf("expected epay object, got %T", respData["epay"])
}
if epay["default_network"] != "tron" {
t.Fatalf("epay.default_network = %v, want tron", epay["default_network"])
}
okpay, ok := respData["okpay"].(map[string]interface{})
if !ok {
t.Fatalf("expected okpay object, got %T", respData["okpay"])
}
if okpay["enabled"] != false {
t.Fatalf("okpay.enabled = %v, want false", okpay["enabled"])
}
allowTokens, ok := okpay["allow_tokens"].([]interface{})
if !ok || len(allowTokens) != 2 {
t.Fatalf("expected okpay.allow_tokens array, got %T %#v", okpay["allow_tokens"], okpay["allow_tokens"])
}
if _, exists := okpay["shop_id"]; exists {
t.Fatalf("public config should not expose okpay.shop_id: %#v", okpay)
}
if _, exists := okpay["shop_token"]; exists {
t.Fatalf("public config should not expose okpay.shop_token: %#v", okpay)
}
seen := map[string]bool{} seen := map[string]bool{}
for _, item := range supports { for _, item := range supports {
@@ -458,6 +572,322 @@ func TestGetSupportedAssetsPublic(t *testing.T) {
t.Fatalf("missing network support: %s", n) t.Fatalf("missing network support: %s", n)
} }
} }
if got := data.GetOkPayCallbackURL(); got != "http://localhost:8080/payments/okpay/v1/notify" {
t.Fatalf("default okpay callback_url = %q, want %q", got, "http://localhost:8080/payments/okpay/v1/notify")
}
}
func TestGetPublicConfig_BrandLegacyFallback(t *testing.T) {
e := setupTestEnv(t)
if err := data.SetSetting(mdb.SettingGroupBrand, mdb.SettingKeyBrandSiteName, "legacy cashier", mdb.SettingTypeString); err != nil {
t.Fatalf("seed brand.site_name: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupBrand, mdb.SettingKeyBrandPageTitle, "legacy title", mdb.SettingTypeString); err != nil {
t.Fatalf("seed brand.page_title: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupBrand, mdb.SettingKeyBrandSupportUrl, "https://legacy.example.com/help", mdb.SettingTypeString); err != nil {
t.Fatalf("seed brand.support_url: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/config", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d, body=%s", rec.Code, rec.Body.String())
}
resp := parseResp(t, rec)
respData, ok := resp["data"].(map[string]interface{})
if !ok {
t.Fatalf("expected object data, got %T", resp["data"])
}
site, ok := respData["site"].(map[string]interface{})
if !ok {
t.Fatalf("expected site object, got %T", respData["site"])
}
if site["cashier_name"] != "legacy cashier" {
t.Fatalf("site.cashier_name = %v, want legacy cashier", site["cashier_name"])
}
if site["website_title"] != "legacy title" {
t.Fatalf("site.website_title = %v, want legacy title", site["website_title"])
}
if site["support_link"] != "https://legacy.example.com/help" {
t.Fatalf("site.support_link = %v", site["support_link"])
}
}
func TestOkPayNotifyRouteExists(t *testing.T) {
e := setupTestEnv(t)
req := httptest.NewRequest(http.MethodPost, "/payments/okpay/v1/notify", strings.NewReader(""))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code == http.StatusNotFound {
t.Fatalf("expected okpay notify route to exist, got 404")
}
}
type okPayNotifyFixture struct {
Parent *mdb.Orders
Child *mdb.Orders
ProviderRow *mdb.ProviderOrder
Payload map[string]interface{}
}
func seedOkPayNotifyFixture(t *testing.T) *okPayNotifyFixture {
t.Helper()
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayEnabled, "true", mdb.SettingTypeBool); err != nil {
t.Fatalf("seed okpay.enabled: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayShopID, "okpay-shop-test", mdb.SettingTypeString); err != nil {
t.Fatalf("seed okpay.shop_id: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayShopToken, "token-1", mdb.SettingTypeString); err != nil {
t.Fatalf("seed okpay.shop_token: %v", err)
}
parent := &mdb.Orders{
TradeId: "parent-okpay-notify-001",
OrderId: "merchant-okpay-notify-001",
Amount: 1,
Currency: "CNY",
ActualAmount: 0.15,
ReceiveAddress: "TTestTronAddress001",
Token: "USDT",
Network: mdb.NetworkTron,
Status: mdb.StatusWaitPay,
NotifyUrl: "http://localhost/notify",
PaymentType: mdb.PaymentTypeEpay,
PayProvider: mdb.PaymentProviderOnChain,
}
if err := dao.Mdb.Create(parent).Error; err != nil {
t.Fatalf("create parent order: %v", err)
}
child := &mdb.Orders{
TradeId: "okpay-unique-test-001",
ParentTradeId: parent.TradeId,
OrderId: "merchant-okpay-notify-001-sub",
Amount: 1,
Currency: "CNY",
ActualAmount: 0.15,
ReceiveAddress: "OKPAY",
Token: "USDT",
Network: mdb.NetworkTron,
Status: mdb.StatusWaitPay,
NotifyUrl: "",
PaymentType: mdb.PaymentTypeEpay,
PayProvider: mdb.PaymentProviderOkPay,
}
if err := dao.Mdb.Create(child).Error; err != nil {
t.Fatalf("create child order: %v", err)
}
providerRow := &mdb.ProviderOrder{
TradeId: child.TradeId,
Provider: mdb.PaymentProviderOkPay,
ProviderOrderID: "okpay-order-test-001",
PayURL: "https://t.me/ExampleWalletBot?start=shop_deposit--okpay-order-test-001",
Amount: 0.15,
Coin: "USDT",
Status: mdb.ProviderOrderStatusPending,
}
if err := dao.Mdb.Create(providerRow).Error; err != nil {
t.Fatalf("create provider row: %v", err)
}
payload := map[string]interface{}{
"code": 200,
"id": "okpay-shop-test",
"status": "success",
"data": map[string]interface{}{
"order_id": providerRow.ProviderOrderID,
"unique_id": child.TradeId,
"pay_user_id": 1234567890,
"amount": "0.15000000",
"coin": "USDT",
"status": 1,
"type": "deposit",
},
}
payload["sign"] = signOkPayNotifyPayload("token-1", payload)
return &okPayNotifyFixture{
Parent: parent,
Child: child,
ProviderRow: providerRow,
Payload: payload,
}
}
func buildOkPayNotifyFormValues(payload map[string]interface{}) url.Values {
values := url.Values{}
values.Set("code", stringifyOkPayTestValue(payload["code"]))
values.Set("id", stringifyOkPayTestValue(payload["id"]))
values.Set("status", stringifyOkPayTestValue(payload["status"]))
values.Set("sign", stringifyOkPayTestValue(payload["sign"]))
dataMap, _ := payload["data"].(map[string]interface{})
values.Set("data[order_id]", stringifyOkPayTestValue(dataMap["order_id"]))
values.Set("data[unique_id]", stringifyOkPayTestValue(dataMap["unique_id"]))
values.Set("data[pay_user_id]", stringifyOkPayTestValue(dataMap["pay_user_id"]))
values.Set("data[amount]", stringifyOkPayTestValue(dataMap["amount"]))
values.Set("data[coin]", stringifyOkPayTestValue(dataMap["coin"]))
values.Set("data[status]", stringifyOkPayTestValue(dataMap["status"]))
values.Set("data[type]", stringifyOkPayTestValue(dataMap["type"]))
return values
}
func assertOkPayNotifyProcessed(t *testing.T, fixture *okPayNotifyFixture) {
t.Helper()
paidChild, err := data.GetOrderInfoByTradeId(fixture.Child.TradeId)
if err != nil {
t.Fatalf("load paid child: %v", err)
}
if paidChild.Status != mdb.StatusPaySuccess {
t.Fatalf("child status = %d, want %d", paidChild.Status, mdb.StatusPaySuccess)
}
if paidChild.BlockTransactionId != fixture.ProviderRow.ProviderOrderID {
t.Fatalf("child block_transaction_id = %q, want %q", paidChild.BlockTransactionId, fixture.ProviderRow.ProviderOrderID)
}
paidParent, err := data.GetOrderInfoByTradeId(fixture.Parent.TradeId)
if err != nil {
t.Fatalf("load paid parent: %v", err)
}
if paidParent.Status != mdb.StatusPaySuccess {
t.Fatalf("parent status = %d, want %d", paidParent.Status, mdb.StatusPaySuccess)
}
if paidParent.PayBySubId != paidChild.ID {
t.Fatalf("parent pay_by_sub_id = %d, want %d", paidParent.PayBySubId, paidChild.ID)
}
savedProviderRow, err := data.GetProviderOrderByTradeIDAndProvider(fixture.Child.TradeId, mdb.PaymentProviderOkPay)
if err != nil {
t.Fatalf("load provider row: %v", err)
}
if savedProviderRow.Status != mdb.ProviderOrderStatusPaid {
t.Fatalf("provider row status = %q, want %q", savedProviderRow.Status, mdb.ProviderOrderStatusPaid)
}
if savedProviderRow.NotifyRaw == "" {
t.Fatal("provider row notify_raw should be saved")
}
}
func TestOkPayNotify_JSONPayloadMarksOrdersPaid(t *testing.T) {
e := setupTestEnv(t)
fixture := seedOkPayNotifyFixture(t)
rawBody, err := json.Marshal(fixture.Payload)
if err != nil {
t.Fatalf("marshal payload: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/payments/okpay/v1/notify", strings.NewReader(string(rawBody)))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("okpay notify failed: %d %s", rec.Code, rec.Body.String())
}
if strings.TrimSpace(rec.Body.String()) != "success" {
t.Fatalf("okpay notify response = %q, want success", rec.Body.String())
}
assertOkPayNotifyProcessed(t, fixture)
}
func TestOkPayNotify_FormPayloadMarksOrdersPaid(t *testing.T) {
e := setupTestEnv(t)
fixture := seedOkPayNotifyFixture(t)
formValues := buildOkPayNotifyFormValues(fixture.Payload)
req := httptest.NewRequest(http.MethodPost, "/payments/okpay/v1/notify", strings.NewReader(formValues.Encode()))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("okpay form notify failed: %d %s", rec.Code, rec.Body.String())
}
if strings.TrimSpace(rec.Body.String()) != "success" {
t.Fatalf("okpay form notify response = %q, want success", rec.Body.String())
}
assertOkPayNotifyProcessed(t, fixture)
}
func TestOkPayNotify_ExpiresSiblingOkPayProviderOrder(t *testing.T) {
e := setupTestEnv(t)
fixture := seedOkPayNotifyFixture(t)
sibling := &mdb.Orders{
TradeId: "okpay-unique-test-002",
ParentTradeId: fixture.Parent.TradeId,
OrderId: "merchant-okpay-notify-001-sibling",
Amount: 1,
Currency: "CNY",
ActualAmount: 0.15,
ReceiveAddress: "OKPAY",
Token: "USDT",
Network: mdb.NetworkTron,
Status: mdb.StatusWaitPay,
NotifyUrl: "",
PaymentType: mdb.PaymentTypeEpay,
PayProvider: mdb.PaymentProviderOkPay,
}
if err := dao.Mdb.Create(sibling).Error; err != nil {
t.Fatalf("create sibling order: %v", err)
}
siblingProvider := &mdb.ProviderOrder{
TradeId: sibling.TradeId,
Provider: mdb.PaymentProviderOkPay,
ProviderOrderID: "okpay-order-test-002",
PayURL: "https://t.me/ExampleWalletBot?start=shop_deposit--okpay-order-test-002",
Amount: 0.15,
Coin: "USDT",
Status: mdb.ProviderOrderStatusPending,
}
if err := dao.Mdb.Create(siblingProvider).Error; err != nil {
t.Fatalf("create sibling provider row: %v", err)
}
rawBody, err := json.Marshal(fixture.Payload)
if err != nil {
t.Fatalf("marshal payload: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/payments/okpay/v1/notify", strings.NewReader(string(rawBody)))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("okpay notify failed: %d %s", rec.Code, rec.Body.String())
}
expiredSibling, err := data.GetOrderInfoByTradeId(sibling.TradeId)
if err != nil {
t.Fatalf("reload sibling order: %v", err)
}
if expiredSibling.Status != mdb.StatusExpired {
t.Fatalf("sibling status = %d, want %d", expiredSibling.Status, mdb.StatusExpired)
}
expiredSiblingProvider, err := data.GetProviderOrderByTradeIDAndProvider(sibling.TradeId, mdb.PaymentProviderOkPay)
if err != nil {
t.Fatalf("reload sibling provider row: %v", err)
}
if expiredSiblingProvider.Status != mdb.ProviderOrderStatusExpired {
t.Fatalf("sibling provider status = %q, want %q", expiredSiblingProvider.Status, mdb.ProviderOrderStatusExpired)
}
} }
// TestCreateOrderNetworkIsolation verifies tron and solana wallets don't mix. // TestCreateOrderNetworkIsolation verifies tron and solana wallets don't mix.
@@ -694,3 +1124,191 @@ func TestSwitchNetwork_WithOrder(t *testing.T) {
t.Fatal("expected data in switch-network response") t.Fatal("expected data in switch-network response")
} }
} }
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}
func stubRestyClient(fn roundTripFunc) *resty.Client {
return resty.NewWithClient(&http.Client{Transport: fn})
}
func TestSwitchNetwork_OkPayCreatesProviderSubOrder(t *testing.T) {
e := setupTestEnv(t)
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayEnabled, "true", mdb.SettingTypeBool); err != nil {
t.Fatalf("seed okpay.enabled: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayShopID, "shop-1", mdb.SettingTypeString); err != nil {
t.Fatalf("seed okpay.shop_id: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayShopToken, "token-1", mdb.SettingTypeString); err != nil {
t.Fatalf("seed okpay.shop_token: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayCallbackURL, "https://example.com/okpay/notify", mdb.SettingTypeString); err != nil {
t.Fatalf("seed okpay.callback_url: %v", err)
}
origFactory := http_client.ClientFactory
http_client.ClientFactory = func() *resty.Client {
return stubRestyClient(func(req *http.Request) (*http.Response, error) {
if req.URL.String() != "https://api.okaypay.me/shop/payLink" {
t.Fatalf("unexpected okpay URL: %s", req.URL.String())
}
if err := req.ParseForm(); err != nil {
t.Fatalf("parse form: %v", err)
}
if got := req.Form.Get("coin"); got != "USDT" {
t.Fatalf("coin = %q, want USDT", got)
}
if req.Form.Get("callback_url") == "" {
t.Fatal("callback_url should be set")
}
body := `{"status":"success","code":200,"data":{"order_id":"okp-order-1","pay_url":"https://pay.okaypay.test/abc"}}`
header := make(http.Header)
header.Set("Content-Type", "application/json")
return &http.Response{
StatusCode: http.StatusOK,
Status: "200 OK",
Header: header,
Body: io.NopCloser(strings.NewReader(body)),
}, nil
})
}
t.Cleanup(func() {
http_client.ClientFactory = origFactory
})
createBody := signBody(map[string]interface{}{
"order_id": "switch-okpay-parent-001",
"amount": 1.00,
"token": "usdt",
"currency": "cny",
"network": "solana",
"notify_url": "http://localhost/notify",
})
createRec := doPost(e, "/payments/gmpay/v1/order/create-transaction", createBody)
if createRec.Code != http.StatusOK {
t.Fatalf("create parent order failed: %d %s", createRec.Code, createRec.Body.String())
}
createResp := parseResp(t, createRec)
tradeID, _ := createResp["data"].(map[string]interface{})["trade_id"].(string)
if tradeID == "" {
t.Fatal("missing parent trade_id")
}
rec := doPost(e, "/pay/switch-network", map[string]interface{}{
"trade_id": tradeID,
"token": "USDT",
"network": "okpay",
})
if rec.Code != http.StatusOK {
t.Fatalf("switch-network okpay failed: %d %s", rec.Code, rec.Body.String())
}
resp := parseResp(t, rec)
dataMap, _ := resp["data"].(map[string]interface{})
if dataMap["payment_url"] != "https://pay.okaypay.test/abc" {
t.Fatalf("payment_url = %v, want okpay url", dataMap["payment_url"])
}
if dataMap["network"] != "tron" {
t.Fatalf("network = %v, want tron", dataMap["network"])
}
if dataMap["receive_address"] != "OKPAY" {
t.Fatalf("receive_address = %v, want OKPAY", dataMap["receive_address"])
}
subTradeID, _ := dataMap["trade_id"].(string)
if subTradeID == "" || subTradeID == tradeID {
t.Fatalf("expected child trade_id, got %q", subTradeID)
}
subOrder, err := data.GetOrderInfoByTradeId(subTradeID)
if err != nil {
t.Fatalf("load sub-order: %v", err)
}
if subOrder.PayProvider != mdb.PaymentProviderOkPay {
t.Fatalf("pay_provider = %q, want %q", subOrder.PayProvider, mdb.PaymentProviderOkPay)
}
providerRow, err := data.GetProviderOrderByTradeIDAndProvider(subTradeID, mdb.PaymentProviderOkPay)
if err != nil {
t.Fatalf("load provider row: %v", err)
}
if providerRow.ProviderOrderID != "okp-order-1" {
t.Fatalf("provider_order_id = %q, want okp-order-1", providerRow.ProviderOrderID)
}
if providerRow.PayURL != "https://pay.okaypay.test/abc" {
t.Fatalf("pay_url = %q", providerRow.PayURL)
}
}
func TestSwitchNetwork_OkPayIntegration(t *testing.T) {
shopID := strings.TrimSpace(os.Getenv("EPUSDT_OKPAY_ID"))
if shopID == "" {
shopID = strings.TrimSpace(os.Getenv("OKPAY_ID"))
}
if shopID == "" {
shopID = strings.TrimSpace(os.Getenv("EPUSDT_OKPAY_TEST_SHOP_ID"))
}
shopToken := strings.TrimSpace(os.Getenv("EPUSDT_OKPAY_TOKEN"))
if shopToken == "" {
shopToken = strings.TrimSpace(os.Getenv("OKPAY_TOKEN"))
}
if shopToken == "" {
shopToken = strings.TrimSpace(os.Getenv("EPUSDT_OKPAY_TEST_SHOP_TOKEN"))
}
if shopID == "" || shopToken == "" {
t.Skip("set EPUSDT_OKPAY_ID/EPUSDT_OKPAY_TOKEN (or OKPAY_ID/OKPAY_TOKEN) to run OkPay integration test")
}
e := setupTestEnv(t)
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayEnabled, "true", mdb.SettingTypeBool); err != nil {
t.Fatalf("seed okpay.enabled: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayShopID, shopID, mdb.SettingTypeString); err != nil {
t.Fatalf("seed okpay.shop_id: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayShopToken, shopToken, mdb.SettingTypeString); err != nil {
t.Fatalf("seed okpay.shop_token: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayCallbackURL, "https://example.com/okpay-notify", mdb.SettingTypeString); err != nil {
t.Fatalf("seed okpay.callback_url: %v", err)
}
if err := data.SetSetting(mdb.SettingGroupOkPay, mdb.SettingKeyOkPayReturnURL, "https://example.com/okpay-return", mdb.SettingTypeString); err != nil {
t.Fatalf("seed okpay.return_url: %v", err)
}
createBody := signBody(map[string]interface{}{
"order_id": "switch-okpay-live-001",
"amount": 1.00,
"token": "usdt",
"currency": "cny",
"network": "solana",
"notify_url": "http://localhost/notify",
})
createRec := doPost(e, "/payments/gmpay/v1/order/create-transaction", createBody)
if createRec.Code != http.StatusOK {
t.Fatalf("create parent order failed: %d %s", createRec.Code, createRec.Body.String())
}
createResp := parseResp(t, createRec)
tradeID, _ := createResp["data"].(map[string]interface{})["trade_id"].(string)
if tradeID == "" {
t.Fatal("missing parent trade_id")
}
rec := doPost(e, "/pay/switch-network", map[string]interface{}{
"trade_id": tradeID,
"token": "USDT",
"network": "okpay",
})
if rec.Code != http.StatusOK {
t.Fatalf("switch-network okpay failed: %d %s", rec.Code, rec.Body.String())
}
resp := parseResp(t, rec)
dataMap, _ := resp["data"].(map[string]interface{})
paymentURL, _ := dataMap["payment_url"].(string)
if paymentURL == "" {
t.Fatalf("missing payment_url in response: %v", dataMap)
}
t.Logf("OkPay payment_url=%s", paymentURL)
}
-546
View File
@@ -1,546 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GMWallet — Crypto Payment</title>
<link rel="icon" href="https://www.gmwallet.app/favicon.png" />
<link rel="preconnect" href="https://cdn.jsdmirror.com" crossorigin />
<link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/@fontsource-variable/noto-sans-sc/index.css" />
<link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/@fontsource-variable/nunito/index.css" />
<script src="https://cdn.jsdmirror.com/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
@theme inline {
--font-sans: "Noto Sans SC Variable", "Noto Sans SC", system-ui, sans-serif;
--font-nunito: "Nunito Variable", "Noto Sans SC Variable", "Noto Sans SC", system-ui, sans-serif;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground:var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground:var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground:var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-success: var(--success);
--color-warning: var(--warning);
--animate-state-in: state-in .28s cubic-bezier(.34,1.56,.64,1);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
}
/* ── Design tokens ── */
:root, [data-theme="light"] {
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground:oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground:oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground:oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--radius: 0.875rem;
--success: oklch(0.721 0.193 143.56);
--warning: oklch(0.741 0.188 55.68);
}
[data-theme="dark"] {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground:oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground:oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground:oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--success: oklch(0.747 0.201 143.5);
--warning: oklch(0.762 0.182 61.47);
}
/* ── Ambient blobs ── */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
@keyframes fill-bar {
from { width: 0%; }
to { width: 100%; }
}
@keyframes slide-out-l {
from { transform: translateX(0); }
to { transform: translateX(calc(-100% - 20px)); }
}
@keyframes slide-out-r {
from { transform: translateX(0); }
to { transform: translateX(calc(100% + 20px)); }
}
@keyframes slide-in-r {
from { transform: translateX(calc(100% + 20px)); }
to { transform: translateX(0); }
}
@keyframes slide-in-l {
from { transform: translateX(calc(-100% - 20px)); }
to { transform: translateX(0); }
}
@keyframes blob-drift {
0% { transform: translate(0,0) scale(1); }
50% { transform: translate(30px,-20px) scale(1.06); }
100% { transform: translate(-20px,30px) scale(0.96); }
}
@layer components {
/* Card */
.card {
@apply bg-card border border-border rounded-2xl shadow-md transition-colors duration-300;
}
/* Chip / pill button */
.chip {
@apply bg-card border border-border rounded-full px-3.5 py-1.5 text-card-foreground shadow-xs transition-all;
}
.chip:hover { @apply bg-accent border-ring; }
.chip:active { @apply opacity-60; }
.chip.w-9 { @apply p-0; }
/* Copy rows */
.row {
@apply flex items-center px-4 py-3.5 transition-colors;
}
.row:active { @apply bg-muted; }
/* Icon button */
.icon-btn {
@apply flex items-center justify-center size-8 rounded-sm text-muted-foreground bg-secondary border border-transparent shrink-0 transition-colors;
}
.icon-btn:hover { @apply bg-accent text-card-foreground border-border; }
/* Primary button */
.btn-primary {
@apply flex items-center justify-center bg-primary text-primary-foreground border border-border rounded-xl px-6 py-3 text-base font-semibold tracking-tight cursor-pointer shadow-md transition-[background,border-color,transform,opacity];
}
.btn-primary:hover { @apply bg-primary/90; }
.btn-primary:active { @apply scale-[0.97] opacity-90; }
.btn-primary:disabled { @apply opacity-45 cursor-not-allowed scale-100; }
/* Secondary button */
.btn-secondary {
@apply inline-flex items-center justify-center bg-card text-card-foreground border border-border rounded-xl px-7 py-3 text-base font-medium cursor-pointer shadow-sm transition-all;
}
.btn-secondary:hover { @apply bg-accent border-ring; }
.btn-secondary:active { @apply opacity-70; }
/* State icon circle */
.state-icon {
@apply size-20 rounded-full flex items-center justify-center mx-auto;
}
/* Dropdown menu */
.menu {
@apply bg-popover border border-border rounded-xl shadow-xl overflow-hidden;
opacity: 0;
transform: translateY(-8px);
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
}
.menu--up { transform: translateY(8px); }
.menu.is-open { opacity: 1; transform: translateY(0); pointer-events: all; }
.menu-item {
@apply flex items-center gap-2.5 px-4 py-2.5 text-sm font-medium text-card-foreground cursor-pointer transition-colors;
}
.menu-item:hover { @apply bg-accent; }
.menu-item.is-selected { @apply text-primary font-semibold; }
.menu-item + .menu-item { @apply border-t border-border; }
.select-trigger.is-open { opacity: 0.8; }
.select-trigger.is-open .select-chevron { transform: rotate(180deg); }
/* QR wrapper */
.qr-wrapper {
@apply bg-white rounded-xl p-3.5 border border-border shadow-md inline-block;
}
/* Timer SVG rings */
.timer-ring-track { stroke: var(--border); transition: stroke .3s; }
.timer-ring-progress { transform: rotate(-90deg); transform-origin: 50% 50%; transition: stroke-dashoffset 1s linear, stroke .5s; }
.timer-icon { @apply text-muted-foreground; }
/* Flippable status card */
.status-card {
position: relative;
perspective: 1600px;
transition: height 0.38s cubic-bezier(.22,1,.36,1);
}
.status-card-inner {
position: relative; width: 100%;
transform-style: preserve-3d;
transition: transform 0.82s cubic-bezier(.22,.9,.24,1);
}
.status-card.is-flipped .status-card-inner { transform: rotateY(180deg); }
.status-face {
@apply absolute inset-0 w-full;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
transform-style: preserve-3d;
}
.status-face-front { z-index: 2; }
.status-face-back { transform: rotateY(180deg); }
.state-panel-shell { @apply pt-px; }
.state-screen { @apply hidden; }
.state-screen.is-active { @apply flex; }
}
/* ── Panel viewport: hide scrollbars ── */
#panel-viewport { scrollbar-width: none; }
#panel-viewport::-webkit-scrollbar { display: none; }
/* ── Toast ── */
#toast {
@apply fixed left-1/2 bottom-9 text-sm font-medium px-5 py-2.5 rounded-xl border border-border shadow-xl opacity-0 pointer-events-none whitespace-nowrap z-9999;
@apply bg-foreground/95 text-background;
@apply dark:bg-card/95 dark:text-card-foreground;
transform: translateX(-50%) translateY(12px);
transition: opacity 0.22s, transform 0.22s;
}
@keyframes state-in {
from { opacity: 0; transform: translateY(12px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
::view-transition-old(root) { z-index: 1; }
::view-transition-new(root) { z-index: 9; }
@media (prefers-reduced-motion: reduce) {
.status-card, .status-card-inner, .status-face, .timer-ring-progress {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
</style>
<script defer src="https://cdn.jsdmirror.com/npm/lucide@latest/dist/umd/lucide.min.js"></script>
<script defer src="https://cdn.jsdmirror.com/npm/qrcodejs@latest/qrcode.min.js"></script>
<script defer src="https://cdn.jsdmirror.com/npm/clipboard@latest/dist/clipboard.min.js"></script>
</head>
<body
class="bg-background text-foreground min-h-screen flex flex-col items-center font-sans transition-colors duration-300">
<!-- Nav bar -->
<header class="w-full max-w-sm px-4 pt-8 pb-6 flex items-center justify-between relative z-10">
<div class="flex items-center gap-2">
<img src="https://www.gmwallet.app/favicon.png" alt="logo" class="h-8 w-8 rounded-sm shadow-sm" />
<span class="text-lg font-semibold tracking-tight text-card-foreground">GM Pay</span>
</div>
<div class="flex items-center gap-2">
<!-- Language pill -->
<div class="select-wrap relative select-none" id="dd-lang">
<div class="select-trigger chip flex items-center gap-1.5 cursor-pointer" onclick="toggleSelect('dd-lang')">
<span class="text-sm font-medium" id="lang-label">EN</span>
<i data-lucide="chevron-down"
class="select-chevron text-muted-foreground shrink-0 transition-transform duration-200" width="11"
height="11" stroke-width="2.5"></i>
</div>
<div class="select-menu menu absolute z-100 top-[calc(100%+12px)] right-0 min-w-40" id="dd-lang-menu">
<div class="select-option menu-item is-selected" data-lang="en" onclick="setLang('en')">🇺🇸&nbsp; English
</div>
<div class="select-option menu-item" data-lang="zh" onclick="setLang('zh')">🇨🇳&nbsp; 中文</div>
<div class="select-option menu-item" data-lang="ja" onclick="setLang('ja')">🇯🇵&nbsp; 日本語</div>
<div class="select-option menu-item" data-lang="ko" onclick="setLang('ko')">🇰🇷&nbsp; 한국어</div>
<div class="select-option menu-item" data-lang="zh-hk" onclick="setLang('zh-hk')">🇭🇰&nbsp; 繁體中文</div>
<div class="select-option menu-item" data-lang="ru" onclick="setLang('ru')">🇷🇺&nbsp; Русский</div>
</div>
</div>
<!-- Theme toggle -->
<button class="chip w-9 h-9 flex items-center justify-center cursor-pointer" onclick="toggleTheme(event)"
title="Toggle theme">
<i data-lucide="moon" id="icon-moon" class="hidden" width="16" height="16" stroke-width="1.8"></i>
<i data-lucide="sun" id="icon-sun" width="16" height="16" stroke-width="1.8"></i>
</button>
<!-- Ambient background blobs -->
<div class="fixed inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div
class="absolute rounded-full blur-[80px] animate-[blob-drift_18s_ease-in-out_infinite_alternate] size-[420px] -top-30 -left-25 bg-[color-mix(in_srgb,var(--muted)_60%,transparent)]">
</div>
<div
class="absolute rounded-full blur-[80px] animate-[blob-drift_18s_ease-in-out_infinite_alternate] [animation-delay:-6s] size-[340px] -bottom-20 -right-15 bg-[color-mix(in_srgb,var(--success)_14%,transparent)]">
</div>
<div
class="absolute rounded-full blur-[80px] animate-[blob-drift_18s_ease-in-out_infinite_alternate] [animation-delay:-12s] size-[260px] top-[45%] left-[55%] bg-[color-mix(in_srgb,var(--warning)_10%,transparent)]">
</div>
</div>
</div>
</header>
<main class="flex-1 flex flex-col items-center justify-center w-full max-w-sm px-5 relative">
<!-- Steps -->
<div id="step-progress" class="flex items-center gap-2 w-full mb-4 select-none">
<div id="step-bar-1" class="h-1 flex-1 rounded-full overflow-hidden"
style="background:color-mix(in srgb,var(--foreground) 10%,transparent)">
<div id="step-fill-1" class="h-full rounded-full" style="width:0%;background:var(--foreground)"></div>
</div>
<div id="step-bar-2" class="h-1 flex-1 rounded-full overflow-hidden"
style="background:color-mix(in srgb,var(--foreground) 10%,transparent)">
<div id="step-fill-2" class="h-full rounded-full" style="width:0%;background:var(--foreground)"></div>
</div>
</div>
<!-- Order info -->
<aside id="order-info" class="card w-full px-6 pt-6 pb-5 mb-4">
<p class="text-sm font-medium text-muted-foreground mb-1" data-i18n="amount_to_pay">Amount to pay</p>
<div class="flex items-end gap-3 justify-between">
<span class="text-4xl font-bold leading-none tracking-tighter text-card-foreground font-nunito"
id="display-amount">--</span>
<button class="icon-btn mb-0.5 shrink-0" id="btn-copy-amount" title="Copy">
<i data-lucide="copy" width="15" height="15" stroke-width="2"></i>
</button>
</div>
<div class="flex items-center gap-1.5 mt-1.5" id="display-network">--</div>
<div class="mt-3 pt-3 border-t border-border/50">
<table class="w-full text-sm text-muted-foreground border-separate border-spacing-y-1">
<tbody>
<tr id="display-fiat"></tr>
<tr id="display-order-id"></tr>
</tbody>
</table>
</div>
</aside>
<!-- Panel viewport: clips horizontal slide -->
<div class="relative w-full" id="panel-viewport">
<!-- Step 1: Select coin + network -->
<section id="step1-panel" class="w-full mb-4 pb-4">
<div class="flex gap-2 mb-4">
<!-- Network -->
<div class="flex-1 min-w-0">
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1.5"
data-i18n="network_label">Network</p>
<div class="select-wrap relative select-none" id="dd-network">
<div
class="select-trigger flex items-center justify-between bg-card border border-border rounded-xl px-3.5 py-3 cursor-pointer gap-2 shadow-sm transition-colors h-[50px]"
onclick="toggleSelect('dd-network')">
<span id="network-label" class="text-sm font-semibold text-card-foreground leading-none"></span>
<i data-lucide="chevron-down"
class="select-chevron text-muted-foreground shrink-0 transition-transform duration-200" width="13"
height="13" stroke-width="2.5"></i>
</div>
<div class="select-menu menu absolute z-100 top-[calc(100%+8px)] left-0 w-full" id="dd-network-menu">
</div>
</div>
</div>
<!-- Currency -->
<div class="flex-1 min-w-0">
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1.5"
data-i18n="currency_label">Currency</p>
<div class="select-wrap relative select-none" id="dd-token">
<div
class="select-trigger flex items-center justify-between bg-card border border-border rounded-xl px-3.5 py-3 cursor-pointer gap-2 shadow-sm transition-colors h-[50px]"
onclick="toggleSelect('dd-token')">
<span id="token-label" class="text-sm font-semibold text-card-foreground"></span>
<i data-lucide="chevron-down"
class="select-chevron text-muted-foreground shrink-0 transition-transform duration-200" width="13"
height="13" stroke-width="2.5"></i>
</div>
<div class="select-menu menu absolute z-100 top-[calc(100%+8px)] left-0 w-full" id="dd-token-menu">
</div>
</div>
</div>
</div>
<button id="btn-confirm-step1" class="btn-primary w-full" onclick="confirmStep1()"
data-i18n="confirm">Confirm</button>
</section>
<!-- Payment panel (Step 2) -->
<section id="payment-panel" class="w-full pb-4" style="display:none">
<!-- QR + address card -->
<div class="card w-full px-5 pt-5 pb-0 mb-4 overflow-hidden">
<!-- Scan title + timer -->
<div class="flex items-center justify-between mb-3">
<p class="text-sm font-semibold text-card-foreground" data-i18n="scan_title">Scan or copy address to pay
</p>
<div id="timer-row" class="relative w-10 h-10 shrink-0">
<svg class="w-10 h-10 absolute inset-0" viewBox="0 0 48 48">
<circle class="timer-ring-track" cx="24" cy="24" r="20" fill="none" stroke-width="3" />
<circle id="ring-track" class="timer-ring-track" cx="24" cy="24" r="20" fill="none" stroke-width="3" />
<circle id="ring" class="timer-ring-progress" cx="24" cy="24" r="20" fill="none" stroke="var(--success)"
stroke-width="3" stroke-linecap="round" stroke-dasharray="125.66" stroke-dashoffset="0" />
</svg>
<div class="absolute inset-0 flex items-center justify-center">
<i data-lucide="timer" class="timer-icon" width="18" height="18" stroke-width="1.8"></i>
</div>
</div>
</div>
<!-- Countdown -->
<p class="text-3xl font-bold font-mono leading-none text-success text-center mb-4" id="countdown">--:--</p>
<!-- QR code -->
<div class="flex justify-center mb-4">
<div class="qr-wrapper">
<div id="qrcode" class="w-44 h-44"></div>
</div>
</div>
<!-- Address row (edge-to-edge inside card) -->
<div class="h-px bg-border/50 -mx-5"></div>
<div id="copy-addr-box" class="row cursor-pointer select-none -mx-5">
<div class="flex-1 min-w-0 pr-3">
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-0.5"
data-i18n="payment_address">Payment address</p>
<p class="text-sm font-medium text-card-foreground break-all leading-relaxed" id="field-address">--</p>
</div>
<span class="icon-btn shrink-0 pointer-events-none self-start mt-0.5" id="btn-copy-addr">
<i data-lucide="copy" width="14" height="14" stroke-width="2"></i>
</span>
</div>
</div>
<span id="countdown-inline" class="sr-only"></span>
<!-- Connect Wallet (above I have transferred) -->
<button class="btn-secondary w-full mb-3" id="btn-connect-wallet" onclick="connectWallet()">
<i data-lucide="wallet" width="16" height="16" stroke-width="1.8" class="mr-1.5 shrink-0"></i>
<span data-i18n="connect_wallet">Connect Wallet to Pay</span>
</button>
<!-- I have transferred -->
<button class="btn-primary w-full mb-3" id="btn-transferred" onclick="handleTransfer()">
<span data-i18n="i_have_transferred">I have transferred</span>
</button>
<!-- Status -->
<div class="flex items-center justify-center gap-1.5 py-1" id="status-row">
<i data-lucide="loader-circle" class="animate-spin text-muted-foreground" width="13" height="13"
stroke-width="2.2"></i>
<span class="text-xs text-muted-foreground" id="status-text" data-i18n="checking_blockchain">Checking
blockchain</span>
</div>
</section>
<!-- State: Success -->
<section id="screen-success"
class="card w-full flex flex-col items-center justify-center text-center py-10 px-6 min-h-96 mb-4" role="status"
aria-live="polite" style="display:none">
<div class="state-icon bg-success/15 mb-6">
<i data-lucide="check" width="38" height="38" stroke-width="2.2" stroke="var(--success)"></i>
</div>
<p class="text-xl font-bold text-card-foreground mb-2" data-i18n="payment_success">Payment Successful</p>
<p class="text-sm text-muted-foreground mb-6" data-i18n="redirecting">Redirecting…</p>
<i data-lucide="loader-circle" class="animate-spin text-muted-foreground" width="22" height="22"
stroke-width="2"></i>
</section>
<!-- State: Expired -->
<section id="screen-expired" class="w-full pb-4" role="status" aria-live="polite" style="display:none">
<div class="card w-full flex flex-col items-center justify-center text-center py-10 px-6 mb-4">
<div class="state-icon bg-destructive/12 mb-6">
<i data-lucide="x" width="38" height="38" stroke-width="2.2" stroke="var(--destructive)"></i>
</div>
<p class="text-xl font-bold text-card-foreground mb-2" data-i18n="payment_expired">Payment Expired</p>
<p class="text-sm text-muted-foreground" data-i18n="expired_sub">Please initiate a new payment</p>
</div>
<button class="btn-secondary w-full" onclick="goBack()" data-i18n="back">Back</button>
</section>
<!-- State: Timeout -->
<section id="screen-timeout" class="w-full pb-4" role="status" aria-live="polite" style="display:none">
<div class="card w-full flex flex-col items-center justify-center text-center py-10 px-6 mb-4">
<div class="state-icon bg-warning/12 mb-6">
<i data-lucide="triangle-alert" width="38" height="38" stroke-width="2.2" stroke="var(--warning)"></i>
</div>
<p class="text-xl font-bold text-card-foreground mb-2" data-i18n="network_timeout">Connection Timeout</p>
<p class="text-sm text-muted-foreground" data-i18n="timeout_sub">Unable to connect to the payment server</p>
</div>
<div class="flex gap-3">
<button class="btn-secondary flex-1" onclick="goBack()" data-i18n="back">Back</button>
<button class="btn-primary flex-1" onclick="retryPolling()" data-i18n="retry">Retry</button>
</div>
</section>
<!-- State: Not Found -->
<section id="screen-not-found"
class="card w-full flex flex-col items-center justify-center text-center py-10 px-6 min-h-96 mb-4" role="status"
aria-live="polite" style="display:none">
<div class="state-icon bg-muted-foreground/12 mb-6">
<i data-lucide="file-x" width="38" height="38" stroke-width="2" stroke="var(--muted-foreground)"></i>
</div>
<p class="text-xl font-bold text-card-foreground mb-2" data-i18n="order_not_found">Order Not Found</p>
<p class="text-sm text-muted-foreground" data-i18n="not_found_sub">The order does not exist or has already
expired
</p>
</section>
</div><!-- /panel viewport -->
</main>
<!-- Footer -->
<footer class="flex items-center justify-center gap-2.5 pb-6 text-xs text-muted-foreground select-none relative z-10">
<span class="flex items-center gap-1.5">
Powered by
<a href="https://www.gmwallet.app" target="_blank" rel="noopener"
class="flex items-center gap-1 font-semibold text-card-foreground hover:opacity-70 transition-opacity">
<img src="https://www.gmwallet.app/favicon.png" alt="GM Wallet" class="h-3.5 w-3.5 rounded-xs" />
GM Wallet
</a>
</span>
<span class="opacity-30">|</span>
<a href="https://github.com/GMwalletApp" target="_blank" rel="noopener"
class="flex items-center gap-1 hover:opacity-70 transition-opacity">
Open source on <span class="font-semibold text-card-foreground">GitHub</span>
</a>
</footer>
<!-- Toast -->
<div id="toast">Copied</div>
<!-- Order data injected by server (Go template) -->
<script>
var ORDER = {
tradeId: "{{.TradeId}}",
amount: "{{.Amount}}",
actualAmount: "{{.ActualAmount}}",
token: "{{.Token}}",
currency: "{{.Currency}}",
network: "{{.Network}}",
receiveAddress: "{{.ReceiveAddress}}",
expirationTime: "{{.ExpirationTime}}",
redirectUrl: "{{.RedirectUrl}}",
createdAt: "{{.CreatedAt}}",
is_selected: "{{.IsSelected}}"
};
</script>
<script defer src="/static/payment.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+4 -8
View File
@@ -13,24 +13,19 @@ func Start() {
go StartBscWebSocketListener() go StartBscWebSocketListener()
go StartPolygonWebSocketListener() go StartPolygonWebSocketListener()
go StartPlasmaWebSocketListener() go StartPlasmaWebSocketListener()
go StartTronBlockScannerListener()
c := cron.New() c := cron.New()
// TRC20 polling
_, err := c.AddJob("@every 5s", ListenTrc20Job{})
if err != nil {
log.Sugar.Errorf("[task] Failed to add ListenTrc20Job: %v", err)
return
}
log.Sugar.Info("[task] ListenTrc20Job scheduled successfully (@every 5s)")
// Solana polling // Solana polling
_, err = c.AddJob("@every 5s", ListenSolJob{}) _, err := c.AddJob("@every 5s", ListenSolJob{})
if err != nil { if err != nil {
log.Sugar.Errorf("[task] Failed to add ListenSolJob: %v", err) log.Sugar.Errorf("[task] Failed to add ListenSolJob: %v", err)
return return
} }
log.Sugar.Info("[task] ListenSolJob scheduled successfully (@every 5s)") log.Sugar.Info("[task] ListenSolJob scheduled successfully (@every 5s)")
// RPC node health checks // RPC node health checks
_, err = c.AddJob("@every 30s", RpcHealthJob{}) _, err = c.AddJob("@every 30s", RpcHealthJob{})
if err != nil { if err != nil {
@@ -38,6 +33,7 @@ func Start() {
return return
} }
log.Sugar.Info("[task] RpcHealthJob scheduled successfully (@every 30s)") log.Sugar.Info("[task] RpcHealthJob scheduled successfully (@every 30s)")
c.Start() c.Start()
log.Sugar.Info("[task] Task scheduler started") log.Sugar.Info("[task] Task scheduler started")
} }
+378
View File
@@ -0,0 +1,378 @@
package task
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"strings"
"time"
tron "github.com/GMWalletApp/epusdt/crypto"
"github.com/GMWalletApp/epusdt/model/service"
"github.com/GMWalletApp/epusdt/util/log"
)
const (
// USDT 合约地址 (TRC20 主网)
USDTContractHex = "41a614f803b6fd780986a42c78ec9c7f77e6ded13c" // Base58: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
// transfer(address,uint256) 方法签名前4字节
TransferMethodID = "a9059cbb"
PollInterval = 3 * time.Second
RequestTimeout = 10 * time.Second
)
func HexToTronAddress(hexAddr string) (string, error) {
hexAddr = strings.TrimPrefix(hexAddr, "0x")
hexAddr = strings.TrimPrefix(hexAddr, "0X")
raw, err := hex.DecodeString(hexAddr)
if err != nil {
return "", err
}
// 确保有 0x41 前缀(TRON 主网地址前缀)
if len(raw) == 20 {
raw = append([]byte{0x41}, raw...)
}
if len(raw) != 21 {
return "", fmt.Errorf("地址长度非法: %d bytes", len(raw))
}
return tron.EncodeCheck(raw), nil
}
type BlockHeader struct {
RawData struct {
Number int64 `json:"number"`
Timestamp int64 `json:"timestamp"`
WitnessAddress string `json:"witness_address"`
ParentHash string `json:"parentHash"`
Version int `json:"version"`
} `json:"raw_data"`
}
type TriggerSmartContractValue struct {
OwnerAddress string `json:"owner_address"`
ContractAddress string `json:"contract_address"`
Data string `json:"data"`
CallValue int64 `json:"call_value"`
}
type ContractParam struct {
TypeURL string `json:"type_url"`
Value json.RawMessage `json:"value"`
}
type Transaction struct {
TxID string `json:"txID"`
RawData struct {
Contract []struct {
Type string `json:"type"`
Parameter ContractParam `json:"parameter"`
} `json:"contract"`
Timestamp int64 `json:"timestamp"`
FeeLimit int64 `json:"fee_limit"`
} `json:"raw_data"`
Ret []struct {
ContractRet string `json:"contractRet"`
} `json:"ret"`
}
type Block struct {
BlockID string `json:"blockID"`
BlockHeader BlockHeader `json:"block_header"`
Transactions []Transaction `json:"transactions"`
}
type USDTTransfer struct {
TxID string
From string
To string
Raw *big.Int // 原始数值(6 decimals
Status string
}
type TRXTransfer struct {
TxID string
From string
To string
RawSun int64 // 单位: SUN
Status string
}
type TransferContractValue struct {
OwnerAddress string `json:"owner_address"`
ToAddress string `json:"to_address"`
Amount int64 `json:"amount"` // 单位: SUN
}
var httpClient = &http.Client{Timeout: RequestTimeout}
func doPost(url string, apiKey string, body interface{}) ([]byte, error) {
data, _ := json.Marshal(body)
req, err := http.NewRequest("POST", url, bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
if apiKey = strings.TrimSpace(apiKey); apiKey != "" {
req.Header.Set("TRON-PRO-API-KEY", apiKey)
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(b))
}
return b, nil
}
func GetNowBlock(baseURL string, apiKey string) (*Block, error) {
b, err := doPost(baseURL+"/wallet/getnowblock", apiKey, map[string]interface{}{})
if err != nil {
return nil, err
}
var block Block
return &block, json.Unmarshal(b, &block)
}
func GetBlockByNum(baseURL string, apiKey string, num int64) (*Block, error) {
b, err := doPost(baseURL+"/wallet/getblockbynum", apiKey, map[string]interface{}{"num": num})
if err != nil {
return nil, err
}
var block Block
return &block, json.Unmarshal(b, &block)
}
func parseUSDTTransfer(tx Transaction) *USDTTransfer {
if len(tx.RawData.Contract) == 0 {
return nil
}
c := tx.RawData.Contract[0]
if c.Type != "TriggerSmartContract" {
return nil
}
var val TriggerSmartContractValue
if err := json.Unmarshal(c.Parameter.Value, &val); err != nil {
return nil
}
// 检查是否是 USDT 合约
contractHex := strings.ToLower(strings.TrimPrefix(val.ContractAddress, "0x"))
if contractHex != USDTContractHex {
return nil
}
// 解析 data 字段
// 格式: [4字节方法ID][32字节 to 地址][32字节 amount]
data := strings.TrimPrefix(strings.ToLower(val.Data), "0x")
if len(data) < 8+64+64 {
return nil
}
if data[:8] != TransferMethodID {
return nil
}
// to 地址:后 40 个十六进制字符(20 字节)
toHex := data[8+24 : 8+64] // 跳过前12字节填充,取后20字节
toAddr, err := HexToTronAddress(toHex)
if err != nil {
return nil
}
// amount:后 32 字节大整数
amountHex := data[8+64 : 8+64+64]
amountBytes, err := hex.DecodeString(amountHex)
if err != nil {
return nil
}
amountBig := new(big.Int).SetBytes(amountBytes)
// from 地址
fromAddr, err := HexToTronAddress(val.OwnerAddress)
if err != nil {
fromAddr = val.OwnerAddress
}
// 交易状态
status := "SUCCESS"
if len(tx.Ret) > 0 && tx.Ret[0].ContractRet != "" && tx.Ret[0].ContractRet != "SUCCESS" {
status = tx.Ret[0].ContractRet
}
return &USDTTransfer{
TxID: tx.TxID,
From: fromAddr,
To: toAddr,
Raw: amountBig,
Status: status,
}
}
func parseTRXTransfer(tx Transaction) *TRXTransfer {
if len(tx.RawData.Contract) == 0 {
return nil
}
c := tx.RawData.Contract[0]
if c.Type != "TransferContract" {
return nil
}
var val TransferContractValue
if err := json.Unmarshal(c.Parameter.Value, &val); err != nil {
return nil
}
if val.Amount <= 0 {
return nil
}
fromAddr, err := HexToTronAddress(val.OwnerAddress)
if err != nil {
fromAddr = val.OwnerAddress
}
toAddr, err := HexToTronAddress(val.ToAddress)
if err != nil {
toAddr = val.ToAddress
}
status := "SUCCESS"
if len(tx.Ret) > 0 && tx.Ret[0].ContractRet != "" && tx.Ret[0].ContractRet != "SUCCESS" {
status = tx.Ret[0].ContractRet
}
return &TRXTransfer{
TxID: tx.TxID,
From: fromAddr,
To: toAddr,
RawSun: val.Amount,
Status: status,
}
}
func processBlock(block *Block) {
blockTsMs := block.BlockHeader.RawData.Timestamp
for _, tx := range block.Transactions {
if t := parseUSDTTransfer(tx); t != nil {
if t.Status != "SUCCESS" {
continue
}
service.TryProcessTronTRC20Transfer(t.To, t.Raw, t.TxID, blockTsMs)
continue
}
if t := parseTRXTransfer(tx); t != nil {
if t.Status != "SUCCESS" {
continue
}
service.TryProcessTronTRXTransfer(t.To, t.RawSun, t.TxID, blockTsMs)
}
}
}
type Scanner struct {
baseURL string
apiKey string
lastBlock int64
// 统计
totalBlocks int64
totalUSDTTxs int64
totalTRXTxs int64
}
func NewScanner() *Scanner {
return &Scanner{}
}
func (s *Scanner) Init() error {
baseURL, apiKey, err := service.ResolveTronNode()
if err != nil {
return fmt.Errorf("resolve tron node: %w", err)
}
s.baseURL = baseURL
s.apiKey = apiKey
log.Sugar.Infof("[TRON-BLOCK] node=%s", s.baseURL)
block, err := GetNowBlock(s.baseURL, s.apiKey)
if err != nil {
return fmt.Errorf("获取初始块失败: %w", err)
}
s.lastBlock = block.BlockHeader.RawData.Number
log.Sugar.Infof("[TRON-BLOCK] start block=%d", s.lastBlock)
return nil
}
func (s *Scanner) Run() {
log.Sugar.Info("[TRON-BLOCK] start scanning (USDT TRC20 + TRX)")
ticker := time.NewTicker(PollInterval)
defer ticker.Stop()
statTicker := time.NewTicker(60 * time.Second)
defer statTicker.Stop()
for {
select {
case <-statTicker.C:
log.Sugar.Infof("[TRON-BLOCK] stats blocks=%d usdt=%d trx=%d", s.totalBlocks, s.totalUSDTTxs, s.totalTRXTxs)
case <-ticker.C:
s.poll()
}
}
}
func (s *Scanner) poll() {
latest, err := GetNowBlock(s.baseURL, s.apiKey)
if err != nil {
log.Sugar.Warnf("[TRON-BLOCK] get latest block: %v", err)
return
}
latestNum := latest.BlockHeader.RawData.Number
if latestNum <= s.lastBlock {
return
}
for num := s.lastBlock + 1; num <= latestNum; num++ {
var block *Block
if num == latestNum {
block = latest
} else {
block, err = GetBlockByNum(s.baseURL, s.apiKey, num)
if err != nil {
log.Sugar.Warnf("[TRON-BLOCK] get block %d: %v", num, err)
continue
}
time.Sleep(200 * time.Millisecond)
}
processBlock(block)
s.lastBlock = num
s.totalBlocks++
for _, tx := range block.Transactions {
if parseUSDTTransfer(tx) != nil {
s.totalUSDTTxs++
} else if parseTRXTransfer(tx) != nil {
s.totalTRXTxs++
}
}
}
}
func StartTronBlockScannerListener() {
scanner := NewScanner()
if err := scanner.Init(); err != nil {
log.Sugar.Errorf("[TRON-BLOCK] init: %v", err)
return
}
scanner.Run()
}
-43
View File
@@ -1,43 +0,0 @@
package task
import (
"sync"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/model/service"
"github.com/GMWalletApp/epusdt/util/log"
)
type ListenTrc20Job struct{}
var gListenTrc20JobLock sync.Mutex
func (r ListenTrc20Job) Run() {
gListenTrc20JobLock.Lock()
defer gListenTrc20JobLock.Unlock()
log.Sugar.Debug("[ListenTrc20Job] Job triggered")
if !data.IsChainEnabled(mdb.NetworkTron) {
log.Sugar.Debug("[ListenTrc20Job] chain disabled, skipping")
return
}
walletAddress, err := data.GetAvailableWalletAddressByNetwork(mdb.NetworkTron)
if err != nil {
log.Sugar.Errorf("[ListenTrc20Job] Failed to get wallet addresses: %v", err)
return
}
if len(walletAddress) <= 0 {
log.Sugar.Debug("[ListenTrc20Job] No available wallet addresses")
return
}
log.Sugar.Infof("[ListenTrc20Job] Found %d wallet addresses to monitor", len(walletAddress))
var wg sync.WaitGroup
for _, address := range walletAddress {
log.Sugar.Infof("[ListenTrc20Job] Listening to address: %s", address.Address)
wg.Add(1)
go service.Trc20CallBack(address.Address, &wg)
}
wg.Wait()
log.Sugar.Debug("[ListenTrc20Job] Job completed")
}
+6
View File
@@ -18,6 +18,9 @@ var Errno = map[int]string{
10013: "order is not awaiting payment", 10013: "order is not awaiting payment",
10014: "chain is not enabled", 10014: "chain is not enabled",
10016: "supported asset not found", 10016: "supported asset not found",
10017: "payment provider is not enabled",
10018: "payment provider configuration is incomplete",
10019: "payment provider does not support this token or network",
} }
var ( var (
@@ -38,6 +41,9 @@ var (
OrderNotWaitPay = Err(10013) OrderNotWaitPay = Err(10013)
ChainNotEnabled = Err(10014) ChainNotEnabled = Err(10014)
SupportedAssetNotFound = Err(10016) SupportedAssetNotFound = Err(10016)
PaymentProviderNotEnabled = Err(10017)
PaymentProviderConfigErr = Err(10018)
PaymentProviderNotSupport = Err(10019)
) )
type RspError struct { type RspError struct {
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./unauthorized-error-AAbXZDVX.js";var t=e;export{t as component};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./unauthorized-error-CgOrMfGt.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./forbidden-Waph2URh.js";var t=e;export{t as component};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./forbidden-oyIuUuBn.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./not-found-error-tF1-JLKB.js";var t=e;export{t as component};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./not-found-error-mipAgOS5.js";var t=e;export{t as component};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./general-error-BryI8zNx.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./general-error-BHe1mAQ8.js";var t=e;export{t as component};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./maintenance-error-bnDJ_mm3.js";var t=e;export{t as component};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./maintenance-error-CECzFgNM.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{$u as e,Qu as t,nd as n}from"./messages-GNZRoNCJ.js";import{o as r,s as i,t as a}from"./useRouter-DP_QL4bS.js";import{t as o}from"./useStore-DcoP6GEY.js";import{f as s}from"./ClientOnly-CB70y-7P.js";import"./redirect-dE0wk0WH.js";import{a as c,i as l,r as u,s as d,t as f}from"./Match-D2TG27aI.js";import{n as p}from"./matchContext-B_MEyiTx.js";import{t as m}from"./atom-DI1Bn-1s.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=n(e(),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=t();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{$d as e,Xd as t,Yd as n}from"./messages-kZy_2fG-.js";import{o as r,s as i,t as a}from"./useRouter-BFRHcVKY.js";import{t as o}from"./useStore-BegLNH-k.js";import{f as s}from"./ClientOnly-CKH5SArO.js";import"./redirect-dE0wk0WH.js";import{a as c,i as l,r as u,s as d,t as f}from"./Match-B0WegrOo.js";import{n as p}from"./matchContext-Cxt9JPh-.js";import{t as m}from"./atom-DI1Bn-1s.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{Qu as e}from"./messages-GNZRoNCJ.js";import{t}from"./_error-D1O444lg.js";import{m as n}from"./search-provider-LWHzuxPK.js";import{n as r,t as i}from"./theme-switch-CnUvKuRz.js";import{t as a}from"./general-error-BHe1mAQ8.js";import{t as o}from"./not-found-error-tF1-JLKB.js";import{t as s}from"./unauthorized-error-AAbXZDVX.js";import{t as c}from"./forbidden-Waph2URh.js";import{t as l}from"./maintenance-error-bnDJ_mm3.js";import{n as u,r as d,t as f}from"./header-DYuf7kWT.js";var p=e();function m(){let{error:e}=t.useParams(),m={unauthorized:s,forbidden:c,"not-found":o,"internal-server-error":a,"maintenance-error":l}[e]||o;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)(r,{}),(0,p.jsx)(i,{}),(0,p.jsx)(n,{}),(0,p.jsx)(d,{})]})]}),(0,p.jsx)(`div`,{className:`flex-1 [&>div]:h-full`,children:(0,p.jsx)(m,{})})]})}export{m as component};
+1
View File
@@ -0,0 +1 @@
import{Yd as e}from"./messages-kZy_2fG-.js";import{m as t}from"./search-provider-BLfAXVyy.js";import{n,t as r}from"./theme-switch-BhK5XnmV.js";import{t as i}from"./general-error-BryI8zNx.js";import{t as a}from"./not-found-error-mipAgOS5.js";import{t as o}from"./_error-DwHJp4LF.js";import{t as s}from"./unauthorized-error-CgOrMfGt.js";import{t as c}from"./forbidden-oyIuUuBn.js";import{t as l}from"./maintenance-error-CECzFgNM.js";import{n as u,r as d,t as f}from"./header-sQjuQN7h.js";var p=e();function m(){let{error:e}=o.useParams(),m={unauthorized:s,forbidden:c,"not-found":a,"internal-server-error":i,"maintenance-error":l}[e]||a;return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(f,{className:`border-b`,fixed:!0,children:[(0,p.jsx)(u,{}),(0,p.jsxs)(`div`,{className:`ms-auto flex items-center space-x-4`,children:[(0,p.jsx)(n,{}),(0,p.jsx)(r,{}),(0,p.jsx)(t,{}),(0,p.jsx)(d,{})]})]}),(0,p.jsx)(`div`,{className:`flex-1 [&>div]:h-full`,children:(0,p.jsx)(m,{})})]})}export{m as component};
-2
View File
@@ -1,2 +0,0 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_error-BgKHMHmk.js","assets/messages-GNZRoNCJ.js","assets/search-provider-LWHzuxPK.js","assets/skeleton-CShhbKn7.js","assets/button-CgehfZOz.js","assets/createLucideIcon-ZOoxJGca.js","assets/dist-BVB_XD7R.js","assets/confirm-dialog-CF6eWZWw.js","assets/dist-C6g_SrVT.js","assets/dist-lfYKV97O.js","assets/dist-y2ssackP.js","assets/dist-BBeIw6Mf.js","assets/es2015-BlGYWzx8.js","assets/dist-BmDzTadk.js","assets/dist-DHfFvH8a.js","assets/dist-DWWis6_E.js","assets/dist-ocY2rTvF.js","assets/dist-CUk2ZAUW.js","assets/dist-C2u_XTZj.js","assets/separator-DCobJKYf.js","assets/tooltip-DCSbmniE.js","assets/dist-B37Isglo.js","assets/auth-store-D6M2fL5v.js","assets/dist-5drZch9N.js","assets/with-selector-DMEP9rYj.js","assets/cookies-D0aVbsDr.js","assets/useRouter-DP_QL4bS.js","assets/useNavigate-ARcBwEET.js","assets/useStore-DcoP6GEY.js","assets/command-BVulVd11.js","assets/circle-check-IaDmPO-c.js","assets/logo-dzDYxUEK.js","assets/shield-check-CkFtGD8a.js","assets/wallet-DyD4tP4D.js","assets/input-DVA35bJk.js","assets/font-provider-DigULrV2.js","assets/theme-provider-C_vGocad.js","assets/theme-switch-CnUvKuRz.js","assets/dropdown-menu-CTXk1Zrr.js","assets/check-Dk399u4y.js","assets/header-DYuf7kWT.js","assets/link-BzoLXRYo.js","assets/ClientOnly-CB70y-7P.js","assets/forbidden-Waph2URh.js","assets/general-error-BHe1mAQ8.js","assets/maintenance-error-bnDJ_mm3.js","assets/not-found-error-tF1-JLKB.js","assets/unauthorized-error-AAbXZDVX.js"])))=>i.map(i=>d[i]);
import{$u as e,nd as t}from"./messages-GNZRoNCJ.js";import{r as n}from"./useRouter-DP_QL4bS.js";import{d as r}from"./useStore-DcoP6GEY.js";import{n as i}from"./route-B0n9n_C5.js";function a(e){return new o(e,{silent:!0}).createRoute}var o=class{constructor(e,t){this.path=e,this.createRoute=e=>{let t=i(e);return t.isRoot=!1,t},this.silent=t?.silent}},s=t(e(),1);function c(e,t){let i,a,o,c,l=()=>(i||=e().then(e=>{i=void 0,a=e[t??`default`]}).catch(e=>{if(o=e,r(o)&&o instanceof Error&&typeof window<`u`&&typeof sessionStorage<`u`){let e=`tanstack_router_reload:${o.message}`;sessionStorage.getItem(e)||(sessionStorage.setItem(e,`1`),c=!0)}}),i),u=function(e){if(c)throw window.location.reload(),new Promise(()=>{});if(o)throw o;if(!a)if(n)n(l());else throw l();return s.createElement(a,e)};return u.preload=l,u}var l=`modulepreload`,u=function(e){return`/`+e},d={},f=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=u(t,n),t in d)return;d[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:l,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},p=a(`/_authenticated/errors/$error`)({component:c(()=>f(()=>import(`./_error-BgKHMHmk.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])),`component`)});export{a as i,f as n,c as r,p as t};
+2
View File
@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_error-CEhiecHC.js","assets/messages-kZy_2fG-.js","assets/search-provider-BLfAXVyy.js","assets/dist-BOyWZkOA.js","assets/dist-e45zkog2.js","assets/confirm-dialog-N7FIzDUo.js","assets/button-CI1sqFDO.js","assets/dist-u5B8Q0Qw.js","assets/dist-Dh5ge3Zd.js","assets/dist-Dz_nSMrn.js","assets/dist-DTN-3dEq.js","assets/es2015-DJNqm-x3.js","assets/dist-BjEfRG8A.js","assets/dist-D_VX1DQ0.js","assets/dist-BB2cKiV1.js","assets/dist-Cc5kEhoc.js","assets/dist-HFEBkKCd.js","assets/dist-8VtOfdBX.js","assets/separator-vMwQK9t4.js","assets/tooltip--TK7dEdL.js","assets/dist-JMRFUguJ.js","assets/dist-vhc0iAwK.js","assets/auth-store-B7YG314a.js","assets/dist-A3SZ_duZ.js","assets/with-selector-Bq6p_41G.js","assets/cookies-C6BqVyqT.js","assets/useRouter-BFRHcVKY.js","assets/useNavigate-DtHrQ0GU.js","assets/useStore-BegLNH-k.js","assets/command-St1f4L7D.js","assets/createLucideIcon-CKkuOIuw.js","assets/x-O4huDRHF.js","assets/skeleton-CFm8kRls.js","assets/circle-check-XGTPrS6o.js","assets/sun-BSCMuhso.js","assets/shield-check-CzRgD9G5.js","assets/wallet-C_Rj46AZ.js","assets/logo-DxhHOFqN.js","assets/input-Bhusz9zP.js","assets/font-provider-BWVtH5eQ.js","assets/theme-provider-BQCmm5ub.js","assets/theme-switch-BhK5XnmV.js","assets/dropdown-menu-CPJ_eMIt.js","assets/check-jWXQT0HB.js","assets/header-sQjuQN7h.js","assets/link-Dw1SMkr2.js","assets/ClientOnly-CKH5SArO.js","assets/forbidden-oyIuUuBn.js","assets/general-error-BryI8zNx.js","assets/maintenance-error-CECzFgNM.js","assets/not-found-error-mipAgOS5.js","assets/unauthorized-error-CgOrMfGt.js"])))=>i.map(i=>d[i]);
import{n as e,r as t,t as n}from"./preload-helper-CGevTkrx.js";var r=t(`/_authenticated/errors/$error`)({component:e(()=>n(()=>import(`./_error-CEhiecHC.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])),`component`)});export{r as t};
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_trade_id-DJoAcotW.js","assets/messages-kZy_2fG-.js","assets/button-CI1sqFDO.js","assets/select-RDYeL0gL.js","assets/dist-JMRFUguJ.js","assets/dist-e45zkog2.js","assets/dist-Dz_nSMrn.js","assets/dist-DTN-3dEq.js","assets/dist-8VtOfdBX.js","assets/dist-BOyWZkOA.js","assets/dist-Cc5kEhoc.js","assets/dist-D_VX1DQ0.js","assets/dist-Dh5ge3Zd.js","assets/es2015-DJNqm-x3.js","assets/dist-HFEBkKCd.js","assets/dist-vhc0iAwK.js","assets/createLucideIcon-CKkuOIuw.js","assets/check-jWXQT0HB.js","assets/chevron-down-CFeWMPkU.js","assets/auth-store-B7YG314a.js","assets/dist-A3SZ_duZ.js","assets/with-selector-Bq6p_41G.js","assets/cookies-C6BqVyqT.js","assets/useNavigate-DtHrQ0GU.js","assets/useRouter-BFRHcVKY.js","assets/copy-to-clipboard-oLBAmuxz.js","assets/arrow-left-CjjQKxui.js","assets/radio-group-ceeciZec.js","assets/dist-BB2cKiV1.js","assets/dist-BjEfRG8A.js","assets/loader-circle-CmKlBt5c.js","assets/x-O4huDRHF.js","assets/checkout-model-CxAw66kk.js"])))=>i.map(i=>d[i]);
import{n as e,r as t,t as n}from"./preload-helper-CGevTkrx.js";import{t as r}from"./createLucideIcon-CKkuOIuw.js";var i=r(`triangle-alert`,[[`path`,{d:`m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3`,key:`wmoenq`}],[`path`,{d:`M12 9v4`,key:`juzpu7`}],[`path`,{d:`M12 17h.01`,key:`p32p05`}]]),a=t(`/cashier/$trade_id/`)({component:e(()=>n(()=>import(`./_trade_id-DJoAcotW.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])),`component`)});export{i as n,a 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{Bu as e,Qu as t,Vu as n,ct as r,dt as i,ft as a,lt as o,pt as s,st as c,ut as l}from"./messages-GNZRoNCJ.js";import{i as u,n as d,t as f}from"./button-CgehfZOz.js";import{n as p,r as m}from"./font-provider-DigULrV2.js";import{n as h}from"./theme-provider-C_vGocad.js";import{t as g}from"./chevron-down-BYS57rWz.js";import{n as _,t as v}from"./radio-group-DuaMJmwK.js";import{r as y,s as b}from"./zod-Ds1e9pAB.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-CltVYR9l.js";import{t as A}from"./page-header-cy8MPm4c.js";import{t as j}from"./show-submitted-data-Pbl_Qppl.js";var M=t(),N=b({theme:y([`light`,`dark`]),font:y(m)});function P(){let{font:t,setFont:a}=p(),{theme:s,setTheme:y}=h(),b={theme:s,font:t},A=w({resolver:S(N),defaultValues:b});function P(e){e.font!==t&&a(e.font),e.theme!==s&&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:i()}),(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:l()}),(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:o()}),(0,M.jsx)(D,{children:r()}),(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:n()})]})}),(0,M.jsx)(x,{children:(0,M.jsxs)(E,{className:`[&:has([data-state=checked])>div]:border-primary`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(_,{className:`sr-only`,value:`dark`})}),(0,M.jsx)(`div`,{className:`items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground`,children:(0,M.jsxs)(`div`,{className:`space-y-2 rounded-sm bg-slate-950 p-2`,children:[(0,M.jsxs)(`div`,{className:`space-y-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-2 w-20 rounded-lg bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]})]})}),(0,M.jsx)(`span`,{className:`block w-full p-2 text-center font-normal`,children:e()})]})})]})]})}),(0,M.jsx)(f,{type:`submit`,children:c()})]})})}function F(){return(0,M.jsx)(A,{description:a(),title:s(),variant:`section`,children:(0,M.jsx)(P,{})})}var I=F;export{I as component}; import{Ld as e,Rd as t,Yd as n,cn as r,dn as i,fn as a,ln as o,on as s,sn as c,un as l}from"./messages-kZy_2fG-.js";import{i as u,n as d,t as f}from"./button-CI1sqFDO.js";import{n as p,r as m}from"./font-provider-BWVtH5eQ.js";import{n as h}from"./theme-provider-BQCmm5ub.js";import{t as g}from"./chevron-down-CFeWMPkU.js";import{n as _,t as v}from"./radio-group-ceeciZec.js";import{r as y,s as b}from"./zod-BDZSAwfF.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-Cb-bizIH.js";import{t as A}from"./page-header-Bto-ujL-.js";import{t as j}from"./show-submitted-data-BpvpfhNu.js";var M=n(),N=b({theme:y([`light`,`dark`]),font:y(m)});function P(){let{font:n,setFont:i}=p(),{theme:a,setTheme:y}=h(),b={theme:a,font:n},A=w({resolver:S(N),defaultValues:b});function P(e){e.font!==n&&i(e.font),e.theme!==a&&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:l()}),(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:o()}),(0,M.jsx)(O,{})]})}),(0,M.jsx)(C,{control:A.control,name:`theme`,render:({field:n})=>(0,M.jsxs)(x,{children:[(0,M.jsx)(E,{children:r()}),(0,M.jsx)(D,{children:c()}),(0,M.jsx)(O,{}),(0,M.jsxs)(v,{className:`grid max-w-md grid-cols-2 gap-8 pt-2`,defaultValue:n.value,onValueChange:n.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:t()})]})}),(0,M.jsx)(x,{children:(0,M.jsxs)(E,{className:`[&:has([data-state=checked])>div]:border-primary`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(_,{className:`sr-only`,value:`dark`})}),(0,M.jsx)(`div`,{className:`items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground`,children:(0,M.jsxs)(`div`,{className:`space-y-2 rounded-sm bg-slate-950 p-2`,children:[(0,M.jsxs)(`div`,{className:`space-y-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-2 w-20 rounded-lg bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]})]})}),(0,M.jsx)(`span`,{className:`block w-full p-2 text-center font-normal`,children:e()})]})})]})]})}),(0,M.jsx)(f,{type:`submit`,children:s()})]})})}function F(){return(0,M.jsx)(A,{description:i(),title:a(),variant:`section`,children:(0,M.jsx)(P,{})})}var I=F;export{I as component};
@@ -1 +1 @@
import{t as e}from"./createLucideIcon-ZOoxJGca.js";var t=e(`arrow-left`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]);export{t}; import{t as e}from"./createLucideIcon-CKkuOIuw.js";var t=e(`arrow-left`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]);export{t};
@@ -1 +1 @@
import{$u as e,Qu as t,nd as n}from"./messages-GNZRoNCJ.js";var r=n(e(),1),i=t(),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{$d as e,Xd as t,Yd as n}from"./messages-kZy_2fG-.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{Qu as e}from"./messages-GNZRoNCJ.js";import{i as t,r as n,s as r}from"./button-CgehfZOz.js";var i=e(),a=n(`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:n=`default`,asChild:o=!1,...s}){return(0,i.jsx)(o?r:`span`,{className:t(a({variant:n}),e),"data-slot":`badge`,"data-variant":n,...s})}export{o as t}; import{Yd as e}from"./messages-kZy_2fG-.js";import{i as t,r as n,s as r}from"./button-CI1sqFDO.js";var i=e(),a=n(`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:n=`default`,asChild:o=!1,...s}){return(0,i.jsx)(o?r:`span`,{className:t(a({variant:n}),e),"data-slot":`badge`,"data-variant":n,...s})}export{o as t};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{Qu as e}from"./messages-GNZRoNCJ.js";import{i as t}from"./button-CgehfZOz.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{Yd as e}from"./messages-kZy_2fG-.js";import{i as t}from"./button-CI1sqFDO.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{$u as e,nd as t}from"./messages-GNZRoNCJ.js";import{a as n,d as r,u as i}from"./auth-store-D6M2fL5v.js";import{t as a}from"./createLucideIcon-ZOoxJGca.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=t(e(),1),c=new i({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),t=r(c,e=>e.loading),i=n.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:t,refetch:i.refetch}}export{o as n,d as t}; import{$d as e,Xd as t}from"./messages-kZy_2fG-.js";import{a as n,d as r,u as i}from"./auth-store-B7YG314a.js";import{t as a}from"./createLucideIcon-CKkuOIuw.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(t(),1),c=new i({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),t=r(c,e=>e.loading),i=n.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:t,refetch:i.refetch}}export{o as n,d as t};
-1
View File
@@ -1 +0,0 @@
import{t as e}from"./createLucideIcon-ZOoxJGca.js";var t=e(`check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]);export{t};
+1
View File
@@ -0,0 +1 @@
import{t as e}from"./createLucideIcon-CKkuOIuw.js";var t=e(`check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]);export{t};
@@ -1 +1 @@
import{$u as e,Qu as t,nd as n}from"./messages-GNZRoNCJ.js";import{t as r}from"./dist-BBeIw6Mf.js";import{d as i,i as a}from"./button-CgehfZOz.js";import{a as o,r as s,t as c}from"./dist-BVB_XD7R.js";import{t as l}from"./dist-BmDzTadk.js";import{t as u}from"./dist-CUk2ZAUW.js";import{t as d}from"./dist-C2u_XTZj.js";import{t as f}from"./check-Dk399u4y.js";var p=n(e(),1),m=t(),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,...a},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=i(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,...a,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:a,hasConsumerStoppedPropagationRef:o,checked:s,defaultChecked:c,required:l,disabled:f,name:h,value:g,form:_,bubbleInput:v,setBubbleInput:b}=y(E,e),x=i(n,b),S=u(s),C=d(a);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:a(`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{$d as e,Xd as t,Yd as n}from"./messages-kZy_2fG-.js";import{t as r}from"./dist-DTN-3dEq.js";import{d as i,i as a}from"./button-CI1sqFDO.js";import{a as o,r as s,t as c}from"./dist-e45zkog2.js";import{t as l}from"./dist-BjEfRG8A.js";import{t as u}from"./dist-HFEBkKCd.js";import{t as d}from"./dist-8VtOfdBX.js";import{t as f}from"./check-jWXQT0HB.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,...a},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=i(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,...a,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:a,hasConsumerStoppedPropagationRef:o,checked:s,defaultChecked:c,required:l,disabled:f,name:h,value:g,form:_,bubbleInput:v,setBubbleInput:b}=y(E,e),x=i(n,b),S=u(s),C=d(a);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:a(`peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs outline-none transition-shadow focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:data-[state=checked]:bg-primary dark:aria-invalid:ring-destructive/40`,e),"data-slot":`checkbox`,...t,children:(0,m.jsx)(T,{className:`grid place-content-center text-current transition-none`,"data-slot":`checkbox-indicator`,children:(0,m.jsx)(f,{className:`size-3.5`})})})}export{j as t};
@@ -0,0 +1 @@
var e=5e3,t=3e3,n=[10008,10013,404],r=10010,i=`Request failed`;function a(e){return{trade_id:e.trade_id,amount:e.amount,actual_amount:e.actual_amount,token:e.token?.toUpperCase(),currency:e.currency?.toUpperCase(),network:e.network,payment_url:e.payment_url,receive_address:e.receive_address,expiration_time:e.expiration_time,redirect_url:e.redirect_url,created_at:e.created_at,is_selected:e.is_selected}}function o(e){if(e!=null)return typeof e==`object`&&`data`in e?e.data:e}function s(e){return typeof e==`boolean`?e:typeof e==`number`?e!==0:[`1`,`true`,`yes`].includes(String(e??``).toLowerCase())}function c(e){if(e&&typeof e==`object`){if(`status_code`in e&&typeof e.status_code==`number`)return e.status_code;if(`code`in e&&typeof e.code==`number`)return e.code;if(`data`in e)return c(e.data)}}function l(e){if(e==null||e===``)return null;if(typeof e==`number`||/^\d+$/.test(String(e))){let t=Number(e);return new Date(t<0xe8d4a51000?t*1e3:t)}let t=new Date(String(e));return Number.isNaN(t.getTime())?null:t}function u(e,t){return String(e??``).toLowerCase()===String(t??``).toLowerCase()}function d(e){let t=Math.max(0,Math.min(1,e));return t>=.5?`#22c55e`:t>=.2?`#f97316`:`#ef4444`}function f(e,t,n){return e.length===0?null:e.find(e=>u(e.network,n?.network)&&u(e.token,n?.token))||e.find(e=>u(e.network,n?.network))||e.find(e=>u(e.token,n?.token))||e.find(e=>u(e.network,t?.network)&&u(e.token,t?.token))||(e.find(e=>u(e.network,t?.network))??e.find(e=>u(e.token,t?.token))??e[0])}function p(e){return e?.supported_assets?.flatMap(e=>(e.tokens??[]).filter(t=>!!(t&&e.network)).map(t=>({network:e.network,token:t.toUpperCase()})))??[]}function m(e,t){return new Promise((n,r)=>{let i=window.setTimeout(n,e);t?.addEventListener(`abort`,()=>{window.clearTimeout(i),r(t.reason)},{once:!0})})}export{t as a,c,l as d,u as f,o as h,e as i,s as l,p as m,i as n,m as o,d as p,n as r,f as s,r as t,a as u};
@@ -1 +1 @@
import{t as e}from"./createLucideIcon-ZOoxJGca.js";var t=e(`chevron-down`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]);export{t}; import{t as e}from"./createLucideIcon-CKkuOIuw.js";var t=e(`chevron-down`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]);export{t};
@@ -1 +1 @@
import{t as e}from"./createLucideIcon-ZOoxJGca.js";var t=e(`chevrons-up-down`,[[`path`,{d:`m7 15 5 5 5-5`,key:`1hf1tw`}],[`path`,{d:`m7 9 5-5 5 5`,key:`sgt6xg`}]]);export{t}; import{t as e}from"./createLucideIcon-CKkuOIuw.js";var t=e(`chevrons-up-down`,[[`path`,{d:`m7 15 5 5 5-5`,key:`1hf1tw`}],[`path`,{d:`m7 9 5-5 5 5`,key:`sgt6xg`}]]);export{t};
@@ -1 +1 @@
import{t as e}from"./createLucideIcon-ZOoxJGca.js";var t=e(`circle-check`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]);export{t}; import{t as e}from"./createLucideIcon-CKkuOIuw.js";var t=e(`circle-check`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]);export{t};
@@ -1 +1 @@
import{t as e}from"./createLucideIcon-ZOoxJGca.js";var t=e(`clock-3`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M12 6v6h4`,key:`135r8i`}]]);export{t}; import{t as e}from"./createLucideIcon-CKkuOIuw.js";var t=e(`clock-3`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M12 6v6h4`,key:`135r8i`}]]);export{t};
@@ -1 +0,0 @@
import{Fu as e,Nu as t,Pu as n,Qu as r}from"./messages-GNZRoNCJ.js";import{d as i,l as a,m as o,p as s,u as c}from"./command-BVulVd11.js";import{t as l}from"./telescope-nKJKrlM1.js";var u=r();function d({open:r,onOpenChange:d}){return(0,u.jsx)(a,{onOpenChange:d,open:r,children:(0,u.jsx)(c,{className:`sm:max-w-sm`,children:(0,u.jsxs)(s,{className:`items-center text-center`,children:[(0,u.jsx)(l,{className:`mb-2 size-12 text-muted-foreground`}),(0,u.jsx)(o,{children:e()}),(0,u.jsxs)(i,{children:[n(),(0,u.jsx)(`br`,{}),(0,u.jsx)(`p`,{className:`mt-2 text-center`,children:t()})]})]})})})}export{d as t};
@@ -0,0 +1 @@
import{Ad as e,Md as t,Yd as n,jd as r}from"./messages-kZy_2fG-.js";import{d as i,l as a,m as o,p as s,u as c}from"./command-St1f4L7D.js";import{t as l}from"./telescope-B6BZXMCK.js";var u=n();function d({open:n,onOpenChange:d}){return(0,u.jsx)(a,{onOpenChange:d,open:n,children:(0,u.jsx)(c,{className:`sm:max-w-sm`,children:(0,u.jsxs)(s,{className:`items-center text-center`,children:[(0,u.jsx)(l,{className:`mb-2 size-12 text-muted-foreground`}),(0,u.jsx)(o,{children:t()}),(0,u.jsxs)(i,{children:[r(),(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
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
import{$u as e,Ku as t,Qu as n,nd as r,qu as i}from"./messages-GNZRoNCJ.js";import{d as a,i as o,l as s,t as c}from"./button-CgehfZOz.js";import{a as l,r as u}from"./dist-BVB_XD7R.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-C6g_SrVT.js";var b=r(e(),1),x=n(),S=`AlertDialog`,[C,te]=l(S,[y]),w=y(),T=e=>{let{__scopeAlertDialog:t,...n}=e,r=w(t);return(0,x.jsx)(g,{...r,...n,modal:!0})};T.displayName=S;var E=`AlertDialogTrigger`,D=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(f,{...i,...r,ref:t})});D.displayName=E;var O=`AlertDialogPortal`,k=e=>{let{__scopeAlertDialog:t,...n}=e,r=w(t);return(0,x.jsx)(d,{...r,...n})};k.displayName=O;var A=`AlertDialogOverlay`,j=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(p,{...i,...r,ref:t})});j.displayName=A;var M=`AlertDialogContent`,[N,P]=C(M),F=s(`AlertDialogContent`),I=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,children:r,...i}=e,o=w(n),s=b.useRef(null),c=a(t,s),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`,...o,...i,ref:c,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:s})]})})})});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),o=w(n),s=a(t,i);return(0,x.jsx)(v,{...o,...r,ref:s})});W.displayName=U;var G=({contentRef:e})=>{let t=`\`${M}\` requires a description for the component to be accessible for screen reader users. import{$d as e,Ud as t,Wd as n,Xd as r,Yd as i}from"./messages-kZy_2fG-.js";import{d as a,i as o,l as s,t as c}from"./button-CI1sqFDO.js";import{a as l,r as u}from"./dist-e45zkog2.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-u5B8Q0Qw.js";var b=e(r(),1),x=i(),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=s(`AlertDialogContent`),I=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,children:r,...i}=e,o=w(n),s=b.useRef(null),c=a(t,s),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`,...o,...i,ref:c,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:s})]})})})});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),o=w(n),s=a(t,i);return(0,x.jsx)(v,{...o,...r,ref:s})});W.displayName=U;var G=({contentRef:e})=>{let t=`\`${M}\` requires a description for the component to be accessible for screen reader users.
You can add a description to the \`${M}\` by passing a \`${z}\` component as a child, which also benefits sighted users by adding visible context to the dialog. You can add a description to the \`${M}\` by passing a \`${z}\` component as a child, which also benefits sighted users by adding visible context to the dialog.
Alternatively, you can use your own component as a description by assigning it an \`id\` and passing the same value to the \`aria-describedby\` prop in \`${M}\`. If the description is confusing or duplicative for sighted users, you can use the \`@radix-ui/react-visually-hidden\` primitive as a wrapper around your description component. Alternatively, you can use your own component as a description by assigning it an \`id\` and passing the same value to the \`aria-describedby\` prop in \`${M}\`. If the description is confusing or duplicative for sighted users, you can use the \`@radix-ui/react-visually-hidden\` primitive as a wrapper around your description component.
For more information, see https://radix-ui.com/primitives/docs/components/alert-dialog`;return b.useEffect(()=>{document.getElementById(e.current?.getAttribute(`aria-describedby`))||console.warn(t)},[t,e]),null},K=T,q=k,J=j,Y=I,X=W,Z=R,Q=B;function ne({...e}){return(0,x.jsx)(K,{"data-slot":`alert-dialog`,...e})}function re({...e}){return(0,x.jsx)(q,{"data-slot":`alert-dialog-portal`,...e})}function ie({className:e,...t}){return(0,x.jsx)(J,{className:o(`data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in`,e),"data-slot":`alert-dialog-overlay`,...t})}function ae({className:e,size:t=`default`,...n}){return(0,x.jsxs)(re,{children:[(0,x.jsx)(ie,{}),(0,x.jsx)(Y,{className:o(`group/alert-dialog-content data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[state=closed]:animate-out data-[state=open]:animate-in data-[size=default]:sm:max-w-lg`,e),"data-size":t,"data-slot":`alert-dialog-content`,...n})]})}function oe({className:e,...t}){return(0,x.jsx)(`div`,{className:o(`grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]`,e),"data-slot":`alert-dialog-header`,...t})}function $({className:e,...t}){return(0,x.jsx)(`div`,{className:o(`flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end`,e),"data-slot":`alert-dialog-footer`,...t})}function se({className:e,...t}){return(0,x.jsx)(Z,{className:o(`font-semibold text-lg sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2`,e),"data-slot":`alert-dialog-title`,...t})}function ce({className:e,...t}){return(0,x.jsx)(Q,{className:o(`text-muted-foreground text-sm`,e),"data-slot":`alert-dialog-description`,...t})}function le({className:e,variant:t=`outline`,size:n=`default`,...r}){return(0,x.jsx)(c,{asChild:!0,size:n,variant:t,children:(0,x.jsx)(X,{className:o(e),"data-slot":`alert-dialog-cancel`,...r})})}function ue(e){let{title:n,desc:r,children:a,className:s,confirmText:l,cancelBtnText:u,destructive:d,isLoading:f,disabled:p=!1,form:m,handleConfirm:h,...g}=e;return(0,x.jsx)(ne,{...g,children:(0,x.jsxs)(ae,{className:o(s&&s),children:[(0,x.jsxs)(oe,{className:`text-start`,children:[(0,x.jsx)(se,{children:n}),(0,x.jsx)(ce,{asChild:!0,children:(0,x.jsx)(`div`,{children:r})})]}),a,(0,x.jsxs)($,{children:[(0,x.jsx)(le,{disabled:f,children:u??i()}),(0,x.jsx)(c,{disabled:p||f,form:m,onClick:h,type:m?`submit`:`button`,variant:d?`destructive`:`default`,children:l??t()})]})]})})}export{ue as t}; For more information, see https://radix-ui.com/primitives/docs/components/alert-dialog`;return b.useEffect(()=>{document.getElementById(e.current?.getAttribute(`aria-describedby`))||console.warn(t)},[t,e]),null},K=T,q=k,J=j,Y=I,X=W,Z=R,Q=B;function ne({...e}){return(0,x.jsx)(K,{"data-slot":`alert-dialog`,...e})}function re({...e}){return(0,x.jsx)(q,{"data-slot":`alert-dialog-portal`,...e})}function ie({className:e,...t}){return(0,x.jsx)(J,{className:o(`data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in`,e),"data-slot":`alert-dialog-overlay`,...t})}function ae({className:e,size:t=`default`,...n}){return(0,x.jsxs)(re,{children:[(0,x.jsx)(ie,{}),(0,x.jsx)(Y,{className:o(`group/alert-dialog-content data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[state=closed]:animate-out data-[state=open]:animate-in data-[size=default]:sm:max-w-lg`,e),"data-size":t,"data-slot":`alert-dialog-content`,...n})]})}function oe({className:e,...t}){return(0,x.jsx)(`div`,{className:o(`grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]`,e),"data-slot":`alert-dialog-header`,...t})}function $({className:e,...t}){return(0,x.jsx)(`div`,{className:o(`flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end`,e),"data-slot":`alert-dialog-footer`,...t})}function se({className:e,...t}){return(0,x.jsx)(Z,{className:o(`font-semibold text-lg sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2`,e),"data-slot":`alert-dialog-title`,...t})}function ce({className:e,...t}){return(0,x.jsx)(Q,{className:o(`text-muted-foreground text-sm`,e),"data-slot":`alert-dialog-description`,...t})}function le({className:e,variant:t=`outline`,size:n=`default`,...r}){return(0,x.jsx)(c,{asChild:!0,size:n,variant:t,children:(0,x.jsx)(X,{className:o(e),"data-slot":`alert-dialog-cancel`,...r})})}function ue(e){let{title:r,desc:i,children:a,className:s,confirmText:l,cancelBtnText:u,destructive:d,isLoading:f,disabled:p=!1,form:m,handleConfirm:h,...g}=e;return(0,x.jsx)(ne,{...g,children:(0,x.jsxs)(ae,{className:o(s&&s),children:[(0,x.jsxs)(oe,{className:`text-start`,children:[(0,x.jsx)(se,{children:r}),(0,x.jsx)(ce,{asChild:!0,children:(0,x.jsx)(`div`,{children:i})})]}),a,(0,x.jsxs)($,{children:[(0,x.jsx)(le,{disabled:f,children:u??n()}),(0,x.jsx)(c,{disabled:p||f,form:m,onClick:h,type:m?`submit`:`button`,variant:d?`destructive`:`default`,children:l??t()})]})]})})}export{ue as t};
@@ -1 +1 @@
import{ed as e}from"./messages-GNZRoNCJ.js";import{t}from"./createLucideIcon-ZOoxJGca.js";var n=t(`copy`,[[`rect`,{width:`14`,height:`14`,x:`8`,y:`8`,rx:`2`,ry:`2`,key:`17jyea`}],[`path`,{d:`M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2`,key:`zix9uf`}]]),r=e(((e,t)=>{t.exports=function(){var e=document.getSelection();if(!e.rangeCount)return function(){};for(var t=document.activeElement,n=[],r=0;r<e.rangeCount;r++)n.push(e.getRangeAt(r));switch(t.tagName.toUpperCase()){case`INPUT`:case`TEXTAREA`:t.blur();break;default:t=null;break}return e.removeAllRanges(),function(){e.type===`Caret`&&e.removeAllRanges(),e.rangeCount||n.forEach(function(t){e.addRange(t)}),t&&t.focus()}}})),i=e(((e,t)=>{var n=r(),i={"text/plain":`Text`,"text/html":`Url`,default:`Text`},a=`Copy to clipboard: #{key}, Enter`;function o(e){var t=(/mac os x/i.test(navigator.userAgent)?``:`Ctrl`)+`+C`;return e.replace(/#{\s*key\s*}/g,t)}function s(e,t){var r,s,c,l,u,d,f=!1;t||={},r=t.debug||!1;try{if(c=n(),l=document.createRange(),u=document.getSelection(),d=document.createElement(`span`),d.textContent=e,d.ariaHidden=`true`,d.style.all=`unset`,d.style.position=`fixed`,d.style.top=0,d.style.clip=`rect(0, 0, 0, 0)`,d.style.whiteSpace=`pre`,d.style.webkitUserSelect=`text`,d.style.MozUserSelect=`text`,d.style.msUserSelect=`text`,d.style.userSelect=`text`,d.addEventListener(`copy`,function(n){if(n.stopPropagation(),t.format)if(n.preventDefault(),n.clipboardData===void 0){r&&console.warn(`unable to use e.clipboardData`),r&&console.warn(`trying IE specific stuff`),window.clipboardData.clearData();var a=i[t.format]||i.default;window.clipboardData.setData(a,e)}else n.clipboardData.clearData(),n.clipboardData.setData(t.format,e);t.onCopy&&(n.preventDefault(),t.onCopy(n.clipboardData))}),document.body.appendChild(d),l.selectNodeContents(d),u.addRange(l),!document.execCommand(`copy`))throw Error(`copy command was unsuccessful`);f=!0}catch(n){r&&console.error(`unable to copy using execCommand: `,n),r&&console.warn(`trying IE specific stuff`);try{window.clipboardData.setData(t.format||`text`,e),t.onCopy&&t.onCopy(window.clipboardData),f=!0}catch(n){r&&console.error(`unable to copy using clipboardData: `,n),r&&console.error(`falling back to prompt`),s=o(`message`in t?t.message:a),window.prompt(s,e)}}finally{u&&(typeof u.removeRange==`function`?u.removeRange(l):u.removeAllRanges()),d&&document.body.removeChild(d),c()}return f}t.exports=s}));export{n,i as t}; import{Zd as e}from"./messages-kZy_2fG-.js";import{t}from"./createLucideIcon-CKkuOIuw.js";var n=t(`copy`,[[`rect`,{width:`14`,height:`14`,x:`8`,y:`8`,rx:`2`,ry:`2`,key:`17jyea`}],[`path`,{d:`M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2`,key:`zix9uf`}]]),r=e(((e,t)=>{t.exports=function(){var e=document.getSelection();if(!e.rangeCount)return function(){};for(var t=document.activeElement,n=[],r=0;r<e.rangeCount;r++)n.push(e.getRangeAt(r));switch(t.tagName.toUpperCase()){case`INPUT`:case`TEXTAREA`:t.blur();break;default:t=null;break}return e.removeAllRanges(),function(){e.type===`Caret`&&e.removeAllRanges(),e.rangeCount||n.forEach(function(t){e.addRange(t)}),t&&t.focus()}}})),i=e(((e,t)=>{var n=r(),i={"text/plain":`Text`,"text/html":`Url`,default:`Text`},a=`Copy to clipboard: #{key}, Enter`;function o(e){var t=(/mac os x/i.test(navigator.userAgent)?``:`Ctrl`)+`+C`;return e.replace(/#{\s*key\s*}/g,t)}function s(e,t){var r,s,c,l,u,d,f=!1;t||={},r=t.debug||!1;try{if(c=n(),l=document.createRange(),u=document.getSelection(),d=document.createElement(`span`),d.textContent=e,d.ariaHidden=`true`,d.style.all=`unset`,d.style.position=`fixed`,d.style.top=0,d.style.clip=`rect(0, 0, 0, 0)`,d.style.whiteSpace=`pre`,d.style.webkitUserSelect=`text`,d.style.MozUserSelect=`text`,d.style.msUserSelect=`text`,d.style.userSelect=`text`,d.addEventListener(`copy`,function(n){if(n.stopPropagation(),t.format)if(n.preventDefault(),n.clipboardData===void 0){r&&console.warn(`unable to use e.clipboardData`),r&&console.warn(`trying IE specific stuff`),window.clipboardData.clearData();var a=i[t.format]||i.default;window.clipboardData.setData(a,e)}else n.clipboardData.clearData(),n.clipboardData.setData(t.format,e);t.onCopy&&(n.preventDefault(),t.onCopy(n.clipboardData))}),document.body.appendChild(d),l.selectNodeContents(d),u.addRange(l),!document.execCommand(`copy`))throw Error(`copy command was unsuccessful`);f=!0}catch(n){r&&console.error(`unable to copy using execCommand: `,n),r&&console.warn(`trying IE specific stuff`);try{window.clipboardData.setData(t.format||`text`,e),t.onCopy&&t.onCopy(window.clipboardData),f=!0}catch(n){r&&console.error(`unable to copy using clipboardData: `,n),r&&console.error(`falling back to prompt`),s=o(`message`in t?t.message:a),window.prompt(s,e)}}finally{u&&(typeof u.removeRange==`function`?u.removeRange(l):u.removeAllRanges()),d&&document.body.removeChild(d),c()}return f}t.exports=s}));export{n,i as t};
@@ -1 +1 @@
import{$u as e,nd as t}from"./messages-GNZRoNCJ.js";var n=t(e()),r=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),i=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),a=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(e,t,n)=>n?n.toUpperCase():t.toLowerCase()),o=e=>{let t=a(e);return t.charAt(0).toUpperCase()+t.slice(1)},s={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},c=e=>{for(let t in e)if(t.startsWith(`aria-`)||t===`role`||t===`title`)return!0;return!1},l=(0,n.createContext)({}),u=()=>(0,n.useContext)(l),d=(0,n.forwardRef)(({color:e,size:t,strokeWidth:i,absoluteStrokeWidth:a,className:o=``,children:l,iconNode:d,...f},p)=>{let{size:m=24,strokeWidth:h=2,absoluteStrokeWidth:g=!1,color:_=`currentColor`,className:v=``}=u()??{},y=a??g?Number(i??h)*24/Number(t??m):i??h;return(0,n.createElement)(`svg`,{ref:p,...s,width:t??m??s.width,height:t??m??s.height,stroke:e??_,strokeWidth:y,className:r(`lucide`,v,o),...!l&&!c(f)&&{"aria-hidden":`true`},...f},[...d.map(([e,t])=>(0,n.createElement)(e,t)),...Array.isArray(l)?l:[l]])}),f=(e,t)=>{let a=(0,n.forwardRef)(({className:a,...s},c)=>(0,n.createElement)(d,{ref:c,iconNode:t,className:r(`lucide-${i(o(e))}`,`lucide-${e}`,a),...s}));return a.displayName=o(e),a};export{f as t}; import{$d as e,Xd as t}from"./messages-kZy_2fG-.js";var n=e(t()),r=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),i=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),a=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(e,t,n)=>n?n.toUpperCase():t.toLowerCase()),o=e=>{let t=a(e);return t.charAt(0).toUpperCase()+t.slice(1)},s={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},c=e=>{for(let t in e)if(t.startsWith(`aria-`)||t===`role`||t===`title`)return!0;return!1},l=(0,n.createContext)({}),u=()=>(0,n.useContext)(l),d=(0,n.forwardRef)(({color:e,size:t,strokeWidth:i,absoluteStrokeWidth:a,className:o=``,children:l,iconNode:d,...f},p)=>{let{size:m=24,strokeWidth:h=2,absoluteStrokeWidth:g=!1,color:_=`currentColor`,className:v=``}=u()??{},y=a??g?Number(i??h)*24/Number(t??m):i??h;return(0,n.createElement)(`svg`,{ref:p,...s,width:t??m??s.width,height:t??m??s.height,stroke:e??_,strokeWidth:y,className:r(`lucide`,v,o),...!l&&!c(f)&&{"aria-hidden":`true`},...f},[...d.map(([e,t])=>(0,n.createElement)(e,t)),...Array.isArray(l)?l:[l]])}),f=(e,t)=>{let a=(0,n.forwardRef)(({className:a,...s},c)=>(0,n.createElement)(d,{ref:c,iconNode:t,className:r(`lucide-${i(o(e))}`,`lucide-${e}`,a),...s}));return a.displayName=o(e),a};export{f as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
import{$ as e,Q as t,Qu as n,X as r,Y as i,Z as a,at as o,et as s,it as c,nt as l,ot as u,rt as d,tt as f}from"./messages-GNZRoNCJ.js";import{t as p}from"./button-CgehfZOz.js";import{t as m}from"./checkbox-DatOgaAb.js";import{c as h,i as g,s as _}from"./zod-Ds1e9pAB.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-CltVYR9l.js";import{t as D}from"./page-header-cy8MPm4c.js";import{t as O}from"./show-submitted-data-Pbl_Qppl.js";var k=n(),A=[{id:`recents`,label:c()},{id:`home`,label:d()},{id:`applications`,label:l()},{id:`desktop`,label:f()},{id:`downloads`,label:s()},{id:`documents`,label:e()}],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:r()})]}),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:i()})]})})}function P(){return(0,k.jsx)(D,{description:o(),title:u(),variant:`section`,children:(0,k.jsx)(N,{})})}var F=P;export{F as component};
+1
View File
@@ -0,0 +1 @@
import{$t as e,Jt as t,Qt as n,Xt as r,Yd as i,Yt as a,Zt as o,an as s,en as c,in as l,nn as u,rn as d,tn as f}from"./messages-kZy_2fG-.js";import{t as p}from"./button-CI1sqFDO.js";import{t as m}from"./checkbox-Buso7MQb.js";import{c as h,i as g,s as _}from"./zod-BDZSAwfF.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-Cb-bizIH.js";import{t as D}from"./page-header-Bto-ujL-.js";import{t as O}from"./show-submitted-data-BpvpfhNu.js";var k=i(),A=[{id:`recents`,label:d()},{id:`home`,label:u()},{id:`applications`,label:f()},{id:`desktop`,label:c()},{id:`downloads`,label:e()},{id:`documents`,label:n()}],j=_({items:g(h()).refine(e=>e.some(e=>e),{message:o()})}),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:r()}),(0,k.jsx)(w,{children:a()})]}),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:t()})]})})}function P(){return(0,k.jsx)(D,{description:l(),title:s(),variant:`section`,children:(0,k.jsx)(N,{})})}var F=P;export{F as component};
@@ -1 +1 @@
import{$u as e,nd as t}from"./messages-GNZRoNCJ.js";import{n}from"./dist-BVB_XD7R.js";var r=t(e(),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{$d as e,Xd as t}from"./messages-kZy_2fG-.js";import{n}from"./dist-e45zkog2.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};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{$u as e,Qu as t,nd as n}from"./messages-GNZRoNCJ.js";import{t as r}from"./dist-BBeIw6Mf.js";import{d as i}from"./button-CgehfZOz.js";import{a,r as o,t as s}from"./dist-BVB_XD7R.js";import{n as c,r as l,t as u}from"./dist-ocY2rTvF.js";import{t as d}from"./dist-BmDzTadk.js";import{n as f}from"./dist-DHfFvH8a.js";import{t as p}from"./dist-CUk2ZAUW.js";import{t as m}from"./dist-C2u_XTZj.js";var h=n(e(),1),g=t(),_=`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)(d,{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),u=p(n),d=m(t);return h.useEffect(()=>{let e=c.current;if(!e)return;let t=window.HTMLInputElement.prototype,r=Object.getOwnPropertyDescriptor(t,`checked`).set;if(u!==n&&r){let t=new Event(`click`,{bubbles:a});r.call(e,n),e.dispatchEvent(t)}},[u,n,a]),(0,g.jsx)(r.input,{type:`radio`,"aria-hidden":!0,defaultChecked:n,...o,tabIndex:-1,ref:l,style:{...o.style,...d,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,[l,y]),M=l(),N=y(),[P,F]=A(k),I=h.forwardRef((e,t)=>{let{__scopeRadioGroup:n,name:i,defaultValue:a,value:o,required:l=!1,disabled:u=!1,orientation:d,dir:p,loop:m=!0,onValueChange:h,..._}=e,v=M(n),y=f(p),[b,x]=s({prop:o,defaultProp:a??null,onChange:h,caller:k});return(0,g.jsx)(P,{scope:n,name:i,required:l,disabled:u,value:b,onValueChange:x,children:(0,g.jsx)(c,{asChild:!0,...v,orientation:d,dir:y,loop:m,children:(0,g.jsx)(r.div,{role:`radiogroup`,"aria-required":l,"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),d=N(n),f=h.useRef(null),p=i(t,f),m=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)(u,{asChild:!0,...l,focusable:!c,active:m,children:(0,g.jsx)(S,{disabled:c,required:s.required,checked:m,...d,...a,name:s.name,ref:p,onCheck:()=>s.onValueChange(a.value),onKeyDown:o(e=>{e.key===`Enter`&&e.preventDefault()}),onFocus:o(a.onFocus,()=>{_.current&&f.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{$d as e,Xd as t,Yd as n}from"./messages-kZy_2fG-.js";import{t as r}from"./dist-DTN-3dEq.js";import{d as i}from"./button-CI1sqFDO.js";import{a,r as o,t as s}from"./dist-e45zkog2.js";import{n as c,r as l,t as u}from"./dist-Cc5kEhoc.js";import{t as d}from"./dist-BjEfRG8A.js";import{n as f}from"./dist-D_VX1DQ0.js";import{t as p}from"./dist-HFEBkKCd.js";import{t as m}from"./dist-8VtOfdBX.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)(d,{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),u=p(n),d=m(t);return h.useEffect(()=>{let e=c.current;if(!e)return;let t=window.HTMLInputElement.prototype,r=Object.getOwnPropertyDescriptor(t,`checked`).set;if(u!==n&&r){let t=new Event(`click`,{bubbles:a});r.call(e,n),e.dispatchEvent(t)}},[u,n,a]),(0,g.jsx)(r.input,{type:`radio`,"aria-hidden":!0,defaultChecked:n,...o,tabIndex:-1,ref:l,style:{...o.style,...d,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,[l,y]),M=l(),N=y(),[P,F]=A(k),I=h.forwardRef((e,t)=>{let{__scopeRadioGroup:n,name:i,defaultValue:a,value:o,required:l=!1,disabled:u=!1,orientation:d,dir:p,loop:m=!0,onValueChange:h,..._}=e,v=M(n),y=f(p),[b,x]=s({prop:o,defaultProp:a??null,onChange:h,caller:k});return(0,g.jsx)(P,{scope:n,name:i,required:l,disabled:u,value:b,onValueChange:x,children:(0,g.jsx)(c,{asChild:!0,...v,orientation:d,dir:y,loop:m,children:(0,g.jsx)(r.div,{role:`radiogroup`,"aria-required":l,"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),d=N(n),f=h.useRef(null),p=i(t,f),m=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)(u,{asChild:!0,...l,focusable:!c,active:m,children:(0,g.jsx)(S,{disabled:c,required:s.required,checked:m,...d,...a,name:s.name,ref:p,onCheck:()=>s.onValueChange(a.value),onKeyDown:o(e=>{e.key===`Enter`&&e.preventDefault()}),onFocus:o(a.onFocus,()=>{_.current&&f.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
View File
@@ -0,0 +1 @@
function e(e,[t,n]){return Math.min(n,Math.max(t,e))}export{e as t};
@@ -1 +1 @@
import{$u as e,nd as t}from"./messages-GNZRoNCJ.js";import{d as n}from"./button-CgehfZOz.js";import{n as r}from"./dist-BVB_XD7R.js";var i=t(e(),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{$d as e,Xd as t}from"./messages-kZy_2fG-.js";import{d as n}from"./button-CI1sqFDO.js";import{n as r}from"./dist-e45zkog2.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{$u as e,Qu as t,nd as n}from"./messages-GNZRoNCJ.js";import{t as r}from"./dist-BBeIw6Mf.js";import{c as i,d as a}from"./button-CgehfZOz.js";import{a as o,r as s,t as c}from"./dist-BVB_XD7R.js";import{n as l,t as u}from"./dist-y2ssackP.js";import{n as d}from"./dist-DHfFvH8a.js";var f=n(e(),1),p=t();function m(e){let t=e+`CollectionProvider`,[n,r]=o(t),[s,c]=n(t,{collectionRef:{current:null},itemMap:new Map}),l=e=>{let{scope:t,children:n}=e,r=f.useRef(null),i=f.useRef(new Map).current;return(0,p.jsx)(s,{scope:t,itemMap:i,collectionRef:r,children:n})};l.displayName=t;let u=e+`CollectionSlot`,d=i(u),m=f.forwardRef((e,t)=>{let{scope:n,children:r}=e;return(0,p.jsx)(d,{ref:a(t,c(u,n).collectionRef),children:r})});m.displayName=u;let h=e+`CollectionItemSlot`,g=`data-radix-collection-item`,_=i(h),v=f.forwardRef((e,t)=>{let{scope:n,children:r,...i}=e,o=f.useRef(null),s=a(t,o),l=c(h,n);return f.useEffect(()=>(l.itemMap.set(o,{ref:o,...i}),()=>void l.itemMap.delete(o))),(0,p.jsx)(_,{[g]:``,ref:s,children:r})});v.displayName=h;function y(t){let n=c(e+`CollectionConsumer`,t);return f.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:l,Slot:m,ItemSlot:v},y,r]}var h=`rovingFocusGroup.onEntryFocus`,g={bubbles:!1,cancelable:!0},_=`RovingFocusGroup`,[v,y,b]=m(_),[x,S]=o(_,[b]),[C,w]=x(_),T=f.forwardRef((e,t)=>(0,p.jsx)(v.Provider,{scope:e.__scopeRovingFocusGroup,children:(0,p.jsx)(v.Slot,{scope:e.__scopeRovingFocusGroup,children:(0,p.jsx)(E,{...e,ref:t})})}));T.displayName=_;var E=f.forwardRef((e,t)=>{let{__scopeRovingFocusGroup:n,orientation:i,loop:o=!1,dir:l,currentTabStopId:m,defaultCurrentTabStopId:v,onCurrentTabStopIdChange:b,onEntryFocus:x,preventScrollOnEntryFocus:S=!1,...w}=e,T=f.useRef(null),E=a(t,T),D=d(l),[O,k]=c({prop:m,defaultProp:v??null,onChange:b,caller:_}),[A,j]=f.useState(!1),N=u(x),P=y(n),F=f.useRef(!1),[I,L]=f.useState(0);return f.useEffect(()=>{let e=T.current;if(e)return e.addEventListener(h,N),()=>e.removeEventListener(h,N)},[N]),(0,p.jsx)(C,{scope:n,orientation:i,dir:D,loop:o,currentTabStopId:O,onItemFocus:f.useCallback(e=>k(e),[k]),onItemShiftTab:f.useCallback(()=>j(!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":i,...w,ref:E,style:{outline:`none`,...e.style},onMouseDown:s(e.onMouseDown,()=>{F.current=!0}),onFocus:s(e.onFocus,e=>{let t=!F.current;if(e.target===e.currentTarget&&t&&!A){let t=new CustomEvent(h,g);if(e.currentTarget.dispatchEvent(t),!t.defaultPrevented){let e=P().filter(e=>e.focusable);M([e.find(e=>e.active),e.find(e=>e.id===O),...e].filter(Boolean).map(e=>e.ref.current),S)}}F.current=!1}),onBlur:s(e.onBlur,()=>j(!1))})})}),D=`RovingFocusGroupItem`,O=f.forwardRef((e,t)=>{let{__scopeRovingFocusGroup:n,focusable:i=!0,active:a=!1,tabStopId:o,children:c,...u}=e,d=l(),m=o||d,h=w(D,n),g=h.currentTabStopId===m,_=y(n),{onFocusableItemAdd:b,onFocusableItemRemove:x,currentTabStopId:S}=h;return f.useEffect(()=>{if(i)return b(),()=>x()},[i,b,x]),(0,p.jsx)(v.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:s(e.onMouseDown,e=>{i?h.onItemFocus(m):e.preventDefault()}),onFocus:s(e.onFocus,()=>h.onItemFocus(m)),onKeyDown:s(e.onKeyDown,e=>{if(e.key===`Tab`&&e.shiftKey){h.onItemShiftTab();return}if(e.target!==e.currentTarget)return;let t=j(e,h.orientation,h.dir);if(t!==void 0){if(e.metaKey||e.ctrlKey||e.altKey||e.shiftKey)return;e.preventDefault();let n=_().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?N(n,r+1):n.slice(r+1)}setTimeout(()=>M(n))}}),children:typeof c==`function`?c({isCurrentTabStop:g,hasTabStop:S!=null}):c})})});O.displayName=D;var k={ArrowLeft:`prev`,ArrowUp:`prev`,ArrowRight:`next`,ArrowDown:`next`,PageUp:`first`,Home:`first`,PageDown:`last`,End:`last`};function A(e,t){return t===`rtl`?e===`ArrowLeft`?`ArrowRight`:e===`ArrowRight`?`ArrowLeft`:e:e}function j(e,t,n){let r=A(e.key,n);if(!(t===`vertical`&&[`ArrowLeft`,`ArrowRight`].includes(r))&&!(t===`horizontal`&&[`ArrowUp`,`ArrowDown`].includes(r)))return k[r]}function M(e,t=!1){let n=document.activeElement;for(let r of e)if(r===n||(r.focus({preventScroll:t}),document.activeElement!==n))return}function N(e,t){return e.map((n,r)=>e[(t+r)%e.length])}var P=T,F=O;export{m as i,P as n,S as r,F as t}; import{$d as e,Xd as t,Yd as n}from"./messages-kZy_2fG-.js";import{t as r}from"./dist-DTN-3dEq.js";import{c as i,d as a}from"./button-CI1sqFDO.js";import{a as o,r as s,t as c}from"./dist-e45zkog2.js";import{n as l,t as u}from"./dist-Dz_nSMrn.js";import{n as d}from"./dist-D_VX1DQ0.js";var f=e(t(),1),p=n();function m(e){let t=e+`CollectionProvider`,[n,r]=o(t),[s,c]=n(t,{collectionRef:{current:null},itemMap:new Map}),l=e=>{let{scope:t,children:n}=e,r=f.useRef(null),i=f.useRef(new Map).current;return(0,p.jsx)(s,{scope:t,itemMap:i,collectionRef:r,children:n})};l.displayName=t;let u=e+`CollectionSlot`,d=i(u),m=f.forwardRef((e,t)=>{let{scope:n,children:r}=e;return(0,p.jsx)(d,{ref:a(t,c(u,n).collectionRef),children:r})});m.displayName=u;let h=e+`CollectionItemSlot`,g=`data-radix-collection-item`,_=i(h),v=f.forwardRef((e,t)=>{let{scope:n,children:r,...i}=e,o=f.useRef(null),s=a(t,o),l=c(h,n);return f.useEffect(()=>(l.itemMap.set(o,{ref:o,...i}),()=>void l.itemMap.delete(o))),(0,p.jsx)(_,{[g]:``,ref:s,children:r})});v.displayName=h;function y(t){let n=c(e+`CollectionConsumer`,t);return f.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:l,Slot:m,ItemSlot:v},y,r]}var h=`rovingFocusGroup.onEntryFocus`,g={bubbles:!1,cancelable:!0},_=`RovingFocusGroup`,[v,y,b]=m(_),[x,S]=o(_,[b]),[C,w]=x(_),T=f.forwardRef((e,t)=>(0,p.jsx)(v.Provider,{scope:e.__scopeRovingFocusGroup,children:(0,p.jsx)(v.Slot,{scope:e.__scopeRovingFocusGroup,children:(0,p.jsx)(E,{...e,ref:t})})}));T.displayName=_;var E=f.forwardRef((e,t)=>{let{__scopeRovingFocusGroup:n,orientation:i,loop:o=!1,dir:l,currentTabStopId:m,defaultCurrentTabStopId:v,onCurrentTabStopIdChange:b,onEntryFocus:x,preventScrollOnEntryFocus:S=!1,...w}=e,T=f.useRef(null),E=a(t,T),D=d(l),[O,k]=c({prop:m,defaultProp:v??null,onChange:b,caller:_}),[A,j]=f.useState(!1),N=u(x),P=y(n),F=f.useRef(!1),[I,L]=f.useState(0);return f.useEffect(()=>{let e=T.current;if(e)return e.addEventListener(h,N),()=>e.removeEventListener(h,N)},[N]),(0,p.jsx)(C,{scope:n,orientation:i,dir:D,loop:o,currentTabStopId:O,onItemFocus:f.useCallback(e=>k(e),[k]),onItemShiftTab:f.useCallback(()=>j(!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":i,...w,ref:E,style:{outline:`none`,...e.style},onMouseDown:s(e.onMouseDown,()=>{F.current=!0}),onFocus:s(e.onFocus,e=>{let t=!F.current;if(e.target===e.currentTarget&&t&&!A){let t=new CustomEvent(h,g);if(e.currentTarget.dispatchEvent(t),!t.defaultPrevented){let e=P().filter(e=>e.focusable);M([e.find(e=>e.active),e.find(e=>e.id===O),...e].filter(Boolean).map(e=>e.ref.current),S)}}F.current=!1}),onBlur:s(e.onBlur,()=>j(!1))})})}),D=`RovingFocusGroupItem`,O=f.forwardRef((e,t)=>{let{__scopeRovingFocusGroup:n,focusable:i=!0,active:a=!1,tabStopId:o,children:c,...u}=e,d=l(),m=o||d,h=w(D,n),g=h.currentTabStopId===m,_=y(n),{onFocusableItemAdd:b,onFocusableItemRemove:x,currentTabStopId:S}=h;return f.useEffect(()=>{if(i)return b(),()=>x()},[i,b,x]),(0,p.jsx)(v.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:s(e.onMouseDown,e=>{i?h.onItemFocus(m):e.preventDefault()}),onFocus:s(e.onFocus,()=>h.onItemFocus(m)),onKeyDown:s(e.onKeyDown,e=>{if(e.key===`Tab`&&e.shiftKey){h.onItemShiftTab();return}if(e.target!==e.currentTarget)return;let t=j(e,h.orientation,h.dir);if(t!==void 0){if(e.metaKey||e.ctrlKey||e.altKey||e.shiftKey)return;e.preventDefault();let n=_().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?N(n,r+1):n.slice(r+1)}setTimeout(()=>M(n))}}),children:typeof c==`function`?c({isCurrentTabStop:g,hasTabStop:S!=null}):c})})});O.displayName=D;var k={ArrowLeft:`prev`,ArrowUp:`prev`,ArrowRight:`next`,ArrowDown:`next`,PageUp:`first`,Home:`first`,PageDown:`last`,End:`last`};function A(e,t){return t===`rtl`?e===`ArrowLeft`?`ArrowRight`:e===`ArrowRight`?`ArrowLeft`:e:e}function j(e,t,n){let r=A(e.key,n);if(!(t===`vertical`&&[`ArrowLeft`,`ArrowRight`].includes(r))&&!(t===`horizontal`&&[`ArrowUp`,`ArrowDown`].includes(r)))return k[r]}function M(e,t=!1){let n=document.activeElement;for(let r of e)if(r===n||(r.focus({preventScroll:t}),document.activeElement!==n))return}function N(e,t){return e.map((n,r)=>e[(t+r)%e.length])}var P=T,F=O;export{m as i,P as n,S as r,F as t};
@@ -1 +1 @@
import{$u as e,Qu as t,ed as n,nd as r}from"./messages-GNZRoNCJ.js";import{c as i}from"./button-CgehfZOz.js";var a=n((t=>{var n=e();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=n.__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:``}t.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=a,t.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)},t.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()}},t.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))},t.prefetchDNS=function(e){typeof e==`string`&&a.d.D(e)},t.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})}},t.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)},t.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})}},t.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)},t.requestFormReset=function(e){a.d.r(e)},t.unstable_batchedUpdates=function(e,t){return e(t)},t.useFormState=function(e,t,n){return c.H.useFormState(e,t,n)},t.useFormStatus=function(){return c.H.useHostTransitionStatus()},t.version=`19.2.5`})),o=n(((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=r(e(),1),c=r(o(),1),l=t(),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{$d as e,Xd as t,Yd as n,Zd as r}from"./messages-kZy_2fG-.js";import{c as i}from"./button-CI1sqFDO.js";var a=r((e=>{var n=t();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=n.__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=r(((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(t(),1),c=e(o(),1),l=n(),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{$u as e,Qu as t,nd as n}from"./messages-GNZRoNCJ.js";var r=n(e(),1),i=t(),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{$d as e,Xd as t,Yd as n}from"./messages-kZy_2fG-.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{$u as e,Qu as t,nd as n}from"./messages-GNZRoNCJ.js";import{n as r,r as i,t as a}from"./dist-BBeIw6Mf.js";import{d as o}from"./button-CgehfZOz.js";import{n as s,r as c}from"./dist-BVB_XD7R.js";import{t as l}from"./dist-y2ssackP.js";var u=n(e(),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=t(),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=n(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{$d as e,Xd as t,Yd as n}from"./messages-kZy_2fG-.js";import{n as r,r as i,t as a}from"./dist-DTN-3dEq.js";import{d as o}from"./button-CI1sqFDO.js";import{n as s,r as c}from"./dist-e45zkog2.js";import{t as l}from"./dist-Dz_nSMrn.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};

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