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.
@@ -13,13 +13,60 @@ import { AAAccountManager, encodeExecute } from './aa-account.js';
13
13
  import { encodeBuyCall, encodeSellCall, encodeApproveCall, encodeTransferCall, PortalQuery, parseOkb, formatOkb, } from './portal-ops.js';
14
14
  import { mapWithConcurrency } from '../utils/concurrency.js';
15
15
  // ============================================================================
16
+ // AA Nonce(EntryPoint nonce)本地分配器
17
+ // ============================================================================
18
+ /**
19
+ * ✅ 仅用于 ERC-4337 AA(EntryPoint.getNonce(sender, 0))
20
+ *
21
+ * 目标:不要在业务代码里手写 `+1/+2` 推导,而是像 BSC bundle 一样用本地 Map
22
+ * 对“同一 sender 多步流程”连续分配 nonce。
23
+ *
24
+ * 注意:
25
+ * - 这是**同一次 SDK 调用/同一条流程**内的 nonce 分配;不解决多进程/多并发任务同时使用同一 sender 的问题。
26
+ * - 对“可能不生成 op”的分支(例如 withdraw 返回 null),使用 peek + commit,避免提前消耗 nonce。
27
+ */
28
+ class AANonceMap {
29
+ constructor() {
30
+ this.nextBySenderLower = new Map();
31
+ }
32
+ init(sender, startNonce) {
33
+ const k = sender.toLowerCase();
34
+ const cur = this.nextBySenderLower.get(k);
35
+ if (cur === undefined || startNonce > cur) {
36
+ this.nextBySenderLower.set(k, startNonce);
37
+ }
38
+ }
39
+ peek(sender) {
40
+ const k = sender.toLowerCase();
41
+ const cur = this.nextBySenderLower.get(k);
42
+ if (cur === undefined) {
43
+ throw new Error(`AANonceMap: sender not initialized: ${sender}`);
44
+ }
45
+ return cur;
46
+ }
47
+ commit(sender, usedNonce) {
48
+ const k = sender.toLowerCase();
49
+ const cur = this.nextBySenderLower.get(k);
50
+ if (cur === undefined) {
51
+ throw new Error(`AANonceMap: sender not initialized: ${sender}`);
52
+ }
53
+ // 只允许“使用当前 nonce”或“重复 commit”(幂等)
54
+ if (usedNonce !== cur && usedNonce !== cur - 1n) {
55
+ throw new Error(`AANonceMap: nonce mismatch for ${sender}: used=${usedNonce.toString()} cur=${cur.toString()}`);
56
+ }
57
+ if (usedNonce === cur) {
58
+ this.nextBySenderLower.set(k, cur + 1n);
59
+ }
60
+ }
61
+ next(sender) {
62
+ const n = this.peek(sender);
63
+ this.commit(sender, n);
64
+ return n;
65
+ }
66
+ }
67
+ // ============================================================================
16
68
  // 捆绑交易执行器
17
69
  // ============================================================================
