four-flap-meme-sdk 1.3.43 → 1.3.45

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.
@@ -80,23 +80,29 @@ const V2_ROUTER_ABI = [
80
80
  'function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)',
81
81
  ];
82
82
  /**
83
- * SwapRouter02 的 V2 方法 ABI (PotatoSwap, PancakeSwap V3 Router)
84
- * SwapRouter02 同时支持 V2 和 V3,V2 方法通过 multicall 调用
83
+ * SwapRouter02 的 V2 方法 ABI (PotatoSwap)
85
84
  *
86
- * 注意:SwapRouter02 的 V2SwapRouter 方法签名:
87
- * - swapExactTokensForTokens(amountIn, amountOutMin, path[], to) - 不含 deadline
88
- * - 卖出时需要先 approve,Router 会自动 pull 代币
85
+ * 重要:SwapRouter02 的 V2 方法只有:
86
+ * - swapExactTokensForTokens(amountIn, amountOutMin, path[], to)
87
+ * - swapTokensForExactTokens(amountOut, amountInMax, path[], to)
88
+ *
89
+ * 没有 swapExactETHForTokens / swapExactTokensForETH!
90
+ * 需要手动处理 ETH/WETH 转换:
91
+ * - 买入(ETH->Token): wrapETH + swapExactTokensForTokens
92
+ * - 卖出(Token->ETH): swapExactTokensForTokens(to=ADDRESS_THIS) + unwrapWETH9
89
93
  */
