four-flap-meme-sdk 1.5.21 → 1.5.23

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.
@@ -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, 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';
11
+ import { XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, DEFAULT_SALT, DEFAULT_GAS_PRICE, DEFAULT_CALL_GAS_LIMIT_SELL, 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,12 +26,16 @@ 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;
29
32
  this.chainId = config.chainId ?? XLAYER_CHAIN_ID;
30
33
  const rpcUrl = config.rpcUrl ?? DEFAULT_RPC_URL;
31
34
  this.rpcUrl = rpcUrl;
32
- this.provider = new JsonRpcProvider(rpcUrl, {
33
- chainId: this.chainId,
34
- name: 'xlayer',
35
+ this.provider = new JsonRpcProvider(rpcUrl, { chainId: this.chainId, name: 'xlayer' }, {
36
+ // 防止 ethers 自动攒太大的 JSON-RPC batch(XLayer 节点对“大 batch”很敏感)
37
+ batchMaxCount: 20,
38
+ batchStallTime: 30,
35
39
  });
36
40
  this.factoryAddress = config.factory ?? SIMPLE_ACCOUNT_FACTORY;
37
41
  this.entryPointAddress = config.entryPoint ?? ENTRYPOINT_V06;
@@ -39,6 +43,9 @@ export class AAAccountManager {
39
43
  this.paymaster = config.paymaster;
40
44
  this.paymasterData = config.paymasterData;
41
45
  this.gasLimitMultiplier = config.gasLimitMultiplier ?? GAS_LIMIT_MULTIPLIER;
46
+ // 默认 fixed:最大化降低 RPC / bundler 请求;如需更稳可显式传 bundlerEstimate
47
+ this.defaultGasPolicy = config.gasPolicy ?? 'fixed';
48
+ this.defaultFixedGas = config.fixedGas;
42
49
  this.factory = new Contract(this.factoryAddress, FACTORY_ABI, this.provider);
43
50
  this.entryPoint = new Contract(this.entryPointAddress, ENTRYPOINT_ABI, this.provider);
44
51
  this.bundler = new BundlerClient({
@@ -76,7 +83,14 @@ export class AAAccountManager {
76
83
  * 获取当前 Fee Data
77
84
  */
78
85
  async getFeeData() {
79
- return await this.provider.getFeeData();
86
+ // 费率在“批量构建 userOp”时会被大量复用;加短 TTL 缓存可显著减少 RPC
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;
80
94
  }
81
95
  /**
82
96
  * 预测 AA Sender 地址
@@ -275,7 +289,7 @@ export class AAAccountManager {
275
289
  * 构建未签名的 UserOperation(使用 Bundler 估算 Gas)
276
290
  */
277
291
  async buildUserOpWithBundlerEstimate(params) {
278
- const feeData = await this.provider.getFeeData();
292
+ const feeData = await this.getFeeData();
279
293
  const legacyGasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? DEFAULT_GAS_PRICE;
280
294
  const paymasterAndData = this.buildPaymasterAndData();
281
295
  // 构建初步 UserOp(Gas 字段为 0,等待估算)
@@ -324,7 +338,7 @@ export class AAAccountManager {
324
338
  if (params.ops.length === 0) {
325
339
  return { userOps: [], prefundWeis: [] };
326
340
  }
327
- const feeData = await this.provider.getFeeData();
341
+ const feeData = await this.getFeeData();
328
342
  const legacyGasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? DEFAULT_GAS_PRICE;
329
343
  const paymasterAndData = this.buildPaymasterAndData();
330
344
  // 先构建 skeleton(Gas=0),然后批量估算
@@ -375,7 +389,7 @@ export class AAAccountManager {
375
389
  * 适用于无法通过 Bundler 估算的场景(如尚未授权的 sell 操作)
376
390
  */
377
391
  async buildUserOpWithLocalEstimate(params) {
378
- const feeData = await this.provider.getFeeData();
392
+ const feeData = await this.getFeeData();
379
393
  const legacyGasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? DEFAULT_GAS_PRICE;
380
394
  const paymasterAndData = this.buildPaymasterAndData();
381
395
  // 估算 callGasLimit
@@ -417,11 +431,43 @@ export class AAAccountManager {
417
431
  const prefundWei = paymasterAndData !== '0x' ? 0n : gasTotal * legacyGasPrice;
418
432
  return { userOp, prefundWei };
419
433
  }
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
+ }
420
466
  /**
421
467
  * 签名 UserOperation
422
468
  */
423
469
  async signUserOp(userOp, ownerWallet) {
424
- const userOpHash = await this.entryPoint.getUserOpHash(userOp);
470
+ const userOpHash = computeUserOpHashV06(userOp, this.entryPointAddress, this.chainId);
425
471
  const signature = await ownerWallet.signMessage(ethers.getBytes(userOpHash));
426
472
  return {
427
473
  userOp: { ...userOp, signature },
@@ -446,7 +492,25 @@ export class AAAccountManager {
446
492
  };
447
493
  let userOp;
448
494
  let prefundWei;
449
- if (params.useBundlerEstimate !== false) {
495
+ const policy = params.gasPolicy ??
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') {
450
514
  const result = await this.buildUserOpWithBundlerEstimate(buildParams);
451
515
  userOp = result.userOp;
452
516
  prefundWei = result.prefundWei;
@@ -520,59 +584,133 @@ export class AAAccountManager {
520
584
  catch { /* ignore */ }
521
585
  }
522
586
  }
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'] });
587
+ // 3) deployed(getCode):小分片 + 并发上限 + 缓存 deployed=true,避免 -32014 batch request too many”
588
+ const deployed = new Array(senders.length).fill(false);
589
+ const needCode = [];
590
+ for (let i = 0; i < senders.length; i++) {
591
+ const sender = senders[i];
592
+ const key = sender.toLowerCase();
593
+ if (this.deployedSenderSet.has(key)) {
594
+ deployed[i] = true;
595
+ continue;
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 };
533
609
  }
534
- for (const s of slice) {
535
- reqs.push({ method: 'eth_getBalance', params: [s, 'latest'] });
610
+ catch (err) {
611
+ return { ok: false, chunk, err };
536
612
  }
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;
613
+ });
614
+ for (const r of batchResults) {
615
+ if (r.ok) {
616
+ for (let i = 0; i < r.chunk.length; i++) {
617
+ const it = r.chunk[i];
618
+ const code = r.codes[i];
619
+ const isDeployed = !!code && code !== '0x';
620
+ deployed[it.idx] = isDeployed;
621
+ if (isDeployed)
622
+ this.deployedSenderSet.add(it.key);
551
623
  }
624
+ continue;
552
625
  }
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 };
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 };
559
630
  });
560
- for (let i = 0; i < fetched.length; i++) {
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
+ }
637
+ }
638
+ }
639
+ // 4) balance(getEthBalance):用 Multicall3.getEthBalance 分片批量查询 OKB(减少 N 次 eth_getBalance)
640
+ const balances = new Array(senders.length).fill(0n);
641
+ const ethBalIface = new Interface(['function getEthBalance(address addr) view returns (uint256)']);
642
+ const balCalls = senders.map((sender) => ({
643
+ target: MULTICALL3,
644
+ allowFailure: true,
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];
561
654
  const idx = cursor + i;
562
- codes[idx] = fetched[i].code ?? '0x';
563
- balances[idx] = fetched[i].bal ?? 0n;
655
+ if (!r?.success || !r.returnData || r.returnData === '0x')
656
+ continue;
657
+ try {
658
+ const decoded = ethBalIface.decodeFunctionResult('getEthBalance', r.returnData);
659
+ balances[idx] = BigInt(decoded?.[0] ?? 0n);
660
+ }
661
+ catch { /* ignore */ }
564
662
  }
565
663
  }
566
664
  }
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
+ }
567
670
  return owners.map((owner, i) => ({
568
671
  owner,
569
672
  sender: senders[i],
570
- deployed: codes[i] != null && String(codes[i]) !== '0x',
673
+ deployed: deployed[i] ?? false,
571
674
  balance: balances[i] ?? 0n,
572
675
  nonce: nonces[i] ?? 0n,
573
676
  }));
574
677
  }
575
678
  }
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
+ }
576
714
  // ============================================================================
577
715
  // 工具函数
578
716
  // ============================================================================
@@ -15,6 +15,11 @@ import { mapWithConcurrency } from '../utils/concurrency.js';
15
15
  // ============================================================================
16
16
  // 捆绑交易执行器
17
17
  // ============================================================================
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;
18
23
  /**
19
24
  * XLayer 捆绑交易执行器
20
25
  *
@@ -109,14 +114,27 @@ export class BundleExecutor {
109
114
  const callData = encodeExecute(this.portalAddress, buyAmountWei, swapData);
110
115
  // 估算前确保 sender 有足够余额(用于模拟)
111
116
  await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, buyAmountWei + parseOkb('0.0003'), `${ownerName ?? 'owner'}/buy-prefund-before-estimate`);
112
- // 使用 Bundler 估算
113
- const { userOp, prefundWei } = await this.aaManager.buildUserOpWithBundlerEstimate({
114
- ownerWallet,
115
- sender: accountInfo.sender,
116
- callData,
117
- nonce: accountInfo.nonce,
118
- initCode,
119
- });
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
+ });
120
138
  // 补足 prefund + 买入金额
121
139
  await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, buyAmountWei + prefundWei + parseOkb('0.0002'), `${ownerName ?? 'owner'}/buy-fund`);
122
140
  // 签名
@@ -134,13 +152,27 @@ export class BundleExecutor {
134
152
  const approveData = encodeApproveCall(spender);
135
153
  const callData = encodeExecute(tokenAddress, 0n, approveData);
136
154
  await this.aaManager.ensureSenderBalance(ownerWallet, sender, parseOkb('0.0002'), `${ownerName ?? 'owner'}/approve-prefund`);
137
- const { userOp, prefundWei } = await this.aaManager.buildUserOpWithLocalEstimate({
138
- ownerWallet,
139
- sender,
140
- callData,
141
- nonce,
142
- initCode,
143
- });
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
+ });
144
176
  await this.aaManager.ensureSenderBalance(ownerWallet, sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/approve-fund`);
145
177
  const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
146
178
  return { ...signed, prefundWei, ownerName };
@@ -152,23 +184,37 @@ export class BundleExecutor {
152
184
  const sellData = encodeSellCall(tokenAddress, sellAmount, 0n);
153
185
  const callData = encodeExecute(this.portalAddress, 0n, sellData);
154
186
  await this.aaManager.ensureSenderBalance(ownerWallet, sender, parseOkb('0.0003'), `${ownerName ?? 'owner'}/sell-prefund`);
155
- // 如果需要 approve(还未执行),estimateGas revert,使用固定值
156
- const { userOp, prefundWei } = needApprove
157
- ? await this.aaManager.buildUserOpWithLocalEstimate({
187
+ const gasPolicy = this.config.gasPolicy ?? 'bundlerEstimate';
188
+ // 如果需要 approve(还未执行),estimateGas 可能 revert;因此默认就用固定 callGasLimit
189
+ const { userOp, prefundWei } = gasPolicy === 'fixed'
190
+ ? await this.aaManager.buildUserOpWithFixedGas({
158
191
  ownerWallet,
159
192
  sender,
160
193
  callData,
161
194
  nonce,
162
195
  initCode,
163
- callGasLimit: DEFAULT_CALL_GAS_LIMIT_SELL,
196
+ deployed: initCode === '0x',
197
+ fixedGas: {
198
+ ...(this.config.fixedGas ?? {}),
199
+ callGasLimit: this.config.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_SELL,
200
+ },
164
201
  })
165
- : await this.aaManager.buildUserOpWithLocalEstimate({
166
- ownerWallet,
167
- sender,
168
- callData,
169
- nonce,
170
- initCode,
171
- });
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
+ });
172
218
  await this.aaManager.ensureSenderBalance(ownerWallet, sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/sell-fund`);
173
219
  const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
174
220
  return { ...signed, prefundWei, ownerName };
@@ -203,13 +249,27 @@ export class BundleExecutor {
203
249
  // 先估算 prefund(使用空调用)
204
250
  const tempCallData = encodeExecute(params.ownerWallet.address, 0n, '0x');
205
251
  await this.aaManager.ensureSenderBalance(params.ownerWallet, params.sender, parseOkb('0.0002'), `${params.ownerName ?? 'owner'}/withdraw-prefund`);
206
- const { prefundWei } = await this.aaManager.buildUserOpWithLocalEstimate({
207
- ownerWallet: params.ownerWallet,
208
- sender: params.sender,
209
- callData: tempCallData,
210
- nonce: params.nonce,
211
- initCode: params.initCode,
212
- });
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
+ });
213
273
  // 计算可归集金额(用已知余额近似;fund 发生时余额会变大,属于可接受的保守近似)
214
274
  const withdrawAmount = senderBalance > prefundWei + params.reserveWei
215
275
  ? senderBalance - prefundWei - params.reserveWei
@@ -219,13 +279,26 @@ export class BundleExecutor {
219
279
  return null;
220
280
  }
221
281
  const callData = encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
222
- const { userOp } = await this.aaManager.buildUserOpWithLocalEstimate({
223
- ownerWallet: params.ownerWallet,
224
- sender: params.sender,
225
- callData,
226
- nonce: params.nonce,
227
- initCode: params.initCode,
228
- });
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
+ });
229
302
  console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(withdrawAmount)} OKB`);
230
303
  const signed = await this.aaManager.signUserOp(userOp, params.ownerWallet);
231
304
  return { ...signed, prefundWei, ownerName: params.ownerName };
@@ -243,12 +316,26 @@ export class BundleExecutor {
243
316
  const transferData = encodeTransferCall(ownerWallet.address, tokenBalance);
244
317
  const callData = encodeExecute(tokenAddress, 0n, transferData);
245
318
  await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, parseOkb('0.0002'), `${ownerName ?? 'owner'}/transfer-prefund`);
246
- const { userOp, prefundWei } = await this.aaManager.buildUserOpWithBundlerEstimate({
247
- ownerWallet,
248
- sender: accountInfo.sender,
249
- callData,
250
- nonce: accountInfo.nonce,
251
- });
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
+ });
252
339
  await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/transfer-fund`);
253
340
  console.log(`\n[${ownerName ?? 'owner'}] transfer token: ${tokenBalance.toString()}`);
254
341
  const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
@@ -279,7 +366,10 @@ export class BundleExecutor {
279
366
  const accountInfos = await this.aaManager.getMultipleAccountInfo(wallets.map((w) => w.address));
280
367
  const buyWeis = buyAmounts.map((a) => parseOkb(a));
281
368
  // 估算前确保 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`)));
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
+ });
283
373
  const buyCallDatas = buyWeis.map((buyWei) => {
284
374
  const swapData = encodeBuyCall(tokenAddress, buyWei, 0n);
285
375
  return encodeExecute(this.portalAddress, buyWei, swapData);
@@ -294,9 +384,11 @@ export class BundleExecutor {
294
384
  })),
295
385
  });
296
386
  // 补足 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])));
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]));
300
392
  const buyOps = signedBuy.map((s) => s.userOp);
301
393
  // 2. 执行买入
302
394
  const buyResult = await this.runHandleOps('buyBundle', buyOps, bundlerSigner, beneficiary);
@@ -322,7 +414,9 @@ export class BundleExecutor {
322
414
  }
323
415
  if (idxs.length > 0) {
324
416
  // 估算前补一点余额(paymaster 会自动跳过)
325
- await Promise.all(idxs.map((i) => this.aaManager.ensureSenderBalance(wallets[i], senders[i], parseOkb('0.0002'), `owner${i + 1}/transfer-prefund`)));
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
+ });
326
420
  // buy 已经成功过一次,因此 transfer 的 nonce = 原 nonce + 1,且 initCode = 0x
327
421
  const { userOps: transferUserOps, prefundWeis: transferPrefunds } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
328
422
  ops: idxs.map((i, k) => ({
@@ -332,8 +426,10 @@ export class BundleExecutor {
332
426
  initCode: '0x',
333
427
  })),
334
428
  });
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]])));
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]]));
337
433
  const transferOps = signedTransfer.map((s) => s.userOp);
338
434
  transferResult =
339
435
  (await this.runHandleOps('transferBundle', transferOps, bundlerSigner, beneficiary)) ?? undefined;