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
|
-
|
|
566
|
-
const hopCount = Math.
|
|
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
|
-
// ✅
|
|
640
|
+
// ✅ 多跳模式:动态生成独立的多跳钱包(不再使用买方钱包)
|
|
642
641
|
console.log('[AA DEX 批量换手] 进入多跳模式 (hopCount > 0):', { hopCount });
|
|
643
|
-
|
|
644
|
-
const
|
|
645
|
-
|
|
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
|
-
|
|
671
|
-
const disperseOpCount = Math.ceil(
|
|
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
|
-
//
|
|
718
|
-
|
|
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
|
|
724
|
-
|
|
725
|
-
// 剩余需要转给后续 hop 的金额 = (totalBuyWei - 已保留的买入金额) + (剩余 hop 的 prefund)
|
|
736
|
+
const hopAi = hopAis[j];
|
|
737
|
+
// 剩余需要转给后续 hop 的金额 = totalBuyWei + (剩余 hop 的 prefund)
|
|
726
738
|
const remainingPrefund = totalHopPrefund - prefixPrefundUsed;
|
|
727
|
-
const
|
|
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 模式:仍使用原来的
|
|
737
|
-
const transferData = erc20Iface.encodeFunctionData('transfer', [next,
|
|
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:
|
|
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
|
|
756
|
-
|
|
757
|
-
const
|
|
758
|
-
|
|
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:
|
|
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
|
-
|
|
858
|
-
|
|
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
|
-
|
|
375
|
-
const hopCount = Math.
|
|
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
|
-
// ✅
|
|
447
|
+
// ✅ 多跳模式:动态生成独立的多跳钱包(不再使用买方钱包)
|
|
449
448
|
console.log('[AA Portal 批量换手] 进入多跳模式 (hopCount > 0):', { hopCount });
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
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
|
|
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
|
|
481
|
-
const
|
|
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
|
-
// ✅
|
|
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
|
|
534
|
-
|
|
535
|
-
// 剩余需要转给后续 hop 的金额 = (totalBuyWei - 已保留的买入金额) + (剩余 hop 的 prefund)
|
|
545
|
+
const hopAi = hopAis[j];
|
|
546
|
+
// 剩余需要转给后续 hop 的金额 = totalBuyWei + (剩余 hop 的 prefund)
|
|
536
547
|
const remainingPrefund = totalHopPrefund - prefixPrefundUsed;
|
|
537
|
-
const
|
|
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 模式:仍使用原来的
|
|
547
|
-
const transferData = erc20Iface.encodeFunctionData('transfer', [next,
|
|
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:
|
|
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
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
|
|
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:
|
|
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
|
-
|
|
653
|
-
|
|
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,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
|
-
}
|