four-flap-meme-sdk 1.5.24 → 1.5.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/xlayer/aa-account.d.ts +32 -1
- package/dist/xlayer/aa-account.js +184 -46
- package/dist/xlayer/bundle.js +149 -53
- package/dist/xlayer/bundler.js +30 -11
- package/dist/xlayer/portal-ops.d.ts +1 -0
- package/dist/xlayer/portal-ops.js +125 -8
- package/dist/xlayer/types.d.ts +20 -0
- package/package.json +2 -2
|
@@ -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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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)
|
|
524
|
-
const
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
535
|
-
|
|
610
|
+
catch (err) {
|
|
611
|
+
return { ok: false, chunk, err };
|
|
536
612
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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 (
|
|
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
|
-
|
|
563
|
-
|
|
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:
|
|
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
|
// ============================================================================
|
package/dist/xlayer/bundle.js
CHANGED
|
@@ -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
|
-
|
|
165
|
-
const { userOp, prefundWei } =
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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 } =
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
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
|
|
393
|
-
|
|
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;
|
package/dist/xlayer/bundler.js
CHANGED
|
@@ -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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
@@ -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
|
|
175
|
-
|
|
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
|
|
184
|
-
|
|
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
|
|
193
|
-
|
|
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
|
}
|
package/dist/xlayer/types.d.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.5.26",
|
|
4
4
|
"description": "SDK for Flap bonding curve and four.meme TokenManager",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"axios": "^1.12.2",
|
|
33
|
-
"ethers": "^6.
|
|
33
|
+
"ethers": "^6.16.0",
|
|
34
34
|
"pinata": "^1.10.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|