four-flap-meme-sdk 1.6.64 → 1.6.66

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.
@@ -17,7 +17,7 @@ import { createAAAccountManager, encodeExecute, createWallet } from './aa-accoun
17
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
- import { FLAP_PORTAL, WOKB, XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, PARTICLE_BUNDLER_URL, } from './constants.js';
20
+ import { FLAP_PORTAL, WOKB, XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, PARTICLE_BUNDLER_URL, POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, } from './constants.js';
21
21
  import { PROFIT_CONFIG } from '../utils/constants.js';
22
22
  import { mapWithConcurrency } from '../utils/concurrency.js';
23
23
  // ============================================================================
@@ -73,11 +73,20 @@ export async function buildBundleBuyOps(params) {
73
73
  const wokb = params.wrappedOkbAddress || WOKB;
74
74
  const deadline = Math.floor(Date.now() / 1000) + 60 * Math.max(1, Number(params.deadlineMinutes ?? 20));
75
75
  const portal = FLAP_PORTAL;
76
- const routerAddress = params.routerAddress || '';
77
- // 参数校验
78
- if ((poolType === 'v2' || poolType === 'v3') && !routerAddress) {
79
- throw new Error(`[buildBundleBuyOps] poolType=${poolType} 时必须提供 routerAddress`);
76
+ // V2/V3 模式使用默认 Router 地址(与 bundle.ts 保持一致)
77
+ let routerAddress = params.routerAddress || '';
78
+ if (poolType === 'v3') {
79
+ if (!routerAddress || routerAddress.toLowerCase() !== POTATOSWAP_V3_ROUTER.toLowerCase()) {
80
+ if (routerAddress && routerAddress !== POTATOSWAP_V3_ROUTER) {
81
+ console.warn(`[buildBundleBuyOps] V3 Router 校正: 前端传入 ${routerAddress} → 使用正确的 ${POTATOSWAP_V3_ROUTER}`);
82
+ }
83
+ routerAddress = POTATOSWAP_V3_ROUTER;
84
+ }
85
+ }
86
+ else if (poolType === 'v2' && !routerAddress) {
87
+ routerAddress = POTATOSWAP_V2_ROUTER;
80
88
  }
89
+ // 参数校验
81
90
  if (params.ownerPrivateKeys.length !== params.buyAmountsOkb.length) {
82
91
  throw new Error('[buildBundleBuyOps] 私钥数量与买入金额数量不一致');
83
92
  }
@@ -240,11 +249,20 @@ export async function buildBundleSellOps(params) {
240
249
  const wokb = params.wrappedOkbAddress || WOKB;
241
250
  const deadline = Math.floor(Date.now() / 1000) + 60 * Math.max(1, Number(params.deadlineMinutes ?? 20));
242
251
  const portal = FLAP_PORTAL;
243
- const routerAddress = params.routerAddress || '';
244
252
  const tokenDecimals = params.tokenDecimals ?? 18;
245
253
  const nonceOffsetMap = params.nonceOffsetMap ?? new Map();
246
- if ((poolType === 'v2' || poolType === 'v3') && !routerAddress) {
247
- throw new Error(`[buildBundleSellOps] poolType=${poolType} 时必须提供 routerAddress`);
254
+ // V2/V3 模式使用默认 Router 地址(与 bundle.ts 保持一致)
255
+ let routerAddress = params.routerAddress || '';
256
+ if (poolType === 'v3') {
257
+ if (!routerAddress || routerAddress.toLowerCase() !== POTATOSWAP_V3_ROUTER.toLowerCase()) {
258
+ if (routerAddress && routerAddress !== POTATOSWAP_V3_ROUTER) {
259
+ console.warn(`[buildBundleSellOps] V3 Router 校正: 前端传入 ${routerAddress} → 使用正确的 ${POTATOSWAP_V3_ROUTER}`);
260
+ }
261
+ routerAddress = POTATOSWAP_V3_ROUTER;
262
+ }
263
+ }
264
+ else if (poolType === 'v2' && !routerAddress) {
265
+ routerAddress = POTATOSWAP_V2_ROUTER;
248
266
  }
249
267
  if (params.ownerPrivateKeys.length !== params.sellAmounts.length) {
250
268
  throw new Error('[buildBundleSellOps] 私钥数量与卖出金额数量不一致');
@@ -13,8 +13,7 @@ const DEX_BUY_CALL_GAS_LIMIT = 650000n; // DEX V3 买入操作
13
13
  const PREFUND_BUFFER_PERCENT = 200n; // prefund buffer 百分比 (200 = 2.0x)
14
14
  import { AAAccountManager, encodeExecute, encodeExecuteBatch, encodeExecuteViaMulticall3 } from './aa-account.js';
15
15
  import { encodeApproveCall, lpFeeProfileToV3Fee, } from './portal-ops.js';
16
- import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, encodeSwapExactTokensForTokensSupportingFee, encodeSwapExactOutputForTokensV3, // ✅ 新增:精确输出买入(V3 exactOutput)
17
- } from './dex.js';
16
+ import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, encodeSwapExactTokensForTokensSupportingFee, } from './dex.js';
18
17
  import { BundleExecutor } from './bundle.js';
19
18
  import { PROFIT_CONFIG, ZERO_ADDRESS } from '../utils/constants.js';
20
19
  const multicallIface = new ethers.Interface([
@@ -204,9 +203,10 @@ export class AADexSwapExecutor {
204
203
  initIfNeeded(buyerSender, !!buyerAi?.deployed, buyerOwner.address);
205
204
  const decimals = await this.bundleExecutor['getErc20Decimals'](tokenAddress);
206
205
  const sellerTokenBal = await this.aaManager.getErc20Balance(tokenAddress, sellerSender);
206
+ // ✅ 当 sellPercent = 100 时直接使用完整余额,避免精度损失
207
207
  const sellAmountWei = sellAmount
208
208
  ? ethers.parseUnits(String(sellAmount), decimals)
209
- : (sellerTokenBal * BigInt(sellPercent)) / 100n;
209
+ : (sellPercent >= 100 ? sellerTokenBal : (sellerTokenBal * BigInt(sellPercent)) / 100n);
210
210
  if (sellAmountWei <= 0n)
211
211
  throw new Error('卖出数量无效');
212
212
  // 授权检查
@@ -472,9 +472,10 @@ export class AADexSwapExecutor {
472
472
  }
473
473
  const decimals = await this.bundleExecutor['getErc20Decimals'](tokenAddress);
474
474
  const sellerTokenBal = await this.aaManager.getErc20Balance(tokenAddress, sellerAi.sender);
475
+ // ✅ 当 sellPercent = 100 时直接使用完整余额,避免精度损失
475
476
  const sellAmountWei = sellAmount
476
477
  ? ethers.parseUnits(String(sellAmount), decimals)
477
- : (sellerTokenBal * BigInt(sellPercent)) / 100n;
478
+ : (sellPercent >= 100 ? sellerTokenBal : (sellerTokenBal * BigInt(sellPercent)) / 100n);
478
479
  let needApprove = false;
479
480
  if (!skipApprovalCheck) {
480
481
  const allowance = await this.aaManager.getErc20Allowance(tokenAddress, sellerAi.sender, effectiveRouter);
@@ -585,19 +586,32 @@ export class AADexSwapExecutor {
585
586
  const totalBuyerPrefundWithBuffer = (useNativeToken && capitalMode)
586
587
  ? buyerAis.reduce((sum, ai) => sum + estimateBuyerPrefundWithBuffer(ai.deployed), 0n)
587
588
  : 0n;
588
- // ✅ 计算利润(不再压缩到“卖出-买入-prefund”上限,按配置直接刮取)
589
- const profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
590
- // 资金充足性提示:不再阻断,记录警告后继续分发
589
+ // ✅ 计算利润(不再压缩到"卖出-买入-prefund"上限,按配置直接刮取)
590
+ let profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
591
+ // ✅ 关键修复:如果分发总额 > 预估卖出所得,按比例缩减分发金额
592
+ // 这样可以避免 seller 需要用自己的 OKB 补贴分发
591
593
  const totalDistributeNeeded = totalBuyWei + totalBuyerPrefundWithBuffer + profitWei;
594
+ let adjustedBuyAmountsWei = buyAmountsWei;
595
+ let adjustedProfitWei = profitWei;
592
596
  if (quotedSellOutWei > 0n && totalDistributeNeeded > quotedSellOutWei) {
593
- console.warn('[AA DEX 批量换手] ⚠️ 预估卖出金额不足,仍继续分发:', {
597
+ // 计算缩减比例,留 5% 安全边际给 seller 的 prefund
598
+ const safeQuoted = (quotedSellOutWei * 95n) / 100n; // 留 5% 给 seller prefund
599
+ const scaleRatio = safeQuoted > 0n ? (safeQuoted * 10000n) / totalDistributeNeeded : 0n;
600
+ // 按比例缩减 buyAmounts
601
+ adjustedBuyAmountsWei = buyAmountsWei.map(w => (w * scaleRatio) / 10000n);
602
+ const adjustedTotalBuyWei = adjustedBuyAmountsWei.reduce((a, b) => a + b, 0n);
603
+ // 按比例缩减 profit
604
+ adjustedProfitWei = (profitWei * scaleRatio) / 10000n;
605
+ console.warn('[AA DEX 批量换手] ⚠️ 分发金额超过预估卖出,已按比例缩减:', {
594
606
  quotedSellOutWei: ethers.formatEther(quotedSellOutWei),
595
- totalBuyWei: ethers.formatEther(totalBuyWei),
596
- totalBuyerPrefund: ethers.formatEther(totalBuyerPrefundWithBuffer),
597
- profitWei: ethers.formatEther(profitWei),
598
- totalNeeded: ethers.formatEther(totalDistributeNeeded),
599
- shortfall: ethers.formatEther(totalDistributeNeeded - quotedSellOutWei),
607
+ originalTotalBuyWei: ethers.formatEther(totalBuyWei),
608
+ adjustedTotalBuyWei: ethers.formatEther(adjustedTotalBuyWei),
609
+ originalProfitWei: ethers.formatEther(profitWei),
610
+ adjustedProfitWei: ethers.formatEther(adjustedProfitWei),
611
+ scaleRatio: `${Number(scaleRatio) / 100}%`,
600
612
  });
613
+ // 更新变量
614
+ profitWei = adjustedProfitWei;
601
615
  }
602
616
  // ✅ 利润在分发阶段通过 AA 内部刮取
603
617
  // ✅ 资金分发逻辑(仅 capitalMode=true 时执行,对应 BSC flapQuickBatchSwapMerkle)
@@ -684,7 +698,7 @@ export class AADexSwapExecutor {
684
698
  // - 原生代币模式:分发金额 = 买入金额 + 买方 prefund
685
699
  // - ERC20 模式:只分发 ERC20 代币,prefund 需要 buyer 自己有 OKB
686
700
  const items = buyerSenders.map((to, i) => {
687
- const buyAmount = buyAmountsWei[i] ?? 0n;
701
+ const buyAmount = adjustedBuyAmountsWei[i] ?? 0n;
688
702
  const prefund = buyerPrefunds[i] ?? 0n;
689
703
  return {
690
704
  to,
@@ -856,7 +870,7 @@ export class AADexSwapExecutor {
856
870
  const chainHops = allGeneratedHopWallets[buyerIdx];
857
871
  const buyerSender = buyerSenders[buyerIdx];
858
872
  const buyerAi = buyerAis[buyerIdx];
859
- const buyAmount = buyAmountsWei[buyerIdx] ?? 0n;
873
+ const buyAmount = adjustedBuyAmountsWei[buyerIdx] ?? 0n;
860
874
  if (buyAmount <= 0n)
861
875
  continue;
862
876
  // ✅ 计算 buyer 的 prefund(用于买入,使用常量)
@@ -997,66 +1011,36 @@ export class AADexSwapExecutor {
997
1011
  });
998
1012
  outOps.push(signedProfitTransfer.userOp);
999
1013
  }
1000
- // ✅ 计算每个买家应该买入的代币数量(用于 V3 exactOutput 模式)
1001
- // 逻辑:卖出 sellAmountWei 个代币 → 买家按比例买回
1002
- // targetTokenAmounts[i] = sellAmountWei * (buyAmountsWei[i] / totalBuyWei)
1003
- //
1004
- // exactOutput 模式优势:
1005
- // 1. 精确控制每个买家买入的代币数量
1006
- // 2. amountInMaximum 设为分发金额,确保不超支
1007
- // 3. 多余的 OKB 通过 refundETH 退回买家钱包
1008
- const targetTokenAmounts = capitalMode && totalBuyWei > 0n
1009
- ? buyAmountsWei.map(buyWei => (sellAmountWei * buyWei) / totalBuyWei)
1010
- : buyAmountsWei.map(() => 0n); // 非资金利用率模式不使用 exactOutput
1011
- console.log('[AA DEX 批量换手] 目标代币数量计算:', {
1012
- sellAmountWei: sellAmountWei.toString(),
1013
- totalBuyWei: ethers.formatEther(totalBuyWei),
1014
- targetTokenAmounts: targetTokenAmounts.map(a => a.toString()),
1015
- useExactOutput: capitalMode && isV3Trade && useNativeToken,
1016
- });
1017
1014
  // Batch Buy ops(分发后再执行)- ✅ 支持 V2/V3,支持 ERC20 稳定币
1015
+ // ⚠️ 使用 exactInput 模式:用分发到的 OKB 尽可能多地买代币
1016
+ // 不使用 exactOutput,因为卖出后价格下跌,可能导致 OKB 不够买回目标数量
1018
1017
  for (let i = 0; i < buyerOwners.length; i++) {
1019
1018
  const ai = buyerAis[i];
1020
- const buyWei = buyAmountsWei[i]; // 分发给买家的 OKB 金额(作为 amountInMaximum)
1021
- const targetTokenOut = targetTokenAmounts[i]; // 目标买入的代币数量
1019
+ const buyWei = adjustedBuyAmountsWei[i]; // 分发给买家的 OKB 金额(已按比例调整)
1022
1020
  let buyCallData;
1023
1021
  if (useNativeToken) {
1024
1022
  // ✅ 原生代币模式:OKB → Token
1023
+ // ⚠️ 重要:使用 exactInput 而不是 exactOutput!
1024
+ // 原因:卖出后价格下跌,exactOutput 需要更多 OKB 买回相同数量代币
1025
+ // 如果 OKB 不够,exactOutput 会 revert,导致买入失败
1026
+ // 解决方案:使用 exactInput,让买家用分到的 OKB 尽可能多地买代币
1025
1027
  let buySwapData;
1026
1028
  if (isV3Trade) {
1027
- // V3 资金利用率模式:使用 exactOutput 精确买入目标代币数量
1028
- // - amountOut: 精确的代币数量(sellAmountWei 按比例分配)
1029
- // - amountInMaximum: 最多花费的 OKB(分发给该买家的金额)
1030
- // - refundETH: 多余的 OKB 自动退回
1031
- if (capitalMode && targetTokenOut > 0n) {
1032
- buySwapData = encodeSwapExactOutputForTokensV3({
1033
- tokenIn: WOKB,
1034
- tokenOut: tokenAddress,
1035
- fee: lpFeeProfileToV3Fee(lpFeeProfile),
1036
- recipient: ai.sender,
1037
- deadline,
1038
- amountOut: targetTokenOut, // 精确买入的代币数量
1039
- amountInMaximum: buyWei, // ✅ 最多花费的 OKB(分发金额)
1040
- sqrtPriceLimitX96: 0n,
1041
- });
1042
- console.log(`[AA DEX V3 exactOutput] Buyer ${i}: 目标代币 ${targetTokenOut.toString()}, 最大花费 ${ethers.formatEther(buyWei)} OKB`);
1043
- }
1044
- else {
1045
- // 非资金利用率模式:使用 exactInput(固定 OKB 买尽可能多的代币)
1046
- buySwapData = encodeSwapExactETHForTokensV3({
1047
- tokenIn: WOKB,
1048
- tokenOut: tokenAddress,
1049
- fee: lpFeeProfileToV3Fee(lpFeeProfile),
1050
- recipient: ai.sender,
1051
- deadline,
1052
- amountIn: buyWei,
1053
- amountOutMinimum: 0n,
1054
- sqrtPriceLimitX96: 0n,
1055
- });
1056
- }
1029
+ // V3 模式:使用 exactInput(固定 OKB 买尽可能多的代币)
1030
+ buySwapData = encodeSwapExactETHForTokensV3({
1031
+ tokenIn: WOKB,
1032
+ tokenOut: tokenAddress,
1033
+ fee: lpFeeProfileToV3Fee(lpFeeProfile),
1034
+ recipient: ai.sender,
1035
+ deadline,
1036
+ amountIn: buyWei,
1037
+ amountOutMinimum: 0n, // 不设最小输出,确保交易成功
1038
+ sqrtPriceLimitX96: 0n,
1039
+ });
1040
+ console.log(`[AA DEX V3 exactInput] Buyer ${i}: 花费 ${ethers.formatEther(buyWei)} OKB 买入代币`);
1057
1041
  }
1058
1042
  else {
1059
- // V2 模式:不支持 exactOutput,使用 exactInput
1043
+ // V2 模式:使用 exactInput
1060
1044
  buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], ai.sender, deadline);
1061
1045
  }
1062
1046
  buyCallData = encodeExecute(effectiveRouter, buyWei, buySwapData);
@@ -16,7 +16,7 @@ import { ethers } from 'ethers';
16
16
  import { createAAAccountManager, encodeExecute, createWallet } from './aa-account.js';
17
17
  import { encodeBuyCall, encodeSellCall, PortalQuery, lpFeeProfileToV3Fee } from './portal-ops.js';
18
18
  import { encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, DexQuery, } from './dex.js';
19
- import { FLAP_PORTAL, WOKB, XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, PARTICLE_BUNDLER_URL, } from './constants.js';
19
+ import { FLAP_PORTAL, WOKB, XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, PARTICLE_BUNDLER_URL, POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, } from './constants.js';
20
20
  import { PROFIT_CONFIG } from '../utils/constants.js';
21
21
  import { mapWithConcurrency } from '../utils/concurrency.js';
22
22
  // ============================================================================
@@ -79,14 +79,26 @@ export async function buildWashOps(params) {
79
79
  const wokb = params.wrappedOkbAddress || WOKB;
80
80
  const deadline = Math.floor(Date.now() / 1000) + 60 * Math.max(1, Number(params.deadlineMinutes ?? 20));
81
81
  const portal = FLAP_PORTAL;
82
- const routerAddress = params.routerAddress || '';
83
- // 参数校验
84
- if (poolType === 'v2' && !routerAddress) {
85
- throw new Error('[buildWashOps] poolType=v2 时必须提供 routerAddress');
82
+ // V2/V3 模式使用默认 Router 地址(与 bundle.ts 保持一致)
83
+ // 避免前端传入错误地址导致交易失败
84
+ let routerAddress = params.routerAddress || '';
85
+ if (poolType === 'v3') {
86
+ // V3 模式:强制使用正确的 PotatoSwap V3 Router
87
+ if (!routerAddress || routerAddress.toLowerCase() !== POTATOSWAP_V3_ROUTER.toLowerCase()) {
88
+ if (routerAddress && routerAddress !== POTATOSWAP_V3_ROUTER) {
89
+ console.warn(`[buildWashOps] V3 Router 校正: 前端传入 ${routerAddress} → 使用正确的 ${POTATOSWAP_V3_ROUTER}`);
90
+ }
91
+ routerAddress = POTATOSWAP_V3_ROUTER;
92
+ }
86
93
  }
87
- if (poolType === 'v3' && !routerAddress) {
88
- throw new Error('[buildWashOps] poolType=v3 时必须提供 routerAddress');
94
+ else if (poolType === 'v2') {
95
+ // V2 模式:使用默认 PotatoSwap V2 Router(如果未提供)
96
+ if (!routerAddress) {
97
+ routerAddress = POTATOSWAP_V2_ROUTER;
98
+ console.log(`[buildWashOps] V2 Router 使用默认值: ${routerAddress}`);
99
+ }
89
100
  }
101
+ // 参数校验
90
102
  if (params.ownerPrivateKeys.length !== params.buyAmountsOkb.length) {
91
103
  throw new Error('[buildWashOps] 私钥数量与买入金额数量不一致');
92
104
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.6.64",
3
+ "version": "1.6.66",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",