UNPKG

four-flap-meme-sdk

Version:

SDK for Flap bonding curve and four.meme TokenManager

428 lines (427 loc) 15.1 kB
/** * BlockRazor Bundle Service 客户端 * * 提供基于 BlockRazor 的 MEV 保护和捆绑交易服务 * * 官方文档: https://blockrazor.gitbook.io/blockrazor/bsc/block-builder/send-bundle * * 特点: * - 支持 BSC 链的 Bundle 提交 * - 激励机制:向 Builder EOA 转账 BNB 可提高优先级 * - 支持 Bundle 合并提高打包率 * * 使用示例: * ```typescript * import { BlockRazorClient } from 'four-flap-meme-sdk'; * * const client = new BlockRazorClient({ * apiKey: 'your-api-key', * chainId: 56 * }); * * const result = await client.sendBundle({ * transactions: [tx1, tx2, tx3], * blockOffset: 10 * }); * ``` */ import { JsonRpcProvider, Transaction, ethers } from 'ethers'; // ============================================================================ // 常量 // ============================================================================ /** * BlockRazor Builder EOA 地址 * 向此地址转账 BNB 可提高 Bundle 优先级 * 来源: https://blockrazor.gitbook.io/blockrazor/bsc/block-builder/send-bundle */ export const BLOCKRAZOR_BUILDER_EOA = '0x1266C6bE60392A8Ff346E8d5ECCd3E69dD9c5F20'; /** * BlockRazor BSC RPC 端点 * 来源: 测试文件 008_blockrazor_smoke_test.ts */ const BLOCKRAZOR_RPC_ENDPOINTS = { BSC: 'https://rpc.blockrazor.builders', // 可以添加其他链的端点 }; /** * 最低 Gas Price 要求 (0.05 Gwei) */ const MIN_GAS_PRICE_GWEI = 0.05; // ============================================================================ // BlockRazor 客户端 // ============================================================================ /** * BlockRazor 客户端 */ export class BlockRazorClient { constructor(config) { // 区块号缓存 this.blockNumberCache = null; this.chainId = config.chainId; this.apiKey = config.apiKey; // 普通 RPC(用于查询) const rpcUrl = config.customRpcUrl || 'https://bsc-dataseed.binance.org'; this.provider = new JsonRpcProvider(rpcUrl, { chainId: this.chainId, name: 'bsc', }); // Builder RPC URL(用于发送 Bundle) // ✅ 正确的端点: https://rpc.blockrazor.builders this.builderUrl = config.builderRpcUrl || BLOCKRAZOR_RPC_ENDPOINTS.BSC; } /** * 获取 Provider */ getProvider() { return this.provider; } /** * 获取 Builder URL */ getBuilderUrl() { return this.builderUrl; } /** * 获取当前区块号(带缓存) */ async getBlockNumber(forceRefresh = false) { const now = Date.now(); if (!forceRefresh && this.blockNumberCache) { const age = now - this.blockNumberCache.timestamp; if (age < BlockRazorClient.BLOCK_CACHE_TTL_MS) { return this.blockNumberCache.value; } } const blockNumber = await this.provider.getBlockNumber(); this.blockNumberCache = { value: blockNumber, timestamp: now }; return blockNumber; } /** * 获取 Gas 价格信息 */ async getFeeData() { return await this.provider.getFeeData(); } /** * 构建激励交易(向 BlockRazor Builder EOA 转账 BNB) * * 根据文档:向 Builder EOA 转账更多 BNB 可提高 Bundle 优先级 * * @param params 激励交易参数 * @returns 已签名的交易 */ async buildIncentiveTransaction(params) { const { wallet, amount, nonce, gasPrice } = params; // 获取 nonce const txNonce = nonce ?? await this.provider.getTransactionCount(wallet.address, 'latest'); // 获取 gas price let txGasPrice = gasPrice; if (!txGasPrice) { const feeData = await this.getFeeData(); txGasPrice = feeData.gasPrice || ethers.parseUnits(String(MIN_GAS_PRICE_GWEI), 'gwei'); } // 构建交易 const tx = { to: BLOCKRAZOR_BUILDER_EOA, value: ethers.parseEther(amount), nonce: txNonce, gasLimit: 21000n, gasPrice: txGasPrice, chainId: this.chainId, type: 0, }; return await wallet.signTransaction(tx); } /** * 发送捆绑交易(底层方法) * * ✅ 使用 fetch 直接发送请求,支持 Authorization 头 * * @param params Bundle 参数 * @returns Bundle Hash */ async sendBundleRaw(params) { if (!params.txs || params.txs.length === 0) { throw new Error('Bundle transactions cannot be empty'); } // 验证 Gas Price(最低 0.05 Gwei) for (const rawTx of params.txs) { try { const tx = Transaction.from(rawTx); const gasPrice = tx.gasPrice || 0n; const minGasPrice = ethers.parseUnits(String(MIN_GAS_PRICE_GWEI), 'gwei'); if (gasPrice < minGasPrice) { } } catch { // 解析失败,跳过检查 } } // 构建请求参数 const bundleParams = { txs: params.txs, }; if (params.maxBlockNumber !== undefined) { bundleParams.maxBlockNumber = params.maxBlockNumber; } if (params.minTimestamp !== undefined) { bundleParams.minTimestamp = params.minTimestamp; } if (params.maxTimestamp !== undefined) { bundleParams.maxTimestamp = params.maxTimestamp; } if (params.revertingTxHashes && params.revertingTxHashes.length > 0) { bundleParams.revertingTxHashes = params.revertingTxHashes; } if (params.noMerge !== undefined) { bundleParams.noMerge = params.noMerge; } try { // ✅ 使用 fetch 发送请求,支持 Authorization 头 const requestBody = { jsonrpc: '2.0', id: '1', method: 'eth_sendBundle', params: [bundleParams] }; const headers = { 'Content-Type': 'application/json', }; // 如果有 API Key,添加 Authorization 头 if (this.apiKey) { headers['Authorization'] = this.apiKey; } const response = await fetch(this.builderUrl, { method: 'POST', headers, body: JSON.stringify(requestBody), }); if (!response.ok) { const text = await response.text(); throw new Error(`HTTP ${response.status}: ${text}`); } const result = await response.json(); // 检查 JSON-RPC 错误 if (result.error) { throw new Error(`RPC Error: ${result.error.message || JSON.stringify(result.error)}`); } // 提取 bundle hash const bundleHash = result.result; if (typeof bundleHash === 'string') { return bundleHash; } if (bundleHash && typeof bundleHash === 'object') { if ('bundleHash' in bundleHash) { return String(bundleHash.bundleHash); } } return JSON.stringify(result.result || result); } catch (error) { const errorMsg = error.message || String(error); console.error(`❌ [BlockRazor] Bundle 发送失败: ${errorMsg}`); throw new Error(`BlockRazor send bundle failed: ${errorMsg}`); } } /** * 发送捆绑交易(高级方法) * * @param options 发送选项 * @returns Bundle 结果 */ async sendBundle(options) { if (!options.transactions || options.transactions.length === 0) { throw new Error('Transactions array cannot be empty'); } const blockOffset = options.blockOffset ?? 100; const maxBlockOffset = options.maxBlockOffset ?? 100; const autoRetry = options.autoRetry ?? false; const maxRetries = options.maxRetries ?? 3; let attempt = 0; let lastError = null; while (attempt <= (autoRetry ? maxRetries : 0)) { try { // 获取当前区块 const currentBlock = await this.getBlockNumber(true); // 计算最大有效区块号 const actualOffset = Math.min(blockOffset, maxBlockOffset); const maxBlockNumber = currentBlock + actualOffset; // 发送 Bundle const bundleHash = await this.sendBundleRaw({ txs: options.transactions, maxBlockNumber, minTimestamp: options.minTimestamp, maxTimestamp: options.maxTimestamp, revertingTxHashes: options.revertingTxHashes, noMerge: options.noMerge, }); // 提取交易哈希 const txHashes = options.transactions.map(rawTx => { try { return Transaction.from(rawTx).hash || ''; } catch { return ''; } }); return { bundleHash, txHashes, maxBlockNumber, txCount: options.transactions.length, }; } catch (error) { lastError = error; if (autoRetry && attempt < maxRetries) { attempt++; await new Promise(resolve => setTimeout(resolve, 3000)); // 等待一个区块 continue; } throw error; } } throw lastError || new Error('Bundle submission failed after all retries'); } /** * 发送带激励的 Bundle(自动添加激励交易) * * @param options 发送选项 * @param incentive 激励参数 * @returns Bundle 结果 */ async sendBundleWithIncentive(options, incentive) { // 获取激励钱包的 nonce const nonce = await this.provider.getTransactionCount(incentive.wallet.address, 'latest'); // 构建激励交易(放在 Bundle 最后) const incentiveTx = await this.buildIncentiveTransaction({ wallet: incentive.wallet, amount: incentive.amount, nonce: nonce, }); // 将激励交易添加到 Bundle 末尾 const allTransactions = [...options.transactions, incentiveTx]; return await this.sendBundle({ ...options, transactions: allTransactions, }); } /** * 等待 Bundle 中的交易确认 * * @param txHashes 交易哈希列表 * @param confirmations 确认数(默认 1) * @param timeout 超时时间(毫秒,默认 120000) * @returns 交易结果列表 */ async waitForBundleConfirmation(txHashes, confirmations = 1, timeout = 120000) { const promises = txHashes.map(async (hash, index) => { try { const receipt = await this.provider.waitForTransaction(hash, confirmations, timeout); if (!receipt) { return { index, hash, success: false, error: 'Transaction not found', }; } let gasCost; if (receipt.gasUsed && receipt.gasPrice) { const costWei = receipt.gasUsed * receipt.gasPrice; const costBnb = Number(costWei) / 1e18; gasCost = costBnb.toFixed(8); } return { index, hash, success: receipt.status === 1, blockNumber: receipt.blockNumber, gasUsed: receipt.gasUsed.toString(), effectiveGasPrice: receipt.gasPrice?.toString(), gasCost, }; } catch (error) { return { index, hash, success: false, error: error.message, }; } }); return await Promise.all(promises); } /** * 签名交易批次 * * @param wallet 钱包 * @param transactions 交易列表 * @param options 可选参数 * @returns 已签名的交易数组 */ async signTransactions(wallet, transactions, options) { if (!transactions || transactions.length === 0) { throw new Error('Transactions array cannot be empty'); } // 获取 nonce let nonce = options?.startNonce; if (nonce === undefined) { nonce = await this.provider.getTransactionCount(wallet.address, 'latest'); } // 获取 Gas Price(确保不低于最低要求) let gasPrice = options?.gasPrice; if (!gasPrice) { const feeData = await this.provider.getFeeData(); const baseGasPrice = feeData.gasPrice || ethers.parseUnits(String(MIN_GAS_PRICE_GWEI), 'gwei'); const multiplier = options?.gasPriceMultiplier || 50; gasPrice = (baseGasPrice * BigInt(100 + multiplier)) / 100n; // 确保不低于最低要求 const minGasPrice = ethers.parseUnits(String(MIN_GAS_PRICE_GWEI), 'gwei'); if (gasPrice < minGasPrice) { gasPrice = minGasPrice; } } // 并行签名所有交易 const signedTxs = await Promise.all(transactions.map(async (tx, i) => { const fullTx = { ...tx, nonce: nonce + i, gasPrice, chainId: this.chainId, }; return await wallet.signTransaction(fullTx); })); return signedTxs; } /** * 检查交易是否已包含在区块中 */ async isTransactionIncluded(txHash) { try { const receipt = await this.provider.getTransactionReceipt(txHash); return receipt !== null && receipt.blockNumber !== null; } catch { return false; } } /** * 获取客户端信息 */ getClientInfo() { return { chainId: this.chainId, builderEOA: BLOCKRAZOR_BUILDER_EOA, minGasPriceGwei: MIN_GAS_PRICE_GWEI, }; } } BlockRazorClient.BLOCK_CACHE_TTL_MS = 1000; // 1秒缓存 /** * 创建 BlockRazor 客户端的便捷函数 */ export function createBlockRazorClient(apiKey) { return new BlockRazorClient({ apiKey, chainId: 56 }); }