four-flap-meme-sdk 1.5.18 → 1.5.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * ECDH + AES-GCM 加密工具(浏览器兼容)
3
+ * 用于将签名交易用服务器公钥加密
4
+ */
5
+ /**
6
+ * 用服务器公钥加密签名交易(ECDH + AES-GCM)
7
+ *
8
+ * @param signedTransactions 签名后的交易数组
9
+ * @param publicKeyBase64 服务器提供的公钥(Base64 格式)
10
+ * @returns JSON 字符串 {e: 临时公钥, i: IV, d: 密文}
11
+ */
12
+ export declare function encryptWithPublicKey(signedTransactions: string[], publicKeyBase64: string): Promise<string>;
13
+ /**
14
+ * 验证公钥格式(Base64)
15
+ */
16
+ export declare function validatePublicKey(publicKeyBase64: string): boolean;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * ECDH + AES-GCM 加密工具(浏览器兼容)
3
+ * 用于将签名交易用服务器公钥加密
4
+ */
5
+ /**
6
+ * 获取全局 crypto 对象(最简单直接的方式)
7
+ */
8
+ function getCryptoAPI() {
9
+ // 尝试所有可能的全局对象,优先浏览器环境
10
+ const cryptoObj = (typeof window !== 'undefined' && window.crypto) ||
11
+ (typeof self !== 'undefined' && self.crypto) ||
12
+ (typeof global !== 'undefined' && global.crypto) ||
13
+ (typeof globalThis !== 'undefined' && globalThis.crypto);
14
+ if (!cryptoObj) {
15
+ const env = typeof window !== 'undefined' ? 'Browser' : 'Node.js';
16
+ const protocol = typeof location !== 'undefined' ? location.protocol : 'unknown';
17
+ throw new Error(`❌ Crypto API 不可用。环境: ${env}, 协议: ${protocol}. ` +
18
+ '请确保在 HTTPS 或 localhost 下运行');
19
+ }
20
+ return cryptoObj;
21
+ }
22
+ /**
23
+ * 获取 SubtleCrypto(用于加密操作)
24
+ */
25
+ function getSubtleCrypto() {
26
+ const crypto = getCryptoAPI();
27
+ if (!crypto.subtle) {
28
+ const protocol = typeof location !== 'undefined' ? location.protocol : 'unknown';
29
+ const hostname = typeof location !== 'undefined' ? location.hostname : 'unknown';
30
+ throw new Error(`❌ SubtleCrypto API 不可用。协议: ${protocol}, 主机: ${hostname}. ` +
31
+ '请确保:1) 使用 HTTPS (或 localhost);2) 浏览器支持 Web Crypto API;' +
32
+ '3) 不在无痕/隐私浏览模式下');
33
+ }
34
+ return crypto.subtle;
35
+ }
36
+ /**
37
+ * Base64 转 ArrayBuffer(优先使用浏览器 API)
38
+ */
39
+ function base64ToArrayBuffer(base64) {
40
+ // 浏览器环境(优先)
41
+ if (typeof atob !== 'undefined') {
42
+ const binaryString = atob(base64);
43
+ const bytes = new Uint8Array(binaryString.length);
44
+ for (let i = 0; i < binaryString.length; i++) {
45
+ bytes[i] = binaryString.charCodeAt(i);
46
+ }
47
+ return bytes.buffer;
48
+ }
49
+ // Node.js 环境(fallback)
50
+ if (typeof Buffer !== 'undefined') {
51
+ return Buffer.from(base64, 'base64').buffer;
52
+ }
53
+ throw new Error('❌ Base64 解码不可用');
54
+ }
55
+ /**
56
+ * ArrayBuffer 转 Base64(优先使用浏览器 API)
57
+ */
58
+ function arrayBufferToBase64(buffer) {
59
+ // 浏览器环境(优先)
60
+ if (typeof btoa !== 'undefined') {
61
+ const bytes = new Uint8Array(buffer);
62
+ let binary = '';
63
+ for (let i = 0; i < bytes.length; i++) {
64
+ binary += String.fromCharCode(bytes[i]);
65
+ }
66
+ return btoa(binary);
67
+ }
68
+ // Node.js 环境(fallback)
69
+ if (typeof Buffer !== 'undefined') {
70
+ return Buffer.from(buffer).toString('base64');
71
+ }
72
+ throw new Error('❌ Base64 编码不可用');
73
+ }
74
+ /**
75
+ * 生成随机 Hex 字符串
76
+ */
77
+ function randomHex(length) {
78
+ const crypto = getCryptoAPI();
79
+ const array = new Uint8Array(length);
80
+ crypto.getRandomValues(array);
81
+ return Array.from(array)
82
+ .map(b => b.toString(16).padStart(2, '0'))
83
+ .join('');
84
+ }
85
+ /**
86
+ * 用服务器公钥加密签名交易(ECDH + AES-GCM)
87
+ *
88
+ * @param signedTransactions 签名后的交易数组
89
+ * @param publicKeyBase64 服务器提供的公钥(Base64 格式)
90
+ * @returns JSON 字符串 {e: 临时公钥, i: IV, d: 密文}
91
+ */
92
+ export async function encryptWithPublicKey(signedTransactions, publicKeyBase64) {
93
+ try {
94
+ // 0. 获取 SubtleCrypto 和 Crypto API
95
+ const subtle = getSubtleCrypto();
96
+ const crypto = getCryptoAPI();
97
+ // 1. 准备数据
98
+ const payload = {
99
+ signedTransactions,
100
+ timestamp: Date.now(),
101
+ nonce: randomHex(8)
102
+ };
103
+ const plaintext = JSON.stringify(payload);
104
+ // 2. 生成临时 ECDH 密钥对
105
+ const ephemeralKeyPair = await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey']);
106
+ // 3. 导入服务器公钥
107
+ const publicKeyBuffer = base64ToArrayBuffer(publicKeyBase64);
108
+ const publicKey = await subtle.importKey('raw', publicKeyBuffer, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
109
+ // 4. 派生共享密钥(AES-256)
110
+ const sharedKey = await subtle.deriveKey({ name: 'ECDH', public: publicKey }, ephemeralKeyPair.privateKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
111
+ // 5. AES-GCM 加密
112
+ const iv = crypto.getRandomValues(new Uint8Array(12));
113
+ const encrypted = await subtle.encrypt({ name: 'AES-GCM', iv }, sharedKey, new TextEncoder().encode(plaintext));
114
+ // 6. 导出临时公钥
115
+ const ephemeralPublicKeyRaw = await subtle.exportKey('raw', ephemeralKeyPair.publicKey);
116
+ // 7. 返回加密包(JSON 格式)
117
+ return JSON.stringify({
118
+ e: arrayBufferToBase64(ephemeralPublicKeyRaw), // 临时公钥
119
+ i: arrayBufferToBase64(iv.buffer), // IV
120
+ d: arrayBufferToBase64(encrypted) // 密文
121
+ });
122
+ }
123
+ catch (error) {
124
+ throw new Error(`加密失败: ${error?.message || String(error)}`);
125
+ }
126
+ }
127
+ /**
128
+ * 验证公钥格式(Base64)
129
+ */
130
+ export function validatePublicKey(publicKeyBase64) {
131
+ try {
132
+ if (!publicKeyBase64)
133
+ return false;
134
+ // Base64 字符集验证
135
+ if (!/^[A-Za-z0-9+/=]+$/.test(publicKeyBase64))
136
+ return false;
137
+ // ECDH P-256 公钥固定长度 65 字节(未压缩)
138
+ // Base64 编码后约 88 字符
139
+ if (publicKeyBase64.length < 80 || publicKeyBase64.length > 100)
140
+ return false;
141
+ return true;
142
+ }
143
+ catch {
144
+ return false;
145
+ }
146
+ }
@@ -16,7 +16,9 @@ import { type GeneratedWallet } from './wallet.js';
16
16
  export declare class NonceManager {
17
17
  private provider;
18
18
  private tempNonceCache;
19
+ private chainIdPromise?;
19
20
  constructor(provider: JsonRpcProvider);
21
+ private getChainId;
20
22
  /**
21
23
  * 获取下一个可用的 nonce
22
24
  * @param wallet 钱包对象或地址
@@ -4,6 +4,47 @@
4
4
  */
5
5
  import { ethers, Wallet } from 'ethers';
6
6
  import { generateWallets } from './wallet.js';
7
+ const GLOBAL_NONCE_TTL_MS = 60 * 1000; // 1 分钟:只用于短时间“pending 不同步”兜底,避免长时间缓存造成 nonce 过高
8
+ const globalNonceCursorByChain = new Map();
9
+ function isFlakyNonceChain(chainId) {
10
+ // ✅ XLayer: 196(OKX X Layer)
11
+ return Number(chainId) === 196;
12
+ }
13
+ function getCursorMap(chainId) {
14
+ let m = globalNonceCursorByChain.get(chainId);
15
+ if (!m) {
16
+ m = new Map();
17
+ globalNonceCursorByChain.set(chainId, m);
18
+ }
19
+ return m;
20
+ }
21
+ function getGlobalNextNonce(chainId, addrLower) {
22
+ const m = globalNonceCursorByChain.get(chainId);
23
+ if (!m)
24
+ return undefined;
25
+ const e = m.get(addrLower);
26
+ if (!e)
27
+ return undefined;
28
+ if (Date.now() - e.updatedAt > GLOBAL_NONCE_TTL_MS) {
29
+ m.delete(addrLower);
30
+ return undefined;
31
+ }
32
+ return e.nextNonce;
33
+ }
34
+ function setGlobalNextNonce(chainId, addrLower, nextNonce) {
35
+ const m = getCursorMap(chainId);
36
+ const cur = m.get(addrLower);
37
+ const next = Number(nextNonce);
38
+ if (!Number.isFinite(next) || next < 0)
39
+ return;
40
+ if (!cur || next > cur.nextNonce || (Date.now() - cur.updatedAt > GLOBAL_NONCE_TTL_MS)) {
41
+ m.set(addrLower, { nextNonce: next, updatedAt: Date.now() });
42
+ }
43
+ else {
44
+ // 刷新时间戳,避免短时间内频繁淘汰
45
+ cur.updatedAt = Date.now();
46
+ }
47
+ }
7
48
  /**
8
49
  * Nonce 管理器
9
50
  * 用于在 bundle 交易中管理多个钱包的 nonce
@@ -19,6 +60,12 @@ export class NonceManager {
19
60
  this.tempNonceCache = new Map();
20
61
  this.provider = provider;
21
62
  }
63
+ async getChainId() {
64
+ if (!this.chainIdPromise) {
65
+ this.chainIdPromise = this.provider.getNetwork().then(n => Number(n.chainId)).catch(() => 0);
66
+ }
67
+ return this.chainIdPromise;
68
+ }
22
69
  /**
23
70
  * 获取下一个可用的 nonce
24
71
  * @param wallet 钱包对象或地址
@@ -27,18 +74,34 @@ export class NonceManager {
27
74
  async getNextNonce(wallet) {
28
75
  const address = typeof wallet === 'string' ? wallet : wallet.address;
29
76
  const key = address.toLowerCase();
77
+ const chainId = await this.getChainId();
78
+ const flaky = isFlakyNonceChain(chainId);
30
79
  // 检查临时缓存
31
80
  if (this.tempNonceCache.has(key)) {
32
81
  const cachedNonce = this.tempNonceCache.get(key);
33
82
  this.tempNonceCache.set(key, cachedNonce + 1);
83
+ if (flaky)
84
+ setGlobalNextNonce(chainId, key, cachedNonce + 1);
34
85
  return cachedNonce;
35
86
  }
36
87
  // ✅ 使用 'pending' 获取 nonce(包含待处理交易)
37
88
  // 由于前端已移除 nonce 缓存,SDK 每次都从链上获取最新状态
38
89
  const onchainNonce = await this.provider.getTransactionCount(address, 'pending');
90
+ // ✅ XLayer:如果链上 pending nonce 暂时没跟上,短时间内允许使用“全局游标”兜底(只允许小幅领先)
91
+ let effectiveNonce = onchainNonce;
92
+ if (flaky) {
93
+ const globalNext = getGlobalNextNonce(chainId, key);
94
+ if (globalNext !== undefined) {
95
+ const delta = globalNext - onchainNonce;
96
+ if (delta > 0 && delta <= 10) {
97
+ effectiveNonce = globalNext;
98
+ }
99
+ }
100
+ setGlobalNextNonce(chainId, key, effectiveNonce + 1);
101
+ }
39
102
  // 缓存下一个值(仅在当前批次内有效)
40
- this.tempNonceCache.set(key, onchainNonce + 1);
41
- return onchainNonce;
103
+ this.tempNonceCache.set(key, effectiveNonce + 1);
104
+ return effectiveNonce;
42
105
  }
43
106
  /**
44
107
  * 批量获取连续 nonce(推荐用于同一地址的批量交易)
@@ -49,6 +112,8 @@ export class NonceManager {
49
112
  async getNextNonceBatch(wallet, count) {
50
113
  const address = typeof wallet === 'string' ? wallet : wallet.address;
51
114
  const key = address.toLowerCase();
115
+ const chainId = await this.getChainId();
116
+ const flaky = isFlakyNonceChain(chainId);
52
117
  let startNonce;
53
118
  if (this.tempNonceCache.has(key)) {
54
119
  startNonce = this.tempNonceCache.get(key);
@@ -56,8 +121,18 @@ export class NonceManager {
56
121
  else {
57
122
  startNonce = await this.provider.getTransactionCount(address, 'pending');
58
123
  }
124
+ if (flaky) {
125
+ const globalNext = getGlobalNextNonce(chainId, key);
126
+ if (globalNext !== undefined) {
127
+ const delta = globalNext - startNonce;
128
+ if (delta > 0 && delta <= 10)
129
+ startNonce = globalNext;
130
+ }
131
+ }
59
132
  // 更新缓存
60
133
  this.tempNonceCache.set(key, startNonce + count);
134
+ if (flaky)
135
+ setGlobalNextNonce(chainId, key, startNonce + count);
61
136
  // 返回连续 nonce 数组
62
137
  return Array.from({ length: count }, (_, i) => startNonce + i);
63
138
  }
@@ -82,6 +157,8 @@ export class NonceManager {
82
157
  async getNextNoncesForWallets(wallets) {
83
158
  const addresses = wallets.map(w => typeof w === 'string' ? w : w.address);
84
159
  const keys = addresses.map(a => a.toLowerCase());
160
+ const chainId = await this.getChainId();
161
+ const flaky = isFlakyNonceChain(chainId);
85
162
  // 分离:已缓存的 vs 需要查询的
86
163
  const needQuery = [];
87
164
  const results = new Array(wallets.length);
@@ -91,6 +168,8 @@ export class NonceManager {
91
168
  const cachedNonce = this.tempNonceCache.get(key);
92
169
  results[i] = cachedNonce;
93
170
  this.tempNonceCache.set(key, cachedNonce + 1);
171
+ if (flaky)
172
+ setGlobalNextNonce(chainId, key, cachedNonce + 1);
94
173
  }
95
174
  else {
96
175
  needQuery.push({ index: i, address: addresses[i] });
@@ -127,9 +206,20 @@ export class NonceManager {
127
206
  // 填充结果并更新缓存
128
207
  for (let i = 0; i < needQuery.length; i++) {
129
208
  const { index, address } = needQuery[i];
130
- const nonce = queryResults[i];
131
- results[index] = nonce;
132
- this.tempNonceCache.set(address.toLowerCase(), nonce + 1);
209
+ const key = address.toLowerCase();
210
+ const onchainNonce = queryResults[i];
211
+ let effectiveNonce = onchainNonce;
212
+ if (flaky) {
213
+ const globalNext = getGlobalNextNonce(chainId, key);
214
+ if (globalNext !== undefined) {
215
+ const delta = globalNext - onchainNonce;
216
+ if (delta > 0 && delta <= 10)
217
+ effectiveNonce = globalNext;
218
+ }
219
+ setGlobalNextNonce(chainId, key, effectiveNonce + 1);
220
+ }
221
+ results[index] = effectiveNonce;
222
+ this.tempNonceCache.set(key, effectiveNonce + 1);
133
223
  }
134
224
  }
135
225
  return results;
@@ -85,22 +85,6 @@ export declare class AAAccountManager {
85
85
  userOp: UserOperation;
86
86
  prefundWei: bigint;
87
87
  }>;
88
- /**
89
- * 批量构建未签名的 UserOperation(使用 Bundler 批量估算 Gas)
90
- *
91
- * 目标:把 N 次 eth_estimateUserOperationGas 合并为 1 次(JSON-RPC batch),显著降低延迟。
92
- */
93
- buildUserOpsWithBundlerEstimateBatch(params: {
94
- ops: Array<{
95
- sender: string;
96
- nonce: bigint;
97
- callData: string;
98
- initCode?: string;
99
- }>;
100
- }): Promise<{
101
- userOps: UserOperation[];
102
- prefundWeis: bigint[];
103
- }>;
104
88
  /**
105
89
  * 构建未签名的 UserOperation(本地估算 Gas,不依赖 Bundler)
106
90
  *
@@ -170,60 +170,6 @@ export class AAAccountManager {
170
170
  const prefundWei = paymasterAndData !== '0x' ? 0n : gasTotal * BigInt(userOp.maxFeePerGas);
171
171
  return { userOp, prefundWei };
172
172
  }
173
- /**
174
- * 批量构建未签名的 UserOperation(使用 Bundler 批量估算 Gas)
175
- *
176
- * 目标:把 N 次 eth_estimateUserOperationGas 合并为 1 次(JSON-RPC batch),显著降低延迟。
177
- */
178
- async buildUserOpsWithBundlerEstimateBatch(params) {
179
- if (params.ops.length === 0) {
180
- return { userOps: [], prefundWeis: [] };
181
- }
182
- const feeData = await this.provider.getFeeData();
183
- const legacyGasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? DEFAULT_GAS_PRICE;
184
- const paymasterAndData = this.buildPaymasterAndData();
185
- // 先构建 skeleton(Gas=0),然后批量估算
186
- const skeletons = params.ops.map((p) => ({
187
- sender: p.sender,
188
- nonce: ethers.toBeHex(p.nonce),
189
- initCode: p.initCode ?? '0x',
190
- callData: p.callData,
191
- callGasLimit: '0x0',
192
- verificationGasLimit: '0x0',
193
- preVerificationGas: '0x0',
194
- maxFeePerGas: ethers.toBeHex(legacyGasPrice * 2n),
195
- maxPriorityFeePerGas: ethers.toBeHex(legacyGasPrice),
196
- paymasterAndData,
197
- signature: '0x',
198
- }));
199
- const estimates = await this.bundler.estimateUserOperationGasBatch(skeletons);
200
- const userOps = [];
201
- const prefundWeis = [];
202
- for (let i = 0; i < skeletons.length; i++) {
203
- const estimate = estimates[i];
204
- let userOp = {
205
- ...skeletons[i],
206
- callGasLimit: estimate.callGasLimit,
207
- verificationGasLimit: estimate.verificationGasLimit,
208
- preVerificationGas: estimate.preVerificationGas,
209
- };
210
- // 使用 Bundler 建议的费率(如果有)
211
- if (estimate.maxFeePerGas && estimate.maxPriorityFeePerGas) {
212
- userOp = {
213
- ...userOp,
214
- maxFeePerGas: estimate.maxFeePerGas,
215
- maxPriorityFeePerGas: estimate.maxPriorityFeePerGas,
216
- };
217
- }
218
- const gasTotal = BigInt(userOp.callGasLimit) +
219
- BigInt(userOp.verificationGasLimit) +
220
- BigInt(userOp.preVerificationGas);
221
- const prefundWei = paymasterAndData !== '0x' ? 0n : gasTotal * BigInt(userOp.maxFeePerGas);
222
- userOps.push(userOp);
223
- prefundWeis.push(prefundWei);
224
- }
225
- return { userOps, prefundWeis };
226
- }
227
173
  /**
228
174
  * 构建未签名的 UserOperation(本地估算 Gas,不依赖 Bundler)
229
175
  *
@@ -7,9 +7,9 @@
7
7
  * - 买卖一体化:买入 -> 授权 -> 卖出 -> 归集
8
8
  * - OKB 归集:将 sender 的 OKB 转回 owner
9
9
  */
10
- import { Wallet, Interface, Contract } from 'ethers';
10
+ import { 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 } from './aa-account.js';
12
+ import { AAAccountManager, encodeExecute, createWallet } from './aa-account.js';
13
13
  import { encodeBuyCall, encodeSellCall, encodeApproveCall, encodeTransferCall, PortalQuery, parseOkb, formatOkb, } from './portal-ops.js';
14
14
  // ============================================================================
15
15
  // 捆绑交易执行器
@@ -249,75 +249,40 @@ export class BundleExecutor {
249
249
  throw new Error('私钥数量和购买金额数量必须一致');
250
250
  }
251
251
  // 使用第一个 owner 作为 bundler signer
252
- const sharedProvider = this.aaManager.getProvider();
253
- const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
252
+ const wallets = privateKeys.map((pk) => createWallet(pk, this.config));
254
253
  const bundlerSigner = wallets[0];
255
254
  const beneficiary = bundlerSigner.address;
256
255
  console.log('=== XLayer Bundle Buy ===');
257
256
  console.log('token:', tokenAddress);
258
257
  console.log('owners:', wallets.length);
259
- // 1. 预取账户信息(并行),并批量估算 gas(减少对 bundler 的 N 次请求)
260
- const accountInfos = await this.aaManager.getMultipleAccountInfo(wallets.map((w) => w.address));
261
- const buyWeis = buyAmounts.map((a) => parseOkb(a));
262
- // 估算前确保 sender 有足够余额(用于 bundler 模拟;paymaster 场景会自动跳过)
263
- 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`)));
264
- const buyCallDatas = buyWeis.map((buyWei) => {
265
- const swapData = encodeBuyCall(tokenAddress, buyWei, 0n);
266
- return encodeExecute(this.portalAddress, buyWei, swapData);
267
- });
268
- const initCodes = accountInfos.map((ai, i) => ai.deployed ? '0x' : this.aaManager.generateInitCode(wallets[i].address));
269
- const { userOps: buyUserOps, prefundWeis } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
270
- ops: accountInfos.map((ai, i) => ({
271
- sender: ai.sender,
272
- nonce: ai.nonce,
273
- callData: buyCallDatas[i],
274
- initCode: initCodes[i],
275
- })),
276
- });
277
- // 补足 prefund + 买入金额(多数情况下上一步的 0.0003 已足够,这里通常不会再转账)
278
- 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`)));
279
- // 签名
280
- const signedBuy = await Promise.all(buyUserOps.map((op, i) => this.aaManager.signUserOp(op, wallets[i])));
281
- const buyOps = signedBuy.map((s) => s.userOp);
258
+ // 1. 构建买入 UserOps
259
+ const buyOps = [];
260
+ for (let i = 0; i < wallets.length; i++) {
261
+ const wallet = wallets[i];
262
+ const buyWei = parseOkb(buyAmounts[i]);
263
+ const signed = await this.buildBuyUserOp(wallet, tokenAddress, buyWei, `owner${i + 1}`);
264
+ buyOps.push(signed.userOp);
265
+ }
282
266
  // 2. 执行买入
