four-flap-meme-sdk 1.6.28 → 1.6.30

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,29 @@ 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;
659
+ // ✅ 关键:为动态生成的 hop 钱包初始化 nonce(新账户从 0 开始)
660
+ for (const hopAi of hopAis) {
661
+ nonceMap.init(hopAi.sender, 0n);
662
+ }
646
663
  // ✅ 关键修复:计算每个 hop 钱包执行 UserOp 需要的 prefund(gas 费用)
647
664
  // EntryPoint 在 validation 阶段会扣除 prefund,此时 hop 钱包还没收到 seller 的转账
648
665
  // 因此需要在 seller 转账时额外包含 hop 的 gas 费用
@@ -667,8 +684,8 @@ export class AADexSwapExecutor {
667
684
  const hopAi = hopAis[j];
668
685
  const hopDeployed = hopAi.deployed;
669
686
  if (isLastHop) {
670
- const rest = buyerSenders.slice(hopSenders.length);
671
- const disperseOpCount = Math.ceil(rest.length / maxPerOp) || 1;
687
+ // 最后一个 hop 需要分发给所有买方
688
+ const disperseOpCount = Math.ceil(buyerSenders.length / maxPerOp) || 1;
672
689
  const prefund = estimateHopPrefund(hopDeployed) * BigInt(disperseOpCount);
673
690
  hopPrefunds.push(prefund);
674
691
  totalHopPrefund += prefund;
@@ -714,18 +731,16 @@ export class AADexSwapExecutor {
714
731
  signOnly: true,
715
732
  });
716
733
  outOps.push(signedToHop0.userOp);
717
- // 修复:每个 hop 在转发时,需要把后续 hop 的 prefund 一起转走
718
- let prefixKept = 0n;
734
+ // 2) hop j -> hop j+1
735
+ // 修复:动态生成的 hop 钱包只做资金中转,不保留任何金额
719
736
  let prefixPrefundUsed = hopPrefunds[0] ?? 0n; // hop0 自己用掉的 prefund
720
737
  for (let j = 0; j < hopSenders.length - 1; j++) {
721
738
  const sender = hopSenders[j];
722
739
  const next = hopSenders[j + 1];
723
- const keep = buyAmountsWei[j] ?? 0n;
724
- prefixKept += keep;
725
- // 剩余需要转给后续 hop 的金额 = (totalBuyWei - 已保留的买入金额) + (剩余 hop 的 prefund)
740
+ const hopAi = hopAis[j];
741
+ // 剩余需要转给后续 hop 的金额 = totalBuyWei + (剩余 hop 的 prefund)
726
742
  const remainingPrefund = totalHopPrefund - prefixPrefundUsed;
727
- const remainingBuyWei = totalBuyWei - prefixKept;
728
- const amountToTransfer = remainingBuyWei + remainingPrefund;
743
+ const amountToTransfer = totalBuyWei + remainingPrefund;
729
744
  if (amountToTransfer <= 0n)
730
745
  break;
731
746
  let callData;
@@ -733,15 +748,15 @@ export class AADexSwapExecutor {
733
748
  callData = encodeExecute(next, amountToTransfer, '0x');
734
749
  }
735
750
  else {
736
- // ERC20 模式:仍使用原来的 remainingBuyWei(不含 prefund,因为 ERC20 模式 prefund 仍为 OKB)
737
- const transferData = erc20Iface.encodeFunctionData('transfer', [next, remainingBuyWei]);
751
+ // ERC20 模式:仍使用原来的 totalBuyWei(不含 prefund,因为 ERC20 模式 prefund 仍为 OKB)
752
+ const transferData = erc20Iface.encodeFunctionData('transfer', [next, totalBuyWei]);
738
753
  callData = encodeExecute(quoteToken, 0n, transferData);
739
754
  }
740
755
  const signedHop = await this.aaManager.buildUserOpWithState({
741
756
  ownerWallet: hopOwners[j],
742
757
  sender,
743
758
  nonce: nonceMap.next(sender),
744
- initCode: consumeInitCode(sender),
759
+ initCode: hopAi.initCode || '0x', // ✅ 使用动态生成的 hop 钱包的 initCode
745
760
  callData,
746
761
  signOnly: true,
747
762
  });
@@ -749,13 +764,26 @@ export class AADexSwapExecutor {
749
764
  // ✅ 更新已使用的 prefund(当前 hop 的 prefund 已被扣除)
750
765
  prefixPrefundUsed += hopPrefunds[j + 1] ?? 0n;
751
766
  }
767
+ // 3) lastHop 分发给所有买方(动态生成 hop 后,所有买方都是真正买方)
752
768
  const lastHopIdx = hopSenders.length - 1;
753
769
  const lastHopSender = hopSenders[lastHopIdx];
754
770
  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);
771
+ const lastHopAi = hopAis[lastHopIdx];
772
+ // 计算每个买方需要的 prefund(用于执行买入 UserOp)
773
+ const estimateBuyerPrefund = (deployed) => {
774
+ const callGas = 650000n; // DEX V3 买入
775
+ const verifyGas = deployed ? 250000n : 800000n;
776
+ const preVerifyGas = 60000n;
777
+ const totalGas = callGas + verifyGas + preVerifyGas;
778
+ return (totalGas * gasPrice * 120n) / 100n;
779
+ };
780
+ const buyerPrefunds = buyerAis.map(ai => estimateBuyerPrefund(ai.deployed));
781
+ // ✅ 分发金额 = 买入金额 + 买方 prefund
782
+ const allItems = buyerSenders.map((to, i) => ({
783
+ to,
784
+ value: (buyAmountsWei[i] ?? 0n) + (buyerPrefunds[i] ?? 0n)
785
+ })).filter(x => x.value > 0n);
786
+ const restChunks = chunkArray(allItems, maxPerOp);
759
787
  for (const ch of restChunks) {
760
788
  let callData;
761
789
  if (useNativeToken) {
@@ -775,12 +803,17 @@ export class AADexSwapExecutor {
775
803
  ownerWallet: lastHopOwner,
776
804
  sender: lastHopSender,
777
805
  nonce: nonceMap.next(lastHopSender),
778
- initCode: consumeInitCode(lastHopSender),
806
+ initCode: lastHopAi.initCode || '0x', // ✅ 使用动态生成的 hop 钱包的 initCode
779
807
  callData,
780
808
  signOnly: true,
781
809
  });
782
810
  outOps.push(signedDisperse.userOp);
783
811
  }
812
+ // ✅ 保存动态生成的多跳钱包信息,用于后续导出
813
+ this._generatedHopWallets = generatedHopWallets.map((w, i) => ({
814
+ address: hopSenders[i],
815
+ privateKey: w.privateKey,
816
+ }));
784
817
  }
