trustbase-seller-backend 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/oracle/c5-glue.js CHANGED
@@ -38,13 +38,36 @@
38
38
  * 幂等: LCD allowances 查已有 grant 直接跳过; 失败 warn 不阻塞后续注册/发放 (状态机无感)。
39
39
  * 商户侧: chain-signer/chain-writes 走 FEE_GRANTER_ADDR 代付 (无 grant 自动回退自付)。
40
40
  *
41
+ * N16 feegrant 前置 (xiaomu 批准): N15 的触发点在「审批后」, 全新净室商户提交首笔 KYC 时
42
+ * 还没拿到 feegrant (0 余额付不了 gas) → 首笔 KYC 卡死。触发点前移到「收到进件材料」:
43
+ * ops-console POST /api/sp/materials 入库成功即 (a) 入队 bootstrap grant 行 (与审核通过
44
+ * 同一 credential_hash 锚点 sha256("license:"+统一社会信用代码), enqueue 按 credential_hash
45
+ * + recipient 双去重防双行双发引导金) + (b) grantFeegrantFor() 立即发放。发放失败 warn
46
+ * 不阻塞材料入库 — 队列行每轮 tick 补发 (stepBootstrapGrant 顶部守卫, LCD 已有 allowance
47
+ * 静默跳过); 审核退回时撤销还没上链的队列行 (ops-console reject 路径), 防退回商户被自动审批发放。
48
+ *
41
49
  * N14 settle crank (2026-09-23): CONFIRMED 订单过 dispute_window (链上 order params,
42
50
  * 默认 259200s=3 天) 后需 MsgSettleOrder 才 settled=true, 否则货款/质押/担保永卡在途。
43
51
  * 每 tick 扫描 (无全局订单列表: 本地台账 chain_order_id + 链上 providers 的
44
52
  * by-seller 订单列表), 到期单发 MsgSettleOrder{caller=oracle} (permissionless,
45
53
  * 签名者=caller 即可); 每 tick 最多 5 单; 已 settled/非 CONFIRMED 的链上返回按完成处理。
54
+ *
55
+ * N17 引导金池自动回补 (2026-09-25, xiaomu 拍板: 全自动 + 开关):
56
+ * 发放端 (MsgDisburseBootstrap) 早已自动, 注资端此前靠手动 `tx reward pool-transfer`。
57
+ * 本机制: 每 tick LCD 查引导金池余额, 低于阈值 → MsgRewardPoolTransfer 从 x/reward
58
+ * 奖励池自动回补 (authority=签名地址, 链上要求 gov 模块或 emergencyAuthority 双轨,
59
+ * 见 x/reward/keeper/msg_server.go checkPoolTransferAuthority)。
60
+ * 三道闸 (均可 env 调):
61
+ * 1. 阈值触发 POOL_REFILL_THRESHOLD_UTCT (默认 500 TCT), 每次补 POOL_REFILL_AMOUNT_UTCT
62
+ * (默认 1000 TCT);
63
+ * 2. 滚动 24h 额度 POOL_REFILL_DAILY_CAP_UTCT (默认 3000 TCT, 本地台账 oracle_pool_refills
64
+ * 求和) + 划转冷却 POOL_REFILL_COOLDOWN_SECS (默认 600s, 防链上确认前重复发);
65
+ * 3. 开关两层: POOL_REFILL_ENABLED 总开关 (默认关, 显式开) + kill-switch 文件
66
+ * (默认 <db目录>/pool-refill.off, touch 即停 rm 即恢复, 不重启进程; xiaomu 紧急权限)。
46
67
  */
47
68
  const crypto = require("crypto");
69
+ const fs = require("fs");
70
+ const path = require("path");
48
71
  const chainWrites = require("../routes/chain-writes");
49
72
 
50
73
  // 链上模块未上线 (v3.9.3 前) 的特征: CLI 本地解码 / CheckTx / DeliverTx / LCD 404 都覆盖
@@ -123,6 +146,13 @@ CREATE TABLE IF NOT EXISTS oracle_credit_grants (
123
146
  updated_at INTEGER NOT NULL,
124
147
  PRIMARY KEY (reason_kind, ref_id)
125
148
  );