283
267
  const buyResult = await this.runHandleOps('buyBundle', buyOps, bundlerSigner, beneficiary);
284
268
  if (!buyResult) {
285
269
  throw new Error('买入交易失败');
286
270
  }
287
271
  // 3. 获取代币余额
288
- const senders = accountInfos.map((ai) => ai.sender);
272
+ const senders = await Promise.all(wallets.map((w) => this.aaManager.predictSenderAddress(w.address)));
289
273
  const tokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, senders);
290
274
  // 4. 可选:转账代币回 owner
291
275
  let transferResult;
292
276
  if (transferBackToOwner) {
293
- const idxs = [];
294
- const transferCallDatas = [];
277
+ const transferOps = [];
295
278
  for (let i = 0; i < wallets.length; i++) {
296
- const sender = senders[i];
297
- const bal = tokenBalances.get(sender) ?? 0n;
298
- if (bal === 0n)
299
- continue;
300
- idxs.push(i);
301
- const transferData = encodeTransferCall(wallets[i].address, bal);
302
- transferCallDatas.push(encodeExecute(tokenAddress, 0n, transferData));
279
+ const signed = await this.buildTransferTokenUserOp(wallets[i], tokenAddress, `owner${i + 1}`);
280
+ if (signed) {
281
+ transferOps.push(signed.userOp);
282
+ }
303
283
  }
304
- if (idxs.length > 0) {
305
- // 估算前补一点余额(paymaster 会自动跳过)
306
- await Promise.all(idxs.map((i) => this.aaManager.ensureSenderBalance(wallets[i], senders[i], parseOkb('0.0002'), `owner${i + 1}/transfer-prefund`)));
307
- // buy 已经成功过一次,因此 transfer 的 nonce = 原 nonce + 1,且 initCode = 0x
308
- const { userOps: transferUserOps, prefundWeis: transferPrefunds } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
309
- ops: idxs.map((i, k) => ({
310
- sender: senders[i],
311
- nonce: accountInfos[i].nonce + 1n,
312
- callData: transferCallDatas[k],
313
- initCode: '0x',
314
- })),
315
- });
316
- await Promise.all(idxs.map((i, k) => this.aaManager.ensureSenderBalance(wallets[i], senders[i], transferPrefunds[k] + parseOkb('0.00005'), `owner${i + 1}/transfer-fund`)));
317
- const signedTransfer = await Promise.all(transferUserOps.map((op, k) => this.aaManager.signUserOp(op, wallets[idxs[k]])));
318
- const transferOps = signedTransfer.map((s) => s.userOp);
319
- transferResult =
320
- (await this.runHandleOps('transferBundle', transferOps, bundlerSigner, beneficiary)) ?? undefined;
284
+ if (transferOps.length > 0) {
285
+ transferResult = await this.runHandleOps('transferBundle', transferOps, bundlerSigner, beneficiary) ?? undefined;
321
286
  }
