@seasonkoh/webaz 0.1.21 → 0.1.22

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.
@@ -24,6 +24,10 @@ import { initSystemUser, transition, getOrderStatus, checkTimeouts, settleFault
24
24
  import { endpointToAction, endpointToReadAction } from './endpoint-actions.js';
25
25
  import { AGENT_RATE_PER_MIN_DEFAULTS, CROSS_USER_READ_DAILY_CAP, MASS_ACTION_TYPES, MASS_ACTION_DAILY_CAPS } from './limits.js';
26
26
  import { initOrderChainSchema, appendOrderEvent, getOrderChain, verifyOrderChain } from '../layer0-foundation/L0-2-state-machine/order-chain.js';
27
+ // RFC-014 PR4 — 正常成交结算走整数 base-units + allocate + 绝对值落库。
28
+ import { toUnits, toDecimal, mulRate, allocate } from '../money.js';
29
+ import { applyWalletDelta, creditColumns } from '../ledger.js';
30
+ import { computeSettlementSplit } from '../settlement-math.js';
27
31
  import { initSnfSchema, snfSend, snfCleanup } from '../layer2-business/L2-7-snf/snf-engine.js';
28
32
  import { initExternalAnchorSchema } from '../layer1-agent/L1-2-external-anchor/anchor-engine.js';
29
33
  import { ensureEvidenceColumns, uploadEvidence, listEvidence as listEvidenceFiles, markEvidenceExpiry, cleanupExpiredEvidence, EVIDENCE_MAX_BYTES, EVIDENCE_ALLOWED_MIME, } from '../layer3-trust/L3-1-dispute-engine/evidence-storage.js';
@@ -8667,7 +8671,9 @@ function settleCommission(orderId, effectiveBase) {
8667
8671
  region = buyer?.region ?? 'global';
8668
8672
  }
8669
8673
  const maxLevels = getRegionMaxLevels(region);
8670
- const pool = Math.round(total * rate * 100) / 100;
8674
+ // RFC-014:佣金池 + 三级拆分走整数 base-units;allocate 保证 L1+L2+L3 pool(精确,修旧版逐项 round2 不守恒暗缝)。
8675
+ const poolU = mulRate(toUnits(total), rate);
8676
+ const pool = toDecimal(poolU);
8671
8677
  // max_levels=0 → 完全禁 MLM,整个 commission pool 入 commission_reserve(三级公池,只进不出)
8672
8678
  // 2026-06-04 修双计 bug:旧版既 redirectToCharity 又 return redirected:pool 让 depositToFund 再入 global_fund → 印钱。
8673
8679
  // 现统一入 commission_reserve 一次,return redirected:0(depositToFund 只拿 1% base)。
@@ -8706,9 +8712,12 @@ function settleCommission(orderId, effectiveBase) {
8706
8712
  // 2026-06-04:所有兜底统一入 commission_reserve(三级公池,独立科目,只进不出)。
8707
8713
  // commission 不再回流 global_fund(PV 资金)—— 三套科目解耦,redirected 始终 0。
8708
8714
  let toCommissionReserve = 0; // → commission_reserve(仅作日志/返回信息用)
8715
+ // 三级金额一次性 allocate(精确求和 ≡ poolU);各级再按 gate 路由到钱包/公池/escrow。
8716
+ const levelAmtU = allocate(poolU, [LEVEL_RATES[1], LEVEL_RATES[2], LEVEL_RATES[3]]);
8709
8717
  for (const { level, beneficiary } of recipients) {
8710
- const amount = Math.round(pool * LEVEL_RATES[level] * 100) / 100;
8711
- if (amount <= 0)
8718
+ const amountU = levelAmtU[level - 1];
8719
+ const amount = toDecimal(amountU);
8720
+ if (amountU <= 0)
8712
8721
  continue;
8713
8722
  // ① region 截断 (level>maxLevels) → 三级公池
8714
8723
  if (level > maxLevels) {
@@ -8756,8 +8765,7 @@ function settleCommission(orderId, effectiveBase) {
8756
8765
  db.prepare(`INSERT INTO commission_records (id, order_id, beneficiary_id, source_buyer_id, level, amount, rate, region, source, source_type)
8757
8766
  VALUES (?,?,?,?,?,?,?,?,?,?)`)
8758
8767
  .run(generateId('comm'), orderId, beneficiary, order.buyer_id, level, amount, rate, region, routeSource, srcType);
8759
- db.prepare("UPDATE wallets SET balance = balance + ?, earned = earned + ? WHERE user_id = ?")
8760
- .run(amount, amount, beneficiary);
8768
+ applyWalletDelta(db, beneficiary, { balance: amountU, earned: amountU });
8761
8769
  }
8762
8770
  catch (e) { /* UNIQUE 冲突 */ }
8763
8771
  }
@@ -8829,22 +8837,30 @@ function depositToFund(orderId, extraFromCommission = 0, effectiveBase) {
8829
8837
  const total = effectiveBase != null ? Number(effectiveBase) : Number(order.total_amount);
8830
8838
  if (total <= 0)
8831
8839
  return { base: 0, redirect: 0, total: 0 };
8832
- const amountBase = Math.round(total * FUND_BASE_RATE() * 100) / 100;
8833
- const amountRedirect = Math.round(Number(extraFromCommission || 0) * 100) / 100;
8840
+ // RFC-014:整数 base-units + 绝对值落库
8841
+ const amountBaseU = mulRate(toUnits(total), FUND_BASE_RATE());
8842
+ const amountRedirectU = toUnits(Number(extraFromCommission || 0));
8834
8843
  const region = order.buyer_region || 'global';
8835
- const totalDeposit = Math.round((amountBase + amountRedirect) * 100) / 100;
8836
- if (totalDeposit > 0) {
8844
+ const totalDepositU = amountBaseU + amountRedirectU;
8845
+ const amountBase = toDecimal(amountBaseU);
8846
+ const amountRedirect = toDecimal(amountRedirectU);
8847
+ const totalDeposit = toDecimal(totalDepositU);
8848
+ if (totalDepositU > 0) {
8837
8849
  // fund_deposits.amount_l3 字段语义已扩为「commission 端回流总额」(区域裁决 + 未 verified)
8838
8850
  db.prepare(`INSERT INTO fund_deposits (id, order_id, amount_base, amount_l3, buyer_region)
8839
8851
  VALUES (?,?,?,?,?)`).run(generateId('fd'), orderId, amountBase, amountRedirect, region);
8840
- db.prepare("UPDATE global_fund SET pool_balance = pool_balance + ? WHERE id = 1").run(totalDeposit);
8852
+ creditColumns(db, 'global_fund', 'id = 1', [], { pool_balance: totalDepositU });
8841
8853
  }
8842
8854
  return { base: amountBase, redirect: amountRedirect, total: totalDeposit };
8843
8855
  }
8844
8856
  function redirectToCommissionReserve(amount, kind, args = {}) {
8845
8857
  if (!Number.isFinite(amount) || amount <= 0)
8846
8858
  return;
8847
- const a = Math.round(amount * 100) / 100;
8859
+ // RFC-014:整数 base-units + 绝对值落库( REAL `col = col + ?` 浮点 dust)。
8860
+ const aU = toUnits(amount);
8861
+ if (aU <= 0)
8862
+ return;
8863
+ const a = toDecimal(aU);
8848
8864
  // Aggregate column mapping (commission_reserve_txns.kind records exact kind):
8849
8865
  // chain_gap → total_chain_gap
8850
8866
  // orphan_sponsor / opt_out_deactivated / escrow_expired → total_orphan_sponsor (no-eligible-recipient bucket)
@@ -8853,7 +8869,8 @@ function redirectToCommissionReserve(amount, kind, args = {}) {
8853
8869
  : kind === 'redirect_region_cap' ? 'total_region_cap'
8854
8870
  : 'total_orphan_sponsor';
8855
8871
  db.transaction(() => {
8856
- db.prepare(`UPDATE commission_reserve SET balance = balance + ?, ${totalCol} = ${totalCol} + ?, updated_at = datetime('now') WHERE id = 'main'`).run(a, a);
8872
+ creditColumns(db, 'commission_reserve', "id = 'main'", [], { balance: aU, [totalCol]: aU });
8873
+ db.prepare(`UPDATE commission_reserve SET updated_at = datetime('now') WHERE id = 'main'`).run();
8857
8874
  db.prepare(`INSERT INTO commission_reserve_txns (id, kind, from_user_id, amount, related_order_id, note)
8858
8875
  VALUES (?,?,?,?,?,?)`).run(generateId('crt'), kind, args.fromUserId || null, a, args.orderId || null, args.note || null);
8859
8876
  })();
@@ -8872,19 +8889,11 @@ function settleOrder(orderId) {
8872
8889
  : db.prepare('SELECT stake_amount, stake_locked_at, category_id FROM products WHERE id = ?').get(order.product_id);
8873
8890
  // M8:协议费率二手 1% (vs 商家 2%) — 鼓励个人发布
8874
8891
  const feeRate = isSecondhand ? 0.01 : 0.02;
8875
- const protocolFee = Math.round(total * feeRate * 100) / 100;
8876
- const protocolToBonus = Math.round(protocolFee * 0.5 * 100) / 100;
8877
- const protocolToOps = Math.round((protocolFee - protocolToBonus) * 100) / 100;
8878
- // 面交无三方物流 → logisticsFee = 0
8879
- const logisticsFee = isInPerson ? 0 : Math.round(total * 0.05 * 100) / 100;
8880
- // P0 2026-05-28:self-fulfill(logistics_id NULL)订单 → 不收 5% 物流费,全额给 seller
8881
- // 之前 sellerAmount 用 logisticsFee 扣,但 line 7780 不付任何人 → 每笔黑洞 5% × total
8882
- const logisticsActual = order.logistics_id ? logisticsFee : 0;
8883
8892
  // QA 轮 9.4 P0:settleOrder default 0 vs settleCommission default 0.10 不一致 → 印钱漏洞
8884
8893
  // 修:统一两处 default = 0.10(跟 PWA orders-create.ts:277 一致)
8885
8894
  const commissionRate = Number(order.snapshot_commission_rate ?? 0.10);
8886
- const commissionPool = Math.round(total * commissionRate * 100) / 100;
8887
- const fundBase = Math.round(total * FUND_BASE_RATE() * 100) / 100;
8895
+ // RFC-014:实际拆分在下方 computeSettlementSplit(整数 base-units,卖家净额 residual 吸收 → 精确守恒)
8896
+ // self-fulfill / 面交不收物流费(chargeLogistics=false);协议费 50/50 allocate 精确拆。
8888
8897
  // M7.2.6 方案 3 + M-4 fix:首单锁定 stake(trusted+ 跳过)
8889
8898
  // 用单语句 UPDATE ... WHERE stake_locked_at IS NULL 原子拿"是否首次"判定,
8890
8899
  // 避免读后写竞态(旧实现两条语句,迁 PG / 多 worker 时可能双锁)
@@ -8902,21 +8911,30 @@ function settleOrder(orderId) {
8902
8911
  stakeToLock = product.stake_amount;
8903
8912
  }
8904
8913
  }
8905
- // sellerAmount 扣掉首单要锁的 stake
8906
- const sellerAmount = Math.round((total - protocolFee - logisticsActual - commissionPool - fundBase - stakeToLock) * 100) / 100;
8907
- db.prepare('UPDATE wallets SET escrowed = escrowed - ? WHERE user_id = ?').run(total, order.buyer_id);
8908
- db.prepare('UPDATE wallets SET balance = balance + ?, earned = earned + ? WHERE user_id = ?').run(sellerAmount, sellerAmount, order.seller_id);
8909
- if (order.logistics_id)
8910
- db.prepare('UPDATE wallets SET balance = balance + ?, earned = earned + ? WHERE user_id = ?').run(logisticsFee, logisticsFee, order.logistics_id);
8911
- // 首单时把 stake 划入 staked(来自 sellerAmount,已扣除)
8912
- if (stakeToLock > 0) {
8913
- db.prepare('UPDATE wallets SET staked = staked + ? WHERE user_id = ?').run(stakeToLock, order.seller_id);
8914
- }
8914
+ // RFC-014:整数 base-units 精确拆分(卖家净额 = total − 其余各项,residual 吸收 → Σ ≡ total)。
8915
+ const totalU = toUnits(total);
8916
+ const split = computeSettlementSplit({
8917
+ totalU,
8918
+ feeRate,
8919
+ logisticsRate: 0.05,
8920
+ chargeLogistics: !isInPerson && !!order.logistics_id, // self-fulfill / 面交 = 不收物流费
8921
+ commissionRate,
8922
+ fundRate: FUND_BASE_RATE(),
8923
+ stakeToLockU: toUnits(stakeToLock),
8924
+ });
8925
+ // 买家 escrow 释放 + 卖家净额 + 物流费(实扣即实付)+ 首单 stake 锁定 —— 全绝对值落库
8926
+ applyWalletDelta(db, order.buyer_id, { escrowed: -totalU });
8927
+ applyWalletDelta(db, order.seller_id, { balance: split.sellerAmountU, earned: split.sellerAmountU });
8928
+ if (order.logistics_id && split.logisticsActualU > 0) {
8929
+ applyWalletDelta(db, order.logistics_id, { balance: split.logisticsActualU, earned: split.logisticsActualU });
8930
+ }
8931
+ if (split.stakeToLockU > 0)
8932
+ applyWalletDelta(db, order.seller_id, { staked: split.stakeToLockU });
8915
8933
  // 协议费拆分:50% 注入管理津贴池,50% 入 sys_protocol 运营
8916
- if (protocolToBonus > 0)
8917
- db.prepare("UPDATE management_bonus_pool SET balance = balance + ? WHERE id = 1").run(protocolToBonus);
8918
- if (protocolToOps > 0)
8919
- db.prepare("UPDATE wallets SET balance = balance + ? WHERE user_id = 'sys_protocol'").run(protocolToOps);
8934
+ if (split.protocolToBonusU > 0)
8935
+ creditColumns(db, 'management_bonus_pool', 'id = 1', [], { balance: split.protocolToBonusU });
8936
+ if (split.protocolToOpsU > 0)
8937
+ applyWalletDelta(db, 'sys_protocol', { balance: split.protocolToOpsU });
8920
8938
  // 推土机分享分润:正常分账 → 钱包;兜底 → commission_reserve(三级公池,独立科目)
8921
8939
  settleCommission(orderId);
8922
8940
  // 原子能:PV 资金池入金 = 仅 1% base(2026-06-04 起 commission 不再回流此池,三科目解耦)
@@ -8987,9 +9005,9 @@ function settleOrder(orderId) {
8987
9005
  }
8988
9006
  // P0.1:RFQ 订单完单成功 → 释放卖家 bid_stake_held(staked → balance)+ synthetic product 转 warehouse(P1.2 顺手修)
8989
9007
  try {
8990
- const heldStake = Number(order.bid_stake_held || 0);
8991
- if (heldStake > 0 && order.source === 'rfq') {
8992
- db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(heldStake, heldStake, order.seller_id);
9008
+ const heldStakeU = toUnits(Number(order.bid_stake_held || 0));
9009
+ if (heldStakeU > 0 && order.source === 'rfq') {
9010
+ applyWalletDelta(db, order.seller_id, { balance: heldStakeU, staked: -heldStakeU });
8993
9011
  }
8994
9012
  // synthetic RFQ product(描述以 [RFQ 开头)卖完后转 warehouse,防 active feed 污染
8995
9013
  const pInfo = db.prepare('SELECT description, stock FROM products WHERE id = ?').get(order.product_id);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Settlement split — settleOrder 正常成交的资金拆分【纯函数】(RFC-014,可测)。
3
+ *
4
+ * 把买家托管的 total 拆成:协议费(再 50/50 拆津贴/运营)+ 物流费 + 佣金池 + 基金 1% + 首单锁定 stake
5
+ * + 卖家净额。卖家净额 = total − 其余各项(residual 吸收)→ 整数单位下【精确守恒】(Σ ≡ total)。
6
+ *
7
+ * server.ts settleOrder 直接调用本函数拿数字,再落库(applyWalletDelta/creditColumns)。
8
+ * 守恒由 tests/test-settlement-math.ts 守(此前 settleOrder 在 server.ts 巨石里、绑 db 闭包,无法隔离单测)。
9
+ */
10
+ import { mulRate, allocate } from './money.js';
11
+ export function computeSettlementSplit(i) {
12
+ const protocolFeeU = mulRate(i.totalU, i.feeRate);
13
+ const [protocolToBonusU, protocolToOpsU] = allocate(protocolFeeU, [1, 1]); // 精确 50/50
14
+ const logisticsFeeU = mulRate(i.totalU, i.logisticsRate);
15
+ const logisticsActualU = i.chargeLogistics ? logisticsFeeU : 0;
16
+ const commissionPoolU = mulRate(i.totalU, i.commissionRate);
17
+ const fundBaseU = mulRate(i.totalU, i.fundRate);
18
+ const stakeToLockU = i.stakeToLockU;
19
+ // 卖家净额 = 残值 → 保证 Σ ≡ total(整数减法精确,不增发/不丢)
20
+ const sellerAmountU = i.totalU - protocolFeeU - logisticsActualU - commissionPoolU - fundBaseU - stakeToLockU;
21
+ return { protocolFeeU, protocolToBonusU, protocolToOpsU, logisticsFeeU, logisticsActualU, commissionPoolU, fundBaseU, stakeToLockU, sellerAmountU };
22
+ }
23
+ /** 守恒校验(供测试 + 运行期可选断言):各去向之和 ≡ total。 */
24
+ export function settlementConserves(totalU, s) {
25
+ return s.protocolToBonusU + s.protocolToOpsU === s.protocolFeeU
26
+ && s.protocolFeeU + s.logisticsActualU + s.commissionPoolU + s.fundBaseU + s.stakeToLockU + s.sellerAmountU === totalU;
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seasonkoh/webaz",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "[PRE-LAUNCH] Agent-native decentralized commerce protocol. Humans and AI agents trade on the same protocol via MCP tools. ⚠️ Repository currently private until W8 public launch — GitHub links may return 404. See https://webaz.xyz for status.",
5
5
  "main": "dist/mcp.js",
6
6
  "bin": {