18
- // 固定 gas(用于大规模减少 RPC);具体值允许通过 config.fixedGas 覆盖
19
- const DEFAULT_CALL_GAS_LIMIT_BUY = DEFAULT_CALL_GAS_LIMIT_SELL; // buy 与 sell 共享一个保守值
20
- const DEFAULT_CALL_GAS_LIMIT_APPROVE = 200000n;
21
- const DEFAULT_CALL_GAS_LIMIT_TRANSFER = 150000n;
22
- const DEFAULT_CALL_GAS_LIMIT_WITHDRAW = 120000n;
23
70
  /**
24
71
  * XLayer 捆绑交易执行器
25
72
  *
@@ -114,27 +161,14 @@ export class BundleExecutor {
114
161
  const callData = encodeExecute(this.portalAddress, buyAmountWei, swapData);
115
162
  // 估算前确保 sender 有足够余额(用于模拟)
116
163
  await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, buyAmountWei + parseOkb('0.0003'), `${ownerName ?? 'owner'}/buy-prefund-before-estimate`);
117
- const gasPolicy = this.config.gasPolicy ?? 'bundlerEstimate';
118
- const { userOp, prefundWei } = gasPolicy === 'fixed'
119
- ? await this.aaManager.buildUserOpWithFixedGas({
120
- ownerWallet,
121
- sender: accountInfo.sender,
122
- callData,
123
- nonce: accountInfo.nonce,
124
- initCode,
125
- deployed: accountInfo.deployed,
126
- fixedGas: {
127
- ...(this.config.fixedGas ?? {}),
128
- callGasLimit: this.config.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_BUY,
129
- },
130
- })
131
- : await this.aaManager.buildUserOpWithBundlerEstimate({
132
- ownerWallet,
133
- sender: accountInfo.sender,
134
- callData,
135
- nonce: accountInfo.nonce,
136
- initCode,
137
- });
164
+ // 使用 Bundler 估算
165
+ const { userOp, prefundWei } = await this.aaManager.buildUserOpWithBundlerEstimate({
166
+ ownerWallet,
167
+ sender: accountInfo.sender,
168
+ callData,
169
+ nonce: accountInfo.nonce,
170
+ initCode,
171
+ });
138
172
  // 补足 prefund + 买入金额
139
173
  await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, buyAmountWei + prefundWei + parseOkb('0.0002'), `${ownerName ?? 'owner'}/buy-fund`);
140
174
  // 签名
@@ -152,27 +186,13 @@ export class BundleExecutor {
152
186
  const approveData = encodeApproveCall(spender);
153
187
  const callData = encodeExecute(tokenAddress, 0n, approveData);
154
188
  await this.aaManager.ensureSenderBalance(ownerWallet, sender, parseOkb('0.0002'), `${ownerName ?? 'owner'}/approve-prefund`);
155
- const gasPolicy = this.config.gasPolicy ?? 'bundlerEstimate';
156
- const { userOp, prefundWei } = gasPolicy === 'fixed'
157
- ? await this.aaManager.buildUserOpWithFixedGas({
158
- ownerWallet,
159
- sender,
160
- callData,
161
- nonce,
162
- initCode,
163
- deployed: initCode === '0x',
164
- fixedGas: {
165
- ...(this.config.fixedGas ?? {}),
166
- callGasLimit: this.config.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_APPROVE,
167
- },
168
- })
169
- : await this.aaManager.buildUserOpWithLocalEstimate({
170
- ownerWallet,
171
- sender,
172
- callData,
173
- nonce,
174
- initCode,
175
- });
189
+ const { userOp, prefundWei } = await this.aaManager.buildUserOpWithLocalEstimate({
190
+ ownerWallet,
191
+ sender,
192
+ callData,
193
+ nonce,
194
+ initCode,
195
+ });
176
196
  await this.aaManager.ensureSenderBalance(ownerWallet, sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/approve-fund`);
177
197
  const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
178
198
  return { ...signed, prefundWei, ownerName };
@@ -184,37 +204,23 @@ export class BundleExecutor {
184
204
  const sellData = encodeSellCall(tokenAddress, sellAmount, 0n);
185
205
  const callData = encodeExecute(this.portalAddress, 0n, sellData);
186
206
  await this.aaManager.ensureSenderBalance(ownerWallet, sender, parseOkb('0.0003'), `${ownerName ?? 'owner'}/sell-prefund`);
187
- const gasPolicy = this.config.gasPolicy ?? 'bundlerEstimate';
188
- // 如果需要 approve(还未执行),estimateGas 可能 revert;因此默认就用固定 callGasLimit
189
- const { userOp, prefundWei } = gasPolicy === 'fixed'
190
- ? await this.aaManager.buildUserOpWithFixedGas({
207
+ // 如果需要 approve(还未执行),estimateGas revert,使用固定值
208
+ const { userOp, prefundWei } = needApprove
209
+ ? await this.aaManager.buildUserOpWithLocalEstimate({
191
210
  ownerWallet,
192
211
  sender,
193
212
  callData,
194
213
  nonce,
195
214
  initCode,
196
- deployed: initCode === '0x',
197
- fixedGas: {
198
- ...(this.config.fixedGas ?? {}),
199
- callGasLimit: this.config.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_SELL,
200
- },
215
+ callGasLimit: DEFAULT_CALL_GAS_LIMIT_SELL,
201
216
  })
202
- : needApprove
203
- ? await this.aaManager.buildUserOpWithLocalEstimate({
204
- ownerWallet,
205
- sender,
206
- callData,
207
- nonce,
208
- initCode,
209
- callGasLimit: DEFAULT_CALL_GAS_LIMIT_SELL,
210
- })
211
- : await this.aaManager.buildUserOpWithLocalEstimate({
212
- ownerWallet,
213
- sender,
214
- callData,
215
- nonce,
216
- initCode,
217
- });
217
+ : await this.aaManager.buildUserOpWithLocalEstimate({
218
+ ownerWallet,
219
+ sender,
220
+ callData,
221
+ nonce,
222
+ initCode,
223
+ });
218
224
  await this.aaManager.ensureSenderBalance(ownerWallet, sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/sell-fund`);
219
225
  const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
220
226
  return { ...signed, prefundWei, ownerName };
@@ -249,27 +255,13 @@ export class BundleExecutor {
249
255
  // 先估算 prefund(使用空调用)
250
256
  const tempCallData = encodeExecute(params.ownerWallet.address, 0n, '0x');
251
257
  await this.aaManager.ensureSenderBalance(params.ownerWallet, params.sender, parseOkb('0.0002'), `${params.ownerName ?? 'owner'}/withdraw-prefund`);
252
- const gasPolicy = this.config.gasPolicy ?? 'bundlerEstimate';
253
- const { prefundWei } = gasPolicy === 'fixed'
254
- ? await this.aaManager.buildUserOpWithFixedGas({
255
- ownerWallet: params.ownerWallet,
256
- sender: params.sender,
257
- callData: tempCallData,
258
- nonce: params.nonce,
259
- initCode: params.initCode,
260
- deployed: params.initCode === '0x',
261
- fixedGas: {
262
- ...(this.config.fixedGas ?? {}),
263
- callGasLimit: this.config.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
264
- },
265
- })
266
- : await this.aaManager.buildUserOpWithLocalEstimate({
267
- ownerWallet: params.ownerWallet,
268
- sender: params.sender,
269
- callData: tempCallData,
270
- nonce: params.nonce,
271
- initCode: params.initCode,
272
- });
258
+ const { prefundWei } = await this.aaManager.buildUserOpWithLocalEstimate({
259
+ ownerWallet: params.ownerWallet,
260
+ sender: params.sender,
261
+ callData: tempCallData,
262
+ nonce: params.nonce,
263
+ initCode: params.initCode,
264
+ });
273
265
  // 计算可归集金额(用已知余额近似;fund 发生时余额会变大,属于可接受的保守近似)
274
266
  const withdrawAmount = senderBalance > prefundWei + params.reserveWei
275
267
  ? senderBalance - prefundWei - params.reserveWei
@@ -279,26 +271,13 @@ export class BundleExecutor {
279
271
  return null;
280
272
  }
281
273
  const callData = encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
282
- const { userOp } = gasPolicy === 'fixed'
283
- ? await this.aaManager.buildUserOpWithFixedGas({
284
- ownerWallet: params.ownerWallet,
285
- sender: params.sender,
286
- callData,
287
- nonce: params.nonce,
288
- initCode: params.initCode,
289
- deployed: params.initCode === '0x',
290
- fixedGas: {
291
- ...(this.config.fixedGas ?? {}),
292
- callGasLimit: this.config.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
293
- },
294
- })
295
- : await this.aaManager.buildUserOpWithLocalEstimate({
296
- ownerWallet: params.ownerWallet,
297
- sender: params.sender,
298
- callData,
299
- nonce: params.nonce,
300
- initCode: params.initCode,
301
- });
274
+ const { userOp } = await this.aaManager.buildUserOpWithLocalEstimate({
275
+ ownerWallet: params.ownerWallet,
276
+ sender: params.sender,
277
+ callData,
278
+ nonce: params.nonce,
279
+ initCode: params.initCode,
280
+ });
302
281
  console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(withdrawAmount)} OKB`);
303
282
  const signed = await this.aaManager.signUserOp(userOp, params.ownerWallet);
304
283
  return { ...signed, prefundWei, ownerName: params.ownerName };
@@ -316,26 +295,12 @@ export class BundleExecutor {
316
295
  const transferData = encodeTransferCall(ownerWallet.address, tokenBalance);
317
296
  const callData = encodeExecute(tokenAddress, 0n, transferData);
318
297
  await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, parseOkb('0.0002'), `${ownerName ?? 'owner'}/transfer-prefund`);
319
- const gasPolicy = this.config.gasPolicy ?? 'bundlerEstimate';
320
- const { userOp, prefundWei } = gasPolicy === 'fixed'
321
- ? await this.aaManager.buildUserOpWithFixedGas({
322
- ownerWallet,
323
- sender: accountInfo.sender,
324
- callData,
325
- nonce: accountInfo.nonce,
326
- initCode: accountInfo.deployed ? '0x' : this.aaManager.generateInitCode(ownerWallet.address),
327
- deployed: accountInfo.deployed,
328
- fixedGas: {
329
- ...(this.config.fixedGas ?? {}),
330
- callGasLimit: this.config.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_TRANSFER,
331
- },
332
- })
333
- : await this.aaManager.buildUserOpWithBundlerEstimate({
334
- ownerWallet,
335
- sender: accountInfo.sender,
336
- callData,
337
- nonce: accountInfo.nonce,
338
- });
298
+ const { userOp, prefundWei } = await this.aaManager.buildUserOpWithBundlerEstimate({
299
+ ownerWallet,
300
+ sender: accountInfo.sender,
301
+ callData,
302
+ nonce: accountInfo.nonce,
303
+ });
339
304
  await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/transfer-fund`);
340
305
  console.log(`\n[${ownerName ?? 'owner'}] transfer token: ${tokenBalance.toString()}`);
341
306
  const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
@@ -365,11 +330,11 @@ export class BundleExecutor {
365
330
  // 1. 预取账户信息(并行),并批量估算 gas(减少对 bundler 的 N 次请求)
366
331
  const accountInfos = await this.aaManager.getMultipleAccountInfo(wallets.map((w) => w.address));
367
332
  const buyWeis = buyAmounts.map((a) => parseOkb(a));
333
+ const nonceMap = new AANonceMap();
334
+ for (const ai of accountInfos)
335
+ nonceMap.init(ai.sender, ai.nonce);
368
336
  // 估算前确保 sender 有足够余额(用于 bundler 模拟;paymaster 场景会自动跳过)
369
- // 避免 Promise.all 突发并发(大规模地址会触发 RPC 限流)
370
- await mapWithConcurrency(accountInfos, 6, async (ai, i) => {
371
- await this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWeis[i] + parseOkb('0.0003'), `owner${i + 1}/buy-prefund-before-estimate`);
372
- });
337
+ 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`)));
373
338
  const buyCallDatas = buyWeis.map((buyWei) => {
374
339
  const swapData = encodeBuyCall(tokenAddress, buyWei, 0n);
375
340
  return encodeExecute(this.portalAddress, buyWei, swapData);
@@ -378,17 +343,15 @@ export class BundleExecutor {
378
343
  const { userOps: buyUserOps, prefundWeis } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
379
344
  ops: accountInfos.map((ai, i) => ({
380
345
  sender: ai.sender,
381
- nonce: ai.nonce,
346
+ nonce: nonceMap.next(ai.sender),
382
347
  callData: buyCallDatas[i],
383
348
  initCode: initCodes[i],
384
349
  })),
385
350
  });
386
351
  // 补足 prefund + 买入金额(多数情况下上一步的 0.0003 已足够,这里通常不会再转账)
387
- await mapWithConcurrency(accountInfos, 6, async (ai, i) => {
388
- await this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWeis[i] + prefundWeis[i] + parseOkb('0.0002'), `owner${i + 1}/buy-fund`);
389
- });
390
- // 签名(受控并发,避免大规模时阻塞)
391
- const signedBuy = await mapWithConcurrency(buyUserOps, 10, async (op, i) => this.aaManager.signUserOp(op, wallets[i]));
352
+ 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`)));
353
+ // 签名
354
+ const signedBuy = await Promise.all(buyUserOps.map((op, i) => this.aaManager.signUserOp(op, wallets[i])));
392
355
  const buyOps = signedBuy.map((s) => s.userOp);
393
356
  // 2. 执行买入
394
357
  const buyResult = await this.runHandleOps('buyBundle', buyOps, bundlerSigner, beneficiary);
@@ -403,6 +366,7 @@ export class BundleExecutor {
403
366
  if (transferBackToOwner) {
404
367
  const idxs = [];
405
368
  const transferCallDatas = [];
369
+ const transferNonces = [];
406
370
  for (let i = 0; i < wallets.length; i++) {
407
371
  const sender = senders[i];
408
372
  const bal = tokenBalances.get(sender) ?? 0n;
@@ -411,25 +375,22 @@ export class BundleExecutor {
411
375
  idxs.push(i);
412
376
  const transferData = encodeTransferCall(wallets[i].address, bal);
413
377
  transferCallDatas.push(encodeExecute(tokenAddress, 0n, transferData));
378
+ transferNonces.push(nonceMap.next(sender));
414
379
  }
415
380
  if (idxs.length > 0) {
416
381
  // 估算前补一点余额(paymaster 会自动跳过)
417
- await mapWithConcurrency(idxs, 6, async (i) => {
418
- await this.aaManager.ensureSenderBalance(wallets[i], senders[i], parseOkb('0.0002'), `owner${i + 1}/transfer-prefund`);
419
- });
382
+ await Promise.all(idxs.map((i) => this.aaManager.ensureSenderBalance(wallets[i], senders[i], parseOkb('0.0002'), `owner${i + 1}/transfer-prefund`)));
420
383
  // buy 已经成功过一次,因此 transfer 的 nonce = 原 nonce + 1,且 initCode = 0x
421
384
  const { userOps: transferUserOps, prefundWeis: transferPrefunds } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
422
385
  ops: idxs.map((i, k) => ({
423
386
  sender: senders[i],
424
- nonce: accountInfos[i].nonce + 1n,
387
+ nonce: transferNonces[k],
425
388
  callData: transferCallDatas[k],
426
389
  initCode: '0x',
427
390
  })),
428
391
  });
429
- await mapWithConcurrency(idxs, 6, async (i, k) => {
430
- await this.aaManager.ensureSenderBalance(wallets[i], senders[i], transferPrefunds[k] + parseOkb('0.00005'), `owner${i + 1}/transfer-fund`);
431
- });
432
- const signedTransfer = await mapWithConcurrency(transferUserOps, 10, async (op, k) => this.aaManager.signUserOp(op, wallets[idxs[k]]));
392
+ await Promise.all(idxs.map((i, k) => this.aaManager.ensureSenderBalance(wallets[i], senders[i], transferPrefunds[k] + parseOkb('0.00005'), `owner${i + 1}/transfer-fund`)));
393
+ const signedTransfer = await Promise.all(transferUserOps.map((op, k) => this.aaManager.signUserOp(op, wallets[idxs[k]])));
433
394
  const transferOps = signedTransfer.map((s) => s.userOp);
434
395
  transferResult =
435
396
  (await this.runHandleOps('transferBundle', transferOps, bundlerSigner, beneficiary)) ?? undefined;
@@ -456,11 +417,15 @@ export class BundleExecutor {
456
417
  // ✅ 批量获取 accountInfo(含 sender/nonce/deployed),避免循环内重复 getAccountInfo
457
418
  const accountInfos = await this.aaManager.getMultipleAccountInfo(wallets.map((w) => w.address));
458
419
  const senders = accountInfos.map((ai) => ai.sender);
420
+ const nonceMap = new AANonceMap();
421
+ for (const ai of accountInfos)
422
+ nonceMap.init(ai.sender, ai.nonce);
459
423
  const tokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, senders);
460
424
  const allowances = await this.portalQuery.getMultipleAllowances(tokenAddress, senders);
461
425
  // 1. 检查授权,必要时先 approve
462
426
  const approveItems = [];
463
427
  const didApprove = new Array(wallets.length).fill(false);
428
+ const touched = new Array(wallets.length).fill(false); // 任意阶段使用过 UserOp(用于决定 initCode=0x)
464
429
  for (let i = 0; i < wallets.length; i++) {
465
430
  const sender = senders[i];
466
431
  const balance = tokenBalances.get(sender) ?? 0n;
@@ -471,8 +436,9 @@ export class BundleExecutor {
471
436
  continue;
472
437
  const ai = accountInfos[i];
473
438
  const initCode = ai.deployed ? '0x' : this.aaManager.generateInitCode(wallets[i].address);
474
- approveItems.push({ i, sender, nonce: ai.nonce, initCode });
439
+ approveItems.push({ i, sender, nonce: nonceMap.next(sender), initCode });
475
440
  didApprove[i] = true;
441
+ touched[i] = true;
476
442
  }
477
443
  const approveOps = [];
478
444
  if (approveItems.length > 0) {
@@ -503,8 +469,9 @@ export class BundleExecutor {
503
469
  const initCode = ai.deployed ? '0x' : this.aaManager.generateInitCode(wallets[i].address);
504
470
  // approve 已单独打包并等待确认,因此这里不需要用“needApprove=真”去走保守 callGasLimit
505
471
  const needApprove = false;
506
- const nonce = ai.nonce + (didApprove[i] ? 1n : 0n);
472
+ const nonce = nonceMap.next(sender);
507
473
  sellItems.push({ i, sender, nonce, initCode: didApprove[i] ? '0x' : initCode, needApprove, sellAmount });
474
+ touched[i] = true;
508
475
  }
509
476
  if (sellItems.length > 0) {
510
477
  const signedSells = await mapWithConcurrency(sellItems, 4, async (it) => {
@@ -524,24 +491,15 @@ export class BundleExecutor {
524
491
  const withdrawOps = [];
525
492
  // 批量获取 sender OKB 余额
526
493
  const okbBalances = await this.portalQuery.getMultipleOkbBalances(senders);
527
- // 计算 sell 后的下一 nonce
528
- const nextNonces = new Array(wallets.length).fill(0n);
529
- const sold = new Array(wallets.length).fill(false);
530
- for (const it of sellItems) {
531
- sold[it.i] = true;
532
- }
533
- for (let i = 0; i < wallets.length; i++) {
534
- const ai = accountInfos[i];
535
- const sellNonceUsed = ai.nonce + (didApprove[i] ? 1n : 0n);
536
- nextNonces[i] = sold[i] ? (sellNonceUsed + 1n) : (ai.nonce + (didApprove[i] ? 1n : 0n));
537
- }
538
494
  const withdrawItems = wallets.map((w, i) => ({
539
495
  i,
540
496
  ownerWallet: w,
541
497
  sender: senders[i],
542
498
  senderBalance: okbBalances.get(senders[i]) ?? 0n,
543
- nonce: nextNonces[i],
544
- initCode: (accountInfos[i].deployed || didApprove[i] || sold[i]) ? '0x' : this.aaManager.generateInitCode(wallets[i].address),
499
+ // ⚠️ 这里 nonce 用 peek(不消耗),只有确实生成 withdraw op 才 commit
500
+ nonce: nonceMap.peek(senders[i]),
501
+ // 如果该 sender 在 approve/sell 阶段已经出现过(或原本已部署),则 withdraw 不再需要 initCode
502
+ initCode: (accountInfos[i].deployed || touched[i]) ? '0x' : this.aaManager.generateInitCode(wallets[i].address),
545
503
  }));
546
504
  const signedWithdraws = await mapWithConcurrency(withdrawItems, 3, async (it) => {
547
505
  const signed = await this.buildWithdrawUserOpWithState({
@@ -553,6 +511,8 @@ export class BundleExecutor {
553
511
  reserveWei,
554
512
  ownerName: `owner${it.i + 1}`,
555
513
  });
514
+ if (signed?.userOp)
515
+ nonceMap.commit(it.sender, it.nonce);
556
516
  return signed?.userOp ?? null;
557
517
  });
558
518
  for (const op of signedWithdraws) {
@@ -587,6 +547,9 @@ export class BundleExecutor {
587
547
  // ✅ 批量获取 accountInfo(含 sender/nonce/deployed)
588
548
  const accountInfos = await this.aaManager.getMultipleAccountInfo(wallets.map((w) => w.address));
589
549
  const senders = accountInfos.map((ai) => ai.sender);
550
+ const nonceMap = new AANonceMap();
551
+ for (const ai of accountInfos)
552
+ nonceMap.init(ai.sender, ai.nonce);
590
553
  // 1. 买入(批量估算 + 并发补余额 + 并发签名)
591
554
  const buyWeis = buyAmounts.map((a) => parseOkb(a));
592
555
  await mapWithConcurrency(accountInfos, 6, async (ai, i) => {
@@ -603,7 +566,7 @@ export class BundleExecutor {
603
566
  const { userOps: buyUserOps, prefundWeis } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
604
567
  ops: accountInfos.map((ai, i) => ({
605
568
  sender: ai.sender,
606
- nonce: ai.nonce,
569
+ nonce: nonceMap.next(ai.sender),
607
570
  callData: buyCallDatas[i],
608
571
  initCode: initCodes[i],
609
572
  })),
@@ -632,19 +595,18 @@ export class BundleExecutor {
632
595
  }
633
596
  const allowance = allowances.get(sender) ?? 0n;
634
597
  const needApprove = allowance < balance;
635
- // buy 已在上一笔 handleOps 执行,因此 nonce = 原 nonce + 1
636
- let nonce = accountInfos[i].nonce + 1n;
637
598
  const initCode = '0x';
638
599
  const out = [];
639
600
  if (needApprove) {
601
+ const nonce = nonceMap.next(sender);
640
602
  const approveOp = await this.buildApproveUserOp(w, tokenAddress, this.portalAddress, sender, nonce, initCode, `owner${i + 1}`);
641
603
  out.push(approveOp.userOp);
642
- nonce = nonce + 1n;
643
604
  }
644
605
  const sellAmount = (balance * BigInt(sellPercent)) / 100n;
645
606
  if (sellAmount === 0n)
646
607
  return out;
647
- const sellOp = await this.buildSellUserOp(w, tokenAddress, sellAmount, sender, nonce, initCode, needApprove, `owner${i + 1}`);
608
+ const sellNonce = nonceMap.next(sender);
609
+ const sellOp = await this.buildSellUserOp(w, tokenAddress, sellAmount, sender, sellNonce, initCode, needApprove, `owner${i + 1}`);
648
610
  out.push(sellOp.userOp);
649
611
  return out;
650
612
  });
@@ -660,22 +622,12 @@ export class BundleExecutor {
660
622
  const withdrawOps = [];
661
623
  // 批量获取 OKB 余额(sell 后状态)
662
624
  const okbBalances = await this.portalQuery.getMultipleOkbBalances(senders);
663
- // sell handleOps 里每个 wallet:一定有 sell op(balance>0)且可能还有 approve op
664
- const nextNonces = wallets.map((_, i) => {
665
- const sender = senders[i];
666
- const bal = tokenBalances.get(sender) ?? 0n;
667
- if (bal === 0n)
668
- return accountInfos[i].nonce + 1n; // buy 后但未 sell
669
- const allowance = allowances.get(sender) ?? 0n;
670
- const needApprove = allowance < bal;
671
- return accountInfos[i].nonce + 1n + (needApprove ? 2n : 1n);
672
- });
673
625
  const withdrawItems = wallets.map((w, i) => ({
674
626
  i,
675
627
  ownerWallet: w,
676
628
  sender: senders[i],
677
629
  senderBalance: okbBalances.get(senders[i]) ?? 0n,
678
- nonce: nextNonces[i],
630
+ nonce: nonceMap.peek(senders[i]),
679
631
  initCode: '0x',
680
632
  }));
681
633
  const signedWithdraws = await mapWithConcurrency(withdrawItems, 3, async (it) => {
@@ -688,6 +640,8 @@ export class BundleExecutor {
688
640
  reserveWei,
689
641
  ownerName: `owner${it.i + 1}`,
690
642
  });
643
+ if (signed?.userOp)
644
+ nonceMap.commit(it.sender, it.nonce);
691
645
  return signed?.userOp ?? null;
692
646
  });
693
647
  for (const op of signedWithdraws) {
@@ -4,7 +4,6 @@
4
4
  * 与 Particle Bundler 交互,提供 ERC-4337 相关 RPC 方法
5
5
  */
