four-flap-meme-sdk 1.5.23 → 1.5.24
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/flap/portal-bundle-merkle/encryption.d.ts +16 -0
- package/dist/flap/portal-bundle-merkle/encryption.js +146 -0
- package/dist/xlayer/aa-account.d.ts +1 -32
- package/dist/xlayer/aa-account.js +46 -184
- package/dist/xlayer/bundle.js +136 -182
- package/dist/xlayer/bundler.js +11 -30
- package/dist/xlayer/portal-ops.d.ts +0 -1
- package/dist/xlayer/portal-ops.js +8 -125
- package/dist/xlayer/types.d.ts +0 -20
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECDH + AES-GCM 加密工具(浏览器兼容)
|
|
3
|
+
* 用于将签名交易用服务器公钥加密
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 用服务器公钥加密签名交易(ECDH + AES-GCM)
|
|
7
|
+
*
|
|
8
|
+
* @param signedTransactions 签名后的交易数组
|
|
9
|
+
* @param publicKeyBase64 服务器提供的公钥(Base64 格式)
|
|
10
|
+
* @returns JSON 字符串 {e: 临时公钥, i: IV, d: 密文}
|
|
11
|
+
*/
|
|
12
|
+
export declare function encryptWithPublicKey(signedTransactions: string[], publicKeyBase64: string): Promise<string>;
|
|
13
|
+
/**
|
|
14
|
+
* 验证公钥格式(Base64)
|
|
15
|
+
*/
|
|
16
|
+
export declare function validatePublicKey(publicKeyBase64: string): boolean;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECDH + AES-GCM 加密工具(浏览器兼容)
|
|
3
|
+
* 用于将签名交易用服务器公钥加密
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 获取全局 crypto 对象(最简单直接的方式)
|
|
7
|
+
*/
|
|
8
|
+
function getCryptoAPI() {
|
|
9
|
+
// 尝试所有可能的全局对象,优先浏览器环境
|
|
10
|
+
const cryptoObj = (typeof window !== 'undefined' && window.crypto) ||
|
|
11
|
+
(typeof self !== 'undefined' && self.crypto) ||
|
|
12
|
+
(typeof global !== 'undefined' && global.crypto) ||
|
|
13
|
+
(typeof globalThis !== 'undefined' && globalThis.crypto);
|
|
14
|
+
if (!cryptoObj) {
|
|
15
|
+
const env = typeof window !== 'undefined' ? 'Browser' : 'Node.js';
|
|
16
|
+
const protocol = typeof location !== 'undefined' ? location.protocol : 'unknown';
|
|
17
|
+
throw new Error(`❌ Crypto API 不可用。环境: ${env}, 协议: ${protocol}. ` +
|
|
18
|
+
'请确保在 HTTPS 或 localhost 下运行');
|
|
19
|
+
}
|
|
20
|
+
return cryptoObj;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 获取 SubtleCrypto(用于加密操作)
|
|
24
|
+
*/
|
|
25
|
+
function getSubtleCrypto() {
|
|
26
|
+
const crypto = getCryptoAPI();
|
|
27
|
+
if (!crypto.subtle) {
|
|
28
|
+
const protocol = typeof location !== 'undefined' ? location.protocol : 'unknown';
|
|
29
|
+
const hostname = typeof location !== 'undefined' ? location.hostname : 'unknown';
|
|
30
|
+
throw new Error(`❌ SubtleCrypto API 不可用。协议: ${protocol}, 主机: ${hostname}. ` +
|
|
31
|
+
'请确保:1) 使用 HTTPS (或 localhost);2) 浏览器支持 Web Crypto API;' +
|
|
32
|
+
'3) 不在无痕/隐私浏览模式下');
|
|
33
|
+
}
|
|
34
|
+
return crypto.subtle;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Base64 转 ArrayBuffer(优先使用浏览器 API)
|
|
38
|
+
*/
|
|
39
|
+
function base64ToArrayBuffer(base64) {
|
|
40
|
+
// 浏览器环境(优先)
|
|
41
|
+
if (typeof atob !== 'undefined') {
|
|
42
|
+
const binaryString = atob(base64);
|
|
43
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
44
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
45
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
46
|
+
}
|
|
47
|
+
return bytes.buffer;
|
|
48
|
+
}
|
|
49
|
+
// Node.js 环境(fallback)
|
|
50
|
+
if (typeof Buffer !== 'undefined') {
|
|
51
|
+
return Buffer.from(base64, 'base64').buffer;
|
|
52
|
+
}
|
|
53
|
+
throw new Error('❌ Base64 解码不可用');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* ArrayBuffer 转 Base64(优先使用浏览器 API)
|
|
57
|
+
*/
|
|
58
|
+
function arrayBufferToBase64(buffer) {
|
|
59
|
+
// 浏览器环境(优先)
|
|
60
|
+
if (typeof btoa !== 'undefined') {
|
|
61
|
+
const bytes = new Uint8Array(buffer);
|
|
62
|
+
let binary = '';
|
|
63
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
64
|
+
binary += String.fromCharCode(bytes[i]);
|
|
65
|
+
}
|
|
66
|
+
return btoa(binary);
|
|
67
|
+
}
|
|
68
|
+
// Node.js 环境(fallback)
|
|
69
|
+
if (typeof Buffer !== 'undefined') {
|
|
70
|
+
return Buffer.from(buffer).toString('base64');
|
|
71
|
+
}
|
|
72
|
+
throw new Error('❌ Base64 编码不可用');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 生成随机 Hex 字符串
|
|
76
|
+
*/
|
|
77
|
+
function randomHex(length) {
|
|
78
|
+
const crypto = getCryptoAPI();
|
|
79
|
+
const array = new Uint8Array(length);
|
|
80
|
+
crypto.getRandomValues(array);
|
|
81
|
+
return Array.from(array)
|
|
82
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
83
|
+
.join('');
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 用服务器公钥加密签名交易(ECDH + AES-GCM)
|
|
87
|
+
*
|
|
88
|
+
* @param signedTransactions 签名后的交易数组
|
|
89
|
+
* @param publicKeyBase64 服务器提供的公钥(Base64 格式)
|
|
90
|
+
* @returns JSON 字符串 {e: 临时公钥, i: IV, d: 密文}
|
|
91
|
+
*/
|
|
92
|
+
export async function encryptWithPublicKey(signedTransactions, publicKeyBase64) {
|
|
93
|
+
try {
|
|
94
|
+
// 0. 获取 SubtleCrypto 和 Crypto API
|
|
95
|
+
const subtle = getSubtleCrypto();
|
|
96
|
+
const crypto = getCryptoAPI();
|
|
97
|
+
// 1. 准备数据
|
|
98
|
+
const payload = {
|
|
99
|
+
signedTransactions,
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
nonce: randomHex(8)
|
|
102
|
+
};
|
|
103
|
+
const plaintext = JSON.stringify(payload);
|
|
104
|
+
// 2. 生成临时 ECDH 密钥对
|
|
105
|
+
const ephemeralKeyPair = await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey']);
|
|
106
|
+
// 3. 导入服务器公钥
|
|
107
|
+
const publicKeyBuffer = base64ToArrayBuffer(publicKeyBase64);
|
|
108
|
+
const publicKey = await subtle.importKey('raw', publicKeyBuffer, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
|
|
109
|
+
// 4. 派生共享密钥(AES-256)
|
|
110
|
+
const sharedKey = await subtle.deriveKey({ name: 'ECDH', public: publicKey }, ephemeralKeyPair.privateKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
|
|
111
|
+
// 5. AES-GCM 加密
|
|
112
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
113
|
+
const encrypted = await subtle.encrypt({ name: 'AES-GCM', iv }, sharedKey, new TextEncoder().encode(plaintext));
|
|
114
|
+
// 6. 导出临时公钥
|
|
115
|
+
const ephemeralPublicKeyRaw = await subtle.exportKey('raw', ephemeralKeyPair.publicKey);
|
|
116
|
+
// 7. 返回加密包(JSON 格式)
|
|
117
|
+
return JSON.stringify({
|
|
118
|
+
e: arrayBufferToBase64(ephemeralPublicKeyRaw), // 临时公钥
|
|
119
|
+
i: arrayBufferToBase64(iv.buffer), // IV
|
|
120
|
+
d: arrayBufferToBase64(encrypted) // 密文
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
throw new Error(`加密失败: ${error?.message || String(error)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 验证公钥格式(Base64)
|
|
129
|
+
*/
|
|
130
|
+
export function validatePublicKey(publicKeyBase64) {
|
|
131
|
+
try {
|
|
132
|
+
if (!publicKeyBase64)
|
|
133
|
+
return false;
|
|
134
|
+
// Base64 字符集验证
|
|
135
|
+
if (!/^[A-Za-z0-9+/=]+$/.test(publicKeyBase64))
|
|
136
|
+
return false;
|
|
137
|
+
// ECDH P-256 公钥固定长度 65 字节(未压缩)
|
|
138
|
+
// Base64 编码后约 88 字符
|
|
139
|
+
if (publicKeyBase64.length < 80 || publicKeyBase64.length > 100)
|
|
140
|
+
return false;
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - initCode 生成
|
|
9
9
|
*/
|
|
10
10
|
import { ethers, JsonRpcProvider, Wallet, Contract } from 'ethers';
|
|
11
|
-
import type { UserOperation, SignedUserOp, AAAccount, XLayerConfig
|
|
11
|
+
import type { UserOperation, SignedUserOp, AAAccount, XLayerConfig } from './types.js';
|
|
12
12
|
import { BundlerClient } from './bundler.js';
|
|
13
13
|
/**
|
|
14
14
|
* AA 账户管理器
|
|
@@ -33,11 +33,6 @@ export declare class AAAccountManager {
|
|
|
33
33
|
private paymasterData?;
|
|
34
34
|
private gasLimitMultiplier;
|
|
35
35
|
private senderCache;
|
|
36
|
-
private deployedSenderSet;
|
|
37
|
-
private feeDataCache?;
|
|
38
|
-
private readonly feeDataCacheTtlMs;
|
|
39
|
-
private defaultGasPolicy?;
|
|
40
|
-
private defaultFixedGas?;
|
|
41
36
|
constructor(config?: XLayerConfig);
|
|
42
37
|
/**
|
|
43
38
|
* 获取 Provider
|
|
@@ -132,23 +127,6 @@ export declare class AAAccountManager {
|
|
|
132
127
|
userOp: UserOperation;
|
|
133
128
|
prefundWei: bigint;
|
|
134
129
|
}>;
|
|
135
|
-
/**
|
|
136
|
-
* 构建未签名的 UserOperation(固定 Gas,不做任何 estimate)
|
|
137
|
-
*
|
|
138
|
-
* 适用于大规模(1000 地址)场景:尽量减少 RPC 调用量。
|
|
139
|
-
*/
|
|
140
|
-
buildUserOpWithFixedGas(params: {
|
|
141
|
-
ownerWallet: Wallet;
|
|
142
|
-
sender: string;
|
|
143
|
-
callData: string;
|
|
144
|
-
nonce: bigint;
|
|
145
|
-
initCode?: string;
|
|
146
|
-
deployed?: boolean;
|
|
147
|
-
fixedGas?: FixedGasConfig;
|
|
148
|
-
}): Promise<{
|
|
149
|
-
userOp: UserOperation;
|
|
150
|
-
prefundWei: bigint;
|
|
151
|
-
}>;
|
|
152
130
|
/**
|
|
153
131
|
* 签名 UserOperation
|
|
154
132
|
*/
|
|
@@ -160,8 +138,6 @@ export declare class AAAccountManager {
|
|
|
160
138
|
ownerWallet: Wallet;
|
|
161
139
|
callData: string;
|
|
162
140
|
value?: bigint;
|
|
163
|
-
gasPolicy?: GasPolicy;
|
|
164
|
-
fixedGas?: FixedGasConfig;
|
|
165
141
|
useBundlerEstimate?: boolean;
|
|
166
142
|
callGasLimit?: bigint;
|
|
167
143
|
}): Promise<SignedUserOp>;
|
|
@@ -183,13 +159,6 @@ export declare class AAAccountManager {
|
|
|
183
159
|
*/
|
|
184
160
|
getMultipleAccountInfo(ownerAddresses: string[]): Promise<AAAccount[]>;
|
|
185
161
|
}
|
|
186
|
-
/**
|
|
187
|
-
* ERC-4337 v0.6 userOpHash 本地计算
|
|
188
|
-
*
|
|
189
|
-
* pack 规则(v0.6):对 (sender,nonce,keccak(initCode),keccak(callData),callGasLimit,verificationGasLimit,preVerificationGas,maxFeePerGas,maxPriorityFeePerGas,keccak(paymasterAndData))
|
|
190
|
-
* 做 abi.encode 后 keccak 得到 packHash;再 abi.encode(packHash, entryPoint, chainId) 后 keccak 得到 userOpHash。
|
|
191
|
-
*/
|
|
192
|
-
export declare function computeUserOpHashV06(userOp: UserOperation, entryPoint: string, chainId: number | bigint): string;
|
|
193
162
|
/**
|
|
194
163
|
* 编码 SimpleAccount.execute 调用
|
|
195
164
|
*/
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - initCode 生成
|
|
9
9
|
*/
|
|
10
10
|
import { ethers, JsonRpcProvider, Wallet, Contract, Interface } from 'ethers';
|
|
11
|
-
import { XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, DEFAULT_SALT, DEFAULT_GAS_PRICE,
|
|
11
|
+
import { XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, DEFAULT_SALT, DEFAULT_GAS_PRICE, GAS_LIMIT_MULTIPLIER, VERIFICATION_GAS_LIMIT_DEPLOY, VERIFICATION_GAS_LIMIT_NORMAL, PRE_VERIFICATION_GAS, FACTORY_ABI, ENTRYPOINT_ABI, SIMPLE_ACCOUNT_ABI, ZERO_ADDRESS, MULTICALL3, } from './constants.js';
|
|
12
12
|
import { BundlerClient } from './bundler.js';
|
|
13
13
|
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
14
14
|
// ============================================================================
|
|
@@ -26,16 +26,12 @@ import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
|
26
26
|
export class AAAccountManager {
|
|
27
27
|
constructor(config = {}) {
|
|
28
28
|
this.senderCache = new Map(); // key: ownerLower -> sender
|
|
29
|
-
// PERF: 本文件的改造会尽量减少逐地址 RPC 调用
|
|
30
|
-
this.deployedSenderSet = new Set(); // key: senderLower(只缓存 deployed=true)
|
|
31
|
-
this.feeDataCacheTtlMs = 1200;
|
|
32
29
|
this.chainId = config.chainId ?? XLAYER_CHAIN_ID;
|
|
33
30
|
const rpcUrl = config.rpcUrl ?? DEFAULT_RPC_URL;
|
|
34
31
|
this.rpcUrl = rpcUrl;
|
|
35
|
-
this.provider = new JsonRpcProvider(rpcUrl, {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
batchStallTime: 30,
|
|
32
|
+
this.provider = new JsonRpcProvider(rpcUrl, {
|
|
33
|
+
chainId: this.chainId,
|
|
34
|
+
name: 'xlayer',
|
|
39
35
|
});
|
|
40
36
|
this.factoryAddress = config.factory ?? SIMPLE_ACCOUNT_FACTORY;
|
|
41
37
|
this.entryPointAddress = config.entryPoint ?? ENTRYPOINT_V06;
|
|
@@ -43,9 +39,6 @@ export class AAAccountManager {
|
|
|
43
39
|
this.paymaster = config.paymaster;
|
|
44
40
|
this.paymasterData = config.paymasterData;
|
|
45
41
|
this.gasLimitMultiplier = config.gasLimitMultiplier ?? GAS_LIMIT_MULTIPLIER;
|
|
46
|
-
// 默认 fixed:最大化降低 RPC / bundler 请求;如需更稳可显式传 bundlerEstimate
|
|
47
|
-
this.defaultGasPolicy = config.gasPolicy ?? 'fixed';
|
|
48
|
-
this.defaultFixedGas = config.fixedGas;
|
|
49
42
|
this.factory = new Contract(this.factoryAddress, FACTORY_ABI, this.provider);
|
|
50
43
|
this.entryPoint = new Contract(this.entryPointAddress, ENTRYPOINT_ABI, this.provider);
|
|
51
44
|
this.bundler = new BundlerClient({
|
|
@@ -83,14 +76,7 @@ export class AAAccountManager {
|
|
|
83
76
|
* 获取当前 Fee Data
|
|
84
77
|
*/
|
|
85
78
|
async getFeeData() {
|
|
86
|
-
|
|
87
|
-
const now = Date.now();
|
|
88
|
-
const hit = this.feeDataCache;
|
|
89
|
-
if (hit && hit.expiresAt > now)
|
|
90
|
-
return hit.value;
|
|
91
|
-
const value = await this.provider.getFeeData();
|
|
92
|
-
this.feeDataCache = { expiresAt: now + this.feeDataCacheTtlMs, value };
|
|
93
|
-
return value;
|
|
79
|
+
return await this.provider.getFeeData();
|
|
94
80
|
}
|
|
95
81
|
/**
|
|
96
82
|
* 预测 AA Sender 地址
|
|
@@ -289,7 +275,7 @@ export class AAAccountManager {
|
|
|
289
275
|
* 构建未签名的 UserOperation(使用 Bundler 估算 Gas)
|
|
290
276
|
*/
|
|
291
277
|
async buildUserOpWithBundlerEstimate(params) {
|
|
292
|
-
const feeData = await this.getFeeData();
|
|
278
|
+
const feeData = await this.provider.getFeeData();
|
|
293
279
|
const legacyGasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? DEFAULT_GAS_PRICE;
|
|
294
280
|
const paymasterAndData = this.buildPaymasterAndData();
|
|
295
281
|
// 构建初步 UserOp(Gas 字段为 0,等待估算)
|
|
@@ -338,7 +324,7 @@ export class AAAccountManager {
|
|
|
338
324
|
if (params.ops.length === 0) {
|
|
339
325
|
return { userOps: [], prefundWeis: [] };
|
|
340
326
|
}
|
|
341
|
-
const feeData = await this.getFeeData();
|
|
327
|
+
const feeData = await this.provider.getFeeData();
|
|
342
328
|
const legacyGasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? DEFAULT_GAS_PRICE;
|
|
343
329
|
const paymasterAndData = this.buildPaymasterAndData();
|
|
344
330
|
// 先构建 skeleton(Gas=0),然后批量估算
|
|
@@ -389,7 +375,7 @@ export class AAAccountManager {
|
|
|
389
375
|
* 适用于无法通过 Bundler 估算的场景(如尚未授权的 sell 操作)
|
|
390
376
|
*/
|
|
391
377
|
async buildUserOpWithLocalEstimate(params) {
|
|
392
|
-
const feeData = await this.getFeeData();
|
|
378
|
+
const feeData = await this.provider.getFeeData();
|
|
393
379
|
const legacyGasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? DEFAULT_GAS_PRICE;
|
|
394
380
|
const paymasterAndData = this.buildPaymasterAndData();
|
|
395
381
|
// 估算 callGasLimit
|
|
@@ -431,43 +417,11 @@ export class AAAccountManager {
|
|
|
431
417
|
const prefundWei = paymasterAndData !== '0x' ? 0n : gasTotal * legacyGasPrice;
|
|
432
418
|
return { userOp, prefundWei };
|
|
433
419
|
}
|
|
434
|
-
/**
|
|
435
|
-
* 构建未签名的 UserOperation(固定 Gas,不做任何 estimate)
|
|
436
|
-
*
|
|
437
|
-
* 适用于大规模(1000 地址)场景:尽量减少 RPC 调用量。
|
|
438
|
-
*/
|
|
439
|
-
async buildUserOpWithFixedGas(params) {
|
|
440
|
-
const feeData = await this.getFeeData();
|
|
441
|
-
const legacyGasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? DEFAULT_GAS_PRICE;
|
|
442
|
-
const paymasterAndData = this.buildPaymasterAndData();
|
|
443
|
-
const callGasLimit = params.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_SELL;
|
|
444
|
-
const verificationGasLimit = params.deployed
|
|
445
|
-
? (params.fixedGas?.verificationGasLimitDeployed ?? VERIFICATION_GAS_LIMIT_NORMAL)
|
|
446
|
-
: (params.fixedGas?.verificationGasLimitUndeployed ?? VERIFICATION_GAS_LIMIT_DEPLOY);
|
|
447
|
-
const preVerificationGas = params.fixedGas?.preVerificationGas ?? PRE_VERIFICATION_GAS;
|
|
448
|
-
const userOp = {
|
|
449
|
-
sender: params.sender,
|
|
450
|
-
nonce: ethers.toBeHex(params.nonce),
|
|
451
|
-
initCode: params.initCode ?? '0x',
|
|
452
|
-
callData: params.callData,
|
|
453
|
-
callGasLimit: ethers.toBeHex(callGasLimit),
|
|
454
|
-
verificationGasLimit: ethers.toBeHex(verificationGasLimit),
|
|
455
|
-
preVerificationGas: ethers.toBeHex(preVerificationGas),
|
|
456
|
-
// XLayer 更像 legacy gasPrice 语义
|
|
457
|
-
maxFeePerGas: ethers.toBeHex(legacyGasPrice),
|
|
458
|
-
maxPriorityFeePerGas: ethers.toBeHex(legacyGasPrice),
|
|
459
|
-
paymasterAndData,
|
|
460
|
-
signature: '0x',
|
|
461
|
-
};
|
|
462
|
-
const gasTotal = callGasLimit + verificationGasLimit + preVerificationGas;
|
|
463
|
-
const prefundWei = paymasterAndData !== '0x' ? 0n : gasTotal * legacyGasPrice;
|
|
464
|
-
return { userOp, prefundWei };
|
|
465
|
-
}
|
|
466
420
|
/**
|
|
467
421
|
* 签名 UserOperation
|
|
468
422
|
*/
|
|
469
423
|
async signUserOp(userOp, ownerWallet) {
|
|
470
|
-
const userOpHash =
|
|
424
|
+
const userOpHash = await this.entryPoint.getUserOpHash(userOp);
|
|
471
425
|
const signature = await ownerWallet.signMessage(ethers.getBytes(userOpHash));
|
|
472
426
|
return {
|
|
473
427
|
userOp: { ...userOp, signature },
|
|
@@ -492,25 +446,7 @@ export class AAAccountManager {
|
|
|
492
446
|
};
|
|
493
447
|
let userOp;
|
|
494
448
|
let prefundWei;
|
|
495
|
-
|
|
496
|
-
this.defaultGasPolicy ??
|
|
497
|
-
(params.useBundlerEstimate === false ? 'localEstimate' : 'bundlerEstimate');
|
|
498
|
-
if (policy === 'fixed') {
|
|
499
|
-
const fixedGasMerged = {
|
|
500
|
-
...(this.defaultFixedGas ?? {}),
|
|
501
|
-
...(params.fixedGas ?? {}),
|
|
502
|
-
...(params.callGasLimit ? { callGasLimit: params.callGasLimit } : {}),
|
|
503
|
-
};
|
|
504
|
-
const fixedGas = Object.keys(fixedGasMerged).length > 0 ? fixedGasMerged : undefined;
|
|
505
|
-
const result = await this.buildUserOpWithFixedGas({
|
|
506
|
-
...buildParams,
|
|
507
|
-
deployed: accountInfo.deployed,
|
|
508
|
-
fixedGas,
|
|
509
|
-
});
|
|
510
|
-
userOp = result.userOp;
|
|
511
|
-
prefundWei = result.prefundWei;
|
|
512
|
-
}
|
|
513
|
-
else if (policy === 'bundlerEstimate') {
|
|
449
|
+
if (params.useBundlerEstimate !== false) {
|
|
514
450
|
const result = await this.buildUserOpWithBundlerEstimate(buildParams);
|
|
515
451
|
userOp = result.userOp;
|
|
516
452
|
prefundWei = result.prefundWei;
|
|
@@ -584,133 +520,59 @@ export class AAAccountManager {
|
|
|
584
520
|
catch { /* ignore */ }
|
|
585
521
|
}
|
|
586
522
|
}
|
|
587
|
-
// 3)
|
|
588
|
-
const
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
needCode.push({ idx: i, sender, key });
|
|
598
|
-
}
|
|
599
|
-
if (needCode.length > 0) {
|
|
600
|
-
const CHUNK = 20;
|
|
601
|
-
const chunks = [];
|
|
602
|
-
for (let c = 0; c < needCode.length; c += CHUNK)
|
|
603
|
-
chunks.push(needCode.slice(c, c + CHUNK));
|
|
604
|
-
const batchResults = await mapWithConcurrency(chunks, 2, async (chunk) => {
|
|
605
|
-
try {
|
|
606
|
-
const reqs = chunk.map((x) => ({ method: 'eth_getCode', params: [x.sender, 'latest'] }));
|
|
607
|
-
const codes = await this.rpcBatch(reqs);
|
|
608
|
-
return { ok: true, chunk, codes };
|
|
523
|
+
// 3) 批量 getCode + getBalance(JSON-RPC batch;分块避免请求体过大)
|
|
524
|
+
const codes = new Array(senders.length).fill('0x');
|
|
525
|
+
const balances = new Array(senders.length).fill(0n);
|
|
526
|
+
const CHUNK = 200;
|
|
527
|
+
for (let cursor = 0; cursor < senders.length; cursor += CHUNK) {
|
|
528
|
+
const slice = senders.slice(cursor, cursor + CHUNK);
|
|
529
|
+
try {
|
|
530
|
+
const reqs = [];
|
|
531
|
+
for (const s of slice) {
|
|
532
|
+
reqs.push({ method: 'eth_getCode', params: [s, 'latest'] });
|
|
609
533
|
}
|
|
610
|
-
|
|
611
|
-
|
|
534
|
+
for (const s of slice) {
|
|
535
|
+
reqs.push({ method: 'eth_getBalance', params: [s, 'latest'] });
|
|
612
536
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
537
|
+
const results = await this.rpcBatch(reqs);
|
|
538
|
+
const codePart = results.slice(0, slice.length);
|
|
539
|
+
const balPart = results.slice(slice.length);
|
|
540
|
+
for (let i = 0; i < slice.length; i++) {
|
|
541
|
+
const idx = cursor + i;
|
|
542
|
+
const c = codePart[i];
|
|
543
|
+
const b = balPart[i];
|
|
544
|
+
if (typeof c === 'string')
|
|
545
|
+
codes[idx] = c;
|
|
546
|
+
try {
|
|
547
|
+
balances[idx] = typeof b === 'string' ? BigInt(b) : 0n;
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
balances[idx] = 0n;
|
|
623
551
|
}
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
// fallback:单请求但受控并发
|
|
627
|
-
const fetched = await mapWithConcurrency(r.chunk, 4, async (it) => {
|
|
628
|
-
const code = await this.provider.getCode(it.sender);
|
|
629
|
-
return { it, code };
|
|
630
|
-
});
|
|
631
|
-
for (const f of fetched) {
|
|
632
|
-
const isDeployed = !!f.code && f.code !== '0x';
|
|
633
|
-
deployed[f.it.idx] = isDeployed;
|
|
634
|
-
if (isDeployed)
|
|
635
|
-
this.deployedSenderSet.add(f.it.key);
|
|
636
552
|
}
|
|
637
553
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
callData: ethBalIface.encodeFunctionData('getEthBalance', [sender]),
|
|
646
|
-
}));
|
|
647
|
-
const BAL_BATCH = 300;
|
|
648
|
-
try {
|
|
649
|
-
for (let cursor = 0; cursor < balCalls.length; cursor += BAL_BATCH) {
|
|
650
|
-
const sliceCalls = balCalls.slice(cursor, cursor + BAL_BATCH);
|
|
651
|
-
const res = await this.multicallAggregate3({ calls: sliceCalls });
|
|
652
|
-
for (let i = 0; i < res.length; i++) {
|
|
653
|
-
const r = res[i];
|
|
554
|
+
catch {
|
|
555
|
+
// fallback:节点不支持 batch 时,改为并发单请求(控制并发)
|
|
556
|
+
const fetched = await mapWithConcurrency(slice, 8, async (s) => {
|
|
557
|
+
const [code, bal] = await Promise.all([this.provider.getCode(s), this.provider.getBalance(s)]);
|
|
558
|
+
return { s, code, bal };
|
|
559
|
+
});
|
|
560
|
+
for (let i = 0; i < fetched.length; i++) {
|
|
654
561
|
const idx = cursor + i;
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
try {
|
|
658
|
-
const decoded = ethBalIface.decodeFunctionResult('getEthBalance', r.returnData);
|
|
659
|
-
balances[idx] = BigInt(decoded?.[0] ?? 0n);
|
|
660
|
-
}
|
|
661
|
-
catch { /* ignore */ }
|
|
562
|
+
codes[idx] = fetched[i].code ?? '0x';
|
|
563
|
+
balances[idx] = fetched[i].bal ?? 0n;
|
|
662
564
|
}
|
|
663
565
|
}
|
|
664
566
|
}
|
|
665
|
-
catch {
|
|
666
|
-
const fetched = await mapWithConcurrency(senders, 6, async (s) => await this.provider.getBalance(s));
|
|
667
|
-
for (let i = 0; i < fetched.length; i++)
|
|
668
|
-
balances[i] = fetched[i] ?? 0n;
|
|
669
|
-
}
|
|
670
567
|
return owners.map((owner, i) => ({
|
|
671
568
|
owner,
|
|
672
569
|
sender: senders[i],
|
|
673
|
-
deployed:
|
|
570
|
+
deployed: codes[i] != null && String(codes[i]) !== '0x',
|
|
674
571
|
balance: balances[i] ?? 0n,
|
|
675
572
|
nonce: nonces[i] ?? 0n,
|
|
676
573
|
}));
|
|
677
574
|
}
|
|
678
575
|
}
|
|
679
|
-
/**
|
|
680
|
-
* ERC-4337 v0.6 userOpHash 本地计算
|
|
681
|
-
*
|
|
682
|
-
* pack 规则(v0.6):对 (sender,nonce,keccak(initCode),keccak(callData),callGasLimit,verificationGasLimit,preVerificationGas,maxFeePerGas,maxPriorityFeePerGas,keccak(paymasterAndData))
|
|
683
|
-
* 做 abi.encode 后 keccak 得到 packHash;再 abi.encode(packHash, entryPoint, chainId) 后 keccak 得到 userOpHash。
|
|
684
|
-
*/
|
|
685
|
-
export function computeUserOpHashV06(userOp, entryPoint, chainId) {
|
|
686
|
-
const coder = ethers.AbiCoder.defaultAbiCoder();
|
|
687
|
-
const packed = coder.encode([
|
|
688
|
-
'address',
|
|
689
|
-
'uint256',
|
|
690
|
-
'bytes32',
|
|
691
|
-
'bytes32',
|
|
692
|
-
'uint256',
|
|
693
|
-
'uint256',
|
|
694
|
-
'uint256',
|
|
695
|
-
'uint256',
|
|
696
|
-
'uint256',
|
|
697
|
-
'bytes32',
|
|
698
|
-
], [
|
|
699
|
-
userOp.sender,
|
|
700
|
-
BigInt(userOp.nonce),
|
|
701
|
-
ethers.keccak256(userOp.initCode ?? '0x'),
|
|
702
|
-
ethers.keccak256(userOp.callData ?? '0x'),
|
|
703
|
-
BigInt(userOp.callGasLimit),
|
|
704
|
-
BigInt(userOp.verificationGasLimit),
|
|
705
|
-
BigInt(userOp.preVerificationGas),
|
|
706
|
-
BigInt(userOp.maxFeePerGas),
|
|
707
|
-
BigInt(userOp.maxPriorityFeePerGas),
|
|
708
|
-
ethers.keccak256(userOp.paymasterAndData ?? '0x'),
|
|
709
|
-
]);
|
|
710
|
-
const packHash = ethers.keccak256(packed);
|
|
711
|
-
const enc = coder.encode(['bytes32', 'address', 'uint256'], [packHash, entryPoint, BigInt(chainId)]);
|
|
712
|
-
return ethers.keccak256(enc);
|
|
713
|
-
}
|
|
714
576
|
// ============================================================================
|
|
715
577
|
// 工具函数
|
|
716
578
|
// ============================================================================
|