four-flap-meme-sdk 1.5.19 → 1.5.21

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.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 并发映射工具(SDK 内部使用)
3
+ * - 用于将可并行的异步任务按并发上限执行,避免 RPC/Bundler 限流或超时
4
+ */
5
+ export declare function mapWithConcurrency<T, R>(items: T[], limit: number, fn: (item: T, idx: number) => Promise<R>): Promise<R[]>;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 并发映射工具(SDK 内部使用)
3
+ * - 用于将可并行的异步任务按并发上限执行,避免 RPC/Bundler 限流或超时
4
+ */
5
+ export async function mapWithConcurrency(items, limit, fn) {
6
+ const res = new Array(items.length);
7
+ let idx = 0;
8
+ const n = Math.max(1, Math.min(Number(limit || 1), items.length || 1));
9
+ const workers = Array.from({ length: n }).map(async () => {
10
+ while (true) {
11
+ const i = idx++;
12
+ if (i >= items.length)
13
+ return;
14
+ res[i] = await fn(items[i], i);
15
+ }
16
+ });
17
+ await Promise.all(workers);
18
+ return res;
19
+ }
@@ -21,6 +21,7 @@ import { BundlerClient } from './bundler.js';
21
21
  */
22
22
  export declare class AAAccountManager {
23
23
  private provider;
24
+ private rpcUrl;
24
25
  private chainId;
25
26
  private factory;
26
27
  private entryPoint;
@@ -31,6 +32,7 @@ export declare class AAAccountManager {
31
32
  private paymaster?;
32
33
  private paymasterData?;
33
34
  private gasLimitMultiplier;
35
+ private senderCache;
34
36
  constructor(config?: XLayerConfig);
35
37
  /**
36
38
  * 获取 Provider
@@ -60,6 +62,14 @@ export declare class AAAccountManager {
60
62
  * @returns 预测的 Sender 地址
61
63
  */
62
64
  predictSenderAddress(ownerAddress: string, salt?: bigint): Promise<string>;
65
+ private rpcBatch;
66
+ private multicallAggregate3;
67
+ private predictSendersByOwnersFast;
68
+ /**
69
+ * 批量预测 Sender 地址(只做地址预测,不额外查询 nonce/balance/code)
70
+ * - 用于“生成/导入钱包”场景:更快、更省 RPC
71
+ */
72
+ predictSendersBatch(ownerAddresses: string[], salt?: bigint): Promise<string[]>;
63
73
  /**
64
74
  * 获取完整的 AA 账户信息
65
75
  */
@@ -85,6 +95,22 @@ export declare class AAAccountManager {
85
95
  userOp: UserOperation;
86
96
  prefundWei: bigint;
87
97
  }>;
98
+ /**
99
+ * 批量构建未签名的 UserOperation(使用 Bundler 批量估算 Gas)
100
+ *
101
+ * 目标:把 N 次 eth_estimateUserOperationGas 合并为 1 次(JSON-RPC batch),显著降低延迟。
102
+ */
103
+ buildUserOpsWithBundlerEstimateBatch(params: {
104
+ ops: Array<{
105
+ sender: string;
106
+ nonce: bigint;
107
+ callData: string;
108
+ initCode?: string;
109
+ }>;
110
+ }): Promise<{
111
+ userOps: UserOperation[];
112
+ prefundWeis: bigint[];
113
+ }>;
88
114
  /**
89
115
  * 构建未签名的 UserOperation(本地估算 Gas,不依赖 Bundler)
90
116
  *
@@ -8,8 +8,9 @@
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, GAS_LIMIT_MULTIPLIER, VERIFICATION_GAS_LIMIT_DEPLOY, VERIFICATION_GAS_LIMIT_NORMAL, PRE_VERIFICATION_GAS, FACTORY_ABI, ENTRYPOINT_ABI, SIMPLE_ACCOUNT_ABI, } from './constants.js';
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
+ import { mapWithConcurrency } from '../utils/concurrency.js';
13
14
  // ============================================================================
14
15
  // AA 账户管理器
15
16
  // ============================================================================
@@ -24,8 +25,10 @@ import { BundlerClient } from './bundler.js';
24
25
  */
25
26
  export class AAAccountManager {
26
27
  constructor(config = {}) {
28
+ this.senderCache = new Map(); // key: ownerLower -> sender
27
29
  this.chainId = config.chainId ?? XLAYER_CHAIN_ID;
28
30
  const rpcUrl = config.rpcUrl ?? DEFAULT_RPC_URL;
31
+ this.rpcUrl = rpcUrl;
29
32
  this.provider = new JsonRpcProvider(rpcUrl, {
30
33
  chainId: this.chainId,
31
34
  name: 'xlayer',
@@ -88,6 +91,148 @@ export class AAAccountManager {
88
91
  // 这比 getAddress 更可靠(某些链上 getAddress 有问题)
89
92
  return await this.factory.createAccount.staticCall(ownerAddress, useSalt);
90
93
  }
94
+ // ============================================================================
95
+ // 内部:RPC batch / multicall(性能优化)
96
+ // ============================================================================
97
+ async rpcBatch(items, timeoutMs = 20000) {
98
+ if (items.length === 0)
99
+ return [];
100
+ const controller = new AbortController();
101
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
102
+ try {
103
+ const body = items.map((it, i) => ({
104
+ jsonrpc: '2.0',
105
+ id: i + 1,
106
+ method: it.method,
107
+ params: it.params,
108
+ }));
109
+ const res = await fetch(this.rpcUrl, {
110
+ method: 'POST',
111
+ headers: { 'content-type': 'application/json' },
112
+ body: JSON.stringify(body),
113
+ signal: controller.signal,
114
+ });
115
+ const text = await res.text();
116
+ let data;
117
+ try {
118
+ data = JSON.parse(text);
119
+ }
120
+ catch {
121
+ throw new Error(`RPC batch 非 JSON 响应 (HTTP ${res.status}): ${text.slice(0, 400)}`);
122
+ }
123
+ if (!res.ok) {
124
+ throw new Error(`RPC batch HTTP ${res.status}: ${JSON.stringify(data).slice(0, 800)}`);
125
+ }
126
+ if (!Array.isArray(data)) {
127
+ throw new Error(`RPC batch 响应格式错误: ${JSON.stringify(data).slice(0, 800)}`);
128
+ }
129
+ const byId = new Map();
130
+ for (const r of data) {
131
+ const id = Number(r?.id);
132
+ if (r?.error)
133
+ throw new Error(`RPC batch error: ${JSON.stringify(r.error)}`);
134
+ if (Number.isFinite(id))
135
+ byId.set(id, r?.result);
136
+ }
137
+ return items.map((_, i) => byId.get(i + 1));
138
+ }
139
+ finally {
140
+ clearTimeout(timer);
141
+ }
142
+ }
143
+ async multicallAggregate3(params) {
144
+ const mcIface = new Interface([
145
+ 'function aggregate3((address target,bool allowFailure,bytes callData)[] calls) view returns ((bool success,bytes returnData)[] returnData)',
146
+ ]);
147
+ const data = mcIface.encodeFunctionData('aggregate3', [params.calls]);
148
+ const raw = await this.provider.call({ to: MULTICALL3, data });
149
+ const decoded = mcIface.decodeFunctionResult('aggregate3', raw)?.[0];
150
+ return decoded || [];
151
+ }
152
+ async predictSendersByOwnersFast(ownerAddresses, salt) {
153
+ const useSalt = salt ?? this.salt;
154
+ const owners = ownerAddresses.map((a) => String(a || '').trim()).filter(Boolean);
155
+ if (owners.length === 0)
156
+ return [];
157
+ const out = new Array(owners.length).fill('');
158
+ const need = [];
159
+ // 缓存命中(owner->sender)
160
+ for (let i = 0; i < owners.length; i++) {
161
+ const key = owners[i].toLowerCase();
162
+ const hit = this.senderCache.get(key);
163
+ if (hit)
164
+ out[i] = hit;
165
+ else
166
+ need.push({ idx: i, owner: owners[i] });
167
+ }
168
+ if (need.length === 0)
169
+ return out;
170
+ const factoryIface = new Interface(FACTORY_ABI);
171
+ const calls = need.map((x) => ({
172
+ target: this.factoryAddress,
173
+ allowFailure: true,
174
+ callData: factoryIface.encodeFunctionData('getAddress', [x.owner, useSalt]),
175
+ }));
176
+ // 分批 multicall,避免 callData 过大
177
+ const BATCH = 300;
178
+ for (let cursor = 0; cursor < calls.length; cursor += BATCH) {
179
+ const sliceCalls = calls.slice(cursor, cursor + BATCH);
180
+ const sliceNeed = need.slice(cursor, cursor + BATCH);
181
+ try {
182
+ const res = await this.multicallAggregate3({ calls: sliceCalls });
183
+ for (let i = 0; i < res.length; i++) {
184
+ const r = res[i];
185
+ const { idx, owner } = sliceNeed[i];
186
+ if (!r?.success || !r.returnData || r.returnData === '0x')
187
+ continue;
188
+ try {
189
+ const decoded = factoryIface.decodeFunctionResult('getAddress', r.returnData);
190
+ const addr = String(decoded?.[0] || '').trim();
191
+ if (addr && addr !== ZERO_ADDRESS) {
192
+ out[idx] = addr;
193
+ this.senderCache.set(owner.toLowerCase(), addr);
194
+ }
195
+ }
196
+ catch { /* ignore */ }
197
+ }
198
+ }
199
+ catch {
200
+ // ignore:fallback below
201
+ }
202
+ }
203
+ // fallback:对仍然缺失的,使用 createAccount.staticCall(更可靠但更慢),并控制并发
204
+ const missingIdxs = [];
205
+ for (let i = 0; i < owners.length; i++) {
206
+ if (!out[i])
207
+ missingIdxs.push(i);
208
+ }
209
+ if (missingIdxs.length > 0) {
210
+ const filled = await mapWithConcurrency(missingIdxs, 6, async (idx) => {
211
+ const owner = owners[idx];
212
+ try {
213
+ const sender = await this.factory.createAccount.staticCall(owner, useSalt);
214
+ return { idx, owner, sender: String(sender) };
215
+ }
216
+ catch {
217
+ return { idx, owner, sender: '' };
218
+ }
219
+ });
220
+ for (const it of filled) {
221
+ if (it.sender) {
222
+ out[it.idx] = it.sender;
223
+ this.senderCache.set(it.owner.toLowerCase(), it.sender);
224
+ }
225
+ }
226
+ }
227
+ return out;
228
+ }
229
+ /**
230
+ * 批量预测 Sender 地址(只做地址预测,不额外查询 nonce/balance/code)
231
+ * - 用于“生成/导入钱包”场景:更快、更省 RPC
232
+ */
233
+ async predictSendersBatch(ownerAddresses, salt) {
234
+ return await this.predictSendersByOwnersFast(ownerAddresses, salt);
235
+ }
91
236
  /**
92
237
  * 获取完整的 AA 账户信息
93
238
  */
@@ -170,6 +315,60 @@ export class AAAccountManager {
170
315
  const prefundWei = paymasterAndData !== '0x' ? 0n : gasTotal * BigInt(userOp.maxFeePerGas);
171
316
  return { userOp, prefundWei };
172
317
  }
318
+ /**
319
+ * 批量构建未签名的 UserOperation(使用 Bundler 批量估算 Gas)
320
+ *
321
+ * 目标:把 N 次 eth_estimateUserOperationGas 合并为 1 次(JSON-RPC batch),显著降低延迟。
322
+ */
323
+ async buildUserOpsWithBundlerEstimateBatch(params) {
324
+ if (params.ops.length === 0) {
325
+ return { userOps: [], prefundWeis: [] };
326
+ }
327
+ const feeData = await this.provider.getFeeData();
328
+ const legacyGasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? DEFAULT_GAS_PRICE;
329
+ const paymasterAndData = this.buildPaymasterAndData();
330
+ // 先构建 skeleton(Gas=0),然后批量估算
331
+ const skeletons = params.ops.map((p) => ({
332
+ sender: p.sender,
333
+ nonce: ethers.toBeHex(p.nonce),
334
+ initCode: p.initCode ?? '0x',
335
+ callData: p.callData,
336
+ callGasLimit: '0x0',
337
+ verificationGasLimit: '0x0',
338
+ preVerificationGas: '0x0',
339
+ maxFeePerGas: ethers.toBeHex(legacyGasPrice * 2n),
340
+ maxPriorityFeePerGas: ethers.toBeHex(legacyGasPrice),
341
+ paymasterAndData,
342
+ signature: '0x',
343
+ }));
344
+ const estimates = await this.bundler.estimateUserOperationGasBatch(skeletons);
345
+ const userOps = [];
346
+ const prefundWeis = [];
347
+ for (let i = 0; i < skeletons.length; i++) {
348
+ const estimate = estimates[i];
349
+ let userOp = {
350
+ ...skeletons[i],
351
+ callGasLimit: estimate.callGasLimit,
352
+ verificationGasLimit: estimate.verificationGasLimit,
353
+ preVerificationGas: estimate.preVerificationGas,
354
+ };
355
+ // 使用 Bundler 建议的费率(如果有)
356
+ if (estimate.maxFeePerGas && estimate.maxPriorityFeePerGas) {
357
+ userOp = {
358
+ ...userOp,
359
+ maxFeePerGas: estimate.maxFeePerGas,
360
+ maxPriorityFeePerGas: estimate.maxPriorityFeePerGas,
361
+ };
362
+ }
363
+ const gasTotal = BigInt(userOp.callGasLimit) +
364
+ BigInt(userOp.verificationGasLimit) +
365
+ BigInt(userOp.preVerificationGas);
366
+ const prefundWei = paymasterAndData !== '0x' ? 0n : gasTotal * BigInt(userOp.maxFeePerGas);
367
+ userOps.push(userOp);
368
+ prefundWeis.push(prefundWei);
369
+ }
370
+ return { userOps, prefundWeis };
371
+ }
173
372
  /**
174
373
  * 构建未签名的 UserOperation(本地估算 Gas,不依赖 Bundler)
175
374
  *
@@ -292,7 +491,86 @@ export class AAAccountManager {
292
491
  * 批量获取多个 owner 的 AA 账户信息
293
492
  */
294
493
  async getMultipleAccountInfo(ownerAddresses) {
295
- return Promise.all(ownerAddresses.map((addr) => this.getAccountInfo(addr)));
494
+ const owners = ownerAddresses.map((a) => String(a || '').trim()).filter(Boolean);
495
+ if (owners.length === 0)
496
+ return [];
497
+ // 1) 批量预测 sender(优先 getAddress+multicall,失败回退 createAccount.staticCall)
498
+ const senders = await this.predictSendersByOwnersFast(owners);
499
+ // 2) 批量 getNonce(multicall EntryPoint.getNonce)
500
+ const epIface = new Interface(ENTRYPOINT_ABI);
501
+ const nonceCalls = senders.map((sender) => ({
502
+ target: this.entryPointAddress,
503
+ allowFailure: true,
504
+ callData: epIface.encodeFunctionData('getNonce', [sender, 0]),
505
+ }));
506
+ const nonces = new Array(senders.length).fill(0n);
507
+ const BATCH = 350;
508
+ for (let cursor = 0; cursor < nonceCalls.length; cursor += BATCH) {
509
+ const sliceCalls = nonceCalls.slice(cursor, cursor + BATCH);
510
+ const res = await this.multicallAggregate3({ calls: sliceCalls });
511
+ for (let i = 0; i < res.length; i++) {
512
+ const r = res[i];
513
+ const idx = cursor + i;
514
+ if (!r?.success || !r.returnData || r.returnData === '0x')
515
+ continue;
516
+ try {
517
+ const decoded = epIface.decodeFunctionResult('getNonce', r.returnData);
518
+ nonces[idx] = BigInt(decoded?.[0] ?? 0n);
519
+ }
520
+ catch { /* ignore */ }
521
+ }
522
+ }
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'] });
533
+ }
534
+ for (const s of slice) {
535
+ reqs.push({ method: 'eth_getBalance', params: [s, 'latest'] });
536
+ }
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;
551
+ }
552
+ }
553
+ }
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++) {
561
+ const idx = cursor + i;
562
+ codes[idx] = fetched[i].code ?? '0x';
563
+ balances[idx] = fetched[i].bal ?? 0n;
564
+ }
565
+ }
566
+ }
567
+ return owners.map((owner, i) => ({
568
+ owner,
569
+ sender: senders[i],
570
+ deployed: codes[i] != null && String(codes[i]) !== '0x',
571
+ balance: balances[i] ?? 0n,
572
+ nonce: nonces[i] ?? 0n,
573
+ }));
296
574
  }
