UNPKG

four-flap-meme-sdk

Version:

SDK for Flap bonding curve and four.meme TokenManager

681 lines (680 loc) 28.7 kB
/** * PancakeSwap 官方 Router 交易模块 * * ✅ 使用官方 PancakeSwap V2/V3 Router(非代理合约) * - V2 Router: 0x10ED43C718714eb63d5aA57B78B54704E256024E * - V3 Router: 0x13f4EA83D0bd40E75C8222255bc855a974568Dd4 * - V3 Quoter: 0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997 */ import { ethers, Wallet, JsonRpcProvider, Contract, Interface } from 'ethers'; import { NonceManager, getOptimizedGasPrice, getDeadline, encodeV3Path, buildProfitHopTransactions, PROFIT_HOP_COUNT } from '../../utils/bundle-helpers.js'; import { ADDRESSES, BLOCKRAZOR_BUILDER_EOA } from '../../utils/constants.js'; import { MULTICALL3_ABI, V2_ROUTER_ABI, V3_ROUTER02_ABI, V3_QUOTER_ABI, ERC20_ABI } from '../../abis/common.js'; import { getTxType, getGasPriceConfig, shouldExtractProfit, calculateProfit, calculateBatchProfit, getProfitRecipient, getBribeAmount } from './config.js'; // ==================== 常量 ==================== const MULTICALL3_ADDRESS = ADDRESSES.BSC.Multicall3; const WBNB_ADDRESS = ADDRESSES.BSC.WBNB; const DEFAULT_GAS_LIMIT = 800000; // ✅ PancakeSwap 官方合约地址 const PANCAKE_V2_ROUTER_ADDRESS = ADDRESSES.BSC.PancakeV2Router; const PANCAKE_V3_ROUTER_ADDRESS = ADDRESSES.BSC.PancakeV3Router; const PANCAKE_V3_QUOTER_ADDRESS = ADDRESSES.BSC.PancakeV3Quoter; // ==================== ABI 别名(从公共模块导入)==================== // ✅ 使用公共 ABI:V2_ROUTER_ABI, V3_ROUTER02_ABI (as V3_ROUTER_ABI), V3_QUOTER_ABI, ERC20_ABI, MULTICALL3_ABI const V3_ROUTER_ABI = V3_ROUTER02_ABI; // ==================== 辅助函数 ==================== /** * 获取 Gas Limit */ function getGasLimit(config, defaultGas = DEFAULT_GAS_LIMIT) { if (config.gasLimit !== undefined) { return typeof config.gasLimit === 'bigint' ? config.gasLimit : BigInt(config.gasLimit); } const multiplier = config.gasLimitMultiplier ?? 1.0; const calculatedGas = Math.ceil(defaultGas * multiplier); return BigInt(calculatedGas); } /** * 查询代币 decimals */ async function getTokenDecimals(tokenAddress, provider) { try { const token = new Contract(tokenAddress, ERC20_ABI, provider); const decimals = await token.decimals(); return Number(decimals); } catch { return 18; } } /** * 判断是否需要发送 BNB */ function needSendBNB(routeType, params) { if (routeType === 'v2' && params.v2Path && params.v2Path[0].toLowerCase() === WBNB_ADDRESS.toLowerCase()) { return true; } if (routeType === 'v3-single' && params.v3TokenIn && params.v3TokenIn.toLowerCase() === WBNB_ADDRESS.toLowerCase()) { return true; } if (routeType === 'v3-multi' && params.v3ExactTokenIn && params.v3ExactTokenIn.toLowerCase() === WBNB_ADDRESS.toLowerCase()) { return true; } return false; } // ✅ getDeadline 从 bundle-helpers.js 导入 /** * ✅ 构建 V2 交易(使用官方 Router) */ async function buildV2Transactions(routers, wallets, amountsWei, minOuts, path, isBuy, needBNB) { const deadline = getDeadline(); return Promise.all(routers.map(async (router, i) => { if (isBuy && needBNB) { // ✅ 买入:BNB → Token,使用 swapExactETHForTokensSupportingFeeOnTransferTokens return router.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(minOuts[i], // amountOutMin path, // path wallets[i].address, // to deadline, // deadline { value: amountsWei[i] } // 发送的 BNB ); } else { // ✅ 卖出:Token → BNB,使用 swapExactTokensForETHSupportingFeeOnTransferTokens return router.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(amountsWei[i], // amountIn minOuts[i], // amountOutMin path, // path wallets[i].address, // to deadline // deadline ); } })); } /** * ✅ 构建 V3 单跳交易(使用官方 SwapRouter02 + multicall) */ async function buildV3SingleTransactions(routers, wallets, tokenIn, tokenOut, fee, amountsWei, minOuts, isBuy, needBNB) { const deadline = getDeadline(); const v3RouterIface = new Interface(V3_ROUTER_ABI); return Promise.all(routers.map(async (router, i) => { const isTokenOutWBNB = tokenOut.toLowerCase() === WBNB_ADDRESS.toLowerCase(); // ✅ 构建 exactInputSingle calldata const exactInputSingleData = v3RouterIface.encodeFunctionData('exactInputSingle', [{ tokenIn: tokenIn, tokenOut: tokenOut, fee: fee, recipient: isTokenOutWBNB ? PANCAKE_V3_ROUTER_ADDRESS : wallets[i].address, // 如果输出是 WBNB,先发给 Router amountIn: amountsWei[i], amountOutMinimum: minOuts[i], sqrtPriceLimitX96: 0n }]); if (isBuy && needBNB) { // ✅ 买入:BNB → Token,只需要 exactInputSingle // 通过 multicall 包装,传入 deadline 和 ETH value return router.multicall.populateTransaction(deadline, [exactInputSingleData], { value: amountsWei[i] }); } else if (!isBuy && isTokenOutWBNB) { // ✅ 卖出:Token → WBNB → BNB,需要 exactInputSingle + unwrapWETH9 const unwrapData = v3RouterIface.encodeFunctionData('unwrapWETH9', [ minOuts[i], // amountMinimum wallets[i].address // recipient ]); return router.multicall.populateTransaction(deadline, [exactInputSingleData, unwrapData]); } else { // ✅ Token → Token,只需要 exactInputSingle return router.multicall.populateTransaction(deadline, [exactInputSingleData]); } })); } /** * ✅ 构建 V3 多跳交易(使用官方 SwapRouter02 的 exactInput) * * @param routers - V3 Router 合约数组 * @param wallets - 钱包数组 * @param tokens - 代币路径 [tokenIn, tokenMid1, ..., tokenOut] * @param fees - 费率路径 [fee0, fee1, ...] * @param amountsWei - 每个钱包的输入金额 * @param minOuts - 最小输出金额 * @param isBuy - 是否为买入操作 * @param needBNB - 是否需要发送 BNB */ async function buildV3MultiHopTransactions(routers, wallets, tokens, fees, amountsWei, minOuts, isBuy, needBNB) { if (!tokens || tokens.length < 2) { throw new Error('V3 多跳需要至少 2 个代币(v3Tokens)'); } if (!fees || fees.length !== tokens.length - 1) { throw new Error(`V3 多跳费率数量 (${fees?.length || 0}) 必须等于代币数量 - 1 (${tokens.length - 1})(v3Fees)`); } const deadline = getDeadline(); const v3RouterIface = new Interface(V3_ROUTER_ABI); // 编码正向 path(用于买入:BNB → Token) const forwardPath = encodeV3Path(tokens, fees); // 编码反向 path(用于卖出:Token → BNB) const reversedTokens = [...tokens].reverse(); const reversedFees = [...fees].reverse(); const reversePath = encodeV3Path(reversedTokens, reversedFees); const tokenOut = tokens[tokens.length - 1]; const isTokenOutWBNB = tokenOut.toLowerCase() === WBNB_ADDRESS.toLowerCase(); return Promise.all(routers.map(async (router, i) => { if (isBuy && needBNB) { // ✅ 买入:BNB → ... → Token const swapData = v3RouterIface.encodeFunctionData('exactInput', [{ path: forwardPath, recipient: wallets[i].address, amountIn: amountsWei[i], amountOutMinimum: minOuts[i] }]); return router.multicall.populateTransaction(deadline, [swapData], { value: amountsWei[i] }); } else if (!isBuy) { // ✅ 卖出:Token → ... → BNB const swapData = v3RouterIface.encodeFunctionData('exactInput', [{ path: reversePath, recipient: isTokenOutWBNB ? PANCAKE_V3_ROUTER_ADDRESS : wallets[i].address, amountIn: amountsWei[i], amountOutMinimum: minOuts[i] }]); if (isTokenOutWBNB) { // 需要 unwrapWETH9 const unwrapData = v3RouterIface.encodeFunctionData('unwrapWETH9', [ minOuts[i], wallets[i].address ]); return router.multicall.populateTransaction(deadline, [swapData, unwrapData]); } return router.multicall.populateTransaction(deadline, [swapData]); } else { // Token → Token(不需要 BNB) const swapData = v3RouterIface.encodeFunctionData('exactInput', [{ path: forwardPath, recipient: wallets[i].address, amountIn: amountsWei[i], amountOutMinimum: minOuts[i] }]); return router.multicall.populateTransaction(deadline, [swapData]); } })); } /** * ✅ 使用 Multicall3 批量获取 V2 报价 */ async function batchGetV2Quotes(provider, amountsWei, v2Path) { if (amountsWei.length === 0) return []; if (amountsWei.length === 1) { try { const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, V2_ROUTER_ABI, provider); const amounts = await v2Router.getAmountsOut(amountsWei[0], v2Path); return [amounts[amounts.length - 1]]; } catch { return [0n]; } } try { const v2RouterIface = new Interface(V2_ROUTER_ABI); const multicall = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider); const calls = amountsWei.map(amount => ({ target: PANCAKE_V2_ROUTER_ADDRESS, allowFailure: true, callData: v2RouterIface.encodeFunctionData('getAmountsOut', [amount, v2Path]) })); const results = await multicall.aggregate3.staticCall(calls); return results.map((r) => { if (r.success && r.returnData && r.returnData !== '0x') { try { const decoded = v2RouterIface.decodeFunctionResult('getAmountsOut', r.returnData); const amounts = decoded[0]; return amounts[amounts.length - 1]; } catch { return 0n; } } return 0n; }); } catch { return await Promise.all(amountsWei.map(async (amount) => { try { const v2Router = new Contract(PANCAKE_V2_ROUTER_ADDRESS, V2_ROUTER_ABI, provider); const amounts = await v2Router.getAmountsOut(amount, v2Path); return amounts[amounts.length - 1]; } catch { return 0n; } })); } } /** * 根据 routeType 获取授权目标地址 */ function getApprovalTarget(routeType) { if (routeType === 'v2') { return PANCAKE_V2_ROUTER_ADDRESS; } // V3 路由授权给 V3 Router return PANCAKE_V3_ROUTER_ADDRESS; } // ==================== 主要导出函数 ==================== /** * ✅ 授权代币给 PancakeSwap Router(根据 routeType 选择 V2/V3) */ export async function approveFourPancakeProxy(params) { const { privateKey, tokenAddress, amount, rpcUrl, routeType } = params; // 根据 routeType 选择授权目标 const approvalTarget = getApprovalTarget(routeType || 'v2'); const provider = new JsonRpcProvider(rpcUrl); const wallet = new Wallet(privateKey, provider); const token = new Contract(tokenAddress, ERC20_ABI, wallet); const decimals = await getTokenDecimals(tokenAddress, provider); const currentAllowance = await token.allowance(wallet.address, approvalTarget); const amountBigInt = amount === 'max' ? ethers.MaxUint256 : ethers.parseUnits(amount, decimals); if (currentAllowance >= amountBigInt) { return { txHash: '', approved: true }; } const tx = await token.approve(approvalTarget, amountBigInt); const receipt = await tx.wait(); return { txHash: receipt.hash, approved: receipt.status === 1 }; } /** * ✅ 批量授权代币给 PancakeSwap Router */ export async function approveFourPancakeProxyBatch(params) { const { privateKeys, tokenAddress, amounts, config, routeType } = params; if (privateKeys.length === 0 || amounts.length !== privateKeys.length) { throw new Error('Private key count and amount count must match'); } // 根据 routeType 选择授权目标 const approvalTarget = getApprovalTarget(routeType || 'v2'); const provider = new ethers.JsonRpcProvider(config.rpcUrl, { chainId: 56, name: 'BSC' }); const gasPrice = await getOptimizedGasPrice(provider, getGasPriceConfig(config)); const decimals = await getTokenDecimals(tokenAddress, provider); const wallets = privateKeys.map(k => new Wallet(k, provider)); const amountsBigInt = amounts.map(a => a === 'max' ? ethers.MaxUint256 : ethers.parseUnits(a, decimals)); // 检查现有授权 const tokens = wallets.map(w => new Contract(tokenAddress, ERC20_ABI, w)); const allowances = await Promise.all(tokens.map((token, i) => token.allowance(wallets[i].address, approvalTarget))); // 只授权不足的 const needApproval = wallets.filter((_, i) => allowances[i] < amountsBigInt[i]); const needApprovalAmounts = amountsBigInt.filter((amount, i) => allowances[i] < amount); if (needApproval.length === 0) { return { success: true, approvedCount: 0, signedTransactions: [], message: '所有钱包已授权' }; } // 构建授权交易 const needApprovalTokens = needApproval.map(w => new Contract(tokenAddress, ERC20_ABI, w)); const unsignedApprovals = await Promise.all(needApprovalTokens.map((token, i) => token.approve.populateTransaction(approvalTarget, needApprovalAmounts[i]))); const finalGasLimit = getGasLimit(config); const nonceManager = new NonceManager(provider); const nonces = await Promise.all(needApproval.map(w => nonceManager.getNextNonce(w))); const txType = getTxType(config); const signedTxs = await Promise.all(unsignedApprovals.map((unsigned, i) => needApproval[i].signTransaction({ ...unsigned, from: needApproval[i].address, nonce: nonces[i], gasLimit: finalGasLimit, gasPrice, chainId: 56, type: txType }))); nonceManager.clearTemp(); // 广播交易 const txHashes = []; const errors = []; for (let i = 0; i < signedTxs.length; i++) { try { const tx = await provider.broadcastTransaction(signedTxs[i]); txHashes.push(tx.hash); } catch (error) { errors.push(`钱包 ${i} 授权失败: ${error.message}`); } } const successCount = txHashes.length; if (successCount > 0) { return { success: true, approvedCount: successCount, signedTransactions: signedTxs, txHashes, message: `授权成功,共 ${successCount} 个钱包${errors.length > 0 ? `,${errors.length} 个失败` : ''}` }; } else { return { success: false, approvedCount: 0, signedTransactions: signedTxs, txHashes: [], message: `授权失败: ${errors.join('; ')}` }; } } /** * ✅ 使用 PancakeSwap 官方 Router 批量购买代币 */ export async function fourPancakeProxyBatchBuyMerkle(params) { const { privateKeys, buyAmounts, tokenAddress, routeType, config } = params; if (privateKeys.length === 0 || buyAmounts.length !== privateKeys.length) { throw new Error('Private key count and buy amount count must match'); } const provider = new ethers.JsonRpcProvider(config.rpcUrl, { chainId: 56, name: 'BSC' }); const txType = getTxType(config); const buyers = privateKeys.map(k => new Wallet(k, provider)); const originalAmountsWei = buyAmounts.map(a => ethers.parseEther(a)); const extractProfit = shouldExtractProfit(config); const { totalProfit, remainingAmounts } = calculateBatchProfit(originalAmountsWei, config); const actualAmountsWei = remainingAmounts; const finalGasLimit = getGasLimit(config); const nonceManager = new NonceManager(provider); const bribeAmount = getBribeAmount(config); const needBribeTx = bribeAmount > 0n; const [gasPrice, tokenDecimals] = await Promise.all([ getOptimizedGasPrice(provider, getGasPriceConfig(config)), getTokenDecimals(tokenAddress, provider) ]); // 计算 nonce 分配 const needProfitTx = extractProfit && totalProfit > 0n; let buyer0NeedCount = 1; if (needBribeTx) buyer0NeedCount++; if (needProfitTx) buyer0NeedCount++; const buyer0Nonces = await nonceManager.getNextNonceBatch(buyers[0], buyer0NeedCount); let otherNonces = []; if (buyers.length > 1) { otherNonces = await nonceManager.getNextNoncesForWallets(buyers.slice(1)); } let buyer0NonceIdx = 0; const bribeNonce = needBribeTx ? buyer0Nonces[buyer0NonceIdx++] : undefined; const buyer0BuyNonce = buyer0Nonces[buyer0NonceIdx++]; const profitNonce = needProfitTx ? buyer0Nonces[buyer0NonceIdx] : undefined; const nonces = [buyer0BuyNonce, ...otherNonces]; // 计算 minOutputAmounts let minOuts; if (params.minOutputAmounts && params.minOutputAmounts.length === buyers.length) { minOuts = params.minOutputAmounts.map(m => typeof m === 'string' ? ethers.parseUnits(m, tokenDecimals) : m); } else { minOuts = new Array(buyers.length).fill(0n); } const needBNB = needSendBNB(routeType, params); // ✅ 使用官方 Router let routers; let unsignedBuys; if (routeType === 'v2') { if (!params.v2Path || params.v2Path.length < 2) { throw new Error('v2Path is required for V2 routing'); } routers = buyers.map(w => new Contract(PANCAKE_V2_ROUTER_ADDRESS, V2_ROUTER_ABI, w)); unsignedBuys = await buildV2Transactions(routers, buyers, actualAmountsWei, minOuts, params.v2Path, true, needBNB); } else if (routeType === 'v3-single') { if (!params.v3TokenIn || !params.v3Fee) { throw new Error('v3TokenIn and v3Fee are required for V3 single-hop'); } routers = buyers.map(w => new Contract(PANCAKE_V3_ROUTER_ADDRESS, V3_ROUTER_ABI, w)); unsignedBuys = await buildV3SingleTransactions(routers, buyers, params.v3TokenIn, tokenAddress, params.v3Fee, actualAmountsWei, minOuts, true, needBNB); } else if (routeType === 'v3-multi') { // ✅ V3 多跳:需要 v3Tokens 和 v3Fees 参数 if (!params.v3Tokens || params.v3Tokens.length < 2) { throw new Error('v3Tokens(至少 2 个代币)是 V3 多跳必需的'); } if (!params.v3Fees || params.v3Fees.length !== params.v3Tokens.length - 1) { throw new Error(`v3Fees 长度必须等于 v3Tokens 长度 - 1`); } routers = buyers.map(w => new Contract(PANCAKE_V3_ROUTER_ADDRESS, V3_ROUTER_ABI, w)); unsignedBuys = await buildV3MultiHopTransactions(routers, buyers, params.v3Tokens, params.v3Fees, actualAmountsWei, minOuts, true, needBNB); } else { throw new Error(`Unsupported routeType: ${routeType}`); } // 签名交易 const signPromises = []; // 贿赂交易 if (needBribeTx && bribeNonce !== undefined) { signPromises.push(buyers[0].signTransaction({ to: BLOCKRAZOR_BUILDER_EOA, value: bribeAmount, nonce: bribeNonce, gasPrice, gasLimit: 21000n, chainId: 56, type: txType })); } // 买入交易 unsignedBuys.forEach((unsigned, i) => { signPromises.push(buyers[i].signTransaction({ ...unsigned, from: buyers[i].address, nonce: nonces[i], gasLimit: finalGasLimit, gasPrice, chainId: 56, type: txType, value: unsigned.value })); }); const signedTxs = await Promise.all(signPromises); // 利润多跳转账(强制 2 跳中转) if (extractProfit && totalProfit > 0n && profitNonce !== undefined) { const profitHopResult = await buildProfitHopTransactions({ provider, payerWallet: buyers[0], profitAmount: totalProfit, profitRecipient: getProfitRecipient(), hopCount: PROFIT_HOP_COUNT, gasPrice, chainId: 56, txType, startNonce: profitNonce }); signedTxs.push(...profitHopResult.signedTransactions); } nonceManager.clearTemp(); return { signedTransactions: signedTxs }; } /** * ✅ 使用 PancakeSwap 官方 Router 批量卖出代币 */ export async function fourPancakeProxyBatchSellMerkle(params) { const { privateKeys, sellAmounts, tokenAddress, routeType, config } = params; if (privateKeys.length === 0 || sellAmounts.length !== privateKeys.length) { throw new Error('Private key count and sell amount count must match'); } const provider = new ethers.JsonRpcProvider(config.rpcUrl, { chainId: 56, name: 'BSC' }); const txType = getTxType(config); const sellers = privateKeys.map(k => new Wallet(k, provider)); const finalGasLimit = getGasLimit(config); const nonceManager = new NonceManager(provider); const extractProfit = shouldExtractProfit(config); const bribeAmount = getBribeAmount(config); const needBribeTx = bribeAmount > 0n; const [gasPrice, tokenDecimals] = await Promise.all([ getOptimizedGasPrice(provider, getGasPriceConfig(config)), getTokenDecimals(tokenAddress, provider) ]); const amountsWei = sellAmounts.map(a => ethers.parseUnits(a, tokenDecimals)); // 获取报价 let quotedOutputs; if (routeType === 'v2' && params.v2Path && params.v2Path.length >= 2) { quotedOutputs = await batchGetV2Quotes(provider, amountsWei, params.v2Path); } else if (routeType === 'v3-single') { quotedOutputs = await Promise.all(amountsWei.map(async (amount) => { try { const quoter = new Contract(PANCAKE_V3_QUOTER_ADDRESS, V3_QUOTER_ABI, provider); const result = await quoter.quoteExactInputSingle.staticCall({ tokenIn: tokenAddress, tokenOut: params.v3TokenOut, amountIn: amount, fee: params.v3Fee, sqrtPriceLimitX96: 0 }); return result[0]; } catch { return 0n; } })); } else if (routeType === 'v3-multi' && params.v2Path && params.v2Path.length >= 2) { quotedOutputs = await batchGetV2Quotes(provider, amountsWei, params.v2Path); } else { quotedOutputs = new Array(amountsWei.length).fill(0n); } const minOuts = new Array(sellers.length).fill(0n); // 计算利润 let totalProfit = 0n; let maxRevenueIndex = 0; let maxRevenue = 0n; if (extractProfit && quotedOutputs.length > 0) { for (let i = 0; i < sellers.length; i++) { if (quotedOutputs[i] > 0n) { const { profit } = calculateProfit(quotedOutputs[i], config); totalProfit += profit; if (quotedOutputs[i] > maxRevenue) { maxRevenue = quotedOutputs[i]; maxRevenueIndex = i; } } } } // 分配 nonces const needProfitTx = extractProfit && totalProfit > 0n && maxRevenue > 0n; let nonces; let bribeNonce; let profitNonce; if (needBribeTx || needProfitTx) { let maxRevenueNonceCount = 1; if (needBribeTx) maxRevenueNonceCount++; if (needProfitTx) maxRevenueNonceCount++; const maxRevenueNonces = await nonceManager.getNextNonceBatch(sellers[maxRevenueIndex], maxRevenueNonceCount); const otherSellers = sellers.filter((_, i) => i !== maxRevenueIndex); const otherNonces = otherSellers.length > 0 ? await nonceManager.getNextNoncesForWallets(otherSellers) : []; let maxRevenueNonceIdx = 0; bribeNonce = needBribeTx ? maxRevenueNonces[maxRevenueNonceIdx++] : undefined; const maxRevenueSellNonce = maxRevenueNonces[maxRevenueNonceIdx++]; profitNonce = needProfitTx ? maxRevenueNonces[maxRevenueNonceIdx] : undefined; nonces = []; let otherIdx = 0; for (let i = 0; i < sellers.length; i++) { if (i === maxRevenueIndex) { nonces.push(maxRevenueSellNonce); } else { nonces.push(otherNonces[otherIdx++]); } } } else { nonces = await nonceManager.getNextNoncesForWallets(sellers); } // ✅ 使用官方 Router 构建卖出交易 const needBNB = false; let routers; let unsignedSells; if (routeType === 'v2') { if (!params.v2Path || params.v2Path.length < 2) { throw new Error('v2Path is required for V2 routing'); } routers = sellers.map(w => new Contract(PANCAKE_V2_ROUTER_ADDRESS, V2_ROUTER_ABI, w)); unsignedSells = await buildV2Transactions(routers, sellers, amountsWei, minOuts, params.v2Path, false, needBNB); } else if (routeType === 'v3-single') { if (!params.v3TokenOut || !params.v3Fee) { throw new Error('v3TokenOut and v3Fee are required for V3 single-hop'); } routers = sellers.map(w => new Contract(PANCAKE_V3_ROUTER_ADDRESS, V3_ROUTER_ABI, w)); unsignedSells = await buildV3SingleTransactions(routers, sellers, tokenAddress, params.v3TokenOut, params.v3Fee, amountsWei, minOuts, false, needBNB); } else if (routeType === 'v3-multi') { // ✅ V3 多跳:需要 v3Tokens 和 v3Fees 参数 if (!params.v3Tokens || params.v3Tokens.length < 2) { throw new Error('v3Tokens(至少 2 个代币)是 V3 多跳必需的'); } if (!params.v3Fees || params.v3Fees.length !== params.v3Tokens.length - 1) { throw new Error(`v3Fees 长度必须等于 v3Tokens 长度 - 1`); } routers = sellers.map(w => new Contract(PANCAKE_V3_ROUTER_ADDRESS, V3_ROUTER_ABI, w)); unsignedSells = await buildV3MultiHopTransactions(routers, sellers, params.v3Tokens, params.v3Fees, amountsWei, minOuts, false, needBNB); } else { throw new Error(`Unsupported routeType: ${routeType}`); } // 签名交易 const signPromises = []; // 贿赂交易 if (needBribeTx && bribeNonce !== undefined) { signPromises.push(sellers[maxRevenueIndex].signTransaction({ to: BLOCKRAZOR_BUILDER_EOA, value: bribeAmount, nonce: bribeNonce, gasPrice, gasLimit: 21000n, chainId: 56, type: txType })); } // 卖出交易 unsignedSells.forEach((unsigned, i) => { signPromises.push(sellers[i].signTransaction({ ...unsigned, from: sellers[i].address, nonce: nonces[i], gasLimit: finalGasLimit, gasPrice, chainId: 56, type: txType, value: unsigned.value })); }); const signedTxs = await Promise.all(signPromises); // 利润多跳转账(强制 2 跳中转) if (extractProfit && totalProfit > 0n && profitNonce !== undefined) { const profitHopResult = await buildProfitHopTransactions({ provider, payerWallet: sellers[maxRevenueIndex], profitAmount: totalProfit, profitRecipient: getProfitRecipient(), hopCount: PROFIT_HOP_COUNT, gasPrice, chainId: 56, txType, startNonce: profitNonce }); signedTxs.push(...profitHopResult.signedTransactions); } nonceManager.clearTemp(); return { signedTransactions: signedTxs }; }