four-flap-meme-sdk 1.6.35 → 1.6.37

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.
@@ -89,6 +89,12 @@ export declare class AAAccountManager {
89
89
  * 获取完整的 AA 账户信息
90
90
  */
91
91
  getAccountInfo(ownerAddress: string, salt?: bigint): Promise<AAAccount>;
92
+ /**
93
+ * 批量获取多个 AA sender 的 nonce
94
+ * @param senderAddresses AA sender 地址列表
95
+ * @returns 对应的 nonce 数组
96
+ */
97
+ batchGetNonces(senderAddresses: string[]): Promise<bigint[]>;
92
98
  /**
93
99
  * 生成 initCode(用于在 UserOp 中部署账户)
94
100
  */
@@ -290,6 +290,40 @@ export class AAAccountManager {
290
290
  nonce,
291
291
  };
292
292
  }
293
+ /**
294
+ * 批量获取多个 AA sender 的 nonce
295
+ * @param senderAddresses AA sender 地址列表
296
+ * @returns 对应的 nonce 数组
297
+ */
298
+ async batchGetNonces(senderAddresses) {
299
+ if (senderAddresses.length === 0)
300
+ return [];
301
+ await this.syncEntryPointIfNeeded();
302
+ const epIface = new Interface(ENTRYPOINT_ABI);
303
+ const nonceCalls = senderAddresses.map((sender) => ({
304
+ target: this.entryPointAddress,
305
+ allowFailure: true,
306
+ callData: epIface.encodeFunctionData('getNonce', [sender, 0]),
307
+ }));
308
+ const nonces = new Array(senderAddresses.length).fill(0n);
309
+ const BATCH = 350;
310
+ for (let cursor = 0; cursor < nonceCalls.length; cursor += BATCH) {
311
+ const sliceCalls = nonceCalls.slice(cursor, cursor + BATCH);
312
+ const res = await this.multicallAggregate3({ calls: sliceCalls });
313
+ for (let i = 0; i < res.length; i++) {
314
+ const r = res[i];
315
+ const idx = cursor + i;
316
+ if (!r?.success || !r.returnData || r.returnData === '0x')
317
+ continue;
318
+ try {
319
+ const decoded = epIface.decodeFunctionResult('getNonce', r.returnData);
320
+ nonces[idx] = BigInt(decoded?.[0] ?? 0n);
321
+ }
322
+ catch { /* ignore */ }
323
+ }
324
+ }
325
+ return nonces;
326
+ }
293
327
  /**
294
328
  * 生成 initCode(用于在 UserOp 中部署账户)
295
329
  */
@@ -426,6 +426,7 @@ export class AADexSwapExecutor {
426
426
  async bundleBatchSwapSign(params) {
427
427
  const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, capitalMode = true, // ✅ 默认资金利用率模式(卖出→分发→买入)
428
428
  disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, tradeType, lpFeeProfile = 0, quoteToken, quoteTokenDecimals = 6, // XLayer USDT/USDC/USDT0 都是 6 位精度
429
+ prefundedHopWallets, // ✅ 预充值多跳:已预充值的 hop 钱包列表
429
430
  } = params;
430
431
  const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
431
432
  const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
@@ -581,17 +582,23 @@ export class AADexSwapExecutor {
581
582
  extractProfit,
582
583
  profitWei: ethers.formatEther(profitWei),
583
584
  });
584
- // ✅ 多跳模式与 Paymaster 的关系:
585
+ // ✅ 多跳模式判断:
585
586
  // ERC-4337 的 handleOps 执行流程是"先验证所有 UserOps,再执行所有 UserOps"
586
587
  // hop 钱包是临时生成的(余额=0),在 validation 阶段无法支付 prefund,会导致 AA21 错误
587
588
  //
588
- // 但是!如果配置了 Paymaster,prefund = 0n,hop 钱包不需要余额即可通过验证
589
- // 因此:只有在 **没有 Paymaster** 时才需要强制禁用多跳
589
+ // 多跳可用的条件:
590
+ // 1. 配置了 Paymaster(prefund = 0n,hop 钱包不需要余额)
591
+ // 2. 传入了预充值好的 hop 钱包(hop 钱包已有余额支付 prefund)
590
592
  const hasPaymaster = this.aaManager.hasPaymaster();
