four-flap-meme-sdk 1.3.13 → 1.3.15

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.
@@ -1,10 +1,15 @@
1
- import { ethers, Wallet } from 'ethers';
1
+ import { ethers, Wallet, Contract, Interface } from 'ethers';
2
2
  import { NonceManager, getOptimizedGasPrice } from '../../utils/bundle-helpers.js';
3
3
  import { ADDRESSES } from '../../utils/constants.js';
4
4
  import { FourClient, buildLoginMessage } from '../../clients/four.js';
5
5
  import { getErrorMessage, getTxType, getGasPriceConfig, shouldExtractProfit, calculateProfit, getProfitRecipient } from './config.js';
6
6
  import { batchCheckAllowances } from '../../utils/erc20.js';
7
7
  import { trySell } from '../tm.js';
8
+ import Helper3Abi from '../../abis/TokenManagerHelper3.json' with { type: 'json' };
9
+ const MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
10
+ const MULTICALL3_ABI = [
11
+ 'function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) external payable returns (tuple(bool success, bytes returnData)[])'
12
+ ];
8
13
  const TM2_ABI = [
9
14
  'function createToken(bytes args, bytes signature) payable',
10
15
  'function buyTokenAMAP(uint256 origin, address token, address to, uint256 funds, uint256 minAmount) payable',
@@ -259,7 +264,7 @@ async function executeBuyFlow(options) {
259
264
  const extractProfit = shouldExtractProfit(config);
260
265
  const analysis = analyzeBuyFunds(buyAmounts, config, extractProfit);
261
266
  const finalGasLimit = getGasLimit(config);
262
- // ✅ 优化:并行执行 populateBuyTransactions 和获取 nonces
267
+ // ✅ 优化:并行执行 populateBuyTransactions 和批量获取 nonces(JSON-RPC 批量请求)
263
268
  const [unsignedBuys, nonces] = await Promise.all([
264
269
  populateBuyTransactions({
265
270
  buyers: wallets,
@@ -267,7 +272,7 @@ async function executeBuyFlow(options) {
267
272
  tokenAddress,
268
273
  fundsList: analysis.fundsList
269
274
  }),
270
- Promise.all(wallets.map((wallet) => nonceManager.getNextNonce(wallet)))
275
+ nonceManager.getNextNoncesForWallets(wallets) // ✅ 批量获取 nonce
271
276
  ]);
272
277
  // ✅ 如果需要利润转账,预先获取 payer 的下一个 nonce
273
278
  let profitNonce;
@@ -309,7 +314,7 @@ async function executeSellFlow(options) {
309
314
  const amountsWei = sellAmounts.map((amount) => ethers.parseUnits(amount, 18));
310
315
  const finalGasLimit = getGasLimit(config);
311
316
  const extractProfit = shouldExtractProfit(config);
312
- // ✅ 优化:并行执行 resolveSellOutputs、ensureSellerBalances、ensureSellAllowances 和获取 nonces
317
+ // ✅ 优化:并行执行 resolveSellOutputs、ensureSellerBalances、ensureSellAllowances 和批量获取 nonces
313
318
  const [sellOutputs, , , nonces] = await Promise.all([
314
319
  resolveSellOutputs({
315
320
  minOutputAmounts,
@@ -326,7 +331,7 @@ async function executeSellFlow(options) {
326
331
  amountsWei,
327
332
  spender: contractAddress
328
333
  }),
329
- Promise.all(wallets.map((wallet) => nonceManager.getNextNonce(wallet)))
334
+ nonceManager.getNextNoncesForWallets(wallets) // ✅ 批量获取 nonce(JSON-RPC 批量请求)
330
335
  ]);
331
336
  // ✅ 构建未签名交易
332
337
  const unsignedSells = await populateSellTransactions({
@@ -397,24 +402,73 @@ function buildBuyMetadata(extractProfit, totalBuyAmount, totalProfit, buyerCount
397
402
  buyerCount
398
403
  };
399
404
  }
405
+ /**
406
+ * ✅ 使用 Multicall3 批量获取卖出报价(单次 RPC)
407
+ * 比 Promise.all 更高效,N 个报价只需 1 次网络请求
408
+ */
400
409
  async function resolveSellOutputs(params) {
401
410
  const { minOutputAmounts, sellers, amountsWei, rpcUrl, tokenAddress } = params;
411
+ // 如果已提供 minOutputAmounts,直接使用,跳过报价查询
402
412
  if (minOutputAmounts && minOutputAmounts.length === sellers.length) {
403
413
  const minOuts = minOutputAmounts.map((m) => (typeof m === 'string' ? ethers.parseEther(m) : m));
404
414
  const quotedOutputs = minOuts.map((value) => (value * 100n) / 95n);
405
415
  return { minOuts, quotedOutputs };
406
416
  }
407
- const quotedOutputs = await Promise.all(amountsWei.map(async (amount) => {
417
+ // 如果只有 1 个,直接调用(避免 multicall 开销)
418
+ if (amountsWei.length === 1) {
408
419
  try {
409
- const result = await trySell('BSC', rpcUrl, tokenAddress, amount);
410
- return result.funds;
420
+ const result = await trySell('BSC', rpcUrl, tokenAddress, amountsWei[0]);
421
+ const quotedOutputs = [result.funds];
422
+ const minOuts = quotedOutputs.map((quote) => (quote * 95n) / 100n);
423
+ return { minOuts, quotedOutputs };
411
424
  }
412
- catch (error) {
413
- return 0n;
425
+ catch {
426
+ return { minOuts: [0n], quotedOutputs: [0n] };
414
427
  }
428
+ }
429
+ // ✅ 使用 Multicall3 批量获取报价
430
+ const provider = new ethers.JsonRpcProvider(rpcUrl, { chainId: 56, name: 'BSC' });
431
+ const helper3Address = ADDRESSES.BSC.TokenManagerHelper3;
432
+ const helper3Iface = new Interface(Helper3Abi);
433
+ const multicall = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
434
+ // 构建批量调用
435
+ const calls = amountsWei.map(amount => ({
436
+ target: helper3Address,
437
+ allowFailure: true, // 允许单个失败
438
+ callData: helper3Iface.encodeFunctionData('trySell', [tokenAddress, amount])
415
439
  }));
416
- const minOuts = quotedOutputs.map((quote) => (quote * 95n) / 100n);
417
- return { minOuts, quotedOutputs };
440
+ try {
441
+ const results = await multicall.aggregate3.staticCall(calls);
442
+ const quotedOutputs = results.map((r) => {
443
+ if (r.success && r.returnData && r.returnData !== '0x') {
444
+ try {
445
+ const decoded = helper3Iface.decodeFunctionResult('trySell', r.returnData);
446
+ // trySell 返回 (tokenManager, quote, funds, fee),我们需要 funds
447
+ return BigInt(decoded.funds);
448
+ }
449
+ catch {
450
+ return 0n;
451
+ }
452
+ }
453
+ return 0n;
454
+ });
455
+ const minOuts = quotedOutputs.map((quote) => (quote * 95n) / 100n);
456
+ return { minOuts, quotedOutputs };
457
+ }
458
+ catch {
459
+ // Multicall 失败,回退到并行调用
460
+ const quotedOutputs = await Promise.all(amountsWei.map(async (amount) => {
461
+ try {
462
+ const result = await trySell('BSC', rpcUrl, tokenAddress, amount);
463
+ return result.funds;
464
+ }
465
+ catch {
466
+ return 0n;
467
+ }
468
+ }));
469
+ const minOuts = quotedOutputs.map((quote) => (quote * 95n) / 100n);
470
+ return { minOuts, quotedOutputs };
471
+ }
418
472
  }
419
473
  async function ensureSellerBalances(params) {
420
474
  const { provider, tokenAddress, sellers, amountsWei } = params;
@@ -1,8 +1,12 @@
1
- import { ethers, Wallet, JsonRpcProvider, Contract } from 'ethers';
1
+ import { ethers, Wallet, JsonRpcProvider, Contract, Interface } from 'ethers';
2
2
  import { NonceManager, getOptimizedGasPrice } from '../../utils/bundle-helpers.js';
3
3
  import { ADDRESSES } from '../../utils/constants.js';
4
4
  import { getTxType, getGasPriceConfig, shouldExtractProfit, calculateProfit, calculateBatchProfit, getProfitRecipient } from './config.js';
5
5
  import { batchCheckAllowances } from '../../utils/erc20.js';
6
+ const MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
7
+ const MULTICALL3_ABI = [
8
+ 'function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) external payable returns (tuple(bool success, bytes returnData)[])'
9
+ ];
6
10
  // 常量
7
11
  const WBNB_ADDRESS = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
8
12
  const FLAT_FEE = ethers.parseEther('0.0001'); // 合约固定手续费
@@ -113,6 +117,60 @@ async function buildV3MultiHopTransactions(proxies, wallets, lpAddresses, exactT
113
117
  return proxy.swapV3MultiHop.populateTransaction(lpAddresses, exactTokenIn, amountsWei[i], minOuts[i], wallets[i].address, { value: txValue });
114
118
  }));
115
119
  }
120
+ /**
121
+ * ✅ 使用 Multicall3 批量获取 V2 报价(单次 RPC)
122
+ */
123
+ async function batchGetV2Quotes(provider, amountsWei, v2Path) {
124
+ if (amountsWei.length === 0)
125
+ return [];
126
+ // 如果只有 1 个,直接调用(避免 multicall 开销)
127
+ if (amountsWei.length === 1) {
128
+ try {
129
+ const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
130
+ const amounts = await v2Router.getAmountsOut(amountsWei[0], v2Path);
131
+ return [amounts[amounts.length - 1]];
132
+ }
133
+ catch {
134
+ return [0n];
135
+ }
136
+ }
137
+ try {
138
+ const v2RouterIface = new Interface(PANCAKE_V2_ROUTER_ABI);
139
+ const multicall = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
140
+ const calls = amountsWei.map(amount => ({
141
+ target: PANCAKE_V2_ROUTER_ADDRESS,
142
+ allowFailure: true,
143
+ callData: v2RouterIface.encodeFunctionData('getAmountsOut', [amount, v2Path])
144
+ }));
145
+ const results = await multicall.aggregate3.staticCall(calls);
146
+ return results.map((r) => {
147
+ if (r.success && r.returnData && r.returnData !== '0x') {
148
+ try {
149
+ const decoded = v2RouterIface.decodeFunctionResult('getAmountsOut', r.returnData);
150
+ const amounts = decoded[0];
151
+ return amounts[amounts.length - 1];
152
+ }
153
+ catch {
154
+ return 0n;
155
+ }
156
+ }
157
+ return 0n;
158
+ });
159
+ }
160
+ catch {
161
+ // Multicall 失败,回退到并行调用
162
+ return await Promise.all(amountsWei.map(async (amount) => {
163
+ try {
164
+ const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
165
+ const amounts = await v2Router.getAmountsOut(amount, v2Path);
166
+ return amounts[amounts.length - 1];
167
+ }
168
+ catch {
169
+ return 0n;
170
+ }
171
+ }));
172
+ }
173
+ }
116
174
  // ==================== 主要导出函数 ====================
117
175
  /**
118
176
  * 授权代币给 PancakeSwapProxy
@@ -245,11 +303,11 @@ export async function fourPancakeProxyBatchBuyMerkle(params) {
245
303
  const actualAmountsWei = remainingAmounts; // 扣除利润后用于购买的金额
246
304
  const finalGasLimit = getGasLimit(config);
247
305
  const nonceManager = new NonceManager(provider);
248
- // ✅ 优化:并行获取 gasPrice、tokenDecimals 和 nonces
306
+ // ✅ 优化:并行获取 gasPrice、tokenDecimals 和 nonces(JSON-RPC 批量请求)
249
307
  const [gasPrice, tokenDecimals, nonces] = await Promise.all([
250
308
  getOptimizedGasPrice(provider, getGasPriceConfig(config)),
251
309
  getTokenDecimals(tokenAddress, provider),
252
- Promise.all(buyers.map(w => nonceManager.getNextNonce(w)))
310
+ nonceManager.getNextNoncesForWallets(buyers) // ✅ 批量获取 nonce
253
311
  ]);
254
312
  // ✅ 如果需要利润转账,预先获取 nonce
255
313
  let profitNonce;
@@ -343,12 +401,12 @@ export async function fourPancakeProxyBatchSellMerkle(params) {
343
401
  const finalGasLimit = getGasLimit(config);
344
402
  const nonceManager = new NonceManager(provider);
345
403
  const extractProfit = shouldExtractProfit(config);
346
- // ✅ 优化:并行获取 gasPrice、tokenDecimals、allowances 和 nonces
404
+ // ✅ 优化:并行获取 gasPrice、tokenDecimals、allowances 和 nonces(JSON-RPC 批量请求)
347
405
  const [gasPrice, tokenDecimals, allowances, nonces] = await Promise.all([
348
406
  getOptimizedGasPrice(provider, getGasPriceConfig(config)),
349
407
  getTokenDecimals(tokenAddress, provider),
350
408
  batchCheckAllowances(provider, tokenAddress, sellers.map(w => w.address), pancakeProxyAddress),
351
- Promise.all(sellers.map(w => nonceManager.getNextNonce(w)))
409
+ nonceManager.getNextNoncesForWallets(sellers) // ✅ 批量获取 nonce
352
410
  ]);
353
411
  const amountsWei = sellAmounts.map(a => ethers.parseUnits(a, tokenDecimals));
354
412
  // 找出需要授权的钱包索引
@@ -368,20 +426,19 @@ export async function fourPancakeProxyBatchSellMerkle(params) {
368
426
  let minOuts;
369
427
  let quotedOutputs;
370
428
  if (params.minOutputAmounts && params.minOutputAmounts.length === sellers.length) {
371
- // 用户提供了 minOutputAmounts
429
+ // 用户提供了 minOutputAmounts,跳过报价查询
372
430
  minOuts = params.minOutputAmounts.map(m => typeof m === 'string' ? ethers.parseEther(m) : m);
373
431
  quotedOutputs = minOuts.map(m => m * 100n / 95n); // 反推预期收益
374
432
  }
375
433
  else {
376
- // ✅ 自动调用 PancakeSwap 获取报价
377
- quotedOutputs = await Promise.all(amountsWei.map(async (amount) => {
378
- try {
379
- if (routeType === 'v2') {
380
- const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
381
- const amounts = await v2Router.getAmountsOut(amount, params.v2Path);
382
- return amounts[amounts.length - 1];
383
- }
384
- else if (routeType === 'v3-single') {
434
+ // ✅ 使用 Multicall3 批量获取报价(仅 V2 路由支持)
435
+ if (routeType === 'v2' && params.v2Path && params.v2Path.length >= 2) {
436
+ quotedOutputs = await batchGetV2Quotes(provider, amountsWei, params.v2Path);
437
+ }
438
+ else if (routeType === 'v3-single') {
439
+ // V3 单跳:并行调用(V3 Quoter non-view 函数,不支持 Multicall3)
440
+ quotedOutputs = await Promise.all(amountsWei.map(async (amount) => {
441
+ try {
385
442
  const quoter = new Contract(PANCAKE_V3_QUOTER_ADDRESS, PANCAKE_V3_QUOTER_ABI, provider);
386
443
  const result = await quoter.quoteExactInputSingle.staticCall({
387
444
  tokenIn: tokenAddress,
@@ -392,20 +449,18 @@ export async function fourPancakeProxyBatchSellMerkle(params) {
392
449
  });
393
450
  return result[0];
394
451
  }
395
- else {
396
- // v3-multi 使用 v2 备选
397
- if (params.v2Path && params.v2Path.length >= 2) {
398
- const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
399
- const amounts = await v2Router.getAmountsOut(amount, params.v2Path);
400
- return amounts[amounts.length - 1];
401
- }
452
+ catch {
402
453
  return 0n;
403
454
  }
404
- }
405
- catch (error) {
406
- return 0n;
407
- }
408
- }));
455
+ }));
456
+ }
457
+ else if (routeType === 'v3-multi' && params.v2Path && params.v2Path.length >= 2) {
458
+ // V3 多跳:使用 V2 备选路由
459
+ quotedOutputs = await batchGetV2Quotes(provider, amountsWei, params.v2Path);
460
+ }
461
+ else {
462
+ quotedOutputs = new Array(amountsWei.length).fill(0n);
463
+ }
409
464
  // minOuts 设为预期收益的 95%(滑点保护)
410
465
  minOuts = quotedOutputs.map(q => q * 95n / 100n);
411
466
  }
@@ -94,15 +94,32 @@ export async function fourBundleBuyFirstMerkle(params) {
94
94
  sellerNonceCount++; // 授权交易
95
95
  if (extractProfit)
96
96
  sellerNonceCount++; // 利润交易
97
- const [sellResult, buyUnsigned, sellUnsigned, buyerNonces, sellerNonces] = await Promise.all([
97
+ // 优化:使用批量 nonce 获取(JSON-RPC 批量请求)
98
+ // 同一地址时使用 getNextNonceBatch 获取连续 nonce
99
+ // 不同地址时使用 getNextNoncesForWallets 一次性获取两个地址的初始 nonce
100
+ const getNoncesPromise = sameAddress
101
+ ? nonceManager.getNextNonceBatch(seller, buyerNonceCount + sellerNonceCount)
102
+ .then(nonces => ({ buyerNonces: [], sellerNonces: nonces }))
103
+ : (async () => {
104
+ // ✅ 优化:一次性获取两个地址的初始 nonce(单次网络往返)
105
+ const initialNonces = await nonceManager.getNextNoncesForWallets([buyer, seller]);
106
+ // buyer 只需要 1 个 nonce
107
+ const buyerNonces = [initialNonces[0]];
108
+ // seller 需要 sellerNonceCount 个连续 nonce,从初始 nonce 开始
109
+ const sellerNonces = Array.from({ length: sellerNonceCount }, (_, i) => initialNonces[1] + i);
110
+ // 更新 NonceManager 缓存
111
+ for (let i = 1; i < sellerNonceCount; i++) {
112
+ await nonceManager.getNextNonce(seller); // 递增缓存
113
+ }
114
+ return { buyerNonces, sellerNonces };
115
+ })();
116
+ const [sellResult, buyUnsigned, sellUnsigned, noncesResult] = await Promise.all([
98
117
  trySell('BSC', config.rpcUrl, tokenAddress, sellAmountWei),
99
118
  tmBuyer.buyTokenAMAP.populateTransaction(0n, tokenAddress, buyer.address, buyerFundsWei, minBuyAmount, { value: buyerFundsWei }),
100
119
  tmSeller.sellToken.populateTransaction(0n, tokenAddress, sellAmountWei, 0n), // minSellFunds 后面更新
101
- sameAddress
102
- ? Promise.resolve([]) // 同一地址,后面统一处理
103
- : nonceManager.getNextNonceBatch(buyer, buyerNonceCount),
104
- nonceManager.getNextNonceBatch(seller, sameAddress ? (buyerNonceCount + sellerNonceCount) : sellerNonceCount)
120
+ getNoncesPromise
105
121
  ]);
122
+ const { buyerNonces, sellerNonces } = noncesResult;
106
123
  const estimatedSellFunds = sellResult.funds;
107
124
  const minSellFunds = (estimatedSellFunds * BigInt(10000 - slippageBps)) / 10000n;
108
125
  const profitAmount = extractProfit ? (estimatedSellFunds * BigInt(profitRateBps)) / 10000n : 0n;
@@ -74,12 +74,26 @@ export async function fourBundleSwapMerkle(params) {
74
74
  sellerNonceCount++; // 授权交易
75
75
  if (extractProfit)
76
76
  sellerNonceCount++; // 利润交易
77
- const [sellUnsigned, buyUnsigned, sellerNonces, buyerNonce] = await Promise.all([
77
+ // 优化:使用批量 nonce 获取(JSON-RPC 批量请求)
78
+ // 先一次性获取 seller 和 buyer 的初始 nonce,然后分配连续 nonce
79
+ const [sellUnsigned, buyUnsigned, noncesResult] = await Promise.all([
78
80
  tmSeller.sellToken.populateTransaction(0n, tokenAddress, sellAmountWei, 0n),
79
81
  tmBuyer.buyTokenAMAP.populateTransaction(0n, tokenAddress, buyer.address, buyerFunds, 0n, { value: buyerFunds }),
80
- nonceManager.getNextNonceBatch(seller, sellerNonceCount),
81
- nonceManager.getNextNonce(buyer)
82
+ (async () => {
83
+ // ✅ 一次性获取两个地址的初始 nonce(单次网络往返)
84
+ const initialNonces = await nonceManager.getNextNoncesForWallets([seller, buyer]);
85
+ // seller 需要 sellerNonceCount 个连续 nonce
86
+ const sellerNonces = Array.from({ length: sellerNonceCount }, (_, i) => initialNonces[0] + i);
87
+ // buyer 只需要 1 个 nonce
88
+ const buyerNonce = initialNonces[1];
89
+ // 更新 NonceManager 缓存(seller 需要额外的 nonce)
90
+ for (let i = 1; i < sellerNonceCount; i++) {
91
+ await nonceManager.getNextNonce(seller); // 递增缓存
92
+ }
93
+ return { sellerNonces, buyerNonce };
94
+ })()
82
95
  ]);
96
+ const { sellerNonces, buyerNonce } = noncesResult;
83
97
  // 分配 nonces
84
98
  let idx = 0;
85
99
  let approvalNonce;
@@ -509,6 +509,13 @@ export async function sweepWithBundleMerkle(params) {
509
509
  const nonces = await nonceManager.getNextNonceBatch(payerWallet, 2);
510
510
  payerProfitNonce = nonces[1]; // 利润交易的 nonce
511
511
  }
512
+ // ✅ 优化:批量获取所有钱包的 nonce(JSON-RPC 批量请求)
513
+ // 过滤出需要归集的钱包
514
+ const walletsToSweep = wallets.filter((_, i) => sweepAmounts[i] > 0n && i !== maxSweepIndex);
515
+ const nonces = walletsToSweep.length > 0
516
+ ? await nonceManager.getNextNoncesForWallets(walletsToSweep)
517
+ : [];
518
+ let nonceIdx = 0;
512
519
  const txPromises = wallets.map(async (w, i) => {
513
520
  const toSend = sweepAmounts[i];
514
521
  if (toSend <= 0n)
@@ -523,10 +530,14 @@ export async function sweepWithBundleMerkle(params) {
523
530
  actualToSend = toSend; // 其他钱包不扣利润,归集全部
524
531
  }
525
532
  }
526
- // ✅ 支付者使用预留的第一个 nonce,其他钱包正常获取
527
- const nonce = (i === maxSweepIndex && payerProfitNonce !== undefined)
528
- ? payerProfitNonce - 1 // 使用预留的第一个 nonce
529
- : await nonceManager.getNextNonce(w);
533
+ // ✅ 支付者使用预留的第一个 nonce,其他钱包使用批量获取的 nonce
534
+ let nonce;
535
+ if (i === maxSweepIndex && payerProfitNonce !== undefined) {
536
+ nonce = payerProfitNonce - 1; // 使用预留的第一个 nonce
537
+ }
538
+ else {
539
+ nonce = nonces[nonceIdx++];
540
+ }
530
541
  const mainTx = await w.signTransaction({
531
542
  to: target,
532
543
  value: actualToSend,
@@ -652,6 +663,13 @@ export async function sweepWithBundleMerkle(params) {
652
663
  const nonces = await nonceManager.getNextNonceBatch(payerWallet, 2);
653
664
  payerProfitNonce = nonces[1]; // 利润交易的 nonce
654
665
  }
666
+ // ✅ 优化:批量获取所有钱包的 nonce(JSON-RPC 批量请求)
667
+ // 过滤出需要归集的钱包(排除支付者,因为支付者已经预留了 nonce)
668
+ const walletsToSweepErc20 = wallets.filter((_, i) => sweepAmounts[i] > 0n && i !== maxSweepIndex);
669
+ const noncesErc20 = walletsToSweepErc20.length > 0
670
+ ? await nonceManager.getNextNoncesForWallets(walletsToSweepErc20)
671
+ : [];
672
+ let nonceIdxErc20 = 0;
655
673
  const txPromises = wallets.map(async (w, i) => {
656
674
  const toSend = sweepAmounts[i];
657
675
  if (toSend <= 0n)
@@ -666,10 +684,14 @@ export async function sweepWithBundleMerkle(params) {
666
684
  actualToSend = toSend; // 其他钱包不扣利润,归集全部
667
685
  }
668
686
  }
669
- // ✅ 支付者使用预留的第一个 nonce,其他钱包正常获取
670
- const nonce = (i === maxSweepIndex && payerProfitNonce !== undefined)
671
- ? payerProfitNonce - 1 // 使用预留的第一个 nonce
672
- : await nonceManager.getNextNonce(w);
687
+ // ✅ 支付者使用预留的第一个 nonce,其他钱包使用批量获取的 nonce
688
+ let nonce;
689
+ if (i === maxSweepIndex && payerProfitNonce !== undefined) {
690
+ nonce = payerProfitNonce - 1; // 使用预留的第一个 nonce
691
+ }
692
+ else {
693
+ nonce = noncesErc20[nonceIdxErc20++];
694
+ }
673
695
  const data = iface.encodeFunctionData('transfer', [target, actualToSend]);
674
696
  const mainTx = await w.signTransaction({
675
697
  to: tokenAddress,
@@ -1,8 +1,12 @@
1
- import { ethers, Wallet, JsonRpcProvider, Contract } from 'ethers';
1
+ import { ethers, Wallet, JsonRpcProvider, Contract, Interface } from 'ethers';
2
2
  import { NonceManager, getOptimizedGasPrice } from '../../utils/bundle-helpers.js';
3
3
  import { ADDRESSES } from '../../utils/constants.js';
4
4
  import { CHAIN_ID_MAP, getTxType, getGasPriceConfig, shouldExtractProfit, calculateProfit, calculateBatchProfit, getProfitRecipient } from './config.js';
5
5
  import { batchCheckAllowances } from '../../utils/erc20.js';
6
+ const MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
7
+ const MULTICALL3_ABI = [
8
+ 'function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) external payable returns (tuple(bool success, bytes returnData)[])'
9
+ ];
6
10
  // 常量
7
11
  const WBNB_ADDRESS = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
8
12
  const FLAT_FEE = ethers.parseEther('0.0001'); // 合约固定手续费
@@ -239,7 +243,7 @@ export async function pancakeProxyBatchBuyMerkle(params) {
239
243
  const originalAmountsWei = buyAmounts.map(amount => ethers.parseEther(amount));
240
244
  const { totalProfit, remainingAmounts } = calculateBatchProfit(originalAmountsWei, config);
241
245
  const maxFundsIndex = findMaxAmountIndex(originalAmountsWei);
242
- // ✅ 优化:第一批并行 - gasPrice、tokenDecimals、nonces
246
+ // ✅ 优化:第一批并行 - gasPrice、tokenDecimals、nonces(JSON-RPC 批量请求)
243
247
  const [gasPrice, tokenDecimals, nonces] = await Promise.all([
244
248
  getOptimizedGasPrice(provider, getGasPriceConfig(config)),
245
249
  getTokenDecimals(tokenAddress, provider),
@@ -307,7 +311,7 @@ export async function pancakeProxyBatchSellMerkle(params) {
307
311
  const finalGasLimit = getGasLimit(config);
308
312
  const extractProfit = shouldExtractProfit(config);
309
313
  const nonceManager = new NonceManager(provider);
310
- // ✅ 优化:第一批并行 - gasPrice、tokenDecimals、allowances、nonces
314
+ // ✅ 优化:第一批并行 - gasPrice、tokenDecimals、allowances、nonces(JSON-RPC 批量请求)
311
315
  const [gasPrice, tokenDecimals, , nonces] = await Promise.all([
312
316
  getOptimizedGasPrice(provider, getGasPriceConfig(config)),
313
317
  getTokenDecimals(tokenAddress, provider),
@@ -317,7 +321,7 @@ export async function pancakeProxyBatchSellMerkle(params) {
317
321
  owners: sellers.map(w => w.address),
318
322
  spender: ADDRESSES.BSC.PancakeProxy
319
323
  }),
320
- Promise.all(sellers.map(wallet => nonceManager.getNextNonce(wallet)))
324
+ nonceManager.getNextNoncesForWallets(sellers) // ✅ 批量获取 nonce
321
325
  ]);
322
326
  const amountsWei = sellAmounts.map(amount => ethers.parseUnits(amount, tokenDecimals));
323
327
  // ✅ 优化:第二批并行 - resolveSellOutputs 和 buildSellTransactions
@@ -422,14 +426,14 @@ async function buildBuyTransactions({ routeType, params, proxies, wallets, token
422
426
  throw new Error(`Unsupported routeType: ${routeType}`);
423
427
  }
424
428
  /**
425
- * ✅ 优化:并行获取所有钱包的 nonce
429
+ * ✅ 优化:批量获取所有钱包的 nonce(JSON-RPC 批量请求)
426
430
  */
427
431
  async function allocateProfitAwareNonces(wallets, extractProfit, maxIndex, totalProfit, nonceManager) {
428
- // ✅ 并行获取所有钱包的初始 nonce
429
- const nonces = await Promise.all(wallets.map(wallet => nonceManager.getNextNonce(wallet)));
432
+ // ✅ 批量获取所有钱包的初始 nonce(单次网络往返)
433
+ const nonces = await nonceManager.getNextNoncesForWallets(wallets);
430
434
  // 如果需要利润转账,maxIndex 钱包需要额外一个 nonce
431
435
  if (extractProfit && totalProfit > 0n && maxIndex >= 0 && maxIndex < wallets.length) {
432
- // 预留一个 nonce 给利润交易(NonceManager 内部会自增)
436
+ // 预留一个 nonce 给利润交易(从缓存获取,不触发 RPC)
433
437
  await nonceManager.getNextNonce(wallets[maxIndex]);
434
438
  }
435
439
  return nonces;
@@ -458,7 +462,11 @@ async function ensureAllowances({ provider, tokenAddress, owners, spender }) {
458
462
  throw new Error(`需要授权: ${needApproval} 个钱包尚未授权。请先完成授权后再卖出。`);
459
463
  }
460
464
  }
465
+ /**
466
+ * ✅ 使用 Multicall3 批量获取卖出报价(单次 RPC)
467
+ */
461
468
  async function resolveSellOutputs({ params, provider, tokenAddress, routeType, amountsWei }) {
469
+ // 如果已提供 minOutputAmounts,直接使用,跳过报价查询
462
470
  if (params.minOutputAmounts && params.minOutputAmounts.length === amountsWei.length) {
463
471
  const minOuts = params.minOutputAmounts.map(m => typeof m === 'string' ? ethers.parseEther(m) : m);
464
472
  return {
@@ -466,52 +474,98 @@ async function resolveSellOutputs({ params, provider, tokenAddress, routeType, a
466
474
  quotedOutputs: minOuts.map(m => m * 100n / 95n)
467
475
  };
468
476
  }
469
- const quotedOutputs = await Promise.all(amountsWei.map(async (amount) => {
477
+ // 如果只有 1 个,直接调用(避免 multicall 开销)
478
+ if (amountsWei.length === 1) {
479
+ const quotedOutput = await getSingleQuote(params, provider, tokenAddress, routeType, amountsWei[0]);
480
+ return {
481
+ quotedOutputs: [quotedOutput],
482
+ minOuts: [quotedOutput * 95n / 100n]
483
+ };
484
+ }
485
+ // ✅ 使用 Multicall3 批量获取报价(仅 V2 路由支持)
486
+ if (routeType === 'v2' && params.v2Path && params.v2Path.length >= 2) {
470
487
  try {
471
- if (routeType === 'v2') {
472
- if (!params.v2Path || params.v2Path.length < 2) {
473
- throw new Error('v2Path is required for V2 routing');
474
- }
475
- const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
476
- const amounts = await v2Router.getAmountsOut(amount, params.v2Path);
477
- return amounts[amounts.length - 1];
478
- }
479
- if (routeType === 'v3-single') {
480
- if (!params.v3TokenOut || !params.v3Fee) {
481
- throw new Error('v3TokenOut and v3Fee are required for V3 single-hop');
482
- }
483
- const quoter = new Contract(PANCAKE_V3_QUOTER_ADDRESS, PANCAKE_V3_QUOTER_ABI, provider);
484
- const result = await quoter.quoteExactInputSingle.staticCall({
485
- tokenIn: tokenAddress,
486
- tokenOut: params.v3TokenOut,
487
- amountIn: amount,
488
- fee: params.v3Fee,
489
- sqrtPriceLimitX96: 0
490
- });
491
- return result[0];
492
- }
493
- if (routeType === 'v3-multi') {
494
- if (!params.v3LpAddresses || params.v3LpAddresses.length === 0 || !params.v3ExactTokenIn) {
495
- throw new Error('v3LpAddresses and v3ExactTokenIn are required for V3 multi-hop');
496
- }
497
- if (params.v2Path && params.v2Path.length >= 2) {
498
- const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
499
- const amounts = await v2Router.getAmountsOut(amount, params.v2Path);
500
- return amounts[amounts.length - 1];
488
+ const v2RouterIface = new Interface(PANCAKE_V2_ROUTER_ABI);
489
+ const multicall = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
490
+ const calls = amountsWei.map(amount => ({
491
+ target: PANCAKE_V2_ROUTER_ADDRESS,
492
+ allowFailure: true,
493
+ callData: v2RouterIface.encodeFunctionData('getAmountsOut', [amount, params.v2Path])
494
+ }));
495
+ const results = await multicall.aggregate3.staticCall(calls);
496
+ const quotedOutputs = results.map((r) => {
497
+ if (r.success && r.returnData && r.returnData !== '0x') {
498
+ try {
499
+ const decoded = v2RouterIface.decodeFunctionResult('getAmountsOut', r.returnData);
500
+ const amounts = decoded[0];
501
+ return amounts[amounts.length - 1];
502
+ }
503
+ catch {
504
+ return 0n;
505
+ }
501
506
  }
502
507
  return 0n;
503
- }
504
- throw new Error(`Unsupported routeType: ${routeType}`);
508
+ });
509
+ return {
510
+ quotedOutputs,
511
+ minOuts: quotedOutputs.map((q) => q * 95n / 100n)
512
+ };
505
513
  }
506
514
  catch {
507
- return 0n;
515
+ // Multicall 失败,回退到并行调用
508
516
  }
509
- }));
517
+ }
518
+ // 回退:并行调用(V3 路由或 Multicall 失败时)
519
+ const quotedOutputs = await Promise.all(amountsWei.map(amount => getSingleQuote(params, provider, tokenAddress, routeType, amount)));
510
520
  return {
511
521
  quotedOutputs,
512
522
  minOuts: quotedOutputs.map(q => q * 95n / 100n)
513
523
  };
514
524
  }
525
+ /**
526
+ * 获取单个报价
527
+ */
528
+ async function getSingleQuote(params, provider, tokenAddress, routeType, amount) {
529
+ try {
530
+ if (routeType === 'v2') {
531
+ if (!params.v2Path || params.v2Path.length < 2) {
532
+ throw new Error('v2Path is required for V2 routing');
533
+ }
534
+ const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
535
+ const amounts = await v2Router.getAmountsOut(amount, params.v2Path);
536
+ return amounts[amounts.length - 1];
537
+ }
538
+ if (routeType === 'v3-single') {
539
+ if (!params.v3TokenOut || !params.v3Fee) {
540
+ throw new Error('v3TokenOut and v3Fee are required for V3 single-hop');
541
+ }
542
+ const quoter = new Contract(PANCAKE_V3_QUOTER_ADDRESS, PANCAKE_V3_QUOTER_ABI, provider);
543
+ const result = await quoter.quoteExactInputSingle.staticCall({
544
+ tokenIn: tokenAddress,
545
+ tokenOut: params.v3TokenOut,
546
+ amountIn: amount,
547
+ fee: params.v3Fee,
548
+ sqrtPriceLimitX96: 0
549
+ });
550
+ return result[0];
551
+ }
552
+ if (routeType === 'v3-multi') {
553
+ if (!params.v3LpAddresses || params.v3LpAddresses.length === 0 || !params.v3ExactTokenIn) {
554
+ throw new Error('v3LpAddresses and v3ExactTokenIn are required for V3 multi-hop');
555
+ }
556
+ if (params.v2Path && params.v2Path.length >= 2) {
557
+ const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
558
+ const amounts = await v2Router.getAmountsOut(amount, params.v2Path);
559
+ return amounts[amounts.length - 1];
560
+ }
561
+ return 0n;
562
+ }
563
+ throw new Error(`Unsupported routeType: ${routeType}`);
564
+ }
565
+ catch {
566
+ return 0n;
567
+ }
568
+ }
515
569
  async function buildSellTransactions({ routeType, params, proxies, wallets, tokenAddress, amountsWei, minOuts }) {
516
570
  const needBNB = false;
517
571
  if (routeType === 'v2') {
@@ -313,8 +313,12 @@ async function validateNativeBalances({ sameAddress, buyerBalance, buyerFundsWei
313
313
  throw new Error(`卖方余额不足: 需要 ${ethers.formatEther(gasCost)} ${nativeToken} (Gas),实际 ${ethers.formatEther(sellerBalance)} ${nativeToken}`);
314
314
  }
315
315
  }
316
+ /**
317
+ * ✅ 优化:使用批量 nonce 获取
318
+ */
316
319
  async function planNonces({ buyer, seller, approvalExists, extractProfit, sameAddress, nonceManager }) {
317
320
  if (sameAddress) {
321
+ // 同一地址:使用 getNextNonceBatch 获取连续 nonce
318
322
  const txCount = countTruthy([approvalExists, true, true, extractProfit]);
319
323
  const nonces = await nonceManager.getNextNonceBatch(buyer, txCount);
320
324
  let idx = 0;
@@ -326,21 +330,24 @@ async function planNonces({ buyer, seller, approvalExists, extractProfit, sameAd
326
330
  return { buyerNonce, sellerNonce, profitNonce };
327
331
  }
328
332
  if (approvalExists || extractProfit) {
333
+ // 卖方需要多个 nonce:使用 getNextNonceBatch
329
334
  const sellerTxCount = countTruthy([approvalExists, true, extractProfit]);
330
- const sellerNonces = await nonceManager.getNextNonceBatch(seller, sellerTxCount);
335
+ // 优化:并行获取 buyer seller 的 nonce(JSON-RPC 批量请求)
336
+ const [sellerNonces, buyerNonces] = await Promise.all([
337
+ nonceManager.getNextNonceBatch(seller, sellerTxCount),
338
+ nonceManager.getNextNoncesForWallets([buyer])
339
+ ]);
331
340
  let idx = 0;
332
341
  if (approvalExists)
333
342
  idx++;
334
343
  const sellerNonce = sellerNonces[idx++];
335
344
  const profitNonce = extractProfit ? sellerNonces[idx] : undefined;
336
- const buyerNonce = await nonceManager.getNextNonce(buyer);
345
+ const buyerNonce = buyerNonces[0];
337
346
  return { buyerNonce, sellerNonce, profitNonce };
338
347
  }
339
- const [buyerNonce, sellerNonce] = await Promise.all([
340
- nonceManager.getNextNonce(buyer),
341
- nonceManager.getNextNonce(seller)
342
- ]);
343
- return { buyerNonce, sellerNonce };
348
+ // 优化:使用 getNextNoncesForWallets 批量获取(单次网络往返)
349
+ const nonces = await nonceManager.getNextNoncesForWallets([buyer, seller]);
350
+ return { buyerNonce: nonces[0], sellerNonce: nonces[1] };
344
351
  }
345
352
  function buildTransactionRequest(unsigned, { from, nonce, gasLimit, gasPrice, priorityFee, chainId, txType, value }) {
346
353
  const tx = {
@@ -275,23 +275,29 @@ async function validateBalances({ buyerNeed, buyerAddress, portalGasCost, provid
275
275
  throw new Error(`卖方余额不足: 需要 ${ethers.formatEther(portalGasCost)} ${chainContext.nativeToken} (Gas),实际 ${ethers.formatEther(sellerBalance)} ${chainContext.nativeToken}`);
276
276
  }
277
277
  }
278
+ /**
279
+ * ✅ 优化:使用批量 nonce 获取(JSON-RPC 批量请求)
280
+ */
278
281
  async function planNonces({ seller, buyer, approvalExists, extractProfit, nonceManager }) {
279
282
  if (approvalExists || extractProfit) {
283
+ // 卖方需要多个 nonce:使用 getNextNonceBatch
280
284
  const sellerTxCount = countTruthy([approvalExists, true, extractProfit]);
281
- const sellerNonces = await nonceManager.getNextNonceBatch(seller, sellerTxCount);
285
+ // 优化:并行获取 seller 和 buyer 的 nonce
286
+ const [sellerNonces, buyerNonces] = await Promise.all([
287
+ nonceManager.getNextNonceBatch(seller, sellerTxCount),
288
+ nonceManager.getNextNoncesForWallets([buyer])
289
+ ]);
282
290
  let idx = 0;
283
291
  if (approvalExists)
284
292
  idx++;
285
293
  const sellerNonce = sellerNonces[idx++];
286
294
  const profitNonce = extractProfit ? sellerNonces[idx] : undefined;
287
- const buyerNonce = await nonceManager.getNextNonce(buyer);
295
+ const buyerNonce = buyerNonces[0];
288
296
  return { sellerNonce, buyerNonce, profitNonce };
289
297
  }
290
- const [sellerNonce, buyerNonce] = await Promise.all([
291
- nonceManager.getNextNonce(seller),
292
- nonceManager.getNextNonce(buyer)
293
- ]);
294
- return { sellerNonce, buyerNonce };
298
+ // 优化:使用 getNextNoncesForWallets 批量获取(单次网络往返)
299
+ const nonces = await nonceManager.getNextNoncesForWallets([seller, buyer]);
300
+ return { sellerNonce: nonces[0], buyerNonce: nonces[1] };
295
301
  }
296
302
  function buildTransactionRequest(unsigned, { from, nonce, gasLimit, gasPrice, priorityFee, chainId, txType, value }) {
297
303
  const tx = {
@@ -38,8 +38,8 @@ export declare class NonceManager {
38
38
  */
39
39
  getTempCachedNonce(address: string): number | undefined;
40
40
  /**
41
- * ✅ 批量获取多个不同地址的 nonce(单次 RPC 批量请求)
42
- * Promise.all + getNextNonce 更快,因为只发一次网络请求
41
+ * ✅ 批量获取多个不同地址的 nonce(真正的单次 JSON-RPC 批量请求)
42
+ * 使用 provider.send 发送批量请求,N 个地址只需 1 次网络往返
43
43
  * @param wallets 钱包数组
44
44
  * @returns nonce 数组(顺序与 wallets 对应)
45
45
  */
@@ -72,8 +72,8 @@ export class NonceManager {
72
72
  return this.tempNonceCache.get(address.toLowerCase());
73
73
  }
74
74
  /**
75
- * ✅ 批量获取多个不同地址的 nonce(单次 RPC 批量请求)
76
- * Promise.all + getNextNonce 更快,因为只发一次网络请求
75
+ * ✅ 批量获取多个不同地址的 nonce(真正的单次 JSON-RPC 批量请求)
76
+ * 使用 provider.send 发送批量请求,N 个地址只需 1 次网络往返
77
77
  * @param wallets 钱包数组
78
78
  * @returns nonce 数组(顺序与 wallets 对应)
79
79
  */
@@ -94,11 +94,32 @@ export class NonceManager {
94
94
  needQuery.push({ index: i, address: addresses[i] });
95
95
  }
96
96
  }
97
- // 如果有需要查询的地址,使用批量 RPC
97
+ // 如果有需要查询的地址,使用真正的 JSON-RPC 批量请求
98
98
  if (needQuery.length > 0) {
99
- // 使用 ethers 的批量请求(内部会合并为单次 RPC)
100
- const queryPromises = needQuery.map(({ address }) => this.provider.getTransactionCount(address, 'latest'));
101
- const queryResults = await Promise.all(queryPromises);
99
+ let queryResults;
100
+ try {
101
+ // 使用 JSON-RPC 批量请求(单次网络往返)
102
+ const batchRequests = needQuery.map(({ address }, idx) => ({
103
+ method: 'eth_getTransactionCount',
104
+ params: [address, 'latest'],
105
+ id: idx + 1,
106
+ jsonrpc: '2.0'
107
+ }));
108
+ // 通过 provider._send 发送批量请求(ethers v6 内部方法)
109
+ const rawResults = await this.provider._send(batchRequests);
110
+ // 解析结果
111
+ queryResults = rawResults.map((r) => {
112
+ if (r.result) {
113
+ return parseInt(r.result, 16);
114
+ }
115
+ return 0;
116
+ });
117
+ }
118
+ catch {
119
+ // 如果批量请求失败,回退到 Promise.all
120
+ const queryPromises = needQuery.map(({ address }) => this.provider.getTransactionCount(address, 'latest'));
121
+ queryResults = await Promise.all(queryPromises);
122
+ }
102
123
  // 填充结果并更新缓存
103
124
  for (let i = 0; i < needQuery.length; i++) {
104
125
  const { index, address } = needQuery[i];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.3.13",
3
+ "version": "1.3.15",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",