four-flap-meme-sdk 1.4.25 → 1.4.26
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/abis/common.d.ts +85 -0
- package/dist/abis/common.js +242 -0
- package/dist/abis/index.d.ts +1 -0
- package/dist/abis/index.js +2 -0
- package/dist/contracts/tm-bundle-merkle/approve-tokenmanager.js +1 -6
- package/dist/contracts/tm-bundle-merkle/core.js +9 -16
- package/dist/contracts/tm-bundle-merkle/internal.js +6 -8
- package/dist/contracts/tm-bundle-merkle/pancake-proxy.d.ts +12 -6
- package/dist/contracts/tm-bundle-merkle/pancake-proxy.js +224 -166
- package/dist/contracts/tm-bundle-merkle/private.js +6 -19
- package/dist/contracts/tm-bundle-merkle/swap-buy-first.js +2 -7
- package/dist/contracts/tm-bundle-merkle/swap-internal.d.ts +1 -1
- package/dist/contracts/tm-bundle-merkle/swap-internal.js +9 -2
- package/dist/contracts/tm-bundle-merkle/swap.js +1 -3
- package/dist/contracts/tm-bundle-merkle/types.d.ts +20 -0
- package/dist/contracts/tm-bundle-merkle/utils.js +2 -3
- package/dist/dex/direct-router.d.ts +2 -1
- package/dist/dex/direct-router.js +25 -140
- package/dist/flap/constants.d.ts +2 -1
- package/dist/flap/constants.js +2 -1
- package/dist/flap/meta.js +6 -4
- package/dist/flap/portal-bundle-merkle/config.js +6 -12
- package/dist/flap/portal-bundle-merkle/core.js +8 -11
- package/dist/flap/portal-bundle-merkle/pancake-proxy.d.ts +12 -10
- package/dist/flap/portal-bundle-merkle/pancake-proxy.js +307 -370
- package/dist/flap/portal-bundle-merkle/private.js +1 -1
- package/dist/flap/portal-bundle-merkle/swap-buy-first.js +12 -30
- package/dist/flap/portal-bundle-merkle/swap.js +13 -26
- package/dist/flap/portal-bundle-merkle/types.d.ts +22 -2
- package/dist/flap/portal-bundle-merkle/utils.js +11 -16
- package/dist/index.d.ts +3 -2
- package/dist/index.js +9 -2
- package/dist/pancake/bundle-buy-first.js +56 -38
- package/dist/pancake/bundle-swap.js +114 -61
- package/dist/utils/bundle-helpers.d.ts +28 -0
- package/dist/utils/bundle-helpers.js +64 -0
- package/dist/utils/constants.d.ts +23 -1
- package/dist/utils/constants.js +37 -7
- package/dist/utils/erc20.js +17 -25
- package/dist/utils/lp-inspect.js +9 -20
- package/dist/utils/private-sale.js +1 -2
- package/dist/utils/quote-helpers.js +3 -29
- package/dist/utils/swap-helpers.js +1 -6
- package/dist/utils/wallet.d.ts +8 -13
- package/dist/utils/wallet.js +154 -342
- package/package.json +1 -1
|
@@ -1,66 +1,57 @@
|
|
|
1
|
-
import { ethers, Wallet, JsonRpcProvider, Contract, Interface } from 'ethers';
|
|
2
|
-
import { NonceManager, getOptimizedGasPrice } from '../../utils/bundle-helpers.js';
|
|
3
|
-
import { ADDRESSES } from '../../utils/constants.js';
|
|
4
|
-
import { CHAIN_ID_MAP, getTxType, getGasPriceConfig, shouldExtractProfit, calculateProfit, calculateBatchProfit, getProfitRecipient, getBribeAmount, BLOCKRAZOR_BUILDER_EOA } from './config.js';
|
|
5
|
-
const MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
|
|
6
|
-
const MULTICALL3_ABI = [
|
|
7
|
-
'function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) external payable returns (tuple(bool success, bytes returnData)[])'
|
|
8
|
-
];
|
|
9
|
-
// 常量
|
|
10
|
-
const WBNB_ADDRESS = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
|
|
11
|
-
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
12
|
-
const FLAT_FEE = 0n; // ✅ 已移除合约固定手续费
|
|
13
|
-
const DEFAULT_GAS_LIMIT = 800000; // ✅ 改为 number,配合 getGasLimit 使用
|
|
14
|
-
const DEADLINE_MINUTES = 20;
|
|
15
|
-
// PancakeSwapProxy ABI(PancakeSwap V3 没有 deadline 参数)
|
|
16
|
-
const PANCAKE_PROXY_ABI = [
|
|
17
|
-
'function swapV2(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) external payable returns (uint256 amountOut)',
|
|
18
|
-
'function swapV3Single(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint256 amountOutMin, address to) external payable returns (uint256 amountOut)',
|
|
19
|
-
'function swapV3MultiHop(address[] calldata lpAddresses, address exactTokenIn, uint256 amountIn, uint256 amountOutMin, address to) external payable returns (uint256 amountOut)'
|
|
20
|
-
];
|
|
21
|
-
// PancakeSwap V2 Router ABI(用于报价)
|
|
22
|
-
const PANCAKE_V2_ROUTER_ABI = [
|
|
23
|
-
'function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)'
|
|
24
|
-
];
|
|
25
|
-
// PancakeSwap V3 QuoterV2 ABI(用于报价)
|
|
26
|
-
const PANCAKE_V3_QUOTER_ABI = [
|
|
27
|
-
'function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96)) external returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)'
|
|
28
|
-
];
|
|
29
|
-
const PANCAKE_V2_ROUTER_ADDRESS = '0x10ED43C718714eb63d5aA57B78B54704E256024E';
|
|
30
|
-
const PANCAKE_V3_QUOTER_ADDRESS = '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997';
|
|
31
|
-
// ERC20 ABI
|
|
32
|
-
const ERC20_ABI = [
|
|
33
|
-
'function approve(address spender, uint256 amount) external returns (bool)',
|
|
34
|
-
'function allowance(address owner, address spender) external view returns (uint256)',
|
|
35
|
-
'function balanceOf(address account) external view returns (uint256)',
|
|
36
|
-
'function decimals() external view returns (uint8)'
|
|
37
|
-
];
|
|
38
|
-
// ==================== 辅助函数 ====================
|
|
39
1
|
/**
|
|
40
|
-
*
|
|
41
|
-
* 优先使用 config.gasLimit,否则使用默认值 * multiplier
|
|
2
|
+
* PancakeSwap 官方 Router 交易模块(Flap 版本,支持多链)
|
|
42
3
|
*
|
|
43
|
-
* ✅
|
|
4
|
+
* ✅ 使用官方 PancakeSwap V2/V3 Router(非代理合约)
|
|
5
|
+
* - V2 Router (BSC): 0x10ED43C718714eb63d5aA57B78B54704E256024E
|
|
6
|
+
* - V3 Router (BSC): 0x13f4EA83D0bd40E75C8222255bc855a974568Dd4
|
|
7
|
+
* - V3 Quoter (BSC): 0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997
|
|
44
8
|
*/
|
|
9
|
+
import { ethers, Wallet, JsonRpcProvider, Contract, Interface } from 'ethers';
|
|
10
|
+
import { NonceManager, getOptimizedGasPrice, getDeadline, encodeV3Path } from '../../utils/bundle-helpers.js';
|
|
11
|
+
import { ADDRESSES, ZERO_ADDRESS } from '../../utils/constants.js';
|
|
12
|
+
import { MULTICALL3_ABI, V2_ROUTER_ABI, V3_ROUTER02_ABI, V3_QUOTER_ABI, ERC20_ABI } from '../../abis/common.js';
|
|
13
|
+
import { CHAIN_ID_MAP, getTxType, getGasPriceConfig, shouldExtractProfit, calculateProfit, calculateBatchProfit, getProfitRecipient, getBribeAmount, BLOCKRAZOR_BUILDER_EOA } from './config.js';
|
|
14
|
+
// ==================== 常量 ====================
|
|
15
|
+
const MULTICALL3_ADDRESS = ADDRESSES.BSC.Multicall3;
|
|
16
|
+
const WBNB_ADDRESS = ADDRESSES.BSC.WBNB;
|
|
17
|
+
const DEFAULT_GAS_LIMIT = 800000;
|
|
18
|
+
// ✅ PancakeSwap 官方合约地址
|
|
19
|
+
const PANCAKE_V2_ROUTER_ADDRESS = ADDRESSES.BSC.PancakeV2Router;
|
|
20
|
+
const PANCAKE_V3_ROUTER_ADDRESS = ADDRESSES.BSC.PancakeV3Router;
|
|
21
|
+
const PANCAKE_V3_QUOTER_ADDRESS = ADDRESSES.BSC.PancakeV3Quoter;
|
|
22
|
+
// ==================== ABI 别名(从公共模块导入)====================
|
|
23
|
+
const V3_ROUTER_ABI = V3_ROUTER02_ABI;
|
|
24
|
+
// ==================== 官方 ABI ====================
|
|
25
|
+
// ✅ ABI 从公共模块导入:V2_ROUTER_ABI, V3_ROUTER02_ABI (as V3_ROUTER_ABI), V3_QUOTER_ABI, ERC20_ABI, MULTICALL3_ABI
|
|
26
|
+
// ✅ 工具函数从 bundle-helpers.js 导入:getDeadline, encodeV3Path
|
|
27
|
+
// ==================== Provider 缓存 ====================
|
|
28
|
+
const providerCache = new Map();
|
|
29
|
+
const PROVIDER_CACHE_TTL_MS = 60 * 1000;
|
|
30
|
+
// Token Decimals 缓存
|
|
31
|
+
const tokenDecimalsCache = new Map();
|
|
32
|
+
function getCachedProvider(chain, rpcUrl, chainId) {
|
|
33
|
+
const cacheKey = `${chain}-${rpcUrl}`;
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const cached = providerCache.get(cacheKey);
|
|
36
|
+
if (cached && cached.expireAt > now) {
|
|
37
|
+
return cached.provider;
|
|
38
|
+
}
|
|
39
|
+
const resolvedChainId = chainId ?? (CHAIN_ID_MAP[chain] || 56);
|
|
40
|
+
const provider = new JsonRpcProvider(rpcUrl, { chainId: resolvedChainId, name: chain });
|
|
41
|
+
providerCache.set(cacheKey, { provider, expireAt: now + PROVIDER_CACHE_TTL_MS });
|
|
42
|
+
return provider;
|
|
43
|
+
}
|
|
44
|
+
// ==================== 辅助函数 ====================
|
|
45
45
|
function getGasLimit(config, defaultGas = DEFAULT_GAS_LIMIT) {
|
|
46
|
-
// 优先使用前端传入的 gasLimit
|
|
47
46
|
if (config.gasLimit !== undefined) {
|
|
48
|
-
// 如果已经是 bigint,直接返回;否则转换
|
|
49
47
|
return typeof config.gasLimit === 'bigint' ? config.gasLimit : BigInt(config.gasLimit);
|
|
50
48
|
}
|
|
51
|
-
// 使用默认值 * multiplier
|
|
52
49
|
const multiplier = config.gasLimitMultiplier ?? 1.0;
|
|
53
50
|
const calculatedGas = Math.ceil(defaultGas * multiplier);
|
|
54
|
-
// JavaScript 原生 BigInt 转换(ethers v6 兼容)
|
|
55
51
|
return BigInt(calculatedGas);
|
|
56
52
|
}
|
|
57
|
-
/**
|
|
58
|
-
* 查询代币 decimals(带缓存)
|
|
59
|
-
* ✅ 代币精度不会变化,缓存后永久有效
|
|
60
|
-
*/
|
|
61
53
|
async function getTokenDecimals(tokenAddress, provider) {
|
|
62
54
|
const cacheKey = tokenAddress.toLowerCase();
|
|
63
|
-
// ✅ 检查缓存
|
|
64
55
|
const cached = tokenDecimalsCache.get(cacheKey);
|
|
65
56
|
if (cached !== undefined) {
|
|
66
57
|
return cached;
|
|
@@ -69,22 +60,15 @@ async function getTokenDecimals(tokenAddress, provider) {
|
|
|
69
60
|
const token = new Contract(tokenAddress, ERC20_ABI, provider);
|
|
70
61
|
const decimals = await token.decimals();
|
|
71
62
|
const result = Number(decimals);
|
|
72
|
-
// ✅ 缓存结果
|
|
73
63
|
tokenDecimalsCache.set(cacheKey, result);
|
|
74
64
|
return result;
|
|
75
65
|
}
|
|
76
66
|
catch {
|
|
77
|
-
// 默认返回 18,兼容大部分 ERC20(也缓存)
|
|
78
67
|
tokenDecimalsCache.set(cacheKey, 18);
|
|
79
68
|
return 18;
|
|
80
69
|
}
|
|
81
70
|
}
|
|
82
|
-
/**
|
|
83
|
-
* 判断是否需要发送 BNB(tokenIn 是 WBNB 且没有使用 ERC20 代币购买时)
|
|
84
|
-
* ✅ 新增:如果使用 quoteToken(如 USDT),则不需要发送 BNB
|
|
85
|
-
*/
|
|
86
71
|
function needSendBNB(routeType, params, useNativeToken = true) {
|
|
87
|
-
// 如果使用 ERC20 代币购买,不需要发送 BNB
|
|
88
72
|
if (!useNativeToken) {
|
|
89
73
|
return false;
|
|
90
74
|
}
|
|
@@ -99,81 +83,134 @@ function needSendBNB(routeType, params, useNativeToken = true) {
|
|
|
99
83
|
}
|
|
100
84
|
return false;
|
|
101
85
|
}
|
|
102
|
-
/**
|
|
103
|
-
* 判断是否使用原生代币(BNB)
|
|
104
|
-
*/
|
|
105
86
|
function isUsingNativeToken(quoteToken) {
|
|
106
87
|
return !quoteToken || quoteToken === ZERO_ADDRESS || quoteToken.toLowerCase() === WBNB_ADDRESS.toLowerCase();
|
|
107
88
|
}
|
|
89
|
+
// ✅ getDeadline 从 bundle-helpers.js 导入
|
|
108
90
|
/**
|
|
109
|
-
*
|
|
91
|
+
* 根据 routeType 获取授权目标地址
|
|
110
92
|
*/
|
|
111
|
-
function
|
|
112
|
-
|
|
93
|
+
function getApprovalTarget(routeType) {
|
|
94
|
+
if (routeType === 'v2') {
|
|
95
|
+
return PANCAKE_V2_ROUTER_ADDRESS;
|
|
96
|
+
}
|
|
97
|
+
return PANCAKE_V3_ROUTER_ADDRESS;
|
|
113
98
|
}
|
|
114
99
|
/**
|
|
115
|
-
* 构建 V2
|
|
100
|
+
* ✅ 构建 V2 交易(使用官方 Router)
|
|
116
101
|
*/
|
|
117
|
-
async function buildV2Transactions(
|
|
102
|
+
async function buildV2Transactions(routers, wallets, amountsWei, minOuts, path, isBuy, needBNB) {
|
|
118
103
|
const deadline = getDeadline();
|
|
119
|
-
return Promise.all(
|
|
120
|
-
|
|
121
|
-
|
|
104
|
+
return Promise.all(routers.map(async (router, i) => {
|
|
105
|
+
if (isBuy && needBNB) {
|
|
106
|
+
return router.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(minOuts[i], path, wallets[i].address, deadline, { value: amountsWei[i] });
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
return router.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(amountsWei[i], minOuts[i], path, wallets[i].address, deadline);
|
|
110
|
+
}
|
|
122
111
|
}));
|
|
123
112
|
}
|
|
124
113
|
/**
|
|
125
|
-
* 构建 V3
|
|
114
|
+
* ✅ 构建 V3 单跳交易(使用官方 SwapRouter02 + multicall)
|
|
126
115
|
*/
|
|
127
|
-
async function buildV3SingleTransactions(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
116
|
+
async function buildV3SingleTransactions(routers, wallets, tokenIn, tokenOut, fee, amountsWei, minOuts, isBuy, needBNB) {
|
|
117
|
+
const deadline = getDeadline();
|
|
118
|
+
const v3RouterIface = new Interface(V3_ROUTER_ABI);
|
|
119
|
+
return Promise.all(routers.map(async (router, i) => {
|
|
120
|
+
const isTokenOutWBNB = tokenOut.toLowerCase() === WBNB_ADDRESS.toLowerCase();
|
|
121
|
+
const exactInputSingleData = v3RouterIface.encodeFunctionData('exactInputSingle', [{
|
|
122
|
+
tokenIn: tokenIn,
|
|
123
|
+
tokenOut: tokenOut,
|
|
124
|
+
fee: fee,
|
|
125
|
+
recipient: isTokenOutWBNB ? PANCAKE_V3_ROUTER_ADDRESS : wallets[i].address,
|
|
126
|
+
amountIn: amountsWei[i],
|
|
127
|
+
amountOutMinimum: minOuts[i],
|
|
128
|
+
sqrtPriceLimitX96: 0n
|
|
129
|
+
}]);
|
|
130
|
+
if (isBuy && needBNB) {
|
|
131
|
+
return router.multicall.populateTransaction(deadline, [exactInputSingleData], { value: amountsWei[i] });
|
|
132
|
+
}
|
|
133
|
+
else if (!isBuy && isTokenOutWBNB) {
|
|
134
|
+
const unwrapData = v3RouterIface.encodeFunctionData('unwrapWETH9', [
|
|
135
|
+
minOuts[i],
|
|
136
|
+
wallets[i].address
|
|
137
|
+
]);
|
|
138
|
+
return router.multicall.populateTransaction(deadline, [exactInputSingleData, unwrapData]);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
return router.multicall.populateTransaction(deadline, [exactInputSingleData]);
|
|
142
|
+
}
|
|
131
143
|
}));
|
|
132
144
|
}
|
|
133
145
|
/**
|
|
134
|
-
* 构建 V3
|
|
146
|
+
* ✅ 构建 V3 多跳交易(使用官方 SwapRouter02 的 exactInput)
|
|
135
147
|
*/
|
|
136
|
-
async function buildV3MultiHopTransactions(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
148
|
+
async function buildV3MultiHopTransactions(routers, wallets, tokens, fees, amountsWei, minOuts, isBuy, needBNB) {
|
|
149
|
+
if (!tokens || tokens.length < 2) {
|
|
150
|
+
throw new Error('V3 多跳需要至少 2 个代币(v3Tokens)');
|
|
151
|
+
}
|
|
152
|
+
if (!fees || fees.length !== tokens.length - 1) {
|
|
153
|
+
throw new Error(`V3 多跳费率数量 (${fees?.length || 0}) 必须等于代币数量 - 1 (${tokens.length - 1})(v3Fees)`);
|
|
154
|
+
}
|
|
155
|
+
const deadline = getDeadline();
|
|
156
|
+
const v3RouterIface = new Interface(V3_ROUTER_ABI);
|
|
157
|
+
const forwardPath = encodeV3Path(tokens, fees);
|
|
158
|
+
const reversedTokens = [...tokens].reverse();
|
|
159
|
+
const reversedFees = [...fees].reverse();
|
|
160
|
+
const reversePath = encodeV3Path(reversedTokens, reversedFees);
|
|
161
|
+
const tokenOut = tokens[tokens.length - 1];
|
|
162
|
+
const isTokenOutWBNB = tokenOut.toLowerCase() === WBNB_ADDRESS.toLowerCase();
|
|
163
|
+
return Promise.all(routers.map(async (router, i) => {
|
|
164
|
+
if (isBuy && needBNB) {
|
|
165
|
+
const swapData = v3RouterIface.encodeFunctionData('exactInput', [{
|
|
166
|
+
path: forwardPath,
|
|
167
|
+
recipient: wallets[i].address,
|
|
168
|
+
amountIn: amountsWei[i],
|
|
169
|
+
amountOutMinimum: minOuts[i]
|
|
170
|
+
}]);
|
|
171
|
+
return router.multicall.populateTransaction(deadline, [swapData], { value: amountsWei[i] });
|
|
172
|
+
}
|
|
173
|
+
else if (!isBuy) {
|
|
174
|
+
const swapData = v3RouterIface.encodeFunctionData('exactInput', [{
|
|
175
|
+
path: reversePath,
|
|
176
|
+
recipient: isTokenOutWBNB ? PANCAKE_V3_ROUTER_ADDRESS : wallets[i].address,
|
|
177
|
+
amountIn: amountsWei[i],
|
|
178
|
+
amountOutMinimum: minOuts[i]
|
|
179
|
+
}]);
|
|
180
|
+
if (isTokenOutWBNB) {
|
|
181
|
+
const unwrapData = v3RouterIface.encodeFunctionData('unwrapWETH9', [minOuts[i], wallets[i].address]);
|
|
182
|
+
return router.multicall.populateTransaction(deadline, [swapData, unwrapData]);
|
|
183
|
+
}
|
|
184
|
+
return router.multicall.populateTransaction(deadline, [swapData]);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const swapData = v3RouterIface.encodeFunctionData('exactInput', [{
|
|
188
|
+
path: forwardPath,
|
|
189
|
+
recipient: wallets[i].address,
|
|
190
|
+
amountIn: amountsWei[i],
|
|
191
|
+
amountOutMinimum: minOuts[i]
|
|
192
|
+
}]);
|
|
193
|
+
return router.multicall.populateTransaction(deadline, [swapData]);
|
|
194
|
+
}
|
|
140
195
|
}));
|
|
141
196
|
}
|
|
142
197
|
// ==================== 主要导出函数 ====================
|
|
143
198
|
/**
|
|
144
|
-
* ✅
|
|
145
|
-
*/
|
|
146
|
-
function getCachedProvider(chain, rpcUrl, chainId) {
|
|
147
|
-
const cacheKey = `${chain}-${rpcUrl}`;
|
|
148
|
-
const now = Date.now();
|
|
149
|
-
const cached = providerCache.get(cacheKey);
|
|
150
|
-
if (cached && cached.expireAt > now) {
|
|
151
|
-
return cached.provider;
|
|
152
|
-
}
|
|
153
|
-
const resolvedChainId = chainId ?? (CHAIN_ID_MAP[chain] || 56);
|
|
154
|
-
const provider = new JsonRpcProvider(rpcUrl, { chainId: resolvedChainId, name: chain });
|
|
155
|
-
providerCache.set(cacheKey, { provider, expireAt: now + PROVIDER_CACHE_TTL_MS });
|
|
156
|
-
return provider;
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* 授权代币给 PancakeSwapProxy
|
|
199
|
+
* ✅ 授权代币给 PancakeSwap Router
|
|
160
200
|
*/
|
|
161
201
|
export async function approvePancakeProxy(params) {
|
|
162
|
-
const { chain, privateKey, tokenAddress, amount, rpcUrl } = params;
|
|
163
|
-
const
|
|
164
|
-
const provider = getCachedProvider(chain, rpcUrl);
|
|
202
|
+
const { chain, privateKey, tokenAddress, amount, rpcUrl, routeType } = params;
|
|
203
|
+
const approvalTarget = getApprovalTarget(routeType || 'v2');
|
|
204
|
+
const provider = getCachedProvider(chain, rpcUrl);
|
|
165
205
|
const wallet = new Wallet(privateKey, provider);
|
|
166
206
|
const token = new Contract(tokenAddress, ERC20_ABI, wallet);
|
|
167
|
-
// 查询 decimals
|
|
168
207
|
const decimals = await getTokenDecimals(tokenAddress, provider);
|
|
169
|
-
|
|
170
|
-
const currentAllowance = await token.allowance(wallet.address, pancakeProxyAddress);
|
|
208
|
+
const currentAllowance = await token.allowance(wallet.address, approvalTarget);
|
|
171
209
|
const amountBigInt = amount === 'max' ? ethers.MaxUint256 : ethers.parseUnits(amount, decimals);
|
|
172
210
|
if (currentAllowance >= amountBigInt) {
|
|
173
211
|
return { txHash: '', approved: true };
|
|
174
212
|
}
|
|
175
|
-
|
|
176
|
-
const tx = await token.approve(pancakeProxyAddress, amountBigInt);
|
|
213
|
+
const tx = await token.approve(approvalTarget, amountBigInt);
|
|
177
214
|
const receipt = await tx.wait();
|
|
178
215
|
return {
|
|
179
216
|
txHash: receipt.hash,
|
|
@@ -181,25 +218,22 @@ export async function approvePancakeProxy(params) {
|
|
|
181
218
|
};
|
|
182
219
|
}
|
|
183
220
|
/**
|
|
184
|
-
* 批量授权代币给
|
|
221
|
+
* ✅ 批量授权代币给 PancakeSwap Router
|
|
185
222
|
*/
|
|
186
223
|
export async function approvePancakeProxyBatch(params) {
|
|
187
|
-
const { chain, privateKeys, tokenAddress, amounts, config } = params;
|
|
224
|
+
const { chain, privateKeys, tokenAddress, amounts, config, routeType } = params;
|
|
188
225
|
if (privateKeys.length === 0 || amounts.length !== privateKeys.length) {
|
|
189
226
|
throw new Error('Private key count and amount count must match');
|
|
190
227
|
}
|
|
191
|
-
const
|
|
228
|
+
const approvalTarget = getApprovalTarget(routeType || 'v2');
|
|
192
229
|
const chainId = CHAIN_ID_MAP[chain];
|
|
193
|
-
const provider = getCachedProvider(chain, config.rpcUrl, chainId);
|
|
230
|
+
const provider = getCachedProvider(chain, config.rpcUrl, chainId);
|
|
194
231
|
const gasPrice = await getOptimizedGasPrice(provider, getGasPriceConfig(config));
|
|
195
|
-
// 查询 decimals
|
|
196
232
|
const decimals = await getTokenDecimals(tokenAddress, provider);
|
|
197
233
|
const wallets = privateKeys.map(k => new Wallet(k, provider));
|
|
198
234
|
const amountsBigInt = amounts.map(a => a === 'max' ? ethers.MaxUint256 : ethers.parseUnits(a, decimals));
|
|
199
|
-
// 检查现有授权,过滤掉已经足够的
|
|
200
235
|
const tokens = wallets.map(w => new Contract(tokenAddress, ERC20_ABI, w));
|
|
201
|
-
const allowances = await Promise.all(tokens.map((token, i) => token.allowance(wallets[i].address,
|
|
202
|
-
// 只授权不足的
|
|
236
|
+
const allowances = await Promise.all(tokens.map((token, i) => token.allowance(wallets[i].address, approvalTarget)));
|
|
203
237
|
const needApproval = wallets.filter((_, i) => allowances[i] < amountsBigInt[i]);
|
|
204
238
|
const needApprovalAmounts = amountsBigInt.filter((amount, i) => allowances[i] < amount);
|
|
205
239
|
if (needApproval.length === 0) {
|
|
@@ -210,12 +244,9 @@ export async function approvePancakeProxyBatch(params) {
|
|
|
210
244
|
message: '所有钱包已授权'
|
|
211
245
|
};
|
|
212
246
|
}
|
|
213
|
-
// 构建授权交易
|
|
214
247
|
const needApprovalTokens = needApproval.map(w => new Contract(tokenAddress, ERC20_ABI, w));
|
|
215
|
-
const unsignedApprovals = await Promise.all(needApprovalTokens.map((token, i) => token.approve.populateTransaction(
|
|
216
|
-
// 使用前端传入的 gasLimit,否则使用默认值
|
|
248
|
+
const unsignedApprovals = await Promise.all(needApprovalTokens.map((token, i) => token.approve.populateTransaction(approvalTarget, needApprovalAmounts[i])));
|
|
217
249
|
const finalGasLimit = getGasLimit(config);
|
|
218
|
-
// 签名授权交易
|
|
219
250
|
const nonceManager = new NonceManager(provider);
|
|
220
251
|
const nonces = await Promise.all(needApproval.map(w => nonceManager.getNextNonce(w)));
|
|
221
252
|
const signedTxs = await Promise.all(unsignedApprovals.map((unsigned, i) => needApproval[i].signTransaction({
|
|
@@ -228,7 +259,6 @@ export async function approvePancakeProxyBatch(params) {
|
|
|
228
259
|
type: getTxType(config)
|
|
229
260
|
})));
|
|
230
261
|
nonceManager.clearTemp();
|
|
231
|
-
// ✅ 内部直接广播提交(逐个发送)
|
|
232
262
|
const txHashes = [];
|
|
233
263
|
const errors = [];
|
|
234
264
|
for (let i = 0; i < signedTxs.length; i++) {
|
|
@@ -261,78 +291,85 @@ export async function approvePancakeProxyBatch(params) {
|
|
|
261
291
|
}
|
|
262
292
|
}
|
|
263
293
|
/**
|
|
264
|
-
*
|
|
265
|
-
* ✅ 精简版:只负责签名交易,不提交到 Merkle
|
|
294
|
+
* ✅ 使用 PancakeSwap 官方 Router 批量购买代币
|
|
266
295
|
*/
|
|
267
296
|
export async function pancakeProxyBatchBuyMerkle(params) {
|
|
268
297
|
const { chain, privateKeys, buyAmounts, tokenAddress, routeType, config, quoteToken, quoteTokenDecimals } = params;
|
|
269
298
|
if (privateKeys.length === 0 || buyAmounts.length !== privateKeys.length) {
|
|
270
299
|
throw new Error('Private key count and buy amount count must match');
|
|
271
300
|
}
|
|
272
|
-
const
|
|
273
|
-
const
|
|
301
|
+
const chainId = CHAIN_ID_MAP[chain];
|
|
302
|
+
const provider = getCachedProvider(chain, config.rpcUrl, chainId);
|
|
303
|
+
const buyers = privateKeys.map(k => new Wallet(k, provider));
|
|
274
304
|
const extractProfit = shouldExtractProfit(config);
|
|
275
305
|
const nonceManager = new NonceManager(provider);
|
|
276
306
|
const finalGasLimit = getGasLimit(config);
|
|
277
|
-
// ✅ 判断是否使用原生代币(BNB)或 ERC20 代币(如 USDT)
|
|
278
307
|
const useNativeToken = isUsingNativeToken(quoteToken);
|
|
279
308
|
const originalAmountsWei = buyAmounts.map(amount => ethers.parseEther(amount));
|
|
280
309
|
const { totalProfit, remainingAmounts } = calculateBatchProfit(originalAmountsWei, config);
|
|
281
310
|
const maxFundsIndex = findMaxAmountIndex(originalAmountsWei);
|
|
282
|
-
// ✅ ERC20 购买时:跳过利润提取(因为用户没有 BNB 可以转)
|
|
283
311
|
const shouldExtractProfitForBuy = extractProfit && useNativeToken;
|
|
284
312
|
const nativeProfitAmount = shouldExtractProfitForBuy ? totalProfit : 0n;
|
|
285
|
-
if (!useNativeToken && extractProfit) {
|
|
286
|
-
}
|
|
287
|
-
// ✅ 如果使用 ERC20 代币购买,需要根据精度转换金额
|
|
288
313
|
let actualAmountsWei = remainingAmounts;
|
|
289
314
|
if (!useNativeToken && quoteTokenDecimals !== undefined && quoteTokenDecimals !== 18) {
|
|
290
315
|
const decimalsDiff = 18 - quoteTokenDecimals;
|
|
291
316
|
const divisor = BigInt(10 ** decimalsDiff);
|
|
292
317
|
actualAmountsWei = remainingAmounts.map(amount => amount / divisor);
|
|
293
318
|
}
|
|
294
|
-
// ✅ 优化:如果前端传入了 gasPrice 和 nonces,跳过 RPC 调用
|
|
295
319
|
const presetGasPrice = config.gasPrice;
|
|
296
320
|
const presetNonces = config.nonces;
|
|
297
|
-
// ✅ 只获取必需的数据(跳过已有的)
|
|
298
321
|
const [gasPrice, tokenDecimals, nonces] = await Promise.all([
|
|
299
|
-
// gasPrice:优先使用前端传入的
|
|
300
322
|
presetGasPrice !== undefined
|
|
301
323
|
? Promise.resolve(presetGasPrice)
|
|
302
324
|
: getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
303
|
-
// tokenDecimals:有缓存
|
|
304
325
|
getTokenDecimals(tokenAddress, provider),
|
|
305
|
-
// nonces:优先使用前端传入的
|
|
306
326
|
presetNonces && presetNonces.length === buyers.length
|
|
307
327
|
? Promise.resolve(presetNonces)
|
|
308
328
|
: allocateProfitAwareNonces(buyers, shouldExtractProfitForBuy, maxFundsIndex, nativeProfitAmount, nonceManager)
|
|
309
329
|
]);
|
|
310
|
-
const minOuts =
|
|
330
|
+
const minOuts = new Array(buyers.length).fill(0n);
|
|
311
331
|
const needBNB = needSendBNB(routeType, params, useNativeToken);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
params
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
actualAmountsWei,
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
332
|
+
// ✅ 使用官方 Router
|
|
333
|
+
let routers;
|
|
334
|
+
let unsignedBuys;
|
|
335
|
+
if (routeType === 'v2') {
|
|
336
|
+
if (!params.v2Path || params.v2Path.length < 2) {
|
|
337
|
+
throw new Error('v2Path is required for V2 routing');
|
|
338
|
+
}
|
|
339
|
+
routers = buyers.map(w => new Contract(PANCAKE_V2_ROUTER_ADDRESS, V2_ROUTER_ABI, w));
|
|
340
|
+
unsignedBuys = await buildV2Transactions(routers, buyers, actualAmountsWei, minOuts, params.v2Path, true, needBNB);
|
|
341
|
+
}
|
|
342
|
+
else if (routeType === 'v3-single') {
|
|
343
|
+
if (!params.v3TokenIn || !params.v3Fee) {
|
|
344
|
+
throw new Error('v3TokenIn and v3Fee are required for V3 single-hop');
|
|
345
|
+
}
|
|
346
|
+
routers = buyers.map(w => new Contract(PANCAKE_V3_ROUTER_ADDRESS, V3_ROUTER_ABI, w));
|
|
347
|
+
unsignedBuys = await buildV3SingleTransactions(routers, buyers, params.v3TokenIn, tokenAddress, params.v3Fee, actualAmountsWei, minOuts, true, needBNB);
|
|
348
|
+
}
|
|
349
|
+
else if (routeType === 'v3-multi') {
|
|
350
|
+
// ✅ V3 多跳:需要 v3Tokens 和 v3Fees 参数
|
|
351
|
+
if (!params.v3Tokens || params.v3Tokens.length < 2) {
|
|
352
|
+
throw new Error('v3Tokens(至少 2 个代币)是 V3 多跳必需的');
|
|
353
|
+
}
|
|
354
|
+
if (!params.v3Fees || params.v3Fees.length !== params.v3Tokens.length - 1) {
|
|
355
|
+
throw new Error(`v3Fees 长度必须等于 v3Tokens 长度 - 1`);
|
|
356
|
+
}
|
|
357
|
+
routers = buyers.map(w => new Contract(PANCAKE_V3_ROUTER_ADDRESS, V3_ROUTER_ABI, w));
|
|
358
|
+
unsignedBuys = await buildV3MultiHopTransactions(routers, buyers, params.v3Tokens, params.v3Fees, actualAmountsWei, minOuts, true, needBNB);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
throw new Error(`Unsupported routeType: ${routeType}`);
|
|
362
|
+
}
|
|
325
363
|
const bribeAmount = getBribeAmount(config);
|
|
326
364
|
const needBribeTx = bribeAmount > 0n && maxFundsIndex >= 0 && buyers.length > 0;
|
|
327
365
|
const txType = getTxType(config);
|
|
328
|
-
// 调整 maxFundsIndex 钱包的 nonce(如果有贿赂交易)
|
|
329
366
|
let bribeNonce;
|
|
367
|
+
const mutableNonces = [...nonces];
|
|
330
368
|
if (needBribeTx) {
|
|
331
|
-
bribeNonce =
|
|
332
|
-
|
|
369
|
+
bribeNonce = mutableNonces[maxFundsIndex];
|
|
370
|
+
mutableNonces[maxFundsIndex] = bribeNonce + 1;
|
|
333
371
|
}
|
|
334
372
|
const signPromises = [];
|
|
335
|
-
// 贿赂交易(索引 0)
|
|
336
373
|
if (needBribeTx && bribeNonce !== undefined) {
|
|
337
374
|
signPromises.push(buyers[maxFundsIndex].signTransaction({
|
|
338
375
|
to: BLOCKRAZOR_BUILDER_EOA,
|
|
@@ -344,13 +381,12 @@ export async function pancakeProxyBatchBuyMerkle(params) {
|
|
|
344
381
|
type: txType
|
|
345
382
|
}));
|
|
346
383
|
}
|
|
347
|
-
// 买入交易(索引 1 ~ N)
|
|
348
384
|
unsignedBuys.forEach((unsigned, i) => {
|
|
349
|
-
const txValue = useNativeToken ? unsigned.value :
|
|
385
|
+
const txValue = useNativeToken ? unsigned.value : 0n;
|
|
350
386
|
signPromises.push(buyers[i].signTransaction({
|
|
351
387
|
...unsigned,
|
|
352
388
|
from: buyers[i].address,
|
|
353
|
-
nonce:
|
|
389
|
+
nonce: mutableNonces[i],
|
|
354
390
|
gasLimit: finalGasLimit,
|
|
355
391
|
gasPrice,
|
|
356
392
|
chainId,
|
|
@@ -358,9 +394,8 @@ export async function pancakeProxyBatchBuyMerkle(params) {
|
|
|
358
394
|
value: txValue
|
|
359
395
|
}));
|
|
360
396
|
});
|
|
361
|
-
// 利润交易(索引 N+1)
|
|
362
397
|
if (shouldExtractProfitForBuy && nativeProfitAmount > 0n && maxFundsIndex >= 0) {
|
|
363
|
-
const profitNonce =
|
|
398
|
+
const profitNonce = mutableNonces[maxFundsIndex] + 1;
|
|
364
399
|
signPromises.push(buyers[maxFundsIndex].signTransaction({
|
|
365
400
|
to: getProfitRecipient(),
|
|
366
401
|
value: nativeProfitAmount,
|
|
@@ -371,7 +406,6 @@ export async function pancakeProxyBatchBuyMerkle(params) {
|
|
|
371
406
|
type: txType
|
|
372
407
|
}));
|
|
373
408
|
}
|
|
374
|
-
// ✅ 并行签名完成后按顺序返回
|
|
375
409
|
const signedTxs = await Promise.all(signPromises);
|
|
376
410
|
nonceManager.clearTemp();
|
|
377
411
|
return {
|
|
@@ -379,46 +413,56 @@ export async function pancakeProxyBatchBuyMerkle(params) {
|
|
|
379
413
|
};
|
|
380
414
|
}
|
|
381
415
|
/**
|
|
382
|
-
* 使用
|
|
383
|
-
* ✅ 自动检查授权,智能处理授权+卖出
|
|
384
|
-
*/
|
|
385
|
-
/**
|
|
386
|
-
* PancakeSwapProxy 批量卖出(仅签名版本 - 不依赖 Merkle)
|
|
387
|
-
* ✅ 精简版:只负责签名交易,不提交到 Merkle
|
|
416
|
+
* ✅ 使用 PancakeSwap 官方 Router 批量卖出代币
|
|
388
417
|
*/
|
|
389
418
|
export async function pancakeProxyBatchSellMerkle(params) {
|
|
390
419
|
const { chain, privateKeys, sellAmounts, tokenAddress, routeType, config } = params;
|
|
391
420
|
if (privateKeys.length === 0 || sellAmounts.length !== privateKeys.length) {
|
|
392
421
|
throw new Error('Private key count and sell amount count must match');
|
|
393
422
|
}
|
|
394
|
-
const
|
|
395
|
-
const
|
|
423
|
+
const chainId = CHAIN_ID_MAP[chain];
|
|
424
|
+
const provider = getCachedProvider(chain, config.rpcUrl, chainId);
|
|
425
|
+
const sellers = privateKeys.map(k => new Wallet(k, provider));
|
|
396
426
|
const finalGasLimit = getGasLimit(config);
|
|
397
427
|
const extractProfit = shouldExtractProfit(config);
|
|
398
428
|
const nonceManager = new NonceManager(provider);
|
|
399
|
-
// ✅ 优化:如果前端传入了 gasPrice 和 nonces,跳过 RPC 调用
|
|
400
429
|
const presetGasPrice = config.gasPrice;
|
|
401
430
|
const presetNonces = config.nonces;
|
|
402
|
-
// ✅ 优化:并行获取 gasPrice 和 tokenDecimals(已移除授权检查)
|
|
403
431
|
const [gasPrice, tokenDecimals] = await Promise.all([
|
|
404
|
-
// gasPrice:优先使用前端传入的
|
|
405
432
|
presetGasPrice !== undefined
|
|
406
433
|
? Promise.resolve(presetGasPrice)
|
|
407
434
|
: getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
408
|
-
// tokenDecimals:有缓存
|
|
409
435
|
getTokenDecimals(tokenAddress, provider)
|
|
410
436
|
]);
|
|
411
437
|
const amountsWei = sellAmounts.map(amount => ethers.parseUnits(amount, tokenDecimals));
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
params
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
438
|
+
// 获取报价
|
|
439
|
+
let quotedOutputs;
|
|
440
|
+
if (routeType === 'v2' && params.v2Path && params.v2Path.length >= 2) {
|
|
441
|
+
quotedOutputs = await batchGetV2Quotes(provider, amountsWei, params.v2Path);
|
|
442
|
+
}
|
|
443
|
+
else if (routeType === 'v3-single') {
|
|
444
|
+
quotedOutputs = await Promise.all(amountsWei.map(async (amount) => {
|
|
445
|
+
try {
|
|
446
|
+
const quoter = new Contract(PANCAKE_V3_QUOTER_ADDRESS, V3_QUOTER_ABI, provider);
|
|
447
|
+
const result = await quoter.quoteExactInputSingle.staticCall({
|
|
448
|
+
tokenIn: tokenAddress,
|
|
449
|
+
tokenOut: params.v3TokenOut,
|
|
450
|
+
amountIn: amount,
|
|
451
|
+
fee: params.v3Fee,
|
|
452
|
+
sqrtPriceLimitX96: 0
|
|
453
|
+
});
|
|
454
|
+
return result[0];
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return 0n;
|
|
458
|
+
}
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
quotedOutputs = new Array(amountsWei.length).fill(0n);
|
|
463
|
+
}
|
|
464
|
+
const minOuts = new Array(sellers.length).fill(0n);
|
|
465
|
+
// 计算利润
|
|
422
466
|
let totalProfit = 0n;
|
|
423
467
|
let maxRevenueIndex = -1;
|
|
424
468
|
let maxRevenue = 0n;
|
|
@@ -435,68 +479,81 @@ export async function pancakeProxyBatchSellMerkle(params) {
|
|
|
435
479
|
}
|
|
436
480
|
}
|
|
437
481
|
}
|
|
438
|
-
// ✅ 修复:根据是否需要贿赂/利润交易,统一分配 nonces
|
|
439
482
|
const bribeAmount = getBribeAmount(config);
|
|
440
483
|
const needBribeTx = bribeAmount > 0n && maxRevenueIndex >= 0;
|
|
441
484
|
const needProfitTx = extractProfit && totalProfit > 0n && maxRevenueIndex >= 0;
|
|
442
|
-
// 计算 maxRevenueIndex 钱包需要的 nonce 数量:贿赂(可选) + 卖出 + 利润(可选)
|
|
443
485
|
const maxRevenueNonceCount = 1 + (needBribeTx ? 1 : 0) + (needProfitTx ? 1 : 0);
|
|
444
486
|
let nonces;
|
|
445
487
|
let bribeNonce;
|
|
446
488
|
let profitNonce;
|
|
447
|
-
// ✅ 优化:如果前端传入了 nonces,直接使用
|
|
448
489
|
if (presetNonces && presetNonces.length === sellers.length) {
|
|
449
490
|
nonces = [...presetNonces];
|
|
450
491
|
if (needBribeTx) {
|
|
451
492
|
bribeNonce = nonces[maxRevenueIndex];
|
|
452
|
-
nonces[maxRevenueIndex] = bribeNonce + 1;
|
|
493
|
+
nonces[maxRevenueIndex] = bribeNonce + 1;
|
|
453
494
|
}
|
|
454
495
|
profitNonce = needProfitTx ? nonces[maxRevenueIndex] + 1 : undefined;
|
|
455
496
|
}
|
|
456
497
|
else if (maxRevenueNonceCount > 1 && maxRevenueIndex >= 0) {
|
|
457
|
-
// maxRevenueIndex 钱包需要多个连续 nonce
|
|
458
498
|
const maxRevenueNonces = await nonceManager.getNextNonceBatch(sellers[maxRevenueIndex], maxRevenueNonceCount);
|
|
459
|
-
// 其他钱包各需要 1 个 nonce
|
|
460
499
|
const otherSellers = sellers.filter((_, i) => i !== maxRevenueIndex);
|
|
461
500
|
const otherNonces = otherSellers.length > 0
|
|
462
501
|
? await nonceManager.getNextNoncesForWallets(otherSellers)
|
|
463
502
|
: [];
|
|
464
|
-
// 组装最终的 nonces 数组(保持原顺序)
|
|
465
503
|
nonces = [];
|
|
466
504
|
let otherIdx = 0;
|
|
467
505
|
let nonceIdx = 0;
|
|
468
506
|
if (needBribeTx) {
|
|
469
|
-
bribeNonce = maxRevenueNonces[nonceIdx++];
|
|
507
|
+
bribeNonce = maxRevenueNonces[nonceIdx++];
|
|
470
508
|
}
|
|
471
509
|
for (let i = 0; i < sellers.length; i++) {
|
|
472
510
|
if (i === maxRevenueIndex) {
|
|
473
|
-
nonces.push(maxRevenueNonces[nonceIdx++]);
|
|
511
|
+
nonces.push(maxRevenueNonces[nonceIdx++]);
|
|
474
512
|
}
|
|
475
513
|
else {
|
|
476
514
|
nonces.push(otherNonces[otherIdx++]);
|
|
477
515
|
}
|
|
478
516
|
}
|
|
479
517
|
if (needProfitTx) {
|
|
480
|
-
profitNonce = maxRevenueNonces[nonceIdx];
|
|
518
|
+
profitNonce = maxRevenueNonces[nonceIdx];
|
|
481
519
|
}
|
|
482
520
|
}
|
|
483
521
|
else {
|
|
484
|
-
// 不需要额外交易,所有钱包各 1 个 nonce
|
|
485
522
|
nonces = await nonceManager.getNextNoncesForWallets(sellers);
|
|
486
523
|
}
|
|
487
|
-
// ✅
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
minOuts
|
|
496
|
-
}
|
|
524
|
+
// ✅ 使用官方 Router 构建卖出交易
|
|
525
|
+
let routers;
|
|
526
|
+
let unsignedSells;
|
|
527
|
+
if (routeType === 'v2') {
|
|
528
|
+
if (!params.v2Path || params.v2Path.length < 2) {
|
|
529
|
+
throw new Error('v2Path is required for V2 routing');
|
|
530
|
+
}
|
|
531
|
+
routers = sellers.map(w => new Contract(PANCAKE_V2_ROUTER_ADDRESS, V2_ROUTER_ABI, w));
|
|
532
|
+
unsignedSells = await buildV2Transactions(routers, sellers, amountsWei, minOuts, params.v2Path, false, false);
|
|
533
|
+
}
|
|
534
|
+
else if (routeType === 'v3-single') {
|
|
535
|
+
if (!params.v3TokenOut || !params.v3Fee) {
|
|
536
|
+
throw new Error('v3TokenOut and v3Fee are required for V3 single-hop');
|
|
537
|
+
}
|
|
538
|
+
routers = sellers.map(w => new Contract(PANCAKE_V3_ROUTER_ADDRESS, V3_ROUTER_ABI, w));
|
|
539
|
+
unsignedSells = await buildV3SingleTransactions(routers, sellers, tokenAddress, params.v3TokenOut, params.v3Fee, amountsWei, minOuts, false, false);
|
|
540
|
+
}
|
|
541
|
+
else if (routeType === 'v3-multi') {
|
|
542
|
+
// ✅ V3 多跳:需要 v3Tokens 和 v3Fees 参数
|
|
543
|
+
if (!params.v3Tokens || params.v3Tokens.length < 2) {
|
|
544
|
+
throw new Error('v3Tokens(至少 2 个代币)是 V3 多跳必需的');
|
|
545
|
+
}
|
|
546
|
+
if (!params.v3Fees || params.v3Fees.length !== params.v3Tokens.length - 1) {
|
|
547
|
+
throw new Error(`v3Fees 长度必须等于 v3Tokens 长度 - 1`);
|
|
548
|
+
}
|
|
549
|
+
routers = sellers.map(w => new Contract(PANCAKE_V3_ROUTER_ADDRESS, V3_ROUTER_ABI, w));
|
|
550
|
+
unsignedSells = await buildV3MultiHopTransactions(routers, sellers, params.v3Tokens, params.v3Fees, amountsWei, minOuts, false, false);
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
throw new Error(`Unsupported routeType: ${routeType}`);
|
|
554
|
+
}
|
|
497
555
|
const txType = getTxType(config);
|
|
498
556
|
const signPromises = [];
|
|
499
|
-
// 贿赂交易(索引 0)
|
|
500
557
|
if (needBribeTx && bribeNonce !== undefined) {
|
|
501
558
|
signPromises.push(sellers[maxRevenueIndex].signTransaction({
|
|
502
559
|
to: BLOCKRAZOR_BUILDER_EOA,
|
|
@@ -508,9 +565,8 @@ export async function pancakeProxyBatchSellMerkle(params) {
|
|
|
508
565
|
type: txType
|
|
509
566
|
}));
|
|
510
567
|
}
|
|
511
|
-
// 卖出交易(索引 1 ~ N)
|
|
512
568
|
unsignedSells.forEach((unsigned, i) => {
|
|
513
|
-
const txValue = unsigned.value;
|
|
569
|
+
const txValue = unsigned.value ?? 0n;
|
|
514
570
|
signPromises.push(sellers[i].signTransaction({
|
|
515
571
|
...unsigned,
|
|
516
572
|
from: sellers[i].address,
|
|
@@ -522,7 +578,6 @@ export async function pancakeProxyBatchSellMerkle(params) {
|
|
|
522
578
|
value: txValue
|
|
523
579
|
}));
|
|
524
580
|
});
|
|
525
|
-
// 利润交易(索引 N+1)
|
|
526
581
|
if (needProfitTx && profitNonce !== undefined) {
|
|
527
582
|
signPromises.push(sellers[maxRevenueIndex].signTransaction({
|
|
528
583
|
to: getProfitRecipient(),
|
|
@@ -534,34 +589,64 @@ export async function pancakeProxyBatchSellMerkle(params) {
|
|
|
534
589
|
type: txType
|
|
535
590
|
}));
|
|
536
591
|
}
|
|
537
|
-
// ✅ 并行签名完成后按顺序返回
|
|
538
592
|
const signedTxs = await Promise.all(signPromises);
|
|
539
593
|
nonceManager.clearTemp();
|
|
540
594
|
return {
|
|
541
595
|
signedTransactions: signedTxs
|
|
542
596
|
};
|
|
543
597
|
}
|
|
544
|
-
//
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
598
|
+
// ==================== 内部工具函数 ====================
|
|
599
|
+
/**
|
|
600
|
+
* ✅ 使用 Multicall3 批量获取 V2 报价
|
|
601
|
+
*/
|
|
602
|
+
async function batchGetV2Quotes(provider, amountsWei, v2Path) {
|
|
603
|
+
if (amountsWei.length === 0)
|
|
604
|
+
return [];
|
|
605
|
+
if (amountsWei.length === 1) {
|
|
606
|
+
try {
|
|
607
|
+
const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, V2_ROUTER_ABI, provider);
|
|
608
|
+
const amounts = await v2Router.getAmountsOut(amountsWei[0], v2Path);
|
|
609
|
+
return [amounts[amounts.length - 1]];
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
return [0n];
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
const v2RouterIface = new Interface(V2_ROUTER_ABI);
|
|
617
|
+
const multicall = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
|
|
618
|
+
const calls = amountsWei.map(amount => ({
|
|
619
|
+
target: PANCAKE_V2_ROUTER_ADDRESS,
|
|
620
|
+
allowFailure: true,
|
|
621
|
+
callData: v2RouterIface.encodeFunctionData('getAmountsOut', [amount, v2Path])
|
|
622
|
+
}));
|
|
623
|
+
const results = await multicall.aggregate3.staticCall(calls);
|
|
624
|
+
return results.map((r) => {
|
|
625
|
+
if (r.success && r.returnData && r.returnData !== '0x') {
|
|
626
|
+
try {
|
|
627
|
+
const decoded = v2RouterIface.decodeFunctionResult('getAmountsOut', r.returnData);
|
|
628
|
+
const amounts = decoded[0];
|
|
629
|
+
return amounts[amounts.length - 1];
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
return 0n;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return 0n;
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
return await Promise.all(amountsWei.map(async (amount) => {
|
|
640
|
+
try {
|
|
641
|
+
const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, V2_ROUTER_ABI, provider);
|
|
642
|
+
const amounts = await v2Router.getAmountsOut(amount, v2Path);
|
|
643
|
+
return amounts[amounts.length - 1];
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
return 0n;
|
|
647
|
+
}
|
|
648
|
+
}));
|
|
557
649
|
}
|
|
558
|
-
// ✅ 创建新 Provider 并缓存
|
|
559
|
-
const provider = new JsonRpcProvider(rpcUrl, { chainId, name: chain });
|
|
560
|
-
providerCache.set(cacheKey, { provider, expireAt: now + PROVIDER_CACHE_TTL_MS });
|
|
561
|
-
return { chainId, provider };
|
|
562
|
-
}
|
|
563
|
-
function createWallets(privateKeys, provider) {
|
|
564
|
-
return privateKeys.map(key => new Wallet(key, provider));
|
|
565
650
|
}
|
|
566
651
|
function findMaxAmountIndex(amounts) {
|
|
567
652
|
if (!amounts.length) {
|
|
@@ -577,56 +662,21 @@ function findMaxAmountIndex(amounts) {
|
|
|
577
662
|
}
|
|
578
663
|
return maxIndex;
|
|
579
664
|
}
|
|
580
|
-
function resolveBuyMinOutputs(_params, walletCount, _tokenDecimals) {
|
|
581
|
-
// ✅ 已移除滑点保护:minOutput 固定为 0
|
|
582
|
-
return new Array(walletCount).fill(0n);
|
|
583
|
-
}
|
|
584
|
-
function createPancakeProxies(wallets, proxyAddress) {
|
|
585
|
-
return wallets.map(wallet => new Contract(proxyAddress, PANCAKE_PROXY_ABI, wallet));
|
|
586
|
-
}
|
|
587
|
-
async function buildBuyTransactions({ routeType, params, proxies, wallets, tokenAddress, actualAmountsWei, minOuts, needBNB }) {
|
|
588
|
-
if (routeType === 'v2') {
|
|
589
|
-
if (!params.v2Path || params.v2Path.length < 2) {
|
|
590
|
-
throw new Error('v2Path is required for V2 routing');
|
|
591
|
-
}
|
|
592
|
-
return await buildV2Transactions(proxies, wallets, actualAmountsWei, minOuts, params.v2Path, true, needBNB);
|
|
593
|
-
}
|
|
594
|
-
if (routeType === 'v3-single') {
|
|
595
|
-
if (!params.v3TokenIn || !params.v3Fee) {
|
|
596
|
-
throw new Error('v3TokenIn and v3Fee are required for V3 single-hop');
|
|
597
|
-
}
|
|
598
|
-
return await buildV3SingleTransactions(proxies, wallets, params.v3TokenIn, tokenAddress, params.v3Fee, actualAmountsWei, minOuts, true, needBNB);
|
|
599
|
-
}
|
|
600
|
-
if (routeType === 'v3-multi') {
|
|
601
|
-
if (!params.v3LpAddresses || params.v3LpAddresses.length === 0 || !params.v3ExactTokenIn) {
|
|
602
|
-
throw new Error('v3LpAddresses and v3ExactTokenIn are required for V3 multi-hop');
|
|
603
|
-
}
|
|
604
|
-
return await buildV3MultiHopTransactions(proxies, wallets, params.v3LpAddresses, params.v3ExactTokenIn, actualAmountsWei, minOuts, true, needBNB);
|
|
605
|
-
}
|
|
606
|
-
throw new Error(`Unsupported routeType: ${routeType}`);
|
|
607
|
-
}
|
|
608
|
-
/**
|
|
609
|
-
* ✅ 修复:明确分配 nonces,避免隐式状态依赖
|
|
610
|
-
*/
|
|
611
665
|
async function allocateProfitAwareNonces(wallets, extractProfit, maxIndex, totalProfit, nonceManager) {
|
|
612
666
|
const needProfitTx = extractProfit && totalProfit > 0n && maxIndex >= 0 && maxIndex < wallets.length;
|
|
613
667
|
if (!needProfitTx) {
|
|
614
|
-
// 不需要利润交易,所有钱包各 1 个 nonce
|
|
615
668
|
return await nonceManager.getNextNoncesForWallets(wallets);
|
|
616
669
|
}
|
|
617
|
-
// 需要利润交易:maxIndex 钱包需要 2 个连续 nonce
|
|
618
670
|
const maxIndexNonces = await nonceManager.getNextNonceBatch(wallets[maxIndex], 2);
|
|
619
|
-
// 其他钱包各需要 1 个 nonce
|
|
620
671
|
const otherWallets = wallets.filter((_, i) => i !== maxIndex);
|
|
621
672
|
const otherNonces = otherWallets.length > 0
|
|
622
673
|
? await nonceManager.getNextNoncesForWallets(otherWallets)
|
|
623
674
|
: [];
|
|
624
|
-
// 组装最终的 nonces 数组(保持原顺序)
|
|
625
675
|
const nonces = [];
|
|
626
676
|
let otherIdx = 0;
|
|
627
677
|
for (let i = 0; i < wallets.length; i++) {
|
|
628
678
|
if (i === maxIndex) {
|
|
629
|
-
nonces.push(maxIndexNonces[0]);
|
|
679
|
+
nonces.push(maxIndexNonces[0]);
|
|
630
680
|
}
|
|
631
681
|
else {
|
|
632
682
|
nonces.push(otherNonces[otherIdx++]);
|
|
@@ -634,116 +684,3 @@ async function allocateProfitAwareNonces(wallets, extractProfit, maxIndex, total
|
|
|
634
684
|
}
|
|
635
685
|
return nonces;
|
|
636
686
|
}
|
|
637
|
-
/**
|
|
638
|
-
* ✅ 获取卖出报价(用于计算利润,不用于滑点保护)
|
|
639
|
-
* ✅ 已移除滑点保护:minOutput 固定为 0
|
|
640
|
-
*/
|
|
641
|
-
async function resolveSellOutputs({ params, provider, tokenAddress, routeType, amountsWei }) {
|
|
642
|
-
// ✅ 已移除滑点保护:minOutput 固定为 0
|
|
643
|
-
const minOuts = new Array(amountsWei.length).fill(0n);
|
|
644
|
-
// 如果只有 1 个,直接调用(避免 multicall 开销)
|
|
645
|
-
if (amountsWei.length === 1) {
|
|
646
|
-
const quotedOutput = await getSingleQuote(params, provider, tokenAddress, routeType, amountsWei[0]);
|
|
647
|
-
return { quotedOutputs: [quotedOutput], minOuts };
|
|
648
|
-
}
|
|
649
|
-
// ✅ 使用 Multicall3 批量获取报价(仅 V2 路由支持,用于计算利润)
|
|
650
|
-
if (routeType === 'v2' && params.v2Path && params.v2Path.length >= 2) {
|
|
651
|
-
try {
|
|
652
|
-
const v2RouterIface = new Interface(PANCAKE_V2_ROUTER_ABI);
|
|
653
|
-
const multicall = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
|
|
654
|
-
const calls = amountsWei.map(amount => ({
|
|
655
|
-
target: PANCAKE_V2_ROUTER_ADDRESS,
|
|
656
|
-
allowFailure: true,
|
|
657
|
-
callData: v2RouterIface.encodeFunctionData('getAmountsOut', [amount, params.v2Path])
|
|
658
|
-
}));
|
|
659
|
-
const results = await multicall.aggregate3.staticCall(calls);
|
|
660
|
-
const quotedOutputs = results.map((r) => {
|
|
661
|
-
if (r.success && r.returnData && r.returnData !== '0x') {
|
|
662
|
-
try {
|
|
663
|
-
const decoded = v2RouterIface.decodeFunctionResult('getAmountsOut', r.returnData);
|
|
664
|
-
const amounts = decoded[0];
|
|
665
|
-
return amounts[amounts.length - 1];
|
|
666
|
-
}
|
|
667
|
-
catch {
|
|
668
|
-
return 0n;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
return 0n;
|
|
672
|
-
});
|
|
673
|
-
return { quotedOutputs, minOuts };
|
|
674
|
-
}
|
|
675
|
-
catch {
|
|
676
|
-
// Multicall 失败,回退到并行调用
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
// 回退:并行调用(V3 路由或 Multicall 失败时)
|
|
680
|
-
const quotedOutputs = await Promise.all(amountsWei.map(amount => getSingleQuote(params, provider, tokenAddress, routeType, amount)));
|
|
681
|
-
return { quotedOutputs, minOuts };
|
|
682
|
-
}
|
|
683
|
-
/**
|
|
684
|
-
* 获取单个报价
|
|
685
|
-
*/
|
|
686
|
-
async function getSingleQuote(params, provider, tokenAddress, routeType, amount) {
|
|
687
|
-
try {
|
|
688
|
-
if (routeType === 'v2') {
|
|
689
|
-
if (!params.v2Path || params.v2Path.length < 2) {
|
|
690
|
-
throw new Error('v2Path is required for V2 routing');
|
|
691
|
-
}
|
|
692
|
-
const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
|
|
693
|
-
const amounts = await v2Router.getAmountsOut(amount, params.v2Path);
|
|
694
|
-
return amounts[amounts.length - 1];
|
|
695
|
-
}
|
|
696
|
-
if (routeType === 'v3-single') {
|
|
697
|
-
if (!params.v3TokenOut || !params.v3Fee) {
|
|
698
|
-
throw new Error('v3TokenOut and v3Fee are required for V3 single-hop');
|
|
699
|
-
}
|
|
700
|
-
const quoter = new Contract(PANCAKE_V3_QUOTER_ADDRESS, PANCAKE_V3_QUOTER_ABI, provider);
|
|
701
|
-
const result = await quoter.quoteExactInputSingle.staticCall({
|
|
702
|
-
tokenIn: tokenAddress,
|
|
703
|
-
tokenOut: params.v3TokenOut,
|
|
704
|
-
amountIn: amount,
|
|
705
|
-
fee: params.v3Fee,
|
|
706
|
-
sqrtPriceLimitX96: 0
|
|
707
|
-
});
|
|
708
|
-
return result[0];
|
|
709
|
-
}
|
|
710
|
-
if (routeType === 'v3-multi') {
|
|
711
|
-
if (!params.v3LpAddresses || params.v3LpAddresses.length === 0 || !params.v3ExactTokenIn) {
|
|
712
|
-
throw new Error('v3LpAddresses and v3ExactTokenIn are required for V3 multi-hop');
|
|
713
|
-
}
|
|
714
|
-
if (params.v2Path && params.v2Path.length >= 2) {
|
|
715
|
-
const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, provider);
|
|
716
|
-
const amounts = await v2Router.getAmountsOut(amount, params.v2Path);
|
|
717
|
-
return amounts[amounts.length - 1];
|
|
718
|
-
}
|
|
719
|
-
return 0n;
|
|
720
|
-
}
|
|
721
|
-
throw new Error(`Unsupported routeType: ${routeType}`);
|
|
722
|
-
}
|
|
723
|
-
catch {
|
|
724
|
-
return 0n;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
async function buildSellTransactions({ routeType, params, proxies, wallets, tokenAddress, amountsWei, minOuts }) {
|
|
728
|
-
const needBNB = false;
|
|
729
|
-
if (routeType === 'v2') {
|
|
730
|
-
if (!params.v2Path || params.v2Path.length < 2) {
|
|
731
|
-
throw new Error('v2Path is required for V2 routing');
|
|
732
|
-
}
|
|
733
|
-
return await buildV2Transactions(proxies, wallets, amountsWei, minOuts, params.v2Path, false, needBNB);
|
|
734
|
-
}
|
|
735
|
-
if (routeType === 'v3-single') {
|
|
736
|
-
if (!params.v3TokenOut || !params.v3Fee) {
|
|
737
|
-
throw new Error('v3TokenOut and v3Fee are required for V3 single-hop');
|
|
738
|
-
}
|
|
739
|
-
return await buildV3SingleTransactions(proxies, wallets, tokenAddress, params.v3TokenOut, params.v3Fee, amountsWei, minOuts, false, needBNB);
|
|
740
|
-
}
|
|
741
|
-
if (routeType === 'v3-multi') {
|
|
742
|
-
if (!params.v3LpAddresses || params.v3LpAddresses.length === 0 || !params.v3ExactTokenIn) {
|
|
743
|
-
throw new Error('v3LpAddresses and v3ExactTokenIn are required for V3 multi-hop');
|
|
744
|
-
}
|
|
745
|
-
return await buildV3MultiHopTransactions(proxies, wallets, params.v3LpAddresses, params.v3ExactTokenIn, amountsWei, minOuts, false, needBNB);
|
|
746
|
-
}
|
|
747
|
-
throw new Error(`Unsupported routeType: ${routeType}`);
|
|
748
|
-
}
|
|
749
|
-
// ✅ 贿赂交易和利润交易已内联到各函数中,确保正确的顺序:贿赂 → 主交易 → 利润
|