four-flap-meme-sdk 1.5.98 → 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
- * 固定开销 = 贿赂 1 笔 + 利润多跳 (PROFIT_HOP_COUNT + 1) 笔
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
- const fixedOverhead = 1 + PROFIT_HOP_COUNT + 1; // 贿赂 + 利润多跳
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 path = v2Path ? [...v2Path].reverse() : [WBNB_ADDRESS, tokenAddress];
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(PANCAKE_V2_ROUTER_ADDRESS, V2_ROUTER_ABI, wallet);
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: WBNB_ADDRESS,
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: PANCAKE_V3_ROUTER_ADDRESS,
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, disperseHopCount);
576
- console.log(`[HoldersMaker] 分发多跳数: ${disperseHopCount}, 每批最大钱包数: ${maxWalletsPerBatch}`);
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, disperseHopCount, provider);
630
- if (disperseHopCount > 0) {
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] 分发多跳: ${disperseHopCount} 跳,共生成 ${allDisperseHopWallets.length} 个中间钱包`);
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
- // 原生模式(无多跳): 贿赂 1 + 分发 N + 利润 (PROFIT_HOP_COUNT + 1)
644
- // 原生模式(有多跳H): 贿赂 1 + 分发首跳 N + 利润 (PROFIT_HOP_COUNT + 1)
645
- // ERC20模式(无多跳): 贿赂 1 + 分发BNB N + 分发ERC20 N + 利润 (PROFIT_HOP_COUNT + 1)
646
- // ERC20模式(有多跳H): 贿赂 1 + 分发BNB首跳 N + 分发ERC20首跳 N + 利润 (PROFIT_HOP_COUNT + 1)
647
- // 注:多跳中间钱包的交易不消耗 payer nonce
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
- ? 1 + batch.length * 2 + PROFIT_HOP_COUNT + 1
650
- : 1 + batch.length + PROFIT_HOP_COUNT + 1;
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
- const bribeTx = await buildNativeTransferTx(payer, BLOCKRAZOR_BUILDER_EOA, bribeAmountWei, payerNonces[payerNonceIdx++], gasPrice, chainId, txType);
655
- signedTxs.push(bribeTx);
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
- const profitHopResult = await buildProfitHopTransactions({
730
- provider,
731
- payerWallet: payer,
732
- profitAmount: profitPerBatch,
733
- profitRecipient: PROFIT_CONFIG.RECIPIENT,
734
- hopCount: PROFIT_HOP_COUNT,
735
- gasPrice,
736
- chainId,
737
- txType,
738
- startNonce: payerNonces[payerNonceIdx]
739
- });
740
- signedTxs.push(...profitHopResult.signedTransactions);
741
- profitHopWallets = profitHopResult.hopWallets; // ✅ 收集利润多跳钱包
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: 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 = disperseHopCount;
824
+ result.disperseHopCount = effectiveDisperseHopCount;
787
825
  result.success = result.successBatchCount > 0;
788
826
  console.log(`[HoldersMaker] 完成: ${result.successBatchCount}/${result.totalBatchCount} 批成功`);
789
827
  }
@@ -808,6 +808,7 @@ export class BundleExecutor {
808
808
  if (dexType === 'V3') {
809
809
  routerAddress = POTATOSWAP_V3_ROUTER;
810
810
  const v3Fee = lpFeeProfileToV3Fee(tokenState.lpFeeProfile);
811
+ // ✅ V3 卖出需要 unwrap WOKB 到 OKB
811
812
  swapData = encodeSwapExactTokensForETHV3({
812
813
  tokenIn: tokenAddress,
813
814
  tokenOut: WOKB,
@@ -816,7 +817,8 @@ export class BundleExecutor {
816
817
  deadline,
817
818
  amountIn: it.sellAmount,
818
819
  amountOutMinimum: 0n,
819
- sqrtPriceLimitX96: 0n
820
+ sqrtPriceLimitX96: 0n,
821
+ unwrapRecipient: it.sender, // ✅ 添加 unwrap 接收者
820
822
  });
821
823
  }
822
824
  else {
@@ -74,6 +74,6 @@ export declare const PORTAL_ABI: readonly ["function swapExactInput((address inp
74
74
  export declare const ERC20_ABI: readonly ["function balanceOf(address account) view returns (uint256)", "function allowance(address owner, address spender) view returns (uint256)", "function approve(address spender, uint256 amount) returns (bool)", "function transfer(address to, uint256 amount) returns (bool)", "function decimals() view returns (uint8)", "function symbol() view returns (string)", "function name() view returns (string)"];
75
75
  /** PotatoSwap V2 Router ABI */
76
76
  export declare const POTATOSWAP_V2_ROUTER_ABI: readonly ["function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) external payable returns (uint[] memory amounts)", "function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)", "function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)", "function swapExactETHForTokensSupportingFeeOnTransferTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) external payable", "function swapExactTokensForETHSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external", "function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external", "function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)", "function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts)", "function WETH() external pure returns (address)"];
77
- /** PotatoSwap V3 Router ABI (SwapRouter02) */
78
- export declare const POTATOSWAP_V3_ROUTER_ABI: readonly ["function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountOut)", "function exactOutputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountOut, uint256 amountInMaximum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountIn)"];
77
+ /** PotatoSwap V3 Router ABI (SwapRouter - Legacy 版本,deadline 在 struct 内部) */
78
+ export declare const POTATOSWAP_V3_ROUTER_ABI: readonly ["function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountOut)", "function exactOutputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountOut, uint256 amountInMaximum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountIn)", "function multicall(bytes[] calldata data) external payable returns (bytes[] memory results)", "function unwrapWETH9(uint256 amountMinimum, address recipient) external payable", "function refundETH() external payable"];
79
79
  export type HexString = `0x${string}`;
@@ -146,8 +146,15 @@ export const POTATOSWAP_V2_ROUTER_ABI = [
146
146
  'function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts)',
147
147
  'function WETH() external pure returns (address)',
148
148
  ];
149
- /** PotatoSwap V3 Router ABI (SwapRouter02) */
149
+ /** PotatoSwap V3 Router ABI (SwapRouter - Legacy 版本,deadline 在 struct 内部) */
150
150
  export const POTATOSWAP_V3_ROUTER_ABI = [
151
+ // exactInputSingle - 单跳交换,deadline 在 params 内部(Legacy 版本)
151
152
  'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountOut)',
152
153
  'function exactOutputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountOut, uint256 amountInMaximum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountIn)',
154
+ // multicall - 打包多个调用(Legacy 版本不含 deadline 参数)
155
+ 'function multicall(bytes[] calldata data) external payable returns (bytes[] memory results)',
156
+ // unwrapWETH9 - 将 WOKB 转换为 OKB(用于卖出后获取原生币)
157
+ 'function unwrapWETH9(uint256 amountMinimum, address recipient) external payable',
158
+ // refundETH - 退还多余的 ETH(用于买入时的找零)
159
+ 'function refundETH() external payable',
153
160
  ];
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { Wallet, ethers } from 'ethers';
5
5
  import { AANonceMap, } from './types.js';
6
- import { POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, WOKB, MULTICALL3, } from './constants.js';
6
+ import { POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, POTATOSWAP_V3_FACTORY, WOKB, MULTICALL3, } from './constants.js';
7
7
  import { AAAccountManager, encodeExecute } from './aa-account.js';
8
8
  import { encodeApproveCall, lpFeeProfileToV3Fee, } from './portal-ops.js';
9
9
  import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, } from './dex.js';
@@ -12,6 +12,64 @@ import { PROFIT_CONFIG } from '../utils/constants.js';
12
12
  const multicallIface = new ethers.Interface([
13
13
  'function aggregate3Value((address target,bool allowFailure,uint256 value,bytes callData)[] calls) payable returns ((bool success,bytes returnData)[] returnData)',
14
14
  ]);
15
+ // ============================================================================
16
+ // V3 Slot0 报价(用于 XLayer 没有 V3 Quoter 的情况)
17
+ // ============================================================================
18
+ const V3_FACTORY_ABI = [
19
+ 'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address pool)',
20
+ ];
21
+ const V3_POOL_ABI = [
22
+ 'function slot0() view returns (uint160 sqrtPriceX96,int24 tick,uint16 observationIndex,uint16 observationCardinality,uint16 observationCardinalityNext,uint8 feeProtocol,bool unlocked)',
23
+ 'function token0() view returns (address)',
24
+ 'function token1() view returns (address)',
25
+ ];
26
+ const V3_FEE_DENOMINATOR = 1000000n;
27
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
28
+ /**
29
+ * 使用 V3 Pool 的 slot0 获取 Token → WOKB 的报价
30
+ * 这是一个现货价计算,比 V3 Quoter 更简单但精度稍低
31
+ */
32
+ async function quoteV3ViaSlot0(params) {
33
+ try {
34
+ const { provider, tokenIn, amountIn, fee } = params;
35
+ if (!tokenIn || amountIn <= 0n)
36
+ return 0n;
37
+ const tokenInLower = tokenIn.toLowerCase();
38
+ const wokbLower = WOKB.toLowerCase();
39
+ if (tokenInLower === wokbLower)
40
+ return amountIn;
41
+ const factory = new ethers.Contract(POTATOSWAP_V3_FACTORY, V3_FACTORY_ABI, provider);
42
+ const poolAddr = await factory.getPool(tokenIn, WOKB, fee);
43
+ if (!poolAddr || poolAddr.toLowerCase() === ZERO_ADDRESS.toLowerCase())
44
+ return 0n;
45
+ const pool = new ethers.Contract(poolAddr, V3_POOL_ABI, provider);
46
+ const [t0, t1, slot0] = await Promise.all([pool.token0(), pool.token1(), pool.slot0()]);
47
+ if (!t0 || !t1 || !slot0)
48
+ return 0n;
49
+ const sqrtPriceX96 = BigInt(slot0[0]);
50
+ if (sqrtPriceX96 <= 0n)
51
+ return 0n;
52
+ // 扣除手续费
53
+ const amountInLessFee = (amountIn * (V3_FEE_DENOMINATOR - BigInt(fee))) / V3_FEE_DENOMINATOR;
54
+ if (amountInLessFee <= 0n)
55
+ return 0n;
56
+ const Q192 = 2n ** 192n;
57
+ const num = sqrtPriceX96 * sqrtPriceX96;
58
+ const t0Lower = String(t0).toLowerCase();
59
+ const t1Lower = String(t1).toLowerCase();
60
+ // sqrtPriceX96 表示 token1/token0 的现货价
61
+ if (tokenInLower === t0Lower && wokbLower === t1Lower) {
62
+ return (amountInLessFee * num) / Q192;
63
+ }
64
+ if (tokenInLower === t1Lower && wokbLower === t0Lower) {
65
+ return (amountInLessFee * Q192) / num;
66
+ }
67
+ return 0n;
68
+ }
69
+ catch {
70
+ return 0n;
71
+ }
72
+ }
15
73
  function chunkArray(arr, size) {
16
74
  const out = [];
17
75
  const n = Math.max(1, Math.floor(size));
@@ -140,15 +198,37 @@ export class AADexSwapExecutor {
140
198
  needApprove = allowance < sellAmountWei;
141
199
  }
142
200
  // 估算卖出输出(用于利润提取与 buy 预算)
201
+ // ✅ V3 模式:V2 报价可能失败,需要使用 slot0 报价
143
202
  const quotedSellOutWei = await (async () => {
203
+ // 如果是 V3 模式,优先使用 slot0 报价
204
+ if (isV3Trade) {
205
+ const v3Fee = lpFeeProfileToV3Fee(lpFeeProfile);
206
+ const slot0Quote = await quoteV3ViaSlot0({
207
+ provider,
208
+ tokenIn: tokenAddress,
209
+ amountIn: sellAmountWei,
210
+ fee: v3Fee,
211
+ });
212
+ if (slot0Quote > 0n) {
213
+ console.log(`[AA V3 捆绑换手] slot0 报价成功: ${ethers.formatEther(slot0Quote)} OKB`);
214
+ return slot0Quote;
215
+ }
216
+ // slot0 报价失败,尝试 V2 fallback
217
+ }
144
218
  try {
219
+ // V2 报价(大多数代币同时有 V2/V3 池子)
145
220
  return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
146
221
  }
147
222
  catch {
223
+ // V2 报价失败时返回 0
148
224
  return 0n;
149
225
  }
150
226
  })();
151
227
  // buyAmount:若未传则按 quotedSellOut 计算;利润从 quotedSellOut 中刮取,但要保证买入资金充足
228
+ // ✅ 修复:如果报价失败且未传入 buyAmountOkb,抛出明确错误
229
+ if (!buyAmountOkb && quotedSellOutWei === 0n) {
230
+ throw new Error('AA 捆绑换手:无法获取报价(V3 池子不存在或流动性不足),请明确传入 buyAmountOkb 参数');
231
+ }
152
232
  const requestedBuyWei = buyAmountOkb
153
233
  ? ethers.parseEther(String(buyAmountOkb))
154
234
  : (quotedSellOutWei * BigInt(10000 - slippageBps)) / 10000n;
@@ -177,16 +257,18 @@ export class AADexSwapExecutor {
177
257
  const deadline = this.getDexDeadline();
178
258
  let sellSwapData;
179
259
  if (isV3Trade) {
180
- // V3: 使用 exactInputSingle
260
+ // V3: 使用 multicall(exactInputSingle + unwrapWETH9)
261
+ // 注意:V3 卖出得到的是 WOKB,需要 unwrap 成 OKB
181
262
  sellSwapData = encodeSwapExactTokensForETHV3({
182
263
  tokenIn: tokenAddress,
183
264
  tokenOut: WOKB,
184
265
  fee: lpFeeProfileToV3Fee(lpFeeProfile),
185
- recipient: sellerSender,
266
+ recipient: sellerSender, // 会被函数内部替换为 ADDRESS_THIS
186
267
  deadline,
187
268
  amountIn: sellAmountWei,
188
269
  amountOutMinimum: 0n,
189
270
  sqrtPriceLimitX96: 0n,
271
+ unwrapRecipient: sellerSender, // ✅ unwrap 后发送到 sellerSender
190
272
  });
191
273
  }
192
274
  else {
@@ -368,16 +450,18 @@ export class AADexSwapExecutor {
368
450
  const deadline = this.getDexDeadline();
369
451
  let sellSwapData;
370
452
  if (isV3Trade) {
371
- // V3: 使用 exactInputSingle
453
+ // V3: 使用 multicall(exactInputSingle + unwrapWETH9)
454
+ // 注意:V3 卖出得到的是 WOKB,需要 unwrap 成 OKB
372
455
  sellSwapData = encodeSwapExactTokensForETHV3({
373
456
  tokenIn: tokenAddress,
374
457
  tokenOut: WOKB,
375
458
  fee: lpFeeProfileToV3Fee(lpFeeProfile),
376
- recipient: sellerAi.sender,
459
+ recipient: sellerAi.sender, // 会被函数内部替换为 ADDRESS_THIS
377
460
  deadline,
378
461
  amountIn: sellAmountWei,
379
462
  amountOutMinimum: 0n,
380
463
  sqrtPriceLimitX96: 0n,
464
+ unwrapRecipient: sellerAi.sender, // ✅ unwrap 后发送到 sellerAi.sender
381
465
  });
382
466
  }
383
467
  else {
@@ -398,7 +482,22 @@ export class AADexSwapExecutor {
398
482
  const buyAmountsWei = buyAmountsOkb.map(a => ethers.parseEther(a));
399
483
  const totalBuyWei = buyAmountsWei.reduce((a, b) => a + (b ?? 0n), 0n);
400
484
  // Profit op:估算卖出输出,按比例刮取(但必须保证分发/买入资金充足)
485
+ // ✅ V3 模式:优先使用 slot0 报价
401
486
  const quotedSellOutWei = await (async () => {
487
+ // 如果是 V3 模式,优先使用 slot0 报价
488
+ if (isV3Trade) {
489
+ const v3Fee = lpFeeProfileToV3Fee(lpFeeProfile);
490
+ const slot0Quote = await quoteV3ViaSlot0({
491
+ provider,
492
+ tokenIn: tokenAddress,
493
+ amountIn: sellAmountWei,
494
+ fee: v3Fee,
495
+ });
496
+ if (slot0Quote > 0n) {
497
+ console.log(`[AA V3 批量换手] slot0 报价成功: ${ethers.formatEther(slot0Quote)} OKB`);
498
+ return slot0Quote;
499
+ }
500
+ }
402
501
  try {
403
502
  return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
404
503
  }
@@ -306,6 +306,7 @@ export class DexBundleExecutor {
306
306
  const sellNonce = nonceMap.next(sender);
307
307
  let sellSwapData;
308
308
  if (isV3) {
309
+ // ✅ V3 卖出需要 unwrap WOKB 到 OKB
309
310
  sellSwapData = encodeSwapExactTokensForETHV3({
310
311
  tokenIn: tokenAddress,
311
312
  tokenOut: WOKB,
@@ -315,6 +316,7 @@ export class DexBundleExecutor {
315
316
  amountIn: sellAmount,
316
317
  amountOutMinimum: 0n,
317
318
  sqrtPriceLimitX96: 0n,
319
+ unwrapRecipient: sender, // ✅ 添加 unwrap 接收者
318
320
  });
319
321
  }
320
322
  else {
@@ -7,21 +7,10 @@
7
7
  * - 通过 AA 账户执行
8
8
  */
9
9
  import type { XLayerConfig, DexSwapResult } from './types.js';
10
- export declare function encodeSwapExactETHForTokensV3(params: {
11
- tokenIn: string;
12
- tokenOut: string;
13
- fee: number;
14
- recipient: string;
15
- deadline: number;
16
- amountIn: bigint;
17
- amountOutMinimum: bigint;
18
- sqrtPriceLimitX96: bigint;
19
- }): string;
20
10
  /**
21
- * 编码 swapExactTokensForETHV3 调用
22
- * 对齐 Uniswap V3 的 exactInputSingle 逻辑
11
+ * V3 exactInputSingle 参数结构(Legacy 版本,包含 deadline)
23
12
  */
24
- export declare function encodeSwapExactTokensForETHV3(params: {
13
+ interface V3ExactInputSingleParams {
25
14
  tokenIn: string;
26
15
  tokenOut: string;
27
16
  fee: number;
@@ -30,6 +19,37 @@ export declare function encodeSwapExactTokensForETHV3(params: {
30
19
  amountIn: bigint;
31
20
  amountOutMinimum: bigint;
32
21
  sqrtPriceLimitX96: bigint;
22
+ }
23
+ /**
24
+ * 编码 unwrapWETH9 调用(将 WOKB 转换为 OKB)
25
+ */
26
+ export declare function encodeUnwrapWETH9(amountMinimum: bigint, recipient: string): string;
27
+ /**
28
+ * 编码 refundETH 调用(退还多余的 ETH)
29
+ */
30
+ export declare function encodeRefundETH(): string;
31
+ /**
32
+ * 编码 V3 multicall 调用(Legacy 版本,不含 deadline)
33
+ */
34
+ export declare function encodeV3Multicall(data: string[]): string;
35
+ /**
36
+ * ✅ 编码 V3 买入调用(OKB → Token)
37
+ *
38
+ * 对于 AA 模式:直接返回 exactInputSingle 编码(不需要 multicall 包装)
39
+ * - AA 的 execute 会正确传递 msg.value
40
+ * - V3 Router 会自动将原生币包装为 WOKB
41
+ *
42
+ * 对于 EOA 模式:调用者需要自己用 multicall 包装
43
+ */
44
+ export declare function encodeSwapExactETHForTokensV3(params: V3ExactInputSingleParams): string;
45
+ /**
46
+ * ✅ 编码 V3 卖出调用(Token → OKB)
47
+ * 使用 multicall 包装 exactInputSingle + unwrapWETH9
48
+ *
49
+ * 注意:V3 卖出返回的是 WOKB(ERC20),需要调用 unwrapWETH9 转换为 OKB
50
+ */
51
+ export declare function encodeSwapExactTokensForETHV3(params: V3ExactInputSingleParams & {
52
+ unwrapRecipient?: string;
33
53
  }): string;
34
54
  /**
35
55
  * 编码 swapExactETHForTokens 调用
@@ -190,3 +210,4 @@ export declare function quoteOkbToToken(okbAmount: bigint, tokenAddress: string,
190
210
  * 快速获取 Token -> OKB 报价
191
211
  */
192
212
  export declare function quoteTokenToOkb(tokenAmount: bigint, tokenAddress: string, config?: DexConfig): Promise<bigint>;
213
+ export {};
@@ -15,15 +15,62 @@ import { encodeApproveCall, parseOkb, formatOkb } from './portal-ops.js';
15
15
  // ============================================================================
16
16
  const routerIface = new Interface(POTATOSWAP_V2_ROUTER_ABI);
17
17
  const v3RouterIface = new Interface(POTATOSWAP_V3_ROUTER_ABI);
18
- export function encodeSwapExactETHForTokensV3(params) {
18
+ /**
19
+ * 编码 exactInputSingle 调用(内部使用)
20
+ */
21
+ function encodeExactInputSingle(params) {
19
22
  return v3RouterIface.encodeFunctionData('exactInputSingle', [params]);
20
23
  }
21
24
  /**
22
- * 编码 swapExactTokensForETHV3 调用
23
- * 对齐 Uniswap V3 的 exactInputSingle 逻辑
25
+ * 编码 unwrapWETH9 调用(将 WOKB 转换为 OKB)
26
+ */
27
+ export function encodeUnwrapWETH9(amountMinimum, recipient) {
28
+ return v3RouterIface.encodeFunctionData('unwrapWETH9', [amountMinimum, recipient]);
29
+ }
30
+ /**
31
+ * 编码 refundETH 调用(退还多余的 ETH)
32
+ */
33
+ export function encodeRefundETH() {
34
+ return v3RouterIface.encodeFunctionData('refundETH', []);
35
+ }
36
+ /**
37
+ * 编码 V3 multicall 调用(Legacy 版本,不含 deadline)
38
+ */
39
+ export function encodeV3Multicall(data) {
40
+ return v3RouterIface.encodeFunctionData('multicall', [data]);
41
+ }
42
+ /**
43
+ * ✅ 编码 V3 买入调用(OKB → Token)
44
+ *
45
+ * 对于 AA 模式:直接返回 exactInputSingle 编码(不需要 multicall 包装)
46
+ * - AA 的 execute 会正确传递 msg.value
47
+ * - V3 Router 会自动将原生币包装为 WOKB
48
+ *
49
+ * 对于 EOA 模式:调用者需要自己用 multicall 包装
50
+ */
51
+ export function encodeSwapExactETHForTokensV3(params) {
52
+ // 直接返回 exactInputSingle 编码
53
+ // V3 Router 会自动处理原生币 → WOKB 的转换
54
+ return encodeExactInputSingle(params);
55
+ }
56
+ /**
57
+ * ✅ 编码 V3 卖出调用(Token → OKB)
58
+ * 使用 multicall 包装 exactInputSingle + unwrapWETH9
59
+ *
60
+ * 注意:V3 卖出返回的是 WOKB(ERC20),需要调用 unwrapWETH9 转换为 OKB
24
61
  */
25
62
  export function encodeSwapExactTokensForETHV3(params) {
26
- return v3RouterIface.encodeFunctionData('exactInputSingle', [params]);
63
+ // V3 卖出需要:1) 先 swap 到 Router 地址;2) 再 unwrap 到最终接收者
64
+ const ADDRESS_THIS = '0x0000000000000000000000000000000000000002'; // V3 Router 的 ADDRESS_THIS
65
+ const swapParams = {
66
+ ...params,
67
+ recipient: ADDRESS_THIS, // 先发送到 Router 内部
68
+ };
69
+ const swapData = encodeExactInputSingle(swapParams);
70
+ // unwrap 到最终接收者
71
+ const finalRecipient = params.unwrapRecipient || params.recipient;
72
+ const unwrapData = encodeUnwrapWETH9(0n, finalRecipient); // amountMinimum = 0,不做最小值检查
73
+ return encodeV3Multicall([swapData, unwrapData]);
27
74
  }
28
75
  /**
29
76
  * 编码 swapExactETHForTokens 调用
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.5.98",
3
+ "version": "1.6.1",
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
- }