@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/layer0-foundation/L0-2-state-machine/engine.js +83 -90
- package/dist/layer1-agent/L1-1-mcp-server/server.js +13 -0
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +138 -168
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +6 -4
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +12 -8
- package/dist/ledger.js +58 -0
- package/dist/money.js +106 -0
- package/dist/pwa/acp-feed.js +103 -0
- package/dist/pwa/public/app.js +8 -7
- package/dist/pwa/public/index.html +26 -1
- package/dist/pwa/routes/auction.js +9 -6
- package/dist/pwa/routes/disputes-write.js +12 -11
- package/dist/pwa/routes/orders-create.js +24 -13
- package/dist/pwa/routes/public-utils.js +11 -0
- package/dist/pwa/server.js +57 -39
- package/dist/settlement-math.js +27 -0
- package/package.json +1 -1
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
|
+
}
|
package/dist/pwa/public/app.js
CHANGED
|
@@ -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-
|
|
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-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
198
|
-
const
|
|
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 >=
|
|
202
|
-
db.
|
|
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
|
|
252
|
-
const
|
|
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 >=
|
|
256
|
-
if (
|
|
257
|
-
db.
|
|
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
|
-
//
|
|
174
|
-
|
|
175
|
-
const
|
|
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
|
|
178
|
-
const
|
|
182
|
+
const insurancePremiumU = buy_insurance ? mulRate(priceAfterCouponU, insuranceRate) : 0;
|
|
183
|
+
const totalAmountU = Math.max(0, priceAfterCouponU + insurancePremiumU);
|
|
179
184
|
// B5 主动捐赠 — 按订单总额 × 比例算(额外扣款,进 charity_fund)
|
|
180
|
-
const
|
|
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 <
|
|
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
|
-
|
|
263
|
-
|
|
272
|
+
// RFC-014:钱包托管锁定走绝对值落库(整数 base-units)
|
|
273
|
+
applyWalletDelta(db, user.id, { balance: -totalAmountU, escrowed: totalAmountU });
|
|
264
274
|
// B5:捐赠 — 从 balance 扣 + 进 charity_fund + 记一笔 donation txn(事务内原子)
|
|
265
|
-
if (
|
|
266
|
-
db.
|
|
267
|
-
db.prepare(
|
|
268
|
-
|
|
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
|