149
+ CREATE TABLE IF NOT EXISTS oracle_pool_refills (
150
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
151
+ recipient TEXT NOT NULL, -- 引导金池地址
152
+ amount TEXT NOT NULL, -- utct
153
+ txhash TEXT NOT NULL,
154
+ created_at INTEGER NOT NULL
155
+ );
126
156
  `);
127
157
  }
128
158
 
@@ -140,11 +170,14 @@ function createOracleGlue(deps) {
140
170
 
141
171
  // ============================== C2 引导金 ==============================
142
172
 
143
- /** 入队 (幂等: credential_hash 已存在 → 返回现状) */
173
+ /** 入队 (幂等: credential_hash 已存在 → 返回现状; N16: 同 recipient 已有行也返回现状 —
174
+ * 材料入库预入队与审核通过正式入队两条路只留一行, 防同商户双行 → 双发引导金) */
144
175
  function enqueueBootstrapGrant({ recipient, credentialHash }) {
145
176
  const ch = credentialHash || sha256Hex(recipient);
146
177
  const existing = db.prepare("SELECT * FROM oracle_bootstrap_grants WHERE credential_hash = ?").get(ch);
147
178
  if (existing) return { enqueued: false, status: existing.status, credential_hash: ch };
179
+ const byRecipient = db.prepare("SELECT * FROM oracle_bootstrap_grants WHERE recipient = ? ORDER BY created_at DESC LIMIT 1").get(recipient);
180
+ if (byRecipient) return { enqueued: false, status: byRecipient.status, credential_hash: byRecipient.credential_hash, deduped_by: "recipient" };
148
181
  db.prepare("INSERT INTO oracle_bootstrap_grants (credential_hash, recipient, status, created_at, updated_at) VALUES (?,?,?,?,?)")
149
182
  .run(ch, recipient, "pending", nowSec(), nowSec());
150
183
  log(`[oracle-glue] bootstrap 入队 recipient=${recipient} credential_hash=${ch.slice(0, 12)}...`);
@@ -223,6 +256,13 @@ function createOracleGlue(deps) {
223
256
  }
224
257
  }
225
258
 
259
+ /** N16: 立即发放 feegrant (材料入库触发; 签名地址现取 — 与 tick 同一取法, CLI/JS 通道皆可)。
260
+ * grantFeegrant 内部 LCD 幂等; 失败返回 false 不抛 (材料入库不阻塞, 队列行下轮补发)。 */
261
+ async function grantFeegrantFor(recipient) {
262
+ const oracleAddr = deps.oracleAddr || (await chainWrites.keyAddress(cfg));
263
+ return grantFeegrant(recipient, oracleAddr);
264
+ }
265
+
226
266
  /** 单条 bootstrap grant 推进一步 (每 tick 调用; 失败留状态下次重试) */
227
267
  async function stepBootstrapGrant(row, oracleAddr) {
228
268
  const upd = (fields) => {
@@ -230,6 +270,12 @@ function createOracleGlue(deps) {
230
270
  .run(...Object.values(fields), nowSec(), row.credential_hash);
231
271
  };
232
272
 
273
+ // 步骤1.5 (N16): feegrant 前置补发 — 材料入库时已发过则这里 LCD 查到 allowance 静默跳过
274
+ // (不刷日志); 查无 (首次/上轮失败) 才真正发。商户提交 KYC 前 gas 就已可代付。
275
+ if (!(await findFeegrant(row.recipient))) {
276
+ await grantFeegrant(row.recipient, oracleAddr);
277
+ }
278
+
233
279
  // 步骤2 前置: 找链上实名记录
234
280
  if (!row.verification_id) {
235
281
  const v = await findVerification(row.recipient);
@@ -268,8 +314,9 @@ function createOracleGlue(deps) {
268
314
  }
269
315
 
270
316
  // 步骤2.4 (N15): 审批已过 → 发 feegrant (商户此后注册/KYC 的手续费由 oracle 代付)。
271
- // 失败不阻塞: 注册/发放继续, 下轮 tick 对未 done 行重试。
272
- if (!row.disburse_txhash) {
317
+ // N16 起顶部守卫通常已发过 — 这里 LCD 查到 allowance 静默跳过 (防 waiting_module 等
318
+ // 多轮停留的行每 tick 刷「已存在」日志); 失败不阻塞: 注册/发放继续, 下轮 tick 重试。
319
+ if (!row.disburse_txhash && !(await findFeegrant(row.recipient))) {
273
320
  await grantFeegrant(row.recipient, oracleAddr);
274
321
  }
275
322
 
@@ -337,6 +384,89 @@ function createOracleGlue(deps) {
337
384
  log(`[oracle-glue] 引导金已发放 recipient=${row.recipient} tx=${r.txhash}`);
338
385
  }
339
386
 
387
+ // ============================== N17 引导金池自动回补 ==============================
388
+
389
+ const POOL_ADDR = process.env.BOOTSTRAP_POOL_ADDR || "tct1l06faad88784ar4cljf8rcary9n355w4umuh3v";
390
+ const REFILL_THRESHOLD = BigInt(process.env.POOL_REFILL_THRESHOLD_UTCT || "500000000"); // 500 TCT
391
+ const REFILL_AMOUNT = process.env.POOL_REFILL_AMOUNT_UTCT || "1000000000"; // 每次 1000 TCT
392
+ const REFILL_DAILY_CAP = BigInt(process.env.POOL_REFILL_DAILY_CAP_UTCT || "3000000000"); // 滚动 24h 上限 3000 TCT
393
+ const REFILL_COOLDOWN = Number(process.env.POOL_REFILL_COOLDOWN_SECS || 600);
394
+ // kill-switch: 文件存在即停 (xiaomu 紧急权限, touch 即停 rm 即恢复, 不用重启进程)
395
+ const REFILL_KILL_FILE = process.env.POOL_REFILL_KILL_FILE
396
+ || path.join(path.dirname(db.name || "."), "pool-refill.off");
397
+ let lastRefillBeatAt = 0;
398
+
399
+ /** 开关状态: env 总开关 (默认关) + kill-switch 文件 (运行时可即时停) */
400
+ function refillEnabled() {
401
+ if (!/^(1|true|yes|on)$/i.test(process.env.POOL_REFILL_ENABLED || "")) {
402
+ return { on: false, why: "POOL_REFILL_ENABLED 未开启" };
403
+ }
404
+ try {
405
+ if (fs.existsSync(REFILL_KILL_FILE)) return { on: false, why: `kill-switch 文件存在: ${REFILL_KILL_FILE}` };
406
+ } catch (_) { /* 检查异常不阻断自动回补 */ }
407
+ return { on: true, why: "" };
408
+ }
409
+
410
+ /** 引导金池 utct 余额 (LCD 不可达 → null, 不动作; 账户无 utct 行 → 0) */
411
+ async function poolBalanceUtct() {
412
+ const j = await lcdGet(`/cosmos/bank/v1beta1/balances/${POOL_ADDR}`);
413
+ const list = j && j.balances;
414
+ if (!Array.isArray(list)) return null;
415
+ const c = list.find((b) => b.denom === "utct");
416
+ return c ? BigInt(c.amount) : 0n;
417
+ }
418
+
419
+ /** 滚动 24h 回补台账: 已划总额 + 最近一次划转时间 (冷却判定用, 抗进程重启) */
420
+ function refillWindow() {
421
+ const since = nowSec() - 86400;
422
+ const r = db.prepare("SELECT COALESCE(SUM(CAST(amount AS INTEGER)),0) AS used, MAX(created_at) AS lastAt FROM oracle_pool_refills WHERE created_at >= ?").get(since);
423
+ return { used: BigInt(r.used || 0), lastAt: Number(r.lastAt || 0) };
424
+ }
425
+
426
+ /** 每 tick: 池余额低于阈值 → MsgRewardPoolTransfer 自动回补 (三道闸见文件头 N17) */
427
+ async function stepPoolRefill(oracleAddr) {
428
+ const sw = refillEnabled();
429
+ if (!sw.on) {
430
+ if (nowSec() - lastRefillBeatAt >= 3600) {
431
+ lastRefillBeatAt = nowSec();
432
+ log(`[oracle-glue] N17 池自动回补关闭 (${sw.why})`);
433
+ }
434
+ return;
435
+ }
436
+ const bal = await poolBalanceUtct();
437
+ if (bal === null) return; // LCD 不可达, 下轮再看
438
+ if (bal >= REFILL_THRESHOLD) return;
439
+ const w = refillWindow();
440
+ if (w.lastAt && nowSec() - w.lastAt < REFILL_COOLDOWN) {
441
+ log(`[oracle-glue] N17 回补冷却中 (${nowSec() - w.lastAt}s < ${REFILL_COOLDOWN}s), 下轮再试`);
442
+ return;
443
+ }
444
+ if (w.used + BigInt(REFILL_AMOUNT) > REFILL_DAILY_CAP) {
445
+ warn(`[oracle-glue] ⚠️ N17 滚动24h额度用尽 (used=${w.used} cap=${REFILL_DAILY_CAP} utct), 池余额 ${bal} utct 仍低于阈值 — 需人工介入`);
446
+ return;
447
+ }
448
+ let r;
449
+ try {
450
+ r = await sendTx([{
451
+ "@type": "/trustchain.reward.v1.MsgRewardPoolTransfer",
452
+ authority: oracleAddr,
453
+ recipient: POOL_ADDR,
454
+ amount: REFILL_AMOUNT,
455
+ }]);
456
+ } catch (e) {
457
+ warn(`[oracle-glue] N17 回补异常: ${e.message} (下轮重试)`);
458
+ return;
459
+ }
460
+ if (r.code !== 0) {
461
+ if (MODULE_NOT_LIVE_RE.test(r.raw_log)) { warn("[oracle-glue] reward 模块未上线, N17 回补留待下轮"); return; }
462
+ warn(`[oracle-glue] N17 回补失败 code=${r.code}: ${String(r.raw_log).slice(0, 200)} (下轮重试)`);
463
+ return;
464
+ }
465
+ db.prepare("INSERT INTO oracle_pool_refills (recipient, amount, txhash, created_at) VALUES (?,?,?,?)")
466
+ .run(POOL_ADDR, REFILL_AMOUNT, r.txhash, nowSec());
467
+ log(`[oracle-glue] N17 引导金池已回补 amount=${REFILL_AMOUNT}utct pool=${POOL_ADDR} tx=${r.txhash} (触发时余额 ${bal} utct < 阈值 ${REFILL_THRESHOLD})`);
468
+ }
469
+
340
470
  // ============================== E6 积分结算 ==============================
341
471
 
342
472
  /** 链上 economy params (奖励金额/月度额度, 不读缓存, 每次结算现查) */
@@ -572,7 +702,7 @@ function createOracleGlue(deps) {
572
702
  }
573
703
  }
574
704
 
575
- /** 每 tick: 推进 bootstrap 队列 + E6 结算 + N14 settle 扫描。签名地址每轮现取 (CLI/JS 通道皆可) */
705
+ /** 每 tick: N17 池回补检查 + 推进 bootstrap 队列 + E6 结算 + N14 settle 扫描。签名地址每轮现取 (CLI/JS 通道皆可) */
576
706
  async function tick() {
577
707
  let oracleAddr = deps.oracleAddr;
578
708
  if (!oracleAddr) {
@@ -583,6 +713,11 @@ function createOracleGlue(deps) {
583
713
  return;
584
714
  }
585
715
  }
716
+ try {
717
+ await stepPoolRefill(oracleAddr);
718
+ } catch (e) {
719
+ warn(`[oracle-glue] N17 池回补检查异常: ${e.message} (不阻塞其他步骤)`);
720
+ }
586
721
  const rows = db.prepare("SELECT * FROM oracle_bootstrap_grants WHERE status NOT IN ('done')").all();
587
722
  for (const row of rows) {
588
723
  try {
@@ -597,7 +732,7 @@ function createOracleGlue(deps) {
597
732
  await settleExpiredOrders(oracleAddr);
598
733
  }
599
734
 
600
- return { tick, enqueueBootstrapGrant, settleConfirmedFacts, settleExpiredOrders, stepBootstrapGrant, getEconomyParams, monthlyUsed, resolveOrderParties, settleFact, grantFeegrant, findFeegrant };
735
+ return { tick, enqueueBootstrapGrant, settleConfirmedFacts, settleExpiredOrders, stepBootstrapGrant, getEconomyParams, monthlyUsed, resolveOrderParties, settleFact, grantFeegrant, grantFeegrantFor, findFeegrant, stepPoolRefill, refillEnabled, poolBalanceUtct, refillWindow };
601
736
  }
602
737
 
603
738
  module.exports = { createOracleGlue, oracleCfg, sha256Hex, MODULE_NOT_LIVE_RE };
@@ -6,6 +6,8 @@
6
6
  * - 账号体系: 首次访问创建管理员 (用户名+密码); scrypt 哈希; 会话 cookie (HMAC 签名, 12h)
7
7
  * - 商家进件资料接收: POST /api/sp/materials (独立低权密钥, 只允许这一个动作)
8
8
  * - 审核台: 列出待审商家 → 查看资料/证件 → 预审通过 (触发 C2 引导金: approve + disburse) / 退回 (带理由)
9
+ * - N16 feegrant 前置: 材料入库成功即预入队 bootstrap grant + 立即发 feegrant (全新净室
10
+ * 商户 0 余额也能付首笔 KYC gas); 退回时撤销还没上链的队列行。幂等与发放细节见 c5-glue.js N16。
9
11
  *
10
12
  * 边界:
11
13
  * - 私钥永不进本模块; 通过 onApprove 回调交给 c5-glue (oracle 侧 CLI/JS 签名)
@@ -15,6 +17,7 @@
15
17
  const crypto = require("crypto");
16
18
  const fs = require("fs");
17
19
  const path = require("path");
20
+ const { createOracleGlue } = require("./c5-glue");
18
21
 
19
22
  const SESSION_COOKIE = "oc_sess";
20
23
  const SESSION_TTL_SEC = 12 * 3600;
@@ -46,6 +49,15 @@ function createOpsConsole(deps) {
46
49
  const say = log || ((...a) => console.log("[ops-console]", ...a));
47
50
  const loginFails = new Map(); // ip -> {count, until}
48
51
 
52
+ // N16: 材料入库触发的 glue 实例 (惰性建)。oracle-sync 只注入 db/materialsDir/onApprove,
53
+ // 拿不到主 glue — 这里自建: createOracleGlue 无定时器无副作用, 共用同一 index.db 连接,
54
+ // 发链走 chain-writes 模块级串行锁, 与 oracle-sync 的主实例并发安全。
55
+ let n16Glue = null;
56
+ function materialsGlue() {
57
+ if (!n16Glue) n16Glue = createOracleGlue({ db });
58
+ return n16Glue;
59
+ }
60
+
49
61
  db.exec(`
