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>
This commit is contained in:
+4
-4
@@ -91,10 +91,10 @@
|
||||
</a>
|
||||
|
||||
<h2>实现计划 / Plans</h2>
|
||||
<a class="doc" href="pay-address-pool-plan.html">
|
||||
<div class="t">pangolin-pay 地址池收款模型(折中方案,#34/34A)<span class="tag html">HTML</span></div>
|
||||
<div class="d">阅读版;执行真相源 <code>docs/superpowers/plans/2026-07-09-pay-address-pool.md</code>(含 checkbox)。从"每单唯一 HD 地址"改为<b>地址池复用</b>:池大小=峰值并行度(初始 10,不够动态派生),地址复用摊薄激活/归集,归集成本 O(订单数)→O(池大小)。规则:最小编号 idle 绑单、无 idle 派生新址、15min 超时释放、同用户单订单。核心安全点:地址复用的<b>迟到付款错配</b>——匹配按"到账 tx+金额+时间戳(晚于建单)"、每单唯一金额、孤儿付款记录。5 Phase:数据模型→建单→watcher 匹配/释放/孤儿→归集(能量租赁)→并发/复用真链验证。</div>
|
||||
<div class="path">docs/pay-address-pool-plan.html · 真相源 docs/superpowers/plans/2026-07-09-pay-address-pool.md</div>
|
||||
<a class="doc" href="pay-single-address-plan.html">
|
||||
<div class="t">pangolin-pay 单地址 + 唯一金额收款模型(定稿,#34/34A)<span class="tag html">HTML</span></div>
|
||||
<div class="d">阅读版;执行真相源 <code>docs/superpowers/plans/2026-07-09-pay-single-address.md</code>(含 checkbox)。<b>定稿:单个固定收款地址 + 每单唯一金额</b>(取代每单唯一 HD 地址/地址池)。归集=1 地址(激活一次、扫一笔)最省;订单靠金额区分。唯一金额=base+微尾数(≤0.01 USDT);精确 == 匹配 + 时间戳(晚于建单)防迟到误配;付错→孤儿人工对账。前端契约不变(POST /order 返回 address+expect_amount)→ 以后升多地址/GasFree 纯后端切、前端零改。5 Phase:数据模型→建单→watcher 匹配/超时/孤儿→归集(能量租赁)→并发/付错真链验证。</div>
|
||||
<div class="path">docs/pay-single-address-plan.html · 真相源 docs/superpowers/plans/2026-07-09-pay-single-address.md</div>
|
||||
</a>
|
||||
<a class="doc" href="crypto-tx-engine-plan.html">
|
||||
<div class="t">pangolin-pay 加密货币交易引擎(#34 第一块,可独立验证)<span class="tag html">HTML</span></div>
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>pangolin-pay 地址池收款模型 实现计划</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0f1117; --panel:#171a22; --panel2:#1d2129; --fg:#e6e8ee; --fg2:#a8afbd;
|
||||
--accent:#e0884f; --accent2:#5fb0c9; --ok:#5ec27a; --bad:#e06a6a; --warn:#e0b84f;
|
||||
--border:#272c36; --mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
|
||||
--sans:-apple-system,"PingFang SC","Helvetica Neue",Arial,sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
|
||||
.wrap{max-width:960px;margin:0 auto;padding:48px 24px 96px}
|
||||
h1{font-size:28px;line-height:1.3;margin:0 0 8px}
|
||||
.sub{color:var(--fg2);font-size:15px;margin:0 0 32px}
|
||||
h2{font-size:20px;margin:40px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border);color:var(--accent)}
|
||||
p{margin:10px 0}
|
||||
code{font-family:var(--mono);font-size:.85em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
|
||||
pre{background:#0a0c11;border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font-family:var(--mono);font-size:12.5px;line-height:1.5;color:#cdd3df}
|
||||
.tag{display:inline-block;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px}
|
||||
.tag.info{background:rgba(95,176,201,.16);color:var(--accent2)}
|
||||
ul.ck{list-style:none;padding-left:4px}
|
||||
ul.ck li{position:relative;padding-left:26px;margin:7px 0}
|
||||
ul.ck li::before{content:"☐";position:absolute;left:0;color:var(--fg2)}
|
||||
ul{padding-left:22px} li{margin:6px 0}
|
||||
.lead{background:linear-gradient(180deg,rgba(224,136,79,.10),transparent);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:0 0 8px}
|
||||
.small{color:var(--fg2);font-size:13px}
|
||||
a{color:var(--accent2)} .back{display:inline-block;margin-bottom:24px;font-size:13px}
|
||||
b{color:#fff}
|
||||
.warnbox{background:rgba(224,106,106,.08);border:1px solid rgba(224,106,106,.35);border-radius:12px;padding:14px 18px;margin:14px 0}
|
||||
.warnbox b{color:var(--bad)}
|
||||
table{width:100%;border-collapse:collapse;margin:16px 0;font-size:13.5px}
|
||||
th,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:top}
|
||||
th{color:var(--fg2);font-weight:600;font-size:13px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<a class="back" href="index.html">← 文档索引</a>
|
||||
<h1>pangolin-pay 地址池收款模型</h1>
|
||||
<p class="sub">2026-07-09 · <span class="tag info">阅读版</span> · 执行真相源 <code>docs/superpowers/plans/2026-07-09-pay-address-pool.md</code> · #34/34A 收款模型修订(折中方案)</p>
|
||||
|
||||
<div class="lead">
|
||||
从"每单唯一 HD 地址"改为<b>地址池复用</b>:池大小 = <b>峰值并行度</b>(初始 10,不够动态派生),地址复用摊薄激活/归集成本;仍多地址,<b>避免并发订单金额撞、对账精确</b>。归集成本从 O(订单数) 降到 <b>O(池大小)</b>,激活一次/地址即长期复用。是"单地址(省钱撞单)"与"每单唯一(精确但贵)"的折中。
|
||||
</div>
|
||||
|
||||
<div class="warnbox">
|
||||
<b>核心风险 · 地址复用的迟到付款错配:</b>地址回收后绑给新订单,若前一个用户超时后才付款,钱打到已绑新订单的地址上 → 可能误配。<b>解法(必须):</b>① 匹配按"到账 tx + 金额 + 时间戳(晚于建单)",<b>不按余额、不只按地址</b>;② <b>每单唯一金额</b>(基准+唯一尾数);③ 匹配不到活跃订单的到账 → 记 <code>orphan_payments</code> 人工对账;④(可选)超时后地址冷却再重绑。
|
||||
</div>
|
||||
|
||||
<h2>设计(规则 + 安全点)</h2>
|
||||
<ul>
|
||||
<li><b>地址池</b>:HD 派生地址(<code>m/44'/195'/0'/0/i</code>),状态 <code>idle|busy</code>,初始 10。</li>
|
||||
<li><b>建单</b>:绑<b>最小编号 idle</b> 地址→busy;无 idle → 派生新地址入池。<b>原子</b>(事务/行锁,防并发抢同址)。</li>
|
||||
<li><b>每单唯一金额</b> ⭐:基准 + 唯一尾数,当前活跃订单间不撞;复用地址上旧单迟到付款不误配新单。</li>
|
||||
<li><b>15min 超时</b> → expired → 地址回 idle(可选冷却)→ 提示重建订单。</li>
|
||||
<li><b>付款完成</b> → paid → 地址回 idle(注意:地址里仍有 USDT 待归集,idle 只表示可重绑)。</li>
|
||||
<li><b>匹配</b>:找"到该地址 + 金额命中 + 区块时间晚于建单"的转入 tx。</li>
|
||||
<li><b>同用户单订单</b>:同时只能一个活跃订单。</li>
|
||||
<li><b>归集</b>:定期扫池内有余额地址→冷钱包(能量租赁 ~$0.1–1/笔);池 ~10,笔数有界;与 busy 解耦。</li>
|
||||
</ul>
|
||||
|
||||
<h2>Phase A — 数据模型</h2>
|
||||
<ul class="ck">
|
||||
<li><b>A.1</b> <code>addresses(id, addr_index唯一, address, state, activated, created_at)</code>,按需派生入池。</li>
|
||||
<li><b>A.2</b> <code>pay_orders</code> 加 <code>user_ref / address_id / expect_amount(唯一金额) / matched_tx_id</code>。</li>
|
||||
<li><b>A.3</b> <code>orphan_payments(tx_id唯一, address, value, block_ts, handled)</code>。</li>
|
||||
<li><b>A.4</b> 复用 addr_cursor 派生新址;迁移 + 索引(status/address/user_ref)。</li>
|
||||
</ul>
|
||||
|
||||
<h2>Phase B — 建单</h2>
|
||||
<ul class="ck">
|
||||
<li><b>B.1</b> CreateOrder(user, sku):校验同用户无活跃单 → 原子选最小 idle 置 busy(无则派生)→ 分配唯一金额 → 写 pending(TTL 15min)。</li>
|
||||
<li><b>B.2</b> 唯一金额分配(基准+唯一尾数,当前活跃单间不撞)。</li>
|
||||
<li><b>B.3</b> GetOrder + releaseAddress 公共方法(paid/expired 复用)。</li>
|
||||
</ul>
|
||||
|
||||
<h2>Phase C — watcher(匹配/释放/孤儿)</h2>
|
||||
<ul class="ck">
|
||||
<li><b>C.1</b> 匹配:pending 单查其地址转入,value 命中唯一金额 且 <code>block_ts>created</code> → paid + 记 tx + 释放地址。</li>
|
||||
<li><b>C.2</b> 超时:pending 过期 → expired + 释放地址(可选冷却)。</li>
|
||||
<li><b>C.3</b> 孤儿:匹配不到活跃订单的到账 → 记 orphan_payments(去重)。</li>
|
||||
<li><b>C.4</b> 幂等(tx_id 去重)+ 崩溃恢复;TronGrid 取 block_timestamp 供时间过滤。</li>
|
||||
</ul>
|
||||
|
||||
<h2>Phase D — 归集</h2>
|
||||
<ul class="ck">
|
||||
<li><b>D.1</b> 扫池内有余额地址 → 冷钱包(能量租赁,先手动 runbook);与订单状态解耦。</li>
|
||||
</ul>
|
||||
|
||||
<h2>Phase E — 测试 + 真链</h2>
|
||||
<ul class="ck">
|
||||
<li><b>E.1</b> 单测:并发不抢同址、复用后迟到付款不误配、唯一金额、超时释放、孤儿、幂等、崩溃恢复、同用户单订单。</li>
|
||||
<li><b>E.2</b> 真链:两并发订单各绑地址付款;一单超时释放后新单复用同址,旧地址补"旧金额"付款应成孤儿不误配;归集。</li>
|
||||
</ul>
|
||||
|
||||
<h2>不在本轮 / 取舍</h2>
|
||||
<ul>
|
||||
<li>GasFree(#35,免 TRX 但每笔固定 1 USDT,更适合少地址批量,另议)。</li>
|
||||
<li>能量租赁自动化(先手动);池增长上限(先宽松);发货侧(独角数卡 + /internal/codes/mint)另排。</li>
|
||||
</ul>
|
||||
|
||||
<p class="small" style="margin-top:32px">相关:<a href="crypto-tx-engine-plan.html">加密货币交易引擎(已实现钱包/tron/sweep)</a> · <a href="payment-clean-usdt-plan.html">#34 收款闭环全景</a></p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>pangolin-pay 单地址 + 唯一金额收款模型 实现计划</title>
|
||||
<style>
|
||||
:root{--bg:#0f1117;--panel:#171a22;--panel2:#1d2129;--fg:#e6e8ee;--fg2:#a8afbd;--accent:#e0884f;--accent2:#5fb0c9;--ok:#5ec27a;--bad:#e06a6a;--warn:#e0b84f;--border:#272c36;--mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;--sans:-apple-system,"PingFang SC","Helvetica Neue",Arial,sans-serif;}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
|
||||
.wrap{max-width:960px;margin:0 auto;padding:48px 24px 96px}
|
||||
h1{font-size:28px;line-height:1.3;margin:0 0 8px}
|
||||
.sub{color:var(--fg2);font-size:15px;margin:0 0 32px}
|
||||
h2{font-size:20px;margin:40px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border);color:var(--accent)}
|
||||
p{margin:10px 0}
|
||||
code{font-family:var(--mono);font-size:.85em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
|
||||
.tag{display:inline-block;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px}
|
||||
.tag.info{background:rgba(95,176,201,.16);color:var(--accent2)}
|
||||
ul.ck{list-style:none;padding-left:4px}
|
||||
ul.ck li{position:relative;padding-left:26px;margin:7px 0}
|
||||
ul.ck li::before{content:"☐";position:absolute;left:0;color:var(--fg2)}
|
||||
ul{padding-left:22px} li{margin:6px 0}
|
||||
.lead{background:linear-gradient(180deg,rgba(224,136,79,.10),transparent);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:0 0 8px}
|
||||
.small{color:var(--fg2);font-size:13px}
|
||||
a{color:var(--accent2)} .back{display:inline-block;margin-bottom:24px;font-size:13px} b{color:#fff}
|
||||
.okbox{background:rgba(94,194,122,.07);border:1px solid rgba(94,194,122,.32);border-radius:12px;padding:14px 18px;margin:14px 0}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<a class="back" href="index.html">← 文档索引</a>
|
||||
<h1>pangolin-pay 单地址 + 唯一金额收款模型</h1>
|
||||
<p class="sub">2026-07-09 · <span class="tag info">阅读版</span> · 执行真相源 <code>docs/superpowers/plans/2026-07-09-pay-single-address.md</code> · #34/34A 收款模型定稿</p>
|
||||
|
||||
<div class="lead">
|
||||
<b>定稿:单个固定收款地址 + 每单唯一金额。</b> 归集 = 1 个地址(激活一次、扫一笔),成本最省;订单靠<b>金额</b>区分,不靠地址。前端契约不变(<code>POST /order</code> 返回 <code>address+expect_amount</code>)→ 以后升多地址/GasFree 是<b>纯后端换实现</b>,前端零改动。
|
||||
</div>
|
||||
|
||||
<div class="okbox">
|
||||
<b>唯一金额:</b><code>expect_amount = base + tail</code>,base=价格×1e6(micro-USDT),<b>tail∈[1,9999] micro</b>(偏差<0.01 USDT,价格几乎不变),迟到窗口内不复用。<b>匹配=精确 ==</b>:找"到收款地址、value==expect_amount、block_ts>建单"的转入 → paid。支付页显示<b>可复制的精确金额</b>。付错/迟到抹尾数 → 孤儿人工对账。
|
||||
</div>
|
||||
|
||||
<h2>Phase A — 数据模型</h2>
|
||||
<ul class="ck">
|
||||
<li><b>A.1</b> pay_orders 加 user_ref / expect_amount(唯一金额)/ matched_tx_id;address 恒为收款地址。</li>
|
||||
<li><b>A.2</b> tail 分配支撑:活跃订单已用 tail + 近期冷却(迟到窗口不复用)。</li>
|
||||
<li><b>A.3</b> orphan_payments(tx_id唯一/value/block_ts/handled)。</li>
|
||||
<li><b>A.4</b> 配置 PAY_RECEIVE_ADDRESS(或从 xpub 派生 index 0)。</li>
|
||||
</ul>
|
||||
<h2>Phase B — 建单</h2>
|
||||
<ul class="ck">
|
||||
<li><b>B.1</b> CreateOrder(user, sku, price):同用户无活跃单 → 分配唯一金额 → 写 pending(TTL 15min)。</li>
|
||||
<li><b>B.2</b> tail 分配(活跃间不撞 + 迟到窗口不复用);GetOrder。</li>
|
||||
</ul>
|
||||
<h2>Phase C — watcher</h2>
|
||||
<ul class="ck">
|
||||
<li><b>C.1</b> 匹配:pending 单查收款地址转入,value==expect_amount 且 block_ts>created → paid + tx。</li>
|
||||
<li><b>C.2</b> 超时 → expired。</li>
|
||||
<li><b>C.3</b> 孤儿:到账不匹配任何活跃订单 → orphan_payments(去重)。</li>
|
||||
<li><b>C.4</b> 幂等 + 崩溃恢复;TronGrid 取 block_timestamp。</li>
|
||||
</ul>
|
||||
<h2>Phase D — 归集</h2>
|
||||
<ul class="ck"><li><b>D.1</b> 扫收款地址余额 → 冷钱包(能量租赁);一地址一笔。</li></ul>
|
||||
<h2>Phase E — 验证</h2>
|
||||
<ul class="ck">
|
||||
<li><b>E.1</b> 单测:唯一金额不撞、精确匹配、付错/迟到成孤儿、超时、同用户单订单、幂等、时间戳过滤。</li>
|
||||
<li><b>E.2</b> 真链:两并发订单不同金额同地址各 paid;付错成孤儿;归集。</li>
|
||||
</ul>
|
||||
<h2>不在本轮</h2>
|
||||
<ul><li>多地址/地址池、GasFree(#35)、能量租赁自动化、发货侧 —— 以后需要纯后端切,前端不动。</li></ul>
|
||||
<p class="small" style="margin-top:32px">相关:<a href="crypto-tx-engine-plan.html">加密货币交易引擎(已实现钱包/tron/sweep)</a> · <a href="payment-channels-overview.html">渠道选型</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,71 +0,0 @@
|
||||
# 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.1–1/笔);池 ~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)另排。
|
||||
@@ -0,0 +1,69 @@
|
||||
# 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)另排。
|
||||
Reference in New Issue
Block a user