297
575
  }
298
576
  // ============================================================================
@@ -374,20 +652,24 @@ export async function generateAAWallets(params) {
374
652
  const wallet = Wallet.createRandom();
375
653
  const owner = wallet.address;
376
654
  const privateKey = wallet.privateKey;
377
- // 预测 AA 地址
378
- const sender = await manager.predictSenderAddress(owner);
379
655
  wallets.push({
380
656
  index: i + 1,
381
657
  owner,
382
658
  privateKey,
383
- sender,
659
+ sender: '',
384
660
  });
385
661
  owners.push(owner);
386
662
  privateKeys.push(privateKey);
387
- senders.push(sender);
663
+ senders.push('');
664
+ }
665
+ // ✅ 批量预测 sender(比逐个 staticCall 快很多)
666
+ const sendersPredicted = await manager.predictSendersBatch(owners);
667
+ for (let i = 0; i < wallets.length; i++) {
668
+ wallets[i].sender = sendersPredicted[i] || '';
669
+ senders[i] = sendersPredicted[i] || '';
388
670
  console.log(`#${i + 1}`);
389
- console.log(` Owner: ${owner}`);
390
- console.log(` Sender: ${sender}`);
671
+ console.log(` Owner: ${owners[i]}`);
672
+ console.log(` Sender: ${senders[i]}`);
391
673
  }
