Files
pangolin/docs/superpowers/plans/2026-07-09-pay-single-address.md
wangjia 543a54c606 docs(pay): 收款模型定稿为单地址+唯一金额,取代地址池 plan(#34/34A)
单个固定收款地址 + 每单唯一金额(base+微尾数≤0.01U)+ 精确==匹配 + 时间戳防迟到误配 +
孤儿人工对账。归集=1地址(激活一次/扫一笔)最省。前端契约不变(POST /order 返 address+amount),
以后升多地址/GasFree 纯后端切。删除已被取代的地址池 plan。双产物 md+html,登记 index。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 15:06:59 +08:00

4.6 KiB
Raw Permalink Blame History

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_amountblock_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_refexpect_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.createdpaid + 记 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)另排。