four-flap-meme-sdk 1.4.11 → 1.4.12
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 +2 -2
- package/dist/pancake/bundle-swap.js +171 -330
- 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
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// V3 策略 2:多跳路径 (代币 → 稳定币 → WBNB)
|
|
104
|
-
for (const stableCoin of STABLE_COINS) {
|
|
105
|
-
if (tokenInLower === stableCoin.toLowerCase())
|
|
106
|
-
continue;
|
|
107
|
-
for (const fee1 of feesToTry) {
|
|
108
|
-
try {
|
|
109
|
-
// 第一跳:代币 → 稳定币
|
|
110
|
-
const midResult = await quoter.quoteExactInputSingle.staticCall({
|
|
111
|
-
tokenIn: params.v3TokenIn,
|
|
112
|
-
tokenOut: stableCoin,
|
|
113
|
-
amountIn: sellAmountWei,
|
|
114
|
-
fee: fee1,
|
|
115
|
-
sqrtPriceLimitX96: 0n
|
|
116
|
-
});
|
|
117
|
-
const midAmount = Array.isArray(midResult) ? midResult[0] : midResult;
|
|
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
|
-
}
|
|
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 };
|
|
148
60
|
}
|
|
149
|
-
// V3
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
}
|
|
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;
|
|
375
211
|
}
|
|
376
|
-
|
|
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
|
-
}
|
|
417
|
-
}
|
|
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,25 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
935
740
|
/**
|
|
936
741
|
* PancakeSwap 快捷批量换手(资金自动流转)
|
|
937
742
|
*
|
|
938
|
-
* 流程:[贿赂] → [卖出] → [
|
|
743
|
+
* 流程:[贿赂] → [卖出] → [转账1, 转账2, ...] → [买入1, 买入2, ...] → [利润]
|
|
939
744
|
*
|
|
940
745
|
* 特点:
|
|
941
746
|
* - 子钱包不需要预先有 BNB 余额
|
|
942
747
|
* - 资金来自主钱包卖出代币所得
|
|
943
748
|
* - 提升资金利用率
|
|
944
749
|
*
|
|
945
|
-
* 限制:最多
|
|
750
|
+
* 限制:最多 23 个买方(转账 + 买入 = 46 笔,加上贿赂/卖出/利润 = 50 笔限制)
|
|
946
751
|
*/
|
|
947
752
|
export async function pancakeQuickBatchSwapMerkle(params) {
|
|
948
753
|
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKeys, buyerRatios, buyerAmounts, tokenAddress, routeParams, config } = params;
|
|
949
|
-
// ✅ 校验买方数量(最多
|
|
950
|
-
|
|
754
|
+
// ✅ 校验买方数量(最多 23 个:贿赂(1) + 卖出(1) + 转账(N) + 买入(N) + 利润(1) ≤ 50)
|
|
755
|
+
// 即 2N + 3 ≤ 50 → N ≤ 23
|
|
756
|
+
const MAX_BUYERS = 23;
|
|
951
757
|
if (buyerPrivateKeys.length === 0) {
|
|
952
758
|
throw new Error('至少需要一个买方钱包');
|
|
953
759
|
}
|
|
954
760
|
if (buyerPrivateKeys.length > MAX_BUYERS) {
|
|
955
|
-
throw new Error(
|
|
761
|
+
throw new Error(`资金利用率模式买方钱包数量超过限制: ${buyerPrivateKeys.length} > ${MAX_BUYERS}`);
|
|
956
762
|
}
|
|
957
763
|
// ✅ 校验分配模式
|
|
958
764
|
if (!buyerRatios && !buyerAmounts) {
|
|
@@ -966,14 +772,27 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
966
772
|
}
|
|
967
773
|
const context = createPancakeContext(config);
|
|
968
774
|
const seller = new Wallet(sellerPrivateKey, context.provider);
|
|
775
|
+
// ✅ 买方需要私钥(因为买方自己执行买入交易)
|
|
969
776
|
const buyers = buyerPrivateKeys.map(pk => new Wallet(pk, context.provider));
|
|
970
777
|
// ✅ 创建共享资源
|
|
971
778
|
const nonceManager = new NonceManager(context.provider);
|
|
972
779
|
const finalGasLimit = getGasLimit(config);
|
|
973
|
-
const gasPrice = await getGasPrice(context.provider, config);
|
|
974
780
|
const txType = config.txType ?? 0;
|
|
975
|
-
// ✅
|
|
976
|
-
const
|
|
781
|
+
// ✅ 并行获取 gasPrice 和卖出数量
|
|
782
|
+
const [gasPrice, sellAmountResult] = await Promise.all([
|
|
783
|
+
getGasPrice(context.provider, config),
|
|
784
|
+
calculateSellAmount(context.provider, tokenAddress, seller.address, sellAmount, sellPercentage)
|
|
785
|
+
]);
|
|
786
|
+
const { amount: sellAmountWei, decimals } = sellAmountResult;
|
|
787
|
+
// ✅ 调试日志:打印卖出数量
|
|
788
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 卖出数量: ${ethers.formatUnits(sellAmountWei, decimals)} (${sellAmountWei} wei, decimals=${decimals})`);
|
|
789
|
+
console.log(`[pancakeQuickBatchSwapMerkle] routeParams:`, JSON.stringify({
|
|
790
|
+
routeType: routeParams.routeType,
|
|
791
|
+
v2Path: routeParams.v2Path,
|
|
792
|
+
v3TokenIn: routeParams.v3TokenIn,
|
|
793
|
+
v3TokenOut: routeParams.v3TokenOut,
|
|
794
|
+
v3Fee: routeParams.v3Fee
|
|
795
|
+
}));
|
|
977
796
|
// ✅ 获取卖出报价
|
|
978
797
|
const quoteResult = await quoteSellOutput({
|
|
979
798
|
routeParams,
|
|
@@ -981,10 +800,15 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
981
800
|
provider: context.provider
|
|
982
801
|
});
|
|
983
802
|
const estimatedBNBOut = quoteResult.estimatedBNBOut;
|
|
803
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 预估卖出所得: ${ethers.formatEther(estimatedBNBOut)} BNB (${estimatedBNBOut} wei)`);
|
|
804
|
+
// ✅ 安全检查:如果报价超过 10 BNB,警告可能有问题
|
|
805
|
+
if (estimatedBNBOut > ethers.parseEther('10')) {
|
|
806
|
+
console.warn(`[pancakeQuickBatchSwapMerkle] ⚠️ 报价异常高: ${ethers.formatEther(estimatedBNBOut)} BNB,请检查路径和数量!`);
|
|
807
|
+
}
|
|
984
808
|
// ✅ 计算利润(从卖出所得中预扣)
|
|
985
809
|
const profitAmount = calculateProfitAmount(estimatedBNBOut);
|
|
986
810
|
const distributableBNB = estimatedBNBOut - profitAmount; // 可分配金额
|
|
987
|
-
// ✅
|
|
811
|
+
// ✅ 计算每个买方分到的 BNB(用于转账和买入)
|
|
988
812
|
let transferAmountsWei;
|
|
989
813
|
if (buyerAmounts && buyerAmounts.length === buyers.length) {
|
|
990
814
|
// 数量模式:使用指定的金额
|
|
@@ -1009,17 +833,19 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
1009
833
|
const bribeAmount = config.bribeAmount && config.bribeAmount > 0
|
|
1010
834
|
? ethers.parseEther(String(config.bribeAmount))
|
|
1011
835
|
: 0n;
|
|
1012
|
-
// ✅ 验证主钱包余额(需要支付 Gas
|
|
836
|
+
// ✅ 验证主钱包余额(需要支付 Gas 费用 + 贿赂)
|
|
1013
837
|
const sellerBalance = await seller.provider.getBalance(seller.address);
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
838
|
+
// 卖方需要的 Gas:贿赂(21000) + 卖出(gasLimit) + N个转账(21000 each) + 利润(21000)
|
|
839
|
+
const sellerGasCost = gasPrice * (21000n + finalGasLimit + 21000n * BigInt(buyers.length) + 21000n);
|
|
840
|
+
const sellerRequired = bribeAmount + sellerGasCost;
|
|
841
|
+
if (sellerBalance < sellerRequired) {
|
|
842
|
+
throw new Error(`主钱包 BNB 余额不足: 需要约 ${ethers.formatEther(sellerRequired)} BNB (贿赂: ${ethers.formatEther(bribeAmount)}, Gas: ${ethers.formatEther(sellerGasCost)}), 实际 ${ethers.formatEther(sellerBalance)} BNB`);
|
|
1017
843
|
}
|
|
1018
844
|
// ==================== 规划 Nonce ====================
|
|
1019
|
-
//
|
|
1020
|
-
// 买方: [买入]
|
|
845
|
+
// 流程: [贿赂] → [卖出] → [转账1, 转账2, ...] → [买入1, 买入2, ...] → [利润]
|
|
1021
846
|
let sellerNonce = await nonceManager.getNextNonce(seller);
|
|
1022
|
-
|
|
847
|
+
const deadline = Math.floor(Date.now() / 1000) + 600;
|
|
848
|
+
// ==================== 1. 贿赂交易 ====================
|
|
1023
849
|
let bribeTx = null;
|
|
1024
850
|
if (bribeAmount > 0n) {
|
|
1025
851
|
bribeTx = await seller.signTransaction({
|
|
@@ -1031,10 +857,10 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
1031
857
|
chainId: context.chainId,
|
|
1032
858
|
type: txType
|
|
1033
859
|
});
|
|
860
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 贿赂交易已签名`);
|
|
1034
861
|
}
|
|
1035
|
-
//
|
|
862
|
+
// ==================== 2. 卖出交易 ====================
|
|
1036
863
|
const proxySeller = new Contract(PANCAKE_PROXY_ADDRESS, PANCAKE_PROXY_ABI, seller);
|
|
1037
|
-
const deadline = Math.floor(Date.now() / 1000) + 600;
|
|
1038
864
|
let sellUnsigned;
|
|
1039
865
|
if (routeParams.routeType === 'v2') {
|
|
1040
866
|
const { v2Path } = routeParams;
|
|
@@ -1057,12 +883,16 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
1057
883
|
chainId: context.chainId,
|
|
1058
884
|
type: txType
|
|
1059
885
|
});
|
|
1060
|
-
|
|
886
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 卖出交易已签名`);
|
|
887
|
+
// ==================== 3. 转账交易(卖方 → 各买方)====================
|
|
1061
888
|
const transferTxs = [];
|
|
1062
889
|
for (let i = 0; i < buyers.length; i++) {
|
|
890
|
+
// 转账金额 = 买入金额 + FLAT_FEE + 买方 Gas 费用
|
|
891
|
+
const buyerGasCost = gasPrice * finalGasLimit;
|
|
892
|
+
const transferValue = transferAmountsWei[i] + FLAT_FEE + buyerGasCost;
|
|
1063
893
|
const transferTx = await seller.signTransaction({
|
|
1064
894
|
to: buyers[i].address,
|
|
1065
|
-
value:
|
|
895
|
+
value: transferValue,
|
|
1066
896
|
nonce: sellerNonce++,
|
|
1067
897
|
gasPrice,
|
|
1068
898
|
gasLimit: 21000n,
|
|
@@ -1071,22 +901,11 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
1071
901
|
});
|
|
1072
902
|
transferTxs.push(transferTx);
|
|
1073
903
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
profitTx = await seller.signTransaction({
|
|
1078
|
-
to: PROFIT_CONFIG.RECIPIENT,
|
|
1079
|
-
value: profitAmount,
|
|
1080
|
-
nonce: sellerNonce++,
|
|
1081
|
-
gasPrice,
|
|
1082
|
-
gasLimit: 21000n,
|
|
1083
|
-
chainId: context.chainId,
|
|
1084
|
-
type: txType
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
// ✅ 并行获取所有买方的 nonce
|
|
904
|
+
console.log(`[pancakeQuickBatchSwapMerkle] ${transferTxs.length} 笔转账交易已签名`);
|
|
905
|
+
// ==================== 4. 买入交易(各买方执行)====================
|
|
906
|
+
// 并行获取所有买方的 nonce
|
|
1088
907
|
const buyerNonces = await Promise.all(buyers.map(buyer => nonceManager.getNextNonce(buyer)));
|
|
1089
|
-
//
|
|
908
|
+
// 并行签名所有买入交易
|
|
1090
909
|
const signedBuys = await Promise.all(buyers.map(async (buyer, i) => {
|
|
1091
910
|
const buyAmount = transferAmountsWei[i];
|
|
1092
911
|
const proxyBuyer = new Contract(PANCAKE_PROXY_ADDRESS, PANCAKE_PROXY_ABI, buyer);
|
|
@@ -1115,16 +934,38 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
1115
934
|
type: txType
|
|
1116
935
|
});
|
|
1117
936
|
}));
|
|
937
|
+
console.log(`[pancakeQuickBatchSwapMerkle] ${signedBuys.length} 笔买入交易已签名`);
|
|
938
|
+
// ==================== 5. 利润交易 ====================
|
|
939
|
+
let profitTx = null;
|
|
940
|
+
if (profitAmount > 0n) {
|
|
941
|
+
profitTx = await seller.signTransaction({
|
|
942
|
+
to: PROFIT_CONFIG.RECIPIENT,
|
|
943
|
+
value: profitAmount,
|
|
944
|
+
nonce: sellerNonce++,
|
|
945
|
+
gasPrice,
|
|
946
|
+
gasLimit: 21000n,
|
|
947
|
+
chainId: context.chainId,
|
|
948
|
+
type: txType
|
|
949
|
+
});
|
|
950
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 利润交易已签名`);
|
|
951
|
+
}
|
|
1118
952
|
nonceManager.clearTemp();
|
|
1119
|
-
//
|
|
953
|
+
// ==================== 组装交易数组 ====================
|
|
954
|
+
// 顺序:贿赂 → 卖出 → 转账1, 转账2, ... → 买入1, 买入2, ... → 利润
|
|
1120
955
|
const signedTransactions = [];
|
|
1121
956
|
if (bribeTx)
|
|
1122
|
-
signedTransactions.push(bribeTx);
|
|
1123
|
-
signedTransactions.push(signedSell);
|
|
1124
|
-
signedTransactions.push(...transferTxs);
|
|
1125
|
-
signedTransactions.push(...signedBuys);
|
|
957
|
+
signedTransactions.push(bribeTx);
|
|
958
|
+
signedTransactions.push(signedSell);
|
|
959
|
+
signedTransactions.push(...transferTxs);
|
|
960
|
+
signedTransactions.push(...signedBuys);
|
|
1126
961
|
if (profitTx)
|
|
1127
|
-
signedTransactions.push(profitTx);
|
|
962
|
+
signedTransactions.push(profitTx);
|
|
963
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 交易组装完成: ${signedTransactions.length} 笔`);
|
|
964
|
+
console.log(` - 贿赂: ${bribeTx ? 1 : 0}`);
|
|
965
|
+
console.log(` - 卖出: 1`);
|
|
966
|
+
console.log(` - 转账: ${transferTxs.length}`);
|
|
967
|
+
console.log(` - 买入: ${signedBuys.length}`);
|
|
968
|
+
console.log(` - 利润: ${profitTx ? 1 : 0}`);
|
|
1128
969
|
return {
|
|
1129
970
|
signedTransactions,
|
|
1130
971
|
metadata: {
|