UNPKG

four-flap-meme-sdk

Version:

SDK for Flap bonding curve and four.meme TokenManager

158 lines (157 loc) 6.21 kB
/** * 换手交易通用工具函数 */ 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) { }