four-flap-meme-sdk 1.3.46 → 1.3.48

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.
@@ -82,31 +82,30 @@ const V2_ROUTER_ABI = [
82
82
  /**
83
83
  * SwapRouter02 的 V2 方法 ABI (PotatoSwap)
84
84
  *
85
- * 根据官方文档,SwapRouter02 支持完整的 V2 方法:
85
+ * 重要:根据实际 ABI,SwapRouter02 V2 方法只有:
86
86
  * - swapExactTokensForTokens(amountIn, amountOutMin, path[], to)
87
- * - swapExactETHForTokens(amountOutMin, path[], to) - ETH 换代币,自动 wrap
88
- * - swapExactTokensForETH(amountIn, amountOutMin, path[], to) - 代币换 ETH,自动 unwrap
89
87
  * - swapTokensForExactTokens(amountOut, amountInMax, path[], to)
90
- * - swapTokensForExactETH(amountOut, amountInMax, path[], to)
91
- * - swapETHForExactTokens(amountOut, path[], to)
88
+ *
89
+ * 没有 swapExactETHForTokens / swapExactTokensForETH!
90
+ * 必须手动处理 ETH/WETH 转换:
91
+ * - 买入(ETH→Token): wrapETH + swapExactTokensForTokens,通过 multicall 组合
92
+ * - 卖出(Token→ETH): swapExactTokensForTokens(to=ADDRESS_THIS) + unwrapWETH9,通过 multicall 组合
92
93
  */
93
94
  const SWAP_ROUTER02_V2_ABI = [
94
- // V2 交换方法 - 精确输入
95
+ // V2 交换方法(只有 token-to-token)
95
96
  'function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to) external payable returns (uint256 amountOut)',
96
- 'function swapExactETHForTokens(uint256 amountOutMin, address[] calldata path, address to) external payable returns (uint256 amountOut)',
97
- 'function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to) external payable returns (uint256 amountOut)',
98
- // V2 交换方法 - 精确输出
99
97
  'function swapTokensForExactTokens(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to) external payable returns (uint256 amountIn)',
100
- 'function swapTokensForExactETH(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to) external payable returns (uint256 amountIn)',
101
- 'function swapETHForExactTokens(uint256 amountOut, address[] calldata path, address to) external payable returns (uint256 amountIn)',
102
98
  // Multicall - 多种重载
103
99
  'function multicall(uint256 deadline, bytes[] calldata data) external payable returns (bytes[] memory results)',
100
+ 'function multicall(bytes32 previousBlockhash, bytes[] calldata data) external payable returns (bytes[] memory results)',
104
101
  'function multicall(bytes[] calldata data) external payable returns (bytes[] memory results)',
105
- // 辅助方法
102
+ // 辅助方法 - ETH/WETH 转换
106
103
  'function wrapETH(uint256 value) external payable',
107
104
  'function unwrapWETH9(uint256 amountMinimum, address recipient) external payable',
108
105
  'function unwrapWETH9(uint256 amountMinimum) external payable',
109
106
  'function refundETH() external payable',
107
+ // 代币操作
108
+ 'function pull(address token, uint256 value) external payable',
110
109
  'function sweepToken(address token, uint256 amountMinimum, address recipient) external payable',
111
110
  'function sweepToken(address token, uint256 amountMinimum) external payable',
112
111
  ];
@@ -270,20 +269,24 @@ async function buildProfitTransaction(wallet, profitAmountWei, nonce, gasPrice,
270
269
  // V2 直接交易
271
270
  // ============================================================================
272
271
  /**
273
- * 判断是否是 SwapRouter02 (PotatoSwap, PancakeSwap V3 等)
274
- * SwapRouter02 的 V2 方法签名不同,需要特殊处理
272
+ * 判断是否是 SwapRouter02 ( XLayer PotatoSwap)
273
+ *
274
+ * 只有 XLayer PotatoSwap 使用 SwapRouter02 进行 V2 交易
275
+ * BSC/Monad 的 V2 交易使用传统的 V2 Router(带 deadline 参数)
276
+ *
277
+ * SwapRouter02 的 V2 方法签名不同:
278
+ * - swapExactETHForTokens(amountOutMin, path[], to) - 没有 deadline
279
+ * - swapExactTokensForETH(amountIn, amountOutMin, path[], to) - 没有 deadline
275
280
  */
276
281
  function isSwapRouter02(chain, routerAddress) {
277
282
  const chainUpper = chain.toUpperCase();
278
283
  const addrLower = routerAddress.toLowerCase();
279
- // XLayer PotatoSwap SwapRouter02
284
+ // ✅ 只有 XLayer PotatoSwap SwapRouter02 走这个逻辑
280
285
  if (chainUpper === 'XLAYER' && addrLower === DIRECT_ROUTERS.XLAYER.POTATOSWAP_V2.toLowerCase()) {
281
286
  return true;
282
287
  }
283
- // BSC PancakeSwap V3 SwapRouter02
284
- if (chainUpper === 'BSC' && addrLower === DIRECT_ROUTERS.BSC.PANCAKESWAP_V3.toLowerCase()) {
285
- return true;
286
- }
288
+ // BSC V2 交易使用传统 PancakeSwap V2 Router,不走 SwapRouter02 逻辑
289
+ // Monad V2 交易使用传统 V2 Router,不走 SwapRouter02 逻辑
287
290
  return false;
288
291
  }
289
292
  /**
@@ -327,17 +330,19 @@ export async function directV2BatchBuy(params) {
327
330
  let txData;
328
331
  let txValue;
329
332
  if (useSwapRouter02) {
330
- // ✅ SwapRouter02: 使用官方文档的 V2 方法
333
+ // ✅ SwapRouter02: 使用 multicall(deadline, bytes[]) 组合调用
334
+ // ABI 中没有 swapExactETHForTokens,只有 swapExactTokensForTokens
331
335
  if (useNative) {
332
- // ETH -> 代币:使用 swapExactETHForTokens
333
- // msg.value ETH 会自动包装为 WETH
334
- // path 第一个元素必须是 WETH
335
- const swapData = routerIface.encodeFunctionData('swapExactETHForTokens', [
336
+ // ETH -> 代币:直接用 swapExactTokensForTokens(参考之前成功的交易)
337
+ // 之前成功的交易只用了一个 swapExactTokensForTokens,没有 wrapETH
338
+ // SwapRouter02 会自动处理 msg.value 的 ETH
339
+ const swapData = routerIface.encodeFunctionData('swapExactTokensForTokens', [
340
+ amountWei,
336
341
  0n, // amountOutMin
337
342
  path, // path: [WETH, ..., tokenOut]
338
343
  wallet.address, // to
339
344
  ]);
340
- // 使用 multicall 包装,传递 deadline
345
+ // 使用 multicall(uint256 deadline, bytes[]) - 带 deadline 的版本
341
346
  txData = routerIface.encodeFunctionData('multicall(uint256,bytes[])', [
342
347
  deadline,
343
348
  [swapData],
@@ -345,7 +350,7 @@ export async function directV2BatchBuy(params) {
345
350
  txValue = amountWei;
346
351
  }
347
352
  else {
348
- // 代币 -> 代币:直接 swapExactTokensForTokens
353
+ // 代币 -> 代币:也用 multicall 包装
349
354
  const swapData = routerIface.encodeFunctionData('swapExactTokensForTokens', [
350
355
  amountWei,
351
356
  0n, // amountOutMin
@@ -484,25 +489,37 @@ export async function directV2BatchSell(params) {
484
489
  // 卖出交易
485
490
  let txData;
486
491
  if (useSwapRouter02) {
487
- // ✅ SwapRouter02: 使用官方文档的 V2 方法
492
+ // ✅ SwapRouter02: 使用 multicall(deadline, bytes[]) 组合调用
493
+ // ABI 中没有 swapExactTokensForETH,只有 swapExactTokensForTokens
488
494
  if (useNativeOutput) {
489
- // 代币 -> ETH:使用 swapExactTokensForETH
490
- // path 最后一个元素必须是 WETH
491
- // 最终 WETH 会自动解包为 ETH 发送给 to
492
- const swapData = routerIface.encodeFunctionData('swapExactTokensForETH', [
495
+ // 代币 -> ETH:swapExactTokensForTokens + unwrapWETH9
496
+ const multicallData = [];
497
+ // SwapRouter02 特殊地址约定:
498
+ // address(2) = ADDRESS_THIS = Router 合约自己
499
+ const ADDRESS_THIS = '0x0000000000000000000000000000000000000002';
500
+ // 1. swapExactTokensForTokens - Token -> WETH,发送到 Router 自己
501
+ // path 最后一个元素是 WETH
502
+ const swapData = routerIface.encodeFunctionData('swapExactTokensForTokens', [
493
503
  sellAmount,
494
504
  0n, // amountOutMin
495
505
  path, // path: [token, ..., WETH]
496
- wallet.address, // to
506
+ ADDRESS_THIS, // to = Router 自己,WETH 留在 Router 中
497
507
  ]);
498
- // 使用 multicall 包装,传递 deadline
508
+ multicallData.push(swapData);
509
+ // 2. unwrapWETH9 - 将 Router 中的 WETH 解包为 ETH 发送给用户
510
+ const unwrapData = routerIface.encodeFunctionData('unwrapWETH9(uint256,address)', [
511
+ 0n, // amountMinimum
512
+ wallet.address, // recipient = 用户
513
+ ]);
514
+ multicallData.push(unwrapData);
515
+ // 使用 multicall(uint256 deadline, bytes[]) - 带 deadline 的版本
499
516
  txData = routerIface.encodeFunctionData('multicall(uint256,bytes[])', [
500
517
  deadline,
501
- [swapData],
518
+ multicallData,
502
519
  ]);
503
520
  }
504
521
  else {
505
- // 代币 -> 代币:直接 swapExactTokensForTokens
522
+ // 代币 -> 代币:也用 multicall 包装
506
523
  const swapData = routerIface.encodeFunctionData('swapExactTokensForTokens', [
507
524
  sellAmount,
508
525
  0n, // amountOutMin
@@ -297,17 +297,62 @@ export async function batchSellWithBundleMerkle(params) {
297
297
  const useNativeOutput = outputToken === ZERO_ADDRESS;
298
298
  // ✅ 优化:如果前端传入了 nonces,直接使用(跳过 RPC 调用)
299
299
  const presetNonces = config.nonces;
300
- // 并行执行 gasPrice、quoteSellOutputs(Multicall3)和 nonces
301
- const [gasPrice, quotedOutputs, nonces] = await Promise.all([
300
+ const extractProfit = shouldExtractProfit(config);
301
+ // 并行执行 gasPrice quoteSellOutputs(Multicall3)
302
+ const [gasPrice, quotedOutputs] = await Promise.all([
302
303
  resolveGasPrice(provider, config),
303
- quoteSellOutputsWithQuote(readOnlyPortal, tokenAddress, amountsWei, outputToken),
304
- presetNonces && presetNonces.length === wallets.length
305
- ? Promise.resolve(presetNonces) // ✅ 使用前端传入的 nonces
306
- : nonceManager.getNextNoncesForWallets(wallets)
304
+ quoteSellOutputsWithQuote(readOnlyPortal, tokenAddress, amountsWei, outputToken)
307
305
  ]);
308
- if (presetNonces) {
306
+ // 计算利润和 maxRevenueIndex(用于 nonce 分配)
307
+ let totalTokenProfit = 0n;
308
+ let maxRevenueIndex = -1;
309
+ let maxRevenue = 0n;
310
+ if (extractProfit && quotedOutputs.length > 0) {
311
+ for (let i = 0; i < wallets.length; i++) {
312
+ const quoted = quotedOutputs[i];
313
+ if (quoted > 0n) {
314
+ const { profit } = calculateProfit(quoted, config);
315
+ totalTokenProfit += profit;
316
+ if (quoted > maxRevenue) {
317
+ maxRevenue = quoted;
318
+ maxRevenueIndex = i;
319
+ }
320
+ }
321
+ }
322
+ }
323
+ // ✅ 修复:根据是否需要利润交易,统一分配 nonces
324
+ const needProfitTx = extractProfit && totalTokenProfit > 0n && maxRevenueIndex >= 0;
325
+ let nonces;
326
+ let profitNonce;
327
+ if (presetNonces && presetNonces.length === wallets.length) {
328
+ nonces = presetNonces;
309
329
  console.log('🚀 SDK 使用前端传入的 nonces:', presetNonces);
310
330
  }
331
+ else if (needProfitTx) {
332
+ // maxRevenueIndex 钱包需要 2 个连续 nonce(卖出 + 利润)
333
+ const maxRevenueNonces = await nonceManager.getNextNonceBatch(wallets[maxRevenueIndex], 2);
334
+ // 其他钱包各需要 1 个 nonce
335
+ const otherWallets = wallets.filter((_, i) => i !== maxRevenueIndex);
336
+ const otherNonces = otherWallets.length > 0
337
+ ? await nonceManager.getNextNoncesForWallets(otherWallets)
338
+ : [];
339
+ // 组装最终的 nonces 数组(保持原顺序)
340
+ nonces = [];
341
+ let otherIdx = 0;
342
+ for (let i = 0; i < wallets.length; i++) {
343
+ if (i === maxRevenueIndex) {
344
+ nonces.push(maxRevenueNonces[0]); // 卖出交易用第一个 nonce
345
+ }
346
+ else {
347
+ nonces.push(otherNonces[otherIdx++]);
348
+ }
349
+ }
350
+ profitNonce = maxRevenueNonces[1]; // 利润交易用第二个 nonce
351
+ }
352
+ else {
353
+ // 不需要利润交易,所有钱包各 1 个 nonce
354
+ nonces = await nonceManager.getNextNoncesForWallets(wallets);
355
+ }
311
356
  const minOuts = resolveMinOutputs(minOutputAmounts, wallets.length, quotedOutputs);
312
357
  // ✅ 优化:构建未签名交易(这里是本地操作,但仍然并行执行以提高效率)
313
358
  const unsignedList = await Promise.all(portals.map((portal, i) => portal.swapExactInput.populateTransaction({
@@ -330,19 +375,31 @@ export async function batchSellWithBundleMerkle(params) {
330
375
  value: 0n // ✅ 卖出交易不发送原生代币
331
376
  })));
332
377
  signedTxs.push(...signedList);
333
- await appendSellProfitTransaction({
334
- extractProfit: shouldExtractProfit(config),
335
- quotedOutputs,
336
- wallets,
337
- nonceManager,
338
- signedTxs,
339
- chainId,
340
- gasPrice,
341
- config,
342
- outputToken, // 传递 outputToken
343
- useNativeOutput, // ✅ 传递是否原生代币输出
344
- provider // ✅ 传递 provider 用于报价
345
- });
378
+ // ✅ 修复:使用预先分配的 profitNonce 添加利润交易
379
+ if (needProfitTx && profitNonce !== undefined) {
380
+ // ERC20 输出时:获取代币利润等值的原生代币(BNB)报价
381
+ let nativeProfitAmount = totalTokenProfit;
382
+ if (!useNativeOutput && outputToken) {
383
+ nativeProfitAmount = await getTokenToNativeQuote(provider, outputToken, totalTokenProfit, chainId);
384
+ console.log('🔍 SDK ERC20 卖出利润转换: ', ethers.formatEther(totalTokenProfit), ' Token -> ', ethers.formatEther(nativeProfitAmount), ' BNB');
385
+ // 如果报价失败(返回 0),跳过利润提取
386
+ if (nativeProfitAmount === 0n) {
387
+ console.log('🔍 SDK ERC20 卖出利润转换失败,跳过利润提取');
388
+ }
389
+ }
390
+ if (nativeProfitAmount > 0n) {
391
+ const profitTx = await wallets[maxRevenueIndex].signTransaction({
392
+ to: getProfitRecipient(),
393
+ value: nativeProfitAmount,
394
+ nonce: profitNonce,
395
+ gasPrice,
396
+ gasLimit: 23000n,
397
+ chainId,
398
+ type: getTxType(config)
399
+ });
400
+ signedTxs.push(profitTx);
401
+ }
402
+ }
346
403
  nonceManager.clearTemp();
347
404
  return {
348
405
  signedTransactions: signedTxs
@@ -434,19 +491,33 @@ function buildGasLimitList(length, config) {
434
491
  return new Array(length).fill(gasLimit);
435
492
  }
436
493
  /**
437
- * ✅ 优化:批量获取所有钱包的 nonce(单次批量 RPC)
438
- * 使用 getNextNoncesForWallets 批量获取,比 Promise.all 更高效
494
+ * ✅ 修复:明确分配 nonces,避免隐式状态依赖
439
495
  */
440
496
  async function allocateBuyerNonces(buyers, extractProfit, maxIndex, totalProfit, nonceManager) {
441
- // 批量获取所有钱包的初始 nonce(内部会合并 RPC 请求)
442
- const initialNonces = await nonceManager.getNextNoncesForWallets(buyers);
443
- // 如果需要提取利润,maxIndex 钱包需要额外一个 nonce(用于利润转账)
444
- // NonceManager 内部已经缓存了,这里只是递增缓存
445
- if (extractProfit && totalProfit > 0n && maxIndex >= 0 && maxIndex < buyers.length) {
446
- // 再获取一个 nonce 给利润交易(从缓存中获取,不会触发 RPC)
447
- await nonceManager.getNextNonce(buyers[maxIndex]);
497
+ const needProfitTx = extractProfit && totalProfit > 0n && maxIndex >= 0 && maxIndex < buyers.length;
498
+ if (!needProfitTx) {
499
+ // 不需要利润交易,所有钱包各 1 nonce
500
+ return await nonceManager.getNextNoncesForWallets(buyers);
448
501
  }
449
- return initialNonces;
502
+ // 需要利润交易:maxIndex 钱包需要 2 个连续 nonce(买入 + 利润)
503
+ const maxIndexNonces = await nonceManager.getNextNonceBatch(buyers[maxIndex], 2);
504
+ // 其他钱包各需要 1 个 nonce
505
+ const otherBuyers = buyers.filter((_, i) => i !== maxIndex);
506
+ const otherNonces = otherBuyers.length > 0
507
+ ? await nonceManager.getNextNoncesForWallets(otherBuyers)
508
+ : [];
509
+ // 组装最终的 nonces 数组(保持原顺序)
510
+ const nonces = [];
511
+ let otherIdx = 0;
512
+ for (let i = 0; i < buyers.length; i++) {
513
+ if (i === maxIndex) {
514
+ nonces.push(maxIndexNonces[0]); // 买入交易用第一个 nonce
515
+ }
516
+ else {
517
+ nonces.push(otherNonces[otherIdx++]);
518
+ }
519
+ }
520
+ return nonces;
450
521
  }
451
522
  async function signBuyTransactions({ unsignedBuys, buyers, nonces, gasLimits, gasPrice, chainId, config, fundsList, useNativeToken = true // ✅ 默认使用原生代币
452
523
  }) {
@@ -471,7 +542,7 @@ async function appendProfitTransaction({ extractProfit, totalProfit, buyers, max
471
542
  value: totalProfit,
472
543
  nonce: profitNonce,
473
544
  gasPrice,
474
- gasLimit: 21000n,
545
+ gasLimit: 23000n,
475
546
  chainId,
476
547
  type: getTxType(config)
477
548
  });
@@ -562,45 +633,4 @@ function resolveMinOutputs(provided, walletCount, _quotedOutputs) {
562
633
  // 原因:大额交易时 5% 滑点可能不够,导致交易失败
563
634
  return Array(walletCount).fill(0n);
564
635
  }
565
- async function appendSellProfitTransaction({ extractProfit, quotedOutputs, wallets, nonceManager, signedTxs, chainId, gasPrice, config, outputToken, useNativeOutput = true, provider }) {
566
- if (!extractProfit || quotedOutputs.length === 0) {
567
- return;
568
- }
569
- let totalTokenProfit = 0n; // 代币利润
570
- let maxRevenueIndex = -1;
571
- let maxRevenue = 0n;
572
- for (let i = 0; i < wallets.length; i++) {
573
- const quoted = quotedOutputs[i];
574
- if (quoted > 0n) {
575
- const { profit } = calculateProfit(quoted, config);
576
- totalTokenProfit += profit;
577
- if (quoted > maxRevenue) {
578
- maxRevenue = quoted;
579
- maxRevenueIndex = i;
580
- }
581
- }
582
- }
583
- if (totalTokenProfit === 0n || maxRevenueIndex < 0) {
584
- return;
585
- }
586
- // ✅ ERC20 输出:获取代币利润等值的原生代币(BNB)报价
587
- let nativeProfitAmount = totalTokenProfit; // 原生代币输出时直接使用
588
- if (!useNativeOutput && outputToken && provider) {
589
- nativeProfitAmount = await getTokenToNativeQuote(provider, outputToken, totalTokenProfit, chainId);
590
- // 如果报价失败(返回 0),跳过利润提取
591
- if (nativeProfitAmount === 0n) {
592
- return;
593
- }
594
- }
595
- const profitNonce = await nonceManager.getNextNonce(wallets[maxRevenueIndex]);
596
- const profitTx = await wallets[maxRevenueIndex].signTransaction({
597
- to: getProfitRecipient(),
598
- value: nativeProfitAmount, // ✅ 转等值原生代币
599
- nonce: profitNonce,
600
- gasPrice,
601
- gasLimit: 21000n,
602
- chainId,
603
- type: getTxType(config)
604
- });
605
- signedTxs.push(profitTx);
606
- }
636
+ // appendSellProfitTransaction 已内联到 batchSellWithBundleMerkle 中,避免 nonce 竞争问题
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.3.46",
3
+ "version": "1.3.48",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",