four-flap-meme-sdk 1.5.24 → 1.5.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@
8
8
  * - initCode 生成
9
9
  */
10
10
  import { ethers, JsonRpcProvider, Wallet, Contract } from 'ethers';
11
- import type { UserOperation, SignedUserOp, AAAccount, XLayerConfig } from './types.js';
11
+ import type { UserOperation, SignedUserOp, AAAccount, XLayerConfig, GasPolicy, FixedGasConfig } from './types.js';
12
12
  import { BundlerClient } from './bundler.js';
13
13
  /**
14
14
  * AA 账户管理器
@@ -33,6 +33,11 @@ export declare class AAAccountManager {
33
33
  private paymasterData?;
34
34
  private gasLimitMultiplier;
35
35
  private senderCache;
36
+ private deployedSenderSet;
37
+ private feeDataCache?;
38
+ private readonly feeDataCacheTtlMs;
39
+ private defaultGasPolicy?;
40
+ private defaultFixedGas?;
36
41
  constructor(config?: XLayerConfig);
37
42
  /**
38
43
  * 获取 Provider
@@ -127,6 +132,23 @@ export declare class AAAccountManager {
127
132
  userOp: UserOperation;
128
133
  prefundWei: bigint;
129
134
  }>;
135
+ /**
136
+ * 构建未签名的 UserOperation(固定 Gas,不做任何 estimate)
137
+ *
138
+ * 适用于大规模(1000 地址)场景:尽量减少 RPC 调用量。
139
+ */
140
+ buildUserOpWithFixedGas(params: {
141
+ ownerWallet: Wallet;
142
+ sender: string;
143
+ callData: string;
144
+ nonce: bigint;
145
+ initCode?: string;
146
+ deployed?: boolean;
147
+ fixedGas?: FixedGasConfig;
148
+ }): Promise<{
149
+ userOp: UserOperation;
150
+ prefundWei: bigint;
151
+ }>;
130
152
  /**
131
153
  * 签名 UserOperation
132
154
  */
@@ -138,6 +160,8 @@ export declare class AAAccountManager {
138
160
  ownerWallet: Wallet;
139
161
  callData: string;
140
162
  value?: bigint;
163
+ gasPolicy?: GasPolicy;
164
+ fixedGas?: FixedGasConfig;
141
165
  useBundlerEstimate?: boolean;
142
166
  callGasLimit?: bigint;
143
167
  }): Promise<SignedUserOp>;
@@ -159,6 +183,13 @@ export declare class AAAccountManager {
159
183
  */
160
184
  getMultipleAccountInfo(ownerAddresses: string[]): Promise<AAAccount[]>;
161
185
  }
186
+ /**
187
+ * ERC-4337 v0.6 userOpHash 本地计算
188
+ *
189
+ * pack 规则(v0.6):对 (sender,nonce,keccak(initCode),keccak(callData),callGasLimit,verificationGasLimit,preVerificationGas,maxFeePerGas,maxPriorityFeePerGas,keccak(paymasterAndData))
190
+ * 做 abi.encode 后 keccak 得到 packHash;再 abi.encode(packHash, entryPoint, chainId) 后 keccak 得到 userOpHash。
191
+ */
192
+ export declare function computeUserOpHashV06(userOp: UserOperation, entryPoint: string, chainId: number | bigint): string;
162
193
  /**
163
194
  * 编码 SimpleAccount.execute 调用
164
195
  */
@@ -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
  // ============================================================================
