UNPKG

@floyddd-vm/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/mev-relay-js).

602 lines 30.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FlashbotsBundleProvider = exports.FlashbotsBundleConflictType = 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"); 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 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.bloXrouteAuthKey = ""; this.connectionInfo = connectionInfoOrUrl; } setBloXrouteKey(bloXrouteAuthKey) { this.bloXrouteAuthKey = bloXrouteAuthKey; } static async throttleCallback() { console.warn('Rate limited'); return false; } 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); } 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; } 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); } } static generateBundleHash(txHashes) { const concatenatedHashes = txHashes.map((txHash) => txHash.slice(2)).join(''); return (0, utils_1.keccak256)(`0x${concatenatedHashes}`); } async sendRawBundle(signedBundledTransactions, targetBlockNumber, opts) { 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 }; const request = JSON.stringify(this.prepareBundleRequest('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.wait(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: "0x" //response.result.bundleHash }; } async sendRawBundle_blxr(signedBundledTransactions, targetBlockNumber, opts) { const params = { transaction: signedBundledTransactions, block_number: `0x${targetBlockNumber.toString(16)}`, min_timestamp: opts === null || opts === void 0 ? void 0 : opts.minTimestamp, max_timestamp: opts === null || opts === void 0 ? void 0 : opts.maxTimestamp, reverting_hashes: opts === null || opts === void 0 ? void 0 : opts.revertingTxHashes }; const request = JSON.stringify(this.prepareBundleRequest('blxr_submit_bundle', [params])); /*const response = await this.request_blxr(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.wait(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: "0x" //response.result.bundleHash }; } async sendBundle(bundledTransactions, targetBlockNumber, opts) { const signedTransactions = await this.signBundle(bundledTransactions); return this.sendRawBundle(signedTransactions, targetBlockNumber, opts); } async signBundle(bundledTransactions) { 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)); } return signedTransactions; } wait(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) => { console.log(`wait on_block ${blockNumber}`); const noncesValid = await Promise.all(Object.entries(minimumNonceByAccount).map(async ([account, nonce]) => { const transactionCount = await this.genericProvider.getTransactionCount(account, blockNumber); return nonce >= transactionCount; })); const allNoncesValid = noncesValid.every(Boolean); if (allNoncesValid) { if (blockNumber < targetBlockNumber) return; const block = await this.genericProvider.getBlock(targetBlockNumber); console.log(`node block number ${block.number}`); for (const bt of transactionAccountNonces) { let res = await this.genericProvider.getTransaction(bt.hash); if (res == null) { console.log(`tx not found in node`); continue; } console.log(`tx in block - ${res.blockNumber}`); } // 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); } else // target block not yet reached, but nonce has become invalid resolve(FlashbotsBundleResolution.AccountNonceTooHigh); if (timer) { clearTimeout(timer); } if (done) { return; } done = true; this.genericProvider.removeListener('block', handler); }; this.genericProvider.on('block', handler); if (typeof timeout === 'number' && 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(); } } }); } bundleWait(signedBundledTransactions, timeout) { const transactionAccountNonces = 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 new Promise((resolve, reject) => { let timer = null; let done = false; let needRemoveListener = 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) => { console.log(`wait on_block ${blockNumber}`); const noncesValid = await Promise.all(Object.entries(minimumNonceByAccount).map(async ([account, nonce]) => { const transactionCount = await this.genericProvider.getTransactionCount(account); //, blockNumber) return nonce >= transactionCount; })); const allNoncesValid = noncesValid.every(Boolean); if (allNoncesValid) { const block = await this.genericProvider.getBlock(blockNumber); console.log(`node block number ${block.number} - ${block.hash}`); // check bundle against block: const blockTransactionsHash = {}; for (const bt of block.transactions) { blockTransactionsHash[bt] = true; } const bundleIncluded = transactionAccountNonces.every((transaction) => blockTransactionsHash[transaction.hash]); if (bundleIncluded) { resolve([FlashbotsBundleResolution.BundleIncluded, blockNumber]); needRemoveListener = true; } else { console.log(); } } else { // target block not yet reached, but nonce has become invalid resolve([FlashbotsBundleResolution.AccountNonceTooHigh, blockNumber]); needRemoveListener = true; } if (timer) { clearTimeout(timer); } if (done) { return; } if (needRemoveListener) { this.genericProvider.removeListener('block', handler); done = true; } }; this.genericProvider.on('block', handler); if (typeof timeout === 'number' && 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(); } } }); } async getUserStats() { const blockDetails = await this.genericProvider.getBlock('latest'); const evmBlockNumber = `0x${blockDetails.number.toString(16)}`; const params = [evmBlockNumber]; const request = JSON.stringify(this.prepareBundleRequest('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; } async getBundleStats(bundleHash, blockNumber) { const evmBlockNumber = `0x${blockNumber.toString(16)}`; const params = [{ bundleHash, blockNumber: evmBlockNumber }]; const request = JSON.stringify(this.prepareBundleRequest('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; } async simulate(signedBundledTransactions, blockTag, stateBlockTag, blockTimestamp) { 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 } ]; const request = JSON.stringify(this.prepareBundleRequest('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 { bundleHash: callResult.bundleHash, coinbaseDiff: ethers_1.BigNumber.from(callResult.coinbaseDiff), results: callResult.results, 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) => { const gasUsed = 'gas_used' in transactionDetail ? transactionDetail.gas_used : transactionDetail.gasUsed; const gasPricePaidBySearcher = ethers_1.BigNumber.from('gas_price' in transactionDetail ? transactionDetail.gas_price : transactionDetail.gasPrice); const priorityFeeReceivedByMiner = gasPricePaidBySearcher.sub(baseFee); const ethSentToCoinbase = 'coinbase_transfer' in transactionDetail ? transactionDetail.coinbase_transfer : 'ethSentToCoinbase' in transactionDetail ? transactionDetail.ethSentToCoinbase : ethers_1.BigNumber.from(0); return { gasUsed: acc.gasUsed + gasUsed, gasFeesPaidBySearcher: acc.gasFeesPaidBySearcher.add(gasPricePaidBySearcher.mul(gasUsed)), priorityFeesReceivedByMiner: acc.priorityFeesReceivedByMiner.add(priorityFeeReceivedByMiner.mul(gasUsed)), 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 }; } 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 }; } 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) { 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: [] }; } 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 request_blxr(request) { const connectionInfo = { ...this.connectionInfo }; connectionInfo.url = "https://54.157.119.190"; // "https://mev.api.blxrbdn.com"; connectionInfo.headers = { "Content-Type": "application/json", "Authorization": `${this.bloXrouteAuthKey}`, ...this.connectionInfo.headers }; let bundle = JSON.parse(request); //qqq.params[0].transaction[0] = qqq.params[0].transaction[0].substring(2); //qqq.params[0].transaction[1] = qqq.params[0].transaction[1].substring(2); for (let i = 0; i < bundle.params[0].transaction.length; i++) { bundle.params[0].transaction[i] = bundle.params[0].transaction[i].substring(2); } let new_request = JSON.stringify(bundle); return (0, web_1.fetchJson)(connectionInfo, new_request); } async fetchReceipts(bundledTransactions) { return Promise.all(bundledTransactions.map((bundledTransaction) => this.genericProvider.getTransactionReceipt(bundledTransaction.hash))); } prepareBundleRequest(method, params) { return { method: method, params: params, id: this._nextId++, jsonrpc: '2.0' }; } } exports.FlashbotsBundleProvider = FlashbotsBundleProvider; //# sourceMappingURL=index.js.map