785
818
  }
786
819
  else if (!capitalMode && extractProfit && profitWei > 0n) {
@@ -852,18 +885,10 @@ export class AADexSwapExecutor {
852
885
  });
853
886
  // ✅ 利润已在 AA 内部刮取(capitalMode=true 在分发阶段;capitalMode=false 单独转账),不需要 Tail Tx
854
887
  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
- }
888
+ // ✅ 收集动态生成的多跳钱包信息,用于交易失败时找回资金
889
+ const hopWallets = this._generatedHopWallets || [];
890
+ // 清理临时存储
891
+ delete this._generatedHopWallets;
867
892
  return {
868
893
  signedTransactions,
869
894
  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,30 @@ 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;
467
+ // ✅ 关键:为动态生成的 hop 钱包初始化 nonce(新账户从 0 开始)
468
+ for (const hopAi of hopAis) {
469
+ nonceMap.init(hopAi.sender, 0n);
470
+ }
453
471
  // ✅ 关键修复:计算每个 hop 钱包执行 UserOp 需要的 prefund(gas 费用)
454
472
  // EntryPoint 在 validation 阶段会扣除 prefund,此时 hop 钱包还没收到 seller 的转账
455
473
  // 因此需要在 seller 转账时额外包含 hop 的 gas 费用
@@ -472,14 +490,13 @@ export class AAPortalSwapExecutor {
472
490
  for (let j = 0; j < hopSenders.length; j++) {
473
491
  // hop[j] 需要执行的 UserOp 数量:
474
492
  // - 中间 hop (j < length-1):1 个转账 UserOp
475
- // - 最后一个 hop:可能有多个分发 UserOp
493
+ // - 最后一个 hop:需要分发给所有买方,可能需要多个 UserOp
476
494
  const isLastHop = j === hopSenders.length - 1;
477
495
  const hopAi = hopAis[j];
478
496
  const hopDeployed = hopAi.deployed;
479
497
  if (isLastHop) {
480
- // 最后一个 hop 需要分发给剩余买方,可能需要多个 UserOp
481
- const rest = buyerSenders.slice(hopSenders.length);
482
- const disperseOpCount = Math.ceil(rest.length / maxPerOp) || 1;
498
+ // 最后一个 hop 需要分发给所有买方(动态生成 hop 钱包后,所有买方都是真正买方)
499
+ const disperseOpCount = Math.ceil(buyerSenders.length / maxPerOp) || 1;
483
500
  const prefund = estimateHopPrefund(hopDeployed) * BigInt(disperseOpCount);
484
501
  hopPrefunds.push(prefund);
485
502
  totalHopPrefund += prefund;
@@ -524,18 +541,15 @@ export class AAPortalSwapExecutor {
524
541
  });
525
542
  outOps.push(signedToHop0.userOp);
526
543
  // 2) hop j -> hop j+1
527
- // ✅ 修复:每个 hop 在转发时,需要把后续 hop 的 prefund 一起转走
528
- let prefixKept = 0n;
544
+ // ✅ 修复:动态生成的 hop 钱包只做资金中转,不保留任何金额
529
545
  let prefixPrefundUsed = hopPrefunds[0] ?? 0n; // hop0 自己用掉的 prefund
530
546
  for (let j = 0; j < hopSenders.length - 1; j++) {
531
547
  const sender = hopSenders[j];
532
548
  const next = hopSenders[j + 1];
533
- const keep = buyAmountsWei[j] ?? 0n;
534
- prefixKept += keep;
535
- // 剩余需要转给后续 hop 的金额 = (totalBuyWei - 已保留的买入金额) + (剩余 hop 的 prefund)
549
+ const hopAi = hopAis[j];
550
+ // 剩余需要转给后续 hop 的金额 = totalBuyWei + (剩余 hop 的 prefund)
536
551
  const remainingPrefund = totalHopPrefund - prefixPrefundUsed;
537
- const remainingBuyWei = totalBuyWei - prefixKept;
538
- const amountToTransfer = remainingBuyWei + remainingPrefund;
552
+ const amountToTransfer = totalBuyWei + remainingPrefund;
539
553
  if (amountToTransfer <= 0n)
540
554
  break;
541
555
  let callData;
@@ -543,15 +557,15 @@ export class AAPortalSwapExecutor {
543
557
  callData = encodeExecute(next, amountToTransfer, '0x');
544
558
  }
545
559
  else {
546
- // ERC20 模式:仍使用原来的 remainingBuyWei(不含 prefund,因为 ERC20 模式 prefund 仍为 OKB)
547
- const transferData = erc20Iface.encodeFunctionData('transfer', [next, remainingBuyWei]);
560
+ // ERC20 模式:仍使用原来的 totalBuyWei(不含 prefund,因为 ERC20 模式 prefund 仍为 OKB)
561
+ const transferData = erc20Iface.encodeFunctionData('transfer', [next, totalBuyWei]);
548
562
  callData = encodeExecute(quoteToken, 0n, transferData);
549
563
  }
550
564
  const signedHop = await this.aaManager.buildUserOpWithState({
551
565
  ownerWallet: hopOwners[j],
552
566
  sender,
553
567
  nonce: nonceMap.next(sender),
554
- initCode: consumeInitCode(sender),
568
+ initCode: hopAi.initCode || '0x',
555
569
  callData,
556
570
  signOnly: true,
557
571
  });
@@ -559,14 +573,26 @@ export class AAPortalSwapExecutor {
559
573
  // ✅ 更新已使用的 prefund(当前 hop 的 prefund 已被扣除)
560
574
  prefixPrefundUsed += hopPrefunds[j + 1] ?? 0n;
561
575
  }
562
- // 3) lastHop 分发给剩余买方
576
+ // 3) lastHop 分发给所有买方(动态生成 hop 后,所有买方都是真正买方)
563
577
  const lastHopIdx = hopSenders.length - 1;
564
578
  const lastHopSender = hopSenders[lastHopIdx];
565
579
  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);
580
+ const lastHopAi = hopAis[lastHopIdx];
581
+ // 计算每个买方需要的 prefund(用于执行买入 UserOp)
582
+ const estimateBuyerPrefund = (deployed) => {
583
+ const callGas = 450000n; // Portal 买入
584
+ const verifyGas = deployed ? 250000n : 800000n;
585
+ const preVerifyGas = 60000n;
586
+ const totalGas = callGas + verifyGas + preVerifyGas;
587
+ return (totalGas * gasPrice * 120n) / 100n;
588
+ };
589
+ const buyerPrefunds = buyerAis.map(ai => estimateBuyerPrefund(ai.deployed));
590
+ // ✅ 分发金额 = 买入金额 + 买方 prefund
591
+ const allItems = buyerSenders.map((to, i) => ({
592
+ to,
593
+ value: (buyAmountsWei[i] ?? 0n) + (buyerPrefunds[i] ?? 0n)
594
+ })).filter(x => x.value > 0n);
595
+ const restChunks = chunkArray(allItems, maxPerOp);
570
596
  for (const ch of restChunks) {
571
597
  let callData;
572
598
  if (useNativeToken) {
@@ -586,12 +612,17 @@ export class AAPortalSwapExecutor {
586
612
  ownerWallet: lastHopOwner,
587
613
  sender: lastHopSender,
588
614
  nonce: nonceMap.next(lastHopSender),
589
- initCode: consumeInitCode(lastHopSender),
615
+ initCode: lastHopAi.initCode || '0x', // ✅ 使用动态生成的 hop 钱包的 initCode
590
616
  callData,
591
617
  signOnly: true,
592
618
  });
593
619
  outOps.push(signedDisperse.userOp);
594
620
  }
621
+ // ✅ 保存动态生成的多跳钱包信息,用于后续导出
622
+ this._generatedHopWallets = generatedHopWallets.map((w, i) => ({
623
+ address: hopSenders[i],
624
+ privateKey: w.privateKey,
625
+ }));
595
626
  }
596
627
  }
597
628
  else if (!capitalMode && extractProfit && profitWei > 0n) {
@@ -647,18 +678,10 @@ export class AAPortalSwapExecutor {
647
678
  });
648
679
  // ✅ 利润已在 AA 内部刮取(capitalMode=true 在分发阶段;capitalMode=false 单独转账),不需要 Tail Tx
649
680
  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
- }
681
+ // ✅ 收集动态生成的多跳钱包信息,用于交易失败时找回资金
682
+ const hopWallets = this._generatedHopWallets || [];
683
+ // 清理临时存储
684
+ delete this._generatedHopWallets;
662
685
  return {
663
686
  signedTransactions,
664
687
  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.30",
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
- }