four-flap-meme-sdk 1.4.9 → 1.4.11
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/pancake/bundle-swap.js +283 -18
- package/package.json +1 -1
|
@@ -33,34 +33,137 @@ async function ensureSellerApproval({ tokenAddress, seller, provider, decimals,
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
async function quoteSellOutput({ routeParams, sellAmountWei, provider }) {
|
|
36
|
+
const tokenIn = routeParams.routeType === 'v2'
|
|
37
|
+
? routeParams.v2Path[0]
|
|
38
|
+
: routeParams.v3TokenIn;
|
|
39
|
+
const tokenInLower = tokenIn.toLowerCase();
|
|
40
|
+
// ==================== V2 报价 ====================
|
|
36
41
|
if (routeParams.routeType === 'v2') {
|
|
37
42
|
const { v2Path } = routeParams;
|
|
38
43
|
const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
// V2 策略 1:直接路径
|
|
45
|
+
try {
|
|
46
|
+
const amounts = await v2Router.getAmountsOut(sellAmountWei, v2Path);
|
|
47
|
+
const amountOut = amounts[amounts.length - 1];
|
|
48
|
+
if (amountOut > 0n) {
|
|
49
|
+
console.log(`[quoteSellOutput] V2 直接路径成功: ${ethers.formatEther(amountOut)} BNB`);
|
|
50
|
+
return { estimatedBNBOut: amountOut };
|
|
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
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw new Error('V2 报价失败: 所有路径均无效');
|
|
41
74
|
}
|
|
75
|
+
// ==================== V3 报价 ====================
|
|
42
76
|
if (routeParams.routeType === 'v3-single') {
|
|
43
77
|
const params = routeParams;
|
|
44
78
|
const quoter = new Contract(PANCAKE_V3_QUOTER_ADDRESS, PANCAKE_V3_QUOTER_ABI, provider);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
// 如果指定了 fee,只用指定的 fee;否则尝试所有费率
|
|
80
|
+
const feesToTry = params.v3Fee ? [params.v3Fee] : V3_FEE_TIERS;
|
|
81
|
+
console.log(`[quoteSellOutput] 开始 V3 报价: tokenIn=${params.v3TokenIn.slice(0, 10)}..., tokenOut=${params.v3TokenOut.slice(0, 10)}..., feesToTry=${feesToTry}`);
|
|
82
|
+
// V3 策略 1:直接路径(尝试多个费率)
|
|
83
|
+
for (const fee of feesToTry) {
|
|
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
148
|
}
|
|
55
|
-
|
|
56
|
-
|
|
149
|
+
// V3 全部失败,尝试 V2 fallback
|
|
150
|
+
if (params.v2Path && params.v2Path.length >= 2) {
|
|
151
|
+
try {
|
|
57
152
|
const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
|
|
58
153
|
const amounts = await v2Router.getAmountsOut(sellAmountWei, params.v2Path);
|
|
59
|
-
|
|
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)}`);
|
|
60
162
|
}
|
|
61
|
-
throw new Error(`V3 QuoterV2 失败 (fee=${params.v3Fee}) 且未提供 v2Path 作为备选。可能的原因: 1. V3 池子不存在 2. 费率档位错误 3. 流动性不足`);
|
|
62
163
|
}
|
|
164
|
+
throw new Error(`V3 QuoterV2 报价失败 (尝试费率: ${feesToTry.join(', ')}) 且 V2 路径无效`);
|
|
63
165
|
}
|
|
166
|
+
// ==================== V3 多跳 ====================
|
|
64
167
|
const params = routeParams;
|
|
65
168
|
if (params.v2Path && params.v2Path.length >= 2) {
|
|
66
169
|
const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
|
|
@@ -191,6 +294,132 @@ function calculateProfitAmount(estimatedBNBOut) {
|
|
|
191
294
|
}
|
|
192
295
|
return (estimatedBNBOut * BigInt(PROFIT_CONFIG.RATE_BPS)) / 10000n;
|
|
193
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* ✅ 获取 ERC20 代币 → 原生代币(BNB)的报价
|
|
299
|
+
* 用于将 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
|
|
307
|
+
*/
|
|
308
|
+
async function getERC20ToNativeQuote(provider, tokenAddress, tokenAmount, version = 'v2', fee) {
|
|
309
|
+
if (tokenAmount <= 0n)
|
|
310
|
+
return 0n;
|
|
311
|
+
const tokenLower = tokenAddress.toLowerCase();
|
|
312
|
+
const wbnbLower = WBNB_ADDRESS.toLowerCase();
|
|
313
|
+
// 如果代币本身就是 WBNB,直接返回
|
|
314
|
+
if (tokenLower === wbnbLower) {
|
|
315
|
+
return tokenAmount;
|
|
316
|
+
}
|
|
317
|
+
// ==================== V2 报价 ====================
|
|
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 报价 ====================
|
|
353
|
+
if (version === 'v3') {
|
|
354
|
+
const quoter = new Contract(PANCAKE_V3_QUOTER_ADDRESS, PANCAKE_V3_QUOTER_ABI, provider);
|
|
355
|
+
const feesToTry = fee ? [fee] : V3_FEE_TIERS;
|
|
356
|
+
// V3 策略 1:直接路径 代币 → WBNB
|
|
357
|
+
for (const tryFee of feesToTry) {
|
|
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
|
+
}
|
|
417
|
+
}
|
|
418
|
+
console.warn(`[getERC20ToNativeQuote] V3 所有报价路径均失败`);
|
|
419
|
+
return 0n;
|
|
420
|
+
}
|
|
421
|
+
return 0n;
|
|
422
|
+
}
|
|
194
423
|
async function validateFinalBalances({ sameAddress, buyerBalance, buyAmountBNB, reserveGas, gasLimit, gasPrice, useNativeToken = true, quoteTokenDecimals = 18, provider, buyerAddress }) {
|
|
195
424
|
const gasCost = gasLimit * gasPrice;
|
|
196
425
|
if (sameAddress) {
|
|
@@ -291,6 +520,14 @@ const PANCAKE_V3_QUOTER_ADDRESS = '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997';
|
|
|
291
520
|
// 代理合约手续费
|
|
292
521
|
const FLAT_FEE = 0n; // ✅ 已移除合约固定手续费
|
|
293
522
|
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%
|
|
294
531
|
const ERC20_ALLOWANCE_ABI = [
|
|
295
532
|
'function allowance(address,address) view returns (uint256)',
|
|
296
533
|
'function approve(address spender,uint256 amount) returns (bool)',
|
|
@@ -352,7 +589,24 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
352
589
|
tokenAddress,
|
|
353
590
|
useNativeToken
|
|
354
591
|
});
|
|
355
|
-
|
|
592
|
+
// ✅ 修复:利润计算应基于 BNB 数量,不是 ERC20 数量
|
|
593
|
+
// 如果输出是原生代币(BNB),直接使用报价结果
|
|
594
|
+
// 如果输出是 ERC20(如 USDT),需要先转换为 BNB 等值
|
|
595
|
+
let profitAmount;
|
|
596
|
+
if (useNativeToken) {
|
|
597
|
+
// 输出是 BNB,直接计算利润
|
|
598
|
+
profitAmount = calculateProfitAmount(quoteResult.estimatedBNBOut);
|
|
599
|
+
console.log(`[pancakeBundleSwapMerkle] 原生代币利润: ${ethers.formatEther(profitAmount)} BNB`);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
// 输出是 ERC20,需要先获取 ERC20 → BNB 的报价
|
|
603
|
+
const version = routeParams.routeType === 'v2' ? 'v2' : 'v3';
|
|
604
|
+
const fee = routeParams.routeType === 'v3-single' ? routeParams.v3Fee : undefined;
|
|
605
|
+
const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, quoteToken, quoteResult.estimatedBNBOut, // 这实际上是 ERC20 数量
|
|
606
|
+
version, fee);
|
|
607
|
+
profitAmount = calculateProfitAmount(estimatedBNBValue);
|
|
608
|
+
console.log(`[pancakeBundleSwapMerkle] ERC20→BNB 报价: ${ethers.formatUnits(quoteResult.estimatedBNBOut, quoteTokenDecimals)} ${quoteToken?.slice(0, 10)}... → ${ethers.formatEther(estimatedBNBValue)} BNB, 利润: ${ethers.formatEther(profitAmount)} BNB`);
|
|
609
|
+
}
|
|
356
610
|
// ✅ 获取贿赂金额
|
|
357
611
|
const bribeAmount = config.bribeAmount && config.bribeAmount > 0
|
|
358
612
|
? ethers.parseEther(String(config.bribeAmount))
|
|
@@ -600,8 +854,19 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
600
854
|
type: txType
|
|
601
855
|
});
|
|
602
856
|
}
|
|
603
|
-
//
|
|
604
|
-
|
|
857
|
+
// ✅ 修复:利润计算应基于 BNB 数量,不是 ERC20 数量
|
|
858
|
+
let profitAmount;
|
|
859
|
+
if (useNativeToken) {
|
|
860
|
+
profitAmount = calculateProfitAmount(estimatedBNBOut);
|
|
861
|
+
console.log(`[pancakeBatchSwapMerkle] 原生代币利润: ${ethers.formatEther(profitAmount)} BNB`);
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
const version = routeParams.routeType === 'v2' ? 'v2' : 'v3';
|
|
865
|
+
const fee = routeParams.routeType === 'v3-single' ? routeParams.v3Fee : undefined;
|
|
866
|
+
const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, quoteToken, estimatedBNBOut, version, fee);
|
|
867
|
+
profitAmount = calculateProfitAmount(estimatedBNBValue);
|
|
868
|
+
console.log(`[pancakeBatchSwapMerkle] ERC20→BNB 报价: ${ethers.formatUnits(estimatedBNBOut, quoteTokenDecimals)} → ${ethers.formatEther(estimatedBNBValue)} BNB, 利润: ${ethers.formatEther(profitAmount)} BNB`);
|
|
869
|
+
}
|
|
605
870
|
let profitTx = null;
|
|
606
871
|
if (profitAmount > 0n) {
|
|
607
872
|
// ✅ 利润由卖方发送(与贿赂交易同一钱包)
|