@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.
package/dist/money.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Money — 协议资金的【唯一】算术面(RFC-014 option A:整数最小单位)。
3
+ *
4
+ * 背景:此前金额用 JS float(SQLite REAL + number + Math.round(x*100)/100),除不尽的金额
5
+ * (33.33 @ 7% 等)会在【个体钱包】留浮点尘(staked=3.3299999999983)。聚合守恒安全(残差0,
6
+ * 靠 residual-absorption),但接真实 USDC(整数 base-unit)对账时会变成真裂缝。诊断见
7
+ * tests/test-money-precision-adversarial.ts。
8
+ *
9
+ * 表示:1 WAZ = 1,000,000 base-units(6 位小数,与 USDC base-units 对齐 → 链上对账 1:1)。
10
+ * 全部算术在【整数 base-units】上做(精确),只在显示层转回 2 位小数。
11
+ * 类型用 number 整数(安全到 ±2^53 ≈ 9.007e9 WAZ,模拟币足够);链上 USDC 边界 viem 已用 BigInt。
12
+ *
13
+ * 守恒利器:allocate() 用最大余数法把总额拆成整数桶且【精确求和 = 总额】——从源头消灭 dust,
14
+ * 且天然守恒(不增发/不丢)。mulRate() 单次舍入到整数单位。
15
+ *
16
+ * 用法(RFC-014 港口策略):资金热路径(engine.settle* / orders / 佣金 / dispute / skill …)
17
+ * 一律 toUnits 进 → 在 units 上 add/sub/mulRate/allocate → toUnits 出存库;显示层用 format。
18
+ * 全部港口完成后 schema 改 INTEGER(PR6),届时 toUnits/toDecimal 退化为存取边界。
19
+ */
20
+ export const MONEY_SCALE = 1_000_000; // 1 WAZ = 1e6 base-units(6 dp,USDC 对齐)
21
+ const MAX_SAFE = Number.MAX_SAFE_INTEGER; // 2^53 - 1 ≈ 9.007e15 units ≈ 9.007e9 WAZ
22
+ function assertUnits(u, ctx) {
23
+ if (!Number.isFinite(u) || !Number.isInteger(u))
24
+ throw new Error(`money[${ctx}]: 非整数 units: ${u}`);
25
+ if (Math.abs(u) > MAX_SAFE)
26
+ throw new Error(`money[${ctx}]: units 超出安全整数范围: ${u}`);
27
+ }
28
+ /** 十进制 WAZ(number/string)→ 整数 base-units(四舍五入到最近 unit)。存量 REAL 价格的入口。 */
29
+ export function toUnits(decimal) {
30
+ const n = typeof decimal === 'string' ? Number(decimal) : decimal;
31
+ if (!Number.isFinite(n))
32
+ throw new Error(`money.toUnits: 非有限数: ${decimal}`);
33
+ const u = Math.round(n * MONEY_SCALE);
34
+ assertUnits(u, 'toUnits');
35
+ return u;
36
+ }
37
+ /** 整数 base-units → 十进制 number(兼容/显示边界;返回 float,仅用于显示或与遗留 REAL 字段衔接)。 */
38
+ export function toDecimal(units) {
39
+ assertUnits(units, 'toDecimal');
40
+ return units / MONEY_SCALE;
41
+ }
42
+ /** 整数 base-units → 定点字符串(默认 2 位,WAZ 展示)。整数运算舍入,不靠 float toFixed。 */
43
+ export function format(units, dp = 2) {
44
+ assertUnits(units, 'format');
45
+ const neg = units < 0;
46
+ let abs = Math.abs(units);
47
+ const grain = Math.round(MONEY_SCALE / 10 ** dp); // dp=2 → 1e4 units 为一格
48
+ abs = Math.round(abs / grain) * grain; // 舍入到 dp 粒度(整数运算)
49
+ const whole = Math.floor(abs / MONEY_SCALE);
50
+ const fracUnits = abs - whole * MONEY_SCALE;
51
+ const fracStr = dp > 0 ? '.' + String(Math.floor(fracUnits / grain)).padStart(dp, '0') : '';
52
+ return (neg ? '-' : '') + String(whole) + fracStr;
53
+ }
54
+ export function add(a, b) { const r = a + b; assertUnits(r, 'add'); return r; }
55
+ export function sub(a, b) { const r = a - b; assertUnits(r, 'sub'); return r; }
56
+ export function sum(xs) { return xs.reduce((acc, x) => add(acc, x), 0); }
57
+ /** 单价 × 整数数量(精确)。 */
58
+ export function mulQty(unit, qty) {
59
+ if (!Number.isInteger(qty) || qty < 0)
60
+ throw new Error(`money.mulQty: qty 非非负整数: ${qty}`);
61
+ const r = unit * qty;
62
+ assertUnits(r, 'mulQty');
63
+ return r;
64
+ }
65
+ /** 金额 × 费率(rate 如 0.07);结果【单次】舍入到整数 base-units。守恒由 allocate 负责,勿用多次 mulRate 凑总额。 */
66
+ export function mulRate(amount, rate) {
67
+ if (!Number.isFinite(rate) || rate < 0)
68
+ throw new Error(`money.mulRate: rate 非法: ${rate}`);
69
+ assertUnits(amount, 'mulRate');
70
+ const r = Math.round(amount * rate);
71
+ assertUnits(r, 'mulRate');
72
+ return r;
73
+ }
74
+ /** clamp 到 [0, cap](cap 省略=只防负)。用于"不超过余额/不超过原佣金"等上限。 */
75
+ export function clamp(units, lo = 0, hi = MAX_SAFE) {
76
+ assertUnits(units, 'clamp');
77
+ return Math.max(lo, Math.min(hi, units));
78
+ }
79
+ /**
80
+ * 把 total 按整数权重拆成 N 桶,【精确求和 = total】(最大余数法)。
81
+ * - 守恒:Σ 输出 === total,无 dust、不增发/不丢。
82
+ * - 余数(floor 后差额)按小数部分从大到小逐 1 派发(确定性、稳定)。
83
+ * - 全 0 权重 → 全 0 桶(调用方决定 total 的去向,通常落 reserve)。
84
+ * 用于佣金/罚没/分润等"一笔总额分给多方"的场景,替代逐项 round 造成的不守恒。
85
+ */
86
+ export function allocate(total, weights) {
87
+ assertUnits(total, 'allocate');
88
+ if (weights.some(w => !Number.isFinite(w) || w < 0))
89
+ throw new Error(`money.allocate: 权重含负/非法`);
90
+ const W = weights.reduce((a, b) => a + b, 0);
91
+ if (W <= 0)
92
+ return weights.map(() => 0);
93
+ const raw = weights.map(w => (total * w) / W);
94
+ const floor = raw.map(Math.floor);
95
+ const used = floor.reduce((a, b) => a + b, 0);
96
+ let remainder = total - used; // 整数,0..N-1(total≥0 时)
97
+ const out = floor.slice();
98
+ const order = raw.map((r, i) => ({ i, frac: r - Math.floor(r) })).sort((a, b) => b.frac - a.frac || a.i - b.i);
99
+ for (let k = 0; remainder > 0 && k < order.length; k++, remainder--)
100
+ out[order[k].i] += 1;
101
+ // total 为负数的极端情况(理论上不该发生)兜底:把残余补到最后一桶,守恒优先
102
+ if (remainder !== 0)
103
+ out[out.length - 1] += remainder;
104
+ out.forEach((u, i) => assertUnits(u, `allocate[${i}]`));
105
+ return out;
106
+ }
@@ -0,0 +1,103 @@
1
+ const BASE = 'https://webaz.xyz';
2
+ // ACP availability 枚举(spec):in_stock | out_of_stock | pre_order | backorder | unknown
3
+ function availability(stock) {
4
+ const n = Number(stock);
5
+ if (!Number.isFinite(n))
6
+ return 'unknown';
7
+ return n > 0 ? 'in_stock' : 'out_of_stock';
8
+ }
9
+ // 绝对化图片 URL(相对路径 → https://webaz.xyz/...);已是 http(s) 的原样返回
10
+ function absolutizeImg(raw) {
11
+ if (typeof raw !== 'string' || !raw.trim())
12
+ return null;
13
+ const s = raw.trim();
14
+ if (/^https?:\/\//i.test(s))
15
+ return s;
16
+ return BASE + (s.startsWith('/') ? s : '/' + s);
17
+ }
18
+ // description 必须 plain text(spec)→ 去 HTML 标签 + 折叠空白 + 截 5000
19
+ function plainText(raw, max) {
20
+ if (typeof raw !== 'string')
21
+ return '';
22
+ return raw.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, max);
23
+ }
24
+ export function buildAcpProductFeed(db, opts = {}) {
25
+ const limit = Math.min(Math.max(opts.limit ?? 2000, 1), 5000);
26
+ let rows = [];
27
+ try {
28
+ rows = db.prepare(`
29
+ SELECT p.id, p.title, p.description, p.price, p.currency, p.stock, p.category,
30
+ p.images, p.brand, p.model, p.return_days, p.product_type,
31
+ p.seller_id, u.name AS seller_name
32
+ FROM products p
33
+ LEFT JOIN users u ON u.id = p.seller_id
34
+ WHERE p.status = 'active'
35
+ ORDER BY p.created_at DESC
36
+ LIMIT ?
37
+ `).all(limit);
38
+ }
39
+ catch {
40
+ rows = [];
41
+ }
42
+ const products = rows.map((p) => {
43
+ let imgs = [];
44
+ try {
45
+ const arr = JSON.parse(p.images || '[]');
46
+ if (Array.isArray(arr))
47
+ imgs = arr.map(absolutizeImg).filter((x) => !!x);
48
+ }
49
+ catch { /* malformed → no images */ }
50
+ const returnDays = Number(p.return_days);
51
+ const hasReturn = Number.isFinite(returnDays) && returnDays > 0;
52
+ const item = {
53
+ item_id: p.id, // 稳定商品 ID(spec: max 100)
54
+ title: (p.title || '').slice(0, 150), // spec: max 150
55
+ description: plainText(p.description, 5000), // spec: plain text, max 5000
56
+ url: `${BASE}/#order-product/${p.id}`, // 商品详情页(SPA hash,200)
57
+ // price: spec = Number + ISO 4217 code。WAZ 非 ISO 4217 → 见 feed 级 _disclosures.currency
58
+ price: { amount: Number(p.price), currency: p.currency || 'WAZ' },
59
+ availability: availability(p.stock),
60
+ is_digital: p.product_type === 'digital',
61
+ seller_name: (p.seller_name || p.seller_id).slice(0, 70),
62
+ seller_url: `${BASE}/#u/${p.seller_id}`,
63
+ // 诚实门控:可发现 ≠ 可经 ACP 购买(ACP /complete 是卡+PSP,WebAZ 未接 —— RFC-015)
64
+ is_eligible_search: true,
65
+ is_eligible_checkout: false,
66
+ is_eligible_ads: false,
67
+ };
68
+ if (imgs.length) {
69
+ item.image_url = imgs[0];
70
+ if (imgs.length > 1)
71
+ item.additional_image_urls = imgs.slice(1).join(',');
72
+ }
73
+ if (p.brand)
74
+ item.brand = String(p.brand).slice(0, 70);
75
+ if (p.model)
76
+ item.mpn = String(p.model).slice(0, 70);
77
+ if (p.category)
78
+ item.product_category = String(p.category);
79
+ if (hasReturn) {
80
+ item.accepts_returns = true;
81
+ item.return_deadline_in_days = Math.round(returnDays);
82
+ }
83
+ return item;
84
+ });
85
+ return {
86
+ feed_version: 1,
87
+ generated_at: new Date().toISOString(),
88
+ source: 'WebAZ',
89
+ spec: {
90
+ based_on: 'OpenAI Agentic Commerce Protocol — product feed',
91
+ api_version_observed: '2025-09-12',
92
+ reference: 'https://developers.openai.com/commerce/specs/feed',
93
+ rfc: `${BASE}/docs/INTEGRATOR.md`,
94
+ },
95
+ _disclosures: {
96
+ phase: 'pre-launch',
97
+ currency: "Each item's price.currency is the protocol's SIMULATED pre-launch internal unit (WAZ, or legacy 'DCP'), NOT an ISO 4217 fiat currency. No real money is involved.",
98
+ checkout: 'is_eligible_checkout is false for every item: ACP checkout is not yet wired. Products are DISCOVERABLE via ACP, not yet PURCHASABLE via ACP. Native WebAZ checkout + escrow + state machine govern real purchases (see RFC-015).',
99
+ },
100
+ product_count: products.length,
101
+ products,
102
+ };
103
+ }
@@ -20959,7 +20959,7 @@ window.handleArbitrate = async (disputeId) => {
20959
20959
  const amtEl = document.querySelector(`.arb-liable-amount[data-party="${partyId}"]`)
20960
20960
  const amt = parseFloat(amtEl?.value || '0')
20961
20961
  if (!amt || amt <= 0) {
20962
- msgEl.innerHTML = alert$('error', `${t('请填写 ')}${chk.dataset.name}${t(' 的赔付金额')}`); return
20962
+ msgEl.innerHTML = alert$('error', `${t('请填写 ')}${escHtml(chk.dataset.name)}${t(' 的赔付金额')}`); return
20963
20963
  }
20964
20964
  const entry = { user_id: partyId, role: chk.dataset.role, amount: amt }
20965
20965
  const insuranceChk = document.querySelector(`.arb-liable-insurance[data-party="${partyId}"]`)
@@ -22122,8 +22122,9 @@ async function renderSeller(app) {
22122
22122
 
22123
22123
  const skillsSection = sellerSubTab === 'skills' ? `
22124
22124
  <div style="font-size:12px;color:#6b7280;margin-bottom:10px">${t('Skill 自动化 · 让 Agent 替你接单/报价/发货')}</div>
22125
- <div id="skill-mgmt-content">${loading$()}</div>
22126
22125
  ` : ''
22126
+ // 注:Skill 真实内容由下方 #my-skills-list 同步渲染(mySkills 已同步就绪);
22127
+ // 此前这里有个 #skill-mgmt-content loading$() 占位但全文件无人填充 → spinner 永转,已删除。
22127
22128
 
22128
22129
  const productsSection = sellerSubTab === 'products' ? `
22129
22130
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
@@ -25200,7 +25201,7 @@ window.doWithdraw = async () => {
25200
25201
  msgEl.innerHTML = ''
25201
25202
  return
25202
25203
  }
25203
- msgEl.innerHTML = alert$('success', `✅ ${res.message}`)
25204
+ msgEl.innerHTML = alert$('success', `✅ ${escHtml(res.message)}`)
25204
25205
  setTimeout(() => renderWallet(document.getElementById('app')), 2000)
25205
25206
  }
25206
25207
 
@@ -26636,7 +26637,7 @@ async function renderSkillPublish(app) {
26636
26637
  <textarea class="form-control" id="skm-preview" rows="3" maxlength="500" placeholder="${t('展示部分内容吸引购买,可留空')}" style="font-size:13px;resize:vertical;margin-bottom:12px"></textarea>
26637
26638
 
26638
26639
  <div style="font-size:12px;color:#374151;font-weight:600;margin-bottom:6px">${t('技能正文(购买后解锁)')}</div>
26639
- <textarea class="form-control" id="skm-content" rows="8" maxlength="20000" placeholder="${t('完整的模板 / 提示词 / 指南 / 清单内容…')}" style="font-size:13px;resize:vertical"></textarea>
26640
+ <textarea class="form-control" id="skm-body" rows="8" maxlength="20000" placeholder="${t('完整的模板 / 提示词 / 指南 / 清单内容…')}" style="font-size:13px;resize:vertical"></textarea>
26640
26641
  </div></div>
26641
26642
  <div id="skm-publish-msg" style="margin-bottom:8px"></div>
26642
26643
  <button class="btn btn-primary" style="width:100%;font-size:14px;padding:10px" onclick="doSkmPublish()">${t('提交审核')}</button>
@@ -26655,7 +26656,7 @@ window.doSkmPublish = async () => {
26655
26656
  const price = parseFloat(document.getElementById('skm-price')?.value) || 0
26656
26657
  const summary = document.getElementById('skm-summary')?.value?.trim()
26657
26658
  const preview = document.getElementById('skm-preview')?.value?.trim()
26658
- const content = document.getElementById('skm-content')?.value
26659
+ const content = document.getElementById('skm-body')?.value
26659
26660
  const msg = document.getElementById('skm-publish-msg')
26660
26661
  if (!title || title.length < 2) { msg.innerHTML = alert$('error', t('请填写标题')); return }
26661
26662
  if (!content || !content.trim()) { msg.innerHTML = alert$('error', t('请填写技能正文内容')); return }
@@ -27554,7 +27555,7 @@ async function renderListingFollow(app, listingId) {
27554
27555
  if (!state.user) { location.hash = '#login'; return }
27555
27556
  if (state.user.role !== 'seller') { app.innerHTML = `<div style="padding:24px">${alert$('warn', t('仅卖家可跟卖'))}</div>`; return }
27556
27557
  const r = await GET('/listings/' + encodeURIComponent(listingId))
27557
- if (r?.error) { app.innerHTML = `<div style="padding:24px">${alert$('error', r.error)}</div>`; return }
27558
+ if (r?.error) { app.innerHTML = `<div style="padding:24px">${alert$('error', escHtml(r.error))}</div>`; return }
27558
27559
  const l = r.listing
27559
27560
 
27560
27561
  app.innerHTML = `
@@ -33287,7 +33288,7 @@ window.submitNote = async (orderId, parentId, draftKey) => {
33287
33288
  body: buf,
33288
33289
  })
33289
33290
  const j = await upResp.json()
33290
- if (!upResp.ok || j.error) { msg.innerHTML = alert$('error', t('图片上传失败') + `:${j.error}`); btn.disabled = false; return }
33291
+ if (!upResp.ok || j.error) { msg.innerHTML = alert$('error', t('图片上传失败') + `:${escHtml(j.error)}`); btn.disabled = false; return }
33291
33292
  hashes.push(hash)
33292
33293
  }
