@bolloon/bolloon-agent 0.5.2 → 0.5.3

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.
Files changed (53) hide show
  1. package/README.md +4 -4
  2. package/dist/agents/chain/transfer-classify.js +99 -0
  3. package/dist/agents/chain/transfer-index.js +354 -0
  4. package/dist/agents/identity/address-binding.js +613 -0
  5. package/dist/agents/network-pulse.js +706 -27
  6. package/dist/agents/x402/direct-payment.js +689 -0
  7. package/dist/agents/x402/order-identity.js +408 -0
  8. package/dist/agents/x402/paid-info-store.js +17 -0
  9. package/dist/agents/x402/seller-signing.js +614 -0
  10. package/dist/agents/x402/seller-summary.js +398 -0
  11. package/dist/cli/identity-command.js +327 -2
  12. package/dist/cli/protocol-envelope.js +4 -0
  13. package/dist/cli/update-commands.js +41 -25
  14. package/dist/cli/x402-buyer-command.js +463 -0
  15. package/dist/cli/x402-seller-command.js +289 -0
  16. package/dist/cli-entry.js +21 -5
  17. package/dist/electron-build/agents/chain/transfer-classify.js +106 -0
  18. package/dist/electron-build/agents/chain/transfer-classify.js.map +1 -0
  19. package/dist/electron-build/agents/chain/transfer-index.js +400 -0
  20. package/dist/electron-build/agents/chain/transfer-index.js.map +1 -0
  21. package/dist/electron-build/agents/identity/address-binding.js +672 -0
  22. package/dist/electron-build/agents/identity/address-binding.js.map +1 -0
  23. package/dist/electron-build/agents/network-pulse.js +714 -28
  24. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  25. package/dist/electron-build/agents/x402/direct-payment.js +744 -0
  26. package/dist/electron-build/agents/x402/direct-payment.js.map +1 -0
  27. package/dist/electron-build/agents/x402/order-identity.js +458 -0
  28. package/dist/electron-build/agents/x402/order-identity.js.map +1 -0
  29. package/dist/electron-build/agents/x402/paid-info-protocol.js.map +1 -1
  30. package/dist/electron-build/agents/x402/paid-info-store.js +18 -0
  31. package/dist/electron-build/agents/x402/paid-info-store.js.map +1 -1
  32. package/dist/electron-build/agents/x402/seller-signing.js +682 -0
  33. package/dist/electron-build/agents/x402/seller-signing.js.map +1 -0
  34. package/dist/electron-build/utils/auto-update.js +2 -2
  35. package/dist/electron-build/utils/auto-update.js.map +1 -1
  36. package/dist/electron-build/utils/runtime-bootstrap.js +1 -1
  37. package/dist/electron-build/utils/runtime-bootstrap.js.map +1 -1
  38. package/dist/electron-build/utils/update-manager.js +3 -3
  39. package/dist/electron-build/utils/update-manager.js.map +1 -1
  40. package/dist/electron-build/utils/version-identity.js +1 -1
  41. package/dist/electron-build/utils/version-identity.js.map +1 -1
  42. package/dist/electron-build/web/routes-x402-info.js +28 -1
  43. package/dist/electron-build/web/routes-x402-info.js.map +1 -1
  44. package/dist/index.js +3 -3
  45. package/dist/utils/auto-update.js +2 -2
  46. package/dist/utils/runtime-bootstrap.js +1 -1
  47. package/dist/utils/update-health.js +2 -2
  48. package/dist/utils/update-manager.js +3 -3
  49. package/dist/utils/version-identity.js +1 -1
  50. package/dist/web/bolloon-web.json +3 -3
  51. package/dist/web/routes-x402-info.js +28 -1
  52. package/dist/web/sw.js +1 -1
  53. package/package.json +2 -1
