four-flap-meme-sdk 1.4.10 → 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 +221 -226
- 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
|
-
|
|
165
|
-
|
|
166
|
-
// ==================== V3 多跳 ====================
|
|
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] };
|
|
78
|
+
// ❌ V3 多跳报价失败时不再 fallback 到 V2,因为价格可能差异很大
|
|
79
|
+
throw new Error(`V3 多跳报价失败: LPs=${params.v3LpAddresses?.join(', ')}, tokenIn=${params.v3ExactTokenIn}`);
|
|
172
80
|
}
|
|
173
|
-
throw new Error(
|
|
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,26 +182,45 @@ 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;
|
|
294
188
|
}
|
|
295
189
|
return (estimatedBNBOut * BigInt(PROFIT_CONFIG.RATE_BPS)) / 10000n;
|
|
296
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* ✅ 获取 ERC20 代币 → 原生代币(BNB)的报价
|
|
193
|
+
* 用于将 ERC20 利润转换为 BNB
|
|
194
|
+
* 使用共享的报价函数
|
|
195
|
+
*/
|
|
196
|
+
async function getERC20ToNativeQuote(provider, tokenAddress, tokenAmount, version = 'v2', fee) {
|
|
197
|
+
if (tokenAmount <= 0n)
|
|
198
|
+
return 0n;
|
|
199
|
+
const tokenLower = tokenAddress.toLowerCase();
|
|
200
|
+
const wbnbLower = WBNB_ADDRESS.toLowerCase();
|
|
201
|
+
// 如果代币本身就是 WBNB,直接返回
|
|
202
|
+
if (tokenLower === wbnbLower) {
|
|
203
|
+
return tokenAmount;
|
|
204
|
+
}
|
|
205
|
+
// 使用共享的报价函数
|
|
206
|
+
if (version === 'v3') {
|
|
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;
|
|
211
|
+
}
|
|
212
|
+
console.warn(`[getERC20ToNativeQuote] V3 报价失败`);
|
|
213
|
+
return 0n;
|
|
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 报价失败`);
|
|
222
|
+
return 0n;
|
|
223
|
+
}
|
|
297
224
|
async function validateFinalBalances({ sameAddress, buyerBalance, buyAmountBNB, reserveGas, gasLimit, gasPrice, useNativeToken = true, quoteTokenDecimals = 18, provider, buyerAddress }) {
|
|
298
225
|
const gasCost = gasLimit * gasPrice;
|
|
299
226
|
if (sameAddress) {
|
|
@@ -343,6 +270,7 @@ import { ethers, Contract, Wallet } from 'ethers';
|
|
|
343
270
|
import { calculateSellAmount } from '../utils/swap-helpers.js';
|
|
344
271
|
import { NonceManager } from '../utils/bundle-helpers.js';
|
|
345
272
|
import { ADDRESSES, PROFIT_CONFIG } from '../utils/constants.js';
|
|
273
|
+
import { quoteV2, quoteV3 } from '../utils/quote-helpers.js';
|
|
346
274
|
// ✅ BlockRazor Builder EOA 地址(用于贿赂)
|
|
347
275
|
// 参考文档: https://blockrazor.gitbook.io/blockrazor/bsc/block-builder/send-bundle
|
|
348
276
|
const BLOCKRAZOR_BUILDER_EOA = '0x1266C6bE60392A8Ff346E8d5ECCd3E69dD9c5F20';
|
|
@@ -378,30 +306,13 @@ const PANCAKE_PROXY_ABI = [
|
|
|
378
306
|
'function swapV3Single(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint256 amountOutMin, address to) payable returns (uint256)',
|
|
379
307
|
'function swapV3MultiHop(address[] calldata lpAddresses, address exactTokenIn, uint256 amountIn, uint256 amountOutMin, address to) payable returns (uint256)'
|
|
380
308
|
];
|
|
381
|
-
// PancakeSwap V2 Router ABI(用于报价)
|
|
382
|
-
const PANCAKE_V2_ROUTER_ABI = [
|
|
383
|
-
'function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)'
|
|
384
|
-
];
|
|
385
|
-
// PancakeSwap V3 QuoterV2 ABI(用于报价)
|
|
386
|
-
const PANCAKE_V3_QUOTER_ABI = [
|
|
387
|
-
'function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96)) external returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)',
|
|
388
|
-
'function quoteExactInput(bytes memory path, uint256 amountIn) external returns (uint256 amountOut, uint160[] memory sqrtPriceX96AfterList, uint32[] memory initializedTicksCrossedList, uint256 gasEstimate)'
|
|
389
|
-
];
|
|
390
309
|
// 兼容旧代码:默认使用 BSC 地址
|
|
391
310
|
const PANCAKE_PROXY_ADDRESS = ADDRESSES.BSC.PancakeProxy;
|
|
392
|
-
|
|
393
|
-
const PANCAKE_V3_QUOTER_ADDRESS = '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997';
|
|
311
|
+
// ✅ V3 Quoter ABI 和地址已移至 ../utils/quote-helpers.ts
|
|
394
312
|
// 代理合约手续费
|
|
395
313
|
const FLAT_FEE = 0n; // ✅ 已移除合约固定手续费
|
|
396
314
|
const WBNB_ADDRESS = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
|
|
397
|
-
// ✅
|
|
398
|
-
const STABLE_COINS = [
|
|
399
|
-
'0x55d398326f99059fF775485246999027B3197955', // USDT
|
|
400
|
-
'0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', // USDC
|
|
401
|
-
'0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56', // BUSD
|
|
402
|
-
];
|
|
403
|
-
// ✅ V3 常用费率档位
|
|
404
|
-
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
|
|
405
316
|
const ERC20_ALLOWANCE_ABI = [
|
|
406
317
|
'function allowance(address,address) view returns (uint256)',
|
|
407
318
|
'function approve(address spender,uint256 amount) returns (bool)',
|
|
@@ -422,12 +333,16 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
422
333
|
const seller = new Wallet(sellerPrivateKey, context.provider);
|
|
423
334
|
const buyer = new Wallet(buyerPrivateKey, context.provider);
|
|
424
335
|
const sameAddress = seller.address.toLowerCase() === buyer.address.toLowerCase();
|
|
425
|
-
// ✅ 提前创建 NonceManager
|
|
336
|
+
// ✅ 提前创建 NonceManager,供所有交易共享
|
|
426
337
|
const nonceManager = new NonceManager(context.provider);
|
|
427
338
|
const finalGasLimit = getGasLimit(config);
|
|
428
|
-
const gasPrice = await getGasPrice(context.provider, config);
|
|
429
339
|
const txType = config.txType ?? 0;
|
|
430
|
-
|
|
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;
|
|
431
346
|
// ✅ 先构建授权交易(会消耗 nonce)
|
|
432
347
|
const approvalTx = await ensureSellerApproval({
|
|
433
348
|
tokenAddress,
|
|
@@ -463,7 +378,24 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
463
378
|
tokenAddress,
|
|
464
379
|
useNativeToken
|
|
465
380
|
});
|
|
466
|
-
|
|
381
|
+
// ✅ 修复:利润计算应基于 BNB 数量,不是 ERC20 数量
|
|
382
|
+
// 如果输出是原生代币(BNB),直接使用报价结果
|
|
383
|
+
// 如果输出是 ERC20(如 USDT),需要先转换为 BNB 等值
|
|
384
|
+
let profitAmount;
|
|
385
|
+
if (useNativeToken) {
|
|
386
|
+
// 输出是 BNB,直接计算利润
|
|
387
|
+
profitAmount = calculateProfitAmount(quoteResult.estimatedBNBOut);
|
|
388
|
+
console.log(`[pancakeBundleSwapMerkle] 原生代币利润: ${ethers.formatEther(profitAmount)} BNB`);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
// 输出是 ERC20,需要先获取 ERC20 → BNB 的报价
|
|
392
|
+
const version = routeParams.routeType === 'v2' ? 'v2' : 'v3';
|
|
393
|
+
const fee = routeParams.routeType === 'v3-single' ? routeParams.v3Fee : undefined;
|
|
394
|
+
const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, quoteToken, quoteResult.estimatedBNBOut, // 这实际上是 ERC20 数量
|
|
395
|
+
version, fee);
|
|
396
|
+
profitAmount = calculateProfitAmount(estimatedBNBValue);
|
|
397
|
+
console.log(`[pancakeBundleSwapMerkle] ERC20→BNB 报价: ${ethers.formatUnits(quoteResult.estimatedBNBOut, quoteTokenDecimals)} ${quoteToken?.slice(0, 10)}... → ${ethers.formatEther(estimatedBNBValue)} BNB, 利润: ${ethers.formatEther(profitAmount)} BNB`);
|
|
398
|
+
}
|
|
467
399
|
// ✅ 获取贿赂金额
|
|
468
400
|
const bribeAmount = config.bribeAmount && config.bribeAmount > 0
|
|
469
401
|
? ethers.parseEther(String(config.bribeAmount))
|
|
@@ -479,10 +411,11 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
479
411
|
needBribeTx, // ✅ 新增
|
|
480
412
|
nonceManager
|
|
481
413
|
});
|
|
482
|
-
// ✅
|
|
483
|
-
|
|
414
|
+
// ✅ 并行签名所有交易
|
|
415
|
+
const signPromises = [];
|
|
416
|
+
// 贿赂交易
|
|
484
417
|
if (needBribeTx && noncePlan.bribeNonce !== undefined) {
|
|
485
|
-
|
|
418
|
+
signPromises.push(seller.signTransaction({
|
|
486
419
|
to: BLOCKRAZOR_BUILDER_EOA,
|
|
487
420
|
value: bribeAmount,
|
|
488
421
|
nonce: noncePlan.bribeNonce,
|
|
@@ -490,9 +423,10 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
490
423
|
gasLimit: 21000n,
|
|
491
424
|
chainId: context.chainId,
|
|
492
425
|
type: txType
|
|
493
|
-
});
|
|
426
|
+
}).then(tx => ({ type: 'bribe', tx })));
|
|
494
427
|
}
|
|
495
|
-
|
|
428
|
+
// 卖出交易
|
|
429
|
+
signPromises.push(seller.signTransaction({
|
|
496
430
|
...swapUnsigned.sellUnsigned,
|
|
497
431
|
from: seller.address,
|
|
498
432
|
nonce: noncePlan.sellerNonce,
|
|
@@ -500,8 +434,9 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
500
434
|
gasPrice,
|
|
501
435
|
chainId: context.chainId,
|
|
502
436
|
type: txType
|
|
503
|
-
});
|
|
504
|
-
|
|
437
|
+
}).then(tx => ({ type: 'sell', tx })));
|
|
438
|
+
// 买入交易
|
|
439
|
+
signPromises.push(buyer.signTransaction({
|
|
505
440
|
...swapUnsigned.buyUnsigned,
|
|
506
441
|
from: buyer.address,
|
|
507
442
|
nonce: noncePlan.buyerNonce,
|
|
@@ -509,16 +444,26 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
509
444
|
gasPrice,
|
|
510
445
|
chainId: context.chainId,
|
|
511
446
|
type: txType
|
|
512
|
-
});
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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;
|
|
522
467
|
nonceManager.clearTemp();
|
|
523
468
|
validateFinalBalances({
|
|
524
469
|
sameAddress,
|
|
@@ -580,10 +525,13 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
580
525
|
// ✅ 创建共享资源
|
|
581
526
|
const nonceManager = new NonceManager(context.provider);
|
|
582
527
|
const finalGasLimit = getGasLimit(config);
|
|
583
|
-
const gasPrice = await getGasPrice(context.provider, config);
|
|
584
528
|
const txType = config.txType ?? 0;
|
|
585
|
-
// ✅
|
|
586
|
-
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;
|
|
587
535
|
// ✅ 构建授权交易(如果需要)
|
|
588
536
|
const approvalTx = await ensureSellerApproval({
|
|
589
537
|
tokenAddress,
|
|
@@ -711,8 +659,19 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
711
659
|
type: txType
|
|
712
660
|
});
|
|
713
661
|
}
|
|
714
|
-
//
|
|
715
|
-
|
|
662
|
+
// ✅ 修复:利润计算应基于 BNB 数量,不是 ERC20 数量
|
|
663
|
+
let profitAmount;
|
|
664
|
+
if (useNativeToken) {
|
|
665
|
+
profitAmount = calculateProfitAmount(estimatedBNBOut);
|
|
666
|
+
console.log(`[pancakeBatchSwapMerkle] 原生代币利润: ${ethers.formatEther(profitAmount)} BNB`);
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
const version = routeParams.routeType === 'v2' ? 'v2' : 'v3';
|
|
670
|
+
const fee = routeParams.routeType === 'v3-single' ? routeParams.v3Fee : undefined;
|
|
671
|
+
const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, quoteToken, estimatedBNBOut, version, fee);
|
|
672
|
+
profitAmount = calculateProfitAmount(estimatedBNBValue);
|
|
673
|
+
console.log(`[pancakeBatchSwapMerkle] ERC20→BNB 报价: ${ethers.formatUnits(estimatedBNBOut, quoteTokenDecimals)} → ${ethers.formatEther(estimatedBNBValue)} BNB, 利润: ${ethers.formatEther(profitAmount)} BNB`);
|
|
674
|
+
}
|
|
716
675
|
let profitTx = null;
|
|
717
676
|
if (profitAmount > 0n) {
|
|
718
677
|
// ✅ 利润由卖方发送(与贿赂交易同一钱包)
|
|
@@ -781,24 +740,25 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
781
740
|
/**
|
|
782
741
|
* PancakeSwap 快捷批量换手(资金自动流转)
|
|
783
742
|
*
|
|
784
|
-
* 流程:[贿赂] → [卖出] → [
|
|
743
|
+
* 流程:[贿赂] → [卖出] → [转账1, 转账2, ...] → [买入1, 买入2, ...] → [利润]
|
|
785
744
|
*
|
|
786
745
|
* 特点:
|
|
787
746
|
* - 子钱包不需要预先有 BNB 余额
|
|
788
747
|
* - 资金来自主钱包卖出代币所得
|
|
789
748
|
* - 提升资金利用率
|
|
790
749
|
*
|
|
791
|
-
* 限制:最多
|
|
750
|
+
* 限制:最多 23 个买方(转账 + 买入 = 46 笔,加上贿赂/卖出/利润 = 50 笔限制)
|
|
792
751
|
*/
|
|
793
752
|
export async function pancakeQuickBatchSwapMerkle(params) {
|
|
794
753
|
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKeys, buyerRatios, buyerAmounts, tokenAddress, routeParams, config } = params;
|
|
795
|
-
// ✅ 校验买方数量(最多
|
|
796
|
-
|
|
754
|
+
// ✅ 校验买方数量(最多 23 个:贿赂(1) + 卖出(1) + 转账(N) + 买入(N) + 利润(1) ≤ 50)
|
|
755
|
+
// 即 2N + 3 ≤ 50 → N ≤ 23
|
|
756
|
+
const MAX_BUYERS = 23;
|
|
797
757
|
if (buyerPrivateKeys.length === 0) {
|
|
798
758
|
throw new Error('至少需要一个买方钱包');
|
|
799
759
|
}
|
|
800
760
|
if (buyerPrivateKeys.length > MAX_BUYERS) {
|
|
801
|
-
throw new Error(
|
|
761
|
+
throw new Error(`资金利用率模式买方钱包数量超过限制: ${buyerPrivateKeys.length} > ${MAX_BUYERS}`);
|
|
802
762
|
}
|
|
803
763
|
// ✅ 校验分配模式
|
|
804
764
|
if (!buyerRatios && !buyerAmounts) {
|
|
@@ -812,14 +772,27 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
812
772
|
}
|
|
813
773
|
const context = createPancakeContext(config);
|
|
814
774
|
const seller = new Wallet(sellerPrivateKey, context.provider);
|
|
775
|
+
// ✅ 买方需要私钥(因为买方自己执行买入交易)
|
|
815
776
|
const buyers = buyerPrivateKeys.map(pk => new Wallet(pk, context.provider));
|
|
816
777
|
// ✅ 创建共享资源
|
|
817
778
|
const nonceManager = new NonceManager(context.provider);
|
|
818
779
|
const finalGasLimit = getGasLimit(config);
|
|
819
|
-
const gasPrice = await getGasPrice(context.provider, config);
|
|
820
780
|
const txType = config.txType ?? 0;
|
|
821
|
-
// ✅
|
|
822
|
-
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
|
+
}));
|
|
823
796
|
// ✅ 获取卖出报价
|
|
824
797
|
const quoteResult = await quoteSellOutput({
|
|
825
798
|
routeParams,
|
|
@@ -827,10 +800,15 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
827
800
|
provider: context.provider
|
|
828
801
|
});
|
|
829
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
|
+
}
|
|
830
808
|
// ✅ 计算利润(从卖出所得中预扣)
|
|
831
809
|
const profitAmount = calculateProfitAmount(estimatedBNBOut);
|
|
832
810
|
const distributableBNB = estimatedBNBOut - profitAmount; // 可分配金额
|
|
833
|
-
// ✅
|
|
811
|
+
// ✅ 计算每个买方分到的 BNB(用于转账和买入)
|
|
834
812
|
let transferAmountsWei;
|
|
835
813
|
if (buyerAmounts && buyerAmounts.length === buyers.length) {
|
|
836
814
|
// 数量模式:使用指定的金额
|
|
@@ -855,17 +833,19 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
855
833
|
const bribeAmount = config.bribeAmount && config.bribeAmount > 0
|
|
856
834
|
? ethers.parseEther(String(config.bribeAmount))
|
|
857
835
|
: 0n;
|
|
858
|
-
// ✅ 验证主钱包余额(需要支付 Gas
|
|
836
|
+
// ✅ 验证主钱包余额(需要支付 Gas 费用 + 贿赂)
|
|
859
837
|
const sellerBalance = await seller.provider.getBalance(seller.address);
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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`);
|
|
863
843
|
}
|
|
864
844
|
// ==================== 规划 Nonce ====================
|
|
865
|
-
//
|
|
866
|
-
// 买方: [买入]
|
|
845
|
+
// 流程: [贿赂] → [卖出] → [转账1, 转账2, ...] → [买入1, 买入2, ...] → [利润]
|
|
867
846
|
let sellerNonce = await nonceManager.getNextNonce(seller);
|
|
868
|
-
|
|
847
|
+
const deadline = Math.floor(Date.now() / 1000) + 600;
|
|
848
|
+
// ==================== 1. 贿赂交易 ====================
|
|
869
849
|
let bribeTx = null;
|
|
870
850
|
if (bribeAmount > 0n) {
|
|
871
851
|
bribeTx = await seller.signTransaction({
|
|
@@ -877,10 +857,10 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
877
857
|
chainId: context.chainId,
|
|
878
858
|
type: txType
|
|
879
859
|
});
|
|
860
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 贿赂交易已签名`);
|
|
880
861
|
}
|
|
881
|
-
//
|
|
862
|
+
// ==================== 2. 卖出交易 ====================
|
|
882
863
|
const proxySeller = new Contract(PANCAKE_PROXY_ADDRESS, PANCAKE_PROXY_ABI, seller);
|
|
883
|
-
const deadline = Math.floor(Date.now() / 1000) + 600;
|
|
884
864
|
let sellUnsigned;
|
|
885
865
|
if (routeParams.routeType === 'v2') {
|
|
886
866
|
const { v2Path } = routeParams;
|
|
@@ -903,12 +883,16 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
903
883
|
chainId: context.chainId,
|
|
904
884
|
type: txType
|
|
905
885
|
});
|
|
906
|
-
|
|
886
|
+
console.log(`[pancakeQuickBatchSwapMerkle] 卖出交易已签名`);
|
|
887
|
+
// ==================== 3. 转账交易(卖方 → 各买方)====================
|
|
907
888
|
const transferTxs = [];
|
|
908
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;
|
|
909
893
|
const transferTx = await seller.signTransaction({
|
|
910
894
|
to: buyers[i].address,
|
|
911
|
-
value:
|
|
895
|
+
value: transferValue,
|
|
912
896
|
nonce: sellerNonce++,
|
|
913
897
|
gasPrice,
|
|
914
898
|
gasLimit: 21000n,
|
|
@@ -917,22 +901,11 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
917
901
|
});
|
|
918
902
|
transferTxs.push(transferTx);
|
|
919
903
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
profitTx = await seller.signTransaction({
|
|
924
|
-
to: PROFIT_CONFIG.RECIPIENT,
|
|
925
|
-
value: profitAmount,
|
|
926
|
-
nonce: sellerNonce++,
|
|
927
|
-
gasPrice,
|
|
928
|
-
gasLimit: 21000n,
|
|
929
|
-
chainId: context.chainId,
|
|
930
|
-
type: txType
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
// ✅ 并行获取所有买方的 nonce
|
|
904
|
+
console.log(`[pancakeQuickBatchSwapMerkle] ${transferTxs.length} 笔转账交易已签名`);
|
|
905
|
+
// ==================== 4. 买入交易(各买方执行)====================
|
|
906
|
+
// 并行获取所有买方的 nonce
|
|
934
907
|
const buyerNonces = await Promise.all(buyers.map(buyer => nonceManager.getNextNonce(buyer)));
|
|
935
|
-
//
|
|
908
|
+
// 并行签名所有买入交易
|
|
936
909
|
const signedBuys = await Promise.all(buyers.map(async (buyer, i) => {
|
|
937
910
|
const buyAmount = transferAmountsWei[i];
|
|
938
911
|
const proxyBuyer = new Contract(PANCAKE_PROXY_ADDRESS, PANCAKE_PROXY_ABI, buyer);
|
|
@@ -961,16 +934,38 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
961
934
|
type: txType
|
|
962
935
|
});
|
|
963
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
|
+
}
|
|
964
952
|
nonceManager.clearTemp();
|
|
965
|
-
//
|
|
953
|
+
// ==================== 组装交易数组 ====================
|
|
954
|
+
// 顺序:贿赂 → 卖出 → 转账1, 转账2, ... → 买入1, 买入2, ... → 利润
|
|
966
955
|
const signedTransactions = [];
|
|
967
956
|
if (bribeTx)
|
|
968
|
-
signedTransactions.push(bribeTx);
|
|
969
|
-
signedTransactions.push(signedSell);
|
|
970
|
-
signedTransactions.push(...transferTxs);
|
|
971
|
-
signedTransactions.push(...signedBuys);
|
|
957
|
+
signedTransactions.push(bribeTx);
|
|
958
|
+
signedTransactions.push(signedSell);
|
|
959
|
+
signedTransactions.push(...transferTxs);
|
|
960
|
+
signedTransactions.push(...signedBuys);
|
|
972
961
|
if (profitTx)
|
|
973
|
-
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}`);
|
|
974
969
|
return {
|
|
975
970
|
signedTransactions,
|
|
976
971
|
metadata: {
|