UNPKG

four-flap-meme-sdk

Version:

SDK for Flap bonding curve and four.meme TokenManager

498 lines (497 loc) 22.3 kB
/** * PancakeSwap V2/V3 通用捆绑换手(Merkle Bundle)- 先买后卖 * * 功能:钱包B先买入代币 → 钱包A卖出相同数量 → 原子执行 */ import { ethers, Contract, Wallet } from 'ethers'; import { NonceManager, getDeadline, buildProfitHopTransactions, PROFIT_HOP_COUNT } from '../utils/bundle-helpers.js'; import { ADDRESSES, PROFIT_CONFIG, BLOCKRAZOR_BUILDER_EOA, ZERO_ADDRESS } from '../utils/constants.js'; import { quoteV2, quoteV3, getTokenToNativeQuote, getWrappedNativeAddress } from '../utils/quote-helpers.js'; import { V2_ROUTER_ABI, V3_ROUTER02_ABI, ERC20_BALANCE_ABI } from '../abis/common.js'; /** * 获取 Gas Limit */ function getGasLimit(config, defaultGas = 800000) { 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); } async function getGasPrice(provider, config) { const feeData = await provider.getFeeData(); let gasPrice = feeData.gasPrice || ethers.parseUnits('3', 'gwei'); if (config.minGasPriceGwei) { const minGas = ethers.parseUnits(config.minGasPriceGwei.toString(), 'gwei'); if (gasPrice < minGas) gasPrice = minGas; } if (config.maxGasPriceGwei) { const maxGas = ethers.parseUnits(config.maxGasPriceGwei.toString(), 'gwei'); if (gasPrice > maxGas) gasPrice = maxGas; } return gasPrice; } // ✅ ABI 别名(从公共模块导入) const PANCAKE_V2_ROUTER_ABI = V2_ROUTER_ABI; const PANCAKE_V3_ROUTER_ABI = V3_ROUTER02_ABI; const ERC20_BALANCE_OF_ABI = ERC20_BALANCE_ABI; // ✅ 使用官方 PancakeSwap Router 地址 const PANCAKE_V2_ROUTER_ADDRESS = ADDRESSES.BSC.PancakeV2Router; const PANCAKE_V3_ROUTER_ADDRESS = ADDRESSES.BSC.PancakeV3Router; // 常量 const FLAT_FEE = 0n; const WBNB_ADDRESS = ADDRESSES.BSC.WBNB; export async function pancakeBundleBuyFirstMerkle(params) { const { buyerPrivateKey, sellerPrivateKey, tokenAddress, routeParams, buyerFunds, buyerFundsPercentage, config, quoteToken, quoteTokenDecimals = 18, startNonces // ✅ 可选:前端预获取的 nonces } = params; // ✅ 判断是否使用原生代币(BNB)或 ERC20 代币(如 USDT) const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS; const context = createPancakeContext(config); const buyer = new Wallet(buyerPrivateKey, context.provider); const seller = new Wallet(sellerPrivateKey, context.provider); const sameAddress = buyer.address.toLowerCase() === seller.address.toLowerCase(); const buyerFundsInfo = await calculateBuyerFunds({ buyer, buyerFunds, buyerFundsPercentage, reserveGas: config.reserveGasBNB, useNativeToken, quoteToken, quoteTokenDecimals, provider: context.provider }); const quoteResult = await quoteTokenOutput({ routeParams, buyerFundsWei: buyerFundsInfo.buyerFundsWei, provider: context.provider }); const swapUnsigned = await buildRouteTransactions({ routeParams, buyerFundsWei: buyerFundsInfo.buyerFundsWei, sellAmountToken: quoteResult.quotedTokenOut, buyer, seller, tokenAddress, useNativeToken }); const quotedNative = await quoteSellerNative({ provider: context.provider, tokenAddress, sellAmountToken: quoteResult.quotedTokenOut, routeParams // ✅ 传递路由参数 }); const buyerNeed = calculateBuyerNeed({ quotedNative, buyerBalance: buyerFundsInfo.buyerBalance, reserveGas: buyerFundsInfo.reserveGas }); const finalGasLimit = getGasLimit(config); const gasPrice = await getGasPrice(context.provider, config); const txType = config.txType ?? 0; const nonceManager = new NonceManager(context.provider); // ✅ 修复:基于买入金额估算利润,而不是基于卖出预估(因为代币可能还没有流动性) const estimatedProfitFromSell = await estimateProfitAmount({ provider: context.provider, tokenAddress, sellAmountToken: quoteResult.quotedTokenOut, routeParams // ✅ 传递路由参数 }); // 万分之六 const profitBase = estimatedProfitFromSell > 0n ? estimatedProfitFromSell : (buyerFundsInfo.buyerFundsWei * BigInt(PROFIT_CONFIG.RATE_BPS_SWAP)) / 10000n; const profitAmount = profitBase; // ✅ 获取贿赂金额 const bribeAmount = config.bribeAmount && config.bribeAmount > 0 ? ethers.parseEther(String(config.bribeAmount)) : 0n; const needBribeTx = bribeAmount > 0n; // ✅ 如果前端传入了 startNonces,直接使用(性能优化,避免 nonce 冲突) const noncePlan = startNonces && startNonces.length >= (sameAddress ? 1 : 2) ? buildNoncePlanFromStartNonces(startNonces, sameAddress, profitAmount > 0n, needBribeTx) : await planNonces({ buyer, seller, sameAddress, extractProfit: profitAmount > 0n, needBribeTx, nonceManager }); // ✅ 贿赂交易放在首位(由卖方发送,与利润交易同一钱包) let bribeTx = null; if (needBribeTx && noncePlan.bribeNonce !== undefined) { bribeTx = await seller.signTransaction({ to: BLOCKRAZOR_BUILDER_EOA, value: bribeAmount, nonce: noncePlan.bribeNonce, gasPrice, gasLimit: 21000n, chainId: context.chainId, type: txType }); } const signedBuy = await buyer.signTransaction({ ...swapUnsigned.buyUnsigned, from: buyer.address, nonce: noncePlan.buyerNonce, gasLimit: finalGasLimit, gasPrice, chainId: context.chainId, type: txType }); const signedSell = await seller.signTransaction({ ...swapUnsigned.sellUnsigned, from: seller.address, nonce: noncePlan.sellerNonce, gasLimit: finalGasLimit, gasPrice, chainId: context.chainId, type: txType }); nonceManager.clearTemp(); validateFinalBalances({ sameAddress, buyerFundsWei: buyerFundsInfo.buyerFundsWei, buyerBalance: buyerFundsInfo.buyerBalance, reserveGas: buyerFundsInfo.reserveGas, gasLimit: finalGasLimit, gasPrice, useNativeToken, quoteTokenDecimals, provider: context.provider, buyerAddress: buyer.address }); // ✅ 组装顺序:贿赂 → 买入 → 卖出 const allTransactions = []; if (bribeTx) allTransactions.push(bribeTx); allTransactions.push(signedBuy, signedSell); // ✅ 利润多跳转账(强制 2 跳中转) const profitTxs = await buildProfitTransaction({ provider: context.provider, seller, profitAmount, profitNonce: noncePlan.profitNonce, gasPrice, chainId: context.chainId, txType }); if (profitTxs) allTransactions.push(...profitTxs); return { signedTransactions: allTransactions, metadata: { buyerAddress: buyer.address, sellerAddress: seller.address, buyAmount: useNativeToken ? ethers.formatEther(buyerFundsInfo.buyerFundsWei) : ethers.formatUnits(buyerFundsInfo.buyerFundsWei, quoteTokenDecimals), sellAmount: quoteResult.quotedTokenOut.toString(), profitAmount: profitAmount > 0n ? ethers.formatEther(profitAmount) : undefined } }; } function createPancakeContext(config) { const chainId = config.chainId ?? 56; const provider = new ethers.JsonRpcProvider(config.rpcUrl, { chainId, name: 'BSC' }); return { chainId, provider }; } async function calculateBuyerFunds({ buyer, buyerFunds, buyerFundsPercentage, reserveGas, useNativeToken = true, quoteToken, quoteTokenDecimals = 18, provider }) { const reserveGasWei = ethers.parseEther((reserveGas ?? 0.0005).toString()); // ✅ 根据是否使用原生代币获取不同的余额 let buyerBalance; if (useNativeToken) { buyerBalance = await buyer.provider.getBalance(buyer.address); } else { // ERC20 代币余额 const erc20 = new Contract(quoteToken, ERC20_BALANCE_OF_ABI, provider || buyer.provider); buyerBalance = await erc20.balanceOf(buyer.address); } let buyerFundsWei; if (buyerFunds !== undefined) { // ✅ 根据代币精度解析金额 buyerFundsWei = useNativeToken ? ethers.parseEther(String(buyerFunds)) : ethers.parseUnits(String(buyerFunds), quoteTokenDecimals); } else if (buyerFundsPercentage !== undefined) { const pct = Math.max(0, Math.min(100, buyerFundsPercentage)); // ✅ 原生代币需要预留 Gas,ERC20 不需要 const spendable = useNativeToken ? (buyerBalance > reserveGasWei ? buyerBalance - reserveGasWei : 0n) : buyerBalance; buyerFundsWei = (spendable * BigInt(Math.floor(pct * 100))) / 10000n; } else { throw new Error('必须提供 buyerFunds 或 buyerFundsPercentage'); } if (buyerFundsWei <= 0n) { throw new Error('buyerFunds 需要大于 0'); } // ✅ 余额检查 if (useNativeToken) { if (buyerBalance < buyerFundsWei + reserveGasWei) { throw new Error(`买方余额不足: 需要 ${ethers.formatEther(buyerFundsWei + reserveGasWei)} BNB,实际 ${ethers.formatEther(buyerBalance)} BNB`); } } else { // ERC20 购买:检查代币余额 if (buyerBalance < buyerFundsWei) { throw new Error(`买方代币余额不足: 需要 ${ethers.formatUnits(buyerFundsWei, quoteTokenDecimals)},实际 ${ethers.formatUnits(buyerBalance, quoteTokenDecimals)}`); } // ✅ ERC20 购买时,还需要检查买方是否有足够 BNB 支付 Gas const buyerBnbBalance = await buyer.provider.getBalance(buyer.address); if (buyerBnbBalance < reserveGasWei) { throw new Error(`买方 BNB 余额不足 (用于支付 Gas): 需要 ${ethers.formatEther(reserveGasWei)} BNB,实际 ${ethers.formatEther(buyerBnbBalance)} BNB`); } } return { buyerFundsWei, buyerBalance, reserveGas: reserveGasWei }; } /** * ✅ 使用 quote-helpers 统一报价 */ async function quoteTokenOutput({ routeParams, buyerFundsWei, provider }) { // V2 路由 if (routeParams.routeType === 'v2') { const { v2Path } = routeParams; const tokenIn = v2Path[0]; const tokenOut = v2Path[v2Path.length - 1]; const result = await quoteV2(provider, tokenIn, tokenOut, buyerFundsWei, 'BSC'); if (result.amountOut <= 0n) { throw new Error('V2 报价失败'); } return { quotedTokenOut: result.amountOut }; } // V3 Single 路由 if (routeParams.routeType === 'v3-single') { const paramsV3 = routeParams; const result = await quoteV3(provider, paramsV3.v3TokenIn, paramsV3.v3TokenOut, buyerFundsWei, 'BSC', paramsV3.v3Fee); if (result.amountOut <= 0n) { throw new Error('V3 报价失败'); } return { quotedTokenOut: result.amountOut }; } // V3 Multi 路由 const paramsV3m = routeParams; if (!paramsV3m.v2Path || paramsV3m.v2Path.length < 2) { throw new Error('V3 多跳需要提供 v2Path 用于路径推断'); } const tokenIn = paramsV3m.v2Path[0]; const tokenOut = paramsV3m.v2Path[paramsV3m.v2Path.length - 1]; const result = await quoteV3(provider, tokenIn, tokenOut, buyerFundsWei, 'BSC'); if (result.amountOut <= 0n) { throw new Error('V3 多跳报价失败'); } return { quotedTokenOut: result.amountOut }; } /** * ✅ 使用 quote-helpers 统一报价(卖出 → 原生代币) */ async function quoteSellerNative({ provider, tokenAddress, sellAmountToken, routeParams }) { const wbnb = getWrappedNativeAddress('BSC'); const version = routeParams.routeType === 'v2' ? 'v2' : 'v3'; const fee = routeParams.routeType === 'v3-single' ? routeParams.v3Fee : undefined; return await getTokenToNativeQuote(provider, tokenAddress, sellAmountToken, 'BSC', version, fee); } function calculateBuyerNeed({ quotedNative, buyerBalance, reserveGas }) { const estimatedBuyerNeed = quotedNative; const buyerNeedTotal = estimatedBuyerNeed + reserveGas; if (buyerBalance < buyerNeedTotal) { throw new Error(`买方余额不足:\n - 需要: ${ethers.formatEther(buyerNeedTotal)} BNB\n - 实际: ${ethers.formatEther(buyerBalance)} BNB`); } const maxBuyerValue = estimatedBuyerNeed > 0n ? estimatedBuyerNeed : buyerBalance - reserveGas; return { buyerNeedTotal, maxBuyerValue }; } async function buildRouteTransactions({ routeParams, buyerFundsWei, sellAmountToken, buyer, seller, tokenAddress, useNativeToken = true }) { const deadline = getDeadline(); // ✅ ERC20 购买时,value 只需要 FLAT_FEE const buyValue = useNativeToken ? buyerFundsWei + FLAT_FEE : FLAT_FEE; if (routeParams.routeType === 'v2') { const { v2Path } = routeParams; const reversePath = [...v2Path].reverse(); // ✅ 使用官方 V2 Router const v2RouterBuyer = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, buyer); const v2RouterSeller = new Contract(PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V2_ROUTER_ABI, seller); // 买入:BNB → Token const buyUnsigned = await v2RouterBuyer.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(0n, v2Path, buyer.address, deadline, { value: buyValue }); // 卖出:Token → BNB const sellUnsigned = await v2RouterSeller.swapExactTokensForETHSupportingFeeOnTransferTokens.populateTransaction(sellAmountToken, 0n, reversePath, seller.address, deadline); return { buyUnsigned, sellUnsigned }; } if (routeParams.routeType === 'v3-single') { const { v3TokenIn, v3TokenOut, v3Fee } = routeParams; const v3RouterIface = new ethers.Interface(PANCAKE_V3_ROUTER_ABI); const v3RouterBuyer = new Contract(PANCAKE_V3_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, buyer); const v3RouterSeller = new Contract(PANCAKE_V3_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, seller); // 买入:WBNB → Token const buySwapData = v3RouterIface.encodeFunctionData('exactInputSingle', [{ tokenIn: v3TokenIn, tokenOut: v3TokenOut, fee: v3Fee, recipient: buyer.address, amountIn: buyerFundsWei, amountOutMinimum: 0n, sqrtPriceLimitX96: 0n }]); const buyUnsigned = await v3RouterBuyer.multicall.populateTransaction(deadline, [buySwapData], { value: buyValue }); // 卖出:Token → WBNB → BNB const sellSwapData = v3RouterIface.encodeFunctionData('exactInputSingle', [{ tokenIn: v3TokenOut, tokenOut: v3TokenIn, fee: v3Fee, recipient: PANCAKE_V3_ROUTER_ADDRESS, amountIn: sellAmountToken, amountOutMinimum: 0n, sqrtPriceLimitX96: 0n }]); const sellUnwrapData = v3RouterIface.encodeFunctionData('unwrapWETH9', [0n, seller.address]); const sellUnsigned = await v3RouterSeller.multicall.populateTransaction(deadline, [sellSwapData, sellUnwrapData]); return { buyUnsigned, sellUnsigned }; } // V3 多跳暂不支持官方合约 throw new Error('V3 多跳路由暂不支持官方合约,请使用 V2 路由或 V3 单跳'); } /** * ✅ 使用 quote-helpers 统一报价(估算利润) */ async function estimateProfitAmount({ provider, tokenAddress, sellAmountToken, routeParams }) { try { const version = routeParams.routeType === 'v2' ? 'v2' : 'v3'; const fee = routeParams.routeType === 'v3-single' ? routeParams.v3Fee : undefined; const estimatedSellFunds = await getTokenToNativeQuote(provider, tokenAddress, sellAmountToken, 'BSC', version, fee); if (estimatedSellFunds <= 0n) { return 0n; } // 万分之六 return (estimatedSellFunds * BigInt(PROFIT_CONFIG.RATE_BPS_SWAP)) / 10000n; } catch { return 0n; } } /** * ✅ 规划 nonce * 交易顺序:贿赂 → 买入 → 卖出 → 利润 */ async function planNonces({ buyer, seller, sameAddress, extractProfit, needBribeTx, nonceManager }) { if (sameAddress) { // 同一地址:贿赂(可选) + 买入 + 卖出 + 利润(可选) const txCount = countTruthy([needBribeTx, true, true, extractProfit]); const nonces = await nonceManager.getNextNonceBatch(buyer, txCount); let idx = 0; const bribeNonce = needBribeTx ? nonces[idx++] : undefined; const buyerNonce = nonces[idx++]; const sellerNonce = nonces[idx++]; const profitNonce = extractProfit ? nonces[idx] : undefined; return { buyerNonce, sellerNonce, bribeNonce, profitNonce }; } if (needBribeTx || extractProfit) { // 卖方需要多个 nonce:贿赂(可选) + 卖出 + 利润(可选) const sellerTxCount = countTruthy([needBribeTx, true, extractProfit]); // ✅ 并行获取 seller 和 buyer 的 nonce const [sellerNonces, buyerNonce] = await Promise.all([ nonceManager.getNextNonceBatch(seller, sellerTxCount), nonceManager.getNextNonce(buyer) ]); let idx = 0; const bribeNonce = needBribeTx ? sellerNonces[idx++] : undefined; const sellerNonce = sellerNonces[idx++]; const profitNonce = extractProfit ? sellerNonces[idx] : undefined; return { buyerNonce, sellerNonce, bribeNonce, profitNonce }; } const [buyerNonce, sellerNonce] = await Promise.all([ nonceManager.getNextNonce(buyer), nonceManager.getNextNonce(seller) ]); return { buyerNonce, sellerNonce }; } /** * 构建利润多跳转账交易(强制 2 跳中转) */ async function buildProfitTransaction({ provider, seller, profitAmount, profitNonce, gasPrice, chainId, txType }) { if (profitNonce === undefined || profitAmount === 0n) { return null; } const profitHopResult = await buildProfitHopTransactions({ provider, payerWallet: seller, profitAmount, profitRecipient: PROFIT_CONFIG.RECIPIENT, hopCount: PROFIT_HOP_COUNT, gasPrice, chainId, txType, startNonce: profitNonce }); return profitHopResult.signedTransactions; } async function validateFinalBalances({ sameAddress, buyerFundsWei, buyerBalance, reserveGas, gasLimit, gasPrice, useNativeToken = true, quoteTokenDecimals = 18, provider, buyerAddress }) { const gasCost = gasLimit * gasPrice; if (sameAddress) { // 同一地址:需要足够的余额支付两笔交易 if (useNativeToken) { const requiredCombined = buyerFundsWei + FLAT_FEE * 2n + gasCost * 2n; if (buyerBalance < requiredCombined) { throw new Error(`账户余额不足:\n - 需要: ${ethers.formatEther(requiredCombined)} BNB(含两笔Gas与两笔手续费)\n - 实际: ${ethers.formatEther(buyerBalance)} BNB`); } } else { // ERC20:检查代币余额 + BNB Gas 余额 const requiredToken = buyerFundsWei + FLAT_FEE * 2n; if (buyerBalance < requiredToken) { throw new Error(`账户代币余额不足:\n - 需要: ${ethers.formatUnits(requiredToken, quoteTokenDecimals)}\n - 实际: ${ethers.formatUnits(buyerBalance, quoteTokenDecimals)}`); } // 检查 BNB Gas if (provider && buyerAddress) { const bnbBalance = await provider.getBalance(buyerAddress); const requiredGas = gasCost * 2n; if (bnbBalance < requiredGas) { throw new Error(`账户 BNB 余额不足 (用于支付 Gas):\n - 需要: ${ethers.formatEther(requiredGas)} BNB\n - 实际: ${ethers.formatEther(bnbBalance)} BNB`); } } } return; } // 不同地址 if (useNativeToken) { const requiredBuyer = buyerFundsWei + FLAT_FEE + reserveGas; if (buyerBalance < requiredBuyer) { throw new Error(`买方余额不足:\n - 需要: ${ethers.formatEther(requiredBuyer)} BNB\n - 实际: ${ethers.formatEther(buyerBalance)} BNB`); } } // ERC20 余额已在 calculateBuyerFunds 中检查过 } function countTruthy(values) { return values.filter(Boolean).length; } /** * ✅ 从前端传入的 startNonces 构建 NoncePlan(用于性能优化,避免 nonce 冲突) * 顺序:同地址时 [baseNonce],不同地址时 [sellerNonce, buyerNonce] */ function buildNoncePlanFromStartNonces(startNonces, sameAddress, profitNeeded, needBribeTx) { if (sameAddress) { // 同一地址:贿赂(可选) + 买入 + 卖出 + 利润(可选) let idx = 0; const baseNonce = startNonces[0]; const bribeNonce = needBribeTx ? baseNonce + idx++ : undefined; const buyerNonce = baseNonce + idx++; const sellerNonce = baseNonce + idx++; const profitNonce = profitNeeded ? baseNonce + idx : undefined; return { buyerNonce, sellerNonce, bribeNonce, profitNonce }; } // 不同地址 let sellerIdx = 0; const sellerBaseNonce = startNonces[0]; const bribeNonce = needBribeTx ? sellerBaseNonce + sellerIdx++ : undefined; const sellerNonce = sellerBaseNonce + sellerIdx++; const profitNonce = profitNeeded ? sellerBaseNonce + sellerIdx : undefined; const buyerNonce = startNonces[1]; return { buyerNonce, sellerNonce, bribeNonce, profitNonce }; }