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
|
-
|
|
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,29 @@ 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;
|
|
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
|
-
|
|
671
|
-
const disperseOpCount = Math.ceil(
|
|
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
|
-
//
|
|
718
|
-
|
|
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
|
|
724
|
-
|
|
725
|
-
// 剩余需要转给后续 hop 的金额 = (totalBuyWei - 已保留的买入金额) + (剩余 hop 的 prefund)
|
|
740
|
+
const hopAi = hopAis[j];
|
|
741
|
+
// 剩余需要转给后续 hop 的金额 = totalBuyWei + (剩余 hop 的 prefund)
|
|
726
742
|
const remainingPrefund = totalHopPrefund - prefixPrefundUsed;
|
|
727
|
-
const
|
|
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 模式:仍使用原来的
|
|
737
|
-
const transferData = erc20Iface.encodeFunctionData('transfer', [next,
|
|
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:
|
|
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
|
|
756
|
-
|
|
757
|
-
const
|
|
758
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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,30 @@ 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;
|
|
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
|
|
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
|
|
481
|
-
const
|
|
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
|
-
// ✅
|
|
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
|
|
534
|
-
|
|
535
|
-
// 剩余需要转给后续 hop 的金额 = (totalBuyWei - 已保留的买入金额) + (剩余 hop 的 prefund)
|
|
549
|
+
const hopAi = hopAis[j];
|
|
550
|
+
// 剩余需要转给后续 hop 的金额 = totalBuyWei + (剩余 hop 的 prefund)
|
|
536
551
|
const remainingPrefund = totalHopPrefund - prefixPrefundUsed;
|
|
537
|
-
const
|
|
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 模式:仍使用原来的
|
|
547
|
-
const transferData = erc20Iface.encodeFunctionData('transfer', [next,
|
|
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:
|
|
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
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
}
|
|
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,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
|
-
}
|