Files
wangjia bced7b35e0 fix(pay): 测试页状态改中文标签(消歧义)
pending/paid/expired 显示层映射为 等待付款/付款完成/已过期,徽章字体改 sans。
枚举值不变(程序仍用英文);发货/开通属门面(独角数卡),不在收款服务里。

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

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 验证(收任何真钱之前必须全过)

派生错 = 把买家的钱打到你没私钥的地址,钱直接丢。上真钱前逐步核死:

① 代码自检(金标准向量)

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:

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 收到(证明签名/广播正确)。

全过 = 收→控→测→归 四环闭合,可放量。

运行

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):

# ① 联网:列出有余额的收款地址
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/签名可恢复)已单测。首次务必小额实跑一遍再放量。

测试

go test ./...
  • wallet:派生一致性(xpub 路径 == 私钥路径)+ 金标准向量(需 A.4 核对)。
  • store/pay/watcher/httpapi:建单/派生地址/侦测到账/幂等/超时/HTTP。