33293
33294
  // 步骤 2: 创建笔记(带 parent_id 时为转发)
@@ -3,7 +3,20 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
- <title>WebAZ</title>
6
+ <title>WebAZ — Agent-native decentralized commerce protocol</title>
7
+ <!-- Crawler / agent discoverability (static, no-JS): so a fetch of this page returns real meaning, not an empty shell. -->
8
+ <meta name="description" content="WebAZ is an open, agent-native decentralized commerce protocol. Every order is escrow-protected and flows through a state machine; on timeout the protocol auto-rules fault and executes remedy. Buyers get escrow + automatic refunds + transparent seller reputation, price history and arbitration precedents; AI agents browse, order, compare and fulfill via MCP. Pre-launch (simulated test currency, no real settlement yet).">
9
+ <link rel="canonical" href="https://webaz.xyz/">
10
+ <meta property="og:type" content="website">
11
+ <meta property="og:site_name" content="WebAZ">
12
+ <meta property="og:url" content="https://webaz.xyz/">
13
+ <meta property="og:title" content="WebAZ — Agent-native decentralized commerce protocol">
14
+ <meta property="og:description" content="Escrow-protected, state-machine commerce: timeouts auto-rule fault + remedy. Buyers get escrow + auto-refund + transparent reputation; AI agents transact via MCP. Pre-launch.">
15
+ <meta property="og:image" content="https://webaz.xyz/webaz-logo.png">
16
+ <meta name="twitter:card" content="summary_large_image">
17
+ <meta name="twitter:title" content="WebAZ — Agent-native decentralized commerce protocol">
18
+ <meta name="twitter:description" content="Escrow-protected, state-machine commerce. Buyers: escrow + auto-refund + transparent reputation. Agents transact via MCP. Pre-launch.">
19
+ <meta name="twitter:image" content="https://webaz.xyz/webaz-logo.png">
7
20
  <link rel="stylesheet" href="/style.css">
