four-flap-meme-sdk 1.6.28 → 1.6.29

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.
@@ -562,15 +562,14 @@ export class AADexSwapExecutor {
562
562
  // 原生代币模式:使用 Multicall3 批量转账
563
563
  // ERC20 模式:使用 ERC20 transfer 批量转账
564
564
  const buyerSenders = buyerAis.map(ai => ai.sender);
565
- const hopCountRaw = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
566
- const hopCount = Math.min(hopCountRaw, buyerSenders.length);
565
+ // 多跳数量不再受买方数量限制,填写多少就生成多少个独立的多跳钱包
566
+ const hopCount = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
567
567
  const maxPerOp = Math.max(1, Math.floor(Number(effectiveConfig.maxTransfersPerUserOpNative ?? 30)));
568
568
  const erc20Iface = new ethers.Interface(['function transfer(address to, uint256 amount) returns (bool)']);
569
569
  // ✅ 调试日志:追踪多跳参数
570
570
  console.log('[AA DEX 批量换手] 分发参数:', {
571
571
  capitalMode,
572
572
  disperseHopCountIn,
573
- hopCountRaw,
574
573
  hopCount,
575
574
  buyerSendersCount: buyerSenders.length,
576
575
  buyerPrivateKeysCount: buyerPrivateKeys.length,
@@ -638,11 +637,25 @@ export class AADexSwapExecutor {
638
637
  }
639
638
  }
640
639
  else {
641
- // ✅ 多跳模式:在第一跳时同时将利润刮取到 profitRecipient
640
+ // ✅ 多跳模式:动态生成独立的多跳钱包(不再使用买方钱包)
642
641
  console.log('[AA DEX 批量换手] 进入多跳模式 (hopCount > 0):', { hopCount });
643
- const hopSenders = buyerSenders.slice(0, hopCount);
644
- const hopOwners = buyerOwners.slice(0, hopCount);
645
- const hopAis = buyerAis.slice(0, hopCount);
642
+ // 动态生成 hopCount 个独立的多跳钱包
643
+ const generatedHopWallets = [];
644
+ for (let i = 0; i < hopCount; i++) {
645
+ const randomWallet = ethers.Wallet.createRandom();
646
+ generatedHopWallets.push(new ethers.Wallet(randomWallet.privateKey));
647
+ }
648
+ // 预测这些 hop 钱包的 AA sender 地址
649
+ const hopAiPromises = generatedHopWallets.map(async (w) => {
650
+ const sender = await this.aaManager.predictSenderAddress(w.address);
651
+ const code = await provider.getCode(sender);
652
+ const deployed = code && code !== '0x';
653
+ const initCode = deployed ? '0x' : this.aaManager.generateInitCode(w.address);
654
+ return { sender, deployed, initCode, ownerAddress: w.address };
655
+ });
656
+ const hopAis = await Promise.all(hopAiPromises);
657
+ const hopSenders = hopAis.map(ai => ai.sender);
658
+ const hopOwners = generatedHopWallets;
646
659
  // ✅ 关键修复:计算每个 hop 钱包执行 UserOp 需要的 prefund(gas 费用)
647
660
  // EntryPoint 在 validation 阶段会扣除 prefund,此时 hop 钱包还没收到 seller 的转账
648
661
  // 因此需要在 seller 转账时额外包含 hop 的 gas 费用
@@ -667,8 +680,8 @@ export class AADexSwapExecutor {
667
680
  const hopAi = hopAis[j];
668
681
  const hopDeployed = hopAi.deployed;
669
682
  if (isLastHop) {
670
- const rest = buyerSenders.slice(hopSenders.length);
671
- const disperseOpCount = Math.ceil(rest.length / maxPerOp) || 1;
683
+ // 最后一个 hop 需要分发给所有买方
684
+ const disperseOpCount = Math.ceil(buyerSenders.length / maxPerOp) || 1;
672
685
  const prefund = estimateHopPrefund(hopDeployed) * BigInt(disperseOpCount);
673
686
  hopPrefunds.push(prefund);
674
687
  totalHopPrefund += prefund;
@@ -714,18 +727,16 @@ export class AADexSwapExecutor {
714
727
  signOnly: true,
715
728
  });
716
729
  outOps.push(signedToHop0.userOp);
717
- // 修复:每个 hop 在转发时,需要把后续 hop 的 prefund 一起转走
718
- let prefixKept = 0n;
730
+ // 2) hop j -> hop j+1
731
+ // 修复:动态生成的 hop 钱包只做资金中转,不保留任何金额
719
732
  let prefixPrefundUsed = hopPrefunds[0] ?? 0n; // hop0 自己用掉的 prefund
720
733
  for (let j = 0; j < hopSenders.length - 1; j++) {
721
734
  const sender = hopSenders[j];
722
735
  const next = hopSenders[j + 1];
723
- const keep = buyAmountsWei[j] ?? 0n;
724
- prefixKept += keep;
725
- // 剩余需要转给后续 hop 的金额 = (totalBuyWei - 已保留的买入金额) + (剩余 hop 的 prefund)
736
+ const hopAi = hopAis[j];
737
+ // 剩余需要转给后续 hop 的金额 = totalBuyWei + (剩余 hop 的 prefund)
726
738
  const remainingPrefund = totalHopPrefund - prefixPrefundUsed;
727
- const remainingBuyWei = totalBuyWei - prefixKept;
728
- const amountToTransfer = remainingBuyWei + remainingPrefund;
739
+ const amountToTransfer = totalBuyWei + remainingPrefund;
729
740
  if (amountToTransfer <= 0n)
730
741
  break;
731
742
  let callData;
@@ -733,15 +744,15 @@ export class AADexSwapExecutor {
733
744
  callData = encodeExecute(next, amountToTransfer, '0x');
734
745
  }
735
746
  else {
736
- // ERC20 模式:仍使用原来的 remainingBuyWei(不含 prefund,因为 ERC20 模式 prefund 仍为 OKB)
737
- const transferData = erc20Iface.encodeFunctionData('transfer', [next, remainingBuyWei]);
747
+ // ERC20 模式:仍使用原来的 totalBuyWei(不含 prefund,因为 ERC20 模式 prefund 仍为 OKB)
748
+ const transferData = erc20Iface.encodeFunctionData('transfer', [next, totalBuyWei]);
738
749
  callData = encodeExecute(quoteToken, 0n, transferData);
739
750
  }
740
751
  const signedHop = await this.aaManager.buildUserOpWithState({
741
752
  ownerWallet: hopOwners[j],
742
753
  sender,
743
754
  nonce: nonceMap.next(sender),
744
- initCode: consumeInitCode(sender),
755
+ initCode: hopAi.initCode || '0x', // ✅ 使用动态生成的 hop 钱包的 initCode
745
756
  callData,
746
757
  signOnly: true,
747
758
  });
@@ -749,13 +760,26 @@ export class AADexSwapExecutor {
749
760
  // ✅ 更新已使用的 prefund(当前 hop 的 prefund 已被扣除)
750
761
  prefixPrefundUsed += hopPrefunds[j + 1] ?? 0n;
751
762
  }
763
+ // 3) lastHop 分发给所有买方(动态生成 hop 后,所有买方都是真正买方)
752
764
  const lastHopIdx = hopSenders.length - 1;
753
765
  const lastHopSender = hopSenders[lastHopIdx];
754
766
  const lastHopOwner = hopOwners[lastHopIdx];
755
- const rest = buyerSenders.slice(hopSenders.length);
756
- const restAmounts = buyAmountsWei.slice(hopSenders.length);
757
- const restItems = rest.map((to, i) => ({ to, value: restAmounts[i] ?? 0n })).filter(x => x.value > 0n);
758
- const restChunks = chunkArray(restItems, maxPerOp);
767
+ const lastHopAi = hopAis[lastHopIdx];
768
+ // 计算每个买方需要的 prefund(用于执行买入 UserOp)
769
+ const estimateBuyerPrefund = (deployed) => {
770
+ const callGas = 650000n; // DEX V3 买入
771
+ const verifyGas = deployed ? 250000n : 800000n;
772
+ const preVerifyGas = 60000n;
773
+ const totalGas = callGas + verifyGas + preVerifyGas;
774
+ return (totalGas * gasPrice * 120n) / 100n;
775
+ };
776
+ const buyerPrefunds = buyerAis.map(ai => estimateBuyerPrefund(ai.deployed));
777
+ // ✅ 分发金额 = 买入金额 + 买方 prefund
778
+ const allItems = buyerSenders.map((to, i) => ({
779
+ to,
780
+ value: (buyAmountsWei[i] ?? 0n) + (buyerPrefunds[i] ?? 0n)
781
+ })).filter(x => x.value > 0n);
782
+ const restChunks = chunkArray(allItems, maxPerOp);
759
783
  for (const ch of restChunks) {
760
784
  let callData;
761
785
  if (useNativeToken) {
@@ -775,12 +799,17 @@ export class AADexSwapExecutor {
775
799
  ownerWallet: lastHopOwner,
776
800
  sender: lastHopSender,
777
801
  nonce: nonceMap.next(lastHopSender),
778
- initCode: consumeInitCode(lastHopSender),
802
+ initCode: lastHopAi.initCode || '0x', // ✅ 使用动态生成的 hop 钱包的 initCode
779
803
  callData,
780
804
  signOnly: true,
781
805
  });
782
806
  outOps.push(signedDisperse.userOp);
783
807
  }
808
+ // ✅ 保存动态生成的多跳钱包信息,用于后续导出
809
+ this._generatedHopWallets = generatedHopWallets.map((w, i) => ({
810
+ address: hopSenders[i],
811
+ privateKey: w.privateKey,
812
+ }));
784
813
  }
785
814
  }
786
815
  else if (!capitalMode && extractProfit && profitWei > 0n) {
@@ -852,18 +881,10 @@ export class AADexSwapExecutor {
852
881
  });
853
882
  // ✅ 利润已在 AA 内部刮取(capitalMode=true 在分发阶段;capitalMode=false 单独转账),不需要 Tail Tx
854
883
  const signedTransactions = [signedHandleOps];
855
- // ✅ 收集多跳钱包信息(用作资金中转的买方钱包),用于交易失败时找回资金
856
- const hopWallets = [];
857
- if (capitalMode && hopCount > 0) {
858
- const hopSenders = buyerAis.slice(0, hopCount).map(ai => ai.sender);
859
- const hopOwnerWallets = buyerOwners.slice(0, hopCount);
860
- for (let i = 0; i < hopCount; i++) {
861
- hopWallets.push({
862
- address: hopSenders[i],
863
- privateKey: hopOwnerWallets[i].privateKey,
864
- });
865
- }
866
- }
884
+ // ✅ 收集动态生成的多跳钱包信息,用于交易失败时找回资金
885
+ const hopWallets = this._generatedHopWallets || [];
886
+ // 清理临时存储
887
+ delete this._generatedHopWallets;
867
888
  return {
868
889
  signedTransactions,
869
890
  hopWallets: hopWallets.length > 0 ? hopWallets : undefined,
@@ -371,15 +371,14 @@ export class AAPortalSwapExecutor {
371
371
  // ✅ 资金分发逻辑(仅 capitalMode=true 时执行,对应 BSC flapQuickBatchSwapMerkle)
372
372
  // capitalMode=false 时跳过分发,买方用自己的 OKB(对应 BSC flapBatchSwapMerkle)
373
373
  const buyerSenders = buyerAis.map(ai => ai.sender);
374
- const hopCountRaw = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
375
- const hopCount = Math.min(hopCountRaw, buyerSenders.length);
374
+ // 多跳数量不再受买方数量限制,填写多少就生成多少个独立的多跳钱包
375
+ const hopCount = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
376
376
  const maxPerOp = Math.max(1, Math.floor(Number(effectiveConfig.maxTransfersPerUserOpNative ?? 30)));
377
377
  const erc20Iface = new ethers.Interface(['function transfer(address to, uint256 amount) returns (bool)']);
378
378
  // ✅ 调试日志:追踪多跳参数
379
379
  console.log('[AA Portal 批量换手] 分发参数:', {
380
380
  capitalMode,
381
381
  disperseHopCountIn,
382
- hopCountRaw,
383
382
  hopCount,
384
383
  buyerSendersCount: buyerSenders.length,
385
384
  buyerPrivateKeysCount: buyerPrivateKeys.length,
@@ -445,11 +444,26 @@ export class AAPortalSwapExecutor {
445
444
  }
446
445
  }
447
446
  else {
448
- // ✅ 多跳模式:在第一跳时同时将利润刮取到 profitRecipient
447
+ // ✅ 多跳模式:动态生成独立的多跳钱包(不再使用买方钱包)
449
448
  console.log('[AA Portal 批量换手] 进入多跳模式 (hopCount > 0):', { hopCount });
450
- const hopSenders = buyerSenders.slice(0, hopCount);
451
- const hopOwners = buyerOwners.slice(0, hopCount);
452
- const hopAis = buyerAis.slice(0, hopCount);
449
+ // 动态生成 hopCount 个独立的多跳钱包
450
+ const generatedHopWallets = [];
451
+ for (let i = 0; i < hopCount; i++) {
452
+ const randomWallet = ethers.Wallet.createRandom();
453
+ generatedHopWallets.push(new ethers.Wallet(randomWallet.privateKey));
454
+ }
455
+ // 预测这些 hop 钱包的 AA sender 地址
456
+ const provider = this.aaManager.getProvider();
457
+ const hopAiPromises = generatedHopWallets.map(async (w) => {
458
+ const sender = await this.aaManager.predictSenderAddress(w.address);
459
+ const code = await provider.getCode(sender);
460
+ const deployed = code && code !== '0x';
461
+ const initCode = deployed ? '0x' : this.aaManager.generateInitCode(w.address);
462
+ return { sender, deployed, initCode, ownerAddress: w.address };
463
+ });
464
+ const hopAis = await Promise.all(hopAiPromises);
465
+ const hopSenders = hopAis.map(ai => ai.sender);
466
+ const hopOwners = generatedHopWallets;
453
467
  // ✅ 关键修复:计算每个 hop 钱包执行 UserOp 需要的 prefund(gas 费用)
454
468
  // EntryPoint 在 validation 阶段会扣除 prefund,此时 hop 钱包还没收到 seller 的转账
455
469
  // 因此需要在 seller 转账时额外包含 hop 的 gas 费用
@@ -472,14 +486,13 @@ export class AAPortalSwapExecutor {
472
486
  for (let j = 0; j < hopSenders.length; j++) {
473
487
  // hop[j] 需要执行的 UserOp 数量:
474
488
  // - 中间 hop (j < length-1):1 个转账 UserOp
475
- // - 最后一个 hop:可能有多个分发 UserOp
489
+ // - 最后一个 hop:需要分发给所有买方,可能需要多个 UserOp
476
490
  const isLastHop = j === hopSenders.length - 1;
477
491
  const hopAi = hopAis[j];
478
492
  const hopDeployed = hopAi.deployed;
479
493
  if (isLastHop) {
480
- // 最后一个 hop 需要分发给剩余买方,可能需要多个 UserOp
481
- const rest = buyerSenders.slice(hopSenders.length);
482
- const disperseOpCount = Math.ceil(rest.length / maxPerOp) || 1;
494
+ // 最后一个 hop 需要分发给所有买方(动态生成 hop 钱包后,所有买方都是真正买方)
495
+ const disperseOpCount = Math.ceil(buyerSenders.length / maxPerOp) || 1;
483
496
  const prefund = estimateHopPrefund(hopDeployed) * BigInt(disperseOpCount);
484
497
  hopPrefunds.push(prefund);
485
498
  totalHopPrefund += prefund;
@@ -524,18 +537,15 @@ export class AAPortalSwapExecutor {
524
537
  });
525
538
  outOps.push(signedToHop0.userOp);
526
539
  // 2) hop j -> hop j+1
527
- // ✅ 修复:每个 hop 在转发时,需要把后续 hop 的 prefund 一起转走
528
- let prefixKept = 0n;
540
+ // ✅ 修复:动态生成的 hop 钱包只做资金中转,不保留任何金额
529
541
  let prefixPrefundUsed = hopPrefunds[0] ?? 0n; // hop0 自己用掉的 prefund
530
542
  for (let j = 0; j < hopSenders.length - 1; j++) {
531
543
  const sender = hopSenders[j];
532
544
  const next = hopSenders[j + 1];
533
- const keep = buyAmountsWei[j] ?? 0n;
534
- prefixKept += keep;
535
- // 剩余需要转给后续 hop 的金额 = (totalBuyWei - 已保留的买入金额) + (剩余 hop 的 prefund)
545
+ const hopAi = hopAis[j];
546
+ // 剩余需要转给后续 hop 的金额 = totalBuyWei + (剩余 hop 的 prefund)
536
547
  const remainingPrefund = totalHopPrefund - prefixPrefundUsed;
537
- const remainingBuyWei = totalBuyWei - prefixKept;
538
- const amountToTransfer = remainingBuyWei + remainingPrefund;
548
+ const amountToTransfer = totalBuyWei + remainingPrefund;
539
549
  if (amountToTransfer <= 0n)
540
550
  break;
541
551
  let callData;
@@ -543,15 +553,15 @@ export class AAPortalSwapExecutor {
543
553
  callData = encodeExecute(next, amountToTransfer, '0x');
544
554
  }
545
555
  else {
546
- // ERC20 模式:仍使用原来的 remainingBuyWei(不含 prefund,因为 ERC20 模式 prefund 仍为 OKB)
547
- const transferData = erc20Iface.encodeFunctionData('transfer', [next, remainingBuyWei]);
556
+ // ERC20 模式:仍使用原来的 totalBuyWei(不含 prefund,因为 ERC20 模式 prefund 仍为 OKB)
557
+ const transferData = erc20Iface.encodeFunctionData('transfer', [next, totalBuyWei]);
548
558
  callData = encodeExecute(quoteToken, 0n, transferData);
549
559
  }
550
560
  const signedHop = await this.aaManager.buildUserOpWithState({
551
561
  ownerWallet: hopOwners[j],
552
562
  sender,
553
563
  nonce: nonceMap.next(sender),
554
- initCode: consumeInitCode(sender),
564
+ initCode: hopAi.initCode || '0x',
555
565
  callData,
556
566
  signOnly: true,
557
567
  });
@@ -559,14 +569,26 @@ export class AAPortalSwapExecutor {
559
569
  // ✅ 更新已使用的 prefund(当前 hop 的 prefund 已被扣除)
560
570
  prefixPrefundUsed += hopPrefunds[j + 1] ?? 0n;
561
571
  }
562
- // 3) lastHop 分发给剩余买方
572
+ // 3) lastHop 分发给所有买方(动态生成 hop 后,所有买方都是真正买方)
563
573
  const lastHopIdx = hopSenders.length - 1;
564
574
  const lastHopSender = hopSenders[lastHopIdx];
565
575
  const lastHopOwner = hopOwners[lastHopIdx];
566
- const rest = buyerSenders.slice(hopSenders.length);
567
- const restAmounts = buyAmountsWei.slice(hopSenders.length);
568
- const restItems = rest.map((to, i) => ({ to, value: restAmounts[i] ?? 0n })).filter(x => x.value > 0n);
569
- const restChunks = chunkArray(restItems, maxPerOp);
576
+ const lastHopAi = hopAis[lastHopIdx];
577
+ // 计算每个买方需要的 prefund(用于执行买入 UserOp)
578
+ const estimateBuyerPrefund = (deployed) => {
579
+ const callGas = 450000n; // Portal 买入
580
+ const verifyGas = deployed ? 250000n : 800000n;
581
+ const preVerifyGas = 60000n;
582
+ const totalGas = callGas + verifyGas + preVerifyGas;
583
+ return (totalGas * gasPrice * 120n) / 100n;
584
+ };
585
+ const buyerPrefunds = buyerAis.map(ai => estimateBuyerPrefund(ai.deployed));
586
+ // ✅ 分发金额 = 买入金额 + 买方 prefund
587
+ const allItems = buyerSenders.map((to, i) => ({
588
+ to,
589
+ value: (buyAmountsWei[i] ?? 0n) + (buyerPrefunds[i] ?? 0n)
590
+ })).filter(x => x.value > 0n);
591
+ const restChunks = chunkArray(allItems, maxPerOp);
570
592
  for (const ch of restChunks) {
571
593
  let callData;
572
594
  if (useNativeToken) {
@@ -586,12 +608,17 @@ export class AAPortalSwapExecutor {
586
608
  ownerWallet: lastHopOwner,
587
609
  sender: lastHopSender,
588
610
  nonce: nonceMap.next(lastHopSender),
589
- initCode: consumeInitCode(lastHopSender),
611
+ initCode: lastHopAi.initCode || '0x', // ✅ 使用动态生成的 hop 钱包的 initCode
590
612
  callData,
591
613
  signOnly: true,
592
614
  });
593
615
  outOps.push(signedDisperse.userOp);
594
616
  }
617
+ // ✅ 保存动态生成的多跳钱包信息,用于后续导出
618
+ this._generatedHopWallets = generatedHopWallets.map((w, i) => ({
619
+ address: hopSenders[i],
620
+ privateKey: w.privateKey,
621
+ }));
595
622
  }
596
623
  }
597
624
  else if (!capitalMode && extractProfit && profitWei > 0n) {
@@ -647,18 +674,10 @@ export class AAPortalSwapExecutor {
647
674
  });
648
675
  // ✅ 利润已在 AA 内部刮取(capitalMode=true 在分发阶段;capitalMode=false 单独转账),不需要 Tail Tx
649
676
  const signedTransactions = [signedHandleOps];
650
- // ✅ 收集多跳钱包信息(用作资金中转的买方钱包),用于交易失败时找回资金
651
- const hopWallets = [];
652
- if (capitalMode && hopCount > 0) {
653
- const hopSenders = buyerAis.slice(0, hopCount).map(ai => ai.sender);
654
- const hopOwnerWallets = buyerOwners.slice(0, hopCount);
655
- for (let i = 0; i < hopCount; i++) {
656
- hopWallets.push({
657
- address: hopSenders[i],
658
- privateKey: hopOwnerWallets[i].privateKey,
659
- });
660
- }
661
- }
677
+ // ✅ 收集动态生成的多跳钱包信息,用于交易失败时找回资金
678
+ const hopWallets = this._generatedHopWallets || [];
679
+ // 清理临时存储
680
+ delete this._generatedHopWallets;
662
681
  return {
663
682
  signedTransactions,
664
683
  hopWallets: hopWallets.length > 0 ? hopWallets : undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.6.28",
3
+ "version": "1.6.29",
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
- }