four-flap-meme-sdk 1.4.28 → 1.4.30
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.
|
@@ -748,23 +748,16 @@ export async function directV3BatchBuy(params) {
|
|
|
748
748
|
const totalFlowWei = flowAmounts.reduce((sum, amt) => sum + amt, 0n);
|
|
749
749
|
const baseProfitWei = calculateProfitAmount(totalFlowWei);
|
|
750
750
|
// ✅ 方案 B:并行获取 nonces、gasPrice 和 ERC20 报价
|
|
751
|
-
// ✅ 修复:ERC20 报价不传 fee 参数,让报价函数自动选择最佳费率
|
|
752
|
-
// 因为 quoteToken/WBNB 池的费率可能与交易池(tokenAddress)的费率不同
|
|
753
751
|
const [nonces, gasPrice, nativeProfitWei] = await Promise.all([
|
|
754
752
|
startNonces && startNonces.length === wallets.length
|
|
755
753
|
? Promise.resolve(startNonces)
|
|
756
754
|
: new NonceManager(provider).getNextNoncesForWallets(wallets),
|
|
757
755
|
getGasPrice(provider, config),
|
|
758
|
-
// ERC20
|
|
756
|
+
// ERC20 报价(V3 买入用 V3 报价)
|
|
759
757
|
(!useNative && baseProfitWei > 0n && quoteToken)
|
|
760
|
-
? getTokenToNativeQuote(provider, quoteToken, baseProfitWei, chain, 'v3')
|
|
758
|
+
? getTokenToNativeQuote(provider, quoteToken, baseProfitWei, chain, 'v3', fee)
|
|
761
759
|
: Promise.resolve(baseProfitWei)
|
|
762
760
|
]);
|
|
763
|
-
// ✅ 添加详细日志
|
|
764
|
-
if (!useNative) {
|
|
765
|
-
console.log(`[V3 Buy Profit] quoteToken=${quoteToken}, baseProfitWei=${baseProfitWei} (${ethers.formatEther(baseProfitWei)} quoteToken)`);
|
|
766
|
-
console.log(`[V3 Buy Profit] nativeProfitWei=${nativeProfitWei} (${ethers.formatEther(nativeProfitWei)} BNB)`);
|
|
767
|
-
}
|
|
768
761
|
const profitWei = nativeProfitWei > 0n ? nativeProfitWei : 0n;
|
|
769
762
|
const gasLimit = getGasLimit(config, 300000);
|
|
770
763
|
const txType = config.txType ?? 0;
|
|
@@ -14,7 +14,6 @@ export interface PancakeBuyFirstSignConfig {
|
|
|
14
14
|
chainId?: number;
|
|
15
15
|
reserveGasBNB?: number;
|
|
16
16
|
skipQuoteOnError?: boolean;
|
|
17
|
-
skipApprovalCheck?: boolean;
|
|
18
17
|
bribeAmount?: number;
|
|
19
18
|
}
|
|
20
19
|
export type SwapRouteType = 'v2' | 'v3-single' | 'v3-multi';
|
|
@@ -43,7 +42,6 @@ export interface PancakeBuyFirstConfig extends CommonBundleConfig {
|
|
|
43
42
|
reserveGasBNB?: number;
|
|
44
43
|
waitForConfirmation?: boolean;
|
|
45
44
|
waitTimeoutMs?: number;
|
|
46
|
-
skipApprovalCheck?: boolean;
|
|
47
45
|
}
|
|
48
46
|
export interface PancakeBundleBuyFirstSignParams {
|
|
49
47
|
buyerPrivateKey: string;
|
|
@@ -55,6 +53,7 @@ export interface PancakeBundleBuyFirstSignParams {
|
|
|
55
53
|
config: PancakeBuyFirstSignConfig;
|
|
56
54
|
quoteToken?: string;
|
|
57
55
|
quoteTokenDecimals?: number;
|
|
56
|
+
startNonces?: number[];
|
|
58
57
|
}
|
|
59
58
|
export interface PancakeBundleBuyFirstParams {
|
|
60
59
|
buyerPrivateKey: string;
|
|
@@ -73,7 +72,6 @@ export type PancakeBuyFirstResult = {
|
|
|
73
72
|
sellerAddress: string;
|
|
74
73
|
buyAmount: string;
|
|
75
74
|
sellAmount: string;
|
|
76
|
-
hasApproval?: boolean;
|
|
77
75
|
profitAmount?: string;
|
|
78
76
|
};
|
|
79
77
|
};
|
|
@@ -7,7 +7,7 @@ import { ethers, Contract, Wallet } from 'ethers';
|
|
|
7
7
|
import { NonceManager, getDeadline } from '../utils/bundle-helpers.js';
|
|
8
8
|
import { ADDRESSES, PROFIT_CONFIG, BLOCKRAZOR_BUILDER_EOA, ZERO_ADDRESS } from '../utils/constants.js';
|
|
9
9
|
import { quoteV2, quoteV3, getTokenToNativeQuote, getWrappedNativeAddress } from '../utils/quote-helpers.js';
|
|
10
|
-
import { V2_ROUTER_ABI, V3_ROUTER02_ABI, ERC20_BALANCE_ABI
|
|
10
|
+
import { V2_ROUTER_ABI, V3_ROUTER02_ABI, ERC20_BALANCE_ABI } from '../abis/common.js';
|
|
11
11
|
/**
|
|
12
12
|
* 获取 Gas Limit
|
|
13
13
|
*/
|
|
@@ -44,10 +44,9 @@ const PANCAKE_V3_ROUTER_ADDRESS = ADDRESSES.BSC.PancakeV3Router;
|
|
|
44
44
|
// 常量
|
|
45
45
|
const FLAT_FEE = 0n;
|
|
46
46
|
const WBNB_ADDRESS = ADDRESSES.BSC.WBNB;
|
|
47
|
-
// ✅ getDeadline 从 bundle-helpers.js 导入
|
|
48
|
-
const APPROVE_INTERFACE = new ethers.Interface(['function approve(address,uint256) returns (bool)']);
|
|
49
47
|
export async function pancakeBundleBuyFirstMerkle(params) {
|
|
50
|
-
const { buyerPrivateKey, sellerPrivateKey, tokenAddress, routeParams, buyerFunds, buyerFundsPercentage, config, quoteToken, quoteTokenDecimals = 18
|
|
48
|
+
const { buyerPrivateKey, sellerPrivateKey, tokenAddress, routeParams, buyerFunds, buyerFundsPercentage, config, quoteToken, quoteTokenDecimals = 18, startNonces // ✅ 可选:前端预获取的 nonces
|
|
49
|
+
} = params;
|
|
51
50
|
// ✅ 判断是否使用原生代币(BNB)或 ERC20 代币(如 USDT)
|
|
52
51
|
const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS;
|
|
53
52
|
const context = createPancakeContext(config);
|
|
@@ -69,7 +68,6 @@ export async function pancakeBundleBuyFirstMerkle(params) {
|
|
|
69
68
|
buyerFundsWei: buyerFundsInfo.buyerFundsWei,
|
|
70
69
|
provider: context.provider
|
|
71
70
|
});
|
|
72
|
-
const decimals = await getTokenDecimals(context.provider, tokenAddress);
|
|
73
71
|
const swapUnsigned = await buildRouteTransactions({
|
|
74
72
|
routeParams,
|
|
75
73
|
buyerFundsWei: buyerFundsInfo.buyerFundsWei,
|
|
@@ -94,17 +92,6 @@ export async function pancakeBundleBuyFirstMerkle(params) {
|
|
|
94
92
|
const gasPrice = await getGasPrice(context.provider, config);
|
|
95
93
|
const txType = config.txType ?? 0;
|
|
96
94
|
const nonceManager = new NonceManager(context.provider);
|
|
97
|
-
const approvalTx = await ensureSellerApproval({
|
|
98
|
-
tokenAddress,
|
|
99
|
-
seller,
|
|
100
|
-
provider: context.provider,
|
|
101
|
-
decimals,
|
|
102
|
-
chainId: context.chainId,
|
|
103
|
-
config,
|
|
104
|
-
nonceManager,
|
|
105
|
-
gasPrice,
|
|
106
|
-
txType
|
|
107
|
-
});
|
|
108
95
|
// ✅ 修复:基于买入金额估算利润,而不是基于卖出预估(因为代币可能还没有流动性)
|
|
109
96
|
// 在先买后卖的场景中,卖出收益 ≈ 买入金额(忽略滑点和手续费)
|
|
110
97
|
const estimatedProfitFromSell = await estimateProfitAmount({
|
|
@@ -121,15 +108,17 @@ export async function pancakeBundleBuyFirstMerkle(params) {
|
|
|
121
108
|
? ethers.parseEther(String(config.bribeAmount))
|
|
122
109
|
: 0n;
|
|
123
110
|
const needBribeTx = bribeAmount > 0n;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
111
|
+
// ✅ 如果前端传入了 startNonces,直接使用(性能优化,避免 nonce 冲突)
|
|
112
|
+
const noncePlan = startNonces && startNonces.length >= (sameAddress ? 1 : 2)
|
|
113
|
+
? buildNoncePlanFromStartNonces(startNonces, sameAddress, profitAmount > 0n, needBribeTx)
|
|
114
|
+
: await planNonces({
|
|
115
|
+
buyer,
|
|
116
|
+
seller,
|
|
117
|
+
sameAddress,
|
|
118
|
+
extractProfit: profitAmount > 0n,
|
|
119
|
+
needBribeTx,
|
|
120
|
+
nonceManager
|
|
121
|
+
});
|
|
133
122
|
// ✅ 贿赂交易放在首位(由卖方发送,与利润交易同一钱包)
|
|
134
123
|
let bribeTx = null;
|
|
135
124
|
if (needBribeTx && noncePlan.bribeNonce !== undefined) {
|
|
@@ -183,12 +172,10 @@ export async function pancakeBundleBuyFirstMerkle(params) {
|
|
|
183
172
|
provider: context.provider,
|
|
184
173
|
buyerAddress: buyer.address
|
|
185
174
|
});
|
|
186
|
-
// ✅ 组装顺序:贿赂 →
|
|
175
|
+
// ✅ 组装顺序:贿赂 → 买入 → 卖出 → 利润
|
|
187
176
|
const allTransactions = [];
|
|
188
177
|
if (bribeTx)
|
|
189
178
|
allTransactions.push(bribeTx);
|
|
190
|
-
if (approvalTx)
|
|
191
|
-
allTransactions.push(approvalTx);
|
|
192
179
|
allTransactions.push(signedBuy, signedSell);
|
|
193
180
|
if (profitTx)
|
|
194
181
|
allTransactions.push(profitTx);
|
|
@@ -201,7 +188,6 @@ export async function pancakeBundleBuyFirstMerkle(params) {
|
|
|
201
188
|
? ethers.formatEther(buyerFundsInfo.buyerFundsWei)
|
|
202
189
|
: ethers.formatUnits(buyerFundsInfo.buyerFundsWei, quoteTokenDecimals),
|
|
203
190
|
sellAmount: quoteResult.quotedTokenOut.toString(),
|
|
204
|
-
hasApproval: !!approvalTx,
|
|
205
191
|
profitAmount: profitAmount > 0n ? ethers.formatEther(profitAmount) : undefined
|
|
206
192
|
}
|
|
207
193
|
};
|
|
@@ -303,10 +289,6 @@ async function quoteTokenOutput({ routeParams, buyerFundsWei, provider }) {
|
|
|
303
289
|
}
|
|
304
290
|
return { quotedTokenOut: result.amountOut };
|
|
305
291
|
}
|
|
306
|
-
async function getTokenDecimals(provider, tokenAddress) {
|
|
307
|
-
const erc20 = new Contract(tokenAddress, ['function decimals() view returns (uint8)'], provider);
|
|
308
|
-
return await erc20.decimals();
|
|
309
|
-
}
|
|
310
292
|
/**
|
|
311
293
|
* ✅ 使用 quote-helpers 统一报价(卖出 → 原生代币)
|
|
312
294
|
*/
|
|
@@ -328,30 +310,6 @@ function calculateBuyerNeed({ quotedNative, buyerBalance, reserveGas }) {
|
|
|
328
310
|
: buyerBalance - reserveGas;
|
|
329
311
|
return { buyerNeedTotal, maxBuyerValue };
|
|
330
312
|
}
|
|
331
|
-
async function ensureSellerApproval({ tokenAddress, seller, provider, decimals, chainId, config, nonceManager, gasPrice, txType }) {
|
|
332
|
-
if (config.skipApprovalCheck) {
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
const erc20Contract = new Contract(tokenAddress, ERC20_ALLOWANCE_ABI, provider);
|
|
336
|
-
// ✅ 使用官方 V2 Router 作为授权目标
|
|
337
|
-
const currentAllowance = await erc20Contract.allowance(seller.address, PANCAKE_V2_ROUTER_ADDRESS);
|
|
338
|
-
// ✅ 阈值:MaxUint256 / 2,如果授权额度超过这个值,认为是"无限授权"
|
|
339
|
-
const halfMaxUint = ethers.MaxUint256 / 2n;
|
|
340
|
-
if (currentAllowance >= halfMaxUint) {
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
const approvalNonce = await nonceManager.getNextNonce(seller);
|
|
344
|
-
return await seller.signTransaction({
|
|
345
|
-
to: tokenAddress,
|
|
346
|
-
data: APPROVE_INTERFACE.encodeFunctionData('approve', [PANCAKE_V2_ROUTER_ADDRESS, ethers.MaxUint256]),
|
|
347
|
-
value: 0n,
|
|
348
|
-
nonce: approvalNonce,
|
|
349
|
-
gasLimit: 80000n,
|
|
350
|
-
gasPrice,
|
|
351
|
-
chainId,
|
|
352
|
-
type: txType
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
313
|
async function buildRouteTransactions({ routeParams, buyerFundsWei, sellAmountToken, buyer, seller, tokenAddress, useNativeToken = true }) {
|
|
356
314
|
const deadline = getDeadline();
|
|
357
315
|
// ✅ ERC20 购买时,value 只需要 FLAT_FEE
|
|
@@ -421,25 +379,23 @@ async function estimateProfitAmount({ provider, tokenAddress, sellAmountToken, r
|
|
|
421
379
|
}
|
|
422
380
|
/**
|
|
423
381
|
* ✅ 规划 nonce
|
|
424
|
-
* 交易顺序:贿赂 →
|
|
382
|
+
* 交易顺序:贿赂 → 买入 → 卖出 → 利润
|
|
425
383
|
*/
|
|
426
|
-
async function planNonces({ buyer, seller, sameAddress,
|
|
384
|
+
async function planNonces({ buyer, seller, sameAddress, extractProfit, needBribeTx, nonceManager }) {
|
|
427
385
|
if (sameAddress) {
|
|
428
|
-
// 同一地址:贿赂(可选) +
|
|
429
|
-
const txCount = countTruthy([needBribeTx,
|
|
386
|
+
// 同一地址:贿赂(可选) + 买入 + 卖出 + 利润(可选)
|
|
387
|
+
const txCount = countTruthy([needBribeTx, true, true, extractProfit]);
|
|
430
388
|
const nonces = await nonceManager.getNextNonceBatch(buyer, txCount);
|
|
431
389
|
let idx = 0;
|
|
432
390
|
const bribeNonce = needBribeTx ? nonces[idx++] : undefined;
|
|
433
|
-
if (approvalExists)
|
|
434
|
-
idx++;
|
|
435
391
|
const buyerNonce = nonces[idx++];
|
|
436
392
|
const sellerNonce = nonces[idx++];
|
|
437
393
|
const profitNonce = extractProfit ? nonces[idx] : undefined;
|
|
438
394
|
return { buyerNonce, sellerNonce, bribeNonce, profitNonce };
|
|
439
395
|
}
|
|
440
|
-
if (needBribeTx ||
|
|
441
|
-
// 卖方需要多个 nonce:贿赂(可选) +
|
|
442
|
-
const sellerTxCount = countTruthy([needBribeTx,
|
|
396
|
+
if (needBribeTx || extractProfit) {
|
|
397
|
+
// 卖方需要多个 nonce:贿赂(可选) + 卖出 + 利润(可选)
|
|
398
|
+
const sellerTxCount = countTruthy([needBribeTx, true, extractProfit]);
|
|
443
399
|
// ✅ 并行获取 seller 和 buyer 的 nonce
|
|
444
400
|
const [sellerNonces, buyerNonce] = await Promise.all([
|
|
445
401
|
nonceManager.getNextNonceBatch(seller, sellerTxCount),
|
|
@@ -447,8 +403,6 @@ async function planNonces({ buyer, seller, sameAddress, approvalExists, extractP
|
|
|
447
403
|
]);
|
|
448
404
|
let idx = 0;
|
|
449
405
|
const bribeNonce = needBribeTx ? sellerNonces[idx++] : undefined;
|
|
450
|
-
if (approvalExists)
|
|
451
|
-
idx++;
|
|
452
406
|
const sellerNonce = sellerNonces[idx++];
|
|
453
407
|
const profitNonce = extractProfit ? sellerNonces[idx] : undefined;
|
|
454
408
|
return { buyerNonce, sellerNonce, bribeNonce, profitNonce };
|
|
@@ -512,3 +466,27 @@ async function validateFinalBalances({ sameAddress, buyerFundsWei, buyerBalance,
|
|
|
512
466
|
function countTruthy(values) {
|
|
513
467
|
return values.filter(Boolean).length;
|
|
514
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* ✅ 从前端传入的 startNonces 构建 NoncePlan(用于性能优化,避免 nonce 冲突)
|
|
471
|
+
* 顺序:同地址时 [baseNonce],不同地址时 [sellerNonce, buyerNonce]
|
|
472
|
+
*/
|
|
473
|
+
function buildNoncePlanFromStartNonces(startNonces, sameAddress, profitNeeded, needBribeTx) {
|
|
474
|
+
if (sameAddress) {
|
|
475
|
+
// 同一地址:贿赂(可选) + 买入 + 卖出 + 利润(可选)
|
|
476
|
+
let idx = 0;
|
|
477
|
+
const baseNonce = startNonces[0];
|
|
478
|
+
const bribeNonce = needBribeTx ? baseNonce + idx++ : undefined;
|
|
479
|
+
const buyerNonce = baseNonce + idx++;
|
|
480
|
+
const sellerNonce = baseNonce + idx++;
|
|
481
|
+
const profitNonce = profitNeeded ? baseNonce + idx : undefined;
|
|
482
|
+
return { buyerNonce, sellerNonce, bribeNonce, profitNonce };
|
|
483
|
+
}
|
|
484
|
+
// 不同地址
|
|
485
|
+
let sellerIdx = 0;
|
|
486
|
+
const sellerBaseNonce = startNonces[0];
|
|
487
|
+
const bribeNonce = needBribeTx ? sellerBaseNonce + sellerIdx++ : undefined;
|
|
488
|
+
const sellerNonce = sellerBaseNonce + sellerIdx++;
|
|
489
|
+
const profitNonce = profitNeeded ? sellerBaseNonce + sellerIdx : undefined;
|
|
490
|
+
const buyerNonce = startNonces[1];
|
|
491
|
+
return { buyerNonce, sellerNonce, bribeNonce, profitNonce };
|
|
492
|
+
}
|