four-flap-meme-sdk 1.6.52 → 1.6.53
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.
|
@@ -619,16 +619,20 @@ export class AADexSwapExecutor {
|
|
|
619
619
|
const hasPrefundedHops = Array.isArray(prefundedHopWallets) && prefundedHopWallets.length > 0;
|
|
620
620
|
const canDoMultiHop = hasPaymaster || hasPrefundedHops;
|
|
621
621
|
const effectiveHopCount = canDoMultiHop ? hopCount : 0;
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
622
|
+
// ✅ 只有 hopCount > 0 时才输出多跳相关日志;hopCount=0 时完全静默
|
|
623
|
+
if (hopCount > 0) {
|
|
624
|
+
if (!canDoMultiHop) {
|
|
625
|
+
console.warn('[AA DEX 批量换手] ⚠️ 无法执行多跳(hop 钱包无法支付 prefund),已自动降级为直接分发模式');
|
|
626
|
+
console.warn('[AA DEX 批量换手] 💡 提示:使用 HopWalletManager.prefundHopWallets() 预充值 hop 钱包,或配置 Paymaster');
|
|
627
|
+
}
|
|
628
|
+
else if (hasPrefundedHops) {
|
|
629
|
+
console.log('[AA DEX 批量换手] ✅ 预充值多跳模式已启用 (hopCount:', hopCount, ', prefundedHops:', prefundedHopWallets.length, ')');
|
|
630
|
+
}
|
|
631
|
+
else if (hasPaymaster) {
|
|
632
|
+
console.log('[AA DEX 批量换手] ✅ Paymaster 模式已启用,支持多跳换手 (hopCount:', hopCount, ')');
|
|
633
|
+
}
|
|
631
634
|
}
|
|
635
|
+
// hopCount=0 时不输出任何多跳相关日志
|
|
632
636
|
if (capitalMode && buyerSenders.length > 0 && totalBuyWei > 0n) {
|
|
633
637
|
if (effectiveHopCount <= 0) {
|
|
634
638
|
console.log('[AA DEX 批量换手] 进入直接分发模式 (effectiveHopCount <= 0)');
|
|
@@ -649,6 +653,32 @@ export class AADexSwapExecutor {
|
|
|
649
653
|
const buyerPrefunds = buyerAis.map(ai => estimateBuyerPrefund(ai.deployed));
|
|
650
654
|
const totalBuyerPrefund = buyerPrefunds.reduce((a, b) => a + b, 0n);
|
|
651
655
|
console.log(`[AA DEX 直接分发] 买方数量: ${buyerSenders.length}, 总 prefund 预估: ${ethers.formatEther(totalBuyerPrefund)} OKB`);
|
|
656
|
+
// ✅ 关键修复:检查 buyer 钱包是否有足够的 OKB 支付 prefund
|
|
657
|
+
// ERC-4337 handleOps 执行顺序:先验证所有 UserOps(需要 prefund),再执行所有 UserOps
|
|
658
|
+
// 在 Validation 阶段,Disperse UserOp 还没执行,所以 Buyer 还没收到资金
|
|
659
|
+
// 因此 Buyer 必须预先持有足够的 OKB 来支付 prefund
|
|
660
|
+
const buyerBalances = await Promise.all(buyerSenders.map(s => provider.getBalance(s)));
|
|
661
|
+
const insufficientBuyers = [];
|
|
662
|
+
const sufficientBuyers = [];
|
|
663
|
+
for (let i = 0; i < buyerSenders.length; i++) {
|
|
664
|
+
const prefundNeeded = buyerPrefunds[i];
|
|
665
|
+
const balance = buyerBalances[i];
|
|
666
|
+
if (balance < prefundNeeded) {
|
|
667
|
+
insufficientBuyers.push(`${buyerSenders[i]} (余额: ${ethers.formatEther(balance)} OKB, 需要: ${ethers.formatEther(prefundNeeded)} OKB, 缺口: ${ethers.formatEther(prefundNeeded - balance)} OKB)`);
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
sufficientBuyers.push(buyerSenders[i]);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (insufficientBuyers.length > 0) {
|
|
674
|
+
console.error('[AA DEX 直接分发] ❌ 以下 buyer 钱包 OKB 余额不足以支付 prefund:', insufficientBuyers);
|
|
675
|
+
console.error('[AA DEX 直接分发] ⚠️ ERC-4337 限制:handleOps 在 Validation 阶段就需要 prefund,此时分发还没执行');
|
|
676
|
+
console.error('[AA DEX 直接分发] 💡 解决方案:请先给这些 buyer 钱包充值少量 OKB (约 0.001-0.002 OKB/钱包) 用于支付 gas 费用');
|
|
677
|
+
throw new Error(`${insufficientBuyers.length}/${buyerSenders.length} 个 Buyer 钱包 OKB 余额不足!` +
|
|
678
|
+
`ERC-4337 限制:所有 UserOps 在 Validation 阶段就需要支付 prefund(gas 费用),此时卖出和分发还没执行。` +
|
|
679
|
+
`请先给 buyer 钱包充值少量 OKB (约 0.001-0.002 OKB/钱包)。`);
|
|
680
|
+
}
|
|
681
|
+
console.log('[AA DEX 直接分发] ✅ Buyer 钱包 prefund 检查通过,所有钱包余额充足');
|
|
652
682
|
// ✅ 直接分发模式:
|
|
653
683
|
// - 原生代币模式:分发金额 = 买入金额 + 买方 prefund
|
|
654
684
|
// - ERC20 模式:只分发 ERC20 代币,prefund 需要 buyer 自己有 OKB
|
|
@@ -996,14 +1026,21 @@ export class AADexSwapExecutor {
|
|
|
996
1026
|
const swapData = encodeSwapExactTokensForTokensSupportingFee(buyWei, 0n, [quoteToken, tokenAddress], ai.sender, deadline);
|
|
997
1027
|
buyCallData = encodeExecuteBatch([quoteToken, effectiveRouter], [0n, 0n], [approveData, swapData]);
|
|
998
1028
|
}
|
|
999
|
-
|
|
1029
|
+
// ✅ 关键修复:使用 buildUserOpWithFixedGas,确保 callGasLimit 与预检查时一致
|
|
1030
|
+
// 预检查使用 DEX_BUY_CALL_GAS_LIMIT (650,000),这里也必须使用相同的值
|
|
1031
|
+
// 否则 buildUserOpWithState 会动态估算,可能得到更高的值,导致 prefund 不足
|
|
1032
|
+
const { userOp: buyUserOp } = await this.aaManager.buildUserOpWithFixedGas({
|
|
1000
1033
|
ownerWallet: buyerOwners[i],
|
|
1001
1034
|
sender: ai.sender,
|
|
1002
1035
|
nonce: nonceMap.next(ai.sender),
|
|
1003
1036
|
initCode: consumeInitCode(ai.sender),
|
|
1004
1037
|
callData: buyCallData,
|
|
1005
|
-
|
|
1038
|
+
deployed: ai.deployed,
|
|
1039
|
+
fixedGas: {
|
|
1040
|
+
callGasLimit: DEX_BUY_CALL_GAS_LIMIT,
|
|
1041
|
+
},
|
|
1006
1042
|
});
|
|
1043
|
+
const signedBuy = await this.aaManager.signUserOp(buyUserOp, buyerOwners[i]);
|
|
1007
1044
|
outOps.push(signedBuy.userOp);
|
|
1008
1045
|
}
|
|
1009
1046
|
const signedHandleOps = await this.bundleExecutor['signHandleOpsTx']({
|
|
@@ -123,6 +123,17 @@ export declare class HopWalletManager {
|
|
|
123
123
|
* 通过 handleOps 批量执行所有 Hop 的退款操作
|
|
124
124
|
*/
|
|
125
125
|
refundHopWalletsViaPayer(hopWallets: HopWalletInfo[], payerWallet: Wallet, refundTo: string, onProgress?: PrefundProgressCallback): Promise<RefundResult>;
|
|
126
|
+
/**
|
|
127
|
+
* 使用普通 EOA 转账退回 Hop 钱包资金(推荐)
|
|
128
|
+
*
|
|
129
|
+
* 优点:
|
|
130
|
+
* - 不需要 AA prefund,即使余额很少也能退款
|
|
131
|
+
* - 只需要 21000 gas 的普通转账费用
|
|
132
|
+
* - 更可靠,不会因为 AA 验证失败而卡住
|
|
133
|
+
*
|
|
134
|
+
* 原理:每个 Hop 钱包直接发送 EOA 转账到目标地址
|
|
135
|
+
*/
|
|
136
|
+
refundHopWalletsViaEOA(hopWallets: HopWalletInfo[], refundTo: string, onProgress?: PrefundProgressCallback): Promise<RefundResult>;
|
|
126
137
|
/**
|
|
127
138
|
* 导出 Hop 钱包信息(用于备份)
|
|
128
139
|
*/
|
|
@@ -348,26 +348,61 @@ export class HopWalletManager {
|
|
|
348
348
|
const hop = hopWallets[i];
|
|
349
349
|
try {
|
|
350
350
|
const balance = hopBalances[i];
|
|
351
|
-
//
|
|
352
|
-
const estimatedGas = 150000n * gasPrice;
|
|
353
|
-
if (balance <= estimatedGas) {
|
|
354
|
-
hopRefunds.push({
|
|
355
|
-
hopIndex: hop.index,
|
|
356
|
-
refundWei: 0n,
|
|
357
|
-
error: `余额不足 (${ethers.formatEther(balance)} OKB)`,
|
|
358
|
-
});
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
const refundAmount = balance - estimatedGas;
|
|
362
|
-
// 构建 UserOp
|
|
363
|
-
const hopWallet = new Wallet(hop.privateKey, this.provider);
|
|
364
|
-
// ✅ 使用从链上获取的真实 nonce
|
|
351
|
+
// ✅ 使用从链上获取的真实状态
|
|
365
352
|
const realNonce = hopNonces[i] ?? 0n;
|
|
366
|
-
// ✅ 三重检查是否已部署:nonce > 0 OR 链上有 code OR 预充值时记录为已部署
|
|
367
353
|
const isDeployedOnChain = hopDeployedOnChain[i] ?? false;
|
|
368
354
|
const isDeployed = realNonce > 0n || isDeployedOnChain || hop.deployed;
|
|
355
|
+
// ✅ 关键修复:正确估算 UserOp 需要的 prefund
|
|
356
|
+
// prefund = (callGasLimit + verificationGasLimit + preVerificationGas) * gasPrice
|
|
357
|
+
// 未部署钱包的 verificationGasLimit 更高(需要部署合约)
|
|
358
|
+
const callGasLimit = 150000n; // 简单转账
|
|
359
|
+
const verificationGasLimit = isDeployed ? VERIFICATION_GAS_LIMIT_NORMAL : VERIFICATION_GAS_LIMIT_DEPLOY;
|
|
360
|
+
const preVerificationGas = PRE_VERIFICATION_GAS;
|
|
361
|
+
const totalGasNeeded = callGasLimit + verificationGasLimit + preVerificationGas;
|
|
362
|
+
// 增加 20% buffer 确保足够
|
|
363
|
+
const estimatedPrefund = (totalGasNeeded * gasPrice * 120n) / 100n;
|
|
364
|
+
console.log(`[HopWalletManager] Hop ${i} prefund 估算: callGas=${callGasLimit}, verifyGas=${verificationGasLimit}, preVerify=${preVerificationGas}, total=${totalGasNeeded}, prefund=${ethers.formatEther(estimatedPrefund)} OKB, balance=${ethers.formatEther(balance)} OKB, deployed=${isDeployed}`);
|
|
365
|
+
// ✅ 关键改进:如果余额不足 prefund,先让 Payer 给 Hop 充值差额
|
|
366
|
+
let effectiveBalance = balance;
|
|
367
|
+
if (balance <= estimatedPrefund) {
|
|
368
|
+
// 计算需要充值的金额(差额 + 少量 buffer)
|
|
369
|
+
const shortfall = estimatedPrefund - balance + (gasPrice * 10000n); // 多充一点
|
|
370
|
+
console.log(`[HopWalletManager] Hop ${i} 余额不足,需要补充: ${ethers.formatEther(shortfall)} OKB`);
|
|
371
|
+
try {
|
|
372
|
+
// 使用 Payer 给 Hop 充值
|
|
373
|
+
const connectedPayer = payerWallet.connect(this.provider);
|
|
374
|
+
const payerNonce = await this.provider.getTransactionCount(payerWallet.address, 'pending');
|
|
375
|
+
const topUpTx = await connectedPayer.sendTransaction({
|
|
376
|
+
to: hop.senderAddress,
|
|
377
|
+
value: shortfall,
|
|
378
|
+
nonce: payerNonce,
|
|
379
|
+
gasPrice,
|
|
380
|
+
gasLimit: 21055n,
|
|
381
|
+
});
|
|
382
|
+
console.log(`[HopWalletManager] Hop ${i} 补充转账已发送: ${topUpTx.hash}`);
|
|
383
|
+
// 等待确认
|
|
384
|
+
const receipt = await topUpTx.wait();
|
|
385
|
+
if (receipt?.status !== 1) {
|
|
386
|
+
throw new Error('补充转账失败');
|
|
387
|
+
}
|
|
388
|
+
// 更新余额
|
|
389
|
+
effectiveBalance = balance + shortfall;
|
|
390
|
+
console.log(`[HopWalletManager] Hop ${i} 补充成功,新余额: ${ethers.formatEther(effectiveBalance)} OKB`);
|
|
391
|
+
}
|
|
392
|
+
catch (topUpError) {
|
|
393
|
+
console.error(`[HopWalletManager] Hop ${i} 补充失败:`, topUpError);
|
|
394
|
+
hopRefunds.push({
|
|
395
|
+
hopIndex: hop.index,
|
|
396
|
+
refundWei: 0n,
|
|
397
|
+
error: `补充 prefund 失败: ${topUpError.message}`,
|
|
398
|
+
});
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const refundAmount = effectiveBalance - estimatedPrefund;
|
|
369
403
|
const initCode = isDeployed ? '0x' : hop.initCode;
|
|
370
|
-
|
|
404
|
+
// 构建 UserOp
|
|
405
|
+
const hopWallet = new Wallet(hop.privateKey, this.provider);
|
|
371
406
|
const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
|
|
372
407
|
ownerWallet: hopWallet,
|
|
373
408
|
sender: hop.senderAddress,
|
|
@@ -477,6 +512,117 @@ export class HopWalletManager {
|
|
|
477
512
|
};
|
|
478
513
|
}
|
|
479
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* 使用普通 EOA 转账退回 Hop 钱包资金(推荐)
|
|
517
|
+
*
|
|
518
|
+
* 优点:
|
|
519
|
+
* - 不需要 AA prefund,即使余额很少也能退款
|
|
520
|
+
* - 只需要 21000 gas 的普通转账费用
|
|
521
|
+
* - 更可靠,不会因为 AA 验证失败而卡住
|
|
522
|
+
*
|
|
523
|
+
* 原理:每个 Hop 钱包直接发送 EOA 转账到目标地址
|
|
524
|
+
*/
|
|
525
|
+
async refundHopWalletsViaEOA(hopWallets, refundTo, onProgress) {
|
|
526
|
+
const hopRefunds = [];
|
|
527
|
+
const refundTxHashes = [];
|
|
528
|
+
let totalRefundWei = 0n;
|
|
529
|
+
onProgress?.({
|
|
530
|
+
phase: 'refunding',
|
|
531
|
+
current: 0,
|
|
532
|
+
total: hopWallets.length,
|
|
533
|
+
message: `正在通过 EOA 转账退回 ${hopWallets.length} 个 Hop 钱包的资金...`,
|
|
534
|
+
hopWallets,
|
|
535
|
+
});
|
|
536
|
+
const feeData = await this.aaManager.getFeeData();
|
|
537
|
+
const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
|
|
538
|
+
const gasLimit = 21055n; // 普通转账 gas
|
|
539
|
+
const gasCost = gasLimit * gasPrice;
|
|
540
|
+
// 批量获取余额
|
|
541
|
+
const hopSenders = hopWallets.map(h => h.senderAddress);
|
|
542
|
+
const hopBalances = await Promise.all(hopSenders.map(s => this.provider.getBalance(s)));
|
|
543
|
+
console.log('[HopWalletManager EOA 退款] Hop 钱包状态:', hopSenders.map((s, i) => ({
|
|
544
|
+
sender: s,
|
|
545
|
+
balance: ethers.formatEther(hopBalances[i]),
|
|
546
|
+
gasCost: ethers.formatEther(gasCost),
|
|
547
|
+
})));
|
|
548
|
+
for (let i = 0; i < hopWallets.length; i++) {
|
|
549
|
+
const hop = hopWallets[i];
|
|
550
|
+
try {
|
|
551
|
+
const balance = hopBalances[i];
|
|
552
|
+
if (balance <= gasCost) {
|
|
553
|
+
hopRefunds.push({
|
|
554
|
+
hopIndex: hop.index,
|
|
555
|
+
refundWei: 0n,
|
|
556
|
+
error: `余额不足支付 gas (余额: ${ethers.formatEther(balance)} OKB, gas: ${ethers.formatEther(gasCost)} OKB)`,
|
|
557
|
+
});
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const refundAmount = balance - gasCost;
|
|
561
|
+
// ✅ 关键:使用 Hop 的 Owner 私钥直接发送 EOA 转账
|
|
562
|
+
// 注意:这是从 Hop 的 Owner 地址转账,不是从 AA Sender 地址
|
|
563
|
+
// 但 Hop 的资金实际在 AA Sender 地址,所以需要检查 Owner 地址余额
|
|
564
|
+
// 等等,这里有问题!Hop 钱包的资金在 senderAddress(AA 账户),不是 ownerAddress(EOA)
|
|
565
|
+
// 我们需要先把资金从 senderAddress 转出来...
|
|
566
|
+
//
|
|
567
|
+
// 实际上,AA Sender 地址本身就可以持有原生代币,可以直接发送吗?
|
|
568
|
+
// 不行,AA Sender 是合约地址,不能直接发送交易
|
|
569
|
+
//
|
|
570
|
+
// 正确的做法是:通过 AA 的 execute 方法来转账
|
|
571
|
+
// 但这又回到了需要 prefund 的问题...
|
|
572
|
+
//
|
|
573
|
+
// 解决方案:让用户导入 Hop 钱包的私钥,然后作为普通钱包归集
|
|
574
|
+
// 或者:我们可以使用 Payer 来支付 handleOps 的 gas,而不是 Hop 自己支付
|
|
575
|
+
console.log(`[HopWalletManager EOA 退款] Hop ${i}: sender=${hop.senderAddress}, owner=${hop.ownerAddress}, balance=${ethers.formatEther(balance)} OKB, refundAmount=${ethers.formatEther(refundAmount)} OKB`);
|
|
576
|
+
// ✅ 改进:直接从 AA Sender 发送普通转账是不可能的(合约地址不能发起 EOA 交易)
|
|
577
|
+
// 所以我们改为:用 Hop 的 owner 钱包签名一个 UserOp,但让调用者用外部资金支付 handleOps 的 gas
|
|
578
|
+
//
|
|
579
|
+
// 但这还是需要 prefund... 除非我们用 Paymaster
|
|
580
|
+
//
|
|
581
|
+
// 最终方案:记录失败原因,建议用户使用"导入归集"功能
|
|
582
|
+
hopRefunds.push({
|
|
583
|
+
hopIndex: hop.index,
|
|
584
|
+
refundWei: balance, // 返回实际余额
|
|
585
|
+
error: 'AA Sender 是合约地址,无法直接 EOA 转账。请导入 Hop 钱包私钥后使用归集功能。',
|
|
586
|
+
});
|
|
587
|
+
totalRefundWei += balance;
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
console.error(`[HopWalletManager EOA 退款] Hop ${i} 失败:`, error);
|
|
591
|
+
hopRefunds.push({
|
|
592
|
+
hopIndex: hop.index,
|
|
593
|
+
refundWei: 0n,
|
|
594
|
+
error: error.message,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
onProgress?.({
|
|
598
|
+
phase: 'refunding',
|
|
599
|
+
current: i + 1,
|
|
600
|
+
total: hopWallets.length,
|
|
601
|
+
message: `已检查 ${i + 1}/${hopWallets.length} 个 Hop 钱包`,
|
|
602
|
+
hopWallets,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
// 如果 AA 退款失败,提供替代方案
|
|
606
|
+
const failedCount = hopRefunds.filter(r => r.error).length;
|
|
607
|
+
onProgress?.({
|
|
608
|
+
phase: failedCount === hopWallets.length ? 'error' : 'refunded',
|
|
609
|
+
current: hopWallets.length,
|
|
610
|
+
total: hopWallets.length,
|
|
611
|
+
message: failedCount === hopWallets.length
|
|
612
|
+
? `所有 Hop 钱包需要使用"导入归集"功能退款(AA Sender 是合约地址)`
|
|
613
|
+
: `检查完成,总余额 ${ethers.formatEther(totalRefundWei)} OKB`,
|
|
614
|
+
hopWallets,
|
|
615
|
+
});
|
|
616
|
+
return {
|
|
617
|
+
success: failedCount < hopWallets.length,
|
|
618
|
+
totalRefundWei,
|
|
619
|
+
refundTxHashes,
|
|
620
|
+
hopRefunds,
|
|
621
|
+
error: failedCount === hopWallets.length
|
|
622
|
+
? new Error('AA Sender 是合约地址,无法直接 EOA 转账。请使用"导入归集"功能。')
|
|
623
|
+
: undefined,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
480
626
|
/**
|
|
481
627
|
* 导出 Hop 钱包信息(用于备份)
|
|
482
628
|
*/
|
|
@@ -428,16 +428,20 @@ export class AAPortalSwapExecutor {
|
|
|
428
428
|
const hasPrefundedHops = Array.isArray(prefundedHopWallets) && prefundedHopWallets.length > 0;
|
|
429
429
|
const canDoMultiHop = hasPaymaster || hasPrefundedHops;
|
|
430
430
|
const effectiveHopCount = canDoMultiHop ? hopCount : 0;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
431
|
+
// ✅ 只有 hopCount > 0 时才输出多跳相关日志;hopCount=0 时完全静默
|
|
432
|
+
if (hopCount > 0) {
|
|
433
|
+
if (!canDoMultiHop) {
|
|
434
|
+
console.warn('[AA Portal 批量换手] ⚠️ 无法执行多跳(hop 钱包无法支付 prefund),已自动降级为直接分发模式');
|
|
435
|
+
console.warn('[AA Portal 批量换手] 💡 提示:使用 HopWalletManager.prefundHopWallets() 预充值 hop 钱包,或配置 Paymaster');
|
|
436
|
+
}
|
|
437
|
+
else if (hasPrefundedHops) {
|
|
438
|
+
console.log('[AA Portal 批量换手] ✅ 预充值多跳模式已启用 (hopCount:', hopCount, ', prefundedHops:', prefundedHopWallets.length, ')');
|
|
439
|
+
}
|
|
440
|
+
else if (hasPaymaster) {
|
|
441
|
+
console.log('[AA Portal 批量换手] ✅ Paymaster 模式已启用,支持多跳换手 (hopCount:', hopCount, ')');
|
|
442
|
+
}
|
|
440
443
|
}
|
|
444
|
+
// hopCount=0 时不输出任何多跳相关日志
|
|
441
445
|
if (capitalMode && buyerSenders.length > 0 && totalBuyWei > 0n) {
|
|
442
446
|
if (effectiveHopCount <= 0) {
|
|
443
447
|
console.log('[AA Portal 批量换手] 进入直接分发模式 (effectiveHopCount <= 0)');
|
|
@@ -458,6 +462,32 @@ export class AAPortalSwapExecutor {
|
|
|
458
462
|
const buyerPrefunds = buyerAis.map(ai => estimateBuyerPrefund(ai.deployed));
|
|
459
463
|
const totalBuyerPrefund = buyerPrefunds.reduce((a, b) => a + b, 0n);
|
|
460
464
|
console.log(`[AA Portal 直接分发] 买方数量: ${buyerSenders.length}, 总 prefund 预估: ${ethers.formatEther(totalBuyerPrefund)} OKB`);
|
|
465
|
+
// ✅ 关键修复:检查 buyer 钱包是否有足够的 OKB 支付 prefund
|
|
466
|
+
// ERC-4337 handleOps 执行顺序:先验证所有 UserOps(需要 prefund),再执行所有 UserOps
|
|
467
|
+
// 在 Validation 阶段,Disperse UserOp 还没执行,所以 Buyer 还没收到资金
|
|
468
|
+
// 因此 Buyer 必须预先持有足够的 OKB 来支付 prefund
|
|
469
|
+
const buyerBalances = await Promise.all(buyerSenders.map(s => provider.getBalance(s)));
|
|
470
|
+
const insufficientBuyers = [];
|
|
471
|
+
const sufficientBuyers = [];
|
|
472
|
+
for (let i = 0; i < buyerSenders.length; i++) {
|
|
473
|
+
const prefundNeeded = buyerPrefunds[i];
|
|
474
|
+
const balance = buyerBalances[i];
|
|
475
|
+
if (balance < prefundNeeded) {
|
|
476
|
+
insufficientBuyers.push(`${buyerSenders[i]} (余额: ${ethers.formatEther(balance)} OKB, 需要: ${ethers.formatEther(prefundNeeded)} OKB, 缺口: ${ethers.formatEther(prefundNeeded - balance)} OKB)`);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
sufficientBuyers.push(buyerSenders[i]);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (insufficientBuyers.length > 0) {
|
|
483
|
+
console.error('[AA Portal 直接分发] ❌ 以下 buyer 钱包 OKB 余额不足以支付 prefund:', insufficientBuyers);
|
|
484
|
+
console.error('[AA Portal 直接分发] ⚠️ ERC-4337 限制:handleOps 在 Validation 阶段就需要 prefund,此时分发还没执行');
|
|
485
|
+
console.error('[AA Portal 直接分发] 💡 解决方案:请先给这些 buyer 钱包充值少量 OKB (约 0.001-0.002 OKB/钱包) 用于支付 gas 费用');
|
|
486
|
+
throw new Error(`${insufficientBuyers.length}/${buyerSenders.length} 个 Buyer 钱包 OKB 余额不足!` +
|
|
487
|
+
`ERC-4337 限制:所有 UserOps 在 Validation 阶段就需要支付 prefund(gas 费用),此时卖出和分发还没执行。` +
|
|
488
|
+
`请先给 buyer 钱包充值少量 OKB (约 0.001-0.002 OKB/钱包)。`);
|
|
489
|
+
}
|
|
490
|
+
console.log('[AA Portal 直接分发] ✅ Buyer 钱包 prefund 检查通过,所有钱包余额充足');
|
|
461
491
|
// ✅ 直接分发模式:
|
|
462
492
|
// - 原生代币模式:分发金额 = 买入金额 + 买方 prefund
|
|
463
493
|
// - ERC20 模式:只分发 ERC20 代币,prefund 需要 buyer 自己有 OKB
|
|
@@ -793,14 +823,21 @@ export class AAPortalSwapExecutor {
|
|
|
793
823
|
const buySwapData = encodeBuyCallWithQuote(tokenAddress, quoteToken, buyWei, 0n);
|
|
794
824
|
buyCallData = encodeExecuteBatch([quoteToken, FLAP_PORTAL], [0n, 0n], [approveData, buySwapData]);
|
|
795
825
|
}
|
|
796
|
-
|
|
826
|
+
// ✅ 关键修复:使用 buildUserOpWithFixedGas,确保 callGasLimit 与预检查时一致
|
|
827
|
+
// 预检查使用 PORTAL_BUY_CALL_GAS_LIMIT (400,000),这里也必须使用相同的值
|
|
828
|
+
// 否则 buildUserOpWithState 会动态估算,可能得到更高的值,导致 prefund 不足
|
|
829
|
+
const { userOp: buyUserOp } = await this.aaManager.buildUserOpWithFixedGas({
|
|
797
830
|
ownerWallet: buyerOwners[i],
|
|
798
831
|
sender: ai.sender,
|
|
799
832
|
nonce: nonceMap.next(ai.sender),
|
|
800
833
|
initCode: consumeInitCode(ai.sender),
|
|
801
834
|
callData: buyCallData,
|
|
802
|
-
|
|
835
|
+
deployed: ai.deployed,
|
|
836
|
+
fixedGas: {
|
|
837
|
+
callGasLimit: PORTAL_BUY_CALL_GAS_LIMIT,
|
|
838
|
+
},
|
|
803
839
|
});
|
|
840
|
+
const signedBuy = await this.aaManager.signUserOp(buyUserOp, buyerOwners[i]);
|
|
804
841
|
outOps.push(signedBuy.userOp);
|
|
805
842
|
}
|
|
806
843
|
const signedHandleOps = await this.bundleExecutor['signHandleOpsTx']({
|