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