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, Contract } from 'ethers';
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
- // BSC 链常量
19
- const BSC_PANCAKE_V2_ROUTER = '0x10ED43C718714eb63d5aA57B78B54704E256024E';
20
- const BSC_WBNB = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
21
- // Monad 链常量
22
- const MONAD_PANCAKE_V2_ROUTER = '0xb1bc24c34e88f7d43d5923034e3a14b24daacff9';
23
- const MONAD_WMON = '0x3bd359c1119da7da1d913d1c4d2b7c461115433a';
24
- const ROUTER_ABI = [
25
- 'function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)'
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
- * @returns 等值的原生代币数量(wei),失败返回 0n
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 0n;
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
- if (chainId === 56) {
40
- // BSC: 使用 PancakeSwap V2 Router
41
- const router = new Contract(BSC_PANCAKE_V2_ROUTER, ROUTER_ABI, provider);
42
- const amounts = await router.getAmountsOut(tokenAmount, [tokenAddress, BSC_WBNB]);
43
- return amounts[1];
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 (chainId === 143) {
46
- // Monad: 使用 PancakeSwap V2 Router
47
- const router = new Contract(MONAD_PANCAKE_V2_ROUTER, ROUTER_ABI, provider);
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
- // 报价失败返回 0(可能是流动性不足或路径不存在)
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
- // 分离有跳转和无跳转的地址(不需要 RPC)
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、余额(传入 chainIdNum 避免 NETWORK_ERROR)
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
- if (withoutHopIndexes.length > 0) {
788
- for (let idx = 0; idx < withoutHopIndexes.length; idx++) {
789
- const i = withoutHopIndexes[idx];
790
- const sourceWallet = sourceWallets[i];
791
- const bal = balances[i];
792
- let toSend = 0n;
793
- if (isNative) {
794
- const gasCost = nativeGasLimit * gasPrice;
795
- const ratioForI = (() => {
796
- if (sources && sources[i] && sources[i].ratioPct !== undefined)
797
- return clamp(sources[i].ratioPct);
798
- if (ratios && ratios[i] !== undefined)
799
- return clamp(ratios[i]);
800
- return ratio;
801
- })();
802
- const amountStrForI = (() => {
803
- if (sources && sources[i] && sources[i].amount !== undefined)
804
- return String(sources[i].amount);
805
- if (amounts && amounts[i] !== undefined)
806
- return String(amounts[i]);
807
- return amount !== undefined ? String(amount) : undefined;
808
- })();
809
- if (ratioForI !== undefined) {
810
- const want = (bal * BigInt(ratioForI)) / 100n;
811
- const maxSendable = bal > gasCost ? (bal - gasCost) : 0n;
812
- toSend = want > maxSendable ? maxSendable : want;
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 ratioForI = (() => {
838
- if (sources && sources[i] && sources[i].ratioPct !== undefined)
839
- return clamp(sources[i].ratioPct);
840
- if (ratios && ratios[i] !== undefined)
841
- return clamp(ratios[i]);
842
- return ratio;
843
- })();
844
- const amountStrForI = (() => {
845
- if (sources && sources[i] && sources[i].amount !== undefined)
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
- const tx = await sourceWallet.signTransaction({
955
- to: fullChain[j + 1].address,
956
- value: gasFeePerHop,
957
- nonce: gasNonces[j],
958
- gasPrice,
959
- gasLimit: nativeGasLimit,
960
- chainId: chainIdNum,
961
- type: txType
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
- // 使用 NonceManager:源钱包查询,中转钱包从 0 开始
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
- // 示例:hopCount=1(2跳),toSend=0.002874
983
- // j=0: 子钱包->中转1,剩余1跳,转 0.002874 + 1*gas = 0.002937
984
- // j=1: 中转1->主钱包,剩余0跳,转 0.002874 + 0 = 0.002874
985
- const tx = await fromWallet.signTransaction({
986
- to: toAddress,
987
- value: valueToTransfer,
988
- nonce,
989
- gasPrice,
990
- gasLimit: finalGasLimit,
991
- chainId: chainIdNum,
992
- type: txType
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
- const tx = await fromWallet.signTransaction({
999
- to: tokenAddress,
1000
- data,
1001
- value: 0n,
1002
- nonce,
1003
- gasPrice,
1004
- gasLimit: finalGasLimit,
1005
- chainId: chainIdNum,
1006
- type: txType
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; // ERC20 累计代币利润
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.4.23",
3
+ "version": "1.4.25",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",