50
62
  CREATE TABLE IF NOT EXISTS oracle_admins (
51
63
  username TEXT PRIMARY KEY,
@@ -563,6 +575,11 @@ scheduleRefresh();
563
575
  }
564
576
  db.prepare("UPDATE sp_materials SET review_status = ?, review_note = ?, reviewer = ?, reviewed_at = ? WHERE tb_address = ?")
565
577
  .run(decision === "approve" ? "approved" : "rejected", note, u.username, Math.floor(Date.now() / 1000), addr);
578
+ if (decision === "reject") {
579
+ // N16: 材料入库已预入队 — 退回即撤销还没上链的队列行 (approve/disburse 都没发才撤),
580
+ // 防被退回商户提交 KYC 后被自动审批 + 发放引导金。重新提交材料会重新入队。
581
+ db.prepare("DELETE FROM oracle_bootstrap_grants WHERE recipient = ? AND IFNULL(approve_txhash,'') = '' AND IFNULL(disburse_txhash,'') = ''").run(addr);
582
+ }
566
583
  say("review", decision, addr, "by", u.username);
567
584
  json(200, { ok: true, message: decision === "approve" ? "已通过, 引导金已入队 (链上模块就绪后自动发放)" : "已退回, 请通知商家修改后重新提交" });
568
585
  });
