four-flap-meme-sdk 1.5.99 → 1.6.1

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.
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { type GeneratedWallet } from './wallet.js';
9
9
  /** 链类型 */
10
- export type HoldersMakerChain = 'BSC' | 'MONAD';
10
+ export type HoldersMakerChain = 'BSC' | 'MONAD' | 'XLAYER';
11
11
  /** 交易类型 */
12
12
  export type TradeType = 'four' | 'flap' | 'v2' | 'v3';
13
13
  /** 基础代币类型 */
@@ -15,13 +15,17 @@ import { V2_ROUTER_ABI, V3_ROUTER02_ABI, ERC20_ABI } from '../abis/common.js';
15
15
  const FOUR_TM2_ABI = [
16
16
  'function buyTokenAMAP(uint256 origin, address token, address to, uint256 funds, uint256 minAmount) payable'
17
17
  ];
18
- // PancakeSwap Router 地址
18
+ // PancakeSwap Router 地址 (BSC)
19
19
  const PANCAKE_V2_ROUTER_ADDRESS = ADDRESSES.BSC.PancakeV2Router;
20
20
  const PANCAKE_V3_ROUTER_ADDRESS = ADDRESSES.BSC.PancakeV3Router;
21
21
  const WBNB_ADDRESS = ADDRESSES.BSC.WBNB;
22
- // ERC20 稳定币地址
22
+ // ERC20 稳定币地址 (BSC)
23
23
  const USDT_ADDRESS = ADDRESSES.BSC.USDT;
24
24
  const USDC_ADDRESS = ADDRESSES.BSC.USDC;
25
+ // ✅ XLayer Router 地址
26
+ const XLAYER_V2_ROUTER_ADDRESS = ADDRESSES.XLAYER.PotatoSwapV2Router;
27
+ const XLAYER_V3_ROUTER_ADDRESS = ADDRESSES.XLAYER.PotatoSwapV3Router;
28
+ const WOKB_ADDRESS = ADDRESSES.XLAYER.WOKB;
25
29
  // ERC20 转账和授权 Gas Limit
26
30
  const ERC20_TRANSFER_GAS_LIMIT = 65000n;
27
31
  const ERC20_APPROVE_GAS_LIMIT = 50000n;
@@ -36,7 +40,9 @@ const NATIVE_TRANSFER_GAS_LIMIT = 21055n; // 原生代币转账 Gas Limit
36
40
  * 动态计算每批最大钱包数
37
41
  *
38
42
  * Bundle 限制 = 50 笔交易
39
- * 固定开销 = 贿赂 1 笔 + 利润多跳 (PROFIT_HOP_COUNT + 1) 笔
43
+ * 固定开销:
44
+ * BSC: 贿赂 1 笔 + 利润多跳 (PROFIT_HOP_COUNT + 1) 笔
45
+ * XLayer: 利润 1 笔(无贿赂,无多跳)
40
46
  *
41
47
  * 原生模式(无分发多跳):
42
48
  * 每钱包开销 = 分发 1 + 买入 1 = 2
@@ -54,8 +60,10 @@ const NATIVE_TRANSFER_GAS_LIMIT = 21055n; // 原生代币转账 Gas Limit
54
60
  * 每钱包开销 = 分发BNB (H+1) + 分发ERC20 (H+1) + 授权 1 + 买入 1 = 2H+4
55
61
  * N ≤ (50 - 固定开销) / (2H+4)
56
62
  */