8
21
  <link rel="manifest" href="/manifest.json">
9
22
  <link rel="icon" type="image/svg+xml" href="/icon.svg">
@@ -15,6 +28,18 @@
15
28
  </head>
16
29
  <body>
17
30
  <div id="app"></div>
31
+ <!-- No-JS / crawler fallback: real content for fetchers that don't run the SPA (fixes "scrape only sees an empty <title>"). -->
32
+ <noscript>
33
+ <main style="max-width:680px;margin:40px auto;padding:0 20px;font-family:system-ui,sans-serif;line-height:1.7;color:#1f2937">
34
+ <h1>WebAZ — Agent-native decentralized commerce protocol</h1>
35
+ <p>WebAZ is an open, agent-native decentralized commerce protocol. Every order is escrow-protected and flows through a state machine; if a responsible party times out, the protocol automatically rules fault and executes the remedy.</p>
36
+ <p><strong>For buyers:</strong> funds are held in escrow and released only after you confirm receipt; sellers who don't accept/ship/deliver in time trigger an automatic refund; disputes are decided with evidence + neutral arbitration; seller reputation, price history and arbitration precedents are public before you buy.</p>
37
+ <p><strong>For AI agents:</strong> browse, compare, order and track fulfillment via the Model Context Protocol (MCP). Protocol state is public at <a href="/.well-known/webaz-protocol.json">/.well-known/webaz-protocol.json</a>; the integration contract is at <a href="/.well-known/webaz-integration.json">/.well-known/webaz-integration.json</a>.</p>
38
+ <p><strong>Status:</strong> pre-launch — WAZ is a simulated test currency; no real money settles yet.</p>
39
+ <p>Browse: <a href="/#discover">/#discover</a> · Learn more: <a href="/#welcome">/#welcome</a> · Source: <a href="https://github.com/seasonsagents-art/webaz">github.com/seasonsagents-art/webaz</a></p>
40
+ <p style="color:#6b7280;font-size:14px">This page is an interactive app and needs JavaScript for full functionality.</p>
41
+ </main>
42
+ </noscript>
18
43
  <script src="/i18n.js"></script>
