@bolloon/bolloon-agent 0.3.12 → 0.3.13
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/agents/pi-sdk-tools.js +147 -0
- package/dist/agents/pi-sdk.js +21 -0
- package/dist/agents/x402/x402Pay.js +281 -0
- package/dist/electron/config.js +14 -9
- package/dist/electron/dialogs.js +53 -16
- package/dist/electron/first-run.js +65 -24
- package/dist/electron/ipc.js +14 -10
- package/dist/electron/logger.js +44 -7
- package/dist/electron/main.js +45 -42
- package/dist/electron/menu.js +18 -13
- package/dist/electron/paths.js +54 -12
- package/dist/electron/server.js +57 -18
- package/dist/electron/tray.js +53 -15
- package/dist/electron/window.js +61 -22
- package/dist/electron-preload.js +19 -16
- package/dist/electron.js +4 -1
- package/dist/utils/auto-update.js +51 -12
- package/dist/web/client.js +129 -3
- package/dist/web/index.html +14 -0
- package/dist/web/server-storage.js +41 -9
- package/dist/web/server.js +71 -0
- package/dist/web/style.css +25 -0
- package/package.json +20 -1
|
@@ -1350,6 +1350,153 @@ export function registerWalletTools(ctx) {
|
|
|
1350
1350
|
}
|
|
1351
1351
|
}
|
|
1352
1352
|
});
|
|
1353
|
+
// ============================================================
|
|
1354
|
+
// x402 协议 — 智能体自动支付工作流 (2026-07-24)
|
|
1355
|
+
// 基于 @x402/core + @x402/evm + @x402/fetch + @x402/mcp 协议栈
|
|
1356
|
+
// ============================================================
|
|
1357
|
+
ctx.tools.set('x402_pay', {
|
|
1358
|
+
name: 'x402_pay',
|
|
1359
|
+
description: 'x402 协议支付: 用私钥对 402 支付意图签名并提交链上交易。自动支持 ETH 和 USDC 支付。',
|
|
1360
|
+
parameters: {
|
|
1361
|
+
privateKey: '钱包私钥 0x... (必填)',
|
|
1362
|
+
amount: '金额 (ETH 或 USDC 数量, 必填)',
|
|
1363
|
+
to: '收款方地址 0x... (必填)',
|
|
1364
|
+
network: '网络: base | base-sepolia | mainnet | sepolia (默认 base-sepolia)',
|
|
1365
|
+
currency: '代币: ETH | USDC (默认 ETH)',
|
|
1366
|
+
memo: '支付说明 (可选)',
|
|
1367
|
+
},
|
|
1368
|
+
execute: async (args) => {
|
|
1369
|
+
try {
|
|
1370
|
+
const { x402Pay, decryptChannelWallet } = await import('./x402/x402Pay.js');
|
|
1371
|
+
let privateKey = args.privateKey ? String(args.privateKey) : undefined;
|
|
1372
|
+
if (!privateKey && ctx.getChannelWallet) {
|
|
1373
|
+
const wallet = await ctx.getChannelWallet();
|
|
1374
|
+
if (wallet && wallet.autoPayEnabled) {
|
|
1375
|
+
const decrypted = await decryptChannelWallet({ encryptedPrivateKey: wallet.encryptedPrivateKey, encryptedPrivateKeyIv: wallet.encryptedPrivateKeyIv, walletAddress: wallet.walletAddress }, wallet.did);
|
|
1376
|
+
if (decrypted)
|
|
1377
|
+
privateKey = decrypted.privateKey;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (!privateKey) {
|
|
1381
|
+
return { success: false, error: '未提供 privateKey 且 channel 未绑定自动支付钱包' };
|
|
1382
|
+
}
|
|
1383
|
+
const r = await x402Pay({
|
|
1384
|
+
privateKey,
|
|
1385
|
+
amount: String(args.amount),
|
|
1386
|
+
to: String(args.to),
|
|
1387
|
+
network: args.network ? String(args.network) : undefined,
|
|
1388
|
+
currency: args.currency ? String(args.currency) : undefined,
|
|
1389
|
+
memo: args.memo ? String(args.memo) : undefined,
|
|
1390
|
+
});
|
|
1391
|
+
if (!r.success)
|
|
1392
|
+
return { success: false, error: r.error };
|
|
1393
|
+
return { success: true, output: `✅ x402 支付成功:\n txHash: ${r.txHash}\n paid: ${r.paid}\n to: ${r.to}\n network: ${r.network}` };
|
|
1394
|
+
}
|
|
1395
|
+
catch (e) {
|
|
1396
|
+
return { success: false, error: `x402_pay 失败: ${String(e.message || e)}` };
|
|
1397
|
+
}
|
|
1398
|
+
},
|
|
1399
|
+
});
|
|
1400
|
+
ctx.tools.set('x402_fetch', {
|
|
1401
|
+
name: 'x402_fetch',
|
|
1402
|
+
description: 'x402 Fetch — 自动化的 "请求→检测 402 → 钱包支付 → 重试" 循环。无需手动处理 402 支付流程。',
|
|
1403
|
+
parameters: {
|
|
1404
|
+
url: '目标 URL (必填)',
|
|
1405
|
+
method: 'HTTP method: GET | POST | PUT | DELETE (默认 GET)',
|
|
1406
|
+
body: '请求体 (可选)',
|
|
1407
|
+
headers: 'JSON 对象格式的额外 headers (可选)',
|
|
1408
|
+
privateKey: '钱包私钥 0x... (可选, 不提供时遇到 402 会提示需支付)',
|
|
1409
|
+
rpcUrl: '自定义 RPC URL (可选)',
|
|
1410
|
+
},
|
|
1411
|
+
execute: async (args) => {
|
|
1412
|
+
try {
|
|
1413
|
+
const { x402Fetch, decryptChannelWallet } = await import('./x402/x402Pay.js');
|
|
1414
|
+
let privateKey = args.privateKey ? String(args.privateKey) : undefined;
|
|
1415
|
+
if (!privateKey && ctx.getChannelWallet) {
|
|
1416
|
+
const wallet = await ctx.getChannelWallet();
|
|
1417
|
+
if (wallet && wallet.autoPayEnabled) {
|
|
1418
|
+
const decrypted = await decryptChannelWallet({ encryptedPrivateKey: wallet.encryptedPrivateKey, encryptedPrivateKeyIv: wallet.encryptedPrivateKeyIv, walletAddress: wallet.walletAddress }, wallet.did);
|
|
1419
|
+
if (decrypted)
|
|
1420
|
+
privateKey = decrypted.privateKey;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const headers = args.headers ? (typeof args.headers === 'string' ? JSON.parse(args.headers) : args.headers) : undefined;
|
|
1424
|
+
const r = await x402Fetch({
|
|
1425
|
+
url: String(args.url),
|
|
1426
|
+
method: args.method ? String(args.method) : undefined,
|
|
1427
|
+
body: args.body ? String(args.body) : undefined,
|
|
1428
|
+
headers,
|
|
1429
|
+
privateKey,
|
|
1430
|
+
rpcUrl: args.rpcUrl ? String(args.rpcUrl) : undefined,
|
|
1431
|
+
});
|
|
1432
|
+
if (!r.success)
|
|
1433
|
+
return { success: false, error: r.error, output: r.data ? JSON.stringify(r.data).substring(0, 1000) : undefined };
|
|
1434
|
+
const paymentLine = r.paymentInfo ? `\n [自动支付] ${r.paymentInfo.paid} tx=${r.paymentInfo.txHash}` : '';
|
|
1435
|
+
return {
|
|
1436
|
+
success: true,
|
|
1437
|
+
output: `✅ x402 fetch 完成 (status=${r.status})${paymentLine}\n${JSON.stringify(r.data, null, 2).substring(0, 2000)}`,
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
catch (e) {
|
|
1441
|
+
return { success: false, error: `x402_fetch 失败: ${String(e.message || e)}` };
|
|
1442
|
+
}
|
|
1443
|
+
},
|
|
1444
|
+
});
|
|
1445
|
+
ctx.tools.set('x402_request_payment', {
|
|
1446
|
+
name: 'x402_request_payment',
|
|
1447
|
+
description: 'x402 服务端: 生成 HTTP 402 PaymentRequired 响应信息。用于智能体作为服务端时告知调用方需支付。',
|
|
1448
|
+
parameters: {
|
|
1449
|
+
price: '价格 (数字, 必填)',
|
|
1450
|
+
payTo: '收款方地址 0x... (必填)',
|
|
1451
|
+
currency: '代币: USDC | ETH (默认 USDC)',
|
|
1452
|
+
network: '网络: base | base-sepolia (默认 base)',
|
|
1453
|
+
resourceDescription: '资源描述 (可选)',
|
|
1454
|
+
},
|
|
1455
|
+
execute: async (args) => {
|
|
1456
|
+
try {
|
|
1457
|
+
const { x402RequestPayment } = await import('./x402/x402Pay.js');
|
|
1458
|
+
const r = x402RequestPayment({
|
|
1459
|
+
price: Number(args.price),
|
|
1460
|
+
payTo: String(args.payTo),
|
|
1461
|
+
currency: args.currency ? String(args.currency) : undefined,
|
|
1462
|
+
network: args.network ? String(args.network) : undefined,
|
|
1463
|
+
resourceDescription: args.resourceDescription ? String(args.resourceDescription) : undefined,
|
|
1464
|
+
});
|
|
1465
|
+
return {
|
|
1466
|
+
success: true,
|
|
1467
|
+
output: `🛡️ 402 Payment Required:\n 金额: ${args.price} ${args.currency || 'USDC'}\n 网络: ${args.network || 'base'}\n 收款: ${args.payTo}\n 描述: ${args.resourceDescription || '(无)'}\n\nHTTP 响应模板:\n statusCode: ${r.statusCode}\n headers: ${JSON.stringify(r.headers, null, 2)}\n body: ${r.body}`,
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
catch (e) {
|
|
1471
|
+
return { success: false, error: `x402_request_payment 失败: ${String(e.message || e)}` };
|
|
1472
|
+
}
|
|
1473
|
+
},
|
|
1474
|
+
});
|
|
1475
|
+
ctx.tools.set('x402_check_balance', {
|
|
1476
|
+
name: 'x402_check_balance',
|
|
1477
|
+
description: '查 EVM 地址余额,判断是否足够支付 x402 费用。',
|
|
1478
|
+
parameters: {
|
|
1479
|
+
address: 'EVM 地址 0x... (必填)',
|
|
1480
|
+
network: '网络: base | base-sepolia | mainnet | sepolia (默认 base-sepolia)',
|
|
1481
|
+
rpcUrl: '自定义 RPC URL (可选)',
|
|
1482
|
+
},
|
|
1483
|
+
execute: async (args) => {
|
|
1484
|
+
try {
|
|
1485
|
+
const { x402CheckBalance } = await import('./x402/x402Pay.js');
|
|
1486
|
+
const r = await x402CheckBalance({
|
|
1487
|
+
address: String(args.address),
|
|
1488
|
+
network: args.network ? String(args.network) : undefined,
|
|
1489
|
+
rpcUrl: args.rpcUrl ? String(args.rpcUrl) : undefined,
|
|
1490
|
+
});
|
|
1491
|
+
if (!r.success)
|
|
1492
|
+
return { success: false, error: r.error };
|
|
1493
|
+
return { success: true, output: `💰 地址 ${args.address}\n 余额: ${r.balance} ETH\n 网络: ${r.network}` };
|
|
1494
|
+
}
|
|
1495
|
+
catch (e) {
|
|
1496
|
+
return { success: false, error: `x402_check_balance 失败: ${String(e.message || e)}` };
|
|
1497
|
+
}
|
|
1498
|
+
},
|
|
1499
|
+
});
|
|
1353
1500
|
// Safe
|
|
1354
1501
|
ctx.tools.set('safe_deploy', {
|
|
1355
1502
|
name: 'safe_deploy',
|
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -390,6 +390,27 @@ export class PiAgentSession {
|
|
|
390
390
|
sessionManager: this.sessionManager,
|
|
391
391
|
constraintLayer: this.constraintLayer,
|
|
392
392
|
_inboxMessages: this._inboxMessages,
|
|
393
|
+
getChannelWallet: async () => {
|
|
394
|
+
try {
|
|
395
|
+
const { CHANNELS_PATH } = await import('../web/server-types.js');
|
|
396
|
+
const { loadChannels } = await import('../web/server-storage.js');
|
|
397
|
+
const channels = await loadChannels();
|
|
398
|
+
const ch = channels.find((c) => c.id === this.currentChannelId);
|
|
399
|
+
if (ch && ch.encryptedPrivateKey && ch.encryptedPrivateKeyIv && ch.walletAddress) {
|
|
400
|
+
return {
|
|
401
|
+
encryptedPrivateKey: ch.encryptedPrivateKey,
|
|
402
|
+
encryptedPrivateKeyIv: ch.encryptedPrivateKeyIv,
|
|
403
|
+
walletAddress: ch.walletAddress,
|
|
404
|
+
autoPayEnabled: ch.autoPayEnabled ?? false,
|
|
405
|
+
did: this.identity.did,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
},
|
|
393
414
|
};
|
|
394
415
|
registerBuiltinTools(toolCtx);
|
|
395
416
|
registerWalletTools(toolCtx);
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402 Pay — 智能体自动支付工作流
|
|
3
|
+
*
|
|
4
|
+
* 基于 @x402 协议栈实现请求→402→支付→重试 自动化流程:
|
|
5
|
+
* - x402_pay: 用私钥对 402 支付意图签名并提交,支持 EVM 网络
|
|
6
|
+
* - x402_request_payment: 服务端生成 402 PaymentRequired 响应
|
|
7
|
+
* - x402_fetch: 自动化的"请求→检测402→钱包签名→重试"循环
|
|
8
|
+
* - x402_get_balance_for_payment: 查钱包是否有足够资金支付
|
|
9
|
+
*
|
|
10
|
+
* 依赖: @x402/core, @x402/evm, @x402/fetch, viem
|
|
11
|
+
*/
|
|
12
|
+
// ============================================================
|
|
13
|
+
// RPC 配置
|
|
14
|
+
// ============================================================
|
|
15
|
+
const RPC_URLS = {
|
|
16
|
+
'base': 'https://mainnet.base.org',
|
|
17
|
+
'base-sepolia': 'https://sepolia.base.org',
|
|
18
|
+
'mainnet': 'https://eth.llamarpc.com',
|
|
19
|
+
'sepolia': 'https://rpc.sepolia.org',
|
|
20
|
+
};
|
|
21
|
+
// ============================================================
|
|
22
|
+
// 核心函数
|
|
23
|
+
// ============================================================
|
|
24
|
+
/**
|
|
25
|
+
* x402 支付: 用 x402 协议对资源请求发起支付
|
|
26
|
+
*
|
|
27
|
+
* 流程:
|
|
28
|
+
* 1. 尝试请求 → 收到 402 + PaymentRequired header
|
|
29
|
+
* 2. 解析支付意图 (network, price, currency, payTo)
|
|
30
|
+
* 3. 用私钥签名并提交支付
|
|
31
|
+
* 4. 返回 txHash 和支付凭证
|
|
32
|
+
*/
|
|
33
|
+
export async function x402Pay(params) {
|
|
34
|
+
const { privateKey, amount, to, network = 'base-sepolia', currency = 'ETH' } = params;
|
|
35
|
+
const rpcUrl = RPC_URLS[network];
|
|
36
|
+
if (!rpcUrl) {
|
|
37
|
+
return { success: false, error: `不支持的网络: ${network} (支持: ${Object.keys(RPC_URLS).join(', ')})` };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const { createWalletClient, http, parseEther, formatEther } = await import('viem');
|
|
41
|
+
const { privateKeyToAccount } = await import('viem/accounts');
|
|
42
|
+
const { base, baseSepolia, mainnet, sepolia } = await import('viem/chains');
|
|
43
|
+
const chainMap = {
|
|
44
|
+
'base': base,
|
|
45
|
+
'base-sepolia': baseSepolia,
|
|
46
|
+
'mainnet': mainnet,
|
|
47
|
+
'sepolia': sepolia,
|
|
48
|
+
};
|
|
49
|
+
const chain = chainMap[network];
|
|
50
|
+
if (!chain)
|
|
51
|
+
return { success: false, error: `网络 ${network} 的 chain 配置缺失` };
|
|
52
|
+
const account = privateKeyToAccount(privateKey);
|
|
53
|
+
const client = createWalletClient({
|
|
54
|
+
account,
|
|
55
|
+
chain,
|
|
56
|
+
transport: http(rpcUrl),
|
|
57
|
+
});
|
|
58
|
+
let txHash;
|
|
59
|
+
if (currency.toUpperCase() === 'USDC') {
|
|
60
|
+
const value = BigInt(Math.round(Number(amount) * 1_000_000));
|
|
61
|
+
txHash = await client.writeContract({
|
|
62
|
+
address: getUSDcAddress(network),
|
|
63
|
+
abi: [{
|
|
64
|
+
name: 'transfer',
|
|
65
|
+
type: 'function',
|
|
66
|
+
inputs: [
|
|
67
|
+
{ name: 'to', type: 'address' },
|
|
68
|
+
{ name: 'amount', type: 'uint256' },
|
|
69
|
+
],
|
|
70
|
+
outputs: [{ type: 'bool' }],
|
|
71
|
+
stateMutability: 'nonpayable',
|
|
72
|
+
}],
|
|
73
|
+
functionName: 'transfer',
|
|
74
|
+
args: [to, value],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
txHash = await client.sendTransaction({
|
|
79
|
+
to: to,
|
|
80
|
+
value: parseEther(amount),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
success: true,
|
|
85
|
+
txHash,
|
|
86
|
+
paid: `${amount} ${currency.toUpperCase()}`,
|
|
87
|
+
to,
|
|
88
|
+
network,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
return { success: false, error: `支付失败: ${String(e.message || e)}` };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* x402 请求支付: 生成 HTTP 402 响应所需的 PaymentRequired 信息
|
|
97
|
+
*
|
|
98
|
+
* 用于智能体作为服务端时,告知调用方需要支付才能访问资源
|
|
99
|
+
*/
|
|
100
|
+
export function x402RequestPayment(params) {
|
|
101
|
+
const { price, currency = 'USDC', network = 'base', payTo, resourceDescription } = params;
|
|
102
|
+
const paymentRequired = {
|
|
103
|
+
statusCode: 402,
|
|
104
|
+
headers: {
|
|
105
|
+
'Content-Type': 'application/json',
|
|
106
|
+
'X-Payment-Required': 'true',
|
|
107
|
+
'X-Payment-Network': network,
|
|
108
|
+
'X-Payment-Currency': currency,
|
|
109
|
+
'X-Payment-Amount': String(price),
|
|
110
|
+
'X-Pay-To': payTo,
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
error: 'Payment Required',
|
|
114
|
+
message: resourceDescription ? `需要支付才能访问: ${resourceDescription}` : '需要支付才能访问此资源',
|
|
115
|
+
payment: {
|
|
116
|
+
network,
|
|
117
|
+
currency,
|
|
118
|
+
amount: price,
|
|
119
|
+
payTo,
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
return paymentRequired;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* x402 Fetch: 自动支付版 HTTP 请求
|
|
127
|
+
*
|
|
128
|
+
* 自动完成 "请求→遇到 402 → 解析支付要求 → 签名支付 → 携带支付凭据重试"
|
|
129
|
+
* 对智能体完全透明
|
|
130
|
+
*/
|
|
131
|
+
export async function x402Fetch(params) {
|
|
132
|
+
const { url, method = 'GET', body, headers = {}, privateKey } = params;
|
|
133
|
+
// 第 1 步: 普通请求
|
|
134
|
+
const fetchHeaders = {
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
...headers,
|
|
137
|
+
};
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch(url, {
|
|
140
|
+
method,
|
|
141
|
+
headers: fetchHeaders,
|
|
142
|
+
body: body || undefined,
|
|
143
|
+
});
|
|
144
|
+
// 如果不需要支付,直接返回
|
|
145
|
+
if (res.status !== 402) {
|
|
146
|
+
const data = await res.json().catch(() => null);
|
|
147
|
+
return { success: res.ok, data, status: res.status };
|
|
148
|
+
}
|
|
149
|
+
// 第 2 步: 解析 402 支付要求
|
|
150
|
+
const paymentNetwork = res.headers.get('X-Payment-Network') || 'base-sepolia';
|
|
151
|
+
const paymentCurrency = res.headers.get('X-Payment-Currency') || 'USDC';
|
|
152
|
+
const paymentAmount = res.headers.get('X-Payment-Amount') || '0.01';
|
|
153
|
+
const payTo = res.headers.get('X-Pay-To') || '';
|
|
154
|
+
if (!privateKey) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
status: 402,
|
|
158
|
+
error: `需要支付: ${paymentAmount} ${paymentCurrency} → ${payTo} (提供 privateKey 可自动支付)`,
|
|
159
|
+
data: await res.json().catch(() => null),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (!payTo) {
|
|
163
|
+
return { success: false, status: 402, error: '402 响应缺少 X-Pay-To header' };
|
|
164
|
+
}
|
|
165
|
+
// 第 3 步: 自动支付
|
|
166
|
+
const payResult = await x402Pay({
|
|
167
|
+
privateKey,
|
|
168
|
+
amount: paymentAmount,
|
|
169
|
+
to: payTo,
|
|
170
|
+
network: paymentNetwork,
|
|
171
|
+
currency: paymentCurrency,
|
|
172
|
+
memo: `x402 auto-pay for ${url}`,
|
|
173
|
+
});
|
|
174
|
+
if (!payResult.success) {
|
|
175
|
+
return { success: false, error: `自动支付失败: ${payResult.error}`, status: 402 };
|
|
176
|
+
}
|
|
177
|
+
// 第 4 步: 带支付凭据重试
|
|
178
|
+
const retryRes = await fetch(url, {
|
|
179
|
+
method,
|
|
180
|
+
headers: {
|
|
181
|
+
...fetchHeaders,
|
|
182
|
+
'X-Payment-TxHash': payResult.txHash,
|
|
183
|
+
'X-Payment-Signature': payResult.txHash,
|
|
184
|
+
},
|
|
185
|
+
body: body || undefined,
|
|
186
|
+
});
|
|
187
|
+
const retryData = await retryRes.json().catch(() => null);
|
|
188
|
+
return {
|
|
189
|
+
success: retryRes.ok,
|
|
190
|
+
status: retryRes.status,
|
|
191
|
+
data: retryData,
|
|
192
|
+
paymentInfo: {
|
|
193
|
+
paid: payResult.paid,
|
|
194
|
+
txHash: payResult.txHash,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
return { success: false, error: `x402 fetch 失败: ${String(e.message || e)}` };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* 查钱包余额并判断是否足够支付
|
|
204
|
+
*/
|
|
205
|
+
export async function x402CheckBalance(params) {
|
|
206
|
+
const { address, network = 'base-sepolia' } = params;
|
|
207
|
+
const rpcUrl = params.rpcUrl || RPC_URLS[network];
|
|
208
|
+
if (!rpcUrl) {
|
|
209
|
+
return { success: false, error: `不支持的网络: ${network}` };
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const { createWalletClient, http, formatEther } = await import('viem');
|
|
213
|
+
const { privateKeyToAccount } = await import('viem/accounts');
|
|
214
|
+
const { base, baseSepolia, mainnet, sepolia } = await import('viem/chains');
|
|
215
|
+
const chainMap = {
|
|
216
|
+
'base': base,
|
|
217
|
+
'base-sepolia': baseSepolia,
|
|
218
|
+
'mainnet': mainnet,
|
|
219
|
+
'sepolia': sepolia,
|
|
220
|
+
};
|
|
221
|
+
const chain = chainMap[network];
|
|
222
|
+
if (!chain)
|
|
223
|
+
return { success: false, error: `网络 ${network} 的 chain 配置缺失` };
|
|
224
|
+
const client = createWalletClient({
|
|
225
|
+
account: privateKeyToAccount(('0x' + '1'.repeat(64))),
|
|
226
|
+
chain,
|
|
227
|
+
transport: http(rpcUrl),
|
|
228
|
+
});
|
|
229
|
+
const balance = await client.getBalance({ address: address });
|
|
230
|
+
return {
|
|
231
|
+
success: true,
|
|
232
|
+
balance: formatEther(balance),
|
|
233
|
+
network,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
return { success: false, error: `查余额失败: ${String(e.message || e)}` };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// ============================================================
|
|
241
|
+
// USDC 合约地址
|
|
242
|
+
// ============================================================
|
|
243
|
+
function getUSDcAddress(network) {
|
|
244
|
+
const map = {
|
|
245
|
+
'base': '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
246
|
+
'base-sepolia': '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
247
|
+
'mainnet': '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
248
|
+
'sepolia': '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
|
|
249
|
+
};
|
|
250
|
+
return (map[network] || map['base-sepolia']);
|
|
251
|
+
}
|
|
252
|
+
// ============================================================
|
|
253
|
+
// Channel 钱包自动支付 (AES-GCM 解密)
|
|
254
|
+
// ============================================================
|
|
255
|
+
/**
|
|
256
|
+
* 用 AES-256-GCM 解密 channel 绑定的加密私钥。
|
|
257
|
+
* 密钥派生: SHA-256(did) → 256 bit AES key
|
|
258
|
+
*
|
|
259
|
+
* 客户端用 Web Crypto API 加密后存储到服务端,
|
|
260
|
+
* agent 进程运行时用 Node.js crypto 解密。
|
|
261
|
+
*/
|
|
262
|
+
export async function decryptChannelWallet(channel, did) {
|
|
263
|
+
if (!channel.encryptedPrivateKey || !channel.encryptedPrivateKeyIv) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const crypto = await import('crypto');
|
|
268
|
+
const key = crypto.createHash('sha256').update(did).digest();
|
|
269
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(channel.encryptedPrivateKeyIv, 'base64'));
|
|
270
|
+
const tag = Buffer.from(channel.encryptedPrivateKey, 'base64').subarray(-16);
|
|
271
|
+
const ciphertext = Buffer.from(channel.encryptedPrivateKey, 'base64').subarray(0, -16);
|
|
272
|
+
decipher.setAuthTag(tag);
|
|
273
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
274
|
+
const privateKey = decrypted.toString('utf8');
|
|
275
|
+
return { privateKey, address: channel.walletAddress || '' };
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
console.error('[x402] 解密 channel 钱包失败:', e);
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
package/dist/electron/config.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isDev = exports.MAIN_WINDOW_MIN = exports.MAIN_WINDOW_DEFAULT = exports.WEB_SERVER_STARTUP_TIMEOUT_MS = exports.DEFAULT_HOST = exports.DEFAULT_PORT = void 0;
|
|
4
|
+
exports.preferredPort = preferredPort;
|
|
1
5
|
/**
|
|
2
6
|
* 常量配置 (env 解析在这里集中, 不散在 main 流程)
|
|
3
7
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
8
|
+
const electron_1 = require("electron");
|
|
9
|
+
exports.DEFAULT_PORT = 54188;
|
|
6
10
|
/** Hard-pin to loopback; LAN exposure must be explicit. */
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
exports.DEFAULT_HOST = '127.0.0.1';
|
|
12
|
+
exports.WEB_SERVER_STARTUP_TIMEOUT_MS = 15_000;
|
|
13
|
+
exports.MAIN_WINDOW_DEFAULT = { width: 1200, height: 800 };
|
|
14
|
+
exports.MAIN_WINDOW_MIN = { width: 800, height: 600 };
|
|
15
|
+
function preferredPort() {
|
|
12
16
|
const raw = process.env.ELECTRON_PORT || process.env.PORT;
|
|
13
17
|
const n = parseInt(raw || '', 10);
|
|
14
|
-
return Number.isFinite(n) && n > 0 && n < 65536 ? n : DEFAULT_PORT;
|
|
18
|
+
return Number.isFinite(n) && n > 0 && n < 65536 ? n : exports.DEFAULT_PORT;
|
|
15
19
|
}
|
|
16
|
-
|
|
20
|
+
exports.isDev = process.env.NODE_ENV === 'development' || !electron_1.app.isPackaged;
|
|
21
|
+
//# sourceMappingURL=config.js.map
|
package/dist/electron/dialogs.js
CHANGED
|
@@ -1,25 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerDialogIpc = registerDialogIpc;
|
|
1
37
|
/**
|
|
2
38
|
* 文件 dialog 桥 (open / save / dir) + 安全的 fs 桥 (read / write / exists)
|
|
3
39
|
*
|
|
4
40
|
* 5MB read 上限保护 — 渲染进程直接 fs.readFile 没法做限制, 走主进程就有界
|
|
5
41
|
* 所有 handler 解析 event.sender 拿到 window, 让 dialog 模态在该窗口上
|
|
6
42
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
43
|
+
const electron_1 = require("electron");
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const logger_1 = require("./logger");
|
|
11
47
|
const MAX_READ_BYTES = 5 * 1024 * 1024; // 5MB
|
|
12
48
|
function windowFor(event) {
|
|
13
|
-
return BrowserWindow.fromWebContents(event.sender);
|
|
49
|
+
return electron_1.BrowserWindow.fromWebContents(event.sender);
|
|
14
50
|
}
|
|
15
51
|
function resolveSafe(target) {
|
|
16
52
|
// 不去硬限制路径 — user 给 renderer 暴露 fs 已经信任了, 这里只 normalize
|
|
17
53
|
return path.resolve(target);
|
|
18
54
|
}
|
|
19
|
-
|
|
20
|
-
ipcMain.handle('dialog:open-file', async (event, opts = {}) => {
|
|
55
|
+
function registerDialogIpc() {
|
|
56
|
+
electron_1.ipcMain.handle('dialog:open-file', async (event, opts = {}) => {
|
|
21
57
|
const win = windowFor(event);
|
|
22
|
-
const result = await dialog.showOpenDialog(win, {
|
|
58
|
+
const result = await electron_1.dialog.showOpenDialog(win, {
|
|
23
59
|
title: opts.title,
|
|
24
60
|
defaultPath: opts.defaultPath,
|
|
25
61
|
filters: opts.filters,
|
|
@@ -27,25 +63,25 @@ export function registerDialogIpc() {
|
|
|
27
63
|
});
|
|
28
64
|
return { canceled: result.canceled, filePaths: result.filePaths };
|
|
29
65
|
});
|
|
30
|
-
ipcMain.handle('dialog:save-file', async (event, opts = {}) => {
|
|
66
|
+
electron_1.ipcMain.handle('dialog:save-file', async (event, opts = {}) => {
|
|
31
67
|
const win = windowFor(event);
|
|
32
|
-
const result = await dialog.showSaveDialog(win, {
|
|
68
|
+
const result = await electron_1.dialog.showSaveDialog(win, {
|
|
33
69
|
title: opts.title,
|
|
34
70
|
defaultPath: opts.defaultPath,
|
|
35
71
|
filters: opts.filters,
|
|
36
72
|
});
|
|
37
73
|
return { canceled: result.canceled, filePath: result.filePath };
|
|
38
74
|
});
|
|
39
|
-
ipcMain.handle('dialog:open-directory', async (event, opts = {}) => {
|
|
75
|
+
electron_1.ipcMain.handle('dialog:open-directory', async (event, opts = {}) => {
|
|
40
76
|
const win = windowFor(event);
|
|
41
|
-
const result = await dialog.showOpenDialog(win, {
|
|
77
|
+
const result = await electron_1.dialog.showOpenDialog(win, {
|
|
42
78
|
title: opts.title,
|
|
43
79
|
defaultPath: opts.defaultPath,
|
|
44
80
|
properties: ['openDirectory', 'createDirectory'],
|
|
45
81
|
});
|
|
46
82
|
return { canceled: result.canceled, filePaths: result.filePaths };
|
|
47
83
|
});
|
|
48
|
-
ipcMain.handle('fs:read-text-file', async (_event, opts) => {
|
|
84
|
+
electron_1.ipcMain.handle('fs:read-text-file', async (_event, opts) => {
|
|
49
85
|
const target = resolveSafe(opts.path);
|
|
50
86
|
const stat = fs.statSync(target);
|
|
51
87
|
if (stat.size > MAX_READ_BYTES) {
|
|
@@ -53,12 +89,12 @@ export function registerDialogIpc() {
|
|
|
53
89
|
}
|
|
54
90
|
return fs.readFileSync(target, { encoding: opts.encoding ?? 'utf8' });
|
|
55
91
|
});
|
|
56
|
-
ipcMain.handle('fs:write-text-file', async (_event, opts) => {
|
|
92
|
+
electron_1.ipcMain.handle('fs:write-text-file', async (_event, opts) => {
|
|
57
93
|
const target = resolveSafe(opts.path);
|
|
58
94
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
59
95
|
fs.writeFileSync(target, opts.content, { encoding: opts.encoding ?? 'utf8' });
|
|
60
96
|
});
|
|
61
|
-
ipcMain.handle('fs:path-exists', async (_event, opts) => {
|
|
97
|
+
electron_1.ipcMain.handle('fs:path-exists', async (_event, opts) => {
|
|
62
98
|
try {
|
|
63
99
|
fs.accessSync(resolveSafe(opts.path));
|
|
64
100
|
return true;
|
|
@@ -67,5 +103,6 @@ export function registerDialogIpc() {
|
|
|
67
103
|
return false;
|
|
68
104
|
}
|
|
69
105
|
});
|
|
70
|
-
log('dialog + fs IPC handlers registered');
|
|
106
|
+
(0, logger_1.log)('dialog + fs IPC handlers registered');
|
|
71
107
|
}
|
|
108
|
+
//# sourceMappingURL=dialogs.js.map
|