322
287
  }
323
288
  return { buyResult, transferResult, tokenBalances };
@@ -329,8 +294,7 @@ export class BundleExecutor {
329
294
  */
330
295
  async bundleSell(params) {
331
296
  const { tokenAddress, privateKeys, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, } = params;
332
- const sharedProvider = this.aaManager.getProvider();
333
- const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
297
+ const wallets = privateKeys.map((pk) => createWallet(pk, this.config));
334
298
  const bundlerSigner = wallets[0];
335
299
  const beneficiary = bundlerSigner.address;
336
300
  const reserveWei = parseOkb(withdrawReserve);
@@ -406,8 +370,7 @@ export class BundleExecutor {
406
370
  if (privateKeys.length !== buyAmounts.length) {
407
371
  throw new Error('私钥数量和购买金额数量必须一致');
408
372
  }
409
- const sharedProvider = this.aaManager.getProvider();
410
- const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
373
+ const wallets = privateKeys.map((pk) => createWallet(pk, this.config));
411
374
  const bundlerSigner = wallets[0];
412
375
  const beneficiary = bundlerSigner.address;
413
376
  const reserveWei = parseOkb(withdrawReserve);
@@ -56,13 +56,6 @@ 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;
66
59
  /**
67
60
  * 获取支持的 EntryPoint 列表
68
61
  */
@@ -74,10 +67,6 @@ export declare class BundlerClient {
74
67
  * @returns Gas 估算结果
75
68
  */
76
69
  estimateUserOperationGas(userOp: UserOperation): Promise<GasEstimate>;
77
- /**
78
- * 批量估算多个 UserOperation Gas(优先使用 JSON-RPC batch;失败则回退)
79
- */
80
- estimateUserOperationGasBatch(userOps: UserOperation[]): Promise<GasEstimate[]>;
81
70
  /**
82
71
  * 发送 UserOperation
83
72
  *
@@ -69,64 +69,6 @@ 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
- }
130
72
  /**
131
73
  * 获取支持的 EntryPoint 列表
132
74
  */
@@ -145,24 +87,6 @@ export class BundlerClient {
145
87
  this.entryPoint,
146
88
  ]);
147
89
  }
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
- }
166
90
  /**
167
91
  * 发送 UserOperation
168
92
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.5.18",
3
+ "version": "1.5.19",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",