@@ -67,6 +67,11 @@ class AANonceMap {
67
67
  // ============================================================================
68
68
  // 捆绑交易执行器
69
69
  // ============================================================================
70
+ // 固定 gas(用于大规模减少 RPC);具体值允许通过 config.fixedGas 覆盖
71
+ const DEFAULT_CALL_GAS_LIMIT_BUY = DEFAULT_CALL_GAS_LIMIT_SELL; // buy 与 sell 共享一个保守值
72
+ const DEFAULT_CALL_GAS_LIMIT_APPROVE = 200000n;
73
+ const DEFAULT_CALL_GAS_LIMIT_TRANSFER = 150000n;
74
+ const DEFAULT_CALL_GAS_LIMIT_WITHDRAW = 120000n;
70
75
  /**
71
76
  * XLayer 捆绑交易执行器
72
77
  *
@@ -161,14 +166,27 @@ export class BundleExecutor {
161
166
  const callData = encodeExecute(this.portalAddress, buyAmountWei, swapData);
162
167
  // 估算前确保 sender 有足够余额(用于模拟)
163
168
  await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, buyAmountWei + parseOkb('0.0003'), `${ownerName ?? 'owner'}/buy-prefund-before-estimate`);
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
- });
169
+ const gasPolicy = this.config.gasPolicy ?? 'bundlerEstimate';
170
+ const { userOp, prefundWei } = gasPolicy === 'fixed'
171
+ ? await this.aaManager.buildUserOpWithFixedGas({
172
+ ownerWallet,
173
+ sender: accountInfo.sender,
174
+ callData,
175
+ nonce: accountInfo.nonce,
176
+ initCode,
177
+ deployed: accountInfo.deployed,
178
+ fixedGas: {
179
+ ...(this.config.fixedGas ?? {}),
180
+ callGasLimit: this.config.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_BUY,
181
+ },
182
+ })
183
+ : await this.aaManager.buildUserOpWithBundlerEstimate({
184
+ ownerWallet,
185
+ sender: accountInfo.sender,
186
+ callData,
187
+ nonce: accountInfo.nonce,
188
+ initCode,
189
+ });
172
190
  // 补足 prefund + 买入金额
173
191
  await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, buyAmountWei + prefundWei + parseOkb('0.0002'), `${ownerName ?? 'owner'}/buy-fund`);
174
192
  // 签名
@@ -186,13 +204,27 @@ export class BundleExecutor {
186
204
  const approveData = encodeApproveCall(spender);
187
205
  const callData = encodeExecute(tokenAddress, 0n, approveData);
188
206
  await this.aaManager.ensureSenderBalance(ownerWallet, sender, parseOkb('0.0002'), `${ownerName ?? 'owner'}/approve-prefund`);
189
- const { userOp, prefundWei } = await this.aaManager.buildUserOpWithLocalEstimate({
190
- ownerWallet,
191
- sender,
192
- callData,
193
- nonce,
194
- initCode,
195
- });
207
+ const gasPolicy = this.config.gasPolicy ?? 'bundlerEstimate';
208
+ const { userOp, prefundWei } = gasPolicy === 'fixed'
209
+ ? await this.aaManager.buildUserOpWithFixedGas({
210
+ ownerWallet,
211
+ sender,
212
+ callData,
213
+ nonce,
214
+ initCode,
215
+ deployed: initCode === '0x',
216
+ fixedGas: {
217
+ ...(this.config.fixedGas ?? {}),
218
+ callGasLimit: this.config.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_APPROVE,
219
+ },
220
+ })
221
+ : await this.aaManager.buildUserOpWithLocalEstimate({
222
+ ownerWallet,
223
+ sender,
224
+ callData,
225
+ nonce,
226
+ initCode,
227
+ });
196
228
  await this.aaManager.ensureSenderBalance(ownerWallet, sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/approve-fund`);
197
229
  const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
198
230
  return { ...signed, prefundWei, ownerName };
@@ -204,23 +236,37 @@ export class BundleExecutor {
204
236
  const sellData = encodeSellCall(tokenAddress, sellAmount, 0n);
205
237
  const callData = encodeExecute(this.portalAddress, 0n, sellData);
206
238
  await this.aaManager.ensureSenderBalance(ownerWallet, sender, parseOkb('0.0003'), `${ownerName ?? 'owner'}/sell-prefund`);
207
- // 如果需要 approve(还未执行),estimateGas revert,使用固定值
208
- const { userOp, prefundWei } = needApprove
209
- ? await this.aaManager.buildUserOpWithLocalEstimate({
239
+ const gasPolicy = this.config.gasPolicy ?? 'bundlerEstimate';
240
+ // 如果需要 approve(还未执行),estimateGas 可能 revert;因此默认就用固定 callGasLimit
241
+ const { userOp, prefundWei } = gasPolicy === 'fixed'
242
+ ? await this.aaManager.buildUserOpWithFixedGas({
210
243
  ownerWallet,
211
244
  sender,
212
245
  callData,
213
246
  nonce,
214
247
  initCode,
215
- callGasLimit: DEFAULT_CALL_GAS_LIMIT_SELL,
248
+ deployed: initCode === '0x',
249
+ fixedGas: {
250
+ ...(this.config.fixedGas ?? {}),
251
+ callGasLimit: this.config.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_SELL,
252
+ },
216
253
  })
217
- : await this.aaManager.buildUserOpWithLocalEstimate({
218
- ownerWallet,
219
- sender,
220
- callData,
221
- nonce,
222
- initCode,
223
- });
254
+ : needApprove
255
+ ? await this.aaManager.buildUserOpWithLocalEstimate({
256
+ ownerWallet,
257
+ sender,
258
+ callData,
259
+ nonce,
260
+ initCode,
261
+ callGasLimit: DEFAULT_CALL_GAS_LIMIT_SELL,
262
+ })
263
+ : await this.aaManager.buildUserOpWithLocalEstimate({
264
+ ownerWallet,
265
+ sender,
266
+ callData,
267
+ nonce,
268
+ initCode,
269
+ });
224
270
  await this.aaManager.ensureSenderBalance(ownerWallet, sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/sell-fund`);
225
271
  const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
226
272
  return { ...signed, prefundWei, ownerName };
@@ -255,13 +301,27 @@ export class BundleExecutor {
255
301
  // 先估算 prefund(使用空调用)
256
302
  const tempCallData = encodeExecute(params.ownerWallet.address, 0n, '0x');
257
303
  await this.aaManager.ensureSenderBalance(params.ownerWallet, params.sender, parseOkb('0.0002'), `${params.ownerName ?? 'owner'}/withdraw-prefund`);
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
- });
304
+ const gasPolicy = this.config.gasPolicy ?? 'bundlerEstimate';
305
+ const { prefundWei } = gasPolicy === 'fixed'
306
+ ? await this.aaManager.buildUserOpWithFixedGas({
307
+ ownerWallet: params.ownerWallet,
308
+ sender: params.sender,
309
+ callData: tempCallData,
310
+ nonce: params.nonce,
311
+ initCode: params.initCode,
312
+ deployed: params.initCode === '0x',
313
+ fixedGas: {
314
+ ...(this.config.fixedGas ?? {}),
315
+ callGasLimit: this.config.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
316
+ },
317
+ })
318
+ : await this.aaManager.buildUserOpWithLocalEstimate({
319
+ ownerWallet: params.ownerWallet,
320
+ sender: params.sender,
321
+ callData: tempCallData,
322
+ nonce: params.nonce,
323
+ initCode: params.initCode,
324
+ });
265
325
  // 计算可归集金额(用已知余额近似;fund 发生时余额会变大,属于可接受的保守近似)
266
326
  const withdrawAmount = senderBalance > prefundWei + params.reserveWei
267
327
  ? senderBalance - prefundWei - params.reserveWei
@@ -271,13 +331,26 @@ export class BundleExecutor {
271
331
  return null;
272
332
  }
273
333
  const callData = encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
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
- });
334
+ const { userOp } = gasPolicy === 'fixed'
335
+ ? await this.aaManager.buildUserOpWithFixedGas({
336
+ ownerWallet: params.ownerWallet,
337
+ sender: params.sender,
338
+ callData,
339
+ nonce: params.nonce,
340
+ initCode: params.initCode,
341
+ deployed: params.initCode === '0x',
342
+ fixedGas: {
343
+ ...(this.config.fixedGas ?? {}),
344
+ callGasLimit: this.config.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
345
+ },
346
+ })
347
+ : await this.aaManager.buildUserOpWithLocalEstimate({
348
+ ownerWallet: params.ownerWallet,
349
+ sender: params.sender,
350
+ callData,
351
+ nonce: params.nonce,
352
+ initCode: params.initCode,
353
+ });
281
354
  console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(withdrawAmount)} OKB`);
282
355
  const signed = await this.aaManager.signUserOp(userOp, params.ownerWallet);
283
356
  return { ...signed, prefundWei, ownerName: params.ownerName };
@@ -295,12 +368,26 @@ export class BundleExecutor {
295
368
  const transferData = encodeTransferCall(ownerWallet.address, tokenBalance);
296
369
  const callData = encodeExecute(tokenAddress, 0n, transferData);
297
370
  await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, parseOkb('0.0002'), `${ownerName ?? 'owner'}/transfer-prefund`);
298
- const { userOp, prefundWei } = await this.aaManager.buildUserOpWithBundlerEstimate({
299
- ownerWallet,
300
- sender: accountInfo.sender,
301
- callData,
302
- nonce: accountInfo.nonce,
303
- });
371
+ const gasPolicy = this.config.gasPolicy ?? 'bundlerEstimate';
372
+ const { userOp, prefundWei } = gasPolicy === 'fixed'
373
+ ? await this.aaManager.buildUserOpWithFixedGas({
374
+ ownerWallet,
375
+ sender: accountInfo.sender,
376
+ callData,
377
+ nonce: accountInfo.nonce,
378
+ initCode: accountInfo.deployed ? '0x' : this.aaManager.generateInitCode(ownerWallet.address),
379
+ deployed: accountInfo.deployed,
380
+ fixedGas: {
381
+ ...(this.config.fixedGas ?? {}),
382
+ callGasLimit: this.config.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_TRANSFER,
383
+ },
384
+ })
385
+ : await this.aaManager.buildUserOpWithBundlerEstimate({
386
+ ownerWallet,
387
+ sender: accountInfo.sender,
388
+ callData,
389
+ nonce: accountInfo.nonce,
390
+ });
304
391
  await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/transfer-fund`);
305
392
  console.log(`\n[${ownerName ?? 'owner'}] transfer token: ${tokenBalance.toString()}`);
306
393
  const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
@@ -334,7 +421,10 @@ export class BundleExecutor {
334
421
  for (const ai of accountInfos)
335
422
  nonceMap.init(ai.sender, ai.nonce);
336
423
  // 估算前确保 sender 有足够余额(用于 bundler 模拟;paymaster 场景会自动跳过)
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`)));
424
+ // 避免 Promise.all 突发并发(大规模地址会触发 RPC 限流)
425
+ await mapWithConcurrency(accountInfos, 6, async (ai, i) => {
426
+ await this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWeis[i] + parseOkb('0.0003'), `owner${i + 1}/buy-prefund-before-estimate`);
427
+ });
338
428
  const buyCallDatas = buyWeis.map((buyWei) => {
339
429
  const swapData = encodeBuyCall(tokenAddress, buyWei, 0n);
340
430
  return encodeExecute(this.portalAddress, buyWei, swapData);
@@ -349,9 +439,11 @@ export class BundleExecutor {
349
439
  })),
350
440
  });
351
441
  // 补足 prefund + 买入金额(多数情况下上一步的 0.0003 已足够,这里通常不会再转账)
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])));
442
+ await mapWithConcurrency(accountInfos, 6, async (ai, i) => {
443
+ await this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWeis[i] + prefundWeis[i] + parseOkb('0.0002'), `owner${i + 1}/buy-fund`);
444
+ });
445
+ // 签名(受控并发,避免大规模时阻塞)
446
+ const signedBuy = await mapWithConcurrency(buyUserOps, 10, async (op, i) => this.aaManager.signUserOp(op, wallets[i]));
355
447
  const buyOps = signedBuy.map((s) => s.userOp);
356
448
  // 2. 执行买入
357
449
  const buyResult = await this.runHandleOps('buyBundle', buyOps, bundlerSigner, beneficiary);
@@ -379,7 +471,9 @@ export class BundleExecutor {
379
471
  }
380
472
  if (idxs.length > 0) {
381
473
  // 估算前补一点余额(paymaster 会自动跳过)
382
- await Promise.all(idxs.map((i) => this.aaManager.ensureSenderBalance(wallets[i], senders[i], parseOkb('0.0002'), `owner${i + 1}/transfer-prefund`)));
474
+ await mapWithConcurrency(idxs, 6, async (i) => {
475
+ await this.aaManager.ensureSenderBalance(wallets[i], senders[i], parseOkb('0.0002'), `owner${i + 1}/transfer-prefund`);
476
+ });
383
477
  // buy 已经成功过一次,因此 transfer 的 nonce = 原 nonce + 1,且 initCode = 0x
384
478
  const { userOps: transferUserOps, prefundWeis: transferPrefunds } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
385
479
  ops: idxs.map((i, k) => ({
@@ -389,8 +483,10 @@ export class BundleExecutor {
389
483
  initCode: '0x',
390
484
  })),
391
485
  });
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]])));
486
+ await mapWithConcurrency(idxs, 6, async (i, k) => {
487
+ await this.aaManager.ensureSenderBalance(wallets[i], senders[i], transferPrefunds[k] + parseOkb('0.00005'), `owner${i + 1}/transfer-fund`);
488
+ });
489
+ const signedTransfer = await mapWithConcurrency(transferUserOps, 10, async (op, k) => this.aaManager.signUserOp(op, wallets[idxs[k]]));
394
490
  const transferOps = signedTransfer.map((s) => s.userOp);
395
491
  transferResult =
396
492
  (await this.runHandleOps('transferBundle', transferOps, bundlerSigner, beneficiary)) ?? undefined;
@@ -4,6 +4,7 @@
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';
7
8
  // ============================================================================
8
9
  // Bundler 客户端类
9
10
  // ============================================================================
@@ -151,17 +152,35 @@ export class BundlerClient {
151
152
  async estimateUserOperationGasBatch(userOps) {
152
153
  if (userOps.length === 0)
153
154
  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
- }
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));
165
184
  }
166
185
  /**
167
186
  * 发送 UserOperation
@@ -57,6 +57,7 @@ export declare class PortalQuery {
57
57
  private portal;
58
58
  private portalAddress;
59
59
  constructor(config?: PortalQueryConfig);
60
+ private multicallAggregate3;
60
61
  /**
61
62
  * 获取 Provider
62
63
  */
@@ -8,7 +8,8 @@
8
8
  * - 代币转账
9
9
  */
10
10
  import { ethers, Interface, Contract, JsonRpcProvider } from 'ethers';
11
- import { FLAP_PORTAL, ZERO_ADDRESS, PORTAL_ABI, ERC20_ABI, DEFAULT_RPC_URL, XLAYER_CHAIN_ID, } from './constants.js';
11
+ import { FLAP_PORTAL, ZERO_ADDRESS, PORTAL_ABI, ERC20_ABI, MULTICALL3, DEFAULT_RPC_URL, XLAYER_CHAIN_ID, } from './constants.js';
12
+ import { mapWithConcurrency } from '../utils/concurrency.js';
12
13
  // ============================================================================
13
14
  // Portal 操作编码器
14
15
  // ============================================================================
@@ -81,10 +82,22 @@ export class PortalQuery {
81
82
  constructor(config = {}) {
82
83
  const rpcUrl = config.rpcUrl ?? DEFAULT_RPC_URL;
83
84
  const chainId = config.chainId ?? XLAYER_CHAIN_ID;
84
- this.provider = new JsonRpcProvider(rpcUrl, { chainId, name: 'xlayer' });
85
+ this.provider = new JsonRpcProvider(rpcUrl, { chainId, name: 'xlayer' }, {
86
+ batchMaxCount: 20,
87
+ batchStallTime: 30,
88
+ });
85
89
  this.portalAddress = config.portalAddress ?? FLAP_PORTAL;
86
90
  this.portal = new Contract(this.portalAddress, PORTAL_ABI, this.provider);
87
91
  }
92
+ async multicallAggregate3(params) {
93
+ const mcIface = new Interface([
94
+ 'function aggregate3((address target,bool allowFailure,bytes callData)[] calls) view returns ((bool success,bytes returnData)[] returnData)',
95
+ ]);
96
+ const data = mcIface.encodeFunctionData('aggregate3', [params.calls]);
97
+ const raw = await this.provider.call({ to: MULTICALL3, data });
98
+ const decoded = mcIface.decodeFunctionResult('aggregate3', raw)?.[0];
99
+ return decoded || [];
100
+ }
88
101
  /**
89
102
  * 获取 Provider
90
103
  */
@@ -171,8 +184,42 @@ export class PortalQuery {
171
184
  */
172
185
  async getMultipleTokenBalances(tokenAddress, accounts) {
173
186
  const balances = new Map();
174
- const results = await Promise.all(accounts.map((acc) => this.getTokenBalance(tokenAddress, acc)));
175
- accounts.forEach((acc, i) => balances.set(acc, results[i]));
187
+ const list = accounts.map((a) => String(a || '').trim()).filter(Boolean);
188
+ if (list.length === 0)
189
+ return balances;
190
+ const calls = list.map((acc) => ({
191
+ target: tokenAddress,
192
+ allowFailure: true,
193
+ callData: erc20Iface.encodeFunctionData('balanceOf', [acc]),
194
+ }));
195
+ const BATCH = 350;
196
+ try {
197
+ for (let cursor = 0; cursor < calls.length; cursor += BATCH) {
198
+ const sliceCalls = calls.slice(cursor, cursor + BATCH);
199
+ const res = await this.multicallAggregate3({ calls: sliceCalls });
200
+ for (let i = 0; i < res.length; i++) {
201
+ const r = res[i];
202
+ const idx = cursor + i;
203
+ const acc = list[idx];
204
+ if (!r?.success || !r.returnData || r.returnData === '0x') {
205
+ balances.set(acc, 0n);
206
+ continue;
207
+ }
208
+ try {
209
+ const decoded = erc20Iface.decodeFunctionResult('balanceOf', r.returnData);
210
+ balances.set(acc, BigInt(decoded?.[0] ?? 0n));
211
+ }
212
+ catch {
213
+ balances.set(acc, 0n);
214
+ }
215
+ }
216
+ }
217
+ return balances;
218
+ }
219
+ catch {
220
+ const results = await mapWithConcurrency(list, 8, async (acc) => this.getTokenBalance(tokenAddress, acc));
221
+ list.forEach((acc, i) => balances.set(acc, results[i] ?? 0n));
222
+ }
176
223
  return balances;
177
224
  }
178
225
  /**
@@ -180,8 +227,43 @@ export class PortalQuery {
180
227
  */
181
228
  async getMultipleOkbBalances(accounts) {
182
229
  const balances = new Map();
183
- const results = await Promise.all(accounts.map((acc) => this.getOkbBalance(acc)));
184
- accounts.forEach((acc, i) => balances.set(acc, results[i]));
230
+ const list = accounts.map((a) => String(a || '').trim()).filter(Boolean);
231
+ if (list.length === 0)
232
+ return balances;
233
+ const ethBalIface = new Interface(['function getEthBalance(address addr) view returns (uint256)']);
234
+ const calls = list.map((acc) => ({
235
+ target: MULTICALL3,
236
+ allowFailure: true,
237
+ callData: ethBalIface.encodeFunctionData('getEthBalance', [acc]),
238
+ }));
239
+ const BATCH = 350;
240
+ try {
241
+ for (let cursor = 0; cursor < calls.length; cursor += BATCH) {
242
+ const sliceCalls = calls.slice(cursor, cursor + BATCH);
243
+ const res = await this.multicallAggregate3({ calls: sliceCalls });
244
+ for (let i = 0; i < res.length; i++) {
245
+ const r = res[i];
246
+ const idx = cursor + i;
247
+ const acc = list[idx];
248
+ if (!r?.success || !r.returnData || r.returnData === '0x') {
249
+ balances.set(acc, 0n);
250
+ continue;
251
+ }
252
+ try {
253
+ const decoded = ethBalIface.decodeFunctionResult('getEthBalance', r.returnData);
254
+ balances.set(acc, BigInt(decoded?.[0] ?? 0n));
255
+ }
256
+ catch {
257
+ balances.set(acc, 0n);
258
+ }
259
+ }
260
+ }
261
+ return balances;
262
+ }
263
+ catch {
264
+ const results = await mapWithConcurrency(list, 8, async (acc) => this.getOkbBalance(acc));
265
+ list.forEach((acc, i) => balances.set(acc, results[i] ?? 0n));
266
+ }
185
267
  return balances;
186
268
  }
187
269
  /**
@@ -189,8 +271,43 @@ export class PortalQuery {
189
271
  */
190
272
  async getMultipleAllowances(tokenAddress, owners, spender) {
191
273
  const allowances = new Map();
192
- const results = await Promise.all(owners.map((owner) => this.getAllowance(tokenAddress, owner, spender)));
193
- owners.forEach((owner, i) => allowances.set(owner, results[i]));
274
+ const list = owners.map((a) => String(a || '').trim()).filter(Boolean);
275
+ if (list.length === 0)
276
+ return allowances;
277
+ const useSpender = spender ?? this.portalAddress;
278
+ const calls = list.map((owner) => ({
279
+ target: tokenAddress,
280
+ allowFailure: true,
281
+ callData: erc20Iface.encodeFunctionData('allowance', [owner, useSpender]),
282
+ }));
283
+ const BATCH = 350;
284
+ try {
285
+ for (let cursor = 0; cursor < calls.length; cursor += BATCH) {
286
+ const sliceCalls = calls.slice(cursor, cursor + BATCH);
287
+ const res = await this.multicallAggregate3({ calls: sliceCalls });
288
+ for (let i = 0; i < res.length; i++) {
289
+ const r = res[i];
290
+ const idx = cursor + i;
291
+ const owner = list[idx];
292
+ if (!r?.success || !r.returnData || r.returnData === '0x') {
293
+ allowances.set(owner, 0n);
294
+ continue;
295
+ }
296
+ try {
297
+ const decoded = erc20Iface.decodeFunctionResult('allowance', r.returnData);
298
+ allowances.set(owner, BigInt(decoded?.[0] ?? 0n));
299
+ }
300
+ catch {
301
+ allowances.set(owner, 0n);
302
+ }
303
+ }
304
+ }
305
+ return allowances;
306
+ }
307
+ catch {
308
+ const results = await mapWithConcurrency(list, 8, async (owner) => this.getAllowance(tokenAddress, owner, useSpender));
309
+ list.forEach((owner, i) => allowances.set(owner, results[i] ?? 0n));
310
+ }
194
311
  return allowances;
195
312
  }
196
313
  }
@@ -41,6 +41,17 @@ export interface GasEstimate {
41
41
  maxFeePerGas?: HexString;
42
42
  maxPriorityFeePerGas?: HexString;
43
43
  }
44
+ export type GasPolicy = 'fixed' | 'localEstimate' | 'bundlerEstimate';
45
+ export interface FixedGasConfig {
46
+ /** callGasLimit(若不提供,SDK 会使用较保守的默认值) */
47
+ callGasLimit?: bigint;
48
+ /** 已部署 sender 的 verificationGasLimit(默认用 SDK 常量) */
49
+ verificationGasLimitDeployed?: bigint;
50
+ /** 未部署 sender 的 verificationGasLimit(默认用 SDK 常量) */
51
+ verificationGasLimitUndeployed?: bigint;
52
+ /** preVerificationGas(默认用 SDK 常量) */
53
+ preVerificationGas?: bigint;
54
+ }
44
55
  /**
45
56
  * XLayer SDK 基础配置
46
57
  */
@@ -65,6 +76,15 @@ export interface XLayerConfig {
65
76
  timeoutMs?: number;
66
77
  /** Gas 估算安全余量倍数 */
67
78
  gasLimitMultiplier?: number;
79
+ /**
80
+ * AA Gas 策略(用于大规模地址时减少 RPC)
81
+ * - fixed:固定 gas(不 estimate)
82
+ * - localEstimate:eth_estimateGas(不走 bundler)
83
+ * - bundlerEstimate:eth_estimateUserOperationGas(最慢但最稳)
84
+ */
85
+ gasPolicy?: GasPolicy;
86
+ /** fixed 策略的默认 gas 配置(可被每次调用覆盖) */
87
+ fixedGas?: FixedGasConfig;
68
88
  }
69
89
  /**
70
90
  * AA 账户信息
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.5.24",
3
+ "version": "1.5.25",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,16 +0,0 @@
1
- /**
2
- * ECDH + AES-GCM 加密工具(浏览器兼容)
3
- * 用于将签名交易用服务器公钥加密
4
- */
5
- /**
6
- * 用服务器公钥加密签名交易(ECDH + AES-GCM)
7
- *
8
- * @param signedTransactions 签名后的交易数组
9
- * @param publicKeyBase64 服务器提供的公钥(Base64 格式)
10
- * @returns JSON 字符串 {e: 临时公钥, i: IV, d: 密文}
11
- */
12
- export declare function encryptWithPublicKey(signedTransactions: string[], publicKeyBase64: string): Promise<string>;
13
- /**
14
- * 验证公钥格式(Base64)
15
- */
16
- export declare function validatePublicKey(publicKeyBase64: string): boolean;
@@ -1,146 +0,0 @@
1
- /**
2
- * ECDH + AES-GCM 加密工具(浏览器兼容)
3
- * 用于将签名交易用服务器公钥加密
4
- */
5
- /**
6
- * 获取全局 crypto 对象(最简单直接的方式)
7
- */
8
- function getCryptoAPI() {
9
- // 尝试所有可能的全局对象,优先浏览器环境
10
- const cryptoObj = (typeof window !== 'undefined' && window.crypto) ||
11
- (typeof self !== 'undefined' && self.crypto) ||
12
- (typeof global !== 'undefined' && global.crypto) ||
13
- (typeof globalThis !== 'undefined' && globalThis.crypto);
14
- if (!cryptoObj) {
15
- const env = typeof window !== 'undefined' ? 'Browser' : 'Node.js';
16
- const protocol = typeof location !== 'undefined' ? location.protocol : 'unknown';
17
- throw new Error(`❌ Crypto API 不可用。环境: ${env}, 协议: ${protocol}. ` +
18
- '请确保在 HTTPS 或 localhost 下运行');
19
- }
20
- return cryptoObj;
21
- }
22
- /**
23
- * 获取 SubtleCrypto(用于加密操作)
24
- */
25
- function getSubtleCrypto() {
26
- const crypto = getCryptoAPI();
27
- if (!crypto.subtle) {
28
- const protocol = typeof location !== 'undefined' ? location.protocol : 'unknown';
29
- const hostname = typeof location !== 'undefined' ? location.hostname : 'unknown';
30
- throw new Error(`❌ SubtleCrypto API 不可用。协议: ${protocol}, 主机: ${hostname}. ` +
31
- '请确保:1) 使用 HTTPS (或 localhost);2) 浏览器支持 Web Crypto API;' +
32
- '3) 不在无痕/隐私浏览模式下');
33
- }
34
- return crypto.subtle;
35
- }
36
- /**
37
- * Base64 转 ArrayBuffer(优先使用浏览器 API)
38
- */
39
- function base64ToArrayBuffer(base64) {
40
- // 浏览器环境(优先)
41
- if (typeof atob !== 'undefined') {
42
- const binaryString = atob(base64);
43
- const bytes = new Uint8Array(binaryString.length);
44
- for (let i = 0; i < binaryString.length; i++) {
45
- bytes[i] = binaryString.charCodeAt(i);
46
- }
47
- return bytes.buffer;
48
- }
49
- // Node.js 环境(fallback)
50
- if (typeof Buffer !== 'undefined') {
51
- return Buffer.from(base64, 'base64').buffer;
52
- }
53
- throw new Error('❌ Base64 解码不可用');
54
- }
55
- /**
56
- * ArrayBuffer 转 Base64(优先使用浏览器 API)
57
- */
58
- function arrayBufferToBase64(buffer) {
59
- // 浏览器环境(优先)
60
- if (typeof btoa !== 'undefined') {
61
- const bytes = new Uint8Array(buffer);
62
- let binary = '';
63
- for (let i = 0; i < bytes.length; i++) {
64
- binary += String.fromCharCode(bytes[i]);
65
- }
66
- return btoa(binary);
67
- }
68
- // Node.js 环境(fallback)
69
- if (typeof Buffer !== 'undefined') {
70
- return Buffer.from(buffer).toString('base64');
71
- }
72
- throw new Error('❌ Base64 编码不可用');
73
- }
74
- /**
75
- * 生成随机 Hex 字符串
76
- */
77
- function randomHex(length) {
78
- const crypto = getCryptoAPI();
79
- const array = new Uint8Array(length);
80
- crypto.getRandomValues(array);
81
- return Array.from(array)
82
- .map(b => b.toString(16).padStart(2, '0'))
83
- .join('');
84
- }
85
- /**
86
- * 用服务器公钥加密签名交易(ECDH + AES-GCM)
87
- *
88
- * @param signedTransactions 签名后的交易数组
89
- * @param publicKeyBase64 服务器提供的公钥(Base64 格式)
90
- * @returns JSON 字符串 {e: 临时公钥, i: IV, d: 密文}
91
- */
92
- export async function encryptWithPublicKey(signedTransactions, publicKeyBase64) {
93
- try {
94
- // 0. 获取 SubtleCrypto 和 Crypto API
95
- const subtle = getSubtleCrypto();
96
- const crypto = getCryptoAPI();
97
- // 1. 准备数据
98
- const payload = {
99
- signedTransactions,
100
- timestamp: Date.now(),
101
- nonce: randomHex(8)
102
- };
103
- const plaintext = JSON.stringify(payload);
104
- // 2. 生成临时 ECDH 密钥对
105
- const ephemeralKeyPair = await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey']);
106
- // 3. 导入服务器公钥
107
- const publicKeyBuffer = base64ToArrayBuffer(publicKeyBase64);
108
- const publicKey = await subtle.importKey('raw', publicKeyBuffer, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
109
- // 4. 派生共享密钥(AES-256)
110
- const sharedKey = await subtle.deriveKey({ name: 'ECDH', public: publicKey }, ephemeralKeyPair.privateKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
111
- // 5. AES-GCM 加密
112
- const iv = crypto.getRandomValues(new Uint8Array(12));
113
- const encrypted = await subtle.encrypt({ name: 'AES-GCM', iv }, sharedKey, new TextEncoder().encode(plaintext));
114
- // 6. 导出临时公钥
115
- const ephemeralPublicKeyRaw = await subtle.exportKey('raw', ephemeralKeyPair.publicKey);
116
- // 7. 返回加密包(JSON 格式)
117
- return JSON.stringify({
118
- e: arrayBufferToBase64(ephemeralPublicKeyRaw), // 临时公钥
119
- i: arrayBufferToBase64(iv.buffer), // IV
120
- d: arrayBufferToBase64(encrypted) // 密文
121
- });
122
- }
123
- catch (error) {
124
- throw new Error(`加密失败: ${error?.message || String(error)}`);
125
- }
126
- }
127
- /**
128
- * 验证公钥格式(Base64)
129
- */
130
- export function validatePublicKey(publicKeyBase64) {
131
- try {
132
- if (!publicKeyBase64)
133
- return false;
134
- // Base64 字符集验证
135
- if (!/^[A-Za-z0-9+/=]+$/.test(publicKeyBase64))
136
- return false;
137
- // ECDH P-256 公钥固定长度 65 字节(未压缩)
138
- // Base64 编码后约 88 字符
139
- if (publicKeyBase64.length < 80 || publicKeyBase64.length > 100)
140
- return false;
141
- return true;
142
- }
143
- catch {
144
- return false;
145
- }
146
- }