c5949a595a
从"每单唯一 HD 地址"改为"单个固定收款地址 + 每单唯一金额",归集成本 O(订单数)→O(1)。 - store: pay_orders 加 user_ref/expect_amount(唯一金额)/matched_tx_id;新 orphan_payments 表; ActiveOrderByUser(同用户单订单)、AmountRecentlyUsed(迟到窗口内金额不复用)、TxHandled(幂等)、 RecordOrphan。去掉每单派生游标。 - pay: CreateOrder(userRef,sku,priceMicro)——同用户单订单校验 + 分配唯一金额(base+随机微尾数[1,9999]、 cooldown 内不复用),address 恒为收款地址。 - tron: Transfer 加 BlockTs(区块时间秒),取 block_timestamp。 - watcher: 单地址取到账,按"金额==expect && block_ts>建单"匹配 → paid;不匹配的到账 → orphan;幂等。 - httpapi: POST /order 加 user_ref,同用户重复 → 409;main 收款地址=PAY_RECEIVE_ADDRESS 或 xpub index0。 - 测试:唯一金额/同地址、同用户单订单、精确匹配、付错成孤儿、迟到不误配新单、付款早于建单不匹配、 超时、幂等、409,全绿。README 更新为单地址模型+API(user_ref/精确金额/orphan)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
135 lines
7.0 KiB
Markdown
135 lines
7.0 KiB
Markdown
# pangolin-pay
|
|
|
|
自托管 USDT(TRC20) 收款服务。**单个固定收款地址 + 每单唯一金额**:所有订单收到同一个地址,
|
|
靠**唯一金额**(基准价 + 微尾数,≤0.01 USDT)区分。watcher 轮询 TronGrid,按
|
|
**"到账 tx + 精确金额 + 区块时间晚于建单"** 匹配订单;匹配不到活跃订单的到账(付错/迟到)→
|
|
记 `orphan_payments` 人工对账。归集(把钱扫到冷钱包)是**独立离线步骤**,本服务**不持私钥**。
|
|
|
|
> 收款地址 = 钱包 A 的地址 0(`m/44'/195'/0'/0/0`),配置注入或由 xpub 派生。
|
|
> 前端契约(`POST /order` 返回 address+expect_amount)与地址模型解耦——以后要升多地址/GasFree
|
|
> 是纯后端换实现,前端零改动。模型定稿见 `docs/pay-single-address-plan.html`。
|
|
|
|
> 属 #34「独角数卡 + USDT 收款闭环」的加密货币交易引擎(计划见
|
|
> `docs/superpowers/plans/2026-07-09-crypto-tx-engine.md`)。概念见 brain
|
|
> `notes/dev/crypto-hd-wallet-basics.html`。
|
|
|
|
## 安全模型(必读)
|
|
|
|
- 本服务(热、联网)**只持 account xpub**——能派生收款地址、能查到账,**拿不到任何私钥**。被脱库也转不走钱。
|
|
- 私钥/助记词**冷存**;要动钱(归集)时才在**离线端**用私钥签名(见 seed.go,OFFLINE ONLY)。
|
|
- 密钥(xpub / TronGrid key)走 Bitwarden,不入 git、不写死。
|
|
|
|
## Phase A —— 离线准备钱包(你在断网机器上做)
|
|
|
|
1. 断网,用 Ian Coleman `bip39-standalone.html`(或 `bip_utils`)生成**两套** 24 词助记词:
|
|
钱包 A(运营收款)、钱包 B(冷备金库)。Coin=TRX、English。
|
|
2. 取**钱包 A 的 Account Extended Public Key**(`m/44'/195'/0'` 的 xpub)→ 就是本服务的 `PAY_ACCOUNT_XPUB`。
|
|
3. 取**钱包 B 的地址0**(`T...`)→ 归集目标(Phase D 用)。
|
|
4. **交叉核对(关键)**:本仓自带的金标准向量(测试助记词 `abandon…about`)必须与 Ian Coleman
|
|
一致 —— 跑 `go test ./internal/wallet -run TestKnownVector -v`,再在 Ian Coleman 里用同一测试
|
|
助记词、Coin=TRX 对照前 3 个地址。一致 = 派生实现可信;不一致 = 有 bug,别上线。
|
|
5. 助记词 A/B 分开冷存 + Bitwarden。**只把 xpub_A 交给本服务。**
|
|
|
|
## Money-critical 验证(收任何真钱之前必须全过)
|
|
|
|
派生错 = 把买家的钱打到你没私钥的地址,钱直接丢。上真钱前逐步核死:
|
|
|
|
**① 代码自检(金标准向量)**
|
|
```bash
|
|
cd pay && go test ./internal/wallet -run TestKnownVector -v # 必须 PASS
|
|
```
|
|
|
|
**② 独立交叉核对(证明代码 vs 独立实现一致)** —— 断网开 Ian Coleman `bip39-standalone.html`,
|
|
输入测试助记词 `abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about`,
|
|
Coin=TRX、BIP44,核对 Derived Addresses 前 3 行必须等于:
|
|
```
|
|
m/44'/195'/0'/0/0 TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH
|
|
m/44'/195'/0'/0/1 TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK
|
|
m/44'/195'/0'/0/2 TYJPRrdB5APNeRs4R7fYZSwW3TcrTKw2gx
|
|
```
|
|
且 Account Extended Public Key = `xpub6D1AabNHCupeiLM65ZR9UStMhJ1vCpyV4XbZdyhMZBiJXALQtmn9p42VTQckoHVn8WNqS7dqnJokZHAHcHGoaQgmv8D45oNUKx6DZMNZBCd`。
|
|
|
|
**③ 你的真钱包 A(护你自己钱的那道闸,最重要)** —— 用你钱包 A 的真 account xpub:
|
|
```bash
|
|
PAY_ACCOUNT_XPUB="<你钱包A的account xpub>" go run ./cmd/paywatch selfcheck 5
|
|
```
|
|
把输出的地址,和你钱包 A(在 Ian Coleman 里同一 xpub、或 TronLink/imToken 里钱包 A)显示的
|
|
地址 0..4 **逐个比对**。**全部一致才可上线收款;有一个不一致,立即停,别收任何钱。**
|
|
|
|
**④ 真链闭环(Phase E,最终证明)** —— 小额:
|
|
1. `POST /order` 拿地址 → 往它转 **1 USDT(TRC20)**。
|
|
2. 用你钱包 A 打开 → **确认这 1 USDT 出现在你钱包 A 名下**(证明你确实控制这个地址)。
|
|
3. `GET /order/{id}` 应变 `paid`(证明 watcher 侦测正确)。
|
|
4. 用 `cmd/sweep` 把这 1 USDT 归集到冷钱包 B → **确认 B 收到**(证明签名/广播正确)。
|
|
|
|
全过 = 收→控→测→归 四环闭合,可放量。
|
|
|
|
## 运行
|
|
|
|
```bash
|
|
export PAY_ACCOUNT_XPUB="xpub..." # 钱包 A 的 account xpub(必填)
|
|
export TRONGRID_API_KEY="..." # TronGrid key(建议)
|
|
export PAY_DB="pay.db" # SQLite 路径(默认 pay.db)
|
|
export PAY_ADDR=":8090" # 监听(默认 :8090)
|
|
export PAY_POLL_SECONDS="20" # 轮询间隔秒(默认 20)
|
|
# USDT_CONTRACT / TRONGRID_BASE 默认主网
|
|
go run ./cmd/paywatch
|
|
```
|
|
|
|
## API
|
|
|
|
```
|
|
POST /order {"user_ref":"buyer123","sku":"pro-year","amount":5000000} # amount = 基准价 micro-USDT(1e-6)
|
|
→ 201 {"order_no","address","expect_amount","status":"pending","expires_at"}
|
|
# expect_amount = 基准 + 唯一微尾数;支付页要显示"请付精确金额 expect_amount"
|
|
→ 409 {"error":"user already has an active order"} # 同一 user_ref 同时只能一个活跃订单
|
|
GET /order/{orderNo} → 200 {..., "status":"pending|paid|expired","tx_id"}
|
|
GET /healthz → 200 ok
|
|
```
|
|
|
|
门面(独角数卡)下单时调 `POST /order`(带 `user_ref`)拿 **address + expect_amount**;
|
|
支付页显示"往 address 付**精确的** expect_amount"(可复制),轮询 `GET /order/{id}` 直到 `paid`。
|
|
用户付错金额 → 该到账进 orphan,需人工对账。
|
|
|
|
配置:`PAY_RECEIVE_ADDRESS`(单收款地址)或 `PAY_ACCOUNT_XPUB`(自动派生 index 0)。
|
|
|
|
## Phase D —— 归集(气隙签名,`cmd/sweep`)
|
|
|
|
把散在各收款地址的 USDT 扫到冷钱包,**助记词只在离线机上出现**,联网机永远拿不到私钥。
|
|
四段式,跨气隙用文件传递(unsigned.json / signed.json):
|
|
|
|
```bash
|
|
# ① 联网:列出有余额的收款地址
|
|
PAY_ACCOUNT_XPUB=xpub... TRONGRID_API_KEY=... go run ./cmd/sweep plan --max 50
|
|
|
|
# ② 联网:构造未签名转账(全额 → 冷钱包),不碰私钥
|
|
PAY_ACCOUNT_XPUB=xpub... TRONGRID_API_KEY=... \
|
|
go run ./cmd/sweep build --cold TColdAddr... --max 50 --fee-limit 30000000 > unsigned.json
|
|
|
|
# ③ 离线(断网机):从 Bitwarden 取助记词进环境,独立验签后签名
|
|
PAY_SWEEP_MNEMONIC="word1 ... word24" \
|
|
go run ./cmd/sweep sign --cold TColdAddr... < unsigned.json > signed.json
|
|
|
|
# ④ 联网:广播
|
|
TRONGRID_API_KEY=... go run ./cmd/sweep broadcast < signed.json
|
|
```
|
|
|
|
**`sign` 独立验签(气隙安全的关键)**——对每笔交易:① 重算 txid=sha256(raw_data) 必须等于声称值
|
|
(防 raw_data 被篡改);② 收款人+金额的 ABI 参数必须内嵌在 raw_data(防换收款人/改额);③ USDT 合约
|
|
必须内嵌(防换币);④ 由助记词派生的地址必须等于 owner(防错钥匙)。任一不符即拒签。
|
|
|
|
**gas**:TRC20 转账要 energy,收款地址身上没 TRX——归集前先给这些地址垫少量 TRX(gas 钱包),
|
|
或用能量租赁。(垫 gas 的辅助后续加;当前 `build` 已设 `--fee-limit`。)
|
|
|
|
> ⚠️ 联网上链部分(`build`/余额/`broadcast`)只在真链(Phase E)验证;纯 crypto
|
|
> (地址解码/ABI/txid/签名可恢复)已单测。首次务必**小额**实跑一遍再放量。
|
|
|
|
## 测试
|
|
|
|
```bash
|
|
go test ./...
|
|
```
|
|
|
|
- `wallet`:派生一致性(xpub 路径 == 私钥路径)+ 金标准向量(需 A.4 核对)。
|
|
- `store`/`pay`/`watcher`/`httpapi`:建单/派生地址/侦测到账/幂等/超时/HTTP。
|