90
94
  const SWAP_ROUTER02_V2_ABI = [
91
- // V2 交换方法 (SwapRouter02 内置)
95
+ // V2 交换方法(只有 token-to-token)
92
96
  'function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to) external payable returns (uint256 amountOut)',
97
+ 'function swapTokensForExactTokens(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to) external payable returns (uint256 amountIn)',
93
98
  // Multicall - 多种重载
94
99
  'function multicall(uint256 deadline, bytes[] calldata data) external payable returns (bytes[] memory results)',
95
100
  'function multicall(bytes[] calldata data) external payable returns (bytes[] memory results)',
96
- // 辅助方法
97
- 'function refundETH() external payable',
101
+ // 辅助方法 - ETH/WETH 转换
102
+ 'function wrapETH(uint256 value) external payable',
98
103
  'function unwrapWETH9(uint256 amountMinimum, address recipient) external payable',
99
104
  'function unwrapWETH9(uint256 amountMinimum) external payable',
105
+ 'function refundETH() external payable',
100
106
  // pull 方法 - 从用户拉取代币到 Router
101
107
  'function pull(address token, uint256 value) external payable',
102
108
  // sweepToken - 将 Router 中的代币发送给用户
@@ -177,8 +183,11 @@ const V3_ROUTER_LEGACY_ABI = [
177
183
  * - Uniswap V3: 0xd6145b2d3f379919e8cdeda7b97e37c4b2ca9c40
178
184
  * - PancakeSwap V3: 0x1b81D678ffb9C0263b24A97847620C99d213eB14
179
185
  *
186
+ * XLayer:
187
+ * - V3 Router (0xBB069e9465BcabC4F488d21e793BDEf0F2d41D41) - 旧版(exactInputSingle 包含 deadline)
188
+ * - SwapRouter02 (0xB45D0149249488333E3F3f9F359807F4b810C1FC) - 新版(V2+V3 混合)
189
+ *
180
190
  * BSC PancakeSwap V3 使用新版 SwapRouter02
181
- * XLayer PotatoSwap SwapRouter02 使用新版
182
191
  */
183
192
  function isLegacySwapRouter(chain, routerAddress) {
184
193
  const chainUpper = chain.toUpperCase();
@@ -192,8 +201,15 @@ function isLegacySwapRouter(chain, routerAddress) {
192
201
  return true;
193
202
  }
194
203
  }
195
- // ✅ XLayer PotatoSwap 使用新版 SwapRouter02
196
- // BSC PancakeSwap V3 使用新版 SwapRouter02
204
+ // ✅ XLayer V3 专用 Router 是旧版(exactInputSingle 包含 deadline)
205
+ if (chainUpper === 'XLAYER') {
206
+ if (routerLower === DIRECT_ROUTERS.XLAYER.V3_ROUTER.toLowerCase()) {
207
+ return true; // V3 Router 是旧版
208
+ }
209
+ // SwapRouter02 是新版
210
+ return false;
211
+ }
212
+ // ✅ BSC PancakeSwap V3 使用新版 SwapRouter02
197
213
  return false;
198
214
  }
199
215
  /** ERC20 ABI */
@@ -310,18 +326,40 @@ export async function directV2BatchBuy(params) {
310
326
  let txData;
311
327
  let txValue;
312
328
  if (useSwapRouter02) {
313
- // ✅ SwapRouter02 V2 方法:使用 multicall 包装
314
- // SwapRouter02.swapExactTokensForTokens 不含 deadline,需要通过 multicall 传递
315
- const swapData = routerIface.encodeFunctionData('swapExactTokensForTokens', [
316
- amountWei,
317
- 0n, // amountOutMin
318
- path,
319
- wallet.address,
320
- ]);
329
+ // ✅ SwapRouter02: 只有 swapExactTokensForTokens,需要手动处理 ETH wrap
330
+ // 文档:没有 swapExactETHForTokens / swapExactTokensForETH
331
+ const multicallData = [];
332
+ if (useNative) {
333
+ // ETH -> 代币:
334
+ // 1. wrapETH(msg.value) - 将 ETH 转为 WETH
335
+ // 2. swapExactTokensForTokens(WETH -> Token)
336
+ const wrapData = routerIface.encodeFunctionData('wrapETH', [amountWei]);
337
+ multicallData.push(wrapData);
338
+ const swapData = routerIface.encodeFunctionData('swapExactTokensForTokens', [
339
+ amountWei,
340
+ 0n, // amountOutMin
341
+ path, // path: [WETH, ..., tokenOut]
342
+ wallet.address, // to
343
+ ]);
344
+ multicallData.push(swapData);
345
+ // refundETH - 退回多余的 ETH(如果有)
346
+ const refundData = routerIface.encodeFunctionData('refundETH', []);
347
+ multicallData.push(refundData);
348
+ }
349
+ else {
350
+ // 代币 -> 代币:直接 swapExactTokensForTokens
351
+ const swapData = routerIface.encodeFunctionData('swapExactTokensForTokens', [
352
+ amountWei,
353
+ 0n, // amountOutMin
354
+ path,
355
+ wallet.address, // to
356
+ ]);
357
+ multicallData.push(swapData);
358
+ }
321
359
  // 使用 multicall 包装,传递 deadline
322
- txData = routerIface.encodeFunctionData('multicall', [
360
+ txData = routerIface.encodeFunctionData('multicall(uint256,bytes[])', [
323
361
  deadline,
324
- [swapData],
362
+ multicallData,
325
363
  ]);
326
364
  txValue = useNative ? amountWei : 0n;
327
365
  }
@@ -450,26 +488,41 @@ export async function directV2BatchSell(params) {
450
488
  // 卖出交易
451
489
  let txData;
452
490
  if (useSwapRouter02) {
453
- // ✅ SwapRouter02: 卖出代币换原生币
454
- // 流程:swapExactTokensForTokens (token -> WETH) + unwrapWETH9 (WETH -> ETH)
491
+ // ✅ SwapRouter02: 只有 swapExactTokensForTokens,需要手动处理 ETH unwrap
455
492
  const router02Iface = new ethers.Interface(SWAP_ROUTER02_V2_ABI);
456
- // 1. swap: token -> WETH (recipient 设为 Router 地址 address(2) 表示 Router 自己)
457
- const ADDRESS_THIS = '0x0000000000000000000000000000000000000002'; // SwapRouter02 特殊地址,表示 Router 自己
458
- const swapData = router02Iface.encodeFunctionData('swapExactTokensForTokens', [
459
- sellAmount,
460
- 0n, // amountOutMin
461
- path,
462
- useNativeOutput ? ADDRESS_THIS : wallet.address, // 如果输出是原生币,先发到 Router
463
- ]);
464
- const multicallData = [swapData];
465
- // 2. 如果输出是原生币,需要 unwrapWETH9
493
+ const multicallData = [];
494
+ // SwapRouter02 特殊地址约定:
495
+ // address(1) = ADDRESS_THIS = Router 合约自己
496
+ // address(2) = MSG_SENDER = 调用者
497
+ const ADDRESS_THIS = '0x0000000000000000000000000000000000000001'; // Router 自己
466
498
  if (useNativeOutput) {
499
+ // 代币 -> ETH:
500
+ // 1. swapExactTokensForTokens(token -> WETH, to = ADDRESS_THIS)
501
+ // 2. unwrapWETH9(WETH -> ETH, to = 用户)
502
+ const swapData = router02Iface.encodeFunctionData('swapExactTokensForTokens', [
503
+ sellAmount,
504
+ 0n, // amountOutMin
505
+ path, // path: [token, ..., WETH]
506
+ ADDRESS_THIS, // to = Router 自己,WETH 留在 Router 中
507
+ ]);
508
+ multicallData.push(swapData);
509
+ // unwrap WETH -> ETH 发送给用户
467
510
  const unwrapData = router02Iface.encodeFunctionData('unwrapWETH9(uint256,address)', [
468
511
  0n, // amountMinimum
469
- wallet.address, // recipient
512
+ wallet.address, // recipient = 用户
470
513
  ]);
471
514
  multicallData.push(unwrapData);
472
515
  }
516
+ else {
517
+ // 代币 -> 代币:直接 swapExactTokensForTokens
518
+ const swapData = router02Iface.encodeFunctionData('swapExactTokensForTokens', [
519
+ sellAmount,
520
+ 0n, // amountOutMin
521
+ path,
522
+ wallet.address, // to
523
+ ]);
524
+ multicallData.push(swapData);
525
+ }
473
526
  // 使用 multicall 包装,传递 deadline
474
527
  txData = router02Iface.encodeFunctionData('multicall(uint256,bytes[])', [
475
528
  deadline,
@@ -297,17 +297,62 @@ export async function batchSellWithBundleMerkle(params) {
297
297
  const useNativeOutput = outputToken === ZERO_ADDRESS;
298
298
  // ✅ 优化:如果前端传入了 nonces,直接使用(跳过 RPC 调用)
299
299
  const presetNonces = config.nonces;
300
- // 并行执行 gasPrice、quoteSellOutputs(Multicall3)和 nonces
301
- const [gasPrice, quotedOutputs, nonces] = await Promise.all([
300
+ const extractProfit = shouldExtractProfit(config);
301
+ // 并行执行 gasPrice quoteSellOutputs(Multicall3)
302
+ const [gasPrice, quotedOutputs] = await Promise.all([
302
303
  resolveGasPrice(provider, config),
303
- quoteSellOutputsWithQuote(readOnlyPortal, tokenAddress, amountsWei, outputToken),
304
- presetNonces && presetNonces.length === wallets.length
305
- ? Promise.resolve(presetNonces) // ✅ 使用前端传入的 nonces
306
- : nonceManager.getNextNoncesForWallets(wallets)
304
+ quoteSellOutputsWithQuote(readOnlyPortal, tokenAddress, amountsWei, outputToken)
307
305
  ]);
308
- if (presetNonces) {
306
+ // 计算利润和 maxRevenueIndex(用于 nonce 分配)
307
+ let totalTokenProfit = 0n;
308
+ let maxRevenueIndex = -1;
309
+ let maxRevenue = 0n;
310
+ if (extractProfit && quotedOutputs.length > 0) {
311
+ for (let i = 0; i < wallets.length; i++) {
312
+ const quoted = quotedOutputs[i];
313
+ if (quoted > 0n) {
314
+ const { profit } = calculateProfit(quoted, config);
315
+ totalTokenProfit += profit;
316
+ if (quoted > maxRevenue) {
317
+ maxRevenue = quoted;
318
+ maxRevenueIndex = i;
319
+ }
320
+ }
321
+ }
322
+ }
323
+ // ✅ 修复:根据是否需要利润交易,统一分配 nonces
324
+ const needProfitTx = extractProfit && totalTokenProfit > 0n && maxRevenueIndex >= 0;
325
+ let nonces;
326
+ let profitNonce;
327
+ if (presetNonces && presetNonces.length === wallets.length) {
328
+ nonces = presetNonces;
309
329
  console.log('🚀 SDK 使用前端传入的 nonces:', presetNonces);
310
330
  }
331
+ else if (needProfitTx) {
332
+ // maxRevenueIndex 钱包需要 2 个连续 nonce(卖出 + 利润)
333
+ const maxRevenueNonces = await nonceManager.getNextNonceBatch(wallets[maxRevenueIndex], 2);
334
+ // 其他钱包各需要 1 个 nonce
335
+ const otherWallets = wallets.filter((_, i) => i !== maxRevenueIndex);
336
+ const otherNonces = otherWallets.length > 0
337
+ ? await nonceManager.getNextNoncesForWallets(otherWallets)
338
+ : [];
339
+ // 组装最终的 nonces 数组(保持原顺序)
340
+ nonces = [];
341
+ let otherIdx = 0;
342
+ for (let i = 0; i < wallets.length; i++) {
343
+ if (i === maxRevenueIndex) {
344
+ nonces.push(maxRevenueNonces[0]); // 卖出交易用第一个 nonce
345
+ }
346
+ else {
347
+ nonces.push(otherNonces[otherIdx++]);
348
+ }
349
+ }
350
+ profitNonce = maxRevenueNonces[1]; // 利润交易用第二个 nonce
351
+ }
352
+ else {
353
+ // 不需要利润交易,所有钱包各 1 个 nonce
354
+ nonces = await nonceManager.getNextNoncesForWallets(wallets);
355
+ }
311
356
  const minOuts = resolveMinOutputs(minOutputAmounts, wallets.length, quotedOutputs);
312
357
  // ✅ 优化:构建未签名交易(这里是本地操作,但仍然并行执行以提高效率)
313
358
  const unsignedList = await Promise.all(portals.map((portal, i) => portal.swapExactInput.populateTransaction({
@@ -330,19 +375,31 @@ export async function batchSellWithBundleMerkle(params) {
330
375
  value: 0n // ✅ 卖出交易不发送原生代币
331
376
  })));
332
377
  signedTxs.push(...signedList);
333
- await appendSellProfitTransaction({
334
- extractProfit: shouldExtractProfit(config),
335
- quotedOutputs,
336
- wallets,
337
- nonceManager,
338
- signedTxs,
339
- chainId,
340
- gasPrice,
341
- config,
342
- outputToken, // 传递 outputToken
343
- useNativeOutput, // ✅ 传递是否原生代币输出
344
- provider // ✅ 传递 provider 用于报价
345
- });
378
+ // ✅ 修复:使用预先分配的 profitNonce 添加利润交易
379
+ if (needProfitTx && profitNonce !== undefined) {
380
+ // ERC20 输出时:获取代币利润等值的原生代币(BNB)报价
381
+ let nativeProfitAmount = totalTokenProfit;
382
+ if (!useNativeOutput && outputToken) {
383
+ nativeProfitAmount = await getTokenToNativeQuote(provider, outputToken, totalTokenProfit, chainId);
384
+ console.log('🔍 SDK ERC20 卖出利润转换: ', ethers.formatEther(totalTokenProfit), ' Token -> ', ethers.formatEther(nativeProfitAmount), ' BNB');
385
+ // 如果报价失败(返回 0),跳过利润提取
386
+ if (nativeProfitAmount === 0n) {
387
+ console.log('🔍 SDK ERC20 卖出利润转换失败,跳过利润提取');
388
+ }
389
+ }
390
+ if (nativeProfitAmount > 0n) {
391
+ const profitTx = await wallets[maxRevenueIndex].signTransaction({
392
+ to: getProfitRecipient(),
393
+ value: nativeProfitAmount,
394
+ nonce: profitNonce,
395
+ gasPrice,
396
+ gasLimit: 23000n,
397
+ chainId,
398
+ type: getTxType(config)
399
+ });
400
+ signedTxs.push(profitTx);
401
+ }
402
+ }
346
403
  nonceManager.clearTemp();
347
404
  return {
348
405
  signedTransactions: signedTxs
@@ -434,19 +491,33 @@ function buildGasLimitList(length, config) {
434
491
  return new Array(length).fill(gasLimit);
435
492
  }
436
493
  /**
437
- * ✅ 优化:批量获取所有钱包的 nonce(单次批量 RPC)
438
- * 使用 getNextNoncesForWallets 批量获取,比 Promise.all 更高效
494
+ * ✅ 修复:明确分配 nonces,避免隐式状态依赖
439
495
  */
440
496
  async function allocateBuyerNonces(buyers, extractProfit, maxIndex, totalProfit, nonceManager) {
441
- // 批量获取所有钱包的初始 nonce(内部会合并 RPC 请求)
442
- const initialNonces = await nonceManager.getNextNoncesForWallets(buyers);
443
- // 如果需要提取利润,maxIndex 钱包需要额外一个 nonce(用于利润转账)
444
- // NonceManager 内部已经缓存了,这里只是递增缓存
445
- if (extractProfit && totalProfit > 0n && maxIndex >= 0 && maxIndex < buyers.length) {
446
- // 再获取一个 nonce 给利润交易(从缓存中获取,不会触发 RPC)
447
- await nonceManager.getNextNonce(buyers[maxIndex]);
497
+ const needProfitTx = extractProfit && totalProfit > 0n && maxIndex >= 0 && maxIndex < buyers.length;
498
+ if (!needProfitTx) {
499
+ // 不需要利润交易,所有钱包各 1 nonce
500
+ return await nonceManager.getNextNoncesForWallets(buyers);
448
501
  }
449
- return initialNonces;
502
+ // 需要利润交易:maxIndex 钱包需要 2 个连续 nonce(买入 + 利润)
503
+ const maxIndexNonces = await nonceManager.getNextNonceBatch(buyers[maxIndex], 2);
504
+ // 其他钱包各需要 1 个 nonce
505
+ const otherBuyers = buyers.filter((_, i) => i !== maxIndex);
506
+ const otherNonces = otherBuyers.length > 0
507
+ ? await nonceManager.getNextNoncesForWallets(otherBuyers)
508
+ : [];
509
+ // 组装最终的 nonces 数组(保持原顺序)
510
+ const nonces = [];
511
+ let otherIdx = 0;
512
+ for (let i = 0; i < buyers.length; i++) {
513
+ if (i === maxIndex) {
514
+ nonces.push(maxIndexNonces[0]); // 买入交易用第一个 nonce
515
+ }
516
+ else {
517
+ nonces.push(otherNonces[otherIdx++]);
518
+ }
519
+ }
520
+ return nonces;
450
521
  }
451
522
  async function signBuyTransactions({ unsignedBuys, buyers, nonces, gasLimits, gasPrice, chainId, config, fundsList, useNativeToken = true // ✅ 默认使用原生代币
452
523
  }) {
@@ -471,7 +542,7 @@ async function appendProfitTransaction({ extractProfit, totalProfit, buyers, max
471
542
  value: totalProfit,
472
543
  nonce: profitNonce,
473
544
  gasPrice,
474
- gasLimit: 21000n,
545
+ gasLimit: 23000n,
475
546
  chainId,
476
547
  type: getTxType(config)
477
548
  });
@@ -562,45 +633,4 @@ function resolveMinOutputs(provided, walletCount, _quotedOutputs) {
562
633
  // 原因:大额交易时 5% 滑点可能不够,导致交易失败
563
634
  return Array(walletCount).fill(0n);
564
635
  }
565
- async function appendSellProfitTransaction({ extractProfit, quotedOutputs, wallets, nonceManager, signedTxs, chainId, gasPrice, config, outputToken, useNativeOutput = true, provider }) {
566
- if (!extractProfit || quotedOutputs.length === 0) {
567
- return;
568
- }
569
- let totalTokenProfit = 0n; // 代币利润
570
- let maxRevenueIndex = -1;
571
- let maxRevenue = 0n;
572
- for (let i = 0; i < wallets.length; i++) {
573
- const quoted = quotedOutputs[i];
574
- if (quoted > 0n) {
575
- const { profit } = calculateProfit(quoted, config);
576
- totalTokenProfit += profit;
577
- if (quoted > maxRevenue) {
578
- maxRevenue = quoted;
579
- maxRevenueIndex = i;
580
- }
581
- }
582
- }
583
- if (totalTokenProfit === 0n || maxRevenueIndex < 0) {
584
- return;
585
- }
586
- // ✅ ERC20 输出:获取代币利润等值的原生代币(BNB)报价
587
- let nativeProfitAmount = totalTokenProfit; // 原生代币输出时直接使用
588
- if (!useNativeOutput && outputToken && provider) {
589
- nativeProfitAmount = await getTokenToNativeQuote(provider, outputToken, totalTokenProfit, chainId);
590
- // 如果报价失败(返回 0),跳过利润提取
591
- if (nativeProfitAmount === 0n) {
592
- return;
593
- }
594
- }
595
- const profitNonce = await nonceManager.getNextNonce(wallets[maxRevenueIndex]);
596
- const profitTx = await wallets[maxRevenueIndex].signTransaction({
597
- to: getProfitRecipient(),
598
- value: nativeProfitAmount, // ✅ 转等值原生代币
599
- nonce: profitNonce,
600
- gasPrice,
601
- gasLimit: 21000n,
602
- chainId,
603
- type: getTxType(config)
604
- });
605
- signedTxs.push(profitTx);
606
- }
636
+ // appendSellProfitTransaction 已内联到 batchSellWithBundleMerkle 中,避免 nonce 竞争问题
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.3.43",
3
+ "version": "1.3.45",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",