four-flap-meme-sdk
Version:
SDK for Flap bonding curve and four.meme TokenManager
158 lines (157 loc) • 6.21 kB
JavaScript
/**
* 换手交易通用工具函数
*/
import { ethers, Contract } from 'ethers';
import { batchCheckAllowances } from './erc20.js';
import { ERC20_ABI } from '../abis/common.js';
const APPROVAL_THRESHOLD = ethers.MaxUint256 / 2n;
/**
* 获取 Gas Limit(授权专用)
* 优先使用 config.gasLimit,否则使用默认值 * multiplier
*
* ✅ ethers v6 最佳实践:直接使用 bigint 类型
*/
function getApprovalGasLimit(config, defaultGas = 80000) {
// 优先使用前端传入的 gasLimit
if (config.gasLimit !== undefined) {
// 如果已经是 bigint,直接返回;否则转换
return typeof config.gasLimit === 'bigint' ? config.gasLimit : BigInt(config.gasLimit);
}
// 使用默认值 * multiplier
const multiplier = config.gasLimitMultiplier ?? 1.0;
const calculatedGas = Math.ceil(defaultGas * multiplier);
// JavaScript 原生 BigInt 转换(ethers v6 兼容)
return BigInt(calculatedGas);
}
/**
* 检查并执行代币授权(如果需要)
* ✅ 与 pancake-proxy.ts 的授权逻辑一致
*/
export async function ensureTokenApproval(provider, merkle, wallet, tokenAddress, spenderAddress, config) {
// 使用Multicall3批量检查(虽然只有一个地址,但保持接口一致)
const allowances = await batchCheckAllowances(provider, tokenAddress, [wallet.address], spenderAddress);
const currentAllowance = allowances[0];
if (currentAllowance >= APPROVAL_THRESHOLD) {
return;
}
// ✅ 使用 NonceManager(与 pancake-proxy.ts 一致)
const { NonceManager, getOptimizedGasPrice } = await import('./bundle-helpers.js');
const nonceManager = new NonceManager(provider);
// 构建授权交易
const tokenContract = new Contract(tokenAddress, ERC20_ABI, wallet);
const approveUnsigned = await tokenContract.approve.populateTransaction(spenderAddress, ethers.MaxUint256);
// ✅ 使用 getApprovalGasLimit 函数处理 Gas Limit(与 pancake-proxy.ts 一致)
const approveGasLimit = getApprovalGasLimit(config, 80000);
// ✅ 使用 getOptimizedGasPrice 处理 Gas Price(与 pancake-proxy.ts 一致)
const gasPriceConfig = {};
if (config.minGasPriceGwei !== undefined) {
gasPriceConfig.baseGasPrice = ethers.parseUnits(String(config.minGasPriceGwei), 'gwei');
gasPriceConfig.multiplierPercent = 0;
}
if (config.maxGasPriceGwei !== undefined) {
gasPriceConfig.maxGasPrice = ethers.parseUnits(String(config.maxGasPriceGwei), 'gwei');
}
const gasPrice = await getOptimizedGasPrice(provider, gasPriceConfig);
// ✅ 使用 NonceManager 获取 nonce
const nonce = await nonceManager.getNextNonce(wallet);
// ✅ 获取 chainId(不硬编码)
const network = await provider.getNetwork();
const chainId = Number(network.chainId);
// 签名
const signedTx = await wallet.signTransaction({
...approveUnsigned,
from: wallet.address,
nonce,
gasLimit: approveGasLimit,
gasPrice,
chainId,
type: config.txType ?? 0
});
// 清理临时 nonce 缓存
nonceManager.clearTemp();
// ✅ 提交 Bundle(使用 blockOffset)
const blockOffset = config.bundleBlockOffset ?? 5;
const bundleResult = await merkle.sendBundle({
transactions: [signedTx],
blockOffset,
minTimestamp: 0,
maxTimestamp: 0
});
// 等待确认
const results = await merkle.waitForBundleConfirmation(bundleResult.txHashes, 1, config.waitTimeoutMs ?? 120000);
if (!results[0]?.success) {
throw new Error(`授权失败: ${results[0]?.error || '未知错误'}`);
}
}
/**
* 获取代币余额
*/
export async function getTokenBalance(provider, tokenAddress, walletAddress) {
const tokenContract = new Contract(tokenAddress, ERC20_ABI, provider);
return await tokenContract.balanceOf(walletAddress);
}
/**
* 获取代币精度
*/
export async function getTokenDecimals(provider, tokenAddress) {
const tokenContract = new Contract(tokenAddress, ERC20_ABI, provider);
return await tokenContract.decimals();
}
/**
* 计算卖出数量(支持百分比)
*/
export async function calculateSellAmount(provider, tokenAddress, walletAddress, sellAmount, sellPercentage) {
if (!sellAmount && !sellPercentage) {
throw new Error('必须指定 sellAmount 或 sellPercentage');
}
if (sellAmount && sellPercentage) {
throw new Error('sellAmount 和 sellPercentage 不能同时指定');
}
// 精确数量:只需要 decimals
if (sellAmount) {
const decimals = await getTokenDecimals(provider, tokenAddress);
return {
amount: ethers.parseUnits(sellAmount, decimals),
decimals
};
}
// 百分比:需要 decimals 和 balance
if (sellPercentage !== undefined) {
if (sellPercentage <= 0 || sellPercentage > 100) {
throw new Error('sellPercentage 必须在 0-100 之间');
}
// ✅ 并行获取 decimals 和 balance
const [decimals, balance] = await Promise.all([
getTokenDecimals(provider, tokenAddress),
getTokenBalance(provider, tokenAddress, walletAddress)
]);
const amount = (balance * BigInt(Math.floor(sellPercentage * 100))) / 10000n;
return { amount, decimals };
}
throw new Error('无效的参数');
}
/**
* 计算换手损耗百分比
*/
export function calculateSwapLoss(sellAmount, buyAmount) {
if (sellAmount === 0n)
return '0%';
const loss = sellAmount - buyAmount;
const lossPercent = (Number(loss) / Number(sellAmount)) * 100;
return lossPercent.toFixed(4) + '%';
}
/**
* 验证钱包BNB余额
*/
export async function validateBNBBalance(provider, walletAddress, requiredBNB, purpose) {
const balance = await provider.getBalance(walletAddress);
if (balance < requiredBNB) {
throw new Error(`${purpose} BNB余额不足: 需要 ${ethers.formatEther(requiredBNB)} BNB, ` +
`实际 ${ethers.formatEther(balance)} BNB`);
}
}
/**
* 格式化换手结果日志
*/
export function logSwapResult(params) {
}