four-flap-meme-sdk 1.4.35 → 1.4.37
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/contracts/tm-bundle-merkle/internal.d.ts +18 -0
- package/dist/contracts/tm-bundle-merkle/internal.js +66 -2
- package/dist/contracts/tm-bundle-merkle/types.d.ts +1 -0
- package/dist/contracts/tm-bundle-merkle/utils.js +281 -101
- package/dist/flap/portal-bundle-merkle/utils.js +2 -2
- package/package.json +1 -1
|
@@ -12,6 +12,24 @@ export declare function normalizeAmounts(recipients: string[], amount?: AmountLi
|
|
|
12
12
|
* ✅ 优化:原生代币也使用 Multicall3 批量获取,减少 RPC 调用
|
|
13
13
|
*/
|
|
14
14
|
export declare function batchGetBalances(provider: JsonRpcProvider, addresses: string[], tokenAddress?: string): Promise<bigint[]>;
|
|
15
|
+
/**
|
|
16
|
+
* 通过模拟交易获取 ERC20 转账的最小 Gas Limit
|
|
17
|
+
* ✅ 使用 eth_estimateGas 预估实际需要的 gas,然后加一个小的缓冲量
|
|
18
|
+
*
|
|
19
|
+
* @param provider - Provider 实例
|
|
20
|
+
* @param tokenAddress - ERC20 代币地址
|
|
21
|
+
* @param from - 发送方地址
|
|
22
|
+
* @param to - 接收方地址
|
|
23
|
+
* @param amount - 转账金额(wei)
|
|
24
|
+
* @param bufferPercent - 缓冲百分比(默认 5%)
|
|
25
|
+
* @returns 预估的 gas limit
|
|
26
|
+
*/
|
|
27
|
+
export declare function estimateErc20TransferGas(provider: JsonRpcProvider, tokenAddress: string, from: string, to: string, amount: bigint, bufferPercent?: number): Promise<bigint>;
|
|
28
|
+
/**
|
|
29
|
+
* 批量预估多个 ERC20 转账的 Gas Limit
|
|
30
|
+
* ✅ 选取最大值作为统一的 gas limit(确保所有转账都能成功)
|
|
31
|
+
*/
|
|
32
|
+
export declare function estimateMaxErc20TransferGas(provider: JsonRpcProvider, tokenAddress: string, from: string, recipients: string[], amounts: bigint[], bufferPercent?: number): Promise<bigint>;
|
|
15
33
|
/**
|
|
16
34
|
* 计算 Gas Limit
|
|
17
35
|
* - 原生代币转账:21000 gas(固定)
|
|
@@ -122,6 +122,70 @@ export async function batchGetBalances(provider, addresses, tokenAddress) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* 通过模拟交易获取 ERC20 转账的最小 Gas Limit
|
|
127
|
+
* ✅ 使用 eth_estimateGas 预估实际需要的 gas,然后加一个小的缓冲量
|
|
128
|
+
*
|
|
129
|
+
* @param provider - Provider 实例
|
|
130
|
+
* @param tokenAddress - ERC20 代币地址
|
|
131
|
+
* @param from - 发送方地址
|
|
132
|
+
* @param to - 接收方地址
|
|
133
|
+
* @param amount - 转账金额(wei)
|
|
134
|
+
* @param bufferPercent - 缓冲百分比(默认 5%)
|
|
135
|
+
* @returns 预估的 gas limit
|
|
136
|
+
*/
|
|
137
|
+
export async function estimateErc20TransferGas(provider, tokenAddress, from, to, amount, bufferPercent = 5) {
|
|
138
|
+
try {
|
|
139
|
+
const iface = new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
|
|
140
|
+
const data = iface.encodeFunctionData('transfer', [to, amount]);
|
|
141
|
+
const estimatedGas = await provider.estimateGas({
|
|
142
|
+
from,
|
|
143
|
+
to: tokenAddress,
|
|
144
|
+
data,
|
|
145
|
+
value: 0n
|
|
146
|
+
});
|
|
147
|
+
// 添加缓冲量(默认 5%)
|
|
148
|
+
const buffer = (estimatedGas * BigInt(bufferPercent)) / 100n;
|
|
149
|
+
const finalGas = estimatedGas + buffer;
|
|
150
|
+
console.log(`[estimateErc20TransferGas] 预估 gas: ${estimatedGas}, 最终 gas limit: ${finalGas} (+${bufferPercent}%)`);
|
|
151
|
+
return finalGas;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.warn(`[estimateErc20TransferGas] 预估失败,使用默认值:`, error);
|
|
155
|
+
// 回退到默认值
|
|
156
|
+
return 52000n;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 批量预估多个 ERC20 转账的 Gas Limit
|
|
161
|
+
* ✅ 选取最大值作为统一的 gas limit(确保所有转账都能成功)
|
|
162
|
+
*/
|
|
163
|
+
export async function estimateMaxErc20TransferGas(provider, tokenAddress, from, recipients, amounts, bufferPercent = 5) {
|
|
164
|
+
if (recipients.length === 0)
|
|
165
|
+
return 52000n;
|
|
166
|
+
// 为了减少 RPC 调用,最多预估前 3 个和最后 1 个
|
|
167
|
+
const sampleIndices = [];
|
|
168
|
+
sampleIndices.push(0); // 第一个
|
|
169
|
+
if (recipients.length > 1)
|
|
170
|
+
sampleIndices.push(Math.min(1, recipients.length - 1));
|
|
171
|
+
if (recipients.length > 2)
|
|
172
|
+
sampleIndices.push(Math.min(2, recipients.length - 1));
|
|
173
|
+
if (recipients.length > 3)
|
|
174
|
+
sampleIndices.push(recipients.length - 1); // 最后一个
|
|
175
|
+
// 去重
|
|
176
|
+
const uniqueIndices = [...new Set(sampleIndices)];
|
|
177
|
+
try {
|
|
178
|
+
const estimates = await Promise.all(uniqueIndices.map(i => estimateErc20TransferGas(provider, tokenAddress, from, recipients[i], amounts[i], bufferPercent)));
|
|
179
|
+
// 取最大值
|
|
180
|
+
const maxGas = estimates.reduce((max, gas) => gas > max ? gas : max, 0n);
|
|
181
|
+
console.log(`[estimateMaxErc20TransferGas] 样本数: ${uniqueIndices.length}, 最大 gas limit: ${maxGas}`);
|
|
182
|
+
return maxGas;
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
console.warn(`[estimateMaxErc20TransferGas] 批量预估失败,使用默认值:`, error);
|
|
186
|
+
return 52000n;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
125
189
|
/**
|
|
126
190
|
* 计算 Gas Limit
|
|
127
191
|
* - 原生代币转账:21000 gas(固定)
|
|
@@ -133,8 +197,8 @@ export function calculateGasLimit(config, isNative, hasHops, hopCount = 0) {
|
|
|
133
197
|
if (config.gasLimit !== undefined) {
|
|
134
198
|
return BigInt(config.gasLimit);
|
|
135
199
|
}
|
|
136
|
-
// ✅ 原生代币: 21000, ERC20 标准 transfer:
|
|
137
|
-
const baseGas = isNative ? 21000 :
|
|
200
|
+
// ✅ 原生代币: 21000, ERC20 标准 transfer: 48000(USDT 最低约 46815)
|
|
201
|
+
const baseGas = isNative ? 21000 : 46815;
|
|
138
202
|
// ✅ 多跳时只需要累加单次转账的 gas,不需要额外乘数
|
|
139
203
|
// 每个中转钱包只执行一笔 transfer
|
|
140
204
|
if (hasHops && hopCount > 0) {
|
|
@@ -11,6 +11,7 @@ export type FourSignConfig = {
|
|
|
11
11
|
chainId?: number;
|
|
12
12
|
prefer21000ForNative?: boolean;
|
|
13
13
|
checkBnbForErc20NoHop?: boolean;
|
|
14
|
+
estimateGas?: boolean;
|
|
14
15
|
customSubmitFn?: (signedTxs: string[]) => Promise<string>;
|
|
15
16
|
spPrivateKey?: string;
|
|
16
17
|
spMode?: 'none' | 'timestampPersonalSign' | 'concatTxHash' | 'rawTimestamp';
|
|
@@ -2,7 +2,7 @@ import { ethers, Wallet } from 'ethers';
|
|
|
2
2
|
import { getOptimizedGasPrice, NonceManager } from '../../utils/bundle-helpers.js';
|
|
3
3
|
import { PROFIT_CONFIG, ZERO_ADDRESS } from '../../utils/constants.js';
|
|
4
4
|
import { getTxType, getGasPriceConfig, shouldExtractProfit, getProfitRecipient } from './config.js';
|
|
5
|
-
import { getErc20DecimalsMerkle as _getErc20DecimalsMerkle, generateHopWallets as _generateHopWallets, normalizeAmounts as _normalizeAmounts, batchGetBalances as _batchGetBalances, calculateGasLimit as _calculateGasLimit, isNativeTokenAddress as _isNativeTokenAddress } from './internal.js';
|
|
5
|
+
import { getErc20DecimalsMerkle as _getErc20DecimalsMerkle, generateHopWallets as _generateHopWallets, normalizeAmounts as _normalizeAmounts, batchGetBalances as _batchGetBalances, calculateGasLimit as _calculateGasLimit, isNativeTokenAddress as _isNativeTokenAddress, estimateErc20TransferGas as _estimateErc20TransferGas } from './internal.js';
|
|
6
6
|
// ==================== 本地利润计算(万分之三)====================
|
|
7
7
|
/**
|
|
8
8
|
* 计算利润金额(万分之三)
|
|
@@ -355,26 +355,45 @@ export async function disperseWithBundleMerkle(params) {
|
|
|
355
355
|
isNative ? Promise.resolve(18) : Promise.resolve(tokenDecimals ?? await _getErc20DecimalsMerkle(provider, tokenAddress, chainIdNum))
|
|
356
356
|
]);
|
|
357
357
|
const iface = isNative ? null : new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
|
|
358
|
-
// ✅
|
|
359
|
-
|
|
360
|
-
|
|
358
|
+
// ✅ Gas limit 设置
|
|
359
|
+
// - 原生代币转账:21000(固定)
|
|
360
|
+
// - ERC20 转账:通过模拟交易获取最小 gas limit(减少中转钱包 BNB 残留)
|
|
361
|
+
const nativeTransferGasLimit = 21000n;
|
|
362
|
+
// ✅ ERC20 多跳:模拟交易获取精确的 gas limit
|
|
363
|
+
let erc20TransferGasLimit = finalGasLimit;
|
|
364
|
+
if (!isNative && config.estimateGas !== false) {
|
|
365
|
+
try {
|
|
366
|
+
// 使用第一个接收者模拟一笔转账,获取精确的 gas limit
|
|
367
|
+
const sampleAmount = ethers.parseUnits(normalizedAmounts[0], decimals);
|
|
368
|
+
const estimatedGas = await _estimateErc20TransferGas(provider, tokenAddress, mainWallet.address, recipients[0], sampleAmount, 0 // ✅ 不加缓冲,最大化减少多跳钱包残留
|
|
369
|
+
);
|
|
370
|
+
erc20TransferGasLimit = estimatedGas;
|
|
371
|
+
console.log(`[disperseWithBundleMerkle] 使用模拟估算的 ERC20 gas limit: ${erc20TransferGasLimit}`);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
console.warn(`[disperseWithBundleMerkle] 模拟估算失败,使用默认值:`, error);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// ✅ 原生代币多跳的 gas 费(每跳只需要 21000)
|
|
378
|
+
const nativeHopGasFee = nativeTransferGasLimit * gasPrice;
|
|
379
|
+
// ✅ ERC20 多跳时,中转钱包需要执行 2 笔交易(转 gas + 转 ERC20)
|
|
380
|
+
const erc20HopGasFee = erc20TransferGasLimit * gasPrice;
|
|
381
|
+
const nativeHopGasFeeForErc20 = nativeTransferGasLimit * gasPrice;
|
|
361
382
|
// ✅ 优化:预先计算主钱包需要的总 nonce 数量
|
|
383
|
+
// - 原生代币多跳:主钱包只需要 1 个 nonce(一笔转账包含后续所有 gas)
|
|
384
|
+
// - ERC20 多跳:主钱包需要 2 个 nonce(转 gas + 转 ERC20)
|
|
362
385
|
let mainWalletNonceCount = 0;
|
|
363
|
-
const recipientNonceNeeds = [];
|
|
364
386
|
for (let i = 0; i < recipients.length; i++) {
|
|
365
387
|
const hopChain = preparedHops[i];
|
|
366
388
|
if (hopChain.length === 0) {
|
|
367
389
|
// 无跳转:1 个 nonce
|
|
368
|
-
recipientNonceNeeds.push(1);
|
|
369
390
|
mainWalletNonceCount += 1;
|
|
370
391
|
}
|
|
371
392
|
else {
|
|
372
393
|
// 有跳转:
|
|
373
|
-
// -
|
|
374
|
-
// -
|
|
375
|
-
|
|
376
|
-
recipientNonceNeeds.push(nonceNeed);
|
|
377
|
-
mainWalletNonceCount += nonceNeed;
|
|
394
|
+
// - Native: 1(一笔转账)
|
|
395
|
+
// - ERC20: 2(转 gas + 转 ERC20)
|
|
396
|
+
mainWalletNonceCount += isNative ? 1 : 2;
|
|
378
397
|
}
|
|
379
398
|
}
|
|
380
399
|
// 利润交易需要额外 1 个 nonce
|
|
@@ -444,32 +463,14 @@ export async function disperseWithBundleMerkle(params) {
|
|
|
444
463
|
// 有跳转
|
|
445
464
|
const fullChain = [mainWallet, ...hopChain.map(pk => new Wallet(pk, provider))];
|
|
446
465
|
const addresses = [...fullChain.map(w => w.address), finalRecipient];
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
for (let j = 0; j <
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
tx: {
|
|
454
|
-
to: fullChain[j + 1].address,
|
|
455
|
-
value: gasFeePerHop,
|
|
456
|
-
nonce,
|
|
457
|
-
gasPrice,
|
|
458
|
-
gasLimit: nativeGasLimit,
|
|
459
|
-
chainId: chainIdNum,
|
|
460
|
-
type: txType
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
// 执行主要的转账链
|
|
466
|
-
for (let j = 0; j < addresses.length - 1; j++) {
|
|
467
|
-
const fromWallet = fullChain[j];
|
|
468
|
-
const toAddress = addresses[j + 1];
|
|
469
|
-
const nonce = j === 0 ? allMainNonces[mainNonceIdx++] : 0;
|
|
470
|
-
if (isNative) {
|
|
466
|
+
if (isNative) {
|
|
467
|
+
// ========== 原生代币多跳:gas 包含在转账金额中逐层传递 ==========
|
|
468
|
+
for (let j = 0; j < addresses.length - 1; j++) {
|
|
469
|
+
const fromWallet = fullChain[j];
|
|
470
|
+
const toAddress = addresses[j + 1];
|
|
471
|
+
const nonce = j === 0 ? allMainNonces[mainNonceIdx++] : 0;
|
|
471
472
|
const remainingHops = addresses.length - 2 - j;
|
|
472
|
-
const additionalGas =
|
|
473
|
+
const additionalGas = nativeHopGasFee * BigInt(remainingHops);
|
|
473
474
|
const transferValue = amountWei + additionalGas;
|
|
474
475
|
txsToSign.push({
|
|
475
476
|
wallet: fromWallet,
|
|
@@ -478,27 +479,112 @@ export async function disperseWithBundleMerkle(params) {
|
|
|
478
479
|
value: transferValue,
|
|
479
480
|
nonce,
|
|
480
481
|
gasPrice,
|
|
481
|
-
gasLimit:
|
|
482
|
+
gasLimit: nativeTransferGasLimit,
|
|
482
483
|
chainId: chainIdNum,
|
|
483
484
|
type: txType
|
|
484
485
|
}
|
|
485
486
|
});
|
|
486
487
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
// ========== ERC20 多跳:gas 也逐层传递(保护隐私)==========
|
|
491
|
+
// 计算每个中转钱包需要的 gas(从后往前)
|
|
492
|
+
// - 最后一个中转钱包:只需要 ERC20 转账的 gas
|
|
493
|
+
// - 其他中转钱包:需要 转 gas 的 gas + ERC20 转账的 gas + 后续所有的 gas
|
|
494
|
+
const hopGasNeeds = [];
|
|
495
|
+
for (let j = hopChain.length - 1; j >= 0; j--) {
|
|
496
|
+
if (j === hopChain.length - 1) {
|
|
497
|
+
// 最后一个中转钱包:只需要 ERC20 转账的 gas
|
|
498
|
+
hopGasNeeds.unshift(erc20HopGasFee);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
// 其他中转钱包:转 gas(21000) + 转 ERC20 + 后续的 gas
|
|
502
|
+
const nextHopGas = hopGasNeeds[0];
|
|
503
|
+
hopGasNeeds.unshift(nativeHopGasFeeForErc20 + erc20HopGasFee + nextHopGas);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// 第一步:主钱包给第一个中转钱包转 gas(包含所有后续的 gas)
|
|
507
|
+
const totalGasForFirstHop = hopGasNeeds[0];
|
|
508
|
+
txsToSign.push({
|
|
509
|
+
wallet: mainWallet,
|
|
510
|
+
tx: {
|
|
511
|
+
to: fullChain[1].address,
|
|
512
|
+
value: totalGasForFirstHop,
|
|
513
|
+
nonce: allMainNonces[mainNonceIdx++],
|
|
514
|
+
gasPrice,
|
|
515
|
+
gasLimit: nativeTransferGasLimit,
|
|
516
|
+
chainId: chainIdNum,
|
|
517
|
+
type: txType
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
// 第二步:主钱包给第一个中转钱包转 ERC20
|
|
521
|
+
const mainToFirstHopData = iface.encodeFunctionData('transfer', [fullChain[1].address, amountWei]);
|
|
522
|
+
txsToSign.push({
|
|
523
|
+
wallet: mainWallet,
|
|
524
|
+
tx: {
|
|
525
|
+
to: tokenAddress,
|
|
526
|
+
data: mainToFirstHopData,
|
|
527
|
+
value: 0n,
|
|
528
|
+
nonce: allMainNonces[mainNonceIdx++],
|
|
529
|
+
gasPrice,
|
|
530
|
+
gasLimit: erc20TransferGasLimit,
|
|
531
|
+
chainId: chainIdNum,
|
|
532
|
+
type: txType
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
// 第三步:中转钱包逐层传递(gas 和 ERC20)
|
|
536
|
+
for (let j = 1; j < fullChain.length; j++) {
|
|
537
|
+
const fromWallet = fullChain[j];
|
|
538
|
+
const toAddress = addresses[j + 1]; // 下一个地址(可能是中转钱包或最终接收者)
|
|
539
|
+
const isLastHop = j === fullChain.length - 1;
|
|
540
|
+
if (!isLastHop) {
|
|
541
|
+
// 非最后一个中转钱包:先转 gas 给下一个中转钱包
|
|
542
|
+
const gasToTransfer = hopGasNeeds[j]; // 下一个中转钱包需要的 gas
|
|
543
|
+
txsToSign.push({
|
|
544
|
+
wallet: fromWallet,
|
|
545
|
+
tx: {
|
|
546
|
+
to: toAddress,
|
|
547
|
+
value: gasToTransfer,
|
|
548
|
+
nonce: 0, // 中转钱包的第一笔交易
|
|
549
|
+
gasPrice,
|
|
550
|
+
gasLimit: nativeTransferGasLimit,
|
|
551
|
+
chainId: chainIdNum,
|
|
552
|
+
type: txType
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
// 再转 ERC20
|
|
556
|
+
const erc20Data = iface.encodeFunctionData('transfer', [toAddress, amountWei]);
|
|
557
|
+
txsToSign.push({
|
|
558
|
+
wallet: fromWallet,
|
|
559
|
+
tx: {
|
|
560
|
+
to: tokenAddress,
|
|
561
|
+
data: erc20Data,
|
|
562
|
+
value: 0n,
|
|
563
|
+
nonce: 1, // 中转钱包的第二笔交易
|
|
564
|
+
gasPrice,
|
|
565
|
+
gasLimit: erc20TransferGasLimit,
|
|
566
|
+
chainId: chainIdNum,
|
|
567
|
+
type: txType
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
// 最后一个中转钱包:只转 ERC20 给最终接收者
|
|
573
|
+
const erc20Data = iface.encodeFunctionData('transfer', [toAddress, amountWei]);
|
|
574
|
+
txsToSign.push({
|
|
575
|
+
wallet: fromWallet,
|
|
576
|
+
tx: {
|
|
577
|
+
to: tokenAddress,
|
|
578
|
+
data: erc20Data,
|
|
579
|
+
value: 0n,
|
|
580
|
+
nonce: 0, // 最后一个中转钱包只有一笔交易
|
|
581
|
+
gasPrice,
|
|
582
|
+
gasLimit: erc20TransferGasLimit,
|
|
583
|
+
chainId: chainIdNum,
|
|
584
|
+
type: txType
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
}
|
|
502
588
|
}
|
|
503
589
|
}
|
|
504
590
|
}
|
|
@@ -915,9 +1001,31 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
915
1001
|
isNative ? Promise.resolve([]) : _batchGetBalances(provider, sourceAddresses)
|
|
916
1002
|
]);
|
|
917
1003
|
const iface = isNative ? null : new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
|
|
918
|
-
// ✅
|
|
919
|
-
const
|
|
920
|
-
|
|
1004
|
+
// ✅ Gas limit 设置(与分散函数保持一致)
|
|
1005
|
+
const nativeTransferGasLimit = 21000n;
|
|
1006
|
+
// ✅ ERC20 多跳:模拟交易获取精确的 gas limit
|
|
1007
|
+
let erc20TransferGasLimit = finalGasLimit;
|
|
1008
|
+
if (!isNative && config.estimateGas !== false) {
|
|
1009
|
+
try {
|
|
1010
|
+
// 找到第一个有余额的钱包来模拟转账
|
|
1011
|
+
const firstWithBalance = balances.findIndex(b => b > 0n);
|
|
1012
|
+
if (firstWithBalance >= 0) {
|
|
1013
|
+
const sampleAmount = balances[firstWithBalance] > 0n ? balances[firstWithBalance] / 2n : 1n;
|
|
1014
|
+
const estimatedGas = await _estimateErc20TransferGas(provider, tokenAddress, sourceAddresses[firstWithBalance], target, sampleAmount, 0 // ✅ 不加缓冲,最大化减少多跳钱包残留
|
|
1015
|
+
);
|
|
1016
|
+
erc20TransferGasLimit = estimatedGas;
|
|
1017
|
+
console.log(`[sweepWithBundleMerkle] 使用模拟估算的 ERC20 gas limit: ${erc20TransferGasLimit}`);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
catch (error) {
|
|
1021
|
+
console.warn(`[sweepWithBundleMerkle] 模拟估算失败,使用默认值:`, error);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
// ✅ 原生代币多跳的 gas 费(每跳只需要 21000)
|
|
1025
|
+
const nativeHopGasFee = nativeTransferGasLimit * gasPrice;
|
|
1026
|
+
// ✅ ERC20 多跳时,中转钱包需要执行 2 笔交易(转 gas + 转 ERC20)
|
|
1027
|
+
const erc20HopGasFee = erc20TransferGasLimit * gasPrice;
|
|
1028
|
+
const nativeHopGasFeeForErc20 = nativeTransferGasLimit * gasPrice;
|
|
921
1029
|
const nonceManager = new NonceManager(provider);
|
|
922
1030
|
// ✅ 用于记录每个钱包的归集金额
|
|
923
1031
|
const sweepAmounts = new Array(sourceWallets.length).fill(0n);
|
|
@@ -1019,7 +1127,8 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
1019
1127
|
const ratioForI = getRatioForI(i);
|
|
1020
1128
|
const amountStrForI = getAmountStrForI(i);
|
|
1021
1129
|
if (isNative) {
|
|
1022
|
-
|
|
1130
|
+
// 原生代币多跳每跳只需要 21000 gas
|
|
1131
|
+
const totalGasCost = nativeTransferGasLimit * gasPrice * BigInt(hopChain.length + 1);
|
|
1023
1132
|
if (ratioForI !== undefined) {
|
|
1024
1133
|
const want = (bal * BigInt(ratioForI)) / 100n;
|
|
1025
1134
|
const maxSendable = bal > totalGasCost ? (bal - totalGasCost) : 0n;
|
|
@@ -1034,7 +1143,13 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
1034
1143
|
}
|
|
1035
1144
|
else {
|
|
1036
1145
|
const bnbBal = bnbBalances[i];
|
|
1037
|
-
|
|
1146
|
+
// 计算 ERC20 多跳所需的 BNB(逐层传递模式):
|
|
1147
|
+
// - 源钱包执行 2 笔交易的 gas(转 gas 21000 + 转 ERC20)
|
|
1148
|
+
// - 源钱包转给第一个中转钱包的 gas(包含后续所有)
|
|
1149
|
+
// 简化计算:每个中转钱包 ≈ (21000 + erc20GasLimit) * gasPrice
|
|
1150
|
+
const sourceGas = (nativeTransferGasLimit + erc20TransferGasLimit) * gasPrice;
|
|
1151
|
+
const hopGas = (nativeHopGasFeeForErc20 + erc20HopGasFee) * BigInt(hopChain.length);
|
|
1152
|
+
const bnbNeeded = sourceGas + hopGas;
|
|
1038
1153
|
if (bnbBal < bnbNeeded && skipIfInsufficient)
|
|
1039
1154
|
continue;
|
|
1040
1155
|
if (ratioForI !== undefined) {
|
|
@@ -1056,9 +1171,9 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
1056
1171
|
}
|
|
1057
1172
|
// ========== 第5步:计算每个有跳转钱包需要的 nonce 数量并批量获取 ==========
|
|
1058
1173
|
// 每个源钱包需要的 nonce 数量:
|
|
1059
|
-
// -
|
|
1060
|
-
// -
|
|
1061
|
-
const hopNonceNeeds = hopTxDataList.map(
|
|
1174
|
+
// - Native: 1(一笔转账包含后续所有 gas)
|
|
1175
|
+
// - ERC20: 2(转 gas + 转 ERC20)
|
|
1176
|
+
const hopNonceNeeds = hopTxDataList.map(() => isNative ? 1 : 2);
|
|
1062
1177
|
const hopSourceWallets = hopTxDataList.map(d => d.sourceWallet);
|
|
1063
1178
|
// ✅ 批量获取所有有跳转钱包的 nonces
|
|
1064
1179
|
const hopNoncesFlat = [];
|
|
@@ -1074,32 +1189,14 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
1074
1189
|
const nonceCount = hopNonceNeeds[dataIdx];
|
|
1075
1190
|
const nonces = hopNoncesFlat.slice(nonceOffset, nonceOffset + nonceCount);
|
|
1076
1191
|
nonceOffset += nonceCount;
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
tx: {
|
|
1084
|
-
to: fullChain[j + 1].address,
|
|
1085
|
-
value: gasFeePerHop,
|
|
1086
|
-
nonce: nonces[nonceIdx++],
|
|
1087
|
-
gasPrice,
|
|
1088
|
-
gasLimit: nativeGasLimit,
|
|
1089
|
-
chainId: chainIdNum,
|
|
1090
|
-
type: txType
|
|
1091
|
-
}
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
// 执行主要的归集链
|
|
1096
|
-
for (let j = 0; j < addresses.length - 1; j++) {
|
|
1097
|
-
const fromWallet = fullChain[j];
|
|
1098
|
-
const toAddress = addresses[j + 1];
|
|
1099
|
-
const nonce = j === 0 ? nonces[nonceIdx++] : 0;
|
|
1100
|
-
if (isNative) {
|
|
1192
|
+
if (isNative) {
|
|
1193
|
+
// ========== 原生代币多跳:gas 包含在转账金额中逐层传递 ==========
|
|
1194
|
+
for (let j = 0; j < addresses.length - 1; j++) {
|
|
1195
|
+
const fromWallet = fullChain[j];
|
|
1196
|
+
const toAddress = addresses[j + 1];
|
|
1197
|
+
const nonce = j === 0 ? nonces[0] : 0;
|
|
1101
1198
|
const remainingHops = addresses.length - 2 - j;
|
|
1102
|
-
const additionalGas =
|
|
1199
|
+
const additionalGas = nativeHopGasFee * BigInt(remainingHops);
|
|
1103
1200
|
const valueToTransfer = toSend + additionalGas;
|
|
1104
1201
|
hopTxsToSign.push({
|
|
1105
1202
|
wallet: fromWallet,
|
|
@@ -1108,27 +1205,110 @@ export async function sweepWithBundleMerkle(params) {
|
|
|
1108
1205
|
value: valueToTransfer,
|
|
1109
1206
|
nonce,
|
|
1110
1207
|
gasPrice,
|
|
1111
|
-
gasLimit:
|
|
1208
|
+
gasLimit: nativeTransferGasLimit,
|
|
1112
1209
|
chainId: chainIdNum,
|
|
1113
1210
|
type: txType
|
|
1114
1211
|
}
|
|
1115
1212
|
});
|
|
1116
1213
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
// ========== ERC20 多跳:gas 也逐层传递(保护隐私)==========
|
|
1217
|
+
// 计算每个中转钱包需要的 gas(从后往前)
|
|
1218
|
+
const hopGasNeeds = [];
|
|
1219
|
+
for (let j = hopChain.length - 1; j >= 0; j--) {
|
|
1220
|
+
if (j === hopChain.length - 1) {
|
|
1221
|
+
// 最后一个中转钱包:只需要 ERC20 转账的 gas
|
|
1222
|
+
hopGasNeeds.unshift(erc20HopGasFee);
|
|
1223
|
+
}
|
|
1224
|
+
else {
|
|
1225
|
+
// 其他中转钱包:转 gas(21000) + 转 ERC20 + 后续的 gas
|
|
1226
|
+
const nextHopGas = hopGasNeeds[0];
|
|
1227
|
+
hopGasNeeds.unshift(nativeHopGasFeeForErc20 + erc20HopGasFee + nextHopGas);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
// 第一步:源钱包给第一个中转钱包转 gas(包含所有后续的 gas)
|
|
1231
|
+
const totalGasForFirstHop = hopGasNeeds[0];
|
|
1232
|
+
hopTxsToSign.push({
|
|
1233
|
+
wallet: sourceWallet,
|
|
1234
|
+
tx: {
|
|
1235
|
+
to: fullChain[1].address,
|
|
1236
|
+
value: totalGasForFirstHop,
|
|
1237
|
+
nonce: nonces[0],
|
|
1238
|
+
gasPrice,
|
|
1239
|
+
gasLimit: nativeTransferGasLimit,
|
|
1240
|
+
chainId: chainIdNum,
|
|
1241
|
+
type: txType
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
// 第二步:源钱包给第一个中转钱包转 ERC20
|
|
1245
|
+
const sourceToFirstHopData = iface.encodeFunctionData('transfer', [fullChain[1].address, toSend]);
|
|
1246
|
+
hopTxsToSign.push({
|
|
1247
|
+
wallet: sourceWallet,
|
|
1248
|
+
tx: {
|
|
1249
|
+
to: tokenAddress,
|
|
1250
|
+
data: sourceToFirstHopData,
|
|
1251
|
+
value: 0n,
|
|
1252
|
+
nonce: nonces[1],
|
|
1253
|
+
gasPrice,
|
|
1254
|
+
gasLimit: erc20TransferGasLimit,
|
|
1255
|
+
chainId: chainIdNum,
|
|
1256
|
+
type: txType
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
1259
|
+
// 第三步:中转钱包逐层传递(gas 和 ERC20)
|
|
1260
|
+
for (let j = 1; j < fullChain.length; j++) {
|
|
1261
|
+
const fromWallet = fullChain[j];
|
|
1262
|
+
const toAddress = addresses[j + 1]; // 下一个地址(可能是中转钱包或最终目标)
|
|
1263
|
+
const isLastHop = j === fullChain.length - 1;
|
|
1264
|
+
if (!isLastHop) {
|
|
1265
|
+
// 非最后一个中转钱包:先转 gas 给下一个中转钱包
|
|
1266
|
+
const gasToTransfer = hopGasNeeds[j]; // 下一个中转钱包需要的 gas
|
|
1267
|
+
hopTxsToSign.push({
|
|
1268
|
+
wallet: fromWallet,
|
|
1269
|
+
tx: {
|
|
1270
|
+
to: toAddress,
|
|
1271
|
+
value: gasToTransfer,
|
|
1272
|
+
nonce: 0, // 中转钱包的第一笔交易
|
|
1273
|
+
gasPrice,
|
|
1274
|
+
gasLimit: nativeTransferGasLimit,
|
|
1275
|
+
chainId: chainIdNum,
|
|
1276
|
+
type: txType
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
// 再转 ERC20
|
|
1280
|
+
const erc20Data = iface.encodeFunctionData('transfer', [toAddress, toSend]);
|
|
1281
|
+
hopTxsToSign.push({
|
|
1282
|
+
wallet: fromWallet,
|
|
1283
|
+
tx: {
|
|
1284
|
+
to: tokenAddress,
|
|
1285
|
+
data: erc20Data,
|
|
1286
|
+
value: 0n,
|
|
1287
|
+
nonce: 1, // 中转钱包的第二笔交易
|
|
1288
|
+
gasPrice,
|
|
1289
|
+
gasLimit: erc20TransferGasLimit,
|
|
1290
|
+
chainId: chainIdNum,
|
|
1291
|
+
type: txType
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
else {
|
|
1296
|
+
// 最后一个中转钱包:只转 ERC20 给最终目标
|
|
1297
|
+
const erc20Data = iface.encodeFunctionData('transfer', [toAddress, toSend]);
|
|
1298
|
+
hopTxsToSign.push({
|
|
1299
|
+
wallet: fromWallet,
|
|
1300
|
+
tx: {
|
|
1301
|
+
to: tokenAddress,
|
|
1302
|
+
data: erc20Data,
|
|
1303
|
+
value: 0n,
|
|
1304
|
+
nonce: 0, // 最后一个中转钱包只有一笔交易
|
|
1305
|
+
gasPrice,
|
|
1306
|
+
gasLimit: erc20TransferGasLimit,
|
|
1307
|
+
chainId: chainIdNum,
|
|
1308
|
+
type: txType
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1132
1312
|
}
|
|
1133
1313
|
}
|
|
1134
1314
|
}
|
|
@@ -137,8 +137,8 @@ function calculateGasLimit(config, isNative, hasHops, _hopCount = 0) {
|
|
|
137
137
|
if (config.gasLimit !== undefined) {
|
|
138
138
|
return BigInt(config.gasLimit);
|
|
139
139
|
}
|
|
140
|
-
// ✅ 原生代币: 21000, ERC20 标准 transfer:
|
|
141
|
-
const baseGas = isNative ? 21000 :
|
|
140
|
+
// ✅ 原生代币: 21000, ERC20 标准 transfer: 48000(USDT 最低约 46815)
|
|
141
|
+
const baseGas = isNative ? 21000 : 48000;
|
|
142
142
|
// ✅ 多跳时每个中转钱包只执行一笔 transfer,使用较小的安全系数
|
|
143
143
|
const multiplier = config.gasLimitMultiplier ?? 1.1;
|
|
144
144
|
return BigInt(Math.ceil(baseGas * multiplier));
|