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