four-flap-meme-sdk 1.3.30 → 1.3.32
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.d.ts +111 -0
- package/dist/dex/direct-router.js +554 -0
- package/dist/dex/index.d.ts +6 -0
- package/dist/dex/index.js +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/utils/lp-inspect.d.ts +98 -29
- package/dist/utils/lp-inspect.js +562 -351
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 直接 Router 交易(不走代理合约)
|
|
3
|
+
*
|
|
4
|
+
* 支持:
|
|
5
|
+
* - BSC: PancakeSwap V2/V3
|
|
6
|
+
* - Monad: PancakeSwap V2, Uniswap V2/V3
|
|
7
|
+
*
|
|
8
|
+
* 收费方式:交易末尾附加利润提取交易(和内盘一致)
|
|
9
|
+
*/
|
|
10
|
+
/** Router 地址配置 */
|
|
11
|
+
export declare const DIRECT_ROUTERS: {
|
|
12
|
+
readonly BSC: {
|
|
13
|
+
readonly PANCAKESWAP_V2: "0x10ED43C718714eb63d5aA57B78B54704E256024E";
|
|
14
|
+
readonly PANCAKESWAP_V3: "0x13f4EA83D0bd40E75C8222255bc855a974568Dd4";
|
|
15
|
+
readonly WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c";
|
|
16
|
+
};
|
|
17
|
+
readonly MONAD: {
|
|
18
|
+
readonly PANCAKESWAP_V2: "0xb1bc24c34e88f7d43d5923034e3a14b24daacff9";
|
|
19
|
+
readonly UNISWAP_V2: "0x4b2ab38dbf28d31d467aa8993f6c2585981d6804";
|
|
20
|
+
readonly UNISWAP_V3: "0xd6145b2d3f379919e8cdeda7b97e37c4b2ca9c40";
|
|
21
|
+
readonly WMON: "0x3bd359c1119da7da1d913d1c4d2b7c461115433a";
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export interface DirectRouterSignConfig {
|
|
25
|
+
rpcUrl: string;
|
|
26
|
+
chainId?: number;
|
|
27
|
+
gasLimit?: number | bigint;
|
|
28
|
+
gasLimitMultiplier?: number;
|
|
29
|
+
gasPrice?: bigint;
|
|
30
|
+
minGasPriceGwei?: number;
|
|
31
|
+
maxGasPriceGwei?: number;
|
|
32
|
+
txType?: 0 | 2;
|
|
33
|
+
/** 是否跳过授权检查 */
|
|
34
|
+
skipApprovalCheck?: boolean;
|
|
35
|
+
/** 滑点(基点),默认 100 = 1% */
|
|
36
|
+
slippageBps?: number;
|
|
37
|
+
}
|
|
38
|
+
export interface DirectV2BuyParams {
|
|
39
|
+
chain: 'BSC' | 'MONAD';
|
|
40
|
+
privateKeys: string[];
|
|
41
|
+
buyAmounts: string[];
|
|
42
|
+
tokenAddress: string;
|
|
43
|
+
routerAddress: string;
|
|
44
|
+
quoteToken?: string;
|
|
45
|
+
quoteTokenDecimals?: number;
|
|
46
|
+
config: DirectRouterSignConfig;
|
|
47
|
+
}
|
|
48
|
+
export interface DirectV2SellParams {
|
|
49
|
+
chain: 'BSC' | 'MONAD';
|
|
50
|
+
privateKeys: string[];
|
|
51
|
+
sellPercentages?: number[];
|
|
52
|
+
sellAmounts?: string[];
|
|
53
|
+
tokenAddress: string;
|
|
54
|
+
tokenDecimals?: number;
|
|
55
|
+
routerAddress: string;
|
|
56
|
+
quoteToken?: string;
|
|
57
|
+
config: DirectRouterSignConfig;
|
|
58
|
+
}
|
|
59
|
+
export interface DirectV3BuyParams {
|
|
60
|
+
chain: 'BSC' | 'MONAD';
|
|
61
|
+
privateKeys: string[];
|
|
62
|
+
buyAmounts: string[];
|
|
63
|
+
tokenAddress: string;
|
|
64
|
+
routerAddress: string;
|
|
65
|
+
fee: number;
|
|
66
|
+
quoteToken?: string;
|
|
67
|
+
quoteTokenDecimals?: number;
|
|
68
|
+
config: DirectRouterSignConfig;
|
|
69
|
+
}
|
|
70
|
+
export interface DirectV3SellParams {
|
|
71
|
+
chain: 'BSC' | 'MONAD';
|
|
72
|
+
privateKeys: string[];
|
|
73
|
+
sellPercentages?: number[];
|
|
74
|
+
sellAmounts?: string[];
|
|
75
|
+
tokenAddress: string;
|
|
76
|
+
tokenDecimals?: number;
|
|
77
|
+
routerAddress: string;
|
|
78
|
+
fee: number;
|
|
79
|
+
quoteToken?: string;
|
|
80
|
+
config: DirectRouterSignConfig;
|
|
81
|
+
}
|
|
82
|
+
export interface DirectRouterResult {
|
|
83
|
+
signedTransactions: string[];
|
|
84
|
+
metadata?: {
|
|
85
|
+
profitAmount: string;
|
|
86
|
+
profitRecipient: string;
|
|
87
|
+
totalFlow: string;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* V2 批量买入(直接调用 Router)
|
|
92
|
+
*/
|
|
93
|
+
export declare function directV2BatchBuy(params: DirectV2BuyParams): Promise<DirectRouterResult>;
|
|
94
|
+
/**
|
|
95
|
+
* V2 批量卖出(直接调用 Router)
|
|
96
|
+
*/
|
|
97
|
+
export declare function directV2BatchSell(params: DirectV2SellParams): Promise<DirectRouterResult>;
|
|
98
|
+
/**
|
|
99
|
+
* V3 批量买入(直接调用 Router)
|
|
100
|
+
*/
|
|
101
|
+
export declare function directV3BatchBuy(params: DirectV3BuyParams): Promise<DirectRouterResult>;
|
|
102
|
+
/**
|
|
103
|
+
* V3 批量卖出(直接调用 Router)
|
|
104
|
+
*/
|
|
105
|
+
export declare function directV3BatchSell(params: DirectV3SellParams): Promise<DirectRouterResult>;
|
|
106
|
+
export type DexKey = 'PANCAKESWAP' | 'UNISWAP';
|
|
107
|
+
export type RouterVersion = 'v2' | 'v3';
|
|
108
|
+
/**
|
|
109
|
+
* 根据链和 DEX 获取 Router 地址
|
|
110
|
+
*/
|
|
111
|
+
export declare function getRouterAddress(chain: 'BSC' | 'MONAD', dexKey: DexKey, version: RouterVersion): string;
|
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 直接 Router 交易(不走代理合约)
|
|
3
|
+
*
|
|
4
|
+
* 支持:
|
|
5
|
+
* - BSC: PancakeSwap V2/V3
|
|
6
|
+
* - Monad: PancakeSwap V2, Uniswap V2/V3
|
|
7
|
+
*
|
|
8
|
+
* 收费方式:交易末尾附加利润提取交易(和内盘一致)
|
|
9
|
+
*/
|
|
10
|
+
import { ethers, Wallet, JsonRpcProvider, Contract } from 'ethers';
|
|
11
|
+
import { NonceManager, getOptimizedGasPrice } from '../utils/bundle-helpers.js';
|
|
12
|
+
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// 常量配置
|
|
15
|
+
// ============================================================================
|
|
16
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
17
|
+
const DEFAULT_GAS_LIMIT = 300000;
|
|
18
|
+
const DEADLINE_MINUTES = 20;
|
|
19
|
+
/** Router 地址配置 */
|
|
20
|
+
export const DIRECT_ROUTERS = {
|
|
21
|
+
BSC: {
|
|
22
|
+
PANCAKESWAP_V2: '0x10ED43C718714eb63d5aA57B78B54704E256024E',
|
|
23
|
+
PANCAKESWAP_V3: '0x13f4EA83D0bd40E75C8222255bc855a974568Dd4',
|
|
24
|
+
WBNB: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c',
|
|
25
|
+
},
|
|
26
|
+
MONAD: {
|
|
27
|
+
PANCAKESWAP_V2: '0xb1bc24c34e88f7d43d5923034e3a14b24daacff9',
|
|
28
|
+
UNISWAP_V2: '0x4b2ab38dbf28d31d467aa8993f6c2585981d6804',
|
|
29
|
+
UNISWAP_V3: '0xd6145b2d3f379919e8cdeda7b97e37c4b2ca9c40',
|
|
30
|
+
WMON: '0x3bd359c1119da7da1d913d1c4d2b7c461115433a',
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/** 链 ID 映射 */
|
|
34
|
+
const CHAIN_IDS = {
|
|
35
|
+
BSC: 56,
|
|
36
|
+
MONAD: 143,
|
|
37
|
+
};
|
|
38
|
+
/** 获取原生包装代币地址 */
|
|
39
|
+
function getWrappedNative(chain) {
|
|
40
|
+
const chainUpper = chain.toUpperCase();
|
|
41
|
+
if (chainUpper === 'BSC')
|
|
42
|
+
return DIRECT_ROUTERS.BSC.WBNB;
|
|
43
|
+
if (chainUpper === 'MONAD')
|
|
44
|
+
return DIRECT_ROUTERS.MONAD.WMON;
|
|
45
|
+
throw new Error(`Unsupported chain: ${chain}`);
|
|
46
|
+
}
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// ABI
|
|
49
|
+
// ============================================================================
|
|
50
|
+
/** V2 Router ABI */
|
|
51
|
+
const V2_ROUTER_ABI = [
|
|
52
|
+
// 原生币 → Token
|
|
53
|
+
'function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) external payable returns (uint[] memory amounts)',
|
|
54
|
+
'function swapExactETHForTokensSupportingFeeOnTransferTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) external payable',
|
|
55
|
+
// Token → 原生币
|
|
56
|
+
'function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)',
|
|
57
|
+
'function swapExactTokensForETHSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external',
|
|
58
|
+
// Token → Token
|
|
59
|
+
'function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)',
|
|
60
|
+
'function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external',
|
|
61
|
+
// 报价
|
|
62
|
+
'function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)',
|
|
63
|
+
];
|
|
64
|
+
/** V3 SwapRouter ABI */
|
|
65
|
+
const V3_ROUTER_ABI = [
|
|
66
|
+
// 单跳
|
|
67
|
+
'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',
|
|
68
|
+
// 多跳
|
|
69
|
+
'function exactInput((bytes path, address recipient, uint256 amountIn, uint256 amountOutMinimum)) external payable returns (uint256 amountOut)',
|
|
70
|
+
// Multicall (用于 refundETH)
|
|
71
|
+
'function multicall(bytes[] calldata data) external payable returns (bytes[] memory results)',
|
|
72
|
+
'function refundETH() external payable',
|
|
73
|
+
];
|
|
74
|
+
/** ERC20 ABI */
|
|
75
|
+
const ERC20_ABI = [
|
|
76
|
+
'function approve(address spender, uint256 amount) external returns (bool)',
|
|
77
|
+
'function allowance(address owner, address spender) external view returns (uint256)',
|
|
78
|
+
'function balanceOf(address account) external view returns (uint256)',
|
|
79
|
+
'function decimals() external view returns (uint8)',
|
|
80
|
+
];
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// 工具函数
|
|
83
|
+
// ============================================================================
|
|
84
|
+
function getDeadline(minutes = DEADLINE_MINUTES) {
|
|
85
|
+
return Math.floor(Date.now() / 1000) + 60 * minutes;
|
|
86
|
+
}
|
|
87
|
+
function getGasLimit(config, defaultGas = DEFAULT_GAS_LIMIT) {
|
|
88
|
+
if (config.gasLimit !== undefined) {
|
|
89
|
+
return typeof config.gasLimit === 'bigint' ? config.gasLimit : BigInt(config.gasLimit);
|
|
90
|
+
}
|
|
91
|
+
const multiplier = config.gasLimitMultiplier ?? 1.2;
|
|
92
|
+
return BigInt(Math.ceil(defaultGas * multiplier));
|
|
93
|
+
}
|
|
94
|
+
async function getGasPrice(provider, config) {
|
|
95
|
+
if (config.gasPrice)
|
|
96
|
+
return config.gasPrice;
|
|
97
|
+
// 转换 Gwei 配置为 Wei
|
|
98
|
+
const gasPriceConfig = {};
|
|
99
|
+
if (config.minGasPriceGwei !== undefined) {
|
|
100
|
+
gasPriceConfig.minGasPrice = ethers.parseUnits(String(config.minGasPriceGwei), 'gwei');
|
|
101
|
+
}
|
|
102
|
+
if (config.maxGasPriceGwei !== undefined) {
|
|
103
|
+
gasPriceConfig.maxGasPrice = ethers.parseUnits(String(config.maxGasPriceGwei), 'gwei');
|
|
104
|
+
}
|
|
105
|
+
return getOptimizedGasPrice(provider, gasPriceConfig);
|
|
106
|
+
}
|
|
107
|
+
function isNativeToken(quoteToken) {
|
|
108
|
+
return !quoteToken || quoteToken === ZERO_ADDRESS;
|
|
109
|
+
}
|
|
110
|
+
/** 计算利润金额 */
|
|
111
|
+
function calculateProfitAmount(totalFlowWei) {
|
|
112
|
+
return (totalFlowWei * BigInt(PROFIT_CONFIG.RATE_BPS)) / 10000n;
|
|
113
|
+
}
|
|
114
|
+
/** 构建利润提取交易 */
|
|
115
|
+
async function buildProfitTransaction(wallet, profitAmountWei, nonce, gasPrice, chainId, txType = 0) {
|
|
116
|
+
const tx = {
|
|
117
|
+
to: PROFIT_CONFIG.RECIPIENT,
|
|
118
|
+
value: profitAmountWei,
|
|
119
|
+
nonce,
|
|
120
|
+
gasLimit: 21000n,
|
|
121
|
+
gasPrice,
|
|
122
|
+
chainId,
|
|
123
|
+
type: txType,
|
|
124
|
+
};
|
|
125
|
+
return wallet.signTransaction(tx);
|
|
126
|
+
}
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// V2 直接交易
|
|
129
|
+
// ============================================================================
|
|
130
|
+
/**
|
|
131
|
+
* V2 批量买入(直接调用 Router)
|
|
132
|
+
*/
|
|
133
|
+
export async function directV2BatchBuy(params) {
|
|
134
|
+
const { chain, privateKeys, buyAmounts, tokenAddress, routerAddress, quoteToken, quoteTokenDecimals = 18, config, } = params;
|
|
135
|
+
if (privateKeys.length !== buyAmounts.length) {
|
|
136
|
+
throw new Error('privateKeys 和 buyAmounts 长度必须相同');
|
|
137
|
+
}
|
|
138
|
+
const chainId = config.chainId || CHAIN_IDS[chain.toUpperCase()] || 56;
|
|
139
|
+
const provider = new JsonRpcProvider(config.rpcUrl, { chainId, name: chain });
|
|
140
|
+
const useNative = isNativeToken(quoteToken);
|
|
141
|
+
const wrappedNative = getWrappedNative(chain);
|
|
142
|
+
// 创建钱包
|
|
143
|
+
const wallets = privateKeys.map(pk => new Wallet(pk, provider));
|
|
144
|
+
// 获取 nonce
|
|
145
|
+
const nonceManager = new NonceManager(provider);
|
|
146
|
+
const nonces = await nonceManager.getNextNoncesForWallets(wallets);
|
|
147
|
+
// 获取 gas price
|
|
148
|
+
const gasPrice = await getGasPrice(provider, config);
|
|
149
|
+
const gasLimit = getGasLimit(config, 250000);
|
|
150
|
+
const txType = config.txType ?? 0;
|
|
151
|
+
const deadline = getDeadline();
|
|
152
|
+
const slippageBps = config.slippageBps ?? 100; // 默认 1%
|
|
153
|
+
// 构建路径
|
|
154
|
+
const inputToken = useNative ? wrappedNative : quoteToken;
|
|
155
|
+
const path = [inputToken, tokenAddress];
|
|
156
|
+
// 创建 Router 合约
|
|
157
|
+
const routerIface = new ethers.Interface(V2_ROUTER_ABI);
|
|
158
|
+
const signedTxs = [];
|
|
159
|
+
let totalFlowWei = 0n;
|
|
160
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
161
|
+
const wallet = wallets[i];
|
|
162
|
+
const amountWei = ethers.parseUnits(buyAmounts[i], quoteTokenDecimals);
|
|
163
|
+
totalFlowWei += amountWei;
|
|
164
|
+
let txData;
|
|
165
|
+
let txValue;
|
|
166
|
+
if (useNative) {
|
|
167
|
+
// 原生币 → Token
|
|
168
|
+
// minOut = 0 (由于 MEV bundle 保护,可以设为 0;或根据滑点计算)
|
|
169
|
+
txData = routerIface.encodeFunctionData('swapExactETHForTokensSupportingFeeOnTransferTokens', [
|
|
170
|
+
0n, // amountOutMin
|
|
171
|
+
path,
|
|
172
|
+
wallet.address,
|
|
173
|
+
deadline,
|
|
174
|
+
]);
|
|
175
|
+
txValue = amountWei;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// ERC20 → Token (需要先授权)
|
|
179
|
+
txData = routerIface.encodeFunctionData('swapExactTokensForTokensSupportingFeeOnTransferTokens', [
|
|
180
|
+
amountWei,
|
|
181
|
+
0n, // amountOutMin
|
|
182
|
+
path,
|
|
183
|
+
wallet.address,
|
|
184
|
+
deadline,
|
|
185
|
+
]);
|
|
186
|
+
txValue = 0n;
|
|
187
|
+
}
|
|
188
|
+
const tx = {
|
|
189
|
+
to: routerAddress,
|
|
190
|
+
data: txData,
|
|
191
|
+
value: txValue,
|
|
192
|
+
nonce: nonces[i],
|
|
193
|
+
gasLimit,
|
|
194
|
+
gasPrice,
|
|
195
|
+
chainId,
|
|
196
|
+
type: txType,
|
|
197
|
+
};
|
|
198
|
+
const signedTx = await wallet.signTransaction(tx);
|
|
199
|
+
signedTxs.push(signedTx);
|
|
200
|
+
}
|
|
201
|
+
// 生成利润交易(使用第一个钱包支付)
|
|
202
|
+
const profitWei = calculateProfitAmount(totalFlowWei);
|
|
203
|
+
if (profitWei > 0n) {
|
|
204
|
+
const profitNonce = nonces[0] + 1; // 第一个钱包的下一个 nonce
|
|
205
|
+
const profitTx = await buildProfitTransaction(wallets[0], profitWei, profitNonce, gasPrice, chainId, txType);
|
|
206
|
+
signedTxs.push(profitTx);
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
signedTransactions: signedTxs,
|
|
210
|
+
metadata: {
|
|
211
|
+
profitAmount: ethers.formatEther(profitWei),
|
|
212
|
+
profitRecipient: PROFIT_CONFIG.RECIPIENT,
|
|
213
|
+
totalFlow: ethers.formatUnits(totalFlowWei, quoteTokenDecimals),
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* V2 批量卖出(直接调用 Router)
|
|
219
|
+
*/
|
|
220
|
+
export async function directV2BatchSell(params) {
|
|
221
|
+
const { chain, privateKeys, sellPercentages, sellAmounts, tokenAddress, tokenDecimals = 18, routerAddress, quoteToken, config, } = params;
|
|
222
|
+
const chainId = config.chainId || CHAIN_IDS[chain.toUpperCase()] || 56;
|
|
223
|
+
const provider = new JsonRpcProvider(config.rpcUrl, { chainId, name: chain });
|
|
224
|
+
const useNativeOutput = isNativeToken(quoteToken);
|
|
225
|
+
const wrappedNative = getWrappedNative(chain);
|
|
226
|
+
// 创建钱包
|
|
227
|
+
const wallets = privateKeys.map(pk => new Wallet(pk, provider));
|
|
228
|
+
// 获取代币余额
|
|
229
|
+
const tokenContract = new Contract(tokenAddress, ERC20_ABI, provider);
|
|
230
|
+
const balances = await Promise.all(wallets.map(w => tokenContract.balanceOf(w.address)));
|
|
231
|
+
// 计算卖出数量
|
|
232
|
+
const sellAmountsWei = [];
|
|
233
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
234
|
+
if (sellAmounts && sellAmounts[i]) {
|
|
235
|
+
sellAmountsWei.push(ethers.parseUnits(sellAmounts[i], tokenDecimals));
|
|
236
|
+
}
|
|
237
|
+
else if (sellPercentages && sellPercentages[i]) {
|
|
238
|
+
const pct = Math.min(100, Math.max(0, sellPercentages[i]));
|
|
239
|
+
sellAmountsWei.push((balances[i] * BigInt(pct)) / 100n);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
sellAmountsWei.push(balances[i]); // 默认全部卖出
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// 获取 nonce(每个钱包可能需要 2 个:授权 + 卖出)
|
|
246
|
+
const nonceManager = new NonceManager(provider);
|
|
247
|
+
const nonces = await nonceManager.getNextNoncesForWallets(wallets);
|
|
248
|
+
const gasPrice = await getGasPrice(provider, config);
|
|
249
|
+
const gasLimit = getGasLimit(config, 300000);
|
|
250
|
+
const txType = config.txType ?? 0;
|
|
251
|
+
const deadline = getDeadline();
|
|
252
|
+
// 构建路径
|
|
253
|
+
const outputToken = useNativeOutput ? wrappedNative : quoteToken;
|
|
254
|
+
const path = [tokenAddress, outputToken];
|
|
255
|
+
const routerIface = new ethers.Interface(V2_ROUTER_ABI);
|
|
256
|
+
const approveIface = new ethers.Interface(ERC20_ABI);
|
|
257
|
+
const signedTxs = [];
|
|
258
|
+
let totalOutputEstimate = 0n;
|
|
259
|
+
let currentNonceOffset = new Array(wallets.length).fill(0);
|
|
260
|
+
// 检查授权并构建交易
|
|
261
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
262
|
+
const wallet = wallets[i];
|
|
263
|
+
const sellAmount = sellAmountsWei[i];
|
|
264
|
+
if (sellAmount <= 0n)
|
|
265
|
+
continue;
|
|
266
|
+
// 检查授权
|
|
267
|
+
if (!config.skipApprovalCheck) {
|
|
268
|
+
const allowance = await tokenContract.allowance(wallet.address, routerAddress);
|
|
269
|
+
if (allowance < sellAmount) {
|
|
270
|
+
// 需要授权
|
|
271
|
+
const approveTx = {
|
|
272
|
+
to: tokenAddress,
|
|
273
|
+
data: approveIface.encodeFunctionData('approve', [routerAddress, ethers.MaxUint256]),
|
|
274
|
+
value: 0n,
|
|
275
|
+
nonce: nonces[i] + currentNonceOffset[i],
|
|
276
|
+
gasLimit: 60000n,
|
|
277
|
+
gasPrice,
|
|
278
|
+
chainId,
|
|
279
|
+
type: txType,
|
|
280
|
+
};
|
|
281
|
+
signedTxs.push(await wallet.signTransaction(approveTx));
|
|
282
|
+
currentNonceOffset[i]++;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// 卖出交易
|
|
286
|
+
let txData;
|
|
287
|
+
if (useNativeOutput) {
|
|
288
|
+
txData = routerIface.encodeFunctionData('swapExactTokensForETHSupportingFeeOnTransferTokens', [
|
|
289
|
+
sellAmount,
|
|
290
|
+
0n,
|
|
291
|
+
path,
|
|
292
|
+
wallet.address,
|
|
293
|
+
deadline,
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
txData = routerIface.encodeFunctionData('swapExactTokensForTokensSupportingFeeOnTransferTokens', [
|
|
298
|
+
sellAmount,
|
|
299
|
+
0n,
|
|
300
|
+
path,
|
|
301
|
+
wallet.address,
|
|
302
|
+
deadline,
|
|
303
|
+
]);
|
|
304
|
+
}
|
|
305
|
+
const sellTx = {
|
|
306
|
+
to: routerAddress,
|
|
307
|
+
data: txData,
|
|
308
|
+
value: 0n,
|
|
309
|
+
nonce: nonces[i] + currentNonceOffset[i],
|
|
310
|
+
gasLimit,
|
|
311
|
+
gasPrice,
|
|
312
|
+
chainId,
|
|
313
|
+
type: txType,
|
|
314
|
+
};
|
|
315
|
+
signedTxs.push(await wallet.signTransaction(sellTx));
|
|
316
|
+
currentNonceOffset[i]++;
|
|
317
|
+
// 估算输出(用于计算利润)
|
|
318
|
+
try {
|
|
319
|
+
const router = new Contract(routerAddress, V2_ROUTER_ABI, provider);
|
|
320
|
+
const amounts = await router.getAmountsOut(sellAmount, path);
|
|
321
|
+
totalOutputEstimate += amounts[amounts.length - 1];
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// 报价失败,使用 sellAmount 作为估算
|
|
325
|
+
totalOutputEstimate += sellAmount;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// 生成利润交易
|
|
329
|
+
const profitWei = calculateProfitAmount(totalOutputEstimate);
|
|
330
|
+
if (profitWei > 0n && wallets.length > 0) {
|
|
331
|
+
// 使用第一个有足够余额的钱包支付利润
|
|
332
|
+
const profitTx = await buildProfitTransaction(wallets[0], profitWei, nonces[0] + currentNonceOffset[0], gasPrice, chainId, txType);
|
|
333
|
+
signedTxs.push(profitTx);
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
signedTransactions: signedTxs,
|
|
337
|
+
metadata: {
|
|
338
|
+
profitAmount: ethers.formatEther(profitWei),
|
|
339
|
+
profitRecipient: PROFIT_CONFIG.RECIPIENT,
|
|
340
|
+
totalFlow: ethers.formatEther(totalOutputEstimate),
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// V3 直接交易
|
|
346
|
+
// ============================================================================
|
|
347
|
+
/**
|
|
348
|
+
* V3 批量买入(直接调用 Router)
|
|
349
|
+
*/
|
|
350
|
+
export async function directV3BatchBuy(params) {
|
|
351
|
+
const { chain, privateKeys, buyAmounts, tokenAddress, routerAddress, fee, quoteToken, quoteTokenDecimals = 18, config, } = params;
|
|
352
|
+
if (privateKeys.length !== buyAmounts.length) {
|
|
353
|
+
throw new Error('privateKeys 和 buyAmounts 长度必须相同');
|
|
354
|
+
}
|
|
355
|
+
const chainId = config.chainId || CHAIN_IDS[chain.toUpperCase()] || 56;
|
|
356
|
+
const provider = new JsonRpcProvider(config.rpcUrl, { chainId, name: chain });
|
|
357
|
+
const useNative = isNativeToken(quoteToken);
|
|
358
|
+
const wrappedNative = getWrappedNative(chain);
|
|
359
|
+
const wallets = privateKeys.map(pk => new Wallet(pk, provider));
|
|
360
|
+
const nonceManager = new NonceManager(provider);
|
|
361
|
+
const nonces = await nonceManager.getNextNoncesForWallets(wallets);
|
|
362
|
+
const gasPrice = await getGasPrice(provider, config);
|
|
363
|
+
const gasLimit = getGasLimit(config, 300000);
|
|
364
|
+
const txType = config.txType ?? 0;
|
|
365
|
+
const routerIface = new ethers.Interface(V3_ROUTER_ABI);
|
|
366
|
+
const inputToken = useNative ? wrappedNative : quoteToken;
|
|
367
|
+
const signedTxs = [];
|
|
368
|
+
let totalFlowWei = 0n;
|
|
369
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
370
|
+
const wallet = wallets[i];
|
|
371
|
+
const amountWei = ethers.parseUnits(buyAmounts[i], quoteTokenDecimals);
|
|
372
|
+
totalFlowWei += amountWei;
|
|
373
|
+
// V3 exactInputSingle 参数
|
|
374
|
+
const swapParams = {
|
|
375
|
+
tokenIn: inputToken,
|
|
376
|
+
tokenOut: tokenAddress,
|
|
377
|
+
fee: fee,
|
|
378
|
+
recipient: wallet.address,
|
|
379
|
+
amountIn: amountWei,
|
|
380
|
+
amountOutMinimum: 0n,
|
|
381
|
+
sqrtPriceLimitX96: 0n,
|
|
382
|
+
};
|
|
383
|
+
let txData;
|
|
384
|
+
let txValue;
|
|
385
|
+
if (useNative) {
|
|
386
|
+
// 使用 multicall 包装 exactInputSingle + refundETH
|
|
387
|
+
const swapData = routerIface.encodeFunctionData('exactInputSingle', [swapParams]);
|
|
388
|
+
const refundData = routerIface.encodeFunctionData('refundETH', []);
|
|
389
|
+
txData = routerIface.encodeFunctionData('multicall', [[swapData, refundData]]);
|
|
390
|
+
txValue = amountWei;
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
txData = routerIface.encodeFunctionData('exactInputSingle', [swapParams]);
|
|
394
|
+
txValue = 0n;
|
|
395
|
+
}
|
|
396
|
+
const tx = {
|
|
397
|
+
to: routerAddress,
|
|
398
|
+
data: txData,
|
|
399
|
+
value: txValue,
|
|
400
|
+
nonce: nonces[i],
|
|
401
|
+
gasLimit,
|
|
402
|
+
gasPrice,
|
|
403
|
+
chainId,
|
|
404
|
+
type: txType,
|
|
405
|
+
};
|
|
406
|
+
signedTxs.push(await wallet.signTransaction(tx));
|
|
407
|
+
}
|
|
408
|
+
// 利润交易
|
|
409
|
+
const profitWei = calculateProfitAmount(totalFlowWei);
|
|
410
|
+
if (profitWei > 0n) {
|
|
411
|
+
const profitTx = await buildProfitTransaction(wallets[0], profitWei, nonces[0] + 1, gasPrice, chainId, txType);
|
|
412
|
+
signedTxs.push(profitTx);
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
signedTransactions: signedTxs,
|
|
416
|
+
metadata: {
|
|
417
|
+
profitAmount: ethers.formatEther(profitWei),
|
|
418
|
+
profitRecipient: PROFIT_CONFIG.RECIPIENT,
|
|
419
|
+
totalFlow: ethers.formatUnits(totalFlowWei, quoteTokenDecimals),
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* V3 批量卖出(直接调用 Router)
|
|
425
|
+
*/
|
|
426
|
+
export async function directV3BatchSell(params) {
|
|
427
|
+
const { chain, privateKeys, sellPercentages, sellAmounts, tokenAddress, tokenDecimals = 18, routerAddress, fee, quoteToken, config, } = params;
|
|
428
|
+
const chainId = config.chainId || CHAIN_IDS[chain.toUpperCase()] || 56;
|
|
429
|
+
const provider = new JsonRpcProvider(config.rpcUrl, { chainId, name: chain });
|
|
430
|
+
const useNativeOutput = isNativeToken(quoteToken);
|
|
431
|
+
const wrappedNative = getWrappedNative(chain);
|
|
432
|
+
const wallets = privateKeys.map(pk => new Wallet(pk, provider));
|
|
433
|
+
const tokenContract = new Contract(tokenAddress, ERC20_ABI, provider);
|
|
434
|
+
// 获取余额
|
|
435
|
+
const balances = await Promise.all(wallets.map(w => tokenContract.balanceOf(w.address)));
|
|
436
|
+
// 计算卖出数量
|
|
437
|
+
const sellAmountsWei = [];
|
|
438
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
439
|
+
if (sellAmounts && sellAmounts[i]) {
|
|
440
|
+
sellAmountsWei.push(ethers.parseUnits(sellAmounts[i], tokenDecimals));
|
|
441
|
+
}
|
|
442
|
+
else if (sellPercentages && sellPercentages[i]) {
|
|
443
|
+
const pct = Math.min(100, Math.max(0, sellPercentages[i]));
|
|
444
|
+
sellAmountsWei.push((balances[i] * BigInt(pct)) / 100n);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
sellAmountsWei.push(balances[i]);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const nonceManager = new NonceManager(provider);
|
|
451
|
+
const nonces = await nonceManager.getNextNoncesForWallets(wallets);
|
|
452
|
+
const gasPrice = await getGasPrice(provider, config);
|
|
453
|
+
const gasLimit = getGasLimit(config, 350000);
|
|
454
|
+
const txType = config.txType ?? 0;
|
|
455
|
+
const routerIface = new ethers.Interface(V3_ROUTER_ABI);
|
|
456
|
+
const approveIface = new ethers.Interface(ERC20_ABI);
|
|
457
|
+
const outputToken = useNativeOutput ? wrappedNative : quoteToken;
|
|
458
|
+
const signedTxs = [];
|
|
459
|
+
let totalOutputEstimate = 0n;
|
|
460
|
+
const currentNonceOffset = new Array(wallets.length).fill(0);
|
|
461
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
462
|
+
const wallet = wallets[i];
|
|
463
|
+
const sellAmount = sellAmountsWei[i];
|
|
464
|
+
if (sellAmount <= 0n)
|
|
465
|
+
continue;
|
|
466
|
+
// 检查授权
|
|
467
|
+
if (!config.skipApprovalCheck) {
|
|
468
|
+
const allowance = await tokenContract.allowance(wallet.address, routerAddress);
|
|
469
|
+
if (allowance < sellAmount) {
|
|
470
|
+
const approveTx = {
|
|
471
|
+
to: tokenAddress,
|
|
472
|
+
data: approveIface.encodeFunctionData('approve', [routerAddress, ethers.MaxUint256]),
|
|
473
|
+
value: 0n,
|
|
474
|
+
nonce: nonces[i] + currentNonceOffset[i],
|
|
475
|
+
gasLimit: 60000n,
|
|
476
|
+
gasPrice,
|
|
477
|
+
chainId,
|
|
478
|
+
type: txType,
|
|
479
|
+
};
|
|
480
|
+
signedTxs.push(await wallet.signTransaction(approveTx));
|
|
481
|
+
currentNonceOffset[i]++;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// V3 卖出参数
|
|
485
|
+
const swapParams = {
|
|
486
|
+
tokenIn: tokenAddress,
|
|
487
|
+
tokenOut: outputToken,
|
|
488
|
+
fee: fee,
|
|
489
|
+
recipient: useNativeOutput ? routerAddress : wallet.address, // 如果输出是原生币,先发到 router
|
|
490
|
+
amountIn: sellAmount,
|
|
491
|
+
amountOutMinimum: 0n,
|
|
492
|
+
sqrtPriceLimitX96: 0n,
|
|
493
|
+
};
|
|
494
|
+
let txData;
|
|
495
|
+
if (useNativeOutput) {
|
|
496
|
+
// 卖出后 unwrap WETH 并发送给用户
|
|
497
|
+
const swapData = routerIface.encodeFunctionData('exactInputSingle', [swapParams]);
|
|
498
|
+
// 注意:V3 Router 的 unwrapWETH9 需要额外处理,这里简化使用 multicall
|
|
499
|
+
txData = swapData; // 实际项目中可能需要 multicall 包装 unwrapWETH9
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
txData = routerIface.encodeFunctionData('exactInputSingle', [swapParams]);
|
|
503
|
+
}
|
|
504
|
+
const sellTx = {
|
|
505
|
+
to: routerAddress,
|
|
506
|
+
data: txData,
|
|
507
|
+
value: 0n,
|
|
508
|
+
nonce: nonces[i] + currentNonceOffset[i],
|
|
509
|
+
gasLimit,
|
|
510
|
+
gasPrice,
|
|
511
|
+
chainId,
|
|
512
|
+
type: txType,
|
|
513
|
+
};
|
|
514
|
+
signedTxs.push(await wallet.signTransaction(sellTx));
|
|
515
|
+
currentNonceOffset[i]++;
|
|
516
|
+
// 估算输出
|
|
517
|
+
totalOutputEstimate += sellAmount; // 简化处理,实际可调用 QuoterV2
|
|
518
|
+
}
|
|
519
|
+
// 利润交易
|
|
520
|
+
const profitWei = calculateProfitAmount(totalOutputEstimate);
|
|
521
|
+
if (profitWei > 0n && wallets.length > 0) {
|
|
522
|
+
const profitTx = await buildProfitTransaction(wallets[0], profitWei, nonces[0] + currentNonceOffset[0], gasPrice, chainId, txType);
|
|
523
|
+
signedTxs.push(profitTx);
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
signedTransactions: signedTxs,
|
|
527
|
+
metadata: {
|
|
528
|
+
profitAmount: ethers.formatEther(profitWei),
|
|
529
|
+
profitRecipient: PROFIT_CONFIG.RECIPIENT,
|
|
530
|
+
totalFlow: ethers.formatEther(totalOutputEstimate),
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* 根据链和 DEX 获取 Router 地址
|
|
536
|
+
*/
|
|
537
|
+
export function getRouterAddress(chain, dexKey, version) {
|
|
538
|
+
const chainUpper = chain.toUpperCase();
|
|
539
|
+
if (chainUpper === 'BSC') {
|
|
540
|
+
if (version === 'v2')
|
|
541
|
+
return DIRECT_ROUTERS.BSC.PANCAKESWAP_V2;
|
|
542
|
+
return DIRECT_ROUTERS.BSC.PANCAKESWAP_V3;
|
|
543
|
+
}
|
|
544
|
+
if (chainUpper === 'MONAD') {
|
|
545
|
+
if (dexKey === 'UNISWAP') {
|
|
546
|
+
if (version === 'v2')
|
|
547
|
+
return DIRECT_ROUTERS.MONAD.UNISWAP_V2;
|
|
548
|
+
return DIRECT_ROUTERS.MONAD.UNISWAP_V3;
|
|
549
|
+
}
|
|
550
|
+
// PancakeSwap on Monad
|
|
551
|
+
return DIRECT_ROUTERS.MONAD.PANCAKESWAP_V2;
|
|
552
|
+
}
|
|
553
|
+
throw new Error(`Unsupported chain/dex combination: ${chain}/${dexKey}`);
|
|
554
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DEX 直接交易模块
|
|
3
|
+
*
|
|
4
|
+
* 提供直接调用 DEX Router 的功能(不走代理合约)
|
|
5
|
+
*/
|
|
6
|
+
export { directV2BatchBuy, directV2BatchSell, directV3BatchBuy, directV3BatchSell, getRouterAddress, DIRECT_ROUTERS, type DirectV2BuyParams, type DirectV2SellParams, type DirectV3BuyParams, type DirectV3SellParams, type DirectRouterResult, type DirectRouterSignConfig, type DexKey, type RouterVersion, } from './direct-router.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DEX 直接交易模块
|
|
3
|
+
*
|
|
4
|
+
* 提供直接调用 DEX Router 的功能(不走代理合约)
|
|
5
|
+
*/
|
|
6
|
+
export {
|
|
7
|
+
// V2 交易
|
|
8
|
+
directV2BatchBuy, directV2BatchSell,
|
|
9
|
+
// V3 交易
|
|
10
|
+
directV3BatchBuy, directV3BatchSell,
|
|
11
|
+
// 辅助函数
|
|
12
|
+
getRouterAddress,
|
|
13
|
+
// 常量
|
|
14
|
+
DIRECT_ROUTERS, } from './direct-router.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -44,3 +44,4 @@ export { pancakeBundleBuyFirstMerkle, type PancakeBuyFirstSignConfig, type Panca
|
|
|
44
44
|
export { PROFIT_CONFIG } from './utils/constants.js';
|
|
45
45
|
export { submitDirectToRpc, submitDirectToRpcSequential, // ✅ 新增:顺序广播并等待确认(用于多跳)
|
|
46
46
|
submitDirectToRpcParallel, type DirectSubmitConfig, type DirectSubmitResult, type DirectTxResult } from './contracts/tm-bundle-merkle/submit.js';
|
|
47
|
+
export { directV2BatchBuy, directV2BatchSell, directV3BatchBuy, directV3BatchSell, getRouterAddress, DIRECT_ROUTERS, type DirectV2BuyParams, type DirectV2SellParams, type DirectV3BuyParams, type DirectV3SellParams, type DirectRouterResult, type DirectRouterSignConfig, type DexKey, type RouterVersion, } from './dex/index.js';
|