@bolloon/bolloon-agent 0.5.2 → 0.5.4
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/README.md +4 -4
- package/dist/agents/chain/transfer-classify.js +99 -0
- package/dist/agents/chain/transfer-index.js +354 -0
- package/dist/agents/identity/address-binding.js +613 -0
- package/dist/agents/network-pulse.js +706 -27
- package/dist/agents/pi-sdk-tools.js +67 -15
- package/dist/agents/pi-sdk.js +7 -1
- package/dist/agents/session-store.js +144 -3
- package/dist/agents/wallet-tools.js +63 -0
- package/dist/agents/x402/direct-payment.js +689 -0
- package/dist/agents/x402/order-identity.js +408 -0
- package/dist/agents/x402/paid-info-store.js +17 -0
- package/dist/agents/x402/seller-signing.js +614 -0
- package/dist/agents/x402/seller-summary.js +398 -0
- package/dist/cli/identity-command.js +327 -2
- package/dist/cli/ink-app.js +79 -27
- package/dist/cli/input-history.js +119 -0
- package/dist/cli/loading-tui.js +66 -6
- package/dist/cli/log-gate.js +39 -1
- package/dist/cli/protocol-envelope.js +4 -0
- package/dist/cli/session-summary.js +129 -0
- package/dist/cli/session-tree.js +141 -0
- package/dist/cli/status-segments.js +70 -0
- package/dist/cli/stores.js +16 -0
- package/dist/cli/update-commands.js +41 -25
- package/dist/cli/wallet-store.js +159 -0
- package/dist/cli/wallet-table.js +46 -0
- package/dist/cli/x402-buyer-command.js +463 -0
- package/dist/cli/x402-seller-command.js +289 -0
- package/dist/cli-entry.js +43 -5
- package/dist/electron-build/agents/chain/transfer-classify.js +106 -0
- package/dist/electron-build/agents/chain/transfer-classify.js.map +1 -0
- package/dist/electron-build/agents/chain/transfer-index.js +400 -0
- package/dist/electron-build/agents/chain/transfer-index.js.map +1 -0
- package/dist/electron-build/agents/identity/address-binding.js +672 -0
- package/dist/electron-build/agents/identity/address-binding.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +714 -28
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +68 -15
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +7 -1
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/session-store.js +144 -3
- package/dist/electron-build/agents/session-store.js.map +1 -1
- package/dist/electron-build/agents/wallet-tools.js +102 -0
- package/dist/electron-build/agents/wallet-tools.js.map +1 -0
- package/dist/electron-build/agents/x402/direct-payment.js +744 -0
- package/dist/electron-build/agents/x402/direct-payment.js.map +1 -0
- package/dist/electron-build/agents/x402/order-identity.js +458 -0
- package/dist/electron-build/agents/x402/order-identity.js.map +1 -0
- package/dist/electron-build/agents/x402/paid-info-protocol.js.map +1 -1
- package/dist/electron-build/agents/x402/paid-info-store.js +18 -0
- package/dist/electron-build/agents/x402/paid-info-store.js.map +1 -1
- package/dist/electron-build/agents/x402/seller-signing.js +682 -0
- package/dist/electron-build/agents/x402/seller-signing.js.map +1 -0
- package/dist/electron-build/cli/wallet-store.js +200 -0
- package/dist/electron-build/cli/wallet-store.js.map +1 -0
- package/dist/electron-build/llm/pi-ai.js +25 -0
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/utils/auto-update.js +2 -2
- package/dist/electron-build/utils/auto-update.js.map +1 -1
- package/dist/electron-build/utils/runtime-bootstrap.js +1 -1
- package/dist/electron-build/utils/runtime-bootstrap.js.map +1 -1
- package/dist/electron-build/utils/update-manager.js +3 -3
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/version-identity.js +1 -1
- package/dist/electron-build/utils/version-identity.js.map +1 -1
- package/dist/electron-build/web/routes-x402-info.js +28 -1
- package/dist/electron-build/web/routes-x402-info.js.map +1 -1
- package/dist/index.js +829 -104
- package/dist/llm/pi-ai.js +23 -0
- package/dist/utils/auto-update.js +2 -2
- package/dist/utils/runtime-bootstrap.js +1 -1
- package/dist/utils/update-health.js +2 -2
- package/dist/utils/update-manager.js +3 -3
- package/dist/utils/version-identity.js +1 -1
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/routes-x402-info.js +28 -1
- package/dist/web/sw.js +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seller-summary.ts — 卖方端点的**公开只读汇总** (bolloon-x402-seller-summary/1)
|
|
3
|
+
*
|
|
4
|
+
* 一句话: 把「真成交」变成网页上看得见、且**能在链上独立核验**的数字。
|
|
5
|
+
*
|
|
6
|
+
* 与卖方签名队列 (bolloon-x402-seller/1) 的分工:
|
|
7
|
+
* · 那条是**私有**通道 (HMAC 认证, 带付款凭据原文) —— 只给卖方本机用;
|
|
8
|
+
* · 这条是**公开只读**聚合 —— 只出**链上可核验的事实**: 每笔带 tx_hash + 块号 +
|
|
9
|
+
* 区块浏览器链接; 绝不带取件 token / 凭据回执 / 任何 EOA 地址。
|
|
10
|
+
*
|
|
11
|
+
* 数据源 (每次请求**实时读盘**, 不缓存; 读不到 → 空数组 + 0 且仍 200, 不 500):
|
|
12
|
+
* ① 直付台账 <home>/.bolloon/x402-direct-txs.json
|
|
13
|
+
* —— 每一条 = 一次**链上核验通过**的买方直付 (receipt.status=1 · 日志里有 USDC
|
|
14
|
+
* Transfer→payTo · value >= accepts.amount · **≥2 条不同 RPC 结论一致**),
|
|
15
|
+
* 由 `direct-payment.ts` 落盘。行里带 tx_hash / block_number ⇒ 可在区块浏览器复核。
|
|
16
|
+
* ② 交付队列 <home>/.bolloon/x402-seller-pending/*.json
|
|
17
|
+
* —— 只用来数 delivered (已签) / awaiting_signature (待签), **不读凭据原文**。
|
|
18
|
+
*
|
|
19
|
+
* 诚实口径 (不许美化):
|
|
20
|
+
* · 计数口径 = 「**本端点收款地址**上、经链上核验的直付成交」; 它**不是**全网站点销量,
|
|
21
|
+
* 也**不是**合约托管/结算总量 (托管结算在链上合约里, 不在这条口径内)。
|
|
22
|
+
* · 每条都附 tx_hash + 块号 ⇒ 买方/任何第三方可**自己**上链复核, 不必信本端点。
|
|
23
|
+
* · 核验依赖公共 RPC (≥2 条一致才算事实) ⇒ 出现矛盾时那笔**不会被记进台账**。
|
|
24
|
+
*
|
|
25
|
+
* 隐私红线 (硬; 见 `auditSellerSummaryLeaks`):
|
|
26
|
+
* 绝不返回 取件 token · 付款凭据原文/回执哈希 · 任何密钥/DID · **任何 EOA 或合约地址**
|
|
27
|
+
* (付款人 `from` / 收款人 `to` / 资产合约 `asset` / item 的 `payTo` 一律不出)。
|
|
28
|
+
* 允许: `tx_hash`(0x+64) 与 `explorer_tx`(区块浏览器交易链接) —— 公开链上事实。
|
|
29
|
+
*/
|
|
30
|
+
import * as fs from 'fs/promises';
|
|
31
|
+
import * as os from 'os';
|
|
32
|
+
import * as path from 'path';
|
|
33
|
+
import { explorerTxUrl, isExplorerUrl } from '../chain/explorer.js';
|
|
34
|
+
import { fromAtomicAmount } from './paid-info-store.js';
|
|
35
|
+
// ────────────────────────────────────────────────────────────── 常量
|
|
36
|
+
export const SELLER_SUMMARY_PROTOCOL = 'bolloon-x402-seller-summary/1';
|
|
37
|
+
/** 队列端点前缀之上; nginx 只放行 `^~ /api/x402/` ⇒ 只能长在这下面 */
|
|
38
|
+
export const SELLER_SUMMARY_PATH = '/api/x402/seller/summary';
|
|
39
|
+
/** 直付台账文件名 (与 direct-payment.ts 的 DIRECT_TX_LEDGER_FILE 同一份事实) */
|
|
40
|
+
export const DIRECT_TX_LEDGER_FILENAME = 'x402-direct-txs.json';
|
|
41
|
+
/** 交付队列目录名 (与 seller-signing.ts 的 SELLER_PENDING_DIRNAME 同一份事实) */
|
|
42
|
+
export const SELLER_PENDING_DIRNAME = 'x402-seller-pending';
|
|
43
|
+
// ────────────────────────────────────────────────────────────── 隐私守卫
|
|
44
|
+
/**
|
|
45
|
+
* 本模块**允许**输出的键名白名单 (精确匹配)。
|
|
46
|
+
* 白名单先判 —— 否则 `awaiting_signature` 会被 `signature` 那条禁用词误伤。
|
|
47
|
+
* 白名单只免掉「键名检查」, 值形态检查照旧全过 (地址/裸哈希越界一样拦)。
|
|
48
|
+
*/
|
|
49
|
+
const ALLOWED_KEYS = new Set([
|
|
50
|
+
'protocol', 'ok', 'scope', 'title', 'ledger', 'verifiable', 'not',
|
|
51
|
+
'generated_at', 'totals', 'chain_verified_sales', 'delivered', 'delivered_unverifiable',
|
|
52
|
+
'awaiting_signature', 'pending_total', 'delivered_tx_hashes', 'txs', 'total_atomic',
|
|
53
|
+
'revenue', 'by_item', 'item_id', 'itemId', 'sales', 'amount', 'amount_atomic', 'amount_display', 'currency',
|
|
54
|
+
'network', 'chain_id', 'block_number', 'tx_hash', 'explorer_tx', 'latest', 'privacy_blocked',
|
|
55
|
+
'settled_at', 'settledAt', 'zh', 'en',
|
|
56
|
+
]);
|
|
57
|
+
/** `txs` 映射里每一行允许出现的字段 (消费方按 txHash 取这三项做交叉核) */
|
|
58
|
+
const TXS_ROW_KEYS = new Set(['itemId', 'amount', 'settledAt']);
|
|
59
|
+
/** 禁用的键名 (命中即剔除): 凭据 / token / 密钥 / DID / 任何地址类键 */
|
|
60
|
+
const FORBIDDEN_KEY_RE = /(receipt|token|payer|payto|pay_to|private|secret|seed|mnemonic|signature|wallet|address|did|^from$|^to$|^asset$|^tx$)/i;
|
|
61
|
+
/**
|
|
62
|
+
* EOA / 合约地址形状 (0x + **正好** 40 hex): 一律不许出现 —— 白名单键也没有例外。
|
|
63
|
+
* 用负向回顾/前瞻把它与 0x+64 的交易哈希区分开 (否则 64 位哈希的前 40 位会被误判成地址)。
|
|
64
|
+
*/
|
|
65
|
+
const ADDRESSISH_RE = /0x[0-9a-fA-F]{40}(?![0-9a-fA-F])/;
|
|
66
|
+
/** 裸交易哈希形状 (0x + 64 hex): **只允许**在 tx_hash 键下 */
|
|
67
|
+
const BARE_TXHASH_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
68
|
+
/** 任何 0x+64 hex 片段: 只允许出现在 tx_hash / explorer_tx 键下 */
|
|
69
|
+
const ANY_TXHASH_RE = /0x[0-9a-fA-F]{64}/;
|
|
70
|
+
/**
|
|
71
|
+
* 汇总自检: 返回违反隐私红线的路径清单 (空 = 通过)。
|
|
72
|
+
* 判据是「**键名 + 值形态**」双重: 光靠键名挡不住调用方把地址塞进自造字段。
|
|
73
|
+
*/
|
|
74
|
+
export function auditSellerSummaryLeaks(obj, at = '$') {
|
|
75
|
+
const issues = [];
|
|
76
|
+
if (obj === null || obj === undefined)
|
|
77
|
+
return issues;
|
|
78
|
+
if (typeof obj === 'string') {
|
|
79
|
+
// 顶层裸字符串: 地址形态 / 裸哈希都算越界 (白名单只在**键**层面成立)
|
|
80
|
+
if (ADDRESSISH_RE.test(obj))
|
|
81
|
+
issues.push(`${at}: 出现 0x+40 地址形态`);
|
|
82
|
+
else if (BARE_TXHASH_RE.test(obj))
|
|
83
|
+
issues.push(`${at}: 裸 0x+64 哈希出现在非白名单键下`);
|
|
84
|
+
return issues;
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(obj)) {
|
|
87
|
+
obj.forEach((v, i) => issues.push(...auditSellerSummaryLeaks(v, `${at}[${i}]`)));
|
|
88
|
+
return issues;
|
|
89
|
+
}
|
|
90
|
+
if (typeof obj !== 'object')
|
|
91
|
+
return issues;
|
|
92
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
93
|
+
const key = `${at}.${k}`;
|
|
94
|
+
const allowedKey = ALLOWED_KEYS.has(k);
|
|
95
|
+
if (!allowedKey && FORBIDDEN_KEY_RE.test(k)) {
|
|
96
|
+
issues.push(`${key}: 键名命中禁用词`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// 白名单第二处: 已交付 txHash 列表 —— 只允许裸交易哈希 (0x+64), 不给地址/凭据
|
|
100
|
+
if (k === 'delivered_tx_hashes') {
|
|
101
|
+
if (!Array.isArray(v))
|
|
102
|
+
issues.push(`${key}: 必须是字符串数组`);
|
|
103
|
+
else
|
|
104
|
+
v.forEach((h, i) => {
|
|
105
|
+
if (typeof h !== 'string' || !BARE_TXHASH_RE.test(h))
|
|
106
|
+
issues.push(`${key}[${i}]: 只允许 0x+64 交易哈希`);
|
|
107
|
+
});
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// 白名单第三处: txHash → 行 的映射。**键本身是哈希**, 所以键形状要单独验;
|
|
111
|
+
// 行内只允许消费方约定的三个字段 (多一个字段 = 可能夹带地址/凭据)。
|
|
112
|
+
if (k === 'txs') {
|
|
113
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) {
|
|
114
|
+
issues.push(`${key}: 必须是 {txHash: 行} 映射`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
for (const [th, row] of Object.entries(v)) {
|
|
118
|
+
const short = `${th.slice(0, 12)}…`;
|
|
119
|
+
if (!BARE_TXHASH_RE.test(th)) {
|
|
120
|
+
issues.push(`${key}.${short}: 键必须是 0x+64 交易哈希`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!row || typeof row !== 'object') {
|
|
124
|
+
issues.push(`${key}.${short}: 行必须是对象`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
for (const f of Object.keys(row)) {
|
|
128
|
+
if (!TXS_ROW_KEYS.has(f))
|
|
129
|
+
issues.push(`${key}.${short}.${f}: 字段不在白名单 (只许 itemId/amount/settledAt)`);
|
|
130
|
+
}
|
|
131
|
+
issues.push(...auditSellerSummaryLeaks(row, `${key}.${short}`));
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (typeof v === 'string') {
|
|
136
|
+
if (ADDRESSISH_RE.test(v))
|
|
137
|
+
issues.push(`${key}: 值出现 0x+40 地址形态`);
|
|
138
|
+
if (BARE_TXHASH_RE.test(v) && k !== 'tx_hash')
|
|
139
|
+
issues.push(`${key}: 裸 0x+64 哈希只允许在 tx_hash 键下`);
|
|
140
|
+
if (ANY_TXHASH_RE.test(v) && k !== 'tx_hash' && k !== 'explorer_tx')
|
|
141
|
+
issues.push(`${key}: 0x+64 只允许在 tx_hash / explorer_tx 键下`);
|
|
142
|
+
if (k === 'explorer_tx' && !isExplorerUrl(v))
|
|
143
|
+
issues.push(`${key}: explorer_tx 形状不对 (只认 /tx/0x64hex)`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
issues.push(...auditSellerSummaryLeaks(v, key));
|
|
147
|
+
}
|
|
148
|
+
return issues;
|
|
149
|
+
}
|
|
150
|
+
// ────────────────────────────────────────────────────────────── 读盘 (实时, 不缓存)
|
|
151
|
+
function ledgerPath(home) {
|
|
152
|
+
return path.join(home, '.bolloon', DIRECT_TX_LEDGER_FILENAME);
|
|
153
|
+
}
|
|
154
|
+
function pendingDir(home) {
|
|
155
|
+
return path.join(home, '.bolloon', SELLER_PENDING_DIRNAME);
|
|
156
|
+
}
|
|
157
|
+
function itemPath(itemId, home) {
|
|
158
|
+
const id = String(itemId).replace(/[^A-Za-z0-9._-]/g, '_');
|
|
159
|
+
return path.join(home, '.bolloon', 'x402-info', `${id}.json`);
|
|
160
|
+
}
|
|
161
|
+
/** 读直付台账 (坏文件 / 不存在 → 空; 绝不抛) */
|
|
162
|
+
async function readLedgerTxs(home) {
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(await fs.readFile(ledgerPath(home), 'utf-8'));
|
|
165
|
+
const txs = parsed?.txs;
|
|
166
|
+
if (!txs || typeof txs !== 'object')
|
|
167
|
+
return [];
|
|
168
|
+
return Object.values(txs).filter((r) => !!r && typeof r === 'object');
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** 读交付队列的**计数与已交付 txHash**(只取状态/时间/链上哈希; 凭据原文一律不读出来) */
|
|
175
|
+
async function readQueueCounts(home) {
|
|
176
|
+
const dir = pendingDir(home);
|
|
177
|
+
let files = [];
|
|
178
|
+
try {
|
|
179
|
+
files = await fs.readdir(dir);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return { delivered: 0, awaiting: 0, total: 0, deliveredTxHashes: [], deliveredUnverifiable: 0 };
|
|
183
|
+
}
|
|
184
|
+
let delivered = 0;
|
|
185
|
+
let awaiting = 0;
|
|
186
|
+
let total = 0;
|
|
187
|
+
let unverifiable = 0;
|
|
188
|
+
const txHashes = [];
|
|
189
|
+
for (const f of files) {
|
|
190
|
+
if (!f.endsWith('.json'))
|
|
191
|
+
continue; // 目录里的 .nonces.json 不算待办
|
|
192
|
+
try {
|
|
193
|
+
const rec = JSON.parse(await fs.readFile(path.join(dir, f), 'utf-8'));
|
|
194
|
+
if (!rec?.pendingId)
|
|
195
|
+
continue;
|
|
196
|
+
total += 1;
|
|
197
|
+
const expired = rec.status === 'awaiting_signature' && Date.parse(rec.expiresAt) < Date.now();
|
|
198
|
+
if (rec.status === 'signed') {
|
|
199
|
+
delivered += 1;
|
|
200
|
+
// 已交付 → 交出它对应的链上 txHash (供统一索引区判定「其中经 x402 流程」)
|
|
201
|
+
const tx = String(rec?.payment?.txHash || '').toLowerCase();
|
|
202
|
+
if (BARE_TXHASH_RE.test(tx))
|
|
203
|
+
txHashes.push(tx);
|
|
204
|
+
else
|
|
205
|
+
unverifiable += 1; // 没有可核验哈希的交付: 计数如实留痕, 不编一个哈希
|
|
206
|
+
}
|
|
207
|
+
else if (rec.status === 'awaiting_signature' && !expired)
|
|
208
|
+
awaiting += 1;
|
|
209
|
+
}
|
|
210
|
+
catch { /* 坏文件跳过 (不因一个坏文件看不到全部) */ }
|
|
211
|
+
}
|
|
212
|
+
return { delivered, awaiting, total, deliveredTxHashes: [...new Set(txHashes)], deliveredUnverifiable: unverifiable };
|
|
213
|
+
}
|
|
214
|
+
/** item 的币种/网络 (取自上架条目本身); 读不到 → 交给调用方按资产地址兜底 */
|
|
215
|
+
async function readItemMeta(itemId, home) {
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(await fs.readFile(itemPath(itemId, home), 'utf-8'));
|
|
218
|
+
const price = parsed?.item?.price;
|
|
219
|
+
if (!price)
|
|
220
|
+
return null;
|
|
221
|
+
return {
|
|
222
|
+
...(price.currency ? { currency: String(price.currency) } : {}),
|
|
223
|
+
...(price.network ? { network: String(price.network) } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// ────────────────────────────────────────────────────────────── 聚合
|
|
231
|
+
function fmt(atomic, currency) {
|
|
232
|
+
return `${fromAtomicAmount(atomic, currency)} ${currency}`;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* 构建公开只读汇总。**每笔都来自链上核验过的台账**; 读不到任何源就回空数组 + 0 (仍 ok)。
|
|
236
|
+
* 不缓存: 每次调用真读盘 (改价/新成交不必重启服务)。
|
|
237
|
+
*/
|
|
238
|
+
export async function buildSellerSummary(home = os.homedir(), now = Date.now()) {
|
|
239
|
+
const [rawTxs, queue] = await Promise.all([readLedgerTxs(home), readQueueCounts(home)]);
|
|
240
|
+
// item → 币种/网络 (每个 item 只读一次)
|
|
241
|
+
const metaCache = new Map();
|
|
242
|
+
const rows = [];
|
|
243
|
+
let blocked = 0;
|
|
244
|
+
for (const r of rawTxs) {
|
|
245
|
+
const txHash = String(r.txHash || '').toLowerCase();
|
|
246
|
+
const itemId = String(r.itemId || '');
|
|
247
|
+
if (!BARE_TXHASH_RE.test(txHash) || !itemId) {
|
|
248
|
+
blocked += 1;
|
|
249
|
+
continue;
|
|
250
|
+
} // 形状不对 = 不进公开面
|
|
251
|
+
const chainId = Number(r.chainId);
|
|
252
|
+
// item 元数据 (币种/网络) 每个 item 只读一次; 读不到就只信台账自己的字段
|
|
253
|
+
if (!metaCache.has(itemId))
|
|
254
|
+
metaCache.set(itemId, await readItemMeta(itemId, home));
|
|
255
|
+
const meta = metaCache.get(itemId);
|
|
256
|
+
const network = String(r.network || meta?.network || '');
|
|
257
|
+
const currency = meta?.currency || 'USDC'; // x402 付费信息默认 USDC
|
|
258
|
+
const amountAtomic = String(r.amount || '0');
|
|
259
|
+
const explorer = explorerTxUrl(chainId, txHash);
|
|
260
|
+
rows.push({
|
|
261
|
+
settled_at: String(r.settledAt || ''),
|
|
262
|
+
item_id: itemId,
|
|
263
|
+
amount_atomic: amountAtomic,
|
|
264
|
+
amount_display: fmt(amountAtomic, currency),
|
|
265
|
+
currency,
|
|
266
|
+
network,
|
|
267
|
+
chain_id: Number.isFinite(chainId) ? chainId : 0,
|
|
268
|
+
block_number: Number(r.blockNumber) || 0,
|
|
269
|
+
tx_hash: txHash,
|
|
270
|
+
...(explorer ? { explorer_tx: explorer } : {}),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
rows.sort((a, b) => String(b.settled_at).localeCompare(String(a.settled_at)));
|
|
274
|
+
// 按 item 汇总
|
|
275
|
+
const byItem = new Map();
|
|
276
|
+
for (const s of rows) {
|
|
277
|
+
const cur = byItem.get(s.item_id) || {
|
|
278
|
+
item_id: s.item_id, sales: 0, amount_atomic: '0',
|
|
279
|
+
amount_display: fmt('0', s.currency), currency: s.currency, network: s.network,
|
|
280
|
+
};
|
|
281
|
+
const summed = (BigInt(cur.amount_atomic) + BigInt(/^\d+$/.test(s.amount_atomic) ? s.amount_atomic : '0')).toString();
|
|
282
|
+
cur.sales += 1;
|
|
283
|
+
cur.amount_atomic = summed;
|
|
284
|
+
cur.amount_display = fmt(summed, cur.currency);
|
|
285
|
+
byItem.set(s.item_id, cur);
|
|
286
|
+
}
|
|
287
|
+
const revenueAtomic = [...byItem.values()]
|
|
288
|
+
.reduce((n, x) => n + BigInt(x.amount_atomic), 0n).toString();
|
|
289
|
+
const revenueCurrency = [...byItem.values()][0]?.currency || 'USDC';
|
|
290
|
+
const summary = {
|
|
291
|
+
protocol: SELLER_SUMMARY_PROTOCOL,
|
|
292
|
+
ok: true,
|
|
293
|
+
scope: {
|
|
294
|
+
title: {
|
|
295
|
+
zh: '卖方本机台账 (链下) — 已交付笔数与对应链上 txHash',
|
|
296
|
+
en: 'Seller-side ledger (off-chain) — delivered count and the matching on-chain txHashes',
|
|
297
|
+
},
|
|
298
|
+
ledger: {
|
|
299
|
+
zh: '这是**卖方本机**的交付台账 (链下记录), 不是链上索引; 每笔对应一笔**已在链上核验过**的直付交易 (收据 status=1 · 日志里有 USDC Transfer→收款地址 · 金额≥报价 · ≥2 条独立 RPC 结论一致), 行内带 tx_hash 与块号。',
|
|
300
|
+
en: 'This is the seller\'s own local delivery ledger (an off-chain record), not a chain index. Every row corresponds to a direct payment that already passed on-chain verification (receipt status=1, a USDC Transfer log to the pay-to address, amount >= the quote, and >=2 independent RPCs agreeing) and carries its tx_hash and block number.',
|
|
301
|
+
},
|
|
302
|
+
verifiable: {
|
|
303
|
+
zh: '成交事实以链上为准: 用每笔的 tx_hash 与块号在区块浏览器上独立复核, 不必信本台账。',
|
|
304
|
+
en: 'The on-chain record is the authority: take each row\'s tx_hash and block number to a block explorer and verify it yourself; you do not need to trust this ledger.',
|
|
305
|
+
},
|
|
306
|
+
not: {
|
|
307
|
+
zh: '不是全网站点销量, 也不是合约托管/结算总量 (托管结算发生在链上合约, 不在这条口径内)。',
|
|
308
|
+
en: 'Not a site-wide sales figure, and not the escrow contract\'s settlement total (escrow settlement lives on-chain in the contract and is out of this scope).',
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
generated_at: new Date(now).toISOString(),
|
|
312
|
+
totals: {
|
|
313
|
+
chain_verified_sales: rows.length,
|
|
314
|
+
delivered: queue.delivered,
|
|
315
|
+
delivered_unverifiable: queue.deliveredUnverifiable,
|
|
316
|
+
awaiting_signature: queue.awaiting,
|
|
317
|
+
pending_total: queue.total,
|
|
318
|
+
},
|
|
319
|
+
delivered_tx_hashes: queue.deliveredTxHashes,
|
|
320
|
+
txs: Object.fromEntries(rows.map((s) => [s.tx_hash, { itemId: s.item_id, amount: s.amount_atomic, settledAt: s.settled_at }])),
|
|
321
|
+
total_atomic: revenueAtomic,
|
|
322
|
+
revenue: {
|
|
323
|
+
amount_atomic: revenueAtomic,
|
|
324
|
+
amount_display: fmt(revenueAtomic, revenueCurrency),
|
|
325
|
+
currency: revenueCurrency,
|
|
326
|
+
},
|
|
327
|
+
by_item: [...byItem.values()],
|
|
328
|
+
latest: rows[0] || null,
|
|
329
|
+
sales: rows,
|
|
330
|
+
privacy_blocked: blocked,
|
|
331
|
+
};
|
|
332
|
+
// 最后一跳守卫 (belt & braces): 逐行与整对象过隐私审计。
|
|
333
|
+
// · 行级泄漏 ⇒ 剔除该行并计数 (绝不静默放行);
|
|
334
|
+
// · 顶层泄漏 (只可能是实现 bug) ⇒ **不对外给这个对象**, 退回空结果 + 如实计数,
|
|
335
|
+
// 宁少报也不泄漏 (这条兜底在真数据上应当恒不触发 —— 单测会钉住)。
|
|
336
|
+
const clean = [];
|
|
337
|
+
for (const s of summary.sales) {
|
|
338
|
+
if (auditSellerSummaryLeaks(s, '$.sales[i]').length === 0)
|
|
339
|
+
clean.push(s);
|
|
340
|
+
else
|
|
341
|
+
blocked += 1;
|
|
342
|
+
}
|
|
343
|
+
summary.sales = clean;
|
|
344
|
+
summary.latest = clean[0] || null;
|
|
345
|
+
summary.txs = Object.fromEntries(clean.map((s) => [s.tx_hash, { itemId: s.item_id, amount: s.amount_atomic, settledAt: s.settled_at }]));
|
|
346
|
+
summary.totals.chain_verified_sales = clean.length;
|
|
347
|
+
if (auditSellerSummaryLeaks(summary).length > 0) {
|
|
348
|
+
summary.sales = [];
|
|
349
|
+
summary.latest = null;
|
|
350
|
+
summary.by_item = [];
|
|
351
|
+
summary.delivered_tx_hashes = [];
|
|
352
|
+
summary.txs = {};
|
|
353
|
+
summary.total_atomic = '0';
|
|
354
|
+
summary.totals.chain_verified_sales = 0;
|
|
355
|
+
summary.revenue = { amount_atomic: '0', amount_display: fmt('0', revenueCurrency), currency: revenueCurrency };
|
|
356
|
+
}
|
|
357
|
+
summary.privacy_blocked = blocked;
|
|
358
|
+
return summary;
|
|
359
|
+
}
|
|
360
|
+
/** HTTP 形状 (server.mjs 直接写回): 永远 200 + no-store; 读不到就空数组 + 0 */
|
|
361
|
+
export async function sellerSummaryResponse(home) {
|
|
362
|
+
let summary;
|
|
363
|
+
try {
|
|
364
|
+
summary = await buildSellerSummary(home);
|
|
365
|
+
}
|
|
366
|
+
catch (e) {
|
|
367
|
+
const empty = {
|
|
368
|
+
protocol: SELLER_SUMMARY_PROTOCOL,
|
|
369
|
+
ok: false,
|
|
370
|
+
scope: {
|
|
371
|
+
title: { zh: '卖方本机台账 (链下) — 已交付笔数与对应链上 txHash', en: 'Seller-side ledger (off-chain) — delivered count and matching on-chain txHashes' },
|
|
372
|
+
ledger: { zh: '卖方本机的交付台账 (链下记录), 不是链上索引; 每笔对应一笔经链上核验的直付交易。', en: 'The seller\'s local delivery ledger (an off-chain record), not a chain index; each row maps to an on-chain-verified direct payment.' },
|
|
373
|
+
verifiable: { zh: '每笔可用 tx_hash 在区块浏览器独立复核。', en: 'Verify each row by tx_hash on a block explorer.' },
|
|
374
|
+
not: { zh: '不是全网站点销量, 也不是合约托管结算总量。', en: 'Not a site-wide figure, not escrow settlement.' },
|
|
375
|
+
},
|
|
376
|
+
generated_at: new Date().toISOString(),
|
|
377
|
+
totals: { chain_verified_sales: 0, delivered: 0, delivered_unverifiable: 0, awaiting_signature: 0, pending_total: 0 },
|
|
378
|
+
delivered_tx_hashes: [],
|
|
379
|
+
txs: {},
|
|
380
|
+
total_atomic: '0',
|
|
381
|
+
revenue: { amount_atomic: '0', amount_display: '0 USDC', currency: 'USDC' },
|
|
382
|
+
by_item: [],
|
|
383
|
+
latest: null,
|
|
384
|
+
sales: [],
|
|
385
|
+
privacy_blocked: 0,
|
|
386
|
+
};
|
|
387
|
+
return {
|
|
388
|
+
status: 200,
|
|
389
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store', 'X-Summary-Degraded': String(e && e.message || e).slice(0, 120) },
|
|
390
|
+
body: JSON.stringify(empty, null, 2),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
status: 200,
|
|
395
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' },
|
|
396
|
+
body: JSON.stringify(summary, null, 2),
|
|
397
|
+
};
|
|
398
|
+
}
|