591
- const effectiveHopCount = hasPaymaster ? hopCount : 0;
592
- if (hopCount > 0 && !hasPaymaster) {
593
- console.warn('[AA DEX 批量换手] ⚠️ Paymaster 模式不支持多跳(hop 钱包无法支付 prefund),已自动降级为直接分发模式');
594
- console.warn('[AA DEX 批量换手] 💡 提示:配置 Paymaster 可启用多跳功能');
593
+ const hasPrefundedHops = Array.isArray(prefundedHopWallets) && prefundedHopWallets.length > 0;
594
+ const canDoMultiHop = hasPaymaster || hasPrefundedHops;
595
+ const effectiveHopCount = canDoMultiHop ? hopCount : 0;
596
+ if (hopCount > 0 && !canDoMultiHop) {
597
+ console.warn('[AA DEX 批量换手] ⚠️ 无法执行多跳(hop 钱包无法支付 prefund),已自动降级为直接分发模式');
598
+ console.warn('[AA DEX 批量换手] 💡 提示:使用 HopWalletManager.prefundHopWallets() 预充值 hop 钱包,或配置 Paymaster');
599
+ }
600
+ else if (hopCount > 0 && hasPrefundedHops) {
601
+ console.log('[AA DEX 批量换手] ✅ 预充值多跳模式已启用 (hopCount:', hopCount, ', prefundedHops:', prefundedHopWallets.length, ')');
595
602
  }
596
603
  else if (hopCount > 0 && hasPaymaster) {
597
604
  console.log('[AA DEX 批量换手] ✅ Paymaster 模式已启用,支持多跳换手 (hopCount:', hopCount, ')');
@@ -655,9 +662,13 @@ export class AADexSwapExecutor {
655
662
  }
656
663
  }
657
664
  else {
658
- // ⚠️ 多跳模式在 XLayer AA 下不可行(effectiveHopCount 已强制为 0)
659
- // 以下代码保留但不会执行,仅供参考
660
- console.log('[AA DEX 批量换手] 进入多跳模式 (effectiveHopCount > 0):', { effectiveHopCount, buyerCount: buyerSenders.length });
665
+ // 多跳模式:使用预充值的 hop 钱包或 Paymaster 模式
666
+ console.log('[AA DEX 批量换手] 进入多跳模式 (effectiveHopCount > 0):', {
667
+ effectiveHopCount,
668
+ buyerCount: buyerSenders.length,
669
+ usePrefundedHops: hasPrefundedHops,
670
+ usePaymaster: hasPaymaster,
671
+ });
661
672
  const feeData = await this.aaManager.getFeeData();
662
673
  const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
663
674
  // 预估 prefund 的函数(使用全局常量)
@@ -667,25 +678,59 @@ export class AADexSwapExecutor {
667
678
  const totalGas = callGas + verifyGas + preVerifyGas;
668
679
  return (totalGas * gasPrice * PREFUND_BUFFER_PERCENT) / 100n;
669
680
  };
670
- // ✅ 为每个买方生成独立的多跳链
671
- // hopChains[buyerIdx] = [hop0, hop1, ..., hop(H-1)] 每条链有 hopCount 个 hop 钱包
681
+ // ✅ 构建 hop 钱包列表
682
+ // 如果有预充值的 hop 钱包,使用它们;否则生成新钱包(需要 Paymaster)
672
683
  const allGeneratedHopWallets = [];
673
- for (let buyerIdx = 0; buyerIdx < buyerSenders.length; buyerIdx++) {
674
- const chainHops = [];
675
- for (let h = 0; h < hopCount; h++) {
676
- const randomWallet = ethers.Wallet.createRandom();
677
- const wallet = new ethers.Wallet(randomWallet.privateKey);
678
- const sender = await this.aaManager.predictSenderAddress(wallet.address);
679
- const code = await provider.getCode(sender);
680
- const deployed = code !== null && code !== '0x';
681
- const initCode = deployed ? '0x' : this.aaManager.generateInitCode(wallet.address);
682
- chainHops.push({ wallet, sender, deployed, initCode });
683
- // 初始化 nonce
684
- nonceMap.init(sender, 0n);
684
+ if (hasPrefundedHops) {
685
+ // 预充值模式:使用传入的 hop 钱包
686
+ // 每个买方分配 effectiveHopCount hop 钱包
687
+ const totalHopsNeeded = buyerSenders.length * effectiveHopCount;
688
+ if (prefundedHopWallets.length < totalHopsNeeded) {
689
+ throw new Error(`预充值 hop 钱包数量不足: 需要 ${totalHopsNeeded} 个,实际 ${prefundedHopWallets.length} 个`);
690
+ }
691
+ // 关键修复:从链上获取所有 hop 钱包的真实 nonce(用户可能重复使用同一批 hop 钱包)
692
+ const hopSenders = prefundedHopWallets.slice(0, totalHopsNeeded).map(h => h.senderAddress);
693
+ const hopNonces = await this.aaManager.batchGetNonces(hopSenders);
694
+ let hopIdx = 0;
695
+ for (let buyerIdx = 0; buyerIdx < buyerSenders.length; buyerIdx++) {
696
+ const chainHops = [];
697
+ for (let h = 0; h < effectiveHopCount; h++) {
698
+ const prefundedHop = prefundedHopWallets[hopIdx];
699
+ const wallet = new ethers.Wallet(prefundedHop.privateKey, provider);
700
+ chainHops.push({
701
+ wallet,
702
+ sender: prefundedHop.senderAddress,
703
+ deployed: prefundedHop.deployed,
704
+ initCode: prefundedHop.initCode,
705
+ });
706
+ // ✅ 使用从链上获取的真实 nonce(而非硬编码 0)
707
+ const realNonce = hopNonces[hopIdx] ?? 0n;
708
+ nonceMap.init(prefundedHop.senderAddress, realNonce);
709
+ hopIdx++;
710
+ }
711
+ allGeneratedHopWallets.push(chainHops);
685
712
  }
686
- allGeneratedHopWallets.push(chainHops);
713
+ console.log(`[AA DEX 多跳] ✅ 使用预充值 hop 钱包: ${totalHopsNeeded} 个 (${buyerSenders.length} 条链 × ${effectiveHopCount} 跳)`);
714
+ }
715
+ else {
716
+ // Paymaster 模式:生成新钱包(不需要预充值,prefund=0)
717
+ for (let buyerIdx = 0; buyerIdx < buyerSenders.length; buyerIdx++) {
718
+ const chainHops = [];
719
+ for (let h = 0; h < effectiveHopCount; h++) {
720
+ const randomWallet = ethers.Wallet.createRandom();
721
+ const wallet = new ethers.Wallet(randomWallet.privateKey, provider);
722
+ const sender = await this.aaManager.predictSenderAddress(wallet.address);
723
+ const code = await provider.getCode(sender);
724
+ const deployed = code !== null && code !== '0x';
725
+ const initCode = deployed ? '0x' : this.aaManager.generateInitCode(wallet.address);
726
+ chainHops.push({ wallet, sender, deployed, initCode });
727
+ // 初始化 nonce
728
+ nonceMap.init(sender, 0n);
729
+ }
730
+ allGeneratedHopWallets.push(chainHops);
731
+ }
732
+ console.log(`[AA DEX 多跳] 使用 Paymaster 模式生成 hop 钱包: ${buyerSenders.length * effectiveHopCount} 个`);
687
733
  }
688
- console.log(`[AA DEX 多跳] 总 hop 钱包数: ${buyerSenders.length * hopCount} (${buyerSenders.length} 条链 × ${hopCount} 跳)`);
689
734
  // ✅ 利润刮取(只在第一笔 seller UserOp 中执行)
690
735
  let profitHandled = false;
691
736
  // ✅ 构建每条多跳链的 UserOps
@@ -696,16 +741,14 @@ export class AADexSwapExecutor {
696
741
  const buyAmount = buyAmountsWei[buyerIdx] ?? 0n;
697
742
  if (buyAmount <= 0n)
698
743
  continue;
699
- // 计算这条链上所有 hop 的 prefund(使用常量)
700
- const hopPrefunds = chainHops.map((hop, idx) => {
701
- // 最后一个 hop 转账给 buyer,中间 hop 转账给下一个 hop
702
- return estimatePrefund(HOP_CALL_GAS_LIMIT, hop.deployed);
703
- });
704
- const totalHopPrefund = hopPrefunds.reduce((a, b) => a + b, 0n);
705
- // 计算 buyer 的 prefund(用于买入,使用常量)
744
+ // 计算 buyer 的 prefund(用于买入,使用常量)
706
745
  const buyerPrefund = estimatePrefund(DEX_BUY_CALL_GAS_LIMIT, buyerAi.deployed);
707
- // 这条链的总金额 = 买入金额 + 所有 hop prefund + buyer prefund
708
- const chainTotalAmount = buyAmount + totalHopPrefund + buyerPrefund;
746
+ // 计算分发金额:
747
+ // - 预充值模式:hop 已有 prefund,分发金额 = 买入金额 + buyer prefund
748
+ // - Paymaster 模式:prefund = 0,分发金额 = 买入金额 + buyer prefund
749
+ // 注意:无论哪种模式,都不需要在分发中包含 hop 的 prefund
750
+ //(预充值模式下 hop 已有余额,Paymaster 模式下 prefund=0)
751
+ const chainTotalAmount = buyAmount + buyerPrefund;
709
752
  // 1) seller → hop0
710
753
  const hop0 = chainHops[0];
711
754
  let callData0;
@@ -744,13 +787,12 @@ export class AADexSwapExecutor {
744
787
  });
745
788
  outOps.push(signedSellerToHop0.userOp);
746
789
  // 2) hop[j] → hop[j+1] (中间跳)
747
- let remainingPrefund = totalHopPrefund;
790
+ // 预充值模式下,hop 钱包自己有 prefund,只需要传递买入金额 + buyer prefund
748
791
  for (let j = 0; j < chainHops.length - 1; j++) {
749
792
  const currentHop = chainHops[j];
750
793
  const nextHop = chainHops[j + 1];
751
- // 当前 hop 扣除自己的 prefund 后转给下一个
752
- remainingPrefund -= hopPrefunds[j];
753
- const amountToTransfer = buyAmount + remainingPrefund + buyerPrefund;
794
+ // 传递金额 = 买入金额 + buyer prefund(hop 自己的 gas 从预充值中支付)
795
+ const amountToTransfer = buyAmount + buyerPrefund;
754
796
  let callData;
755
797
  if (useNativeToken) {
756
798
  callData = encodeExecute(nextHop.sender, amountToTransfer, '0x');
@@ -0,0 +1,143 @@
1
+ /**
2
+ * XLayer AA 多跳钱包管理器
3
+ *
4
+ * 功能:
5
+ * 1. 生成临时 Hop 钱包(可导出私钥)
6
+ * 2. 预充值 Hop 钱包(支持取消)
7
+ * 3. 退回预充值资金(原路返回)
8
+ * 4. 状态回调(进度通知)
9
+ */
10
+ import { Wallet } from 'ethers';
11
+ import { AAAccountManager } from './aa-account.js';
12
+ import { XLayerConfig } from './types.js';
13
+ /** Hop 钱包信息 */
14
+ export interface HopWalletInfo {
15
+ /** 钱包索引 */
16
+ index: number;
17
+ /** Owner 私钥(可导出) */
18
+ privateKey: string;
19
+ /** Owner 地址 */
20
+ ownerAddress: string;
21
+ /** AA Sender 地址 */
22
+ senderAddress: string;
23
+ /** 是否已部署 */
24
+ deployed: boolean;
25
+ /** initCode(未部署时需要) */
26
+ initCode: string;
27
+ /** 预充值金额(wei) */
28
+ prefundWei: bigint;
29
+ /** 预充值状态 */
30
+ prefundStatus: 'pending' | 'funded' | 'refunded' | 'failed';
31
+ /** 预充值交易哈希 */
32
+ prefundTxHash?: string;
33
+ /** 退款交易哈希 */
34
+ refundTxHash?: string;
35
+ }
36
+ /** 预充值进度回调 */
37
+ export interface PrefundProgressCallback {
38
+ (progress: {
39
+ phase: 'generating' | 'prefunding' | 'confirming' | 'ready' | 'cancelled' | 'refunding' | 'refunded' | 'error';
40
+ current: number;
41
+ total: number;
42
+ message: string;
43
+ hopWallets?: HopWalletInfo[];
44
+ error?: Error;
45
+ }): void;
46
+ }
47
+ /** 预充值配置 */
48
+ export interface PrefundConfig {
49
+ /** Hop 数量 */
50
+ hopCount: number;
51
+ /** 每个 Hop 的预估 gas 消耗 */
52
+ hopCallGasLimit?: bigint;
53
+ /** 预留缓冲百分比(默认 150%) */
54
+ bufferPercent?: bigint;
55
+ /** Payer 钱包 */
56
+ payerWallet: Wallet;
57
+ /** 进度回调 */
58
+ onProgress?: PrefundProgressCallback;
59
+ /** 取消信号 */
60
+ abortSignal?: AbortSignal;
61
+ }
62
+ /** 预充值结果 */
63
+ export interface PrefundResult {
64
+ /** 是否成功 */
65
+ success: boolean;
66
+ /** Hop 钱包列表 */
67
+ hopWallets: HopWalletInfo[];
68
+ /** 总预充值金额(wei) */
69
+ totalPrefundWei: bigint;
70
+ /** 预充值交易哈希 */
71
+ prefundTxHash?: string;
72
+ /** 错误信息 */
73
+ error?: Error;
74
+ /** 是否被取消 */
75
+ cancelled?: boolean;
76
+ }
77
+ /** 退款结果 */
78
+ export interface RefundResult {
79
+ /** 是否成功 */
80
+ success: boolean;
81
+ /** 退款总金额(wei) */
82
+ totalRefundWei: bigint;
83
+ /** 退款交易哈希列表 */
84
+ refundTxHashes: string[];
85
+ /** 各 Hop 退款详情 */
86
+ hopRefunds: {
87
+ hopIndex: number;
88
+ refundWei: bigint;
89
+ txHash?: string;
90
+ error?: string;
91
+ }[];
92
+ /** 错误信息 */
93
+ error?: Error;
94
+ }
95
+ export declare class HopWalletManager {
96
+ private aaManager;
97
+ private provider;
98
+ private config;
99
+ constructor(config?: XLayerConfig);
100
+ /**
101
+ * 生成 Hop 钱包列表
102
+ */
103
+ generateHopWallets(count: number): Promise<HopWalletInfo[]>;
104
+ /**
105
+ * 计算单个 Hop 需要的 prefund
106
+ */
107
+ calculateHopPrefund(deployed: boolean, callGasLimit?: bigint): Promise<bigint>;
108
+ /**
109
+ * 预充值 Hop 钱包
110
+ *
111
+ * 使用 Payer 发送一笔批量转账交易,给所有 Hop 钱包充值 prefund
112
+ */
113
+ prefundHopWallets(config: PrefundConfig): Promise<PrefundResult>;
114
+ /**
115
+ * 退回 Hop 钱包的预充值资金
116
+ *
117
+ * 将所有 Hop 钱包的余额转回给指定地址(通常是 Payer)
118
+ */
119
+ refundHopWallets(hopWallets: HopWalletInfo[], refundTo: string, onProgress?: PrefundProgressCallback): Promise<RefundResult>;
120
+ /**
121
+ * 使用 Payer 批量退回 Hop 钱包资金
122
+ *
123
+ * 通过 handleOps 批量执行所有 Hop 的退款操作
124
+ */
125
+ refundHopWalletsViaPayer(hopWallets: HopWalletInfo[], payerWallet: Wallet, refundTo: string, onProgress?: PrefundProgressCallback): Promise<RefundResult>;
126
+ /**
127
+ * 导出 Hop 钱包信息(用于备份)
128
+ */
129
+ exportHopWallets(hopWallets: HopWalletInfo[]): string;
130
+ /**
131
+ * 导入 Hop 钱包信息
132
+ */
133
+ importHopWallets(jsonStr: string): Promise<HopWalletInfo[]>;
134
+ /**
135
+ * 编码 execute 调用
136
+ */
137
+ private encodeExecute;
138
+ /**
139
+ * 获取 AAAccountManager
140
+ */
141
+ getAAManager(): AAAccountManager;
142
+ }
143
+ export declare function createHopWalletManager(config?: XLayerConfig): HopWalletManager;
@@ -0,0 +1,526 @@
1
+ /**
2
+ * XLayer AA 多跳钱包管理器
3
+ *
4
+ * 功能:
5
+ * 1. 生成临时 Hop 钱包(可导出私钥)
6
+ * 2. 预充值 Hop 钱包(支持取消)
7
+ * 3. 退回预充值资金(原路返回)
8
+ * 4. 状态回调(进度通知)
9
+ */
10
+ import { ethers, Wallet } from 'ethers';
11
+ import { AAAccountManager } from './aa-account.js';
12
+ import { VERIFICATION_GAS_LIMIT_DEPLOY, VERIFICATION_GAS_LIMIT_NORMAL, PRE_VERIFICATION_GAS, } from './constants.js';
13
+ // ============================================================================
14
+ // HopWalletManager
15
+ // ============================================================================
16
+ const DEFAULT_HOP_CALL_GAS_LIMIT = 150000n;
17
+ const DEFAULT_BUFFER_PERCENT = 150n; // 1.5x
18
+ export class HopWalletManager {
19
+ constructor(config = {}) {
20
+ this.config = config;
21
+ this.aaManager = new AAAccountManager(config);
22
+ this.provider = this.aaManager.getProvider();
23
+ }
24
+ /**
25
+ * 生成 Hop 钱包列表
26
+ */
27
+ async generateHopWallets(count) {
28
+ const hopWallets = [];
29
+ for (let i = 0; i < count; i++) {
30
+ const randomWallet = ethers.Wallet.createRandom();
31
+ const wallet = new Wallet(randomWallet.privateKey);
32
+ const ownerAddress = wallet.address;
33
+ const senderAddress = await this.aaManager.predictSenderAddress(ownerAddress);
34
+ const code = await this.provider.getCode(senderAddress);
35
+ const deployed = code !== null && code !== '0x';
36
+ const initCode = deployed ? '0x' : this.aaManager.generateInitCode(ownerAddress);
37
+ hopWallets.push({
38
+ index: i,
39
+ privateKey: randomWallet.privateKey,
40
+ ownerAddress,
41
+ senderAddress,
42
+ deployed,
43
+ initCode,
44
+ prefundWei: 0n,
45
+ prefundStatus: 'pending',
46
+ });
47
+ }
48
+ return hopWallets;
49
+ }
50
+ /**
51
+ * 计算单个 Hop 需要的 prefund
52
+ */
53
+ async calculateHopPrefund(deployed, callGasLimit) {
54
+ const feeData = await this.aaManager.getFeeData();
55
+ const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
56
+ const callGas = callGasLimit ?? DEFAULT_HOP_CALL_GAS_LIMIT;
57
+ const verifyGas = deployed ? VERIFICATION_GAS_LIMIT_NORMAL : VERIFICATION_GAS_LIMIT_DEPLOY;
58
+ const preVerifyGas = PRE_VERIFICATION_GAS;
59
+ const totalGas = callGas + verifyGas + preVerifyGas;
60
+ return (totalGas * gasPrice * DEFAULT_BUFFER_PERCENT) / 100n;
61
+ }
62
+ /**
63
+ * 预充值 Hop 钱包
64
+ *
65
+ * 使用 Payer 发送一笔批量转账交易,给所有 Hop 钱包充值 prefund
66
+ */
67
+ async prefundHopWallets(config) {
68
+ const { hopCount, hopCallGasLimit, bufferPercent, payerWallet, onProgress, abortSignal } = config;
69
+ // 检查取消信号
70
+ const checkAbort = () => {
71
+ if (abortSignal?.aborted) {
72
+ throw new Error('PREFUND_CANCELLED');
73
+ }
74
+ };
75
+ try {
76
+ // 1. 生成 Hop 钱包
77
+ onProgress?.({
78
+ phase: 'generating',
79
+ current: 0,
80
+ total: hopCount,
81
+ message: `正在生成 ${hopCount} 个 Hop 钱包...`,
82
+ });
83
+ checkAbort();
84
+ const hopWallets = await this.generateHopWallets(hopCount);
85
+ onProgress?.({
86
+ phase: 'generating',
87
+ current: hopCount,
88
+ total: hopCount,
89
+ message: `已生成 ${hopCount} 个 Hop 钱包`,
90
+ hopWallets,
91
+ });
92
+ // 2. 计算每个 Hop 需要的 prefund
93
+ checkAbort();
94
+ const feeData = await this.aaManager.getFeeData();
95
+ const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
96
+ const effectiveBufferPercent = bufferPercent ?? DEFAULT_BUFFER_PERCENT;
97
+ for (const hop of hopWallets) {
98
+ const callGas = hopCallGasLimit ?? DEFAULT_HOP_CALL_GAS_LIMIT;
99
+ const verifyGas = hop.deployed ? VERIFICATION_GAS_LIMIT_NORMAL : VERIFICATION_GAS_LIMIT_DEPLOY;
100
+ const preVerifyGas = PRE_VERIFICATION_GAS;
101
+ const totalGas = callGas + verifyGas + preVerifyGas;
102
+ hop.prefundWei = (totalGas * gasPrice * effectiveBufferPercent) / 100n;
103
+ }
104
+ const totalPrefundWei = hopWallets.reduce((sum, h) => sum + h.prefundWei, 0n);
105
+ // 3. 检查 Payer 余额
106
+ const payerAddress = payerWallet.address;
107
+ const payerBalance = await this.provider.getBalance(payerAddress);
108
+ const estimatedGas = 21000n * BigInt(hopCount) + 50000n; // 预估 gas
109
+ const totalNeeded = totalPrefundWei + estimatedGas * gasPrice;
110
+ if (payerBalance < totalNeeded) {
111
+ throw new Error(`Payer 余额不足: 需要 ${ethers.formatEther(totalNeeded)} OKB, 当前 ${ethers.formatEther(payerBalance)} OKB`);
112
+ }
113
+ // 4. 发送预充值交易
114
+ onProgress?.({
115
+ phase: 'prefunding',
116
+ current: 0,
117
+ total: hopCount,
118
+ message: `正在发送预充值交易...`,
119
+ hopWallets,
120
+ });
121
+ checkAbort();
122
+ // 使用批量转账(逐笔发送,XLayer 不支持 Bundle)
123
+ const connectedPayer = payerWallet.connect(this.provider);
124
+ const nonce = await this.provider.getTransactionCount(payerAddress, 'pending');
125
+ const txHashes = [];
126
+ for (let i = 0; i < hopWallets.length; i++) {
127
+ checkAbort();
128
+ const hop = hopWallets[i];
129
+ const tx = await connectedPayer.sendTransaction({
130
+ to: hop.senderAddress,
131
+ value: hop.prefundWei,
132
+ nonce: nonce + i,
133
+ gasPrice,
134
+ gasLimit: 21055n,
135
+ });
136
+ hop.prefundTxHash = tx.hash;
137
+ txHashes.push(tx.hash);
138
+ onProgress?.({
139
+ phase: 'prefunding',
140
+ current: i + 1,
141
+ total: hopCount,
142
+ message: `已发送 ${i + 1}/${hopCount} 笔预充值交易`,
143
+ hopWallets,
144
+ });
145
+ }
146
+ // 5. 等待所有交易确认
147
+ onProgress?.({
148
+ phase: 'confirming',
149
+ current: 0,
150
+ total: hopCount,
151
+ message: `正在等待交易确认...`,
152
+ hopWallets,
153
+ });
154
+ for (let i = 0; i < txHashes.length; i++) {
155
+ checkAbort();
156
+ const receipt = await this.provider.waitForTransaction(txHashes[i], 1, 60000);
157
+ if (receipt?.status === 1) {
158
+ hopWallets[i].prefundStatus = 'funded';
159
+ }
160
+ else {
161
+ hopWallets[i].prefundStatus = 'failed';
162
+ }
163
+ onProgress?.({
164
+ phase: 'confirming',
165
+ current: i + 1,
166
+ total: hopCount,
167
+ message: `已确认 ${i + 1}/${hopCount} 笔交易`,
168
+ hopWallets,
169
+ });
170
+ }
171
+ // 检查是否全部成功
172
+ const allFunded = hopWallets.every(h => h.prefundStatus === 'funded');
173
+ if (!allFunded) {
174
+ const failedCount = hopWallets.filter(h => h.prefundStatus === 'failed').length;
175
+ throw new Error(`${failedCount} 笔预充值交易失败`);
176
+ }
177
+ onProgress?.({
178
+ phase: 'ready',
179
+ current: hopCount,
180
+ total: hopCount,
181
+ message: `✅ 预充值完成,共 ${ethers.formatEther(totalPrefundWei)} OKB`,
182
+ hopWallets,
183
+ });
184
+ return {
185
+ success: true,
186
+ hopWallets,
187
+ totalPrefundWei,
188
+ prefundTxHash: txHashes[0],
189
+ };
190
+ }
191
+ catch (error) {
192
+ const err = error;
193
+ if (err.message === 'PREFUND_CANCELLED') {
194
+ onProgress?.({
195
+ phase: 'cancelled',
196
+ current: 0,
197
+ total: hopCount,
198
+ message: '预充值已取消',
199
+ });
200
+ return {
201
+ success: false,
202
+ hopWallets: [],
203
+ totalPrefundWei: 0n,
204
+ cancelled: true,
205
+ };
206
+ }
207
+ onProgress?.({
208
+ phase: 'error',
209
+ current: 0,
210
+ total: hopCount,
211
+ message: `预充值失败: ${err.message}`,
212
+ error: err,
213
+ });
214
+ return {
215
+ success: false,
216
+ hopWallets: [],
217
+ totalPrefundWei: 0n,
218
+ error: err,
219
+ };
220
+ }
221
+ }
222
+ /**
223
+ * 退回 Hop 钱包的预充值资金
224
+ *
225
+ * 将所有 Hop 钱包的余额转回给指定地址(通常是 Payer)
226
+ */
227
+ async refundHopWallets(hopWallets, refundTo, onProgress) {
228
+ const hopRefunds = [];
229
+ const refundTxHashes = [];
230
+ let totalRefundWei = 0n;
231
+ onProgress?.({
232
+ phase: 'refunding',
233
+ current: 0,
234
+ total: hopWallets.length,
235
+ message: `正在退回 ${hopWallets.length} 个 Hop 钱包的资金...`,
236
+ hopWallets,
237
+ });
238
+ const feeData = await this.aaManager.getFeeData();
239
+ const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
240
+ const gasLimit = 21055n;
241
+ const gasCost = gasLimit * gasPrice;
242
+ for (let i = 0; i < hopWallets.length; i++) {
243
+ const hop = hopWallets[i];
244
+ try {
245
+ // 检查 Hop sender 余额
246
+ const balance = await this.provider.getBalance(hop.senderAddress);
247
+ if (balance <= gasCost) {
248
+ // 余额不足以支付 gas,跳过
249
+ hopRefunds.push({
250
+ hopIndex: hop.index,
251
+ refundWei: 0n,
252
+ error: '余额不足以支付 gas',
253
+ });
254
+ continue;
255
+ }
256
+ const refundAmount = balance - gasCost;
257
+ // 使用 AA 账户转账(通过 UserOp)
258
+ const hopWallet = new Wallet(hop.privateKey, this.provider);
259
+ // 构建简单的 native 转账 UserOp
260
+ const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
261
+ ownerWallet: hopWallet,
262
+ sender: hop.senderAddress,
263
+ nonce: 0n,
264
+ initCode: hop.initCode,
265
+ callData: this.encodeExecute(refundTo, refundAmount, '0x'),
266
+ deployed: hop.deployed,
267
+ });
268
+ // 签名(buildUserOpWithFixedGas 不签名,需要手动签名)
269
+ const signedOp = await this.aaManager.signUserOp(userOp, hopWallet);
270
+ // 发送 handleOps
271
+ const entryPoint = this.aaManager.getEntryPoint();
272
+ const connectedEntryPoint = entryPoint.connect(new Wallet(hop.privateKey, this.provider));
273
+ // 由于 Hop 没有足够的 gas 发送 handleOps,需要用 Payer 来发送
274
+ // 这里我们返回 UserOp,让调用者处理
275
+ hopRefunds.push({
276
+ hopIndex: hop.index,
277
+ refundWei: refundAmount,
278
+ error: '需要外部 Payer 发送 handleOps(Hop 余额仅够 prefund)',
279
+ });
280
+ totalRefundWei += refundAmount;
281
+ }
282
+ catch (error) {
283
+ hopRefunds.push({
284
+ hopIndex: hop.index,
285
+ refundWei: 0n,
286
+ error: error.message,
287
+ });
288
+ }
289
+ onProgress?.({
290
+ phase: 'refunding',
291
+ current: i + 1,
292
+ total: hopWallets.length,
293
+ message: `已处理 ${i + 1}/${hopWallets.length} 个 Hop 钱包`,
294
+ hopWallets,
295
+ });
296
+ }
297
+ onProgress?.({
298
+ phase: 'refunded',
299
+ current: hopWallets.length,
300
+ total: hopWallets.length,
301
+ message: `退款完成,共 ${ethers.formatEther(totalRefundWei)} OKB`,
302
+ hopWallets,
303
+ });
304
+ return {
305
+ success: true,
306
+ totalRefundWei,
307
+ refundTxHashes,
308
+ hopRefunds,
309
+ };
310
+ }
311
+ /**
312
+ * 使用 Payer 批量退回 Hop 钱包资金
313
+ *
314
+ * 通过 handleOps 批量执行所有 Hop 的退款操作
315
+ */
316
+ async refundHopWalletsViaPayer(hopWallets, payerWallet, refundTo, onProgress) {
317
+ const hopRefunds = [];
318
+ const refundTxHashes = [];
319
+ let totalRefundWei = 0n;
320
+ onProgress?.({
321
+ phase: 'refunding',
322
+ current: 0,
323
+ total: hopWallets.length,
324
+ message: `正在构建退款 UserOps...`,
325
+ hopWallets,
326
+ });
327
+ const feeData = await this.aaManager.getFeeData();
328
+ const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
329
+ // 构建所有退款 UserOps
330
+ const userOps = [];
331
+ const hopWalletsToRefund = [];
332
+ // ✅ 关键修复:从链上批量获取所有 hop 钱包的真实 nonce
333
+ const hopSenders = hopWallets.map(h => h.senderAddress);
334
+ const hopNonces = await this.aaManager.batchGetNonces(hopSenders);
335
+ for (let i = 0; i < hopWallets.length; i++) {
336
+ const hop = hopWallets[i];
337
+ try {
338
+ const balance = await this.provider.getBalance(hop.senderAddress);
339
+ // 估算 UserOp 执行需要的 gas
340
+ const estimatedGas = 150000n * gasPrice;
341
+ if (balance <= estimatedGas) {
342
+ hopRefunds.push({
343
+ hopIndex: hop.index,
344
+ refundWei: 0n,
345
+ error: '余额不足',
346
+ });
347
+ continue;
348
+ }
349
+ const refundAmount = balance - estimatedGas;
350
+ // 构建 UserOp
351
+ const hopWallet = new Wallet(hop.privateKey, this.provider);
352
+ // ✅ 使用从链上获取的真实 nonce
353
+ const realNonce = hopNonces[i] ?? 0n;
354
+ // ✅ 检查 hop 钱包是否已部署(根据 nonce 判断,nonce > 0 说明已用过,肯定已部署)
355
+ const isDeployed = realNonce > 0n || hop.deployed;
356
+ const initCode = isDeployed ? '0x' : hop.initCode;
357
+ const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
358
+ ownerWallet: hopWallet,
359
+ sender: hop.senderAddress,
360
+ nonce: realNonce,
361
+ initCode,
362
+ callData: this.encodeExecute(refundTo, refundAmount, '0x'),
363
+ deployed: isDeployed,
364
+ });
365
+ // 签名
366
+ const signedOp = await this.aaManager.signUserOp(userOp, hopWallet);
367
+ userOps.push(signedOp.userOp);
368
+ hopWalletsToRefund.push(hop);
369
+ totalRefundWei += refundAmount;
370
+ hopRefunds.push({
371
+ hopIndex: hop.index,
372
+ refundWei: refundAmount,
373
+ });
374
+ }
375
+ catch (error) {
376
+ hopRefunds.push({
377
+ hopIndex: hop.index,
378
+ refundWei: 0n,
379
+ error: error.message,
380
+ });
381
+ }
382
+ }
383
+ if (userOps.length === 0) {
384
+ onProgress?.({
385
+ phase: 'refunded',
386
+ current: hopWallets.length,
387
+ total: hopWallets.length,
388
+ message: '没有可退款的 Hop 钱包',
389
+ hopWallets,
390
+ });
391
+ return {
392
+ success: true,
393
+ totalRefundWei: 0n,
394
+ refundTxHashes: [],
395
+ hopRefunds,
396
+ };
397
+ }
398
+ onProgress?.({
399
+ phase: 'refunding',
400
+ current: 0,
401
+ total: userOps.length,
402
+ message: `正在发送 handleOps 退款交易 (${userOps.length} 个 UserOps)...`,
403
+ hopWallets,
404
+ });
405
+ // 发送 handleOps
406
+ try {
407
+ const connectedPayer = payerWallet.connect(this.provider);
408
+ const entryPoint = this.aaManager.getEntryPoint();
409
+ const entryPointWithSigner = entryPoint.connect(connectedPayer);
410
+ const tx = await entryPointWithSigner.handleOps(userOps, payerWallet.address, {
411
+ gasPrice,
412
+ gasLimit: 500000n * BigInt(userOps.length),
413
+ });
414
+ refundTxHashes.push(tx.hash);
415
+ onProgress?.({
416
+ phase: 'confirming',
417
+ current: 0,
418
+ total: 1,
419
+ message: `等待 handleOps 交易确认...`,
420
+ hopWallets,
421
+ });
422
+ const receipt = await tx.wait();
423
+ if (receipt?.status === 1) {
424
+ for (const hop of hopWalletsToRefund) {
425
+ hop.prefundStatus = 'refunded';
426
+ hop.refundTxHash = tx.hash;
427
+ }
428
+ onProgress?.({
429
+ phase: 'refunded',
430
+ current: hopWallets.length,
431
+ total: hopWallets.length,
432
+ message: `✅ 退款完成,共 ${ethers.formatEther(totalRefundWei)} OKB`,
433
+ hopWallets,
434
+ });
435
+ return {
436
+ success: true,
437
+ totalRefundWei,
438
+ refundTxHashes,
439
+ hopRefunds,
440
+ };
441
+ }
442
+ else {
443
+ throw new Error('handleOps 交易失败');
444
+ }
445
+ }
446
+ catch (error) {
447
+ onProgress?.({
448
+ phase: 'error',
449
+ current: 0,
450
+ total: hopWallets.length,
451
+ message: `退款失败: ${error.message}`,
452
+ error: error,
453
+ hopWallets,
454
+ });
455
+ return {
456
+ success: false,
457
+ totalRefundWei: 0n,
458
+ refundTxHashes,
459
+ hopRefunds,
460
+ error: error,
461
+ };
462
+ }
463
+ }
464
+ /**
465
+ * 导出 Hop 钱包信息(用于备份)
466
+ */
467
+ exportHopWallets(hopWallets) {
468
+ const exportData = hopWallets.map(h => ({
469
+ index: h.index,
470
+ privateKey: h.privateKey,
471
+ ownerAddress: h.ownerAddress,
472
+ senderAddress: h.senderAddress,
473
+ prefundWei: h.prefundWei.toString(),
474
+ prefundStatus: h.prefundStatus,
475
+ prefundTxHash: h.prefundTxHash,
476
+ }));
477
+ return JSON.stringify(exportData, null, 2);
478
+ }
479
+ /**
480
+ * 导入 Hop 钱包信息
481
+ */
482
+ async importHopWallets(jsonStr) {
483
+ const data = JSON.parse(jsonStr);
484
+ const hopWallets = [];
485
+ for (const item of data) {
486
+ const wallet = new Wallet(item.privateKey);
487
+ const senderAddress = await this.aaManager.predictSenderAddress(wallet.address);
488
+ const code = await this.provider.getCode(senderAddress);
489
+ const deployed = code !== null && code !== '0x';
490
+ const initCode = deployed ? '0x' : this.aaManager.generateInitCode(wallet.address);
491
+ hopWallets.push({
492
+ index: item.index,
493
+ privateKey: item.privateKey,
494
+ ownerAddress: item.ownerAddress,
495
+ senderAddress,
496
+ deployed,
497
+ initCode,
498
+ prefundWei: BigInt(item.prefundWei || '0'),
499
+ prefundStatus: item.prefundStatus || 'pending',
500
+ prefundTxHash: item.prefundTxHash,
501
+ });
502
+ }
503
+ return hopWallets;
504
+ }
505
+ /**
506
+ * 编码 execute 调用
507
+ */
508
+ encodeExecute(to, value, data) {
509
+ const iface = new ethers.Interface([
510
+ 'function execute(address dest, uint256 value, bytes func) external',
511
+ ]);
512
+ return iface.encodeFunctionData('execute', [to, value, data]);
513
+ }
514
+ /**
515
+ * 获取 AAAccountManager
516
+ */
517
+ getAAManager() {
518
+ return this.aaManager;
519
+ }
520
+ }
521
+ // ============================================================================
522
+ // 工厂函数
523
+ // ============================================================================
524
+ export function createHopWalletManager(config = {}) {
525
+ return new HopWalletManager(config);
526
+ }
@@ -66,6 +66,7 @@ export { BundleExecutor, createBundleExecutor, bundleBuy, bundleSell, bundleBuyS
66
66
  export { DexBundleExecutor, createDexBundleExecutor, dexBundleBuySell, type DexBundleConfig, type DexBundleBuySellParams, } from './dex-bundle.js';
67
67
  export { AAPortalSwapExecutor, } from './portal-bundle-swap.js';
68
68
  export { AADexSwapExecutor, } from './dex-bundle-swap.js';
69
+ export { HopWalletManager, createHopWalletManager, type HopWalletInfo, type PrefundProgressCallback, type PrefundConfig, type PrefundResult, type RefundResult, } from './hop-wallet-manager.js';
69
70
  export { AAPortalBuyFirstExecutor, createAAPortalBuyFirstExecutor, } from './portal-buy-first.js';
70
71
  export { AADexBuyFirstExecutor, createAADexBuyFirstExecutor, } from './dex-buy-first.js';
71
72
  export { BuyFirstVolumeExecutor, createBuyFirstVolumeExecutor, makeBuyFirstVolume, } from './buy-first-volume.js';
@@ -94,6 +94,10 @@ export { DexBundleExecutor, createDexBundleExecutor, dexBundleBuySell, } from '.
94
94
  export { AAPortalSwapExecutor, } from './portal-bundle-swap.js';
95
95
  export { AADexSwapExecutor, } from './dex-bundle-swap.js';
96
96
  // ============================================================================
97
+ // Hop 钱包管理器(多跳预充值)
98
+ // ============================================================================
99
+ export { HopWalletManager, createHopWalletManager, } from './hop-wallet-manager.js';
100
+ // ============================================================================
97
101
  // AA Buy-First(刷量专用)
98
102
  // ============================================================================
99
103
  export { AAPortalBuyFirstExecutor, createAAPortalBuyFirstExecutor, } from './portal-buy-first.js';
@@ -277,7 +277,8 @@ export class AAPortalSwapExecutor {
277
277
  */
278
278
  async bundleBatchSwapSign(params) {
279
279
  const { tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, capitalMode = true, // ✅ 默认资金利用率模式(卖出→分发→买入)
280
- disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, quoteToken, quoteTokenDecimals = 6, } = params;
280
+ disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, quoteToken, quoteTokenDecimals = 6, prefundedHopWallets, // ✅ 预充值多跳:已预充值的 hop 钱包列表
281
+ } = params;
281
282
  const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
282
283
  const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
283
284
  // ✅ ERC20 稳定币支持
@@ -390,17 +391,23 @@ export class AAPortalSwapExecutor {
390
391
  extractProfit,
391
392
  profitWei: ethers.formatEther(profitWei),
392
393
  });
393
- // ✅ 多跳模式与 Paymaster 的关系:
394
+ // ✅ 多跳模式判断:
394
395
  // ERC-4337 的 handleOps 执行流程是"先验证所有 UserOps,再执行所有 UserOps"
395
396
  // hop 钱包是临时生成的(余额=0),在 validation 阶段无法支付 prefund,会导致 AA21 错误
396
397
  //
397
- // 但是!如果配置了 Paymaster,prefund = 0n,hop 钱包不需要余额即可通过验证
398
- // 因此:只有在 **没有 Paymaster** 时才需要强制禁用多跳
398
+ // 多跳可用的条件:
399
+ // 1. 配置了 Paymaster(prefund = 0n,hop 钱包不需要余额)
400
+ // 2. 传入了预充值好的 hop 钱包(hop 钱包已有余额支付 prefund)
399
401
  const hasPaymaster = this.aaManager.hasPaymaster();
400
- const effectiveHopCount = hasPaymaster ? hopCount : 0;
401
- if (hopCount > 0 && !hasPaymaster) {
402
- console.warn('[AA Portal 批量换手] ⚠️ Paymaster 模式不支持多跳(hop 钱包无法支付 prefund),已自动降级为直接分发模式');
403
- console.warn('[AA Portal 批量换手] 💡 提示:配置 Paymaster 可启用多跳功能');
402
+ const hasPrefundedHops = Array.isArray(prefundedHopWallets) && prefundedHopWallets.length > 0;
403
+ const canDoMultiHop = hasPaymaster || hasPrefundedHops;
404
+ const effectiveHopCount = canDoMultiHop ? hopCount : 0;
405
+ if (hopCount > 0 && !canDoMultiHop) {
406
+ console.warn('[AA Portal 批量换手] ⚠️ 无法执行多跳(hop 钱包无法支付 prefund),已自动降级为直接分发模式');
407
+ console.warn('[AA Portal 批量换手] 💡 提示:使用 HopWalletManager.prefundHopWallets() 预充值 hop 钱包,或配置 Paymaster');
408
+ }
409
+ else if (hopCount > 0 && hasPrefundedHops) {
410
+ console.log('[AA Portal 批量换手] ✅ 预充值多跳模式已启用 (hopCount:', hopCount, ', prefundedHops:', prefundedHopWallets.length, ')');
404
411
  }
405
412
  else if (hopCount > 0 && hasPaymaster) {
406
413
  console.log('[AA Portal 批量换手] ✅ Paymaster 模式已启用,支持多跳换手 (hopCount:', hopCount, ')');
@@ -462,9 +469,13 @@ export class AAPortalSwapExecutor {
462
469
  }
463
470
  }
464
471
  else {
465
- // ⚠️ 多跳模式在 XLayer AA 下不可行(effectiveHopCount 已强制为 0)
466
- // 以下代码保留但不会执行,仅供参考
467
- console.log('[AA Portal 批量换手] 进入多跳模式 (effectiveHopCount > 0):', { effectiveHopCount, buyerCount: buyerSenders.length });
472
+ // 多跳模式:使用预充值的 hop 钱包或 Paymaster 模式
473
+ console.log('[AA Portal 批量换手] 进入多跳模式 (effectiveHopCount > 0):', {
474
+ effectiveHopCount,
475
+ buyerCount: buyerSenders.length,
476
+ usePrefundedHops: hasPrefundedHops,
477
+ usePaymaster: hasPaymaster,
478
+ });
468
479
  const feeData = await this.aaManager.getFeeData();
469
480
  const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
470
481
  const provider = this.aaManager.getProvider();
@@ -475,25 +486,57 @@ export class AAPortalSwapExecutor {
475
486
  const totalGas = callGas + verifyGas + preVerifyGas;
476
487
  return (totalGas * gasPrice * PREFUND_BUFFER_PERCENT) / 100n;
477
488
  };
478
- // ✅ 为每个买方生成独立的多跳链
479
- // hopChains[buyerIdx] = [hop0, hop1, ..., hop(H-1)] 每条链有 hopCount 个 hop 钱包
489
+ // ✅ 构建 hop 钱包列表
490
+ // 如果有预充值的 hop 钱包,使用它们;否则生成新钱包(需要 Paymaster)
480
491
  const allGeneratedHopWallets = [];
481
- for (let buyerIdx = 0; buyerIdx < buyerSenders.length; buyerIdx++) {
482
- const chainHops = [];
483
- for (let h = 0; h < hopCount; h++) {
484
- const randomWallet = ethers.Wallet.createRandom();
485
- const wallet = new ethers.Wallet(randomWallet.privateKey);
486
- const sender = await this.aaManager.predictSenderAddress(wallet.address);
487
- const code = await provider.getCode(sender);
488
- const deployed = code !== null && code !== '0x';
489
- const initCode = deployed ? '0x' : this.aaManager.generateInitCode(wallet.address);
490
- chainHops.push({ wallet, sender, deployed, initCode });
491
- // 初始化 nonce
492
- nonceMap.init(sender, 0n);
492
+ if (hasPrefundedHops) {
493
+ // 预充值模式:使用传入的 hop 钱包
494
+ const totalHopsNeeded = buyerSenders.length * effectiveHopCount;
495
+ if (prefundedHopWallets.length < totalHopsNeeded) {
496
+ throw new Error(`预充值 hop 钱包数量不足: 需要 ${totalHopsNeeded} 个,实际 ${prefundedHopWallets.length} 个`);
497
+ }
498
+ // 关键修复:从链上获取所有 hop 钱包的真实 nonce(用户可能重复使用同一批 hop 钱包)
499
+ const hopSenders = prefundedHopWallets.slice(0, totalHopsNeeded).map(h => h.senderAddress);
500
+ const hopNonces = await this.aaManager.batchGetNonces(hopSenders);
501
+ let hopIdx = 0;
502
+ for (let buyerIdx = 0; buyerIdx < buyerSenders.length; buyerIdx++) {
503
+ const chainHops = [];
504
+ for (let h = 0; h < effectiveHopCount; h++) {
505
+ const prefundedHop = prefundedHopWallets[hopIdx];
506
+ const wallet = new ethers.Wallet(prefundedHop.privateKey, provider);
507
+ chainHops.push({
508
+ wallet,
509
+ sender: prefundedHop.senderAddress,
510
+ deployed: prefundedHop.deployed,
511
+ initCode: prefundedHop.initCode,
512
+ });
513
+ // ✅ 使用从链上获取的真实 nonce(而非硬编码 0)
514
+ const realNonce = hopNonces[hopIdx] ?? 0n;
515
+ nonceMap.init(prefundedHop.senderAddress, realNonce);
516
+ hopIdx++;
517
+ }
518
+ allGeneratedHopWallets.push(chainHops);
519
+ }
520
+ console.log(`[AA Portal 多跳] ✅ 使用预充值 hop 钱包: ${totalHopsNeeded} 个`);
521
+ }
522
+ else {
523
+ // Paymaster 模式:生成新钱包
524
+ for (let buyerIdx = 0; buyerIdx < buyerSenders.length; buyerIdx++) {
525
+ const chainHops = [];
526
+ for (let h = 0; h < effectiveHopCount; h++) {
527
+ const randomWallet = ethers.Wallet.createRandom();
528
+ const wallet = new ethers.Wallet(randomWallet.privateKey, provider);
529
+ const sender = await this.aaManager.predictSenderAddress(wallet.address);
530
+ const code = await provider.getCode(sender);
531
+ const deployed = code !== null && code !== '0x';
532
+ const initCode = deployed ? '0x' : this.aaManager.generateInitCode(wallet.address);
533
+ chainHops.push({ wallet, sender, deployed, initCode });
534
+ nonceMap.init(sender, 0n);
535
+ }
536
+ allGeneratedHopWallets.push(chainHops);
493
537
  }
494
- allGeneratedHopWallets.push(chainHops);
538
+ console.log(`[AA Portal 多跳] 使用 Paymaster 模式生成 hop 钱包`);
495
539
  }
496
- console.log(`[AA 多跳] 总 hop 钱包数: ${buyerSenders.length * hopCount} (${buyerSenders.length} 条链 × ${hopCount} 跳)`);
497
540
  // ✅ 利润刮取(只在第一笔 seller UserOp 中执行)
498
541
  let profitHandled = false;
499
542
  // ✅ 构建每条多跳链的 UserOps
@@ -552,6 +552,24 @@ export interface BundleBatchSwapSignParams extends BundleBatchSwapParams {
552
552
  payerStartNonce?: number;
553
553
  handleOpsGasLimit?: bigint;
554
554
  routeAddress?: string;
555
+ /**
556
+ * ✅ 预充值多跳:传入已预充值好的 Hop 钱包列表
557
+ *
558
+ * 如果提供此参数,将使用这些 hop 钱包进行多跳转账,
559
+ * 而不是自动生成新钱包(需要 Paymaster 才能工作的模式)
560
+ *
561
+ * 使用流程:
562
+ * 1. 调用 HopWalletManager.prefundHopWallets() 预充值
563
+ * 2. 将返回的 hopWallets 传入此参数
564
+ * 3. 多跳将使用这些已有余额的 hop 钱包
565
+ */
566
+ prefundedHopWallets?: Array<{
567
+ privateKey: string;
568
+ ownerAddress: string;
569
+ senderAddress: string;
570
+ deployed: boolean;
571
+ initCode: string;
572
+ }>;
555
573
  }
556
574
  export interface BundleBatchSwapSignResult {
557
575
  signedTransactions: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.6.35",
3
+ "version": "1.6.37",
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
- }