6
6
  import { PARTICLE_BUNDLER_URL, XLAYER_CHAIN_ID, ENTRYPOINT_V06, } from './constants.js';
7
- import { mapWithConcurrency } from '../utils/concurrency.js';
8
7
  // ============================================================================
9
8
  // Bundler 客户端类
10
9
  // ============================================================================
@@ -152,35 +151,17 @@ export class BundlerClient {
152
151
  async estimateUserOperationGasBatch(userOps) {
153
152
  if (userOps.length === 0)
154
153
  return [];
155
- const maxBatchSize = 30;
156
- const maxSingleConcurrency = 4;
157
- const estimateChunk = async (chunk, batchSize) => {
158
- if (chunk.length === 0)
159
- return [];
160
- if (batchSize <= 1 || chunk.length === 1) {
161
- // 最终兜底:受控并发的单请求(避免 Promise.all 突发)
162
- return await mapWithConcurrency(chunk, maxSingleConcurrency, async (op) => await this.estimateUserOperationGas(op));
163
- }
164
- const out = [];
165
- for (let cursor = 0; cursor < chunk.length; cursor += batchSize) {
166
- const slice = chunk.slice(cursor, cursor + batchSize);
167
- try {
168
- const res = await this.rpcBatch(slice.map((op) => ({
169
- method: 'eth_estimateUserOperationGas',
170
- params: [op, this.entryPoint],
171
- })));
172
- out.push(...res);
173
- }
174
- catch (err) {
175
- // 降级:拆分为更小 batch(直到 1)
176
- const next = Math.max(1, Math.floor(batchSize / 2));
177
- const res = await estimateChunk(slice, next);
178
- out.push(...res);
179
- }
180
- }
181
- return out;
182
- };
183
- return await estimateChunk(userOps, Math.min(maxBatchSize, userOps.length));
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
+ }
184
165
  }
185
166
  /**
186
167
  * 发送 UserOperation
@@ -57,7 +57,6 @@ export declare class PortalQuery {
57
57
  private portal;
58
58
  private portalAddress;
59
59
  constructor(config?: PortalQueryConfig);
60
- private multicallAggregate3;
61
60
  /**
62
61
  * 获取 Provider
63
62
  */