@@ -605,6 +622,20 @@ scheduleRefresh();
605
622
  review_status = 'pending', review_note = '', reviewer = '', reviewed_at = 0`)
606
623
  .run(addr, String(fields.merchant_name || ""), JSON.stringify(fields), JSON.stringify(files), String(b.bundle_hash || ""), Math.floor(Date.now() / 1000));
607
624
  say("materials received:", addr, "files:", Object.keys(files).join(","));
625
+ // N16: 材料入库成功 → 预入队 bootstrap grant + 立即发 feegrant (feegrant 前置)。
626
+ // credential_hash 与审核通过同一锚点 (sha256("license:"+统一社会信用代码));
627
+ // enqueue 双去重 + LCD allowances 去重 → 重复提交材料不重复发。失败只 warn
628
+ // 不阻塞材料入库 — 队列行每轮 tick 补发 (c5-glue stepBootstrapGrant 顶部守卫)。
629
+ try {
630
+ const licKey = String(fields.license_no || "").toUpperCase().replace(/[\s-]/g, "");
631
+ const licHash = licKey ? crypto.createHash("sha256").update("license:" + licKey).digest("hex") : undefined;
632
+ const enq = materialsGlue().enqueueBootstrapGrant({ recipient: addr, credentialHash: licHash });
633
+ void materialsGlue().grantFeegrantFor(addr).then(
634
+ (ok) => say("N16 feegrant 前置:", addr, "enqueued:", enq.enqueued, "granted:", ok),
635
+ (e) => say("⚠️ N16 feegrant 前置失败 (tick 补发):", addr, String(e && e.message || e)));
636
+ } catch (e) {
637
+ say("⚠️ N16 预入队失败 (不阻塞材料入库):", addr, String(e && e.message || e));
638
+ }
608
639
  json(200, { ok: true, message: "资料已收到, 等待审核" });
609
640
  });
610
641
  return true;
package/package.json CHANGED
@@ -1,73 +1,73 @@
1
- {
2
- "name": "trustbase-seller-backend",
3
- "version": "0.1.5",
4
- "description": "TrustBase Seller Backend (merchant edition) — 商品上链/订单/支付/质押 write API for TrustBase 保障链生态. 单卖家自托管形态: 签名 key 即卖家身份, 私钥不出门.",
5
- "main": "run-unified.js",
6
- "bin": {
7
- "tb-seller-backend": "./bin/tb-seller-backend.js",
8
- "tb-seller-doctor": "./bin/tb-doctor.js"
9
- },
10
- "scripts": {
11
- "start": "node run-unified.js",
12
- "doctor": "node bin/tb-doctor.js"
13
- },
14
- "engines": {
15
- "node": ">=18"
16
- },
17
- "keywords": [
18
- "trustbase",
19
- "trustchain",
20
- "marketplace",
21
- "seller",
22
- "insurance",
23
- "cosmos"
24
- ],
25
- "license": "MIT",
26
- "dependencies": {
27
- "@chainsafe/libp2p-noise": "^17.0.0",
28
- "@cosmjs/amino": "^0.38.1",
29
- "@cosmjs/crypto": "^0.38.1",
30
- "@cosmjs/encoding": "^0.38.1",
31
- "@cosmjs/proto-signing": "^0.38.1",
32
- "@cosmjs/stargate": "^0.38.1",
33
- "@maxmind/geoip2-node": "^7.1.0",
34
- "@modelcontextprotocol/sdk": "^1.29.0",
35
- "@noble/curves": "^1.9.7",
36
- "@scure/bip32": "^2.4.0",
37
- "@scure/bip39": "^1.2.0",
38
- "axios": "^1.16.1",
39
- "bcrypt": "^6.0.0",
40
- "bech32": "^2.0.0",
41
- "better-sqlite3": "^11.0.0",
42
- "bip39": "^3.1.0",
43
- "cors": "^2.8.6",
44
- "dotenv": "^17.4.2",
45
- "eventemitter3": "^5.0.0",
46
- "express": "^4.21.0",
47
- "http-proxy-middleware": "^3.0.5",
48
- "it-handshake": "^4.1.3",
49
- "it-length-prefixed": "^11.0.1",
50
- "it-pipe": "^3.0.1",
51
- "jsqr": "^1.4.0",
52
- "nodemailer": "^10.0.10",
53
- "pg": "^8.23.0",
54
- "pino": "^9.0.0",
55
- "protobufjs": "^8.8.0",
56
- "qrcode": "^1.5.4",
57
- "selfsigned": "^5.5.0",
58
- "thirty-two": "^1.0.2",
59
- "uint8arrays": "^6.1.1",
60
- "uuid": "^9.0.0",
61
- "ws": "^8.20.1",
62
- "zod": "^3.23.0"
63
- },
64
- "optionalDependencies": {
65
- "canvas": "^3.2.3"
66
- },
67
- "overrides": {
68
- "requires-port": "^1.0.0"
69
- },
70
- "allowScripts": {
71
- "better-sqlite3@11.10.0": true
72
- }
73
- }
1
+ {
2
+ "name": "trustbase-seller-backend",
3
+ "version": "0.1.7",
4
+ "description": "TrustBase Seller Backend (merchant edition) — 商品上链/订单/支付/质押 write API for TrustBase 保障链生态. 单卖家自托管形态: 签名 key 即卖家身份, 私钥不出门.",
5
+ "main": "run-unified.js",
6
+ "bin": {
7
+ "tb-seller-backend": "./bin/tb-seller-backend.js",
8
+ "tb-seller-doctor": "./bin/tb-doctor.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node run-unified.js",
12
+ "doctor": "node bin/tb-doctor.js"
13
+ },
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "keywords": [
18
+ "trustbase",
19
+ "trustchain",
20
+ "marketplace",
21
+ "seller",
22
+ "insurance",
23
+ "cosmos"
24
+ ],
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@chainsafe/libp2p-noise": "^17.0.0",
28
+ "@cosmjs/amino": "^0.38.1",
29
+ "@cosmjs/crypto": "^0.38.1",
30
+ "@cosmjs/encoding": "^0.38.1",
31
+ "@cosmjs/proto-signing": "^0.38.1",
32
+ "@cosmjs/stargate": "^0.38.1",
33
+ "@maxmind/geoip2-node": "^7.1.0",
34
+ "@modelcontextprotocol/sdk": "^1.29.0",
35
+ "@noble/curves": "^1.9.7",
36
+ "@scure/bip32": "^2.4.0",
37
+ "@scure/bip39": "^1.2.0",
38
+ "axios": "^1.16.1",
39
+ "bcrypt": "^6.0.0",
40
+ "bech32": "^2.0.0",
41
+ "better-sqlite3": "^11.0.0",
42
+ "bip39": "^3.1.0",
43
+ "cors": "^2.8.6",
44
+ "dotenv": "^17.4.2",
45
+ "eventemitter3": "^5.0.0",
46
+ "express": "^4.21.0",
47
+ "http-proxy-middleware": "^3.0.5",
48
+ "it-handshake": "^4.1.3",
49
+ "it-length-prefixed": "^11.0.1",
50
+ "it-pipe": "^3.0.1",
51
+ "jsqr": "^1.4.0",
52
+ "nodemailer": "^10.0.10",
53
+ "pg": "^8.23.0",
54
+ "pino": "^9.0.0",
55
+ "protobufjs": "^8.8.0",
56
+ "qrcode": "^1.5.4",
57
+ "selfsigned": "^5.5.0",
58
+ "thirty-two": "^1.0.2",
59
+ "uint8arrays": "^6.1.1",
60
+ "uuid": "^9.0.0",
61
+ "ws": "^8.20.1",
62
+ "zod": "^3.23.0"
63
+ },
64
+ "optionalDependencies": {
65
+ "canvas": "^3.2.3"
66
+ },
67
+ "overrides": {
68
+ "requires-port": "^1.0.0"
69
+ },
70
+ "allowScripts": {
71
+ "better-sqlite3@11.10.0": true
72
+ }
73
+ }
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ /**
3
+ * profitsharing.js — 微信分账 APIv3 骨架 (服务商 partner / 直连通用, 2026-09-26)
4
+ *
5
+ * 纯函数模块, 不依赖 Express, 可独立单测:
6
+ * addReceivers(receivers) POST /v3/profitsharing/receivers/add
7
+ * requestProfitsharing(params) POST /v3/profitsharing/orders
8
+ * queryProfitsharing(outOrderNo, opts) GET /v3/profitsharing/orders/{out_order_no}
9
+ *
10
+ * 开关: env WXPAY_PROFITSHARING_ENABLED=true 才真调微信; 默认 mock (log + 返回 {mock:true})。
11
+ * 身份: SP 模式 (WXPAY_SP_MCHID 已配) 用服务商身份签名 (wxSignAuth override),
12
+ * body 带 appid + sp_mchid (+ sub_mchid); 未配则回落直连商户身份。
13
+ * 金额护栏: 单笔接收方金额 ≤ 交易金额 × 30% (硬顶, 与 sp-commission MAX_RATE_BP 对齐),
14
+ * 且 ≤ 交易金额本身; 护栏在 mock 模式同样生效 (先于一切网络调用)。
15
+ */
16
+ const agentPay = require("../../routes/agent-pay");
17
+
18
+ const MAX_SHARE_BP = 3000; // 单笔分账 ≤ 交易金额 30% (硬顶)
19
+
20
+ /** env 现读 (测试可改 env, 无缓存状态) */
21
+ function wxCfg() {
22
+ return {
23
+ gateway: process.env.WXPAY_GATEWAY || "https://api.mch.weixin.qq.com",
24
+ appid: process.env.WXPAY_APPID || "",
25
+ mchid: process.env.WXPAY_MCHID || "",
26
+ spMchid: process.env.WXPAY_SP_MCHID || "",
27
+ spSerial: process.env.WXPAY_SP_SERIAL || "",
28
+ spKeyPath: process.env.WXPAY_SP_KEY_PATH || "",
29
+ enabled: String(process.env.WXPAY_PROFITSHARING_ENABLED || "").toLowerCase() === "true",
30
+ };
31
+ }
32
+
33
+ /** SP 模式签名身份 override (未配 SP → undefined, wxSignAuth 回落直连商户身份) */
34
+ function spOverride(c) {
35
+ return c.spMchid ? { mchid: c.spMchid, serial: c.spSerial, keyPath: c.spKeyPath } : undefined;
36
+ }
37
+
38
+ /** 统一调用入口: mock 模式只 log 不打网; 真调走 APIv3 签名 (复用 agent-pay wxSignAuth) */
39
+ async function wxCall(method, urlPath, bodyObj) {
40
+ const c = wxCfg();
41
+ if (!c.enabled) {
42
+ console.log(`[profitsharing] mock ${method} ${urlPath}${bodyObj ? " " + JSON.stringify(bodyObj).slice(0, 200) : ""}`);
43
+ return { mock: true };
44
+ }
45
+ const bodyStr = bodyObj ? JSON.stringify(bodyObj) : "";
46
+ const r = await fetch(c.gateway + urlPath, {
47
+ method,
48
+ headers: {
49
+ "Authorization": agentPay.wxSignAuth(method, urlPath, bodyStr, spOverride(c)),
50
+ "Content-Type": "application/json",
51
+ "Accept": "application/json",
52
+ "User-Agent": "trustbase-seller-backend/1.0",
53
+ },
54
+ body: method === "GET" ? undefined : bodyStr,
55
+ signal: AbortSignal.timeout(15000),
56
+ });
57
+ const j = await r.json().catch(() => ({}));
58
+ return { status: r.status, json: j };
59
+ }
60
+
61
+ /** 金额护栏: 正整数分, ≤ 交易金额, 且 ≤ 交易金额×30% (mock 模式同样生效) */
62
+ function assertShareAmount(tradeAmount, shareAmount) {
63
+ if (!Number.isInteger(tradeAmount) || tradeAmount <= 0) {
64
+ throw new Error("trade_amount 必须是正整数 (分)");
65
+ }
66
+ if (!Number.isInteger(shareAmount) || shareAmount <= 0) {
67
+ throw new Error("分账金额必须是正整数 (分)");
68
+ }
69
+ if (shareAmount > tradeAmount) {
70
+ throw new Error(`分账金额 ${shareAmount} 超过交易金额 ${tradeAmount}`);
71
+ }
72
+ const cap = Math.floor(tradeAmount * MAX_SHARE_BP / 10000);
73
+ if (shareAmount > cap) {
74
+ throw new Error(`分账金额 ${shareAmount} 超过 30% 硬顶 (${cap})`);
75
+ }
76
+ }
77
+
78
+ /** 添加分账接收方 (服务商自己 mchid 或指定商户号)
79
+ * receivers: [{account, type?, name?, relation_type?, sub_mchid?}] */
80
+ async function addReceivers(receivers) {
81
+ const c = wxCfg();
82
+ const list = Array.isArray(receivers) ? receivers : [receivers];
83
+ if (!list.length) throw new Error("receivers 必填且非空");
84
+ const results = [];
85
+ for (const rcv of list) {
86
+ if (!rcv || !rcv.account) throw new Error("receiver.account 必填");
87
+ const body = {
88
+ appid: c.appid,
89
+ type: rcv.type || "MERCHANT_ID",
90
+ account: String(rcv.account),
91
+ relation_type: rcv.relation_type || "SERVICE_PROVIDER",
92
+ };
93
+ if (rcv.name) body.name = String(rcv.name); // MERCHANT_ID 真调时微信要求 name
94
+ if (c.spMchid) {
95
+ body.sp_mchid = c.spMchid;
96
+ if (rcv.sub_mchid) body.sub_mchid = String(rcv.sub_mchid);
97
+ }
98
+ const r = await wxCall("POST", "/v3/profitsharing/receivers/add", body);
99
+ results.push(r.mock ? { mock: true, account: body.account } : r);
100
+ }
101
+ return results.length === 1 ? results[0] : results;
102
+ }
103
+
104
+ /** 请求分账
105
+ * params: {outTradeNo(分账单号 out_order_no, 幂等键), transactionId(微信支付单号),
106
+ * tradeAmount(交易金额分, 护栏基准), subMchid?, unfreezeUnsplit?,
107
+ * receivers: [{account, amount, description, type?}]} */
108
+ async function requestProfitsharing(params) {
109
+ const c = wxCfg();
110
+ const p = params || {};
111
+ if (!p.outTradeNo || !p.transactionId) throw new Error("outTradeNo/transactionId 必填");
112
+ if (!Array.isArray(p.receivers) || !p.receivers.length) throw new Error("receivers 必填且非空");
113
+ for (const rcv of p.receivers) {
114
+ if (!rcv || !rcv.account) throw new Error("receiver.account 必填");
115
+ assertShareAmount(p.tradeAmount, rcv.amount); // 护栏先于一切网络调用
116
+ }
117
+ const body = {
118
+ appid: c.appid,
119
+ transaction_id: String(p.transactionId),
120
+ out_order_no: String(p.outTradeNo),
121
+ receivers: p.receivers.map((r) => ({
122
+ type: r.type || "MERCHANT_ID",
123
+ account: String(r.account),
124
+ amount: r.amount,
125
+ description: String(r.description || "服务商分账").slice(0, 80),
126
+ })),
127
+ unfreeze_unsplit: p.unfreezeUnsplit !== false,
128
+ };
129
+ if (c.spMchid) {
130
+ body.sp_mchid = c.spMchid;
131
+ if (p.subMchid) body.sub_mchid = String(p.subMchid);
132
+ }
133
+ const r = await wxCall("POST", "/v3/profitsharing/orders", body);
134
+ return r.mock ? { mock: true, out_order_no: body.out_order_no, receivers: body.receivers } : r;
135
+ }
136
+
137
+ /** 查询分账结果 (partner 模式须带 sub_mchid 查询串) */
138
+ async function queryProfitsharing(outOrderNo, opts) {
139
+ const c = wxCfg();
140
+ if (!outOrderNo) throw new Error("outOrderNo 必填");
141
+ let urlPath = `/v3/profitsharing/orders/${encodeURIComponent(String(outOrderNo))}`;
142
+ const subMchid = opts && opts.subMchid;
143
+ if (c.spMchid && subMchid) urlPath += `?sub_mchid=${encodeURIComponent(String(subMchid))}`;
144
+ const r = await wxCall("GET", urlPath, null);
145
+ return r.mock ? { mock: true, out_order_no: String(outOrderNo) } : r;
146
+ }
147
+
148
+ module.exports = { addReceivers, requestProfitsharing, queryProfitsharing, assertShareAmount, MAX_SHARE_BP };