four-flap-meme-sdk 1.4.2 → 1.4.4
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/swap.d.ts +1 -0
- package/dist/contracts/tm-bundle-merkle/types.d.ts +1 -0
- package/dist/dex/direct-router.js +190 -284
- package/dist/flap/portal-bundle-merkle/swap.d.ts +1 -0
- package/dist/utils/erc20.d.ts +3 -0
- package/dist/utils/erc20.js +6 -3
- package/package.json +1 -1
|
@@ -14,6 +14,32 @@ import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
|
14
14
|
// 常量配置
|
|
15
15
|
// ============================================================================
|
|
16
16
|
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// ✅ 方案 C:Provider 缓存(避免重复创建)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
const providerCache = new Map();
|
|
21
|
+
const PROVIDER_CACHE_TTL = 60000; // 60秒后过期
|
|
22
|
+
const providerCacheTimestamps = new Map();
|
|
23
|
+
/**
|
|
24
|
+
* 获取缓存的 Provider 实例
|
|
25
|
+
* - 根据 rpcUrl + chainId 作为缓存 key
|
|
26
|
+
* - 60秒后自动失效,避免长连接问题
|
|
27
|
+
*/
|
|
28
|
+
function getCachedProvider(rpcUrl, chainId, chainName) {
|
|
29
|
+
const cacheKey = `${rpcUrl}_${chainId}`;
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
// 检查缓存是否存在且未过期
|
|
32
|
+
const cachedProvider = providerCache.get(cacheKey);
|
|
33
|
+
const timestamp = providerCacheTimestamps.get(cacheKey) || 0;
|
|
34
|
+
if (cachedProvider && (now - timestamp) < PROVIDER_CACHE_TTL) {
|
|
35
|
+
return cachedProvider;
|
|
36
|
+
}
|
|
37
|
+
// 创建新 Provider 并缓存
|
|
38
|
+
const provider = new JsonRpcProvider(rpcUrl, { chainId, name: chainName });
|
|
39
|
+
providerCache.set(cacheKey, provider);
|
|
40
|
+
providerCacheTimestamps.set(cacheKey, now);
|
|
41
|
+
return provider;
|
|
42
|
+
}
|
|
17
43
|
const DEFAULT_GAS_LIMIT = 300000;
|
|
18
44
|
const DEADLINE_MINUTES = 20;
|
|
19
45
|
// ✅ BlockRazor Builder EOA 地址(用于贿赂,仅 BSC 链)
|
|
@@ -671,130 +697,102 @@ export async function directV2BatchBuy(params) {
|
|
|
671
697
|
throw new Error('privateKeys 和 buyAmounts 长度必须相同');
|
|
672
698
|
}
|
|
673
699
|
const chainId = config.chainId || CHAIN_IDS[chain.toUpperCase()] || 56;
|
|
674
|
-
|
|
700
|
+
// ✅ 方案 C:使用缓存的 Provider
|
|
701
|
+
const provider = getCachedProvider(config.rpcUrl, chainId, chain);
|
|
675
702
|
const useNative = isNativeToken(quoteToken);
|
|
676
703
|
const wrappedNative = getWrappedNative(chain);
|
|
677
704
|
// 创建钱包
|
|
678
705
|
const wallets = privateKeys.map(pk => new Wallet(pk, provider));
|
|
679
|
-
// ✅
|
|
680
|
-
const
|
|
706
|
+
// ✅ 预先计算所有金额(同步操作,无 RPC)
|
|
707
|
+
const flowAmounts = buyAmounts.map(amount => ethers.parseUnits(amount, quoteTokenDecimals));
|
|
708
|
+
const totalFlowWei = flowAmounts.reduce((sum, amt) => sum + amt, 0n);
|
|
709
|
+
const baseProfitWei = calculateProfitAmount(totalFlowWei);
|
|
710
|
+
// ✅ 方案 B:并行获取 nonces、gasPrice 和 ERC20 报价
|
|
711
|
+
const [nonces, gasPrice, nativeProfitWei] = await Promise.all([
|
|
681
712
|
startNonces && startNonces.length === wallets.length
|
|
682
713
|
? Promise.resolve(startNonces)
|
|
683
714
|
: new NonceManager(provider).getNextNoncesForWallets(wallets),
|
|
684
|
-
getGasPrice(provider, config)
|
|
715
|
+
getGasPrice(provider, config),
|
|
716
|
+
// ERC20 报价提前并行获取
|
|
717
|
+
(!useNative && baseProfitWei > 0n && quoteToken)
|
|
718
|
+
? getTokenToNativeQuote(provider, quoteToken, baseProfitWei, chain)
|
|
719
|
+
: Promise.resolve(baseProfitWei)
|
|
685
720
|
]);
|
|
721
|
+
// 确定最终利润金额
|
|
722
|
+
const profitWei = nativeProfitWei > 0n ? nativeProfitWei : 0n;
|
|
686
723
|
const gasLimit = getGasLimit(config, 250000);
|
|
687
724
|
const txType = config.txType ?? 0;
|
|
688
725
|
const deadline = getDeadline();
|
|
689
|
-
const slippageBps = config.slippageBps ?? 100; // 默认 1%
|
|
690
726
|
// 构建路径
|
|
691
727
|
const inputToken = useNative ? wrappedNative : quoteToken;
|
|
692
728
|
const path = [inputToken, tokenAddress];
|
|
693
|
-
//
|
|
729
|
+
// 判断 Router 类型
|
|
694
730
|
const useSwapRouter02 = isSwapRouter02(chain, routerAddress);
|
|
695
731
|
const useDYORSwap = isDYORSwap(chain, routerAddress);
|
|
696
|
-
// 创建 Router 合约接口 (DYORSwap 使用手动编码,不需要 ABI)
|
|
697
732
|
const routerIface = useSwapRouter02
|
|
698
733
|
? new ethers.Interface(SWAP_ROUTER02_V2_ABI)
|
|
699
734
|
: new ethers.Interface(V2_ROUTER_ABI);
|
|
700
|
-
//
|
|
701
|
-
const flowAmounts = buyAmounts.map(amount => ethers.parseUnits(amount, quoteTokenDecimals));
|
|
702
|
-
const totalFlowWei = flowAmounts.reduce((sum, amt) => sum + amt, 0n);
|
|
703
|
-
// ✅ 优化:构建未签名交易的辅助函数
|
|
735
|
+
// 构建交易数据的辅助函数
|
|
704
736
|
const buildTxData = (wallet, amountWei) => {
|
|
705
737
|
if (useDYORSwap) {
|
|
706
738
|
const DYORSWAP_FACTORY = DIRECT_ROUTERS.XLAYER.DYORSWAP_FACTORY;
|
|
707
739
|
const multicallData = [];
|
|
708
740
|
multicallData.push(encodeDYORRefundETH(30));
|
|
709
741
|
multicallData.push(encodeDYORSwapExactTokensForTokens(useNative ? 0n : amountWei, 0n, path, [], wallet.address, 1n, DYORSWAP_FACTORY));
|
|
710
|
-
return {
|
|
711
|
-
txData: encodeDYORMulticall(BigInt(deadline), multicallData),
|
|
712
|
-
txValue: useNative ? amountWei : 0n
|
|
713
|
-
};
|
|
742
|
+
return { txData: encodeDYORMulticall(BigInt(deadline), multicallData), txValue: useNative ? amountWei : 0n };
|
|
714
743
|
}
|
|
715
744
|
else if (useSwapRouter02) {
|
|
716
|
-
const swapData = routerIface.encodeFunctionData('swapExactTokensForTokens', [
|
|
717
|
-
|
|
718
|
-
0n,
|
|
719
|
-
path,
|
|
720
|
-
wallet.address,
|
|
721
|
-
]);
|
|
722
|
-
return {
|
|
723
|
-
txData: routerIface.encodeFunctionData('multicall(uint256,bytes[])', [deadline, [swapData]]),
|
|
724
|
-
txValue: useNative ? amountWei : 0n
|
|
725
|
-
};
|
|
745
|
+
const swapData = routerIface.encodeFunctionData('swapExactTokensForTokens', [amountWei, 0n, path, wallet.address]);
|
|
746
|
+
return { txData: routerIface.encodeFunctionData('multicall(uint256,bytes[])', [deadline, [swapData]]), txValue: useNative ? amountWei : 0n };
|
|
726
747
|
}
|
|
727
748
|
else if (useNative) {
|
|
728
|
-
return {
|
|
729
|
-
txData: routerIface.encodeFunctionData('swapExactETHForTokensSupportingFeeOnTransferTokens', [
|
|
730
|
-
0n,
|
|
731
|
-
path,
|
|
732
|
-
wallet.address,
|
|
733
|
-
deadline,
|
|
734
|
-
]),
|
|
735
|
-
txValue: amountWei
|
|
736
|
-
};
|
|
749
|
+
return { txData: routerIface.encodeFunctionData('swapExactETHForTokensSupportingFeeOnTransferTokens', [0n, path, wallet.address, deadline]), txValue: amountWei };
|
|
737
750
|
}
|
|
738
751
|
else {
|
|
739
|
-
return {
|
|
740
|
-
txData: routerIface.encodeFunctionData('swapExactTokensForTokensSupportingFeeOnTransferTokens', [
|
|
741
|
-
amountWei,
|
|
742
|
-
0n,
|
|
743
|
-
path,
|
|
744
|
-
wallet.address,
|
|
745
|
-
deadline,
|
|
746
|
-
]),
|
|
747
|
-
txValue: 0n
|
|
748
|
-
};
|
|
752
|
+
return { txData: routerIface.encodeFunctionData('swapExactTokensForTokensSupportingFeeOnTransferTokens', [amountWei, 0n, path, wallet.address, deadline]), txValue: 0n };
|
|
749
753
|
}
|
|
750
754
|
};
|
|
751
|
-
//
|
|
755
|
+
// 选择金额最大的钱包支付贿赂和利润
|
|
752
756
|
const maxFlowIndex = findMaxFlowIndex(flowAmounts);
|
|
753
|
-
//
|
|
757
|
+
// 计算贿赂金额(仅 BSC 链)
|
|
754
758
|
const bribeWei = getBribeAmount(config, chain);
|
|
755
759
|
const hasBribe = bribeWei > 0n && wallets.length > 0;
|
|
756
|
-
|
|
760
|
+
const hasProfit = profitWei > 0n;
|
|
761
|
+
// 计算 nonce 偏移
|
|
757
762
|
const nonceOffsets = wallets.map((_, i) => i === maxFlowIndex && hasBribe ? 1 : 0);
|
|
758
|
-
// ✅
|
|
759
|
-
const
|
|
763
|
+
// ✅ 方案 A:并行签名所有交易(贿赂、主交易、利润)
|
|
764
|
+
const signPromises = [];
|
|
765
|
+
// 贿赂交易
|
|
760
766
|
if (hasBribe) {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
bribeTxs.push(bribeTx);
|
|
767
|
+
signPromises.push(buildBribeTransaction(wallets[maxFlowIndex], bribeWei, nonces[maxFlowIndex], gasPrice, chainId, txType)
|
|
768
|
+
.then(tx => ({ type: 'bribe', index: 0, tx })));
|
|
764
769
|
}
|
|
765
|
-
//
|
|
766
|
-
|
|
770
|
+
// 主交易(并行签名)
|
|
771
|
+
wallets.forEach((wallet, i) => {
|
|
767
772
|
const { txData, txValue } = buildTxData(wallet, flowAmounts[i]);
|
|
768
|
-
|
|
773
|
+
signPromises.push(wallet.signTransaction({
|
|
769
774
|
to: routerAddress,
|
|
770
775
|
data: txData,
|
|
771
776
|
value: txValue,
|
|
772
|
-
nonce: nonces[i] + nonceOffsets[i],
|
|
777
|
+
nonce: nonces[i] + nonceOffsets[i],
|
|
773
778
|
gasLimit,
|
|
774
779
|
gasPrice,
|
|
775
780
|
chainId,
|
|
776
781
|
type: txType,
|
|
777
|
-
});
|
|
778
|
-
})
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
profitWei = nativeProfitWei;
|
|
785
|
-
}
|
|
786
|
-
else {
|
|
787
|
-
profitWei = 0n; // 报价失败,跳过利润
|
|
788
|
-
}
|
|
782
|
+
}).then(tx => ({ type: 'swap', index: i, tx })));
|
|
783
|
+
});
|
|
784
|
+
// 利润交易
|
|
785
|
+
if (hasProfit) {
|
|
786
|
+
const profitNonce = nonces[maxFlowIndex] + nonceOffsets[maxFlowIndex] + 1;
|
|
787
|
+
signPromises.push(buildProfitTransaction(wallets[maxFlowIndex], profitWei, profitNonce, gasPrice, chainId, txType)
|
|
788
|
+
.then(tx => ({ type: 'profit', index: 0, tx })));
|
|
789
789
|
}
|
|
790
|
-
// ✅
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
// ✅ 组装最终交易列表:贿赂 → 交易 → 利润
|
|
790
|
+
// ✅ 并行执行所有签名
|
|
791
|
+
const signedResults = await Promise.all(signPromises);
|
|
792
|
+
// 按类型分组并按顺序组装:贿赂 → 交易 → 利润
|
|
793
|
+
const bribeTxs = signedResults.filter(r => r.type === 'bribe').map(r => r.tx);
|
|
794
|
+
const swapTxs = signedResults.filter(r => r.type === 'swap').sort((a, b) => a.index - b.index).map(r => r.tx);
|
|
795
|
+
const profitTxs = signedResults.filter(r => r.type === 'profit').map(r => r.tx);
|
|
798
796
|
const signedTxs = [...bribeTxs, ...swapTxs, ...profitTxs];
|
|
799
797
|
return {
|
|
800
798
|
signedTransactions: signedTxs,
|
|
@@ -811,27 +809,22 @@ export async function directV2BatchBuy(params) {
|
|
|
811
809
|
export async function directV2BatchSell(params) {
|
|
812
810
|
const { chain, privateKeys, sellPercentages, sellAmounts, tokenAddress, tokenDecimals: inputDecimals, routerAddress, quoteToken, startNonces, config, } = params;
|
|
813
811
|
const chainId = config.chainId || CHAIN_IDS[chain.toUpperCase()] || 56;
|
|
814
|
-
|
|
812
|
+
// ✅ 方案 C:使用缓存的 Provider
|
|
813
|
+
const provider = getCachedProvider(config.rpcUrl, chainId, chain);
|
|
815
814
|
const useNativeOutput = isNativeToken(quoteToken);
|
|
816
815
|
const wrappedNative = getWrappedNative(chain);
|
|
817
816
|
// 创建钱包
|
|
818
817
|
const wallets = privateKeys.map(pk => new Wallet(pk, provider));
|
|
819
|
-
// 获取代币合约
|
|
820
818
|
const tokenContract = new Contract(tokenAddress, ERC20_ABI, provider);
|
|
821
819
|
// ✅ 性能优化:并行获取所有独立的 RPC 数据
|
|
822
|
-
// ✅ 如果前端已传入 nonces,则跳过 RPC 获取
|
|
823
820
|
const [fetchedDecimals, balances, nonces, gasPrice] = await Promise.all([
|
|
824
|
-
// 1. 获取代币精度(如果未提供)
|
|
825
821
|
inputDecimals !== undefined
|
|
826
822
|
? Promise.resolve(inputDecimals)
|
|
827
823
|
: tokenContract.decimals().then((d) => Number(d)).catch(() => 18),
|
|
828
|
-
// 2. 批量获取余额
|
|
829
824
|
Promise.all(wallets.map(w => tokenContract.balanceOf(w.address))),
|
|
830
|
-
// 3. 批量获取 nonce(如果前端已传入则跳过)
|
|
831
825
|
startNonces && startNonces.length === wallets.length
|
|
832
826
|
? Promise.resolve(startNonces)
|
|
833
827
|
: new NonceManager(provider).getNextNoncesForWallets(wallets),
|
|
834
|
-
// 4. 获取 gas price
|
|
835
828
|
getGasPrice(provider, config)
|
|
836
829
|
]);
|
|
837
830
|
const tokenDecimals = fetchedDecimals;
|
|
@@ -842,7 +835,6 @@ export async function directV2BatchSell(params) {
|
|
|
842
835
|
const sellAmountsWei = [];
|
|
843
836
|
for (let i = 0; i < wallets.length; i++) {
|
|
844
837
|
if (sellAmounts && sellAmounts[i]) {
|
|
845
|
-
// ✅ 截断小数位数,避免超过代币精度导致 parseUnits 报错
|
|
846
838
|
const truncatedAmount = truncateDecimals(sellAmounts[i], tokenDecimals);
|
|
847
839
|
sellAmountsWei.push(ethers.parseUnits(truncatedAmount, tokenDecimals));
|
|
848
840
|
}
|
|
@@ -851,29 +843,31 @@ export async function directV2BatchSell(params) {
|
|
|
851
843
|
sellAmountsWei.push((balances[i] * BigInt(pct)) / 100n);
|
|
852
844
|
}
|
|
853
845
|
else {
|
|
854
|
-
sellAmountsWei.push(balances[i]);
|
|
846
|
+
sellAmountsWei.push(balances[i]);
|
|
855
847
|
}
|
|
856
848
|
}
|
|
857
|
-
//
|
|
849
|
+
// ✅ 方案 B:提前计算利润并并行获取 ERC20 报价
|
|
850
|
+
const totalOutputEstimate = sellAmountsWei.reduce((sum, o) => sum + o, 0n);
|
|
851
|
+
const baseProfitWei = calculateProfitAmount(totalOutputEstimate);
|
|
852
|
+
// ERC20 报价(如果需要)- 可以在签名同时进行
|
|
853
|
+
const nativeProfitPromise = (!useNativeOutput && baseProfitWei > 0n && quoteToken)
|
|
854
|
+
? getTokenToNativeQuote(provider, quoteToken, baseProfitWei, chain)
|
|
855
|
+
: Promise.resolve(baseProfitWei);
|
|
856
|
+
// 构建路径和 Router
|
|
858
857
|
const outputToken = useNativeOutput ? wrappedNative : quoteToken;
|
|
859
858
|
const path = [tokenAddress, outputToken];
|
|
860
|
-
// ✅ 判断是否是 SwapRouter02 或 DYORSwap
|
|
861
859
|
const useSwapRouter02 = isSwapRouter02(chain, routerAddress);
|
|
862
860
|
const useDYORSwap = isDYORSwap(chain, routerAddress);
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
? new ethers.Interface(SWAP_ROUTER02_V2_ABI)
|
|
866
|
-
: new ethers.Interface(V2_ROUTER_ABI);
|
|
867
|
-
// ✅ 优化:构建卖出交易数据的辅助函数(同步)
|
|
861
|
+
const routerIface = useSwapRouter02 ? new ethers.Interface(SWAP_ROUTER02_V2_ABI) : new ethers.Interface(V2_ROUTER_ABI);
|
|
862
|
+
// 构建卖出交易数据的辅助函数
|
|
868
863
|
const buildSellTxData = (wallet, sellAmount) => {
|
|
869
864
|
if (useDYORSwap) {
|
|
870
865
|
const DYORSWAP_FACTORY = DIRECT_ROUTERS.XLAYER.DYORSWAP_FACTORY;
|
|
871
866
|
const ADDRESS_THIS = 2n;
|
|
872
867
|
const multicallData = [];
|
|
873
868
|
multicallData.push(encodeDYORSwapExactTokensForTokens(sellAmount, 0n, path, [DYORSWAP_FACTORY], ADDRESS_THIS, 1n, DYORSWAP_FACTORY));
|
|
874
|
-
if (useNativeOutput)
|
|
869
|
+
if (useNativeOutput)
|
|
875
870
|
multicallData.push(encodeDYORUnwrapWETH9(0n, wallet.address, '0x'));
|
|
876
|
-
}
|
|
877
871
|
return encodeDYORMulticall(BigInt(deadline), multicallData);
|
|
878
872
|
}
|
|
879
873
|
else if (useSwapRouter02) {
|
|
@@ -886,8 +880,7 @@ export async function directV2BatchSell(params) {
|
|
|
886
880
|
return routerIface.encodeFunctionData('multicall(uint256,bytes[])', [deadline, multicallData]);
|
|
887
881
|
}
|
|
888
882
|
else {
|
|
889
|
-
|
|
890
|
-
return routerIface.encodeFunctionData('multicall(uint256,bytes[])', [deadline, [swapData]]);
|
|
883
|
+
return routerIface.encodeFunctionData('multicall(uint256,bytes[])', [deadline, [routerIface.encodeFunctionData('swapExactTokensForTokens', [sellAmount, 0n, path, wallet.address])]]);
|
|
891
884
|
}
|
|
892
885
|
}
|
|
893
886
|
else if (useNativeOutput) {
|
|
@@ -897,26 +890,22 @@ export async function directV2BatchSell(params) {
|
|
|
897
890
|
return routerIface.encodeFunctionData('swapExactTokensForTokensSupportingFeeOnTransferTokens', [sellAmount, 0n, path, wallet.address, deadline]);
|
|
898
891
|
}
|
|
899
892
|
};
|
|
900
|
-
// ✅ 性能优化:直接用 sellAmountsWei 找最大输出钱包(跳过 N 次 getAmountsOut RPC 调用)
|
|
901
|
-
// 因为卖出金额越大,输出也越大(假设线性关系),无需额外报价
|
|
902
893
|
const maxOutputIndex = findMaxFlowIndex(sellAmountsWei);
|
|
903
|
-
// ✅ 计算贿赂金额(仅 BSC 链)
|
|
904
894
|
const bribeWei = getBribeAmount(config, chain);
|
|
905
895
|
const hasBribe = bribeWei > 0n && wallets.length > 0;
|
|
906
|
-
// ✅ 计算 nonce 偏移(仅贿赂,已移除授权)
|
|
907
896
|
const nonceOffsets = wallets.map((_, i) => i === maxOutputIndex && hasBribe ? 1 : 0);
|
|
908
|
-
// ✅
|
|
909
|
-
const
|
|
897
|
+
// ✅ 方案 A:并行签名所有交易(贿赂、卖出、利润)+ 并行获取 ERC20 报价
|
|
898
|
+
const signPromises = [];
|
|
899
|
+
// 贿赂交易
|
|
910
900
|
if (hasBribe) {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
bribeTxs.push(bribeTx);
|
|
901
|
+
signPromises.push(buildBribeTransaction(wallets[maxOutputIndex], bribeWei, nonces[maxOutputIndex], gasPrice, chainId, txType)
|
|
902
|
+
.then(tx => ({ type: 'bribe', index: 0, tx })));
|
|
914
903
|
}
|
|
915
|
-
//
|
|
916
|
-
|
|
904
|
+
// 卖出交易(并行签名)
|
|
905
|
+
wallets.forEach((wallet, i) => {
|
|
917
906
|
if (sellAmountsWei[i] <= 0n)
|
|
918
|
-
return
|
|
919
|
-
|
|
907
|
+
return;
|
|
908
|
+
signPromises.push(wallet.signTransaction({
|
|
920
909
|
to: routerAddress,
|
|
921
910
|
data: buildSellTxData(wallet, sellAmountsWei[i]),
|
|
922
911
|
value: 0n,
|
|
@@ -925,31 +914,25 @@ export async function directV2BatchSell(params) {
|
|
|
925
914
|
gasPrice,
|
|
926
915
|
chainId,
|
|
927
916
|
type: txType,
|
|
928
|
-
});
|
|
929
|
-
})
|
|
930
|
-
// ✅
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
}
|
|
939
|
-
else {
|
|
940
|
-
profitWei = 0n;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
// ✅ 利润交易
|
|
944
|
-
const profitTxs = [];
|
|
917
|
+
}).then(tx => ({ type: 'swap', index: i, tx })));
|
|
918
|
+
});
|
|
919
|
+
// ✅ 并行执行:签名 + ERC20 报价
|
|
920
|
+
const [signedResults, nativeProfitWei] = await Promise.all([
|
|
921
|
+
Promise.all(signPromises),
|
|
922
|
+
nativeProfitPromise
|
|
923
|
+
]);
|
|
924
|
+
const profitWei = nativeProfitWei > 0n ? nativeProfitWei : 0n;
|
|
925
|
+
// 利润交易(需要等 ERC20 报价完成才能确定金额)
|
|
926
|
+
let profitTx = null;
|
|
945
927
|
if (profitWei > 0n && wallets.length > 0) {
|
|
946
|
-
// 利润交易 nonce = 原始 nonce + 贿赂偏移 + 1(卖出交易)
|
|
947
928
|
const profitNonce = nonces[maxOutputIndex] + nonceOffsets[maxOutputIndex] + 1;
|
|
948
|
-
|
|
949
|
-
profitWei, profitNonce, gasPrice, chainId, txType);
|
|
950
|
-
profitTxs.push(profitTx);
|
|
929
|
+
profitTx = await buildProfitTransaction(wallets[maxOutputIndex], profitWei, profitNonce, gasPrice, chainId, txType);
|
|
951
930
|
}
|
|
952
|
-
//
|
|
931
|
+
// 按类型分组并按顺序组装
|
|
932
|
+
const validResults = signedResults.filter((r) => r !== null);
|
|
933
|
+
const bribeTxs = validResults.filter(r => r.type === 'bribe').map(r => r.tx);
|
|
934
|
+
const swapTxs = validResults.filter(r => r.type === 'swap').sort((a, b) => a.index - b.index).map(r => r.tx);
|
|
935
|
+
const profitTxs = profitTx ? [profitTx] : [];
|
|
953
936
|
const signedTxs = [...bribeTxs, ...swapTxs, ...profitTxs];
|
|
954
937
|
return {
|
|
955
938
|
signedTransactions: signedTxs,
|
|
@@ -976,121 +959,79 @@ export async function directV3BatchBuy(params) {
|
|
|
976
959
|
throw new Error('privateKeys 和 buyAmounts 长度必须相同');
|
|
977
960
|
}
|
|
978
961
|
const chainId = config.chainId || CHAIN_IDS[chain.toUpperCase()] || 56;
|
|
979
|
-
|
|
962
|
+
// ✅ 方案 C:使用缓存的 Provider
|
|
963
|
+
const provider = getCachedProvider(config.rpcUrl, chainId, chain);
|
|
980
964
|
const useNative = isNativeToken(quoteToken);
|
|
981
965
|
const wrappedNative = getWrappedNative(chain);
|
|
982
|
-
// ✅ 判断是否使用旧版 SwapRouter(Monad Uniswap V3)
|
|
983
966
|
const useLegacyRouter = isLegacySwapRouter(chain, routerAddress);
|
|
984
967
|
const routerAbi = useLegacyRouter ? V3_ROUTER_LEGACY_ABI : V3_ROUTER02_ABI;
|
|
985
968
|
const wallets = privateKeys.map(pk => new Wallet(pk, provider));
|
|
986
|
-
// ✅
|
|
987
|
-
const
|
|
969
|
+
// ✅ 预先计算所有金额(同步操作,无 RPC)
|
|
970
|
+
const flowAmounts = buyAmounts.map(amount => ethers.parseUnits(amount, quoteTokenDecimals));
|
|
971
|
+
const totalFlowWei = flowAmounts.reduce((sum, amt) => sum + amt, 0n);
|
|
972
|
+
const baseProfitWei = calculateProfitAmount(totalFlowWei);
|
|
973
|
+
// ✅ 方案 B:并行获取 nonces、gasPrice 和 ERC20 报价
|
|
974
|
+
const [nonces, gasPrice, nativeProfitWei] = await Promise.all([
|
|
988
975
|
startNonces && startNonces.length === wallets.length
|
|
989
976
|
? Promise.resolve(startNonces)
|
|
990
977
|
: new NonceManager(provider).getNextNoncesForWallets(wallets),
|
|
991
|
-
getGasPrice(provider, config)
|
|
978
|
+
getGasPrice(provider, config),
|
|
979
|
+
(!useNative && baseProfitWei > 0n && quoteToken)
|
|
980
|
+
? getTokenToNativeQuote(provider, quoteToken, baseProfitWei, chain)
|
|
981
|
+
: Promise.resolve(baseProfitWei)
|
|
992
982
|
]);
|
|
983
|
+
const profitWei = nativeProfitWei > 0n ? nativeProfitWei : 0n;
|
|
993
984
|
const gasLimit = getGasLimit(config, 300000);
|
|
994
985
|
const txType = config.txType ?? 0;
|
|
995
986
|
const routerIface = new ethers.Interface(routerAbi);
|
|
996
987
|
const inputToken = useNative ? wrappedNative : quoteToken;
|
|
997
988
|
const deadline = getDeadline();
|
|
998
|
-
//
|
|
999
|
-
const flowAmounts = buyAmounts.map(amount => ethers.parseUnits(amount, quoteTokenDecimals));
|
|
1000
|
-
const totalFlowWei = flowAmounts.reduce((sum, amt) => sum + amt, 0n);
|
|
1001
|
-
// ✅ 优化:构建交易数据的辅助函数
|
|
989
|
+
// 构建交易数据的辅助函数
|
|
1002
990
|
const buildV3BuyTxData = (wallet, amountWei) => {
|
|
1003
991
|
if (useLegacyRouter) {
|
|
1004
|
-
const swapParams = {
|
|
1005
|
-
tokenIn: inputToken,
|
|
1006
|
-
tokenOut: tokenAddress,
|
|
1007
|
-
fee: fee,
|
|
1008
|
-
recipient: wallet.address,
|
|
1009
|
-
deadline: deadline,
|
|
1010
|
-
amountIn: amountWei,
|
|
1011
|
-
amountOutMinimum: 0n,
|
|
1012
|
-
sqrtPriceLimitX96: 0n,
|
|
1013
|
-
};
|
|
992
|
+
const swapParams = { tokenIn: inputToken, tokenOut: tokenAddress, fee, recipient: wallet.address, deadline, amountIn: amountWei, amountOutMinimum: 0n, sqrtPriceLimitX96: 0n };
|
|
1014
993
|
if (useNative) {
|
|
1015
994
|
const swapData = routerIface.encodeFunctionData('exactInputSingle', [swapParams]);
|
|
1016
995
|
const refundData = routerIface.encodeFunctionData('refundETH', []);
|
|
1017
|
-
return {
|
|
1018
|
-
txData: routerIface.encodeFunctionData('multicall(bytes[])', [[swapData, refundData]]),
|
|
1019
|
-
txValue: amountWei
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
else {
|
|
1023
|
-
return { txData: routerIface.encodeFunctionData('exactInputSingle', [swapParams]), txValue: 0n };
|
|
996
|
+
return { txData: routerIface.encodeFunctionData('multicall(bytes[])', [[swapData, refundData]]), txValue: amountWei };
|
|
1024
997
|
}
|
|
998
|
+
return { txData: routerIface.encodeFunctionData('exactInputSingle', [swapParams]), txValue: 0n };
|
|
1025
999
|
}
|
|
1026
1000
|
else {
|
|
1027
|
-
const swapParams = {
|
|
1028
|
-
tokenIn: inputToken,
|
|
1029
|
-
tokenOut: tokenAddress,
|
|
1030
|
-
fee: fee,
|
|
1031
|
-
recipient: wallet.address,
|
|
1032
|
-
amountIn: amountWei,
|
|
1033
|
-
amountOutMinimum: 0n,
|
|
1034
|
-
sqrtPriceLimitX96: 0n,
|
|
1035
|
-
};
|
|
1001
|
+
const swapParams = { tokenIn: inputToken, tokenOut: tokenAddress, fee, recipient: wallet.address, amountIn: amountWei, amountOutMinimum: 0n, sqrtPriceLimitX96: 0n };
|
|
1036
1002
|
if (useNative) {
|
|
1037
1003
|
const swapData = routerIface.encodeFunctionData('exactInputSingle', [swapParams]);
|
|
1038
1004
|
const refundData = routerIface.encodeFunctionData('refundETH', []);
|
|
1039
|
-
return {
|
|
1040
|
-
txData: routerIface.encodeFunctionData('multicall(uint256,bytes[])', [deadline, [swapData, refundData]]),
|
|
1041
|
-
txValue: amountWei
|
|
1042
|
-
};
|
|
1043
|
-
}
|
|
1044
|
-
else {
|
|
1045
|
-
return { txData: routerIface.encodeFunctionData('exactInputSingle', [swapParams]), txValue: 0n };
|
|
1005
|
+
return { txData: routerIface.encodeFunctionData('multicall(uint256,bytes[])', [deadline, [swapData, refundData]]), txValue: amountWei };
|
|
1046
1006
|
}
|
|
1007
|
+
return { txData: routerIface.encodeFunctionData('exactInputSingle', [swapParams]), txValue: 0n };
|
|
1047
1008
|
}
|
|
1048
1009
|
};
|
|
1049
|
-
// ✅ 优化:选择金额最大的钱包支付贿赂和利润
|
|
1050
1010
|
const maxFlowIndex = findMaxFlowIndex(flowAmounts);
|
|
1051
|
-
// ✅ 计算贿赂金额(仅 BSC 链)
|
|
1052
1011
|
const bribeWei = getBribeAmount(config, chain);
|
|
1053
1012
|
const hasBribe = bribeWei > 0n && wallets.length > 0;
|
|
1054
|
-
|
|
1013
|
+
const hasProfit = profitWei > 0n;
|
|
1055
1014
|
const nonceOffsets = wallets.map((_, i) => i === maxFlowIndex && hasBribe ? 1 : 0);
|
|
1056
|
-
// ✅
|
|
1057
|
-
const
|
|
1015
|
+
// ✅ 方案 A:并行签名所有交易(贿赂、主交易、利润)
|
|
1016
|
+
const signPromises = [];
|
|
1058
1017
|
if (hasBribe) {
|
|
1059
|
-
|
|
1060
|
-
|
|
1018
|
+
signPromises.push(buildBribeTransaction(wallets[maxFlowIndex], bribeWei, nonces[maxFlowIndex], gasPrice, chainId, txType)
|
|
1019
|
+
.then(tx => ({ type: 'bribe', index: 0, tx })));
|
|
1061
1020
|
}
|
|
1062
|
-
|
|
1063
|
-
const swapTxs = await Promise.all(wallets.map(async (wallet, i) => {
|
|
1021
|
+
wallets.forEach((wallet, i) => {
|
|
1064
1022
|
const { txData, txValue } = buildV3BuyTxData(wallet, flowAmounts[i]);
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
chainId,
|
|
1073
|
-
type: txType,
|
|
1074
|
-
});
|
|
1075
|
-
}));
|
|
1076
|
-
let profitWei = calculateProfitAmount(totalFlowWei);
|
|
1077
|
-
// ✅ 修复:ERC20 交易时,将利润转换为原生代币
|
|
1078
|
-
if (!useNative && profitWei > 0n && quoteToken) {
|
|
1079
|
-
const nativeProfitWei = await getTokenToNativeQuote(provider, quoteToken, profitWei, chain);
|
|
1080
|
-
if (nativeProfitWei > 0n) {
|
|
1081
|
-
profitWei = nativeProfitWei;
|
|
1082
|
-
}
|
|
1083
|
-
else {
|
|
1084
|
-
profitWei = 0n;
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
// ✅ 利润交易
|
|
1088
|
-
const profitTxs = [];
|
|
1089
|
-
if (profitWei > 0n) {
|
|
1090
|
-
const profitTx = await buildProfitTransaction(wallets[maxFlowIndex], profitWei, nonces[maxFlowIndex] + nonceOffsets[maxFlowIndex] + 1, gasPrice, chainId, txType);
|
|
1091
|
-
profitTxs.push(profitTx);
|
|
1023
|
+
signPromises.push(wallet.signTransaction({ to: routerAddress, data: txData, value: txValue, nonce: nonces[i] + nonceOffsets[i], gasLimit, gasPrice, chainId, type: txType })
|
|
1024
|
+
.then(tx => ({ type: 'swap', index: i, tx })));
|
|
1025
|
+
});
|
|
1026
|
+
if (hasProfit) {
|
|
1027
|
+
const profitNonce = nonces[maxFlowIndex] + nonceOffsets[maxFlowIndex] + 1;
|
|
1028
|
+
signPromises.push(buildProfitTransaction(wallets[maxFlowIndex], profitWei, profitNonce, gasPrice, chainId, txType)
|
|
1029
|
+
.then(tx => ({ type: 'profit', index: 0, tx })));
|
|
1092
1030
|
}
|
|
1093
|
-
|
|
1031
|
+
const signedResults = await Promise.all(signPromises);
|
|
1032
|
+
const bribeTxs = signedResults.filter(r => r.type === 'bribe').map(r => r.tx);
|
|
1033
|
+
const swapTxs = signedResults.filter(r => r.type === 'swap').sort((a, b) => a.index - b.index).map(r => r.tx);
|
|
1034
|
+
const profitTxs = signedResults.filter(r => r.type === 'profit').map(r => r.tx);
|
|
1094
1035
|
const signedTxs = [...bribeTxs, ...swapTxs, ...profitTxs];
|
|
1095
1036
|
return {
|
|
1096
1037
|
signedTransactions: signedTxs,
|
|
@@ -1111,24 +1052,20 @@ export async function directV3BatchBuy(params) {
|
|
|
1111
1052
|
export async function directV3BatchSell(params) {
|
|
1112
1053
|
const { chain, privateKeys, sellPercentages, sellAmounts, tokenAddress, tokenDecimals = 18, routerAddress, fee, quoteToken, startNonces, config, } = params;
|
|
1113
1054
|
const chainId = config.chainId || CHAIN_IDS[chain.toUpperCase()] || 56;
|
|
1114
|
-
|
|
1055
|
+
// ✅ 方案 C:使用缓存的 Provider
|
|
1056
|
+
const provider = getCachedProvider(config.rpcUrl, chainId, chain);
|
|
1115
1057
|
const useNativeOutput = isNativeToken(quoteToken);
|
|
1116
1058
|
const wrappedNative = getWrappedNative(chain);
|
|
1117
|
-
// ✅ 判断是否使用旧版 SwapRouter(Monad Uniswap V3)
|
|
1118
1059
|
const useLegacyRouter = isLegacySwapRouter(chain, routerAddress);
|
|
1119
1060
|
const routerAbi = useLegacyRouter ? V3_ROUTER_LEGACY_ABI : V3_ROUTER02_ABI;
|
|
1120
1061
|
const wallets = privateKeys.map(pk => new Wallet(pk, provider));
|
|
1121
1062
|
const tokenContract = new Contract(tokenAddress, ERC20_ABI, provider);
|
|
1122
|
-
// ✅
|
|
1123
|
-
// ✅ 如果前端已传入 nonces,则跳过 RPC 获取
|
|
1063
|
+
// ✅ 并行获取所有 RPC 数据
|
|
1124
1064
|
const [balances, nonces, gasPrice] = await Promise.all([
|
|
1125
|
-
// 1. 批量获取余额
|
|
1126
1065
|
Promise.all(wallets.map(w => tokenContract.balanceOf(w.address))),
|
|
1127
|
-
// 2. 批量获取 nonce(如果前端已传入则跳过)
|
|
1128
1066
|
startNonces && startNonces.length === wallets.length
|
|
1129
1067
|
? Promise.resolve(startNonces)
|
|
1130
1068
|
: new NonceManager(provider).getNextNoncesForWallets(wallets),
|
|
1131
|
-
// 3. 获取 gas price
|
|
1132
1069
|
getGasPrice(provider, config)
|
|
1133
1070
|
]);
|
|
1134
1071
|
const gasLimit = getGasLimit(config, 350000);
|
|
@@ -1147,71 +1084,46 @@ export async function directV3BatchSell(params) {
|
|
|
1147
1084
|
sellAmountsWei.push(balances[i]);
|
|
1148
1085
|
}
|
|
1149
1086
|
}
|
|
1087
|
+
// ✅ 方案 B:提前计算利润并并行获取 ERC20 报价
|
|
1088
|
+
const totalOutputEstimate = sellAmountsWei.reduce((sum, o) => sum + o, 0n);
|
|
1089
|
+
const baseProfitWei = calculateProfitAmount(totalOutputEstimate);
|
|
1090
|
+
const nativeProfitPromise = (!useNativeOutput && baseProfitWei > 0n && quoteToken)
|
|
1091
|
+
? getTokenToNativeQuote(provider, quoteToken, baseProfitWei, chain)
|
|
1092
|
+
: Promise.resolve(baseProfitWei);
|
|
1150
1093
|
const routerIface = new ethers.Interface(routerAbi);
|
|
1151
1094
|
const outputToken = useNativeOutput ? wrappedNative : quoteToken;
|
|
1152
1095
|
const deadline = getDeadline();
|
|
1153
|
-
//
|
|
1096
|
+
// 构建卖出交易数据的辅助函数
|
|
1154
1097
|
const buildV3SellTxData = (wallet, sellAmount) => {
|
|
1155
1098
|
if (useLegacyRouter) {
|
|
1156
|
-
const swapParams = {
|
|
1157
|
-
tokenIn: tokenAddress,
|
|
1158
|
-
tokenOut: outputToken,
|
|
1159
|
-
fee: fee,
|
|
1160
|
-
recipient: useNativeOutput ? routerAddress : wallet.address,
|
|
1161
|
-
deadline: deadline,
|
|
1162
|
-
amountIn: sellAmount,
|
|
1163
|
-
amountOutMinimum: 0n,
|
|
1164
|
-
sqrtPriceLimitX96: 0n,
|
|
1165
|
-
};
|
|
1099
|
+
const swapParams = { tokenIn: tokenAddress, tokenOut: outputToken, fee, recipient: useNativeOutput ? routerAddress : wallet.address, deadline, amountIn: sellAmount, amountOutMinimum: 0n, sqrtPriceLimitX96: 0n };
|
|
1166
1100
|
if (useNativeOutput) {
|
|
1167
|
-
|
|
1168
|
-
const unwrapData = routerIface.encodeFunctionData('unwrapWETH9', [0n, wallet.address]);
|
|
1169
|
-
return routerIface.encodeFunctionData('multicall(bytes[])', [[swapData, unwrapData]]);
|
|
1170
|
-
}
|
|
1171
|
-
else {
|
|
1172
|
-
return routerIface.encodeFunctionData('exactInputSingle', [swapParams]);
|
|
1101
|
+
return routerIface.encodeFunctionData('multicall(bytes[])', [[routerIface.encodeFunctionData('exactInputSingle', [swapParams]), routerIface.encodeFunctionData('unwrapWETH9', [0n, wallet.address])]]);
|
|
1173
1102
|
}
|
|
1103
|
+
return routerIface.encodeFunctionData('exactInputSingle', [swapParams]);
|
|
1174
1104
|
}
|
|
1175
1105
|
else {
|
|
1176
|
-
const swapParams = {
|
|
1177
|
-
tokenIn: tokenAddress,
|
|
1178
|
-
tokenOut: outputToken,
|
|
1179
|
-
fee: fee,
|
|
1180
|
-
recipient: useNativeOutput ? routerAddress : wallet.address,
|
|
1181
|
-
amountIn: sellAmount,
|
|
1182
|
-
amountOutMinimum: 0n,
|
|
1183
|
-
sqrtPriceLimitX96: 0n,
|
|
1184
|
-
};
|
|
1106
|
+
const swapParams = { tokenIn: tokenAddress, tokenOut: outputToken, fee, recipient: useNativeOutput ? routerAddress : wallet.address, amountIn: sellAmount, amountOutMinimum: 0n, sqrtPriceLimitX96: 0n };
|
|
1185
1107
|
if (useNativeOutput) {
|
|
1186
|
-
|
|
1187
|
-
const unwrapData = routerIface.encodeFunctionData('unwrapWETH9', [0n, wallet.address]);
|
|
1188
|
-
return routerIface.encodeFunctionData('multicall(uint256,bytes[])', [deadline, [swapData, unwrapData]]);
|
|
1189
|
-
}
|
|
1190
|
-
else {
|
|
1191
|
-
return routerIface.encodeFunctionData('exactInputSingle', [swapParams]);
|
|
1108
|
+
return routerIface.encodeFunctionData('multicall(uint256,bytes[])', [deadline, [routerIface.encodeFunctionData('exactInputSingle', [swapParams]), routerIface.encodeFunctionData('unwrapWETH9', [0n, wallet.address])]]);
|
|
1192
1109
|
}
|
|
1110
|
+
return routerIface.encodeFunctionData('exactInputSingle', [swapParams]);
|
|
1193
1111
|
}
|
|
1194
1112
|
};
|
|
1195
|
-
// ✅ 先找出输出最大的钱包索引(用于支付贿赂和利润)
|
|
1196
|
-
// 对于 V3 卖出,使用卖出金额作为输出估计
|
|
1197
1113
|
const maxOutputIndex = findMaxFlowIndex(sellAmountsWei);
|
|
1198
|
-
// ✅ 计算贿赂金额(仅 BSC 链)
|
|
1199
1114
|
const bribeWei = getBribeAmount(config, chain);
|
|
1200
1115
|
const hasBribe = bribeWei > 0n && wallets.length > 0;
|
|
1201
|
-
// ✅ 计算 nonce 偏移(仅贿赂,已移除授权)
|
|
1202
1116
|
const nonceOffsets = wallets.map((_, i) => i === maxOutputIndex && hasBribe ? 1 : 0);
|
|
1203
|
-
// ✅
|
|
1204
|
-
const
|
|
1117
|
+
// ✅ 方案 A:并行签名所有交易(贿赂、卖出)+ 并行获取 ERC20 报价
|
|
1118
|
+
const signPromises = [];
|
|
1205
1119
|
if (hasBribe) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
bribeTxs.push(bribeTx);
|
|
1120
|
+
signPromises.push(buildBribeTransaction(wallets[maxOutputIndex], bribeWei, nonces[maxOutputIndex], gasPrice, chainId, txType)
|
|
1121
|
+
.then(tx => ({ type: 'bribe', index: 0, tx })));
|
|
1209
1122
|
}
|
|
1210
|
-
|
|
1211
|
-
const swapTxs = await Promise.all(wallets.map(async (wallet, i) => {
|
|
1123
|
+
wallets.forEach((wallet, i) => {
|
|
1212
1124
|
if (sellAmountsWei[i] <= 0n)
|
|
1213
|
-
return
|
|
1214
|
-
|
|
1125
|
+
return;
|
|
1126
|
+
signPromises.push(wallet.signTransaction({
|
|
1215
1127
|
to: routerAddress,
|
|
1216
1128
|
data: buildV3SellTxData(wallet, sellAmountsWei[i]),
|
|
1217
1129
|
value: 0n,
|
|
@@ -1220,31 +1132,25 @@ export async function directV3BatchSell(params) {
|
|
|
1220
1132
|
gasPrice,
|
|
1221
1133
|
chainId,
|
|
1222
1134
|
type: txType,
|
|
1223
|
-
});
|
|
1224
|
-
})
|
|
1225
|
-
// ✅
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1234
|
-
else {
|
|
1235
|
-
profitWei = 0n;
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
// ✅ 利润交易
|
|
1239
|
-
const profitTxs = [];
|
|
1135
|
+
}).then(tx => ({ type: 'swap', index: i, tx })));
|
|
1136
|
+
});
|
|
1137
|
+
// ✅ 并行执行:签名 + ERC20 报价
|
|
1138
|
+
const [signedResults, nativeProfitWei] = await Promise.all([
|
|
1139
|
+
Promise.all(signPromises),
|
|
1140
|
+
nativeProfitPromise
|
|
1141
|
+
]);
|
|
1142
|
+
const profitWei = nativeProfitWei > 0n ? nativeProfitWei : 0n;
|
|
1143
|
+
// 利润交易(需要等 ERC20 报价完成才能确定金额)
|
|
1144
|
+
let profitTx = null;
|
|
1240
1145
|
if (profitWei > 0n && wallets.length > 0) {
|
|
1241
|
-
// 利润交易 nonce = 原始 nonce + 贿赂偏移 + 1(卖出交易)
|
|
1242
1146
|
const profitNonce = nonces[maxOutputIndex] + nonceOffsets[maxOutputIndex] + 1;
|
|
1243
|
-
|
|
1244
|
-
profitWei, profitNonce, gasPrice, chainId, txType);
|
|
1245
|
-
profitTxs.push(profitTx);
|
|
1147
|
+
profitTx = await buildProfitTransaction(wallets[maxOutputIndex], profitWei, profitNonce, gasPrice, chainId, txType);
|
|
1246
1148
|
}
|
|
1247
|
-
//
|
|
1149
|
+
// 按类型分组并按顺序组装
|
|
1150
|
+
const validResults = signedResults.filter((r) => r !== null);
|
|
1151
|
+
const bribeTxs = validResults.filter(r => r.type === 'bribe').map(r => r.tx);
|
|
1152
|
+
const swapTxs = validResults.filter(r => r.type === 'swap').sort((a, b) => a.index - b.index).map(r => r.tx);
|
|
1153
|
+
const profitTxs = profitTx ? [profitTx] : [];
|
|
1248
1154
|
const signedTxs = [...bribeTxs, ...swapTxs, ...profitTxs];
|
|
1249
1155
|
return {
|
|
1250
1156
|
signedTransactions: signedTxs,
|
|
@@ -15,6 +15,7 @@ export interface FlapSwapSignConfig {
|
|
|
15
15
|
reserveGasETH?: number;
|
|
16
16
|
skipQuoteOnError?: boolean;
|
|
17
17
|
skipApprovalCheck?: boolean;
|
|
18
|
+
bribeAmount?: number;
|
|
18
19
|
}
|
|
19
20
|
export type FlapChain = 'bsc' | 'xlayer' | 'base';
|
|
20
21
|
export interface FlapSwapConfig extends CommonBundleConfig {
|
package/dist/utils/erc20.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { JsonRpcProvider } from 'ethers';
|
|
2
|
+
import { NonceManager } from './bundle-helpers.js';
|
|
2
3
|
export type EnsureAllowanceResult = {
|
|
3
4
|
alreadyApproved: boolean;
|
|
4
5
|
currentAllowance: bigint;
|
|
@@ -190,6 +191,8 @@ export type ApproveTokenBatchRawParams = {
|
|
|
190
191
|
gasLimit?: number;
|
|
191
192
|
/** 链ID(可选,默认56=BSC,signOnly=true 时生效) */
|
|
192
193
|
chainId?: number;
|
|
194
|
+
/** 外部 NonceManager(可选,用于同一钱包签多笔交易时共享 nonce 状态) */
|
|
195
|
+
nonceManager?: NonceManager;
|
|
193
196
|
};
|
|
194
197
|
/** signOnly=false 时的返回结果(直接发送交易) */
|
|
195
198
|
export type ApproveTokenBatchResult = {
|
package/dist/utils/erc20.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Contract, Wallet, JsonRpcProvider, Interface, parseUnits } from 'ethers';
|
|
2
2
|
import { ADDRESSES } from './constants.js';
|
|
3
|
+
import { NonceManager } from './bundle-helpers.js';
|
|
3
4
|
const ERC20_ABI = [
|
|
4
5
|
{ "constant": false, "inputs": [{ "name": "spender", "type": "address" }, { "name": "value", "type": "uint256" }], "name": "approve", "outputs": [{ "name": "", "type": "bool" }], "type": "function" },
|
|
5
6
|
{ "constant": true, "inputs": [{ "name": "owner", "type": "address" }, { "name": "spender", "type": "address" }], "name": "allowance", "outputs": [{ "name": "", "type": "uint256" }], "type": "function" },
|
|
@@ -488,7 +489,7 @@ export async function approveTokenBatch(params) {
|
|
|
488
489
|
return approveTokenBatchRaw({ rpcUrl, privateKeys, tokenAddress, spenderAddress, amounts, signOnly, gasPriceGwei, gasLimit, chainId });
|
|
489
490
|
}
|
|
490
491
|
export async function approveTokenBatchRaw(params) {
|
|
491
|
-
const { rpcUrl, privateKeys, tokenAddress, spenderAddress, amounts, signOnly, gasPriceGwei, gasLimit, chainId = 56 } = params;
|
|
492
|
+
const { rpcUrl, privateKeys, tokenAddress, spenderAddress, amounts, signOnly, gasPriceGwei, gasLimit, chainId = 56, nonceManager: externalNonceManager } = params;
|
|
492
493
|
if (privateKeys.length === 0 || amounts.length !== privateKeys.length) {
|
|
493
494
|
throw new Error('❌ 私钥数量和授权数量必须匹配');
|
|
494
495
|
}
|
|
@@ -507,10 +508,12 @@ export async function approveTokenBatchRaw(params) {
|
|
|
507
508
|
: amount);
|
|
508
509
|
// ==================== signOnly=true:只签名不提交 ====================
|
|
509
510
|
if (signOnly) {
|
|
511
|
+
// ✅ 使用 NonceManager 管理 nonce(和买卖交易一样)
|
|
512
|
+
const nonceManager = externalNonceManager || new NonceManager(provider);
|
|
510
513
|
// ✅ 并行获取:当前授权额度 + nonces + gasPrice
|
|
511
514
|
const [currentAllowances, nonces, fetchedGasPrice] = await Promise.all([
|
|
512
515
|
batchCheckAllowances(provider, normalizedToken, ownerAddresses, normalizedSpender),
|
|
513
|
-
|
|
516
|
+
nonceManager.getNextNoncesForWallets(wallets), // ✅ 使用 NonceManager 批量获取 nonce
|
|
514
517
|
gasPriceGwei ? Promise.resolve(parseUnits(gasPriceGwei.toString(), 'gwei')) : provider.getFeeData().then(fee => fee.gasPrice || parseUnits('3', 'gwei'))
|
|
515
518
|
]);
|
|
516
519
|
const finalGasPrice = fetchedGasPrice;
|
|
@@ -537,7 +540,7 @@ export async function approveTokenBatchRaw(params) {
|
|
|
537
540
|
const signedTx = await wallet.signTransaction({
|
|
538
541
|
to: normalizedToken,
|
|
539
542
|
data: txData,
|
|
540
|
-
nonce: nonces[i],
|
|
543
|
+
nonce: nonces[i], // ✅ NonceManager 已经处理好递增
|
|
541
544
|
gasLimit: finalGasLimit,
|
|
542
545
|
gasPrice: finalGasPrice,
|
|
543
546
|
chainId,
|