543a54c606
单个固定收款地址 + 每单唯一金额(base+微尾数≤0.01U)+ 精确==匹配 + 时间戳防迟到误配 + 孤儿人工对账。归集=1地址(激活一次/扫一笔)最省。前端契约不变(POST /order 返 address+amount), 以后升多地址/GasFree 纯后端切。删除已被取代的地址池 plan。双产物 md+html,登记 index。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
4.6 KiB
Markdown
70 lines
4.6 KiB
Markdown
# pangolin-pay 单地址 + 唯一金额收款模型(定稿)
|
||
|
||
> #34/34A 收款模型**定稿**。取代"每单唯一 HD 地址"(激活/归集随订单数线性涨)与"地址池"(仍多地址)。
|
||
> 最终选:**单个固定收款地址 + 每单唯一金额**。归集 = **1 个地址**(激活一次、扫一笔),成本最省;
|
||
> 订单靠**金额**区分,不靠地址。
|
||
>
|
||
> **前端契约不变**:`POST /order` 仍返回 `{address, expect_amount}`,client 只用每单返回值 → 以后要升
|
||
> 多地址/GasFree 是**纯后端换实现**,前端零改动(见下"契约约定")。
|
||
>
|
||
> 已实现的 wallet 派生 / tron(TronGrid 读到账、建交易、签名)/ cmd/sweep 复用;主要改 **store / pay(建单)/
|
||
> watcher(匹配)**。现有代码是"每单派生新址",本轮改为"单地址 + 唯一金额"。
|
||
|
||
## 决策(已定)
|
||
|
||
- **收款地址**:钱包 A 的地址 0(`m/44'/195'/0'/0/0`),从配置注入或由 xpub 派生。所有订单收到**这一个地址**。
|
||
- **唯一金额** ⭐:`expect_amount = base + tail`。base = 价格 ×1e6(micro-USDT);**tail 取方案 A(微尾数)**:
|
||
`tail ∈ [1, 9999]` micro(偏差 ≤ 0.009999 USDT,**价格几乎不变**)。tail 在**迟到窗口内不复用**。
|
||
- **匹配 = 精确 `==`**:watcher 找"到收款地址、`value == expect_amount`、`block_ts > order.created`"的 TRC20 转入 → paid。
|
||
支付页显示**可一键复制的精确金额** + "请付精确金额"提示。
|
||
- **15min 超时** → expired → 提示重建订单(该 tail 一段时间内不复用)。
|
||
- **同用户单订单**:同时只能一个活跃(pending)订单。
|
||
- **孤儿付款**:到该地址但 `value` 不匹配任何活跃订单(付错/迟到抹了尾数)→ 记 `orphan_payments`,人工对账/补发。
|
||
- **归集**:定期扫**这一个地址**余额 → 冷钱包(**能量租赁** ~$0.1–1/笔)。激活一次、归集一笔。
|
||
|
||
## 契约约定(钉死,保证以后单↔多地址纯后端)
|
||
|
||
> **client(下单页/独角数卡)只使用每单 `POST /order` 返回的 `address` + `expect_amount`,绝不硬编码/缓存地址。**
|
||
> 只要守住这条,单地址 ↔ 多地址 ↔ GasFree 都是后端内部换实现,前端与 VPN app 都不改。
|
||
|
||
## Phase A — 数据模型
|
||
|
||
- [ ] `pay_orders` 改:加 `user_ref`、`expect_amount`(唯一金额,micro-USDT)、`matched_tx_id`;`address` 恒为收款地址。
|
||
- [ ] tail 分配支撑:记录**当前活跃订单已用 tail**(避免撞)+ **近期已用 tail 冷却**(迟到窗口内不复用);或全局计数器 + 去重校验。
|
||
- [ ] `orphan_payments` 表:`tx_id(唯一) / value / block_ts / created_at / handled(bool)`。
|
||
- [ ] 配置:`PAY_RECEIVE_ADDRESS`(或从 `PAY_ACCOUNT_XPUB` 派生 index 0,与钱包 A 地址 0 一致)。
|
||
|
||
## Phase B — 建单(pay 服务)
|
||
|
||
- [ ] `CreateOrder(userRef, sku, priceMicro)`:① 校验**同用户无活跃单**(有则返回现有/拒);② 分配**唯一金额**(base + 未占用 tail);③ 写 `pending` 单(TTL 15min),`address` = 收款地址。
|
||
- [ ] **tail 分配**:与当前所有活跃订单不撞 + 迟到窗口内不复用(记录+回收)。
|
||
- [ ] `GetOrder`。
|
||
|
||
## Phase C — watcher 改造(匹配 / 超时 / 孤儿)
|
||
|
||
- [ ] **匹配**:对每个 `pending` 单,查**收款地址**的 TRC20 转入,筛 `value == expect_amount` **且 `block_ts > order.created`** → `paid` + 记 `matched_tx_id`。
|
||
- [ ] **超时**:`pending` 过期 → `expired`。
|
||
- [ ] **孤儿**:收款地址的到账 tx 匹配不到任何活跃订单 → 记 `orphan_payments`(按 tx_id 去重)。
|
||
- [ ] **幂等**(tx_id 全局去重,含 orphan)+ **崩溃恢复**;TronGrid 需取 `block_timestamp` 供时间过滤。
|
||
|
||
## Phase D — 归集(复用 cmd/sweep)
|
||
|
||
- [ ] 扫**收款地址**余额 → 冷钱包(**能量租赁**,先手动 runbook);一地址一笔,与订单状态解耦。
|
||
|
||
## Phase E — 测试 + 真链验证
|
||
|
||
- [ ] 单测:唯一金额分配不撞、精确匹配、**付错/迟到金额成孤儿**、超时、同用户单订单、幂等、**时间戳过滤**(旧到账不误配新单)、崩溃恢复。
|
||
- [ ] 真链:两并发订单**不同唯一金额、同一地址**各自付款到 paid;故意**付错金额** → 成孤儿不误配;归集该地址到冷钱包。
|
||
|
||
## Verification / 判据
|
||
|
||
- `go test ./...`(store/pay/watcher 新逻辑全绿,重点覆盖唯一金额/精确匹配/孤儿/时间戳)。
|
||
- 真链:并发不同金额 + 付错成孤儿 两场景正确;归集一笔搞定。
|
||
- 成本:激活一次、归集 O(1);小额高频不再被激活/归集费拖累。
|
||
|
||
## 不在本轮 / 以后(纯后端可切)
|
||
|
||
- 多地址 / 地址池(容错更好但更贵,以后纯后端切,前端不动)。
|
||
- GasFree(#35,免 TRX 但每笔固定 1 USDT,适合少地址批量)。
|
||
- 能量租赁自动化;发货侧(独角数卡 + /internal/codes/mint)另排。
|