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