four-flap-meme-sdk
Version:
SDK for Flap bonding curve and four.meme TokenManager
681 lines (680 loc) • 28.7 kB
JavaScript
/**
* 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
};
}