four-flap-meme-sdk 1.3.8 → 1.3.10
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/clients/merkle.d.ts +4 -0
- package/dist/clients/merkle.js +20 -24
- package/dist/contracts/tm-bundle-merkle/core.js +66 -128
- package/dist/contracts/tm-bundle-merkle/pancake-proxy.js +57 -121
- package/dist/contracts/tm-bundle-merkle/submit.d.ts +4 -0
- package/dist/contracts/tm-bundle-merkle/submit.js +21 -7
- package/dist/contracts/tm-bundle-merkle/swap-buy-first.js +117 -135
- package/dist/contracts/tm-bundle-merkle/swap.js +101 -127
- package/dist/contracts/tm-bundle-merkle/types.d.ts +4 -0
- package/dist/contracts/tm-bundle-merkle/utils.d.ts +2 -0
- package/dist/contracts/tm-bundle-merkle/utils.js +201 -156
- package/dist/flap/portal-bundle-merkle/core.js +0 -24
- package/dist/flap/portal-bundle-merkle/pancake-proxy.js +68 -162
- package/dist/flap/portal-bundle-merkle/swap-buy-first.js +69 -70
- package/dist/flap/portal-bundle-merkle/swap.js +80 -65
- package/package.json +1 -1
package/dist/clients/merkle.d.ts
CHANGED
|
@@ -78,6 +78,10 @@ export interface SendBundleOptions {
|
|
|
78
78
|
autoRetry?: boolean;
|
|
79
79
|
/** 最大重试次数(默认2) */
|
|
80
80
|
maxRetries?: number;
|
|
81
|
+
/** ✅ 新增:使用时间戳模式(Merkle 会在时间范围内尽快打包) */
|
|
82
|
+
useTimestampMode?: boolean;
|
|
83
|
+
/** ✅ 新增:时间戳窗口秒数(默认15秒,仅在 useTimestampMode=true 时生效) */
|
|
84
|
+
timestampWindowSeconds?: number;
|
|
81
85
|
}
|
|
82
86
|
/**
|
|
83
87
|
* Bundle 发送结果
|
package/dist/clients/merkle.js
CHANGED
|
@@ -133,7 +133,7 @@ export class MerkleClient {
|
|
|
133
133
|
if (!options.transactions || options.transactions.length === 0) {
|
|
134
134
|
throw new Error('Transactions array cannot be empty');
|
|
135
135
|
}
|
|
136
|
-
const minBlockOffset = options.minBlockOffset ?? 3;
|
|
136
|
+
const minBlockOffset = options.minBlockOffset ?? 3;
|
|
137
137
|
const autoRetry = options.autoRetry ?? false;
|
|
138
138
|
const maxRetries = options.maxRetries ?? 2;
|
|
139
139
|
let attempt = 0;
|
|
@@ -146,47 +146,47 @@ export class MerkleClient {
|
|
|
146
146
|
let targetBlock;
|
|
147
147
|
if (options.targetBlock) {
|
|
148
148
|
targetBlock = options.targetBlock;
|
|
149
|
-
// 检查目标区块是否已经过期
|
|
150
149
|
if (targetBlock <= currentBlock) {
|
|
151
150
|
if (autoRetry && attempt < maxRetries) {
|
|
152
151
|
attempt++;
|
|
153
|
-
await new Promise(resolve => setTimeout(resolve, 300));
|
|
152
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
154
153
|
continue;
|
|
155
154
|
}
|
|
156
155
|
throw new Error(`Target block ${targetBlock} has already passed (current block: ${currentBlock})`);
|
|
157
156
|
}
|
|
158
157
|
}
|
|
159
158
|
else {
|
|
160
|
-
// 动态计算:确保至少有 minBlockOffset 个区块的窗口
|
|
161
159
|
const requestedOffset = options.blockOffset ?? 3;
|
|
162
160
|
const actualOffset = Math.max(requestedOffset, minBlockOffset);
|
|
163
161
|
targetBlock = currentBlock + actualOffset;
|
|
164
162
|
}
|
|
165
|
-
// 转换为十六进制
|
|
166
163
|
const targetBlockHex = '0x' + targetBlock.toString(16);
|
|
164
|
+
// ✅ 优化:时间戳模式 - Merkle 会在时间范围内尽快打包
|
|
165
|
+
let minTimestamp = options.minTimestamp;
|
|
166
|
+
let maxTimestamp = options.maxTimestamp;
|
|
167
|
+
if (options.useTimestampMode && !minTimestamp && !maxTimestamp) {
|
|
168
|
+
const now = Math.floor(Date.now() / 1000);
|
|
169
|
+
const windowSeconds = options.timestampWindowSeconds ?? 15;
|
|
170
|
+
minTimestamp = now;
|
|
171
|
+
maxTimestamp = now + windowSeconds;
|
|
172
|
+
}
|
|
167
173
|
// 发送 Bundle
|
|
168
174
|
const bundleHash = await this.sendBundleRaw({
|
|
169
175
|
txs: options.transactions,
|
|
170
176
|
blockNumber: targetBlockHex,
|
|
171
|
-
minTimestamp
|
|
172
|
-
maxTimestamp
|
|
177
|
+
minTimestamp,
|
|
178
|
+
maxTimestamp,
|
|
173
179
|
});
|
|
174
|
-
//
|
|
175
|
-
const txHashes =
|
|
176
|
-
for (let i = 0; i < options.transactions.length; i++) {
|
|
180
|
+
// ✅ 优化:并行提取交易哈希
|
|
181
|
+
const txHashes = options.transactions.map(rawTx => {
|
|
177
182
|
try {
|
|
178
|
-
const tx = Transaction.from(
|
|
179
|
-
|
|
180
|
-
txHashes.push(tx.hash);
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
txHashes.push('');
|
|
184
|
-
}
|
|
183
|
+
const tx = Transaction.from(rawTx);
|
|
184
|
+
return tx.hash || '';
|
|
185
185
|
}
|
|
186
|
-
catch
|
|
187
|
-
|
|
186
|
+
catch {
|
|
187
|
+
return '';
|
|
188
188
|
}
|
|
189
|
-
}
|
|
189
|
+
});
|
|
190
190
|
return {
|
|
191
191
|
bundleHash,
|
|
192
192
|
txHashes,
|
|
@@ -196,19 +196,15 @@ export class MerkleClient {
|
|
|
196
196
|
}
|
|
197
197
|
catch (error) {
|
|
198
198
|
lastError = error;
|
|
199
|
-
// 如果启用了重试且还有重试次数
|
|
200
199
|
if (autoRetry && attempt < maxRetries) {
|
|
201
200
|
attempt++;
|
|
202
|
-
// 等待一个区块时间再重试
|
|
203
201
|
const blockTime = this.chainId === 56 ? 3000 : 12000;
|
|
204
202
|
await new Promise(resolve => setTimeout(resolve, blockTime));
|
|
205
203
|
continue;
|
|
206
204
|
}
|
|
207
|
-
// 没有重试或已达最大重试次数
|
|
208
205
|
throw error;
|
|
209
206
|
}
|
|
210
207
|
}
|
|
211
|
-
// 如果执行到这里,说明所有重试都失败了
|
|
212
208
|
throw lastError || new Error('Bundle submission failed after all retries');
|
|
213
209
|
}
|
|
214
210
|
/**
|
|
@@ -254,71 +254,52 @@ async function resolveTokenImage(fourClient, tokenInfo, accessToken) {
|
|
|
254
254
|
}
|
|
255
255
|
throw new Error(getErrorMessage('IMAGE_REQUIRED'));
|
|
256
256
|
}
|
|
257
|
-
function resolveTokenAddress(createResp) {
|
|
258
|
-
return (createResp?.tokenAddr ||
|
|
259
|
-
createResp?.address ||
|
|
260
|
-
createResp?.token ||
|
|
261
|
-
ZERO_ADDRESS);
|
|
262
|
-
}
|
|
263
257
|
async function executeBuyFlow(options) {
|
|
264
258
|
const { wallets, buyAmounts, tokenAddress, config, nonceManager, contractAddress, gasPrice, chainId, profitMode, txType } = options;
|
|
265
259
|
const extractProfit = shouldExtractProfit(config);
|
|
266
260
|
const analysis = analyzeBuyFunds(buyAmounts, config, extractProfit);
|
|
267
|
-
const unsignedBuys = await populateBuyTransactions({
|
|
268
|
-
buyers: wallets,
|
|
269
|
-
contractAddress,
|
|
270
|
-
tokenAddress,
|
|
271
|
-
fundsList: analysis.fundsList
|
|
272
|
-
});
|
|
273
261
|
const finalGasLimit = getGasLimit(config);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
262
|
+
// ✅ 优化:并行执行 populateBuyTransactions 和获取 nonces
|
|
263
|
+
const [unsignedBuys, nonces] = await Promise.all([
|
|
264
|
+
populateBuyTransactions({
|
|
265
|
+
buyers: wallets,
|
|
266
|
+
contractAddress,
|
|
267
|
+
tokenAddress,
|
|
268
|
+
fundsList: analysis.fundsList
|
|
269
|
+
}),
|
|
270
|
+
Promise.all(wallets.map((wallet) => nonceManager.getNextNonce(wallet)))
|
|
271
|
+
]);
|
|
272
|
+
// ✅ 如果需要利润转账,预先获取 payer 的下一个 nonce
|
|
273
|
+
let profitNonce;
|
|
274
|
+
if (extractProfit && analysis.totalProfit > 0n && profitMode.type === 'aggregated') {
|
|
275
|
+
const payer = profitMode.payer ?? wallets[0];
|
|
276
|
+
profitNonce = await nonceManager.getNextNonce(payer);
|
|
277
|
+
}
|
|
278
|
+
// ✅ 并行签名所有买入交易
|
|
279
|
+
const signedBuys = await Promise.all(unsignedBuys.map((unsigned, index) => wallets[index].signTransaction({
|
|
280
|
+
...unsigned,
|
|
281
|
+
from: wallets[index].address,
|
|
282
|
+
nonce: nonces[index],
|
|
278
283
|
gasLimit: finalGasLimit,
|
|
279
284
|
gasPrice,
|
|
280
285
|
chainId,
|
|
281
|
-
txType,
|
|
282
|
-
|
|
283
|
-
});
|
|
286
|
+
type: txType,
|
|
287
|
+
value: analysis.fundsList[index]
|
|
288
|
+
})));
|
|
284
289
|
const signedTxs = [...signedBuys];
|
|
285
|
-
// ✅
|
|
286
|
-
if (extractProfit) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
chainId,
|
|
299
|
-
type: txType
|
|
300
|
-
});
|
|
301
|
-
signedTxs.push(profitTx);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
// 聚合模式:由指定的 payer 支付总利润
|
|
307
|
-
if (analysis.totalProfit > 0n) {
|
|
308
|
-
const payer = profitMode.payer ?? wallets[0];
|
|
309
|
-
const profitNonce = await nonceManager.getNextNonce(payer);
|
|
310
|
-
const profitTx = await payer.signTransaction({
|
|
311
|
-
to: getProfitRecipient(),
|
|
312
|
-
value: analysis.totalProfit,
|
|
313
|
-
nonce: profitNonce,
|
|
314
|
-
gasPrice,
|
|
315
|
-
gasLimit: 21000n,
|
|
316
|
-
chainId,
|
|
317
|
-
type: txType
|
|
318
|
-
});
|
|
319
|
-
signedTxs.push(profitTx);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
290
|
+
// ✅ 添加利润转账交易(使用预先获取的 nonce)
|
|
291
|
+
if (extractProfit && profitNonce !== undefined && profitMode.type === 'aggregated') {
|
|
292
|
+
const payer = profitMode.payer ?? wallets[0];
|
|
293
|
+
const profitTx = await payer.signTransaction({
|
|
294
|
+
to: getProfitRecipient(),
|
|
295
|
+
value: analysis.totalProfit,
|
|
296
|
+
nonce: profitNonce,
|
|
297
|
+
gasPrice,
|
|
298
|
+
gasLimit: 21000n,
|
|
299
|
+
chainId,
|
|
300
|
+
type: txType
|
|
301
|
+
});
|
|
302
|
+
signedTxs.push(profitTx);
|
|
322
303
|
}
|
|
323
304
|
const metadata = buildBuyMetadata(extractProfit, analysis.totalBuyAmount, analysis.totalProfit, wallets.length);
|
|
324
305
|
return { signedTxs, metadata };
|
|
@@ -326,21 +307,28 @@ async function executeBuyFlow(options) {
|
|
|
326
307
|
async function executeSellFlow(options) {
|
|
327
308
|
const { wallets, sellAmounts, tokenAddress, minOutputAmounts, config, provider, nonceManager, contractAddress, gasPrice, chainId, rpcUrl, txType } = options;
|
|
328
309
|
const amountsWei = sellAmounts.map((amount) => ethers.parseUnits(amount, 18));
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
sellers: wallets,
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
310
|
+
const finalGasLimit = getGasLimit(config);
|
|
311
|
+
const extractProfit = shouldExtractProfit(config);
|
|
312
|
+
// ✅ 优化:并行执行 resolveSellOutputs、ensureSellerBalances、ensureSellAllowances 和获取 nonces
|
|
313
|
+
const [sellOutputs, , , nonces] = await Promise.all([
|
|
314
|
+
resolveSellOutputs({
|
|
315
|
+
minOutputAmounts,
|
|
316
|
+
sellers: wallets,
|
|
317
|
+
amountsWei,
|
|
318
|
+
rpcUrl,
|
|
319
|
+
tokenAddress
|
|
320
|
+
}),
|
|
321
|
+
ensureSellerBalances({ provider, tokenAddress, sellers: wallets, amountsWei }),
|
|
322
|
+
ensureSellAllowances({
|
|
323
|
+
provider,
|
|
324
|
+
tokenAddress,
|
|
325
|
+
sellers: wallets,
|
|
326
|
+
amountsWei,
|
|
327
|
+
spender: contractAddress
|
|
328
|
+
}),
|
|
329
|
+
Promise.all(wallets.map((wallet) => nonceManager.getNextNonce(wallet)))
|
|
330
|
+
]);
|
|
331
|
+
// ✅ 构建未签名交易
|
|
344
332
|
const unsignedSells = await populateSellTransactions({
|
|
345
333
|
sellers: wallets,
|
|
346
334
|
contractAddress,
|
|
@@ -348,18 +336,17 @@ async function executeSellFlow(options) {
|
|
|
348
336
|
amountsWei,
|
|
349
337
|
minOuts: sellOutputs.minOuts
|
|
350
338
|
});
|
|
351
|
-
|
|
352
|
-
const signedSells = await
|
|
353
|
-
|
|
354
|
-
wallets,
|
|
355
|
-
|
|
339
|
+
// ✅ 并行签名所有卖出交易
|
|
340
|
+
const signedSells = await Promise.all(unsignedSells.map((unsigned, index) => wallets[index].signTransaction({
|
|
341
|
+
...unsigned,
|
|
342
|
+
from: wallets[index].address,
|
|
343
|
+
nonce: nonces[index],
|
|
356
344
|
gasLimit: finalGasLimit,
|
|
357
345
|
gasPrice,
|
|
358
346
|
chainId,
|
|
359
|
-
txType
|
|
360
|
-
});
|
|
347
|
+
type: txType
|
|
348
|
+
})));
|
|
361
349
|
const signedTxs = [...signedSells];
|
|
362
|
-
const extractProfit = shouldExtractProfit(config);
|
|
363
350
|
const { totalProfit, payer } = summarizeSellProfits(sellOutputs.quotedOutputs, wallets, config);
|
|
364
351
|
await appendAggregatedProfitTransfer({
|
|
365
352
|
extractProfit,
|
|
@@ -399,42 +386,6 @@ async function populateBuyTransactions(params) {
|
|
|
399
386
|
const contracts = buyers.map((wallet) => new ethers.Contract(contractAddress, TM2_ABI, wallet));
|
|
400
387
|
return Promise.all(contracts.map((contract, index) => contract.buyTokenAMAP.populateTransaction(0n, tokenAddress, buyers[index].address, fundsList[index], 0n, { value: fundsList[index] })));
|
|
401
388
|
}
|
|
402
|
-
async function signBuyTransactions(params) {
|
|
403
|
-
const { unsignedTxs, wallets, nonceManager, gasLimit, gasPrice, chainId, txType, valueList } = params;
|
|
404
|
-
const nonces = await Promise.all(wallets.map((wallet) => nonceManager.getNextNonce(wallet)));
|
|
405
|
-
return Promise.all(unsignedTxs.map((unsigned, index) => wallets[index].signTransaction({
|
|
406
|
-
...unsigned,
|
|
407
|
-
from: wallets[index].address,
|
|
408
|
-
nonce: nonces[index],
|
|
409
|
-
gasLimit,
|
|
410
|
-
gasPrice,
|
|
411
|
-
chainId,
|
|
412
|
-
type: txType,
|
|
413
|
-
value: valueList[index]
|
|
414
|
-
})));
|
|
415
|
-
}
|
|
416
|
-
async function appendIndividualProfitTransfers(params) {
|
|
417
|
-
const { extractProfit, profitList, wallets, nonceManager, gasPrice, chainId, txType, signedTxs } = params;
|
|
418
|
-
if (!extractProfit || profitList.length === 0) {
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
for (let i = 0; i < profitList.length; i++) {
|
|
422
|
-
if (profitList[i] <= 0n) {
|
|
423
|
-
continue;
|
|
424
|
-
}
|
|
425
|
-
const profitNonce = await nonceManager.getNextNonce(wallets[i]);
|
|
426
|
-
const profitTx = await wallets[i].signTransaction({
|
|
427
|
-
to: getProfitRecipient(),
|
|
428
|
-
value: profitList[i],
|
|
429
|
-
nonce: profitNonce,
|
|
430
|
-
gasPrice,
|
|
431
|
-
gasLimit: 21000n,
|
|
432
|
-
chainId,
|
|
433
|
-
type: txType
|
|
434
|
-
});
|
|
435
|
-
signedTxs.push(profitTx);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
389
|
function buildBuyMetadata(extractProfit, totalBuyAmount, totalProfit, buyerCount) {
|
|
439
390
|
if (!extractProfit || totalBuyAmount <= 0n || totalProfit <= 0n) {
|
|
440
391
|
return undefined;
|
|
@@ -493,19 +444,6 @@ async function populateSellTransactions(params) {
|
|
|
493
444
|
const contracts = sellers.map((wallet) => new ethers.Contract(contractAddress, TM2_ABI, wallet));
|
|
494
445
|
return Promise.all(contracts.map((contract, index) => contract.sellToken.populateTransaction(0n, tokenAddress, amountsWei[index], minOuts[index])));
|
|
495
446
|
}
|
|
496
|
-
async function signSellTransactions(params) {
|
|
497
|
-
const { unsignedTxs, wallets, nonceManager, gasLimit, gasPrice, chainId, txType } = params;
|
|
498
|
-
const nonces = await Promise.all(wallets.map((wallet) => nonceManager.getNextNonce(wallet)));
|
|
499
|
-
return Promise.all(unsignedTxs.map((unsigned, index) => wallets[index].signTransaction({
|
|
500
|
-
...unsigned,
|
|
501
|
-
from: wallets[index].address,
|
|
502
|
-
nonce: nonces[index],
|
|
503
|
-
gasLimit,
|
|
504
|
-
gasPrice,
|
|
505
|
-
chainId,
|
|
506
|
-
type: txType
|
|
507
|
-
})));
|
|
508
|
-
}
|
|
509
447
|
function summarizeSellProfits(quotedOutputs, sellers, config) {
|
|
510
448
|
let totalProfit = 0n;
|
|
511
449
|
let maxRevenue = 0n;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ethers, Wallet, JsonRpcProvider, Contract } from 'ethers';
|
|
2
2
|
import { NonceManager, getOptimizedGasPrice } from '../../utils/bundle-helpers.js';
|
|
3
3
|
import { ADDRESSES } from '../../utils/constants.js';
|
|
4
|
-
import { getTxType,
|
|
4
|
+
import { getTxType, getGasPriceConfig, shouldExtractProfit, calculateProfit, calculateBatchProfit, getProfitRecipient } from './config.js';
|
|
5
5
|
import { batchCheckAllowances } from '../../utils/erc20.js';
|
|
6
6
|
// 常量
|
|
7
7
|
const WBNB_ADDRESS = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
|
|
@@ -113,66 +113,6 @@ async function buildV3MultiHopTransactions(proxies, wallets, lpAddresses, exactT
|
|
|
113
113
|
return proxy.swapV3MultiHop.populateTransaction(lpAddresses, exactTokenIn, amountsWei[i], minOuts[i], wallets[i].address, { value: txValue });
|
|
114
114
|
}));
|
|
115
115
|
}
|
|
116
|
-
/**
|
|
117
|
-
* 签名并提交 Bundle
|
|
118
|
-
*/
|
|
119
|
-
async function signAndSendBundle(merkle, wallets, unsignedTxs, gasPrice, gasLimit, chainId, config, blockOffset) {
|
|
120
|
-
const nonceManager = new NonceManager(merkle.getProvider());
|
|
121
|
-
const txType = getTxType(config);
|
|
122
|
-
try {
|
|
123
|
-
const nonces = await Promise.all(wallets.map(w => nonceManager.getNextNonce(w)));
|
|
124
|
-
const signedTxs = await Promise.all(unsignedTxs.map((unsigned, i) => wallets[i].signTransaction({
|
|
125
|
-
...unsigned,
|
|
126
|
-
from: wallets[i].address,
|
|
127
|
-
nonce: nonces[i],
|
|
128
|
-
gasLimit,
|
|
129
|
-
gasPrice,
|
|
130
|
-
chainId,
|
|
131
|
-
type: txType,
|
|
132
|
-
value: unsigned.value // ✅ 显式保留 value(BNB 金额 + 手续费)
|
|
133
|
-
})));
|
|
134
|
-
const bundleResult = await merkle.sendBundle({
|
|
135
|
-
transactions: signedTxs,
|
|
136
|
-
...getBundleOptions(config, blockOffset)
|
|
137
|
-
});
|
|
138
|
-
return bundleResult;
|
|
139
|
-
}
|
|
140
|
-
finally {
|
|
141
|
-
// 确保清理临时 nonce 缓存
|
|
142
|
-
nonceManager.clearTemp();
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* 等待 Bundle 确认并返回状态
|
|
147
|
-
*/
|
|
148
|
-
async function waitForBundleResult(merkle, bundleResult, waitForConfirmation, waitTimeoutMs) {
|
|
149
|
-
if (!waitForConfirmation) {
|
|
150
|
-
return {
|
|
151
|
-
bundleHash: bundleResult.bundleHash,
|
|
152
|
-
txHashes: bundleResult.txHashes,
|
|
153
|
-
results: [],
|
|
154
|
-
successCount: 0,
|
|
155
|
-
totalGasUsed: 0n,
|
|
156
|
-
targetBlock: bundleResult.targetBlock
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
const results = await merkle.waitForBundleConfirmation(bundleResult.txHashes, 1, waitTimeoutMs);
|
|
160
|
-
const successCount = results.filter(r => r.success).length;
|
|
161
|
-
const totalGasUsed = results.reduce((sum, r) => {
|
|
162
|
-
if (r.gasUsed) {
|
|
163
|
-
return sum + BigInt(r.gasUsed);
|
|
164
|
-
}
|
|
165
|
-
return sum;
|
|
166
|
-
}, 0n);
|
|
167
|
-
return {
|
|
168
|
-
bundleHash: bundleResult.bundleHash,
|
|
169
|
-
txHashes: bundleResult.txHashes,
|
|
170
|
-
results,
|
|
171
|
-
successCount,
|
|
172
|
-
totalGasUsed,
|
|
173
|
-
targetBlock: bundleResult.targetBlock
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
116
|
// ==================== 主要导出函数 ====================
|
|
177
117
|
/**
|
|
178
118
|
* 授权代币给 PancakeSwapProxy
|
|
@@ -303,11 +243,19 @@ export async function fourPancakeProxyBatchBuyMerkle(params) {
|
|
|
303
243
|
const extractProfit = shouldExtractProfit(config);
|
|
304
244
|
const { totalProfit, remainingAmounts } = calculateBatchProfit(originalAmountsWei, config);
|
|
305
245
|
const actualAmountsWei = remainingAmounts; // 扣除利润后用于购买的金额
|
|
306
|
-
|
|
307
|
-
const
|
|
246
|
+
const finalGasLimit = getGasLimit(config);
|
|
247
|
+
const nonceManager = new NonceManager(provider);
|
|
248
|
+
// ✅ 优化:并行获取 gasPrice、tokenDecimals 和 nonces
|
|
249
|
+
const [gasPrice, tokenDecimals, nonces] = await Promise.all([
|
|
308
250
|
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
309
|
-
getTokenDecimals(tokenAddress, provider)
|
|
251
|
+
getTokenDecimals(tokenAddress, provider),
|
|
252
|
+
Promise.all(buyers.map(w => nonceManager.getNextNonce(w)))
|
|
310
253
|
]);
|
|
254
|
+
// ✅ 如果需要利润转账,预先获取 nonce
|
|
255
|
+
let profitNonce;
|
|
256
|
+
if (extractProfit && totalProfit > 0n) {
|
|
257
|
+
profitNonce = await nonceManager.getNextNonce(buyers[0]);
|
|
258
|
+
}
|
|
311
259
|
// 计算 minOutputAmounts
|
|
312
260
|
let minOuts;
|
|
313
261
|
if (params.minOutputAmounts && params.minOutputAmounts.length === buyers.length) {
|
|
@@ -342,11 +290,7 @@ export async function fourPancakeProxyBatchBuyMerkle(params) {
|
|
|
342
290
|
else {
|
|
343
291
|
throw new Error(`Unsupported routeType: ${routeType}`);
|
|
344
292
|
}
|
|
345
|
-
// ✅
|
|
346
|
-
const finalGasLimit = getGasLimit(config);
|
|
347
|
-
// ✅ 优化:并行获取 nonces(直接签名和提交以支持利润转账)
|
|
348
|
-
const nonceManager = new NonceManager(provider);
|
|
349
|
-
const nonces = await Promise.all(buyers.map(w => nonceManager.getNextNonce(w)));
|
|
293
|
+
// ✅ 并行签名所有买入交易
|
|
350
294
|
const signedTxs = await Promise.all(unsignedBuys.map((unsigned, i) => buyers[i].signTransaction({
|
|
351
295
|
...unsigned,
|
|
352
296
|
from: buyers[i].address,
|
|
@@ -357,9 +301,8 @@ export async function fourPancakeProxyBatchBuyMerkle(params) {
|
|
|
357
301
|
type: txType,
|
|
358
302
|
value: unsigned.value // ✅ 显式保留 value(BNB 金额 + 手续费)
|
|
359
303
|
})));
|
|
360
|
-
// ✅
|
|
361
|
-
if (extractProfit && totalProfit > 0n) {
|
|
362
|
-
const profitNonce = await nonceManager.getNextNonce(buyers[0]);
|
|
304
|
+
// ✅ 添加利润转账(使用预先获取的 nonce)
|
|
305
|
+
if (extractProfit && totalProfit > 0n && profitNonce !== undefined) {
|
|
363
306
|
const profitTx = await buyers[0].signTransaction({
|
|
364
307
|
to: getProfitRecipient(),
|
|
365
308
|
value: totalProfit,
|
|
@@ -397,14 +340,17 @@ export async function fourPancakeProxyBatchSellMerkle(params) {
|
|
|
397
340
|
});
|
|
398
341
|
const txType = getTxType(config);
|
|
399
342
|
const sellers = privateKeys.map(k => new Wallet(k, provider));
|
|
400
|
-
|
|
401
|
-
const
|
|
343
|
+
const finalGasLimit = getGasLimit(config);
|
|
344
|
+
const nonceManager = new NonceManager(provider);
|
|
345
|
+
const extractProfit = shouldExtractProfit(config);
|
|
346
|
+
// ✅ 优化:并行获取 gasPrice、tokenDecimals、allowances 和 nonces
|
|
347
|
+
const [gasPrice, tokenDecimals, allowances, nonces] = await Promise.all([
|
|
402
348
|
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
403
|
-
getTokenDecimals(tokenAddress, provider)
|
|
349
|
+
getTokenDecimals(tokenAddress, provider),
|
|
350
|
+
batchCheckAllowances(provider, tokenAddress, sellers.map(w => w.address), pancakeProxyAddress),
|
|
351
|
+
Promise.all(sellers.map(w => nonceManager.getNextNonce(w)))
|
|
404
352
|
]);
|
|
405
353
|
const amountsWei = sellAmounts.map(a => ethers.parseUnits(a, tokenDecimals));
|
|
406
|
-
// ✅ Step 1: 使用 Multicall3 批量检查授权状态
|
|
407
|
-
const allowances = await batchCheckAllowances(provider, tokenAddress, sellers.map(w => w.address), pancakeProxyAddress);
|
|
408
354
|
// 找出需要授权的钱包索引
|
|
409
355
|
const needApprovalIndexes = [];
|
|
410
356
|
const APPROVAL_THRESHOLD = ethers.MaxUint256 / 2n;
|
|
@@ -463,6 +409,27 @@ export async function fourPancakeProxyBatchSellMerkle(params) {
|
|
|
463
409
|
// minOuts 设为预期收益的 95%(滑点保护)
|
|
464
410
|
minOuts = quotedOutputs.map(q => q * 95n / 100n);
|
|
465
411
|
}
|
|
412
|
+
// ✅ 计算利润并找出收益最多的钱包(提前计算,预先获取 nonce)
|
|
413
|
+
let totalProfit = 0n;
|
|
414
|
+
let maxRevenueIndex = 0;
|
|
415
|
+
let maxRevenue = 0n;
|
|
416
|
+
if (extractProfit && quotedOutputs.length > 0) {
|
|
417
|
+
for (let i = 0; i < sellers.length; i++) {
|
|
418
|
+
if (quotedOutputs[i] > 0n) {
|
|
419
|
+
const { profit } = calculateProfit(quotedOutputs[i], config);
|
|
420
|
+
totalProfit += profit;
|
|
421
|
+
if (quotedOutputs[i] > maxRevenue) {
|
|
422
|
+
maxRevenue = quotedOutputs[i];
|
|
423
|
+
maxRevenueIndex = i;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// ✅ 如果需要利润转账,预先获取 nonce
|
|
429
|
+
let profitNonce;
|
|
430
|
+
if (extractProfit && totalProfit > 0n && maxRevenue > 0n) {
|
|
431
|
+
profitNonce = await nonceManager.getNextNonce(sellers[maxRevenueIndex]);
|
|
432
|
+
}
|
|
466
433
|
// 卖出不需要发送 BNB,只需要 flatFee
|
|
467
434
|
const needBNB = false;
|
|
468
435
|
// 构建交易
|
|
@@ -489,18 +456,7 @@ export async function fourPancakeProxyBatchSellMerkle(params) {
|
|
|
489
456
|
else {
|
|
490
457
|
throw new Error(`Unsupported routeType: ${routeType}`);
|
|
491
458
|
}
|
|
492
|
-
// ✅
|
|
493
|
-
const balances = await Promise.all(sellers.map(w => provider.getBalance(w.address)));
|
|
494
|
-
const minRequiredBNB = ethers.parseEther('0.001'); // 至少需要 0.001 BNB(包括 gas 和手续费)
|
|
495
|
-
const insufficientBalances = balances.filter(b => b < minRequiredBNB);
|
|
496
|
-
if (insufficientBalances.length > 0) {
|
|
497
|
-
throw new Error(`${insufficientBalances.length} 个钱包 BNB 余额不足 0.001 BNB`);
|
|
498
|
-
}
|
|
499
|
-
// ✅ 使用前端传入的 gasLimit,否则使用默认值
|
|
500
|
-
const finalGasLimit = getGasLimit(config);
|
|
501
|
-
// ✅ 直接签名和提交(内联处理以支持利润转账)
|
|
502
|
-
const nonceManager = new NonceManager(provider);
|
|
503
|
-
const nonces = await Promise.all(sellers.map(w => nonceManager.getNextNonce(w)));
|
|
459
|
+
// ✅ 并行签名所有卖出交易
|
|
504
460
|
const signedTxs = await Promise.all(unsignedSells.map((unsigned, i) => sellers[i].signTransaction({
|
|
505
461
|
...unsigned,
|
|
506
462
|
from: sellers[i].address,
|
|
@@ -511,38 +467,18 @@ export async function fourPancakeProxyBatchSellMerkle(params) {
|
|
|
511
467
|
type: txType,
|
|
512
468
|
value: unsigned.value // ✅ 显式保留 value(手续费)
|
|
513
469
|
})));
|
|
514
|
-
// ✅
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (quotedOutputs[i] > maxRevenue) {
|
|
527
|
-
maxRevenue = quotedOutputs[i];
|
|
528
|
-
maxRevenueIndex = i;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
// ✅ 由收益最多的钱包一次性转出总利润(该钱包最有能力支付)
|
|
533
|
-
if (totalProfit > 0n && maxRevenue > 0n) {
|
|
534
|
-
const profitNonce = await nonceManager.getNextNonce(sellers[maxRevenueIndex]);
|
|
535
|
-
const profitTx = await sellers[maxRevenueIndex].signTransaction({
|
|
536
|
-
to: getProfitRecipient(),
|
|
537
|
-
value: totalProfit,
|
|
538
|
-
nonce: profitNonce,
|
|
539
|
-
gasPrice,
|
|
540
|
-
gasLimit: 21000n,
|
|
541
|
-
chainId: 56,
|
|
542
|
-
type: txType
|
|
543
|
-
});
|
|
544
|
-
signedTxs.push(profitTx);
|
|
545
|
-
}
|
|
470
|
+
// ✅ 添加利润转账(使用预先获取的 nonce)
|
|
471
|
+
if (extractProfit && totalProfit > 0n && profitNonce !== undefined) {
|
|
472
|
+
const profitTx = await sellers[maxRevenueIndex].signTransaction({
|
|
473
|
+
to: getProfitRecipient(),
|
|
474
|
+
value: totalProfit,
|
|
475
|
+
nonce: profitNonce,
|
|
476
|
+
gasPrice,
|
|
477
|
+
gasLimit: 21000n,
|
|
478
|
+
chainId: 56,
|
|
479
|
+
type: txType
|
|
480
|
+
});
|
|
481
|
+
signedTxs.push(profitTx);
|
|
546
482
|
}
|
|
547
483
|
// ✅ 清理临时 nonce 缓存
|
|
548
484
|
nonceManager.clearTemp();
|
|
@@ -22,6 +22,10 @@ export interface MerkleSubmitConfig {
|
|
|
22
22
|
autoRetryBundle?: boolean;
|
|
23
23
|
/** 最大重试次数(可选,默认2) */
|
|
24
24
|
maxBundleRetries?: number;
|
|
25
|
+
/** ✅ 新增:使用时间戳模式(Merkle 会在时间范围内尽快打包) */
|
|
26
|
+
useTimestampMode?: boolean;
|
|
27
|
+
/** ✅ 新增:时间戳窗口秒数(默认15秒,仅在 useTimestampMode=true 时生效) */
|
|
28
|
+
timestampWindowSeconds?: number;
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
27
31
|
* Bundle提交结果
|
|
@@ -6,6 +6,21 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { MerkleClient } from '../../clients/merkle.js';
|
|
8
8
|
import { ethers } from 'ethers';
|
|
9
|
+
// ✅ 优化:MerkleClient 缓存,避免重复创建
|
|
10
|
+
const merkleClientCache = new Map();
|
|
11
|
+
function getMerkleClient(config) {
|
|
12
|
+
const cacheKey = `${config.apiKey}-${config.chainId ?? 56}-${config.customRpcUrl ?? ''}`;
|
|
13
|
+
let client = merkleClientCache.get(cacheKey);
|
|
14
|
+
if (!client) {
|
|
15
|
+
client = new MerkleClient({
|
|
16
|
+
apiKey: config.apiKey,
|
|
17
|
+
chainId: config.chainId ?? 56,
|
|
18
|
+
customRpcUrl: config.customRpcUrl
|
|
19
|
+
});
|
|
20
|
+
merkleClientCache.set(cacheKey, client);
|
|
21
|
+
}
|
|
22
|
+
return client;
|
|
23
|
+
}
|
|
9
24
|
/**
|
|
10
25
|
* 提交已签名的交易到Merkle(服务器端使用)
|
|
11
26
|
*
|
|
@@ -52,19 +67,18 @@ export async function submitBundleToMerkle(signedTransactions, config) {
|
|
|
52
67
|
error: 'apiKey is required in config'
|
|
53
68
|
};
|
|
54
69
|
}
|
|
55
|
-
//
|
|
56
|
-
const merkle =
|
|
57
|
-
apiKey: config.apiKey,
|
|
58
|
-
chainId: config.chainId ?? 56,
|
|
59
|
-
customRpcUrl: config.customRpcUrl
|
|
60
|
-
});
|
|
70
|
+
// ✅ 优化:使用缓存的 MerkleClient(避免重复创建 JsonRpcProvider)
|
|
71
|
+
const merkle = getMerkleClient(config);
|
|
61
72
|
// 提交Bundle
|
|
62
73
|
const bundleResult = await merkle.sendBundle({
|
|
63
74
|
transactions: signedTransactions,
|
|
64
75
|
blockOffset: config.bundleBlockOffset ?? 3,
|
|
65
76
|
minBlockOffset: config.minBlockOffset ?? 3,
|
|
66
77
|
autoRetry: config.autoRetryBundle ?? false,
|
|
67
|
-
maxRetries: config.maxBundleRetries ?? 2
|
|
78
|
+
maxRetries: config.maxBundleRetries ?? 2,
|
|
79
|
+
// ✅ 新增:时间戳模式
|
|
80
|
+
useTimestampMode: config.useTimestampMode,
|
|
81
|
+
timestampWindowSeconds: config.timestampWindowSeconds
|
|
68
82
|
});
|
|
69
83
|
// ✅ 提交成功
|
|
70
84
|
return {
|