four-flap-meme-sdk 1.6.60 → 1.6.63

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.
@@ -503,8 +503,16 @@ export class AAAccountManager {
503
503
  */
504
504
  async buildUserOpWithFixedGas(params) {
505
505
  await this.syncEntryPointIfNeeded();
506
- const feeData = await this.getFeeData();
507
- const legacyGasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? DEFAULT_GAS_PRICE;
506
+ // 关键修复:优先使用传入的 fixedGas.gasPrice,确保整个流程使用一致的 gasPrice
507
+ // 这样可以避免 feeData 缓存过期导致的 gasPrice 不一致问题
508
+ let legacyGasPrice;
509
+ if (params.fixedGas?.gasPrice !== undefined) {
510
+ legacyGasPrice = params.fixedGas.gasPrice;
511
+ }
512
+ else {
513
+ const feeData = await this.getFeeData();
514
+ legacyGasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? DEFAULT_GAS_PRICE;
515
+ }
508
516
  const paymasterAndData = this.buildPaymasterAndData();
509
517
  const callGasLimit = params.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_SELL;
510
518
  const verificationGasLimit = params.deployed
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { ethers } from 'ethers';
16
16
  import { createAAAccountManager, encodeExecute, createWallet } from './aa-account.js';
17
- import { encodeBuyCall, encodeSellCall } from './portal-ops.js';
17
+ import { encodeBuyCall, encodeSellCall, PortalQuery, lpFeeProfileToV3Fee } from './portal-ops.js';
18
18
  import { encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, } from './dex.js';
19
19
  import { encodeApproveCall } from './portal-ops.js';
20
20
  import { FLAP_PORTAL, WOKB, XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, PARTICLE_BUNDLER_URL, } from './constants.js';
@@ -71,7 +71,6 @@ function normalizeConfig(cfg) {
71
71
  export async function buildBundleBuyOps(params) {
72
72
  const poolType = params.poolType || 'flap';
73
73
  const wokb = params.wrappedOkbAddress || WOKB;
74
- const v3Fee = params.v3Fee || 2500;
75
74
  const deadline = Math.floor(Date.now() / 1000) + 60 * Math.max(1, Number(params.deadlineMinutes ?? 20));
76
75
  const portal = FLAP_PORTAL;
77
76
  const routerAddress = params.routerAddress || '';
@@ -85,6 +84,22 @@ export async function buildBundleBuyOps(params) {
85
84
  // ✅ 归一化配置,填充默认值(与前端 normalizeXLayerAAConfig 保持一致)
86
85
  const config = normalizeConfig(params.config);
87
86
  const aaManager = createAAAccountManager(config);
87
+ // ✅ V3 模式:直接从链上读取 lpFeeProfile 获取正确的 fee(与 bundleGraduateBuy 保持一致)
88
+ let v3Fee = params.v3Fee || 2500;
89
+ if (poolType === 'v3') {
90
+ try {
91
+ const portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
92
+ const tokenState = await portalQuery.getTokenV7(params.tokenAddress);
93
+ const correctV3Fee = lpFeeProfileToV3Fee(tokenState.lpFeeProfile);
94
+ if (correctV3Fee !== v3Fee) {
95
+ console.log(`[buildBundleBuyOps] V3 Fee 校正: 前端传入 ${v3Fee}, 链上 lpFeeProfile=${tokenState.lpFeeProfile} → 正确 fee=${correctV3Fee}`);
96
+ }
97
+ v3Fee = correctV3Fee;
98
+ }
99
+ catch (e) {
100
+ console.warn(`[buildBundleBuyOps] 读取 lpFeeProfile 失败,使用前端传入的 v3Fee=${v3Fee}:`, e);
101
+ }
102
+ }
88
103
  const ownerWallets = params.ownerPrivateKeys.map(pk => createWallet(pk, config));
89
104
  const owners = ownerWallets.map(w => w.address);
90
105
  const accounts = await aaManager.getMultipleAccountInfo(owners);
@@ -223,7 +238,6 @@ export async function buildBundleBuyOps(params) {
223
238
  export async function buildBundleSellOps(params) {
224
239
  const poolType = params.poolType || 'flap';
225
240
  const wokb = params.wrappedOkbAddress || WOKB;
226
- const v3Fee = params.v3Fee || 2500;
227
241
  const deadline = Math.floor(Date.now() / 1000) + 60 * Math.max(1, Number(params.deadlineMinutes ?? 20));
228
242
  const portal = FLAP_PORTAL;
229
243
  const routerAddress = params.routerAddress || '';
@@ -238,6 +252,22 @@ export async function buildBundleSellOps(params) {
238
252
  // ✅ 归一化配置,填充默认值(与前端 normalizeXLayerAAConfig 保持一致)
239
253
  const config = normalizeConfig(params.config);
240
254
  const aaManager = createAAAccountManager(config);
255
+ // ✅ V3 模式:直接从链上读取 lpFeeProfile 获取正确的 fee(与 bundleGraduateBuy 保持一致)
256
+ let v3Fee = params.v3Fee || 2500;
257
+ if (poolType === 'v3') {
258
+ try {
259
+ const portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
260
+ const tokenState = await portalQuery.getTokenV7(params.tokenAddress);
261
+ const correctV3Fee = lpFeeProfileToV3Fee(tokenState.lpFeeProfile);
262
+ if (correctV3Fee !== v3Fee) {
263
+ console.log(`[buildBundleSellOps] V3 Fee 校正: 前端传入 ${v3Fee}, 链上 lpFeeProfile=${tokenState.lpFeeProfile} → 正确 fee=${correctV3Fee}`);
264
+ }
265
+ v3Fee = correctV3Fee;
266
+ }
267
+ catch (e) {
268
+ console.warn(`[buildBundleSellOps] 读取 lpFeeProfile 失败,使用前端传入的 v3Fee=${v3Fee}:`, e);
269
+ }
270
+ }
241
271
  const ownerWallets = params.ownerPrivateKeys.map(pk => createWallet(pk, config));
242
272
  const owners = ownerWallets.map(w => w.address);
243
273
  const accounts = await aaManager.getMultipleAccountInfo(owners);
@@ -7,12 +7,13 @@ import { POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, POTATOSWAP_V3_FACTORY, WOKB
7
7
  // 多跳 prefund 估算用的 gas 常量(可配置)
8
8
  const HOP_CALL_GAS_LIMIT = 150000n; // hop 转账操作
9
9
  const DEX_BUY_CALL_GAS_LIMIT = 650000n; // DEX V3 买入操作
10
- // ✅ 增加余额检查 buffer 从 120% 到 180%,确保 gasPrice 波动时也有足够余额
11
- // 预充值使用 200% buffer,这里使用 180% 作为安全阈值
12
- const PREFUND_BUFFER_PERCENT = 180n; // prefund buffer 百分比 (180 = 1.8x)
10
+ // ✅ 增加余额检查 buffer 从 180% 到 250%,确保 gasPrice 波动时也有足够余额
11
+ // 预充值使用 200% buffer,这里使用 250% 作为安全阈值(覆盖更大的 gasPrice 波动)
12
+ const PREFUND_BUFFER_PERCENT = 250n; // prefund buffer 百分比 (250 = 2.5x)
13
13
  import { AAAccountManager, encodeExecute, encodeExecuteBatch, encodeExecuteViaMulticall3 } from './aa-account.js';
14
14
  import { encodeApproveCall, lpFeeProfileToV3Fee, } from './portal-ops.js';
15
- import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, encodeSwapExactTokensForTokensSupportingFee, } from './dex.js';
15
+ import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, encodeSwapExactTokensForTokensSupportingFee, encodeSwapExactOutputForTokensV3, // ✅ 新增:精确输出买入(V3 exactOutput)
16
+ } from './dex.js';
16
17
  import { BundleExecutor } from './bundle.js';
17
18
  import { PROFIT_CONFIG, ZERO_ADDRESS } from '../utils/constants.js';
18
19
  const multicallIface = new ethers.Interface([
@@ -478,6 +479,10 @@ export class AADexSwapExecutor {
478
479
  const allowance = await this.aaManager.getErc20Allowance(tokenAddress, sellerAi.sender, effectiveRouter);
479
480
  needApprove = allowance < sellAmountWei;
480
481
  }
482
+ // ✅ 关键修复:在构建 UserOps 之前获取 feeData,整个方法使用同一个 gasPrice
483
+ // 这样可以避免 feeData 缓存过期导致的不一致问题
484
+ const fixedFeeData = await this.aaManager.getFeeData();
485
+ const fixedGasPrice = fixedFeeData.gasPrice ?? fixedFeeData.maxFeePerGas ?? 5000000000n;
481
486
  const outOps = [];
482
487
  if (needApprove) {
483
488
  const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
@@ -487,6 +492,7 @@ export class AADexSwapExecutor {
487
492
  initCode: consumeInitCode(sellerAi.sender),
488
493
  callData: encodeExecute(tokenAddress, 0n, encodeApproveCall(effectiveRouter)),
489
494
  deployed: sellerAi.deployed,
495
+ fixedGas: { gasPrice: fixedGasPrice }, // ✅ 使用统一的 gasPrice
490
496
  });
491
497
  const signedApprove = await this.aaManager.signUserOp(userOp, sellerOwner);
492
498
  outOps.push(signedApprove.userOp);
@@ -557,31 +563,36 @@ export class AADexSwapExecutor {
557
563
  return 0n;
558
564
  }
559
565
  })();
560
- // ✅ 关键修复:提前计算 buyerPrefund,用于资金充足性检查
561
- // capitalMode 下,分发金额 = 买入金额 + buyerPrefund + 利润
562
- // 必须确保卖出所得 >= 分发金额,否则交易会失败
563
- const feeDataEarly = await this.aaManager.getFeeData();
564
- const gasPriceEarly = feeDataEarly.gasPrice ?? feeDataEarly.maxFeePerGas ?? 5000000000n;
565
- const estimateBuyerPrefundEarly = (deployed) => {
566
+ // ✅ 估算 prefund 的函数(整个方法统一使用)
567
+ // 注意:分发金额使用带 buffer prefund,确保 buyer 有足够余额
568
+ const estimateBuyerPrefundWithBuffer = (deployed) => {
566
569
  const callGas = DEX_BUY_CALL_GAS_LIMIT;
567
570
  const verifyGas = deployed ? VERIFICATION_GAS_LIMIT_NORMAL : VERIFICATION_GAS_LIMIT_DEPLOY;
568
571
  const preVerifyGas = PRE_VERIFICATION_GAS;
569
572
  const totalGas = callGas + verifyGas + preVerifyGas;
570
- return (totalGas * gasPriceEarly * PREFUND_BUFFER_PERCENT) / 100n;
573
+ return (totalGas * fixedGasPrice * PREFUND_BUFFER_PERCENT) / 100n;
574
+ };
575
+ // ✅ 估算实际 prefund(不带 buffer,用于 UserOp 构建时的计算)
576
+ const estimateActualPrefund = (deployed) => {
577
+ const callGas = DEX_BUY_CALL_GAS_LIMIT;
578
+ const verifyGas = deployed ? VERIFICATION_GAS_LIMIT_NORMAL : VERIFICATION_GAS_LIMIT_DEPLOY;
579
+ const preVerifyGas = PRE_VERIFICATION_GAS;
580
+ const totalGas = callGas + verifyGas + preVerifyGas;
581
+ return totalGas * fixedGasPrice;
571
582
  };
572
583
  // ✅ 计算所有 buyer 需要的 prefund 总和(仅原生代币模式 + capitalMode 需要)
573
- const totalBuyerPrefundEarly = (useNativeToken && capitalMode)
574
- ? buyerAis.reduce((sum, ai) => sum + estimateBuyerPrefundEarly(ai.deployed), 0n)
584
+ const totalBuyerPrefundWithBuffer = (useNativeToken && capitalMode)
585
+ ? buyerAis.reduce((sum, ai) => sum + estimateBuyerPrefundWithBuffer(ai.deployed), 0n)
575
586
  : 0n;
576
587
  // ✅ 计算利润(不再压缩到“卖出-买入-prefund”上限,按配置直接刮取)
577
588
  const profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
578
589
  // 资金充足性提示:不再阻断,记录警告后继续分发
579
- const totalDistributeNeeded = totalBuyWei + totalBuyerPrefundEarly + profitWei;
590
+ const totalDistributeNeeded = totalBuyWei + totalBuyerPrefundWithBuffer + profitWei;
580
591
  if (quotedSellOutWei > 0n && totalDistributeNeeded > quotedSellOutWei) {
581
592
  console.warn('[AA DEX 批量换手] ⚠️ 预估卖出金额不足,仍继续分发:', {
582
593
  quotedSellOutWei: ethers.formatEther(quotedSellOutWei),
583
594
  totalBuyWei: ethers.formatEther(totalBuyWei),
584
- totalBuyerPrefund: ethers.formatEther(totalBuyerPrefundEarly),
595
+ totalBuyerPrefund: ethers.formatEther(totalBuyerPrefundWithBuffer),
585
596
  profitWei: ethers.formatEther(profitWei),
586
597
  totalNeeded: ethers.formatEther(totalDistributeNeeded),
587
598
  shortfall: ethers.formatEther(totalDistributeNeeded - quotedSellOutWei),
@@ -636,23 +647,12 @@ export class AADexSwapExecutor {
636
647
  if (capitalMode && buyerSenders.length > 0 && totalBuyWei > 0n) {
637
648
  if (effectiveHopCount <= 0) {
638
649
  console.log('[AA DEX 批量换手] 进入直接分发模式 (effectiveHopCount <= 0)');
639
- // ✅ 关键修复:计算每个买方执行买入 UserOp 需要的 prefund(gas 费用)
640
- // EntryPoint validation 阶段会扣除 prefund,此时分发还没执行
641
- // 因此需要为每个买方额外预留 prefund
642
- const feeData = await this.aaManager.getFeeData();
643
- const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n; // 5 gwei fallback
644
- const estimateBuyerPrefund = (deployed) => {
645
- // 买入操作的 gas(使用常量)
646
- const callGas = DEX_BUY_CALL_GAS_LIMIT;
647
- const verifyGas = deployed ? VERIFICATION_GAS_LIMIT_NORMAL : VERIFICATION_GAS_LIMIT_DEPLOY;
648
- const preVerifyGas = PRE_VERIFICATION_GAS;
649
- const totalGas = callGas + verifyGas + preVerifyGas;
650
- return (totalGas * gasPrice * PREFUND_BUFFER_PERCENT) / 100n;
651
- };
652
- // 计算每个买方需要的 prefund
653
- const buyerPrefunds = buyerAis.map(ai => estimateBuyerPrefund(ai.deployed));
650
+ // ✅ 关键修复:使用开始时获取的 fixedGasPrice 计算 prefund
651
+ // 这样确保检查、分发和 UserOp 构建使用同一个 gasPrice,避免不一致
652
+ // 计算每个买方需要的 prefund(带 buffer,用于余额检查和分发)
653
+ const buyerPrefunds = buyerAis.map(ai => estimateBuyerPrefundWithBuffer(ai.deployed));
654
654
  const totalBuyerPrefund = buyerPrefunds.reduce((a, b) => a + b, 0n);
655
- console.log(`[AA DEX 直接分发] 买方数量: ${buyerSenders.length}, 总 prefund 预估: ${ethers.formatEther(totalBuyerPrefund)} OKB`);
655
+ console.log(`[AA DEX 直接分发] 买方数量: ${buyerSenders.length}, 总 prefund 预估: ${ethers.formatEther(totalBuyerPrefund)} OKB, fixedGasPrice: ${fixedGasPrice.toString()}`);
656
656
  // ✅ 关键修复:检查 buyer 钱包是否有足够的 OKB 支付 prefund
657
657
  // ERC-4337 handleOps 执行顺序:先验证所有 UserOps(需要 prefund),再执行所有 UserOps
658
658
  // 在 Validation 阶段,Disperse UserOp 还没执行,所以 Buyer 还没收到资金
@@ -738,14 +738,13 @@ export class AADexSwapExecutor {
738
738
  usePrefundedHops: hasPrefundedHops,
739
739
  usePaymaster: hasPaymaster,
740
740
  });
741
- const feeData = await this.aaManager.getFeeData();
742
- const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
743
- // 预估 prefund 的函数(使用全局常量)
741
+ // 使用开始时获取的 fixedGasPrice,确保整个方法一致性
742
+ // 预估 prefund 的函数(使用 fixedGasPrice)
744
743
  const estimatePrefund = (callGas, deployed) => {
745
744
  const verifyGas = deployed ? VERIFICATION_GAS_LIMIT_NORMAL : VERIFICATION_GAS_LIMIT_DEPLOY;
746
745
  const preVerifyGas = PRE_VERIFICATION_GAS;
747
746
  const totalGas = callGas + verifyGas + preVerifyGas;
748
- return (totalGas * gasPrice * PREFUND_BUFFER_PERCENT) / 100n;
747
+ return (totalGas * fixedGasPrice * PREFUND_BUFFER_PERCENT) / 100n;
749
748
  };
750
749
  // ✅ 构建 hop 钱包列表
751
750
  // 如果有预充值的 hop 钱包,使用它们;否则生成新钱包(需要 Paymaster)
@@ -931,6 +930,7 @@ export class AADexSwapExecutor {
931
930
  deployed: currentHop.deployed,
932
931
  fixedGas: {
933
932
  callGasLimit: HOP_CALL_GAS_LIMIT,
933
+ gasPrice: fixedGasPrice, // ✅ 使用统一的 gasPrice
934
934
  },
935
935
  });
936
936
  const signedHop = await this.aaManager.signUserOp(hopUserOp, currentHop.wallet);
@@ -957,6 +957,7 @@ export class AADexSwapExecutor {
957
957
  deployed: lastHop.deployed,
958
958
  fixedGas: {
959
959
  callGasLimit: HOP_CALL_GAS_LIMIT,
960
+ gasPrice: fixedGasPrice, // ✅ 使用统一的 gasPrice
960
961
  },
961
962
  });
962
963
  const signedLastHop = await this.aaManager.signUserOp(lastHopUserOp, lastHop.wallet);
@@ -995,40 +996,80 @@ export class AADexSwapExecutor {
995
996
  });
996
997
  outOps.push(signedProfitTransfer.userOp);
997
998
  }
999
+ // ✅ 计算每个买家应该买入的代币数量(用于 V3 exactOutput 模式)
1000
+ // 逻辑:卖出 sellAmountWei 个代币 → 买家按比例买回
1001
+ // targetTokenAmounts[i] = sellAmountWei * (buyAmountsWei[i] / totalBuyWei)
1002
+ //
1003
+ // exactOutput 模式优势:
1004
+ // 1. 精确控制每个买家买入的代币数量
1005
+ // 2. amountInMaximum 设为分发金额,确保不超支
1006
+ // 3. 多余的 OKB 通过 refundETH 退回买家钱包
1007
+ const targetTokenAmounts = capitalMode && totalBuyWei > 0n
1008
+ ? buyAmountsWei.map(buyWei => (sellAmountWei * buyWei) / totalBuyWei)
1009
+ : buyAmountsWei.map(() => 0n); // 非资金利用率模式不使用 exactOutput
1010
+ console.log('[AA DEX 批量换手] 目标代币数量计算:', {
1011
+ sellAmountWei: sellAmountWei.toString(),
1012
+ totalBuyWei: ethers.formatEther(totalBuyWei),
1013
+ targetTokenAmounts: targetTokenAmounts.map(a => a.toString()),
1014
+ useExactOutput: capitalMode && isV3Trade && useNativeToken,
1015
+ });
998
1016
  // Batch Buy ops(分发后再执行)- ✅ 支持 V2/V3,支持 ERC20 稳定币
999
1017
  for (let i = 0; i < buyerOwners.length; i++) {
1000
1018
  const ai = buyerAis[i];
1001
- const buyWei = buyAmountsWei[i];
1019
+ const buyWei = buyAmountsWei[i]; // 分发给买家的 OKB 金额(作为 amountInMaximum)
1020
+ const targetTokenOut = targetTokenAmounts[i]; // 目标买入的代币数量
1002
1021
  let buyCallData;
1003
1022
  if (useNativeToken) {
1004
1023
  // ✅ 原生代币模式:OKB → Token
1005
1024
  let buySwapData;
1006
1025
  if (isV3Trade) {
1007
- buySwapData = encodeSwapExactETHForTokensV3({
1008
- tokenIn: WOKB,
1009
- tokenOut: tokenAddress,
1010
- fee: lpFeeProfileToV3Fee(lpFeeProfile),
1011
- recipient: ai.sender,
1012
- deadline,
1013
- amountIn: buyWei,
1014
- amountOutMinimum: 0n,
1015
- sqrtPriceLimitX96: 0n,
1016
- });
1026
+ // V3 资金利用率模式:使用 exactOutput 精确买入目标代币数量
1027
+ // - amountOut: 精确的代币数量(sellAmountWei 按比例分配)
1028
+ // - amountInMaximum: 最多花费的 OKB(分发给该买家的金额)
1029
+ // - refundETH: 多余的 OKB 自动退回
1030
+ if (capitalMode && targetTokenOut > 0n) {
1031
+ buySwapData = encodeSwapExactOutputForTokensV3({
1032
+ tokenIn: WOKB,
1033
+ tokenOut: tokenAddress,
1034
+ fee: lpFeeProfileToV3Fee(lpFeeProfile),
1035
+ recipient: ai.sender,
1036
+ deadline,
1037
+ amountOut: targetTokenOut, // ✅ 精确买入的代币数量
1038
+ amountInMaximum: buyWei, // ✅ 最多花费的 OKB(分发金额)
1039
+ sqrtPriceLimitX96: 0n,
1040
+ });
1041
+ console.log(`[AA DEX V3 exactOutput] Buyer ${i}: 目标代币 ${targetTokenOut.toString()}, 最大花费 ${ethers.formatEther(buyWei)} OKB`);
1042
+ }
1043
+ else {
1044
+ // 非资金利用率模式:使用 exactInput(固定 OKB 买尽可能多的代币)
1045
+ buySwapData = encodeSwapExactETHForTokensV3({
1046
+ tokenIn: WOKB,
1047
+ tokenOut: tokenAddress,
1048
+ fee: lpFeeProfileToV3Fee(lpFeeProfile),
1049
+ recipient: ai.sender,
1050
+ deadline,
1051
+ amountIn: buyWei,
1052
+ amountOutMinimum: 0n,
1053
+ sqrtPriceLimitX96: 0n,
1054
+ });
1055
+ }
1017
1056
  }
1018
1057
  else {
1058
+ // V2 模式:不支持 exactOutput,使用 exactInput
1019
1059
  buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], ai.sender, deadline);
1020
1060
  }
1021
1061
  buyCallData = encodeExecute(effectiveRouter, buyWei, buySwapData);
1022
1062
  }
1023
1063
  else {
1024
1064
  // ✅ ERC20 模式:quoteToken → Token(需要先 approve)
1065
+ // 注意:ERC20 模式暂不支持 exactOutput,保持 exactInput
1025
1066
  const approveData = encodeApproveCall(effectiveRouter, buyWei);
1026
1067
  const swapData = encodeSwapExactTokensForTokensSupportingFee(buyWei, 0n, [quoteToken, tokenAddress], ai.sender, deadline);
1027
1068
  buyCallData = encodeExecuteBatch([quoteToken, effectiveRouter], [0n, 0n], [approveData, swapData]);
1028
1069
  }
1029
- // ✅ 关键修复:使用 buildUserOpWithFixedGas,确保 callGasLimit 与预检查时一致
1030
- // 预检查使用 DEX_BUY_CALL_GAS_LIMIT (650,000),这里也必须使用相同的值
1031
- // 否则 buildUserOpWithState 会动态估算,可能得到更高的值,导致 prefund 不足
1070
+ // ✅ 关键修复:使用 buildUserOpWithFixedGas,确保 callGasLimit 和 gasPrice 与预检查时一致
1071
+ // 预检查使用 DEX_BUY_CALL_GAS_LIMIT (650,000) 和 fixedGasPrice
1072
+ // 这里必须使用相同的值,避免 gasPrice 不一致导致 prefund 不足
1032
1073
  const { userOp: buyUserOp } = await this.aaManager.buildUserOpWithFixedGas({
1033
1074
  ownerWallet: buyerOwners[i],
1034
1075
  sender: ai.sender,
@@ -1038,6 +1079,7 @@ export class AADexSwapExecutor {
1038
1079
  deployed: ai.deployed,
1039
1080
  fixedGas: {
1040
1081
  callGasLimit: DEX_BUY_CALL_GAS_LIMIT,
1082
+ gasPrice: fixedGasPrice, // ✅ 使用统一的 gasPrice
1041
1083
  },
1042
1084
  });
1043
1085
  const signedBuy = await this.aaManager.signUserOp(buyUserOp, buyerOwners[i]);
@@ -20,6 +20,20 @@ interface V3ExactInputSingleParams {
20
20
  amountOutMinimum: bigint;
21
21
  sqrtPriceLimitX96: bigint;
22
22
  }
23
+ /**
24
+ * ✅ V3 exactOutputSingle 参数结构
25
+ * 精确输出模式:固定输出代币数量,输入金额可变(不超过 amountInMaximum)
26
+ */
27
+ export interface V3ExactOutputSingleParams {
28
+ tokenIn: string;
29
+ tokenOut: string;
30
+ fee: number;
31
+ recipient: string;
32
+ deadline: number;
33
+ amountOut: bigint;
34
+ amountInMaximum: bigint;
35
+ sqrtPriceLimitX96: bigint;
36
+ }
23
37
  /**
24
38
  * 编码 unwrapWETH9 调用(将 WOKB 转换为 OKB)
25
39
  */
@@ -33,7 +47,7 @@ export declare function encodeRefundETH(): string;
33
47
  */
34
48
  export declare function encodeV3Multicall(data: string[]): string;
35
49
  /**
36
- * ✅ 编码 V3 买入调用(OKB → Token
50
+ * ✅ 编码 V3 买入调用(OKB → Token)- exactInput 模式
37
51
  *
38
52
  * 对于 AA 模式:直接返回 exactInputSingle 编码(不需要 multicall 包装)
39
53
  * - AA 的 execute 会正确传递 msg.value
@@ -42,6 +56,28 @@ export declare function encodeV3Multicall(data: string[]): string;
42
56
  * 对于 EOA 模式:调用者需要自己用 multicall 包装
43
57
  */
44
58
  export declare function encodeSwapExactETHForTokensV3(params: V3ExactInputSingleParams): string;
59
+ /**
60
+ * ✅ 编码 V3 精确输出买入调用(OKB → Token)- exactOutput 模式
61
+ *
62
+ * 使用 multicall 包装 exactOutputSingle + refundETH:
63
+ * 1. exactOutputSingle: 用最多 amountInMaximum OKB 买入精确 amountOut 个代币
64
+ * 2. refundETH: 将未使用的 OKB 退回给调用者
65
+ *
66
+ * 优势:
67
+ * - 精确控制买入的代币数量
68
+ * - 多余的 OKB 自动退回,不会浪费
69
+ *
70
+ * 适用场景:
71
+ * - 资金利用率模式:卖出后需要精确买回固定数量的代币
72
+ * - 确保每个买家获得精确的代币数量
73
+ *
74
+ * @param params.amountOut - 要买入的精确代币数量
75
+ * @param params.amountInMaximum - 最多花费的 OKB 数量(通常设为分发金额)
76
+ * @param params.refundRecipient - 退款接收者(默认为 recipient)
77
+ */
78
+ export declare function encodeSwapExactOutputForTokensV3(params: V3ExactOutputSingleParams & {
79
+ refundRecipient?: string;
80
+ }): string;
45
81
  /**
46
82
  * ✅ 编码 V3 卖出调用(Token → OKB)
47
83
  * 使用 multicall 包装 exactInputSingle + unwrapWETH9
@@ -23,6 +23,24 @@ const v3RouterIface = new Interface(POTATOSWAP_V3_ROUTER_ABI);
23
23
  function encodeExactInputSingle(params) {
24
24
  return v3RouterIface.encodeFunctionData('exactInputSingle', [params]);
25
25
  }
26
+ /**
27
+ * ✅ 编码 exactOutputSingle 调用(精确输出模式)
28
+ *
29
+ * 使用场景:
30
+ * - 需要精确控制买入代币数量时
31
+ * - 资金利用率模式下,卖出后按比例买回精确数量的代币
32
+ *
33
+ * 优势:
34
+ * - 精确控制每个地址买到的代币数量
35
+ * - 避免因滑点导致买入数量不一致
36
+ *
37
+ * 注意:
38
+ * - 如果需要的 OKB > amountInMaximum,交易会失败
39
+ * - 多余的 OKB 需要通过 refundETH 退回
40
+ */
41
+ function encodeExactOutputSingle(params) {
42
+ return v3RouterIface.encodeFunctionData('exactOutputSingle', [params]);
43
+ }
26
44
  /**
27
45
  * 编码 unwrapWETH9 调用(将 WOKB 转换为 OKB)
28
46
  */
@@ -42,7 +60,7 @@ export function encodeV3Multicall(data) {
42
60
  return v3RouterIface.encodeFunctionData('multicall', [data]);
43
61
  }
44
62
  /**
45
- * ✅ 编码 V3 买入调用(OKB → Token
63
+ * ✅ 编码 V3 买入调用(OKB → Token)- exactInput 模式
46
64
  *
47
65
  * 对于 AA 模式:直接返回 exactInputSingle 编码(不需要 multicall 包装)
48
66
  * - AA 的 execute 会正确传递 msg.value
@@ -55,6 +73,34 @@ export function encodeSwapExactETHForTokensV3(params) {
55
73
  // V3 Router 会自动处理原生币 → WOKB 的转换
56
74
  return encodeExactInputSingle(params);
57
75
  }
76
+ /**
77
+ * ✅ 编码 V3 精确输出买入调用(OKB → Token)- exactOutput 模式
78
+ *
79
+ * 使用 multicall 包装 exactOutputSingle + refundETH:
80
+ * 1. exactOutputSingle: 用最多 amountInMaximum OKB 买入精确 amountOut 个代币
81
+ * 2. refundETH: 将未使用的 OKB 退回给调用者
82
+ *
83
+ * 优势:
84
+ * - 精确控制买入的代币数量
85
+ * - 多余的 OKB 自动退回,不会浪费
86
+ *
87
+ * 适用场景:
88
+ * - 资金利用率模式:卖出后需要精确买回固定数量的代币
89
+ * - 确保每个买家获得精确的代币数量
90
+ *
91
+ * @param params.amountOut - 要买入的精确代币数量
92
+ * @param params.amountInMaximum - 最多花费的 OKB 数量(通常设为分发金额)
93
+ * @param params.refundRecipient - 退款接收者(默认为 recipient)
94
+ */
95
+ export function encodeSwapExactOutputForTokensV3(params) {
96
+ // 1. exactOutputSingle: 买入精确数量的代币
97
+ const swapData = encodeExactOutputSingle(params);
98
+ // 2. refundETH: 退还未使用的 OKB
99
+ // 注意:refundETH 会将多余的原生币退回给 msg.sender(AA 钱包)
100
+ const refundData = encodeRefundETH();
101
+ // 使用 multicall 打包两个调用
102
+ return encodeV3Multicall([swapData, refundData]);
103
+ }
58
104
  /**
59
105
  * ✅ 编码 V3 卖出调用(Token → OKB)
60
106
  * 使用 multicall 包装 exactInputSingle + unwrapWETH9
@@ -7,9 +7,9 @@ import { FLAP_PORTAL, ZERO_ADDRESS, MULTICALL3, VERIFICATION_GAS_LIMIT_DEPLOY, V
7
7
  // 多跳 prefund 估算用的 gas 常量(可配置)
8
8
  const HOP_CALL_GAS_LIMIT = 150000n; // hop 转账操作
9
9
  const PORTAL_BUY_CALL_GAS_LIMIT = 450000n; // Portal 买入操作
10
- // ✅ 增加余额检查 buffer 从 120% 到 180%,确保 gasPrice 波动时也有足够余额
11
- // 预充值使用 200% buffer,这里使用 180% 作为安全阈值
12
- const PREFUND_BUFFER_PERCENT = 180n; // prefund buffer 百分比 (180 = 1.8x)
10
+ // ✅ 增加余额检查 buffer 从 180% 到 250%,确保 gasPrice 波动时也有足够余额
11
+ // 预充值使用 200% buffer,这里使用 250% 作为安全阈值(覆盖更大的 gasPrice 波动)
12
+ const PREFUND_BUFFER_PERCENT = 250n; // prefund buffer 百分比 (250 = 2.5x)
13
13
  import { AAAccountManager, encodeExecute, encodeExecuteBatch, encodeExecuteViaMulticall3 } from './aa-account.js';
14
14
  import { encodeBuyCall, encodeSellCall, encodeBuyCallWithQuote, encodeSellCallWithQuote, encodeApproveCall, PortalQuery, parseOkb, } from './portal-ops.js';
15
15
  import { BundleExecutor } from './bundle.js';
@@ -51,6 +51,8 @@ export interface FixedGasConfig {
51
51
  verificationGasLimitUndeployed?: bigint;
52
52
  /** preVerificationGas(默认用 SDK 常量) */
53
53
  preVerificationGas?: bigint;
54
+ /** ✅ 固定 gasPrice(若不提供,SDK 会调用 getFeeData 获取) */
55
+ gasPrice?: bigint;
54
56
  }
55
57
  /**
56
58
  * XLayer SDK 基础配置
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { ethers } from 'ethers';
16
16
  import { createAAAccountManager, encodeExecute, createWallet } from './aa-account.js';
17
- import { encodeBuyCall, encodeSellCall, PortalQuery } from './portal-ops.js';
17
+ import { encodeBuyCall, encodeSellCall, PortalQuery, lpFeeProfileToV3Fee } from './portal-ops.js';
18
18
  import { encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, DexQuery, } from './dex.js';
19
19
  import { FLAP_PORTAL, WOKB, XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, PARTICLE_BUNDLER_URL, } from './constants.js';
20
20
  import { PROFIT_CONFIG } from '../utils/constants.js';
@@ -77,7 +77,6 @@ function normalizeConfig(cfg) {
77
77
  export async function buildWashOps(params) {
78
78
  const poolType = params.poolType || 'flap';
79
79
  const wokb = params.wrappedOkbAddress || WOKB;
80
- const v3Fee = params.v3Fee || 2500;
81
80
  const deadline = Math.floor(Date.now() / 1000) + 60 * Math.max(1, Number(params.deadlineMinutes ?? 20));
82
81
  const portal = FLAP_PORTAL;
83
82
  const routerAddress = params.routerAddress || '';
@@ -88,15 +87,29 @@ export async function buildWashOps(params) {
88
87
  if (poolType === 'v3' && !routerAddress) {
89
88
  throw new Error('[buildWashOps] poolType=v3 时必须提供 routerAddress');
90
89
  }
91
- if (poolType === 'v3' && !params.v3Fee) {
92
- throw new Error('[buildWashOps] poolType=v3 时必须提供 v3Fee');
93
- }
94
90
  if (params.ownerPrivateKeys.length !== params.buyAmountsOkb.length) {
95
91
  throw new Error('[buildWashOps] 私钥数量与买入金额数量不一致');
96
92
  }
97
93
  // ✅ 归一化配置,填充默认值(与前端 normalizeXLayerAAConfig 保持一致)
98
94
  const config = normalizeConfig(params.config);
99
95
  const aaManager = createAAAccountManager(config);
96
+ // ✅ V3 模式:直接从链上读取 lpFeeProfile 获取正确的 fee(与 bundleGraduateBuy 保持一致)
97
+ // 不再依赖前端传入的 v3Fee 参数,避免前端传入错误值导致交易失败
98
+ let v3Fee = params.v3Fee || 2500;
99
+ if (poolType === 'v3') {
100
+ try {
101
+ const portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
102
+ const tokenState = await portalQuery.getTokenV7(params.tokenAddress);
103
+ const correctV3Fee = lpFeeProfileToV3Fee(tokenState.lpFeeProfile);
104
+ if (correctV3Fee !== v3Fee) {
105
+ console.log(`[buildWashOps] V3 Fee 校正: 前端传入 ${v3Fee}, 链上 lpFeeProfile=${tokenState.lpFeeProfile} → 正确 fee=${correctV3Fee}`);
106
+ }
107
+ v3Fee = correctV3Fee;
108
+ }
109
+ catch (e) {
110
+ console.warn(`[buildWashOps] 读取 lpFeeProfile 失败,使用前端传入的 v3Fee=${v3Fee}:`, e);
111
+ }
112
+ }
100
113
  // ✅ 预先过滤无效私钥并保持索引对应
101
114
  const validEntries = [];
102
115
  for (let i = 0; i < params.ownerPrivateKeys.length; i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.6.60",
3
+ "version": "1.6.63",
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
- }