19
44
  <script src="/app.js"></script>
20
45
  </body>
@@ -1,3 +1,6 @@
1
+ // RFC-014 PR6 — 拍卖 stake 锁定/释放走整数 base-units + 绝对值落库。
2
+ import { toUnits } from '../../money.js';
3
+ import { applyWalletDelta } from '../../ledger.js';
1
4
  // ─── 拍卖常量(域内)──────────────────────────────────────────
2
5
  const AUC_MAX_WINDOW_MIN = 14 * 24 * 60; // 14 天上限
3
6
  const AUC_MIN_WINDOW_MIN = 5;
@@ -118,7 +121,7 @@ export function registerAuctionRoutes(app, deps) {
118
121
  seller_stake_locked, notes)
119
122
  VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now', '+' || ? || ' minutes'),?,?,?)
120
123
  `).run(id, user.id, body.listing_id ? String(body.listing_id) : null, productId, title, body.spec_json ? JSON.stringify(body.spec_json) : null, qty, cat, startingPrice, startingPrice, minIncrement, reservePrice, windowMin, sniperExtend, sellerStake, body.notes ? String(body.notes).slice(0, 500) : null);
121
- db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?').run(sellerStake, sellerStake, user.id);
124
+ applyWalletDelta(db, user.id, { balance: -toUnits(sellerStake), staked: toUnits(sellerStake) });
122
125
  if (productId)
123
126
  db.prepare("UPDATE products SET status = 'auction_pending', updated_at = datetime('now') WHERE id = ?").run(productId);
124
127
  })();
@@ -346,19 +349,19 @@ export function registerAuctionRoutes(app, deps) {
346
349
  if (myPrev) {
347
350
  db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(myPrev.id);
348
351
  if (myPrev.stake_locked > 0)
349
- db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(myPrev.stake_locked, myPrev.stake_locked, user.id);
352
+ applyWalletDelta(db, user.id, { balance: toUnits(myPrev.stake_locked), staked: -toUnits(myPrev.stake_locked) });
350
353
  }
351
354
  // 释放别人的最高 active bid
352
355
  const others = db.prepare("SELECT id, buyer_id, stake_locked FROM auction_bids WHERE auction_id = ? AND status = 'active' AND buyer_id != ?").all(req.params.id, user.id);
353
356
  for (const o of others) {
354
357
  db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(o.id);
355
358
  if (o.stake_locked > 0)
356
- db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(o.stake_locked, o.stake_locked, o.buyer_id);
359
+ applyWalletDelta(db, o.buyer_id, { balance: toUnits(o.stake_locked), staked: -toUnits(o.stake_locked) });
357
360
  }
358
361
  // 插入新 bid
359
362
  db.prepare(`INSERT INTO auction_bids (id, auction_id, buyer_id, price, stake_locked) VALUES (?,?,?,?,?)`)
360
363
  .run(id, req.params.id, user.id, price, stake);
361
- db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?').run(stake, stake, user.id);
364
+ applyWalletDelta(db, user.id, { balance: -toUnits(stake), staked: toUnits(stake) });
362
365
  // P1 #9:卖家 stake 动态补足(5% × current_price,余额不足则尽量补)
363
366
  const targetSellerStake = Math.max(1, Math.round(price * AUC_SELLER_STAKE_PCT * 100) / 100);
364
367
  const curSellerStake = Number(fresh.seller_stake_locked) || 0;
@@ -367,7 +370,7 @@ export function registerAuctionRoutes(app, deps) {
367
370
  const sWal = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(auc.seller_id);
368
371
  const canTopup = sWal ? Math.min(delta, Number(sWal.balance)) : 0;
369
372
  if (canTopup > 0) {
370
- db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?').run(canTopup, canTopup, auc.seller_id);
373
+ applyWalletDelta(db, auc.seller_id, { balance: -toUnits(canTopup), staked: toUnits(canTopup) });
371
374
  db.prepare('UPDATE auctions SET seller_stake_locked = seller_stake_locked + ? WHERE id = ?').run(canTopup, req.params.id);
372
375
  sellerTopup = canTopup;
373
376
  }
@@ -420,7 +423,7 @@ export function registerAuctionRoutes(app, deps) {
420
423
  db.transaction(() => {
421
424
  db.prepare("UPDATE auctions SET status = 'cancelled', updated_at = datetime('now') WHERE id = ?").run(req.params.id);
422
425
  if (sellerStake > 0)
423
- db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(sellerStake, sellerStake, user.id);
426
+ applyWalletDelta(db, user.id, { balance: toUnits(sellerStake), staked: -toUnits(sellerStake) });
424
427
  if (auc.product_id)
425
428
  db.prepare("UPDATE products SET status = 'active', updated_at = datetime('now') WHERE id = ? AND status = 'auction_pending'").run(auc.product_id);
426
429
  })();
@@ -1,6 +1,9 @@
1
1
  import express from 'express';
2
2
  // RFC-007 stage 5:客观拒单仲裁翻案直接复用 L0 状态机/结算(纯 db 函数,无副作用)
3
3
  import { transition, settleFault, settleDeclinedNoFault } from '../../layer0-foundation/L0-2-state-machine/engine.js';
4
+ // RFC-014 PR5 — 争议后佣金/基金 clawback 走整数 base-units + 绝对值落库。
5
+ import { toUnits, toDecimal, mulRate } from '../../money.js';
6
+ import { applyWalletDelta } from '../../ledger.js';
4
7
  export function registerDisputesWriteRoutes(app, deps) {
5
8
  const { db, auth, generateId, detectFraud, errorRes, isEligibleArbitrator, requireHumanPresence, getDisputeDetails, respondToDispute, arbitrateDispute, addPartyEvidence, requestEvidence, markEvidenceExpiry, uploadEvidence, EVIDENCE_MAX_BYTES, EVIDENCE_ALLOWED_MIME, appendOrderEvent, FUND_BASE_RATE, settleCommission, depositToFund, calculatePv, recordDisputeReputation, issueAgentStrike, publishDisputeCase, logAdminAction, snfSend, getProtocolParam } = deps;
6
9
  // ── RFC-007 stage 5:客观拒单【临时判责】的仲裁翻案 ─────────────────────────────
@@ -194,12 +197,11 @@ export function registerDisputesWriteRoutes(app, deps) {
194
197
  return;
195
198
  const total = Number(order.total_amount);
196
199
  const commRate = Number(order.snapshot_commission_rate ?? 0);
197
- const commPool = Math.round(total * commRate * 100) / 100;
198
- const fundBase = Math.round(total * FUND_BASE_RATE() * 100) / 100;
199
- const deduct = commPool + fundBase;
200
+ const deductU = mulRate(toUnits(total), commRate) + mulRate(toUnits(total), FUND_BASE_RATE());
201
+ const deduct = toDecimal(deductU);
200
202
  const sellerWallet = db.prepare("SELECT balance FROM wallets WHERE user_id = ?").get(order.seller_id);
201
- if (sellerWallet && sellerWallet.balance >= deduct) {
202
- db.prepare("UPDATE wallets SET balance = balance - ?, earned = earned - ? WHERE user_id = ?").run(deduct, deduct, order.seller_id);
203
+ if (sellerWallet && toUnits(sellerWallet.balance) >= deductU) {
204
+ applyWalletDelta(db, order.seller_id, { balance: -deductU, earned: -deductU });
203
205
  const { redirected: disputeRedirected } = settleCommission(dispute.order_id);
204
206
  depositToFund(dispute.order_id, disputeRedirected);
205
207
  const productRow = db.prepare("SELECT category_id FROM products WHERE id = ?").get(order.product_id);
@@ -248,13 +250,12 @@ export function registerDisputesWriteRoutes(app, deps) {
248
250
  if (effectiveBase <= 0)
249
251
  return;
250
252
  const commRate = Number(order.snapshot_commission_rate ?? 0);
251
- const commPool = Math.round(effectiveBase * commRate * 100) / 100;
252
- const fundBase = Math.round(effectiveBase * FUND_BASE_RATE() * 100) / 100;
253
- const deduct = Math.round((commPool + fundBase) * 100) / 100;
253
+ const deductU = mulRate(toUnits(effectiveBase), commRate) + mulRate(toUnits(effectiveBase), FUND_BASE_RATE());
254
+ const deduct = toDecimal(deductU);
254
255
  const sellerWallet = db.prepare("SELECT balance FROM wallets WHERE user_id = ?").get(order.seller_id);
255
- if (sellerWallet && sellerWallet.balance >= deduct) {
256
- if (deduct > 0) {
257
- db.prepare("UPDATE wallets SET balance = balance - ?, earned = earned - ? WHERE user_id = ?").run(deduct, deduct, order.seller_id);
256
+ if (sellerWallet && toUnits(sellerWallet.balance) >= deductU) {
257
+ if (deductU > 0) {
258
+ applyWalletDelta(db, order.seller_id, { balance: -deductU, earned: -deductU });
258
259
  }
259
260
  const { redirected } = settleCommission(dispute.order_id, effectiveBase);
260
261
  depositToFund(dispute.order_id, redirected, effectiveBase);
@@ -1,4 +1,7 @@
1
1
  import { buildCartMandate, buildPaymentMandate, signMandate } from './ap2-mandate.js';
2
+ // RFC-014 PR3 — 金额走整数 base-units;钱包写绝对值(防 REAL 浮点加法 dust)。
3
+ import { toUnits, toDecimal, mulQty, mulRate } from '../../money.js';
4
+ import { applyWalletDelta } from '../../ledger.js';
2
5
  export function registerOrdersCreateRoutes(app, deps) {
3
6
  const { db, auth, isTrustedRole, generateId, generateRecipientCode, DONATION_VALID_PCTS, INTERNAL_AUDITOR_ID, addHours, getActiveFlashSale, applyCouponToOrder, getProtocolParam, getProductShareChain, isAllowedSponsor, checkStockAndMaybeDelist, auditSponsorChainCross, appendOrderEvent, transition, notifyTransition, shouldAutoAccept, ensureCharityRep, broadcastSystemEvent, signPassport, issuerAddress } = deps;
4
7
  app.post('/api/orders', async (req, res) => {
@@ -170,18 +173,25 @@ export function registerOrdersCreateRoutes(app, deps) {
170
173
  db.prepare(`UPDATE price_sessions SET used_at = datetime('now') WHERE token = ?`).run(session_token);
171
174
  }
172
175
  const basePrice = product.price;
173
- // 多件:subtotal = basePrice * qty,减 coupon,再加保险(按 subtotal 计费)
174
- const subtotal = Math.round(basePrice * reqQty * 100) / 100;
175
- const priceAfterCoupon = Math.max(0, Math.round((subtotal - couponDiscount) * 100) / 100);
176
+ // RFC-014:全部金额在整数 base-units 上算(精确),再 toDecimal 落库/响应。
177
+ // 多件:subtotal = basePrice × qty,减 coupon,再加保险(按 subtotal 计费)
178
+ const basePriceU = toUnits(basePrice);
179
+ const subtotalU = mulQty(basePriceU, reqQty);
180
+ const priceAfterCouponU = Math.max(0, subtotalU - toUnits(couponDiscount));
176
181
  const insuranceRate = getProtocolParam('order_insurance_rate', 0.01);
177
- const insurancePremium = buy_insurance ? Math.round(priceAfterCoupon * insuranceRate * 100) / 100 : 0;
178
- const totalAmount = Math.max(0, Math.round((priceAfterCoupon + insurancePremium) * 100) / 100);
182
+ const insurancePremiumU = buy_insurance ? mulRate(priceAfterCouponU, insuranceRate) : 0;
183
+ const totalAmountU = Math.max(0, priceAfterCouponU + insurancePremiumU);
179
184
  // B5 主动捐赠 — 按订单总额 × 比例算(额外扣款,进 charity_fund)
180
- const donationAmount = donationPctNum > 0 ? Math.round(totalAmount * donationPctNum * 100) / 100 : 0;
185
+ const donationAmountU = donationPctNum > 0 ? mulRate(totalAmountU, donationPctNum) : 0;
186
+ // decimal 视图(落库 / 下游 AP2 / 响应,均为 base-unit 干净值)
187
+ const subtotal = toDecimal(subtotalU);
188
+ const insurancePremium = toDecimal(insurancePremiumU);
189
+ const totalAmount = toDecimal(totalAmountU);
190
+ const donationAmount = toDecimal(donationAmountU);
181
191
  const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
182
192
  if (!wallet)
183
193
  return void res.status(500).json({ error: '钱包记录缺失', error_code: 'WALLET_MISSING' });
184
- if (wallet.balance < totalAmount + donationAmount)
194
+ if (toUnits(wallet.balance) < totalAmountU + donationAmountU)
185
195
  return void res.json({ error: `余额不足:需 ${(totalAmount + donationAmount).toFixed(2)} WAZ(含 ${donationAmount} WAZ 捐赠),当前 ${wallet.balance} WAZ` });
186
196
  const now = new Date();
187
197
  const orderId = generateId('ord');
@@ -259,13 +269,14 @@ export function registerOrdersCreateRoutes(app, deps) {
259
269
  catch (e) {
260
270
  console.warn('[order-chain] genesis event failed:', e.message);
261
271
  }
262
- db.prepare('UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ?')
263
- .run(totalAmount, totalAmount, user.id);
272
+ // RFC-014:钱包托管锁定走绝对值落库(整数 base-units)
273
+ applyWalletDelta(db, user.id, { balance: -totalAmountU, escrowed: totalAmountU });
264
274
  // B5:捐赠 — 从 balance 扣 + 进 charity_fund + 记一笔 donation txn(事务内原子)
265
- if (donationAmount > 0) {
266
- db.prepare('UPDATE wallets SET balance = balance - ? WHERE user_id = ?').run(donationAmount, user.id);
267
- db.prepare(`UPDATE charity_fund SET balance = balance + ?, total_donated = total_donated + ?, updated_at = datetime('now') WHERE id = 'main'`)
268
- .run(donationAmount, donationAmount);
275
+ if (donationAmountU > 0) {
276
+ applyWalletDelta(db, user.id, { balance: -donationAmountU });
277
+ const cf = db.prepare("SELECT COALESCE(balance,0) balance, COALESCE(total_donated,0) total_donated FROM charity_fund WHERE id = 'main'").get();
278
+ db.prepare(`UPDATE charity_fund SET balance = ?, total_donated = ?, updated_at = datetime('now') WHERE id = 'main'`)
279
+ .run(toDecimal(toUnits(cf?.balance ?? 0) + donationAmountU), toDecimal(toUnits(cf?.total_donated ?? 0) + donationAmountU));
269
280
  db.prepare(`INSERT INTO charity_fund_txns (id, kind, from_user_id, to_user_id, amount, related_order_id, note)
