four-flap-meme-sdk 1.3.73 → 1.3.76

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.
@@ -0,0 +1,1019 @@
1
+ import { ethers, Wallet, Contract } from 'ethers';
2
+ import { getOptimizedGasPrice, NonceManager } from '../../utils/bundle-helpers.js';
3
+ import { getTxType, getGasPriceConfig, shouldExtractProfit, calculateProfit, getProfitRecipient } from './config.js';
4
+ // ==================== 链配置 ====================
5
+ const CHAIN_ID_MAP = {
6
+ 'BSC': 56,
7
+ 'MONAD': 143,
8
+ 'XLAYER': 196
9
+ };
10
+ // ==================== ERC20 → 原生代币报价 ====================
11
+ // BSC 链常量
12
+ const BSC_PANCAKE_V2_ROUTER = '0x10ED43C718714eb63d5aA57B78B54704E256024E';
13
+ const BSC_WBNB = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
14
+ // Monad 链常量
15
+ const MONAD_PANCAKE_V2_ROUTER = '0xb1bc24c34e88f7d43d5923034e3a14b24daacff9';
16
+ const MONAD_WMON = '0x3bd359c1119da7da1d913d1c4d2b7c461115433a';
17
+ // XLayer 链常量(PotatoSwap V2 Router)
18
+ const XLAYER_POTATOSWAP_V2_ROUTER = '0x881fb2f98c13d521009464e7d1cbf16e1b394e8e';
19
+ const XLAYER_WOKB = '0xe538905cf8410324e03a5a23c1c177a474d59b2b';
20
+ const ROUTER_ABI = [
21
+ 'function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)'
22
+ ];
23
+ /**
24
+ * 获取 ERC20 代币 → 原生代币的报价
25
+ */
26
+ async function getTokenToNativeQuote(provider, tokenAddress, tokenAmount, chainId) {
27
+ if (tokenAmount <= 0n)
28
+ return 0n;
29
+ try {
30
+ if (chainId === 56) {
31
+ const router = new Contract(BSC_PANCAKE_V2_ROUTER, ROUTER_ABI, provider);
32
+ const amounts = await router.getAmountsOut(tokenAmount, [tokenAddress, BSC_WBNB]);
33
+ return amounts[1];
34
+ }
35
+ if (chainId === 143) {
36
+ const router = new Contract(MONAD_PANCAKE_V2_ROUTER, ROUTER_ABI, provider);
37
+ const amounts = await router.getAmountsOut(tokenAmount, [tokenAddress, MONAD_WMON]);
38
+ return amounts[1];
39
+ }
40
+ if (chainId === 196) {
41
+ // XLayer: 使用 PotatoSwap V2 Router
42
+ const router = new Contract(XLAYER_POTATOSWAP_V2_ROUTER, ROUTER_ABI, provider);
43
+ const amounts = await router.getAmountsOut(tokenAmount, [tokenAddress, XLAYER_WOKB]);
44
+ return amounts[1];
45
+ }
46
+ return 0n;
47
+ }
48
+ catch {
49
+ return 0n;
50
+ }
51
+ }
52
+ // ==================== 内部辅助函数 ====================
53
+ const decimalsCache = new Map();
54
+ async function getErc20Decimals(provider, token) {
55
+ const network = await provider.getNetwork();
56
+ const key = `${network.chainId}_${token.toLowerCase()}`;
57
+ if (decimalsCache.has(key))
58
+ return decimalsCache.get(key);
59
+ try {
60
+ const erc20 = new ethers.Contract(token, ['function decimals() view returns (uint8)'], provider);
61
+ const d = await erc20.decimals();
62
+ if (!Number.isFinite(d) || d < 0 || d > 36)
63
+ return 18;
64
+ decimalsCache.set(key, d);
65
+ return d;
66
+ }
67
+ catch {
68
+ return 18;
69
+ }
70
+ }
71
+ function generateHopWallets(recipientCount, hopCount) {
72
+ const hopCounts = Array.isArray(hopCount) ? hopCount : new Array(recipientCount).fill(hopCount);
73
+ if (hopCounts.every(h => h <= 0))
74
+ return null;
75
+ const result = [];
76
+ for (let i = 0; i < recipientCount; i++) {
77
+ const chain = [];
78
+ for (let j = 0; j < hopCounts[i]; j++) {
79
+ chain.push(Wallet.createRandom().privateKey);
80
+ }
81
+ result.push(chain);
82
+ }
83
+ return result;
84
+ }
85
+ function normalizeAmounts(recipients, amount, amounts) {
86
+ if (amounts && amounts.length > 0) {
87
+ if (amounts.length !== recipients.length) {
88
+ throw new Error(`amounts length (${amounts.length}) must match recipients length (${recipients.length})`);
89
+ }
90
+ return amounts;
91
+ }
92
+ if (amount !== undefined && amount.trim().length > 0) {
93
+ return new Array(recipients.length).fill(amount);
94
+ }
95
+ throw new Error('Either amount or amounts must be provided');
96
+ }
97
+ async function batchGetBalances(provider, addresses, tokenAddress) {
98
+ if (addresses.length === 0)
99
+ return [];
100
+ if (!tokenAddress) {
101
+ return Promise.all(addresses.map(addr => provider.getBalance(addr).catch(() => 0n)));
102
+ }
103
+ else {
104
+ const MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
105
+ const MULTICALL3_ABI = [
106
+ 'function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) view returns (tuple(bool success, bytes returnData)[] returnData)'
107
+ ];
108
+ const multicall = new ethers.Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
109
+ const ERC20_ABI = ['function balanceOf(address) view returns (uint256)'];
110
+ const iface = new ethers.Interface(ERC20_ABI);
111
+ const calls = addresses.map(addr => ({
112
+ target: tokenAddress,
113
+ allowFailure: true,
114
+ callData: iface.encodeFunctionData('balanceOf', [addr])
115
+ }));
116
+ try {
117
+ const results = await multicall.aggregate3(calls);
118
+ return results.map((result) => {
119
+ if (result.success) {
120
+ return iface.decodeFunctionResult('balanceOf', result.returnData)[0];
121
+ }
122
+ return 0n;
123
+ });
124
+ }
125
+ catch {
126
+ const fallbackCalls = addresses.map(addr => provider
127
+ .call({ to: tokenAddress, data: iface.encodeFunctionData('balanceOf', [addr]) })
128
+ .then(raw => iface.decodeFunctionResult('balanceOf', raw)[0])
129
+ .catch(() => 0n));
130
+ return Promise.all(fallbackCalls);
131
+ }
132
+ }
133
+ }
134
+ function calculateGasLimit(config, isNative, hasHops, hopCount = 0) {
135
+ if (config.gasLimit !== undefined) {
136
+ return BigInt(config.gasLimit);
137
+ }
138
+ let baseGas = isNative ? 21000 : 65000;
139
+ if (hasHops) {
140
+ baseGas += hopCount * (isNative ? 21000 : 65000);
141
+ }
142
+ const multiplier = config.gasLimitMultiplier ?? 1.2;
143
+ return BigInt(Math.ceil(baseGas * multiplier));
144
+ }
145
+ function isNativeTokenAddress(tokenAddress) {
146
+ if (!tokenAddress)
147
+ return true;
148
+ const v = tokenAddress.trim().toLowerCase();
149
+ if (!v)
150
+ return true;
151
+ if (v === 'native')
152
+ return true;
153
+ if (v === '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee')
154
+ return true;
155
+ return false;
156
+ }
157
+ // ==================== 分散函数 ====================
158
+ /**
159
+ * Flap Protocol: 分散(仅签名版本)
160
+ * ✅ 支持利润提取
161
+ * ✅ 支持多跳
162
+ * ✅ 支持 ERC20→原生代币报价
163
+ * ✅ 支持 Multicall3 批量获取余额
164
+ */
165
+ export async function flapDisperseWithBundleMerkle(params) {
166
+ const { chain, fromPrivateKey, recipients, amount, amounts, tokenAddress, tokenDecimals, hopCount = 0, hopPrivateKeys, items, config, startNonce } = params;
167
+ if (!recipients || recipients.length === 0) {
168
+ return { signedTransactions: [], hopWallets: undefined };
169
+ }
170
+ const chainIdNum = CHAIN_ID_MAP[chain] ?? 56;
171
+ const provider = new ethers.JsonRpcProvider(config.rpcUrl, { chainId: chainIdNum, name: chain });
172
+ const mainWallet = new Wallet(fromPrivateKey, provider);
173
+ const isNative = isNativeTokenAddress(tokenAddress);
174
+ const txType = getTxType(config);
175
+ // 预处理数据
176
+ const normalizedAmounts = items && items.length > 0
177
+ ? items.map(it => (typeof it.amount === 'bigint' ? it.amount.toString() : String(it.amount)))
178
+ : normalizeAmounts(recipients, amount, amounts);
179
+ const providedHops = (() => {
180
+ if (hopPrivateKeys && hopPrivateKeys.length > 0) {
181
+ if (hopPrivateKeys.length !== recipients.length) {
182
+ throw new Error(`hopPrivateKeys length (${hopPrivateKeys.length}) must match recipients length (${recipients.length})`);
183
+ }
184
+ return hopPrivateKeys.every(h => h.length === 0) ? null : hopPrivateKeys;
185
+ }
186
+ if (items && items.length > 0) {
187
+ const hops = items.map(it => it.hopPrivateKeys ?? []);
188
+ return hops.every(h => h.length === 0) ? null : hops;
189
+ }
190
+ return null;
191
+ })();
192
+ const hopCountInput = (() => {
193
+ if (items && items.length > 0) {
194
+ const baseArray = Array.isArray(hopCount) ? hopCount : new Array(recipients.length).fill(hopCount);
195
+ return items.map((it, i) => (typeof it.hopCount === 'number' ? it.hopCount : (baseArray[i] ?? 0)));
196
+ }
197
+ return hopCount;
198
+ })();
199
+ const preparedHops = providedHops ?? generateHopWallets(recipients.length, hopCountInput);
200
+ const hasHops = preparedHops !== null;
201
+ const maxHopCount = hasHops ? Math.max(...preparedHops.map(h => h.length)) : 0;
202
+ const finalGasLimit = calculateGasLimit(config, isNative, hasHops, maxHopCount);
203
+ const nativeGasLimit = 21000n;
204
+ const signedTxs = [];
205
+ const extractProfit = shouldExtractProfit(config);
206
+ let totalProfit = 0n;
207
+ let totalAmountBeforeProfit = 0n;
208
+ const nonceManager = new NonceManager(provider);
209
+ if (!preparedHops) {
210
+ // ========== 无多跳:直接批量转账 ==========
211
+ const extraTxCount = extractProfit ? 1 : 0;
212
+ const totalTxCount = recipients.length + extraTxCount;
213
+ const [gasPrice, nonces] = await Promise.all([
214
+ getOptimizedGasPrice(provider, getGasPriceConfig(config)),
215
+ startNonce !== undefined
216
+ ? Promise.resolve(Array.from({ length: totalTxCount }, (_, i) => startNonce + i))
217
+ : nonceManager.getNextNonceBatch(mainWallet, totalTxCount)
218
+ ]);
219
+ if (isNative) {
220
+ const txDataList = recipients.map((to, i) => {
221
+ const originalAmount = ethers.parseEther(normalizedAmounts[i]);
222
+ totalAmountBeforeProfit += originalAmount;
223
+ let actualAmount = originalAmount;
224
+ if (extractProfit) {
225
+ const { profit, remaining } = calculateProfit(originalAmount, config);
226
+ actualAmount = remaining;
227
+ totalProfit += profit;
228
+ }
229
+ return { to, value: actualAmount, nonce: nonces[i] };
230
+ });
231
+ const txPromises = txDataList.map(({ to, value, nonce }) => mainWallet.signTransaction({
232
+ to,
233
+ value,
234
+ nonce,
235
+ gasPrice,
236
+ gasLimit: nativeGasLimit,
237
+ chainId: chainIdNum,
238
+ type: txType
239
+ }));
240
+ if (extractProfit && totalProfit > 0n) {
241
+ txPromises.push(mainWallet.signTransaction({
242
+ to: getProfitRecipient(),
243
+ value: totalProfit,
244
+ nonce: nonces[recipients.length],
245
+ gasPrice,
246
+ gasLimit: 21000n,
247
+ chainId: chainIdNum,
248
+ type: txType
249
+ }));
250
+ }
251
+ signedTxs.push(...(await Promise.all(txPromises)));
252
+ }
253
+ else {
254
+ const decimals = tokenDecimals ?? (await getErc20Decimals(provider, tokenAddress));
255
+ const iface = new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
256
+ let totalTokenProfit = 0n;
257
+ const txDataList = recipients.map((to, i) => {
258
+ const originalAmount = ethers.parseUnits(normalizedAmounts[i], decimals);
259
+ totalAmountBeforeProfit += originalAmount;
260
+ let actualAmount = originalAmount;
261
+ if (extractProfit) {
262
+ const { profit, remaining } = calculateProfit(originalAmount, config);
263
+ actualAmount = remaining;
264
+ totalTokenProfit += profit;
265
+ }
266
+ const data = iface.encodeFunctionData('transfer', [to, actualAmount]);
267
+ return { data, nonce: nonces[i] };
268
+ });
269
+ let nativeProfitAmount = 0n;
270
+ if (extractProfit && totalTokenProfit > 0n) {
271
+ nativeProfitAmount = await getTokenToNativeQuote(provider, tokenAddress, totalTokenProfit, chainIdNum);
272
+ totalProfit = nativeProfitAmount;
273
+ }
274
+ const txPromises = txDataList.map(({ data, nonce }) => mainWallet.signTransaction({
275
+ to: tokenAddress,
276
+ data,
277
+ value: 0n,
278
+ nonce,
279
+ gasPrice,
280
+ gasLimit: finalGasLimit,
281
+ chainId: chainIdNum,
282
+ type: txType
283
+ }));
284
+ if (extractProfit && nativeProfitAmount > 0n) {
285
+ txPromises.push(mainWallet.signTransaction({
286
+ to: getProfitRecipient(),
287
+ value: nativeProfitAmount,
288
+ nonce: nonces[recipients.length],
289
+ gasPrice,
290
+ gasLimit: 21000n,
291
+ chainId: chainIdNum,
292
+ type: txType
293
+ }));
294
+ }
295
+ signedTxs.push(...(await Promise.all(txPromises)));
296
+ }
297
+ }
298
+ else {
299
+ // ========== 有多跳:构建跳转链 ==========
300
+ const [gasPrice, decimals] = await Promise.all([
301
+ getOptimizedGasPrice(provider, getGasPriceConfig(config)),
302
+ isNative ? Promise.resolve(18) : Promise.resolve(tokenDecimals ?? await getErc20Decimals(provider, tokenAddress))
303
+ ]);
304
+ const iface = isNative ? null : new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
305
+ const gasFeePerHop = finalGasLimit * gasPrice;
306
+ let mainWalletNonceCount = 0;
307
+ for (let i = 0; i < recipients.length; i++) {
308
+ const hopChain = preparedHops[i];
309
+ if (hopChain.length === 0) {
310
+ mainWalletNonceCount += 1;
311
+ }
312
+ else {
313
+ const nonceNeed = isNative ? 1 : (hopChain.length + 1);
314
+ mainWalletNonceCount += nonceNeed;
315
+ }
316
+ }
317
+ if (extractProfit)
318
+ mainWalletNonceCount += 1;
319
+ const allMainNonces = startNonce !== undefined
320
+ ? Array.from({ length: mainWalletNonceCount }, (_, i) => startNonce + i)
321
+ : await nonceManager.getNextNonceBatch(mainWallet, mainWalletNonceCount);
322
+ let mainNonceIdx = 0;
323
+ const txsToSign = [];
324
+ let totalTokenProfit = 0n;
325
+ for (let i = 0; i < recipients.length; i++) {
326
+ const finalRecipient = recipients[i];
327
+ const originalAmountWei = isNative
328
+ ? ethers.parseEther(normalizedAmounts[i])
329
+ : ethers.parseUnits(normalizedAmounts[i], decimals);
330
+ totalAmountBeforeProfit += originalAmountWei;
331
+ let amountWei = originalAmountWei;
332
+ if (extractProfit) {
333
+ const { profit, remaining } = calculateProfit(originalAmountWei, config);
334
+ amountWei = remaining;
335
+ if (isNative) {
336
+ totalProfit += profit;
337
+ }
338
+ else {
339
+ totalTokenProfit += profit;
340
+ }
341
+ }
342
+ const hopChain = preparedHops[i];
343
+ if (hopChain.length === 0) {
344
+ const nonce = allMainNonces[mainNonceIdx++];
345
+ if (isNative) {
346
+ txsToSign.push({
347
+ wallet: mainWallet,
348
+ tx: { to: finalRecipient, value: amountWei, nonce, gasPrice, gasLimit: nativeGasLimit, chainId: chainIdNum, type: txType }
349
+ });
350
+ }
351
+ else {
352
+ const data = iface.encodeFunctionData('transfer', [finalRecipient, amountWei]);
353
+ txsToSign.push({
354
+ wallet: mainWallet,
355
+ tx: { to: tokenAddress, data, value: 0n, nonce, gasPrice, gasLimit: finalGasLimit, chainId: chainIdNum, type: txType }
356
+ });
357
+ }
358
+ continue;
359
+ }
360
+ const fullChain = [mainWallet, ...hopChain.map(pk => new Wallet(pk, provider))];
361
+ const addresses = [...fullChain.map(w => w.address), finalRecipient];
362
+ if (!isNative) {
363
+ for (let j = 0; j < hopChain.length; j++) {
364
+ const nonce = allMainNonces[mainNonceIdx++];
365
+ txsToSign.push({
366
+ wallet: mainWallet,
367
+ tx: { to: fullChain[j + 1].address, value: gasFeePerHop, nonce, gasPrice, gasLimit: nativeGasLimit, chainId: chainIdNum, type: txType }
368
+ });
369
+ }
370
+ }
371
+ for (let j = 0; j < addresses.length - 1; j++) {
372
+ const fromWallet = fullChain[j];
373
+ const toAddress = addresses[j + 1];
374
+ const nonce = j === 0 ? allMainNonces[mainNonceIdx++] : 0;
375
+ if (isNative) {
376
+ const remainingHops = addresses.length - 2 - j;
377
+ const additionalGas = gasFeePerHop * BigInt(remainingHops);
378
+ const transferValue = amountWei + additionalGas;
379
+ txsToSign.push({
380
+ wallet: fromWallet,
381
+ tx: { to: toAddress, value: transferValue, nonce, gasPrice, gasLimit: finalGasLimit, chainId: chainIdNum, type: txType }
382
+ });
383
+ }
384
+ else {
385
+ const data = iface.encodeFunctionData('transfer', [toAddress, amountWei]);
386
+ txsToSign.push({
387
+ wallet: fromWallet,
388
+ tx: { to: tokenAddress, data, value: 0n, nonce, gasPrice, gasLimit: finalGasLimit, chainId: chainIdNum, type: txType }
389
+ });
390
+ }
391
+ }
392
+ }
393
+ if (!isNative && extractProfit && totalTokenProfit > 0n) {
394
+ const nativeProfitAmount = await getTokenToNativeQuote(provider, tokenAddress, totalTokenProfit, chainIdNum);
395
+ totalProfit = nativeProfitAmount;
396
+ }
397
+ if (extractProfit && totalProfit > 0n) {
398
+ const profitNonce = allMainNonces[mainNonceIdx++];
399
+ txsToSign.push({
400
+ wallet: mainWallet,
401
+ tx: { to: getProfitRecipient(), value: totalProfit, nonce: profitNonce, gasPrice, gasLimit: 21000n, chainId: chainIdNum, type: txType }
402
+ });
403
+ }
404
+ const signedTxsResult = await Promise.all(txsToSign.map(({ wallet, tx }) => wallet.signTransaction(tx)));
405
+ signedTxs.push(...signedTxsResult);
406
+ }
407
+ return {
408
+ signedTransactions: signedTxs,
409
+ hopWallets: preparedHops || undefined,
410
+ metadata: extractProfit ? {
411
+ totalAmount: ethers.formatEther(totalAmountBeforeProfit),
412
+ profitAmount: ethers.formatEther(totalProfit),
413
+ profitRecipient: getProfitRecipient(),
414
+ recipientCount: recipients.length,
415
+ isNative,
416
+ tokenAddress: isNative ? undefined : tokenAddress
417
+ } : undefined
418
+ };
419
+ }
420
+ // ==================== 归集函数 ====================
421
+ /**
422
+ * Flap Protocol: 归集(仅签名版本)
423
+ * ✅ 支持利润提取
424
+ * ✅ 查询余额最大的钱包支付利润
425
+ * ✅ 支持多跳
426
+ * ✅ 支持 ERC20→原生代币报价
427
+ * ✅ 支持 Multicall3 批量获取余额
428
+ */
429
+ export async function flapSweepWithBundleMerkle(params) {
430
+ const { chain, sourcePrivateKeys, target, ratioPct, ratios, amount, amounts, tokenAddress, tokenDecimals, skipIfInsufficient = true, hopCount = 0, hopPrivateKeys, sources, config } = params;
431
+ if (!sourcePrivateKeys || sourcePrivateKeys.length === 0) {
432
+ return { signedTransactions: [], hopWallets: undefined };
433
+ }
434
+ const chainIdNum = CHAIN_ID_MAP[chain] ?? 56;
435
+ const provider = new ethers.JsonRpcProvider(config.rpcUrl, { chainId: chainIdNum, name: chain });
436
+ const isNative = isNativeTokenAddress(tokenAddress);
437
+ const txType = getTxType(config);
438
+ const clamp = (n) => (typeof n === 'number' && Number.isFinite(n)
439
+ ? Math.max(0, Math.min(100, Math.floor(n)))
440
+ : undefined);
441
+ const ratio = clamp(ratioPct);
442
+ const actualKeys = (() => {
443
+ if (sources && sources.length > 0)
444
+ return sources.map(s => s.privateKey);
445
+ return sourcePrivateKeys;
446
+ })();
447
+ const providedHops = (() => {
448
+ if (hopPrivateKeys && hopPrivateKeys.length > 0) {
449
+ if (hopPrivateKeys.length !== actualKeys.length) {
450
+ throw new Error(`hopPrivateKeys length (${hopPrivateKeys.length}) must match sourcePrivateKeys length (${actualKeys.length})`);
451
+ }
452
+ return hopPrivateKeys.every(h => h.length === 0) ? null : hopPrivateKeys;
453
+ }
454
+ if (sources && sources.length > 0) {
455
+ const hops = sources.map(s => s.hopPrivateKeys ?? []);
456
+ return hops.every(h => h.length === 0) ? null : hops;
457
+ }
458
+ return null;
459
+ })();
460
+ const hopCountInput = (() => {
461
+ if (sources && sources.length > 0) {
462
+ const baseArray = Array.isArray(hopCount) ? hopCount : new Array(actualKeys.length).fill(hopCount);
463
+ return sources.map((s, i) => (typeof s.hopCount === 'number' ? s.hopCount : (baseArray[i] ?? 0)));
464
+ }
465
+ return hopCount;
466
+ })();
467
+ const preparedHops = providedHops ?? generateHopWallets(actualKeys.length, hopCountInput);
468
+ const hasHops = preparedHops !== null;
469
+ const maxHopCount = hasHops ? Math.max(...preparedHops.map(h => h.length)) : 0;
470
+ const finalGasLimit = calculateGasLimit(config, isNative, hasHops, maxHopCount);
471
+ const nativeGasLimit = 21000n;
472
+ const signedTxs = [];
473
+ const extractProfit = shouldExtractProfit(config);
474
+ let totalProfit = 0n;
475
+ let totalAmountBeforeProfit = 0n;
476
+ if (!preparedHops) {
477
+ // ========== 无多跳:直接批量归集 ==========
478
+ const wallets = actualKeys.map(pk => new Wallet(pk, provider));
479
+ const addresses = wallets.map(w => w.address);
480
+ const nonceManager = new NonceManager(provider);
481
+ if (isNative) {
482
+ const [gasPrice, balances] = await Promise.all([
483
+ getOptimizedGasPrice(provider, getGasPriceConfig(config)),
484
+ batchGetBalances(provider, addresses)
485
+ ]);
486
+ const gasCostBase = nativeGasLimit * gasPrice;
487
+ const profitTxGas = nativeGasLimit * gasPrice;
488
+ // 第一步:计算所有钱包的归集金额,找出余额最大的钱包
489
+ const sweepAmounts = [];
490
+ let maxSweepIndex = -1;
491
+ let maxSweepAmount = 0n;
492
+ for (let i = 0; i < wallets.length; i++) {
493
+ const bal = balances[i];
494
+ let toSend = 0n;
495
+ const ratioForI = (() => {
496
+ if (sources && sources[i] && sources[i].ratioPct !== undefined)
497
+ return clamp(sources[i].ratioPct);
498
+ if (ratios && ratios[i] !== undefined)
499
+ return clamp(ratios[i]);
500
+ return ratio;
501
+ })();
502
+ const amountStrForI = (() => {
503
+ if (sources && sources[i] && sources[i].amount !== undefined)
504
+ return String(sources[i].amount);
505
+ if (amounts && amounts[i] !== undefined)
506
+ return String(amounts[i]);
507
+ return amount !== undefined ? String(amount) : undefined;
508
+ })();
509
+ const gasCost = gasCostBase;
510
+ if (ratioForI !== undefined) {
511
+ const want = (bal * BigInt(ratioForI)) / 100n;
512
+ const maxSendable = bal > gasCost ? (bal - gasCost) : 0n;
513
+ toSend = want > maxSendable ? maxSendable : want;
514
+ }
515
+ else if (amountStrForI && amountStrForI.trim().length > 0) {
516
+ const amt = ethers.parseEther(amountStrForI);
517
+ const need = amt + gasCost;
518
+ if (!skipIfInsufficient || bal >= need)
519
+ toSend = amt;
520
+ }
521
+ sweepAmounts.push(toSend);
522
+ totalAmountBeforeProfit += toSend;
523
+ // ✅ 找出归集金额最大的钱包
524
+ if (toSend > maxSweepAmount) {
525
+ maxSweepAmount = toSend;
526
+ maxSweepIndex = i;
527
+ }
528
+ if (extractProfit && toSend > 0n) {
529
+ const { profit } = calculateProfit(toSend, config);
530
+ totalProfit += profit;
531
+ }
532
+ }
533
+ // 检查支付者余额
534
+ if (extractProfit && totalProfit > 0n && maxSweepIndex >= 0) {
535
+ const payerBalance = balances[maxSweepIndex];
536
+ const payerSweepAmount = sweepAmounts[maxSweepIndex];
537
+ const payerNeedGas = gasCostBase + profitTxGas;
538
+ if (payerBalance < payerSweepAmount + payerNeedGas) {
539
+ const maxPayerSweep = payerBalance > payerNeedGas ? payerBalance - payerNeedGas : 0n;
540
+ sweepAmounts[maxSweepIndex] = maxPayerSweep;
541
+ totalAmountBeforeProfit = sweepAmounts.reduce((sum, amt) => sum + amt, 0n);
542
+ totalProfit = 0n;
543
+ for (let i = 0; i < sweepAmounts.length; i++) {
544
+ if (sweepAmounts[i] > 0n) {
545
+ totalProfit += calculateProfit(sweepAmounts[i], config).profit;
546
+ }
547
+ }
548
+ }
549
+ }
550
+ // 第二步:生成归集交易
551
+ let payerProfitNonce;
552
+ if (extractProfit && totalProfit > 0n && maxSweepIndex >= 0) {
553
+ const payerWallet = wallets[maxSweepIndex];
554
+ const nonces = await nonceManager.getNextNonceBatch(payerWallet, 2);
555
+ payerProfitNonce = nonces[1];
556
+ }
557
+ const walletsToSweep = wallets.filter((_, i) => sweepAmounts[i] > 0n && i !== maxSweepIndex);
558
+ const nonces = walletsToSweep.length > 0
559
+ ? await nonceManager.getNextNoncesForWallets(walletsToSweep)
560
+ : [];
561
+ let nonceIdx = 0;
562
+ const txPromises = wallets.map(async (w, i) => {
563
+ const toSend = sweepAmounts[i];
564
+ if (toSend <= 0n)
565
+ return null;
566
+ let actualToSend = toSend;
567
+ if (extractProfit) {
568
+ if (i === maxSweepIndex && totalProfit > 0n) {
569
+ actualToSend = toSend - totalProfit;
570
+ }
571
+ }
572
+ let nonce;
573
+ if (i === maxSweepIndex && payerProfitNonce !== undefined) {
574
+ nonce = payerProfitNonce - 1;
575
+ }
576
+ else {
577
+ nonce = nonces[nonceIdx++];
578
+ }
579
+ const mainTx = await w.signTransaction({
580
+ to: target,
581
+ value: actualToSend,
582
+ nonce,
583
+ gasPrice,
584
+ gasLimit: nativeGasLimit,
585
+ chainId: chainIdNum,
586
+ type: txType
587
+ });
588
+ return mainTx;
589
+ });
590
+ const allTxs = (await Promise.all(txPromises)).filter(tx => tx !== null);
591
+ signedTxs.push(...allTxs);
592
+ // 第三步:生成利润交易
593
+ if (extractProfit && totalProfit > 0n && maxSweepIndex >= 0 && payerProfitNonce !== undefined) {
594
+ const payerWallet = wallets[maxSweepIndex];
595
+ const profitTx = await payerWallet.signTransaction({
596
+ to: getProfitRecipient(),
597
+ value: totalProfit,
598
+ nonce: payerProfitNonce,
599
+ gasPrice,
600
+ gasLimit: nativeGasLimit,
601
+ chainId: chainIdNum,
602
+ type: txType
603
+ });
604
+ signedTxs.push(profitTx);
605
+ }
606
+ }
607
+ else {
608
+ // ERC20 归集
609
+ const [gasPrice, decimals, balances, nativeBalances] = await Promise.all([
610
+ getOptimizedGasPrice(provider, getGasPriceConfig(config)),
611
+ Promise.resolve(tokenDecimals ?? await getErc20Decimals(provider, tokenAddress)),
612
+ batchGetBalances(provider, addresses, tokenAddress),
613
+ batchGetBalances(provider, addresses)
614
+ ]);
615
+ const iface = new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
616
+ const profitTxGas = finalGasLimit * gasPrice;
617
+ const sweepAmounts = [];
618
+ let maxSweepIndex = -1;
619
+ let maxSweepAmount = 0n;
620
+ for (let i = 0; i < wallets.length; i++) {
621
+ const bal = balances[i];
622
+ let toSend = 0n;
623
+ const ratioForI = (() => {
624
+ if (sources && sources[i] && sources[i].ratioPct !== undefined)
625
+ return clamp(sources[i].ratioPct);
626
+ if (ratios && ratios[i] !== undefined)
627
+ return clamp(ratios[i]);
628
+ return ratio;
629
+ })();
630
+ const amountStrForI = (() => {
631
+ if (sources && sources[i] && sources[i].amount !== undefined)
632
+ return String(sources[i].amount);
633
+ if (amounts && amounts[i] !== undefined)
634
+ return String(amounts[i]);
635
+ return amount !== undefined ? String(amount) : undefined;
636
+ })();
637
+ if (ratioForI !== undefined) {
638
+ toSend = (bal * BigInt(ratioForI)) / 100n;
639
+ }
640
+ else if (amountStrForI && amountStrForI.trim().length > 0) {
641
+ toSend = ethers.parseUnits(amountStrForI, decimals);
642
+ }
643
+ if (toSend <= 0n || (skipIfInsufficient && bal < toSend)) {
644
+ sweepAmounts.push(0n);
645
+ continue;
646
+ }
647
+ const totalGasNeeded = finalGasLimit * gasPrice;
648
+ const nativeBal = nativeBalances[i] ?? 0n;
649
+ if (skipIfInsufficient && nativeBal < totalGasNeeded) {
650
+ sweepAmounts.push(0n);
651
+ continue;
652
+ }
653
+ sweepAmounts.push(toSend);
654
+ totalAmountBeforeProfit += toSend;
655
+ // ✅ 找出归集金额最大的钱包
656
+ if (toSend > maxSweepAmount) {
657
+ maxSweepAmount = toSend;
658
+ maxSweepIndex = i;
659
+ }
660
+ if (extractProfit && toSend > 0n) {
661
+ const { profit } = calculateProfit(toSend, config);
662
+ totalProfit += profit;
663
+ }
664
+ }
665
+ // ERC20 归集:获取代币利润等值的原生代币报价
666
+ let nativeProfitAmount = 0n;
667
+ if (extractProfit && totalProfit > 0n) {
668
+ nativeProfitAmount = await getTokenToNativeQuote(provider, tokenAddress, totalProfit, chainIdNum);
669
+ }
670
+ // 检查支付者原生代币余额
671
+ if (extractProfit && nativeProfitAmount > 0n && maxSweepIndex >= 0) {
672
+ const payerNativeBalance = nativeBalances[maxSweepIndex] ?? 0n;
673
+ const payerNeedGas = finalGasLimit * gasPrice + profitTxGas + nativeProfitAmount;
674
+ if (payerNativeBalance < payerNeedGas) {
675
+ nativeProfitAmount = 0n;
676
+ }
677
+ }
678
+ let payerProfitNonce;
679
+ if (extractProfit && nativeProfitAmount > 0n && maxSweepIndex >= 0) {
680
+ const payerWallet = wallets[maxSweepIndex];
681
+ const nonces = await nonceManager.getNextNonceBatch(payerWallet, 2);
682
+ payerProfitNonce = nonces[1];
683
+ }
684
+ const walletsToSweepErc20 = wallets.filter((_, i) => sweepAmounts[i] > 0n && i !== maxSweepIndex);
685
+ const noncesErc20 = walletsToSweepErc20.length > 0
686
+ ? await nonceManager.getNextNoncesForWallets(walletsToSweepErc20)
687
+ : [];
688
+ let nonceIdxErc20 = 0;
689
+ const txPromises = wallets.map(async (w, i) => {
690
+ const toSend = sweepAmounts[i];
691
+ if (toSend <= 0n)
692
+ return null;
693
+ let nonce;
694
+ if (i === maxSweepIndex && payerProfitNonce !== undefined) {
695
+ nonce = payerProfitNonce - 1;
696
+ }
697
+ else {
698
+ nonce = noncesErc20[nonceIdxErc20++];
699
+ }
700
+ const data = iface.encodeFunctionData('transfer', [target, toSend]);
701
+ const mainTx = await w.signTransaction({
702
+ to: tokenAddress,
703
+ data,
704
+ value: 0n,
705
+ nonce,
706
+ gasPrice,
707
+ gasLimit: finalGasLimit,
708
+ chainId: chainIdNum,
709
+ type: txType
710
+ });
711
+ return mainTx;
712
+ });
713
+ const allTxs = (await Promise.all(txPromises)).filter(tx => tx !== null);
714
+ signedTxs.push(...allTxs);
715
+ if (extractProfit && nativeProfitAmount > 0n && maxSweepIndex >= 0 && payerProfitNonce !== undefined) {
716
+ const payerWallet = wallets[maxSweepIndex];
717
+ totalProfit = nativeProfitAmount;
718
+ const profitTx = await payerWallet.signTransaction({
719
+ to: getProfitRecipient(),
720
+ value: nativeProfitAmount,
721
+ nonce: payerProfitNonce,
722
+ gasPrice,
723
+ gasLimit: 21000n,
724
+ chainId: chainIdNum,
725
+ type: txType
726
+ });
727
+ signedTxs.push(profitTx);
728
+ }
729
+ }
730
+ }
731
+ else {
732
+ // ========== 有多跳:构建跳转链归集 ==========
733
+ const sourceWallets = actualKeys.map(pk => new Wallet(pk, provider));
734
+ const withHopIndexes = [];
735
+ const withoutHopIndexes = [];
736
+ for (let i = 0; i < preparedHops.length; i++) {
737
+ if (preparedHops[i].length > 0) {
738
+ withHopIndexes.push(i);
739
+ }
740
+ else {
741
+ withoutHopIndexes.push(i);
742
+ }
743
+ }
744
+ const sourceAddresses = sourceWallets.map(w => w.address);
745
+ const [gasPrice, decimals, balances, nativeBalances] = await Promise.all([
746
+ getOptimizedGasPrice(provider, getGasPriceConfig(config)),
747
+ isNative ? Promise.resolve(18) : Promise.resolve(tokenDecimals ?? await getErc20Decimals(provider, tokenAddress)),
748
+ batchGetBalances(provider, sourceAddresses, tokenAddress),
749
+ isNative ? Promise.resolve([]) : batchGetBalances(provider, sourceAddresses)
750
+ ]);
751
+ const iface = isNative ? null : new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
752
+ const gasFeePerHop = finalGasLimit * gasPrice;
753
+ const nonceManager = new NonceManager(provider);
754
+ const sweepAmounts = new Array(sourceWallets.length).fill(0n);
755
+ // 处理无跳转的地址
756
+ for (const i of withoutHopIndexes) {
757
+ const sourceWallet = sourceWallets[i];
758
+ const bal = balances[i];
759
+ let toSend = 0n;
760
+ if (isNative) {
761
+ const gasCost = nativeGasLimit * gasPrice;
762
+ const ratioForI = (() => {
763
+ if (sources && sources[i] && sources[i].ratioPct !== undefined)
764
+ return clamp(sources[i].ratioPct);
765
+ if (ratios && ratios[i] !== undefined)
766
+ return clamp(ratios[i]);
767
+ return ratio;
768
+ })();
769
+ const amountStrForI = (() => {
770
+ if (sources && sources[i] && sources[i].amount !== undefined)
771
+ return String(sources[i].amount);
772
+ if (amounts && amounts[i] !== undefined)
773
+ return String(amounts[i]);
774
+ return amount !== undefined ? String(amount) : undefined;
775
+ })();
776
+ if (ratioForI !== undefined) {
777
+ const want = (bal * BigInt(ratioForI)) / 100n;
778
+ const maxSendable = bal > gasCost ? (bal - gasCost) : 0n;
779
+ toSend = want > maxSendable ? maxSendable : want;
780
+ }
781
+ else if (amountStrForI && amountStrForI.trim().length > 0) {
782
+ const amt = ethers.parseEther(amountStrForI);
783
+ const need = amt + gasCost;
784
+ if (!skipIfInsufficient || bal >= need)
785
+ toSend = amt;
786
+ }
787
+ if (toSend > 0n) {
788
+ sweepAmounts[i] = toSend;
789
+ totalAmountBeforeProfit += toSend;
790
+ const nonce = await nonceManager.getNextNonce(sourceWallet);
791
+ const tx = await sourceWallet.signTransaction({
792
+ to: target,
793
+ value: toSend,
794
+ nonce,
795
+ gasPrice,
796
+ gasLimit: nativeGasLimit,
797
+ chainId: chainIdNum,
798
+ type: txType
799
+ });
800
+ signedTxs.push(tx);
801
+ }
802
+ }
803
+ else {
804
+ const ratioForI = (() => {
805
+ if (sources && sources[i] && sources[i].ratioPct !== undefined)
806
+ return clamp(sources[i].ratioPct);
807
+ if (ratios && ratios[i] !== undefined)
808
+ return clamp(ratios[i]);
809
+ return ratio;
810
+ })();
811
+ const amountStrForI = (() => {
812
+ if (sources && sources[i] && sources[i].amount !== undefined)
813
+ return String(sources[i].amount);
814
+ if (amounts && amounts[i] !== undefined)
815
+ return String(amounts[i]);
816
+ return amount !== undefined ? String(amount) : undefined;
817
+ })();
818
+ if (ratioForI !== undefined) {
819
+ toSend = (bal * BigInt(ratioForI)) / 100n;
820
+ }
821
+ else if (amountStrForI && amountStrForI.trim().length > 0) {
822
+ toSend = ethers.parseUnits(amountStrForI, decimals);
823
+ }
824
+ if (toSend > 0n && (!skipIfInsufficient || bal >= toSend)) {
825
+ sweepAmounts[i] = toSend;
826
+ totalAmountBeforeProfit += toSend;
827
+ const nonce = await nonceManager.getNextNonce(sourceWallet);
828
+ const data = iface.encodeFunctionData('transfer', [target, toSend]);
829
+ const tx = await sourceWallet.signTransaction({
830
+ to: tokenAddress,
831
+ data,
832
+ value: 0n,
833
+ nonce,
834
+ gasPrice,
835
+ gasLimit: finalGasLimit,
836
+ chainId: chainIdNum,
837
+ type: txType
838
+ });
839
+ signedTxs.push(tx);
840
+ }
841
+ }
842
+ }
843
+ // 处理有跳转的地址
844
+ for (const i of withHopIndexes) {
845
+ const sourceWallet = sourceWallets[i];
846
+ const hopChain = preparedHops[i];
847
+ let toSend = 0n;
848
+ const bal = balances[i];
849
+ if (isNative) {
850
+ const totalGasCost = finalGasLimit * gasPrice * BigInt(hopChain.length + 1);
851
+ const ratioForI = (() => {
852
+ if (sources && sources[i] && sources[i].ratioPct !== undefined)
853
+ return clamp(sources[i].ratioPct);
854
+ if (ratios && ratios[i] !== undefined)
855
+ return clamp(ratios[i]);
856
+ return ratio;
857
+ })();
858
+ const amountStrForI = (() => {
859
+ if (sources && sources[i] && sources[i].amount !== undefined)
860
+ return String(sources[i].amount);
861
+ if (amounts && amounts[i] !== undefined)
862
+ return String(amounts[i]);
863
+ return amount !== undefined ? String(amount) : undefined;
864
+ })();
865
+ if (ratioForI !== undefined) {
866
+ const want = (bal * BigInt(ratioForI)) / 100n;
867
+ const maxSendable = bal > totalGasCost ? (bal - totalGasCost) : 0n;
868
+ toSend = want > maxSendable ? maxSendable : want;
869
+ }
870
+ else if (amountStrForI && amountStrForI.trim().length > 0) {
871
+ const amt = ethers.parseEther(amountStrForI);
872
+ const need = amt + totalGasCost;
873
+ if (!skipIfInsufficient || bal >= need)
874
+ toSend = amt;
875
+ }
876
+ }
877
+ else {
878
+ const nativeBal = nativeBalances[i];
879
+ const nativeNeeded = (gasFeePerHop * BigInt(hopChain.length)) + (finalGasLimit * gasPrice);
880
+ if (nativeBal < nativeNeeded && skipIfInsufficient)
881
+ continue;
882
+ const ratioForI = (() => {
883
+ if (sources && sources[i] && sources[i].ratioPct !== undefined)
884
+ return clamp(sources[i].ratioPct);
885
+ if (ratios && ratios[i] !== undefined)
886
+ return clamp(ratios[i]);
887
+ return ratio;
888
+ })();
889
+ const amountStrForI = (() => {
890
+ if (sources && sources[i] && sources[i].amount !== undefined)
891
+ return String(sources[i].amount);
892
+ if (amounts && amounts[i] !== undefined)
893
+ return String(amounts[i]);
894
+ return amount !== undefined ? String(amount) : undefined;
895
+ })();
896
+ if (ratioForI !== undefined) {
897
+ toSend = (bal * BigInt(ratioForI)) / 100n;
898
+ }
899
+ else if (amountStrForI && amountStrForI.trim().length > 0) {
900
+ toSend = ethers.parseUnits(amountStrForI, decimals);
901
+ }
902
+ if (skipIfInsufficient && bal < toSend)
903
+ toSend = 0n;
904
+ }
905
+ if (toSend <= 0n)
906
+ continue;
907
+ sweepAmounts[i] = toSend;
908
+ totalAmountBeforeProfit += toSend;
909
+ const fullChain = [sourceWallet, ...hopChain.map(pk => new Wallet(pk, provider))];
910
+ const addresses = [...fullChain.map(w => w.address), target];
911
+ if (!isNative) {
912
+ const gasNonces = await nonceManager.getNextNonceBatch(sourceWallet, hopChain.length);
913
+ for (let j = 0; j < hopChain.length; j++) {
914
+ const tx = await sourceWallet.signTransaction({
915
+ to: fullChain[j + 1].address,
916
+ value: gasFeePerHop,
917
+ nonce: gasNonces[j],
918
+ gasPrice,
919
+ gasLimit: nativeGasLimit,
920
+ chainId: chainIdNum,
921
+ type: txType
922
+ });
923
+ signedTxs.push(tx);
924
+ }
925
+ }
926
+ for (let j = 0; j < addresses.length - 1; j++) {
927
+ const fromWallet = fullChain[j];
928
+ const toAddress = addresses[j + 1];
929
+ const nonce = j === 0
930
+ ? await nonceManager.getNextNonce(sourceWallet)
931
+ : 0;
932
+ if (isNative) {
933
+ const remainingHops = addresses.length - 2 - j;
934
+ const additionalGas = gasFeePerHop * BigInt(remainingHops);
935
+ const valueToTransfer = toSend + additionalGas;
936
+ const tx = await fromWallet.signTransaction({
937
+ to: toAddress,
938
+ value: valueToTransfer,
939
+ nonce,
940
+ gasPrice,
941
+ gasLimit: finalGasLimit,
942
+ chainId: chainIdNum,
943
+ type: txType
944
+ });
945
+ signedTxs.push(tx);
946
+ }
947
+ else {
948
+ const data = iface.encodeFunctionData('transfer', [toAddress, toSend]);
949
+ const tx = await fromWallet.signTransaction({
950
+ to: tokenAddress,
951
+ data,
952
+ value: 0n,
953
+ nonce,
954
+ gasPrice,
955
+ gasLimit: finalGasLimit,
956
+ chainId: chainIdNum,
957
+ type: txType
958
+ });
959
+ signedTxs.push(tx);
960
+ }
961
+ }
962
+ }
963
+ // 多跳模式:计算利润并添加利润转账
964
+ if (extractProfit && totalAmountBeforeProfit > 0n) {
965
+ // ✅ 找出归集金额最大的钱包作为支付者
966
+ let maxSweepIndex = -1;
967
+ let maxSweepAmount = 0n;
968
+ for (let i = 0; i < sweepAmounts.length; i++) {
969
+ if (sweepAmounts[i] > maxSweepAmount) {
970
+ maxSweepAmount = sweepAmounts[i];
971
+ maxSweepIndex = i;
972
+ }
973
+ }
974
+ let totalTokenProfit = 0n;
975
+ for (let i = 0; i < sweepAmounts.length; i++) {
976
+ if (sweepAmounts[i] > 0n) {
977
+ const { profit } = calculateProfit(sweepAmounts[i], config);
978
+ if (isNative) {
979
+ totalProfit += profit;
980
+ }
981
+ else {
982
+ totalTokenProfit += profit;
983
+ }
984
+ }
985
+ }
986
+ let nativeProfitAmount = isNative ? totalProfit : 0n;
987
+ if (!isNative && totalTokenProfit > 0n) {
988
+ nativeProfitAmount = await getTokenToNativeQuote(provider, tokenAddress, totalTokenProfit, chainIdNum);
989
+ totalProfit = nativeProfitAmount;
990
+ }
991
+ if (nativeProfitAmount > 0n && maxSweepIndex >= 0) {
992
+ const payerWallet = sourceWallets[maxSweepIndex];
993
+ const profitNonce = await nonceManager.getNextNonce(payerWallet);
994
+ const profitTx = await payerWallet.signTransaction({
995
+ to: getProfitRecipient(),
996
+ value: nativeProfitAmount,
997
+ nonce: profitNonce,
998
+ gasPrice,
999
+ gasLimit: 21000n,
1000
+ chainId: chainIdNum,
1001
+ type: txType
1002
+ });
1003
+ signedTxs.push(profitTx);
1004
+ }
1005
+ }
1006
+ }
1007
+ return {
1008
+ signedTransactions: signedTxs,
1009
+ hopWallets: preparedHops || undefined,
1010
+ metadata: extractProfit ? {
1011
+ totalAmount: isNative ? ethers.formatEther(totalAmountBeforeProfit) : ethers.formatUnits(totalAmountBeforeProfit, tokenDecimals ?? 18),
1012
+ profitAmount: ethers.formatEther(totalProfit),
1013
+ profitRecipient: getProfitRecipient(),
1014
+ sourceCount: actualKeys.length,
1015
+ isNative,
1016
+ tokenAddress: isNative ? undefined : tokenAddress
1017
+ } : undefined
1018
+ };
1019
+ }