@bolloon/bolloon-agent 0.4.23 → 0.4.25
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/execution-supervisor.js +391 -0
- package/dist/agents/goal-store.js +451 -0
- package/dist/agents/pi-harness.js +263 -0
- package/dist/agents/pi-sdk.js +568 -126
- package/dist/agents/run-store.js +772 -0
- package/dist/agents/runner-resolver.js +225 -0
- package/dist/agents/skills-manager.js +435 -0
- package/dist/agents/supervisor-host.js +249 -0
- package/dist/cron/tick-lock.js +1 -1
- package/dist/index.js +428 -22
- package/dist/ios/agent-delegate-server.js +58 -12
- package/dist/ios/icons/icon-1024x1024.png +0 -0
- package/dist/ios/icons/icon-1024x1024.webp +0 -0
- package/dist/ios/icons/icon-216x216.png +0 -0
- package/dist/ios/icons/icon-216x216.webp +0 -0
- package/dist/ios/index.html +95 -18
- package/dist/ios/manifest.json +1 -1
- package/dist/ios/mobile-agent.js +244 -5
- package/dist/ios/mobile-chain.js +481 -0
- package/dist/ios/mobile-core.js +25177 -24664
- package/dist/ios/mobile-helia.js +683 -0
- package/dist/ios/mobile-ipfs.js +680 -0
- package/dist/ios/mobile-orbit.js +268 -0
- package/dist/ios/mobile-p2p.js +130 -1
- package/dist/ios/mobile-social.js +624 -0
- package/dist/ios/mobile-sync.js +193 -0
- package/dist/ios/mobile-trade.js +492 -0
- package/dist/ios/mobile-wallet.js +11 -0
- package/dist/ios/mobile.css +39 -0
- package/dist/ios/mobile.html +95 -18
- package/dist/ios/mobile.js +1054 -55
- package/dist/ios/routes-x402-info.js +194 -0
- package/dist/ios/server.js +350 -9
- package/dist/ios/sw.js +26 -2
- package/dist/web/agent-delegate-server.js +58 -12
- package/dist/web/icons/icon-1024x1024.png +0 -0
- package/dist/web/icons/icon-1024x1024.webp +0 -0
- package/dist/web/icons/icon-216x216.png +0 -0
- package/dist/web/icons/icon-216x216.webp +0 -0
- package/dist/web/manifest.json +1 -1
- package/dist/web/mobile-agent.js +197 -3
- package/dist/web/mobile-core.js +16417 -16090
- package/dist/web/mobile-privacy.js +185 -0
- package/dist/web/mobile.css +15 -0
- package/dist/web/mobile.html +21 -1
- package/dist/web/mobile.js +179 -6
- package/dist/web/server.js +437 -4
- package/package.json +2 -2
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mobile-chain.ts — 手机端独立链上模块 (WebView / Capacitor, 不需要电脑端)
|
|
3
|
+
*
|
|
4
|
+
* 用户需求 (2026-09-11):
|
|
5
|
+
* 让手机自己用钱包私钥 ① 签 x402 支付授权 (EIP-712 / EIP-3009 TransferWithAuthorization,
|
|
6
|
+
* 产出可直接放进 HTTP 头的 `X-PAYMENT`) ② 自己构造/签名/广播 EVM 交易, 包括把资源/服务
|
|
7
|
+
* 注册上链 (ResourceERC721.mint(address,uint256,string), tokenUri = ipfs://<CID>)。
|
|
8
|
+
*
|
|
9
|
+
* 设计约束:
|
|
10
|
+
* - 纯浏览器实现, **不 import 任何 node 内置模块** (fs/path/os/crypto 一律不碰)。
|
|
11
|
+
* - 所有网络访问走可注入 `fetchImpl`; 所有配置读写走可注入 `storage` (默认 localStorage)。
|
|
12
|
+
* - 全部导出函数失败**绝不抛异常** → 统一返回 `{ ok:false, error }`。
|
|
13
|
+
* (`rpcRequest` 例外: 它抛可捕获错误, 由上层包成 ok:false。)
|
|
14
|
+
* - 签名 / calldata 全部用 viem (与 desktop 端 @x402 一致, 浏览器安全)。
|
|
15
|
+
*/
|
|
16
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
17
|
+
import { encodeFunctionData, getAddress, keccak256, numberToHex, toHex } from 'viem';
|
|
18
|
+
// ============================================================
|
|
19
|
+
// 常量 / 注册表
|
|
20
|
+
// ============================================================
|
|
21
|
+
const CONFIG_KEY = 'bolloon_chain_config';
|
|
22
|
+
export const DEFAULT_CHAIN_CONFIG = {
|
|
23
|
+
rpcUrl: 'https://mainnet.base.org',
|
|
24
|
+
chainId: 8453,
|
|
25
|
+
network: 'base',
|
|
26
|
+
token: 'USDC',
|
|
27
|
+
};
|
|
28
|
+
const DEFAULT_VALID_FOR_SEC = 600;
|
|
29
|
+
const DEFAULT_ERC20_GAS_LIMIT = 100000n;
|
|
30
|
+
const DEFAULT_MINT_GAS_LIMIT = 250000n;
|
|
31
|
+
const DEFAULT_MAX_FEE_PER_GAS = 1000000000n; // 1 gwei
|
|
32
|
+
const DEFAULT_MAX_PRIORITY_FEE_PER_GAS = 100000000n; // 0.1 gwei
|
|
33
|
+
export const NETWORKS = {
|
|
34
|
+
base: { chainId: 8453, caip2: 'eip155:8453', v1: 'base' },
|
|
35
|
+
'base-sepolia': { chainId: 84532, caip2: 'eip155:84532', v1: 'base-sepolia' },
|
|
36
|
+
mainnet: { chainId: 1, caip2: 'eip155:1', v1: 'mainnet' },
|
|
37
|
+
sepolia: { chainId: 11155111, caip2: 'eip155:11155111', v1: 'sepolia' },
|
|
38
|
+
};
|
|
39
|
+
/** network → 符号 → 代币元数据 (EIP-712 domain name/version + decimals) */
|
|
40
|
+
export const TOKENS = {
|
|
41
|
+
base: {
|
|
42
|
+
USDC: { asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', name: 'USD Coin', version: '2', decimals: 6 },
|
|
43
|
+
},
|
|
44
|
+
'base-sepolia': {
|
|
45
|
+
USDC: { asset: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', name: 'USDC', version: '2', decimals: 6 },
|
|
46
|
+
},
|
|
47
|
+
mainnet: {
|
|
48
|
+
USDC: { asset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', name: 'USD Coin', version: '2', decimals: 6 },
|
|
49
|
+
},
|
|
50
|
+
sepolia: {
|
|
51
|
+
USDC: { asset: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', name: 'USDC', version: '2', decimals: 6 },
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
/** EIP-3009 TransferWithAuthorization 类型 (与 @x402/evm 一致) */
|
|
55
|
+
export const AUTHORIZATION_TYPES = {
|
|
56
|
+
TransferWithAuthorization: [
|
|
57
|
+
{ name: 'from', type: 'address' },
|
|
58
|
+
{ name: 'to', type: 'address' },
|
|
59
|
+
{ name: 'value', type: 'uint256' },
|
|
60
|
+
{ name: 'validAfter', type: 'uint256' },
|
|
61
|
+
{ name: 'validBefore', type: 'uint256' },
|
|
62
|
+
{ name: 'nonce', type: 'bytes32' },
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
/** ERC-20 transfer(address,uint256) */
|
|
66
|
+
export const ERC20_TRANSFER_ABI = [
|
|
67
|
+
{
|
|
68
|
+
name: 'transfer',
|
|
69
|
+
type: 'function',
|
|
70
|
+
inputs: [
|
|
71
|
+
{ name: 'to', type: 'address' },
|
|
72
|
+
{ name: 'amount', type: 'uint256' },
|
|
73
|
+
],
|
|
74
|
+
outputs: [{ type: 'bool' }],
|
|
75
|
+
stateMutability: 'nonpayable',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
/** ResourceERC721.mint(address,uint256,string) */
|
|
79
|
+
export const RESOURCE_ERC721_ABI = [
|
|
80
|
+
{
|
|
81
|
+
name: 'mint',
|
|
82
|
+
type: 'function',
|
|
83
|
+
inputs: [
|
|
84
|
+
{ name: 'to', type: 'address' },
|
|
85
|
+
{ name: 'tokenId', type: 'uint256' },
|
|
86
|
+
{ name: 'tokenUri', type: 'string' },
|
|
87
|
+
],
|
|
88
|
+
outputs: [],
|
|
89
|
+
stateMutability: 'nonpayable',
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
// ============================================================
|
|
93
|
+
// 小工具 (无 node 依赖)
|
|
94
|
+
// ============================================================
|
|
95
|
+
function errMsg(e) {
|
|
96
|
+
if (e instanceof Error)
|
|
97
|
+
return e.message;
|
|
98
|
+
return String(e);
|
|
99
|
+
}
|
|
100
|
+
const memoryFallback = (() => {
|
|
101
|
+
const m = new Map();
|
|
102
|
+
return {
|
|
103
|
+
getItem: (k) => (m.has(k) ? m.get(k) : null),
|
|
104
|
+
setItem: (k, v) => {
|
|
105
|
+
m.set(k, v);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
})();
|
|
109
|
+
function resolveStorage(storage) {
|
|
110
|
+
if (storage)
|
|
111
|
+
return storage;
|
|
112
|
+
const g = globalThis;
|
|
113
|
+
if (g && g.localStorage && typeof g.localStorage.getItem === 'function')
|
|
114
|
+
return g.localStorage;
|
|
115
|
+
return memoryFallback;
|
|
116
|
+
}
|
|
117
|
+
function resolveFetch(fetchImpl) {
|
|
118
|
+
if (fetchImpl)
|
|
119
|
+
return fetchImpl;
|
|
120
|
+
const g = globalThis;
|
|
121
|
+
if (g && typeof g.fetch === 'function')
|
|
122
|
+
return g.fetch.bind(globalThis);
|
|
123
|
+
throw new Error('fetch 不可用: 请注入 fetchImpl');
|
|
124
|
+
}
|
|
125
|
+
function normalizePrivateKey(privateKey) {
|
|
126
|
+
const t = (privateKey || '').trim();
|
|
127
|
+
return (t.startsWith('0x') ? t : `0x${t}`);
|
|
128
|
+
}
|
|
129
|
+
function randomNonceHex() {
|
|
130
|
+
const g = globalThis;
|
|
131
|
+
const bytes = new Uint8Array(32);
|
|
132
|
+
if (g.crypto && typeof g.crypto.getRandomValues === 'function') {
|
|
133
|
+
g.crypto.getRandomValues(bytes);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
for (let i = 0; i < bytes.length; i++)
|
|
137
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
138
|
+
}
|
|
139
|
+
return toHex(bytes);
|
|
140
|
+
}
|
|
141
|
+
const B64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
142
|
+
/** 纯 JS bytes → base64 (不依赖 btoa/Buffer, 浏览器与 node 通吃) */
|
|
143
|
+
function bytesToBase64(bytes) {
|
|
144
|
+
let out = '';
|
|
145
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
146
|
+
const b0 = bytes[i];
|
|
147
|
+
const b1 = i + 1 < bytes.length ? bytes[i + 1] : undefined;
|
|
148
|
+
const b2 = i + 2 < bytes.length ? bytes[i + 2] : undefined;
|
|
149
|
+
out += B64_CHARS[b0 >> 2];
|
|
150
|
+
out += B64_CHARS[((b0 & 3) << 4) | ((b1 ?? 0) >> 4)];
|
|
151
|
+
out += b1 === undefined ? '=' : B64_CHARS[((b1 & 15) << 2) | ((b2 ?? 0) >> 6)];
|
|
152
|
+
out += b2 === undefined ? '=' : B64_CHARS[b2 & 63];
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
/** UTF-8 字符串 → base64 */
|
|
157
|
+
export function base64EncodeUtf8(str) {
|
|
158
|
+
return bytesToBase64(new TextEncoder().encode(str));
|
|
159
|
+
}
|
|
160
|
+
/** 人类单位金额 → 最小单位 (bigint); 非法金额抛错 (调用方已包 ok:false) */
|
|
161
|
+
export function toBaseUnits(amount, decimals) {
|
|
162
|
+
const s = typeof amount === 'number' ? String(amount) : String(amount ?? '').trim();
|
|
163
|
+
if (!/^\d+(\.\d+)?$/.test(s))
|
|
164
|
+
throw new Error(`无效金额: ${amount}`);
|
|
165
|
+
const [intPart, fracPart = ''] = s.split('.');
|
|
166
|
+
const paddedFrac = (fracPart + '0'.repeat(decimals)).slice(0, decimals);
|
|
167
|
+
return BigInt(intPart) * 10n ** BigInt(decimals) + BigInt(paddedFrac || '0');
|
|
168
|
+
}
|
|
169
|
+
/** 归一化网络名 (支持 'base' 与 'eip155:8453') */
|
|
170
|
+
export function normalizeNetwork(network) {
|
|
171
|
+
const n = (network || DEFAULT_CHAIN_CONFIG.network).trim();
|
|
172
|
+
if (NETWORKS[n])
|
|
173
|
+
return n;
|
|
174
|
+
if (n.startsWith('eip155:')) {
|
|
175
|
+
const chainId = parseInt(n.split(':')[1], 10);
|
|
176
|
+
const found = Object.keys(NETWORKS).find((k) => NETWORKS[k].chainId === chainId);
|
|
177
|
+
if (found)
|
|
178
|
+
return found;
|
|
179
|
+
}
|
|
180
|
+
throw new Error(`不支持的链上网络: ${network}`);
|
|
181
|
+
}
|
|
182
|
+
/** 解析某网络下的代币元数据 */
|
|
183
|
+
export function resolveToken(network, symbol = 'USDC') {
|
|
184
|
+
const table = TOKENS[network];
|
|
185
|
+
if (!table)
|
|
186
|
+
return undefined;
|
|
187
|
+
return table[symbol.toUpperCase()];
|
|
188
|
+
}
|
|
189
|
+
// ============================================================
|
|
190
|
+
// 配置持久化
|
|
191
|
+
// ============================================================
|
|
192
|
+
/** 读取链上配置 (缺省 = Base mainnet 8453 + USDC) */
|
|
193
|
+
export function getChainConfig(storage) {
|
|
194
|
+
try {
|
|
195
|
+
const raw = resolveStorage(storage).getItem(CONFIG_KEY);
|
|
196
|
+
if (!raw)
|
|
197
|
+
return { ...DEFAULT_CHAIN_CONFIG };
|
|
198
|
+
const parsed = JSON.parse(raw);
|
|
199
|
+
if (!parsed || typeof parsed !== 'object')
|
|
200
|
+
return { ...DEFAULT_CHAIN_CONFIG };
|
|
201
|
+
return { ...DEFAULT_CHAIN_CONFIG, ...parsed };
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return { ...DEFAULT_CHAIN_CONFIG };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** 写入链上配置 (与默认值/既有值合并) */
|
|
208
|
+
export function setChainConfig(cfg, storage) {
|
|
209
|
+
const merged = { ...getChainConfig(storage), ...(cfg || {}) };
|
|
210
|
+
try {
|
|
211
|
+
resolveStorage(storage).setItem(CONFIG_KEY, JSON.stringify(merged));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
/* 存储不可用时仅返回内存态, 不抛 */
|
|
215
|
+
}
|
|
216
|
+
return merged;
|
|
217
|
+
}
|
|
218
|
+
// ============================================================
|
|
219
|
+
// JSON-RPC over fetch
|
|
220
|
+
// ============================================================
|
|
221
|
+
let rpcSeq = 1;
|
|
222
|
+
/**
|
|
223
|
+
* 发一条 JSON-RPC 请求, 返回 result。
|
|
224
|
+
* RPC 报错 / 网络异常 / 无 result → 抛可捕获 Error (上层包 ok:false)。
|
|
225
|
+
*/
|
|
226
|
+
export async function rpcRequest(method, params = [], opts = {}) {
|
|
227
|
+
const rpcUrl = opts.rpcUrl || getChainConfig().rpcUrl || DEFAULT_CHAIN_CONFIG.rpcUrl;
|
|
228
|
+
const f = resolveFetch(opts.fetchImpl);
|
|
229
|
+
const id = rpcSeq++;
|
|
230
|
+
const res = await f(rpcUrl, {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: { 'content-type': 'application/json' },
|
|
233
|
+
body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
|
|
234
|
+
});
|
|
235
|
+
if (!res || typeof res.json !== 'function') {
|
|
236
|
+
throw new Error('RPC 响应无效 (无 json())');
|
|
237
|
+
}
|
|
238
|
+
const json = (await res.json());
|
|
239
|
+
if (!json)
|
|
240
|
+
throw new Error('RPC 空响应');
|
|
241
|
+
if (json.error) {
|
|
242
|
+
const code = json.error.code !== undefined ? `${json.error.code} ` : '';
|
|
243
|
+
throw new Error(`RPC 错误: ${code}${json.error.message || '未知错误'}`);
|
|
244
|
+
}
|
|
245
|
+
return json.result;
|
|
246
|
+
}
|
|
247
|
+
// ============================================================
|
|
248
|
+
// 账户
|
|
249
|
+
// ============================================================
|
|
250
|
+
/** 私钥 → 账户 (确定性地址)。失败返回 { ok:false, error }。 */
|
|
251
|
+
export function accountFromPrivateKey(privateKey) {
|
|
252
|
+
try {
|
|
253
|
+
if (!privateKey || typeof privateKey !== 'string')
|
|
254
|
+
throw new Error('私钥不能为空');
|
|
255
|
+
const account = privateKeyToAccount(normalizePrivateKey(privateKey));
|
|
256
|
+
return { ok: true, address: account.address, account };
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
return { ok: false, error: `解析私钥失败: ${errMsg(e)}` };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// ============================================================
|
|
263
|
+
// x402 支付授权 (EIP-712 / EIP-3009)
|
|
264
|
+
// ============================================================
|
|
265
|
+
/**
|
|
266
|
+
* 用私钥签 x402 支付授权, 产出可直接放进 HTTP 头的 X-PAYMENT。
|
|
267
|
+
*
|
|
268
|
+
* payload 形状与 @x402 一致:
|
|
269
|
+
* { x402Version: 2, payload: { authorization, signature } }
|
|
270
|
+
* header = base64(JSON.stringify(payload))
|
|
271
|
+
*/
|
|
272
|
+
export async function signX402Authorization(params) {
|
|
273
|
+
try {
|
|
274
|
+
const { privateKey, to, amount } = params;
|
|
275
|
+
if (!to)
|
|
276
|
+
throw new Error('收款地址 to 不能为空');
|
|
277
|
+
const currency = (params.currency || DEFAULT_CHAIN_CONFIG.token || 'USDC').toUpperCase();
|
|
278
|
+
const network = normalizeNetwork(params.network);
|
|
279
|
+
const net = NETWORKS[network];
|
|
280
|
+
const token = resolveToken(network, currency);
|
|
281
|
+
if (!token)
|
|
282
|
+
throw new Error(`网络 ${network} 不支持代币 ${currency}`);
|
|
283
|
+
const account = privateKeyToAccount(normalizePrivateKey(privateKey));
|
|
284
|
+
const value = toBaseUnits(amount, token.decimals);
|
|
285
|
+
const now = typeof params.now === 'number' ? params.now : Math.floor(Date.now() / 1000);
|
|
286
|
+
const validFor = typeof params.validForSec === 'number' ? params.validForSec : DEFAULT_VALID_FOR_SEC;
|
|
287
|
+
const authorization = {
|
|
288
|
+
from: account.address,
|
|
289
|
+
to: getAddress(to),
|
|
290
|
+
value: value.toString(),
|
|
291
|
+
validAfter: '0',
|
|
292
|
+
validBefore: String(now + validFor),
|
|
293
|
+
nonce: params.nonce || randomNonceHex(),
|
|
294
|
+
};
|
|
295
|
+
const domain = {
|
|
296
|
+
name: token.name,
|
|
297
|
+
version: token.version,
|
|
298
|
+
chainId: net.chainId,
|
|
299
|
+
verifyingContract: getAddress(token.asset),
|
|
300
|
+
};
|
|
301
|
+
const message = {
|
|
302
|
+
from: getAddress(authorization.from),
|
|
303
|
+
to: getAddress(authorization.to),
|
|
304
|
+
value: BigInt(authorization.value),
|
|
305
|
+
validAfter: BigInt(authorization.validAfter),
|
|
306
|
+
validBefore: BigInt(authorization.validBefore),
|
|
307
|
+
nonce: authorization.nonce,
|
|
308
|
+
};
|
|
309
|
+
const signature = await account.signTypedData({
|
|
310
|
+
domain,
|
|
311
|
+
types: AUTHORIZATION_TYPES,
|
|
312
|
+
primaryType: 'TransferWithAuthorization',
|
|
313
|
+
message,
|
|
314
|
+
});
|
|
315
|
+
const x402Version = params.x402Version ?? 2;
|
|
316
|
+
const header = base64EncodeUtf8(JSON.stringify({ x402Version, payload: { authorization, signature } }));
|
|
317
|
+
return {
|
|
318
|
+
ok: true,
|
|
319
|
+
authorization,
|
|
320
|
+
signature,
|
|
321
|
+
header,
|
|
322
|
+
x402Version,
|
|
323
|
+
typedData: {
|
|
324
|
+
domain,
|
|
325
|
+
types: AUTHORIZATION_TYPES,
|
|
326
|
+
primaryType: 'TransferWithAuthorization',
|
|
327
|
+
message: authorization,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
return { ok: false, error: `签 x402 授权失败: ${errMsg(e)}` };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async function buildSignAndSend(p) {
|
|
336
|
+
const account = privateKeyToAccount(normalizePrivateKey(p.privateKey));
|
|
337
|
+
const opts = { rpcUrl: p.rpcUrl, fetchImpl: p.fetchImpl };
|
|
338
|
+
const nonceHex = await rpcRequest('eth_getTransactionCount', [account.address, 'pending'], opts);
|
|
339
|
+
const nonce = Number(BigInt(nonceHex || '0x0'));
|
|
340
|
+
let maxFeePerGas = DEFAULT_MAX_FEE_PER_GAS;
|
|
341
|
+
let maxPriorityFeePerGas = DEFAULT_MAX_PRIORITY_FEE_PER_GAS;
|
|
342
|
+
try {
|
|
343
|
+
const gasPriceHex = await rpcRequest('eth_gasPrice', [], opts);
|
|
344
|
+
if (gasPriceHex) {
|
|
345
|
+
maxPriorityFeePerGas = BigInt(gasPriceHex);
|
|
346
|
+
maxFeePerGas = maxPriorityFeePerGas * 2n;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
/* 用默认费, 不阻塞 */
|
|
351
|
+
}
|
|
352
|
+
let gas = p.gasLimit ?? DEFAULT_ERC20_GAS_LIMIT;
|
|
353
|
+
try {
|
|
354
|
+
const estHex = await rpcRequest('eth_estimateGas', [{ from: account.address, to: p.to, data: p.data, value: numberToHex(p.value ?? 0n) }], opts);
|
|
355
|
+
if (estHex)
|
|
356
|
+
gas = (BigInt(estHex) * 12n) / 10n; // +20% 缓冲
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
/* 用默认 gas, 不阻塞 */
|
|
360
|
+
}
|
|
361
|
+
const raw = await account.signTransaction({
|
|
362
|
+
chainId: p.chainId,
|
|
363
|
+
to: getAddress(p.to),
|
|
364
|
+
data: p.data,
|
|
365
|
+
value: p.value ?? 0n,
|
|
366
|
+
nonce,
|
|
367
|
+
gas,
|
|
368
|
+
maxFeePerGas,
|
|
369
|
+
maxPriorityFeePerGas,
|
|
370
|
+
type: 'eip1559',
|
|
371
|
+
});
|
|
372
|
+
const txHash = await rpcRequest('eth_sendRawTransaction', [raw], opts);
|
|
373
|
+
return { raw, txHash: String(txHash) };
|
|
374
|
+
}
|
|
375
|
+
/** 自己构造 + 签名 + 广播一笔 ERC-20 transfer */
|
|
376
|
+
export async function erc20Transfer(params) {
|
|
377
|
+
try {
|
|
378
|
+
const network = normalizeNetwork(params.network);
|
|
379
|
+
const net = NETWORKS[network];
|
|
380
|
+
const chainId = params.chainId ?? net.chainId;
|
|
381
|
+
let tokenAddress = params.token;
|
|
382
|
+
let decimals = params.decimals;
|
|
383
|
+
if (!tokenAddress || decimals === undefined) {
|
|
384
|
+
const meta = resolveToken(network, DEFAULT_CHAIN_CONFIG.token || 'USDC');
|
|
385
|
+
if (meta) {
|
|
386
|
+
decimals = decimals ?? meta.decimals;
|
|
387
|
+
tokenAddress = tokenAddress || meta.asset;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!tokenAddress)
|
|
391
|
+
throw new Error(`无法确定代币合约 (network=${network})`);
|
|
392
|
+
if (decimals === undefined)
|
|
393
|
+
decimals = 6;
|
|
394
|
+
const data = encodeFunctionData({
|
|
395
|
+
abi: ERC20_TRANSFER_ABI,
|
|
396
|
+
functionName: 'transfer',
|
|
397
|
+
args: [getAddress(params.to), toBaseUnits(params.amount, decimals)],
|
|
398
|
+
});
|
|
399
|
+
const { raw, txHash } = await buildSignAndSend({
|
|
400
|
+
privateKey: params.privateKey,
|
|
401
|
+
to: tokenAddress,
|
|
402
|
+
data,
|
|
403
|
+
value: 0n,
|
|
404
|
+
chainId,
|
|
405
|
+
rpcUrl: params.rpcUrl,
|
|
406
|
+
fetchImpl: params.fetchImpl,
|
|
407
|
+
gasLimit: DEFAULT_ERC20_GAS_LIMIT,
|
|
408
|
+
});
|
|
409
|
+
return { ok: true, txHash, raw };
|
|
410
|
+
}
|
|
411
|
+
catch (e) {
|
|
412
|
+
return { ok: false, error: `ERC-20 转账失败: ${errMsg(e)}` };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/** 自己上链把资源铸成 ResourceERC721 (calldata = mint(address,uint256,string)) */
|
|
416
|
+
export async function mintResourceToken(params) {
|
|
417
|
+
try {
|
|
418
|
+
const network = normalizeNetwork(params.network);
|
|
419
|
+
const net = NETWORKS[network];
|
|
420
|
+
const chainId = params.chainId ?? net.chainId;
|
|
421
|
+
const tokenIdStr = String(params.tokenId);
|
|
422
|
+
const tokenUri = params.tokenUri.startsWith('ipfs://') || params.tokenUri.includes('://')
|
|
423
|
+
? params.tokenUri
|
|
424
|
+
: `ipfs://${params.tokenUri}`;
|
|
425
|
+
const data = encodeFunctionData({
|
|
426
|
+
abi: RESOURCE_ERC721_ABI,
|
|
427
|
+
functionName: 'mint',
|
|
428
|
+
args: [getAddress(params.to), BigInt(params.tokenId), tokenUri],
|
|
429
|
+
});
|
|
430
|
+
const { raw, txHash } = await buildSignAndSend({
|
|
431
|
+
privateKey: params.privateKey,
|
|
432
|
+
to: params.contract,
|
|
433
|
+
data,
|
|
434
|
+
value: 0n,
|
|
435
|
+
chainId,
|
|
436
|
+
rpcUrl: params.rpcUrl,
|
|
437
|
+
fetchImpl: params.fetchImpl,
|
|
438
|
+
gasLimit: DEFAULT_MINT_GAS_LIMIT,
|
|
439
|
+
});
|
|
440
|
+
return { ok: true, txHash, raw, data, tokenId: tokenIdStr, tokenUri };
|
|
441
|
+
}
|
|
442
|
+
catch (e) {
|
|
443
|
+
return { ok: false, error: `上链铸资源失败: ${errMsg(e)}` };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/** 由 agentId + serviceName 确定性派生 tokenId */
|
|
447
|
+
export function deriveTokenId(agentId, serviceName) {
|
|
448
|
+
return BigInt(keccak256(toHex(`${agentId}:${serviceName}`)));
|
|
449
|
+
}
|
|
450
|
+
/** 把服务/资源注册上链 (内部调 mintResourceToken, tokenUri = ipfs://<cid>) */
|
|
451
|
+
export async function registerServiceOnChain(params) {
|
|
452
|
+
try {
|
|
453
|
+
const account = privateKeyToAccount(normalizePrivateKey(params.privateKey));
|
|
454
|
+
const to = params.to ? getAddress(params.to) : account.address;
|
|
455
|
+
const tokenId = params.tokenId !== undefined ? params.tokenId : deriveTokenId(params.agentId, params.serviceName);
|
|
456
|
+
const cid = params.cid.startsWith('ipfs://') ? params.cid : `ipfs://${params.cid}`;
|
|
457
|
+
const mint = await mintResourceToken({
|
|
458
|
+
privateKey: params.privateKey,
|
|
459
|
+
contract: params.contract,
|
|
460
|
+
to,
|
|
461
|
+
tokenId,
|
|
462
|
+
tokenUri: cid,
|
|
463
|
+
network: params.network,
|
|
464
|
+
rpcUrl: params.rpcUrl,
|
|
465
|
+
fetchImpl: params.fetchImpl,
|
|
466
|
+
chainId: params.chainId,
|
|
467
|
+
});
|
|
468
|
+
if (!mint.ok)
|
|
469
|
+
return { ...mint, agentId: params.agentId, serviceName: params.serviceName };
|
|
470
|
+
return {
|
|
471
|
+
...mint,
|
|
472
|
+
agentId: params.agentId,
|
|
473
|
+
serviceName: params.serviceName,
|
|
474
|
+
tokenId: String(tokenId),
|
|
475
|
+
tokenUri: cid,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
catch (e) {
|
|
479
|
+
return { ok: false, error: `注册服务上链失败: ${errMsg(e)}` };
|
|
480
|
+
}
|
|
481
|
+
}
|