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.
- package/dist/contracts/tm-bundle-merkle/core.js +66 -12
- package/dist/contracts/tm-bundle-merkle/pancake-proxy.js +82 -27
- package/dist/contracts/tm-bundle-merkle/swap-buy-first.js +22 -5
- package/dist/contracts/tm-bundle-merkle/swap.js +17 -3
- package/dist/contracts/tm-bundle-merkle/utils.js +30 -8
- package/dist/flap/portal-bundle-merkle/pancake-proxy.js +97 -43
- package/dist/flap/portal-bundle-merkle/swap-buy-first.js +14 -7
- package/dist/flap/portal-bundle-merkle/swap.js +13 -7
- package/dist/utils/bundle-helpers.d.ts +2 -2
- package/dist/utils/bundle-helpers.js +27 -6
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
417
|
+
// 如果只有 1 个,直接调用(避免 multicall 开销)
|
|
418
|
+
if (amountsWei.length === 1) {
|
|
408
419
|
try {
|
|
409
|
-
const result = await trySell('BSC', rpcUrl, tokenAddress,
|
|
410
|
-
|
|
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
|
|
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
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ✅
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
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
|
-
* ✅
|
|
429
|
+
* ✅ 优化:批量获取所有钱包的 nonce(JSON-RPC 批量请求)
|
|
426
430
|
*/
|
|
427
431
|
async function allocateProfitAwareNonces(wallets, extractProfit, maxIndex, totalProfit, nonceManager) {
|
|
428
|
-
// ✅
|
|
429
|
-
const nonces = await
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
508
|
+
});
|
|
509
|
+
return {
|
|
510
|
+
quotedOutputs,
|
|
511
|
+
minOuts: quotedOutputs.map((q) => q * 95n / 100n)
|
|
512
|
+
};
|
|
505
513
|
}
|
|
506
514
|
catch {
|
|
507
|
-
|
|
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
|
-
|
|
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 =
|
|
345
|
+
const buyerNonce = buyerNonces[0];
|
|
337
346
|
return { buyerNonce, sellerNonce, profitNonce };
|
|
338
347
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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 =
|
|
295
|
+
const buyerNonce = buyerNonces[0];
|
|
288
296
|
return { sellerNonce, buyerNonce, profitNonce };
|
|
289
297
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
42
|
-
*
|
|
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
|
|
76
|
-
*
|
|
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
|
-
//
|
|
97
|
+
// 如果有需要查询的地址,使用真正的 JSON-RPC 批量请求
|
|
98
98
|
if (needQuery.length > 0) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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];
|