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.
- package/dist/flap/portal-bundle-merkle/encryption.d.ts +16 -0
- package/dist/flap/portal-bundle-merkle/encryption.js +146 -0
- package/dist/utils/concurrency.d.ts +5 -0
- package/dist/utils/concurrency.js +19 -0
- package/dist/xlayer/aa-account.d.ts +10 -0
- package/dist/xlayer/aa-account.js +257 -20
- package/dist/xlayer/bundle.d.ts +5 -0
- package/dist/xlayer/bundle.js +202 -72
- package/package.json +1 -1
|
@@ -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,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
|
-
|
|
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(
|
|
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: ${
|
|
444
|
-
console.log(` 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(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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(
|
|
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: ${
|
|
530
|
-
console.log(` Sender: ${
|
|
766
|
+
console.log(` Owner: ${owners[i]}`);
|
|
767
|
+
console.log(` Sender: ${senders[i]}`);
|
|
531
768
|
}
|
|
532
769
|
const formatted = formatWalletOutput(wallets);
|
|
533
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
|
@@ -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,
|
|
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)
|
|
@@ -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
|
-
//
|
|
342
|
-
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);
|
|
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
|
|
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
|
|
356
|
-
const
|
|
357
|
-
|
|
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
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
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
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
//
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
455
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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;
|