@@ -0,0 +1,408 @@
1
+ /**
2
+ * order-identity.ts — x402 直付的**订单标识** (约定 v1, 非索引标签 `BOL1`)
3
+ *
4
+ * ── 要解决的事 ───────────────────────────────────────────────────────────────
5
+ * 普通 USDC 转账在链上只是 `Transfer(from, to, value)` —— **没有任何"买的是哪件东西"的痕迹**。
6
+ * 于是"公示区只认链上事实"这条口径下, 一笔转账与一件商品之间**对不上号**:
7
+ * 事后谁也没法从链上证明这笔钱是付给哪件物品的。
8
+ *
9
+ * EIP-3009 (`transferWithAuthorization`) 给了一个免费的把手: 授权里的 `nonce` 是**付款方自选
10
+ * 的 32 字节**, 且会在同一条交易的 `AuthorizationUsed(authorizer, nonce)` 事件里**原样记上链**。
11
+ * 所以只要把"订单身份"编进这 32 字节, "谁付 · 多少 · 给谁 · 买什么" 就在**同一笔交易里自证**。
12
+ *
13
+ * ── 约定 v1 原文 (字节级, 不可自行发挥) ──────────────────────────────────────
14
+ * ```
15
+ * nonce (32 字节) =
16
+ * 偏移 0 .. 3 : 标签 0x424f4c31 = ASCII "BOL1" (4 字节, 大端按字节序)
17
+ * 偏移 4 .. 7 : orderSeq, uint32 大端 (4 字节; 同一买家重复买同件时递增)
18
+ * 偏移 8 .. 31 : hash24 = keccak256( utf8(itemId) ‖ uint256be(orderSeq) )[0..23] (24 字节)
19
+ * ```
20
+ * 其中 `uint256be(orderSeq)` = orderSeq 的 32 字节大端表示; `[0..23]` = 取前 24 字节。
21
+ *
22
+ * **为什么不是"后 28 字节 = keccak256(itemId‖orderSeq) 截断 28 字节"** (这是本约定唯一一处
23
+ * 与最初口头描述的差异, 必须写清而不是含糊过去): 那样编出来的 nonce 里**没有 orderSeq**,
24
+ * 而卖方要判"自证"必须**自己复算** `keccak256(itemId‖orderSeq)` —— 没有 orderSeq 就复算不了
25
+ * (只能对 orderSeq 做暴力枚举, 既有假阳性风险又不确定)。所以 v1 把 4 字节 orderSeq 显式编进去,
26
+ * 哈希只占 24 字节。安全强度: 24 字节 = 192 bit 抗碰撞, 对"订单标识"这个用途远远够用。
27
+ *
28
+ * ── 去重语义 (必须写在实现旁边, 否则用错就是"自以为自证了") ────────────────
29
+ * · EIP-3009 的 `(authorizer, nonce)` **只能用一次** (链上 `authorizationState` 置位, 再用必 revert)。
30
+ * ⇒ **同一买家重复买同一件** 必须换 `orderSeq` (否则第二笔必 revert, 不是"卖方不认"是**链不认**)。
31
+ * · **不同买家**可以用**相同 nonce** —— 去重键是 `(authorizer, nonce)`, authorizer 不同就不冲突。
32
+ * · 所以 `(payer, nonce)` 才是唯一键; `nonce` 单独**不是**唯一键。
33
+ *
34
+ * ── 零依赖红线 ──────────────────────────────────────────────────────────────
35
+ * 本模块要能在**没有 node_modules 的服务器**上跑 (ECS 只放编译产物 `lib/x402/*.js`) ——
36
+ * 所以 keccak-256 是**本文件内的纯 TS 实现** (node:crypto 只有 sha3-256, 与 keccak-256 的
37
+ * padding 不同, 不能替代), **绝不 import ethers**。正确性靠测试与 ethers 对表 (见
38
+ * `src/test/x402-order-identity.test.ts`) + 真链 DOMAIN_SEPARATOR 交叉核对。
39
+ */
40
+ import * as crypto from 'crypto';
41
+ // ────────────────────────────────────────────────────────────── 常量 (唯一事实源)
42
+ export const ORDER_IDENTITY_PROTOCOL = 'bolloon-x402-order-identity/1';
43
+ /** 标签: ASCII "BOL1" (Bolloon order 1) */
44
+ export const ORDER_NONCE_TAG_ASCII = 'BOL1';
45
+ export const ORDER_NONCE_TAG = '0x424f4c31';
46
+ export const ORDER_TAG_BYTES = 4;
47
+ export const ORDER_SEQ_BYTES = 4;
48
+ export const ORDER_HASH_BYTES = 24;
49
+ /**
50
+ * EIP-3009 `AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)` 的 topic0。
51
+ * ★ 与正文里那个实测值必须一致 (本文件只是抄一份常量, 不复制逻辑)。
52
+ * 非索引参数无; 两个参数都是 indexed ⇒ authorizer 在 topics[1], nonce 在 **topics[2]**。
53
+ */
54
+ export const EIP3009_AUTHORIZATION_USED_TOPIC0 = '0x98de503528ee59b575ef0c0a2576a82497bfc029a5685b209e9ec333479b10a5';
55
+ /** `transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32)` */
56
+ export const EIP3009_TRANSFER_WITH_AUTHORIZATION_SELECTOR = '0xe3ee160e';
57
+ /** EIP-712 域类型串 (USDC / FiatToken 家族用的是这个标准串) */
58
+ export const EIP712_DOMAIN_TYPEHASH_STR = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)';
59
+ /** EIP-3009 授权结构类型串 */
60
+ export const EIP3009_TYPEHASH_STR = 'TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)';
61
+ /**
62
+ * USDC (Base) 的 EIP-712 domain。
63
+ * ★ 实测 (2026-09-29, 两条 RPC 一致): `name()` 回 **"USD Coin"**, `version()` 回 "2",
64
+ * `DOMAIN_SEPARATOR()` = 0x02fa7265e7c5d81118673727957699e4d68f74cd74b7db77da710fe8a2c7834f。
65
+ * ⇒ **"USDC" 是错的** (`name()` 是 ERC20 名, EIP-712 域用的是它, 不是符号)。
66
+ * 用错的 name 签出来的授权链上必 revert (token 侧 ecrecover 结果对不上 authorizer) ——
67
+ * 这不是"可能不兼容", 是**必失败**。买方 CLI 因此**不写死**: 见 `resolveTokenDomain()`。
68
+ */
69
+ export const USDC_EIP712_NAME_CANDIDATES = ['USD Coin', 'USDC', 'USDC.e', 'Bridged USDC (Base)'];
70
+ export const USDC_EIP712_VERSION_CANDIDATES = ['2', '1'];
71
+ /** DOMAIN_SEPARATOR() / name() / version() 的 4 字节 selector (只读探针用) */
72
+ export const SELECTOR_DOMAIN_SEPARATOR = '0x3644e515';
73
+ export const SELECTOR_NAME = '0x06fdde03';
74
+ export const SELECTOR_VERSION = '0x54fd4d50';
75
+ // ────────────────────────────────────────────────────────────── keccak-256 (纯 TS, 零依赖)
76
+ const MASK64 = (1n << 64n) - 1n;
77
+ /** Keccak-f[1600] 轮常数 (24 轮) */
78
+ const RC = [
79
+ 0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
80
+ 0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
81
+ 0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
82
+ 0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
83
+ 0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
84
+ 0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
85
+ ];
86
+ /** ρ 旋转偏移 r[x][y] (x = 列, y = 行) */
87
+ const RHO = [
88
+ [0, 36, 3, 41, 18],
89
+ [1, 44, 10, 45, 2],
90
+ [62, 6, 43, 15, 61],
91
+ [28, 55, 25, 21, 56],
92
+ [27, 20, 39, 8, 14],
93
+ ];
94
+ function rotl64(v, n) {
95
+ if (n === 0)
96
+ return v & MASK64;
97
+ const k = BigInt(n);
98
+ return ((v << k) | (v >> (64n - k))) & MASK64;
99
+ }
100
+ function keccakF(a) {
101
+ const c = new Array(5);
102
+ const d = new Array(5);
103
+ const b = new Array(25);
104
+ for (let round = 0; round < 24; round++) {
105
+ // θ
106
+ for (let x = 0; x < 5; x++)
107
+ c[x] = a[x] ^ a[x + 5] ^ a[x + 10] ^ a[x + 15] ^ a[x + 20];
108
+ for (let x = 0; x < 5; x++)
109
+ d[x] = c[(x + 4) % 5] ^ rotl64(c[(x + 1) % 5], 1);
110
+ for (let x = 0; x < 5; x++)
111
+ for (let y = 0; y < 5; y++)
112
+ a[x + 5 * y] ^= d[x];
113
+ // ρ + π: B[y][(2x+3y) mod 5] = rot(A[x][y], r[x][y])
114
+ for (let x = 0; x < 5; x++) {
115
+ for (let y = 0; y < 5; y++) {
116
+ b[y + 5 * ((2 * x + 3 * y) % 5)] = rotl64(a[x + 5 * y], RHO[x][y]);
117
+ }
118
+ }
119
+ // χ
120
+ for (let x = 0; x < 5; x++) {
121
+ for (let y = 0; y < 5; y++) {
122
+ a[x + 5 * y] = b[x + 5 * y] ^ ((~b[((x + 1) % 5) + 5 * y] & MASK64) & b[((x + 2) % 5) + 5 * y]);
123
+ }
124
+ }
125
+ // ι
126
+ a[0] ^= RC[round];
127
+ }
128
+ }
129
+ const KECCAK_RATE = 136; // 1088 bit = keccak-256 的 rate
130
+ /**
131
+ * Keccak-256 (以太坊用的那个, padding = 0x01 … 0x80; **不是** NIST SHA3-256 的 0x06)。
132
+ * 返回 32 字节。
133
+ */
134
+ export function keccak256(input) {
135
+ const msg = typeof input === 'string' ? Buffer.from(input, 'utf-8') : Buffer.from(input);
136
+ const a = new Array(25).fill(0n);
137
+ // 吸收: 每 136 字节一块
138
+ const padded = (() => {
139
+ const padLen = KECCAK_RATE - (msg.length % KECCAK_RATE);
140
+ const pad = Buffer.alloc(padLen);
141
+ pad[0] = 0x01;
142
+ pad[padLen - 1] |= 0x80;
143
+ return Buffer.concat([msg, pad]);
144
+ })();
145
+ for (let off = 0; off < padded.length; off += KECCAK_RATE) {
146
+ for (let i = 0; i < KECCAK_RATE / 8; i++) {
147
+ a[i] ^= padded.readBigUInt64LE(off + i * 8);
148
+ }
149
+ keccakF(a);
150
+ }
151
+ const out = Buffer.alloc(32);
152
+ for (let i = 0; i < 4; i++)
153
+ out.writeBigUInt64LE(a[i] & MASK64, i * 8);
154
+ return out;
155
+ }
156
+ /** keccak256 → 0x + 64 hex */
157
+ export function keccak256Hex(input) {
158
+ return `0x${Buffer.from(keccak256(input)).toString('hex')}`;
159
+ }
160
+ // ────────────────────────────────────────────────────────────── 约定 v1: 编 / 解 nonce
161
+ function hexToBytes(hex) {
162
+ const h = String(hex || '').replace(/^0x/, '');
163
+ return Buffer.from(h.length % 2 ? `0${h}` : h, 'hex');
164
+ }
165
+ function bytesToHex(b) {
166
+ return `0x${Buffer.from(b).toString('hex')}`;
167
+ }
168
+ function uint256be(n) {
169
+ const b = Buffer.alloc(32);
170
+ b.writeBigUInt64BE(BigInt(n) & MASK64, 24);
171
+ return b;
172
+ }
173
+ /**
174
+ * 订单哈希: keccak256( utf8(itemId) ‖ uint256be(orderSeq) ) 的前 24 字节。
175
+ * 卖方复算的就是这一串 —— 拿自己店里的 itemId + 从 nonce 里读出的 orderSeq 算一遍比。
176
+ */
177
+ export function orderItemHash24(itemId, orderSeq) {
178
+ const h = keccak256(Buffer.concat([Buffer.from(String(itemId), 'utf-8'), uint256be(orderSeq)]));
179
+ return bytesToHex(h.subarray(0, ORDER_HASH_BYTES));
180
+ }
181
+ /** 按约定 v1 编 nonce (32 字节 hex) */
182
+ export function computeOrderNonce(itemId, orderSeq = 0) {
183
+ const seq = BigInt(orderSeq);
184
+ if (seq < 0n || seq > 0xffffffffn)
185
+ throw new Error(`orderSeq 超出 uint32 范围: ${seq}`);
186
+ const tag = hexToBytes(ORDER_NONCE_TAG);
187
+ const seqBytes = Buffer.alloc(ORDER_SEQ_BYTES);
188
+ seqBytes.writeUInt32BE(Number(seq), 0);
189
+ const hashBytes = hexToBytes(orderItemHash24(itemId, seq));
190
+ if (hashBytes.length !== ORDER_HASH_BYTES)
191
+ throw new Error('内部错误: 哈希长度不为 24 字节');
192
+ return bytesToHex(Buffer.concat([tag, seqBytes, hashBytes]));
193
+ }
194
+ /** 解 nonce。**只负责解, 不判对错** —— 判"是不是本店 item"用 `matchOrderNonce`。 */
195
+ export function decodeOrderNonce(nonce) {
196
+ const raw = String(nonce || '').trim().toLowerCase();
197
+ const shell = { nonce: raw, shape: 'not-hex', tagPresent: false, tag: null, orderSeq: null, itemIdHash: null };
198
+ if (!/^0x[0-9a-f]{64}$/.test(raw)) {
199
+ return { ...shell, detail: `nonce 形状不对 (要 32 字节 hex): ${raw.slice(0, 42)}` };
200
+ }
201
+ const bytes = hexToBytes(raw);
202
+ if (bytes.length !== 32)
203
+ return { ...shell, shape: 'wrong-length', detail: `nonce 长度 ${bytes.length} 字节, 不是 32` };
204
+ const tagHex = bytesToHex(bytes.subarray(0, ORDER_TAG_BYTES));
205
+ if (tagHex !== ORDER_NONCE_TAG) {
206
+ return {
207
+ ...shell, shape: 'no-tag',
208
+ detail: `nonce 前 4 字节 ${tagHex} ≠ ${ORDER_NONCE_TAG} ("BOL1") —— 这不是按约定 v1 编的订单标识`,
209
+ };
210
+ }
211
+ const orderSeq = bytes.readUInt32BE(ORDER_TAG_BYTES);
212
+ const itemIdHash = bytesToHex(bytes.subarray(ORDER_TAG_BYTES + ORDER_SEQ_BYTES));
213
+ return {
214
+ nonce: raw, shape: 'ok', tagPresent: true, tag: ORDER_NONCE_TAG_ASCII, orderSeq, itemIdHash,
215
+ detail: `BOL1 订单标识: orderSeq=${orderSeq}, itemIdHash=${itemIdHash.slice(0, 18)}…`,
216
+ };
217
+ }
218
+ /** 用**本店真实 item** 复算哈希并比对。返回命中的 itemId 列表 (正常情况下 0 或 1 个)。 */
219
+ export function matchOrderNonce(nonce, itemIds) {
220
+ const d = typeof nonce === 'string' ? decodeOrderNonce(nonce) : nonce;
221
+ if (d.shape !== 'ok' || d.orderSeq === null)
222
+ return [];
223
+ const hits = [];
224
+ for (const id of itemIds) {
225
+ if (orderItemHash24(id, d.orderSeq) === d.itemIdHash)
226
+ hits.push(id);
227
+ }
228
+ return hits;
229
+ }
230
+ // ────────────────────────────────────────────────────────────── EIP-712 (纯 keccak, 零依赖)
231
+ /**
232
+ * EIP-712 域分隔符: keccak256( abi.encode(typehash, keccak256(name), keccak256(version), chainId, verifyingContract) )
233
+ * 用来跟链上 `DOMAIN_SEPARATOR()` 对比 —— **签名用的域必须与链上一致**, 一致了才敢签。
234
+ */
235
+ export function eip712DomainSeparator(d) {
236
+ const word = (b) => Buffer.from(b);
237
+ const nameHash = word(keccak256(Buffer.from(d.name, 'utf-8')));
238
+ const versionHash = word(keccak256(Buffer.from(d.version, 'utf-8')));
239
+ const chain = Buffer.alloc(32);
240
+ chain.writeBigUInt64BE(BigInt(d.chainId) & MASK64, 24);
241
+ const addr = Buffer.alloc(32);
242
+ Buffer.from(String(d.verifyingContract).replace(/^0x/, '').toLowerCase(), 'hex').copy(addr, 12);
243
+ const encoded = Buffer.concat([
244
+ word(keccak256(Buffer.from(EIP712_DOMAIN_TYPEHASH_STR, 'utf-8'))),
245
+ nameHash, versionHash, chain, addr,
246
+ ]);
247
+ return `0x${Buffer.from(keccak256(encoded)).toString('hex')}`;
248
+ }
249
+ /** keccak256(EIP712Domain(...)) —— 类型串哈希 (便于外部核对) */
250
+ export function eip712DomainTypehash() {
251
+ return keccak256Hex(EIP712_DOMAIN_TYPEHASH_STR);
252
+ }
253
+ /** keccak256(TransferWithAuthorization(...)) —— EIP-3009 结构类型哈希 */
254
+ export function eip3009Typehash() {
255
+ return keccak256Hex(EIP3009_TYPEHASH_STR);
256
+ }
257
+ function addrOf(topic) {
258
+ const h = String(topic || '').replace(/^0x/, '').toLowerCase();
259
+ return h.length >= 40 ? `0x${h.slice(-40)}` : '';
260
+ }
261
+ /**
262
+ * ★ 唯一入口 (纯函数): 给一笔交易的**日志数组**, 返回订单标识。
263
+ *
264
+ * 判据顺序 (缺 AuthorizationUsed 就是降级, 绝不假装自证):
265
+ * ① 在 `asset` 合约的日志里找 `AuthorizationUsed`, 取 (authorizer, nonce)
266
+ * ② nonce 解出来不是 "BOL1" → `not-bol1` (有授权事实, 但没带订单标识)
267
+ * ③ 是 "BOL1" → 拿本店 itemId + 解出的 orderSeq 复算哈希: 对上 → `self-attested`; 对不上 → `item-mismatch`
268
+ * ④ 一条都没有 → `no-authorization-used` (普通转账, 链上没有订单身份)
269
+ *
270
+ * 只读: 不看余额、不写盘、不发交易。
271
+ */
272
+ export function orderIdentityFromLogs(logs, opts) {
273
+ const itemIds = (opts.itemIds || []).map(String).filter(Boolean);
274
+ const base = {
275
+ protocol: ORDER_IDENTITY_PROTOCOL,
276
+ nonce: null, tagPresent: false, tag: null,
277
+ orderSeq: null, itemIdHash: null,
278
+ payer: (opts.payerHint ? String(opts.payerHint).toLowerCase() : null),
279
+ matchedItemIds: [], checkedItemIds: itemIds,
280
+ };
281
+ const wantAsset = opts.asset ? String(opts.asset).toLowerCase() : '';
282
+ const list = Array.isArray(logs) ? logs : [];
283
+ let found = null;
284
+ for (const l of list) {
285
+ if (wantAsset && String(l?.address || '').toLowerCase() !== wantAsset)
286
+ continue;
287
+ const topics = Array.isArray(l?.topics) ? l.topics : [];
288
+ // 非索引参数无 ⇒ authorizer 在 topics[1], nonce 在 topics[2]
289
+ if (String(topics[0] || '').toLowerCase() !== EIP3009_AUTHORIZATION_USED_TOPIC0)
290
+ continue;
291
+ if (topics.length < 3)
292
+ continue;
293
+ found = { payer: addrOf(topics[1]), nonce: String(topics[2] || '').toLowerCase() };
294
+ break;
295
+ }
296
+ if (!found) {
297
+ return {
298
+ ...base, mode: 'no-authorization-used', selfAttested: false, degraded: true,
299
+ detail: '直转(无订单标识): 这笔交易的日志里没有 EIP-3009 AuthorizationUsed 事件 ⇒ 链上只有「谁付·多少·给谁」, 读不出买的是哪件东西',
300
+ };
301
+ }
302
+ const decoded = decodeOrderNonce(found.nonce);
303
+ // 形状对但没有 BOL1 标签 = 别的客户端/随机 nonce (有授权事实, 没有订单标识) —— 这才是"降级", 不是脏数据
304
+ if (decoded.shape === 'no-tag') {
305
+ return {
306
+ ...base, mode: 'not-bol1', selfAttested: false, degraded: true,
307
+ nonce: decoded.nonce, payer: found.payer || base.payer,
308
+ detail: `有 AuthorizationUsed (nonce=${decoded.nonce.slice(0, 18)}…) 但这个 nonce 不是约定 v1 编的 (前 4 字节不是 ${ORDER_NONCE_TAG_ASCII}/${ORDER_NONCE_TAG}) ⇒ 链上读不出订单身份, 如实降级为「直转(无订单标识)」`,
309
+ };
310
+ }
311
+ if (decoded.shape !== 'ok') {
312
+ return {
313
+ ...base, mode: 'malformed-nonce', selfAttested: false, degraded: true,
314
+ nonce: decoded.nonce, payer: found.payer || base.payer,
315
+ detail: `有 AuthorizationUsed 但 nonce 不可解: ${decoded.detail}`,
316
+ };
317
+ }
318
+ const matched = matchOrderNonce(decoded, itemIds);
319
+ const common = {
320
+ ...base, nonce: decoded.nonce, tagPresent: true, tag: decoded.tag,
321
+ orderSeq: decoded.orderSeq, itemIdHash: decoded.itemIdHash,
322
+ payer: found.payer || base.payer, matchedItemIds: matched,
323
+ };
324
+ if (matched.length === 0) {
325
+ return {
326
+ ...common, mode: 'item-mismatch', selfAttested: false, degraded: true,
327
+ detail: `nonce 带了 BOL1 订单标识, 但 keccak256(itemId‖orderSeq=${decoded.orderSeq}) 与本店 item (${itemIds.join(', ') || '无'}) 都对不上 ⇒ 如实降级为「直转(无订单标识)」, 不假装自证`,
328
+ };
329
+ }
330
+ return {
331
+ ...common, mode: 'self-attested', selfAttested: true, degraded: false,
332
+ detail: `订单自证成功: nonce = BOL1 ‖ orderSeq=${decoded.orderSeq} ‖ keccak256(itemId‖seq)[0..23] —— 与本店 item「${matched.join(', ')}」复算哈希一致`,
333
+ };
334
+ }
335
+ // ────────────────────────────────────────────────────────────── 只读链路: 从 txHash 自读 (给索引线用)
336
+ const TXHASH_RE = /^0x[0-9a-fA-F]{64}$/;
337
+ async function rpcCall(url, method, params, fetchImpl, timeoutMs) {
338
+ const ac = new AbortController();
339
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
340
+ try {
341
+ const res = await fetchImpl(url, {
342
+ method: 'POST', headers: { 'content-type': 'application/json' },
343
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: ac.signal,
344
+ });
345
+ if (!res.ok)
346
+ throw new Error(`HTTP ${res.status}`);
347
+ const j = await res.json();
348
+ if (j?.error)
349
+ throw new Error(`RPC error: ${String(j?.error?.message || 'unknown').slice(0, 120)}`);
350
+ return j?.result;
351
+ }
352
+ finally {
353
+ clearTimeout(timer);
354
+ }
355
+ }
356
+ /**
357
+ * ★ 只读: 给定一笔交易, 从**同一条交易的日志**里取 nonce → 返回 `orderIdentity`。
358
+ * 给公示/索引那条线用 (它自己按 txHash 扫到一笔 USDC 转账后, 调这个函数判"有没有订单身份")。
359
+ *
360
+ * 不做任何核验/交付/写盘 —— 索引线拿到的只是**链上事实的投影**, 不是"卖方承认过"。
361
+ * (两者含义不同: 自证成功 = 这笔付款的 nonce 声明了本店某件 item; 卖方是否真的交付过
362
+ * 要另看卖方台账 —— `GET /api/x402/seller/summary`。)
363
+ */
364
+ export async function readOrderIdentityFromTx(input) {
365
+ const txHash = String(input.txHash || '').trim();
366
+ if (!TXHASH_RE.test(txHash))
367
+ return { ok: false, txHash, error: `txHash 形状不对 (要 0x + 64 hex)` };
368
+ const f = input.fetchImpl ?? fetch;
369
+ let host = 'invalid-url';
370
+ try {
371
+ host = new URL(input.rpcUrl).host;
372
+ }
373
+ catch { /* 保持 invalid-url */ }
374
+ try {
375
+ const receipt = await rpcCall(input.rpcUrl, 'eth_getTransactionReceipt', [txHash], f, input.timeoutMs ?? 12_000);
376
+ if (!receipt)
377
+ return { ok: false, txHash, rpc: host, error: '链上还没有这个 txHash 的 receipt (未打包/不存在)' };
378
+ const logs = Array.isArray(receipt.logs) ? receipt.logs : [];
379
+ const orderIdentity = orderIdentityFromLogs(logs, { itemIds: input.itemIds, asset: input.asset });
380
+ return {
381
+ ok: true, txHash, rpc: host,
382
+ blockNumber: Number(BigInt(receipt.blockNumber ?? '0x0')),
383
+ orderIdentity,
384
+ };
385
+ }
386
+ catch (e) {
387
+ return { ok: false, txHash, rpc: host, error: `RPC 读不到: ${String(e?.message || e).slice(0, 160)}` };
388
+ }
389
+ }
390
+ // ────────────────────────────────────────────────────────────── 其它小工具 (给买方 CLI 共用)
391
+ /** 订单标识的短写 (日志/回显用) */
392
+ export function shortOrderIdentity(o) {
393
+ if (!o)
394
+ return 'none';
395
+ if (o.mode === 'self-attested')
396
+ return `BOL1/seq=${o.orderSeq}/item=${o.matchedItemIds[0] || '?'}`;
397
+ return `${o.mode}${o.tagPresent ? `/seq=${o.orderSeq}` : ''}`;
398
+ }
399
+ /** 稳定摘要 (台账/回执里只放摘要, 不放原文也行; 这里给的是 sha256 短写) */
400
+ export function orderIdentityDigest(o) {
401
+ if (!o)
402
+ return 'none';
403
+ const s = JSON.stringify({
404
+ mode: o.mode, nonce: o.nonce, orderSeq: o.orderSeq, itemIdHash: o.itemIdHash,
405
+ matched: [...o.matchedItemIds].sort(), payer: o.payer,
406
+ });
407
+ return `sha256:${crypto.createHash('sha256').update(s).digest('hex')}`;
408
+ }
@@ -121,6 +121,23 @@ export function toAtomicAmount(amount, currency) {
121
121
  const int = i.replace(/[^0-9]/g, '') || '0';
122
122
  return `${int}${frac}`.replace(/^0+(?=\d)/, '');
123
123
  }
