5fd0d3c148
pay-server 自带 /_test 测试页:创建订单→显示收款地址+精确金额(可复制)
+过期倒计时→轮询状态到 paid。同源、无 CORS/CSP 摩擦,不碰营销站安全头;
纯调 POST /order + GET /order/{id},无私钥无密钥。生产收银台后续走独角数卡门面。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
80 lines
5.2 KiB
Markdown
80 lines
5.2 KiB
Markdown
# pangolin-pay 加密货币交易引擎(钱包 + watcher + 收款侦测 + 归集)
|
|
|
|
> #34 的**第一块、可独立执行、能用真实小额 USDT 端到端验证**的基础件。**只做加密货币的"交易"核心**——
|
|
> 生成/派生地址、侦测链上到账、归集(sweep)——**不含**独角数卡对接、不含 `/internal/codes/mint` 发码
|
|
> (那是"发货侧",本引擎跑通后再接)。
|
|
>
|
|
> **成果物**:一个独立 Go 服务 `pangolin-pay`(部署在独立海外 VPS),对外提供"建单→给收款地址→侦测到账→
|
|
> 标记已付"的 HTTP 接口 + 一套离线归集工具。跑通后,任何门面(独角数卡 / 自建下单页)都能挂上来。
|
|
>
|
|
> **安全铁律(贯穿)**:热服务只持 **xpub**(watch-only 派生地址)+ TronGrid key + 金库收款地址,**绝不持私钥**;
|
|
> 私钥/助记词冷存,归集在离线端签名。参考知识:brain `notes/dev/crypto-hd-wallet-basics.html`。
|
|
>
|
|
> **关键常量(开工核对无误再写死)**:USDT-TRC20 合约 `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t`(6 位小数);
|
|
> TRON 派生 `m/44'/195'/0'/0/i`;TronGrid `https://api.trongrid.io`。
|
|
|
|
---
|
|
|
|
## Phase A — 钱包与密钥(全程离线)
|
|
|
|
- [ ] A.1 **离线**生成**两套**助记词(断网 Mac 跑 Ian Coleman standalone / `bip_utils`,选 Coin=TRX、24 词、English):
|
|
- **钱包 A · 运营收款**;**钱包 B · 冷备金库**(另一套,独立冷存)
|
|
- [ ] A.2 导出**钱包 A 的 account xpub**(`m/44'/195'/0'` 的 Account Extended **Public** Key);取**钱包 B 的地址0**(`T...`)当**归集目标**
|
|
- [ ] A.3 冷存:助记词 A/B **分开**手抄/钢板 + Bitwarden;**只把** xpub_A + sweep 地址_B 放进 `pangolin-pay` 配置(Bitwarden 管密钥)
|
|
- [ ] A.4 **交叉核对**:B 阶段的 Go 派生实现算出的地址 0..4,必须与 Ian Coleman 显示的**逐个一致**(防派生实现不一致 → 收款地址对不上 → 收不到钱)
|
|
|
|
---
|
|
|
|
## Phase B — 服务骨架 + HD 派生
|
|
|
|
- [ ] B.1 建 Go 服务 `pangolin-pay`(独立 cmd 或独立仓)。配置:`xpub_A` / `sweep_addr_B` / `TRONGRID_API_KEY` / 确认数 / 订单超时。自带 SQLite
|
|
- [ ] B.2 **HD 派生模块**:`tyler-smith/go-bip39` + `btcsuite/.../hdkeychain` 从 **xpub_A** watch-only 派生 `.../0/i` 公钥 →
|
|
TRON 地址(公钥 → Keccak-256 取后 20 字节 → 前缀 `0x41` → Base58Check → `T...`)。**单测:派生地址对齐 Ian Coleman(A.4)**
|
|
- [ ] B.3 数据模型:
|
|
- `pay_orders(order_no PK, sku, expect_amount, addr_index, address, status, tx_id, confirmations, created_at, expires_at)`
|
|
- `addr_cursor(next_index)` — HD 派生游标,单调递增,地址**不复用**
|
|
|
|
---
|
|
|
|
## Phase C — 收款(建单 + 侦测到账)
|
|
|
|
- [ ] C.1 `POST /order`:入参 `sku`/基准金额 → cursor++ 派生**新收款地址** → 写单(`expect_amount` 做**唯一尾数**去重、`expires=+15min`)→ 返回 `{order_no, address, expect_amount, expires_at}`
|
|
- [ ] C.2 `GET /order/{order_no}`:返回 `status`(pending/paid/expired)+ 到账信息
|
|
- [ ] C.3 **TronGrid watcher**(轮询):对每个 pending 单地址查 TRC20 转入
|
|
- `GET /v1/accounts/{addr}/transactions/trc20?only_confirmed=true&contract_address=<USDT>&limit=...`(头带 `TRON-PRO-API-KEY`)
|
|
- 匹配:`to==address` 且 `token==USDT` 且 `value≥expect_amount`(USDT 6 位小数,注意单位换算)且已确认 → `status=paid`、记 `tx_id`
|
|
- [ ] C.4 **幂等**(同 `tx_id` 只认一次)+ **超时** pending→expired(地址不复用)+ **崩溃恢复**(从 DB 未决单继续轮询)
|
|
- [ ] C.5 TronGrid 限流退避 + 重试;测试用 mock TronGrid 覆盖:金额匹配 / 确认门槛 / 超时 / 幂等 / 恢复
|
|
|
|
---
|
|
|
|
## Phase D — 归集(sweep,离线签名)
|
|
|
|
- [ ] D.1 **归集清单**:watcher(xpub)列出有余额地址 `[(index, address, balance)]`
|
|
- [ ] D.2 **gas**:从一个 `gas 钱包`给每个待归集地址打少量 **TRX**(TRC20 转账吃 energy;收款地址无 TRX);量大改**能量租赁**降本
|
|
- [ ] D.3 **离线签名**(离线机,导入**钱包 A 助记词**):派生 `address_i` 私钥 → 构造 TRC20 `transfer(sweep_addr_B, balance)` → **离线签名**
|
|
- [ ] D.4 **广播**:把签好的交易经 TronGrid `POST /wallet/broadcasttransaction` 上链(**只有签名后的交易出门,私钥不出门**)
|
|
- [ ] D.5 半自动脚本(离线端)+ **归集 runbook**;触发=阈值(攒够 X)或定时;热派生地址不长期囤大额
|
|
|
|
---
|
|
|
|
## Phase E — 端到端验证(真实小额 USDT)
|
|
|
|
- [ ] E.1 真单:`POST /order` → 往返回的地址转 **1 USDT(TRC20)** → watcher 侦测 + 确认 → `GET /order` 变 `paid`
|
|
- [ ] E.2 归集:对该地址执行 D 流程 → USDT 到达**钱包 B 冷地址**
|
|
- [ ] E.3 安全核对:`pangolin-pay` 进程/配置**无任何私钥/助记词**(只有 xpub);密钥不入 git(红线扫描);地址不复用
|
|
|
|
---
|
|
|
|
## Verification(判据)
|
|
|
|
- `go test ./...`:HD 派生对齐 Ian Coleman、watcher 匹配/确认/超时/幂等/恢复全绿。
|
|
- 真机:一笔真 1 USDT 从建单→侦测→确认→归集到冷地址全通。
|
|
- 安全:热服务无私钥、密钥不入 git、每单唯一地址不复用——逐项核对。
|
|
|
|
## 不在本轮
|
|
|
|
- 独角数卡门面对接 + epay 支付口(下一块:接门面)。
|
|
- `/internal/codes/mint` JIT 发码 + 兑换闭环(发货侧,#34 后续)。
|
|
- HD 自动 sweep(先人工离线归集)、多币种(先只 USDT-TRC20)、watcher 高可用。
|