four-flap-meme-sdk 1.4.23 → 1.4.25
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.
|
@@ -236,6 +236,10 @@ export type DisperseMerkleParams = {
|
|
|
236
236
|
config: FourBundleMerkleConfig;
|
|
237
237
|
};
|
|
238
238
|
/** ✅ 分发参数(仅签名版本 - 精简) */
|
|
239
|
+
/** 代币池子类型 */
|
|
240
|
+
export type TokenPoolType = 'flap' | 'four' | 'v2' | 'v3';
|
|
241
|
+
/** 报价代币类型(池子的计价代币) */
|
|
242
|
+
export type QuoteTokenType = 'native' | 'usdt' | 'usdc';
|
|
239
243
|
export type DisperseSignParams = {
|
|
240
244
|
fromPrivateKey: string;
|
|
241
245
|
recipients: string[];
|
|
@@ -254,6 +258,10 @@ export type DisperseSignParams = {
|
|
|
254
258
|
config: FourSignConfig;
|
|
255
259
|
/** ✅ 新增:起始 nonce(用于并行调用时避免 nonce 冲突) */
|
|
256
260
|
startNonce?: number;
|
|
261
|
+
/** ✅ 新增:代币池子类型(用于利润报价),默认 'v2' */
|
|
262
|
+
tokenPoolType?: TokenPoolType;
|
|
263
|
+
/** ✅ 新增:报价代币类型(池子的计价代币),默认 'native'(BNB/MON) */
|
|
264
|
+
quoteToken?: QuoteTokenType;
|
|
257
265
|
};
|
|
258
266
|
/**
|
|
259
267
|
* ✅ 分发结果(简化版)
|
|
@@ -305,6 +313,10 @@ export type SweepSignParams = {
|
|
|
305
313
|
config: FourSignConfig;
|
|
306
314
|
/** ✅ 新增:起始 nonce(用于并行调用时避免 nonce 冲突,仅对第一个源钱包生效) */
|
|
307
315
|
startNonce?: number;
|
|
316
|
+
/** ✅ 新增:代币池子类型(用于利润报价),默认 'v2' */
|
|
317
|
+
tokenPoolType?: TokenPoolType;
|
|
318
|
+
/** ✅ 新增:报价代币类型(池子的计价代币),默认 'native'(BNB/MON) */
|
|
319
|
+
quoteToken?: QuoteTokenType;
|
|
308
320
|
};
|
|
309
321
|
/**
|
|
310
322
|
* ✅ 归集结果(简化版)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { ethers, Wallet
|
|
2
|
-
// import { MerkleClient } from '../../clients/merkle.js';
|
|
1
|
+
import { ethers, Wallet } from 'ethers';
|
|
3
2
|
import { getOptimizedGasPrice, NonceManager } from '../../utils/bundle-helpers.js';
|
|
4
3
|
import { PROFIT_CONFIG } from '../../utils/constants.js';
|
|
5
4
|
import { getTxType, getGasPriceConfig, shouldExtractProfit, getProfitRecipient } from './config.js';
|
|
@@ -15,46 +14,182 @@ function calculateProfit(amount) {
|
|
|
15
14
|
return { profit, remaining };
|
|
16
15
|
}
|
|
17
16
|
// ==================== ERC20 → 原生代币报价 ====================
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
'
|
|
26
|
-
|
|
17
|
+
import { quote, QUOTE_CONFIG } from '../../utils/quote-helpers.js';
|
|
18
|
+
import { Helper3 } from '../helper3.js';
|
|
19
|
+
import { FlapPortal } from '../../flap/portal.js';
|
|
20
|
+
/** 最低利润(Wei):无法获取报价时使用,0.0001 BNB */
|
|
21
|
+
const MIN_PROFIT_WEI = 100000000000000n; // 0.0001 BNB = 10^14 wei
|
|
22
|
+
/** 链 ID → 链名称 映射 */
|
|
23
|
+
const CHAIN_ID_TO_NAME = {
|
|
24
|
+
56: 'BSC',
|
|
25
|
+
143: 'MONAD',
|
|
26
|
+
196: 'XLAYER',
|
|
27
|
+
};
|
|
28
|
+
/** 各链的稳定币地址 */
|
|
29
|
+
const STABLE_COINS = {
|
|
30
|
+
BSC: {
|
|
31
|
+
usdt: '0x55d398326f99059fF775485246999027B3197955',
|
|
32
|
+
usdc: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d',
|
|
33
|
+
},
|
|
34
|
+
MONAD: {
|
|
35
|
+
// TODO: 添加 Monad 链的稳定币地址
|
|
36
|
+
},
|
|
37
|
+
XLAYER: {
|
|
38
|
+
usdt: '0x1E4a5963aBFD975d8c9021ce480b42188849D41d',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* 获取 FOUR 内盘代币 → BNB 的报价
|
|
43
|
+
*/
|
|
44
|
+
async function getFourInnerQuote(rpcUrl, tokenAddress, tokenAmount) {
|
|
45
|
+
try {
|
|
46
|
+
const helper = Helper3.connectByChain('BSC', rpcUrl);
|
|
47
|
+
const result = await helper.trySell(tokenAddress, tokenAmount);
|
|
48
|
+
// result = { tokenManager, quote, funds, fee }
|
|
49
|
+
// funds 是卖出代币能获得的 BNB 数量
|
|
50
|
+
const funds = result.funds;
|
|
51
|
+
console.log(`[getFourInnerQuote] FOUR 内盘报价成功: ${funds} wei`);
|
|
52
|
+
return funds;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.warn(`[getFourInnerQuote] FOUR 内盘报价失败:`, error);
|
|
56
|
+
return 0n;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** 零地址(原生代币) */
|
|
60
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
61
|
+
/**
|
|
62
|
+
* 获取 FLAP 内盘代币 → 原生代币的报价
|
|
63
|
+
* ✅ 与 portal-bundle-merkle/core.ts 使用相同的 quoteExactInput 方法
|
|
64
|
+
*/
|
|
65
|
+
async function getFlapInnerQuote(rpcUrl, chainId, tokenAddress, tokenAmount) {
|
|
66
|
+
try {
|
|
67
|
+
// 根据链 ID 确定链名称
|
|
68
|
+
const chainName = chainId === 56 ? 'BSC' : chainId === 143 ? 'MONAD' : chainId === 196 ? 'XLAYER' : null;
|
|
69
|
+
if (!chainName) {
|
|
70
|
+
console.warn(`[getFlapInnerQuote] 不支持的链 ID: ${chainId}`);
|
|
71
|
+
return 0n;
|
|
72
|
+
}
|
|
73
|
+
const portal = new FlapPortal({ chain: chainName, rpcUrl });
|
|
74
|
+
// ✅ 使用 quoteExactInput 与 core.ts 保持一致
|
|
75
|
+
const funds = await portal.quoteExactInput({
|
|
76
|
+
inputToken: tokenAddress,
|
|
77
|
+
outputToken: ZERO_ADDRESS, // 输出原生代币
|
|
78
|
+
inputAmount: tokenAmount
|
|
79
|
+
});
|
|
80
|
+
console.log(`[getFlapInnerQuote] FLAP 内盘报价成功: ${funds} wei`);
|
|
81
|
+
return funds;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.warn(`[getFlapInnerQuote] FLAP 内盘报价失败:`, error);
|
|
85
|
+
return 0n;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
27
88
|
/**
|
|
28
89
|
* 获取 ERC20 代币 → 原生代币的报价
|
|
90
|
+
*
|
|
29
91
|
* @param provider - Provider 实例
|
|
30
92
|
* @param tokenAddress - ERC20 代币地址
|
|
31
93
|
* @param tokenAmount - 代币数量(wei)
|
|
32
94
|
* @param chainId - 链 ID(56=BSC, 143=Monad)
|
|
33
|
-
* @
|
|
95
|
+
* @param poolType - 池子类型:'flap'/'four' 内盘,'v2'/'v3' 外盘
|
|
96
|
+
* @param quoteToken - 报价代币:'native'(BNB/MON),'usdt','usdc'
|
|
97
|
+
* @param rpcUrl - RPC URL(内盘报价需要)
|
|
98
|
+
* @returns 等值的原生代币数量(wei),失败返回 MIN_PROFIT_WEI
|
|
34
99
|
*/
|
|
35
|
-
async function getTokenToNativeQuote(provider, tokenAddress, tokenAmount, chainId) {
|
|
100
|
+
async function getTokenToNativeQuote(provider, tokenAddress, tokenAmount, chainId, poolType = 'v2', quoteToken = 'native', rpcUrl) {
|
|
36
101
|
if (tokenAmount <= 0n)
|
|
37
|
-
return
|
|
102
|
+
return MIN_PROFIT_WEI;
|
|
103
|
+
// ✅ FOUR 内盘:通过 Helper3.trySell 获取真实报价
|
|
104
|
+
if (poolType === 'four') {
|
|
105
|
+
const url = rpcUrl || provider._getConnection?.()?.url || '';
|
|
106
|
+
if (!url) {
|
|
107
|
+
console.warn(`[getTokenToNativeQuote] FOUR 内盘需要 rpcUrl,使用最低利润`);
|
|
108
|
+
return MIN_PROFIT_WEI;
|
|
109
|
+
}
|
|
110
|
+
const funds = await getFourInnerQuote(url, tokenAddress, tokenAmount);
|
|
111
|
+
return funds > 0n ? funds : MIN_PROFIT_WEI;
|
|
112
|
+
}
|
|
113
|
+
// ✅ FLAP 内盘:通过 FlapPortal.previewSell 获取真实报价
|
|
114
|
+
if (poolType === 'flap') {
|
|
115
|
+
const url = rpcUrl || provider._getConnection?.()?.url || '';
|
|
116
|
+
if (!url) {
|
|
117
|
+
console.warn(`[getTokenToNativeQuote] FLAP 内盘需要 rpcUrl,使用最低利润`);
|
|
118
|
+
return MIN_PROFIT_WEI;
|
|
119
|
+
}
|
|
120
|
+
const funds = await getFlapInnerQuote(url, chainId, tokenAddress, tokenAmount);
|
|
121
|
+
return funds > 0n ? funds : MIN_PROFIT_WEI;
|
|
122
|
+
}
|
|
123
|
+
const chainName = CHAIN_ID_TO_NAME[chainId];
|
|
124
|
+
if (!chainName) {
|
|
125
|
+
console.warn(`[getTokenToNativeQuote] 不支持的链 ID: ${chainId},使用最低利润`);
|
|
126
|
+
return MIN_PROFIT_WEI;
|
|
127
|
+
}
|
|
128
|
+
const chainConfig = QUOTE_CONFIG[chainName];
|
|
129
|
+
if (!chainConfig) {
|
|
130
|
+
console.warn(`[getTokenToNativeQuote] 不支持的链: ${chainName},使用最低利润`);
|
|
131
|
+
return MIN_PROFIT_WEI;
|
|
132
|
+
}
|
|
38
133
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
134
|
+
const version = poolType === 'v3' ? 'v3' : 'v2';
|
|
135
|
+
let nativeAmount = 0n;
|
|
136
|
+
if (quoteToken === 'native') {
|
|
137
|
+
// ✅ 直接路径:TOKEN → WBNB
|
|
138
|
+
console.log(`[getTokenToNativeQuote] 使用 ${version} 直接路径报价 (TOKEN → WBNB)`);
|
|
139
|
+
const result = await quote({
|
|
140
|
+
provider,
|
|
141
|
+
tokenIn: tokenAddress,
|
|
142
|
+
tokenOut: chainConfig.wrappedNative,
|
|
143
|
+
amountIn: tokenAmount,
|
|
144
|
+
chain: chainName,
|
|
145
|
+
version
|
|
146
|
+
});
|
|
147
|
+
nativeAmount = result.amountOut;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// ✅ 稳定币路径:TOKEN → USDT/USDC → WBNB(两步报价)
|
|
151
|
+
const stableCoins = STABLE_COINS[chainName];
|
|
152
|
+
const stableCoinAddress = stableCoins?.[quoteToken];
|
|
153
|
+
if (!stableCoinAddress) {
|
|
154
|
+
console.warn(`[getTokenToNativeQuote] 链 ${chainName} 不支持 ${quoteToken},使用最低利润`);
|
|
155
|
+
return MIN_PROFIT_WEI;
|
|
156
|
+
}
|
|
157
|
+
console.log(`[getTokenToNativeQuote] 使用 ${version} 稳定币路径报价 (TOKEN → ${quoteToken.toUpperCase()} → WBNB)`);
|
|
158
|
+
// 第一步:TOKEN → USDT/USDC
|
|
159
|
+
const step1 = await quote({
|
|
160
|
+
provider,
|
|
161
|
+
tokenIn: tokenAddress,
|
|
162
|
+
tokenOut: stableCoinAddress,
|
|
163
|
+
amountIn: tokenAmount,
|
|
164
|
+
chain: chainName,
|
|
165
|
+
version
|
|
166
|
+
});
|
|
167
|
+
if (step1.amountOut <= 0n) {
|
|
168
|
+
console.warn(`[getTokenToNativeQuote] TOKEN → ${quoteToken.toUpperCase()} 报价失败`);
|
|
169
|
+
return MIN_PROFIT_WEI;
|
|
170
|
+
}
|
|
171
|
+
// 第二步:USDT/USDC → WBNB
|
|
172
|
+
const step2 = await quote({
|
|
173
|
+
provider,
|
|
174
|
+
tokenIn: stableCoinAddress,
|
|
175
|
+
tokenOut: chainConfig.wrappedNative,
|
|
176
|
+
amountIn: step1.amountOut,
|
|
177
|
+
chain: chainName,
|
|
178
|
+
version
|
|
179
|
+
});
|
|
180
|
+
nativeAmount = step2.amountOut;
|
|
44
181
|
}
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const amounts = await router.getAmountsOut(tokenAmount, [tokenAddress, MONAD_WMON]);
|
|
49
|
-
return amounts[1];
|
|
182
|
+
if (nativeAmount > 0n) {
|
|
183
|
+
console.log(`[getTokenToNativeQuote] 报价成功: ${nativeAmount} wei`);
|
|
184
|
+
return nativeAmount;
|
|
50
185
|
}
|
|
51
|
-
// 其他链暂不支持报价,返回 0
|
|
52
|
-
return 0n;
|
|
53
186
|
}
|
|
54
|
-
catch {
|
|
55
|
-
|
|
56
|
-
return 0n;
|
|
187
|
+
catch (error) {
|
|
188
|
+
console.warn(`[getTokenToNativeQuote] 报价失败:`, error);
|
|
57
189
|
}
|
|
190
|
+
// ✅ 报价失败,返回最低利润(0.0001 BNB)
|
|
191
|
+
console.log(`[getTokenToNativeQuote] 无法获取报价,使用最低利润: ${MIN_PROFIT_WEI}`);
|
|
192
|
+
return MIN_PROFIT_WEI;
|
|
58
193
|
}
|
|
59
194
|
/**
|
|
60
195
|
* 分发(仅签名版本 - 不依赖 Merkle)
|
|
@@ -62,7 +197,7 @@ async function getTokenToNativeQuote(provider, tokenAddress, tokenAmount, chainI
|
|
|
62
197
|
* ✅ 优化版:支持 startNonce 参数避免并行调用时的 nonce 冲突
|
|
63
198
|
*/
|
|
64
199
|
export async function disperseWithBundleMerkle(params) {
|
|
65
|
-
const { fromPrivateKey, recipients, amount, amounts, tokenAddress, tokenDecimals, hopCount = 0, hopPrivateKeys, items, config, startNonce } = params;
|
|
200
|
+
const { fromPrivateKey, recipients, amount, amounts, tokenAddress, tokenDecimals, hopCount = 0, hopPrivateKeys, items, config, startNonce, tokenPoolType = 'v2', quoteToken = 'native' } = params;
|
|
66
201
|
// 快速返回空结果
|
|
67
202
|
if (!recipients || recipients.length === 0) {
|
|
68
203
|
return {
|
|
@@ -184,7 +319,7 @@ export async function disperseWithBundleMerkle(params) {
|
|
|
184
319
|
// ✅ 获取 ERC20 利润等值的原生代币(BNB)报价
|
|
185
320
|
let nativeProfitAmount = 0n;
|
|
186
321
|
if (extractProfit && totalTokenProfit > 0n) {
|
|
187
|
-
nativeProfitAmount = await getTokenToNativeQuote(provider, tokenAddress, totalTokenProfit, chainIdNum);
|
|
322
|
+
nativeProfitAmount = await getTokenToNativeQuote(provider, tokenAddress, totalTokenProfit, chainIdNum, tokenPoolType, quoteToken, config.rpcUrl);
|
|
188
323
|
totalProfit = nativeProfitAmount; // 更新为原生代币利润
|
|
189
324
|
}
|
|
190
325
|
// ✅ 并行签名所有交易
|
|
@@ -368,7 +503,7 @@ export async function disperseWithBundleMerkle(params) {
|
|
|
368
503
|
}
|
|
369
504
|
// ✅ ERC20 多跳:获取代币利润等值的原生代币报价
|
|
370
505
|
if (!isNative && extractProfit && totalTokenProfit > 0n) {
|
|
371
|
-
const nativeProfitAmount = await getTokenToNativeQuote(provider, tokenAddress, totalTokenProfit, chainIdNum);
|
|
506
|
+
const nativeProfitAmount = await getTokenToNativeQuote(provider, tokenAddress, totalTokenProfit, chainIdNum, tokenPoolType, quoteToken, config.rpcUrl);
|
|
372
507
|
totalProfit = nativeProfitAmount; // 更新为原生代币利润
|
|
373
508
|
}
|
|
374
509
|
// 利润转账(转等值原生代币)
|
|
@@ -411,7 +546,7 @@ export async function disperseWithBundleMerkle(params) {
|
|
|
411
546
|
* ✅ 优化版:支持 startNonce 参数避免并行调用时的 nonce 冲突
|
|
412
547
|
*/
|
|
413
548
|
export async function sweepWithBundleMerkle(params) {
|
|
414
|
-
const { sourcePrivateKeys, target, ratioPct, ratios, amount, amounts, tokenAddress, tokenDecimals, skipIfInsufficient = true, hopCount = 0, hopPrivateKeys, sources, config, startNonce } = params;
|
|
549
|
+
const { sourcePrivateKeys, target, ratioPct, ratios, amount, amounts, tokenAddress, tokenDecimals, skipIfInsufficient = true, hopCount = 0, hopPrivateKeys, sources, config, startNonce, tokenPoolType = 'v2', quoteToken = 'native' } = params;
|
|
415
550
|
// 快速返回空结果
|
|
416
551
|
if (!sourcePrivateKeys || sourcePrivateKeys.length === 0) {
|
|
417
552
|
return {
|
|
@@ -684,7 +819,7 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
684
819
|
// ✅ ERC20 归集:获取代币利润等值的原生代币(BNB)报价
|
|
685
820
|
let nativeProfitAmount = 0n;
|
|
686
821
|
if (extractProfit && totalProfit > 0n) {
|
|
687
|
-
nativeProfitAmount = await getTokenToNativeQuote(provider, tokenAddress, totalProfit, chainIdNum);
|
|
822
|
+
nativeProfitAmount = await getTokenToNativeQuote(provider, tokenAddress, totalProfit, chainIdNum, tokenPoolType, quoteToken, config.rpcUrl);
|
|
688
823
|
}
|
|
689
824
|
// ✅ 如果需要提取利润,检查支付者是否有足够 BNB 支付利润转账的额外 gas
|
|
690
825
|
if (extractProfit && nativeProfitAmount > 0n && maxSweepIndex >= 0 && config.checkBnbForErc20NoHop) {
|
|
@@ -758,7 +893,7 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
758
893
|
}
|
|
759
894
|
else {
|
|
760
895
|
// ========== 有多跳:构建跳转链归集 ==========
|
|
761
|
-
//
|
|
896
|
+
// ✅ 优化版:批量计算 + 批量获取 nonce + 并行签名
|
|
762
897
|
const sourceWallets = actualKeys.map(pk => new Wallet(pk, provider));
|
|
763
898
|
const withHopIndexes = [];
|
|
764
899
|
const withoutHopIndexes = [];
|
|
@@ -771,7 +906,7 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
771
906
|
}
|
|
772
907
|
}
|
|
773
908
|
const sourceAddresses = sourceWallets.map(w => w.address);
|
|
774
|
-
// ✅ 优化:并行获取 gasPrice、decimals
|
|
909
|
+
// ✅ 优化:并行获取 gasPrice、decimals、余额
|
|
775
910
|
const [gasPrice, decimals, balances, bnbBalances] = await Promise.all([
|
|
776
911
|
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
777
912
|
isNative ? Promise.resolve(18) : Promise.resolve(tokenDecimals ?? await _getErc20DecimalsMerkle(provider, tokenAddress, chainIdNum)),
|
|
@@ -781,122 +916,107 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
781
916
|
const iface = isNative ? null : new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
|
|
782
917
|
const gasFeePerHop = finalGasLimit * gasPrice;
|
|
783
918
|
const nonceManager = new NonceManager(provider);
|
|
784
|
-
// ✅
|
|
919
|
+
// ✅ 用于记录每个钱包的归集金额
|
|
785
920
|
const sweepAmounts = new Array(sourceWallets.length).fill(0n);
|
|
786
|
-
//
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
814
|
-
else if (amountStrForI && amountStrForI.trim().length > 0) {
|
|
815
|
-
const amt = ethers.parseEther(amountStrForI);
|
|
816
|
-
const need = amt + gasCost;
|
|
817
|
-
if (!skipIfInsufficient || bal >= need)
|
|
818
|
-
toSend = amt;
|
|
819
|
-
}
|
|
820
|
-
if (toSend > 0n) {
|
|
821
|
-
sweepAmounts[i] = toSend; // ✅ 记录归集金额
|
|
822
|
-
totalAmountBeforeProfit += toSend;
|
|
823
|
-
const nonce = await nonceManager.getNextNonce(sourceWallet);
|
|
824
|
-
const tx = await sourceWallet.signTransaction({
|
|
825
|
-
to: target,
|
|
826
|
-
value: toSend,
|
|
827
|
-
nonce,
|
|
828
|
-
gasPrice,
|
|
829
|
-
gasLimit: nativeGasLimit,
|
|
830
|
-
chainId: chainIdNum,
|
|
831
|
-
type: txType
|
|
832
|
-
});
|
|
833
|
-
signedTxs.push(tx);
|
|
834
|
-
}
|
|
921
|
+
// ✅ 辅助函数:获取比例和金额
|
|
922
|
+
const getRatioForI = (i) => {
|
|
923
|
+
if (sources && sources[i] && sources[i].ratioPct !== undefined)
|
|
924
|
+
return clamp(sources[i].ratioPct);
|
|
925
|
+
if (ratios && ratios[i] !== undefined)
|
|
926
|
+
return clamp(ratios[i]);
|
|
927
|
+
return ratio;
|
|
928
|
+
};
|
|
929
|
+
const getAmountStrForI = (i) => {
|
|
930
|
+
if (sources && sources[i] && sources[i].amount !== undefined)
|
|
931
|
+
return String(sources[i].amount);
|
|
932
|
+
if (amounts && amounts[i] !== undefined)
|
|
933
|
+
return String(amounts[i]);
|
|
934
|
+
return amount !== undefined ? String(amount) : undefined;
|
|
935
|
+
};
|
|
936
|
+
const noHopTxDataList = [];
|
|
937
|
+
for (const i of withoutHopIndexes) {
|
|
938
|
+
const bal = balances[i];
|
|
939
|
+
let toSend = 0n;
|
|
940
|
+
const ratioForI = getRatioForI(i);
|
|
941
|
+
const amountStrForI = getAmountStrForI(i);
|
|
942
|
+
if (isNative) {
|
|
943
|
+
const gasCost = nativeGasLimit * gasPrice;
|
|
944
|
+
if (ratioForI !== undefined) {
|
|
945
|
+
const want = (bal * BigInt(ratioForI)) / 100n;
|
|
946
|
+
const maxSendable = bal > gasCost ? (bal - gasCost) : 0n;
|
|
947
|
+
toSend = want > maxSendable ? maxSendable : want;
|
|
835
948
|
}
|
|
836
|
-
else {
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
return String(sources[i].amount);
|
|
847
|
-
if (amounts && amounts[i] !== undefined)
|
|
848
|
-
return String(amounts[i]);
|
|
849
|
-
return amount !== undefined ? String(amount) : undefined;
|
|
850
|
-
})();
|
|
851
|
-
if (ratioForI !== undefined) {
|
|
852
|
-
toSend = (bal * BigInt(ratioForI)) / 100n;
|
|
853
|
-
}
|
|
854
|
-
else if (amountStrForI && amountStrForI.trim().length > 0) {
|
|
855
|
-
toSend = ethers.parseUnits(amountStrForI, decimals);
|
|
856
|
-
}
|
|
857
|
-
if (toSend > 0n && (!skipIfInsufficient || bal >= toSend)) {
|
|
858
|
-
sweepAmounts[i] = toSend; // ✅ 记录归集金额
|
|
859
|
-
totalAmountBeforeProfit += toSend;
|
|
860
|
-
const nonce = await nonceManager.getNextNonce(sourceWallet);
|
|
861
|
-
const data = iface.encodeFunctionData('transfer', [target, toSend]);
|
|
862
|
-
const tx = await sourceWallet.signTransaction({
|
|
863
|
-
to: tokenAddress,
|
|
864
|
-
data,
|
|
865
|
-
value: 0n,
|
|
866
|
-
nonce,
|
|
867
|
-
gasPrice,
|
|
868
|
-
gasLimit: finalGasLimit,
|
|
869
|
-
chainId: chainIdNum,
|
|
870
|
-
type: txType
|
|
871
|
-
});
|
|
872
|
-
signedTxs.push(tx);
|
|
873
|
-
}
|
|
949
|
+
else if (amountStrForI && amountStrForI.trim().length > 0) {
|
|
950
|
+
const amt = ethers.parseEther(amountStrForI);
|
|
951
|
+
const need = amt + gasCost;
|
|
952
|
+
if (!skipIfInsufficient || bal >= need)
|
|
953
|
+
toSend = amt;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
if (ratioForI !== undefined) {
|
|
958
|
+
toSend = (bal * BigInt(ratioForI)) / 100n;
|
|
874
959
|
}
|
|
960
|
+
else if (amountStrForI && amountStrForI.trim().length > 0) {
|
|
961
|
+
toSend = ethers.parseUnits(amountStrForI, decimals);
|
|
962
|
+
}
|
|
963
|
+
if (skipIfInsufficient && bal < toSend)
|
|
964
|
+
toSend = 0n;
|
|
965
|
+
}
|
|
966
|
+
if (toSend > 0n) {
|
|
967
|
+
sweepAmounts[i] = toSend;
|
|
968
|
+
totalAmountBeforeProfit += toSend;
|
|
969
|
+
noHopTxDataList.push({ index: i, wallet: sourceWallets[i], toSend });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// ========== 第2步:批量获取无跳转钱包的 nonces ==========
|
|
973
|
+
const noHopWallets = noHopTxDataList.map(d => d.wallet);
|
|
974
|
+
const noHopNonces = noHopWallets.length > 0
|
|
975
|
+
? await nonceManager.getNextNoncesForWallets(noHopWallets)
|
|
976
|
+
: [];
|
|
977
|
+
// ========== 第3步:并行签名无跳转交易 ==========
|
|
978
|
+
const noHopTxPromises = noHopTxDataList.map((data, idx) => {
|
|
979
|
+
const { wallet, toSend } = data;
|
|
980
|
+
const nonce = noHopNonces[idx];
|
|
981
|
+
if (isNative) {
|
|
982
|
+
return wallet.signTransaction({
|
|
983
|
+
to: target,
|
|
984
|
+
value: toSend,
|
|
985
|
+
nonce,
|
|
986
|
+
gasPrice,
|
|
987
|
+
gasLimit: nativeGasLimit,
|
|
988
|
+
chainId: chainIdNum,
|
|
989
|
+
type: txType
|
|
990
|
+
});
|
|
875
991
|
}
|
|
992
|
+
else {
|
|
993
|
+
const txData = iface.encodeFunctionData('transfer', [target, toSend]);
|
|
994
|
+
return wallet.signTransaction({
|
|
995
|
+
to: tokenAddress,
|
|
996
|
+
data: txData,
|
|
997
|
+
value: 0n,
|
|
998
|
+
nonce,
|
|
999
|
+
gasPrice,
|
|
1000
|
+
gasLimit: finalGasLimit,
|
|
1001
|
+
chainId: chainIdNum,
|
|
1002
|
+
type: txType
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
if (noHopTxPromises.length > 0) {
|
|
1007
|
+
const noHopSignedTxs = await Promise.all(noHopTxPromises);
|
|
1008
|
+
signedTxs.push(...noHopSignedTxs);
|
|
876
1009
|
}
|
|
877
|
-
|
|
1010
|
+
const hopTxDataList = [];
|
|
878
1011
|
for (const i of withHopIndexes) {
|
|
879
1012
|
const sourceWallet = sourceWallets[i];
|
|
880
1013
|
const hopChain = preparedHops[i];
|
|
881
|
-
// 计算要归集的金额
|
|
882
|
-
let toSend = 0n;
|
|
883
1014
|
const bal = balances[i];
|
|
1015
|
+
let toSend = 0n;
|
|
1016
|
+
const ratioForI = getRatioForI(i);
|
|
1017
|
+
const amountStrForI = getAmountStrForI(i);
|
|
884
1018
|
if (isNative) {
|
|
885
1019
|
const totalGasCost = finalGasLimit * gasPrice * BigInt(hopChain.length + 1);
|
|
886
|
-
const ratioForI = (() => {
|
|
887
|
-
if (sources && sources[i] && sources[i].ratioPct !== undefined)
|
|
888
|
-
return clamp(sources[i].ratioPct);
|
|
889
|
-
if (ratios && ratios[i] !== undefined)
|
|
890
|
-
return clamp(ratios[i]);
|
|
891
|
-
return ratio;
|
|
892
|
-
})();
|
|
893
|
-
const amountStrForI = (() => {
|
|
894
|
-
if (sources && sources[i] && sources[i].amount !== undefined)
|
|
895
|
-
return String(sources[i].amount);
|
|
896
|
-
if (amounts && amounts[i] !== undefined)
|
|
897
|
-
return String(amounts[i]);
|
|
898
|
-
return amount !== undefined ? String(amount) : undefined;
|
|
899
|
-
})();
|
|
900
1020
|
if (ratioForI !== undefined) {
|
|
901
1021
|
const want = (bal * BigInt(ratioForI)) / 100n;
|
|
902
1022
|
const maxSendable = bal > totalGasCost ? (bal - totalGasCost) : 0n;
|
|
@@ -910,25 +1030,10 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
910
1030
|
}
|
|
911
1031
|
}
|
|
912
1032
|
else {
|
|
913
|
-
// ERC20:检查 BNB 余额是否足够支付 gas
|
|
914
1033
|
const bnbBal = bnbBalances[i];
|
|
915
1034
|
const bnbNeeded = (gasFeePerHop * BigInt(hopChain.length)) + (finalGasLimit * gasPrice);
|
|
916
1035
|
if (bnbBal < bnbNeeded && skipIfInsufficient)
|
|
917
1036
|
continue;
|
|
918
|
-
const ratioForI = (() => {
|
|
919
|
-
if (sources && sources[i] && sources[i].ratioPct !== undefined)
|
|
920
|
-
return clamp(sources[i].ratioPct);
|
|
921
|
-
if (ratios && ratios[i] !== undefined)
|
|
922
|
-
return clamp(ratios[i]);
|
|
923
|
-
return ratio;
|
|
924
|
-
})();
|
|
925
|
-
const amountStrForI = (() => {
|
|
926
|
-
if (sources && sources[i] && sources[i].amount !== undefined)
|
|
927
|
-
return String(sources[i].amount);
|
|
928
|
-
if (amounts && amounts[i] !== undefined)
|
|
929
|
-
return String(amounts[i]);
|
|
930
|
-
return amount !== undefined ? String(amount) : undefined;
|
|
931
|
-
})();
|
|
932
1037
|
if (ratioForI !== undefined) {
|
|
933
1038
|
toSend = (bal * BigInt(ratioForI)) / 100n;
|
|
934
1039
|
}
|
|
@@ -940,76 +1045,96 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
940
1045
|
}
|
|
941
1046
|
if (toSend <= 0n)
|
|
942
1047
|
continue;
|
|
943
|
-
// ✅ 记录归集金额
|
|
944
1048
|
sweepAmounts[i] = toSend;
|
|
945
1049
|
totalAmountBeforeProfit += toSend;
|
|
946
|
-
// 构建跳转链: 子钱包 -> 中转1 -> 中转2 -> ... -> 目标地址
|
|
947
1050
|
const fullChain = [sourceWallet, ...hopChain.map(pk => new Wallet(pk, provider))];
|
|
948
1051
|
const addresses = [...fullChain.map(w => w.address), target];
|
|
1052
|
+
hopTxDataList.push({ index: i, sourceWallet, toSend, hopChain, fullChain, addresses });
|
|
1053
|
+
}
|
|
1054
|
+
// ========== 第5步:计算每个有跳转钱包需要的 nonce 数量并批量获取 ==========
|
|
1055
|
+
// 每个源钱包需要的 nonce 数量:
|
|
1056
|
+
// - ERC20: hopChain.length (gas费交易) + 1 (主转账)
|
|
1057
|
+
// - Native: 1 (主转账)
|
|
1058
|
+
const hopNonceNeeds = hopTxDataList.map(d => isNative ? 1 : d.hopChain.length + 1);
|
|
1059
|
+
const hopSourceWallets = hopTxDataList.map(d => d.sourceWallet);
|
|
1060
|
+
// ✅ 批量获取所有有跳转钱包的 nonces
|
|
1061
|
+
const hopNoncesFlat = [];
|
|
1062
|
+
if (hopSourceWallets.length > 0) {
|
|
1063
|
+
const batchNoncePromises = hopSourceWallets.map((w, idx) => nonceManager.getNextNonceBatch(w, hopNonceNeeds[idx]));
|
|
1064
|
+
const batchNonces = await Promise.all(batchNoncePromises);
|
|
1065
|
+
batchNonces.forEach(nonces => hopNoncesFlat.push(...nonces));
|
|
1066
|
+
}
|
|
1067
|
+
const hopTxsToSign = [];
|
|
1068
|
+
let nonceOffset = 0;
|
|
1069
|
+
for (let dataIdx = 0; dataIdx < hopTxDataList.length; dataIdx++) {
|
|
1070
|
+
const { sourceWallet, toSend, hopChain, fullChain, addresses } = hopTxDataList[dataIdx];
|
|
1071
|
+
const nonceCount = hopNonceNeeds[dataIdx];
|
|
1072
|
+
const nonces = hopNoncesFlat.slice(nonceOffset, nonceOffset + nonceCount);
|
|
1073
|
+
nonceOffset += nonceCount;
|
|
1074
|
+
let nonceIdx = 0;
|
|
949
1075
|
// ERC20 多跳:先给中转钱包转 gas 费
|
|
950
1076
|
if (!isNative) {
|
|
951
|
-
const gasNonces = await nonceManager.getNextNonceBatch(sourceWallet, hopChain.length);
|
|
952
|
-
const gasTxs = [];
|
|
953
1077
|
for (let j = 0; j < hopChain.length; j++) {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1078
|
+
hopTxsToSign.push({
|
|
1079
|
+
wallet: sourceWallet,
|
|
1080
|
+
tx: {
|
|
1081
|
+
to: fullChain[j + 1].address,
|
|
1082
|
+
value: gasFeePerHop,
|
|
1083
|
+
nonce: nonces[nonceIdx++],
|
|
1084
|
+
gasPrice,
|
|
1085
|
+
gasLimit: nativeGasLimit,
|
|
1086
|
+
chainId: chainIdNum,
|
|
1087
|
+
type: txType
|
|
1088
|
+
}
|
|
962
1089
|
});
|
|
963
|
-
gasTxs.push(tx);
|
|
964
1090
|
}
|
|
965
|
-
signedTxs.push(...gasTxs);
|
|
966
1091
|
}
|
|
967
1092
|
// 执行主要的归集链
|
|
968
1093
|
for (let j = 0; j < addresses.length - 1; j++) {
|
|
969
1094
|
const fromWallet = fullChain[j];
|
|
970
1095
|
const toAddress = addresses[j + 1];
|
|
971
|
-
|
|
972
|
-
const nonce = j === 0
|
|
973
|
-
? await nonceManager.getNextNonce(sourceWallet)
|
|
974
|
-
: 0;
|
|
1096
|
+
const nonce = j === 0 ? nonces[nonceIdx++] : 0;
|
|
975
1097
|
if (isNative) {
|
|
976
|
-
// 原生币归集:每一跳需要附加后续跳数的gas费
|
|
977
|
-
// toSend = 最终主钱包收到的净金额
|
|
978
|
-
// 剩余跳数 = 还需要几跳才能到目标地址
|
|
979
1098
|
const remainingHops = addresses.length - 2 - j;
|
|
980
1099
|
const additionalGas = gasFeePerHop * BigInt(remainingHops);
|
|
981
1100
|
const valueToTransfer = toSend + additionalGas;
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1101
|
+
hopTxsToSign.push({
|
|
1102
|
+
wallet: fromWallet,
|
|
1103
|
+
tx: {
|
|
1104
|
+
to: toAddress,
|
|
1105
|
+
value: valueToTransfer,
|
|
1106
|
+
nonce,
|
|
1107
|
+
gasPrice,
|
|
1108
|
+
gasLimit: finalGasLimit,
|
|
1109
|
+
chainId: chainIdNum,
|
|
1110
|
+
type: txType
|
|
1111
|
+
}
|
|
993
1112
|
});
|
|
994
|
-
signedTxs.push(tx);
|
|
995
1113
|
}
|
|
996
1114
|
else {
|
|
997
1115
|
const data = iface.encodeFunctionData('transfer', [toAddress, toSend]);
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1116
|
+
hopTxsToSign.push({
|
|
1117
|
+
wallet: fromWallet,
|
|
1118
|
+
tx: {
|
|
1119
|
+
to: tokenAddress,
|
|
1120
|
+
data,
|
|
1121
|
+
value: 0n,
|
|
1122
|
+
nonce,
|
|
1123
|
+
gasPrice,
|
|
1124
|
+
gasLimit: finalGasLimit,
|
|
1125
|
+
chainId: chainIdNum,
|
|
1126
|
+
type: txType
|
|
1127
|
+
}
|
|
1007
1128
|
});
|
|
1008
|
-
signedTxs.push(tx);
|
|
1009
1129
|
}
|
|
1010
1130
|
}
|
|
1011
1131
|
}
|
|
1012
|
-
// ✅
|
|
1132
|
+
// ✅ 并行签名所有有跳转的交易
|
|
1133
|
+
if (hopTxsToSign.length > 0) {
|
|
1134
|
+
const hopSignedTxs = await Promise.all(hopTxsToSign.map(({ wallet, tx }) => wallet.signTransaction(tx)));
|
|
1135
|
+
signedTxs.push(...hopSignedTxs);
|
|
1136
|
+
}
|
|
1137
|
+
// ========== 第7步:计算利润并添加利润转账 ==========
|
|
1013
1138
|
if (extractProfit && totalAmountBeforeProfit > 0n) {
|
|
1014
1139
|
// 找出归集金额最大的钱包作为支付者
|
|
1015
1140
|
let maxSweepIndex = -1;
|
|
@@ -1026,26 +1151,26 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
1026
1151
|
if (sweepAmounts[i] > 0n) {
|
|
1027
1152
|
const { profit } = calculateProfit(sweepAmounts[i]);
|
|
1028
1153
|
if (isNative) {
|
|
1029
|
-
totalProfit += profit;
|
|
1154
|
+
totalProfit += profit;
|
|
1030
1155
|
}
|
|
1031
1156
|
else {
|
|
1032
|
-
totalTokenProfit += profit;
|
|
1157
|
+
totalTokenProfit += profit;
|
|
1033
1158
|
}
|
|
1034
1159
|
}
|
|
1035
1160
|
}
|
|
1036
|
-
// ✅ ERC20
|
|
1161
|
+
// ✅ ERC20:获取代币利润等值的原生代币报价
|
|
1037
1162
|
let nativeProfitAmount = isNative ? totalProfit : 0n;
|
|
1038
1163
|
if (!isNative && totalTokenProfit > 0n) {
|
|
1039
|
-
nativeProfitAmount = await getTokenToNativeQuote(provider, tokenAddress, totalTokenProfit, chainIdNum);
|
|
1040
|
-
totalProfit = nativeProfitAmount;
|
|
1164
|
+
nativeProfitAmount = await getTokenToNativeQuote(provider, tokenAddress, totalTokenProfit, chainIdNum, tokenPoolType, quoteToken, config.rpcUrl);
|
|
1165
|
+
totalProfit = nativeProfitAmount;
|
|
1041
1166
|
}
|
|
1042
|
-
//
|
|
1167
|
+
// 由归集金额最大的钱包支付利润
|
|
1043
1168
|
if (nativeProfitAmount > 0n && maxSweepIndex >= 0) {
|
|
1044
1169
|
const payerWallet = sourceWallets[maxSweepIndex];
|
|
1045
1170
|
const profitNonce = await nonceManager.getNextNonce(payerWallet);
|
|
1046
1171
|
const profitTx = await payerWallet.signTransaction({
|
|
1047
1172
|
to: getProfitRecipient(),
|
|
1048
|
-
value: nativeProfitAmount,
|
|
1173
|
+
value: nativeProfitAmount,
|
|
1049
1174
|
nonce: profitNonce,
|
|
1050
1175
|
gasPrice,
|
|
1051
1176
|
gasLimit: 21000n,
|