270
281
  VALUES (?, 'donation', ?, NULL, ?, ?, ?)`).run(generateId('cft'), user.id, donationAmount, orderId, `下单时捐赠 ${(donationPctNum * 100).toFixed(1)}%`);
271
282
  // 同步 charity_reputation(捐款荣誉)
@@ -10,6 +10,7 @@ import { buildIntegrationContract } from '../integration-contract.js';
10
10
  import { buildVerifiabilityIndex } from '../verifiability-index.js';
11
11
  import { buildEconomicParticipation } from '../economic-participation.js';
12
12
  import { buildNegativeSpace } from '../negative-space.js';
13
+ import { buildAcpProductFeed } from '../acp-feed.js';
13
14
  export function registerPublicUtilsRoutes(app, deps) {
14
15
  const { db, MASTER_SEED, NODE_ENV, SERVICE_START_MS, rateLimitOk, generateManifest, getUser, logError, issuerAddress } = deps;
15
16
  app.get('/api/health', (_req, res) => {
@@ -138,6 +139,7 @@ export function registerPublicUtilsRoutes(app, deps) {
138
139
  event_stream: 'https://webaz.xyz/api/agent/events?since=<cursor>', // ⑥ 事件游标流(party-gated,需 auth)
139
140
  passport: 'https://webaz.xyz/api/me/agents/:apiKeyPrefix/passport', // ⑤ 可验护照
140
141
  did: 'https://webaz.xyz/.well-known/did.json',
142
+ acp_product_feed: 'https://webaz.xyz/.well-known/webaz-acp-feed.json', // RFC-015 P0 — ACP 风格商品发现 feed(只读;is_eligible_checkout=false)
141
143
  },
142
144
  // 路线图 — 回应"知道还有哪些没做"的诚实化第三层。哲学:公开当前到达点 + 已知未做项,不承诺时间表。
143
145
  roadmap: {
@@ -270,6 +272,15 @@ export function registerPublicUtilsRoutes(app, deps) {
270
272
  };
271
273
  app.get('/.well-known/webaz-negative-space.json', negativeSpace);
272
274
  app.get('/api/agent/negative-space', negativeSpace);
275
+ // RFC-015 P0 —— ACP product feed:把现有商品投影成 OpenAI Agentic Commerce 的 feed 形状,
276
+ // 让 ACP/ChatGPT agent 能【发现】WebAZ 商品(只读,无钱)。诚实门控:is_eligible_checkout 恒 false
277
+ // (ACP /complete 是卡+PSP,WebAZ 未接);currency=WAZ 是模拟单位。详见 buildAcpProductFeed + RFC-015。
278
+ const acpFeed = (_req, res) => {
279
+ res.setHeader('Cache-Control', 'public, max-age=300');
280
+ res.json(buildAcpProductFeed(db));
281
+ };
282
+ app.get('/.well-known/webaz-acp-feed.json', acpFeed);
283
+ app.get('/api/agent/acp-feed', acpFeed);
273
284
  // W3C DID Document(B.6 b DID 短期 mapping,2026-05-30):
274
285
  // did:web:webaz.xyz 通过 HTTPS 解析到这里(W3C did:web spec §3.2)
275
286
  // verificationMethod 用 EcdsaSecp256k1RecoveryMethod2020 + CAIP-10 blockchainAccountId