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.
- package/dist/flap/portal-bundle-merkle/encryption.d.ts +16 -0
- package/dist/flap/portal-bundle-merkle/encryption.js +146 -0
- package/dist/utils/bundle-helpers.d.ts +2 -0
- package/dist/utils/bundle-helpers.js +95 -5
- package/dist/xlayer/aa-account.d.ts +0 -16
- package/dist/xlayer/aa-account.js +0 -54
- package/dist/xlayer/bundle.js +21 -58
- package/dist/xlayer/bundler.d.ts +0 -11
- package/dist/xlayer/bundler.js +0 -76
- 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
|
+
}
|
|
@@ -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,
|
|
41
|
-
return
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
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
|
*
|
package/dist/xlayer/bundle.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* - 买卖一体化:买入 -> 授权 -> 卖出 -> 归集
|
|
8
8
|
* - OKB 归集:将 sender 的 OKB 转回 owner
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
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
|
|
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.
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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 =
|
|
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
|
|
294
|
-
const transferCallDatas = [];
|
|
277
|
+
const transferOps = [];
|
|
295
278
|
for (let i = 0; i < wallets.length; i++) {
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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 (
|
|
305
|
-
|
|
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
|
|
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
|
|
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);
|
package/dist/xlayer/bundler.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/xlayer/bundler.js
CHANGED
|
@@ -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
|
*
|