Files
pangolin/docs/superpowers/plans/2026-07-09-pay-address-pool.md
T
wangjia 5055afdbd7 docs(pay): 地址池收款模型 plan(折中方案,#34/34A)
从每单唯一 HD 地址改为地址池复用(池=峰值并行度、复用摊薄激活/归集)。规则:最小编号
idle 绑单/无 idle 派生/15min 超时释放/同用户单订单。核心安全点:地址复用迟到付款错配
→ 匹配按 tx+金额+时间戳(晚于建单)、每单唯一金额、孤儿付款记录。双产物 md+html,登记 index。
待用户确认后执行(tier-1)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 14:44:22 +08:00

72 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# pangolin-pay 地址池收款模型(折中方案,取代"每单唯一 HD 地址")
> #34/34A 收款模型修订。**Context**:每单唯一 HD 地址 → 每地址激活(~1 TRX)+ 归集(每笔一次转账)成本随
> **订单总数线性增长**(#34 暴露的严峻成本)。改为**地址池复用**:池大小 = **峰值并行度**(初始 10,不够动态派生),
> 地址**复用**摊薄激活/归集;仍是多地址,**避免并发订单金额撞、对账精确**。这是"单地址(省钱但撞单)"与
> "每单唯一(精确但贵)"的折中。
>
> 归集成本从"O(订单数)"降到"O(池大小)";激活成本一次性(每个池地址激活一次即长期复用)。
>
> **本 plan = 用户定的规则 + 补齐的安全点**(核心:地址复用的**迟到付款错配**)。已实现的 Phase B(钱包派生)、
> tron(TronGrid 读到账/建/广播/签名)、cmd/sweep 复用;主要改 store / pay(建单)/ watcher(匹配)。
## 设计(规则 + 安全点)
- **地址池**:HD 派生地址(路径 `m/44'/195'/0'/0/i`,与现有一致),各有状态 `idle | busy`。初始池 10。
- **建单**:绑**最小编号的 idle** 地址 → 置 `busy`;**无 idle → 派生下一个新地址**(编号递增)入池并绑之。
- **每单唯一金额** ⭐必需:基准价 + 唯一尾数(如按订单序号映射的微分),保证**当前活跃订单之间金额不撞**,
且复用地址上"旧订单迟到付款"不会误配"新订单"。
- **15 分钟超时**:超时 → `expired` → 地址回 `idle`(可选**冷却期**再重绑,降低迟到错配)→ 提示用户**重建订单**、用新订单地址付款。
- **付款完成**:watcher 确认到账 → 订单 `paid` → 地址回 `idle`。**注意**:此时地址里**仍有 USDT**(待归集);
`idle` 只表示"可重绑",不代表钱清了。
- **匹配规则(核心)**:watcher 找**到该地址、金额 == 该单唯一金额(或 ≥,带容差)、且到账区块时间晚于订单创建**
的 TRC20 转入 **tx** → 匹配。**绝不按余额、不只按地址**(地址会累积余额、会复用)。
- **同用户单订单**:同一用户同时只能有**一个**活跃(pending)订单。
- **孤儿付款**:到账 tx 匹配不到任何活跃订单(迟到 / 欠额 / 误差)→ 记 `orphan_payments`,人工对账/补发。
- **归集**:定期扫池内**有余额**的地址 → 冷钱包(**能量租赁** ~$0.11/笔);池 ~10 地址,**归集笔数有界**;
归集与 `busy` 无关(随时可扫,不影响待付订单——到账是未来的独立 tx)。
- **池增长上限**(可选):无空闲就新建,设上限防滥用无限增长(超限则排队/拒单)。
## Phase A — 数据模型改造
- [ ] `addresses` 表:`id / addr_index(唯一) / address / state('idle'|'busy') / activated(bool) / created_at`;初始不预建,按需派生入池。
- [ ] `pay_orders` 改:加 `user_ref`(同用户单订单约束用)、`address_id`(绑定的池地址)、`expect_amount`(唯一金额,micro-USDT)、`matched_tx_id`;保留 status/created_at/expires_at。
- [ ] `orphan_payments` 表:`tx_id(唯一) / address / value / block_ts / created_at / handled(bool)`
- [ ] 复用现有 `addr_cursor`(HD 派生游标)派生新池地址。迁移脚本 + 索引(status、address、user_ref)。
## Phase B — 建单(pay 服务)
- [ ] `CreateOrder(userRef, sku)`:① 校验**同用户无活跃订单**(有则返回现有/拒);② **原子**选最小编号 `idle` 地址(事务 + 行锁)置 `busy`;③ 无 idle → `NextAddrIndex` 派生新地址(`AddressFromAccountXpub`)入池并绑;④ 分配**唯一金额**;⑤ 写 `pending` 单(TTL 15min)。
- [ ] **唯一金额分配**:基准金额 + 唯一尾数,保证与**当前所有活跃订单**不撞(可用订单序号 → 尾数微分,micro-USDT 级);记录基准+尾数便于展示。
- [ ] `GetOrder`;释放地址的公共方法 `releaseAddress(addrID)`(paid/expired 复用)。
## Phase C — watcher 改造(匹配 + 释放 + 孤儿)
- [ ] **匹配**:对每个 `pending` 单,查其绑定地址的 TRC20 转入;筛 `value` 命中唯一金额(==,或 ≥ 带小容差)**且 `block_ts > order.created_at`** → `paid` + 记 `matched_tx_id`;**释放地址 → idle**。
- [ ] **超时**:`pending` 过期 → `expired` + 释放地址 → idle(可选冷却期后才允许重绑)。
- [ ] **孤儿**:遍历池地址的到账 tx,凡匹配不到任何活跃订单的转入 → 写 `orphan_payments`(去重 by tx_id)。
- [ ] **幂等**(tx_id 全局去重,含 orphan)、崩溃恢复(从 DB pending + 池状态重建)。TronGrid 需能取到账时间戳(`block_timestamp`)用于时间过滤。
## Phase D — 归集(复用 cmd/sweep)
- [ ] 扫池内**有余额**地址 → 冷钱包;接**能量租赁**(先手动 runbook,后续自动化);归集与订单状态解耦。
- [ ] 归集不清空 `busy` 语义;只搬余额。runbook 写清"哪些地址、多久扫一次、能量怎么租"。
## Phase E — 测试 + 真链验证
- [ ] 单测:并发建单**不抢同址**(原子性)、地址**复用后旧订单迟到付款不误配新订单**(唯一金额+时间戳)、唯一金额不撞、超时释放、孤儿记录、幂等、崩溃恢复、同用户单订单约束。
- [ ] 真链:两并发订单各绑不同地址各自付款到 paid;一单超时释放后新单**复用同址**,再对旧地址补一笔"旧金额"付款 → 应成**孤儿**、不误配新单;归集池地址到冷钱包。
## Verification / 判据
- `go test ./...`(store/pay/watcher 新逻辑全绿,重点覆盖复用错配)。
- 真链:并发 + 复用 + 迟到付款三场景表现正确;归集笔数 = 有余额的池地址数(有界)。
- 成本:激活一次/地址、归集 O(池大小);对小额高频订单不再被激活费线性拖累。
## 不在本轮 / 取舍
- GasFree(#35,免 TRX 但每笔固定 1 USDT,更适合少地址批量,另议)。
- 能量租赁自动化(先手动)。
- 池增长上限/排队(先不设,或设一个宽上限)。
- 发货侧(独角数卡 + /internal/codes/mint)另排。