57
- function calculateMaxWalletsPerBatch(isERC20Mode, disperseHopCount) {
58
- const fixedOverhead = 1 + PROFIT_HOP_COUNT + 1; // 贿赂 + 利润多跳
63
+ function calculateMaxWalletsPerBatch(isERC20Mode, disperseHopCount, chain) {
64
+ // XLayer 不需要贿赂和利润多跳
65
+ const isXLayer = chain === 'XLAYER';
66
+ const fixedOverhead = isXLayer ? 1 : (1 + PROFIT_HOP_COUNT + 1); // XLayer: 利润 1; BSC: 贿赂 + 利润多跳
59
67
  const maxTxs = 50 - fixedOverhead;
60
68
  if (isERC20Mode) {
61
69
  // ERC20: 分发BNB (H+1) + 分发ERC20 (H+1) + 授权 1 + 买入 1
@@ -220,16 +228,23 @@ function getBaseTokenAddress(baseToken, chain, customAddress) {
220
228
  /**
221
229
  * 获取 Router 地址(用于授权)
222
230
  */
223
- function getRouterAddress(tradeType) {
231
+ function getRouterAddress(tradeType, chain) {
232
+ const isXLayer = chain === 'XLAYER';
224
233
  switch (tradeType) {
225
234
  case 'v2':
226
- return PANCAKE_V2_ROUTER_ADDRESS;
235
+ return isXLayer ? XLAYER_V2_ROUTER_ADDRESS : PANCAKE_V2_ROUTER_ADDRESS;
227
236
  case 'v3':
228
- return PANCAKE_V3_ROUTER_ADDRESS;
237
+ return isXLayer ? XLAYER_V3_ROUTER_ADDRESS : PANCAKE_V3_ROUTER_ADDRESS;
229
238
  default:
230
239
  throw new Error(`ERC20 模式不支持交易类型: ${tradeType},仅支持 v2/v3`);
231
240
  }
232
241
  }
242
+ /**
243
+ * 获取包装原生代币地址
244
+ */
245
+ function getWrappedNativeAddress(chain) {
246
+ return chain === 'XLAYER' ? WOKB_ADDRESS : WBNB_ADDRESS;
247
+ }
233
248
  /**
234
249
  * 构建 ERC20 转账交易
235
250
  */
@@ -376,13 +391,15 @@ async function buildFourBuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice,
376
391
  }
377
392
  /**
378
393
  * 构建 V2 买入交易
379
- * BNB → Token (使用 swapExactETHForTokensSupportingFeeOnTransferTokens)
394
+ * BNB/OKB → Token (使用 swapExactETHForTokensSupportingFeeOnTransferTokens)
380
395
  */
381
- async function buildV2BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, gasLimit, chainId, txType, v2Path) {
382
- // 默认路径: WBNB → Token
383
- const path = v2Path ? [...v2Path].reverse() : [WBNB_ADDRESS, tokenAddress];
396
+ async function buildV2BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, gasLimit, chainId, txType, v2Path, chain) {
397
+ // 默认路径: WBNB/WOKB → Token
398
+ const wrappedNative = getWrappedNativeAddress(chain);
399
+ const routerAddress = chain === 'XLAYER' ? XLAYER_V2_ROUTER_ADDRESS : PANCAKE_V2_ROUTER_ADDRESS;
400
+ const path = v2Path ? [...v2Path].reverse() : [wrappedNative, tokenAddress];
384
401
  const deadline = getDeadline();
385
- const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, V2_ROUTER_ABI, wallet);
402
+ const v2Router = new Contract(routerAddress, V2_ROUTER_ABI, wallet);
386
403
  const unsigned = await v2Router.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(0n, // amountOutMin(不设滑点保护)
387
404
  path, wallet.address, deadline, { value: buyAmount });
388
405
  const tx = {
@@ -404,17 +421,19 @@ async function buildV2BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, ga
404
421
  }
405
422
  /**
406
423
  * 构建 V3 单跳买入交易
407
- * BNB → Token (使用 exactInputSingle + multicall)
424
+ * BNB/OKB → Token (使用 exactInputSingle + multicall)
408
425
  *
409
426
  * ✅ 修复:使用 ethers.Interface 手动编码 calldata,避免 multicall 重载问题
410
427
  */
411
- async function buildV3BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, gasLimit, chainId, txType, v3Fee = 2500 // 默认 0.25% 手续费
412
- ) {
428
+ async function buildV3BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, gasLimit, chainId, txType, v3Fee = 2500, // 默认 0.25% 手续费
429
+ chain) {
413
430
  const deadline = getDeadline();
414
431
  const v3RouterIface = new ethers.Interface(V3_ROUTER02_ABI);
432
+ const wrappedNative = getWrappedNativeAddress(chain);
433
+ const routerAddress = chain === 'XLAYER' ? XLAYER_V3_ROUTER_ADDRESS : PANCAKE_V3_ROUTER_ADDRESS;
415
434
  // 构建 exactInputSingle calldata
416
435
  const swapParams = {
417
- tokenIn: WBNB_ADDRESS,
436
+ tokenIn: wrappedNative,
418
437
  tokenOut: tokenAddress,
419
438
  fee: v3Fee,
420
439
  recipient: wallet.address,
@@ -423,7 +442,7 @@ async function buildV3BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, ga
423
442
  sqrtPriceLimitX96: 0n
424
443
  };
425
444
  const exactInputSingleData = v3RouterIface.encodeFunctionData('exactInputSingle', [swapParams]);
426
- // ✅ 添加 refundETH,退还多余的 ETH
445
+ // ✅ 添加 refundETH,退还多余的 ETH/OKB
427
446
  const refundETHData = v3RouterIface.encodeFunctionData('refundETH', []);
428
447
  // ✅ 使用明确的函数签名编码 multicall,避免重载问题
429
448
  const multicallData = v3RouterIface.encodeFunctionData('multicall(uint256,bytes[])', [
@@ -431,7 +450,7 @@ async function buildV3BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, ga
431
450
  [exactInputSingleData, refundETHData]
432
451
  ]);
433
452
  const tx = {
434
- to: PANCAKE_V3_ROUTER_ADDRESS,
453
+ to: routerAddress,
435
454
  data: multicallData,
436
455
  value: buyAmount,
437
456
  nonce,
@@ -571,9 +590,11 @@ export async function holdersMaker(params) {
571
590
  return result;
572
591
  }
573
592
  // ✅ 根据分发多跳数动态计算每批最大钱包数
593
+ // XLayer 强制禁用分发多跳
594
+ const effectiveDisperseHopCount = chain === 'XLAYER' ? 0 : disperseHopCount;
574
595
  const maxWalletsPerBatch = config.maxWalletsPerBatch ||
575
- calculateMaxWalletsPerBatch(isERC20Mode, disperseHopCount);
576
- console.log(`[HoldersMaker] 分发多跳数: ${disperseHopCount}, 每批最大钱包数: ${maxWalletsPerBatch}`);
596
+ calculateMaxWalletsPerBatch(isERC20Mode, effectiveDisperseHopCount, chain);
597
+ console.log(`[HoldersMaker] chain: ${chain}, 分发多跳数: ${effectiveDisperseHopCount}, 每批最大钱包数: ${maxWalletsPerBatch}`);
577
598
  try {
578
599
  // 1. 初始化
579
600
  const provider = new JsonRpcProvider(rpcUrl);
@@ -625,34 +646,43 @@ export async function holdersMaker(params) {
625
646
  const profitPerBatch = totalProfit / BigInt(walletBatches.length);
626
647
  console.log(`[HoldersMaker] 总利润: ${ethers.formatEther(totalProfit)} BNB`);
627
648
  // 6. 生成分发多跳路径(如果启用)
649
+ // XLayer 强制禁用分发多跳
628
650
  let allDisperseHopWallets = [];
629
- const disperseHopPaths = generateDisperseHopPaths(newWallets, disperseHopCount, provider);
630
- if (disperseHopCount > 0) {
651
+ const disperseHopPaths = generateDisperseHopPaths(newWallets, effectiveDisperseHopCount, provider);
652
+ if (effectiveDisperseHopCount > 0) {
631
653
  // 收集所有中间钱包信息用于导出
632
654
  for (const path of disperseHopPaths) {
633
655
  allDisperseHopWallets.push(...path.hopWalletsInfo);
634
656
  }
635
657
  result.disperseHopWallets = allDisperseHopWallets;
636
- console.log(`[HoldersMaker] 分发多跳: ${disperseHopCount} 跳,共生成 ${allDisperseHopWallets.length} 个中间钱包`);
658
+ console.log(`[HoldersMaker] 分发多跳: ${effectiveDisperseHopCount} 跳,共生成 ${allDisperseHopWallets.length} 个中间钱包`);
637
659
  }
660
+ // ✅ XLayer 特殊处理:不需要贿赂和利润多跳
661
+ const isXLayer = chain === 'XLAYER';
662
+ const needBribe = !isXLayer; // XLayer 不需要贿赂
663
+ const needProfitHop = !isXLayer; // XLayer 不需要利润多跳
664
+ const effectiveProfitHopCount = needProfitHop ? PROFIT_HOP_COUNT : 0;
638
665
  // 7. 并行生成所有批次的签名
639
666
  const batchPromises = walletBatches.map(async (batch, batchIdx) => {
640
667
  try {
641
668
  const signedTxs = [];
642
669
  // 计算这批需要的 payer nonce 数量
643
- // 原生模式(无多跳): 贿赂 1 + 分发 N + 利润 (PROFIT_HOP_COUNT + 1)
644
- // 原生模式(有多跳H): 贿赂 1 + 分发首跳 N + 利润 (PROFIT_HOP_COUNT + 1)
645
- // ERC20模式(无多跳): 贿赂 1 + 分发BNB N + 分发ERC20 N + 利润 (PROFIT_HOP_COUNT + 1)
646
- // ERC20模式(有多跳H): 贿赂 1 + 分发BNB首跳 N + 分发ERC20首跳 N + 利润 (PROFIT_HOP_COUNT + 1)
647
- // 注:多跳中间钱包的交易不消耗 payer nonce
670
+ // XLayer: 分发 N + 买入后利润 1
671
+ // BSC 原生模式(无多跳): 贿赂 1 + 分发 N + 利润 (PROFIT_HOP_COUNT + 1)
672
+ // BSC 原生模式(有多跳H): 贿赂 1 + 分发首跳 N + 利润 (PROFIT_HOP_COUNT + 1)
673
+ // BSC ERC20模式(无多跳): 贿赂 1 + 分发BNB N + 分发ERC20 N + 利润 (PROFIT_HOP_COUNT + 1)
674
+ const bribeCount = needBribe ? 1 : 0;
675
+ const profitCount = needProfitHop ? (PROFIT_HOP_COUNT + 1) : 1; // XLayer 只有 1 笔利润转账
648
676
  const payerNonceCount = isERC20Mode
649
- ? 1 + batch.length * 2 + PROFIT_HOP_COUNT + 1
650
- : 1 + batch.length + PROFIT_HOP_COUNT + 1;
677
+ ? bribeCount + batch.length * 2 + profitCount
678
+ : bribeCount + batch.length + profitCount;
651
679
  const payerNonces = await nonceManager.getNextNonceBatch(payer, payerNonceCount);
652
680
  let payerNonceIdx = 0;
653
- // (1) 贿赂交易
654
- const bribeTx = await buildNativeTransferTx(payer, BLOCKRAZOR_BUILDER_EOA, bribeAmountWei, payerNonces[payerNonceIdx++], gasPrice, chainId, txType);
655
- signedTxs.push(bribeTx);
681
+ // (1) 贿赂交易(仅 BSC)
682
+ if (needBribe) {
683
+ const bribeTx = await buildNativeTransferTx(payer, BLOCKRAZOR_BUILDER_EOA, bribeAmountWei, payerNonces[payerNonceIdx++], gasPrice, chainId, txType);
684
+ signedTxs.push(bribeTx);
685
+ }
656
686
  // 获取当前批次对应的多跳路径
657
687
  const batchStartIdx = batchIdx * maxWalletsPerBatch;
658
688
  const batchPaths = disperseHopPaths.slice(batchStartIdx, batchStartIdx + batch.length);
@@ -673,7 +703,7 @@ export async function holdersMaker(params) {
673
703
  }
674
704
  // (4) ERC20 模式:授权交易(新钱包 nonce=0)
675
705
  if (isERC20Mode && erc20TokenAddress) {
676
- const routerAddress = getRouterAddress(tradeType);
706
+ const routerAddress = getRouterAddress(tradeType, chain);
677
707
  for (const newWallet of batch) {
678
708
  const buyerWallet = new Wallet(newWallet.privateKey, provider);
679
709
  const approveTx = await buildERC20ApproveTx(buyerWallet, erc20TokenAddress, routerAddress, ethers.MaxUint256, // 无限授权
@@ -712,10 +742,10 @@ export async function holdersMaker(params) {
712
742
  buyTx = await buildFlapBuyTx(buyerWallet, tokenAddress, buyAmountWei, buyNonce, gasPrice, gasLimit, chainId, txType, chain);
713
743
  break;
714
744
  case 'v2':
715
- buyTx = await buildV2BuyTx(buyerWallet, tokenAddress, buyAmountWei, buyNonce, gasPrice, gasLimit, chainId, txType, config.v2Path);
745
+ buyTx = await buildV2BuyTx(buyerWallet, tokenAddress, buyAmountWei, buyNonce, gasPrice, gasLimit, chainId, txType, config.v2Path, chain);
716
746
  break;
717
747
  case 'v3':
718
- buyTx = await buildV3BuyTx(buyerWallet, tokenAddress, buyAmountWei, buyNonce, gasPrice, gasLimit, chainId, txType, config.v3Fee);
748
+ buyTx = await buildV3BuyTx(buyerWallet, tokenAddress, buyAmountWei, buyNonce, gasPrice, gasLimit, chainId, txType, config.v3Fee, chain);
719
749
  break;
720
750
  default:
721
751
  throw new Error(`不支持的交易类型: ${tradeType}`);
@@ -723,22 +753,30 @@ export async function holdersMaker(params) {
723
753
  }
724
754
  signedTxs.push(buyTx);
725
755
  }
726
- // (6) 利润多跳
756
+ // (6) 利润转账(XLayer 直接转账,BSC 使用多跳)
727
757
  let profitHopWallets;
728
758
  if (profitPerBatch > 0n) {
729
- const profitHopResult = await buildProfitHopTransactions({
730
- provider,
731
- payerWallet: payer,
732
- profitAmount: profitPerBatch,
733
- profitRecipient: PROFIT_CONFIG.RECIPIENT,
734
- hopCount: PROFIT_HOP_COUNT,
735
- gasPrice,
736
- chainId,
737
- txType,
738
- startNonce: payerNonces[payerNonceIdx]
739
- });
740
- signedTxs.push(...profitHopResult.signedTransactions);
741
- profitHopWallets = profitHopResult.hopWallets; // ✅ 收集利润多跳钱包
759
+ if (needProfitHop) {
760
+ // BSC: 使用利润多跳
761
+ const profitHopResult = await buildProfitHopTransactions({
762
+ provider,
763
+ payerWallet: payer,
764
+ profitAmount: profitPerBatch,
765
+ profitRecipient: PROFIT_CONFIG.RECIPIENT,
766
+ hopCount: PROFIT_HOP_COUNT,
767
+ gasPrice,
768
+ chainId,
769
+ txType,
770
+ startNonce: payerNonces[payerNonceIdx]
771
+ });
772
+ signedTxs.push(...profitHopResult.signedTransactions);
773
+ profitHopWallets = profitHopResult.hopWallets;
774
+ }
775
+ else {
776
+ // XLayer: 直接转账给利润接收者
777
+ const profitTx = await buildNativeTransferTx(payer, PROFIT_CONFIG.RECIPIENT, profitPerBatch, payerNonces[payerNonceIdx], gasPrice, chainId, txType);
778
+ signedTxs.push(profitTx);
779
+ }
742
780
  }
743
781
  return {
744
782
  batchIndex: batchIdx,
@@ -775,7 +813,7 @@ export async function holdersMaker(params) {
775
813
  signedTransactions: res.signedTransactions,
776
814
  error: res.error,
777
815
  walletCount: res.walletCount,
778
- disperseHopCount: disperseHopCount // ✅ 添加分发多跳数
816
+ disperseHopCount: effectiveDisperseHopCount // ✅ 添加分发多跳数
779
817
  });
780
818
  }
781
819
  // ✅ 添加利润多跳钱包到结果
@@ -783,7 +821,7 @@ export async function holdersMaker(params) {
783
821
  result.profitHopWallets = allProfitHopWallets;
784
822
  }
785
823
  // ✅ 添加分发多跳数到结果
786
- result.disperseHopCount = disperseHopCount;
824
+ result.disperseHopCount = effectiveDisperseHopCount;
787
825
  result.success = result.successBatchCount > 0;
788
826
  console.log(`[HoldersMaker] 完成: ${result.successBatchCount}/${result.totalBatchCount} 批成功`);
789
827
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.5.99",
3
+ "version": "1.6.1",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
- }