From a8a3b8697f5b92d81948281dc2fcabe31ef57173 Mon Sep 17 00:00:00 2001 From: line-6000 <154492442+line-6000@users.noreply.github.com> Date: Mon, 11 May 2026 12:20:36 +0800 Subject: [PATCH 1/8] fix: print initial admin credentials to console on first bootstrap --- src/bootstrap/bootstrap.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/bootstrap/bootstrap.go b/src/bootstrap/bootstrap.go index 2b09723..88d0cac 100644 --- a/src/bootstrap/bootstrap.go +++ b/src/bootstrap/bootstrap.go @@ -38,14 +38,16 @@ func InitApp() { } // Seed admin account and JWT secret so the management console is // immediately usable on a fresh install. Both are idempotent. - _, isNew, err := data.EnsureDefaultAdmin() + initialPassword, isNew, err := data.EnsureDefaultAdmin() if err != nil { color.Red.Printf("[bootstrap] ensure default admin err=%s\n", err) } if isNew { color.Yellow.Println("╔════════════════════════════════════════════════════════════════════════╗") - color.Yellow.Println("║ Default admin account created. Fetch one-time password via API first!║") - color.Yellow.Printf("║ Username: admin ║\n") + color.Yellow.Println("║ Default admin account created. Save these credentials now. ║") + color.Yellow.Printf("║ Username: %-54s║\n", "admin") + color.Yellow.Printf("║ Password: %-54s║\n", initialPassword) + color.Yellow.Println("║ The one-time password API remains available until first fetch. ║") color.Yellow.Println("║ GET /admin/api/v1/auth/init-password (one-time) ║") color.Yellow.Println("╚════════════════════════════════════════════════════════════════════════╝") } From 3e0be9bb20db56eef31629ef8cc70ce4a2bf0008 Mon Sep 17 00:00:00 2001 From: line-6000 <154492442+line-6000@users.noreply.github.com> Date: Mon, 11 May 2026 12:45:18 +0800 Subject: [PATCH 2/8] docs: split README into English and Simplified Chinese versions --- README.en.md | 181 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 172 ++++----------------------------------------- README.zh-CN.md | 182 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 375 insertions(+), 160 deletions(-) create mode 100644 README.en.md create mode 100644 README.zh-CN.md diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..715e25b --- /dev/null +++ b/README.en.md @@ -0,0 +1,181 @@ +# Epusdt — Easy Payment USDT + +
+
+
+ Open-source multi-chain crypto payment gateway · Top real-world adoption +
+ + + + + + + +--- + +## What is Epusdt? + +**Epusdt** (Easy Payment USDT) is a self-hosted **multi-chain, multi-token crypto payment gateway** built with Go. It started as a TRC20-only solution and evolved into a broader **multi-chain receiving platform**, allowing websites and applications to accept crypto payments across multiple networks and token types. No third-party custody, no platform fees — funds go directly into your wallet. + +> **GitHub Star 3000+** · **10+ supported platform integrations** · **Top real-world adoption among crypto payment tools** + +Deploy it privately, integrate with the HTTP API, and start receiving crypto payments in minutes. + +### Supported Networks and Tokens + +| Network | Tokens | +|---------|--------| +| **TRC20** (Tron) | USDT, TRX | +| **ERC20** (Ethereum) | USDT, USDC, ETH | +| **Solana** | USDT, USDC | +| **BEP20** (BSC) | USDT, USDC, BNB | +| **Polygon** | USDT, USDC | +| **More** | Expanding continuously | + +> Actual support depends on the [latest release](https://github.com/GMWalletApp/epusdt/releases) and the [official docs](https://epusdt.com). + +--- + +## Broad Compatibility, Plug and Play + +No matter what kind of platform you operate, Epusdt can fit into existing payment integration patterns with **minimal or no business logic refactor**, giving you a fast path to crypto payments for global use cases: + +| Category | Supported Systems | +|----------|-------------------| +| **AI Distribution** | [OneAPI](https://github.com/songquanpeng/one-api), [NewAPI](https://github.com/QuantumNous/new-api) | +| **Card/Voucher Platforms** | [Dujiaoka](https://dujiao-next.com/), [ACG Faka](https://github.com/lizhipay/acg-faka) | +| **Proxy Panels** | [V2Board](https://github.com/v2board/v2board), [XBoard](https://github.com/cedar2025/Xboard), [xiaoV2board](https://github.com/wyx2685/v2board/), [SSPanel](https://github.com/anankke/sspanel-uim) | +| **Website Ecosystem** | [WordPress](https://wordpress.com/), [WHMCS](https://www.whmcs.com/) | +| **Epay-Compatible Flow** | Works with platforms already using Epay-style payment interfaces | +| **More** | Simple HTTP API, often integrated within 10 minutes | + +See more integrations and plugins here: [plugins/](./plugins/) + +--- + +## Core Features + +- **Multi-chain and multi-token** — TRC20, ERC20, BEP20, Polygon, and more +- **Self-hosted** — full control of funds and wallet infrastructure +- **Low-dependency runtime** — a single binary can run the service; low-traffic setups do not require MySQL + Redis +- **Cross-platform** — supports x86 / ARM on Windows / Linux / Mac +- **Multi-wallet rotation** — rotates receiving addresses to improve concurrency handling +- **Async callback queue** — handles merchant notifications efficiently +- **HTTP API** — easy integration from any language or framework +- **Telegram Bot** — instant payment notifications and lightweight operations + +--- + +## Documentation and Guides + +Full documentation: **[epusdt.com](https://epusdt.com)** + +Quick-start links: + +| Guide | Description | +|-------|-------------| +| [Docker Deployment](https://epusdt.com/guide/installation/docker) | Recommended one-command setup | +| [aaPanel Deployment](https://epusdt.com/guide/installation/aapanel) | Great for aaPanel users | +| [Manual Deployment](https://epusdt.com/guide/installation/manual.html) | Full manual control | +| [Developer API Docs](https://epusdt.com) | Integration reference | + +--- + +## Project Structure + +```text +Epusdt +├── plugins/ Integrated plugins and platform adapters +├── src/ Core project source code +├── sdk/ Integration SDKs +├── sql/ Database install / upgrade scripts +└── wiki/ Documentation assets and knowledge base +``` + +--- + +## How It Works + +Epusdt listens to supported blockchain APIs or RPC nodes across multiple networks such as TRC20, ERC20, BEP20, and Polygon. It captures incoming token transfers in real time and matches ownership using **amount differentiation** and **time-bounded locks**: + +```text +Workflow: +1. A customer initiates payment and needs to send 20.05 USDT +2. The system searches for an available wallet-address + amount combination +3. If address_1:20.05 is free -> lock it for 10 minutes and return it +4. If already occupied -> add 0.0001 and try the next amount combination +5. Background listeners keep watching wallet deposits and mark the order paid once the amount matches +``` + + + +--- + +## Community and Support + +If you hit an issue, please open a [GitHub Issue](https://github.com/GMWalletApp/epusdt/issues) first. That is the best way to get attention and help. + +| Channel | Link | +|---------|------| +| Telegram Channel | [https://t.me/epusdt](https://t.me/epusdt) | +| Telegram Group | [https://t.me/epusdt_group](https://t.me/epusdt_group) | +| Official Docs | [https://epusdt.com](https://epusdt.com) | + +--- + +## Star History + + ++ + Keywords: USDT Payment Gateway · Crypto Payment · Multi-chain Payment · TRC20 Payment · ERC20 Payment · BEP20 Payment · + Self-hosted Crypto Gateway · OneAPI Payment · NewAPI Payment · Dujiaoka Payment · ACG Faka Payment · + V2Board Payment · XBoard Payment · SSPanel Payment Interface · + WordPress Crypto Payment · WHMCS USDT Payment · Polygon USDT · + Epusdt · Easy Payment USDT · Open Source Payment Gateway · Multi-chain Collection + +
diff --git a/README.md b/README.md index 50a0405..a718a4a 100644 --- a/README.md +++ b/README.md @@ -1,177 +1,29 @@ -# Epusdt — Easy Payment USDT +# Epusdt
- 开源多链多币种 Crypto 支付网关 · 实际采用率 Top 1 + Open-source multi-chain crypto payment gateway
- - --- -## 🌍 What is Epusdt? +Choose your preferred language: -**Epusdt** (Easy Payment USDT) is a self-hosted **multi-chain, multi-token crypto payment gateway** built with Go. It has evolved from a TRC20-only solution into a comprehensive **multi-chain receiving platform**, enabling any website or application to accept crypto payments across multiple blockchain networks and token types. No third-party fees, no custodial risk — funds go directly into your wallet. +- [English Documentation](./README.en.md) +- [简体中文文档](./README.zh-CN.md) -> **⭐ GitHub Star 3000+** · **🔌 已支持站点解决方案 10+** · **🏆 Crypto 支付工具实际采用率 Top 1** +Quick links: -Deploy it privately, integrate via HTTP API, and start receiving **crypto payments** in minutes. That's it. 🎉 +- [Official Docs](https://epusdt.com) +- [Telegram Channel](https://t.me/epusdt) +- [Telegram Group](https://t.me/epusdt_group) +- [Plugins](./plugins/) -### 🔗 已支持网络与代币 - -| 网络 | 代币 | -|------|------| -| **TRC20** (Tron) | USDT、TRX| -| **ERC20** (Ethereum) | USDT、USDC、ETH | -| **Solana** | USDT、USDC | -| **BEP20** (BSC) | USDT、USDC、BNB | -| **Polygon** | USDT、USDC | -| **更多** | 持续扩展中… | - -> 💡 具体支持的链与代币以 [最新版本](https://github.com/GMWalletApp/epusdt/releases) 及 [官方文档](https://epusdt.com) 为准。 - ---- - -## 🔌 广泛兼容,即插即用 - -无论你运营的是哪类系统,Epusdt 均可基于现有接口方案,**无需重构业务逻辑**,快速接入,立即获得 Crypto 收款能力,低成本扩展全球支付场景: - -| 领域 | 已支持系统 | -|------|-----------| -| **AI 分发** | [OneAPI](https://github.com/songquanpeng/one-api)、[NewAPI](https://github.com/QuantumNous/new-api) | -| **发卡系统** | [独角数卡(Dujiaoka)](https://dujiao-next.com/)、[异次元发卡](https://github.com/lizhipay/acg-faka) | -| **代理面板** | [V2Board](https://github.com/v2board/v2board)、[XBoard](https://github.com/cedar2025/Xboard)、[xiaoV2board](https://github.com/wyx2685/v2board/)、[SSPanel](https://github.com/anankke/sspanel-uim) | -| **建站生态** | [WordPress](https://wordpress.com/)、[WHMCS](https://www.whmcs.com/) | -| **Epay兼容** | 兼容各类支持Epay易支付接口的平台 | -| **更多** | 简易HTTP API 10分钟内接入 | - -👉 查看更多集成列表与插件:[plugins/](plugins/) - ---- - -## ✨ 核心特性 - -- **多链多币种** — 支持 TRC20、ERC20、BEP20、Polygon 等主流网络,不再局限于单一链 -- **私有化部署** — 无需担心钱包被篡改、吞单,资金完全自主掌控 -- **零依赖运行** — 单个二进制文件即可启动,低并发场景无需安装 MySQL + Redis,一键部署零成本维护 -- **跨平台** — 支持 x86 / ARM 架构的 Windows / Linux / Mac 设备 -- **多钱包轮询** — 自动轮换收款地址,提高订单并发处理能力 -- **异步队列** — 高性能消息回调,优雅处理高并发场景 -- **HTTP API** — 标准化接口,任何语言 / 框架均可10min内集成 -- **Telegram Bot** — 实时支付通知,快捷管理与监控 - ---- - -## 📖 文档与教程 - -完整文档请访问 👉 **[epusdt.com](https://epusdt.com)** - -快速入门: - -| 教程 | 说明 | -|------|------| -| [Docker 部署](https://epusdt.com/guide/installation/docker) | 推荐方式,一键启动 | -| [宝塔面板部署](https://epusdt.com/guide/installation/aapanel) | 适合宝塔用户 | -| [手动部署](https://epusdt.com/guide/installation/manual.html) | 完全手动控制 | -| [开发者 API 文档](https://epusdt.com/zh/guide/integration/gmpay.html) | 接口集成指南 | - ---- - -## 🏗️ 项目结构 - -``` -Epusdt -├── plugins/ → 已集成的系统插件(独角数卡等) -├── src/ → 项目核心代码 -├── sdk/ → 接入 SDK -├── sql/ → 数据库安装 / 升级脚本 -└── wiki/ → 文档与知识库 -``` - ---- - -## 🔧 实现原理 - -Epusdt 通过监听多条区块链网络(TRC20、ERC20、BEP20、Polygon 等)的 API 或RPC节点,实时捕获钱包地址的代币入账事件,利用**金额差异**与**时效性**精确匹配交易归属: - -``` -工作流程: -1. 客户发起支付,需支付 20.05 USDT -2. 系统在哈希表中查找可用的 钱包地址 + 金额 组合 -3. 若 address_1:20.05 未被占用 → 锁定该组合(有效期 10 分钟),返回给客户 -4. 若已被占用 → 自动累加 0.0001 尝试下一个金额组合(最多 100 次) -5. 后台线程持续监听所有钱包的入账事件,金额匹配则确认支付成功 -``` - - - ---- - -## 💬 社区与支持 - -**遇到问题?** 请优先在 GitHub 提交 [Issue](https://github.com/GMWalletApp/epusdt/issues),我们会**优先处理** Issue 中的反馈。 - -加入 Telegram 社区,获取最新开发动态,参与需求调研,与超过5000名活跃用户交流使用经验,对接商业资源: - -| 渠道 | 链接 | -|------|------| -| 📢 **Epusdt 频道** | [https://t.me/epusdt](https://t.me/epusdt) | -| 💬 **Epusdt 交流群** | [https://t.me/epusdt_group](https://t.me/epusdt_group) | -| 📚 **官方文档站** | [https://epusdt.com](https://epusdt.com) | - ---- - -## Star History - - -- - Keywords: USDT Payment Gateway · Crypto Payment · Multi-chain Payment · TRC20 Payment · ERC20 Payment · BEP20 Payment · - Self-hosted Crypto Gateway · OneAPI Payment · NewAPI Payment · 独角数卡支付 · 异次元发卡支付方式 · - V2Board Payment · XBoard Payment · SSPanel 支付接口 · - WordPress Crypto Payment · WHMCS USDT Payment · Polygon USDT · - Epusdt · Easy Payment USDT · Open Source Payment Gateway · 多链收款 - -
diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..46c7c9b --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,182 @@ +# Epusdt — Easy Payment USDT + +
+
+
+ 开源多链多币种 Crypto 支付网关 · 实际采用率 Top 1 +
+ + + + + + + +--- + +## What is Epusdt? + +**Epusdt** (Easy Payment USDT) 是一个基于 Go 构建、支持私有化部署的 **多链多币种 Crypto 支付网关**。它从最初的 TRC20 单链方案逐步演进为完整的 **多链收款平台**,让任意网站或应用都能快速接入多条链、多种代币的加密支付能力。没有第三方托管,没有平台抽成,资金直接进入你的钱包。 + +> **GitHub Star 3000+** · **已支持站点解决方案 10+** · **Crypto 支付工具实际采用率 Top 1** + +私有部署,按 HTTP API 接入,几分钟内就可以开始接收 **Crypto Payments**。 + +### 已支持网络与代币 + +| 网络 | 代币 | +|------|------| +| **TRC20** (Tron) | USDT、TRX | +| **ERC20** (Ethereum) | USDT、USDC、ETH | +| **Solana** | USDT、USDC | +| **BEP20** (BSC) | USDT、USDC、BNB | +| **Polygon** | USDT、USDC | +| **更多** | 持续扩展中… | + +> 具体支持的链与代币以 [最新版本](https://github.com/GMWalletApp/epusdt/releases) 及 [官方文档](https://epusdt.com) 为准。 + +--- + +## 广泛兼容,即插即用 + +无论你运营的是哪类系统,Epusdt 均可基于现有接口方案,**无需重构业务逻辑**,快速接入,立即获得 Crypto 收款能力,低成本扩展全球支付场景: + +| 领域 | 已支持系统 | +|------|-----------| +| **AI 分发** | [OneAPI](https://github.com/songquanpeng/one-api)、[NewAPI](https://github.com/QuantumNous/new-api) | +| **发卡系统** | [独角数卡(Dujiaoka)](https://dujiao-next.com/)、[异次元发卡](https://github.com/lizhipay/acg-faka) | +| **代理面板** | [V2Board](https://github.com/v2board/v2board)、[XBoard](https://github.com/cedar2025/Xboard)、[xiaoV2board](https://github.com/wyx2685/v2board/)、[SSPanel](https://github.com/anankke/sspanel-uim) | +| **建站生态** | [WordPress](https://wordpress.com/)、[WHMCS](https://www.whmcs.com/) | +| **Epay 兼容** | 兼容各类支持 Epay 易支付接口的平台 | +| **更多** | 简易 HTTP API,10 分钟内接入 | + +查看更多集成列表与插件:[plugins/](./plugins/) + +--- + +## 核心特性 + +- **多链多币种** — 支持 TRC20、ERC20、BEP20、Polygon 等主流网络 +- **私有化部署** — 资金完全自主掌控 +- **零依赖运行** — 单个二进制即可启动,低并发场景无需 MySQL + Redis +- **跨平台** — 支持 x86 / ARM 架构的 Windows / Linux / Mac +- **多钱包轮询** — 自动轮换收款地址,提高并发处理能力 +- **异步队列** — 高性能消息回调,适配高并发场景 +- **HTTP API** — 标准化接口,任何语言 / 框架都能快速集成 +- **Telegram Bot** — 实时支付通知,快捷管理与监控 + +--- + +## 文档与教程 + +完整文档请访问:**[epusdt.com](https://epusdt.com)** + +快速入门: + +| 教程 | 说明 | +|------|------| +| [Docker 部署](https://epusdt.com/guide/installation/docker) | 推荐方式,一键启动 | +| [宝塔面板部署](https://epusdt.com/guide/installation/aapanel) | 适合宝塔用户 | +| [手动部署](https://epusdt.com/guide/installation/manual.html) | 完全手动控制 | +| [开发者 API 文档](https://epusdt.com/zh/guide/integration/gmpay.html) | 接口集成指南 | + +--- + +## 项目结构 + +```text +Epusdt +├── plugins/ 已集成的系统插件(独角数卡等) +├── src/ 项目核心代码 +├── sdk/ 接入 SDK +├── sql/ 数据库安装 / 升级脚本 +└── wiki/ 文档与知识库 +``` + +--- + +## 实现原理 + +Epusdt 通过监听多条区块链网络(TRC20、ERC20、BEP20、Polygon 等)的 API 或 RPC 节点,实时捕获钱包地址的代币入账事件,利用**金额差异**与**时效性**精确匹配交易归属: + +```text +工作流程: +1. 客户发起支付,需支付 20.05 USDT +2. 系统在哈希表中查找可用的钱包地址 + 金额组合 +3. 若 address_1:20.05 未被占用 -> 锁定该组合(有效期 10 分钟),返回给客户 +4. 若已被占用 -> 自动累加 0.0001 尝试下一个金额组合(最多 100 次) +5. 后台线程持续监听所有钱包的入账事件,金额匹配则确认支付成功 +``` + + + +--- + +## 社区与支持 + +**遇到问题?** 请优先在 GitHub 提交 [Issue](https://github.com/GMWalletApp/epusdt/issues),我们会优先处理反馈。 + +| 渠道 | 链接 | +|------|------| +| Epusdt 频道 | [https://t.me/epusdt](https://t.me/epusdt) | +| Epusdt 交流群 | [https://t.me/epusdt_group](https://t.me/epusdt_group) | +| 官方文档站 | [https://epusdt.com](https://epusdt.com) | + +--- + +## Star History + + ++ + Keywords: USDT Payment Gateway · Crypto Payment · Multi-chain Payment · TRC20 Payment · ERC20 Payment · BEP20 Payment · + Self-hosted Crypto Gateway · OneAPI Payment · NewAPI Payment · 独角数卡支付 · 异次元发卡支付方式 · + V2Board Payment · XBoard Payment · SSPanel 支付接口 · + WordPress Crypto Payment · WHMCS USDT Payment · Polygon USDT · + Epusdt · Easy Payment USDT · Open Source Payment Gateway · 多链收款 + +
+ From 7c9e3e123e5c8ccb77cd78a4db9f6a96d6d20bb5 Mon Sep 17 00:00:00 2001 From: line-6000 <154492442+line-6000@users.noreply.github.com> Date: Mon, 11 May 2026 13:16:42 +0800 Subject: [PATCH 3/8] fix: (update tron logic) --- src/crypto/tron.go | 23 ++++++ src/model/service/task_service.go | 43 +++++++---- src/task/listen_trc20_block.go | 109 +++++++++++++++++++++------- src/task/listen_trc20_block_test.go | 23 ++++++ 4 files changed, 160 insertions(+), 38 deletions(-) create mode 100644 src/task/listen_trc20_block_test.go diff --git a/src/crypto/tron.go b/src/crypto/tron.go index abf36f3..cdc2d18 100644 --- a/src/crypto/tron.go +++ b/src/crypto/tron.go @@ -1,7 +1,9 @@ package crypto import ( + "bytes" "crypto/sha256" + "fmt" "github.com/shengdoushi/base58" ) @@ -28,3 +30,24 @@ func EncodeCheck(input []byte) string { return Encode(inputCheck) } + +// DecodeCheck decodes a Base58Check string and validates its 4-byte checksum. +func DecodeCheck(input string) ([]byte, error) { + raw, err := base58.Decode(input, base58.BitcoinAlphabet) + if err != nil { + return nil, err + } + if len(raw) < 4 { + return nil, fmt.Errorf("base58check payload too short") + } + + payload := raw[:len(raw)-4] + checksum := raw[len(raw)-4:] + + h0 := sha256.Sum256(payload) + h1 := sha256.Sum256(h0[:]) + if !bytes.Equal(checksum, h1[:4]) { + return nil, fmt.Errorf("base58check checksum mismatch") + } + return payload, nil +} diff --git a/src/model/service/task_service.go b/src/model/service/task_service.go index 6c15789..9e24043 100644 --- a/src/model/service/task_service.go +++ b/src/model/service/task_service.go @@ -36,47 +36,64 @@ func ResolveTronNode() (string, string, error) { return resolveTronNode() } -func TryProcessTronTRC20Transfer(toAddr string, rawValue *big.Int, txHash string, blockTsMs int64) { +func TryProcessTronTRC20Transfer(token mdb.ChainToken, toAddr string, rawValue *big.Int, txHash string, blockTsMs int64) { + tokenSym := strings.ToUpper(strings.TrimSpace(token.Symbol)) defer func() { if err := recover(); err != nil { - log.Sugar.Errorf("[TRC20][%s] TryProcessTronTRC20Transfer panic: %v", toAddr, err) + log.Sugar.Errorf("[TRC20-%s][%s] TryProcessTronTRC20Transfer panic: %v", tokenSym, toAddr, err) } }() addr := strings.TrimSpace(toAddr) - if addr == "" || rawValue == nil || rawValue.Sign() <= 0 { + if tokenSym == "" || addr == "" || rawValue == nil || rawValue.Sign() <= 0 { return } + decimals := token.Decimals + if decimals < 0 { + decimals = 0 + } decimalQuant := decimal.NewFromBigInt(rawValue, 0) - amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1_000_000)).InexactFloat64(), 2) + amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.New(1, int32(decimals))).InexactFloat64(), 2) if amount <= 0 { return } + if token.MinAmount > 0 && amount < token.MinAmount { + log.Sugar.Debugf("[TRC20-%s][%s] skip below min amount hash=%s amount=%.2f min=%.2f", tokenSym, addr, txHash, amount, token.MinAmount) + return + } - tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, addr, "USDT", amount) + tradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, addr, tokenSym, amount) if err != nil { - log.Sugar.Warnf("[TRC20][%s] lock lookup: %v", addr, err) + log.Sugar.Warnf("[TRC20-%s][%s] lock lookup: %v", tokenSym, addr, err) return } if tradeID == "" { - log.Sugar.Debugf("[TRC20][%s] skip unmatched tx hash=%s amount=%.2f", addr, txHash, amount) + log.Sugar.Debugf("[TRC20-%s][%s] skip unmatched tx hash=%s amount=%.2f", tokenSym, addr, txHash, amount) return } order, err := data.GetOrderInfoByTradeId(tradeID) if err != nil { - log.Sugar.Warnf("[TRC20][%s] load order: %v", addr, err) + log.Sugar.Warnf("[TRC20-%s][%s] load order: %v", tokenSym, addr, err) + return + } + if strings.ToLower(strings.TrimSpace(order.Network)) != mdb.NetworkTron { + log.Sugar.Warnf("[TRC20-%s][%s] skip trade_id=%s network=%q", tokenSym, addr, tradeID, order.Network) + return + } + if strings.ToUpper(strings.TrimSpace(order.Token)) != tokenSym { + log.Sugar.Warnf("[TRC20-%s][%s] skip trade_id=%s token mismatch order=%s", tokenSym, addr, tradeID, order.Token) 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()) + log.Sugar.Warnf("[TRC20-%s][%s] skip tx %s because block time %d is before order create time %d", tokenSym, addr, txHash, blockTsMs, order.CreatedAt.TimestampMilli()) return } req := &request.OrderProcessingRequest{ ReceiveAddress: addr, - Token: "USDT", + Token: tokenSym, Network: mdb.NetworkTron, TradeId: tradeID, Amount: amount, @@ -85,15 +102,15 @@ func TryProcessTronTRC20Transfer(toAddr string, rawValue *big.Int, txHash string 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) + log.Sugar.Infof("[TRC20-%s][%s] skip resolved transfer trade_id=%s hash=%s err=%v", tokenSym, addr, tradeID, txHash, err) return } - log.Sugar.Errorf("[TRC20][%s] OrderProcessing trade_id=%s hash=%s: %v", addr, tradeID, txHash, err) + log.Sugar.Errorf("[TRC20-%s][%s] OrderProcessing trade_id=%s hash=%s: %v", tokenSym, addr, tradeID, txHash, err) return } sendPaymentNotification(order) - log.Sugar.Infof("[TRC20][%s] payment processed trade_id=%s hash=%s", addr, tradeID, txHash) + log.Sugar.Infof("[TRC20-%s][%s] payment processed trade_id=%s hash=%s", tokenSym, addr, tradeID, txHash) } func TryProcessTronTRXTransfer(toAddr string, rawSun int64, txHash string, blockTsMs int64) { diff --git a/src/task/listen_trc20_block.go b/src/task/listen_trc20_block.go index dea5509..0cac2df 100644 --- a/src/task/listen_trc20_block.go +++ b/src/task/listen_trc20_block.go @@ -12,13 +12,13 @@ import ( "time" tron "github.com/GMWalletApp/epusdt/crypto" + "github.com/GMWalletApp/epusdt/model/data" + "github.com/GMWalletApp/epusdt/model/mdb" "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" @@ -46,6 +46,31 @@ func HexToTronAddress(hexAddr string) (string, error) { return tron.EncodeCheck(raw), nil } +func TronAddressToHex(addr string) (string, error) { + addr = strings.TrimSpace(addr) + if addr == "" { + return "", fmt.Errorf("地址为空") + } + raw, err := tron.DecodeCheck(addr) + if err != nil { + return "", err + } + if len(raw) != 21 { + return "", fmt.Errorf("地址长度非法: %d bytes", len(raw)) + } + if raw[0] != 0x41 { + return "", fmt.Errorf("非法 TRON 主网地址前缀: 0x%x", raw[0]) + } + return strings.ToLower(hex.EncodeToString(raw)), nil +} + +func normalizeTronHexAddress(hexAddr string) string { + hexAddr = strings.TrimSpace(hexAddr) + hexAddr = strings.TrimPrefix(hexAddr, "0x") + hexAddr = strings.TrimPrefix(hexAddr, "0X") + return strings.ToLower(hexAddr) +} + type BlockHeader struct { RawData struct { Number int64 `json:"number"` @@ -89,12 +114,13 @@ type Block struct { Transactions []Transaction `json:"transactions"` } -type USDTTransfer struct { +type TRC20Transfer struct { TxID string From string To string - Raw *big.Int // 原始数值(6 decimals) + Raw *big.Int Status string + Token mdb.ChainToken } type TRXTransfer struct { @@ -156,7 +182,33 @@ func GetBlockByNum(baseURL string, apiKey string, num int64) (*Block, error) { return &block, json.Unmarshal(b, &block) } -func parseUSDTTransfer(tx Transaction) *USDTTransfer { +func loadTronTRC20TokenMap() map[string]mdb.ChainToken { + tokens, err := data.ListEnabledChainTokensByNetwork(mdb.NetworkTron) + if err != nil { + log.Sugar.Warnf("[TRON-BLOCK] load chain_tokens: %v", err) + return nil + } + tokenMap := make(map[string]mdb.ChainToken) + for _, token := range tokens { + symbol := strings.ToUpper(strings.TrimSpace(token.Symbol)) + contractAddress := strings.TrimSpace(token.ContractAddress) + if symbol == "TRX" || contractAddress == "" { + continue + } + contractHex, err := TronAddressToHex(contractAddress) + if err != nil { + log.Sugar.Warnf("[TRON-BLOCK] skip invalid TRC20 contract symbol=%s address=%s err=%v", symbol, contractAddress, err) + continue + } + tokenMap[contractHex] = token + } + return tokenMap +} + +func parseTRC20Transfer(tx Transaction, tokenMap map[string]mdb.ChainToken) *TRC20Transfer { + if len(tokenMap) == 0 { + return nil + } if len(tx.RawData.Contract) == 0 { return nil } @@ -170,9 +222,9 @@ func parseUSDTTransfer(tx Transaction) *USDTTransfer { return nil } - // 检查是否是 USDT 合约 - contractHex := strings.ToLower(strings.TrimPrefix(val.ContractAddress, "0x")) - if contractHex != USDTContractHex { + contractHex := normalizeTronHexAddress(val.ContractAddress) + token, ok := tokenMap[contractHex] + if !ok { return nil } @@ -213,12 +265,13 @@ func parseUSDTTransfer(tx Transaction) *USDTTransfer { status = tx.Ret[0].ContractRet } - return &USDTTransfer{ + return &TRC20Transfer{ TxID: tx.TxID, From: fromAddr, To: toAddr, Raw: amountBig, Status: status, + Token: token, } } @@ -262,14 +315,14 @@ func parseTRXTransfer(tx Transaction) *TRXTransfer { } } -func processBlock(block *Block) { +func processBlock(block *Block, tokenMap map[string]mdb.ChainToken) { blockTsMs := block.BlockHeader.RawData.Timestamp for _, tx := range block.Transactions { - if t := parseUSDTTransfer(tx); t != nil { + if t := parseTRC20Transfer(tx, tokenMap); t != nil { if t.Status != "SUCCESS" { continue } - service.TryProcessTronTRC20Transfer(t.To, t.Raw, t.TxID, blockTsMs) + service.TryProcessTronTRC20Transfer(t.Token, t.To, t.Raw, t.TxID, blockTsMs) continue } if t := parseTRXTransfer(tx); t != nil { @@ -286,9 +339,9 @@ type Scanner struct { apiKey string lastBlock int64 // 统计 - totalBlocks int64 - totalUSDTTxs int64 - totalTRXTxs int64 + totalBlocks int64 + totalTRC20Txs int64 + totalTRXTxs int64 } func NewScanner() *Scanner { @@ -314,7 +367,7 @@ func (s *Scanner) Init() error { } func (s *Scanner) Run() { - log.Sugar.Info("[TRON-BLOCK] start scanning (USDT TRC20 + TRX)") + log.Sugar.Info("[TRON-BLOCK] start scanning (TRC20 + TRX)") ticker := time.NewTicker(PollInterval) defer ticker.Stop() @@ -324,7 +377,7 @@ func (s *Scanner) Run() { for { select { case <-statTicker.C: - log.Sugar.Infof("[TRON-BLOCK] stats blocks=%d usdt=%d trx=%d", s.totalBlocks, s.totalUSDTTxs, s.totalTRXTxs) + log.Sugar.Infof("[TRON-BLOCK] stats blocks=%d trc20=%d trx=%d", s.totalBlocks, s.totalTRC20Txs, s.totalTRXTxs) case <-ticker.C: s.poll() } @@ -342,6 +395,7 @@ func (s *Scanner) poll() { return } + tokenMap := loadTronTRC20TokenMap() for num := s.lastBlock + 1; num <= latestNum; num++ { var block *Block if num == latestNum { @@ -354,13 +408,13 @@ func (s *Scanner) poll() { } time.Sleep(200 * time.Millisecond) } - processBlock(block) + processBlock(block, tokenMap) s.lastBlock = num s.totalBlocks++ for _, tx := range block.Transactions { - if parseUSDTTransfer(tx) != nil { - s.totalUSDTTxs++ + if parseTRC20Transfer(tx, tokenMap) != nil { + s.totalTRC20Txs++ } else if parseTRXTransfer(tx) != nil { s.totalTRXTxs++ } @@ -369,10 +423,15 @@ func (s *Scanner) poll() { } func StartTronBlockScannerListener() { - scanner := NewScanner() - if err := scanner.Init(); err != nil { - log.Sugar.Errorf("[TRON-BLOCK] init: %v", err) - return + for { + scanner := NewScanner() + if err := scanner.Init(); err != nil { + log.Sugar.Errorf("[TRON-BLOCK] init: %v, retrying...", err) + time.Sleep(10 * time.Second) + continue + } + scanner.Run() + log.Sugar.Warn("[TRON-BLOCK] scanner stopped, restarting...") + time.Sleep(3 * time.Second) } - scanner.Run() } diff --git a/src/task/listen_trc20_block_test.go b/src/task/listen_trc20_block_test.go new file mode 100644 index 0000000..6b7dee9 --- /dev/null +++ b/src/task/listen_trc20_block_test.go @@ -0,0 +1,23 @@ +package task + +import "testing" + +func TestTronAddressToHexConvertsBase58Contract(t *testing.T) { + got, err := TronAddressToHex("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t") + if err != nil { + t.Fatalf("TronAddressToHex(): %v", err) + } + + const want = "41a614f803b6fd780986a42c78ec9c7f77e6ded13c" + if got != want { + t.Fatalf("TronAddressToHex() = %q, want %q", got, want) + } +} + +func TestNormalizeTronHexAddress(t *testing.T) { + got := normalizeTronHexAddress("0X41A614F803B6FD780986A42C78EC9C7F77E6DED13C") + const want = "41a614f803b6fd780986a42c78ec9c7f77e6ded13c" + if got != want { + t.Fatalf("normalizeTronHexAddress() = %q, want %q", got, want) + } +} From 84972ea0f0a0150e59baaf216d31218caae56694 Mon Sep 17 00:00:00 2001 From: line-6000 <154492442+line-6000@users.noreply.github.com> Date: Mon, 11 May 2026 13:40:07 +0800 Subject: [PATCH 4/8] fix: resolve chain tokens dynamically in block listeners - resolve EVM ERC20 tokens by chain_tokens contract_address instead of hardcoded contracts --- src/model/service/task_service.go | 73 ++++++++------------------ src/model/service/task_service_test.go | 67 +++++++++++++++++++++++ 2 files changed, 89 insertions(+), 51 deletions(-) diff --git a/src/model/service/task_service.go b/src/model/service/task_service.go index 9e24043..8b94b51 100644 --- a/src/model/service/task_service.go +++ b/src/model/service/task_service.go @@ -195,64 +195,31 @@ func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, to } }() - var usdt, usdc common.Address - var polygonUsdcE common.Address - switch chainNetwork { - 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 - } - - 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) + tokenConfig, err := data.GetEnabledChainTokenByContract(chainNetwork, contract.Hex()) + if err != nil { + log.Sugar.Warnf("[%s-WS] load chain token contract=%s: %v", net, contract.Hex(), err) + return + } + if tokenConfig == nil || tokenConfig.ID == 0 { + log.Sugar.Warnf("[%s-WS] skip unconfigured contract %s", net, contract.Hex()) + return + } + tokenSym := strings.ToUpper(strings.TrimSpace(tokenConfig.Symbol)) + if tokenSym == "" { + log.Sugar.Warnf("[%s-WS] skip contract %s with empty token symbol", net, contract.Hex()) + return + } walletAddr := strings.ToLower(toAddr.Hex()) if rawValue == nil || rawValue.Sign() <= 0 { log.Sugar.Infof("[%s-%s][%s] skip non-positive or nil amount", net, tokenSym, walletAddr) return } - - chainTokens, err := data.ListChainTokens(chainNetwork) - if err != nil { - log.Sugar.Warnf("[%s-%s] load chain tokens: %v", net, tokenSym, err) - return + decimals := tokenConfig.Decimals + if decimals < 0 { + decimals = 0 } - 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()) + pow := decimal.New(1, int32(decimals)) decimalQuant := decimal.NewFromBigInt(rawValue, 0) amount := math.MustParsePrecFloat64(decimalQuant.Div(pow).InexactFloat64(), 2) @@ -260,6 +227,10 @@ func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, to log.Sugar.Warnf("[%s-%s][%s] skip non-positive amount %.2f", net, tokenSym, walletAddr, amount) return } + if tokenConfig.MinAmount > 0 && amount < tokenConfig.MinAmount { + log.Sugar.Debugf("[%s-%s][%s] skip below min amount hash=%s amount=%.2f min=%.2f", net, tokenSym, walletAddr, txHash, amount, tokenConfig.MinAmount) + return + } log.Sugar.Debugf("[%s-%s][%s] processing transfer hash=%s amount=%.2f", net, tokenSym, walletAddr, txHash, amount) diff --git a/src/model/service/task_service_test.go b/src/model/service/task_service_test.go index 6a2134a..5d0e80d 100644 --- a/src/model/service/task_service_test.go +++ b/src/model/service/task_service_test.go @@ -1,14 +1,17 @@ package service import ( + "math/big" "strings" "testing" "time" "github.com/GMWalletApp/epusdt/internal/testutil" "github.com/GMWalletApp/epusdt/model/dao" + "github.com/GMWalletApp/epusdt/model/data" "github.com/GMWalletApp/epusdt/model/mdb" "github.com/GMWalletApp/epusdt/notify" + "github.com/ethereum/go-ethereum/common" ) func TestSendPaymentNotificationUsesLatestOrderUpdatedAt(t *testing.T) { @@ -77,3 +80,67 @@ func TestSendPaymentNotificationUsesLatestOrderUpdatedAt(t *testing.T) { t.Fatal("timed out waiting for notification") } } + +func TestTryProcessEvmERC20TransferUsesChainTokenContract(t *testing.T) { + cleanup := testutil.SetupTestDatabases(t) + defer cleanup() + + const ( + tradeID = "T202605110001" + orderID = "ORD202605110001" + tokenSym = "FOO" + amount = 12.34 + ) + contract := common.HexToAddress("0x1111111111111111111111111111111111111111") + receiveAddress := common.HexToAddress("0x2222222222222222222222222222222222222222") + + if err := dao.Mdb.Create(&mdb.ChainToken{ + Network: mdb.NetworkEthereum, + Symbol: tokenSym, + ContractAddress: contract.Hex(), + Decimals: 8, + Enabled: true, + }).Error; err != nil { + t.Fatalf("seed chain token: %v", err) + } + + order := &mdb.Orders{ + TradeId: tradeID, + OrderId: orderID, + Amount: 100, + Currency: "CNY", + ActualAmount: amount, + Token: tokenSym, + Network: mdb.NetworkEthereum, + ReceiveAddress: strings.ToLower(receiveAddress.Hex()), + Status: mdb.StatusWaitPay, + } + if err := dao.Mdb.Create(order).Error; err != nil { + t.Fatalf("seed order: %v", err) + } + if err := data.LockTransaction(mdb.NetworkEthereum, order.ReceiveAddress, order.Token, order.TradeId, order.ActualAmount, time.Hour); err != nil { + t.Fatalf("lock transaction: %v", err) + } + + rawValue := big.NewInt(1_234_000_000) // 12.34 with 8 decimals + TryProcessEvmERC20Transfer(mdb.NetworkEthereum, contract, receiveAddress, rawValue, "0xfoo-hash", time.Now().UnixMilli()) + + paid, err := data.GetOrderInfoByTradeId(tradeID) + if err != nil { + t.Fatalf("load paid order: %v", err) + } + if paid.Status != mdb.StatusPaySuccess { + t.Fatalf("order status = %d, want %d", paid.Status, mdb.StatusPaySuccess) + } + if paid.BlockTransactionId != "0xfoo-hash" { + t.Fatalf("block transaction id = %q, want %q", paid.BlockTransactionId, "0xfoo-hash") + } + + lockTradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkEthereum, receiveAddress.Hex(), tokenSym, amount) + if err != nil { + t.Fatalf("lookup released lock: %v", err) + } + if lockTradeID != "" { + t.Fatalf("runtime lock still exists for trade_id=%q", lockTradeID) + } +} From 1c805ed061d6b5f082ae4f898ac2aac3beef2922 Mon Sep 17 00:00:00 2001 From: line-6000 <154492442+line-6000@users.noreply.github.com> Date: Mon, 11 May 2026 16:03:32 +0800 Subject: [PATCH 5/8] feat: support background_color background_image_url --- src/controller/admin/settings_controller.go | 4 +- .../comm/supported_asset_controller.go | 10 ++- src/model/data/settings_data.go | 8 ++ src/model/mdb/settings.go | 2 + src/model/response/support_response.go | 10 ++- src/route/admin_router_test.go | 24 ++++-- src/route/router_test.go | 12 +++ src/www/assets/401-DqXW4OqI.js | 1 + src/www/assets/401-sRzu7TEC.js | 1 - src/www/assets/403-CvkLNZh5.js | 1 - src/www/assets/403-O20TZ6yp.js | 1 + src/www/assets/404-CJVd46Ij.js | 1 + src/www/assets/404-DFYtFuXs.js | 1 - src/www/assets/500-C16mruGe.js | 1 - src/www/assets/500-m16UXgTh.js | 1 + src/www/assets/503-CYbyYdpL.js | 1 + src/www/assets/503-Dpo0f61r.js | 1 - ...nly-CKH5SArO.js => ClientOnly-CsPcFrh_.js} | 2 +- .../{Match-B0WegrOo.js => Match-QXywLUYo.js} | 2 +- ...atches-BXZBcz7O.js => Matches-DbrSCo1X.js} | 2 +- src/www/assets/_error-CEhiecHC.js | 1 - src/www/assets/_error-CtApCCLO.js | 2 + src/www/assets/_error-DnygZm1m.js | 1 + src/www/assets/_error-DwHJp4LF.js | 2 - src/www/assets/_trade_id-DJoAcotW.js | 1 - src/www/assets/_trade_id-DWezhsZd.js | 2 + src/www/assets/_trade_id-c2PR7KoD.js | 1 + src/www/assets/_trade_id-fAQORKAU.js | 2 - src/www/assets/addresses-BKAykCaO.js | 3 - src/www/assets/addresses-DofW5ea6.js | 3 + ...nce-C6CweejF.js => appearance-qof1Ceqk.js} | 2 +- ...eft-CjjQKxui.js => arrow-left-DUY_wbdK.js} | 2 +- ....js => auth-animation-context-CApZ-3Mm.js} | 2 +- ...ore-B7YG314a.js => auth-store-DjM0ORVQ.js} | 2 +- .../{badge-CKj7AThQ.js => badge-Cc5lH1Q8.js} | 2 +- ...{button-CI1sqFDO.js => button-B60XXJe-.js} | 2 +- .../{card-D1KNrKfr.js => card-C8dA7TUs.js} | 2 +- src/www/assets/chains-CCDNqXpb.js | 1 + src/www/assets/chains-C_mLrLHU.js | 1 - ...e-Do-NE2MF.js => chains-store-zSK41IlU.js} | 2 +- src/www/assets/check-BArzkFik.js | 1 + src/www/assets/check-jWXQT0HB.js | 1 - ...ckbox-Buso7MQb.js => checkbox-BzjV1jIh.js} | 2 +- ...n-CFeWMPkU.js => chevron-down-CdqbdsrC.js} | 2 +- ...EkSPwJ.js => chevrons-up-down-DLRsY497.js} | 2 +- src/www/assets/circle-check-XGTPrS6o.js | 1 - ...lock-3-wrSifNdF.js => clock-3-CzKYCITb.js} | 2 +- src/www/assets/coming-soon-dialog-DJFqf5r9.js | 1 + src/www/assets/coming-soon-dialog-WvGFRUST.js | 1 - ...ommand-St1f4L7D.js => command-Bh7MlPL5.js} | 2 +- ...N7FIzDUo.js => confirm-dialog-C_1PQXUD.js} | 4 +- ...Amuxz.js => copy-to-clipboard-BA9d7uOy.js} | 2 +- ...kuOIuw.js => createLucideIcon-BYirCMks.js} | 2 +- src/www/assets/dashboard-BSDIqTN-.js | 42 ++++++++++ src/www/assets/dashboard-jkQFKOjv.js | 42 ---------- ...ble-DckmiBCR.js => data-table-BQmAnSac.js} | 4 +- ...isplay-D6xq_oLS.js => display-DCuHnl3i.js} | 2 +- .../{dist-HFEBkKCd.js => dist--hVmcUxR.js} | 2 +- .../{dist-JMRFUguJ.js => dist-0ifVQ567.js} | 2 +- .../{dist-BjEfRG8A.js => dist-B8uWn8Nr.js} | 2 +- .../{dist-BB2cKiV1.js => dist-BCJRbhCs.js} | 2 +- .../{dist-A3SZ_duZ.js => dist-CfXybCHU.js} | 2 +- .../{dist-Dh5ge3Zd.js => dist-ChY8My-s.js} | 2 +- .../{dist-u5B8Q0Qw.js => dist-CqB9Ke6B.js} | 2 +- .../{dist-vhc0iAwK.js => dist-CvxPG4A5.js} | 2 +- .../{dist-e45zkog2.js => dist-DVIXAmXm.js} | 2 +- .../{dist-8VtOfdBX.js => dist-Gcp12U8O.js} | 2 +- .../{dist-Dz_nSMrn.js => dist-H7iGCGhd.js} | 2 +- .../{dist-DTN-3dEq.js => dist-bLadKS9Y.js} | 2 +- .../{dist-Cc5kEhoc.js => dist-hdsj8hLM.js} | 2 +- .../{dist-D_VX1DQ0.js => dist-ryF24_UT.js} | 2 +- ...nload-BS2DlH4c.js => download-DUoo_uFp.js} | 2 +- ...-CPJ_eMIt.js => dropdown-menu-DPUyjJFn.js} | 2 +- ...ipsis-BTvNQjRv.js => ellipsis-pbmIhRD0.js} | 2 +- ...te-Q56LDatF.js => empty-state-oPcnyRNf.js} | 2 +- src/www/assets/epay-BGa_2t41.js | 1 - src/www/assets/epay-CAjYukDz.js | 1 + ...{es2015-DJNqm-x3.js => es2015-Ckkq3MV2.js} | 2 +- .../{eye-CdR5ptdv.js => eye-AS0khnpZ.js} | 2 +- ...ye-off-CoMc8-kP.js => eye-off-BDzClwT_.js} | 2 +- ...-BWVtH5eQ.js => font-provider-aU50-jt9.js} | 2 +- ...dden-oyIuUuBn.js => forbidden-B5vn65Go.js} | 2 +- .../{form-Cb-bizIH.js => form-BBW8C8bl.js} | 2 +- src/www/assets/general-error-BryI8zNx.js | 1 - src/www/assets/general-error-wYLXT3cx.js | 1 + ...{header-sQjuQN7h.js => header-C3Kjo9mr.js} | 2 +- .../{help-CKbNVwIA.js => help-Cz5Aw4C0.js} | 2 +- .../{index-C0cm7RJv.js => index-BM5iUQtk.js} | 6 +- src/www/assets/index-D4E-D57l.css | 2 - src/www/assets/index-a99oFdbe.css | 2 + .../{input-Bhusz9zP.js => input-Z_0A8Pmk.js} | 2 +- src/www/assets/install-BHxgXpyy.js | 1 - src/www/assets/install-CP-LleT8.js | 1 + ...s-CO1LUV52.js => integrations-BmMU3vfc.js} | 2 +- src/www/assets/keys-DEv2pxLG.js | 2 - src/www/assets/keys-DgEgzpmW.js | 2 + .../{label-B6I7r7Hn.js => label-hTeA0dtz.js} | 2 +- .../{lib-bV16CnBP.js => lib-D5a_8E1C.js} | 2 +- .../{link-Dw1SMkr2.js => link-BaS6brMf.js} | 2 +- ...-CmKlBt5c.js => loader-circle-BEQOp5F6.js} | 2 +- src/www/assets/logo-DxhHOFqN.js | 1 - src/www/assets/logo-v1DcqW1s.js | 1 + ...text-WZH5brmn.js => long-text-MoJ2CH4i.js} | 2 +- .../{main-CEEtgVzp.js => main-DnAi5_9Q.js} | 2 +- ...zFgNM.js => maintenance-error-b6nEmzx5.js} | 2 +- ...t-Cxt9JPh-.js => matchContext-U_Lz2mXl.js} | 2 +- src/www/assets/messages-hIvVqckc.js | 1 + src/www/assets/messages-kZy_2fG-.js | 1 - src/www/assets/not-found-error-DssGa61q.js | 1 + src/www/assets/not-found-error-mipAgOS5.js | 1 - ...-D0jCBzjr.js => notifications-DRQh8N_S.js} | 2 +- src/www/assets/okpay-CNTHFJoz.js | 1 - src/www/assets/okpay-CvsnKwR_.js | 1 + src/www/assets/orders-BtA_Wrva.js | 1 - src/www/assets/orders-CtEKYVyJ.js | 1 + ...er-Bto-ujL-.js => page-header-DBzs8fNp.js} | 2 +- src/www/assets/password-BD550HR6.js | 1 + src/www/assets/password-CO8PaL5D.js | 1 - ...DV9gFRez.js => password-input-edLe6Lap.js} | 2 +- ...-CAbn0mjw.js => payment-setup-ALul9XNo.js} | 2 +- ...opover-CtzAjxka.js => popover-B-Zr22hG.js} | 2 +- ...CGevTkrx.js => preload-helper-L_hOzau-.js} | 2 +- ...up-ceeciZec.js => radio-group-whAqa1zt.js} | 2 +- src/www/assets/rate-CefVARKL.js | 1 + src/www/assets/rate-Dn2si9Q2.js | 1 - .../{route-B6vpNqI-.js => route-BS1-amG6.js} | 2 +- src/www/assets/route-BbD5ZcM9.js | 1 + .../{route-RlB6Q6Ee.js => route-CK30llLv.js} | 2 +- src/www/assets/route-D5X12Sfb.js | 1 + .../{route-BQ1uBVnF.js => route-DAVdP0gA.js} | 2 +- src/www/assets/route-DVRD2L0n.js | 1 - .../{route-B7-h3scF.js => route-DcJxa21-.js} | 2 +- src/www/assets/route-DoCbCBPZ.js | 1 + src/www/assets/route-PptyOHrt.js | 1 - src/www/assets/route-RW2oeJjf.js | 1 - src/www/assets/router-BiKDWbPS.js | 2 + src/www/assets/router-CAFGtbfd.js | 2 - src/www/assets/rpc-DLQ7yPUT.js | 1 - src/www/assets/rpc-uPUYuf02.js | 1 + src/www/assets/search-provider-BLfAXVyy.js | 1 - src/www/assets/search-provider-HdTCy0rA.js | 1 + ...{select-RDYeL0gL.js => select-CvmCbELa.js} | 2 +- ...ator-vMwQK9t4.js => separator-D9ID46HE.js} | 2 +- src/www/assets/settings-AHjWkdyO.js | 1 + src/www/assets/settings-D7ShhW3h.js | 1 - ...k-CzRgD9G5.js => shield-check-NvhRlVbA.js} | 2 +- ...hNu.js => show-submitted-data-C1Bex_2C.js} | 2 +- ...av-Cg2xIXCl.js => sidebar-nav-CwLV4eAv.js} | 2 +- src/www/assets/sign-in-CZZmK4mH.js | 1 + src/www/assets/sign-in-D2eAuFap.js | 1 - src/www/assets/skeleton-CFm8kRls.js | 1 - src/www/assets/skeleton-rjHUre08.js | 1 + .../{sun-BSCMuhso.js => sun-iuXy3HzF.js} | 2 +- ...{switch-G9JqQET5.js => switch-CNK7s2s5.js} | 2 +- src/www/assets/telegram-BF0Fq-N8.js | 1 - src/www/assets/telegram-BQxE1tuf.js | 1 + ...cope-B6BZXMCK.js => telescope-CSlN4Wb8.js} | 2 +- ...tarea-DVJ0f7tO.js => textarea-DZeGC45D.js} | 2 +- ...BQCmm5ub.js => theme-provider-DHS4QVII.js} | 2 +- ...h-BhK5XnmV.js => theme-switch-BwUuzI1g.js} | 2 +- ...ooltip--TK7dEdL.js => tooltip-Ds0o9jib.js} | 2 +- ...rash-2-Cedx40kx.js => trash-2-Cw2AyOMY.js} | 2 +- src/www/assets/triangle-alert-B0S0V27z.js | 1 + ...MfGt.js => unauthorized-error-D3YkBxJR.js} | 2 +- ...SnF.js => use-table-url-state-DaepOij6.js} | 2 +- src/www/assets/useNavigate-D0eaUeb9.js | 1 + src/www/assets/useNavigate-DtHrQ0GU.js | 1 - ...uter-BFRHcVKY.js => useRouter-CAvx9Nc8.js} | 2 +- ...C_7NOQt-.js => useRouterState-15RJwLRr.js} | 2 +- ...arch-IOx88JfZ.js => useSearch-mO_G_23x.js} | 2 +- ...Store-BegLNH-k.js => useStore-BEO2Hk8E.js} | 2 +- src/www/assets/wallet-C_Rj46AZ.js | 1 - src/www/assets/wallet-ffro97i_.js | 1 + ...-Bq6p_41G.js => with-selector-7a2GSOiC.js} | 2 +- .../assets/{x-O4huDRHF.js => x-DcD2_yMP.js} | 2 +- .../{zod-BDZSAwfF.js => zod-DT8bUV2P.js} | 2 +- src/www/index.html | 84 +++++++++---------- src/www/sw.js | 2 +- 178 files changed, 278 insertions(+), 242 deletions(-) create mode 100644 src/www/assets/401-DqXW4OqI.js delete mode 100644 src/www/assets/401-sRzu7TEC.js delete mode 100644 src/www/assets/403-CvkLNZh5.js create mode 100644 src/www/assets/403-O20TZ6yp.js create mode 100644 src/www/assets/404-CJVd46Ij.js delete mode 100644 src/www/assets/404-DFYtFuXs.js delete mode 100644 src/www/assets/500-C16mruGe.js create mode 100644 src/www/assets/500-m16UXgTh.js create mode 100644 src/www/assets/503-CYbyYdpL.js delete mode 100644 src/www/assets/503-Dpo0f61r.js rename src/www/assets/{ClientOnly-CKH5SArO.js => ClientOnly-CsPcFrh_.js} (98%) rename src/www/assets/{Match-B0WegrOo.js => Match-QXywLUYo.js} (99%) rename src/www/assets/{Matches-BXZBcz7O.js => Matches-DbrSCo1X.js} (80%) delete mode 100644 src/www/assets/_error-CEhiecHC.js create mode 100644 src/www/assets/_error-CtApCCLO.js create mode 100644 src/www/assets/_error-DnygZm1m.js delete mode 100644 src/www/assets/_error-DwHJp4LF.js delete mode 100644 src/www/assets/_trade_id-DJoAcotW.js create mode 100644 src/www/assets/_trade_id-DWezhsZd.js create mode 100644 src/www/assets/_trade_id-c2PR7KoD.js delete mode 100644 src/www/assets/_trade_id-fAQORKAU.js delete mode 100644 src/www/assets/addresses-BKAykCaO.js create mode 100644 src/www/assets/addresses-DofW5ea6.js rename src/www/assets/{appearance-C6CweejF.js => appearance-qof1Ceqk.js} (74%) rename src/www/assets/{arrow-left-CjjQKxui.js => arrow-left-DUY_wbdK.js} (61%) rename src/www/assets/{auth-animation-context-CDfHqaZr.js => auth-animation-context-CApZ-3Mm.js} (76%) rename src/www/assets/{auth-store-B7YG314a.js => auth-store-DjM0ORVQ.js} (98%) rename src/www/assets/{badge-CKj7AThQ.js => badge-Cc5lH1Q8.js} (90%) rename src/www/assets/{button-CI1sqFDO.js => button-B60XXJe-.js} (99%) rename src/www/assets/{card-D1KNrKfr.js => card-C8dA7TUs.js} (86%) create mode 100644 src/www/assets/chains-CCDNqXpb.js delete mode 100644 src/www/assets/chains-C_mLrLHU.js rename src/www/assets/{chains-store-Do-NE2MF.js => chains-store-zSK41IlU.js} (76%) create mode 100644 src/www/assets/check-BArzkFik.js delete mode 100644 src/www/assets/check-jWXQT0HB.js rename src/www/assets/{checkbox-Buso7MQb.js => checkbox-BzjV1jIh.js} (91%) rename src/www/assets/{chevron-down-CFeWMPkU.js => chevron-down-CdqbdsrC.js} (50%) rename src/www/assets/{chevrons-up-down-B-EkSPwJ.js => chevrons-up-down-DLRsY497.js} (63%) delete mode 100644 src/www/assets/circle-check-XGTPrS6o.js rename src/www/assets/{clock-3-wrSifNdF.js => clock-3-CzKYCITb.js} (62%) create mode 100644 src/www/assets/coming-soon-dialog-DJFqf5r9.js delete mode 100644 src/www/assets/coming-soon-dialog-WvGFRUST.js rename src/www/assets/{command-St1f4L7D.js => command-Bh7MlPL5.js} (97%) rename src/www/assets/{confirm-dialog-N7FIzDUo.js => confirm-dialog-C_1PQXUD.js} (91%) rename src/www/assets/{copy-to-clipboard-oLBAmuxz.js => copy-to-clipboard-BA9d7uOy.js} (94%) rename src/www/assets/{createLucideIcon-CKkuOIuw.js => createLucideIcon-BYirCMks.js} (95%) create mode 100644 src/www/assets/dashboard-BSDIqTN-.js delete mode 100644 src/www/assets/dashboard-jkQFKOjv.js rename src/www/assets/{data-table-DckmiBCR.js => data-table-BQmAnSac.js} (80%) rename src/www/assets/{display-D6xq_oLS.js => display-DCuHnl3i.js} (57%) rename src/www/assets/{dist-HFEBkKCd.js => dist--hVmcUxR.js} (75%) rename src/www/assets/{dist-JMRFUguJ.js => dist-0ifVQ567.js} (98%) rename src/www/assets/{dist-BjEfRG8A.js => dist-B8uWn8Nr.js} (93%) rename src/www/assets/{dist-BB2cKiV1.js => dist-BCJRbhCs.js} (87%) rename src/www/assets/{dist-A3SZ_duZ.js => dist-CfXybCHU.js} (99%) rename src/www/assets/{dist-Dh5ge3Zd.js => dist-ChY8My-s.js} (90%) rename src/www/assets/{dist-u5B8Q0Qw.js => dist-CqB9Ke6B.js} (91%) rename src/www/assets/{dist-vhc0iAwK.js => dist-CvxPG4A5.js} (70%) rename src/www/assets/{dist-e45zkog2.js => dist-DVIXAmXm.js} (94%) rename src/www/assets/{dist-8VtOfdBX.js => dist-Gcp12U8O.js} (76%) rename src/www/assets/{dist-Dz_nSMrn.js => dist-H7iGCGhd.js} (65%) rename src/www/assets/{dist-DTN-3dEq.js => dist-bLadKS9Y.js} (84%) rename src/www/assets/{dist-Cc5kEhoc.js => dist-hdsj8hLM.js} (93%) rename src/www/assets/{dist-D_VX1DQ0.js => dist-ryF24_UT.js} (51%) rename src/www/assets/{download-BS2DlH4c.js => download-DUoo_uFp.js} (72%) rename src/www/assets/{dropdown-menu-CPJ_eMIt.js => dropdown-menu-DPUyjJFn.js} (96%) rename src/www/assets/{ellipsis-BTvNQjRv.js => ellipsis-pbmIhRD0.js} (71%) rename src/www/assets/{empty-state-Q56LDatF.js => empty-state-oPcnyRNf.js} (87%) delete mode 100644 src/www/assets/epay-BGa_2t41.js create mode 100644 src/www/assets/epay-CAjYukDz.js rename src/www/assets/{es2015-DJNqm-x3.js => es2015-Ckkq3MV2.js} (98%) rename src/www/assets/{eye-CdR5ptdv.js => eye-AS0khnpZ.js} (75%) rename src/www/assets/{eye-off-CoMc8-kP.js => eye-off-BDzClwT_.js} (85%) rename src/www/assets/{font-provider-BWVtH5eQ.js => font-provider-aU50-jt9.js} (73%) rename src/www/assets/{forbidden-oyIuUuBn.js => forbidden-B5vn65Go.js} (58%) rename src/www/assets/{form-Cb-bizIH.js => form-BBW8C8bl.js} (99%) delete mode 100644 src/www/assets/general-error-BryI8zNx.js create mode 100644 src/www/assets/general-error-wYLXT3cx.js rename src/www/assets/{header-sQjuQN7h.js => header-C3Kjo9mr.js} (71%) rename src/www/assets/{help-CKbNVwIA.js => help-Cz5Aw4C0.js} (52%) rename src/www/assets/{index-C0cm7RJv.js => index-BM5iUQtk.js} (99%) delete mode 100644 src/www/assets/index-D4E-D57l.css create mode 100644 src/www/assets/index-a99oFdbe.css rename src/www/assets/{input-Bhusz9zP.js => input-Z_0A8Pmk.js} (84%) delete mode 100644 src/www/assets/install-BHxgXpyy.js create mode 100644 src/www/assets/install-CP-LleT8.js rename src/www/assets/{integrations-CO1LUV52.js => integrations-BmMU3vfc.js} (66%) delete mode 100644 src/www/assets/keys-DEv2pxLG.js create mode 100644 src/www/assets/keys-DgEgzpmW.js rename src/www/assets/{label-B6I7r7Hn.js => label-hTeA0dtz.js} (72%) rename src/www/assets/{lib-bV16CnBP.js => lib-D5a_8E1C.js} (76%) rename src/www/assets/{link-Dw1SMkr2.js => link-BaS6brMf.js} (91%) rename src/www/assets/{loader-circle-CmKlBt5c.js => loader-circle-BEQOp5F6.js} (55%) delete mode 100644 src/www/assets/logo-DxhHOFqN.js create mode 100644 src/www/assets/logo-v1DcqW1s.js rename src/www/assets/{long-text-WZH5brmn.js => long-text-MoJ2CH4i.js} (77%) rename src/www/assets/{main-CEEtgVzp.js => main-DnAi5_9Q.js} (79%) rename src/www/assets/{maintenance-error-CECzFgNM.js => maintenance-error-b6nEmzx5.js} (71%) rename src/www/assets/{matchContext-Cxt9JPh-.js => matchContext-U_Lz2mXl.js} (54%) create mode 100644 src/www/assets/messages-hIvVqckc.js delete mode 100644 src/www/assets/messages-kZy_2fG-.js create mode 100644 src/www/assets/not-found-error-DssGa61q.js delete mode 100644 src/www/assets/not-found-error-mipAgOS5.js rename src/www/assets/{notifications-D0jCBzjr.js => notifications-DRQh8N_S.js} (87%) delete mode 100644 src/www/assets/okpay-CNTHFJoz.js create mode 100644 src/www/assets/okpay-CvsnKwR_.js delete mode 100644 src/www/assets/orders-BtA_Wrva.js create mode 100644 src/www/assets/orders-CtEKYVyJ.js rename src/www/assets/{page-header-Bto-ujL-.js => page-header-DBzs8fNp.js} (87%) create mode 100644 src/www/assets/password-BD550HR6.js delete mode 100644 src/www/assets/password-CO8PaL5D.js rename src/www/assets/{password-input-DV9gFRez.js => password-input-edLe6Lap.js} (66%) rename src/www/assets/{payment-setup-CAbn0mjw.js => payment-setup-ALul9XNo.js} (54%) rename src/www/assets/{popover-CtzAjxka.js => popover-B-Zr22hG.js} (90%) rename src/www/assets/{preload-helper-CGevTkrx.js => preload-helper-L_hOzau-.js} (90%) rename src/www/assets/{radio-group-ceeciZec.js => radio-group-whAqa1zt.js} (82%) create mode 100644 src/www/assets/rate-CefVARKL.js delete mode 100644 src/www/assets/rate-Dn2si9Q2.js rename src/www/assets/{route-B6vpNqI-.js => route-BS1-amG6.js} (99%) create mode 100644 src/www/assets/route-BbD5ZcM9.js rename src/www/assets/{route-RlB6Q6Ee.js => route-CK30llLv.js} (59%) create mode 100644 src/www/assets/route-D5X12Sfb.js rename src/www/assets/{route-BQ1uBVnF.js => route-DAVdP0gA.js} (87%) delete mode 100644 src/www/assets/route-DVRD2L0n.js rename src/www/assets/{route-B7-h3scF.js => route-DcJxa21-.js} (87%) create mode 100644 src/www/assets/route-DoCbCBPZ.js delete mode 100644 src/www/assets/route-PptyOHrt.js delete mode 100644 src/www/assets/route-RW2oeJjf.js create mode 100644 src/www/assets/router-BiKDWbPS.js delete mode 100644 src/www/assets/router-CAFGtbfd.js delete mode 100644 src/www/assets/rpc-DLQ7yPUT.js create mode 100644 src/www/assets/rpc-uPUYuf02.js delete mode 100644 src/www/assets/search-provider-BLfAXVyy.js create mode 100644 src/www/assets/search-provider-HdTCy0rA.js rename src/www/assets/{select-RDYeL0gL.js => select-CvmCbELa.js} (96%) rename src/www/assets/{separator-vMwQK9t4.js => separator-D9ID46HE.js} (76%) create mode 100644 src/www/assets/settings-AHjWkdyO.js delete mode 100644 src/www/assets/settings-D7ShhW3h.js rename src/www/assets/{shield-check-CzRgD9G5.js => shield-check-NvhRlVbA.js} (80%) rename src/www/assets/{show-submitted-data-BpvpfhNu.js => show-submitted-data-C1Bex_2C.js} (64%) rename src/www/assets/{sidebar-nav-Cg2xIXCl.js => sidebar-nav-CwLV4eAv.js} (74%) create mode 100644 src/www/assets/sign-in-CZZmK4mH.js delete mode 100644 src/www/assets/sign-in-D2eAuFap.js delete mode 100644 src/www/assets/skeleton-CFm8kRls.js create mode 100644 src/www/assets/skeleton-rjHUre08.js rename src/www/assets/{sun-BSCMuhso.js => sun-iuXy3HzF.js} (89%) rename src/www/assets/{switch-G9JqQET5.js => switch-CNK7s2s5.js} (89%) delete mode 100644 src/www/assets/telegram-BF0Fq-N8.js create mode 100644 src/www/assets/telegram-BQxE1tuf.js rename src/www/assets/{telescope-B6BZXMCK.js => telescope-CSlN4Wb8.js} (89%) rename src/www/assets/{textarea-DVJ0f7tO.js => textarea-DZeGC45D.js} (80%) rename src/www/assets/{theme-provider-BQCmm5ub.js => theme-provider-DHS4QVII.js} (81%) rename src/www/assets/{theme-switch-BhK5XnmV.js => theme-switch-BwUuzI1g.js} (52%) rename src/www/assets/{tooltip--TK7dEdL.js => tooltip-Ds0o9jib.js} (94%) rename src/www/assets/{trash-2-Cedx40kx.js => trash-2-Cw2AyOMY.js} (85%) create mode 100644 src/www/assets/triangle-alert-B0S0V27z.js rename src/www/assets/{unauthorized-error-CgOrMfGt.js => unauthorized-error-D3YkBxJR.js} (51%) rename src/www/assets/{use-table-url-state-2gqZlSnF.js => use-table-url-state-DaepOij6.js} (96%) create mode 100644 src/www/assets/useNavigate-D0eaUeb9.js delete mode 100644 src/www/assets/useNavigate-DtHrQ0GU.js rename src/www/assets/{useRouter-BFRHcVKY.js => useRouter-CAvx9Nc8.js} (90%) rename src/www/assets/{useRouterState-C_7NOQt-.js => useRouterState-15RJwLRr.js} (57%) rename src/www/assets/{useSearch-IOx88JfZ.js => useSearch-mO_G_23x.js} (69%) rename src/www/assets/{useStore-BegLNH-k.js => useStore-BEO2Hk8E.js} (96%) delete mode 100644 src/www/assets/wallet-C_Rj46AZ.js create mode 100644 src/www/assets/wallet-ffro97i_.js rename src/www/assets/{with-selector-Bq6p_41G.js => with-selector-7a2GSOiC.js} (76%) rename src/www/assets/{x-O4huDRHF.js => x-DcD2_yMP.js} (58%) rename src/www/assets/{zod-BDZSAwfF.js => zod-DT8bUV2P.js} (99%) diff --git a/src/controller/admin/settings_controller.go b/src/controller/admin/settings_controller.go index f9a51da..9b9b31c 100644 --- a/src/controller/admin/settings_controller.go +++ b/src/controller/admin/settings_controller.go @@ -40,6 +40,8 @@ import ( // brand.site_title (string) — payment page title / website title (preferred) // brand.success_copy (string) — text shown on payment success (preferred) // brand.support_url (string) — support / help URL +// brand.background_color (string) — checkout background color +// brand.background_image_url (string) — checkout background image 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 @@ -88,7 +90,7 @@ func (c *BaseAdminController) ListSettings(ctx echo.Context) error { // @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 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 brand group keys: brand.checkout_name, brand.logo_url, brand.site_title, brand.success_copy, brand.support_url, brand.background_color, brand.background_image_url. Legacy aliases brand.site_name, brand.page_title and brand.pay_success_text are also supported. // @Description system group keys: system.order_expiration_time. // @Tags Admin Settings // @Security AdminJWT diff --git a/src/controller/comm/supported_asset_controller.go b/src/controller/comm/supported_asset_controller.go index ddd4d7e..b01888a 100644 --- a/src/controller/comm/supported_asset_controller.go +++ b/src/controller/comm/supported_asset_controller.go @@ -95,10 +95,12 @@ func (c *BaseCommController) GetPublicConfig(ctx echo.Context) error { return c.SucJson(ctx, response.PublicConfigResponse{ SupportedAssets: supports, Site: response.SitePublicConfig{ - CashierName: data.GetBrandCashierName(), - LogoURL: data.GetBrandLogoURL(), - WebsiteTitle: data.GetBrandWebsiteTitle(), - SupportLink: data.GetBrandSupportURL(), + CashierName: data.GetBrandCashierName(), + LogoURL: data.GetBrandLogoURL(), + WebsiteTitle: data.GetBrandWebsiteTitle(), + SupportLink: data.GetBrandSupportURL(), + BackgroundColor: data.GetBrandBackgroundColor(), + BackgroundImageURL: data.GetBrandBackgroundImageURL(), }, Epay: response.EpayPublicConfig{ DefaultToken: data.GetSettingString(mdb.SettingKeyEpayDefaultToken, "usdt"), diff --git a/src/model/data/settings_data.go b/src/model/data/settings_data.go index cad97b3..b07a5f9 100644 --- a/src/model/data/settings_data.go +++ b/src/model/data/settings_data.go @@ -184,6 +184,14 @@ func GetBrandSupportURL() string { return strings.TrimSpace(GetSettingString(mdb.SettingKeyBrandSupportUrl, "")) } +func GetBrandBackgroundColor() string { + return strings.TrimSpace(GetSettingString(mdb.SettingKeyBrandBackgroundColor, "")) +} + +func GetBrandBackgroundImageURL() string { + return strings.TrimSpace(GetSettingString(mdb.SettingKeyBrandBackgroundImageUrl, "")) +} + // 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 { diff --git a/src/model/mdb/settings.go b/src/model/mdb/settings.go index 3f972ca..69c90bd 100644 --- a/src/model/mdb/settings.go +++ b/src/model/mdb/settings.go @@ -38,6 +38,8 @@ const ( SettingKeyBrandSuccessCopy = "brand.success_copy" SettingKeyBrandPaySuccess = "brand.pay_success_text" SettingKeyBrandSupportUrl = "brand.support_url" + SettingKeyBrandBackgroundColor = "brand.background_color" + SettingKeyBrandBackgroundImageUrl = "brand.background_image_url" SettingKeyRateForcedUsdt = "rate.forced_usdt_rate" SettingKeyRateAdjustPercent = "rate.adjust_percent" SettingKeyRateOkxC2cEnabled = "rate.okx_c2c_enabled" diff --git a/src/model/response/support_response.go b/src/model/response/support_response.go index 6ca8d0b..b03bb37 100644 --- a/src/model/response/support_response.go +++ b/src/model/response/support_response.go @@ -12,10 +12,12 @@ type EpayPublicConfig struct { } 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"` + 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"` + BackgroundColor string `json:"background_color" example:"#0f172a"` + BackgroundImageURL string `json:"background_image_url" example:"https://cdn.example.com/background.png"` } type OkPayPublicConfig struct { diff --git a/src/route/admin_router_test.go b/src/route/admin_router_test.go index f1b0046..8dcd1be 100644 --- a/src/route/admin_router_test.go +++ b/src/route/admin_router_test.go @@ -775,19 +775,21 @@ func TestAdminConfig_ExposesOkPayCredentials(t *testing.T) { func TestAdminSettings_UpsertBrandReflectsInConfig(t *testing.T) { e, token := setupAdminTestEnv(t) - updateBrand := func(cashier, logo, title, support string) { + updateBrand := func(cashier, logo, title, support, bgColor, bgImage 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}, + {"group": mdb.SettingGroupBrand, "key": mdb.SettingKeyBrandBackgroundColor, "value": bgColor, "type": mdb.SettingTypeString}, + {"group": mdb.SettingGroupBrand, "key": mdb.SettingKeyBrandBackgroundImageUrl, "value": bgImage, "type": mdb.SettingTypeString}, }, }, token) assertOK(t, rec) } - assertSite := func(path, wantCashier, wantLogo, wantTitle, wantSupport string) { + assertSite := func(path, wantCashier, wantLogo, wantTitle, wantSupport, wantBgColor, wantBgImage string) { rec := doGet(e, path) if strings.HasPrefix(path, "/admin/") { rec = doGetAdmin(e, path, token) @@ -810,15 +812,21 @@ func TestAdminSettings_UpsertBrandReflectsInConfig(t *testing.T) { if site["support_link"] != wantSupport { t.Fatalf("%s support_link=%v, want %s", path, site["support_link"], wantSupport) } + if site["background_color"] != wantBgColor { + t.Fatalf("%s background_color=%v, want %s", path, site["background_color"], wantBgColor) + } + if site["background_image_url"] != wantBgImage { + t.Fatalf("%s background_image_url=%v, want %s", path, site["background_image_url"], wantBgImage) + } } - 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-v1", "https://cdn.example.com/v1.png", "title-v1", "https://example.com/help-v1", "#111111", "https://cdn.example.com/bg-v1.png") + assertSite("/payments/gmpay/v1/config", "cashier-v1", "https://cdn.example.com/v1.png", "title-v1", "https://example.com/help-v1", "#111111", "https://cdn.example.com/bg-v1.png") + assertSite("/admin/api/v1/config", "cashier-v1", "https://cdn.example.com/v1.png", "title-v1", "https://example.com/help-v1", "#111111", "https://cdn.example.com/bg-v1.png") - 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") + updateBrand("cashier-v2", "https://cdn.example.com/v2.png", "title-v2", "https://example.com/help-v2", "#222222", "https://cdn.example.com/bg-v2.png") + assertSite("/payments/gmpay/v1/config", "cashier-v2", "https://cdn.example.com/v2.png", "title-v2", "https://example.com/help-v2", "#222222", "https://cdn.example.com/bg-v2.png") + assertSite("/admin/api/v1/config", "cashier-v2", "https://cdn.example.com/v2.png", "title-v2", "https://example.com/help-v2", "#222222", "https://cdn.example.com/bg-v2.png") } // ─── Notification Channels ─────────────────────────────────────────────────── diff --git a/src/route/router_test.go b/src/route/router_test.go index dfc024a..62e5e59 100644 --- a/src/route/router_test.go +++ b/src/route/router_test.go @@ -495,6 +495,12 @@ func TestGetPublicConfig(t *testing.T) { if err := data.SetSetting(mdb.SettingGroupBrand, mdb.SettingKeyBrandSupportUrl, "https://example.com/support", mdb.SettingTypeString); err != nil { t.Fatalf("seed brand.support_url: %v", err) } + if err := data.SetSetting(mdb.SettingGroupBrand, mdb.SettingKeyBrandBackgroundColor, "#123456", mdb.SettingTypeString); err != nil { + t.Fatalf("seed brand.background_color: %v", err) + } + if err := data.SetSetting(mdb.SettingGroupBrand, mdb.SettingKeyBrandBackgroundImageUrl, "https://cdn.example.com/bg.png", mdb.SettingTypeString); err != nil { + t.Fatalf("seed brand.background_image_url: %v", err) + } req := httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/config", nil) rec := httptest.NewRecorder() @@ -536,6 +542,12 @@ func TestGetPublicConfig(t *testing.T) { if site["support_link"] != "https://example.com/support" { t.Fatalf("site.support_link = %v", site["support_link"]) } + if site["background_color"] != "#123456" { + t.Fatalf("site.background_color = %v, want #123456", site["background_color"]) + } + if site["background_image_url"] != "https://cdn.example.com/bg.png" { + t.Fatalf("site.background_image_url = %v", site["background_image_url"]) + } epay, ok := respData["epay"].(map[string]interface{}) if !ok { t.Fatalf("expected epay object, got %T", respData["epay"]) diff --git a/src/www/assets/401-DqXW4OqI.js b/src/www/assets/401-DqXW4OqI.js new file mode 100644 index 0000000..ac4a5a9 --- /dev/null +++ b/src/www/assets/401-DqXW4OqI.js @@ -0,0 +1 @@ +import{t as e}from"./unauthorized-error-D3YkBxJR.js";var t=e;export{t as component}; \ No newline at end of file diff --git a/src/www/assets/401-sRzu7TEC.js b/src/www/assets/401-sRzu7TEC.js deleted file mode 100644 index 90260cd..0000000 --- a/src/www/assets/401-sRzu7TEC.js +++ /dev/null @@ -1 +0,0 @@ -import{t as e}from"./unauthorized-error-CgOrMfGt.js";var t=e;export{t as component}; \ No newline at end of file diff --git a/src/www/assets/403-CvkLNZh5.js b/src/www/assets/403-CvkLNZh5.js deleted file mode 100644 index ec40fd5..0000000 --- a/src/www/assets/403-CvkLNZh5.js +++ /dev/null @@ -1 +0,0 @@ -import{t as e}from"./forbidden-oyIuUuBn.js";var t=e;export{t as component}; \ No newline at end of file diff --git a/src/www/assets/403-O20TZ6yp.js b/src/www/assets/403-O20TZ6yp.js new file mode 100644 index 0000000..3331038 --- /dev/null +++ b/src/www/assets/403-O20TZ6yp.js @@ -0,0 +1 @@ +import{t as e}from"./forbidden-B5vn65Go.js";var t=e;export{t as component}; \ No newline at end of file diff --git a/src/www/assets/404-CJVd46Ij.js b/src/www/assets/404-CJVd46Ij.js new file mode 100644 index 0000000..edba884 --- /dev/null +++ b/src/www/assets/404-CJVd46Ij.js @@ -0,0 +1 @@ +import{t as e}from"./not-found-error-DssGa61q.js";var t=e;export{t as component}; \ No newline at end of file diff --git a/src/www/assets/404-DFYtFuXs.js b/src/www/assets/404-DFYtFuXs.js deleted file mode 100644 index 3b8b27a..0000000 --- a/src/www/assets/404-DFYtFuXs.js +++ /dev/null @@ -1 +0,0 @@ -import{t as e}from"./not-found-error-mipAgOS5.js";var t=e;export{t as component}; \ No newline at end of file diff --git a/src/www/assets/500-C16mruGe.js b/src/www/assets/500-C16mruGe.js deleted file mode 100644 index 481529f..0000000 --- a/src/www/assets/500-C16mruGe.js +++ /dev/null @@ -1 +0,0 @@ -import{t as e}from"./general-error-BryI8zNx.js";var t=e;export{t as component}; \ No newline at end of file diff --git a/src/www/assets/500-m16UXgTh.js b/src/www/assets/500-m16UXgTh.js new file mode 100644 index 0000000..96bb478 --- /dev/null +++ b/src/www/assets/500-m16UXgTh.js @@ -0,0 +1 @@ +import{t as e}from"./general-error-wYLXT3cx.js";var t=e;export{t as component}; \ No newline at end of file diff --git a/src/www/assets/503-CYbyYdpL.js b/src/www/assets/503-CYbyYdpL.js new file mode 100644 index 0000000..a94092e --- /dev/null +++ b/src/www/assets/503-CYbyYdpL.js @@ -0,0 +1 @@ +import{t as e}from"./maintenance-error-b6nEmzx5.js";var t=e;export{t as component}; \ No newline at end of file diff --git a/src/www/assets/503-Dpo0f61r.js b/src/www/assets/503-Dpo0f61r.js deleted file mode 100644 index 87a0edc..0000000 --- a/src/www/assets/503-Dpo0f61r.js +++ /dev/null @@ -1 +0,0 @@ -import{t as e}from"./maintenance-error-CECzFgNM.js";var t=e;export{t as component}; \ No newline at end of file diff --git a/src/www/assets/ClientOnly-CKH5SArO.js b/src/www/assets/ClientOnly-CsPcFrh_.js similarity index 98% rename from src/www/assets/ClientOnly-CKH5SArO.js rename to src/www/assets/ClientOnly-CsPcFrh_.js index a6dcf83..a453325 100644 --- a/src/www/assets/ClientOnly-CKH5SArO.js +++ b/src/www/assets/ClientOnly-CsPcFrh_.js @@ -1 +1 @@ -import{$d as e,Xd as t,Yd as n}from"./messages-kZy_2fG-.js";import{m as r,n as i}from"./useStore-BegLNH-k.js";function a(e){let t=new Map,n,r,i=e=>{e.next&&(e.prev?(e.prev.next=e.next,e.next.prev=e.prev,e.next=void 0,r&&(r.next=e,e.prev=r)):(e.next.prev=void 0,n=e.next,e.next=void 0,r&&(e.prev=r,r.next=e)),r=e)};return{get(e){let n=t.get(e);if(n)return i(n),n.value},set(a,o){if(t.size>=e&&n){let e=n;t.delete(e.key),e.next&&(n=e.next,e.next.prev=void 0),e===r&&(r=void 0)}let s=t.get(a);if(s)s.value=o,i(s);else{let e={key:a,value:o,prev:r};r&&(r.next=e),r=e,n||=e,t.set(a,e)}},clear(){t.clear(),n=void 0,r=void 0}}}var o=4,s=5;function c(e){let t=e.indexOf(`{`);if(t===-1)return null;let n=e.indexOf(`}`,t);return n===-1||t+1>=e.length?null:[t,n]}function l(e,t,n=new Uint16Array(6)){let r=e.indexOf(`/`,t),i=r===-1?e.length:r,a=e.substring(t,i);if(!a||!a.includes(`$`))return n[0]=0,n[1]=t,n[2]=t,n[3]=i,n[4]=i,n[5]=i,n;if(a===`$`){let r=e.length;return n[0]=2,n[1]=t,n[2]=t,n[3]=r,n[4]=r,n[5]=r,n}if(a.charCodeAt(0)===36)return n[0]=1,n[1]=t,n[2]=t+1,n[3]=i,n[4]=i,n[5]=i,n;let o=c(a);if(o){let[r,s]=o,c=a.charCodeAt(r+1);if(c===45){if(r+2m[t>>>3]|=e<<7-(t&7)),new t(u,r,m,c)}getModule(e,t){return 0<=e&&e m[t>>>3]|=e<<7-(t&7)),new t(u,r,m,c)}getModule(e,t){return 0<=e&&e {if(e??=10,n??=r===10?`s`:`,`,typeof n!=`function`&&(!(r%1)&&(n=mC(n)).precision==null&&(n.trim=!0),n=EC(n)),e===1/0)return n;let o=Math.max(1,r*e/t.ticks().length);return e=>{let t=e/a(Math.round(i(e)));return t*r {var i=typeof l==`function`?l(e.value,r):e.value;return p===`width`?JI(WM(i,{fontSize:t,letterSpacing:n}),m,d):WM(i,{fontSize:t,letterSpacing:n})[p]},g=i[0],_=i[1],v=i.length>=2&&g!=null&&_!=null?zc(_.coordinate-g.coordinate):1,y=YI(a,v,p);return c===`equidistantPreserveStart`?QI(v,y,h,i,o):c===`equidistantPreserveEnd`?$I(v,y,h,i,o):(f=c===`preserveStart`||c===`preserveStartEnd`?oL(v,y,h,i,o,c===`preserveStartEnd`):aL(v,y,h,i,o),f.filter(e=>e.isShow))}var cL=e=>{var{ticks:t,label:n,labelGapWithTick:r=5,tickSize:i=0,tickMargin:a=0}=e,o=0;if(t){Array.from(t).forEach(e=>{if(e){var t=e.getBoundingClientRect();t.width>o&&(o=t.width)}});var s=n?n.getBoundingClientRect().width:0,c=i+a,l=o+c+s+(n?r:0);return Math.round(l)}return 0},lL=qf({name:`renderedTicks`,initialState:{xAxis:{},yAxis:{}},reducers:{setRenderedTicks:(e,t)=>{var{axisType:n,axisId:r,ticks:i}=t.payload;e[n][r]=Z(i)},removeRenderedTicks:(e,t)=>{var{axisType:n,axisId:r}=t.payload;delete e[n][r]}}}),{setRenderedTicks:uL,removeRenderedTicks:dL}=lL.actions,fL=lL.reducer,pL=[`axisLine`,`width`,`height`,`className`,`hide`,`ticks`,`axisType`,`axisId`];function mL(e,t){if(e==null)return{};var n,r,i=hL(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;re.slice(1,-1).indexOf(`:`)===-1?void 0:(()=>{let t=e.slice(1,-1),n=t.indexOf(`:`),r=t.slice(0,n);return r?x+r:void 0})(),re=e=>{let{theme:t,classGroups:n}=e;return w(n,t)},w=(e,t)=>{let n=v();for(let r in e){let i=e[r];T(i,n,r,t)}return n},T=(e,t,n,r)=>{let i=e.length;for(let a=0;a{if(typeof e==`string`){D(e,t,n);return}if(typeof e==`function`){O(e,t,n,r);return}k(e,t,n,r)},D=(e,t,n)=>{let r=e===``?t:A(t,e);r.classGroupId=n},O=(e,t,n,r)=>{if(ie(e)){T(e(r),t,n,r);return}t.validators===null&&(t.validators=[]),t.validators.push(_(n,e))},k=(e,t,n,r)=>{let i=Object.entries(e),a=i.length;for(let e=0;e{let n=e,r=t.split(y),i=r.length;for(let e=0;e`isThemeGetter`in e&&e.isThemeGetter===!0,j=e=>{if(e<1)return{get:()=>void 0,set:()=>{}};let t=0,n=Object.create(null),r=Object.create(null),i=(i,a)=>{n[i]=a,t++,t>e&&(t=0,r=n,n=Object.create(null))};return{get(e){let t=n[e];if(t!==void 0)return t;if((t=r[e])!==void 0)return i(e,t),t},set(e,t){e in n?n[e]=t:i(e,t)}}},M=`!`,N=`:`,ae=[],P=(e,t,n,r,i)=>({modifiers:e,hasImportantModifier:t,baseClassName:n,maybePostfixModifierPosition:r,isExternal:i}),F=e=>{let{prefix:t,experimentalParseClassName:n}=e,r=e=>{let t=[],n=0,r=0,i=0,a,o=e.length;for(let s=0;se.slice(1,-1).indexOf(`:`)===-1?void 0:(()=>{let t=e.slice(1,-1),n=t.indexOf(`:`),r=t.slice(0,n);return r?x+r:void 0})(),re=e=>{let{theme:t,classGroups:n}=e;return w(n,t)},w=(e,t)=>{let n=v();for(let r in e){let i=e[r];T(i,n,r,t)}return n},T=(e,t,n,r)=>{let i=e.length;for(let a=0;a{if(typeof e==`string`){D(e,t,n);return}if(typeof e==`function`){O(e,t,n,r);return}k(e,t,n,r)},D=(e,t,n)=>{let r=e===``?t:A(t,e);r.classGroupId=n},O=(e,t,n,r)=>{if(ie(e)){T(e(r),t,n,r);return}t.validators===null&&(t.validators=[]),t.validators.push(_(n,e))},k=(e,t,n,r)=>{let i=Object.entries(e),a=i.length;for(let e=0;e{let n=e,r=t.split(y),i=r.length;for(let e=0;e`isThemeGetter`in e&&e.isThemeGetter===!0,j=e=>{if(e<1)return{get:()=>void 0,set:()=>{}};let t=0,n=Object.create(null),r=Object.create(null),i=(i,a)=>{n[i]=a,t++,t>e&&(t=0,r=n,n=Object.create(null))};return{get(e){let t=n[e];if(t!==void 0)return t;if((t=r[e])!==void 0)return i(e,t),t},set(e,t){e in n?n[e]=t:i(e,t)}}},M=`!`,N=`:`,ae=[],P=(e,t,n,r,i)=>({modifiers:e,hasImportantModifier:t,baseClassName:n,maybePostfixModifierPosition:r,isExternal:i}),F=e=>{let{prefix:t,experimentalParseClassName:n}=e,r=e=>{let t=[],n=0,r=0,i=0,a,o=e.length;for(let s=0;s0){for(var n,r,i=0,a=e[0].length,o;i0){for(var n=0,r=e[t[0]],i,a=r.length;n0)||!((a=(i=e[t[0]]).length)>0))){for(var n=0,r=1,i,a,o;r{Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){return e===`__proto__`}e.isUnsafeProperty=t})),kc=M((e=>{Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){switch(typeof e){case`number`:case`symbol`:return!1;case`string`:return e.includes(`.`)||e.includes(`[`)||e.includes(`]`)}}e.isDeepKey=t})),Ac=M((e=>{Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){return typeof e==`string`||typeof e==`symbol`?e:Object.is(e?.valueOf?.(),-0)?`-0`:String(e)}e.toKey=t})),jc=M((e=>{Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e){if(e==null)return``;if(typeof e==`string`)return e;if(Array.isArray(e))return e.map(t).join(`,`);let n=String(e);return n===`0`&&Object.is(Number(e),-0)?`-0`:n}e.toString=t})),Mc=M((e=>{Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=jc(),n=Ac();function r(e){if(Array.isArray(e))return e.map(n.toKey);if(typeof e==`symbol`)return[e];e=t.toString(e);let r=[],i=e.length;if(i===0)return r;let a=0,o=``,s=``,c=!1;for(e.charCodeAt(0)===46&&(r.push(``),a++);a{Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=Oc(),n=kc(),r=Ac(),i=Mc();function a(e,s,c){if(e==null)return c;switch(typeof s){case`string`:{if(t.isUnsafeProperty(s))return c;let r=e[s];return r===void 0?n.isDeepKey(s)?a(e,i.toPath(s),c):c:r}case`number`:case`symbol`:{typeof s==`number`&&(s=r.toKey(s));let t=e[s];return t===void 0?c:t}default:{if(Array.isArray(s))return o(e,s,c);if(s=Object.is(s?.valueOf(),-0)?`-0`:String(s),t.isUnsafeProperty(s))return c;let n=e[s];return n===void 0?c:n}}}function o(e,n,r){if(n.length===0)return r;let i=e;for(let e=0;e{if(t===`horizontal`)return e.relativeX;if(t===`vertical`)return e.relativeY},mm=(e,t)=>t===`centric`?e.angle:e.radius,hm=e=>e.layout.width,gm=e=>e.layout.height,_m=e=>e.layout.scale,vm=e=>e.layout.margin,ym=K(e=>e.cartesianAxis.xAxis,e=>Object.values(e)),bm=K(e=>e.cartesianAxis.yAxis,e=>Object.values(e)),xm=`data-recharts-item-index`;function Sm(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function Cm(e){for(var t=1;t0))return[];if(e===t)return[e];let r=t=t)&&(n=t);else{let r=-1;for(let i of e)(i=t(i,++r,e))!=null&&(n=i)&&(n=i)}return n}function Nx(e,t){let n;if(t===void 0)for(let t of e)t!=null&&(n>t||n===void 0&&t>=t)&&(n=t);else{let r=-1;for(let i of e)(i=t(i,++r,e))!=null&&(n>i||n===void 0&&i>=i)&&(n=i)}return n}function Px(e,t,n=0,r=1/0,i){if(t=Math.floor(t),n=Math.floor(Math.max(0,n)),r=Math.floor(Math.min(e.length-1,r)),!(n<=t&&t<=r))return e;for(i=i===void 0?wx:Cx(i);r>n;){if(r-n>600){let a=r-n+1,o=t-n+1,s=Math.log(a),c=.5*Math.exp(2*s/3),l=.5*Math.sqrt(s*c*(a-c)/a)*(o-a/2<0?-1:1),u=Math.max(n,Math.floor(t-o*c/a+l)),d=Math.min(r,Math.floor(t+(a-o)*c/a+l));Px(e,t,u,d,i)}let a=e[t],o=n,s=r;for(Fx(e,n,t),i(e[r],a)>0&&Fx(e,n,r);o0;)--s}i(e[n],a)===0?Fx(e,n,s):(++s,Fx(e,s,r)),s<=t&&(n=s+1),t<=s&&(r=s-1)}return e}function Fx(e,t,n){let r=e[t];e[t]=e[n],e[n]=r}function Ix(e,t,n){if(e=Float64Array.from(hx(e,n)),!(!(r=e.length)||isNaN(t=+t))){if(t<=0||r<2)return Nx(e);if(t>=1)return Mx(e);var r,i=(r-1)*t,a=Math.floor(i),o=Mx(Px(e,a).subarray(0,a+1));return o+(Nx(e.subarray(a+1))-o)*(i-a)}}function Lx(e,t,n=mx){if(!(!(r=e.length)||isNaN(t=+t))){if(t<=0||r<2)return+n(e[0],0,e);if(t>=1)return+n(e[r-1],r-1,e);var r,i=(r-1)*t,a=Math.floor(i),o=+n(e[a],a,e);return o+(+n(e[a+1],a+1,e)-o)*(i-a)}}function Rx(e,t,n){e=+e,t=+t,n=(i=arguments.length)<2?(t=e,e=0,1):i<3?1:+n;for(var r=-1,i=Math.max(0,Math.ceil((t-e)/n))|0,a=Array(i);++r>8&15|t>>4&240,t>>4&15|t&240,(t&15)<<4|t&15,1):n===8?mS(t>>24&255,t>>16&255,t>>8&255,(t&255)/255):n===4?mS(t>>12&15|t>>8&240,t>>8&15|t>>4&240,t>>4&15|t&240,((t&15)<<4|t&15)/255):null):(t=tS.exec(e))?new _S(t[1],t[2],t[3],1):(t=nS.exec(e))?new _S(t[1]*255/100,t[2]*255/100,t[3]*255/100,1):(t=rS.exec(e))?mS(t[1],t[2],t[3],t[4]):(t=iS.exec(e))?mS(t[1]*255/100,t[2]*255/100,t[3]*255/100,t[4]):(t=aS.exec(e))?wS(t[1],t[2]/100,t[3]/100,1):(t=oS.exec(e))?wS(t[1],t[2]/100,t[3]/100,t[4]):sS.hasOwnProperty(e)?pS(sS[e]):e===`transparent`?new _S(NaN,NaN,NaN,0):null}function pS(e){return new _S(e>>16&255,e>>8&255,e&255,1)}function mS(e,t,n,r){return r<=0&&(e=t=n=NaN),new _S(e,t,n,r)}function hS(e){return e instanceof Jx||(e=fS(e)),e?(e=e.rgb(),new _S(e.r,e.g,e.b,e.opacity)):new _S}function gS(e,t,n,r){return arguments.length===1?hS(e):new _S(e,t,n,r??1)}function _S(e,t,n,r){this.r=+e,this.g=+t,this.b=+n,this.opacity=+r}Kx(_S,gS,qx(Jx,{brighter(e){return e=e==null?Xx:Xx**+e,new _S(this.r*e,this.g*e,this.b*e,this.opacity)},darker(e){return e=e==null?Yx:Yx**+e,new _S(this.r*e,this.g*e,this.b*e,this.opacity)},rgb(){return this},clamp(){return new _S(SS(this.r),SS(this.g),SS(this.b),xS(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:vS,formatHex:vS,formatHex8:yS,formatRgb:bS,toString:bS}));function vS(){return`#${CS(this.r)}${CS(this.g)}${CS(this.b)}`}function yS(){return`#${CS(this.r)}${CS(this.g)}${CS(this.b)}${CS((isNaN(this.opacity)?1:this.opacity)*255)}`}function bS(){let e=xS(this.opacity);return`${e===1?`rgb(`:`rgba(`}${SS(this.r)}, ${SS(this.g)}, ${SS(this.b)}${e===1?`)`:`, ${e})`}`}function xS(e){return isNaN(e)?1:Math.max(0,Math.min(1,e))}function SS(e){return Math.max(0,Math.min(255,Math.round(e)||0))}function CS(e){return e=SS(e),(e<16?`0`:``)+e.toString(16)}function wS(e,t,n,r){return r<=0?e=t=n=NaN:n<=0||n>=1?e=t=NaN:t<=0&&(e=NaN),new DS(e,t,n,r)}function TS(e){if(e instanceof DS)return new DS(e.h,e.s,e.l,e.opacity);if(e instanceof Jx||(e=fS(e)),!e)return new DS;if(e instanceof DS)return e;e=e.rgb();var t=e.r/255,n=e.g/255,r=e.b/255,i=Math.min(t,n,r),a=Math.max(t,n,r),o=NaN,s=a-i,c=(a+i)/2;return s?(o=t===a?(n-r)/s+(n