four-flap-meme-sdk 1.4.11 → 1.4.13
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/dex/direct-router.js +3 -307
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/pancake/bundle-swap.d.ts +8 -4
- package/dist/pancake/bundle-swap.js +305 -357
- package/dist/utils/quote-helpers.d.ts +111 -0
- package/dist/utils/quote-helpers.js +358 -0
- package/package.json +1 -1
|
@@ -33,144 +33,52 @@ async function ensureSellerApproval({ tokenAddress, seller, provider, decimals,
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
async function quoteSellOutput({ routeParams, sellAmountWei, provider }) {
|
|
36
|
-
|
|
37
|
-
? routeParams.v2Path[0]
|
|
38
|
-
: routeParams.v3TokenIn;
|
|
39
|
-
const tokenInLower = tokenIn.toLowerCase();
|
|
36
|
+
console.log(`[quoteSellOutput] 开始报价, routeType=${routeParams.routeType}, sellAmount=${sellAmountWei} wei`);
|
|
40
37
|
// ==================== V2 报价 ====================
|
|
41
38
|
if (routeParams.routeType === 'v2') {
|
|
42
39
|
const { v2Path } = routeParams;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
console.log(`[quoteSellOutput] V2 直接路径失败: ${String(err).slice(0, 100)}`);
|
|
55
|
-
}
|
|
56
|
-
// V2 策略 2:多跳路径 (代币 → 稳定币 → WBNB)
|
|
57
|
-
for (const stableCoin of STABLE_COINS) {
|
|
58
|
-
if (tokenInLower === stableCoin.toLowerCase())
|
|
59
|
-
continue;
|
|
60
|
-
try {
|
|
61
|
-
const multiHopPath = [tokenIn, stableCoin, WBNB_ADDRESS];
|
|
62
|
-
const amounts = await v2Router.getAmountsOut(sellAmountWei, multiHopPath);
|
|
63
|
-
const amountOut = amounts[amounts.length - 1];
|
|
64
|
-
if (amountOut > 0n) {
|
|
65
|
-
console.log(`[quoteSellOutput] V2 多跳路径成功 (via ${stableCoin.slice(0, 10)}...): ${ethers.formatEther(amountOut)} BNB`);
|
|
66
|
-
return { estimatedBNBOut: amountOut };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
40
|
+
const tokenIn = v2Path[0];
|
|
41
|
+
const tokenOut = v2Path[v2Path.length - 1];
|
|
42
|
+
console.log(`[quoteSellOutput] V2 路径: ${v2Path.join(' → ')}`);
|
|
43
|
+
// ✅ 只用 V2 报价
|
|
44
|
+
const result = await quoteV2(provider, tokenIn, tokenOut, sellAmountWei, 'BSC');
|
|
45
|
+
if (result.amountOut > 0n) {
|
|
46
|
+
console.log(`[quoteSellOutput] V2 报价成功: ${ethers.formatEther(result.amountOut)} BNB`);
|
|
47
|
+
return { estimatedBNBOut: result.amountOut };
|
|
72
48
|
}
|
|
73
49
|
throw new Error('V2 报价失败: 所有路径均无效');
|
|
74
50
|
}
|
|
75
|
-
// ==================== V3
|
|
51
|
+
// ==================== V3 单跳报价 ====================
|
|
76
52
|
if (routeParams.routeType === 'v3-single') {
|
|
77
53
|
const params = routeParams;
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const result = await quoter.quoteExactInputSingle.staticCall({
|
|
86
|
-
tokenIn: params.v3TokenIn,
|
|
87
|
-
tokenOut: params.v3TokenOut,
|
|
88
|
-
amountIn: sellAmountWei,
|
|
89
|
-
fee: fee,
|
|
90
|
-
sqrtPriceLimitX96: 0n
|
|
91
|
-
});
|
|
92
|
-
const amountOut = Array.isArray(result) ? result[0] : result;
|
|
93
|
-
if (amountOut && amountOut > 0n) {
|
|
94
|
-
console.log(`[quoteSellOutput] V3 直接路径成功 (fee=${fee}): ${ethers.formatEther(amountOut)} BNB`);
|
|
95
|
-
return { estimatedBNBOut: amountOut };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
console.log(`[quoteSellOutput] V3 直接路径失败 (fee=${fee}): ${String(err).slice(0, 100)}`);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
54
|
+
console.log(`[quoteSellOutput] V3 单跳: ${params.v3TokenIn} → ${params.v3TokenOut}, fee=${params.v3Fee}`);
|
|
55
|
+
// ✅ 只用 V3 报价,不 fallback 到 V2
|
|
56
|
+
const result = await quoteV3(provider, params.v3TokenIn, params.v3TokenOut, sellAmountWei, 'BSC', params.v3Fee);
|
|
57
|
+
if (result.amountOut > 0n) {
|
|
58
|
+
console.log(`[quoteSellOutput] V3 报价成功 (fee=${result.fee}): ${ethers.formatEther(result.amountOut)} BNB`);
|
|
59
|
+
return { estimatedBNBOut: result.amountOut };
|
|
102
60
|
}
|
|
103
|
-
// V3
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!midAmount || midAmount <= 0n)
|
|
119
|
-
continue;
|
|
120
|
-
console.log(`[quoteSellOutput] V3 第一跳成功: 代币 → ${stableCoin.slice(0, 10)}... = ${midAmount}`);
|
|
121
|
-
// 第二跳:稳定币 → WBNB(尝试多个费率)
|
|
122
|
-
const stableFees = [100, 500, 2500];
|
|
123
|
-
for (const fee2 of stableFees) {
|
|
124
|
-
try {
|
|
125
|
-
const finalResult = await quoter.quoteExactInputSingle.staticCall({
|
|
126
|
-
tokenIn: stableCoin,
|
|
127
|
-
tokenOut: WBNB_ADDRESS,
|
|
128
|
-
amountIn: midAmount,
|
|
129
|
-
fee: fee2,
|
|
130
|
-
sqrtPriceLimitX96: 0n
|
|
131
|
-
});
|
|
132
|
-
const amountOut = Array.isArray(finalResult) ? finalResult[0] : finalResult;
|
|
133
|
-
if (amountOut && amountOut > 0n) {
|
|
134
|
-
console.log(`[quoteSellOutput] V3 多跳路径成功 (${fee1}→${fee2}): ${ethers.formatEther(amountOut)} BNB`);
|
|
135
|
-
return { estimatedBNBOut: amountOut };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
console.log(`[quoteSellOutput] V3 第一跳失败 (fee=${fee1}): ${String(err).slice(0, 100)}`);
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// V3 全部失败,尝试 V2 fallback
|
|
150
|
-
if (params.v2Path && params.v2Path.length >= 2) {
|
|
151
|
-
try {
|
|
152
|
-
const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
|
|
153
|
-
const amounts = await v2Router.getAmountsOut(sellAmountWei, params.v2Path);
|
|
154
|
-
const amountOut = amounts[amounts.length - 1];
|
|
155
|
-
if (amountOut > 0n) {
|
|
156
|
-
console.log(`[quoteSellOutput] V3→V2 fallback 成功: ${ethers.formatEther(amountOut)} BNB`);
|
|
157
|
-
return { estimatedBNBOut: amountOut };
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
catch (v2Error) {
|
|
161
|
-
console.log(`[quoteSellOutput] V3→V2 fallback 失败: ${String(v2Error).slice(0, 100)}`);
|
|
61
|
+
// ❌ V3 报价失败时不再 fallback 到 V2,因为价格可能差异很大
|
|
62
|
+
throw new Error(`V3 单跳报价失败: tokenIn=${params.v3TokenIn}, tokenOut=${params.v3TokenOut}, fee=${params.v3Fee}`);
|
|
63
|
+
}
|
|
64
|
+
// ==================== V3 多跳报价 ====================
|
|
65
|
+
if (routeParams.routeType === 'v3-multi') {
|
|
66
|
+
const params = routeParams;
|
|
67
|
+
console.log(`[quoteSellOutput] V3 多跳: LPs=${params.v3LpAddresses?.join(', ')}`);
|
|
68
|
+
// ✅ V3 多跳:只用 V3 报价,不 fallback 到 V2
|
|
69
|
+
if (params.v3LpAddresses && params.v3LpAddresses.length > 0) {
|
|
70
|
+
const tokenIn = params.v3ExactTokenIn;
|
|
71
|
+
const WBNB = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
|
|
72
|
+
const result = await quoteV3(provider, tokenIn, WBNB, sellAmountWei, 'BSC');
|
|
73
|
+
if (result.amountOut > 0n) {
|
|
74
|
+
console.log(`[quoteSellOutput] V3 多跳报价成功: ${ethers.formatEther(result.amountOut)} BNB`);
|
|
75
|
+
return { estimatedBNBOut: result.amountOut };
|
|
162
76
|
}
|
|
163
77
|
}
|
|
164
|
-
|
|
78
|
+
// ❌ V3 多跳报价失败时不再 fallback 到 V2,因为价格可能差异很大
|
|
79
|
+
throw new Error(`V3 多跳报价失败: LPs=${params.v3LpAddresses?.join(', ')}, tokenIn=${params.v3ExactTokenIn}`);
|
|
165
80
|
}
|
|
166
|
-
|
|
167
|
-
const params = routeParams;
|
|
168
|
-
if (params.v2Path && params.v2Path.length >= 2) {
|
|
169
|
-
const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
|
|
170
|
-
const amounts = await v2Router.getAmountsOut(sellAmountWei, params.v2Path);
|
|
171
|
-
return { estimatedBNBOut: amounts[amounts.length - 1] };
|
|
172
|
-
}
|
|
173
|
-
throw new Error('V3 多跳需要提供 v2Path 用于价格预估');
|
|
81
|
+
throw new Error(`不支持的路由类型: ${routeParams.routeType}`);
|
|
174
82
|
}
|
|
175
83
|
async function buildSwapTransactions({ routeParams, sellAmountWei, buyAmountBNB, buyer, seller, tokenAddress, useNativeToken = true }) {
|
|
176
84
|
const proxySeller = new Contract(PANCAKE_PROXY_ADDRESS, PANCAKE_PROXY_ABI, seller);
|
|
@@ -274,20 +182,6 @@ async function planNonces({ seller, buyer, sameAddress, approvalExists, profitNe
|
|
|
274
182
|
]);
|
|
275
183
|
return { sellerNonce, buyerNonce };
|
|
276
184
|
}
|
|
277
|
-
async function buildProfitTransaction({ wallet, profitAmount, profitNonce, gasPrice, chainId, txType }) {
|
|
278
|
-
if (!profitNonce || profitAmount === 0n) {
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
return await wallet.signTransaction({
|
|
282
|
-
to: PROFIT_CONFIG.RECIPIENT,
|
|
283
|
-
value: profitAmount,
|
|
284
|
-
nonce: profitNonce,
|
|
285
|
-
gasPrice,
|
|
286
|
-
gasLimit: 21000n,
|
|
287
|
-
chainId,
|
|
288
|
-
type: txType
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
185
|
function calculateProfitAmount(estimatedBNBOut) {
|
|
292
186
|
if (estimatedBNBOut <= 0n) {
|
|
293
187
|
return 0n;
|
|
@@ -297,13 +191,7 @@ function calculateProfitAmount(estimatedBNBOut) {
|
|
|
297
191
|
/**
|
|
298
192
|
* ✅ 获取 ERC20 代币 → 原生代币(BNB)的报价
|
|
299
193
|
* 用于将 ERC20 利润转换为 BNB
|
|
300
|
-
*
|
|
301
|
-
* @param provider - Provider 实例
|
|
302
|
-
* @param tokenAddress - ERC20 代币地址(如 USDT)
|
|
303
|
-
* @param tokenAmount - 代币数量(wei)
|
|
304
|
-
* @param version - 'v2' | 'v3',指定使用哪个版本的报价
|
|
305
|
-
* @param fee - V3 费率档位(仅 V3 时使用)
|
|
306
|
-
* @returns 等值的 BNB 数量(wei),失败返回 0n
|
|
194
|
+
* 使用共享的报价函数
|
|
307
195
|
*/
|
|
308
196
|
async function getERC20ToNativeQuote(provider, tokenAddress, tokenAmount, version = 'v2', fee) {
|
|
309
197
|
if (tokenAmount <= 0n)
|
|
@@ -314,110 +202,23 @@ async function getERC20ToNativeQuote(provider, tokenAddress, tokenAmount, versio
|
|
|
314
202
|
if (tokenLower === wbnbLower) {
|
|
315
203
|
return tokenAmount;
|
|
316
204
|
}
|
|
317
|
-
//
|
|
318
|
-
if (version === 'v2') {
|
|
319
|
-
const router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
|
|
320
|
-
// V2 策略 1:直接路径 代币 → WBNB
|
|
321
|
-
try {
|
|
322
|
-
const amounts = await router.getAmountsOut(tokenAmount, [tokenAddress, WBNB_ADDRESS]);
|
|
323
|
-
const nativeAmount = amounts[1];
|
|
324
|
-
if (nativeAmount > 0n) {
|
|
325
|
-
console.log(`[getERC20ToNativeQuote] V2 直接路径成功: ${ethers.formatEther(nativeAmount)} BNB`);
|
|
326
|
-
return nativeAmount;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
330
|
-
// V2 直接路径失败,尝试多跳
|
|
331
|
-
}
|
|
332
|
-
// V2 策略 2:多跳路径 代币 → 稳定币 → WBNB
|
|
333
|
-
for (const stableCoin of STABLE_COINS) {
|
|
334
|
-
if (tokenLower === stableCoin.toLowerCase()) {
|
|
335
|
-
// 代币本身就是稳定币
|
|
336
|
-
try {
|
|
337
|
-
const amounts = await router.getAmountsOut(tokenAmount, [stableCoin, WBNB_ADDRESS]);
|
|
338
|
-
const nativeAmount = amounts[1];
|
|
339
|
-
if (nativeAmount > 0n) {
|
|
340
|
-
console.log(`[getERC20ToNativeQuote] V2 稳定币直接路径成功: ${ethers.formatEther(nativeAmount)} BNB`);
|
|
341
|
-
return nativeAmount;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
catch {
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
console.warn(`[getERC20ToNativeQuote] V2 所有报价路径均失败`);
|
|
350
|
-
return 0n;
|
|
351
|
-
}
|
|
352
|
-
// ==================== V3 报价 ====================
|
|
205
|
+
// 使用共享的报价函数
|
|
353
206
|
if (version === 'v3') {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
try {
|
|
359
|
-
const result = await quoter.quoteExactInputSingle.staticCall({
|
|
360
|
-
tokenIn: tokenAddress,
|
|
361
|
-
tokenOut: WBNB_ADDRESS,
|
|
362
|
-
amountIn: tokenAmount,
|
|
363
|
-
fee: tryFee,
|
|
364
|
-
sqrtPriceLimitX96: 0n
|
|
365
|
-
});
|
|
366
|
-
const amountOut = Array.isArray(result) ? result[0] : result;
|
|
367
|
-
if (amountOut && amountOut > 0n) {
|
|
368
|
-
console.log(`[getERC20ToNativeQuote] V3 直接路径成功 (fee=${tryFee}): ${ethers.formatEther(amountOut)} BNB`);
|
|
369
|
-
return amountOut;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
catch {
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
// V3 策略 2:多跳路径 代币 → 稳定币 → WBNB
|
|
377
|
-
for (const stableCoin of STABLE_COINS) {
|
|
378
|
-
if (tokenLower === stableCoin.toLowerCase())
|
|
379
|
-
continue;
|
|
380
|
-
for (const fee1 of feesToTry) {
|
|
381
|
-
try {
|
|
382
|
-
const midResult = await quoter.quoteExactInputSingle.staticCall({
|
|
383
|
-
tokenIn: tokenAddress,
|
|
384
|
-
tokenOut: stableCoin,
|
|
385
|
-
amountIn: tokenAmount,
|
|
386
|
-
fee: fee1,
|
|
387
|
-
sqrtPriceLimitX96: 0n
|
|
388
|
-
});
|
|
389
|
-
const midAmount = Array.isArray(midResult) ? midResult[0] : midResult;
|
|
390
|
-
if (!midAmount || midAmount <= 0n)
|
|
391
|
-
continue;
|
|
392
|
-
const stableFees = [100, 500, 2500];
|
|
393
|
-
for (const fee2 of stableFees) {
|
|
394
|
-
try {
|
|
395
|
-
const finalResult = await quoter.quoteExactInputSingle.staticCall({
|
|
396
|
-
tokenIn: stableCoin,
|
|
397
|
-
tokenOut: WBNB_ADDRESS,
|
|
398
|
-
amountIn: midAmount,
|
|
399
|
-
fee: fee2,
|
|
400
|
-
sqrtPriceLimitX96: 0n
|
|
401
|
-
});
|
|
402
|
-
const amountOut = Array.isArray(finalResult) ? finalResult[0] : finalResult;
|
|
403
|
-
if (amountOut && amountOut > 0n) {
|
|
404
|
-
console.log(`[getERC20ToNativeQuote] V3 多跳路径成功 (${fee1}→${fee2}): ${ethers.formatEther(amountOut)} BNB`);
|
|
405
|
-
return amountOut;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
catch {
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
catch {
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
207
|
+
const result = await quoteV3(provider, tokenAddress, WBNB_ADDRESS, tokenAmount, 'BSC', fee);
|
|
208
|
+
if (result.amountOut > 0n) {
|
|
209
|
+
console.log(`[getERC20ToNativeQuote] V3 报价成功: ${ethers.formatEther(result.amountOut)} BNB`);
|
|
210
|
+
return result.amountOut;
|
|
417
211
|
}
|
|
418
|
-
console.warn(`[getERC20ToNativeQuote] V3
|
|
212
|
+
console.warn(`[getERC20ToNativeQuote] V3 报价失败`);
|
|
419
213
|
return 0n;
|
|
420
214
|
}
|
|
215
|
+
// V2 报价
|
|
216
|
+
const result = await quoteV2(provider, tokenAddress, WBNB_ADDRESS, tokenAmount, 'BSC');
|
|
217
|
+
if (result.amountOut > 0n) {
|
|
218
|
+
console.log(`[getERC20ToNativeQuote] V2 报价成功: ${ethers.formatEther(result.amountOut)} BNB`);
|
|
219
|
+
return result.amountOut;
|
|
220
|
+
}
|
|
221
|
+
console.warn(`[getERC20ToNativeQuote] V2 报价失败`);
|
|
421
222
|
return 0n;
|
|
422
223
|
}
|
|
423
224
|
async function validateFinalBalances({ sameAddress, buyerBalance, buyAmountBNB, reserveGas, gasLimit, gasPrice, useNativeToken = true, quoteTokenDecimals = 18, provider, buyerAddress }) {
|
|
@@ -469,6 +270,7 @@ import { ethers, Contract, Wallet } from 'ethers';
|
|
|
469
270
|
import { calculateSellAmount } from '../utils/swap-helpers.js';
|
|
470
271
|
import { NonceManager } from '../utils/bundle-helpers.js';
|
|
471
272
|
import { ADDRESSES, PROFIT_CONFIG } from '../utils/constants.js';
|
|
273
|
+
import { quoteV2, quoteV3 } from '../utils/quote-helpers.js';
|
|
472
274
|
// ✅ BlockRazor Builder EOA 地址(用于贿赂)
|
|
473
275
|
// 参考文档: https://blockrazor.gitbook.io/blockrazor/bsc/block-builder/send-bundle
|
|
474
276
|
const BLOCKRAZOR_BUILDER_EOA = '0x1266C6bE60392A8Ff346E8d5ECCd3E69dD9c5F20';
|
|
@@ -504,30 +306,13 @@ const PANCAKE_PROXY_ABI = [
|
|
|
504
306
|
'function swapV3Single(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint256 amountOutMin, address to) payable returns (uint256)',
|
|
505
307
|
'function swapV3MultiHop(address[] calldata lpAddresses, address exactTokenIn, uint256 amountIn, uint256 amountOutMin, address to) payable returns (uint256)'
|
|
506
308
|
];
|
|
507
|
-
// PancakeSwap V2 Router ABI(用于报价)
|
|
508
|
-
const PANCAKE_V2_ROUTER_ABI = [
|
|
509
|
-
'function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)'
|
|
510
|
-
];
|
|
511
|
-
// PancakeSwap V3 QuoterV2 ABI(用于报价)
|
|
512
|
-
const PANCAKE_V3_QUOTER_ABI = [
|
|
513
|
-
'function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96)) external returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)',
|
|
514
|
-
'function quoteExactInput(bytes memory path, uint256 amountIn) external returns (uint256 amountOut, uint160[] memory sqrtPriceX96AfterList, uint32[] memory initializedTicksCrossedList, uint256 gasEstimate)'
|
|
515
|
-
];
|
|
516
309
|
// 兼容旧代码:默认使用 BSC 地址
|
|
517
310
|
const PANCAKE_PROXY_ADDRESS = ADDRESSES.BSC.PancakeProxy;
|
|
518
|
-
|
|
519
|
-
const PANCAKE_V3_QUOTER_ADDRESS = '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997';
|
|
311
|
+
// ✅ V3 Quoter ABI 和地址已移至 ../utils/quote-helpers.ts
|
|
520
312
|
// 代理合约手续费
|
|
521
313
|
const FLAT_FEE = 0n; // ✅ 已移除合约固定手续费
|
|
522
314
|
const WBNB_ADDRESS = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
|
|
523
|
-
// ✅
|
|
524
|
-
const STABLE_COINS = [
|
|
525
|
-
'0x55d398326f99059fF775485246999027B3197955', // USDT
|
|
526
|
-
'0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', // USDC
|
|
527
|
-
'0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56', // BUSD
|
|
528
|
-
];
|
|
529
|
-
// ✅ V3 常用费率档位
|
|
530
|
-
const V3_FEE_TIERS = [100, 500, 2500, 10000]; // 0.01%, 0.05%, 0.25%, 1%
|
|
315
|
+
// ✅ STABLE_COINS 和 V3_FEE_TIERS 已移至 ../utils/quote-helpers.ts
|
|
531
316
|
const ERC20_ALLOWANCE_ABI = [
|
|
532
317
|
'function allowance(address,address) view returns (uint256)',
|
|
533
318
|
'function approve(address spender,uint256 amount) returns (bool)',
|
|
@@ -548,12 +333,16 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
548
333
|
const seller = new Wallet(sellerPrivateKey, context.provider);
|
|
549
334
|
const buyer = new Wallet(buyerPrivateKey, context.provider);
|
|
550
335
|
const sameAddress = seller.address.toLowerCase() === buyer.address.toLowerCase();
|
|
551
|
-
// ✅ 提前创建 NonceManager
|
|
336
|
+
// ✅ 提前创建 NonceManager,供所有交易共享
|
|
552
337
|
const nonceManager = new NonceManager(context.provider);
|
|
553
338
|
const finalGasLimit = getGasLimit(config);
|
|
554
|
-
const gasPrice = await getGasPrice(context.provider, config);
|
|
555
339
|
const txType = config.txType ?? 0;
|
|
556
|
-
|
|
340
|
+
// ✅ 并行获取 gasPrice 和卖出数量
|
|
341
|
+
const [gasPrice, sellAmountResult] = await Promise.all([
|
|
342
|
+
getGasPrice(context.provider, config),
|
|
343
|
+
calculateSellAmount(context.provider, tokenAddress, seller.address, sellAmount, sellPercentage)
|
|
344
|
+
]);
|
|
345
|
+
const { amount: sellAmountWei, decimals } = sellAmountResult;
|
|
557
346
|
// ✅ 先构建授权交易(会消耗 nonce)
|
|
558
347
|
const approvalTx = await ensureSellerApproval({
|
|
559
348
|
tokenAddress,
|
|
@@ -622,10 +411,11 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
622
411
|
needBribeTx, // ✅ 新增
|
|
623
412
|
nonceManager
|
|
624
413
|
});
|
|
625
|
-
// ✅
|
|
626
|
-
|
|
414
|
+
// ✅ 并行签名所有交易
|
|
415
|
+
const signPromises = [];
|
|
416
|
+
// 贿赂交易
|
|
627
417
|
if (needBribeTx && noncePlan.bribeNonce !== undefined) {
|
|
628
|
-
|
|
418
|
+
signPromises.push(seller.signTransaction({
|
|
629
419
|
to: BLOCKRAZOR_BUILDER_EOA,
|
|
630
420
|
value: bribeAmount,
|
|
631
421
|
nonce: noncePlan.bribeNonce,
|
|
@@ -633,9 +423,10 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
633
423
|
gasLimit: 21000n,
|
|
634
424
|
chainId: context.chainId,
|
|
635
425
|
type: txType
|
|
636
|
-
});
|
|
426
|
+
}).then(tx => ({ type: 'bribe', tx })));
|
|
637
427
|
}
|
|
638
|
-
|
|
428
|
+
// 卖出交易
|
|
429
|
+
signPromises.push(seller.signTransaction({
|
|
639
430
|
...swapUnsigned.sellUnsigned,
|
|
640
431
|
from: seller.address,
|
|
641
432
|
nonce: noncePlan.sellerNonce,
|
|
@@ -643,8 +434,9 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
643
434
|
gasPrice,
|
|
644
435
|
chainId: context.chainId,
|
|
645
436
|
type: txType
|
|
646
|
-
});
|
|
647
|
-
|
|
437
|
+
}).then(tx => ({ type: 'sell', tx })));
|
|
438
|
+
// 买入交易
|
|
439
|
+
signPromises.push(buyer.signTransaction({
|
|
648
440
|
...swapUnsigned.buyUnsigned,
|
|
649
441
|
from: buyer.address,
|
|
650
442
|
nonce: noncePlan.buyerNonce,
|
|
@@ -652,16 +444,26 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
652
444
|
gasPrice,
|
|
653
445
|
chainId: context.chainId,
|
|
654
446
|
type: txType
|
|
655
|
-
});
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
447
|
+
}).then(tx => ({ type: 'buy', tx })));
|
|
448
|
+
// 利润交易
|
|
449
|
+
if (profitAmount > 0n && noncePlan.profitNonce !== undefined) {
|
|
450
|
+
signPromises.push(seller.signTransaction({
|
|
451
|
+
to: PROFIT_CONFIG.RECIPIENT,
|
|
452
|
+
value: profitAmount,
|
|
453
|
+
nonce: noncePlan.profitNonce,
|
|
454
|
+
gasPrice,
|
|
455
|
+
gasLimit: 21000n,
|
|
456
|
+
chainId: context.chainId,
|
|
457
|
+
type: txType
|
|
458
|
+
}).then(tx => ({ type: 'profit', tx })));
|
|
459
|
+
}
|
|
460
|
+
// ✅ 并行执行所有签名
|
|
461
|
+
const signedResults = await Promise.all(signPromises);
|
|
462
|
+
// 按类型提取结果
|
|
463
|
+
const bribeTx = signedResults.find(r => r.type === 'bribe')?.tx || null;
|
|
464
|
+
const signedSell = signedResults.find(r => r.type === 'sell').tx;
|
|
465
|
+
const signedBuy = signedResults.find(r => r.type === 'buy').tx;
|
|
466
|
+
const profitTx = signedResults.find(r => r.type === 'profit')?.tx || null;
|
|
665
467
|
nonceManager.clearTemp();
|
|
666
468
|
validateFinalBalances({
|
|
667
469
|
sameAddress,
|
|
@@ -723,10 +525,13 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
723
525
|
// ✅ 创建共享资源
|
|
724
526
|
const nonceManager = new NonceManager(context.provider);
|
|
725
527
|
const finalGasLimit = getGasLimit(config);
|
|
726
|
-
const gasPrice = await getGasPrice(context.provider, config);
|
|
727
528
|
const txType = config.txType ?? 0;
|
|
728
|
-
// ✅
|
|
729
|
-
const
|
|
529
|
+
// ✅ 并行获取 gasPrice 和卖出数量
|
|
530
|
+
const [gasPrice, sellAmountResult] = await Promise.all([
|
|
531
|
+
getGasPrice(context.provider, config),
|
|
532
|
+
calculateSellAmount(context.provider, tokenAddress, seller.address, sellAmount, sellPercentage)
|
|
533
|
+
]);
|
|
534
|
+
const { amount: sellAmountWei, decimals } = sellAmountResult;
|
|
730
535
|
// ✅ 构建授权交易(如果需要)
|
|
731
536
|
const approvalTx = await ensureSellerApproval({
|
|
732
537
|
tokenAddress,
|
|
@@ -935,24 +740,33 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
935
740
|
/**
|
|
936
741
|
* PancakeSwap 快捷批量换手(资金自动流转)
|
|
937
742
|
*
|
|
938
|
-
* 流程:[贿赂] → [卖出] → [
|
|
743
|
+
* 流程:[贿赂] → [卖出] → [转账1, 转账2, ...] → [买入1, 买入2, ...] → [利润]
|
|
939
744
|
*
|
|
940
745
|
* 特点:
|
|
941
|
-
* -
|
|
746
|
+
* - 子钱包不需要预先有余额
|
|
942
747
|
* - 资金来自主钱包卖出代币所得
|
|
943
748
|
* - 提升资金利用率
|
|
749
|
+
* - 支持 BNB 和 ERC20(如 USDT)两种模式
|
|
944
750
|
*
|
|
945
|
-
* 限制:最多
|
|
751
|
+
* 限制:最多 23 个买方(转账 + 买入 = 46 笔,加上贿赂/卖出/利润 = 50 笔限制)
|
|
946
752
|
*/
|
|
947
753
|
export async function pancakeQuickBatchSwapMerkle(params) {
|
|
948
|
-
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKeys, buyerRatios, buyerAmounts, tokenAddress, routeParams, config } = params;
|
|
949
|
-
// ✅
|
|
950
|
-
const
|
|
754
|
+
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKeys, buyerRatios, buyerAmounts, tokenAddress, routeParams, config, quoteToken, quoteTokenDecimals = 18 } = params;
|
|
755
|
+
// ✅ 判断是否使用原生代币
|
|
756
|
+
const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS;
|
|
757
|
+
const WBNB = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c'.toLowerCase();
|
|
758
|
+
// ✅ 校验买方数量
|
|
759
|
+
// BNB 模式:贿赂(1) + 卖出(1) + 转账(N) + 买入(N) + 利润(1) ≤ 50 → 2N + 3 ≤ 50 → N ≤ 23
|
|
760
|
+
// ERC20 模式:贿赂(1) + 卖出(1) + ERC20转账(N) + BNB Gas转账(N) + 买入(N) + 利润(1) ≤ 50 → 3N + 3 ≤ 50 → N ≤ 15
|
|
761
|
+
const MAX_BUYERS_NATIVE = 23;
|
|
762
|
+
const MAX_BUYERS_ERC20 = 15;
|
|
763
|
+
const MAX_BUYERS = useNativeToken ? MAX_BUYERS_NATIVE : MAX_BUYERS_ERC20;
|
|
951
764
|
if (buyerPrivateKeys.length === 0) {
|
|
952
765
|
throw new Error('至少需要一个买方钱包');
|
|
953
766
|
}
|
|
954
767
|
if (buyerPrivateKeys.length > MAX_BUYERS) {
|
|
955
|
-
|
|
768
|
+
const mode = useNativeToken ? 'BNB' : 'ERC20';
|
|
769
|
+
throw new Error(`资金利用率模式(${mode})买方钱包数量超过限制: ${buyerPrivateKeys.length} > ${MAX_BUYERS}`);
|
|
956
770
|
}
|
|
957
771
|
// ✅ 校验分配模式
|
|
958
772
|
if (!buyerRatios && !buyerAmounts) {
|
|
@@ -967,39 +781,101 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
967
781
|
const context = createPancakeContext(config);
|
|
968
782
|
const seller = new Wallet(sellerPrivateKey, context.provider);
|
|
969
783
|
const buyers = buyerPrivateKeys.map(pk => new Wallet(pk, context.provider));
|
|
784
|
+
// ✅ 校验卖出路径输出代币是否匹配
|
|
785
|
+
let sellOutputToken;
|
|
786
|
+
if (routeParams.routeType === 'v2') {
|
|
787
|
+
const { v2Path } = routeParams;
|
|
788
|
+
sellOutputToken = v2Path[v2Path.length - 1];
|
|
789
|
+
}
|
|
790
|
+
else if (routeParams.routeType === 'v3-single') {
|
|
791
|
+
const { v3TokenOut } = routeParams;
|
|
792
|
+
sellOutputToken = v3TokenOut;
|
|
793
|
+
}
|
|
794
|
+
else if (routeParams.routeType === 'v3-multi') {
|
|
795
|
+
const { v2Path } = routeParams;
|
|
796
|
+
if (v2Path && v2Path.length > 0) {
|
|
797
|
+
sellOutputToken = v2Path[v2Path.length - 1];
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// 校验输出代币
|
|
801
|
+
if (useNativeToken) {
|
|
802
|
+
// 原生代币模式:输出必须是 WBNB
|
|
803
|
+
if (!sellOutputToken || sellOutputToken.toLowerCase() !== WBNB) {
|
|
804
|
+
throw new Error(`原生代币模式要求卖出路径以 WBNB 结尾(当前输出: ${sellOutputToken || '未知'})。` +
|
|
805
|
+
`请切换交易对或使用 ERC20 模式。`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
// ERC20 模式:输出必须是指定的 quoteToken
|
|
810
|
+
if (!sellOutputToken || sellOutputToken.toLowerCase() !== quoteToken.toLowerCase()) {
|
|
811
|
+
throw new Error(`ERC20 模式要求卖出路径以 ${quoteToken} 结尾(当前输出: ${sellOutputToken || '未知'})。`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
970
814
|
// ✅ 创建共享资源
|
|
971
815
|
const nonceManager = new NonceManager(context.provider);
|
|
972
816
|
const finalGasLimit = getGasLimit(config);
|
|
973
|
-
const gasPrice = await getGasPrice(context.provider, config);
|
|
974
817
|
const txType = config.txType ?? 0;
|
|
975
|
-
//
|
|
976
|
-
|
|
818
|
+
const ERC20_TRANSFER_GAS = 65000n; // ERC20 transfer 的 gas 限制
|
|
819
|
+
// ✅ 并行获取 gasPrice 和卖出数量
|
|
820
|
+
const [gasPrice, sellAmountResult] = await Promise.all([
|
|
821
|
+
getGasPrice(context.provider, config),
|
|
822
|
+
calculateSellAmount(context.provider, tokenAddress, seller.address, sellAmount, sellPercentage)
|
|
823
|
+
]);
|
|
824
|
+
const { amount: sellAmountWei, decimals } = sellAmountResult;
|
|
825
|
+
// ✅ 调试日志
|
|
826
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 模式: ${useNativeToken ? 'BNB' : 'ERC20'}`);
|
|
827
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 卖出数量: ${ethers.formatUnits(sellAmountWei, decimals)}`);
|
|
828
|
+
console.log(`[pancakeQuickBatchSwapMerkle] routeParams:`, JSON.stringify({
|
|
829
|
+
routeType: routeParams.routeType,
|
|
830
|
+
v2Path: routeParams.v2Path,
|
|
831
|
+
v3TokenIn: routeParams.v3TokenIn,
|
|
832
|
+
v3TokenOut: routeParams.v3TokenOut,
|
|
833
|
+
v3Fee: routeParams.v3Fee
|
|
834
|
+
}));
|
|
977
835
|
// ✅ 获取卖出报价
|
|
978
836
|
const quoteResult = await quoteSellOutput({
|
|
979
837
|
routeParams,
|
|
980
838
|
sellAmountWei,
|
|
981
839
|
provider: context.provider
|
|
982
840
|
});
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
841
|
+
const estimatedOutput = quoteResult.estimatedBNBOut;
|
|
842
|
+
const outputFormatted = useNativeToken
|
|
843
|
+
? ethers.formatEther(estimatedOutput)
|
|
844
|
+
: ethers.formatUnits(estimatedOutput, quoteTokenDecimals);
|
|
845
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 预估卖出所得: ${outputFormatted} ${useNativeToken ? 'BNB' : 'ERC20'}`);
|
|
846
|
+
// ✅ 计算利润(基于 BNB 价值)
|
|
847
|
+
let profitAmount;
|
|
848
|
+
if (useNativeToken) {
|
|
849
|
+
profitAmount = calculateProfitAmount(estimatedOutput);
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
// ERC20 模式:需要将 ERC20 价值转换为 BNB
|
|
853
|
+
const version = routeParams.routeType === 'v2' ? 'v2' : 'v3';
|
|
854
|
+
const fee = routeParams.routeType === 'v3-single' ? routeParams.v3Fee : undefined;
|
|
855
|
+
const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, quoteToken, estimatedOutput, version, fee);
|
|
856
|
+
profitAmount = calculateProfitAmount(estimatedBNBValue);
|
|
857
|
+
console.log(`[pancakeQuickBatchSwapMerkle] ERC20→BNB 报价: ${outputFormatted} → ${ethers.formatEther(estimatedBNBValue)} BNB`);
|
|
858
|
+
}
|
|
859
|
+
const distributableAmount = estimatedOutput - (useNativeToken ? profitAmount : 0n);
|
|
860
|
+
// ✅ 计算每个买方分到的金额
|
|
988
861
|
let transferAmountsWei;
|
|
989
862
|
if (buyerAmounts && buyerAmounts.length === buyers.length) {
|
|
990
|
-
//
|
|
991
|
-
transferAmountsWei = buyerAmounts.map(amt =>
|
|
992
|
-
|
|
863
|
+
// 数量模式
|
|
864
|
+
transferAmountsWei = buyerAmounts.map(amt => useNativeToken
|
|
865
|
+
? ethers.parseEther(amt)
|
|
866
|
+
: ethers.parseUnits(amt, quoteTokenDecimals));
|
|
993
867
|
const totalTransfer = transferAmountsWei.reduce((a, b) => a + b, 0n);
|
|
994
|
-
if (totalTransfer >
|
|
995
|
-
|
|
868
|
+
if (totalTransfer > distributableAmount) {
|
|
869
|
+
const formatted = useNativeToken
|
|
870
|
+
? ethers.formatEther(distributableAmount)
|
|
871
|
+
: ethers.formatUnits(distributableAmount, quoteTokenDecimals);
|
|
872
|
+
throw new Error(`指定的买入总金额超过可分配金额 (${formatted})`);
|
|
996
873
|
}
|
|
997
874
|
}
|
|
998
875
|
else if (buyerRatios && buyerRatios.length === buyers.length) {
|
|
999
|
-
//
|
|
876
|
+
// 比例模式
|
|
1000
877
|
transferAmountsWei = buyerRatios.map(ratio => {
|
|
1001
|
-
|
|
1002
|
-
return amount;
|
|
878
|
+
return (distributableAmount * BigInt(Math.round(ratio * 10000))) / 10000n;
|
|
1003
879
|
});
|
|
1004
880
|
}
|
|
1005
881
|
else {
|
|
@@ -1009,17 +885,30 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
1009
885
|
const bribeAmount = config.bribeAmount && config.bribeAmount > 0
|
|
1010
886
|
? ethers.parseEther(String(config.bribeAmount))
|
|
1011
887
|
: 0n;
|
|
1012
|
-
// ✅
|
|
888
|
+
// ✅ 验证主钱包余额
|
|
1013
889
|
const sellerBalance = await seller.provider.getBalance(seller.address);
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
890
|
+
let sellerGasCost;
|
|
891
|
+
let sellerRequired;
|
|
892
|
+
if (useNativeToken) {
|
|
893
|
+
// BNB 模式:贿赂(21000) + 卖出(gasLimit) + N个原生转账(21000 each) + 利润(21000)
|
|
894
|
+
sellerGasCost = gasPrice * (21000n + finalGasLimit + 21000n * BigInt(buyers.length) + 21000n);
|
|
895
|
+
sellerRequired = bribeAmount + sellerGasCost;
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
// ERC20 模式:
|
|
899
|
+
// - 贿赂(21000) + 卖出(gasLimit) + N个ERC20转账(65000 each) + N个BNB Gas转账(21000 each) + 利润(21000)
|
|
900
|
+
// - 还需要给买家转 BNB 用于支付 FLAT_FEE 和买入 Gas
|
|
901
|
+
sellerGasCost = gasPrice * (21000n + finalGasLimit + (ERC20_TRANSFER_GAS + 21000n) * BigInt(buyers.length) + 21000n);
|
|
902
|
+
const buyerGasNeeded = (gasPrice * finalGasLimit + FLAT_FEE) * BigInt(buyers.length);
|
|
903
|
+
sellerRequired = bribeAmount + sellerGasCost + buyerGasNeeded;
|
|
904
|
+
}
|
|
905
|
+
if (sellerBalance < sellerRequired) {
|
|
906
|
+
throw new Error(`主钱包 BNB 余额不足: 需要约 ${ethers.formatEther(sellerRequired)} BNB (贿赂: ${ethers.formatEther(bribeAmount)}, Gas: ${ethers.formatEther(sellerGasCost)}), 实际 ${ethers.formatEther(sellerBalance)} BNB`);
|
|
1017
907
|
}
|
|
1018
908
|
// ==================== 规划 Nonce ====================
|
|
1019
|
-
// 卖方: [贿赂(可选)] → [卖出] → [转账1, 转账2, ...] → [利润(可选)]
|
|
1020
|
-
// 买方: [买入]
|
|
1021
909
|
let sellerNonce = await nonceManager.getNextNonce(seller);
|
|
1022
|
-
|
|
910
|
+
const deadline = Math.floor(Date.now() / 1000) + 600;
|
|
911
|
+
// ==================== 1. 贿赂交易 ====================
|
|
1023
912
|
let bribeTx = null;
|
|
1024
913
|
if (bribeAmount > 0n) {
|
|
1025
914
|
bribeTx = await seller.signTransaction({
|
|
@@ -1031,10 +920,10 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
1031
920
|
chainId: context.chainId,
|
|
1032
921
|
type: txType
|
|
1033
922
|
});
|
|
923
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 贿赂交易已签名`);
|
|
1034
924
|
}
|
|
1035
|
-
//
|
|
925
|
+
// ==================== 2. 卖出交易 ====================
|
|
1036
926
|
const proxySeller = new Contract(PANCAKE_PROXY_ADDRESS, PANCAKE_PROXY_ABI, seller);
|
|
1037
|
-
const deadline = Math.floor(Date.now() / 1000) + 600;
|
|
1038
927
|
let sellUnsigned;
|
|
1039
928
|
if (routeParams.routeType === 'v2') {
|
|
1040
929
|
const { v2Path } = routeParams;
|
|
@@ -1057,40 +946,72 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
1057
946
|
chainId: context.chainId,
|
|
1058
947
|
type: txType
|
|
1059
948
|
});
|
|
1060
|
-
|
|
949
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 卖出交易已签名`);
|
|
950
|
+
// ==================== 3. 转账交易 ====================
|
|
1061
951
|
const transferTxs = [];
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
952
|
+
if (useNativeToken) {
|
|
953
|
+
// ✅ 原生代币模式:直接 BNB 转账
|
|
954
|
+
for (let i = 0; i < buyers.length; i++) {
|
|
955
|
+
const buyerGasCost = gasPrice * finalGasLimit;
|
|
956
|
+
const transferValue = transferAmountsWei[i] + FLAT_FEE + buyerGasCost;
|
|
957
|
+
const transferTx = await seller.signTransaction({
|
|
958
|
+
to: buyers[i].address,
|
|
959
|
+
value: transferValue,
|
|
960
|
+
nonce: sellerNonce++,
|
|
961
|
+
gasPrice,
|
|
962
|
+
gasLimit: 21000n,
|
|
963
|
+
chainId: context.chainId,
|
|
964
|
+
type: txType
|
|
965
|
+
});
|
|
966
|
+
transferTxs.push(transferTx);
|
|
967
|
+
}
|
|
1073
968
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
969
|
+
else {
|
|
970
|
+
// ✅ ERC20 模式:ERC20 transfer 调用
|
|
971
|
+
const erc20Interface = new ethers.Interface([
|
|
972
|
+
'function transfer(address to, uint256 amount) returns (bool)'
|
|
973
|
+
]);
|
|
974
|
+
for (let i = 0; i < buyers.length; i++) {
|
|
975
|
+
const transferData = erc20Interface.encodeFunctionData('transfer', [
|
|
976
|
+
buyers[i].address,
|
|
977
|
+
transferAmountsWei[i]
|
|
978
|
+
]);
|
|
979
|
+
const transferTx = await seller.signTransaction({
|
|
980
|
+
to: quoteToken,
|
|
981
|
+
data: transferData,
|
|
982
|
+
value: 0n,
|
|
983
|
+
nonce: sellerNonce++,
|
|
984
|
+
gasPrice,
|
|
985
|
+
gasLimit: ERC20_TRANSFER_GAS,
|
|
986
|
+
chainId: context.chainId,
|
|
987
|
+
type: txType
|
|
988
|
+
});
|
|
989
|
+
transferTxs.push(transferTx);
|
|
990
|
+
}
|
|
991
|
+
// ERC20 模式:额外转账 Gas 费用给买家(用于支付 FLAT_FEE 和买入 Gas)
|
|
992
|
+
for (let i = 0; i < buyers.length; i++) {
|
|
993
|
+
const buyerGasCost = gasPrice * finalGasLimit + FLAT_FEE;
|
|
994
|
+
const gasTx = await seller.signTransaction({
|
|
995
|
+
to: buyers[i].address,
|
|
996
|
+
value: buyerGasCost,
|
|
997
|
+
nonce: sellerNonce++,
|
|
998
|
+
gasPrice,
|
|
999
|
+
gasLimit: 21000n,
|
|
1000
|
+
chainId: context.chainId,
|
|
1001
|
+
type: txType
|
|
1002
|
+
});
|
|
1003
|
+
transferTxs.push(gasTx);
|
|
1004
|
+
}
|
|
1086
1005
|
}
|
|
1087
|
-
|
|
1006
|
+
console.log(`[pancakeQuickBatchSwapMerkle] ${transferTxs.length} 笔转账交易已签名`);
|
|
1007
|
+
// ==================== 4. 买入交易 ====================
|
|
1088
1008
|
const buyerNonces = await Promise.all(buyers.map(buyer => nonceManager.getNextNonce(buyer)));
|
|
1089
|
-
// ✅ 并行构建买入交易
|
|
1090
1009
|
const signedBuys = await Promise.all(buyers.map(async (buyer, i) => {
|
|
1091
1010
|
const buyAmount = transferAmountsWei[i];
|
|
1092
1011
|
const proxyBuyer = new Contract(PANCAKE_PROXY_ADDRESS, PANCAKE_PROXY_ABI, buyer);
|
|
1093
|
-
|
|
1012
|
+
// BNB 模式:value = buyAmount + FLAT_FEE
|
|
1013
|
+
// ERC20 模式:value = FLAT_FEE(买入金额是 ERC20,通过授权支付)
|
|
1014
|
+
const buyValue = useNativeToken ? buyAmount + FLAT_FEE : FLAT_FEE;
|
|
1094
1015
|
let buyUnsigned;
|
|
1095
1016
|
if (routeParams.routeType === 'v2') {
|
|
1096
1017
|
const { v2Path } = routeParams;
|
|
@@ -1115,25 +1036,52 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
1115
1036
|
type: txType
|
|
1116
1037
|
});
|
|
1117
1038
|
}));
|
|
1039
|
+
console.log(`[pancakeQuickBatchSwapMerkle] ${signedBuys.length} 笔买入交易已签名`);
|
|
1040
|
+
// ==================== 5. 利润交易 ====================
|
|
1041
|
+
let profitTx = null;
|
|
1042
|
+
if (profitAmount > 0n) {
|
|
1043
|
+
profitTx = await seller.signTransaction({
|
|
1044
|
+
to: PROFIT_CONFIG.RECIPIENT,
|
|
1045
|
+
value: profitAmount,
|
|
1046
|
+
nonce: sellerNonce++,
|
|
1047
|
+
gasPrice,
|
|
1048
|
+
gasLimit: 21000n,
|
|
1049
|
+
chainId: context.chainId,
|
|
1050
|
+
type: txType
|
|
1051
|
+
});
|
|
1052
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 利润交易已签名`);
|
|
1053
|
+
}
|
|
1118
1054
|
nonceManager.clearTemp();
|
|
1119
|
-
//
|
|
1055
|
+
// ==================== 组装交易数组 ====================
|
|
1120
1056
|
const signedTransactions = [];
|
|
1121
1057
|
if (bribeTx)
|
|
1122
|
-
signedTransactions.push(bribeTx);
|
|
1123
|
-
signedTransactions.push(signedSell);
|
|
1124
|
-
signedTransactions.push(...transferTxs);
|
|
1125
|
-
signedTransactions.push(...signedBuys);
|
|
1058
|
+
signedTransactions.push(bribeTx);
|
|
1059
|
+
signedTransactions.push(signedSell);
|
|
1060
|
+
signedTransactions.push(...transferTxs);
|
|
1061
|
+
signedTransactions.push(...signedBuys);
|
|
1126
1062
|
if (profitTx)
|
|
1127
|
-
signedTransactions.push(profitTx);
|
|
1063
|
+
signedTransactions.push(profitTx);
|
|
1064
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 交易组装完成: ${signedTransactions.length} 笔`);
|
|
1065
|
+
console.log(` - 贿赂: ${bribeTx ? 1 : 0}`);
|
|
1066
|
+
console.log(` - 卖出: 1`);
|
|
1067
|
+
console.log(` - 转账: ${transferTxs.length}`);
|
|
1068
|
+
console.log(` - 买入: ${signedBuys.length}`);
|
|
1069
|
+
console.log(` - 利润: ${profitTx ? 1 : 0}`);
|
|
1070
|
+
const outputUnit = useNativeToken ? 'BNB' : 'ERC20';
|
|
1128
1071
|
return {
|
|
1129
1072
|
signedTransactions,
|
|
1130
1073
|
metadata: {
|
|
1131
1074
|
sellerAddress: seller.address,
|
|
1132
1075
|
buyerAddresses: buyers.map(b => b.address),
|
|
1133
1076
|
sellAmount: ethers.formatUnits(sellAmountWei, decimals),
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1077
|
+
estimatedOutput: useNativeToken
|
|
1078
|
+
? ethers.formatEther(estimatedOutput)
|
|
1079
|
+
: ethers.formatUnits(estimatedOutput, quoteTokenDecimals),
|
|
1080
|
+
transferAmounts: transferAmountsWei.map(amt => useNativeToken
|
|
1081
|
+
? ethers.formatEther(amt)
|
|
1082
|
+
: ethers.formatUnits(amt, quoteTokenDecimals)),
|
|
1083
|
+
profitAmount: profitAmount > 0n ? ethers.formatEther(profitAmount) : undefined,
|
|
1084
|
+
useNativeToken
|
|
1137
1085
|
}
|
|
1138
1086
|
};
|
|
1139
1087
|
}
|