UNPKG

four-flap-meme-sdk

Version:

SDK for Flap bonding curve and four.meme TokenManager

527 lines (526 loc) 18 kB
/** * 独立的Bundle提交方法 * * 用于服务器端接收前端构建的签名交易后,提交到Merkle或BlockRazor * 以及 Monad 等不支持 Bundle 的链的逐笔广播 */ import { MerkleClient } from '../../clients/merkle.js'; import { BlockRazorClient } from '../../clients/blockrazor.js'; import { ethers } from 'ethers'; // ✅ MerkleClient 缓存(复用连接,减少初始化开销) const merkleClientCache = new Map(); const CLIENT_CACHE_TTL_MS = 60 * 1000; // 60秒缓存 /** * 获取或创建 MerkleClient(带缓存) */ function getMerkleClient(config) { const cacheKey = `${config.apiKey}-${config.chainId ?? 56}-${config.customRpcUrl || ''}`; const now = Date.now(); const cached = merkleClientCache.get(cacheKey); if (cached && cached.expireAt > now) { return cached.client; } // 创建新客户端并缓存 const client = new MerkleClient({ apiKey: config.apiKey, chainId: config.chainId ?? 56, customRpcUrl: config.customRpcUrl }); merkleClientCache.set(cacheKey, { client, expireAt: now + CLIENT_CACHE_TTL_MS }); return client; } /** * 提交已签名的交易到Merkle(服务器端使用) * * 这个方法接收前端构建和签名好的交易,直接提交到Merkle服务 * * @param signedTransactions 签名后的交易数组 * @param config Merkle提交配置(精简版,只需要提交相关参数) * @returns Bundle提交结果 * * @example * ```typescript * // 服务器端代码 * import { submitBundleToMerkle } from 'four-flap-meme-sdk'; * * const result = await submitBundleToMerkle(signedTransactions, { * apiKey: process.env.MERKLE_API_KEY, * customRpcUrl: process.env.MERKLE_RPC_URL, * bundleBlockOffset: 5 * }); * * if (result.code) { * } else { * console.error('❌ Bundle提交失败:', result.error); * } * ``` */ export async function submitBundleToMerkle(signedTransactions, config) { try { const totalTransactions = signedTransactions?.length ?? 0; // 验证输入 if (!signedTransactions || signedTransactions.length === 0) { return { code: false, totalTransactions, error: 'signedTransactions cannot be empty' }; } if (!config.apiKey) { return { code: false, totalTransactions, error: 'apiKey is required in config' }; } // ✅ 使用缓存的 MerkleClient(复用连接 + 区块号缓存) const merkle = getMerkleClient(config); // 提交Bundle const bundleResult = await merkle.sendBundle({ transactions: signedTransactions, blockOffset: config.bundleBlockOffset ?? 3, minBlockOffset: config.minBlockOffset ?? 3, autoRetry: config.autoRetryBundle ?? false, maxRetries: config.maxBundleRetries ?? 2 }); // ✅ 提交成功 return { code: true, totalTransactions, bundleHash: bundleResult.bundleHash, txHashes: bundleResult.txHashes, targetBlock: bundleResult.targetBlock, txCount: bundleResult.txCount }; } catch (error) { // ❌ 提交失败 return { code: false, totalTransactions: signedTransactions?.length ?? 0, error: error?.message || String(error) }; } } /** * 批量提交多个Bundle到Merkle(顺序执行) * * @param bundles 多个Bundle的签名交易数组 * @param config Merkle提交配置 * @returns Bundle提交结果数组 */ export async function submitMultipleBundles(bundles, config) { const results = []; for (const signedTransactions of bundles) { const result = await submitBundleToMerkle(signedTransactions, config); results.push(result); } return results; } /** * 并行提交多个Bundle到Merkle(适用于独立的Bundle) * * @param bundles 多个Bundle的签名交易数组 * @param config Merkle提交配置 * @returns Bundle提交结果数组 */ export async function submitMultipleBundlesParallel(bundles, config) { const promises = bundles.map(signedTransactions => submitBundleToMerkle(signedTransactions, config)); return await Promise.all(promises); } // ==================== BlockRazor Bundle 提交方法 ==================== // ✅ BlockRazorClient 缓存(复用连接,减少初始化开销) const blockRazorClientCache = new Map(); /** * 获取或创建 BlockRazorClient(带缓存) */ function getBlockRazorClient(config) { const cacheKey = `${config.apiKey || 'default'}-${config.customRpcUrl || ''}-${config.builderRpcUrl || ''}`; const now = Date.now(); const cached = blockRazorClientCache.get(cacheKey); if (cached && cached.expireAt > now) { return cached.client; } // 创建新客户端并缓存 const client = new BlockRazorClient({ apiKey: config.apiKey, chainId: 56, // BlockRazor 目前只支持 BSC customRpcUrl: config.customRpcUrl, builderRpcUrl: config.builderRpcUrl }); blockRazorClientCache.set(cacheKey, { client, expireAt: now + CLIENT_CACHE_TTL_MS }); return client; } /** * 提交已签名的交易到 BlockRazor(服务器端使用) * * BlockRazor 是 BSC 链的 Block Builder 服务,支持 Bundle 提交 * * 特点: * - 向 Builder EOA 转账 BNB 可提高优先级 * - 支持 Bundle 合并提高打包率 * - 最低 Gas Price 要求: 0.05 Gwei * * @param signedTransactions 签名后的交易数组 * @param config BlockRazor 提交配置 * @returns Bundle 提交结果 * * @example * ```typescript * // 服务器端代码 * import { submitBundleToBlockRazor } from 'four-flap-meme-sdk'; * * const result = await submitBundleToBlockRazor(signedTransactions, { * blockOffset: 10, * noMerge: false, // 允许合并 * autoRetry: true * }); * * if (result.code) { * } else { * console.error('❌ Bundle 提交失败:', result.error); * } * ``` */ export async function submitBundleToBlockRazor(signedTransactions, config) { try { const totalTransactions = signedTransactions?.length ?? 0; // 验证输入 if (!signedTransactions || signedTransactions.length === 0) { return { code: false, totalTransactions, error: 'signedTransactions cannot be empty' }; } // ✅ 使用缓存的 BlockRazorClient const client = getBlockRazorClient(config); // 提交 Bundle const bundleResult = await client.sendBundle({ transactions: signedTransactions, blockOffset: config.blockOffset ?? 100, maxBlockOffset: config.maxBlockOffset ?? 100, noMerge: config.noMerge ?? false, revertingTxHashes: config.revertingTxHashes, autoRetry: config.autoRetry ?? false, maxRetries: config.maxRetries ?? 3 }); // ✅ 提交成功 return { code: true, totalTransactions, bundleHash: bundleResult.bundleHash, txHashes: bundleResult.txHashes, maxBlockNumber: bundleResult.maxBlockNumber, txCount: bundleResult.txCount }; } catch (error) { // ❌ 提交失败 return { code: false, totalTransactions: signedTransactions?.length ?? 0, error: error?.message || String(error) }; } } /** * 批量提交多个 Bundle 到 BlockRazor(顺序执行) * * @param bundles 多个 Bundle 的签名交易数组 * @param config BlockRazor 提交配置 * @returns Bundle 提交结果数组 */ export async function submitMultipleBundlesToBlockRazor(bundles, config) { const results = []; for (const signedTransactions of bundles) { const result = await submitBundleToBlockRazor(signedTransactions, config); results.push(result); } return results; } /** * 并行提交多个 Bundle 到 BlockRazor(适用于独立的 Bundle) * * @param bundles 多个 Bundle 的签名交易数组 * @param config BlockRazor 提交配置 * @returns Bundle 提交结果数组 */ export async function submitMultipleBundlesToBlockRazorParallel(bundles, config) { const promises = bundles.map(signedTransactions => submitBundleToBlockRazor(signedTransactions, config)); return await Promise.all(promises); } /** * 并行广播到 RPC(用于不支持 Bundle 的链,如 Monad) * * ✅ 优化:默认使用并行广播,速度更快 * * @param signedTransactions 签名后的交易数组 * @param config 直接广播配置 * @returns 广播结果 * * @example * ```typescript * // 服务器端代码(Monad 链) * import { submitDirectToRpc } from 'four-flap-meme-sdk'; * * const result = await submitDirectToRpc(signedTransactions, { * rpcUrl: 'https://rpc-mainnet.monadinfra.com', * chainId: 143, * chainName: 'MONAD' * }); * * if (result.code) { * } else { * console.error('❌ 全部广播失败:', result.errorSummary); * } * ``` */ export async function submitDirectToRpc(signedTransactions, config) { const totalTransactions = signedTransactions?.length ?? 0; // 验证输入 if (!signedTransactions || signedTransactions.length === 0) { return { code: false, totalTransactions: 0, successCount: 0, failedCount: 0, txHashes: [], results: [], errorSummary: 'signedTransactions cannot be empty' }; } if (!config.rpcUrl) { return { code: false, totalTransactions, successCount: 0, failedCount: totalTransactions, txHashes: [], results: [], errorSummary: 'rpcUrl is required in config' }; } const chainId = config.chainId ?? 143; const chainName = config.chainName ?? 'MONAD'; // 创建 Provider const provider = new ethers.JsonRpcProvider(config.rpcUrl, { chainId, name: chainName }); // ✅ 并行广播所有交易 const broadcastPromises = signedTransactions.map(async (signedTx, i) => { try { const txResponse = await provider.broadcastTransaction(signedTx); // 如果需要等待确认 if (config.waitForConfirmation) { try { const receipt = await provider.waitForTransaction(txResponse.hash, 1, config.confirmationTimeout ?? 30000); if (receipt && receipt.status === 0) { console.warn(`⚠️ [${chainName}] 交易 ${txResponse.hash} 执行失败(已上链但状态为0)`); } } catch (waitError) { console.warn(`⚠️ [${chainName}] 等待交易确认超时: ${txResponse.hash}`); } } return { index: i, success: true, txHash: txResponse.hash }; } catch (error) { const errorMessage = error?.message || String(error); console.error(`❌ [${chainName}] 交易 ${i + 1}/${totalTransactions} 广播失败:`, errorMessage); return { index: i, success: false, error: errorMessage }; } }); const results = await Promise.all(broadcastPromises); // 按索引排序结果 results.sort((a, b) => a.index - b.index); const txHashes = results.filter(r => r.success && r.txHash).map(r => r.txHash); const errors = results.filter(r => !r.success).map(r => `交易 ${r.index + 1}: ${r.error}`); const successCount = txHashes.length; const failedCount = totalTransactions - successCount; return { code: successCount > 0, totalTransactions, successCount, failedCount, txHashes, results, errorSummary: errors.length > 0 ? errors.join('; ') : undefined }; } /** * ✅ 顺序广播并等待确认(用于多跳交易等需要顺序执行的场景) * * 每笔交易广播后会等待上链确认,确保后续交易能正确执行 * * @param signedTransactions 签名后的交易数组 * @param config 直接广播配置 * @returns 广播结果 */ export async function submitDirectToRpcSequential(signedTransactions, config) { const totalTransactions = signedTransactions?.length ?? 0; if (!signedTransactions || signedTransactions.length === 0) { return { code: false, totalTransactions: 0, successCount: 0, failedCount: 0, txHashes: [], results: [], errorSummary: 'signedTransactions cannot be empty' }; } if (!config.rpcUrl) { return { code: false, totalTransactions, successCount: 0, failedCount: totalTransactions, txHashes: [], results: [], errorSummary: 'rpcUrl is required in config' }; } const chainId = config.chainId ?? 143; const chainName = config.chainName ?? 'MONAD'; const confirmationTimeout = config.confirmationTimeout ?? 60000; // 默认60秒超时 const provider = new ethers.JsonRpcProvider(config.rpcUrl, { chainId, name: chainName }); const results = []; const txHashes = []; const errors = []; // 顺序广播并等待确认 for (let i = 0; i < signedTransactions.length; i++) { const signedTx = signedTransactions[i]; try { // 广播交易 const txResponse = await provider.broadcastTransaction(signedTx); // ✅ 等待交易确认 const receipt = await provider.waitForTransaction(txResponse.hash, 1, // 等待1个确认 confirmationTimeout); if (receipt && receipt.status === 1) { results.push({ index: i, success: true, txHash: txResponse.hash }); txHashes.push(txResponse.hash); } else { const errorMsg = `交易执行失败(status=${receipt?.status})`; results.push({ index: i, success: false, txHash: txResponse.hash, error: errorMsg }); errors.push(`交易 ${i + 1}: ${errorMsg}`); // ✅ 多跳场景:如果某笔失败,后续交易可能也会失败,继续尝试但标记 } } catch (error) { const errorMessage = error?.message || String(error); results.push({ index: i, success: false, error: errorMessage }); errors.push(`交易 ${i + 1}: ${errorMessage}`); // ✅ 多跳场景:如果某笔失败,后续交易可能也会失败,继续尝试 } } const successCount = txHashes.length; const failedCount = totalTransactions - successCount; return { code: successCount > 0, totalTransactions, successCount, failedCount, txHashes, results, errorSummary: errors.length > 0 ? errors.join('; ') : undefined }; } /** * 并行逐笔广播到 RPC(速度更快,但可能有 nonce 冲突) * * ⚠️ 注意:仅适用于不同钱包的交易,同一钱包的多笔交易应使用顺序广播 * * @param signedTransactions 签名后的交易数组 * @param config 直接广播配置 * @returns 广播结果 */ export async function submitDirectToRpcParallel(signedTransactions, config) { const totalTransactions = signedTransactions?.length ?? 0; if (!signedTransactions || signedTransactions.length === 0) { return { code: false, totalTransactions: 0, successCount: 0, failedCount: 0, txHashes: [], results: [], errorSummary: 'signedTransactions cannot be empty' }; } if (!config.rpcUrl) { return { code: false, totalTransactions, successCount: 0, failedCount: totalTransactions, txHashes: [], results: [], errorSummary: 'rpcUrl is required in config' }; } const chainId = config.chainId ?? 143; const chainName = config.chainName ?? 'MONAD'; const provider = new ethers.JsonRpcProvider(config.rpcUrl, { chainId, name: chainName }); // 并行广播 const broadcastPromises = signedTransactions.map(async (signedTx, i) => { try { const txResponse = await provider.broadcastTransaction(signedTx); return { index: i, success: true, txHash: txResponse.hash }; } catch (error) { return { index: i, success: false, error: error?.message || String(error) }; } }); const results = await Promise.all(broadcastPromises); const txHashes = results.filter(r => r.success && r.txHash).map(r => r.txHash); const errors = results.filter(r => !r.success).map(r => `交易 ${r.index + 1}: ${r.error}`); const successCount = txHashes.length; const failedCount = totalTransactions - successCount; return { code: successCount > 0, totalTransactions, successCount, failedCount, txHashes, results, errorSummary: errors.length > 0 ? errors.join('; ') : undefined }; }