four-flap-meme-sdk 1.5.20 → 1.5.21

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