392
674
  // 生成格式化输出
393
675
  const formatted = formatWalletOutput(wallets);
@@ -414,29 +696,34 @@ export async function generateAAWalletsFromMnemonic(mnemonic, count, startIndex
414
696
  const privateKeys = [];
415
697
  const senders = [];
416
698
  console.log(`\n从助记词派生 ${count} 个 XLayer AA 钱包 (起始索引: ${startIndex})...\n`);
699
+ const paths = [];
417
700
  for (let i = 0; i < count; i++) {
418
701
  const index = startIndex + i;
419
702
  const path = `m/44'/60'/0'/0/${index}`;
703
+ paths.push(path);
420
704
  // 从助记词派生
421
705
  const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic, undefined, path);
422
706
  const owner = hdNode.address;
423
707
  const privateKey = hdNode.privateKey;
424
- // 预测 AA 地址
425
- const sender = await manager.predictSenderAddress(owner);
426
708
  wallets.push({
427
709
  index: i + 1,
428
710
  owner,
429
711
  privateKey,
430
- sender,
712
+ sender: '',
431
713
  mnemonic: i === 0 ? mnemonic : undefined, // 只在第一个记录助记词
432
714
  derivationPath: path,
433
715
  });
434
716
  owners.push(owner);
435
717
  privateKeys.push(privateKey);
436
- senders.push(sender);
437
- console.log(`#${i + 1} (path: ${path})`);
438
- console.log(` Owner: ${owner}`);
439
- console.log(` Sender: ${sender}`);
718
+ senders.push('');
719
+ }
720
+ const sendersPredicted = await manager.predictSendersBatch(owners);
721
+ for (let i = 0; i < wallets.length; i++) {
722
+ wallets[i].sender = sendersPredicted[i] || '';
723
+ senders[i] = sendersPredicted[i] || '';
724
+ console.log(`#${i + 1} (path: ${paths[i]})`);
725
+ console.log(` Owner: ${owners[i]}`);
726
+ console.log(` Sender: ${senders[i]}`);
440
727
  }
441
728
  const formatted = formatWalletOutput(wallets, mnemonic);
