four-flap-meme-sdk 1.5.19 → 1.5.20
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/xlayer/aa-account.d.ts +16 -0
- package/dist/xlayer/aa-account.js +54 -0
- package/dist/xlayer/bundle.js +58 -21
- package/dist/xlayer/bundler.d.ts +11 -0
- package/dist/xlayer/bundler.js +76 -0
- package/package.json +1 -1
- package/dist/flap/portal-bundle-merkle/encryption.d.ts +0 -16
- package/dist/flap/portal-bundle-merkle/encryption.js +0 -146
|
@@ -85,6 +85,22 @@ 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
|
+
}>;
|
|
88
104
|
/**
|
|
89
105
|
* 构建未签名的 UserOperation(本地估算 Gas,不依赖 Bundler)
|
|
90
106
|
*
|
|
@@ -170,6 +170,60 @@ 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
|
+
}
|
|
173
227
|
/**
|
|
174
228
|
* 构建未签名的 UserOperation(本地估算 Gas,不依赖 Bundler)
|
|
175
229
|
*
|
package/dist/xlayer/bundle.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* - 买卖一体化:买入 -> 授权 -> 卖出 -> 归集
|
|
8
8
|
* - OKB 归集:将 sender 的 OKB 转回 owner
|
|
9
9
|
*/
|
|
10
|
-
import { Interface, Contract } from 'ethers';
|
|
10
|
+
import { Wallet, Interface, Contract } from 'ethers';
|
|
11
11
|
import { FLAP_PORTAL, ENTRYPOINT_ABI, DEFAULT_CALL_GAS_LIMIT_SELL, DEFAULT_WITHDRAW_RESERVE, } from './constants.js';
|
|
12
|
-
import { AAAccountManager, encodeExecute
|
|
12
|
+
import { AAAccountManager, encodeExecute } from './aa-account.js';
|
|
13
13
|
import { encodeBuyCall, encodeSellCall, encodeApproveCall, encodeTransferCall, PortalQuery, parseOkb, formatOkb, } from './portal-ops.js';
|
|
14
14
|
// ============================================================================
|
|
15
15
|
// 捆绑交易执行器
|
|
@@ -249,40 +249,75 @@ export class BundleExecutor {
|
|
|
249
249
|
throw new Error('私钥数量和购买金额数量必须一致');
|
|
250
250
|
}
|
|
251
251
|
// 使用第一个 owner 作为 bundler signer
|
|
252
|
-
const
|
|
252
|
+
const sharedProvider = this.aaManager.getProvider();
|
|
253
|
+
const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
|
|
253
254
|
const bundlerSigner = wallets[0];
|
|
254
255
|
const beneficiary = bundlerSigner.address;
|
|
255
256
|
console.log('=== XLayer Bundle Buy ===');
|
|
256
257
|
console.log('token:', tokenAddress);
|
|
257
258
|
console.log('owners:', wallets.length);
|
|
258
|
-
// 1.
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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);
|
|
266
282
|
// 2. 执行买入
|
|
267
283
|
const buyResult = await this.runHandleOps('buyBundle', buyOps, bundlerSigner, beneficiary);
|
|
268
284
|
if (!buyResult) {
|
|
269
285
|
throw new Error('买入交易失败');
|
|
270
286
|
}
|
|
271
287
|
// 3. 获取代币余额
|
|
272
|
-
const senders =
|
|
288
|
+
const senders = accountInfos.map((ai) => ai.sender);
|
|
273
289
|
const tokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, senders);
|
|
274
290
|
// 4. 可选:转账代币回 owner
|
|
275
291
|
let transferResult;
|
|
276
292
|
if (transferBackToOwner) {
|
|
277
|
-
const
|
|
293
|
+
const idxs = [];
|
|
294
|
+
const transferCallDatas = [];
|
|
278
295
|
for (let i = 0; i < wallets.length; i++) {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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));
|
|
283
303
|
}
|
|
284
|
-
if (
|
|
285
|
-
|
|
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;
|
|
286
321
|
}
|
|
287
322
|
}
|
|
288
323
|
return { buyResult, transferResult, tokenBalances };
|
|
@@ -294,7 +329,8 @@ export class BundleExecutor {
|
|
|
294
329
|
*/
|
|
295
330
|
async bundleSell(params) {
|
|
296
331
|
const { tokenAddress, privateKeys, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, } = params;
|
|
297
|
-
const
|
|
332
|
+
const sharedProvider = this.aaManager.getProvider();
|
|
333
|
+
const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
|
|
298
334
|
const bundlerSigner = wallets[0];
|
|
299
335
|
const beneficiary = bundlerSigner.address;
|
|
300
336
|
const reserveWei = parseOkb(withdrawReserve);
|
|
@@ -370,7 +406,8 @@ export class BundleExecutor {
|
|
|
370
406
|
if (privateKeys.length !== buyAmounts.length) {
|
|
371
407
|
throw new Error('私钥数量和购买金额数量必须一致');
|
|
372
408
|
}
|
|
373
|
-
const
|
|
409
|
+
const sharedProvider = this.aaManager.getProvider();
|
|
410
|
+
const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
|
|
374
411
|
const bundlerSigner = wallets[0];
|
|
375
412
|
const beneficiary = bundlerSigner.address;
|
|
376
413
|
const reserveWei = parseOkb(withdrawReserve);
|
package/dist/xlayer/bundler.d.ts
CHANGED
|
@@ -56,6 +56,13 @@ export declare class BundlerClient {
|
|
|
56
56
|
* 发送 JSON-RPC 请求到 Bundler
|
|
57
57
|
*/
|
|
58
58
|
private rpc;
|
|
59
|
+
/**
|
|
60
|
+
* 发送 JSON-RPC Batch 请求到 Bundler
|
|
61
|
+
*
|
|
62
|
+
* 注意:JSON-RPC batch 是一个数组;Particle bundler 通常也支持该格式。
|
|
63
|
+
* 为兼容其非标准字段,这里为每个 request 也附带 chainId 字段。
|
|
64
|
+
*/
|
|
65
|
+
private rpcBatch;
|
|
59
66
|
/**
|
|
60
67
|
* 获取支持的 EntryPoint 列表
|
|
61
68
|
*/
|
|
@@ -67,6 +74,10 @@ export declare class BundlerClient {
|
|
|
67
74
|
* @returns Gas 估算结果
|
|
68
75
|
*/
|
|
69
76
|
estimateUserOperationGas(userOp: UserOperation): Promise<GasEstimate>;
|
|
77
|
+
/**
|
|
78
|
+
* 批量估算多个 UserOperation Gas(优先使用 JSON-RPC batch;失败则回退)
|
|
79
|
+
*/
|
|
80
|
+
estimateUserOperationGasBatch(userOps: UserOperation[]): Promise<GasEstimate[]>;
|
|
70
81
|
/**
|
|
71
82
|
* 发送 UserOperation
|
|
72
83
|
*
|
package/dist/xlayer/bundler.js
CHANGED
|
@@ -69,6 +69,64 @@ export class BundlerClient {
|
|
|
69
69
|
clearTimeout(timer);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* 发送 JSON-RPC Batch 请求到 Bundler
|
|
74
|
+
*
|
|
75
|
+
* 注意:JSON-RPC batch 是一个数组;Particle bundler 通常也支持该格式。
|
|
76
|
+
* 为兼容其非标准字段,这里为每个 request 也附带 chainId 字段。
|
|
77
|
+
*/
|
|
78
|
+
async rpcBatch(calls) {
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
81
|
+
// 为每个 call 分配独立 id,便于按 id 回填结果
|
|
82
|
+
const ids = calls.map(() => nextRpcId++);
|
|
83
|
+
const body = calls.map((c, i) => ({
|
|
84
|
+
jsonrpc: '2.0',
|
|
85
|
+
id: ids[i],
|
|
86
|
+
method: c.method,
|
|
87
|
+
params: c.params ?? [],
|
|
88
|
+
chainId: this.chainId,
|
|
89
|
+
}));
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(this.url, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'content-type': 'application/json',
|
|
95
|
+
...this.headers,
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify(body),
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
});
|
|
100
|
+
const text = await res.text();
|
|
101
|
+
let data;
|
|
102
|
+
try {
|
|
103
|
+
data = JSON.parse(text);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new Error(`非 JSON 响应 (HTTP ${res.status}): ${text.slice(0, 400)}`);
|
|
107
|
+
}
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
throw new Error(`HTTP ${res.status}: ${JSON.stringify(data).slice(0, 800)}`);
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(data)) {
|
|
112
|
+
throw new Error(`RPC batch 非数组响应: ${JSON.stringify(data).slice(0, 800)}`);
|
|
113
|
+
}
|
|
114
|
+
// 建立 id -> result 映射,确保按 calls 顺序返回
|
|
115
|
+
const byId = new Map();
|
|
116
|
+
for (const item of data) {
|
|
117
|
+
if (item?.error) {
|
|
118
|
+
throw new Error(`RPC batch error: ${JSON.stringify(item.error)}`);
|
|
119
|
+
}
|
|
120
|
+
if (typeof item?.id === 'number') {
|
|
121
|
+
byId.set(item.id, item.result);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return ids.map((id) => byId.get(id));
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
72
130
|
/**
|
|
73
131
|
* 获取支持的 EntryPoint 列表
|
|
74
132
|
*/
|
|
@@ -87,6 +145,24 @@ export class BundlerClient {
|
|
|
87
145
|
this.entryPoint,
|
|
88
146
|
]);
|
|
89
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* 批量估算多个 UserOperation Gas(优先使用 JSON-RPC batch;失败则回退)
|
|
150
|
+
*/
|
|
151
|
+
async estimateUserOperationGasBatch(userOps) {
|
|
152
|
+
if (userOps.length === 0)
|
|
153
|
+
return [];
|
|
154
|
+
// 先尝试 batch(显著降低 HTTP 往返次数)
|
|
155
|
+
try {
|
|
156
|
+
return await this.rpcBatch(userOps.map((op) => ({
|
|
157
|
+
method: 'eth_estimateUserOperationGas',
|
|
158
|
+
params: [op, this.entryPoint],
|
|
159
|
+
})));
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// fallback:并发单请求(比串行快,但可能触发限流)
|
|
163
|
+
return await Promise.all(userOps.map((op) => this.estimateUserOperationGas(op)));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
90
166
|
/**
|
|
91
167
|
* 发送 UserOperation
|
|
92
168
|
*
|
package/package.json
CHANGED
|
@@ -1,16 +0,0 @@
|
|
|
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;
|
|
@@ -1,146 +0,0 @@
|
|
|
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
|
-
}
|