124
+ /**
125
+ * 原子单位 → 人类可读 (**toAtomicAmount 的逆**; 纯整数运算, 不经过浮点)。
126
+ * 10000 + USDC(6) → "0.01" · 1000000 + USDC → "1" · 1 + ETH(18) → "0.000000000000000001"。
127
+ * 只用于**展示**; 台账/接口里一律以原子串为准 (展示值不参与任何判定)。
128
+ * 非纯数字输入 → 原样返回 (宁可显示原子串, 也不编一个折算过的数)。
129
+ */
130
+ export function fromAtomicAmount(atomic, currency) {
131
+ const decimals = currency === 'USDC' ? 6 : 18;
132
+ const s = String(atomic ?? '').trim();
133
+ if (!/^\d+$/.test(s))
134
+ return s;
135
+ const digits = s.replace(/^0+(?=\d)/, '');
136
+ const padded = digits.padStart(decimals + 1, '0');
137
+ const int = padded.slice(0, padded.length - decimals);
138
+ const frac = decimals > 0 ? padded.slice(padded.length - decimals).replace(/0+$/, '') : '';
139
+ return frac ? `${int}.${frac}` : int;
140
+ }
124
141
  /** 生成 x402 v2 规范的 402 响应体 */
125
142
  export function buildPaymentRequired(item, url, error) {
126
143
  const network = item.price.network;