442
729
  return {
@@ -462,18 +749,22 @@ export async function predictSendersFromPrivateKeys(privateKeys, config) {
462
749
  for (let i = 0; i < privateKeys.length; i++) {
463
750
  const wallet = new Wallet(privateKeys[i]);
464
751
  const owner = wallet.address;
465
- const sender = await manager.predictSenderAddress(owner);
466
752
  wallets.push({
467
753
  index: i + 1,
468
754
  owner,
469
755
  privateKey: privateKeys[i],
470
- sender,
756
+ sender: '',
471
757
  });
472
758
  owners.push(owner);
473
- senders.push(sender);
759
+ senders.push('');
760
+ }
761
+ const sendersPredicted = await manager.predictSendersBatch(owners);
762
+ for (let i = 0; i < wallets.length; i++) {
763
+ wallets[i].sender = sendersPredicted[i] || '';
764
+ senders[i] = sendersPredicted[i] || '';
474
765
  console.log(`#${i + 1}`);
475
- console.log(` Owner: ${owner}`);
476
- console.log(` Sender: ${sender}`);
766
+ console.log(` Owner: ${owners[i]}`);
767
+ console.log(` Sender: ${senders[i]}`);
477
768
  }
478
769
  const formatted = formatWalletOutput(wallets);
479
770
  return {
@@ -52,6 +52,11 @@ export declare class BundleExecutor {
52
52
  * 构建归集 UserOp(将 OKB 从 sender 转回 owner)
53
53
  */
54
54
  private buildWithdrawUserOp;
55
+ /**
56
+ * 构建归集 UserOp(已知 sender/nonce/balance 的快速版本)
57
+ * - 用于批量流程:避免重复 getAccountInfo / getOkbBalance
58
+ */
59
+ private buildWithdrawUserOpWithState;
55
60
  /**
56
61
  * 构建代币转账 UserOp(将代币从 sender 转回 owner)
57
62
  */
@@ -7,10 +7,11 @@
7
7
  * - 买卖一体化:买入 -> 授权 -> 卖出 -> 归集
8
8
  * - OKB 归集:将 sender 的 OKB 转回 owner
9
9
  */
10
- import { Interface, Contract } from 'ethers';
10
+ import { Wallet, Interface, Contract } from 'ethers';
11
11
  import { FLAP_PORTAL, ENTRYPOINT_ABI, DEFAULT_CALL_GAS_LIMIT_SELL, DEFAULT_WITHDRAW_RESERVE, } from './constants.js';
12
- import { AAAccountManager, encodeExecute, createWallet } from './aa-account.js';
12
+ import { AAAccountManager, encodeExecute } from './aa-account.js';
13
13
  import { encodeBuyCall, encodeSellCall, encodeApproveCall, encodeTransferCall, PortalQuery, parseOkb, formatOkb, } from './portal-ops.js';
14
+ import { mapWithConcurrency } from '../utils/concurrency.js';
14
15
  // ============================================================================
15
16
  // 捆绑交易执行器
16
17
  // ============================================================================
@@ -129,45 +130,46 @@ export class BundleExecutor {
129
130
  /**
130
131
  * 构建授权 UserOp
131
132
  */
132
- async buildApproveUserOp(ownerWallet, tokenAddress, spender, nonce, ownerName) {
133
- const accountInfo = await this.aaManager.getAccountInfo(ownerWallet.address);
133
+ async buildApproveUserOp(ownerWallet, tokenAddress, spender, sender, nonce, initCode, ownerName) {
134
134
  const approveData = encodeApproveCall(spender);
135
135
  const callData = encodeExecute(tokenAddress, 0n, approveData);
136
- await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, parseOkb('0.0002'), `${ownerName ?? 'owner'}/approve-prefund`);
136
+ await this.aaManager.ensureSenderBalance(ownerWallet, sender, parseOkb('0.0002'), `${ownerName ?? 'owner'}/approve-prefund`);
137
137
  const { userOp, prefundWei } = await this.aaManager.buildUserOpWithLocalEstimate({
138
138
  ownerWallet,
139
- sender: accountInfo.sender,
139
+ sender,
140
140
  callData,
141
141
  nonce,
142
+ initCode,
142
143
  });
143
- await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/approve-fund`);
144
+ await this.aaManager.ensureSenderBalance(ownerWallet, sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/approve-fund`);
144
145
  const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
145
146
  return { ...signed, prefundWei, ownerName };
146
147
  }
147
148
  /**
148
149
  * 构建卖出 UserOp
149
150
  */
150
- async buildSellUserOp(ownerWallet, tokenAddress, sellAmount, nonce, needApprove, ownerName) {
151
- const accountInfo = await this.aaManager.getAccountInfo(ownerWallet.address);
151
+ async buildSellUserOp(ownerWallet, tokenAddress, sellAmount, sender, nonce, initCode, needApprove, ownerName) {
152
152
  const sellData = encodeSellCall(tokenAddress, sellAmount, 0n);
153
153
  const callData = encodeExecute(this.portalAddress, 0n, sellData);
154
- await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, parseOkb('0.0003'), `${ownerName ?? 'owner'}/sell-prefund`);
154
+ await this.aaManager.ensureSenderBalance(ownerWallet, sender, parseOkb('0.0003'), `${ownerName ?? 'owner'}/sell-prefund`);
155
155
  // 如果需要 approve(还未执行),estimateGas 会 revert,使用固定值
156
156
  const { userOp, prefundWei } = needApprove
157
157
  ? await this.aaManager.buildUserOpWithLocalEstimate({
158
158
  ownerWallet,
159
- sender: accountInfo.sender,
159
+ sender,
160
160
  callData,
161
161
  nonce,
162
+ initCode,
162
163
  callGasLimit: DEFAULT_CALL_GAS_LIMIT_SELL,
163
164
  })
164
165
  : await this.aaManager.buildUserOpWithLocalEstimate({
165
166
  ownerWallet,
166
- sender: accountInfo.sender,
167
+ sender,
167
168
  callData,
168
169
  nonce,
170
+ initCode,
169
171
  });
170
- await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/sell-fund`);
172
+ await this.aaManager.ensureSenderBalance(ownerWallet, sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/sell-fund`);
171
173
  const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
172
174
  return { ...signed, prefundWei, ownerName };
173
175
  }
@@ -177,39 +179,56 @@ export class BundleExecutor {
177
179
  async buildWithdrawUserOp(ownerWallet, reserveWei, ownerName) {
178
180
  const accountInfo = await this.aaManager.getAccountInfo(ownerWallet.address);
179
181
  const senderBalance = await this.portalQuery.getOkbBalance(accountInfo.sender);
180
- if (senderBalance <= reserveWei) {
181
- console.log(`\n[${ownerName ?? 'owner'}] sender OKB 太少,跳过归集:${formatOkb(senderBalance)} OKB`);
182
+ const initCode = accountInfo.deployed ? '0x' : this.aaManager.generateInitCode(ownerWallet.address);
183
+ return await this.buildWithdrawUserOpWithState({
184
+ ownerWallet,
185
+ sender: accountInfo.sender,
186
+ nonce: accountInfo.nonce,
187
+ initCode,
188
+ senderBalance,
189
+ reserveWei,
190
+ ownerName,
191
+ });
192
+ }
193
+ /**
194
+ * 构建归集 UserOp(已知 sender/nonce/balance 的快速版本)
195
+ * - 用于批量流程:避免重复 getAccountInfo / getOkbBalance
196
+ */
197
+ async buildWithdrawUserOpWithState(params) {
198
+ const senderBalance = params.senderBalance;
199
+ if (senderBalance <= params.reserveWei) {
200
+ console.log(`\n[${params.ownerName ?? 'owner'}] sender OKB 太少,跳过归集:${formatOkb(senderBalance)} OKB`);
182
201
  return null;
183
202
  }
184
- // 先估算 prefund
185
- const tempCallData = encodeExecute(ownerWallet.address, 0n, '0x');
186
- await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, parseOkb('0.0002'), `${ownerName ?? 'owner'}/withdraw-prefund`);
203
+ // 先估算 prefund(使用空调用)
204
+ const tempCallData = encodeExecute(params.ownerWallet.address, 0n, '0x');
205
+ await this.aaManager.ensureSenderBalance(params.ownerWallet, params.sender, parseOkb('0.0002'), `${params.ownerName ?? 'owner'}/withdraw-prefund`);
187
206
  const { prefundWei } = await this.aaManager.buildUserOpWithLocalEstimate({
188
- ownerWallet,
189
- sender: accountInfo.sender,
207
+ ownerWallet: params.ownerWallet,
208
+ sender: params.sender,
190
209
  callData: tempCallData,
191
- nonce: accountInfo.nonce,
210
+ nonce: params.nonce,
211
+ initCode: params.initCode,
192
212
  });
193
- // 计算可归集金额
194
- const currentBalance = await this.portalQuery.getOkbBalance(accountInfo.sender);
195
- const withdrawAmount = currentBalance > prefundWei + reserveWei
196
- ? currentBalance - prefundWei - reserveWei
213
+ // 计算可归集金额(用已知余额近似;fund 发生时余额会变大,属于可接受的保守近似)
214
+ const withdrawAmount = senderBalance > prefundWei + params.reserveWei
215
+ ? senderBalance - prefundWei - params.reserveWei
197
216
  : 0n;
198
217
  if (withdrawAmount <= 0n) {
199
- console.log(`\n[${ownerName ?? 'owner'}] 归集后可转出=0(余额不足以覆盖 prefund+reserve)`);
218
+ console.log(`\n[${params.ownerName ?? 'owner'}] 归集后可转出=0(余额不足以覆盖 prefund+reserve)`);
200
219
  return null;
201
220
  }
202
- // 真正的 callData
203
- const callData = encodeExecute(ownerWallet.address, withdrawAmount, '0x');
221
+ const callData = encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
204
222
  const { userOp } = await this.aaManager.buildUserOpWithLocalEstimate({
205
- ownerWallet,
206
- sender: accountInfo.sender,
223
+ ownerWallet: params.ownerWallet,
224
+ sender: params.sender,
207
225
  callData,
208
- nonce: accountInfo.nonce,
226
+ nonce: params.nonce,
227
+ initCode: params.initCode,
209
228
  });
210
- console.log(`\n[${ownerName ?? 'owner'}] withdraw: ${formatOkb(withdrawAmount)} OKB`);
211
- const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
212
- return { ...signed, prefundWei, ownerName };
229
+ console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(withdrawAmount)} OKB`);
230
+ const signed = await this.aaManager.signUserOp(userOp, params.ownerWallet);
231
+ return { ...signed, prefundWei, ownerName: params.ownerName };
213
232
  }
214
233
  /**
215
234
  * 构建代币转账 UserOp(将代币从 sender 转回 owner)
@@ -249,40 +268,75 @@ export class BundleExecutor {
249
268
  throw new Error('私钥数量和购买金额数量必须一致');
250
269
  }
251
270
  // 使用第一个 owner 作为 bundler signer
252
- const wallets = privateKeys.map((pk) => createWallet(pk, this.config));
271
+ const sharedProvider = this.aaManager.getProvider();
272
+ const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
253
273
  const bundlerSigner = wallets[0];
254
274
  const beneficiary = bundlerSigner.address;
255
275
  console.log('=== XLayer Bundle Buy ===');
256
276
  console.log('token:', tokenAddress);
257
277
  console.log('owners:', wallets.length);
258
- // 1. 构建买入 UserOps
259
- const buyOps = [];
260
- for (let i = 0; i < wallets.length; i++) {
261
- const wallet = wallets[i];
262
- const buyWei = parseOkb(buyAmounts[i]);
263
- const signed = await this.buildBuyUserOp(wallet, tokenAddress, buyWei, `owner${i + 1}`);
264
- buyOps.push(signed.userOp);
265
- }
278
+ // 1. 预取账户信息(并行),并批量估算 gas(减少对 bundler 的 N 次请求)
279
+ const accountInfos = await this.aaManager.getMultipleAccountInfo(wallets.map((w) => w.address));
280
+ const buyWeis = buyAmounts.map((a) => parseOkb(a));
281
+ // 估算前确保 sender 有足够余额(用于 bundler 模拟;paymaster 场景会自动跳过)
282
+ await Promise.all(accountInfos.map((ai, i) => this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWeis[i] + parseOkb('0.0003'), `owner${i + 1}/buy-prefund-before-estimate`)));
283
+ const buyCallDatas = buyWeis.map((buyWei) => {
284
+ const swapData = encodeBuyCall(tokenAddress, buyWei, 0n);
285
+ return encodeExecute(this.portalAddress, buyWei, swapData);
286
+ });
287
+ const initCodes = accountInfos.map((ai, i) => ai.deployed ? '0x' : this.aaManager.generateInitCode(wallets[i].address));
288
+ const { userOps: buyUserOps, prefundWeis } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
289
+ ops: accountInfos.map((ai, i) => ({
290
+ sender: ai.sender,
291
+ nonce: ai.nonce,
292
+ callData: buyCallDatas[i],
293
+ initCode: initCodes[i],
294
+ })),
295
+ });
296
+ // 补足 prefund + 买入金额(多数情况下上一步的 0.0003 已足够,这里通常不会再转账)
297
+ await Promise.all(accountInfos.map((ai, i) => this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWeis[i] + prefundWeis[i] + parseOkb('0.0002'), `owner${i + 1}/buy-fund`)));
298
+ // 签名
299
+ const signedBuy = await Promise.all(buyUserOps.map((op, i) => this.aaManager.signUserOp(op, wallets[i])));
300
+ const buyOps = signedBuy.map((s) => s.userOp);
266
301
  // 2. 执行买入
267
302
  const buyResult = await this.runHandleOps('buyBundle', buyOps, bundlerSigner, beneficiary);
268
303
  if (!buyResult) {
269
304
  throw new Error('买入交易失败');
270
305
  }
271
306
  // 3. 获取代币余额
272
- const senders = await Promise.all(wallets.map((w) => this.aaManager.predictSenderAddress(w.address)));
307
+ const senders = accountInfos.map((ai) => ai.sender);
273
308
  const tokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, senders);
274
309
  // 4. 可选:转账代币回 owner
275
310
  let transferResult;
276
311
  if (transferBackToOwner) {
277
- const transferOps = [];
312
+ const idxs = [];
313
+ const transferCallDatas = [];
278
314
  for (let i = 0; i < wallets.length; i++) {
279
- const signed = await this.buildTransferTokenUserOp(wallets[i], tokenAddress, `owner${i + 1}`);
280
- if (signed) {
281
- transferOps.push(signed.userOp);
282
- }
315
+ const sender = senders[i];
316
+ const bal = tokenBalances.get(sender) ?? 0n;
317
+ if (bal === 0n)
318
+ continue;
319
+ idxs.push(i);
320
+ const transferData = encodeTransferCall(wallets[i].address, bal);
321
+ transferCallDatas.push(encodeExecute(tokenAddress, 0n, transferData));
283
322
  }
284
- if (transferOps.length > 0) {
285
- transferResult = await this.runHandleOps('transferBundle', transferOps, bundlerSigner, beneficiary) ?? undefined;
323
+ if (idxs.length > 0) {
324
+ // 估算前补一点余额(paymaster 会自动跳过)
325
+ await Promise.all(idxs.map((i) => this.aaManager.ensureSenderBalance(wallets[i], senders[i], parseOkb('0.0002'), `owner${i + 1}/transfer-prefund`)));
326
+ // buy 已经成功过一次,因此 transfer 的 nonce = 原 nonce + 1,且 initCode = 0x
327
+ const { userOps: transferUserOps, prefundWeis: transferPrefunds } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
328
+ ops: idxs.map((i, k) => ({
329
+ sender: senders[i],
330
+ nonce: accountInfos[i].nonce + 1n,
331
+ callData: transferCallDatas[k],
332
+ initCode: '0x',
333
+ })),
334
+ });
335
+ await Promise.all(idxs.map((i, k) => this.aaManager.ensureSenderBalance(wallets[i], senders[i], transferPrefunds[k] + parseOkb('0.00005'), `owner${i + 1}/transfer-fund`)));
336
+ const signedTransfer = await Promise.all(transferUserOps.map((op, k) => this.aaManager.signUserOp(op, wallets[idxs[k]])));
337
+ const transferOps = signedTransfer.map((s) => s.userOp);
338
+ transferResult =
339
+ (await this.runHandleOps('transferBundle', transferOps, bundlerSigner, beneficiary)) ?? undefined;
286
340
  }
287
341
  }
288
342
  return { buyResult, transferResult, tokenBalances };
@@ -294,7 +348,8 @@ export class BundleExecutor {
294
348
  */
295
349
  async bundleSell(params) {
296
350
  const { tokenAddress, privateKeys, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, } = params;
297
- const wallets = privateKeys.map((pk) => createWallet(pk, this.config));
351
+ const sharedProvider = this.aaManager.getProvider();
352
+ const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
298
353
  const bundlerSigner = wallets[0];
299
354
  const beneficiary = bundlerSigner.address;
300
355
  const reserveWei = parseOkb(withdrawReserve);
@@ -302,12 +357,14 @@ export class BundleExecutor {
302
357
  console.log('token:', tokenAddress);
303
358
  console.log('owners:', wallets.length);
304
359
  console.log('sellPercent:', sellPercent);
305
- // 获取 sender 列表和余额
306
- const senders = await Promise.all(wallets.map((w) => this.aaManager.predictSenderAddress(w.address)));
360
+ // 批量获取 accountInfo(含 sender/nonce/deployed),避免循环内重复 getAccountInfo
361
+ const accountInfos = await this.aaManager.getMultipleAccountInfo(wallets.map((w) => w.address));
362
+ const senders = accountInfos.map((ai) => ai.sender);
307
363
  const tokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, senders);
308
364
  const allowances = await this.portalQuery.getMultipleAllowances(tokenAddress, senders);
309
365
  // 1. 检查授权,必要时先 approve
310
- const approveOps = [];
366
+ const approveItems = [];
367
+ const didApprove = new Array(wallets.length).fill(false);
311
368
  for (let i = 0; i < wallets.length; i++) {
312
369
  const sender = senders[i];
313
370
  const balance = tokenBalances.get(sender) ?? 0n;
@@ -316,9 +373,20 @@ export class BundleExecutor {
316
373
  continue;
317
374
  if (allowance >= balance)
318
375
  continue;
319
- const accountInfo = await this.aaManager.getAccountInfo(wallets[i].address);
320
- const signed = await this.buildApproveUserOp(wallets[i], tokenAddress, this.portalAddress, accountInfo.nonce, `owner${i + 1}`);
321
- approveOps.push(signed.userOp);
376
+ const ai = accountInfos[i];
377
+ const initCode = ai.deployed ? '0x' : this.aaManager.generateInitCode(wallets[i].address);
378
+ approveItems.push({ i, sender, nonce: ai.nonce, initCode });
379
+ didApprove[i] = true;
380
+ }
381
+ const approveOps = [];
382
+ if (approveItems.length > 0) {
383
+ const signedApproves = await mapWithConcurrency(approveItems, 4, async (it) => {
384
+ const i = it.i;
385
+ const signed = await this.buildApproveUserOp(wallets[i], tokenAddress, this.portalAddress, it.sender, it.nonce, it.initCode, `owner${i + 1}`);
386
+ return { i, userOp: signed.userOp };
387
+ });
388
+ for (const r of signedApproves)
389
+ approveOps.push(r.userOp);
322
390
  }
323
391
  let approveResult;
324
392
  if (approveOps.length > 0) {
@@ -326,6 +394,7 @@ export class BundleExecutor {
326
394
  }
327
395
  // 2. 卖出
328
396
  const sellOps = [];
397
+ const sellItems = [];
329
398
  for (let i = 0; i < wallets.length; i++) {
330
399
  const sender = senders[i];
331
400
  const balance = tokenBalances.get(sender) ?? 0n;
@@ -334,11 +403,20 @@ export class BundleExecutor {
334
403
  const sellAmount = (balance * BigInt(sellPercent)) / 100n;
335
404
  if (sellAmount === 0n)
336
405
  continue;
337
- const accountInfo = await this.aaManager.getAccountInfo(wallets[i].address);
338
- const allowance = allowances.get(sender) ?? 0n;
339
- const needApprove = allowance < sellAmount && approveOps.length > 0;
340
- const signed = await this.buildSellUserOp(wallets[i], tokenAddress, sellAmount, accountInfo.nonce, needApprove, `owner${i + 1}`);
341
- sellOps.push(signed.userOp);
406
+ const ai = accountInfos[i];
407
+ const initCode = ai.deployed ? '0x' : this.aaManager.generateInitCode(wallets[i].address);
408
+ // approve 已单独打包并等待确认,因此这里不需要用“needApprove=真”去走保守 callGasLimit
409
+ const needApprove = false;
410
+ const nonce = ai.nonce + (didApprove[i] ? 1n : 0n);
411
+ sellItems.push({ i, sender, nonce, initCode: didApprove[i] ? '0x' : initCode, needApprove, sellAmount });
412
+ }
413
+ if (sellItems.length > 0) {
414
+ const signedSells = await mapWithConcurrency(sellItems, 4, async (it) => {
415
+ const i = it.i;
416
+ const signed = await this.buildSellUserOp(wallets[i], tokenAddress, it.sellAmount, it.sender, it.nonce, it.initCode, it.needApprove, `owner${i + 1}`);
417
+ return signed.userOp;
418
+ });
419
+ sellOps.push(...signedSells);
342
420
  }
343
421
  const sellResult = await this.runHandleOps('sellBundle', sellOps, bundlerSigner, beneficiary);
344
422
  if (!sellResult) {
@@ -348,11 +426,42 @@ export class BundleExecutor {
348
426
  let withdrawResult;
349
427
  if (withdrawToOwner) {
350
428
  const withdrawOps = [];
429
+ // 批量获取 sender OKB 余额
430
+ const okbBalances = await this.portalQuery.getMultipleOkbBalances(senders);
431
+ // 计算 sell 后的下一 nonce
432
+ const nextNonces = new Array(wallets.length).fill(0n);
433
+ const sold = new Array(wallets.length).fill(false);
434
+ for (const it of sellItems) {
435
+ sold[it.i] = true;
436
+ }
351
437
  for (let i = 0; i < wallets.length; i++) {
352
- const signed = await this.buildWithdrawUserOp(wallets[i], reserveWei, `owner${i + 1}`);
353
- if (signed) {
354
- withdrawOps.push(signed.userOp);
355
- }
438
+ const ai = accountInfos[i];
439
+ const sellNonceUsed = ai.nonce + (didApprove[i] ? 1n : 0n);
440
+ nextNonces[i] = sold[i] ? (sellNonceUsed + 1n) : (ai.nonce + (didApprove[i] ? 1n : 0n));
441
+ }
442
+ const withdrawItems = wallets.map((w, i) => ({
443
+ i,
444
+ ownerWallet: w,
445
+ sender: senders[i],
446
+ senderBalance: okbBalances.get(senders[i]) ?? 0n,
447
+ nonce: nextNonces[i],
448
+ initCode: (accountInfos[i].deployed || didApprove[i] || sold[i]) ? '0x' : this.aaManager.generateInitCode(wallets[i].address),
449
+ }));
450
+ const signedWithdraws = await mapWithConcurrency(withdrawItems, 3, async (it) => {
451
+ const signed = await this.buildWithdrawUserOpWithState({
452
+ ownerWallet: it.ownerWallet,
453
+ sender: it.sender,
454
+ nonce: it.nonce,
455
+ initCode: it.initCode,
456
+ senderBalance: it.senderBalance,
457
+ reserveWei,
458
+ ownerName: `owner${it.i + 1}`,
459
+ });
460
+ return signed?.userOp ?? null;
461
+ });
462
+ for (const op of signedWithdraws) {
463
+ if (op)
464
+ withdrawOps.push(op);
356
465
  }
357
466
  if (withdrawOps.length > 0) {
358
467
  withdrawResult = await this.runHandleOps('withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
@@ -370,7 +479,8 @@ export class BundleExecutor {
370
479
  if (privateKeys.length !== buyAmounts.length) {
371
480
  throw new Error('私钥数量和购买金额数量必须一致');
372
481
  }
373
- const wallets = privateKeys.map((pk) => createWallet(pk, this.config));
482
+ const sharedProvider = this.aaManager.getProvider();
483
+ const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
374
484
  const bundlerSigner = wallets[0];
375
485
  const beneficiary = bundlerSigner.address;
376
486
  const reserveWei = parseOkb(withdrawReserve);
@@ -378,15 +488,36 @@ export class BundleExecutor {
378
488
  console.log('token:', tokenAddress);
379
489
  console.log('owners:', wallets.length);
380
490
  console.log('sellPercent:', sellPercent);
381
- // 获取 sender 列表
382
- const senders = await Promise.all(wallets.map((w) => this.aaManager.predictSenderAddress(w.address)));
383
- // 1. 买入
384
- const buyOps = [];
385
- for (let i = 0; i < wallets.length; i++) {
386
- const buyWei = parseOkb(buyAmounts[i]);
387
- const signed = await this.buildBuyUserOp(wallets[i], tokenAddress, buyWei, `owner${i + 1}`);
388
- buyOps.push(signed.userOp);
389
- }
491
+ // 批量获取 accountInfo(含 sender/nonce/deployed)
492
+ const accountInfos = await this.aaManager.getMultipleAccountInfo(wallets.map((w) => w.address));
493
+ const senders = accountInfos.map((ai) => ai.sender);
494
+ // 1. 买入(批量估算 + 并发补余额 + 并发签名)
495
+ const buyWeis = buyAmounts.map((a) => parseOkb(a));
496
+ await mapWithConcurrency(accountInfos, 6, async (ai, i) => {
497
+ const buyWei = buyWeis[i] ?? 0n;
498
+ if (buyWei <= 0n)
499
+ return;
500
+ await this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWei + parseOkb('0.0003'), `owner${i + 1}/buy-prefund-before-estimate`);
501
+ });
502
+ const buyCallDatas = buyWeis.map((buyWei) => {
503
+ const swapData = encodeBuyCall(tokenAddress, buyWei, 0n);
504
+ return encodeExecute(this.portalAddress, buyWei, swapData);
505
+ });
506
+ const initCodes = accountInfos.map((ai, i) => (ai.deployed ? '0x' : this.aaManager.generateInitCode(wallets[i].address)));
507
+ const { userOps: buyUserOps, prefundWeis } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
508
+ ops: accountInfos.map((ai, i) => ({
509
+ sender: ai.sender,
510
+ nonce: ai.nonce,
511
+ callData: buyCallDatas[i],
512
+ initCode: initCodes[i],
513
+ })),
514
+ });
515
+ await mapWithConcurrency(accountInfos, 6, async (ai, i) => {
516
+ const buyWei = buyWeis[i] ?? 0n;
517
+ await this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWei + (prefundWeis[i] ?? 0n) + parseOkb('0.0002'), `owner${i + 1}/buy-fund`);
518
+ });
519
+ const signedBuy = await mapWithConcurrency(buyUserOps, 10, async (op, i) => this.aaManager.signUserOp(op, wallets[i]));
520
+ const buyOps = signedBuy.map((s) => s.userOp);
390
521
  const buyResult = await this.runHandleOps('buyBundle', buyOps, bundlerSigner, beneficiary);
391
522
  if (!buyResult) {
392
523
  throw new Error('买入交易失败');
@@ -396,27 +527,33 @@ export class BundleExecutor {
396
527
  const allowances = await this.portalQuery.getMultipleAllowances(tokenAddress, senders);
397
528
  // 2. 授权 + 卖出(可以合并到同一笔 handleOps)
398
529
  const sellOps = [];
399
- for (let i = 0; i < wallets.length; i++) {
530
+ const sellPerWallet = await mapWithConcurrency(wallets, 4, async (w, i) => {
400
531
  const sender = senders[i];
401
532
  const balance = tokenBalances.get(sender) ?? 0n;
402
533
  if (balance === 0n) {
403
534
  console.log(`[owner${i + 1}] 没买到代币,跳过卖出`);
404
- continue;
535
+ return [];
405
536
  }
406
537
  const allowance = allowances.get(sender) ?? 0n;
407
- let baseNonce = (await this.aaManager.getAccountInfo(wallets[i].address)).nonce;
408
- // 如果需要授权,先添加 approve op
409
538
  const needApprove = allowance < balance;
539
+ // buy 已在上一笔 handleOps 执行,因此 nonce = 原 nonce + 1
540
+ let nonce = accountInfos[i].nonce + 1n;
541
+ const initCode = '0x';
542
+ const out = [];
410
543
  if (needApprove) {
411
- const approveOp = await this.buildApproveUserOp(wallets[i], tokenAddress, this.portalAddress, baseNonce, `owner${i + 1}`);
412
- sellOps.push(approveOp.userOp);
413
- baseNonce = baseNonce + 1n;
544
+ const approveOp = await this.buildApproveUserOp(w, tokenAddress, this.portalAddress, sender, nonce, initCode, `owner${i + 1}`);
545
+ out.push(approveOp.userOp);
546
+ nonce = nonce + 1n;
414
547
  }
415
- // 添加 sell op
416
548
  const sellAmount = (balance * BigInt(sellPercent)) / 100n;
417
- const sellOp = await this.buildSellUserOp(wallets[i], tokenAddress, sellAmount, baseNonce, needApprove, `owner${i + 1}`);
418
- sellOps.push(sellOp.userOp);
419
- }
549
+ if (sellAmount === 0n)
550
+ return out;
551
+ const sellOp = await this.buildSellUserOp(w, tokenAddress, sellAmount, sender, nonce, initCode, needApprove, `owner${i + 1}`);
552
+ out.push(sellOp.userOp);
553
+ return out;
554
+ });
555
+ for (const ops of sellPerWallet)
556
+ sellOps.push(...ops);
420
557
  const sellResult = await this.runHandleOps('sellBundle', sellOps, bundlerSigner, beneficiary);
421
558
  if (!sellResult) {
422
559
  throw new Error('卖出交易失败');
@@ -425,11 +562,41 @@ export class BundleExecutor {
425
562
  let withdrawResult;
426
563
  if (withdrawToOwner) {
427
564
  const withdrawOps = [];
428
- for (let i = 0; i < wallets.length; i++) {
429
- const signed = await this.buildWithdrawUserOp(wallets[i], reserveWei, `owner${i + 1}`);
430
- if (signed) {
431
- withdrawOps.push(signed.userOp);
432
- }
565
+ // 批量获取 OKB 余额(sell 后状态)
566
+ const okbBalances = await this.portalQuery.getMultipleOkbBalances(senders);
567
+ // sell handleOps 里每个 wallet:一定有 sell op(balance>0)且可能还有 approve op
568
+ const nextNonces = wallets.map((_, i) => {
569
+ const sender = senders[i];
570
+ const bal = tokenBalances.get(sender) ?? 0n;
571
+ if (bal === 0n)
572
+ return accountInfos[i].nonce + 1n; // buy 后但未 sell
573
+ const allowance = allowances.get(sender) ?? 0n;
574
+ const needApprove = allowance < bal;
575
+ return accountInfos[i].nonce + 1n + (needApprove ? 2n : 1n);
576
+ });
577
+ const withdrawItems = wallets.map((w, i) => ({
578
+ i,
579
+ ownerWallet: w,
580
+ sender: senders[i],
581
+ senderBalance: okbBalances.get(senders[i]) ?? 0n,
582
+ nonce: nextNonces[i],
583
+ initCode: '0x',
584
+ }));
585
+ const signedWithdraws = await mapWithConcurrency(withdrawItems, 3, async (it) => {
586
+ const signed = await this.buildWithdrawUserOpWithState({
587
+ ownerWallet: it.ownerWallet,
588
+ sender: it.sender,
589
+ nonce: it.nonce,
590
+ initCode: it.initCode,
591
+ senderBalance: it.senderBalance,
592
+ reserveWei,
593
+ ownerName: `owner${it.i + 1}`,
594
+ });
595
+ return signed?.userOp ?? null;
596
+ });
597
+ for (const op of signedWithdraws) {
598
+ if (op)
599
+ withdrawOps.push(op);
433
600
  }
434
601
  if (withdrawOps.length > 0) {
435
602
  withdrawResult = await this.runHandleOps('withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
@@ -56,6 +56,13 @@ export declare class BundlerClient {
56
56
  * 发送 JSON-RPC 请求到 Bundler
57
57
  */
58
58
  private rpc;
59
+ /**
60
+ * 发送 JSON-RPC Batch 请求到 Bundler
61
+ *
62
+ * 注意:JSON-RPC batch 是一个数组;Particle bundler 通常也支持该格式。
63
+ * 为兼容其非标准字段,这里为每个 request 也附带 chainId 字段。
64
+ */
65
+ private rpcBatch;
59
66
  /**
60
67
  * 获取支持的 EntryPoint 列表
61
68
  */
@@ -67,6 +74,10 @@ export declare class BundlerClient {
67
74
  * @returns Gas 估算结果
68
75
  */
69
76
  estimateUserOperationGas(userOp: UserOperation): Promise<GasEstimate>;
77
+ /**
78
+ * 批量估算多个 UserOperation Gas(优先使用 JSON-RPC batch;失败则回退)
79
+ */
80
+ estimateUserOperationGasBatch(userOps: UserOperation[]): Promise<GasEstimate[]>;
70
81
  /**
71
82
  * 发送 UserOperation
72
83
  *
@@ -69,6 +69,64 @@ export class BundlerClient {
69
69
  clearTimeout(timer);
70
70
  }
71
71
  }
72
+ /**
73
+ * 发送 JSON-RPC Batch 请求到 Bundler
74
+ *
75
+ * 注意:JSON-RPC batch 是一个数组;Particle bundler 通常也支持该格式。
76
+ * 为兼容其非标准字段,这里为每个 request 也附带 chainId 字段。
77
+ */
78
+ async rpcBatch(calls) {
79
+ const controller = new AbortController();
80
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
81
+ // 为每个 call 分配独立 id,便于按 id 回填结果
82
+ const ids = calls.map(() => nextRpcId++);
83
+ const body = calls.map((c, i) => ({
84
+ jsonrpc: '2.0',
85
+ id: ids[i],
86
+ method: c.method,
87
+ params: c.params ?? [],
88
+ chainId: this.chainId,
89
+ }));
90
+ try {
91
+ const res = await fetch(this.url, {
92
+ method: 'POST',
93
+ headers: {
94
+ 'content-type': 'application/json',
95
+ ...this.headers,
96
+ },
97
+ body: JSON.stringify(body),
98
+ signal: controller.signal,
99
+ });
100
+ const text = await res.text();
101
+ let data;
102
+ try {
103
+ data = JSON.parse(text);
104
+ }
105
+ catch {
106
+ throw new Error(`非 JSON 响应 (HTTP ${res.status}): ${text.slice(0, 400)}`);
107
+ }
108
+ if (!res.ok) {
109
+ throw new Error(`HTTP ${res.status}: ${JSON.stringify(data).slice(0, 800)}`);
110
+ }
111
+ if (!Array.isArray(data)) {
112
+ throw new Error(`RPC batch 非数组响应: ${JSON.stringify(data).slice(0, 800)}`);
113
+ }
114
+ // 建立 id -> result 映射,确保按 calls 顺序返回
115
+ const byId = new Map();
116
+ for (const item of data) {
117
+ if (item?.error) {
118
+ throw new Error(`RPC batch error: ${JSON.stringify(item.error)}`);
119
+ }
120
+ if (typeof item?.id === 'number') {
121
+ byId.set(item.id, item.result);
122
+ }
123
+ }
124
+ return ids.map((id) => byId.get(id));
125
+ }
126
+ finally {
127
+ clearTimeout(timer);
128
+ }
129
+ }
72
130
  /**
73
131
  * 获取支持的 EntryPoint 列表
74
132
  */
@@ -87,6 +145,24 @@ export class BundlerClient {
87
145
  this.entryPoint,
88
146
  ]);
89
147
  }
148
+ /**
149
+ * 批量估算多个 UserOperation Gas(优先使用 JSON-RPC batch;失败则回退)
150
+ */
151
+ async estimateUserOperationGasBatch(userOps) {
152
+ if (userOps.length === 0)
153
+ return [];
154
+ // 先尝试 batch(显著降低 HTTP 往返次数)
155
+ try {
156
+ return await this.rpcBatch(userOps.map((op) => ({
157
+ method: 'eth_estimateUserOperationGas',
158
+ params: [op, this.entryPoint],
159
+ })));
160
+ }
161
+ catch {
162
+ // fallback:并发单请求(比串行快,但可能触发限流)
163
+ return await Promise.all(userOps.map((op) => this.estimateUserOperationGas(op)));
164
+ }
165
+ }
90
166
  /**
91
167
  * 发送 UserOperation
92
168
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.5.19",
3
+ "version": "1.5.21",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",