four-flap-meme-sdk 1.5.19 → 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.
- package/dist/utils/concurrency.d.ts +5 -0
- package/dist/utils/concurrency.js +19 -0
- package/dist/xlayer/aa-account.d.ts +26 -0
- package/dist/xlayer/aa-account.js +311 -20
- package/dist/xlayer/bundle.d.ts +5 -0
- package/dist/xlayer/bundle.js +260 -93
- package/dist/xlayer/bundler.d.ts +11 -0
- package/dist/xlayer/bundler.js +76 -0
- package/package.json +1 -1
|
@@ -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
|
*/
|
|
@@ -85,6 +95,22 @@ export declare class AAAccountManager {
|
|
|
85
95
|
userOp: UserOperation;
|
|
86
96
|
prefundWei: bigint;
|
|
87
97
|
}>;
|
|
98
|
+
/**
|
|
99
|
+
* 批量构建未签名的 UserOperation(使用 Bundler 批量估算 Gas)
|
|
100
|
+
*
|
|
101
|
+
* 目标:把 N 次 eth_estimateUserOperationGas 合并为 1 次(JSON-RPC batch),显著降低延迟。
|
|
102
|
+
*/
|
|
103
|
+
buildUserOpsWithBundlerEstimateBatch(params: {
|
|
104
|
+
ops: Array<{
|
|
105
|
+
sender: string;
|
|
106
|
+
nonce: bigint;
|
|
107
|
+
callData: string;
|
|
108
|
+
initCode?: string;
|
|
109
|
+
}>;
|
|
110
|
+
}): Promise<{
|
|
111
|
+
userOps: UserOperation[];
|
|
112
|
+
prefundWeis: bigint[];
|
|
113
|
+
}>;
|
|
88
114
|
/**
|
|
89
115
|
* 构建未签名的 UserOperation(本地估算 Gas,不依赖 Bundler)
|
|
90
116
|
*
|
|
@@ -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
|
*/
|
|
@@ -170,6 +315,60 @@ export class AAAccountManager {
|
|
|
170
315
|
const prefundWei = paymasterAndData !== '0x' ? 0n : gasTotal * BigInt(userOp.maxFeePerGas);
|
|
171
316
|
return { userOp, prefundWei };
|
|
172
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* 批量构建未签名的 UserOperation(使用 Bundler 批量估算 Gas)
|
|
320
|
+
*
|
|
321
|
+
* 目标:把 N 次 eth_estimateUserOperationGas 合并为 1 次(JSON-RPC batch),显著降低延迟。
|
|
322
|
+
*/
|
|
323
|
+
async buildUserOpsWithBundlerEstimateBatch(params) {
|
|
324
|
+
if (params.ops.length === 0) {
|
|
325
|
+
return { userOps: [], prefundWeis: [] };
|
|
326
|
+
}
|
|
327
|
+
const feeData = await this.provider.getFeeData();
|
|
328
|
+
const legacyGasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? DEFAULT_GAS_PRICE;
|
|
329
|
+
const paymasterAndData = this.buildPaymasterAndData();
|
|
330
|
+
// 先构建 skeleton(Gas=0),然后批量估算
|
|
331
|
+
const skeletons = params.ops.map((p) => ({
|
|
332
|
+
sender: p.sender,
|
|
333
|
+
nonce: ethers.toBeHex(p.nonce),
|
|
334
|
+
initCode: p.initCode ?? '0x',
|
|
335
|
+
callData: p.callData,
|
|
336
|
+
callGasLimit: '0x0',
|
|
337
|
+
verificationGasLimit: '0x0',
|
|
338
|
+
preVerificationGas: '0x0',
|
|
339
|
+
maxFeePerGas: ethers.toBeHex(legacyGasPrice * 2n),
|
|
340
|
+
maxPriorityFeePerGas: ethers.toBeHex(legacyGasPrice),
|
|
341
|
+
paymasterAndData,
|
|
342
|
+
signature: '0x',
|
|
343
|
+
}));
|
|
344
|
+
const estimates = await this.bundler.estimateUserOperationGasBatch(skeletons);
|
|
345
|
+
const userOps = [];
|
|
346
|
+
const prefundWeis = [];
|
|
347
|
+
for (let i = 0; i < skeletons.length; i++) {
|
|
348
|
+
const estimate = estimates[i];
|
|
349
|
+
let userOp = {
|
|
350
|
+
...skeletons[i],
|
|
351
|
+
callGasLimit: estimate.callGasLimit,
|
|
352
|
+
verificationGasLimit: estimate.verificationGasLimit,
|
|
353
|
+
preVerificationGas: estimate.preVerificationGas,
|
|
354
|
+
};
|
|
355
|
+
// 使用 Bundler 建议的费率(如果有)
|
|
356
|
+
if (estimate.maxFeePerGas && estimate.maxPriorityFeePerGas) {
|
|
357
|
+
userOp = {
|
|
358
|
+
...userOp,
|
|
359
|
+
maxFeePerGas: estimate.maxFeePerGas,
|
|
360
|
+
maxPriorityFeePerGas: estimate.maxPriorityFeePerGas,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const gasTotal = BigInt(userOp.callGasLimit) +
|
|
364
|
+
BigInt(userOp.verificationGasLimit) +
|
|
365
|
+
BigInt(userOp.preVerificationGas);
|
|
366
|
+
const prefundWei = paymasterAndData !== '0x' ? 0n : gasTotal * BigInt(userOp.maxFeePerGas);
|
|
367
|
+
userOps.push(userOp);
|
|
368
|
+
prefundWeis.push(prefundWei);
|
|
369
|
+
}
|
|
370
|
+
return { userOps, prefundWeis };
|
|
371
|
+
}
|
|
173
372
|
/**
|
|
174
373
|
* 构建未签名的 UserOperation(本地估算 Gas,不依赖 Bundler)
|
|
175
374
|
*
|
|
@@ -292,7 +491,86 @@ export class AAAccountManager {
|
|
|
292
491
|
* 批量获取多个 owner 的 AA 账户信息
|
|
293
492
|
*/
|
|
294
493
|
async getMultipleAccountInfo(ownerAddresses) {
|
|
295
|
-
|
|
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
|
+
}));
|
|
296
574
|
}
|
|
297
575
|
}
|
|
298
576
|
// ============================================================================
|
|
@@ -374,20 +652,24 @@ export async function generateAAWallets(params) {
|
|
|
374
652
|
const wallet = Wallet.createRandom();
|
|
375
653
|
const owner = wallet.address;
|
|
376
654
|
const privateKey = wallet.privateKey;
|
|
377
|
-
// 预测 AA 地址
|
|
378
|
-
const sender = await manager.predictSenderAddress(owner);
|
|
379
655
|
wallets.push({
|
|
380
656
|
index: i + 1,
|
|
381
657
|
owner,
|
|
382
658
|
privateKey,
|
|
383
|
-
sender,
|
|
659
|
+
sender: '',
|
|
384
660
|
});
|
|
385
661
|
owners.push(owner);
|
|
386
662
|
privateKeys.push(privateKey);
|
|
387
|
-
senders.push(
|
|
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] || '';
|
|
388
670
|
console.log(`#${i + 1}`);
|
|
389
|
-
console.log(` Owner: ${
|
|
390
|
-
console.log(` Sender: ${
|
|
671
|
+
console.log(` Owner: ${owners[i]}`);
|
|
672
|
+
console.log(` Sender: ${senders[i]}`);
|
|
391
673
|
}
|
|
392
674
|
// 生成格式化输出
|
|
393
675
|
const formatted = formatWalletOutput(wallets);
|
|
@@ -414,29 +696,34 @@ export async function generateAAWalletsFromMnemonic(mnemonic, count, startIndex
|
|
|
414
696
|
const privateKeys = [];
|
|
415
697
|
const senders = [];
|
|
416
698
|
console.log(`\n从助记词派生 ${count} 个 XLayer AA 钱包 (起始索引: ${startIndex})...\n`);
|
|
699
|
+
const paths = [];
|
|
417
700
|
for (let i = 0; i < count; i++) {
|
|
418
701
|
const index = startIndex + i;
|
|
419
702
|
const path = `m/44'/60'/0'/0/${index}`;
|
|
703
|
+
paths.push(path);
|
|
420
704
|
// 从助记词派生
|
|
421
705
|
const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic, undefined, path);
|
|
422
706
|
const owner = hdNode.address;
|
|
423
707
|
const privateKey = hdNode.privateKey;
|
|
424
|
-
// 预测 AA 地址
|
|
425
|
-
const sender = await manager.predictSenderAddress(owner);
|
|
426
708
|
wallets.push({
|
|
427
709
|
index: i + 1,
|
|
428
710
|
owner,
|
|
429
711
|
privateKey,
|
|
430
|
-
sender,
|
|
712
|
+
sender: '',
|
|
431
713
|
mnemonic: i === 0 ? mnemonic : undefined, // 只在第一个记录助记词
|
|
432
714
|
derivationPath: path,
|
|
433
715
|
});
|
|
434
716
|
owners.push(owner);
|
|
435
717
|
privateKeys.push(privateKey);
|
|
436
|
-
senders.push(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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]}`);
|
|
440
727
|
}
|
|
441
728
|
const formatted = formatWalletOutput(wallets, mnemonic);
|
|
442
729
|
return {
|
|
@@ -462,18 +749,22 @@ export async function predictSendersFromPrivateKeys(privateKeys, config) {
|
|
|
462
749
|
for (let i = 0; i < privateKeys.length; i++) {
|
|
463
750
|
const wallet = new Wallet(privateKeys[i]);
|
|
464
751
|
const owner = wallet.address;
|
|
465
|
-
const sender = await manager.predictSenderAddress(owner);
|
|
466
752
|
wallets.push({
|
|
467
753
|
index: i + 1,
|
|
468
754
|
owner,
|
|
469
755
|
privateKey: privateKeys[i],
|
|
470
|
-
sender,
|
|
756
|
+
sender: '',
|
|
471
757
|
});
|
|
472
758
|
owners.push(owner);
|
|
473
|
-
senders.push(
|
|
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] || '';
|
|
474
765
|
console.log(`#${i + 1}`);
|
|
475
|
-
console.log(` Owner: ${
|
|
476
|
-
console.log(` Sender: ${
|
|
766
|
+
console.log(` Owner: ${owners[i]}`);
|
|
767
|
+
console.log(` Sender: ${senders[i]}`);
|
|
477
768
|
}
|
|
478
769
|
const formatted = formatWalletOutput(wallets);
|
|
479
770
|
return {
|
package/dist/xlayer/bundle.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/xlayer/bundle.js
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
* - 买卖一体化:买入 -> 授权 -> 卖出 -> 归集
|
|
8
8
|
* - OKB 归集:将 sender 的 OKB 转回 owner
|
|
9
9
|
*/
|
|
10
|
-
import { Interface, Contract } from 'ethers';
|
|
10
|
+
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
|
-
import { AAAccountManager, encodeExecute
|
|
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,
|
|
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
|
|
139
|
+
sender,
|
|
140
140
|
callData,
|
|
141
141
|
nonce,
|
|
142
|
+
initCode,
|
|
142
143
|
});
|
|
143
|
-
await this.aaManager.ensureSenderBalance(ownerWallet,
|
|
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,
|
|
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
|
|
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
|
|
167
|
+
sender,
|
|
167
168
|
callData,
|
|
168
169
|
nonce,
|
|
170
|
+
initCode,
|
|
169
171
|
});
|
|
170
|
-
await this.aaManager.ensureSenderBalance(ownerWallet,
|
|
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
|
-
|
|
181
|
-
|
|
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,
|
|
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:
|
|
207
|
+
ownerWallet: params.ownerWallet,
|
|
208
|
+
sender: params.sender,
|
|
190
209
|
callData: tempCallData,
|
|
191
|
-
nonce:
|
|
210
|
+
nonce: params.nonce,
|
|
211
|
+
initCode: params.initCode,
|
|
192
212
|
});
|
|
193
|
-
//
|
|
194
|
-
const
|
|
195
|
-
|
|
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
|
-
|
|
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:
|
|
223
|
+
ownerWallet: params.ownerWallet,
|
|
224
|
+
sender: params.sender,
|
|
207
225
|
callData,
|
|
208
|
-
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)
|
|
@@ -249,40 +268,75 @@ export class BundleExecutor {
|
|
|
249
268
|
throw new Error('私钥数量和购买金额数量必须一致');
|
|
250
269
|
}
|
|
251
270
|
// 使用第一个 owner 作为 bundler signer
|
|
252
|
-
const
|
|
271
|
+
const sharedProvider = this.aaManager.getProvider();
|
|
272
|
+
const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
|
|
253
273
|
const bundlerSigner = wallets[0];
|
|
254
274
|
const beneficiary = bundlerSigner.address;
|
|
255
275
|
console.log('=== XLayer Bundle Buy ===');
|
|
256
276
|
console.log('token:', tokenAddress);
|
|
257
277
|
console.log('owners:', wallets.length);
|
|
258
|
-
// 1.
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
278
|
+
// 1. 预取账户信息(并行),并批量估算 gas(减少对 bundler 的 N 次请求)
|
|
279
|
+
const accountInfos = await this.aaManager.getMultipleAccountInfo(wallets.map((w) => w.address));
|
|
280
|
+
const buyWeis = buyAmounts.map((a) => parseOkb(a));
|
|
281
|
+
// 估算前确保 sender 有足够余额(用于 bundler 模拟;paymaster 场景会自动跳过)
|
|
282
|
+
await Promise.all(accountInfos.map((ai, i) => this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWeis[i] + parseOkb('0.0003'), `owner${i + 1}/buy-prefund-before-estimate`)));
|
|
283
|
+
const buyCallDatas = buyWeis.map((buyWei) => {
|
|
284
|
+
const swapData = encodeBuyCall(tokenAddress, buyWei, 0n);
|
|
285
|
+
return encodeExecute(this.portalAddress, buyWei, swapData);
|
|
286
|
+
});
|
|
287
|
+
const initCodes = accountInfos.map((ai, i) => ai.deployed ? '0x' : this.aaManager.generateInitCode(wallets[i].address));
|
|
288
|
+
const { userOps: buyUserOps, prefundWeis } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
|
|
289
|
+
ops: accountInfos.map((ai, i) => ({
|
|
290
|
+
sender: ai.sender,
|
|
291
|
+
nonce: ai.nonce,
|
|
292
|
+
callData: buyCallDatas[i],
|
|
293
|
+
initCode: initCodes[i],
|
|
294
|
+
})),
|
|
295
|
+
});
|
|
296
|
+
// 补足 prefund + 买入金额(多数情况下上一步的 0.0003 已足够,这里通常不会再转账)
|
|
297
|
+
await Promise.all(accountInfos.map((ai, i) => this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWeis[i] + prefundWeis[i] + parseOkb('0.0002'), `owner${i + 1}/buy-fund`)));
|
|
298
|
+
// 签名
|
|
299
|
+
const signedBuy = await Promise.all(buyUserOps.map((op, i) => this.aaManager.signUserOp(op, wallets[i])));
|
|
300
|
+
const buyOps = signedBuy.map((s) => s.userOp);
|
|
266
301
|
// 2. 执行买入
|
|
267
302
|
const buyResult = await this.runHandleOps('buyBundle', buyOps, bundlerSigner, beneficiary);
|
|
268
303
|
if (!buyResult) {
|
|
269
304
|
throw new Error('买入交易失败');
|
|
270
305
|
}
|
|
271
306
|
// 3. 获取代币余额
|
|
272
|
-
const senders =
|
|
307
|
+
const senders = accountInfos.map((ai) => ai.sender);
|
|
273
308
|
const tokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, senders);
|
|
274
309
|
// 4. 可选:转账代币回 owner
|
|
275
310
|
let transferResult;
|
|
276
311
|
if (transferBackToOwner) {
|
|
277
|
-
const
|
|
312
|
+
const idxs = [];
|
|
313
|
+
const transferCallDatas = [];
|
|
278
314
|
for (let i = 0; i < wallets.length; i++) {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
315
|
+
const sender = senders[i];
|
|
316
|
+
const bal = tokenBalances.get(sender) ?? 0n;
|
|
317
|
+
if (bal === 0n)
|
|
318
|
+
continue;
|
|
319
|
+
idxs.push(i);
|
|
320
|
+
const transferData = encodeTransferCall(wallets[i].address, bal);
|
|
321
|
+
transferCallDatas.push(encodeExecute(tokenAddress, 0n, transferData));
|
|
283
322
|
}
|
|
284
|
-
if (
|
|
285
|
-
|
|
323
|
+
if (idxs.length > 0) {
|
|
324
|
+
// 估算前补一点余额(paymaster 会自动跳过)
|
|
325
|
+
await Promise.all(idxs.map((i) => this.aaManager.ensureSenderBalance(wallets[i], senders[i], parseOkb('0.0002'), `owner${i + 1}/transfer-prefund`)));
|
|
326
|
+
// buy 已经成功过一次,因此 transfer 的 nonce = 原 nonce + 1,且 initCode = 0x
|
|
327
|
+
const { userOps: transferUserOps, prefundWeis: transferPrefunds } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
|
|
328
|
+
ops: idxs.map((i, k) => ({
|
|
329
|
+
sender: senders[i],
|
|
330
|
+
nonce: accountInfos[i].nonce + 1n,
|
|
331
|
+
callData: transferCallDatas[k],
|
|
332
|
+
initCode: '0x',
|
|
333
|
+
})),
|
|
334
|
+
});
|
|
335
|
+
await Promise.all(idxs.map((i, k) => this.aaManager.ensureSenderBalance(wallets[i], senders[i], transferPrefunds[k] + parseOkb('0.00005'), `owner${i + 1}/transfer-fund`)));
|
|
336
|
+
const signedTransfer = await Promise.all(transferUserOps.map((op, k) => this.aaManager.signUserOp(op, wallets[idxs[k]])));
|
|
337
|
+
const transferOps = signedTransfer.map((s) => s.userOp);
|
|
338
|
+
transferResult =
|
|
339
|
+
(await this.runHandleOps('transferBundle', transferOps, bundlerSigner, beneficiary)) ?? undefined;
|
|
286
340
|
}
|
|
287
341
|
}
|
|
288
342
|
return { buyResult, transferResult, tokenBalances };
|
|
@@ -294,7 +348,8 @@ export class BundleExecutor {
|
|
|
294
348
|
*/
|
|
295
349
|
async bundleSell(params) {
|
|
296
350
|
const { tokenAddress, privateKeys, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, } = params;
|
|
297
|
-
const
|
|
351
|
+
const sharedProvider = this.aaManager.getProvider();
|
|
352
|
+
const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
|
|
298
353
|
const bundlerSigner = wallets[0];
|
|
299
354
|
const beneficiary = bundlerSigner.address;
|
|
300
355
|
const reserveWei = parseOkb(withdrawReserve);
|
|
@@ -302,12 +357,14 @@ export class BundleExecutor {
|
|
|
302
357
|
console.log('token:', tokenAddress);
|
|
303
358
|
console.log('owners:', wallets.length);
|
|
304
359
|
console.log('sellPercent:', sellPercent);
|
|
305
|
-
//
|
|
306
|
-
const
|
|
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);
|
|
307
363
|
const tokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, senders);
|
|
308
364
|
const allowances = await this.portalQuery.getMultipleAllowances(tokenAddress, senders);
|
|
309
365
|
// 1. 检查授权,必要时先 approve
|
|
310
|
-
const
|
|
366
|
+
const approveItems = [];
|
|
367
|
+
const didApprove = new Array(wallets.length).fill(false);
|
|
311
368
|
for (let i = 0; i < wallets.length; i++) {
|
|
312
369
|
const sender = senders[i];
|
|
313
370
|
const balance = tokenBalances.get(sender) ?? 0n;
|
|
@@ -316,9 +373,20 @@ export class BundleExecutor {
|
|
|
316
373
|
continue;
|
|
317
374
|
if (allowance >= balance)
|
|
318
375
|
continue;
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
|
|
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);
|
|
322
390
|
}
|
|
323
391
|
let approveResult;
|
|
324
392
|
if (approveOps.length > 0) {
|
|
@@ -326,6 +394,7 @@ export class BundleExecutor {
|
|
|
326
394
|
}
|
|
327
395
|
// 2. 卖出
|
|
328
396
|
const sellOps = [];
|
|
397
|
+
const sellItems = [];
|
|
329
398
|
for (let i = 0; i < wallets.length; i++) {
|
|
330
399
|
const sender = senders[i];
|
|
331
400
|
const balance = tokenBalances.get(sender) ?? 0n;
|
|
@@ -334,11 +403,20 @@ export class BundleExecutor {
|
|
|
334
403
|
const sellAmount = (balance * BigInt(sellPercent)) / 100n;
|
|
335
404
|
if (sellAmount === 0n)
|
|
336
405
|
continue;
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
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);
|
|
342
420
|
}
|
|
343
421
|
const sellResult = await this.runHandleOps('sellBundle', sellOps, bundlerSigner, beneficiary);
|
|
344
422
|
if (!sellResult) {
|
|
@@ -348,11 +426,42 @@ export class BundleExecutor {
|
|
|
348
426
|
let withdrawResult;
|
|
349
427
|
if (withdrawToOwner) {
|
|
350
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
|
+
}
|
|
351
437
|
for (let i = 0; i < wallets.length; i++) {
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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);
|
|
356
465
|
}
|
|
357
466
|
if (withdrawOps.length > 0) {
|
|
358
467
|
withdrawResult = await this.runHandleOps('withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
|
|
@@ -370,7 +479,8 @@ export class BundleExecutor {
|
|
|
370
479
|
if (privateKeys.length !== buyAmounts.length) {
|
|
371
480
|
throw new Error('私钥数量和购买金额数量必须一致');
|
|
372
481
|
}
|
|
373
|
-
const
|
|
482
|
+
const sharedProvider = this.aaManager.getProvider();
|
|
483
|
+
const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
|
|
374
484
|
const bundlerSigner = wallets[0];
|
|
375
485
|
const beneficiary = bundlerSigner.address;
|
|
376
486
|
const reserveWei = parseOkb(withdrawReserve);
|
|
@@ -378,15 +488,36 @@ export class BundleExecutor {
|
|
|
378
488
|
console.log('token:', tokenAddress);
|
|
379
489
|
console.log('owners:', wallets.length);
|
|
380
490
|
console.log('sellPercent:', sellPercent);
|
|
381
|
-
//
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
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);
|
|
390
521
|
const buyResult = await this.runHandleOps('buyBundle', buyOps, bundlerSigner, beneficiary);
|
|
391
522
|
if (!buyResult) {
|
|
392
523
|
throw new Error('买入交易失败');
|
|
@@ -396,27 +527,33 @@ export class BundleExecutor {
|
|
|
396
527
|
const allowances = await this.portalQuery.getMultipleAllowances(tokenAddress, senders);
|
|
397
528
|
// 2. 授权 + 卖出(可以合并到同一笔 handleOps)
|
|
398
529
|
const sellOps = [];
|
|
399
|
-
|
|
530
|
+
const sellPerWallet = await mapWithConcurrency(wallets, 4, async (w, i) => {
|
|
400
531
|
const sender = senders[i];
|
|
401
532
|
const balance = tokenBalances.get(sender) ?? 0n;
|
|
402
533
|
if (balance === 0n) {
|
|
403
534
|
console.log(`[owner${i + 1}] 没买到代币,跳过卖出`);
|
|
404
|
-
|
|
535
|
+
return [];
|
|
405
536
|
}
|
|
406
537
|
const allowance = allowances.get(sender) ?? 0n;
|
|
407
|
-
let baseNonce = (await this.aaManager.getAccountInfo(wallets[i].address)).nonce;
|
|
408
|
-
// 如果需要授权,先添加 approve op
|
|
409
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 = [];
|
|
410
543
|
if (needApprove) {
|
|
411
|
-
const approveOp = await this.buildApproveUserOp(
|
|
412
|
-
|
|
413
|
-
|
|
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;
|
|
414
547
|
}
|
|
415
|
-
// 添加 sell op
|
|
416
548
|
const sellAmount = (balance * BigInt(sellPercent)) / 100n;
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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);
|
|
420
557
|
const sellResult = await this.runHandleOps('sellBundle', sellOps, bundlerSigner, beneficiary);
|
|
421
558
|
if (!sellResult) {
|
|
422
559
|
throw new Error('卖出交易失败');
|
|
@@ -425,11 +562,41 @@ export class BundleExecutor {
|
|
|
425
562
|
let withdrawResult;
|
|
426
563
|
if (withdrawToOwner) {
|
|
427
564
|
const withdrawOps = [];
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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);
|
|
433
600
|
}
|
|
434
601
|
if (withdrawOps.length > 0) {
|
|
435
602
|
withdrawResult = await this.runHandleOps('withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
|
package/dist/xlayer/bundler.d.ts
CHANGED
|
@@ -56,6 +56,13 @@ export declare class BundlerClient {
|
|
|
56
56
|
* 发送 JSON-RPC 请求到 Bundler
|
|
57
57
|
*/
|
|
58
58
|
private rpc;
|
|
59
|
+
/**
|
|
60
|
+
* 发送 JSON-RPC Batch 请求到 Bundler
|
|
61
|
+
*
|
|
62
|
+
* 注意:JSON-RPC batch 是一个数组;Particle bundler 通常也支持该格式。
|
|
63
|
+
* 为兼容其非标准字段,这里为每个 request 也附带 chainId 字段。
|
|
64
|
+
*/
|
|
65
|
+
private rpcBatch;
|
|
59
66
|
/**
|
|
60
67
|
* 获取支持的 EntryPoint 列表
|
|
61
68
|
*/
|
|
@@ -67,6 +74,10 @@ export declare class BundlerClient {
|
|
|
67
74
|
* @returns Gas 估算结果
|
|
68
75
|
*/
|
|
69
76
|
estimateUserOperationGas(userOp: UserOperation): Promise<GasEstimate>;
|
|
77
|
+
/**
|
|
78
|
+
* 批量估算多个 UserOperation Gas(优先使用 JSON-RPC batch;失败则回退)
|
|
79
|
+
*/
|
|
80
|
+
estimateUserOperationGasBatch(userOps: UserOperation[]): Promise<GasEstimate[]>;
|
|
70
81
|
/**
|
|
71
82
|
* 发送 UserOperation
|
|
72
83
|
*
|
package/dist/xlayer/bundler.js
CHANGED
|
@@ -69,6 +69,64 @@ export class BundlerClient {
|
|
|
69
69
|
clearTimeout(timer);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* 发送 JSON-RPC Batch 请求到 Bundler
|
|
74
|
+
*
|
|
75
|
+
* 注意:JSON-RPC batch 是一个数组;Particle bundler 通常也支持该格式。
|
|
76
|
+
* 为兼容其非标准字段,这里为每个 request 也附带 chainId 字段。
|
|
77
|
+
*/
|
|
78
|
+
async rpcBatch(calls) {
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
81
|
+
// 为每个 call 分配独立 id,便于按 id 回填结果
|
|
82
|
+
const ids = calls.map(() => nextRpcId++);
|
|
83
|
+
const body = calls.map((c, i) => ({
|
|
84
|
+
jsonrpc: '2.0',
|
|
85
|
+
id: ids[i],
|
|
86
|
+
method: c.method,
|
|
87
|
+
params: c.params ?? [],
|
|
88
|
+
chainId: this.chainId,
|
|
89
|
+
}));
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(this.url, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'content-type': 'application/json',
|
|
95
|
+
...this.headers,
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify(body),
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
});
|
|
100
|
+
const text = await res.text();
|
|
101
|
+
let data;
|
|
102
|
+
try {
|
|
103
|
+
data = JSON.parse(text);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new Error(`非 JSON 响应 (HTTP ${res.status}): ${text.slice(0, 400)}`);
|
|
107
|
+
}
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
throw new Error(`HTTP ${res.status}: ${JSON.stringify(data).slice(0, 800)}`);
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(data)) {
|
|
112
|
+
throw new Error(`RPC batch 非数组响应: ${JSON.stringify(data).slice(0, 800)}`);
|
|
113
|
+
}
|
|
114
|
+
// 建立 id -> result 映射,确保按 calls 顺序返回
|
|
115
|
+
const byId = new Map();
|
|
116
|
+
for (const item of data) {
|
|
117
|
+
if (item?.error) {
|
|
118
|
+
throw new Error(`RPC batch error: ${JSON.stringify(item.error)}`);
|
|
119
|
+
}
|
|
120
|
+
if (typeof item?.id === 'number') {
|
|
121
|
+
byId.set(item.id, item.result);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return ids.map((id) => byId.get(id));
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
72
130
|
/**
|
|
73
131
|
* 获取支持的 EntryPoint 列表
|
|
74
132
|
*/
|
|
@@ -87,6 +145,24 @@ export class BundlerClient {
|
|
|
87
145
|
this.entryPoint,
|
|
88
146
|
]);
|
|
89
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* 批量估算多个 UserOperation Gas(优先使用 JSON-RPC batch;失败则回退)
|
|
150
|
+
*/
|
|
151
|
+
async estimateUserOperationGasBatch(userOps) {
|
|
152
|
+
if (userOps.length === 0)
|
|
153
|
+
return [];
|
|
154
|
+
// 先尝试 batch(显著降低 HTTP 往返次数)
|
|
155
|
+
try {
|
|
156
|
+
return await this.rpcBatch(userOps.map((op) => ({
|
|
157
|
+
method: 'eth_estimateUserOperationGas',
|
|
158
|
+
params: [op, this.entryPoint],
|
|
159
|
+
})));
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// fallback:并发单请求(比串行快,但可能触发限流)
|
|
163
|
+
return await Promise.all(userOps.map((op) => this.estimateUserOperationGas(op)));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
90
166
|
/**
|
|
91
167
|
* 发送 UserOperation
|
|
92
168
|
*
|