four-flap-meme-sdk
Version:
SDK for Flap bonding curve and four.meme TokenManager
428 lines (427 loc) • 15.1 kB
JavaScript
/**
* 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 });
}