mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 18:26:16 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 049a15cb95 | |||
| 2a36a10e7f | |||
| 864eb5969c | |||
| b0093c48c7 | |||
| 4a69b7c863 | |||
| 52b171b49b | |||
| 4dc1ba88cd | |||
| 7a905ba307 | |||
| 07d1d8345f | |||
| 8d2ddbd045 | |||
| 1c805ed061 | |||
| 84972ea0f0 | |||
| 7c9e3e123e | |||
| 3e0be9bb20 | |||
| a8a3b8697f | |||
| 2d546e3478 | |||
| 1a35cab919 |
+176
@@ -0,0 +1,176 @@
|
||||
# Epusdt — Easy Payment USDT
|
||||
|
||||
<p align="center">
|
||||
<img src="https://gmwallet.app/favicon.png" alt="Epusdt Logo - Multi-chain Crypto Payment Gateway" width="120">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>Open-source multi-chain crypto payment gateway · Top real-world adoption</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.en.md">English</a> |
|
||||
<a href="./README.zh-CN.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://epusdt.com"><img src="https://img.shields.io/badge/Docs-epusdt.com-blue?style=for-the-badge" alt="Official Docs"></a>
|
||||
<a href="https://t.me/epusdt"><img src="https://img.shields.io/badge/Telegram-Channel-26A5E4?style=for-the-badge&logo=telegram&logoColor=white" alt="Telegram Channel"></a>
|
||||
<a href="https://t.me/epusdt_group"><img src="https://img.shields.io/badge/Telegram-Group-26A5E4?style=for-the-badge&logo=telegram&logoColor=white" alt="Telegram Group"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/GMWalletApp/epusdt/stargazers"><img src="https://img.shields.io/github/stars/GMWalletApp/epusdt?style=flat-square&color=f5c542" alt="GitHub Stars 3000+"></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/badge/License-GPLv3-blue?style=flat-square" alt="GPLv3 License"></a>
|
||||
<a href="https://golang.org"><img src="https://img.shields.io/badge/Go-1.16+-00ADD8?style=flat-square&logo=go&logoColor=white" alt="Go 1.16+"></a>
|
||||
<a href="https://github.com/GMWalletApp/epusdt/releases"><img src="https://img.shields.io/github/v/release/GMWalletApp/epusdt?style=flat-square&color=green" alt="Latest Release"></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
<a href="https://www.star-history.com/?type=date&repos=gmwalletapp%2Fepusdt">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=gmwalletapp/epusdt&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=gmwalletapp/epusdt&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=gmwalletapp/epusdt&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Epusdt is released under the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer and Terms of Use
|
||||
|
||||
EPusdt is developed and published by Good Morning Technology, LLC as free, open-source, non-profit, and non-custodial software for learning, research, and technical communication. Nothing in this project constitutes investment, financial, legal, tax, compliance, or any other professional advice, nor should it be interpreted as a guarantee regarding any asset, transaction outcome, revenue, fund safety, technical availability, or fitness for a particular purpose.
|
||||
|
||||
Good Morning Technology, LLC is an entity organized under United States law and will comply with applicable legal and regulatory obligations as required. Under U.S. regulatory frameworks, whether virtual currencies, digital assets, or related technical services constitute money transmission, a money services business (MSB), or another regulated activity generally depends on the specific business model, including whether the operator receives, holds, controls, or transmits funds or value, provides custody on behalf of users, or engages in exchange, payment, clearing, settlement, or other intermediary financial services.
|
||||
|
||||
As a free, open-source, non-profit, and non-custodial software project, the source code, documentation, technical communication, and related development activities of EPusdt should not be interpreted as authorization, endorsement, control, participation, warranty, or commitment by Good Morning Technology, LLC or its contributors regarding any user's downstream use, modification, deployment, integration, distribution, or secondary development.
|
||||
|
||||
Users independently determine how they use this project, for what purpose, and in what context. Good Morning Technology, LLC and its contributors cannot control, review, or restrict such conduct. Users are solely responsible for ensuring that their use, modification, deployment, integration, distribution, or derivative development complies with applicable laws, regulations, sanctions rules, and third-party rights in the relevant jurisdictions, and they bear all resulting risks, liabilities, and consequences.
|
||||
|
||||
Crypto assets are a high-risk emerging asset class. Stablecoins and other digital assets may experience sharp volatility, de-pegging, insufficient liquidity, technical failures, regulatory changes, or total loss of value. All code, documentation, and related materials in this project are provided on an "as is" and "as available" basis. Except where mandatory law provides otherwise, Good Morning Technology, LLC and its contributors are not liable for any direct or indirect loss resulting from use, inability to use, misuse, unlawful use, modification, deployment, integration, distribution, derivative development, or reliance on this project.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<sub>
|
||||
<b>Keywords:</b> 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
|
||||
</sub>
|
||||
</p>
|
||||
@@ -8,6 +8,11 @@
|
||||
<strong>开源多链多币种 Crypto 支付网关 · 实际采用率 Top 1</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.en.md">English</a> |
|
||||
<a href="./README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://epusdt.com"><img src="https://img.shields.io/badge/官网文档-epusdt.com-blue?style=for-the-badge" alt="Official Docs"></a>
|
||||
<a href="https://t.me/epusdt"><img src="https://img.shields.io/badge/Telegram-频道-26A5E4?style=for-the-badge&logo=telegram&logoColor=white" alt="Telegram Channel"></a>
|
||||
@@ -23,30 +28,30 @@
|
||||
|
||||
---
|
||||
|
||||
## 🌍 What is Epusdt?
|
||||
## What is Epusdt?
|
||||
|
||||
**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.
|
||||
**Epusdt** (Easy Payment USDT) 是一个基于 Go 构建、支持私有化部署的 **多链多币种 Crypto 支付网关**。它从最初的 TRC20 单链方案逐步演进为完整的 **多链收款平台**,让任意网站或应用都能快速接入多条链、多种代币的加密支付能力。没有第三方托管,没有平台抽成,资金直接进入你的钱包。
|
||||
|
||||
> **⭐ GitHub Star 3000+** · **🔌 已支持站点解决方案 10+** · **🏆 Crypto 支付工具实际采用率 Top 1**
|
||||
> **GitHub Star 3000+** · **已支持站点解决方案 10+** · **Crypto 支付工具实际采用率 Top 1**
|
||||
|
||||
Deploy it privately, integrate via HTTP API, and start receiving **crypto payments** in minutes. That's it. 🎉
|
||||
私有部署,按 HTTP API 接入,几分钟内就可以开始接收 **Crypto Payments**。
|
||||
|
||||
### 🔗 已支持网络与代币
|
||||
### 已支持网络与代币
|
||||
|
||||
| 网络 | 代币 |
|
||||
|------|------|
|
||||
| **TRC20** (Tron) | USDT、TRX|
|
||||
| **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) 为准。
|
||||
> 具体支持的链与代币以 [最新版本](https://github.com/GMWalletApp/epusdt/releases) 及 [官方文档](https://epusdt.com) 为准。
|
||||
|
||||
---
|
||||
|
||||
## 🔌 广泛兼容,即插即用
|
||||
## 广泛兼容,即插即用
|
||||
|
||||
无论你运营的是哪类系统,Epusdt 均可基于现有接口方案,**无需重构业务逻辑**,快速接入,立即获得 Crypto 收款能力,低成本扩展全球支付场景:
|
||||
|
||||
@@ -56,29 +61,27 @@ Deploy it privately, integrate via HTTP API, and start receiving **crypto paymen
|
||||
| **发卡系统** | [独角数卡(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/)
|
||||
| **Epay 兼容** | 兼容各类支持 Epay 易支付接口的平台 |
|
||||
| **更多** | 简易 HTTP API,10 分钟内接入 |
|
||||
|
||||
---
|
||||
|
||||
## ✨ 核心特性
|
||||
## 核心特性
|
||||
|
||||
- **多链多币种** — 支持 TRC20、ERC20、BEP20、Polygon 等主流网络,不再局限于单一链
|
||||
- **私有化部署** — 无需担心钱包被篡改、吞单,资金完全自主掌控
|
||||
- **零依赖运行** — 单个二进制文件即可启动,低并发场景无需安装 MySQL + Redis,一键部署零成本维护
|
||||
- **跨平台** — 支持 x86 / ARM 架构的 Windows / Linux / Mac 设备
|
||||
- **多钱包轮询** — 自动轮换收款地址,提高订单并发处理能力
|
||||
- **异步队列** — 高性能消息回调,优雅处理高并发场景
|
||||
- **HTTP API** — 标准化接口,任何语言 / 框架均可10min内集成
|
||||
- **多链多币种** — 支持 TRC20、ERC20、BEP20、Polygon 等主流网络
|
||||
- **私有化部署** — 资金完全自主掌控
|
||||
- **零依赖运行** — 单个二进制即可启动,低并发场景无需 MySQL + Redis
|
||||
- **跨平台** — 支持 x86 / ARM 架构的 Windows / Linux / Mac
|
||||
- **多钱包轮询** — 自动轮换收款地址,提高并发处理能力
|
||||
- **异步队列** — 高性能消息回调,适配高并发场景
|
||||
- **HTTP API** — 标准化接口,任何语言 / 框架都能快速集成
|
||||
- **Telegram Bot** — 实时支付通知,快捷管理与监控
|
||||
|
||||
---
|
||||
|
||||
## 📖 文档与教程
|
||||
## 文档与教程
|
||||
|
||||
完整文档请访问 👉 **[epusdt.com](https://epusdt.com)**
|
||||
完整文档请访问:**[epusdt.com](https://epusdt.com)**
|
||||
|
||||
快速入门:
|
||||
|
||||
@@ -91,29 +94,26 @@ Deploy it privately, integrate via HTTP API, and start receiving **crypto paymen
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 项目结构
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
```text
|
||||
Epusdt
|
||||
├── plugins/ → 已集成的系统插件(独角数卡等)
|
||||
├── src/ → 项目核心代码
|
||||
├── sdk/ → 接入 SDK
|
||||
├── sql/ → 数据库安装 / 升级脚本
|
||||
└── wiki/ → 文档与知识库
|
||||
├── src/ 项目核心代码
|
||||
└── wiki/ 文档与知识库
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 实现原理
|
||||
## 实现原理
|
||||
|
||||
Epusdt 通过监听多条区块链网络(TRC20、ERC20、BEP20、Polygon 等)的 API 或RPC节点,实时捕获钱包地址的代币入账事件,利用**金额差异**与**时效性**精确匹配交易归属:
|
||||
Epusdt 通过监听多条区块链网络(TRC20、ERC20、BEP20、Polygon 等)的 API 或 RPC 节点,实时捕获钱包地址的代币入账事件,利用**金额差异**与**时效性**精确匹配交易归属:
|
||||
|
||||
```
|
||||
```text
|
||||
工作流程:
|
||||
1. 客户发起支付,需支付 20.05 USDT
|
||||
2. 系统在哈希表中查找可用的 钱包地址 + 金额 组合
|
||||
3. 若 address_1:20.05 未被占用 → 锁定该组合(有效期 10 分钟),返回给客户
|
||||
4. 若已被占用 → 自动累加 0.0001 尝试下一个金额组合(最多 100 次)
|
||||
2. 系统在哈希表中查找可用的钱包地址 + 金额组合
|
||||
3. 若 address_1:20.05 未被占用 -> 锁定该组合(有效期 10 分钟),返回给客户
|
||||
4. 若已被占用 -> 自动累加 0.0001 尝试下一个金额组合(最多 100 次)
|
||||
5. 后台线程持续监听所有钱包的入账事件,金额匹配则确认支付成功
|
||||
```
|
||||
|
||||
@@ -121,17 +121,15 @@ Epusdt 通过监听多条区块链网络(TRC20、ERC20、BEP20、Polygon 等
|
||||
|
||||
---
|
||||
|
||||
## 💬 社区与支持
|
||||
## 社区与支持
|
||||
|
||||
**遇到问题?** 请优先在 GitHub 提交 [Issue](https://github.com/GMWalletApp/epusdt/issues),我们会**优先处理** Issue 中的反馈。
|
||||
|
||||
加入 Telegram 社区,获取最新开发动态,参与需求调研,与超过5000名活跃用户交流使用经验,对接商业资源:
|
||||
**遇到问题?** 请优先在 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) |
|
||||
| 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) |
|
||||
|
||||
---
|
||||
|
||||
@@ -147,15 +145,23 @@ Epusdt 通过监听多条区块链网络(TRC20、ERC20、BEP20、Polygon 等
|
||||
|
||||
---
|
||||
|
||||
## 📜 开源协议
|
||||
## 开源协议
|
||||
|
||||
Epusdt 遵守 [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html) 开源协议。
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 免责声明
|
||||
## 免责声明及使用条款
|
||||
|
||||
本项目仅供学习与技术交流使用,用户在使用过程中需自行遵守所在地法律法规。由于涉及加密资产及资金安全,用户应自行审查相关代码与风险。加密资产属于高风险新兴资产(包括稳定币),其价值可能波动甚至归零,GM Wallet 不对任何资产或使用结果作出保证。本内容不构成任何投资、税务、法律或金融建议,所有代码均按原样提供,仅供教育用途,相关决策请咨询专业人士。
|
||||
EPusdt 由 Good Morning Technology, LLC 以免费、开源、非盈利及非托管性质开发和披露,仅供学习、研究与技术交流使用。项目本身不构成投资、金融、法律、税务、合规或任何其他专业建议,也不应被视为对任何资产、交易结果、收益、资金安全、技术可用性或特定用途作出任何保证。
|
||||
|
||||
Good Morning Technology, LLC 为依据美国法律设立的主体,并将在适用法律法规范围内履行相应合规义务。就美国监管框架而言,虚拟货币、数字资产及相关技术服务是否构成 money transmission、money services business(MSB)或其他受监管活动,通常取决于具体业务模式,包括项目方是否接收、持有、控制或传输资金或价值,是否代表用户托管资产,以及是否从事兑换、支付、清算、结算或其他中介性金融服务。
|
||||
|
||||
作为免费、开源、非盈利、非托管的软件项目,EPusdt 的代码披露、文档说明、技术交流及相关开发活动,本身不应被理解为 Good Morning Technology, LLC 或其贡献者对任何用户后续使用、修改、部署、集成、分发或二次开发行为的授权、背书、控制、参与、保证或承诺。
|
||||
|
||||
用户对本项目的实际使用方式、使用目的及后续行为由其自行决定,Good Morning Technology, LLC 及其贡献者无法控制、审查或限制该等行为。用户应自行确保其使用、修改、部署、集成、分发或二次开发行为符合所在地适用法律法规、监管要求、制裁规则及第三方权利,并独立承担由此产生的全部风险、责任与后果。
|
||||
|
||||
加密资产属于高风险新兴资产类别,包括稳定币在内的数字资产均可能发生剧烈波动、脱锚、流动性不足、技术故障、监管变化或价值归零等风险。本项目所有代码、文档及相关材料均按“现状”和“可用状态”提供。除适用法律另有强制规定外,Good Morning Technology, LLC 及其贡献者不因用户使用、无法使用、错误使用、违法使用、修改、部署、集成、分发、二次开发或依赖本项目而产生的任何直接或间接损失承担责任。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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` |
|
||||
|
||||
示例:
|
||||

|
||||
@@ -1,101 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* The file was created by Assimon.
|
||||
*
|
||||
* @author GMWalletApp<ashang@utf8.hk>
|
||||
* @copyright GMWalletApp<ashang@utf8.hk>
|
||||
* @link http://utf8.hk/
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Pay;
|
||||
|
||||
|
||||
use App\Exceptions\RuleValidationException;
|
||||
use App\Http\Controllers\PayController;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EpusdtController extends PayController
|
||||
{
|
||||
public function gateway(string $payway, string $orderSN)
|
||||
{
|
||||
try {
|
||||
// 加载网关
|
||||
$this->loadGateWay($orderSN, $payway);
|
||||
//构造要请求的参数数组,无需改动
|
||||
$parameter = [
|
||||
"amount" => (float)$this->order->actual_price,//原价
|
||||
"order_id" => $this->order->order_sn, //可以是用户ID,站内商户订单号,用户名
|
||||
'redirect_url' => route('epusdt-return', ['order_id' => $this->order->order_sn]),
|
||||
'notify_url' => url($this->payGateway->pay_handleroute . '/notify_url'),
|
||||
];
|
||||
$parameter['signature'] = $this->epusdtSign($parameter, $this->payGateway->merchant_id);
|
||||
$client = new Client([
|
||||
'headers' => [ 'Content-Type' => 'application/json' ]
|
||||
]);
|
||||
$response = $client->post($this->payGateway->merchant_pem, ['body' => json_encode($parameter)]);
|
||||
$body = json_decode($response->getBody()->getContents(), true);
|
||||
if (!isset($body['status_code']) || $body['status_code'] != 200) {
|
||||
return $this->err(__('dujiaoka.prompt.abnormal_payment_channel') . $body['message']);
|
||||
}
|
||||
return redirect()->away($body['data']['payment_url']);
|
||||
} catch (RuleValidationException $exception) {
|
||||
} catch (GuzzleException $exception) {
|
||||
return $this->err($exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function epusdtSign(array $parameter, string $signKey)
|
||||
{
|
||||
ksort($parameter);
|
||||
reset($parameter); //内部指针指向数组中的第一个元素
|
||||
$sign = '';
|
||||
$urls = '';
|
||||
foreach ($parameter as $key => $val) {
|
||||
if ($val == '') continue;
|
||||
if ($key != 'signature') {
|
||||
if ($sign != '') {
|
||||
$sign .= "&";
|
||||
$urls .= "&";
|
||||
}
|
||||
$sign .= "$key=$val"; //拼接为url参数形式
|
||||
$urls .= "$key=" . urlencode($val); //拼接为url参数形式
|
||||
}
|
||||
}
|
||||
$sign = md5($sign . $signKey);//密码追加进入开始MD5签名
|
||||
return $sign;
|
||||
}
|
||||
|
||||
public function notifyUrl(Request $request)
|
||||
{
|
||||
$data = $request->all();
|
||||
$order = $this->orderService->detailOrderSN($data['order_id']);
|
||||
if (!$order) {
|
||||
return 'fail';
|
||||
}
|
||||
$payGateway = $this->payService->detail($order->pay_id);
|
||||
if (!$payGateway) {
|
||||
return 'fail';
|
||||
}
|
||||
$signature = $this->epusdtSign($data, $payGateway->merchant_id);
|
||||
if ($data['signature'] != $signature) { //不合法的数据
|
||||
return 'fail'; //返回失败 继续补单
|
||||
} else {
|
||||
//合法的数据
|
||||
//业务处理
|
||||
$this->orderProcessService->completedOrder($data['order_id'], $data['amount'], $data['trade_id']);
|
||||
return 'ok';
|
||||
}
|
||||
}
|
||||
|
||||
public function returnUrl(Request $request)
|
||||
{
|
||||
$oid = $request->get('order_id');
|
||||
// 异步通知还没到就跳转了,所以这里休眠2秒
|
||||
sleep(2);
|
||||
return redirect(url('detail-order-sn', ['orderSN' => $oid]));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* The file was created by Assimon.
|
||||
*
|
||||
* @author GMWalletApp<ashang@utf8.hk>
|
||||
* @copyright GMWalletApp<ashang@utf8.hk>
|
||||
* @link http://utf8.hk/
|
||||
*/
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('pay-gateway/{handle}/{payway}/{orderSN}', 'PayController@redirectGateway');
|
||||
|
||||
// 支付相关
|
||||
Route::group(['prefix' => 'pay', 'namespace' => 'Pay', 'middleware' => ['dujiaoka.pay_gate_way']], function () {
|
||||
// 支付宝
|
||||
Route::get('alipay/{payway}/{orderSN}', 'AlipayController@gateway');
|
||||
Route::post('alipay/notify_url', 'AlipayController@notifyUrl');
|
||||
// 微信
|
||||
Route::get('wepay/{payway}/{orderSN}', 'WepayController@gateway');
|
||||
Route::post('wepay/notify_url', 'WepayController@notifyUrl');
|
||||
// 码支付
|
||||
Route::get('mapay/{payway}/{orderSN}', 'MapayController@gateway');
|
||||
Route::post('mapay/notify_url', 'MapayController@notifyUrl');
|
||||
// Paysapi
|
||||
Route::get('paysapi/{payway}/{orderSN}', 'PaysapiController@gateway');
|
||||
Route::post('paysapi/notify_url', 'PaysapiController@notifyUrl');
|
||||
Route::get('paysapi/return_url', 'PaysapiController@returnUrl')->name('paysapi-return');
|
||||
// payjs
|
||||
Route::get('payjs/{payway}/{orderSN}', 'PayjsController@gateway');
|
||||
Route::post('payjs/notify_url', 'PayjsController@notifyUrl');
|
||||
// 易支付
|
||||
Route::get('yipay/{payway}/{orderSN}', 'YipayController@gateway');
|
||||
Route::get('yipay/notify_url', 'YipayController@notifyUrl');
|
||||
Route::get('yipay/return_url', 'YipayController@returnUrl')->name('yipay-return');
|
||||
// paypal
|
||||
Route::get('paypal/{payway}/{orderSN}', 'PaypalPayController@gateway');
|
||||
Route::get('paypal/return_url', 'PaypalPayController@returnUrl')->name('paypal-return');
|
||||
Route::any('paypal/notify_url', 'PaypalPayController@notifyUrl');
|
||||
// V免签
|
||||
Route::get('vpay/{payway}/{orderSN}', 'VpayController@gateway');
|
||||
Route::get('vpay/notify_url', 'VpayController@notifyUrl');
|
||||
Route::get('vpay/return_url', 'VpayController@returnUrl')->name('vpay-return');
|
||||
// stripe
|
||||
Route::get('stripe/{payway}/{oid}','StripeController@gateway');
|
||||
Route::get('stripe/return_url','StripeController@returnUrl');
|
||||
Route::get('stripe/check','StripeController@check');
|
||||
Route::get('stripe/charge','StripeController@charge');
|
||||
// Coinbase
|
||||
Route::get('coinbase/{payway}/{orderSN}', 'CoinbaseController@gateway');
|
||||
Route::post('coinbase/notify_url', 'CoinbaseController@notifyUrl');
|
||||
// epusdt
|
||||
Route::get('epusdt/{payway}/{orderSN}', 'EpusdtController@gateway');
|
||||
Route::post('epusdt/notify_url', 'EpusdtController@notifyUrl');
|
||||
Route::get('epusdt/return_url', 'EpusdtController@returnUrl')->name('epusdt-return');
|
||||
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
-- auto-generated definition
|
||||
create table orders
|
||||
(
|
||||
id int auto_increment
|
||||
primary key,
|
||||
trade_id varchar(32) not null comment 'epusdt订单号',
|
||||
order_id varchar(32) not null comment '客户交易id',
|
||||
block_transaction_id varchar(128) null comment '区块唯一编号',
|
||||
actual_amount decimal(19, 4) not null comment '订单实际需要支付的金额,保留4位小数',
|
||||
amount decimal(19, 4) not null comment '订单金额,保留4位小数',
|
||||
token varchar(50) not null comment '所属钱包地址',
|
||||
status int default 1 not null comment '1:等待支付,2:支付成功,3:已过期',
|
||||
notify_url varchar(128) not null comment '异步回调地址',
|
||||
redirect_url varchar(128) null comment '同步回调地址',
|
||||
callback_num int default 0 null comment '回调次数',
|
||||
callback_confirm int default 2 null comment '回调是否已确认? 1是 2否',
|
||||
created_at timestamp null,
|
||||
updated_at timestamp null,
|
||||
deleted_at timestamp null,
|
||||
constraint orders_order_id_uindex
|
||||
unique (order_id),
|
||||
constraint orders_trade_id_uindex
|
||||
unique (trade_id)
|
||||
);
|
||||
|
||||
create index orders_block_transaction_id_index
|
||||
on orders (block_transaction_id);
|
||||
|
||||
-- auto-generated definition
|
||||
create table wallet_address
|
||||
(
|
||||
id int auto_increment
|
||||
primary key,
|
||||
token varchar(50) not null comment '钱包token',
|
||||
status int default 1 not null comment '1:启用 2:禁用',
|
||||
created_at timestamp null,
|
||||
updated_at timestamp null,
|
||||
deleted_at timestamp null
|
||||
)
|
||||
comment '钱包表';
|
||||
|
||||
create index wallet_address_token_index
|
||||
on wallet_address (token);
|
||||
|
||||
@@ -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("╚════════════════════════════════════════════════════════════════════════╝")
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||
"github.com/GMWalletApp/epusdt/model/request"
|
||||
"github.com/GMWalletApp/epusdt/model/service"
|
||||
"github.com/GMWalletApp/epusdt/util/log"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
@@ -147,23 +148,43 @@ func (c *BaseAdminController) CloseOrder(ctx echo.Context) error {
|
||||
// @Router /admin/api/v1/orders/{trade_id}/mark-paid [post]
|
||||
func (c *BaseAdminController) MarkOrderPaid(ctx echo.Context) error {
|
||||
tradeID := ctx.Param("trade_id")
|
||||
adminUserID := currentAdminUserID(ctx)
|
||||
req := new(MarkPaidRequest)
|
||||
if err := ctx.Bind(req); err != nil {
|
||||
log.Sugar.Warnf("[admin-order] mark-paid bind failed admin_user_id=%d trade_id=%s err=%v", adminUserID, tradeID, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
req.BlockTransactionId = strings.TrimSpace(req.BlockTransactionId)
|
||||
if err := c.ValidateStruct(ctx, req); err != nil {
|
||||
log.Sugar.Warnf("[admin-order] mark-paid validation failed admin_user_id=%d trade_id=%s block_transaction_id=%s err=%v", adminUserID, tradeID, req.BlockTransactionId, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||
if err != nil {
|
||||
log.Sugar.Warnf("[admin-order] mark-paid load failed admin_user_id=%d trade_id=%s block_transaction_id=%s err=%v", adminUserID, tradeID, req.BlockTransactionId, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
if order.ID == 0 {
|
||||
return c.FailJson(ctx, errors.New("order not found"))
|
||||
err = errors.New("order not found")
|
||||
log.Sugar.Warnf("[admin-order] mark-paid rejected admin_user_id=%d trade_id=%s block_transaction_id=%s err=%v", adminUserID, tradeID, req.BlockTransactionId, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
if order.Status != mdb.StatusWaitPay {
|
||||
return c.FailJson(ctx, errors.New("order is not waiting payment"))
|
||||
err = errors.New("order is not waiting payment")
|
||||
log.Sugar.Warnf("[admin-order] mark-paid rejected admin_user_id=%d trade_id=%s status=%d block_transaction_id=%s err=%v", adminUserID, tradeID, order.Status, req.BlockTransactionId, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
if !isOnChainOrder(order.PayProvider) {
|
||||
err = errors.New("order is not an on-chain payment order")
|
||||
log.Sugar.Warnf("[admin-order] mark-paid rejected admin_user_id=%d trade_id=%s pay_provider=%s block_transaction_id=%s err=%v", adminUserID, tradeID, order.PayProvider, req.BlockTransactionId, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
verifiedBlockTransactionID, err := service.ValidateManualOrderPayment(order, req.BlockTransactionId)
|
||||
if err != nil {
|
||||
log.Sugar.Warnf("[admin-order] mark-paid chain verification failed admin_user_id=%d trade_id=%s network=%s token=%s amount=%.8f block_transaction_id=%s err=%v", adminUserID, tradeID, order.Network, order.Token, order.ActualAmount, req.BlockTransactionId, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
req.BlockTransactionId = verifiedBlockTransactionID
|
||||
err = service.OrderProcessing(&request.OrderProcessingRequest{
|
||||
ReceiveAddress: order.ReceiveAddress,
|
||||
Currency: order.Currency,
|
||||
@@ -174,8 +195,10 @@ func (c *BaseAdminController) MarkOrderPaid(ctx echo.Context) error {
|
||||
BlockTransactionId: req.BlockTransactionId,
|
||||
})
|
||||
if err != nil {
|
||||
log.Sugar.Warnf("[admin-order] mark-paid processing failed admin_user_id=%d trade_id=%s block_transaction_id=%s err=%v", adminUserID, tradeID, req.BlockTransactionId, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
log.Sugar.Infof("[admin-order] mark-paid success admin_user_id=%d trade_id=%s block_transaction_id=%s", adminUserID, tradeID, req.BlockTransactionId)
|
||||
return c.SucJson(ctx, nil)
|
||||
}
|
||||
|
||||
@@ -193,16 +216,46 @@ func (c *BaseAdminController) MarkOrderPaid(ctx echo.Context) error {
|
||||
// @Router /admin/api/v1/orders/{trade_id}/resend-callback [post]
|
||||
func (c *BaseAdminController) ResendCallback(ctx echo.Context) error {
|
||||
tradeID := ctx.Param("trade_id")
|
||||
adminUserID := currentAdminUserID(ctx)
|
||||
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||
if err != nil {
|
||||
log.Sugar.Warnf("[admin-order] resend-callback load failed admin_user_id=%d trade_id=%s err=%v", adminUserID, tradeID, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
if order.ID == 0 {
|
||||
err = errors.New("order not found")
|
||||
log.Sugar.Warnf("[admin-order] resend-callback rejected admin_user_id=%d trade_id=%s err=%v", adminUserID, tradeID, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
if order.Status != mdb.StatusPaySuccess {
|
||||
err = errors.New("order is not paid (callback not applicable)")
|
||||
log.Sugar.Warnf("[admin-order] resend-callback rejected admin_user_id=%d trade_id=%s status=%d err=%v", adminUserID, tradeID, order.Status, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
if strings.TrimSpace(order.NotifyUrl) == "" {
|
||||
err = errors.New("order has empty notify_url")
|
||||
log.Sugar.Warnf("[admin-order] resend-callback rejected admin_user_id=%d trade_id=%s err=%v", adminUserID, tradeID, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
ok, err := data.ReopenOrderCallback(tradeID)
|
||||
if err != nil {
|
||||
log.Sugar.Warnf("[admin-order] resend-callback reopen failed admin_user_id=%d trade_id=%s err=%v", adminUserID, tradeID, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
if !ok {
|
||||
return c.FailJson(ctx, errors.New("order is not paid (callback not applicable)"))
|
||||
err = errors.New("resend callback failed (concurrent state change)")
|
||||
log.Sugar.Warnf("[admin-order] resend-callback rejected admin_user_id=%d trade_id=%s err=%v", adminUserID, tradeID, err)
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
log.Sugar.Infof("[admin-order] resend-callback queued admin_user_id=%d trade_id=%s", adminUserID, tradeID)
|
||||
return c.SucJson(ctx, nil)
|
||||
}
|
||||
|
||||
func isOnChainOrder(payProvider string) bool {
|
||||
payProvider = strings.TrimSpace(payProvider)
|
||||
return payProvider == "" || payProvider == mdb.PaymentProviderOnChain
|
||||
}
|
||||
|
||||
// ExportOrders streams matching orders as CSV. No pagination — the
|
||||
// caller drives what rows to include via filter params. Capped at
|
||||
// 10k rows to keep the response bounded.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/GMWalletApp/epusdt/model/dao"
|
||||
@@ -40,12 +42,15 @@ 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
|
||||
//
|
||||
// - group=system:
|
||||
// system.order_expiration_time (int) — order expiry in minutes
|
||||
// system.amount_precision (int) — payment amount precision, 2-6 decimals (default 2)
|
||||
type SettingUpsertItem struct {
|
||||
Group string `json:"group" enums:"brand,rate,system,epay,okpay" example:"epay"`
|
||||
Key string `json:"key" example:"epay.default_network"`
|
||||
@@ -88,8 +93,8 @@ 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 system group keys: system.order_expiration_time.
|
||||
// @Description brand group keys: brand.checkout_name, brand.logo_url, brand.site_title, brand.success_copy, brand.support_url, brand.background_color, brand.background_image_url. Legacy aliases brand.site_name, brand.page_title and brand.pay_success_text are also supported.
|
||||
// @Description system group keys: system.order_expiration_time, system.amount_precision (int, 2-6, default 2).
|
||||
// @Tags Admin Settings
|
||||
// @Security AdminJWT
|
||||
// @Accept json
|
||||
@@ -118,6 +123,14 @@ func (c *BaseAdminController) UpsertSettings(ctx echo.Context) error {
|
||||
out = append(out, result{Key: item.Key, OK: false, Error: "key required"})
|
||||
continue
|
||||
}
|
||||
if err := validateSettingItem(item.Group, key, item.Value); err != nil {
|
||||
out = append(out, result{Key: key, OK: false, Error: err.Error()})
|
||||
continue
|
||||
}
|
||||
if key == mdb.SettingKeyAmountPrecision {
|
||||
item.Group = mdb.SettingGroupSystem
|
||||
item.Type = mdb.SettingTypeInt
|
||||
}
|
||||
if err := data.SetSetting(item.Group, key, item.Value, item.Type); err != nil {
|
||||
out = append(out, result{Key: key, OK: false, Error: err.Error()})
|
||||
continue
|
||||
@@ -146,6 +159,23 @@ func (c *BaseAdminController) UpsertSettings(ctx echo.Context) error {
|
||||
return c.SucJson(ctx, out)
|
||||
}
|
||||
|
||||
func validateSettingItem(group, key, value string) error {
|
||||
switch key {
|
||||
case mdb.SettingKeyAmountPrecision:
|
||||
if strings.ToLower(strings.TrimSpace(group)) != mdb.SettingGroupSystem {
|
||||
return fmt.Errorf("%s must use group %s", key, mdb.SettingGroupSystem)
|
||||
}
|
||||
precision, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s must be an integer", key)
|
||||
}
|
||||
if precision < data.MinAmountPrecision || precision > data.MaxAmountPrecision {
|
||||
return fmt.Errorf("%s must be between %d and %d", key, data.MinAmountPrecision, data.MaxAmountPrecision)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSetting removes one row. The next read of that key will fall
|
||||
// back to the hardcoded default (see settings_data.GetSetting*).
|
||||
// @Summary Delete setting
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package comm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/GMWalletApp/epusdt/model/response"
|
||||
"github.com/GMWalletApp/epusdt/model/service"
|
||||
"github.com/labstack/echo/v4"
|
||||
@@ -10,23 +8,18 @@ import (
|
||||
|
||||
// CheckoutCounter 收银台
|
||||
// @Summary Checkout counter page
|
||||
// @Description Render the payment checkout counter JSON response for a given trade
|
||||
// @Description Return checkout initialization data when the order exists. This endpoint only confirms order existence and returns base order data; call /pay/check-status/{trade_id} for the current order status (1=waiting payment, 2=paid, 3=expired).
|
||||
// @Tags Payment
|
||||
// @Produce json
|
||||
// @Param trade_id path string true "Trade ID"
|
||||
// @Success 200 {object} response.CheckoutCounterResponse
|
||||
// @Success 200 {object} response.ApiResponse{data=response.CheckoutCounterResponse}
|
||||
// @Failure 400 {object} response.ApiResponse "Order not found (status_code=10008) or other request error"
|
||||
// @Router /pay/checkout-counter-resp/{trade_id} [get]
|
||||
func (c *BaseCommController) CheckoutCounter(ctx echo.Context) (err error) {
|
||||
tradeId := ctx.Param("trade_id")
|
||||
resp, err := service.GetCheckoutCounterByTradeId(tradeId)
|
||||
if err != nil {
|
||||
if err == service.ErrOrder {
|
||||
// Unknown trade id: render the page with empty payload
|
||||
// (client side shows a friendly "order not found" screen).
|
||||
emptyResp := response.CheckoutCounterResponse{}
|
||||
return c.SucJson(ctx, emptyResp)
|
||||
}
|
||||
return ctx.String(http.StatusInternalServerError, err.Error())
|
||||
return c.FailJson(ctx, err)
|
||||
}
|
||||
|
||||
return c.SucJson(ctx, resp)
|
||||
@@ -34,12 +27,12 @@ func (c *BaseCommController) CheckoutCounter(ctx echo.Context) (err error) {
|
||||
|
||||
// CheckStatus 支付状态检测
|
||||
// @Summary Check payment status
|
||||
// @Description Check the payment status of an order by trade ID
|
||||
// @Description Return the current order status by trade ID. Status: 1=waiting payment, 2=paid, 3=expired.
|
||||
// @Tags Payment
|
||||
// @Produce json
|
||||
// @Param trade_id path string true "Trade ID"
|
||||
// @Success 200 {object} response.ApiResponse{data=response.CheckStatusResponse}
|
||||
// @Failure 400 {object} response.ApiResponse
|
||||
// @Failure 400 {object} response.ApiResponse "Order not found (status_code=10008) or other request error"
|
||||
// @Router /pay/check-status/{trade_id} [get]
|
||||
func (c *BaseCommController) CheckStatus(ctx echo.Context) (err error) {
|
||||
tradeId := ctx.Param("trade_id")
|
||||
|
||||
@@ -53,8 +53,13 @@ func buildSupportedAssets() ([]response.NetworkTokenSupport, error) {
|
||||
continue
|
||||
}
|
||||
sort.Strings(symbols)
|
||||
displayName := strings.TrimSpace(ch.DisplayName)
|
||||
if displayName == "" {
|
||||
displayName = network
|
||||
}
|
||||
supports = append(supports, response.NetworkTokenSupport{
|
||||
Network: network,
|
||||
DisplayName: displayName,
|
||||
Tokens: symbols,
|
||||
})
|
||||
}
|
||||
@@ -99,6 +104,8 @@ func (c *BaseCommController) GetPublicConfig(ctx echo.Context) error {
|
||||
LogoURL: data.GetBrandLogoURL(),
|
||||
WebsiteTitle: data.GetBrandWebsiteTitle(),
|
||||
SupportLink: data.GetBrandSupportURL(),
|
||||
BackgroundColor: data.GetBrandBackgroundColor(),
|
||||
BackgroundImageURL: data.GetBrandBackgroundImageURL(),
|
||||
},
|
||||
Epay: response.EpayPublicConfig{
|
||||
DefaultToken: data.GetSettingString(mdb.SettingKeyEpayDefaultToken, "usdt"),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ func seedDefaultSettings() {
|
||||
okPayCallbackURL += "/payments/okpay/v1/notify"
|
||||
}
|
||||
defaults := []mdb.Setting{
|
||||
{Group: mdb.SettingGroupSystem, Key: mdb.SettingKeyAmountPrecision, Value: "2", Type: mdb.SettingTypeInt},
|
||||
{Group: mdb.SettingGroupEpay, Key: mdb.SettingKeyEpayDefaultToken, Value: "usdt", Type: mdb.SettingTypeString},
|
||||
{Group: mdb.SettingGroupEpay, Key: mdb.SettingKeyEpayDefaultCurrency, Value: "cny", Type: mdb.SettingTypeString},
|
||||
{Group: mdb.SettingGroupEpay, Key: mdb.SettingKeyEpayDefaultNetwork, Value: "tron", Type: mdb.SettingTypeString},
|
||||
|
||||
@@ -44,6 +44,9 @@ func RuntimeInit() error {
|
||||
if err = RuntimeDB.Exec("DROP INDEX IF EXISTS transaction_lock_address_token_amount_uindex").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err = RuntimeDB.Exec("DROP INDEX IF EXISTS transaction_lock_network_address_token_amount_uindex").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err = RuntimeDB.AutoMigrate(&mdb.TransactionLock{}); err != nil {
|
||||
color.Red.Printf("[runtime_db] sqlite migrate DB(TransactionLock),err=%s\n", err)
|
||||
return err
|
||||
|
||||
+120
-23
@@ -23,9 +23,14 @@ type PendingCallbackOrder struct {
|
||||
UpdatedAt carbon.Time `gorm:"column:updated_at"`
|
||||
}
|
||||
|
||||
func normalizeAmount(amount float64, precision int) (int64, string) {
|
||||
precision = NormalizeAmountPrecision(precision)
|
||||
value := decimal.NewFromFloat(amount).Round(int32(precision))
|
||||
return value.Shift(int32(precision)).IntPart(), value.StringFixed(int32(precision))
|
||||
}
|
||||
|
||||
func normalizeLockAmount(amount float64) (int64, string) {
|
||||
value := decimal.NewFromFloat(amount).Round(2)
|
||||
return value.Shift(2).IntPart(), value.StringFixed(2)
|
||||
return normalizeAmount(amount, GetAmountPrecision())
|
||||
}
|
||||
|
||||
func normalizeLockNetwork(network string) string {
|
||||
@@ -53,6 +58,30 @@ func applyLockAddressFilter(tx *gorm.DB, network, address string) *gorm.DB {
|
||||
return tx.Where("address = ?", address)
|
||||
}
|
||||
|
||||
func activeLocksForAddress(tx *gorm.DB, network, address, token string, now time.Time) *gorm.DB {
|
||||
query := tx.Model(&mdb.TransactionLock{}).
|
||||
Where("network = ?", normalizeLockNetwork(network)).
|
||||
Where("token = ?", normalizeLockToken(token)).
|
||||
Where("expires_at > ?", now)
|
||||
return applyLockAddressFilter(query, network, address)
|
||||
}
|
||||
|
||||
func lockMatchesAmount(lock mdb.TransactionLock, amount float64) bool {
|
||||
precision := NormalizeAmountPrecision(lock.AmountPrecision)
|
||||
scaledAmount, _ := normalizeAmount(amount, precision)
|
||||
return lock.AmountScaled == scaledAmount
|
||||
}
|
||||
|
||||
func lockAmountDecimal(lock mdb.TransactionLock) decimal.Decimal {
|
||||
if strings.TrimSpace(lock.AmountText) != "" {
|
||||
if value, err := decimal.NewFromString(lock.AmountText); err == nil {
|
||||
return value
|
||||
}
|
||||
}
|
||||
precision := NormalizeAmountPrecision(lock.AmountPrecision)
|
||||
return decimal.NewFromInt(lock.AmountScaled).Shift(int32(-precision))
|
||||
}
|
||||
|
||||
// GetOrderInfoByOrderId fetches an order by merchant order id.
|
||||
func GetOrderInfoByOrderId(orderId string) (*mdb.Orders, error) {
|
||||
order := new(mdb.Orders)
|
||||
@@ -79,6 +108,56 @@ func GetOrderByBlockIdWithTransaction(tx *gorm.DB, blockID string) (*mdb.Orders,
|
||||
return order, err
|
||||
}
|
||||
|
||||
// GetOrderByBlockTransactionIDs fetches the first order whose stored tx id
|
||||
// matches any equivalent spelling supplied by the caller.
|
||||
func GetOrderByBlockTransactionIDs(blockIDs []string) (*mdb.Orders, error) {
|
||||
order := new(mdb.Orders)
|
||||
seen := make(map[string]struct{}, len(blockIDs))
|
||||
candidates := make([]string, 0, len(blockIDs))
|
||||
for _, blockID := range blockIDs {
|
||||
blockID = strings.TrimSpace(blockID)
|
||||
if blockID == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[blockID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[blockID] = struct{}{}
|
||||
candidates = append(candidates, blockID)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return order, nil
|
||||
}
|
||||
err := dao.Mdb.Model(order).Where("block_transaction_id IN ?", candidates).Limit(1).Find(order).Error
|
||||
return order, err
|
||||
}
|
||||
|
||||
// GetOrderByBlockTransactionIDsCaseInsensitive fetches the first order whose
|
||||
// stored tx id matches any candidate after ASCII case folding. This is used
|
||||
// only for hex-based chain tx ids; case-sensitive signatures must keep using
|
||||
// GetOrderByBlockTransactionIDs.
|
||||
func GetOrderByBlockTransactionIDsCaseInsensitive(blockIDs []string) (*mdb.Orders, error) {
|
||||
order := new(mdb.Orders)
|
||||
seen := make(map[string]struct{}, len(blockIDs))
|
||||
candidates := make([]string, 0, len(blockIDs))
|
||||
for _, blockID := range blockIDs {
|
||||
blockID = strings.ToLower(strings.TrimSpace(blockID))
|
||||
if blockID == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[blockID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[blockID] = struct{}{}
|
||||
candidates = append(candidates, blockID)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return order, nil
|
||||
}
|
||||
err := dao.Mdb.Model(order).Where("LOWER(block_transaction_id) IN ?", candidates).Limit(1).Find(order).Error
|
||||
return order, err
|
||||
}
|
||||
|
||||
// OrderSuccessWithTransaction marks an order as paid only if it is still waiting for payment.
|
||||
func OrderSuccessWithTransaction(tx *gorm.DB, req *request.OrderProcessingRequest) (bool, error) {
|
||||
result := tx.Model(&mdb.Orders{}).
|
||||
@@ -278,29 +357,27 @@ func ExpireOrderByTradeId(tradeId string) error {
|
||||
func GetTradeIdByWalletAddressAndAmountAndToken(network string, address string, token string, amount float64) (string, error) {
|
||||
network = normalizeLockNetwork(network)
|
||||
address = normalizeLockAddress(network, address)
|
||||
scaledAmount, _ := normalizeLockAmount(amount)
|
||||
var lock mdb.TransactionLock
|
||||
query := dao.RuntimeDB.Model(&mdb.TransactionLock{}).
|
||||
Where("network = ?", network).
|
||||
Where("token = ?", normalizeLockToken(token)).
|
||||
Where("amount_scaled = ?", scaledAmount).
|
||||
Where("expires_at > ?", time.Now())
|
||||
query = applyLockAddressFilter(query, network, address)
|
||||
err := query.Limit(1).Find(&lock).Error
|
||||
var locks []mdb.TransactionLock
|
||||
err := activeLocksForAddress(dao.RuntimeDB, network, address, token, time.Now()).
|
||||
Order("created_at ASC").
|
||||
Find(&locks).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if lock.ID <= 0 {
|
||||
return "", nil
|
||||
}
|
||||
for _, lock := range locks {
|
||||
if lockMatchesAmount(lock, amount) {
|
||||
return lock.TradeId, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// LockTransaction reserves a network+address+token+amount pair in sqlite until expiration.
|
||||
func LockTransaction(network, address, token, tradeID string, amount float64, expirationTime time.Duration) error {
|
||||
network = normalizeLockNetwork(network)
|
||||
address = normalizeLockAddress(network, address)
|
||||
scaledAmount, amountText := normalizeLockAmount(amount)
|
||||
precision := GetAmountPrecision()
|
||||
scaledAmount, amountText := normalizeAmount(amount, precision)
|
||||
normalizedToken := normalizeLockToken(token)
|
||||
now := time.Now()
|
||||
lock := &mdb.TransactionLock{
|
||||
@@ -309,6 +386,7 @@ func LockTransaction(network, address, token, tradeID string, amount float64, ex
|
||||
Token: normalizedToken,
|
||||
AmountScaled: scaledAmount,
|
||||
AmountText: amountText,
|
||||
AmountPrecision: precision,
|
||||
TradeId: tradeID,
|
||||
ExpiresAt: now.Add(expirationTime),
|
||||
}
|
||||
@@ -316,7 +394,6 @@ func LockTransaction(network, address, token, tradeID string, amount float64, ex
|
||||
return dao.RuntimeDB.Transaction(func(tx *gorm.DB) error {
|
||||
expiredQuery := tx.Where("network = ?", network).
|
||||
Where("token = ?", normalizedToken).
|
||||
Where("amount_scaled = ?", scaledAmount).
|
||||
Where("expires_at <= ?", now)
|
||||
expiredQuery = applyLockAddressFilter(expiredQuery, network, address)
|
||||
if err := expiredQuery.Delete(&mdb.TransactionLock{}).Error; err != nil {
|
||||
@@ -326,6 +403,18 @@ func LockTransaction(network, address, token, tradeID string, amount float64, ex
|
||||
return err
|
||||
}
|
||||
|
||||
var existing []mdb.TransactionLock
|
||||
if err := activeLocksForAddress(tx, network, address, normalizedToken, now).
|
||||
Find(&existing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
candidateAmount := lockAmountDecimal(*lock)
|
||||
for _, item := range existing {
|
||||
if lockAmountDecimal(item).Equal(candidateAmount) {
|
||||
return ErrTransactionLocked
|
||||
}
|
||||
}
|
||||
|
||||
result := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(lock)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
@@ -341,13 +430,21 @@ func LockTransaction(network, address, token, tradeID string, amount float64, ex
|
||||
func UnLockTransaction(network string, address string, token string, amount float64) error {
|
||||
network = normalizeLockNetwork(network)
|
||||
address = normalizeLockAddress(network, address)
|
||||
scaledAmount, _ := normalizeLockAmount(amount)
|
||||
query := dao.RuntimeDB.
|
||||
Where("network = ?", network).
|
||||
Where("token = ?", normalizeLockToken(token)).
|
||||
Where("amount_scaled = ?", scaledAmount)
|
||||
query = applyLockAddressFilter(query, network, address)
|
||||
return query.Delete(&mdb.TransactionLock{}).Error
|
||||
var locks []mdb.TransactionLock
|
||||
if err := activeLocksForAddress(dao.RuntimeDB, network, address, token, time.Now()).
|
||||
Find(&locks).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
ids := make([]uint64, 0, len(locks))
|
||||
for _, lock := range locks {
|
||||
if lockMatchesAmount(lock, amount) {
|
||||
ids = append(ids, lock.ID)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
return dao.RuntimeDB.Where("id IN ?", ids).Delete(&mdb.TransactionLock{}).Error
|
||||
}
|
||||
|
||||
func UnLockTransactionByTradeId(tradeID string) error {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -41,6 +42,51 @@ func TestEvmTransactionLockAddressIsCaseInsensitive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionLockPrecisionPreventsEquivalentAmountsOnly(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := SetSetting(mdb.SettingGroupSystem, mdb.SettingKeyAmountPrecision, "2", mdb.SettingTypeInt); err != nil {
|
||||
t.Fatalf("set precision 2: %v", err)
|
||||
}
|
||||
if err := LockTransaction(mdb.NetworkTron, "TPrecisionAddress001", "USDT", "trade-old", 1.23, time.Hour); err != nil {
|
||||
t.Fatalf("lock old transaction: %v", err)
|
||||
}
|
||||
|
||||
if err := SetSetting(mdb.SettingGroupSystem, mdb.SettingKeyAmountPrecision, "4", mdb.SettingTypeInt); err != nil {
|
||||
t.Fatalf("set precision 4: %v", err)
|
||||
}
|
||||
if err := LockTransaction(mdb.NetworkTron, "TPrecisionAddress001", "USDT", "trade-equivalent", 1.2300, time.Hour); !errors.Is(err, ErrTransactionLocked) {
|
||||
t.Fatalf("equivalent lock error = %v, want %v", err, ErrTransactionLocked)
|
||||
}
|
||||
if err := LockTransaction(mdb.NetworkTron, "TPrecisionAddress001", "USDT", "trade-new", 1.2301, time.Hour); err != nil {
|
||||
t.Fatalf("distinct precision lock: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionLockLookupUsesStoredPrecision(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := SetSetting(mdb.SettingGroupSystem, mdb.SettingKeyAmountPrecision, "4", mdb.SettingTypeInt); err != nil {
|
||||
t.Fatalf("set precision 4: %v", err)
|
||||
}
|
||||
if err := LockTransaction(mdb.NetworkTron, "TPrecisionAddress002", "USDT", "trade-precise", 1.2345, time.Hour); err != nil {
|
||||
t.Fatalf("lock precise transaction: %v", err)
|
||||
}
|
||||
if err := SetSetting(mdb.SettingGroupSystem, mdb.SettingKeyAmountPrecision, "2", mdb.SettingTypeInt); err != nil {
|
||||
t.Fatalf("set precision 2: %v", err)
|
||||
}
|
||||
|
||||
gotTradeID, err := GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkTron, "TPrecisionAddress002", "USDT", 1.2345)
|
||||
if err != nil {
|
||||
t.Fatalf("lookup transaction lock: %v", err)
|
||||
}
|
||||
if gotTradeID != "trade-precise" {
|
||||
t.Fatalf("trade id = %q, want trade-precise", gotTradeID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonEvmTransactionLockAddressRemainsCaseSensitive(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -119,6 +119,25 @@ func GetSettingBool(key string, fallback bool) bool {
|
||||
return b
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultAmountPrecision = 2
|
||||
MinAmountPrecision = 2
|
||||
MaxAmountPrecision = 6
|
||||
)
|
||||
|
||||
// NormalizeAmountPrecision clamps external precision values to the supported
|
||||
// range used by order creation and transaction matching.
|
||||
func NormalizeAmountPrecision(precision int) int {
|
||||
if precision < MinAmountPrecision || precision > MaxAmountPrecision {
|
||||
return DefaultAmountPrecision
|
||||
}
|
||||
return precision
|
||||
}
|
||||
|
||||
func GetAmountPrecision() int {
|
||||
return NormalizeAmountPrecision(GetSettingInt(mdb.SettingKeyAmountPrecision, DefaultAmountPrecision))
|
||||
}
|
||||
|
||||
// SetSetting upserts a setting row and refreshes the cache entry.
|
||||
func SetSetting(group, key, value, valueType string) error {
|
||||
if valueType == "" {
|
||||
@@ -184,6 +203,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 {
|
||||
|
||||
@@ -30,6 +30,7 @@ const (
|
||||
SettingKeyInitAdminPasswordFetched = "system.init_admin_password_fetched"
|
||||
SettingKeyInitAdminPasswordChanged = "system.init_admin_password_changed"
|
||||
SettingKeyOrderExpiration = "system.order_expiration_time"
|
||||
SettingKeyAmountPrecision = "system.amount_precision"
|
||||
SettingKeyBrandCheckoutName = "brand.checkout_name"
|
||||
SettingKeyBrandSiteName = "brand.site_name"
|
||||
SettingKeyBrandLogoUrl = "brand.logo_url"
|
||||
@@ -38,6 +39,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"
|
||||
|
||||
@@ -9,6 +9,7 @@ type TransactionLock struct {
|
||||
Token string `gorm:"column:token;uniqueIndex:transaction_lock_network_address_token_amount_uindex,priority:3" json:"token"`
|
||||
AmountScaled int64 `gorm:"column:amount_scaled;uniqueIndex:transaction_lock_network_address_token_amount_uindex,priority:4" json:"amount_scaled"`
|
||||
AmountText string `gorm:"column:amount_text" json:"amount_text"`
|
||||
AmountPrecision int `gorm:"column:amount_precision;default:2;uniqueIndex:transaction_lock_network_address_token_amount_uindex,priority:5" json:"amount_precision"`
|
||||
TradeId string `gorm:"column:trade_id;index:transaction_lock_trade_id_index" json:"trade_id"`
|
||||
ExpiresAt time.Time `gorm:"column:expires_at;index:transaction_lock_expires_at_index" json:"expires_at"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
|
||||
@@ -9,7 +9,7 @@ const (
|
||||
NetworkTron = "tron"
|
||||
NetworkSolana = "solana"
|
||||
NetworkEthereum = "ethereum"
|
||||
NetworkBsc = "bsc"
|
||||
NetworkBsc = "binance"
|
||||
NetworkPolygon = "polygon"
|
||||
NetworkPlasma = "plasma"
|
||||
)
|
||||
|
||||
@@ -4,9 +4,9 @@ package response
|
||||
type CreateTransactionResponse struct {
|
||||
TradeId string `json:"trade_id" example:"T2026041612345678"` // epusdt订单号
|
||||
OrderId string `json:"order_id" example:"ORD20260416001"` // 客户交易id
|
||||
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,保留4位小数
|
||||
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,按 system.amount_precision 保留小数
|
||||
Currency string `json:"currency" example:"CNY"` // 订单货币类型 CNY USD......
|
||||
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额,保留4位小数
|
||||
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额,按 system.amount_precision 保留小数
|
||||
ReceiveAddress string `json:"receive_address" example:"TTestTronAddress001"` // 收款钱包地址
|
||||
Token string `json:"token" example:"USDT"` // 所属币种 TRX USDT......
|
||||
ExpirationTime int64 `json:"expiration_time" example:"1713264600"` // 过期时间 时间戳
|
||||
@@ -18,8 +18,8 @@ type OrderNotifyResponse struct {
|
||||
Pid string `json:"pid" example:"1000"` // 签名使用的商户 PID,商户据此查本地 secret 验签
|
||||
TradeId string `json:"trade_id" example:"T2026041612345678"` // epusdt订单号
|
||||
OrderId string `json:"order_id" example:"ORD20260416001"` // 客户交易id
|
||||
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,保留4位小数
|
||||
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额,保留4位小数
|
||||
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,按 system.amount_precision 保留小数
|
||||
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额,按 system.amount_precision 保留小数
|
||||
ReceiveAddress string `json:"receive_address" example:"TTestTronAddress001"` // 收款钱包地址
|
||||
Token string `json:"token" example:"USDT"` // 所属币种 TRX USDT......
|
||||
BlockTransactionId string `json:"block_transaction_id" example:"0xabc123..."` // 区块id
|
||||
|
||||
@@ -2,8 +2,8 @@ package response
|
||||
|
||||
type CheckoutCounterResponse struct {
|
||||
TradeId string `json:"trade_id" example:"T2026041612345678"` // epusdt订单号
|
||||
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,保留4位小数 法币金额
|
||||
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额,保留4位小数 加密货币金额
|
||||
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,按 system.amount_precision 保留小数 法币金额
|
||||
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额,按 system.amount_precision 保留小数 加密货币金额
|
||||
Token string `json:"token" example:"USDT"` // 所属币种 TRX USDT......
|
||||
Currency string `json:"currency" example:"CNY"` // 法币币种 CNY USD ...
|
||||
ReceiveAddress string `json:"receive_address" example:"TTestTronAddress001"` // 收款钱包地址
|
||||
|
||||
@@ -2,6 +2,7 @@ package response
|
||||
|
||||
type NetworkTokenSupport struct {
|
||||
Network string `json:"network" example:"tron"`
|
||||
DisplayName string `json:"display_name" example:"TRON"`
|
||||
Tokens []string `json:"tokens" example:"USDT,USDC"`
|
||||
}
|
||||
|
||||
@@ -16,6 +17,8 @@ type SitePublicConfig struct {
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,832 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
tron "github.com/GMWalletApp/epusdt/crypto"
|
||||
"github.com/GMWalletApp/epusdt/model/data"
|
||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||
"github.com/GMWalletApp/epusdt/util/constant"
|
||||
"github.com/GMWalletApp/epusdt/util/math"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const manualVerifyRequestTimeout = 15 * time.Second
|
||||
|
||||
var erc20TransferEventHash = common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
|
||||
|
||||
var (
|
||||
manualOrderPaymentValidatorMu sync.Mutex
|
||||
manualOrderPaymentValidator manualOrderPaymentValidatorFunc = validateManualOrderPaymentDefault
|
||||
)
|
||||
|
||||
type manualOrderPaymentValidatorFunc func(*mdb.Orders, string) (string, error)
|
||||
|
||||
// ValidateManualOrderPayment verifies that the supplied chain transaction
|
||||
// really settles the order before an admin manually marks it paid. It returns
|
||||
// the canonical transaction id that should be persisted for duplicate checks.
|
||||
func ValidateManualOrderPayment(order *mdb.Orders, blockTransactionID string) (string, error) {
|
||||
manualOrderPaymentValidatorMu.Lock()
|
||||
validator := manualOrderPaymentValidator
|
||||
manualOrderPaymentValidatorMu.Unlock()
|
||||
return validator(order, blockTransactionID)
|
||||
}
|
||||
|
||||
// SetManualOrderPaymentValidatorForTest swaps the chain verifier in tests so
|
||||
// route/controller tests don't depend on public RPC availability.
|
||||
func SetManualOrderPaymentValidatorForTest(fn func(*mdb.Orders, string) (string, error)) func() {
|
||||
manualOrderPaymentValidatorMu.Lock()
|
||||
old := manualOrderPaymentValidator
|
||||
if fn == nil {
|
||||
manualOrderPaymentValidator = validateManualOrderPaymentDefault
|
||||
} else {
|
||||
manualOrderPaymentValidator = fn
|
||||
}
|
||||
manualOrderPaymentValidatorMu.Unlock()
|
||||
return func() {
|
||||
manualOrderPaymentValidatorMu.Lock()
|
||||
manualOrderPaymentValidator = old
|
||||
manualOrderPaymentValidatorMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func validateManualOrderPaymentDefault(order *mdb.Orders, blockTransactionID string) (string, error) {
|
||||
if order == nil || order.ID == 0 {
|
||||
return "", fmt.Errorf("order not found")
|
||||
}
|
||||
txID := strings.TrimSpace(blockTransactionID)
|
||||
if txID == "" {
|
||||
return "", fmt.Errorf("block_transaction_id is required")
|
||||
}
|
||||
|
||||
var canonicalTxID string
|
||||
var err error
|
||||
switch strings.ToLower(strings.TrimSpace(order.Network)) {
|
||||
case mdb.NetworkTron:
|
||||
canonicalTxID, err = validateManualTronPayment(order, txID)
|
||||
case mdb.NetworkSolana:
|
||||
canonicalTxID, err = validateManualSolanaPayment(order, txID)
|
||||
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma:
|
||||
canonicalTxID, err = validateManualEvmPayment(order, txID)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported manual payment verification network: %s", order.Network)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = ensureManualBlockTransactionUnused(order, canonicalTxID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return canonicalTxID, nil
|
||||
}
|
||||
|
||||
func ensureManualBlockTransactionUnused(order *mdb.Orders, canonicalTxID string) error {
|
||||
candidates := equivalentManualBlockTransactionIDs(order.Network, canonicalTxID)
|
||||
var existing *mdb.Orders
|
||||
var err error
|
||||
if manualBlockTransactionIDIsHex(order.Network) {
|
||||
existing, err = data.GetOrderByBlockTransactionIDsCaseInsensitive(candidates)
|
||||
} else {
|
||||
existing, err = data.GetOrderByBlockTransactionIDs(candidates)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing.ID > 0 && existing.ID != order.ID {
|
||||
return constant.OrderBlockAlreadyProcess
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func manualBlockTransactionIDIsHex(network string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(network)) {
|
||||
case mdb.NetworkTron, mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func equivalentManualBlockTransactionIDs(network, canonicalTxID string) []string {
|
||||
network = strings.ToLower(strings.TrimSpace(network))
|
||||
canonicalTxID = strings.TrimSpace(canonicalTxID)
|
||||
seen := make(map[string]struct{})
|
||||
out := make([]string, 0, 6)
|
||||
add := func(value string) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[value]; ok {
|
||||
return
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
out = append(out, value)
|
||||
}
|
||||
|
||||
add(canonicalTxID)
|
||||
switch network {
|
||||
case mdb.NetworkEthereum, mdb.NetworkBsc, mdb.NetworkPolygon, mdb.NetworkPlasma:
|
||||
body := strings.TrimPrefix(strings.TrimPrefix(canonicalTxID, "0x"), "0X")
|
||||
body = strings.ToLower(body)
|
||||
add("0x" + body)
|
||||
add("0x" + strings.ToUpper(body))
|
||||
add("0X" + body)
|
||||
add("0X" + strings.ToUpper(body))
|
||||
add(body)
|
||||
add(strings.ToUpper(body))
|
||||
case mdb.NetworkTron:
|
||||
body := strings.TrimPrefix(strings.TrimPrefix(canonicalTxID, "0x"), "0X")
|
||||
body = strings.ToLower(body)
|
||||
add(body)
|
||||
add(strings.ToUpper(body))
|
||||
add("0x" + body)
|
||||
add("0x" + strings.ToUpper(body))
|
||||
add("0X" + body)
|
||||
add("0X" + strings.ToUpper(body))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func validateManualEvmPayment(order *mdb.Orders, txID string) (string, error) {
|
||||
txHash, canonicalTxID, err := canonicalEvmHash(txID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token, err := data.GetEnabledChainTokenBySymbol(order.Network, order.Token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if token == nil || token.ID == 0 || strings.TrimSpace(token.ContractAddress) == "" {
|
||||
return "", fmt.Errorf("enabled token contract not configured for %s/%s", order.Network, order.Token)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), manualVerifyRequestTimeout)
|
||||
defer cancel()
|
||||
|
||||
clients, err := dialManualEvmClients(ctx, order.Network)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer closeManualEvmClients(clients)
|
||||
|
||||
if err = validateManualEvmPaymentAcrossClients(ctx, clients, order, txHash, token); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return canonicalTxID, nil
|
||||
}
|
||||
|
||||
type evmChainReader interface {
|
||||
TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error)
|
||||
HeaderByNumber(context.Context, *big.Int) (*types.Header, error)
|
||||
}
|
||||
|
||||
type manualEvmClient struct {
|
||||
label string
|
||||
reader evmChainReader
|
||||
close func()
|
||||
}
|
||||
|
||||
func closeManualEvmClients(clients []manualEvmClient) {
|
||||
for _, item := range clients {
|
||||
if item.close != nil {
|
||||
item.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateManualEvmPaymentAcrossClients(ctx context.Context, clients []manualEvmClient, order *mdb.Orders, txHash common.Hash, token *mdb.ChainToken) error {
|
||||
var verifyErrors []string
|
||||
for _, item := range clients {
|
||||
if err := validateManualEvmPaymentWithClient(ctx, item.reader, order, txHash, token); err != nil {
|
||||
verifyErrors = append(verifyErrors, fmt.Sprintf("%s: %v", item.label, err))
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(verifyErrors) > 0 {
|
||||
return fmt.Errorf("manual EVM verification failed: %s", strings.Join(verifyErrors, "; "))
|
||||
}
|
||||
return fmt.Errorf("no enabled %s WS/HTTP RPC node configured", order.Network)
|
||||
}
|
||||
|
||||
func validateManualEvmPaymentWithClient(ctx context.Context, client evmChainReader, order *mdb.Orders, txHash common.Hash, token *mdb.ChainToken) error {
|
||||
receipt, err := client.TransactionReceipt(ctx, txHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch transaction receipt: %w", err)
|
||||
}
|
||||
if receipt.Status != types.ReceiptStatusSuccessful {
|
||||
return fmt.Errorf("transaction is not successful")
|
||||
}
|
||||
if receipt.BlockNumber == nil {
|
||||
return fmt.Errorf("transaction receipt missing block number")
|
||||
}
|
||||
txHeader, err := client.HeaderByNumber(ctx, receipt.BlockNumber)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch transaction block header: %w", err)
|
||||
}
|
||||
if err = ensureEvmTransactionNotBeforeOrder(txHeader.Time, order); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = ensureEvmConfirmations(ctx, client, order.Network, receipt.BlockNumber); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contract, err := normalizeEvmAddress(token.ContractAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid token contract address: %w", err)
|
||||
}
|
||||
to, err := normalizeEvmAddress(order.ReceiveAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid order receive address: %w", err)
|
||||
}
|
||||
amountMismatch := false
|
||||
for _, item := range receipt.Logs {
|
||||
if item == nil || !strings.EqualFold(item.Address.Hex(), contract.Hex()) {
|
||||
continue
|
||||
}
|
||||
if len(item.Topics) < 3 || item.Topics[0] != erc20TransferEventHash {
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(common.BytesToAddress(item.Topics[2].Bytes()).Hex(), to.Hex()) {
|
||||
continue
|
||||
}
|
||||
rawAmount := new(big.Int).SetBytes(item.Data)
|
||||
if amountMatchesRaw(order.ActualAmount, rawAmount, token.Decimals) {
|
||||
return nil
|
||||
}
|
||||
amountMismatch = true
|
||||
}
|
||||
|
||||
if amountMismatch {
|
||||
return fmt.Errorf("transaction amount mismatch")
|
||||
}
|
||||
return fmt.Errorf("matching token transfer to order address not found")
|
||||
}
|
||||
|
||||
func dialManualEvmClients(ctx context.Context, network string) ([]manualEvmClient, error) {
|
||||
var clients []manualEvmClient
|
||||
var connectErrors []string
|
||||
for _, nodeType := range []string{mdb.RpcNodeTypeWs, mdb.RpcNodeTypeHttp} {
|
||||
node, err := data.SelectRpcNode(network, nodeType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if node == nil || node.ID == 0 || strings.TrimSpace(node.Url) == "" {
|
||||
continue
|
||||
}
|
||||
rpcURL := strings.TrimSpace(node.Url)
|
||||
client, err := ethclient.DialContext(ctx, rpcURL)
|
||||
if err == nil {
|
||||
clients = append(clients, manualEvmClient{
|
||||
label: fmt.Sprintf("%s %s", nodeType, rpcURL),
|
||||
reader: client,
|
||||
close: client.Close,
|
||||
})
|
||||
continue
|
||||
}
|
||||
connectErrors = append(connectErrors, fmt.Sprintf("%s %s: %v", nodeType, rpcURL, err))
|
||||
}
|
||||
if len(clients) > 0 {
|
||||
return clients, nil
|
||||
}
|
||||
if len(connectErrors) > 0 {
|
||||
return nil, fmt.Errorf("connect %s RPC failed: %s", network, strings.Join(connectErrors, "; "))
|
||||
}
|
||||
return nil, fmt.Errorf("no enabled %s WS/HTTP RPC node configured", network)
|
||||
}
|
||||
|
||||
func ensureEvmTransactionNotBeforeOrder(blockTime uint64, order *mdb.Orders) error {
|
||||
if order == nil {
|
||||
return fmt.Errorf("order not found")
|
||||
}
|
||||
if int64(blockTime)*1000 < order.CreatedAt.TimestampMilli() {
|
||||
return fmt.Errorf("transaction predates the order")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureEvmConfirmations(ctx context.Context, client evmChainReader, network string, txBlock *big.Int) error {
|
||||
chain, err := data.GetChainByNetwork(network)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
minConfirmations := 1
|
||||
if chain != nil && chain.MinConfirmations > 0 {
|
||||
minConfirmations = chain.MinConfirmations
|
||||
}
|
||||
header, err := client.HeaderByNumber(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch latest block: %w", err)
|
||||
}
|
||||
confirmations := new(big.Int).Sub(header.Number, txBlock)
|
||||
confirmations.Add(confirmations, big.NewInt(1))
|
||||
if confirmations.Sign() < 0 || confirmations.Cmp(big.NewInt(int64(minConfirmations))) < 0 {
|
||||
return fmt.Errorf("transaction confirmations %s below required %d", confirmations.String(), minConfirmations)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type tronContractParam struct {
|
||||
TypeURL string `json:"type_url"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
}
|
||||
|
||||
type manualTronTransaction struct {
|
||||
TxID string `json:"txID"`
|
||||
RawData struct {
|
||||
Contract []struct {
|
||||
Type string `json:"type"`
|
||||
Parameter tronContractParam `json:"parameter"`
|
||||
} `json:"contract"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
} `json:"raw_data"`
|
||||
Ret []struct {
|
||||
ContractRet string `json:"contractRet"`
|
||||
} `json:"ret"`
|
||||
}
|
||||
|
||||
type manualTronTxInfo struct {
|
||||
ID string `json:"id"`
|
||||
BlockNumber int64 `json:"blockNumber"`
|
||||
BlockTimeStamp int64 `json:"blockTimeStamp"`
|
||||
ContractResult []string `json:"contractResult"`
|
||||
Log []manualTronEventLog `json:"log"`
|
||||
Receipt struct {
|
||||
Result string `json:"result"`
|
||||
} `json:"receipt"`
|
||||
}
|
||||
|
||||
type manualTronEventLog struct {
|
||||
Address string `json:"address"`
|
||||
Topics []string `json:"topics"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type manualTronBlock struct {
|
||||
BlockHeader struct {
|
||||
RawData struct {
|
||||
Number int64 `json:"number"`
|
||||
} `json:"raw_data"`
|
||||
} `json:"block_header"`
|
||||
}
|
||||
|
||||
type manualTronTransferContractValue struct {
|
||||
OwnerAddress string `json:"owner_address"`
|
||||
ToAddress string `json:"to_address"`
|
||||
Amount int64 `json:"amount"`
|
||||
}
|
||||
|
||||
func validateManualTronPayment(order *mdb.Orders, txID string) (string, error) {
|
||||
normalizedTxID, err := normalizeTronTxID(txID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
baseURL, apiKey, err := ResolveTronNode()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var tx manualTronTransaction
|
||||
if err = tronPostJSON(baseURL, apiKey, "/wallet/gettransactionbyid", map[string]interface{}{"value": normalizedTxID}, &tx); err != nil {
|
||||
return "", fmt.Errorf("fetch tron transaction: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(tx.TxID) == "" {
|
||||
return "", fmt.Errorf("transaction not found")
|
||||
}
|
||||
if strings.TrimSpace(tx.TxID) != "" && !strings.EqualFold(strings.TrimSpace(tx.TxID), normalizedTxID) {
|
||||
return "", fmt.Errorf("transaction id mismatch")
|
||||
}
|
||||
if len(tx.Ret) > 0 && tx.Ret[0].ContractRet != "" && tx.Ret[0].ContractRet != "SUCCESS" {
|
||||
return "", fmt.Errorf("transaction is not successful: %s", tx.Ret[0].ContractRet)
|
||||
}
|
||||
|
||||
var info manualTronTxInfo
|
||||
if err = tronPostJSON(baseURL, apiKey, "/wallet/gettransactioninfobyid", map[string]interface{}{"value": normalizedTxID}, &info); err != nil {
|
||||
return "", fmt.Errorf("fetch tron transaction info: %w", err)
|
||||
}
|
||||
if info.BlockNumber <= 0 {
|
||||
return "", fmt.Errorf("transaction is not confirmed")
|
||||
}
|
||||
if info.Receipt.Result != "" && info.Receipt.Result != "SUCCESS" {
|
||||
return "", fmt.Errorf("transaction is not successful: %s", info.Receipt.Result)
|
||||
}
|
||||
if err = ensureTronConfirmations(baseURL, apiKey, order.Network, info.BlockNumber); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if info.BlockTimeStamp <= 0 {
|
||||
return "", fmt.Errorf("transaction block timestamp missing")
|
||||
}
|
||||
if info.BlockTimeStamp < order.CreatedAt.TimestampMilli() {
|
||||
return "", fmt.Errorf("transaction predates the order")
|
||||
}
|
||||
|
||||
if strings.EqualFold(strings.TrimSpace(order.Token), "TRX") {
|
||||
if err = validateManualTronNativeTransfer(order, &tx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return normalizedTxID, nil
|
||||
}
|
||||
if err = validateManualTronTRC20Transfer(order, &tx, &info); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return normalizedTxID, nil
|
||||
}
|
||||
|
||||
func validateManualTronNativeTransfer(order *mdb.Orders, tx *manualTronTransaction) error {
|
||||
if len(tx.RawData.Contract) == 0 || tx.RawData.Contract[0].Type != "TransferContract" {
|
||||
return fmt.Errorf("transaction is not a TRX transfer")
|
||||
}
|
||||
var val manualTronTransferContractValue
|
||||
if err := json.Unmarshal(tx.RawData.Contract[0].Parameter.Value, &val); err != nil {
|
||||
return err
|
||||
}
|
||||
toAddr, err := tronHexToAddress(val.ToAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid TRX recipient address: %w", err)
|
||||
}
|
||||
if !strings.EqualFold(toAddr, order.ReceiveAddress) {
|
||||
return fmt.Errorf("transaction recipient mismatch")
|
||||
}
|
||||
if !amountMatchesRaw(order.ActualAmount, big.NewInt(val.Amount), 6) {
|
||||
return fmt.Errorf("transaction amount mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateManualTronTRC20Transfer(order *mdb.Orders, tx *manualTronTransaction, info *manualTronTxInfo) error {
|
||||
token, err := data.GetEnabledChainTokenBySymbol(mdb.NetworkTron, order.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if token == nil || token.ID == 0 || strings.TrimSpace(token.ContractAddress) == "" {
|
||||
return fmt.Errorf("enabled token contract not configured for tron/%s", order.Token)
|
||||
}
|
||||
if len(tx.RawData.Contract) == 0 || tx.RawData.Contract[0].Type != "TriggerSmartContract" {
|
||||
return fmt.Errorf("transaction is not a TRC20 transfer")
|
||||
}
|
||||
contractHex, err := tronAddressToHex(token.ContractAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid configured TRC20 contract: %w", err)
|
||||
}
|
||||
return validateManualTronTRC20TransferEvent(order, info, contractHex, token.Decimals)
|
||||
}
|
||||
|
||||
func validateManualTronTRC20TransferEvent(order *mdb.Orders, info *manualTronTxInfo, contractHex string, decimals int) error {
|
||||
if info == nil || len(info.Log) == 0 {
|
||||
return fmt.Errorf("matching TRC20 transfer event to order address not found")
|
||||
}
|
||||
toHex, err := tronAddressToHex(order.ReceiveAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid order receive address: %w", err)
|
||||
}
|
||||
transferTopic := strings.TrimPrefix(erc20TransferEventHash.Hex(), "0x")
|
||||
amountMismatch := false
|
||||
for _, event := range info.Log {
|
||||
eventContractHex, err := normalizeTronAddressHex(event.Address)
|
||||
if err != nil || eventContractHex != contractHex {
|
||||
continue
|
||||
}
|
||||
if len(event.Topics) < 3 || normalizeEventTopic(event.Topics[0]) != transferTopic {
|
||||
continue
|
||||
}
|
||||
eventToHex, err := tronTopicAddressToHex(event.Topics[2])
|
||||
if err != nil || eventToHex != toHex {
|
||||
continue
|
||||
}
|
||||
rawAmount, err := tronEventDataAmount(event.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid TRC20 transfer event amount: %w", err)
|
||||
}
|
||||
if amountMatchesRaw(order.ActualAmount, rawAmount, decimals) {
|
||||
return nil
|
||||
}
|
||||
amountMismatch = true
|
||||
}
|
||||
if amountMismatch {
|
||||
return fmt.Errorf("transaction amount mismatch")
|
||||
}
|
||||
return fmt.Errorf("matching TRC20 transfer event to order address not found")
|
||||
}
|
||||
|
||||
func ensureTronConfirmations(baseURL, apiKey, network string, txBlock int64) error {
|
||||
var latest manualTronBlock
|
||||
if err := tronPostJSON(baseURL, apiKey, "/wallet/getnowblock", map[string]interface{}{}, &latest); err != nil {
|
||||
return fmt.Errorf("fetch latest tron block: %w", err)
|
||||
}
|
||||
chain, err := data.GetChainByNetwork(network)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
minConfirmations := 1
|
||||
if chain != nil && chain.MinConfirmations > 0 {
|
||||
minConfirmations = chain.MinConfirmations
|
||||
}
|
||||
confirmations := latest.BlockHeader.RawData.Number - txBlock + 1
|
||||
if confirmations < int64(minConfirmations) {
|
||||
return fmt.Errorf("transaction confirmations %d below required %d", confirmations, minConfirmations)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tronPostJSON(baseURL, apiKey, path string, body interface{}, out interface{}) error {
|
||||
payload, _ := json.Marshal(body)
|
||||
req, err := http.NewRequest(http.MethodPost, strings.TrimRight(baseURL, "/")+path, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if apiKey = strings.TrimSpace(apiKey); apiKey != "" {
|
||||
req.Header.Set("TRON-PRO-API-KEY", apiKey)
|
||||
}
|
||||
client := &http.Client{Timeout: manualVerifyRequestTimeout}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(raw))
|
||||
}
|
||||
return json.Unmarshal(raw, out)
|
||||
}
|
||||
|
||||
func validateManualSolanaPayment(order *mdb.Orders, sig string) (string, error) {
|
||||
sig = strings.TrimSpace(sig)
|
||||
txData, err := SolGetTransaction(sig)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch solana transaction: %w", err)
|
||||
}
|
||||
if !gjson.GetBytes(txData, "result").Exists() || gjson.GetBytes(txData, "result").Type == gjson.Null {
|
||||
return "", fmt.Errorf("transaction not found")
|
||||
}
|
||||
if err = ensureSolanaConfirmations(order.Network, sig); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tokens, err := data.ListEnabledChainTokensByNetwork(mdb.NetworkSolana)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
mintTokens := make(map[string]*mdb.ChainToken, len(tokens))
|
||||
var nativeSolToken *mdb.ChainToken
|
||||
for i := range tokens {
|
||||
sym := strings.ToUpper(strings.TrimSpace(tokens[i].Symbol))
|
||||
mint := strings.TrimSpace(tokens[i].ContractAddress)
|
||||
if sym == "SOL" && mint == "" {
|
||||
nativeSolToken = &tokens[i]
|
||||
continue
|
||||
}
|
||||
if mint != "" {
|
||||
mintTokens[mint] = &tokens[i]
|
||||
}
|
||||
}
|
||||
|
||||
instructions := gjson.GetBytes(txData, "result.transaction.message.instructions").Array()
|
||||
amountMismatch := false
|
||||
for _, instruction := range instructions {
|
||||
transferInfo, parseErr := ParseTransferInfoFromInstruction(instruction, txData)
|
||||
if parseErr != nil || transferInfo == nil {
|
||||
continue
|
||||
}
|
||||
if !isTransferToAddress(transferInfo, order.ReceiveAddress) {
|
||||
continue
|
||||
}
|
||||
token, amount := resolveSolTokenAndAmount(transferInfo, mintTokens, nativeSolToken)
|
||||
if !strings.EqualFold(token, order.Token) {
|
||||
continue
|
||||
}
|
||||
if err = ensureSolanaTransferNotBeforeOrder(transferInfo.BlockTime, order); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if amountMatchesFloat(order.ActualAmount, amount) {
|
||||
return sig, nil
|
||||
}
|
||||
amountMismatch = true
|
||||
}
|
||||
if amountMismatch {
|
||||
return "", fmt.Errorf("transaction amount mismatch")
|
||||
}
|
||||
return "", fmt.Errorf("matching solana transfer to order address not found")
|
||||
}
|
||||
|
||||
func ensureSolanaTransferNotBeforeOrder(blockTime int64, order *mdb.Orders) error {
|
||||
if order == nil {
|
||||
return fmt.Errorf("order not found")
|
||||
}
|
||||
if blockTime <= 0 {
|
||||
return fmt.Errorf("transaction block time missing")
|
||||
}
|
||||
if blockTime*1000 < order.CreatedAt.TimestampMilli() {
|
||||
return fmt.Errorf("transaction predates the order")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureSolanaConfirmations(network, sig string) error {
|
||||
chain, err := data.GetChainByNetwork(network)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
minConfirmations := 1
|
||||
if chain != nil && chain.MinConfirmations > 0 {
|
||||
minConfirmations = chain.MinConfirmations
|
||||
}
|
||||
if minConfirmations <= 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
body, err := SolRetryClient("getSignatureStatuses", []interface{}{
|
||||
[]string{sig},
|
||||
map[string]interface{}{"searchTransactionHistory": true},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch solana signature status: %w", err)
|
||||
}
|
||||
status := gjson.GetBytes(body, "result.value.0")
|
||||
if !status.Exists() || status.Type == gjson.Null {
|
||||
return fmt.Errorf("transaction status not found")
|
||||
}
|
||||
if errValue := status.Get("err"); errValue.Exists() && errValue.Type != gjson.Null {
|
||||
return fmt.Errorf("transaction is not successful: %s", errValue.Raw)
|
||||
}
|
||||
if status.Get("confirmationStatus").String() == "finalized" {
|
||||
return nil
|
||||
}
|
||||
confirmations := status.Get("confirmations").Int()
|
||||
if confirmations < int64(minConfirmations) {
|
||||
return fmt.Errorf("transaction confirmations %d below required %d", confirmations, minConfirmations)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func amountMatchesRaw(expected float64, rawAmount *big.Int, decimals int) bool {
|
||||
if rawAmount == nil || rawAmount.Sign() <= 0 {
|
||||
return false
|
||||
}
|
||||
if decimals < 0 {
|
||||
decimals = 0
|
||||
}
|
||||
actual := decimal.NewFromBigInt(rawAmount, -int32(decimals))
|
||||
return roundedAmount(expected).Equal(actual.Round(int32(data.GetAmountPrecision())))
|
||||
}
|
||||
|
||||
func amountMatchesFloat(expected, actual float64) bool {
|
||||
return roundedAmount(expected).Equal(roundedAmount(actual))
|
||||
}
|
||||
|
||||
func roundedAmount(amount float64) decimal.Decimal {
|
||||
precision := data.GetAmountPrecision()
|
||||
return decimal.NewFromFloat(math.MustParsePrecFloat64(amount, precision)).Round(int32(precision))
|
||||
}
|
||||
|
||||
func tronHexToAddress(hexAddr string) (string, error) {
|
||||
hexAddr, err := normalizeTronAddressHex(hexAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
raw, err := hex.DecodeString(hexAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tron.EncodeCheck(raw), nil
|
||||
}
|
||||
|
||||
func tronAddressToHex(addr string) (string, error) {
|
||||
raw, err := tron.DecodeCheck(strings.TrimSpace(addr))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(raw) != 21 || raw[0] != tron.PrefixMainnet {
|
||||
return "", fmt.Errorf("invalid tron address")
|
||||
}
|
||||
return strings.ToLower(hex.EncodeToString(raw)), nil
|
||||
}
|
||||
|
||||
func normalizeTronAddressHex(hexAddr string) (string, error) {
|
||||
hexAddr = normalizeTronHexAddress(hexAddr)
|
||||
raw, err := hex.DecodeString(hexAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch len(raw) {
|
||||
case 20:
|
||||
raw = append([]byte{tron.PrefixMainnet}, raw...)
|
||||
case 21:
|
||||
if raw[0] != tron.PrefixMainnet {
|
||||
return "", fmt.Errorf("invalid tron address prefix")
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("invalid address length: %d", len(raw))
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
func normalizeEventTopic(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
value = strings.TrimPrefix(value, "0x")
|
||||
value = strings.TrimPrefix(value, "0X")
|
||||
return strings.ToLower(value)
|
||||
}
|
||||
|
||||
func tronTopicAddressToHex(topic string) (string, error) {
|
||||
topic = normalizeEventTopic(topic)
|
||||
if len(topic) != 64 {
|
||||
return "", fmt.Errorf("invalid TRON event address topic length")
|
||||
}
|
||||
if _, err := hex.DecodeString(topic); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return normalizeTronAddressHex(topic[24:])
|
||||
}
|
||||
|
||||
func tronEventDataAmount(data string) (*big.Int, error) {
|
||||
data = normalizeEventTopic(data)
|
||||
if data == "" {
|
||||
return nil, fmt.Errorf("empty event data")
|
||||
}
|
||||
if len(data)%2 != 0 {
|
||||
return nil, fmt.Errorf("odd-length event data")
|
||||
}
|
||||
raw, err := hex.DecodeString(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return new(big.Int).SetBytes(raw), nil
|
||||
}
|
||||
|
||||
func isEvmHash(value string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
value = strings.TrimPrefix(value, "0x")
|
||||
value = strings.TrimPrefix(value, "0X")
|
||||
if len(value) != 64 {
|
||||
return false
|
||||
}
|
||||
_, err := hex.DecodeString(value)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func canonicalEvmHash(value string) (common.Hash, string, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
value = strings.TrimPrefix(value, "0x")
|
||||
value = strings.TrimPrefix(value, "0X")
|
||||
if len(value) != 64 {
|
||||
return common.Hash{}, "", fmt.Errorf("invalid EVM transaction hash")
|
||||
}
|
||||
if _, err := hex.DecodeString(value); err != nil {
|
||||
return common.Hash{}, "", err
|
||||
}
|
||||
hash := common.HexToHash("0x" + strings.ToLower(value))
|
||||
return hash, hash.Hex(), nil
|
||||
}
|
||||
|
||||
func normalizeTronTxID(value string) (string, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
value = strings.TrimPrefix(value, "0x")
|
||||
value = strings.TrimPrefix(value, "0X")
|
||||
if len(value) != 64 {
|
||||
return "", fmt.Errorf("invalid TRON transaction id length")
|
||||
}
|
||||
if _, err := hex.DecodeString(value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.ToLower(value), nil
|
||||
}
|
||||
|
||||
func normalizeEvmAddress(value string) (common.Address, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
value = strings.TrimPrefix(value, "0x")
|
||||
value = strings.TrimPrefix(value, "0X")
|
||||
if len(value) != 40 {
|
||||
return common.Address{}, fmt.Errorf("invalid address length")
|
||||
}
|
||||
if _, err := hex.DecodeString(value); err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
return common.HexToAddress("0x" + value), nil
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/GMWalletApp/epusdt/internal/testutil"
|
||||
"github.com/GMWalletApp/epusdt/model/dao"
|
||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||
"github.com/dromara/carbon/v2"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
func TestManualVerifyEvmHashAcceptsOptional0x(t *testing.T) {
|
||||
hash := strings.Repeat("a", 64)
|
||||
if !isEvmHash(hash) {
|
||||
t.Fatal("expected bare EVM hash to be valid")
|
||||
}
|
||||
if !isEvmHash("0x" + hash) {
|
||||
t.Fatal("expected 0x-prefixed EVM hash to be valid")
|
||||
}
|
||||
if isEvmHash("0x" + strings.Repeat("a", 63)) {
|
||||
t.Fatal("expected short EVM hash to be invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualVerifyNormalizeEvmAddressAcceptsOptional0x(t *testing.T) {
|
||||
addr := "1111111111111111111111111111111111111111"
|
||||
want := common.HexToAddress("0x" + addr)
|
||||
got, err := normalizeEvmAddress(addr)
|
||||
if err != nil {
|
||||
t.Fatalf("normalize bare address: %v", err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("bare address = %s, want %s", got.Hex(), want.Hex())
|
||||
}
|
||||
got, err = normalizeEvmAddress("0X" + strings.ToUpper(addr))
|
||||
if err != nil {
|
||||
t.Fatalf("normalize prefixed address: %v", err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("prefixed address = %s, want %s", got.Hex(), want.Hex())
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualVerifyNormalizeTronAddressHexAcceptsOptionalPrefix(t *testing.T) {
|
||||
body := "a614f803b6fd780986a42c78ec9c7f77e6ded13c"
|
||||
want := "41" + body
|
||||
|
||||
for _, input := range []string{body, "0x" + body, want, "0X" + strings.ToUpper(want)} {
|
||||
got, err := normalizeTronAddressHex(input)
|
||||
if err != nil {
|
||||
t.Fatalf("normalizeTronAddressHex(%q): %v", input, err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("normalizeTronAddressHex(%q) = %q, want %q", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualVerifyNormalizeTronTxIDAcceptsOptional0x(t *testing.T) {
|
||||
txID := strings.Repeat("a", 64)
|
||||
got, err := normalizeTronTxID(txID)
|
||||
if err != nil {
|
||||
t.Fatalf("normalize bare txid: %v", err)
|
||||
}
|
||||
if got != txID {
|
||||
t.Fatalf("bare txid = %q, want %q", got, txID)
|
||||
}
|
||||
got, err = normalizeTronTxID("0X" + strings.ToUpper(txID))
|
||||
if err != nil {
|
||||
t.Fatalf("normalize prefixed txid: %v", err)
|
||||
}
|
||||
if got != txID {
|
||||
t.Fatalf("prefixed txid = %q, want %q", got, txID)
|
||||
}
|
||||
if _, err = normalizeTronTxID("0x" + strings.Repeat("a", 63)); err == nil {
|
||||
t.Fatal("expected short txid to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualVerifyDialEvmClientsIncludesHTTPNode(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := dao.Mdb.Create(&mdb.RpcNode{
|
||||
Network: mdb.NetworkEthereum,
|
||||
Type: mdb.RpcNodeTypeHttp,
|
||||
Url: "http://127.0.0.1:1",
|
||||
Enabled: true,
|
||||
Status: mdb.RpcNodeStatusOk,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create rpc node: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
clients, err := dialManualEvmClients(ctx, mdb.NetworkEthereum)
|
||||
if err != nil {
|
||||
t.Fatalf("dialManualEvmClients(): %v", err)
|
||||
}
|
||||
defer closeManualEvmClients(clients)
|
||||
if len(clients) != 1 {
|
||||
t.Fatalf("client count = %d, want 1", len(clients))
|
||||
}
|
||||
if !strings.Contains(clients[0].label, mdb.RpcNodeTypeHttp) {
|
||||
t.Fatalf("client label = %q, want HTTP node", clients[0].label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualVerifyEvmRejectsTransactionBeforeOrder(t *testing.T) {
|
||||
order := &mdb.Orders{BaseModel: mdb.BaseModel{CreatedAt: *carbon.NewTime(carbon.CreateFromTimestampMilli(time.Now().UnixMilli()))}}
|
||||
txTime := uint64(time.Now().Add(-time.Hour).Unix())
|
||||
if err := ensureEvmTransactionNotBeforeOrder(txTime, order); err == nil {
|
||||
t.Fatal("expected transaction before order to be rejected")
|
||||
}
|
||||
txTime = uint64(time.Now().Add(time.Minute).Unix())
|
||||
if err := ensureEvmTransactionNotBeforeOrder(txTime, order); err != nil {
|
||||
t.Fatalf("expected transaction after order to pass: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualVerifyCanonicalEvmHash(t *testing.T) {
|
||||
hash := strings.Repeat("a", 64)
|
||||
_, canonical, err := canonicalEvmHash("0X" + strings.ToUpper(hash))
|
||||
if err != nil {
|
||||
t.Fatalf("canonicalEvmHash(): %v", err)
|
||||
}
|
||||
if canonical != "0x"+hash {
|
||||
t.Fatalf("canonical hash = %q, want %q", canonical, "0x"+hash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualVerifyEquivalentBlockIDsCatchLegacyVariants(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
hash := strings.Repeat("b", 64)
|
||||
mixedHash := "0x" + strings.Repeat("bB", 32)
|
||||
if err := dao.Mdb.Create(&mdb.Orders{
|
||||
TradeId: "paid-legacy-hash",
|
||||
OrderId: "paid-legacy-hash",
|
||||
Status: mdb.StatusPaySuccess,
|
||||
Network: mdb.NetworkEthereum,
|
||||
BlockTransactionId: mixedHash,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create existing order: %v", err)
|
||||
}
|
||||
|
||||
order := &mdb.Orders{BaseModel: mdb.BaseModel{ID: 999}, Network: mdb.NetworkEthereum}
|
||||
if err := ensureManualBlockTransactionUnused(order, "0x"+hash); err == nil {
|
||||
t.Fatal("expected legacy hash variant to be treated as already processed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualVerifyEvmRequestFailureFallsBackToNextClient(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
contract := common.HexToAddress("0x1111111111111111111111111111111111111111")
|
||||
to := common.HexToAddress("0x2222222222222222222222222222222222222222")
|
||||
rawAmount := big.NewInt(1230000)
|
||||
receipt := &types.Receipt{
|
||||
Status: types.ReceiptStatusSuccessful,
|
||||
BlockNumber: big.NewInt(10),
|
||||
Logs: []*types.Log{{
|
||||
Address: contract,
|
||||
Topics: []common.Hash{
|
||||
erc20TransferEventHash,
|
||||
common.Hash{},
|
||||
common.BytesToHash(to.Bytes()),
|
||||
},
|
||||
Data: common.LeftPadBytes(rawAmount.Bytes(), 32),
|
||||
}},
|
||||
}
|
||||
order := &mdb.Orders{
|
||||
Network: mdb.NetworkEthereum,
|
||||
Token: "USDT",
|
||||
ActualAmount: 1.23,
|
||||
ReceiveAddress: to.Hex(),
|
||||
BaseModel: mdb.BaseModel{CreatedAt: *carbon.NewTime(carbon.CreateFromTimestampMilli(time.Now().Add(-time.Minute).UnixMilli()))},
|
||||
}
|
||||
token := &mdb.ChainToken{Network: mdb.NetworkEthereum, Symbol: "USDT", ContractAddress: contract.Hex(), Decimals: 6}
|
||||
|
||||
err := validateManualEvmPaymentAcrossClients(context.Background(), []manualEvmClient{
|
||||
{label: "ws bad", reader: &fakeEvmReader{receiptErr: errors.New("ws receipt failed")}},
|
||||
{label: "http ok", reader: &fakeEvmReader{
|
||||
receipt: receipt,
|
||||
headers: map[string]*types.Header{
|
||||
"10": {Number: big.NewInt(10), Time: uint64(time.Now().Unix())},
|
||||
"latest": {Number: big.NewInt(12), Time: uint64(time.Now().Unix())},
|
||||
},
|
||||
}},
|
||||
}, order, common.HexToHash("0x"+strings.Repeat("c", 64)), token)
|
||||
if err != nil {
|
||||
t.Fatalf("validateManualEvmPaymentAcrossClients(): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualVerifyTronTRC20UsesTransferEvent(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
contractHex := "411111111111111111111111111111111111111111"
|
||||
recipientHex := "41a614f803b6fd780986a42c78ec9c7f77e6ded13c"
|
||||
contractAddress, err := tronHexToAddress(contractHex)
|
||||
if err != nil {
|
||||
t.Fatalf("contract address: %v", err)
|
||||
}
|
||||
recipientAddress, err := tronHexToAddress(recipientHex)
|
||||
if err != nil {
|
||||
t.Fatalf("recipient address: %v", err)
|
||||
}
|
||||
if err = dao.Mdb.Create(&mdb.ChainToken{
|
||||
Network: mdb.NetworkTron,
|
||||
Symbol: "USDT",
|
||||
ContractAddress: contractAddress,
|
||||
Decimals: 6,
|
||||
Enabled: true,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create token: %v", err)
|
||||
}
|
||||
|
||||
rawAmount := big.NewInt(1230000)
|
||||
tx := manualTronTransactionFromCallData(t, contractHex, recipientHex, rawAmount)
|
||||
info := &manualTronTxInfo{Log: []manualTronEventLog{{
|
||||
Address: strings.TrimPrefix(contractHex, "41"),
|
||||
Topics: []string{
|
||||
"0x" + strings.TrimPrefix(erc20TransferEventHash.Hex(), "0x"),
|
||||
strings.Repeat("0", 64),
|
||||
"0x" + strings.Repeat("0", 24) + strings.TrimPrefix(recipientHex, "41"),
|
||||
},
|
||||
Data: "0x" + fmt.Sprintf("%064x", rawAmount),
|
||||
}}}
|
||||
order := &mdb.Orders{ReceiveAddress: recipientAddress, Token: "USDT", ActualAmount: 1.23}
|
||||
|
||||
if err = validateManualTronTRC20Transfer(order, &tx, info); err != nil {
|
||||
t.Fatalf("validateManualTronTRC20Transfer(): %v", err)
|
||||
}
|
||||
|
||||
info.Log[0].Data = "0x" + fmt.Sprintf("%064x", big.NewInt(1240000))
|
||||
if err = validateManualTronTRC20Transfer(order, &tx, info); err == nil {
|
||||
t.Fatal("expected event amount mismatch to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualVerifyTronPaymentHTTPFlow(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
txID := strings.Repeat("a", 64)
|
||||
contractHex := "411111111111111111111111111111111111111111"
|
||||
recipientHex := "41a614f803b6fd780986a42c78ec9c7f77e6ded13c"
|
||||
contractAddress, err := tronHexToAddress(contractHex)
|
||||
if err != nil {
|
||||
t.Fatalf("contract address: %v", err)
|
||||
}
|
||||
recipientAddress, err := tronHexToAddress(recipientHex)
|
||||
if err != nil {
|
||||
t.Fatalf("recipient address: %v", err)
|
||||
}
|
||||
|
||||
rawAmount := big.NewInt(1230000)
|
||||
blockTimeMs := time.Now().Add(time.Minute).UnixMilli()
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req map[string]string
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
t.Errorf("decode tron request: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if r.URL.Path != "/wallet/getnowblock" && req["value"] != txID {
|
||||
t.Errorf("request tx id = %q, want %q", req["value"], txID)
|
||||
http.Error(w, "bad tx id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.URL.Path {
|
||||
case "/wallet/gettransactionbyid":
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"txID": txID,
|
||||
"raw_data": map[string]interface{}{
|
||||
"contract": []map[string]interface{}{{
|
||||
"type": "TriggerSmartContract",
|
||||
"parameter": map[string]interface{}{
|
||||
"value": map[string]interface{}{},
|
||||
},
|
||||
}},
|
||||
},
|
||||
"ret": []map[string]string{{"contractRet": "SUCCESS"}},
|
||||
})
|
||||
case "/wallet/gettransactioninfobyid":
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": txID,
|
||||
"blockNumber": 100,
|
||||
"blockTimeStamp": blockTimeMs,
|
||||
"receipt": map[string]string{"result": "SUCCESS"},
|
||||
"log": []map[string]interface{}{{
|
||||
"address": strings.TrimPrefix(contractHex, "41"),
|
||||
"topics": []string{
|
||||
strings.TrimPrefix(erc20TransferEventHash.Hex(), "0x"),
|
||||
strings.Repeat("0", 64),
|
||||
"0X" + strings.ToUpper(strings.Repeat("0", 24)+strings.TrimPrefix(recipientHex, "41")),
|
||||
},
|
||||
"data": fmt.Sprintf("%064x", rawAmount),
|
||||
}},
|
||||
})
|
||||
case "/wallet/getnowblock":
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"block_header": map[string]interface{}{
|
||||
"raw_data": map[string]interface{}{"number": 110},
|
||||
},
|
||||
})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err = dao.Mdb.Create(&mdb.RpcNode{
|
||||
Network: mdb.NetworkTron,
|
||||
Type: mdb.RpcNodeTypeHttp,
|
||||
Url: server.URL,
|
||||
Enabled: true,
|
||||
Status: mdb.RpcNodeStatusOk,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create tron rpc node: %v", err)
|
||||
}
|
||||
if err = dao.Mdb.Create(&mdb.ChainToken{
|
||||
Network: mdb.NetworkTron,
|
||||
Symbol: "USDT",
|
||||
ContractAddress: contractAddress,
|
||||
Decimals: 6,
|
||||
Enabled: true,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create token: %v", err)
|
||||
}
|
||||
order := &mdb.Orders{
|
||||
TradeId: "manual-tron-http-flow",
|
||||
OrderId: "manual-tron-http-flow",
|
||||
ActualAmount: 1.23,
|
||||
ReceiveAddress: recipientAddress,
|
||||
Token: "USDT",
|
||||
Network: mdb.NetworkTron,
|
||||
Status: mdb.StatusWaitPay,
|
||||
PayProvider: mdb.PaymentProviderOnChain,
|
||||
}
|
||||
if err = dao.Mdb.Create(order).Error; err != nil {
|
||||
t.Fatalf("create order: %v", err)
|
||||
}
|
||||
|
||||
got, err := ValidateManualOrderPayment(order, "0X"+strings.ToUpper(txID))
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateManualOrderPayment(): %v", err)
|
||||
}
|
||||
if got != txID {
|
||||
t.Fatalf("canonical tx id = %q, want %q", got, txID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualVerifySolanaRejectsMissingBlockTime(t *testing.T) {
|
||||
order := &mdb.Orders{BaseModel: mdb.BaseModel{CreatedAt: *carbon.NewTime(carbon.CreateFromTimestampMilli(time.Now().UnixMilli()))}}
|
||||
if err := ensureSolanaTransferNotBeforeOrder(0, order); err == nil {
|
||||
t.Fatal("expected missing block time to fail")
|
||||
}
|
||||
if err := ensureSolanaTransferNotBeforeOrder(time.Now().Add(time.Minute).Unix(), order); err != nil {
|
||||
t.Fatalf("expected future block time to pass: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeEvmReader struct {
|
||||
receipt *types.Receipt
|
||||
receiptErr error
|
||||
headers map[string]*types.Header
|
||||
headerErr error
|
||||
}
|
||||
|
||||
func (f *fakeEvmReader) TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error) {
|
||||
if f.receiptErr != nil {
|
||||
return nil, f.receiptErr
|
||||
}
|
||||
return f.receipt, nil
|
||||
}
|
||||
|
||||
func (f *fakeEvmReader) HeaderByNumber(_ context.Context, number *big.Int) (*types.Header, error) {
|
||||
if f.headerErr != nil {
|
||||
return nil, f.headerErr
|
||||
}
|
||||
key := "latest"
|
||||
if number != nil {
|
||||
key = number.String()
|
||||
}
|
||||
header := f.headers[key]
|
||||
if header == nil {
|
||||
return nil, fmt.Errorf("missing header %s", key)
|
||||
}
|
||||
return header, nil
|
||||
}
|
||||
|
||||
func manualTronTransactionFromCallData(t *testing.T, contractHex, recipientHex string, amount *big.Int) manualTronTransaction {
|
||||
t.Helper()
|
||||
body := strings.TrimPrefix(recipientHex, "41")
|
||||
raw := fmt.Sprintf(`{
|
||||
"raw_data": {
|
||||
"contract": [{
|
||||
"type": "TriggerSmartContract",
|
||||
"parameter": {
|
||||
"value": {
|
||||
"contract_address": %q,
|
||||
"data": %q
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}`, contractHex, "a9059cbb"+strings.Repeat("0", 24)+body+fmt.Sprintf("%064x", amount))
|
||||
var tx manualTronTransaction
|
||||
if err := json.Unmarshal([]byte(raw), &tx); err != nil {
|
||||
t.Fatalf("unmarshal tron tx: %v", err)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func createOkPayDepositOrder(uniqueID string, amount float64, coin string, retur
|
||||
form := map[string]string{
|
||||
"unique_id": uniqueID,
|
||||
"name": uniqueID,
|
||||
"amount": fmt.Sprintf("%.2f", amount),
|
||||
"amount": fmt.Sprintf("%.*f", data.GetAmountPrecision(), amount),
|
||||
"coin": strings.ToUpper(strings.TrimSpace(coin)),
|
||||
"callback_url": callbackURL,
|
||||
"return_url": returnURL,
|
||||
@@ -254,8 +254,9 @@ func HandleOkPayNotify(form map[string]string, rawFormData string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid okpay amount: %w", err)
|
||||
}
|
||||
if fmt.Sprintf("%.2f", notifyAmount) != fmt.Sprintf("%.2f", order.ActualAmount) {
|
||||
return fmt.Errorf("okpay amount mismatch: got=%.2f want=%.2f", notifyAmount, order.ActualAmount)
|
||||
precision := data.GetAmountPrecision()
|
||||
if fmt.Sprintf("%.*f", precision, notifyAmount) != fmt.Sprintf("%.*f", precision, order.ActualAmount) {
|
||||
return fmt.Errorf("okpay amount mismatch: got=%.*f want=%.*f", precision, notifyAmount, precision, order.ActualAmount)
|
||||
}
|
||||
|
||||
err = OrderProcessing(&request.OrderProcessingRequest{
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
const (
|
||||
CnyMinimumPaymentAmount = 0.01
|
||||
UsdtMinimumPaymentAmount = 0.01
|
||||
UsdtAmountPerIncrement = 0.01
|
||||
IncrementalMaximumNumber = 100
|
||||
)
|
||||
|
||||
@@ -61,7 +60,8 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
|
||||
token := strings.ToUpper(strings.TrimSpace(req.Token))
|
||||
currency := strings.ToUpper(strings.TrimSpace(req.Currency))
|
||||
network := strings.ToLower(strings.TrimSpace(req.Network))
|
||||
payAmount := math.MustParsePrecFloat64(req.Amount, 2)
|
||||
amountPrecision := data.GetAmountPrecision()
|
||||
payAmount := math.MustParsePrecFloat64(req.Amount, amountPrecision)
|
||||
rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(currency))
|
||||
if rate <= 0 {
|
||||
return nil, constant.RateAmountErr
|
||||
@@ -96,7 +96,7 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
|
||||
}
|
||||
|
||||
tradeID := GenerateCode()
|
||||
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
|
||||
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), amountPrecision)
|
||||
availableAddress, availableAmount, err := ReserveAvailableWalletAndAmount(tradeID, network, token, amount, walletAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -109,7 +109,7 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
|
||||
order := &mdb.Orders{
|
||||
TradeId: tradeID,
|
||||
OrderId: req.OrderId,
|
||||
Amount: req.Amount,
|
||||
Amount: payAmount,
|
||||
Currency: currency,
|
||||
ActualAmount: availableAmount,
|
||||
ReceiveAddress: availableAddress,
|
||||
@@ -280,6 +280,7 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
|
||||
func ReserveAvailableWalletAndAmount(tradeID string, network string, token string, amount float64, walletAddress []mdb.WalletAddress) (string, float64, error) {
|
||||
availableAddress := ""
|
||||
availableAmount := amount
|
||||
amountPrecision := data.GetAmountPrecision()
|
||||
|
||||
tryLockWalletFunc := func(targetAmount float64) (string, error) {
|
||||
for _, address := range walletAddress {
|
||||
@@ -303,8 +304,8 @@ func ReserveAvailableWalletAndAmount(tradeID string, network string, token strin
|
||||
}
|
||||
if address == "" {
|
||||
decimalOldAmount := decimal.NewFromFloat(availableAmount)
|
||||
decimalIncr := decimal.NewFromFloat(UsdtAmountPerIncrement)
|
||||
availableAmount = decimalOldAmount.Add(decimalIncr).InexactFloat64()
|
||||
decimalIncr := decimal.New(1, int32(-amountPrecision))
|
||||
availableAmount = math.MustParsePrecFloat64(decimalOldAmount.Add(decimalIncr).InexactFloat64(), amountPrecision)
|
||||
continue
|
||||
}
|
||||
availableAddress = address
|
||||
@@ -414,7 +415,7 @@ func SwitchNetwork(req *request.SwitchNetworkRequest) (*response.CheckoutCounter
|
||||
}
|
||||
|
||||
subTradeID := GenerateCode()
|
||||
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
|
||||
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), data.GetAmountPrecision())
|
||||
availableAddress, availableAmount, err := ReserveAvailableWalletAndAmount(subTradeID, network, token, amount, walletAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -531,7 +532,7 @@ func switchToOkPay(parent *mdb.Orders, token string) (*response.CheckoutCounterR
|
||||
}
|
||||
|
||||
subTradeID := GenerateCode()
|
||||
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
|
||||
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), data.GetAmountPrecision())
|
||||
returnURL := strings.TrimSpace(parent.RedirectUrl)
|
||||
if returnURL == "" {
|
||||
returnURL = data.GetOkPayReturnURL()
|
||||
|
||||
@@ -97,6 +97,59 @@ func TestCreateTransactionAssignsIncrementedAmountsAndLocks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTransactionUsesConfiguredAmountPrecision(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := data.SetSetting(mdb.SettingGroupSystem, mdb.SettingKeyAmountPrecision, "4", mdb.SettingTypeInt); err != nil {
|
||||
t.Fatalf("set amount precision: %v", err)
|
||||
}
|
||||
if _, err := data.AddWalletAddress("wallet_precision_1"); err != nil {
|
||||
t.Fatalf("add wallet: %v", err)
|
||||
}
|
||||
|
||||
resp1, err := CreateTransaction(newCreateTransactionRequest("order_precision_1", 1), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("create first transaction: %v", err)
|
||||
}
|
||||
resp2, err := CreateTransaction(newCreateTransactionRequest("order_precision_2", 1), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("create second transaction: %v", err)
|
||||
}
|
||||
|
||||
if got := fmt.Sprintf("%.4f", resp1.ActualAmount); got != "1.0000" {
|
||||
t.Fatalf("first actual amount = %s, want 1.0000", got)
|
||||
}
|
||||
if got := fmt.Sprintf("%.4f", resp2.ActualAmount); got != "1.0001" {
|
||||
t.Fatalf("second actual amount = %s, want 1.0001", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTransactionStoresNormalizedMerchantAmount(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if _, err := data.AddWalletAddress("wallet_normalized_1"); err != nil {
|
||||
t.Fatalf("add wallet: %v", err)
|
||||
}
|
||||
|
||||
resp, err := CreateTransaction(newCreateTransactionRequest("order_normalized_1", 100.129), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("create transaction: %v", err)
|
||||
}
|
||||
if got := fmt.Sprintf("%.2f", resp.Amount); got != "100.13" {
|
||||
t.Fatalf("response amount = %s, want 100.13", got)
|
||||
}
|
||||
|
||||
order, err := data.GetOrderInfoByTradeId(resp.TradeId)
|
||||
if err != nil {
|
||||
t.Fatalf("load order: %v", err)
|
||||
}
|
||||
if got := fmt.Sprintf("%.2f", order.Amount); got != "100.13" {
|
||||
t.Fatalf("stored amount = %s, want 100.13", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTransactionUsesRateAPIWhenForcedSettingIsNotPositive(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/GMWalletApp/epusdt/config"
|
||||
"github.com/GMWalletApp/epusdt/model/data"
|
||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||
"github.com/GMWalletApp/epusdt/model/response"
|
||||
"github.com/GMWalletApp/epusdt/util/constant"
|
||||
)
|
||||
|
||||
var ErrOrder = errors.New("不存在待支付订单或已过期")
|
||||
// ErrOrder is returned when checkout initialization cannot find the trade id.
|
||||
var ErrOrder = constant.OrderNotExists
|
||||
|
||||
// GetCheckoutCounterByTradeId returns checkout info for a pending order.
|
||||
// GetCheckoutCounterByTradeId returns checkout initialization data for an existing order.
|
||||
// It does not decide the payment state; callers should use CheckStatus for that.
|
||||
func GetCheckoutCounterByTradeId(tradeId string) (*response.CheckoutCounterResponse, error) {
|
||||
orderInfo, err := data.GetOrderInfoByTradeId(tradeId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if orderInfo.ID <= 0 || orderInfo.Status != mdb.StatusWaitPay {
|
||||
if orderInfo.ID <= 0 {
|
||||
return nil, ErrOrder
|
||||
}
|
||||
|
||||
|
||||
@@ -471,7 +471,7 @@ const (
|
||||
Token2022ProgramID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
||||
)
|
||||
|
||||
// ADJustAmount 将链上原始金额转为可读金额(除以 10^decimals,保留 2 位小数)
|
||||
// ADJustAmount 将链上原始金额转为可读金额(除以 10^decimals,保留到系统支持的最大匹配精度)
|
||||
func ADJustAmount(amount uint64, decimals int) float64 {
|
||||
if amount == 0 {
|
||||
return 0
|
||||
@@ -480,8 +480,7 @@ func ADJustAmount(amount uint64, decimals int) float64 {
|
||||
// 10^decimals
|
||||
decimalDivisor := decimal.New(1, int32(decimals))
|
||||
adjustedAmount := decimalAmount.Div(decimalDivisor)
|
||||
// Round to 2 decimal places
|
||||
return math.MustParsePrecFloat64(adjustedAmount.InexactFloat64(), 2)
|
||||
return math.MustParsePrecFloat64(adjustedAmount.InexactFloat64(), data.MaxAmountPrecision)
|
||||
}
|
||||
|
||||
func MatchUsdtAtaAddress(address string, ataTo string) bool {
|
||||
|
||||
@@ -236,7 +236,7 @@ func TestAdjustAmount(t *testing.T) {
|
||||
name: "USDT amount (6 decimals)",
|
||||
amount: 123456789,
|
||||
decimals: 6,
|
||||
want: 123.46,
|
||||
want: 123.456789,
|
||||
},
|
||||
{
|
||||
name: "USDC amount (6 decimals)",
|
||||
@@ -260,7 +260,7 @@ func TestAdjustAmount(t *testing.T) {
|
||||
name: "Small amount",
|
||||
amount: 1,
|
||||
decimals: 6,
|
||||
want: 0.0,
|
||||
want: 0.000001,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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(), data.MaxAmountPrecision)
|
||||
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) {
|
||||
@@ -109,7 +126,7 @@ func TryProcessTronTRXTransfer(toAddr string, rawSun int64, txHash string, block
|
||||
}
|
||||
|
||||
decimalQuant := decimal.NewFromInt(rawSun)
|
||||
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1_000_000)).InexactFloat64(), 2)
|
||||
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1_000_000)).InexactFloat64(), data.MaxAmountPrecision)
|
||||
if amount <= 0 {
|
||||
return
|
||||
}
|
||||
@@ -178,71 +195,42 @@ 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:
|
||||
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
|
||||
}
|
||||
|
||||
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())
|
||||
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
|
||||
}
|
||||
|
||||
net := evmChainLogLabel(chainNetwork)
|
||||
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)
|
||||
amount := math.MustParsePrecFloat64(decimalQuant.Div(pow).InexactFloat64(), data.MaxAmountPrecision)
|
||||
if amount <= 0 {
|
||||
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)
|
||||
|
||||
@@ -305,11 +293,13 @@ func sendPaymentNotification(order *mdb.Orders) {
|
||||
}
|
||||
}
|
||||
|
||||
precision := data.GetAmountPrecision()
|
||||
amountFormat := fmt.Sprintf("%%.%df", precision)
|
||||
msg := fmt.Sprintf(
|
||||
"🎉 <b>收款成功通知</b>\n\n"+
|
||||
"💰 <b>金额信息</b>\n"+
|
||||
"├ 订单金额:<code>%.2f %s</code>\n"+
|
||||
"└ 实际到账:<code>%.2f %s</code>\n\n"+
|
||||
"├ 订单金额:<code>"+amountFormat+" %s</code>\n"+
|
||||
"└ 实际到账:<code>"+amountFormat+" %s</code>\n\n"+
|
||||
"📋 <b>订单信息</b>\n"+
|
||||
"├ 交易号:<code>%s</code>\n"+
|
||||
"├ 订单号:<code>%s</code>\n"+
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package route
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/GMWalletApp/epusdt/model/dao"
|
||||
"github.com/GMWalletApp/epusdt/model/data"
|
||||
"github.com/GMWalletApp/epusdt/model/mdb"
|
||||
"github.com/GMWalletApp/epusdt/model/service"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
@@ -647,6 +649,138 @@ func TestAdminOrders_MarkPaidNotFound(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminOrders_MarkPaidSuccessAfterVerification(t *testing.T) {
|
||||
e, token := setupAdminTestEnv(t)
|
||||
order := &mdb.Orders{
|
||||
TradeId: "trade-admin-mark-paid-ok",
|
||||
OrderId: "order-admin-mark-paid-ok",
|
||||
Amount: 10,
|
||||
Currency: "CNY",
|
||||
ActualAmount: 1.23,
|
||||
ReceiveAddress: "TTestTronAddress001",
|
||||
Token: "USDT",
|
||||
Network: mdb.NetworkTron,
|
||||
Status: mdb.StatusWaitPay,
|
||||
NotifyUrl: "https://merchant.example/notify",
|
||||
PayProvider: mdb.PaymentProviderOnChain,
|
||||
}
|
||||
if err := dao.Mdb.Create(order).Error; err != nil {
|
||||
t.Fatalf("create order: %v", err)
|
||||
}
|
||||
|
||||
verified := false
|
||||
restore := service.SetManualOrderPaymentValidatorForTest(func(got *mdb.Orders, blockID string) (string, error) {
|
||||
verified = true
|
||||
if got.TradeId != order.TradeId {
|
||||
t.Fatalf("validator trade_id = %s, want %s", got.TradeId, order.TradeId)
|
||||
}
|
||||
if blockID != "block-admin-ok" {
|
||||
t.Fatalf("validator block id = %s, want block-admin-ok", blockID)
|
||||
}
|
||||
return "canonical-block-admin-ok", nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
rec := doPostAdmin(e, "/admin/api/v1/orders/"+order.TradeId+"/mark-paid", map[string]interface{}{
|
||||
"block_transaction_id": "block-admin-ok",
|
||||
}, token)
|
||||
t.Logf("MarkOrderPaid success: status=%d body=%s", rec.Code, rec.Body.String())
|
||||
assertOK(t, rec)
|
||||
if !verified {
|
||||
t.Fatal("expected chain verifier to be called")
|
||||
}
|
||||
|
||||
paid, err := data.GetOrderInfoByTradeId(order.TradeId)
|
||||
if err != nil {
|
||||
t.Fatalf("reload order: %v", err)
|
||||
}
|
||||
if paid.Status != mdb.StatusPaySuccess {
|
||||
t.Fatalf("status = %d, want %d", paid.Status, mdb.StatusPaySuccess)
|
||||
}
|
||||
if paid.BlockTransactionId != "canonical-block-admin-ok" {
|
||||
t.Fatalf("block_transaction_id = %q", paid.BlockTransactionId)
|
||||
}
|
||||
if paid.CallBackConfirm != mdb.CallBackConfirmNo {
|
||||
t.Fatalf("callback_confirm = %d, want %d", paid.CallBackConfirm, mdb.CallBackConfirmNo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminOrders_MarkPaidRejectsVerificationFailure(t *testing.T) {
|
||||
e, token := setupAdminTestEnv(t)
|
||||
order := &mdb.Orders{
|
||||
TradeId: "trade-admin-mark-paid-bad-proof",
|
||||
OrderId: "order-admin-mark-paid-bad-proof",
|
||||
Amount: 10,
|
||||
Currency: "CNY",
|
||||
ActualAmount: 1.23,
|
||||
ReceiveAddress: "TTestTronAddress001",
|
||||
Token: "USDT",
|
||||
Network: mdb.NetworkTron,
|
||||
Status: mdb.StatusWaitPay,
|
||||
NotifyUrl: "https://merchant.example/notify",
|
||||
PayProvider: mdb.PaymentProviderOnChain,
|
||||
}
|
||||
if err := dao.Mdb.Create(order).Error; err != nil {
|
||||
t.Fatalf("create order: %v", err)
|
||||
}
|
||||
restore := service.SetManualOrderPaymentValidatorForTest(func(*mdb.Orders, string) (string, error) {
|
||||
return "", errors.New("transaction amount mismatch")
|
||||
})
|
||||
defer restore()
|
||||
|
||||
rec := doPostAdmin(e, "/admin/api/v1/orders/"+order.TradeId+"/mark-paid", map[string]interface{}{
|
||||
"block_transaction_id": "block-admin-bad",
|
||||
}, token)
|
||||
t.Logf("MarkOrderPaid verifier failure: status=%d body=%s", rec.Code, rec.Body.String())
|
||||
if rec.Code == http.StatusOK {
|
||||
t.Fatalf("expected failure, got 200: %s", rec.Body.String())
|
||||
}
|
||||
reloaded, err := data.GetOrderInfoByTradeId(order.TradeId)
|
||||
if err != nil {
|
||||
t.Fatalf("reload order: %v", err)
|
||||
}
|
||||
if reloaded.Status != mdb.StatusWaitPay || reloaded.BlockTransactionId != "" {
|
||||
t.Fatalf("order changed after failed verification: status=%d block=%q", reloaded.Status, reloaded.BlockTransactionId)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminOrders_MarkPaidRejectsNonOnChainOrder(t *testing.T) {
|
||||
e, token := setupAdminTestEnv(t)
|
||||
order := &mdb.Orders{
|
||||
TradeId: "trade-admin-mark-paid-provider",
|
||||
OrderId: "order-admin-mark-paid-provider",
|
||||
Amount: 10,
|
||||
Currency: "CNY",
|
||||
ActualAmount: 1.23,
|
||||
ReceiveAddress: "TTestTronAddress001",
|
||||
Token: "USDT",
|
||||
Network: mdb.NetworkTron,
|
||||
Status: mdb.StatusWaitPay,
|
||||
NotifyUrl: "https://merchant.example/notify",
|
||||
PayProvider: mdb.PaymentProviderOkPay,
|
||||
}
|
||||
if err := dao.Mdb.Create(order).Error; err != nil {
|
||||
t.Fatalf("create order: %v", err)
|
||||
}
|
||||
called := false
|
||||
restore := service.SetManualOrderPaymentValidatorForTest(func(*mdb.Orders, string) (string, error) {
|
||||
called = true
|
||||
return "block-admin-provider", nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
rec := doPostAdmin(e, "/admin/api/v1/orders/"+order.TradeId+"/mark-paid", map[string]interface{}{
|
||||
"block_transaction_id": "block-admin-provider",
|
||||
}, token)
|
||||
t.Logf("MarkOrderPaid provider order: status=%d body=%s", rec.Code, rec.Body.String())
|
||||
if rec.Code == http.StatusOK {
|
||||
t.Fatalf("expected failure, got 200: %s", rec.Body.String())
|
||||
}
|
||||
if called {
|
||||
t.Fatal("verifier should not run for non-on-chain order")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminOrders_ResendCallbackNotFound verifies resend-callback graceful error.
|
||||
func TestAdminOrders_ResendCallbackNotFound(t *testing.T) {
|
||||
e, token := setupAdminTestEnv(t)
|
||||
@@ -657,6 +791,59 @@ func TestAdminOrders_ResendCallbackNotFound(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminOrders_ResendCallbackRejectsEmptyNotifyURL(t *testing.T) {
|
||||
e, token := setupAdminTestEnv(t)
|
||||
order := &mdb.Orders{
|
||||
TradeId: "trade-admin-resend-empty-url",
|
||||
OrderId: "order-admin-resend-empty-url",
|
||||
Status: mdb.StatusPaySuccess,
|
||||
CallBackConfirm: mdb.CallBackConfirmOk,
|
||||
CallbackNum: 3,
|
||||
}
|
||||
if err := dao.Mdb.Create(order).Error; err != nil {
|
||||
t.Fatalf("create order: %v", err)
|
||||
}
|
||||
|
||||
rec := doPostAdmin(e, "/admin/api/v1/orders/"+order.TradeId+"/resend-callback", nil, token)
|
||||
t.Logf("ResendCallback empty notify_url: status=%d body=%s", rec.Code, rec.Body.String())
|
||||
if rec.Code == http.StatusOK {
|
||||
t.Fatalf("expected failure, got 200: %s", rec.Body.String())
|
||||
}
|
||||
reloaded, err := data.GetOrderInfoByTradeId(order.TradeId)
|
||||
if err != nil {
|
||||
t.Fatalf("reload order: %v", err)
|
||||
}
|
||||
if reloaded.CallBackConfirm != mdb.CallBackConfirmOk || reloaded.CallbackNum != 3 {
|
||||
t.Fatalf("callback state changed: confirm=%d num=%d", reloaded.CallBackConfirm, reloaded.CallbackNum)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminOrders_ResendCallbackRequeuesPaidOrder(t *testing.T) {
|
||||
e, token := setupAdminTestEnv(t)
|
||||
order := &mdb.Orders{
|
||||
TradeId: "trade-admin-resend-ok",
|
||||
OrderId: "order-admin-resend-ok",
|
||||
Status: mdb.StatusPaySuccess,
|
||||
NotifyUrl: "https://merchant.example/notify",
|
||||
CallBackConfirm: mdb.CallBackConfirmOk,
|
||||
CallbackNum: 3,
|
||||
}
|
||||
if err := dao.Mdb.Create(order).Error; err != nil {
|
||||
t.Fatalf("create order: %v", err)
|
||||
}
|
||||
|
||||
rec := doPostAdmin(e, "/admin/api/v1/orders/"+order.TradeId+"/resend-callback", nil, token)
|
||||
t.Logf("ResendCallback success: status=%d body=%s", rec.Code, rec.Body.String())
|
||||
assertOK(t, rec)
|
||||
reloaded, err := data.GetOrderInfoByTradeId(order.TradeId)
|
||||
if err != nil {
|
||||
t.Fatalf("reload order: %v", err)
|
||||
}
|
||||
if reloaded.CallBackConfirm != mdb.CallBackConfirmNo || reloaded.CallbackNum != 0 {
|
||||
t.Fatalf("callback state = confirm %d num %d, want no/0", reloaded.CallBackConfirm, reloaded.CallbackNum)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Dashboard ───────────────────────────────────────────────────────────────
|
||||
|
||||
// TestAdminDashboard_AllRoutes verifies all dashboard endpoints return 200.
|
||||
@@ -718,6 +905,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 +1019,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 +1056,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 ───────────────────────────────────────────────────
|
||||
|
||||
+179
-30
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/GMWalletApp/epusdt/util/http_client"
|
||||
"github.com/GMWalletApp/epusdt/util/log"
|
||||
"github.com/GMWalletApp/epusdt/util/sign"
|
||||
"github.com/dromara/carbon/v2"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/spf13/viper"
|
||||
@@ -495,6 +496,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 +543,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"])
|
||||
@@ -566,6 +579,12 @@ func TestGetPublicConfig(t *testing.T) {
|
||||
row := item.(map[string]interface{})
|
||||
network := row["network"].(string)
|
||||
seen[network] = true
|
||||
if _, ok := row["display_name"].(string); !ok {
|
||||
t.Fatalf("supported_assets[%s].display_name missing or not string: %#v", network, row["display_name"])
|
||||
}
|
||||
if network == "tron" && row["display_name"] != "TRON" {
|
||||
t.Fatalf("supported_assets.tron.display_name = %v, want TRON", row["display_name"])
|
||||
}
|
||||
}
|
||||
for _, n := range []string{"tron", "solana"} {
|
||||
if !seen[n] {
|
||||
@@ -977,51 +996,64 @@ func TestCheckStatus_NotFound(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
t.Logf("CheckStatus(not found): status=%d body=%s", rec.Code, rec.Body.String())
|
||||
if rec.Code >= 500 {
|
||||
t.Fatalf("unexpected server error: %d %s", rec.Code, rec.Body.String())
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if rec.Code == http.StatusNotFound && rec.Body.Len() == 0 {
|
||||
t.Fatalf("route returned 404 with empty body — route may not be registered")
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal check-status error response: %v", err)
|
||||
}
|
||||
if got := int(resp["status_code"].(float64)); got != 10008 {
|
||||
t.Fatalf("status_code = %d, want 10008; response=%v", got, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckStatus_WithOrder verifies /pay/check-status/:trade_id returns 200
|
||||
// and a status field when the order exists.
|
||||
// TestCheckStatus_WithOrder verifies /pay/check-status/:trade_id returns the
|
||||
// real order status for existing orders in all checkout-relevant states.
|
||||
func TestCheckStatus_WithOrder(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
|
||||
// Create an order first via the GMPAY route.
|
||||
body := signBody(map[string]interface{}{
|
||||
"order_id": "check-status-001",
|
||||
"amount": 1.00,
|
||||
"token": "usdt",
|
||||
"currency": "cny",
|
||||
"network": "tron",
|
||||
"notify_url": "http://localhost/notify",
|
||||
})
|
||||
createRec := doPost(e, "/payments/gmpay/v1/order/create-transaction", body)
|
||||
if createRec.Code != http.StatusOK {
|
||||
t.Fatalf("create order failed: %d %s", createRec.Code, createRec.Body.String())
|
||||
}
|
||||
var createResp map[string]interface{}
|
||||
json.Unmarshal(createRec.Body.Bytes(), &createResp)
|
||||
tradeId, _ := createResp["data"].(map[string]interface{})["trade_id"].(string)
|
||||
if tradeId == "" {
|
||||
t.Fatal("no trade_id in create response")
|
||||
cases := []struct {
|
||||
name string
|
||||
status int
|
||||
}{
|
||||
{name: "wait-pay", status: mdb.StatusWaitPay},
|
||||
{name: "paid", status: mdb.StatusPaySuccess},
|
||||
{name: "expired", status: mdb.StatusExpired},
|
||||
}
|
||||
|
||||
// Now check status.
|
||||
req := httptest.NewRequest(http.MethodGet, "/pay/check-status/"+tradeId, nil)
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tradeID := createCheckoutCounterRespTestOrder(t, e, "check-status-"+tc.name)
|
||||
if err := dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("trade_id = ?", tradeID).
|
||||
Update("status", tc.status).Error; err != nil {
|
||||
t.Fatalf("update status: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/pay/check-status/"+tradeID, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
t.Logf("CheckStatus: status=%d body=%s", rec.Code, rec.Body.String())
|
||||
t.Logf("CheckStatus(%s): status=%d body=%s", tc.name, rec.Code, rec.Body.String())
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["data"] == nil {
|
||||
t.Fatal("expected data in check-status response")
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal check-status response: %v", err)
|
||||
}
|
||||
data, ok := resp["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected data in check-status response, got: %v", resp)
|
||||
}
|
||||
if got, _ := data["trade_id"].(string); got != tradeID {
|
||||
t.Fatalf("trade_id = %q, want %q", got, tradeID)
|
||||
}
|
||||
if got := int(data["status"].(float64)); got != tc.status {
|
||||
t.Fatalf("status = %d, want %d", got, tc.status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1049,6 +1081,123 @@ func TestCheckoutCounter_NotFound(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutCounterResp_ReturnsPaidOrder(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
tradeID := createCheckoutCounterRespTestOrder(t, e, "checkout-counter-paid-001")
|
||||
|
||||
if err := dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("trade_id = ?", tradeID).
|
||||
Update("status", mdb.StatusPaySuccess).Error; err != nil {
|
||||
t.Fatalf("mark order paid: %v", err)
|
||||
}
|
||||
|
||||
data := getCheckoutCounterRespData(t, e, tradeID)
|
||||
if got, _ := data["trade_id"].(string); got != tradeID {
|
||||
t.Fatalf("trade_id = %q, want %q; data=%v", got, tradeID, data)
|
||||
}
|
||||
if got, _ := data["redirect_url"].(string); got != "https://merchant.example/return" {
|
||||
t.Fatalf("redirect_url = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutCounterResp_ReturnsExpiredOrder(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
tradeID := createCheckoutCounterRespTestOrder(t, e, "checkout-counter-expired-001")
|
||||
expiredCreatedAt := carbon.Now().SubMinutes(config.GetOrderExpirationTime() + 1)
|
||||
|
||||
if err := dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("trade_id = ?", tradeID).
|
||||
Updates(map[string]interface{}{
|
||||
"status": mdb.StatusExpired,
|
||||
"created_at": expiredCreatedAt,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("mark order expired: %v", err)
|
||||
}
|
||||
|
||||
data := getCheckoutCounterRespData(t, e, tradeID)
|
||||
if got, _ := data["trade_id"].(string); got != tradeID {
|
||||
t.Fatalf("trade_id = %q, want %q; data=%v", got, tradeID, data)
|
||||
}
|
||||
expirationTime, _ := data["expiration_time"].(float64)
|
||||
if int64(expirationTime) > carbon.Now().TimestampMilli() {
|
||||
t.Fatalf("expiration_time = %.0f, want expired timestamp", expirationTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutCounterResp_UnknownOrderReturnsClearError(t *testing.T) {
|
||||
e := setupTestEnv(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/pay/checkout-counter-resp/nonexistent-trade-id", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal checkout counter error response: %v", err)
|
||||
}
|
||||
if got := int(resp["status_code"].(float64)); got != 10008 {
|
||||
t.Fatalf("status_code = %d, want 10008; response=%v", got, resp)
|
||||
}
|
||||
if resp["message"] == "" {
|
||||
t.Fatalf("expected error message, got: %v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func createCheckoutCounterRespTestOrder(t *testing.T, e *echo.Echo, orderID string) string {
|
||||
t.Helper()
|
||||
|
||||
body := signBody(map[string]interface{}{
|
||||
"order_id": orderID,
|
||||
"amount": 1.00,
|
||||
"token": "usdt",
|
||||
"currency": "cny",
|
||||
"network": "tron",
|
||||
"notify_url": "http://localhost/notify",
|
||||
"redirect_url": "https://merchant.example/return",
|
||||
})
|
||||
rec := doPost(e, "/payments/gmpay/v1/order/create-transaction", body)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("create order failed: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal create response: %v", err)
|
||||
}
|
||||
data, ok := resp["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected create response data, got: %v", resp)
|
||||
}
|
||||
tradeID, _ := data["trade_id"].(string)
|
||||
if tradeID == "" {
|
||||
t.Fatalf("missing trade_id in create response: %v", resp)
|
||||
}
|
||||
return tradeID
|
||||
}
|
||||
|
||||
func getCheckoutCounterRespData(t *testing.T, e *echo.Echo, tradeID string) map[string]interface{} {
|
||||
t.Helper()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/pay/checkout-counter-resp/"+tradeID, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal checkout counter response: %v", err)
|
||||
}
|
||||
data, ok := resp["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected checkout counter data, got: %v", resp)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// TestSwitchNetwork_MissingFields verifies that /pay/switch-network validates
|
||||
// required fields and returns a graceful error when they are missing.
|
||||
func TestSwitchNetwork_MissingFields(t *testing.T) {
|
||||
|
||||
@@ -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 {
|
||||
@@ -287,7 +340,7 @@ type Scanner struct {
|
||||
lastBlock int64
|
||||
// 统计
|
||||
totalBlocks int64
|
||||
totalUSDTTxs int64
|
||||
totalTRC20Txs int64
|
||||
totalTRXTxs int64
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
for {
|
||||
scanner := NewScanner()
|
||||
if err := scanner.Init(); err != nil {
|
||||
log.Sugar.Errorf("[TRON-BLOCK] init: %v", err)
|
||||
return
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
import{t as e}from"./unauthorized-error-MToTZ-fm.js";var t=e;export{t as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{t as e}from"./unauthorized-error-CgOrMfGt.js";var t=e;export{t as component};
|
||||
@@ -0,0 +1 @@
|
||||
import{t as e}from"./forbidden-Dnfepx4V.js";var t=e;export{t as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{t as e}from"./forbidden-oyIuUuBn.js";var t=e;export{t as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{t as e}from"./not-found-error-mipAgOS5.js";var t=e;export{t as component};
|
||||
@@ -0,0 +1 @@
|
||||
import{t as e}from"./not-found-error-Zz2FacG5.js";var t=e;export{t as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{t as e}from"./general-error-BryI8zNx.js";var t=e;export{t as component};
|
||||
@@ -0,0 +1 @@
|
||||
import{t as e}from"./general-error-B5vyck1f.js";var t=e;export{t as component};
|
||||
@@ -0,0 +1 @@
|
||||
import{t as e}from"./maintenance-error-CSCtVDBb.js";var t=e;export{t as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{t as e}from"./maintenance-error-CECzFgNM.js";var t=e;export{t as component};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{$d as e,Xd as t,Yd as n}from"./messages-kZy_2fG-.js";import{o as r,s as i,t as a}from"./useRouter-BFRHcVKY.js";import{t as o}from"./useStore-BegLNH-k.js";import{f as s}from"./ClientOnly-CKH5SArO.js";import"./redirect-dE0wk0WH.js";import{a as c,i as l,r as u,s as d,t as f}from"./Match-B0WegrOo.js";import{n as p}from"./matchContext-Cxt9JPh-.js";import{t as m}from"./atom-DI1Bn-1s.js";function h(e){if(typeof document<`u`&&document.querySelector){let t=e.stores.location.get(),n=t.state.__hashScrollIntoViewOptions??!0;if(n&&t.hash!==``){let e=document.getElementById(t.hash);e&&e.scrollIntoView(n)}}}var g=e(t(),1);function _(){let e=a(),t=g.useRef({router:e,mounted:!1}),[n,c]=g.useState(!1),l=o(e.stores.isLoading,e=>e),u=o(e.stores.hasPending,e=>e),f=i(l),p=l||n||u,_=i(p),v=l||u,y=i(v);return e.startTransition=e=>{c(!0),g.startTransition(()=>{e(),c(!1)})},g.useEffect(()=>{let t=e.history.subscribe(e.load),n=e.buildLocation({to:e.latestLocation.pathname,search:!0,params:!0,hash:!0,state:!0,_includeValidateSearch:!0});return s(e.latestLocation.publicHref)!==s(n.publicHref)&&e.commitLocation({...n,replace:!0}),()=>{t()}},[e,e.history]),r(()=>{typeof window<`u`&&e.ssr||t.current.router===e&&t.current.mounted||(t.current={router:e,mounted:!0},(async()=>{try{await e.load()}catch(e){console.error(e)}})())},[e]),r(()=>{f&&!l&&e.emit({type:`onLoad`,...d(e.stores.location.get(),e.stores.resolvedLocation.get())})},[f,e,l]),r(()=>{y&&!v&&e.emit({type:`onBeforeRouteMount`,...d(e.stores.location.get(),e.stores.resolvedLocation.get())})},[v,y,e]),r(()=>{if(_&&!p){let t=d(e.stores.location.get(),e.stores.resolvedLocation.get());e.emit({type:`onResolved`,...t}),m(()=>{e.stores.status.set(`idle`),e.stores.resolvedLocation.set(e.stores.location.get())}),t.hrefChanged&&h(e)}},[p,_,e]),null}var v=n();function y(){let e=a(),t=e.routesById.__root__.options.pendingComponent??e.options.defaultPendingComponent,n=t?(0,v.jsx)(t,{}):null,r=(0,v.jsxs)(typeof document<`u`&&e.ssr?u:g.Suspense,{fallback:n,children:[(0,v.jsx)(_,{}),(0,v.jsx)(b,{})]});return e.options.InnerWrap?(0,v.jsx)(e.options.InnerWrap,{children:r}):r}function b(){let e=a(),t=o(e.stores.firstId,e=>e),n=o(e.stores.loadedAt,e=>e),r=t?(0,v.jsx)(f,{matchId:t}):null;return(0,v.jsx)(p.Provider,{value:t,children:e.options.disableGlobalCatchBoundary?r:(0,v.jsx)(l,{getResetKey:()=>n,errorComponent:c,onCatch:void 0,children:r})})}function x(){let e=a();return o(e.stores.matchRouteDeps,e=>e),g.useCallback(t=>{let{pending:n,caseSensitive:r,fuzzy:i,includeSearch:a,...o}=t;return e.matchRoute(o,{pending:n,caseSensitive:r,fuzzy:i,includeSearch:a})},[e])}export{x as n,y as t};
|
||||
import{af as e,cf as t,if as n}from"./messages-BSgosaSa.js";import{o as r,s as i,t as a}from"./useRouter-BP80Cbua.js";import{t as o}from"./useStore-Exbiv5rk.js";import{f as s}from"./ClientOnly-Z4uY-q9B.js";import"./redirect-dE0wk0WH.js";import{a as c,i as l,r as u,s as d,t as f}from"./Match-CiZKPccq.js";import{n as p}from"./matchContext-B9SKKWgV.js";import{t as m}from"./atom-DI1Bn-1s.js";function h(e){if(typeof document<`u`&&document.querySelector){let t=e.stores.location.get(),n=t.state.__hashScrollIntoViewOptions??!0;if(n&&t.hash!==``){let e=document.getElementById(t.hash);e&&e.scrollIntoView(n)}}}var g=t(e(),1);function _(){let e=a(),t=g.useRef({router:e,mounted:!1}),[n,c]=g.useState(!1),l=o(e.stores.isLoading,e=>e),u=o(e.stores.hasPending,e=>e),f=i(l),p=l||n||u,_=i(p),v=l||u,y=i(v);return e.startTransition=e=>{c(!0),g.startTransition(()=>{e(),c(!1)})},g.useEffect(()=>{let t=e.history.subscribe(e.load),n=e.buildLocation({to:e.latestLocation.pathname,search:!0,params:!0,hash:!0,state:!0,_includeValidateSearch:!0});return s(e.latestLocation.publicHref)!==s(n.publicHref)&&e.commitLocation({...n,replace:!0}),()=>{t()}},[e,e.history]),r(()=>{typeof window<`u`&&e.ssr||t.current.router===e&&t.current.mounted||(t.current={router:e,mounted:!0},(async()=>{try{await e.load()}catch(e){console.error(e)}})())},[e]),r(()=>{f&&!l&&e.emit({type:`onLoad`,...d(e.stores.location.get(),e.stores.resolvedLocation.get())})},[f,e,l]),r(()=>{y&&!v&&e.emit({type:`onBeforeRouteMount`,...d(e.stores.location.get(),e.stores.resolvedLocation.get())})},[v,y,e]),r(()=>{if(_&&!p){let t=d(e.stores.location.get(),e.stores.resolvedLocation.get());e.emit({type:`onResolved`,...t}),m(()=>{e.stores.status.set(`idle`),e.stores.resolvedLocation.set(e.stores.location.get())}),t.hrefChanged&&h(e)}},[p,_,e]),null}var v=n();function y(){let e=a(),t=e.routesById.__root__.options.pendingComponent??e.options.defaultPendingComponent,n=t?(0,v.jsx)(t,{}):null,r=(0,v.jsxs)(typeof document<`u`&&e.ssr?u:g.Suspense,{fallback:n,children:[(0,v.jsx)(_,{}),(0,v.jsx)(b,{})]});return e.options.InnerWrap?(0,v.jsx)(e.options.InnerWrap,{children:r}):r}function b(){let e=a(),t=o(e.stores.firstId,e=>e),n=o(e.stores.loadedAt,e=>e),r=t?(0,v.jsx)(f,{matchId:t}):null;return(0,v.jsx)(p.Provider,{value:t,children:e.options.disableGlobalCatchBoundary?r:(0,v.jsx)(l,{getResetKey:()=>n,errorComponent:c,onCatch:void 0,children:r})})}function x(){let e=a();return o(e.stores.matchRouteDeps,e=>e),g.useCallback(t=>{let{pending:n,caseSensitive:r,fuzzy:i,includeSearch:a,...o}=t;return e.matchRoute(o,{pending:n,caseSensitive:r,fuzzy:i,includeSearch:a})},[e])}export{x as n,y as t};
|
||||
@@ -0,0 +1 @@
|
||||
import{if as e}from"./messages-BSgosaSa.js";import{m as t}from"./search-provider-Dt_F-pkS.js";import{n,t as r}from"./theme-switch-DzxlVGFK.js";import{t as i}from"./general-error-B5vyck1f.js";import{t as a}from"./not-found-error-Zz2FacG5.js";import{t as o}from"./_error-DfUrvDhK.js";import{t as s}from"./unauthorized-error-MToTZ-fm.js";import{t as c}from"./forbidden-Dnfepx4V.js";import{t as l}from"./maintenance-error-CSCtVDBb.js";import{n as u,r as d,t as f}from"./header-BoelcHSz.js";var p=e();function m(){let{error:e}=o.useParams(),m={unauthorized:s,forbidden:c,"not-found":a,"internal-server-error":i,"maintenance-error":l}[e]||a;return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(f,{className:`border-b`,fixed:!0,children:[(0,p.jsx)(u,{}),(0,p.jsxs)(`div`,{className:`ms-auto flex items-center space-x-4`,children:[(0,p.jsx)(n,{}),(0,p.jsx)(r,{}),(0,p.jsx)(t,{}),(0,p.jsx)(d,{})]})]}),(0,p.jsx)(`div`,{className:`flex-1 [&>div]:h-full`,children:(0,p.jsx)(m,{})})]})}export{m as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{Yd as e}from"./messages-kZy_2fG-.js";import{m as t}from"./search-provider-BLfAXVyy.js";import{n,t as r}from"./theme-switch-BhK5XnmV.js";import{t as i}from"./general-error-BryI8zNx.js";import{t as a}from"./not-found-error-mipAgOS5.js";import{t as o}from"./_error-DwHJp4LF.js";import{t as s}from"./unauthorized-error-CgOrMfGt.js";import{t as c}from"./forbidden-oyIuUuBn.js";import{t as l}from"./maintenance-error-CECzFgNM.js";import{n as u,r as d,t as f}from"./header-sQjuQN7h.js";var p=e();function m(){let{error:e}=o.useParams(),m={unauthorized:s,forbidden:c,"not-found":a,"internal-server-error":i,"maintenance-error":l}[e]||a;return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(f,{className:`border-b`,fixed:!0,children:[(0,p.jsx)(u,{}),(0,p.jsxs)(`div`,{className:`ms-auto flex items-center space-x-4`,children:[(0,p.jsx)(n,{}),(0,p.jsx)(r,{}),(0,p.jsx)(t,{}),(0,p.jsx)(d,{})]})]}),(0,p.jsx)(`div`,{className:`flex-1 [&>div]:h-full`,children:(0,p.jsx)(m,{})})]})}export{m as component};
|
||||
@@ -0,0 +1,2 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_error-B6Nmwvub.js","assets/messages-BSgosaSa.js","assets/search-provider-Dt_F-pkS.js","assets/dist-BTQ1T4Z1.js","assets/dist-BX7-FFVx.js","assets/confirm-dialog-QFIvwt2-.js","assets/button-gsP5wmrs.js","assets/dist-C-eeUszQ.js","assets/dist-2NlRabHs.js","assets/dist-C3yztW6j.js","assets/dist-fWONwgan.js","assets/es2015-BoIOHfI5.js","assets/dist-CnRb3IO0.js","assets/dist-I_iv6zg5.js","assets/dist-VmAyFekA.js","assets/dist-ywuA9477.js","assets/dist-AW7rpZT7.js","assets/dist-BIHhvYX3.js","assets/dist-8mLdRaED.js","assets/separator-Ba9h8msf.js","assets/tooltip-DUTki-7-.js","assets/dist-FnsKqpTh.js","assets/dist-C1p3aui5.js","assets/auth-store-BFY9rMhZ.js","assets/dist-CFhr9Gp2.js","assets/with-selector-dqI4ImyX.js","assets/cookies-DtK8C3EL.js","assets/useRouter-BP80Cbua.js","assets/useNavigate-BNXxwDKM.js","assets/useStore-Exbiv5rk.js","assets/command-BOJV7P-9.js","assets/createLucideIcon-D--4ckBc.js","assets/x-Bm8bW9NP.js","assets/skeleton-UXXshNZm.js","assets/wallet-nm6u5kXR.js","assets/sun-BoHcAocg.js","assets/shield-check-Mb6GAKRJ.js","assets/logo-DvJMKKKo.js","assets/input-C0_2W2RZ.js","assets/font-provider-BJBBVY9T.js","assets/theme-provider-B5C7xoKA.js","assets/theme-switch-DzxlVGFK.js","assets/dropdown-menu-DLlHTZjq.js","assets/check-Cg6qKB39.js","assets/header-BoelcHSz.js","assets/link-fq4EwgoM.js","assets/ClientOnly-Z4uY-q9B.js","assets/forbidden-Dnfepx4V.js","assets/general-error-B5vyck1f.js","assets/maintenance-error-CSCtVDBb.js","assets/not-found-error-Zz2FacG5.js","assets/unauthorized-error-MToTZ-fm.js"])))=>i.map(i=>d[i]);
|
||||
import{n as e,r as t,t as n}from"./preload-helper-BeOhTAPJ.js";var r=t(`/_authenticated/errors/$error`)({component:e(()=>n(()=>import(`./_error-B6Nmwvub.js`),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51])),`component`)});export{r as t};
|
||||
@@ -1,2 +0,0 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_error-CEhiecHC.js","assets/messages-kZy_2fG-.js","assets/search-provider-BLfAXVyy.js","assets/dist-BOyWZkOA.js","assets/dist-e45zkog2.js","assets/confirm-dialog-N7FIzDUo.js","assets/button-CI1sqFDO.js","assets/dist-u5B8Q0Qw.js","assets/dist-Dh5ge3Zd.js","assets/dist-Dz_nSMrn.js","assets/dist-DTN-3dEq.js","assets/es2015-DJNqm-x3.js","assets/dist-BjEfRG8A.js","assets/dist-D_VX1DQ0.js","assets/dist-BB2cKiV1.js","assets/dist-Cc5kEhoc.js","assets/dist-HFEBkKCd.js","assets/dist-8VtOfdBX.js","assets/separator-vMwQK9t4.js","assets/tooltip--TK7dEdL.js","assets/dist-JMRFUguJ.js","assets/dist-vhc0iAwK.js","assets/auth-store-B7YG314a.js","assets/dist-A3SZ_duZ.js","assets/with-selector-Bq6p_41G.js","assets/cookies-C6BqVyqT.js","assets/useRouter-BFRHcVKY.js","assets/useNavigate-DtHrQ0GU.js","assets/useStore-BegLNH-k.js","assets/command-St1f4L7D.js","assets/createLucideIcon-CKkuOIuw.js","assets/x-O4huDRHF.js","assets/skeleton-CFm8kRls.js","assets/circle-check-XGTPrS6o.js","assets/sun-BSCMuhso.js","assets/shield-check-CzRgD9G5.js","assets/wallet-C_Rj46AZ.js","assets/logo-DxhHOFqN.js","assets/input-Bhusz9zP.js","assets/font-provider-BWVtH5eQ.js","assets/theme-provider-BQCmm5ub.js","assets/theme-switch-BhK5XnmV.js","assets/dropdown-menu-CPJ_eMIt.js","assets/check-jWXQT0HB.js","assets/header-sQjuQN7h.js","assets/link-Dw1SMkr2.js","assets/ClientOnly-CKH5SArO.js","assets/forbidden-oyIuUuBn.js","assets/general-error-BryI8zNx.js","assets/maintenance-error-CECzFgNM.js","assets/not-found-error-mipAgOS5.js","assets/unauthorized-error-CgOrMfGt.js"])))=>i.map(i=>d[i]);
|
||||
import{n as e,r as t,t as n}from"./preload-helper-CGevTkrx.js";var r=t(`/_authenticated/errors/$error`)({component:e(()=>n(()=>import(`./_error-CEhiecHC.js`),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51])),`component`)});export{r as t};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_trade_id-DcU1zP3I.js","assets/messages-BSgosaSa.js","assets/button-gsP5wmrs.js","assets/select-L52xnO1_.js","assets/dist-FnsKqpTh.js","assets/dist-BX7-FFVx.js","assets/dist-C3yztW6j.js","assets/dist-fWONwgan.js","assets/dist-8mLdRaED.js","assets/dist-BTQ1T4Z1.js","assets/dist-AW7rpZT7.js","assets/dist-I_iv6zg5.js","assets/dist-2NlRabHs.js","assets/es2015-BoIOHfI5.js","assets/dist-BIHhvYX3.js","assets/dist-C1p3aui5.js","assets/createLucideIcon-D--4ckBc.js","assets/check-Cg6qKB39.js","assets/chevron-down-gjYmTBfu.js","assets/auth-store-BFY9rMhZ.js","assets/dist-CFhr9Gp2.js","assets/with-selector-dqI4ImyX.js","assets/cookies-DtK8C3EL.js","assets/useNavigate-BNXxwDKM.js","assets/useRouter-BP80Cbua.js","assets/copy-to-clipboard-OC73aS5y.js","assets/arrow-left-Ob-XM86J.js","assets/radio-group-D5H97-KZ.js","assets/dist-VmAyFekA.js","assets/dist-CnRb3IO0.js","assets/dist-ywuA9477.js","assets/loader-circle-o4dbK3VT.js","assets/triangle-alert-k9A8_9KW.js","assets/x-Bm8bW9NP.js","assets/checkout-model-mW5oFIG-.js","assets/checkout-display-CSXzV3v2.js","assets/copy-DIIY1PDt.js"])))=>i.map(i=>d[i]);
|
||||
import{n as e,r as t,t as n}from"./preload-helper-BeOhTAPJ.js";var r=t(`/cashier/$trade_id/`)({component:e(()=>n(()=>import(`./_trade_id-DcU1zP3I.js`),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36])),`component`)});export{r as t};
|
||||
@@ -1,2 +0,0 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/_trade_id-DJoAcotW.js","assets/messages-kZy_2fG-.js","assets/button-CI1sqFDO.js","assets/select-RDYeL0gL.js","assets/dist-JMRFUguJ.js","assets/dist-e45zkog2.js","assets/dist-Dz_nSMrn.js","assets/dist-DTN-3dEq.js","assets/dist-8VtOfdBX.js","assets/dist-BOyWZkOA.js","assets/dist-Cc5kEhoc.js","assets/dist-D_VX1DQ0.js","assets/dist-Dh5ge3Zd.js","assets/es2015-DJNqm-x3.js","assets/dist-HFEBkKCd.js","assets/dist-vhc0iAwK.js","assets/createLucideIcon-CKkuOIuw.js","assets/check-jWXQT0HB.js","assets/chevron-down-CFeWMPkU.js","assets/auth-store-B7YG314a.js","assets/dist-A3SZ_duZ.js","assets/with-selector-Bq6p_41G.js","assets/cookies-C6BqVyqT.js","assets/useNavigate-DtHrQ0GU.js","assets/useRouter-BFRHcVKY.js","assets/copy-to-clipboard-oLBAmuxz.js","assets/arrow-left-CjjQKxui.js","assets/radio-group-ceeciZec.js","assets/dist-BB2cKiV1.js","assets/dist-BjEfRG8A.js","assets/loader-circle-CmKlBt5c.js","assets/x-O4huDRHF.js","assets/checkout-model-CxAw66kk.js"])))=>i.map(i=>d[i]);
|
||||
import{n as e,r as t,t as n}from"./preload-helper-CGevTkrx.js";import{t as r}from"./createLucideIcon-CKkuOIuw.js";var i=r(`triangle-alert`,[[`path`,{d:`m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3`,key:`wmoenq`}],[`path`,{d:`M12 9v4`,key:`juzpu7`}],[`path`,{d:`M12 17h.01`,key:`p32p05`}]]),a=t(`/cashier/$trade_id/`)({component:e(()=>n(()=>import(`./_trade_id-DJoAcotW.js`),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32])),`component`)});export{i as n,a as t};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{Ld as e,Rd as t,Yd as n,cn as r,dn as i,fn as a,ln as o,on as s,sn as c,un as l}from"./messages-kZy_2fG-.js";import{i as u,n as d,t as f}from"./button-CI1sqFDO.js";import{n as p,r as m}from"./font-provider-BWVtH5eQ.js";import{n as h}from"./theme-provider-BQCmm5ub.js";import{t as g}from"./chevron-down-CFeWMPkU.js";import{n as _,t as v}from"./radio-group-ceeciZec.js";import{r as y,s as b}from"./zod-BDZSAwfF.js";import{a as x,c as S,i as C,l as w,n as T,o as E,r as D,s as O,t as k}from"./form-Cb-bizIH.js";import{t as A}from"./page-header-Bto-ujL-.js";import{t as j}from"./show-submitted-data-BpvpfhNu.js";var M=n(),N=b({theme:y([`light`,`dark`]),font:y(m)});function P(){let{font:n,setFont:i}=p(),{theme:a,setTheme:y}=h(),b={theme:a,font:n},A=w({resolver:S(N),defaultValues:b});function P(e){e.font!==n&&i(e.font),e.theme!==a&&y(e.theme),j(e)}return(0,M.jsx)(k,{...A,children:(0,M.jsxs)(`form`,{className:`space-y-8`,onSubmit:A.handleSubmit(P),children:[(0,M.jsx)(C,{control:A.control,name:`font`,render:({field:e})=>(0,M.jsxs)(x,{children:[(0,M.jsx)(E,{children:l()}),(0,M.jsxs)(`div`,{className:`relative w-max`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(`select`,{className:u(d({variant:`outline`}),`w-50 appearance-none font-normal capitalize`,`dark:bg-background dark:hover:bg-background`),...e,children:m.map(e=>(0,M.jsx)(`option`,{value:e,children:e},e))})}),(0,M.jsx)(g,{className:`absolute inset-e-3 top-2.5 h-4 w-4 opacity-50`})]}),(0,M.jsx)(D,{className:`font-manrope`,children:o()}),(0,M.jsx)(O,{})]})}),(0,M.jsx)(C,{control:A.control,name:`theme`,render:({field:n})=>(0,M.jsxs)(x,{children:[(0,M.jsx)(E,{children:r()}),(0,M.jsx)(D,{children:c()}),(0,M.jsx)(O,{}),(0,M.jsxs)(v,{className:`grid max-w-md grid-cols-2 gap-8 pt-2`,defaultValue:n.value,onValueChange:n.onChange,children:[(0,M.jsx)(x,{children:(0,M.jsxs)(E,{className:`[&:has([data-state=checked])>div]:border-primary`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(_,{className:`sr-only`,value:`light`})}),(0,M.jsx)(`div`,{className:`items-center rounded-md border-2 border-muted p-1 hover:border-accent`,children:(0,M.jsxs)(`div`,{className:`space-y-2 rounded-sm bg-[#ecedef] p-2`,children:[(0,M.jsxs)(`div`,{className:`space-y-2 rounded-md bg-white p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-2 w-20 rounded-lg bg-[#ecedef]`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-[#ecedef]`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-white p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-[#ecedef]`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-[#ecedef]`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-white p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-[#ecedef]`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-[#ecedef]`})]})]})}),(0,M.jsx)(`span`,{className:`block w-full p-2 text-center font-normal`,children:t()})]})}),(0,M.jsx)(x,{children:(0,M.jsxs)(E,{className:`[&:has([data-state=checked])>div]:border-primary`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(_,{className:`sr-only`,value:`dark`})}),(0,M.jsx)(`div`,{className:`items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground`,children:(0,M.jsxs)(`div`,{className:`space-y-2 rounded-sm bg-slate-950 p-2`,children:[(0,M.jsxs)(`div`,{className:`space-y-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-2 w-20 rounded-lg bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]})]})}),(0,M.jsx)(`span`,{className:`block w-full p-2 text-center font-normal`,children:e()})]})})]})]})}),(0,M.jsx)(f,{type:`submit`,children:s()})]})})}function F(){return(0,M.jsx)(A,{description:i(),title:a(),variant:`section`,children:(0,M.jsx)(P,{})})}var I=F;export{I as component};
|
||||
import{Kd as e,an as t,cn as n,dn as r,if as i,ln as a,on as o,qd as s,sn as c,un as l}from"./messages-BSgosaSa.js";import{i as u,n as d,t as f}from"./button-gsP5wmrs.js";import{n as p,r as m}from"./font-provider-BJBBVY9T.js";import{n as h}from"./theme-provider-B5C7xoKA.js";import{t as g}from"./chevron-down-gjYmTBfu.js";import{n as _,t as v}from"./radio-group-D5H97-KZ.js";import{r as y,s as b}from"./zod-Cmg_v3kW.js";import{a as x,c as S,i as C,l as w,n as T,o as E,r as D,s as O,t as k}from"./form-s9C7GZ5q.js";import{t as A}from"./page-header-lpyPiG0-.js";import{t as j}from"./show-submitted-data-CczCj3vK.js";var M=i(),N=b({theme:y([`light`,`dark`]),font:y(m)});function P(){let{font:r,setFont:i}=p(),{theme:l,setTheme:y}=h(),b={theme:l,font:r},A=w({resolver:S(N),defaultValues:b});function P(e){e.font!==r&&i(e.font),e.theme!==l&&y(e.theme),j(e)}return(0,M.jsx)(k,{...A,children:(0,M.jsxs)(`form`,{className:`space-y-8`,onSubmit:A.handleSubmit(P),children:[(0,M.jsx)(C,{control:A.control,name:`font`,render:({field:e})=>(0,M.jsxs)(x,{children:[(0,M.jsx)(E,{children:a()}),(0,M.jsxs)(`div`,{className:`relative w-max`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(`select`,{className:u(d({variant:`outline`}),`w-50 appearance-none font-normal capitalize`,`dark:bg-background dark:hover:bg-background`),...e,children:m.map(e=>(0,M.jsx)(`option`,{value:e,children:e},e))})}),(0,M.jsx)(g,{className:`absolute inset-e-3 top-2.5 h-4 w-4 opacity-50`})]}),(0,M.jsx)(D,{className:`font-manrope`,children:n()}),(0,M.jsx)(O,{})]})}),(0,M.jsx)(C,{control:A.control,name:`theme`,render:({field:t})=>(0,M.jsxs)(x,{children:[(0,M.jsx)(E,{children:c()}),(0,M.jsx)(D,{children:o()}),(0,M.jsx)(O,{}),(0,M.jsxs)(v,{className:`grid max-w-md grid-cols-2 gap-8 pt-2`,defaultValue:t.value,onValueChange:t.onChange,children:[(0,M.jsx)(x,{children:(0,M.jsxs)(E,{className:`[&:has([data-state=checked])>div]:border-primary`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(_,{className:`sr-only`,value:`light`})}),(0,M.jsx)(`div`,{className:`items-center rounded-md border-2 border-muted p-1 hover:border-accent`,children:(0,M.jsxs)(`div`,{className:`space-y-2 rounded-sm bg-[#ecedef] p-2`,children:[(0,M.jsxs)(`div`,{className:`space-y-2 rounded-md bg-white p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-2 w-20 rounded-lg bg-[#ecedef]`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-[#ecedef]`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-white p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-[#ecedef]`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-[#ecedef]`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-white p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-[#ecedef]`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-[#ecedef]`})]})]})}),(0,M.jsx)(`span`,{className:`block w-full p-2 text-center font-normal`,children:s()})]})}),(0,M.jsx)(x,{children:(0,M.jsxs)(E,{className:`[&:has([data-state=checked])>div]:border-primary`,children:[(0,M.jsx)(T,{children:(0,M.jsx)(_,{className:`sr-only`,value:`dark`})}),(0,M.jsx)(`div`,{className:`items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground`,children:(0,M.jsxs)(`div`,{className:`space-y-2 rounded-sm bg-slate-950 p-2`,children:[(0,M.jsxs)(`div`,{className:`space-y-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-2 w-20 rounded-lg bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]}),(0,M.jsxs)(`div`,{className:`flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-xs`,children:[(0,M.jsx)(`div`,{className:`h-4 w-4 rounded-full bg-slate-400`}),(0,M.jsx)(`div`,{className:`h-2 w-25 rounded-lg bg-slate-400`})]})]})}),(0,M.jsx)(`span`,{className:`block w-full p-2 text-center font-normal`,children:e()})]})})]})]})}),(0,M.jsx)(f,{type:`submit`,children:t()})]})})}function F(){return(0,M.jsx)(A,{description:l(),title:r(),variant:`section`,children:(0,M.jsx)(P,{})})}var I=F;export{I as component};
|
||||
@@ -1 +1 @@
|
||||
import{t as e}from"./createLucideIcon-CKkuOIuw.js";var t=e(`arrow-left`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]);export{t};
|
||||
import{t as e}from"./createLucideIcon-D--4ckBc.js";var t=e(`arrow-left`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]);export{t};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{$d as e,Xd as t,Yd as n}from"./messages-kZy_2fG-.js";var r=e(t(),1),i=n(),a=(0,r.createContext)({isTyping:!1,passwordLength:0,setIsTyping:()=>void 0,setPasswordLength:()=>void 0,setShowPassword:()=>void 0,showPassword:!1});function o({children:e}){let[t,n]=(0,r.useState)(!1),[o,s]=(0,r.useState)(!1),[c,l]=(0,r.useState)(0);return(0,i.jsx)(a.Provider,{value:{isTyping:t,passwordLength:c,setIsTyping:n,setPasswordLength:l,setShowPassword:s,showPassword:o},children:e})}function s(){return(0,r.useContext)(a)}export{s as n,o as t};
|
||||
import{af as e,cf as t,if as n}from"./messages-BSgosaSa.js";var r=t(e(),1),i=n(),a=(0,r.createContext)({isTyping:!1,passwordLength:0,setIsTyping:()=>void 0,setPasswordLength:()=>void 0,setShowPassword:()=>void 0,showPassword:!1});function o({children:e}){let[t,n]=(0,r.useState)(!1),[o,s]=(0,r.useState)(!1),[c,l]=(0,r.useState)(0);return(0,i.jsx)(a.Provider,{value:{isTyping:t,passwordLength:c,setIsTyping:n,setPasswordLength:l,setShowPassword:s,showPassword:o},children:e})}function s(){return(0,r.useContext)(a)}export{s as n,o as t};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{Yd as e}from"./messages-kZy_2fG-.js";import{i as t,r as n,s as r}from"./button-CI1sqFDO.js";var i=e(),a=n(`inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border border-transparent px-2 py-0.5 font-medium text-xs transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3`,{variants:{variant:{default:`bg-primary text-primary-foreground [a&]:hover:bg-primary/90`,secondary:`bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90`,destructive:`bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90`,outline:`border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground`,ghost:`[a&]:hover:bg-accent [a&]:hover:text-accent-foreground`,link:`text-primary underline-offset-4 [a&]:hover:underline`}},defaultVariants:{variant:`default`}});function o({className:e,variant:n=`default`,asChild:o=!1,...s}){return(0,i.jsx)(o?r:`span`,{className:t(a({variant:n}),e),"data-slot":`badge`,"data-variant":n,...s})}export{o as t};
|
||||
import{if as e}from"./messages-BSgosaSa.js";import{i as t,r as n,s as r}from"./button-gsP5wmrs.js";var i=e(),a=n(`inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border border-transparent px-2 py-0.5 font-medium text-xs transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3`,{variants:{variant:{default:`bg-primary text-primary-foreground [a&]:hover:bg-primary/90`,secondary:`bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90`,destructive:`bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90`,outline:`border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground`,ghost:`[a&]:hover:bg-accent [a&]:hover:text-accent-foreground`,link:`text-primary underline-offset-4 [a&]:hover:underline`}},defaultVariants:{variant:`default`}});function o({className:e,variant:n=`default`,asChild:o=!1,...s}){return(0,i.jsx)(o?r:`span`,{className:t(a({variant:n}),e),"data-slot":`badge`,"data-variant":n,...s})}export{o as t};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{Yd as e}from"./messages-kZy_2fG-.js";import{i as t}from"./button-CI1sqFDO.js";var n=e();function r({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm`,e),"data-slot":`card`,...r})}function i({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6`,e),"data-slot":`card-header`,...r})}function a({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`font-semibold leading-none`,e),"data-slot":`card-title`,...r})}function o({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`text-muted-foreground text-sm`,e),"data-slot":`card-description`,...r})}function s({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`px-6`,e),"data-slot":`card-content`,...r})}export{a,i,s as n,o as r,r as t};
|
||||
import{if as e}from"./messages-BSgosaSa.js";import{i as t}from"./button-gsP5wmrs.js";var n=e();function r({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm`,e),"data-slot":`card`,...r})}function i({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6`,e),"data-slot":`card-header`,...r})}function a({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`font-semibold leading-none`,e),"data-slot":`card-title`,...r})}function o({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`text-muted-foreground text-sm`,e),"data-slot":`card-description`,...r})}function s({className:e,...r}){return(0,n.jsx)(`div`,{className:t(`px-6`,e),"data-slot":`card-content`,...r})}export{a,i,s as n,o as r,r as t};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{$d as e,Xd as t}from"./messages-kZy_2fG-.js";import{a as n,d as r,u as i}from"./auth-store-B7YG314a.js";import{t as a}from"./createLucideIcon-CKkuOIuw.js";var o=a(`pen-line`,[[`path`,{d:`M13 21h8`,key:`1jsn5i`}],[`path`,{d:`M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z`,key:`1a8usu`}]]),s=e(t(),1),c=new i({chains:[],loading:!1});function l(e){c.setState(t=>({...t,chains:e}))}function u(e){c.setState(t=>({...t,loading:e}))}function d(){let e=r(c,e=>e.chains),t=r(c,e=>e.loading),i=n.useQuery(`get`,`/admin/api/v1/chains`);return(0,s.useEffect)(()=>{u(i.isLoading)},[i.isLoading]),(0,s.useEffect)(()=>{i.data?.data&&l(i.data.data)},[i.data?.data]),{chains:e,loading:t,refetch:i.refetch}}export{o as n,d as t};
|
||||
import{af as e,cf as t}from"./messages-BSgosaSa.js";import{a as n,d as r,u as i}from"./auth-store-BFY9rMhZ.js";import{t as a}from"./createLucideIcon-D--4ckBc.js";var o=a(`pen-line`,[[`path`,{d:`M13 21h8`,key:`1jsn5i`}],[`path`,{d:`M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z`,key:`1a8usu`}]]),s=t(e(),1),c=new i({chains:[],loading:!1});function l(e){c.setState(t=>({...t,chains:e}))}function u(e){c.setState(t=>({...t,loading:e}))}function d(){let e=r(c,e=>e.chains),t=r(c,e=>e.loading),i=n.useQuery(`get`,`/admin/api/v1/chains`);return(0,s.useEffect)(()=>{u(i.isLoading)},[i.isLoading]),(0,s.useEffect)(()=>{i.data?.data&&l(i.data.data)},[i.data?.data]),{chains:e,loading:t,refetch:i.refetch}}export{o as n,d as t};
|
||||
@@ -0,0 +1 @@
|
||||
import{t as e}from"./createLucideIcon-D--4ckBc.js";var t=e(`check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]);export{t};
|
||||
@@ -1 +0,0 @@
|
||||
import{t as e}from"./createLucideIcon-CKkuOIuw.js";var t=e(`check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]);export{t};
|
||||
@@ -1 +1 @@
|
||||
import{$d as e,Xd as t,Yd as n}from"./messages-kZy_2fG-.js";import{t as r}from"./dist-DTN-3dEq.js";import{d as i,i as a}from"./button-CI1sqFDO.js";import{a as o,r as s,t as c}from"./dist-e45zkog2.js";import{t as l}from"./dist-BjEfRG8A.js";import{t as u}from"./dist-HFEBkKCd.js";import{t as d}from"./dist-8VtOfdBX.js";import{t as f}from"./check-jWXQT0HB.js";var p=e(t(),1),m=n(),h=`Checkbox`,[g,_]=o(h),[v,y]=g(h);function b(e){let{__scopeCheckbox:t,checked:n,children:r,defaultChecked:i,disabled:a,form:o,name:s,onCheckedChange:l,required:u,value:d=`on`,internal_do_not_use_render:f}=e,[g,_]=c({prop:n,defaultProp:i??!1,onChange:l,caller:h}),[y,b]=p.useState(null),[x,S]=p.useState(null),C=p.useRef(!1),w=y?!!o||!!y.closest(`form`):!0,T={checked:g,disabled:a,setChecked:_,control:y,setControl:b,name:s,form:o,value:d,hasConsumerStoppedPropagationRef:C,required:u,defaultChecked:k(i)?!1:i,isFormControl:w,bubbleInput:x,setBubbleInput:S};return(0,m.jsx)(v,{scope:t,...T,children:O(f)?f(T):r})}var x=`CheckboxTrigger`,S=p.forwardRef(({__scopeCheckbox:e,onKeyDown:t,onClick:n,...a},o)=>{let{control:c,value:l,disabled:u,checked:d,required:f,setControl:h,setChecked:g,hasConsumerStoppedPropagationRef:_,isFormControl:v,bubbleInput:b}=y(x,e),S=i(o,h),C=p.useRef(d);return p.useEffect(()=>{let e=c?.form;if(e){let t=()=>g(C.current);return e.addEventListener(`reset`,t),()=>e.removeEventListener(`reset`,t)}},[c,g]),(0,m.jsx)(r.button,{type:`button`,role:`checkbox`,"aria-checked":k(d)?`mixed`:d,"aria-required":f,"data-state":A(d),"data-disabled":u?``:void 0,disabled:u,value:l,...a,ref:S,onKeyDown:s(t,e=>{e.key===`Enter`&&e.preventDefault()}),onClick:s(n,e=>{g(e=>k(e)?!0:!e),b&&v&&(_.current=e.isPropagationStopped(),_.current||e.stopPropagation())})})});S.displayName=x;var C=p.forwardRef((e,t)=>{let{__scopeCheckbox:n,name:r,checked:i,defaultChecked:a,required:o,disabled:s,value:c,onCheckedChange:l,form:u,...d}=e;return(0,m.jsx)(b,{__scopeCheckbox:n,checked:i,defaultChecked:a,disabled:s,required:o,onCheckedChange:l,name:r,form:u,value:c,internal_do_not_use_render:({isFormControl:e})=>(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(S,{...d,ref:t,__scopeCheckbox:n}),e&&(0,m.jsx)(D,{__scopeCheckbox:n})]})})});C.displayName=h;var w=`CheckboxIndicator`,T=p.forwardRef((e,t)=>{let{__scopeCheckbox:n,forceMount:i,...a}=e,o=y(w,n);return(0,m.jsx)(l,{present:i||k(o.checked)||o.checked===!0,children:(0,m.jsx)(r.span,{"data-state":A(o.checked),"data-disabled":o.disabled?``:void 0,...a,ref:t,style:{pointerEvents:`none`,...e.style}})})});T.displayName=w;var E=`CheckboxBubbleInput`,D=p.forwardRef(({__scopeCheckbox:e,...t},n)=>{let{control:a,hasConsumerStoppedPropagationRef:o,checked:s,defaultChecked:c,required:l,disabled:f,name:h,value:g,form:_,bubbleInput:v,setBubbleInput:b}=y(E,e),x=i(n,b),S=u(s),C=d(a);p.useEffect(()=>{let e=v;if(!e)return;let t=window.HTMLInputElement.prototype,n=Object.getOwnPropertyDescriptor(t,`checked`).set,r=!o.current;if(S!==s&&n){let t=new Event(`click`,{bubbles:r});e.indeterminate=k(s),n.call(e,k(s)?!1:s),e.dispatchEvent(t)}},[v,S,s,o]);let w=p.useRef(k(s)?!1:s);return(0,m.jsx)(r.input,{type:`checkbox`,"aria-hidden":!0,defaultChecked:c??w.current,required:l,disabled:f,name:h,value:g,form:_,...t,tabIndex:-1,ref:x,style:{...t.style,...C,position:`absolute`,pointerEvents:`none`,opacity:0,margin:0,transform:`translateX(-100%)`}})});D.displayName=E;function O(e){return typeof e==`function`}function k(e){return e===`indeterminate`}function A(e){return k(e)?`indeterminate`:e?`checked`:`unchecked`}function j({className:e,...t}){return(0,m.jsx)(C,{className:a(`peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs outline-none transition-shadow focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:data-[state=checked]:bg-primary dark:aria-invalid:ring-destructive/40`,e),"data-slot":`checkbox`,...t,children:(0,m.jsx)(T,{className:`grid place-content-center text-current transition-none`,"data-slot":`checkbox-indicator`,children:(0,m.jsx)(f,{className:`size-3.5`})})})}export{j as t};
|
||||
import{af as e,cf as t,if as n}from"./messages-BSgosaSa.js";import{t as r}from"./dist-fWONwgan.js";import{d as i,i as a}from"./button-gsP5wmrs.js";import{a as o,r as s,t as c}from"./dist-BX7-FFVx.js";import{t as l}from"./dist-CnRb3IO0.js";import{t as u}from"./dist-BIHhvYX3.js";import{t as d}from"./dist-8mLdRaED.js";import{t as f}from"./check-Cg6qKB39.js";var p=t(e(),1),m=n(),h=`Checkbox`,[g,_]=o(h),[v,y]=g(h);function b(e){let{__scopeCheckbox:t,checked:n,children:r,defaultChecked:i,disabled:a,form:o,name:s,onCheckedChange:l,required:u,value:d=`on`,internal_do_not_use_render:f}=e,[g,_]=c({prop:n,defaultProp:i??!1,onChange:l,caller:h}),[y,b]=p.useState(null),[x,S]=p.useState(null),C=p.useRef(!1),w=y?!!o||!!y.closest(`form`):!0,T={checked:g,disabled:a,setChecked:_,control:y,setControl:b,name:s,form:o,value:d,hasConsumerStoppedPropagationRef:C,required:u,defaultChecked:k(i)?!1:i,isFormControl:w,bubbleInput:x,setBubbleInput:S};return(0,m.jsx)(v,{scope:t,...T,children:O(f)?f(T):r})}var x=`CheckboxTrigger`,S=p.forwardRef(({__scopeCheckbox:e,onKeyDown:t,onClick:n,...a},o)=>{let{control:c,value:l,disabled:u,checked:d,required:f,setControl:h,setChecked:g,hasConsumerStoppedPropagationRef:_,isFormControl:v,bubbleInput:b}=y(x,e),S=i(o,h),C=p.useRef(d);return p.useEffect(()=>{let e=c?.form;if(e){let t=()=>g(C.current);return e.addEventListener(`reset`,t),()=>e.removeEventListener(`reset`,t)}},[c,g]),(0,m.jsx)(r.button,{type:`button`,role:`checkbox`,"aria-checked":k(d)?`mixed`:d,"aria-required":f,"data-state":A(d),"data-disabled":u?``:void 0,disabled:u,value:l,...a,ref:S,onKeyDown:s(t,e=>{e.key===`Enter`&&e.preventDefault()}),onClick:s(n,e=>{g(e=>k(e)?!0:!e),b&&v&&(_.current=e.isPropagationStopped(),_.current||e.stopPropagation())})})});S.displayName=x;var C=p.forwardRef((e,t)=>{let{__scopeCheckbox:n,name:r,checked:i,defaultChecked:a,required:o,disabled:s,value:c,onCheckedChange:l,form:u,...d}=e;return(0,m.jsx)(b,{__scopeCheckbox:n,checked:i,defaultChecked:a,disabled:s,required:o,onCheckedChange:l,name:r,form:u,value:c,internal_do_not_use_render:({isFormControl:e})=>(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(S,{...d,ref:t,__scopeCheckbox:n}),e&&(0,m.jsx)(D,{__scopeCheckbox:n})]})})});C.displayName=h;var w=`CheckboxIndicator`,T=p.forwardRef((e,t)=>{let{__scopeCheckbox:n,forceMount:i,...a}=e,o=y(w,n);return(0,m.jsx)(l,{present:i||k(o.checked)||o.checked===!0,children:(0,m.jsx)(r.span,{"data-state":A(o.checked),"data-disabled":o.disabled?``:void 0,...a,ref:t,style:{pointerEvents:`none`,...e.style}})})});T.displayName=w;var E=`CheckboxBubbleInput`,D=p.forwardRef(({__scopeCheckbox:e,...t},n)=>{let{control:a,hasConsumerStoppedPropagationRef:o,checked:s,defaultChecked:c,required:l,disabled:f,name:h,value:g,form:_,bubbleInput:v,setBubbleInput:b}=y(E,e),x=i(n,b),S=u(s),C=d(a);p.useEffect(()=>{let e=v;if(!e)return;let t=window.HTMLInputElement.prototype,n=Object.getOwnPropertyDescriptor(t,`checked`).set,r=!o.current;if(S!==s&&n){let t=new Event(`click`,{bubbles:r});e.indeterminate=k(s),n.call(e,k(s)?!1:s),e.dispatchEvent(t)}},[v,S,s,o]);let w=p.useRef(k(s)?!1:s);return(0,m.jsx)(r.input,{type:`checkbox`,"aria-hidden":!0,defaultChecked:c??w.current,required:l,disabled:f,name:h,value:g,form:_,...t,tabIndex:-1,ref:x,style:{...t.style,...C,position:`absolute`,pointerEvents:`none`,opacity:0,margin:0,transform:`translateX(-100%)`}})});D.displayName=E;function O(e){return typeof e==`function`}function k(e){return e===`indeterminate`}function A(e){return k(e)?`indeterminate`:e?`checked`:`unchecked`}function j({className:e,...t}){return(0,m.jsx)(C,{className:a(`peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs outline-none transition-shadow focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:data-[state=checked]:bg-primary dark:aria-invalid:ring-destructive/40`,e),"data-slot":`checkbox`,...t,children:(0,m.jsx)(T,{className:`grid place-content-center text-current transition-none`,"data-slot":`checkbox-indicator`,children:(0,m.jsx)(f,{className:`size-3.5`})})})}export{j as t};
|
||||
@@ -0,0 +1 @@
|
||||
import{af as e,cf as t,if as n}from"./messages-BSgosaSa.js";import{i as r,t as i}from"./button-gsP5wmrs.js";import{t as a}from"./check-Cg6qKB39.js";import{t as o}from"./copy-DIIY1PDt.js";var s=t(e(),1),c=n(),l=1400,u=`https://cdn.jsdmirror.com/gh/GMWalletApp/crypto-icons@latest/assets/64/`,d=`https://cdn.jsdmirror.com/gh/GMWalletApp/crypto-icons@latest/maps/colors.json`,f=null,p=null;function m(e){return String(e??``).trim().toLowerCase()}function h(e){let t=m(e);return t?`${u}${t}.png`:null}function g(){return f?Promise.resolve(f):(p||=fetch(d).then(e=>{if(!e.ok)throw Error(`Failed to load icon colors: ${e.status}`);return e.json()}).then(e=>(f=e,e)).catch(()=>(p=null,{})),p)}function _(e,t){let n=e.replace(`#`,``);if(/^[\da-f]{6}$/i.test(n))return`rgba(${Number.parseInt(n.slice(0,2),16)}, ${Number.parseInt(n.slice(2,4),16)}, ${Number.parseInt(n.slice(4,6),16)}, ${t})`}function v(e){return h(e)}function y(e,t){return t?.trim()||e||`--`}function b(e){return h(e)}function x(e){let t=m(e),[n,r]=(0,s.useState)(()=>f?.[t]);return(0,s.useEffect)(()=>{let e=!1;if(!t){r(void 0);return}if(f){r(f[t]);return}return g().then(n=>{e||r(n[t])}),()=>{e=!0}},[t]),n}function S(e){if(e)return{background:_(e,.16),color:e}}function C(e){let t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60,i=e=>String(e).padStart(2,`0`);return t>0?`${i(t)}:${i(n)}:${i(r)}`:`${i(n)}:${i(r)}`}function w({displayName:e,network:t}){let n=v(t);return(0,c.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2`,children:[n?(0,c.jsx)(`img`,{alt:``,className:`size-4 rounded-full`,height:16,src:n,width:16}):null,(0,c.jsx)(`span`,{className:`truncate`,children:y(t,e)})]})}function T({token:e}){let t=b(e);return(0,c.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2`,children:[t?(0,c.jsx)(`img`,{alt:``,className:r(`size-4 rounded-full`),height:16,src:t,width:16}):null,(0,c.jsx)(`span`,{className:`truncate`,children:e})]})}function E({className:e,onClick:t}){let[n,u]=(0,s.useState)(!1),d=(0,s.useRef)(void 0);return(0,s.useEffect)(()=>()=>{d.current!==void 0&&window.clearTimeout(d.current)},[]),(0,c.jsx)(i,{className:r(`mb-0.5 shrink-0`,e),onClick:()=>{if(d.current!==void 0&&window.clearTimeout(d.current),t()===!1){u(!1);return}u(!0),d.current=window.setTimeout(()=>{u(!1),d.current=void 0},l)},size:`icon-sm`,type:`button`,variant:`secondary`,children:n?(0,c.jsx)(a,{className:`text-emerald-600 dark:text-emerald-400`}):(0,c.jsx)(o,{})})}export{y as a,b as c,v as i,x as l,w as n,C as o,T as r,S as s,E as t};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
var e=5e3,t=3e3,n=[10008,10013,404],r=10010,i=`Request failed`;function a(e){return{trade_id:e.trade_id,amount:e.amount,actual_amount:e.actual_amount,token:e.token?.toUpperCase(),currency:e.currency?.toUpperCase(),network:e.network,payment_url:e.payment_url,receive_address:e.receive_address,expiration_time:e.expiration_time,redirect_url:e.redirect_url,created_at:e.created_at,is_selected:e.is_selected}}function o(e){if(e!=null)return typeof e==`object`&&`data`in e?e.data:e}function s(e){return typeof e==`boolean`?e:typeof e==`number`?e!==0:[`1`,`true`,`yes`].includes(String(e??``).toLowerCase())}function c(e){if(e&&typeof e==`object`){if(`status_code`in e&&typeof e.status_code==`number`)return e.status_code;if(`code`in e&&typeof e.code==`number`)return e.code;if(`data`in e)return c(e.data)}}function l(e){if(e==null||e===``)return null;if(typeof e==`number`||/^\d+$/.test(String(e))){let t=Number(e);return new Date(t<0xe8d4a51000?t*1e3:t)}let t=new Date(String(e));return Number.isNaN(t.getTime())?null:t}function u(e,t){return String(e??``).toLowerCase()===String(t??``).toLowerCase()}function d(e){let t=Math.max(0,Math.min(1,e));return t>=.5?`#22c55e`:t>=.2?`#f97316`:`#ef4444`}function f(e,t,n){return e.length===0?null:e.find(e=>u(e.network,n?.network)&&u(e.token,n?.token))||e.find(e=>u(e.network,n?.network))||e.find(e=>u(e.token,n?.token))||e.find(e=>u(e.network,t?.network)&&u(e.token,t?.token))||(e.find(e=>u(e.network,t?.network))??e.find(e=>u(e.token,t?.token))??e[0])}function p(e){return e?.supported_assets?.flatMap(e=>(e.tokens??[]).filter(t=>!!(t&&e.network)).map(t=>({network:e.network,token:t.toUpperCase()})))??[]}function m(e,t){return new Promise((n,r)=>{let i=window.setTimeout(n,e);t?.addEventListener(`abort`,()=>{window.clearTimeout(i),r(t.reason)},{once:!0})})}export{t as a,c,l as d,u as f,o as h,e as i,s as l,p as m,i as n,m as o,d as p,n as r,f as s,r as t,a as u};
|
||||
var e=5e3,t=3e3,n=[10008,10013,404],r=10010,i=`Request failed`;function a(e){return{trade_id:e.trade_id,amount:e.amount,actual_amount:e.actual_amount,token:e.token?.toUpperCase(),currency:e.currency?.toUpperCase(),network:e.network,payment_url:e.payment_url,receive_address:e.receive_address,expiration_time:e.expiration_time,redirect_url:e.redirect_url,created_at:e.created_at,is_selected:e.is_selected}}function o(e){if(e!=null)return typeof e==`object`&&`data`in e?e.data:e}function s(e){return typeof e==`boolean`?e:typeof e==`number`?e!==0:[`1`,`true`,`yes`].includes(String(e??``).toLowerCase())}function c(e){if(e&&typeof e==`object`){if(`status_code`in e&&typeof e.status_code==`number`)return e.status_code;if(`code`in e&&typeof e.code==`number`)return e.code;if(`data`in e)return c(e.data)}}function l(e){if(e==null||e===``)return null;if(typeof e==`number`||/^\d+$/.test(String(e))){let t=Number(e);return new Date(t<0xe8d4a51000?t*1e3:t)}let t=new Date(String(e));return Number.isNaN(t.getTime())?null:t}function u(e,t){return String(e??``).toLowerCase()===String(t??``).toLowerCase()}function d(e){let t=Math.max(0,Math.min(1,e));return t>=.5?`#22c55e`:t>=.2?`#f97316`:`#ef4444`}function f(e,t,n){return e.length===0?null:e.find(e=>u(e.network,n?.network)&&u(e.token,n?.token))||e.find(e=>u(e.network,n?.network))||e.find(e=>u(e.token,n?.token))||e.find(e=>u(e.network,t?.network)&&u(e.token,t?.token))||(e.find(e=>u(e.network,t?.network))??e.find(e=>u(e.token,t?.token))??e[0])}function p(e){return e?.supported_assets?.flatMap(e=>(e.tokens??[]).filter(t=>!!(t&&e.network)).map(t=>({network:e.network,token:t.toUpperCase(),displayName:e.display_name})))??[]}function m(e,t){return new Promise((n,r)=>{let i=window.setTimeout(n,e);t?.addEventListener(`abort`,()=>{window.clearTimeout(i),r(t.reason)},{once:!0})})}export{t as a,c,l as d,u as f,o as h,e as i,s as l,p as m,i as n,m as o,d as p,n as r,f as s,r as t,a as u};
|
||||
@@ -1 +1 @@
|
||||
import{t as e}from"./createLucideIcon-CKkuOIuw.js";var t=e(`chevron-down`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]);export{t};
|
||||
import{t as e}from"./createLucideIcon-D--4ckBc.js";var t=e(`chevron-down`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]);export{t};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{t as e}from"./createLucideIcon-CKkuOIuw.js";var t=e(`chevrons-up-down`,[[`path`,{d:`m7 15 5 5 5-5`,key:`1hf1tw`}],[`path`,{d:`m7 9 5-5 5 5`,key:`sgt6xg`}]]);export{t};
|
||||
import{t as e}from"./createLucideIcon-D--4ckBc.js";var t=e(`chevrons-up-down`,[[`path`,{d:`m7 15 5 5 5-5`,key:`1hf1tw`}],[`path`,{d:`m7 9 5-5 5 5`,key:`sgt6xg`}]]);export{t};
|
||||
@@ -1 +0,0 @@
|
||||
import{t as e}from"./createLucideIcon-CKkuOIuw.js";var t=e(`circle-check`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]);export{t};
|
||||
@@ -1 +1 @@
|
||||
import{t as e}from"./createLucideIcon-CKkuOIuw.js";var t=e(`clock-3`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M12 6v6h4`,key:`135r8i`}]]);export{t};
|
||||
import{t as e}from"./createLucideIcon-D--4ckBc.js";var t=e(`clock-3`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M12 6v6h4`,key:`135r8i`}]]);export{t};
|
||||
@@ -0,0 +1 @@
|
||||
import{Bd as e,Vd as t,if as n,zd as r}from"./messages-BSgosaSa.js";import{d as i,l as a,m as o,p as s,u as c}from"./command-BOJV7P-9.js";import{t as l}from"./telescope-1xQBKzAV.js";var u=n();function d({open:n,onOpenChange:d}){return(0,u.jsx)(a,{onOpenChange:d,open:n,children:(0,u.jsx)(c,{className:`sm:max-w-sm`,children:(0,u.jsxs)(s,{className:`items-center text-center`,children:[(0,u.jsx)(l,{className:`mb-2 size-12 text-muted-foreground`}),(0,u.jsx)(o,{children:t()}),(0,u.jsxs)(i,{children:[e(),(0,u.jsx)(`br`,{}),(0,u.jsx)(`p`,{className:`mt-2 text-center`,children:r()})]})]})})})}export{d as t};
|
||||
@@ -1 +0,0 @@
|
||||
import{Ad as e,Md as t,Yd as n,jd as r}from"./messages-kZy_2fG-.js";import{d as i,l as a,m as o,p as s,u as c}from"./command-St1f4L7D.js";import{t as l}from"./telescope-B6BZXMCK.js";var u=n();function d({open:n,onOpenChange:d}){return(0,u.jsx)(a,{onOpenChange:d,open:n,children:(0,u.jsx)(c,{className:`sm:max-w-sm`,children:(0,u.jsxs)(s,{className:`items-center text-center`,children:[(0,u.jsx)(l,{className:`mb-2 size-12 text-muted-foreground`}),(0,u.jsx)(o,{children:t()}),(0,u.jsxs)(i,{children:[r(),(0,u.jsx)(`br`,{}),(0,u.jsx)(`p`,{className:`mt-2 text-center`,children:e()})]})]})})})}export{d as t};
|
||||
File diff suppressed because one or more lines are too long
+2
-2
@@ -1,7 +1,7 @@
|
||||
import{$d as e,Ud as t,Wd as n,Xd as r,Yd as i}from"./messages-kZy_2fG-.js";import{d as a,i as o,l as s,t as c}from"./button-CI1sqFDO.js";import{a as l,r as u}from"./dist-e45zkog2.js";import{a as d,c as f,i as p,l as m,n as h,o as g,r as _,s as ee,t as v,u as y}from"./dist-u5B8Q0Qw.js";var b=e(r(),1),x=i(),S=`AlertDialog`,[C,te]=l(S,[y]),w=y(),T=e=>{let{__scopeAlertDialog:t,...n}=e,r=w(t);return(0,x.jsx)(g,{...r,...n,modal:!0})};T.displayName=S;var E=`AlertDialogTrigger`,D=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(f,{...i,...r,ref:t})});D.displayName=E;var O=`AlertDialogPortal`,k=e=>{let{__scopeAlertDialog:t,...n}=e,r=w(t);return(0,x.jsx)(d,{...r,...n})};k.displayName=O;var A=`AlertDialogOverlay`,j=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(p,{...i,...r,ref:t})});j.displayName=A;var M=`AlertDialogContent`,[N,P]=C(M),F=s(`AlertDialogContent`),I=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,children:r,...i}=e,o=w(n),s=b.useRef(null),c=a(t,s),l=b.useRef(null);return(0,x.jsx)(m,{contentName:M,titleName:L,docsSlug:`alert-dialog`,children:(0,x.jsx)(N,{scope:n,cancelRef:l,children:(0,x.jsxs)(h,{role:`alertdialog`,...o,...i,ref:c,onOpenAutoFocus:u(i.onOpenAutoFocus,e=>{e.preventDefault(),l.current?.focus({preventScroll:!0})}),onPointerDownOutside:e=>e.preventDefault(),onInteractOutside:e=>e.preventDefault(),children:[(0,x.jsx)(F,{children:r}),(0,x.jsx)(G,{contentRef:s})]})})})});I.displayName=M;var L=`AlertDialogTitle`,R=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(ee,{...i,...r,ref:t})});R.displayName=L;var z=`AlertDialogDescription`,B=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(_,{...i,...r,ref:t})});B.displayName=z;var V=`AlertDialogAction`,H=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(v,{...i,...r,ref:t})});H.displayName=V;var U=`AlertDialogCancel`,W=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,{cancelRef:i}=P(U,n),o=w(n),s=a(t,i);return(0,x.jsx)(v,{...o,...r,ref:s})});W.displayName=U;var G=({contentRef:e})=>{let t=`\`${M}\` requires a description for the component to be accessible for screen reader users.
|
||||
import{$d as e,Qd as t,af as n,cf as r,if as i}from"./messages-BSgosaSa.js";import{d as a,i as o,l as s,t as c}from"./button-gsP5wmrs.js";import{a as l,r as u}from"./dist-BX7-FFVx.js";import{a as d,c as f,i as p,l as m,n as h,o as g,r as _,s as ee,t as v,u as y}from"./dist-C-eeUszQ.js";var b=r(n(),1),x=i(),S=`AlertDialog`,[C,te]=l(S,[y]),w=y(),T=e=>{let{__scopeAlertDialog:t,...n}=e,r=w(t);return(0,x.jsx)(g,{...r,...n,modal:!0})};T.displayName=S;var E=`AlertDialogTrigger`,D=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(f,{...i,...r,ref:t})});D.displayName=E;var O=`AlertDialogPortal`,k=e=>{let{__scopeAlertDialog:t,...n}=e,r=w(t);return(0,x.jsx)(d,{...r,...n})};k.displayName=O;var A=`AlertDialogOverlay`,j=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(p,{...i,...r,ref:t})});j.displayName=A;var M=`AlertDialogContent`,[N,P]=C(M),F=s(`AlertDialogContent`),I=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,children:r,...i}=e,o=w(n),s=b.useRef(null),c=a(t,s),l=b.useRef(null);return(0,x.jsx)(m,{contentName:M,titleName:L,docsSlug:`alert-dialog`,children:(0,x.jsx)(N,{scope:n,cancelRef:l,children:(0,x.jsxs)(h,{role:`alertdialog`,...o,...i,ref:c,onOpenAutoFocus:u(i.onOpenAutoFocus,e=>{e.preventDefault(),l.current?.focus({preventScroll:!0})}),onPointerDownOutside:e=>e.preventDefault(),onInteractOutside:e=>e.preventDefault(),children:[(0,x.jsx)(F,{children:r}),(0,x.jsx)(G,{contentRef:s})]})})})});I.displayName=M;var L=`AlertDialogTitle`,R=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(ee,{...i,...r,ref:t})});R.displayName=L;var z=`AlertDialogDescription`,B=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(_,{...i,...r,ref:t})});B.displayName=z;var V=`AlertDialogAction`,H=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,i=w(n);return(0,x.jsx)(v,{...i,...r,ref:t})});H.displayName=V;var U=`AlertDialogCancel`,W=b.forwardRef((e,t)=>{let{__scopeAlertDialog:n,...r}=e,{cancelRef:i}=P(U,n),o=w(n),s=a(t,i);return(0,x.jsx)(v,{...o,...r,ref:s})});W.displayName=U;var G=({contentRef:e})=>{let t=`\`${M}\` requires a description for the component to be accessible for screen reader users.
|
||||
|
||||
You can add a description to the \`${M}\` by passing a \`${z}\` component as a child, which also benefits sighted users by adding visible context to the dialog.
|
||||
|
||||
Alternatively, you can use your own component as a description by assigning it an \`id\` and passing the same value to the \`aria-describedby\` prop in \`${M}\`. If the description is confusing or duplicative for sighted users, you can use the \`@radix-ui/react-visually-hidden\` primitive as a wrapper around your description component.
|
||||
|
||||
For more information, see https://radix-ui.com/primitives/docs/components/alert-dialog`;return b.useEffect(()=>{document.getElementById(e.current?.getAttribute(`aria-describedby`))||console.warn(t)},[t,e]),null},K=T,q=k,J=j,Y=I,X=W,Z=R,Q=B;function ne({...e}){return(0,x.jsx)(K,{"data-slot":`alert-dialog`,...e})}function re({...e}){return(0,x.jsx)(q,{"data-slot":`alert-dialog-portal`,...e})}function ie({className:e,...t}){return(0,x.jsx)(J,{className:o(`data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in`,e),"data-slot":`alert-dialog-overlay`,...t})}function ae({className:e,size:t=`default`,...n}){return(0,x.jsxs)(re,{children:[(0,x.jsx)(ie,{}),(0,x.jsx)(Y,{className:o(`group/alert-dialog-content data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[state=closed]:animate-out data-[state=open]:animate-in data-[size=default]:sm:max-w-lg`,e),"data-size":t,"data-slot":`alert-dialog-content`,...n})]})}function oe({className:e,...t}){return(0,x.jsx)(`div`,{className:o(`grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]`,e),"data-slot":`alert-dialog-header`,...t})}function $({className:e,...t}){return(0,x.jsx)(`div`,{className:o(`flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end`,e),"data-slot":`alert-dialog-footer`,...t})}function se({className:e,...t}){return(0,x.jsx)(Z,{className:o(`font-semibold text-lg sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2`,e),"data-slot":`alert-dialog-title`,...t})}function ce({className:e,...t}){return(0,x.jsx)(Q,{className:o(`text-muted-foreground text-sm`,e),"data-slot":`alert-dialog-description`,...t})}function le({className:e,variant:t=`outline`,size:n=`default`,...r}){return(0,x.jsx)(c,{asChild:!0,size:n,variant:t,children:(0,x.jsx)(X,{className:o(e),"data-slot":`alert-dialog-cancel`,...r})})}function ue(e){let{title:r,desc:i,children:a,className:s,confirmText:l,cancelBtnText:u,destructive:d,isLoading:f,disabled:p=!1,form:m,handleConfirm:h,...g}=e;return(0,x.jsx)(ne,{...g,children:(0,x.jsxs)(ae,{className:o(s&&s),children:[(0,x.jsxs)(oe,{className:`text-start`,children:[(0,x.jsx)(se,{children:r}),(0,x.jsx)(ce,{asChild:!0,children:(0,x.jsx)(`div`,{children:i})})]}),a,(0,x.jsxs)($,{children:[(0,x.jsx)(le,{disabled:f,children:u??n()}),(0,x.jsx)(c,{disabled:p||f,form:m,onClick:h,type:m?`submit`:`button`,variant:d?`destructive`:`default`,children:l??t()})]})]})})}export{ue as t};
|
||||
For more information, see https://radix-ui.com/primitives/docs/components/alert-dialog`;return b.useEffect(()=>{document.getElementById(e.current?.getAttribute(`aria-describedby`))||console.warn(t)},[t,e]),null},K=T,q=k,J=j,Y=I,X=W,Z=R,Q=B;function ne({...e}){return(0,x.jsx)(K,{"data-slot":`alert-dialog`,...e})}function re({...e}){return(0,x.jsx)(q,{"data-slot":`alert-dialog-portal`,...e})}function ie({className:e,...t}){return(0,x.jsx)(J,{className:o(`data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in`,e),"data-slot":`alert-dialog-overlay`,...t})}function ae({className:e,size:t=`default`,...n}){return(0,x.jsxs)(re,{children:[(0,x.jsx)(ie,{}),(0,x.jsx)(Y,{className:o(`group/alert-dialog-content data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[state=closed]:animate-out data-[state=open]:animate-in data-[size=default]:sm:max-w-lg`,e),"data-size":t,"data-slot":`alert-dialog-content`,...n})]})}function oe({className:e,...t}){return(0,x.jsx)(`div`,{className:o(`grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]`,e),"data-slot":`alert-dialog-header`,...t})}function $({className:e,...t}){return(0,x.jsx)(`div`,{className:o(`flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end`,e),"data-slot":`alert-dialog-footer`,...t})}function se({className:e,...t}){return(0,x.jsx)(Z,{className:o(`font-semibold text-lg sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2`,e),"data-slot":`alert-dialog-title`,...t})}function ce({className:e,...t}){return(0,x.jsx)(Q,{className:o(`text-muted-foreground text-sm`,e),"data-slot":`alert-dialog-description`,...t})}function le({className:e,variant:t=`outline`,size:n=`default`,...r}){return(0,x.jsx)(c,{asChild:!0,size:n,variant:t,children:(0,x.jsx)(X,{className:o(e),"data-slot":`alert-dialog-cancel`,...r})})}function ue(n){let{title:r,desc:i,children:a,className:s,confirmText:l,cancelBtnText:u,destructive:d,isLoading:f,disabled:p=!1,form:m,handleConfirm:h,...g}=n;return(0,x.jsx)(ne,{...g,children:(0,x.jsxs)(ae,{className:o(s&&s),children:[(0,x.jsxs)(oe,{className:`text-start`,children:[(0,x.jsx)(se,{children:r}),(0,x.jsx)(ce,{asChild:!0,children:(0,x.jsx)(`div`,{children:i})})]}),a,(0,x.jsxs)($,{children:[(0,x.jsx)(le,{disabled:f,children:u??e()}),(0,x.jsx)(c,{disabled:p||f,form:m,onClick:h,type:m?`submit`:`button`,variant:d?`destructive`:`default`,children:l??t()})]})]})})}export{ue as t};
|
||||
@@ -0,0 +1 @@
|
||||
import{t as e}from"./createLucideIcon-D--4ckBc.js";var t=e(`copy`,[[`rect`,{width:`14`,height:`14`,x:`8`,y:`8`,rx:`2`,ry:`2`,key:`17jyea`}],[`path`,{d:`M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2`,key:`zix9uf`}]]);export{t};
|
||||
@@ -0,0 +1 @@
|
||||
import{of as e}from"./messages-BSgosaSa.js";var t=e(((e,t)=>{t.exports=function(){var e=document.getSelection();if(!e.rangeCount)return function(){};for(var t=document.activeElement,n=[],r=0;r<e.rangeCount;r++)n.push(e.getRangeAt(r));switch(t.tagName.toUpperCase()){case`INPUT`:case`TEXTAREA`:t.blur();break;default:t=null;break}return e.removeAllRanges(),function(){e.type===`Caret`&&e.removeAllRanges(),e.rangeCount||n.forEach(function(t){e.addRange(t)}),t&&t.focus()}}})),n=e(((e,n)=>{var r=t(),i={"text/plain":`Text`,"text/html":`Url`,default:`Text`},a=`Copy to clipboard: #{key}, Enter`;function o(e){var t=(/mac os x/i.test(navigator.userAgent)?`⌘`:`Ctrl`)+`+C`;return e.replace(/#{\s*key\s*}/g,t)}function s(e,t){var n,s,c,l,u,d,f=!1;t||={},n=t.debug||!1;try{if(c=r(),l=document.createRange(),u=document.getSelection(),d=document.createElement(`span`),d.textContent=e,d.ariaHidden=`true`,d.style.all=`unset`,d.style.position=`fixed`,d.style.top=0,d.style.clip=`rect(0, 0, 0, 0)`,d.style.whiteSpace=`pre`,d.style.webkitUserSelect=`text`,d.style.MozUserSelect=`text`,d.style.msUserSelect=`text`,d.style.userSelect=`text`,d.addEventListener(`copy`,function(r){if(r.stopPropagation(),t.format)if(r.preventDefault(),r.clipboardData===void 0){n&&console.warn(`unable to use e.clipboardData`),n&&console.warn(`trying IE specific stuff`),window.clipboardData.clearData();var a=i[t.format]||i.default;window.clipboardData.setData(a,e)}else r.clipboardData.clearData(),r.clipboardData.setData(t.format,e);t.onCopy&&(r.preventDefault(),t.onCopy(r.clipboardData))}),document.body.appendChild(d),l.selectNodeContents(d),u.addRange(l),!document.execCommand(`copy`))throw Error(`copy command was unsuccessful`);f=!0}catch(r){n&&console.error(`unable to copy using execCommand: `,r),n&&console.warn(`trying IE specific stuff`);try{window.clipboardData.setData(t.format||`text`,e),t.onCopy&&t.onCopy(window.clipboardData),f=!0}catch(r){n&&console.error(`unable to copy using clipboardData: `,r),n&&console.error(`falling back to prompt`),s=o(`message`in t?t.message:a),window.prompt(s,e)}}finally{u&&(typeof u.removeRange==`function`?u.removeRange(l):u.removeAllRanges()),d&&document.body.removeChild(d),c()}return f}n.exports=s}));export{n as t};
|
||||
@@ -1 +0,0 @@
|
||||
import{Zd as e}from"./messages-kZy_2fG-.js";import{t}from"./createLucideIcon-CKkuOIuw.js";var n=t(`copy`,[[`rect`,{width:`14`,height:`14`,x:`8`,y:`8`,rx:`2`,ry:`2`,key:`17jyea`}],[`path`,{d:`M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2`,key:`zix9uf`}]]),r=e(((e,t)=>{t.exports=function(){var e=document.getSelection();if(!e.rangeCount)return function(){};for(var t=document.activeElement,n=[],r=0;r<e.rangeCount;r++)n.push(e.getRangeAt(r));switch(t.tagName.toUpperCase()){case`INPUT`:case`TEXTAREA`:t.blur();break;default:t=null;break}return e.removeAllRanges(),function(){e.type===`Caret`&&e.removeAllRanges(),e.rangeCount||n.forEach(function(t){e.addRange(t)}),t&&t.focus()}}})),i=e(((e,t)=>{var n=r(),i={"text/plain":`Text`,"text/html":`Url`,default:`Text`},a=`Copy to clipboard: #{key}, Enter`;function o(e){var t=(/mac os x/i.test(navigator.userAgent)?`⌘`:`Ctrl`)+`+C`;return e.replace(/#{\s*key\s*}/g,t)}function s(e,t){var r,s,c,l,u,d,f=!1;t||={},r=t.debug||!1;try{if(c=n(),l=document.createRange(),u=document.getSelection(),d=document.createElement(`span`),d.textContent=e,d.ariaHidden=`true`,d.style.all=`unset`,d.style.position=`fixed`,d.style.top=0,d.style.clip=`rect(0, 0, 0, 0)`,d.style.whiteSpace=`pre`,d.style.webkitUserSelect=`text`,d.style.MozUserSelect=`text`,d.style.msUserSelect=`text`,d.style.userSelect=`text`,d.addEventListener(`copy`,function(n){if(n.stopPropagation(),t.format)if(n.preventDefault(),n.clipboardData===void 0){r&&console.warn(`unable to use e.clipboardData`),r&&console.warn(`trying IE specific stuff`),window.clipboardData.clearData();var a=i[t.format]||i.default;window.clipboardData.setData(a,e)}else n.clipboardData.clearData(),n.clipboardData.setData(t.format,e);t.onCopy&&(n.preventDefault(),t.onCopy(n.clipboardData))}),document.body.appendChild(d),l.selectNodeContents(d),u.addRange(l),!document.execCommand(`copy`))throw Error(`copy command was unsuccessful`);f=!0}catch(n){r&&console.error(`unable to copy using execCommand: `,n),r&&console.warn(`trying IE specific stuff`);try{window.clipboardData.setData(t.format||`text`,e),t.onCopy&&t.onCopy(window.clipboardData),f=!0}catch(n){r&&console.error(`unable to copy using clipboardData: `,n),r&&console.error(`falling back to prompt`),s=o(`message`in t?t.message:a),window.prompt(s,e)}}finally{u&&(typeof u.removeRange==`function`?u.removeRange(l):u.removeAllRanges()),d&&document.body.removeChild(d),c()}return f}t.exports=s}));export{n,i as t};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{$d as e,Xd as t}from"./messages-kZy_2fG-.js";var n=e(t()),r=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),i=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),a=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(e,t,n)=>n?n.toUpperCase():t.toLowerCase()),o=e=>{let t=a(e);return t.charAt(0).toUpperCase()+t.slice(1)},s={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},c=e=>{for(let t in e)if(t.startsWith(`aria-`)||t===`role`||t===`title`)return!0;return!1},l=(0,n.createContext)({}),u=()=>(0,n.useContext)(l),d=(0,n.forwardRef)(({color:e,size:t,strokeWidth:i,absoluteStrokeWidth:a,className:o=``,children:l,iconNode:d,...f},p)=>{let{size:m=24,strokeWidth:h=2,absoluteStrokeWidth:g=!1,color:_=`currentColor`,className:v=``}=u()??{},y=a??g?Number(i??h)*24/Number(t??m):i??h;return(0,n.createElement)(`svg`,{ref:p,...s,width:t??m??s.width,height:t??m??s.height,stroke:e??_,strokeWidth:y,className:r(`lucide`,v,o),...!l&&!c(f)&&{"aria-hidden":`true`},...f},[...d.map(([e,t])=>(0,n.createElement)(e,t)),...Array.isArray(l)?l:[l]])}),f=(e,t)=>{let a=(0,n.forwardRef)(({className:a,...s},c)=>(0,n.createElement)(d,{ref:c,iconNode:t,className:r(`lucide-${i(o(e))}`,`lucide-${e}`,a),...s}));return a.displayName=o(e),a};export{f as t};
|
||||
import{af as e,cf as t}from"./messages-BSgosaSa.js";var n=t(e()),r=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),i=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),a=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(e,t,n)=>n?n.toUpperCase():t.toLowerCase()),o=e=>{let t=a(e);return t.charAt(0).toUpperCase()+t.slice(1)},s={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},c=e=>{for(let t in e)if(t.startsWith(`aria-`)||t===`role`||t===`title`)return!0;return!1},l=(0,n.createContext)({}),u=()=>(0,n.useContext)(l),d=(0,n.forwardRef)(({color:e,size:t,strokeWidth:i,absoluteStrokeWidth:a,className:o=``,children:l,iconNode:d,...f},p)=>{let{size:m=24,strokeWidth:h=2,absoluteStrokeWidth:g=!1,color:_=`currentColor`,className:v=``}=u()??{},y=a??g?Number(i??h)*24/Number(t??m):i??h;return(0,n.createElement)(`svg`,{ref:p,...s,width:t??m??s.width,height:t??m??s.height,stroke:e??_,strokeWidth:y,className:r(`lucide`,v,o),...!l&&!c(f)&&{"aria-hidden":`true`},...f},[...d.map(([e,t])=>(0,n.createElement)(e,t)),...Array.isArray(l)?l:[l]])}),f=(e,t)=>{let a=(0,n.forwardRef)(({className:a,...s},c)=>(0,n.createElement)(d,{ref:c,iconNode:t,className:r(`lucide-${i(o(e))}`,`lucide-${e}`,a),...s}));return a.displayName=o(e),a};export{f as t};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{$t as e,Jt as t,Qt as n,Xt as r,Yt as i,Zt as a,en as o,if as s,in as c,nn as l,qt as u,rn as d,tn as f}from"./messages-BSgosaSa.js";import{t as p}from"./button-gsP5wmrs.js";import{t as m}from"./checkbox-CurizX_C.js";import{c as h,i as g,s as _}from"./zod-Cmg_v3kW.js";import{a as v,c as y,i as b,l as x,n as S,o as C,r as w,s as T,t as E}from"./form-s9C7GZ5q.js";import{t as D}from"./page-header-lpyPiG0-.js";import{t as O}from"./show-submitted-data-CczCj3vK.js";var k=s(),A=[{id:`recents`,label:l()},{id:`home`,label:f()},{id:`applications`,label:o()},{id:`desktop`,label:e()},{id:`downloads`,label:n()},{id:`documents`,label:a()}],j=_({items:g(h()).refine(e=>e.some(e=>e),{message:r()})}),M={items:[`recents`,`home`]};function N(){let e=x({resolver:y(j),defaultValues:M});return(0,k.jsx)(E,{...e,children:(0,k.jsxs)(`form`,{className:`space-y-8`,onSubmit:e.handleSubmit(e=>O(e)),children:[(0,k.jsx)(b,{control:e.control,name:`items`,render:()=>(0,k.jsxs)(v,{children:[(0,k.jsxs)(`div`,{className:`mb-4`,children:[(0,k.jsx)(C,{className:`text-base`,children:i()}),(0,k.jsx)(w,{children:t()})]}),A.map(t=>(0,k.jsx)(b,{control:e.control,name:`items`,render:({field:e})=>(0,k.jsxs)(v,{className:`flex flex-row items-start`,children:[(0,k.jsx)(S,{children:(0,k.jsx)(m,{checked:e.value?.includes(t.id),onCheckedChange:n=>n?e.onChange([...e.value,t.id]):e.onChange(e.value?.filter(e=>e!==t.id))})}),(0,k.jsx)(C,{className:`font-normal`,children:t.label})]},t.id)},t.id)),(0,k.jsx)(T,{})]})}),(0,k.jsx)(p,{type:`submit`,children:u()})]})})}function P(){return(0,k.jsx)(D,{description:d(),title:c(),variant:`section`,children:(0,k.jsx)(N,{})})}var F=P;export{F as component};
|
||||
@@ -1 +0,0 @@
|
||||
import{$t as e,Jt as t,Qt as n,Xt as r,Yd as i,Yt as a,Zt as o,an as s,en as c,in as l,nn as u,rn as d,tn as f}from"./messages-kZy_2fG-.js";import{t as p}from"./button-CI1sqFDO.js";import{t as m}from"./checkbox-Buso7MQb.js";import{c as h,i as g,s as _}from"./zod-BDZSAwfF.js";import{a as v,c as y,i as b,l as x,n as S,o as C,r as w,s as T,t as E}from"./form-Cb-bizIH.js";import{t as D}from"./page-header-Bto-ujL-.js";import{t as O}from"./show-submitted-data-BpvpfhNu.js";var k=i(),A=[{id:`recents`,label:d()},{id:`home`,label:u()},{id:`applications`,label:f()},{id:`desktop`,label:c()},{id:`downloads`,label:e()},{id:`documents`,label:n()}],j=_({items:g(h()).refine(e=>e.some(e=>e),{message:o()})}),M={items:[`recents`,`home`]};function N(){let e=x({resolver:y(j),defaultValues:M});return(0,k.jsx)(E,{...e,children:(0,k.jsxs)(`form`,{className:`space-y-8`,onSubmit:e.handleSubmit(e=>O(e)),children:[(0,k.jsx)(b,{control:e.control,name:`items`,render:()=>(0,k.jsxs)(v,{children:[(0,k.jsxs)(`div`,{className:`mb-4`,children:[(0,k.jsx)(C,{className:`text-base`,children:r()}),(0,k.jsx)(w,{children:a()})]}),A.map(t=>(0,k.jsx)(b,{control:e.control,name:`items`,render:({field:e})=>(0,k.jsxs)(v,{className:`flex flex-row items-start`,children:[(0,k.jsx)(S,{children:(0,k.jsx)(m,{checked:e.value?.includes(t.id),onCheckedChange:n=>n?e.onChange([...e.value,t.id]):e.onChange(e.value?.filter(e=>e!==t.id))})}),(0,k.jsx)(C,{className:`font-normal`,children:t.label})]},t.id)},t.id)),(0,k.jsx)(T,{})]})}),(0,k.jsx)(p,{type:`submit`,children:t()})]})})}function P(){return(0,k.jsx)(D,{description:l(),title:s(),variant:`section`,children:(0,k.jsx)(N,{})})}var F=P;export{F as component};
|
||||
@@ -1 +1 @@
|
||||
import{$d as e,Xd as t,Yd as n}from"./messages-kZy_2fG-.js";import{n as r,r as i,t as a}from"./dist-DTN-3dEq.js";import{d as o}from"./button-CI1sqFDO.js";import{n as s,r as c}from"./dist-e45zkog2.js";import{t as l}from"./dist-Dz_nSMrn.js";var u=e(t(),1);function d(e,t=globalThis?.document){let n=l(e);u.useEffect(()=>{let e=e=>{e.key===`Escape`&&n(e)};return t.addEventListener(`keydown`,e,{capture:!0}),()=>t.removeEventListener(`keydown`,e,{capture:!0})},[n,t])}var f=n(),p=`DismissableLayer`,m=`dismissableLayer.update`,h=`dismissableLayer.pointerDownOutside`,g=`dismissableLayer.focusOutside`,_,v=u.createContext({layers:new Set,layersWithOutsidePointerEventsDisabled:new Set,branches:new Set}),y=u.forwardRef((e,t)=>{let{disableOutsidePointerEvents:n=!1,onEscapeKeyDown:r,onPointerDownOutside:i,onFocusOutside:s,onInteractOutside:l,onDismiss:p,...h}=e,g=u.useContext(v),[y,b]=u.useState(null),x=y?.ownerDocument??globalThis?.document,[,T]=u.useState({}),E=o(t,e=>b(e)),D=Array.from(g.layers),[O]=[...g.layersWithOutsidePointerEventsDisabled].slice(-1),k=D.indexOf(O),A=y?D.indexOf(y):-1,j=g.layersWithOutsidePointerEventsDisabled.size>0,M=A>=k,N=S(e=>{let t=e.target,n=[...g.branches].some(e=>e.contains(t));!M||n||(i?.(e),l?.(e),e.defaultPrevented||p?.())},x),P=C(e=>{let t=e.target;[...g.branches].some(e=>e.contains(t))||(s?.(e),l?.(e),e.defaultPrevented||p?.())},x);return d(e=>{A===g.layers.size-1&&(r?.(e),!e.defaultPrevented&&p&&(e.preventDefault(),p()))},x),u.useEffect(()=>{if(y)return n&&(g.layersWithOutsidePointerEventsDisabled.size===0&&(_=x.body.style.pointerEvents,x.body.style.pointerEvents=`none`),g.layersWithOutsidePointerEventsDisabled.add(y)),g.layers.add(y),w(),()=>{n&&g.layersWithOutsidePointerEventsDisabled.size===1&&(x.body.style.pointerEvents=_)}},[y,x,n,g]),u.useEffect(()=>()=>{y&&(g.layers.delete(y),g.layersWithOutsidePointerEventsDisabled.delete(y),w())},[y,g]),u.useEffect(()=>{let e=()=>T({});return document.addEventListener(m,e),()=>document.removeEventListener(m,e)},[]),(0,f.jsx)(a.div,{...h,ref:E,style:{pointerEvents:j?M?`auto`:`none`:void 0,...e.style},onFocusCapture:c(e.onFocusCapture,P.onFocusCapture),onBlurCapture:c(e.onBlurCapture,P.onBlurCapture),onPointerDownCapture:c(e.onPointerDownCapture,N.onPointerDownCapture)})});y.displayName=p;var b=`DismissableLayerBranch`,x=u.forwardRef((e,t)=>{let n=u.useContext(v),r=u.useRef(null),i=o(t,r);return u.useEffect(()=>{let e=r.current;if(e)return n.branches.add(e),()=>{n.branches.delete(e)}},[n.branches]),(0,f.jsx)(a.div,{...e,ref:i})});x.displayName=b;function S(e,t=globalThis?.document){let n=l(e),r=u.useRef(!1),i=u.useRef(()=>{});return u.useEffect(()=>{let e=e=>{if(e.target&&!r.current){let r=function(){T(h,n,a,{discrete:!0})},a={originalEvent:e};e.pointerType===`touch`?(t.removeEventListener(`click`,i.current),i.current=r,t.addEventListener(`click`,i.current,{once:!0})):r()}else t.removeEventListener(`click`,i.current);r.current=!1},a=window.setTimeout(()=>{t.addEventListener(`pointerdown`,e)},0);return()=>{window.clearTimeout(a),t.removeEventListener(`pointerdown`,e),t.removeEventListener(`click`,i.current)}},[t,n]),{onPointerDownCapture:()=>r.current=!0}}function C(e,t=globalThis?.document){let n=l(e),r=u.useRef(!1);return u.useEffect(()=>{let e=e=>{e.target&&!r.current&&T(g,n,{originalEvent:e},{discrete:!1})};return t.addEventListener(`focusin`,e),()=>t.removeEventListener(`focusin`,e)},[t,n]),{onFocusCapture:()=>r.current=!0,onBlurCapture:()=>r.current=!1}}function w(){let e=new CustomEvent(m);document.dispatchEvent(e)}function T(e,t,n,{discrete:i}){let a=n.originalEvent.target,o=new CustomEvent(e,{bubbles:!1,cancelable:!0,detail:n});t&&a.addEventListener(e,t,{once:!0}),i?r(a,o):a.dispatchEvent(o)}var E=e(i(),1),D=`Portal`,O=u.forwardRef((e,t)=>{let{container:n,...r}=e,[i,o]=u.useState(!1);s(()=>o(!0),[]);let c=n||i&&globalThis?.document?.body;return c?E.createPortal((0,f.jsx)(a.div,{...r,ref:t}),c):null});O.displayName=D;export{y as n,O as t};
|
||||
import{af as e,cf as t,if as n}from"./messages-BSgosaSa.js";import{n as r,r as i,t as a}from"./dist-fWONwgan.js";import{d as o}from"./button-gsP5wmrs.js";import{n as s,r as c}from"./dist-BX7-FFVx.js";import{t as l}from"./dist-C3yztW6j.js";var u=t(e(),1);function d(e,t=globalThis?.document){let n=l(e);u.useEffect(()=>{let e=e=>{e.key===`Escape`&&n(e)};return t.addEventListener(`keydown`,e,{capture:!0}),()=>t.removeEventListener(`keydown`,e,{capture:!0})},[n,t])}var f=n(),p=`DismissableLayer`,m=`dismissableLayer.update`,h=`dismissableLayer.pointerDownOutside`,g=`dismissableLayer.focusOutside`,_,v=u.createContext({layers:new Set,layersWithOutsidePointerEventsDisabled:new Set,branches:new Set}),y=u.forwardRef((e,t)=>{let{disableOutsidePointerEvents:n=!1,onEscapeKeyDown:r,onPointerDownOutside:i,onFocusOutside:s,onInteractOutside:l,onDismiss:p,...h}=e,g=u.useContext(v),[y,b]=u.useState(null),x=y?.ownerDocument??globalThis?.document,[,T]=u.useState({}),E=o(t,e=>b(e)),D=Array.from(g.layers),[O]=[...g.layersWithOutsidePointerEventsDisabled].slice(-1),k=D.indexOf(O),A=y?D.indexOf(y):-1,j=g.layersWithOutsidePointerEventsDisabled.size>0,M=A>=k,N=S(e=>{let t=e.target,n=[...g.branches].some(e=>e.contains(t));!M||n||(i?.(e),l?.(e),e.defaultPrevented||p?.())},x),P=C(e=>{let t=e.target;[...g.branches].some(e=>e.contains(t))||(s?.(e),l?.(e),e.defaultPrevented||p?.())},x);return d(e=>{A===g.layers.size-1&&(r?.(e),!e.defaultPrevented&&p&&(e.preventDefault(),p()))},x),u.useEffect(()=>{if(y)return n&&(g.layersWithOutsidePointerEventsDisabled.size===0&&(_=x.body.style.pointerEvents,x.body.style.pointerEvents=`none`),g.layersWithOutsidePointerEventsDisabled.add(y)),g.layers.add(y),w(),()=>{n&&g.layersWithOutsidePointerEventsDisabled.size===1&&(x.body.style.pointerEvents=_)}},[y,x,n,g]),u.useEffect(()=>()=>{y&&(g.layers.delete(y),g.layersWithOutsidePointerEventsDisabled.delete(y),w())},[y,g]),u.useEffect(()=>{let e=()=>T({});return document.addEventListener(m,e),()=>document.removeEventListener(m,e)},[]),(0,f.jsx)(a.div,{...h,ref:E,style:{pointerEvents:j?M?`auto`:`none`:void 0,...e.style},onFocusCapture:c(e.onFocusCapture,P.onFocusCapture),onBlurCapture:c(e.onBlurCapture,P.onBlurCapture),onPointerDownCapture:c(e.onPointerDownCapture,N.onPointerDownCapture)})});y.displayName=p;var b=`DismissableLayerBranch`,x=u.forwardRef((e,t)=>{let n=u.useContext(v),r=u.useRef(null),i=o(t,r);return u.useEffect(()=>{let e=r.current;if(e)return n.branches.add(e),()=>{n.branches.delete(e)}},[n.branches]),(0,f.jsx)(a.div,{...e,ref:i})});x.displayName=b;function S(e,t=globalThis?.document){let n=l(e),r=u.useRef(!1),i=u.useRef(()=>{});return u.useEffect(()=>{let e=e=>{if(e.target&&!r.current){let r=function(){T(h,n,a,{discrete:!0})},a={originalEvent:e};e.pointerType===`touch`?(t.removeEventListener(`click`,i.current),i.current=r,t.addEventListener(`click`,i.current,{once:!0})):r()}else t.removeEventListener(`click`,i.current);r.current=!1},a=window.setTimeout(()=>{t.addEventListener(`pointerdown`,e)},0);return()=>{window.clearTimeout(a),t.removeEventListener(`pointerdown`,e),t.removeEventListener(`click`,i.current)}},[t,n]),{onPointerDownCapture:()=>r.current=!0}}function C(e,t=globalThis?.document){let n=l(e),r=u.useRef(!1);return u.useEffect(()=>{let e=e=>{e.target&&!r.current&&T(g,n,{originalEvent:e},{discrete:!1})};return t.addEventListener(`focusin`,e),()=>t.removeEventListener(`focusin`,e)},[t,n]),{onFocusCapture:()=>r.current=!0,onBlurCapture:()=>r.current=!1}}function w(){let e=new CustomEvent(m);document.dispatchEvent(e)}function T(e,t,n,{discrete:i}){let a=n.originalEvent.target,o=new CustomEvent(e,{bubbles:!1,cancelable:!0,detail:n});t&&a.addEventListener(e,t,{once:!0}),i?r(a,o):a.dispatchEvent(o)}var E=t(i(),1),D=`Portal`,O=u.forwardRef((e,t)=>{let{container:n,...r}=e,[i,o]=u.useState(!1);s(()=>o(!0),[]);let c=n||i&&globalThis?.document?.body;return c?E.createPortal((0,f.jsx)(a.div,{...r,ref:t}),c):null});O.displayName=D;export{y as n,O as t};
|
||||
@@ -1 +1 @@
|
||||
import{$d as e,Xd as t}from"./messages-kZy_2fG-.js";import{n}from"./dist-e45zkog2.js";var r=e(t(),1);function i(e){let[t,i]=r.useState(void 0);return n(()=>{if(e){i({width:e.offsetWidth,height:e.offsetHeight});let t=new ResizeObserver(t=>{if(!Array.isArray(t)||!t.length)return;let n=t[0],r,a;if(`borderBoxSize`in n){let e=n.borderBoxSize,t=Array.isArray(e)?e[0]:e;r=t.inlineSize,a=t.blockSize}else r=e.offsetWidth,a=e.offsetHeight;i({width:r,height:a})});return t.observe(e,{box:`border-box`}),()=>t.unobserve(e)}else i(void 0)},[e]),t}export{i as t};
|
||||
import{af as e,cf as t}from"./messages-BSgosaSa.js";import{n}from"./dist-BX7-FFVx.js";var r=t(e(),1);function i(e){let[t,i]=r.useState(void 0);return n(()=>{if(e){i({width:e.offsetWidth,height:e.offsetHeight});let t=new ResizeObserver(t=>{if(!Array.isArray(t)||!t.length)return;let n=t[0],r,a;if(`borderBoxSize`in n){let e=n.borderBoxSize,t=Array.isArray(e)?e[0]:e;r=t.inlineSize,a=t.blockSize}else r=e.offsetWidth,a=e.offsetHeight;i({width:r,height:a})});return t.observe(e,{box:`border-box`}),()=>t.unobserve(e)}else i(void 0)},[e]),t}export{i as t};
|
||||
@@ -0,0 +1 @@
|
||||
import{af as e,cf as t,if as n}from"./messages-BSgosaSa.js";import{c as r,d as i}from"./button-gsP5wmrs.js";import{a}from"./dist-BX7-FFVx.js";var o=t(e(),1),s=n();function c(e){let t=e+`CollectionProvider`,[n,c]=a(t),[l,u]=n(t,{collectionRef:{current:null},itemMap:new Map}),d=e=>{let{scope:t,children:n}=e,r=o.useRef(null),i=o.useRef(new Map).current;return(0,s.jsx)(l,{scope:t,itemMap:i,collectionRef:r,children:n})};d.displayName=t;let f=e+`CollectionSlot`,p=r(f),m=o.forwardRef((e,t)=>{let{scope:n,children:r}=e;return(0,s.jsx)(p,{ref:i(t,u(f,n).collectionRef),children:r})});m.displayName=f;let h=e+`CollectionItemSlot`,g=`data-radix-collection-item`,_=r(h),v=o.forwardRef((e,t)=>{let{scope:n,children:r,...a}=e,c=o.useRef(null),l=i(t,c),d=u(h,n);return o.useEffect(()=>(d.itemMap.set(c,{ref:c,...a}),()=>void d.itemMap.delete(c))),(0,s.jsx)(_,{[g]:``,ref:l,children:r})});v.displayName=h;function y(t){let n=u(e+`CollectionConsumer`,t);return o.useCallback(()=>{let e=n.collectionRef.current;if(!e)return[];let t=Array.from(e.querySelectorAll(`[${g}]`));return Array.from(n.itemMap.values()).sort((e,n)=>t.indexOf(e.ref.current)-t.indexOf(n.ref.current))},[n.collectionRef,n.itemMap])}return[{Provider:d,Slot:m,ItemSlot:v},y,c]}export{c as t};
|
||||
@@ -1 +1 @@
|
||||
import{$d as e,Xd as t}from"./messages-kZy_2fG-.js";var n=e(t(),1);function r(e){let t=n.useRef({value:e,previous:e});return n.useMemo(()=>(t.current.value!==e&&(t.current.previous=t.current.value,t.current.value=e),t.current.previous),[e])}export{r as t};
|
||||
import{af as e,cf as t}from"./messages-BSgosaSa.js";var n=t(e(),1);function r(e){let t=n.useRef({value:e,previous:e});return n.useMemo(()=>(t.current.value!==e&&(t.current.previous=t.current.value,t.current.value=e),t.current.previous),[e])}export{r as t};
|
||||
@@ -1 +1 @@
|
||||
import{$d as e,Xd as t,Yd as n}from"./messages-kZy_2fG-.js";var r=e(t(),1),i=n();function a(e,t){let n=r.createContext(t),a=e=>{let{children:t,...a}=e,o=r.useMemo(()=>a,Object.values(a));return(0,i.jsx)(n.Provider,{value:o,children:t})};a.displayName=e+`Provider`;function o(i){let a=r.useContext(n);if(a)return a;if(t!==void 0)return t;throw Error(`\`${i}\` must be used within \`${e}\``)}return[a,o]}function o(e,t=[]){let n=[];function a(t,a){let o=r.createContext(a),s=n.length;n=[...n,a];let c=t=>{let{scope:n,children:a,...c}=t,l=n?.[e]?.[s]||o,u=r.useMemo(()=>c,Object.values(c));return(0,i.jsx)(l.Provider,{value:u,children:a})};c.displayName=t+`Provider`;function l(n,i){let c=i?.[e]?.[s]||o,l=r.useContext(c);if(l)return l;if(a!==void 0)return a;throw Error(`\`${n}\` must be used within \`${t}\``)}return[c,l]}let o=()=>{let t=n.map(e=>r.createContext(e));return function(n){let i=n?.[e]||t;return r.useMemo(()=>({[`__scope${e}`]:{...n,[e]:i}}),[n,i])}};return o.scopeName=e,[a,s(o,...t)]}function s(...e){let t=e[0];if(e.length===1)return t;let n=()=>{let n=e.map(e=>({useScope:e(),scopeName:e.scopeName}));return function(e){let i=n.reduce((t,{useScope:n,scopeName:r})=>{let i=n(e)[`__scope${r}`];return{...t,...i}},{});return r.useMemo(()=>({[`__scope${t.scopeName}`]:i}),[i])}};return n.scopeName=t.scopeName,n}typeof window<`u`&&window.document&&window.document.createElement;function c(e,t,{checkForDefaultPrevented:n=!0}={}){return function(r){if(e?.(r),n===!1||!r.defaultPrevented)return t?.(r)}}var l=globalThis?.document?r.useLayoutEffect:()=>{},u=r.useInsertionEffect||l;function d({prop:e,defaultProp:t,onChange:n=()=>{},caller:i}){let[a,o,s]=f({defaultProp:t,onChange:n}),c=e!==void 0,l=c?e:a;{let t=r.useRef(e!==void 0);r.useEffect(()=>{let e=t.current;e!==c&&console.warn(`${i} is changing from ${e?`controlled`:`uncontrolled`} to ${c?`controlled`:`uncontrolled`}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`),t.current=c},[c,i])}return[l,r.useCallback(t=>{if(c){let n=p(t)?t(e):t;n!==e&&s.current?.(n)}else o(t)},[c,e,o,s])]}function f({defaultProp:e,onChange:t}){let[n,i]=r.useState(e),a=r.useRef(n),o=r.useRef(t);return u(()=>{o.current=t},[t]),r.useEffect(()=>{a.current!==n&&(o.current?.(n),a.current=n)},[n,a]),[n,i,o]}function p(e){return typeof e==`function`}export{o as a,a as i,l as n,c as r,d as t};
|
||||
import{af as e,cf as t,if as n}from"./messages-BSgosaSa.js";var r=t(e(),1),i=n();function a(e,t){let n=r.createContext(t),a=e=>{let{children:t,...a}=e,o=r.useMemo(()=>a,Object.values(a));return(0,i.jsx)(n.Provider,{value:o,children:t})};a.displayName=e+`Provider`;function o(i){let a=r.useContext(n);if(a)return a;if(t!==void 0)return t;throw Error(`\`${i}\` must be used within \`${e}\``)}return[a,o]}function o(e,t=[]){let n=[];function a(t,a){let o=r.createContext(a),s=n.length;n=[...n,a];let c=t=>{let{scope:n,children:a,...c}=t,l=n?.[e]?.[s]||o,u=r.useMemo(()=>c,Object.values(c));return(0,i.jsx)(l.Provider,{value:u,children:a})};c.displayName=t+`Provider`;function l(n,i){let c=i?.[e]?.[s]||o,l=r.useContext(c);if(l)return l;if(a!==void 0)return a;throw Error(`\`${n}\` must be used within \`${t}\``)}return[c,l]}let o=()=>{let t=n.map(e=>r.createContext(e));return function(n){let i=n?.[e]||t;return r.useMemo(()=>({[`__scope${e}`]:{...n,[e]:i}}),[n,i])}};return o.scopeName=e,[a,s(o,...t)]}function s(...e){let t=e[0];if(e.length===1)return t;let n=()=>{let n=e.map(e=>({useScope:e(),scopeName:e.scopeName}));return function(e){let i=n.reduce((t,{useScope:n,scopeName:r})=>{let i=n(e)[`__scope${r}`];return{...t,...i}},{});return r.useMemo(()=>({[`__scope${t.scopeName}`]:i}),[i])}};return n.scopeName=t.scopeName,n}typeof window<`u`&&window.document&&window.document.createElement;function c(e,t,{checkForDefaultPrevented:n=!0}={}){return function(r){if(e?.(r),n===!1||!r.defaultPrevented)return t?.(r)}}var l=globalThis?.document?r.useLayoutEffect:()=>{},u=r.useInsertionEffect||l;function d({prop:e,defaultProp:t,onChange:n=()=>{},caller:i}){let[a,o,s]=f({defaultProp:t,onChange:n}),c=e!==void 0,l=c?e:a;{let t=r.useRef(e!==void 0);r.useEffect(()=>{let e=t.current;e!==c&&console.warn(`${i} is changing from ${e?`controlled`:`uncontrolled`} to ${c?`controlled`:`uncontrolled`}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`),t.current=c},[c,i])}return[l,r.useCallback(t=>{if(c){let n=p(t)?t(e):t;n!==e&&s.current?.(n)}else o(t)},[c,e,o,s])]}function f({defaultProp:e,onChange:t}){let[n,i]=r.useState(e),a=r.useRef(n),o=r.useRef(t);return u(()=>{o.current=t},[t]),r.useEffect(()=>{a.current!==n&&(o.current?.(n),a.current=n)},[n,a]),[n,i,o]}function p(e){return typeof e==`function`}export{o as a,a as i,l as n,c as r,d as t};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user