diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..bd0dad1 --- /dev/null +++ b/README.en.md @@ -0,0 +1,176 @@ +# 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 | + +--- + +## 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 +├── src/ Core project source code +└── 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..3a17df7 100644 --- a/README.md +++ b/README.md @@ -1,177 +1,27 @@ -# 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. 🎉 - -### 🔗 已支持网络与代币 - -| 网络 | 代币 | -|------|------| -| **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 · 多链收款 - -
+- [Official Docs](https://epusdt.com) +- [Telegram Channel](https://t.me/epusdt) +- [Telegram Group](https://t.me/epusdt_group) diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..a25d0bb --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,176 @@ +# 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 分钟内接入 | + +--- + +## 核心特性 + +- **多链多币种** — 支持 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 +├── src/ 项目核心代码 +└── 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 · 多链收款 + +
diff --git a/plugins/.gitkeep b/plugins/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/dujiaoka/README.md b/plugins/dujiaoka/README.md deleted file mode 100644 index 9a85724..0000000 --- a/plugins/dujiaoka/README.md +++ /dev/null @@ -1,12 +0,0 @@ -## 使用方法 -### 注意此插件仅适用于独角数卡2.0.4版本以下,2.0.4版本或以上的独角数卡已经内置此插件,无需配置 - -1.将`app`和`routes`目录覆盖到网站根目录。 -2.在独角数卡后台添加一个支付方式。 - -| 支付选项 | 商户id | 商户key | 商户密钥 | 备注 | -| :-----| :----- | :----- | :----- |:-----| -| Epusdt | api接口认证token | 空 | epusdt收银台地址+/api/v1/order/create-transaction| 如果独角数卡和epusdt在同一服务器则填写`127.0.0.1`不要填域名,例如`http://127.0.0.1:8000/api/v1/order/create-transaction` | - -示例: - \ No newline at end of file diff --git a/plugins/dujiaoka/app/Http/Controllers/Pay/EpusdtController.php b/plugins/dujiaoka/app/Http/Controllers/Pay/EpusdtController.php deleted file mode 100644 index e18aa85..0000000 --- a/plugins/dujiaoka/app/Http/Controllers/Pay/EpusdtController.php +++ /dev/null @@ -1,101 +0,0 @@ - - * @copyright GMWalletApp%.2f %s\n"+
- "└ 实际到账:%.2f %s\n\n"+
+ "├ 订单金额:"+amountFormat+" %s\n"+
+ "└ 实际到账:"+amountFormat+" %s\n\n"+
"📋 订单信息\n"+
"├ 交易号:%s\n"+
"├ 订单号:%s\n"+
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)
+ }
+}
diff --git a/src/route/admin_router_test.go b/src/route/admin_router_test.go
index f1b0046..c614cb7 100644
--- a/src/route/admin_router_test.go
+++ b/src/route/admin_router_test.go
@@ -718,6 +718,63 @@ func TestAdminSettings_ListAndUpsert(t *testing.T) {
assertOK(t, rec)
}
+func TestAdminSettings_AmountPrecisionValidationAndListing(t *testing.T) {
+ e, token := setupAdminTestEnv(t)
+
+ rec := doPutAdmin(e, "/admin/api/v1/settings", map[string]interface{}{
+ "items": []map[string]interface{}{
+ {"group": "system", "key": mdb.SettingKeyAmountPrecision, "value": "4", "type": "int"},
+ },
+ }, token)
+ resp := assertOK(t, rec)
+ results, ok := resp["data"].([]interface{})
+ if !ok || len(results) != 1 {
+ t.Fatalf("expected one result, got %T %v", resp["data"], resp["data"])
+ }
+ result, _ := results[0].(map[string]interface{})
+ if result["ok"] != true {
+ t.Fatalf("amount precision upsert result = %v", result)
+ }
+
+ rec = doGetAdmin(e, "/admin/api/v1/settings?group=system", token)
+ resp = assertOK(t, rec)
+ rows, ok := resp["data"].([]interface{})
+ if !ok {
+ t.Fatalf("expected settings array, got %T", resp["data"])
+ }
+ found := false
+ for _, row := range rows {
+ item, _ := row.(map[string]interface{})
+ if item["key"] == mdb.SettingKeyAmountPrecision {
+ found = true
+ if item["value"] != "4" {
+ t.Fatalf("amount precision value = %v, want 4", item["value"])
+ }
+ }
+ }
+ if !found {
+ t.Fatalf("expected settings list to include %s", mdb.SettingKeyAmountPrecision)
+ }
+
+ rec = doPutAdmin(e, "/admin/api/v1/settings", map[string]interface{}{
+ "items": []map[string]interface{}{
+ {"group": "system", "key": mdb.SettingKeyAmountPrecision, "value": "7", "type": "int"},
+ {"group": "system", "key": mdb.SettingKeyAmountPrecision, "value": "abc", "type": "int"},
+ },
+ }, token)
+ resp = assertOK(t, rec)
+ results, ok = resp["data"].([]interface{})
+ if !ok || len(results) != 2 {
+ t.Fatalf("expected two results, got %T %v", resp["data"], resp["data"])
+ }
+ for _, item := range results {
+ result, _ := item.(map[string]interface{})
+ if result["ok"] != false {
+ t.Fatalf("invalid amount precision result = %v, want ok=false", result)
+ }
+ }
+}
+
// TestAdminSettings_DeleteNonExistent verifies deleting a non-existent setting.
func TestAdminSettings_DeleteNonExistent(t *testing.T) {
e, token := setupAdminTestEnv(t)
@@ -775,19 +832,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 +869,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/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)
+ }
+}
diff --git a/src/www/assets/401-B1y7LwVO.js b/src/www/assets/401-B1y7LwVO.js
new file mode 100644
index 0000000..79c3d91
--- /dev/null
+++ b/src/www/assets/401-B1y7LwVO.js
@@ -0,0 +1 @@
+import{t as e}from"./unauthorized-error-NzfN6z7p.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-BVpBgFtB.js b/src/www/assets/403-BVpBgFtB.js
new file mode 100644
index 0000000..166f8cc
--- /dev/null
+++ b/src/www/assets/403-BVpBgFtB.js
@@ -0,0 +1 @@
+import{t as e}from"./forbidden-BTlzw0ab.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/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/404-DmWQ1ykX.js b/src/www/assets/404-DmWQ1ykX.js
new file mode 100644
index 0000000..e9826ee
--- /dev/null
+++ b/src/www/assets/404-DmWQ1ykX.js
@@ -0,0 +1 @@
+import{t as e}from"./not-found-error-AwhoAjmy.js";var t=e;export{t as component};
\ No newline at end of file
diff --git a/src/www/assets/500-By6ceH65.js b/src/www/assets/500-By6ceH65.js
new file mode 100644
index 0000000..fd94dc6
--- /dev/null
+++ b/src/www/assets/500-By6ceH65.js
@@ -0,0 +1 @@
+import{t as e}from"./general-error-_bJc_DOu.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/503-BOfcSfVR.js b/src/www/assets/503-BOfcSfVR.js
new file mode 100644
index 0000000..6efd240
--- /dev/null
+++ b/src/www/assets/503-BOfcSfVR.js
@@ -0,0 +1 @@
+import{t as e}from"./maintenance-error-CV0HxTkm.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-CEDaVo0i.js
similarity index 98%
rename from src/www/assets/ClientOnly-CKH5SArO.js
rename to src/www/assets/ClientOnly-CEDaVo0i.js
index a6dcf83..7f4c94e 100644
--- a/src/www/assets/ClientOnly-CKH5SArO.js
+++ b/src/www/assets/ClientOnly-CEDaVo0i.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=0&&n[r]===`ET`;r--)n[r]=`EN`;for(r=e+1;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