four-flap-meme-sdk
Version:
SDK for Flap bonding curve and four.meme TokenManager
527 lines (526 loc) • 18 kB
JavaScript
/**
* 独立的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
};
}