├── .eslintrc ├── .github └── workflows │ ├── lint.yml │ └── nodejs.yml ├── .gitignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── src ├── demo-research.ts ├── demo.ts └── index.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["prettier", "eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": 12, 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["prettier", "@typescript-eslint"], 13 | "rules": { 14 | "prettier/prettier": ["error"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: 'lint' 2 | on: [pull_request] 3 | 4 | jobs: 5 | eslint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | 10 | - name: Set up Node.js 11 | uses: actions/setup-node@v3 12 | with: 13 | node-version: 16 14 | 15 | - name: Install Node.js dependencies 16 | run: npm ci 17 | 18 | - name: Run lint 19 | run: npm run lint 20 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # GitHub Nodejs CI 2 | name: nodejs 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: ['10.x', '12.x', '14.x'] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm install codecov -g 23 | npm install 24 | npm run build 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | /build 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "semi": false, 4 | "trailingComma": "none", 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ethers-provider-flashbots-bundle 2 | 3 | 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). **`mev-relay` is a hosted service; it is not necessary to run `mev-relay` or `mev-geth` to proceed with this example.** 4 | 5 | Flashbots-enabled relays and miners expose two new jsonrpc endpoints: `eth_sendBundle` and `eth_callBundle`. Since these are non-standard endpoints, ethers.js and other libraries do not natively support these requests (like `getTransactionCount`). In order to interact with these endpoints, you will need access to another full-featured (non-Flashbots) endpoint for nonce-calculation, gas estimation, and transaction status. 6 | 7 | One key feature this library provides is **payload signing**, a requirement to submit Flashbot bundles to the `mev-relay` service. This library takes care of the signing process via the `authSigner` passed into the constructor. [Read more about relay signatures here](https://github.com/flashbots/mev-relay-js#authentication) 8 | 9 | This library is not a fully functional ethers.js implementation, just a simple provider class, designed to interact with an existing [ethers.js v5 installation](https://github.com/ethers-io/ethers.js/). 10 | 11 | ## Example 12 | 13 | Install ethers.js and the Flashbots ethers bundle provider. 14 | 15 | ```bash 16 | npm install --save ethers 17 | npm install --save @flashbots-sdk/ethers-provider-bundle 18 | ``` 19 | 20 | Open up a new TypeScript file (this also works with JavaScript if you prefer) 21 | 22 | ```ts 23 | import { providers, Wallet } from "ethers"; 24 | import { FlashbotsBundleProvider } from "@flashbots-sdk/ethers-provider-bundle"; 25 | 26 | // Standard json rpc provider directly from ethers.js (NOT Flashbots) 27 | const provider = new providers.JsonRpcProvider({ url: ETHEREUM_RPC_URL }, 1) 28 | 29 | // `authSigner` is an Ethereum private key that does NOT store funds and is NOT your bot's primary key. 30 | // This is an identifying key for signing payloads to establish reputation and whitelisting 31 | // In production, this should be used across multiple bundles to build relationship. In this example, we generate a new wallet each time 32 | const authSigner = Wallet.createRandom(); 33 | 34 | // Flashbots provider requires passing in a standard provider 35 | const flashbotsProvider = await FlashbotsBundleProvider.create( 36 | provider, // a normal ethers.js provider, to perform gas estimiations and nonce lookups 37 | authSigner // ethers.js signer wallet, only for signing request payloads, not transactions 38 | ) 39 | ``` 40 | 41 | From here, you have a `flashbotsProvider` object setup which can now perform either an `eth_callBundle` (via `simulate()`) or `eth_sendBundle` (via `sendBundle`). Each of these functions act on an array of `Bundle Transactions` 42 | 43 | ## Bundle Transactions 44 | 45 | Both `simulate` and `sendBundle` operate on a bundle of strictly-ordered transactions. While the miner requires signed transactions, the provider library will accept a mix of pre-signed transaction and `TransactionRequest + Signer` transactions (which it will estimate, nonce-calculate, and sign before sending to the `mev-relay`) 46 | 47 | ```ts 48 | const wallet = new Wallet(PRIVATE_KEY) 49 | const transaction = { 50 | to: CONTRACT_ADDRESS, 51 | data: CALL_DATA 52 | } 53 | const transactionBundle = [ 54 | { 55 | signedTransaction: SIGNED_ORACLE_UPDATE_FROM_PENDING_POOL // serialized signed transaction hex 56 | }, 57 | { 58 | signer: wallet, // ethers signer 59 | transaction: transaction // ethers populated transaction object 60 | } 61 | ] 62 | ``` 63 | 64 | ## Block Targeting 65 | 66 | The last thing required for `sendBundle()` is block targeting. Every bundle specifically references a single block. If your bundle is valid for multiple blocks (including all blocks until it is mined), `sendBundle()` must be called for every block, ideally on one of the blocks immediately prior. This gives you a chance to re-evaluate the opportunity you are capturing and re-sign your transactions with a higher nonce, if necessary. 67 | 68 | The block should always be a _future_ block, never the current one. 69 | 70 | ```ts 71 | const targetBlockNumber = (await provider.getBlockNumber()) + 1 72 | ``` 73 | 74 | ## Gas Prices and EIP-1559 75 | 76 | Before EIP-1559 was activated, the most common way for searchers to submit transactions is with `gasPrice=0`, with an on-chain payment to `block.coinbase` conditional on the transaction's success. All transactions must pay `baseFee` now, an attribute of a block. For an example of how to ensure you are using this `baseFee`, see `demo.ts` in this repository. 77 | 78 | ```js 79 | const block = await provider.getBlock(blockNumber) 80 | const maxBaseFeeInFutureBlock = FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock(block.baseFeePerGas, BLOCKS_IN_THE_FUTURE) 81 | const eip1559Transaction = { 82 | to: wallet.address, 83 | type: 2, 84 | maxFeePerGas: PRIORITY_FEE.add(maxBaseFeeInFutureBlock), 85 | maxPriorityFeePerGas: PRIORITY_FEE, 86 | gasLimit: 21000, 87 | data: '0x', 88 | chainId: CHAIN_ID 89 | } 90 | ``` 91 | 92 | `FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock` calculates the maximum baseFee that is possible `BLOCKS_IN_THE_FUTURE` blocks, given maximum expansion on each block. You won't pay this fee, so long as it is specified as `maxFeePerGas`, you will only pay the block's `baseFee`. 93 | 94 | Additionally if you have the `baseFeePerGas`, `gasUsed`, and `gasLimit` from the current block and are only targeting one block in the future you can get the exact base fee for the next block using `FlashbotsBundleProvider.getBaseFeeInNextBlock`. This method implements the math based on the [EIP1559 definition](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) 95 | 96 | ## Simulate and Send 97 | 98 | Now that we have: 99 | 100 | 1. Flashbots Provider `flashbotsProvider` 101 | 2. Bundle of transactions `transactionBundle` 102 | 3. Block Number `targetBlockNumber` 103 | 104 | We can run simulations and submit directly to miners, via the `mev-relay`. 105 | 106 | Simulate: 107 | 108 | ```ts 109 | const signedTransactions = await flashbotsProvider.signBundle(transactionBundle) 110 | const simulation = await flashbotsProvider.simulate(signedTransactions, targetBlockNumber) 111 | console.log(JSON.stringify(simulation, null, 2)) 112 | ``` 113 | 114 | Send: 115 | 116 | ```ts 117 | const flashbotsTransactionResponse = await flashbotsProvider.sendBundle( 118 | transactionBundle, 119 | targetBlockNumber, 120 | ) 121 | ``` 122 | 123 | ## FlashbotsTransactionResponse 124 | 125 | After calling `sendBundle`, this provider will return a Promise of an object with helper functions related to the bundle you submitted. 126 | 127 | These functions return metadata available at transaction submission time, as well as the following functions which can wait, track, and simulate the bundle's behavior. 128 | 129 | - `bundleTransactions()` - An array of transaction descriptions sent to the relay, including hash, nonce, and the raw transaction. 130 | - `receipts()` - Returns promise of an array of transaction receipts corresponding to the transaction hashes that were relayed as part of the bundle. Will not wait for block to be mined; could return incomplete information 131 | - `wait()` - Returns a promise which will wait for target block number to be reached _OR_ one of the transactions to become invalid due to nonce-issues (including, but not limited to, one of the transactions from your bundle being included too early). Returns the wait resolution as a status enum 132 | - `simulate()` - Returns a promise of the transaction simulation, once the proper block height has been reached. Use this function to troubleshoot failing bundles and verify miner profitability 133 | 134 | ## Optional eth_sendBundle arguments 135 | 136 | Beyond target block number, an object can be passed in with optional attributes: 137 | 138 | ```ts 139 | { 140 | minTimestamp, // optional minimum timestamp at which this bundle is valid (inclusive) 141 | maxTimestamp, // optional maximum timestamp at which this bundle is valid (inclusive) 142 | revertingTxHashes: [tx1, tx2] // optional list of transaction hashes allowed to revert. Without specifying here, any revert invalidates the entire bundle. 143 | } 144 | ``` 145 | 146 | ### minTimestamp / maxTimestamp 147 | 148 | While each bundle targets only a single block, you can add a filter for validity based on the block's timestamp. This does _not_ allow for targeting any block number based on a timestamp or instruct miners on what timestamp to use, it merely serves as a secondary filter. 149 | 150 | If your bundle is not valid before a certain time or includes an expiring opportunity, setting these values allows the miner to skip bundle processing earlier in the phase. 151 | 152 | Additionally, you could target several blocks in the future, but with a strict maxTimestamp, to ensure your bundle is considered for inclusion up to a specific time, regardless of how quickly blocks are mined in that timeframe. 153 | 154 | ### Reverting Transaction Hashes 155 | 156 | Transaction bundles will not be considered for inclusion if they include _any_ transactions that revert or fail. While this is normally desirable, there are some advanced use-cases where a searcher might WANT to bring a failing transaction to the chain. This is normally desirable for nonce management. Consider: 157 | 158 | Transaction Nonce #1 = Failed (unrelated) token transfer 159 | Transaction Nonce #2 = DEX trade 160 | 161 | If a searcher wants to bring #2 to the chain, #1 must be included first, and its failure is not related to the desired transaction #2. This is especially common during high gas times. 162 | 163 | Optional parameter `revertingTxHashes` allows a searcher to specify an array of transactions that can (but are not required to) revert. 164 | 165 | ## Paying for your bundle 166 | 167 | In addition to paying for a bundle with gas price, bundles can also conditionally pay a miner via: 168 | `block.coinbase.transfer(_minerReward)` 169 | or 170 | `block.coinbase.call{value: _minerReward}("");` 171 | 172 | (assuming _minerReward is a solidity `uint256` with the wei-value to be transferred directly to the miner) 173 | 174 | The entire value of the bundle is added up at the end, so not every transaction needs to have a gas price or `block.coinbase` payment, so long as at least one does, and pays enough to support the gas used in non-paying transactions. 175 | 176 | Note: Gas-fees will ONLY benefit your bundle if the transaction is not already present in the mempool. When including a pending transaction in your bundle, it is similar to that transaction having a gas price of `0`; other transactions in your bundle will need to pay more for the gas it uses. 177 | 178 | ## Bundle and User Statistics 179 | 180 | The Flashbots relay can also return statistics about you as a user (identified solely by your signing address) and any bundle previously submitted. 181 | 182 | - `getUserStats()` returns aggregate metrics about past performance, including if your signing key is currently eligible for the "high priority" queue. [Read more about searcher reputation here](https://docs.flashbots.net/flashbots-auction/searchers/advanced/reputation) 183 | - `getBundleStats(bundleHash, targetBlockNumber)` returns data specific to a single bundle submission, including detailed timestamps for the various phases a bundle goes before reaching miners. You can get the bundleHash from the simulation: 184 | 185 | ```js 186 | const simulation = await flashbotsProvider.simulate(signedTransactions, targetBlockNumber) 187 | console.log(simulation.bundleHash) 188 | ``` 189 | 190 | ## Investigating Losses 191 | 192 | When your bundle fails to land in the specified block, there are many reasons why this could have occurred. For a list of reasons, check out [Flashbots Docs : Why didn't my transaction get included?](https://docs.flashbots.net/flashbots-auction/searchers/faq/#why-didnt-my-transaction-get-included). To aid in troubleshooting, this library offers a method that will simulate a bundle in multiple positions to identify the competing bundle that landed on chain (if there was one) and calculate the relevant pricing. 193 | 194 | For usage instructions, check out the `demo-research.ts`. 195 | 196 | ## How to run demo.ts 197 | 198 | Included is a simple demo of how to construct the FlashbotsProvider with auth signer authentication and submit a [non-functional] bundle. This will not yield any mev, but serves as a sample initialization to help integrate into your own functional searcher. 199 | 200 | ## Sending a Private Transaction 201 | 202 | To send a _single_ transaction without having to send it as a bundle, use the `sendPrivateTransaction` function. This method allows us to process transactions faster and more efficiently than regular bundles. When you send a transaction using this method, we will try to send it to miners over the next 25 blocks (up to, but not past the target block number). 203 | 204 | ```js 205 | const tx = { 206 | from: wallet.address, 207 | to: wallet.address, 208 | value: "0x42", 209 | gasPrice: BigNumber.from(99).mul(1e9), // 99 gwei 210 | gasLimit: BigNumber.from(21000), 211 | } 212 | const privateTx = { 213 | transaction: tx, 214 | signer: wallet, 215 | } 216 | 217 | const res = await flashbotsProvider.sendPrivateTransaction(privateTx) 218 | ``` 219 | 220 | Optionally, you can set the following parameters to fine-tune your submission: 221 | 222 | ```js 223 | // highest block number your tx can be included in 224 | const maxBlockNumber = (await provider.getBlockNumber()) + 10; 225 | // timestamp for simulations 226 | const minTimestamp = 1645753192; 227 | 228 | const res = await flashbotsProvider.sendPrivateTransaction( 229 | privateTx, 230 | {maxBlockNumber, minTimestamp} 231 | ) 232 | ``` 233 | 234 | ## Flashbots on Goerli 235 | 236 | To test Flashbots before going to mainnet, you can use the Goerli Flashbots relay, which works in conjunction with a Flashbots-enabled Goerli validator. Flashbots on Goerli requires two simple changes: 237 | 238 | 1. Ensure your genericProvider passed in to the FlashbotsBundleProvider constructor is connected to Goerli (gas estimates and nonce requests need to correspond to the correct chain): 239 | 240 | ```ts 241 | import { providers } from 'ethers' 242 | const provider = providers.getDefaultProvider('goerli') 243 | ``` 244 | 245 | 2. Set the relay endpoint to `https://relay-goerli.flashbots.net/` 246 | 247 | ```ts 248 | const flashbotsProvider = await FlashbotsBundleProvider.create( 249 | provider, 250 | authSigner, 251 | 'https://relay-goerli.flashbots.net/', 252 | 'goerli') 253 | ``` 254 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flashbots-sdk/ethers-provider-bundle", 3 | "version": "0.6.2", 4 | "description": "", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/flashbots-sdk/ethers-provider-flashbots-bundle.git" 10 | }, 11 | "scripts": { 12 | "demo": "npx ts-node --project tsconfig.json src/demo.ts", 13 | "build": "npx tsc", 14 | "clean": "rm -rf build/", 15 | "prepare": "npm run clean && npm run build", 16 | "lint": "npx eslint src" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "devDependencies": { 21 | "@types/node": "14.14.10", 22 | "@types/uuid": "8.3.4", 23 | "@typescript-eslint/eslint-plugin": "4.28.5", 24 | "@typescript-eslint/parser": "4.28.5", 25 | "eslint": "7.32.0", 26 | "eslint-config-prettier": "8.3.0", 27 | "eslint-plugin-prettier": "3.4.0", 28 | "ethers": "5.7.2", 29 | "prettier": "2.2.1", 30 | "ts-node": "10.9.1", 31 | "typescript": "4.1.2", 32 | "uuid": "9.0.0" 33 | }, 34 | "peerDependencies": { 35 | "ethers": "5.7.2" 36 | }, 37 | "dependencies": { 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/demo-research.ts: -------------------------------------------------------------------------------- 1 | import { providers, utils, Wallet } from 'ethers' 2 | import { FlashbotsBundleConflictType, FlashbotsBundleProvider, FlashbotsGasPricing } from './index' 3 | 4 | const FLASHBOTS_AUTH_KEY = process.env.FLASHBOTS_AUTH_KEY 5 | 6 | // ===== Uncomment this for mainnet ======= 7 | const CHAIN_ID = 1 8 | const provider = new providers.JsonRpcProvider( 9 | { url: process.env.ETHEREUM_RPC_URL || 'http://127.0.0.1:8545' }, 10 | { chainId: CHAIN_ID, ensAddress: '', name: 'mainnet' } 11 | ) 12 | const FLASHBOTS_EP = undefined 13 | // ===== Uncomment this for mainnet ======= 14 | 15 | // ===== Uncomment this for Goerli ======= 16 | // const CHAIN_ID = 5 17 | // const provider = new providers.InfuraProvider(CHAIN_ID, process.env.INFURA_API_KEY) 18 | // const FLASHBOTS_EP = 'https://relay-goerli.flashbots.net/' 19 | // ===== Uncomment this for Goerli ======= 20 | 21 | function printGasPricing(gasPricing: FlashbotsGasPricing) { 22 | console.log(`Gas Used: ${gasPricing.gasUsed} in ${gasPricing.txCount} txs`) 23 | console.log(`[searcher] Gas Fees: ${utils.formatUnits(gasPricing.gasFeesPaidBySearcher)} ETH`) 24 | console.log(`[searcher] Effective Gas Price: ${utils.formatUnits(gasPricing.effectiveGasPriceToSearcher, 'gwei')} gwei`) 25 | console.log(`[miner] Priority Fees: ${utils.formatUnits(gasPricing.priorityFeesReceivedByMiner)} ETH`) 26 | console.log(`[miner] Effective Priority Fee Per Gas: ${utils.formatUnits(gasPricing.effectivePriorityFeeToMiner, 'gwei')} gwei`) 27 | } 28 | 29 | async function main() { 30 | const authSigner = FLASHBOTS_AUTH_KEY ? new Wallet(FLASHBOTS_AUTH_KEY) : Wallet.createRandom() 31 | const flashbotsProvider = await FlashbotsBundleProvider.create(provider, authSigner, FLASHBOTS_EP) 32 | 33 | //// Conflicting By Gas Used (Opportunity gone, tx does not revert) 34 | const conflictReport = await flashbotsProvider.getConflictingBundle( 35 | [ 36 | '0xf903438247d9860c192ff21bd08307f6c494c040afa5d1c50b8970ececfb3fdfaec2fe44f9e580b902d91003f4863028b093fdac9cf7fd67c0df6866ac3c7a60070fd72adbced27fd10108000000000000000000006cbefa95e42960e579c2a3058c05c6a08e2498e9000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20200000000000000000000006b3595068778dd592e39a122f4f5a5cf09c90fe206000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139ae64e36bd08a25300000000000000000000000000000000000000000000001390a439b0d6a9339d000000000000000000000000000000000000000000000000226ea0aea1b3a8008abb0156557c9d04a21b74c98f7a1e568fce9ce706eaeaeaaf13abadab000800010000000000000000fa6de2697d59e88ed7fc4dfe5a33dac43565ea410000000000000000000000006b3595068778dd592e39a122f4f5a5cf09c90fe20000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f98400000000000000000000000000000000000000000000000001d452934ce60a430000000000000000000000000000000000000000000000000131668bcc7ed58a000000000000000000000000000000000000000000000030725865ef11c80000cd97f4ca351672c24be7cb5ebb3d8ebb9bed99e0070fd72adbced27fd10800000000000000000000001d42064fc4beb5f8aaf85f4617ae8b3b5b8bd8010000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f984010000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001553a12e4e6b70599f922f86000000000000000000000000000000000000000015511e6d96f16b51931db09400000000000000000000000000000000000000000000005f2d176a52eefc00001ca01a6dad86b54953f74db59bfecca32b0f2158fab77826cd5088a93750bf52bfd9a01439a44c79a90df3a1a8e4fa5022a725748e4487c36cdb5fa3cc22b8f70c21e0' 37 | ], 38 | 13417951 39 | ) 40 | 41 | //// Nonce collision (likely same tx, but could be any tx at that from/nonce) 42 | // const conflictReport = await flashbotsProvider.getConflictingBundle( 43 | // [ 44 | // '0x02f90192011f8477359400852ea3491a808303bb6a94d9e1ce17f2641f24ae83637ab66a2cca9c378b9f80b9012438ed1739000000000000000000000000000000000000000000000000000000d18c2e2800000000000000000000000000000000000000000000000aa609340447868bb14100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000f9124e7b6ced254fdd13a43f06920c01d47e3cae00000000000000000000000000000000000000000000000000000000612f95880000000000000000000000000000000000000000000000000000000000000003000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000d291e7a03283640fdc51b121ac401383a46cc623c001a0e0e5af01da94cac4adbb4f6db0d6ab617262e56b9fc8b079db15b9c2c5beb105a065ec52ea471776f9a01d0b2e82f507d3dd6ea9a002d23842943d7a94084b3cd4', 45 | // ], 46 | // 13140328 47 | // ) 48 | 49 | //// No Bundles 50 | // const conflictReport = await flashbotsProvider.getConflictingBundle( 51 | // [ 52 | // '0xf901ad82095a852ea3491a80830dbba09407b9b7d3354fea8f651e39e97aabdfac4176da5880b90144b3dfe91400000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2030000000000000000004db7ae1ed05522740000000000000000503827419ce132760000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000600000000000000000000006b3595068778dd592e39a122f4f5a5cf09c90fe202000000000000000000000000795065dcc9f64b5614c407a6efdc400da6221fb00000000000000000000000d291e7a03283640fdc51b121ac401383a46cc623020000000000000000000000008c8d312554011f564aa54b0c2335139087037c840000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc201000000000000000000000000dc2b82bc1106c9c5286e59344896fb0ceb932f5326a036804d0a9f48f3f1154d8a8a52937bb31976bb3d4d5d5acd46cc9c605459dd8ca032f028ba273ab9fa0547d5abc785fb896946d4bab03ca990b68f5269f6735d5f' 53 | // ], 54 | // 13140329 55 | // ) 56 | 57 | console.log('Target Bundle Gas Pricing') 58 | printGasPricing(conflictReport.targetBundleGasPricing) 59 | 60 | if (conflictReport.conflictingBundleGasPricing !== undefined) { 61 | console.log('\nConflicting Bundle:', conflictReport.conflictingBundle) 62 | console.log('\nConflicting Bundle Gas Pricing') 63 | printGasPricing(conflictReport.conflictingBundleGasPricing) 64 | } 65 | console.log('Conflict Type: ' + FlashbotsBundleConflictType[conflictReport.conflictType]) 66 | } 67 | 68 | main() 69 | -------------------------------------------------------------------------------- /src/demo.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, providers, Wallet } from 'ethers' 2 | import { FlashbotsBundleProvider, FlashbotsBundleResolution } from './index' 3 | import { TransactionRequest } from '@ethersproject/abstract-provider' 4 | import { v4 as uuidv4 } from 'uuid' 5 | 6 | const FLASHBOTS_AUTH_KEY = process.env.FLASHBOTS_AUTH_KEY 7 | 8 | const GWEI = BigNumber.from(10).pow(9) 9 | const PRIORITY_FEE = GWEI.mul(3) 10 | const LEGACY_GAS_PRICE = GWEI.mul(12) 11 | const BLOCKS_IN_THE_FUTURE = 2 12 | 13 | // ===== Uncomment this for mainnet ======= 14 | // const CHAIN_ID = 1 15 | // const provider = new providers.JsonRpcProvider( 16 | // { url: process.env.ETHEREUM_RPC_URL || 'http://127.0.0.1:8545' }, 17 | // { chainId: CHAIN_ID, ensAddress: '', name: 'mainnet' } 18 | // ) 19 | // const FLASHBOTS_EP = 'https://relay.flashbots.net/' 20 | // ===== Uncomment this for mainnet ======= 21 | 22 | // ===== Uncomment this for Goerli ======= 23 | const CHAIN_ID = 5 24 | const provider = new providers.InfuraProvider(CHAIN_ID, process.env.INFURA_API_KEY) 25 | const FLASHBOTS_EP = 'https://relay-goerli.flashbots.net/' 26 | // ===== Uncomment this for Goerli ======= 27 | 28 | for (const e of ['FLASHBOTS_AUTH_KEY', 'INFURA_API_KEY', 'ETHEREUM_RPC_URL', 'PRIVATE_KEY']) { 29 | if (!process.env[e]) { 30 | // don't warn for skipping ETHEREUM_RPC_URL if using goerli 31 | if (FLASHBOTS_EP.includes('goerli') && e === 'ETHEREUM_RPC_URL') { 32 | continue 33 | } 34 | console.warn(`${e} should be defined as an environment variable`) 35 | } 36 | } 37 | 38 | async function main() { 39 | const authSigner = FLASHBOTS_AUTH_KEY ? new Wallet(FLASHBOTS_AUTH_KEY) : Wallet.createRandom() 40 | const wallet = new Wallet(process.env.PRIVATE_KEY || '', provider) 41 | const flashbotsProvider = await FlashbotsBundleProvider.create(provider, authSigner, FLASHBOTS_EP) 42 | 43 | const userStats = flashbotsProvider.getUserStats() 44 | if (process.env.TEST_V2) { 45 | try { 46 | const userStats2 = await flashbotsProvider.getUserStatsV2() 47 | console.log('userStatsV2', userStats2) 48 | } catch (e) { 49 | console.error('[v2 error]', e) 50 | } 51 | } 52 | 53 | const legacyTransaction = { 54 | to: wallet.address, 55 | gasPrice: LEGACY_GAS_PRICE, 56 | gasLimit: 21000, 57 | data: '0x', 58 | nonce: await provider.getTransactionCount(wallet.address), 59 | chainId: CHAIN_ID 60 | } 61 | 62 | provider.on('block', async (blockNumber) => { 63 | const block = await provider.getBlock(blockNumber) 64 | const replacementUuid = uuidv4() 65 | 66 | let eip1559Transaction: TransactionRequest 67 | if (block.baseFeePerGas == null) { 68 | console.warn('This chain is not EIP-1559 enabled, defaulting to two legacy transactions for demo') 69 | eip1559Transaction = { ...legacyTransaction } 70 | // We set a nonce in legacyTransaction above to limit validity to a single landed bundle. Delete that nonce for tx#2, and allow bundle provider to calculate it 71 | delete eip1559Transaction.nonce 72 | } else { 73 | const maxBaseFeeInFutureBlock = FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock(block.baseFeePerGas, BLOCKS_IN_THE_FUTURE) 74 | eip1559Transaction = { 75 | to: wallet.address, 76 | type: 2, 77 | maxFeePerGas: PRIORITY_FEE.add(maxBaseFeeInFutureBlock), 78 | maxPriorityFeePerGas: PRIORITY_FEE, 79 | gasLimit: 21000, 80 | data: '0x', 81 | chainId: CHAIN_ID 82 | } 83 | } 84 | 85 | const signedTransactions = await flashbotsProvider.signBundle([ 86 | { 87 | signer: wallet, 88 | transaction: legacyTransaction 89 | }, 90 | { 91 | signer: wallet, 92 | transaction: eip1559Transaction 93 | } 94 | ]) 95 | const targetBlock = blockNumber + BLOCKS_IN_THE_FUTURE 96 | const simulation = await flashbotsProvider.simulate(signedTransactions, targetBlock) 97 | 98 | // Using TypeScript discrimination 99 | if ('error' in simulation) { 100 | console.warn(`Simulation Error: ${simulation.error.message}`) 101 | process.exit(1) 102 | } else { 103 | console.log(`Simulation Success: ${JSON.stringify(simulation, null, 2)}`) 104 | } 105 | 106 | const bundleSubmission = await flashbotsProvider.sendRawBundle(signedTransactions, targetBlock, { replacementUuid }) 107 | console.log('bundle submitted, waiting') 108 | if ('error' in bundleSubmission) { 109 | throw new Error(bundleSubmission.error.message) 110 | } 111 | 112 | const cancelResult = await flashbotsProvider.cancelBundles(replacementUuid) 113 | console.log('cancel response', cancelResult) 114 | 115 | const waitResponse = await bundleSubmission.wait() 116 | console.log(`Wait Response: ${FlashbotsBundleResolution[waitResponse]}`) 117 | if (waitResponse === FlashbotsBundleResolution.BundleIncluded || waitResponse === FlashbotsBundleResolution.AccountNonceTooHigh) { 118 | process.exit(0) 119 | } else { 120 | console.log({ 121 | bundleStats: await flashbotsProvider.getBundleStats(simulation.bundleHash, targetBlock), 122 | bundleStatsV2: process.env.TEST_V2 && (await flashbotsProvider.getBundleStatsV2(simulation.bundleHash, targetBlock)), 123 | userStats: await userStats 124 | }) 125 | } 126 | }) 127 | } 128 | 129 | main() 130 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { BlockTag, TransactionReceipt, TransactionRequest } from '@ethersproject/abstract-provider' 2 | import { Networkish } from '@ethersproject/networks' 3 | import { BaseProvider } from '@ethersproject/providers' 4 | import { ConnectionInfo, fetchJson } from '@ethersproject/web' 5 | import { BigNumber, ethers, providers, Signer } from 'ethers' 6 | import { id, keccak256 } from 'ethers/lib/utils' 7 | import { serialize } from '@ethersproject/transactions' 8 | 9 | export const DEFAULT_FLASHBOTS_RELAY = 'https://relay.flashbots.net' 10 | export const BASE_FEE_MAX_CHANGE_DENOMINATOR = 8 11 | 12 | export enum FlashbotsBundleResolution { 13 | BundleIncluded, 14 | BlockPassedWithoutInclusion, 15 | AccountNonceTooHigh 16 | } 17 | 18 | export enum FlashbotsTransactionResolution { 19 | TransactionIncluded, 20 | TransactionDropped 21 | } 22 | 23 | export enum FlashbotsBundleConflictType { 24 | NoConflict, 25 | NonceCollision, 26 | Error, 27 | CoinbasePayment, 28 | GasUsed, 29 | NoBundlesInBlock 30 | } 31 | 32 | export interface FlashbotsBundleRawTransaction { 33 | signedTransaction: string 34 | } 35 | 36 | export interface FlashbotsBundleTransaction { 37 | transaction: TransactionRequest 38 | signer: Signer 39 | } 40 | 41 | export interface FlashbotsOptions { 42 | minTimestamp?: number 43 | maxTimestamp?: number 44 | revertingTxHashes?: Array 45 | replacementUuid?: string 46 | } 47 | 48 | export interface TransactionAccountNonce { 49 | hash: string 50 | signedTransaction: string 51 | account: string 52 | nonce: number 53 | } 54 | 55 | export interface FlashbotsTransactionResponse { 56 | bundleTransactions: Array 57 | wait: () => Promise 58 | simulate: () => Promise 59 | receipts: () => Promise> 60 | bundleHash: string 61 | } 62 | 63 | export interface FlashbotsPrivateTransactionResponse { 64 | transaction: TransactionAccountNonce 65 | wait: () => Promise 66 | simulate: () => Promise 67 | receipts: () => Promise> 68 | } 69 | 70 | export interface TransactionSimulationBase { 71 | txHash: string 72 | gasUsed: number 73 | gasFees: string 74 | gasPrice: string 75 | toAddress: string 76 | fromAddress: string 77 | coinbaseDiff: string 78 | } 79 | 80 | export interface TransactionSimulationSuccess extends TransactionSimulationBase { 81 | value: string 82 | ethSentToCoinbase: string 83 | coinbaseDiff: string 84 | } 85 | 86 | export interface TransactionSimulationRevert extends TransactionSimulationBase { 87 | error: string 88 | revert: string 89 | } 90 | 91 | export type TransactionSimulation = TransactionSimulationSuccess | TransactionSimulationRevert 92 | 93 | export interface RelayResponseError { 94 | error: { 95 | message: string 96 | code: number 97 | } 98 | } 99 | 100 | export interface SimulationResponseSuccess { 101 | bundleGasPrice: BigNumber 102 | bundleHash: string 103 | coinbaseDiff: BigNumber 104 | ethSentToCoinbase: BigNumber 105 | gasFees: BigNumber 106 | results: Array 107 | totalGasUsed: number 108 | stateBlockNumber: number 109 | firstRevert?: TransactionSimulation 110 | } 111 | 112 | export type SimulationResponse = SimulationResponseSuccess | RelayResponseError 113 | 114 | export type FlashbotsTransaction = FlashbotsTransactionResponse | RelayResponseError 115 | 116 | export type FlashbotsPrivateTransaction = FlashbotsPrivateTransactionResponse | RelayResponseError 117 | 118 | export interface GetUserStatsResponseSuccess { 119 | is_high_priority: boolean 120 | all_time_miner_payments: string 121 | all_time_gas_simulated: string 122 | last_7d_miner_payments: string 123 | last_7d_gas_simulated: string 124 | last_1d_miner_payments: string 125 | last_1d_gas_simulated: string 126 | } 127 | 128 | export interface GetUserStatsResponseSuccessV2 { 129 | isHighPriority: boolean 130 | allTimeValidatorPayments: string 131 | allTimeGasSimulated: string 132 | last7dValidatorPayments: string 133 | last7dGasSimulated: string 134 | last1dValidatorPayments: string 135 | last1dGasSimulated: string 136 | } 137 | 138 | export type GetUserStatsResponse = GetUserStatsResponseSuccess | RelayResponseError 139 | export type GetUserStatsResponseV2 = GetUserStatsResponseSuccessV2 | RelayResponseError 140 | 141 | interface PubKeyTimestamp { 142 | pubkey: string 143 | timestamp: string 144 | } 145 | 146 | export interface GetBundleStatsResponseSuccess { 147 | isSimulated: boolean 148 | isSentToMiners: boolean 149 | isHighPriority: boolean 150 | simulatedAt: string 151 | submittedAt: string 152 | sentToMinersAt: string 153 | consideredByBuildersAt: Array 154 | sealedByBuildersAt: Array 155 | } 156 | 157 | export interface GetBundleStatsResponseSuccessV2 { 158 | isSimulated: boolean 159 | isHighPriority: boolean 160 | simulatedAt: string 161 | receivedAt: string 162 | consideredByBuildersAt: Array 163 | sealedByBuildersAt: Array 164 | } 165 | 166 | export type GetBundleStatsResponse = GetBundleStatsResponseSuccess | RelayResponseError 167 | export type GetBundleStatsResponseV2 = GetBundleStatsResponseSuccessV2 | RelayResponseError 168 | 169 | interface BlocksApiResponseTransactionDetails { 170 | transaction_hash: string 171 | tx_index: number 172 | bundle_type: 'rogue' | 'flashbots' | 'mempool' 173 | bundle_index: number 174 | block_number: number 175 | eoa_address: string 176 | to_address: string 177 | gas_used: number 178 | gas_price: string 179 | coinbase_transfer: string 180 | eth_sent_to_fee_recipient: string 181 | total_miner_reward: string 182 | fee_recipient_eth_diff: string 183 | } 184 | 185 | interface BlocksApiResponseBlockDetails { 186 | block_number: number 187 | fee_recipient: string 188 | fee_recipient_eth_diff: string 189 | miner_reward: string 190 | miner: string 191 | coinbase_transfers: string 192 | eth_sent_to_fee_recipient: string 193 | gas_used: number 194 | gas_price: string 195 | transactions: Array 196 | } 197 | 198 | export interface BlocksApiResponse { 199 | latest_block_number: number 200 | blocks: Array 201 | } 202 | 203 | export interface FlashbotsBundleConflict { 204 | conflictingBundle: Array 205 | initialSimulation: SimulationResponseSuccess 206 | conflictType: FlashbotsBundleConflictType 207 | } 208 | 209 | export interface FlashbotsGasPricing { 210 | txCount: number 211 | gasUsed: number 212 | gasFeesPaidBySearcher: BigNumber 213 | priorityFeesReceivedByMiner: BigNumber 214 | ethSentToCoinbase: BigNumber 215 | effectiveGasPriceToSearcher: BigNumber 216 | effectivePriorityFeeToMiner: BigNumber 217 | } 218 | 219 | export interface FlashbotsBundleConflictWithGasPricing extends FlashbotsBundleConflict { 220 | targetBundleGasPricing: FlashbotsGasPricing 221 | conflictingBundleGasPricing?: FlashbotsGasPricing 222 | } 223 | 224 | export interface FlashbotsCancelBidResponseSuccess { 225 | bundleHashes: string[] 226 | } 227 | 228 | export type FlashbotsCancelBidResponse = FlashbotsCancelBidResponseSuccess | RelayResponseError 229 | 230 | type RpcParams = Array> 231 | 232 | const TIMEOUT_MS = 5 * 60 * 1000 233 | 234 | export class FlashbotsBundleProvider extends providers.JsonRpcProvider { 235 | private genericProvider: BaseProvider 236 | private authSigner: Signer 237 | private connectionInfo: ConnectionInfo 238 | 239 | constructor(genericProvider: BaseProvider, authSigner: Signer, connectionInfoOrUrl: ConnectionInfo, network: Networkish) { 240 | super(connectionInfoOrUrl, network) 241 | this.genericProvider = genericProvider 242 | this.authSigner = authSigner 243 | this.connectionInfo = connectionInfoOrUrl 244 | } 245 | 246 | static async throttleCallback(): Promise { 247 | console.warn('Rate limited') 248 | return false 249 | } 250 | 251 | /** 252 | * Creates a new Flashbots provider. 253 | * @param genericProvider ethers.js mainnet provider 254 | * @param authSigner account to sign bundles 255 | * @param connectionInfoOrUrl (optional) connection settings 256 | * @param network (optional) network settings 257 | * 258 | * @example 259 | * ```typescript 260 | * const {providers, Wallet} = require("ethers") 261 | * const {FlashbotsBundleProvider} = require("@flashbots/ethers-provider-bundle") 262 | * const authSigner = Wallet.createRandom() 263 | * const provider = new providers.JsonRpcProvider("http://localhost:8545") 264 | * const fbProvider = await FlashbotsBundleProvider.create(provider, authSigner) 265 | * ``` 266 | */ 267 | static async create( 268 | genericProvider: BaseProvider, 269 | authSigner: Signer, 270 | connectionInfoOrUrl?: ConnectionInfo | string, 271 | network?: Networkish 272 | ): Promise { 273 | const connectionInfo: ConnectionInfo = 274 | typeof connectionInfoOrUrl === 'string' || typeof connectionInfoOrUrl === 'undefined' 275 | ? { 276 | url: connectionInfoOrUrl || DEFAULT_FLASHBOTS_RELAY 277 | } 278 | : { 279 | ...connectionInfoOrUrl 280 | } 281 | if (connectionInfo.headers === undefined) connectionInfo.headers = {} 282 | connectionInfo.throttleCallback = FlashbotsBundleProvider.throttleCallback 283 | const networkish: Networkish = { 284 | chainId: 0, 285 | name: '' 286 | } 287 | if (typeof network === 'string') { 288 | networkish.name = network 289 | } else if (typeof network === 'number') { 290 | networkish.chainId = network 291 | } else if (typeof network === 'object') { 292 | networkish.name = network.name 293 | networkish.chainId = network.chainId 294 | } 295 | 296 | if (networkish.chainId === 0) { 297 | networkish.chainId = (await genericProvider.getNetwork()).chainId 298 | } 299 | 300 | return new FlashbotsBundleProvider(genericProvider, authSigner, connectionInfo, networkish) 301 | } 302 | 303 | /** 304 | * Calculates maximum base fee in a future block. 305 | * @param baseFee current base fee 306 | * @param blocksInFuture number of blocks in the future 307 | */ 308 | static getMaxBaseFeeInFutureBlock(baseFee: BigNumber, blocksInFuture: number): BigNumber { 309 | let maxBaseFee = BigNumber.from(baseFee) 310 | for (let i = 0; i < blocksInFuture; i++) { 311 | maxBaseFee = maxBaseFee.mul(1125).div(1000).add(1) 312 | } 313 | return maxBaseFee 314 | } 315 | 316 | /** 317 | * Calculates base fee for the next block. 318 | * @param currentBaseFeePerGas base fee of current block (wei) 319 | * @param currentGasUsed gas used by tx in simulation 320 | * @param currentGasLimit gas limit of transaction 321 | */ 322 | static getBaseFeeInNextBlock(currentBaseFeePerGas: BigNumber, currentGasUsed: BigNumber, currentGasLimit: BigNumber): BigNumber { 323 | const currentGasTarget = currentGasLimit.div(2) 324 | 325 | if (currentGasUsed.eq(currentGasTarget)) { 326 | return currentBaseFeePerGas 327 | } else if (currentGasUsed.gt(currentGasTarget)) { 328 | const gasUsedDelta = currentGasUsed.sub(currentGasTarget) 329 | const baseFeePerGasDelta = currentBaseFeePerGas.mul(gasUsedDelta).div(currentGasTarget).div(BASE_FEE_MAX_CHANGE_DENOMINATOR) 330 | 331 | return currentBaseFeePerGas.add(baseFeePerGasDelta) 332 | } else { 333 | const gasUsedDelta = currentGasTarget.sub(currentGasUsed) 334 | const baseFeePerGasDelta = currentBaseFeePerGas.mul(gasUsedDelta).div(currentGasTarget).div(BASE_FEE_MAX_CHANGE_DENOMINATOR) 335 | 336 | return currentBaseFeePerGas.sub(baseFeePerGasDelta) 337 | } 338 | } 339 | 340 | /** 341 | * Calculates a bundle hash locally. 342 | * @param txHashes hashes of transactions in the bundle 343 | */ 344 | static generateBundleHash(txHashes: Array): string { 345 | const concatenatedHashes = txHashes.map((txHash) => txHash.slice(2)).join('') 346 | return keccak256(`0x${concatenatedHashes}`) 347 | } 348 | 349 | /** 350 | * Sends a signed flashbots bundle to Flashbots Relay. 351 | * @param signedBundledTransactions array of raw signed transactions 352 | * @param targetBlockNumber block to target for bundle inclusion 353 | * @param opts (optional) settings 354 | * @returns callbacks for handling results, and the bundle hash 355 | * 356 | * @example 357 | * ```typescript 358 | * const bundle: Array = [ 359 | * {signedTransaction: "0x02..."}, 360 | * {signedTransaction: "0x02..."}, 361 | * ] 362 | * const signedBundle = await fbProvider.signBundle(bundle) 363 | * const blockNum = await provider.getBlockNumber() 364 | * const bundleRes = await fbProvider.sendRawBundle(signedBundle, blockNum + 1) 365 | * const success = (await bundleRes.wait()) === FlashbotsBundleResolution.BundleIncluded 366 | * ``` 367 | */ 368 | public async sendRawBundle( 369 | signedBundledTransactions: Array, 370 | targetBlockNumber: number, 371 | opts?: FlashbotsOptions 372 | ): Promise { 373 | const params = { 374 | txs: signedBundledTransactions, 375 | blockNumber: `0x${targetBlockNumber.toString(16)}`, 376 | minTimestamp: opts?.minTimestamp, 377 | maxTimestamp: opts?.maxTimestamp, 378 | revertingTxHashes: opts?.revertingTxHashes, 379 | replacementUuid: opts?.replacementUuid, 380 | builders: ["rsync", "beaverbuild.org", "builder0x69"] 381 | } 382 | 383 | const request = JSON.stringify(this.prepareRelayRequest('eth_sendBundle', [params])) 384 | const response = await this.request(request) 385 | if (response.error !== undefined && response.error !== null) { 386 | return { 387 | error: { 388 | message: response.error.message, 389 | code: response.error.code 390 | } 391 | } 392 | } 393 | 394 | const bundleTransactions = signedBundledTransactions.map((signedTransaction) => { 395 | const transactionDetails = ethers.utils.parseTransaction(signedTransaction) 396 | return { 397 | signedTransaction, 398 | hash: ethers.utils.keccak256(signedTransaction), 399 | account: transactionDetails.from || '0x0', 400 | nonce: transactionDetails.nonce 401 | } 402 | }) 403 | 404 | return { 405 | bundleTransactions, 406 | wait: () => this.waitForBundleInclusion(bundleTransactions, targetBlockNumber, TIMEOUT_MS), 407 | simulate: () => 408 | this.simulate( 409 | bundleTransactions.map((tx) => tx.signedTransaction), 410 | targetBlockNumber, 411 | undefined, 412 | opts?.minTimestamp 413 | ), 414 | receipts: () => this.fetchReceipts(bundleTransactions), 415 | bundleHash: response?.result?.bundleHash 416 | } 417 | } 418 | 419 | /** 420 | * Sends a bundle to Flashbots, supports multiple transaction interfaces. 421 | * @param bundledTransactions array of transactions, either signed or provided with a signer. 422 | * @param targetBlockNumber block to target for bundle inclusion 423 | * @param opts (optional) settings 424 | * @returns callbacks for handling results, and the bundle hash 425 | */ 426 | public async sendBundle( 427 | bundledTransactions: Array, 428 | targetBlockNumber: number, 429 | opts?: FlashbotsOptions 430 | ): Promise { 431 | const signedTransactions = await this.signBundle(bundledTransactions) 432 | return this.sendRawBundle(signedTransactions, targetBlockNumber, opts) 433 | } 434 | 435 | /** Cancel any bundles submitted with the given `replacementUuid` 436 | * @param replacementUuid specified in `sendBundle` 437 | * @returns bundle hashes of the cancelled bundles 438 | */ 439 | public async cancelBundles(replacementUuid: string): Promise { 440 | const params = { 441 | replacementUuid: replacementUuid 442 | } 443 | 444 | const request = JSON.stringify(this.prepareRelayRequest('eth_cancelBundle', [params])) 445 | const response = await this.request(request) 446 | 447 | if (response.error !== undefined && response.error !== null) { 448 | return { 449 | error: { 450 | message: response.error.message, 451 | code: response.error.code 452 | } 453 | } 454 | } 455 | return { 456 | bundleHashes: response.result 457 | } 458 | } 459 | 460 | /** 461 | * Sends a single private transaction to Flashbots. 462 | * @param transaction transaction, either signed or provided with a signer 463 | * @param opts (optional) settings 464 | * @returns callbacks for handling results, and transaction data 465 | * 466 | * @example 467 | * ```typescript 468 | * const tx: FlashbotsBundleRawTransaction = {signedTransaction: "0x02..."} 469 | * const blockNum = await provider.getBlockNumber() 470 | * // try sending for 5 blocks 471 | * const response = await fbProvider.sendPrivateTransaction(tx, {maxBlockNumber: blockNum + 5}) 472 | * const success = (await response.wait()) === FlashbotsTransactionResolution.TransactionIncluded 473 | * ``` 474 | */ 475 | public async sendPrivateTransaction( 476 | transaction: FlashbotsBundleTransaction | FlashbotsBundleRawTransaction, 477 | opts?: { 478 | maxBlockNumber?: number 479 | simulationTimestamp?: number 480 | } 481 | ): Promise { 482 | const startBlockNumberPromise = this.genericProvider.getBlockNumber() 483 | 484 | let signedTransaction: string 485 | if ('signedTransaction' in transaction) { 486 | signedTransaction = transaction.signedTransaction 487 | } else { 488 | signedTransaction = await transaction.signer.signTransaction(transaction.transaction) 489 | } 490 | 491 | const params = { 492 | tx: signedTransaction, 493 | maxBlockNumber: opts?.maxBlockNumber 494 | } 495 | const request = JSON.stringify(this.prepareRelayRequest('eth_sendPrivateTransaction', [params])) 496 | const response = await this.request(request) 497 | if (response.error !== undefined && response.error !== null) { 498 | return { 499 | error: { 500 | message: response.error.message, 501 | code: response.error.code 502 | } 503 | } 504 | } 505 | 506 | const transactionDetails = ethers.utils.parseTransaction(signedTransaction) 507 | const privateTransaction = { 508 | signedTransaction: signedTransaction, 509 | hash: ethers.utils.keccak256(signedTransaction), 510 | account: transactionDetails.from || '0x0', 511 | nonce: transactionDetails.nonce 512 | } 513 | const startBlockNumber = await startBlockNumberPromise 514 | 515 | return { 516 | transaction: privateTransaction, 517 | wait: () => this.waitForTxInclusion(privateTransaction.hash, opts?.maxBlockNumber || startBlockNumber + 25, TIMEOUT_MS), 518 | simulate: () => this.simulate([privateTransaction.signedTransaction], startBlockNumber, undefined, opts?.simulationTimestamp), 519 | receipts: () => this.fetchReceipts([privateTransaction]) 520 | } 521 | } 522 | 523 | /** 524 | * Attempts to cancel a pending private transaction. 525 | * 526 | * **_Note_**: This function removes the transaction from the Flashbots 527 | * bundler, but miners may still include it if they have received it already. 528 | * @param txHash transaction hash corresponding to pending tx 529 | * @returns true if transaction was cancelled successfully 530 | * 531 | * @example 532 | * ```typescript 533 | * const pendingTxHash = (await fbProvider.sendPrivateTransaction(tx)).transaction.hash 534 | * const isTxCanceled = await fbProvider.cancelPrivateTransaction(pendingTxHash) 535 | * ``` 536 | */ 537 | public async cancelPrivateTransaction(txHash: string): Promise { 538 | const params = { 539 | txHash 540 | } 541 | const request = JSON.stringify(this.prepareRelayRequest('eth_cancelPrivateTransaction', [params])) 542 | const response = await this.request(request) 543 | if (response.error !== undefined && response.error !== null) { 544 | return { 545 | error: { 546 | message: response.error.message, 547 | code: response.error.code 548 | } 549 | } 550 | } 551 | 552 | return true 553 | } 554 | 555 | /** 556 | * Signs a Flashbots bundle with this provider's `authSigner` key. 557 | * @param bundledTransactions 558 | * @returns signed bundle 559 | * 560 | * @example 561 | * ```typescript 562 | * const bundle: Array = [ 563 | * {signedTransaction: "0x02..."}, 564 | * {signedTransaction: "0x02..."}, 565 | * ] 566 | * const signedBundle = await fbProvider.signBundle(bundle) 567 | * const blockNum = await provider.getBlockNumber() 568 | * const simResult = await fbProvider.simulate(signedBundle, blockNum + 1) 569 | * ``` 570 | */ 571 | public async signBundle(bundledTransactions: Array): Promise> { 572 | const nonces: { [address: string]: BigNumber } = {} 573 | const signedTransactions = new Array() 574 | for (const tx of bundledTransactions) { 575 | if ('signedTransaction' in tx) { 576 | // in case someone is mixing pre-signed and signing transactions, decode to add to nonce object 577 | const transactionDetails = ethers.utils.parseTransaction(tx.signedTransaction) 578 | if (transactionDetails.from === undefined) throw new Error('Could not decode signed transaction') 579 | nonces[transactionDetails.from] = BigNumber.from(transactionDetails.nonce + 1) 580 | signedTransactions.push(tx.signedTransaction) 581 | continue 582 | } 583 | const transaction = { ...tx.transaction } 584 | const address = await tx.signer.getAddress() 585 | if (typeof transaction.nonce === 'string') throw new Error('Bad nonce') 586 | const nonce = 587 | transaction.nonce !== undefined 588 | ? BigNumber.from(transaction.nonce) 589 | : nonces[address] || BigNumber.from(await this.genericProvider.getTransactionCount(address, 'latest')) 590 | nonces[address] = nonce.add(1) 591 | if (transaction.nonce === undefined) transaction.nonce = nonce 592 | if ((transaction.type == null || transaction.type == 0) && transaction.gasPrice === undefined) 593 | transaction.gasPrice = BigNumber.from(0) 594 | if (transaction.gasLimit === undefined) transaction.gasLimit = await tx.signer.estimateGas(transaction) // TODO: Add target block number and timestamp when supported by geth 595 | signedTransactions.push(await tx.signer.signTransaction(transaction)) 596 | } 597 | return signedTransactions 598 | } 599 | 600 | /** 601 | * Watches for a specific block to see if a bundle was included in it. 602 | * @param transactionAccountNonces bundle transactions 603 | * @param targetBlockNumber block number to check for bundle inclusion 604 | * @param timeout ms 605 | */ 606 | private waitForBundleInclusion(transactionAccountNonces: Array, targetBlockNumber: number, timeout: number) { 607 | return new Promise((resolve, reject) => { 608 | let timer: NodeJS.Timer | null = null 609 | let done = false 610 | 611 | const minimumNonceByAccount = transactionAccountNonces.reduce((acc, accountNonce) => { 612 | if (accountNonce.nonce > 0) { 613 | if (!acc[accountNonce.account] || accountNonce.nonce < acc[accountNonce.account]) { 614 | acc[accountNonce.account] = accountNonce.nonce 615 | } 616 | } 617 | return acc 618 | }, {} as { [account: string]: number }) 619 | 620 | const handler = async (blockNumber: number) => { 621 | if (blockNumber < targetBlockNumber) { 622 | const noncesValid = await Promise.all( 623 | Object.entries(minimumNonceByAccount).map(async ([account, nonce]) => { 624 | const transactionCount = await this.genericProvider.getTransactionCount(account) 625 | return nonce >= transactionCount 626 | }) 627 | ) 628 | const allNoncesValid = noncesValid.every(Boolean) 629 | if (allNoncesValid) return 630 | // target block not yet reached, but nonce has become invalid 631 | resolve(FlashbotsBundleResolution.AccountNonceTooHigh) 632 | } else { 633 | const block = await this.genericProvider.getBlock(targetBlockNumber) 634 | // check bundle against block: 635 | const blockTransactionsHash: { [key: string]: boolean } = {} 636 | for (const bt of block.transactions) { 637 | blockTransactionsHash[bt] = true 638 | } 639 | const bundleIncluded = transactionAccountNonces.every((transaction) => blockTransactionsHash[transaction.hash]) 640 | resolve(bundleIncluded ? FlashbotsBundleResolution.BundleIncluded : FlashbotsBundleResolution.BlockPassedWithoutInclusion) 641 | } 642 | 643 | if (timer) { 644 | clearTimeout(timer) 645 | } 646 | if (done) { 647 | return 648 | } 649 | done = true 650 | this.genericProvider.removeListener('block', handler) 651 | } 652 | this.genericProvider.on('block', handler) 653 | 654 | if (timeout > 0) { 655 | timer = setTimeout(() => { 656 | if (done) { 657 | return 658 | } 659 | timer = null 660 | done = true 661 | 662 | this.genericProvider.removeListener('block', handler) 663 | reject('Timed out') 664 | }, timeout) 665 | if (timer.unref) { 666 | timer.unref() 667 | } 668 | } 669 | }) 670 | } 671 | 672 | /** 673 | * Waits for a transaction to be included on-chain. 674 | * @param transactionHash 675 | * @param maxBlockNumber highest block number to check before stopping 676 | * @param timeout ms 677 | */ 678 | private waitForTxInclusion(transactionHash: string, maxBlockNumber: number, timeout: number) { 679 | return new Promise((resolve, reject) => { 680 | let timer: NodeJS.Timer | null = null 681 | let done = false 682 | 683 | // runs on new block event 684 | const handler = async (blockNumber: number) => { 685 | if (blockNumber <= maxBlockNumber) { 686 | // check tx status on mainnet 687 | const sentTxStatus = await this.genericProvider.getTransaction(transactionHash) 688 | if (sentTxStatus && sentTxStatus.confirmations >= 1) { 689 | resolve(FlashbotsTransactionResolution.TransactionIncluded) 690 | } else { 691 | return 692 | } 693 | } else { 694 | // tx not included in specified range, bail 695 | this.genericProvider.removeListener('block', handler) 696 | resolve(FlashbotsTransactionResolution.TransactionDropped) 697 | } 698 | 699 | if (timer) { 700 | clearTimeout(timer) 701 | } 702 | if (done) { 703 | return 704 | } 705 | done = true 706 | this.genericProvider.removeListener('block', handler) 707 | } 708 | 709 | this.genericProvider.on('block', handler) 710 | 711 | // time out if we've been trying for too long 712 | if (timeout > 0) { 713 | timer = setTimeout(() => { 714 | if (done) { 715 | return 716 | } 717 | timer = null 718 | done = true 719 | 720 | this.genericProvider.removeListener('block', handler) 721 | reject('Timed out') 722 | }, timeout) 723 | if (timer.unref) { 724 | timer.unref() 725 | } 726 | } 727 | }) 728 | } 729 | 730 | /** 731 | * Gets stats for provider instance's `authSigner` address. 732 | * @deprecated use {@link getUserStatsV2} instead. 733 | */ 734 | public async getUserStats(): Promise { 735 | const blockDetails = await this.genericProvider.getBlock('latest') 736 | const evmBlockNumber = `0x${blockDetails.number.toString(16)}` 737 | const params = [evmBlockNumber] 738 | const request = JSON.stringify(this.prepareRelayRequest('flashbots_getUserStats', params)) 739 | const response = await this.request(request) 740 | if (response.error !== undefined && response.error !== null) { 741 | return { 742 | error: { 743 | message: response.error.message, 744 | code: response.error.code 745 | } 746 | } 747 | } 748 | 749 | return response.result 750 | } 751 | 752 | /** 753 | * Gets stats for provider instance's `authSigner` address. 754 | */ 755 | public async getUserStatsV2(): Promise { 756 | const blockDetails = await this.genericProvider.getBlock('latest') 757 | const evmBlockNumber = `0x${blockDetails.number.toString(16)}` 758 | const params = [{ blockNumber: evmBlockNumber }] 759 | const request = JSON.stringify(this.prepareRelayRequest('flashbots_getUserStatsV2', params)) 760 | const response = await this.request(request) 761 | if (response.error !== undefined && response.error !== null) { 762 | return { 763 | error: { 764 | message: response.error.message, 765 | code: response.error.code 766 | } 767 | } 768 | } 769 | 770 | return response.result 771 | } 772 | 773 | /** 774 | * Gets information about a specific bundle. 775 | * @param bundleHash hash of bundle to investigate 776 | * @param blockNumber block in which the bundle should be included 777 | * @deprecated use {@link getBundleStatsV2} instead. 778 | */ 779 | public async getBundleStats(bundleHash: string, blockNumber: number): Promise { 780 | const evmBlockNumber = `0x${blockNumber.toString(16)}` 781 | 782 | const params = [{ bundleHash, blockNumber: evmBlockNumber }] 783 | const request = JSON.stringify(this.prepareRelayRequest('flashbots_getBundleStats', params)) 784 | const response = await this.request(request) 785 | if (response.error !== undefined && response.error !== null) { 786 | return { 787 | error: { 788 | message: response.error.message, 789 | code: response.error.code 790 | } 791 | } 792 | } 793 | 794 | return response.result 795 | } 796 | 797 | /** 798 | * Gets information about a specific bundle. 799 | * @param bundleHash hash of bundle to investigate 800 | * @param blockNumber block in which the bundle should be included 801 | */ 802 | public async getBundleStatsV2(bundleHash: string, blockNumber: number): Promise { 803 | const evmBlockNumber = `0x${blockNumber.toString(16)}` 804 | 805 | const params = [{ bundleHash, blockNumber: evmBlockNumber }] 806 | const request = JSON.stringify(this.prepareRelayRequest('flashbots_getBundleStatsV2', params)) 807 | const response = await this.request(request) 808 | if (response.error !== undefined && response.error !== null) { 809 | return { 810 | error: { 811 | message: response.error.message, 812 | code: response.error.code 813 | } 814 | } 815 | } 816 | 817 | return response.result 818 | } 819 | 820 | /** 821 | * Simluates a bundle on a given block. 822 | * @param signedBundledTransactions signed Flashbots bundle 823 | * @param blockTag block tag to simulate against, can use "latest" 824 | * @param stateBlockTag (optional) simulated block state tag 825 | * @param blockTimestamp (optional) simulated timestamp 826 | * 827 | * @example 828 | * ```typescript 829 | * const bundle: Array = [ 830 | * {signedTransaction: "0x1..."}, 831 | * {signedTransaction: "0x2..."}, 832 | * ] 833 | * const signedBundle = await fbProvider.signBundle(bundle) 834 | * const blockNum = await provider.getBlockNumber() 835 | * const simResult = await fbProvider.simulate(signedBundle, blockNum + 1) 836 | * ``` 837 | */ 838 | public async simulate( 839 | signedBundledTransactions: Array, 840 | blockTag: BlockTag, 841 | stateBlockTag?: BlockTag, 842 | blockTimestamp?: number, 843 | coinbase?: string 844 | ): Promise { 845 | let evmBlockNumber: string 846 | if (typeof blockTag === 'number') { 847 | evmBlockNumber = `0x${blockTag.toString(16)}` 848 | } else { 849 | const blockTagDetails = await this.genericProvider.getBlock(blockTag) 850 | const blockDetails = blockTagDetails !== null ? blockTagDetails : await this.genericProvider.getBlock('latest') 851 | evmBlockNumber = `0x${blockDetails.number.toString(16)}` 852 | } 853 | 854 | let evmBlockStateNumber: string 855 | if (typeof stateBlockTag === 'number') { 856 | evmBlockStateNumber = `0x${stateBlockTag.toString(16)}` 857 | } else if (!stateBlockTag) { 858 | evmBlockStateNumber = 'latest' 859 | } else { 860 | evmBlockStateNumber = stateBlockTag 861 | } 862 | 863 | const params: RpcParams = [ 864 | { 865 | txs: signedBundledTransactions, 866 | blockNumber: evmBlockNumber, 867 | stateBlockNumber: evmBlockStateNumber, 868 | timestamp: blockTimestamp, 869 | coinbase 870 | } 871 | ] 872 | const request = JSON.stringify(this.prepareRelayRequest('eth_callBundle', params)) 873 | const response = await this.request(request) 874 | if (response.error !== undefined && response.error !== null) { 875 | return { 876 | error: { 877 | message: response.error.message, 878 | code: response.error.code 879 | } 880 | } 881 | } 882 | 883 | const callResult = response.result 884 | return { 885 | bundleGasPrice: BigNumber.from(callResult.bundleGasPrice), 886 | bundleHash: callResult.bundleHash, 887 | coinbaseDiff: BigNumber.from(callResult.coinbaseDiff), 888 | ethSentToCoinbase: BigNumber.from(callResult.ethSentToCoinbase), 889 | gasFees: BigNumber.from(callResult.gasFees), 890 | results: callResult.results, 891 | stateBlockNumber: callResult.stateBlockNumber, 892 | totalGasUsed: callResult.results.reduce((a: number, b: TransactionSimulation) => a + b.gasUsed, 0), 893 | firstRevert: callResult.results.find((txSim: TransactionSimulation) => 'revert' in txSim || 'error' in txSim) 894 | } 895 | } 896 | 897 | private calculateBundlePricing( 898 | bundleTransactions: Array, 899 | baseFee: BigNumber 900 | ) { 901 | const bundleGasPricing = bundleTransactions.reduce( 902 | (acc, transactionDetail) => { 903 | // see: https://blocks.flashbots.net/ and https://github.com/flashbots/ethers-provider-flashbots-bundle/issues/62 904 | const gasUsed = 'gas_used' in transactionDetail ? transactionDetail.gas_used : transactionDetail.gasUsed 905 | const ethSentToCoinbase = 906 | 'coinbase_transfer' in transactionDetail 907 | ? transactionDetail.coinbase_transfer 908 | : 'ethSentToCoinbase' in transactionDetail 909 | ? transactionDetail.ethSentToCoinbase 910 | : BigNumber.from(0) 911 | const totalMinerReward = 912 | 'total_miner_reward' in transactionDetail 913 | ? BigNumber.from(transactionDetail.total_miner_reward) 914 | : 'coinbaseDiff' in transactionDetail 915 | ? BigNumber.from(transactionDetail.coinbaseDiff) 916 | : BigNumber.from(0) 917 | const priorityFeeReceivedByMiner = totalMinerReward.sub(ethSentToCoinbase) 918 | return { 919 | gasUsed: acc.gasUsed + gasUsed, 920 | gasFeesPaidBySearcher: acc.gasFeesPaidBySearcher.add(baseFee.mul(gasUsed).add(priorityFeeReceivedByMiner)), 921 | priorityFeesReceivedByMiner: acc.priorityFeesReceivedByMiner.add(priorityFeeReceivedByMiner), 922 | ethSentToCoinbase: acc.ethSentToCoinbase.add(ethSentToCoinbase) 923 | } 924 | }, 925 | { 926 | gasUsed: 0, 927 | gasFeesPaidBySearcher: BigNumber.from(0), 928 | priorityFeesReceivedByMiner: BigNumber.from(0), 929 | ethSentToCoinbase: BigNumber.from(0) 930 | } 931 | ) 932 | const effectiveGasPriceToSearcher = 933 | bundleGasPricing.gasUsed > 0 934 | ? bundleGasPricing.ethSentToCoinbase.add(bundleGasPricing.gasFeesPaidBySearcher).div(bundleGasPricing.gasUsed) 935 | : BigNumber.from(0) 936 | const effectivePriorityFeeToMiner = 937 | bundleGasPricing.gasUsed > 0 938 | ? bundleGasPricing.ethSentToCoinbase.add(bundleGasPricing.priorityFeesReceivedByMiner).div(bundleGasPricing.gasUsed) 939 | : BigNumber.from(0) 940 | return { 941 | ...bundleGasPricing, 942 | txCount: bundleTransactions.length, 943 | effectiveGasPriceToSearcher, 944 | effectivePriorityFeeToMiner 945 | } 946 | } 947 | 948 | /** 949 | * Gets information about a conflicting bundle. Useful if you're competing 950 | * for well-known MEV and want to know why your bundle didn't land. 951 | * @param targetSignedBundledTransactions signed bundle 952 | * @param targetBlockNumber block in which bundle should be included 953 | * @returns conflict and gas price details 954 | */ 955 | public async getConflictingBundle( 956 | targetSignedBundledTransactions: Array, 957 | targetBlockNumber: number 958 | ): Promise { 959 | const baseFee = (await this.genericProvider.getBlock(targetBlockNumber)).baseFeePerGas || BigNumber.from(0) 960 | const conflictDetails = await this.getConflictingBundleWithoutGasPricing(targetSignedBundledTransactions, targetBlockNumber) 961 | return { 962 | ...conflictDetails, 963 | targetBundleGasPricing: this.calculateBundlePricing(conflictDetails.initialSimulation.results, baseFee), 964 | conflictingBundleGasPricing: 965 | conflictDetails.conflictingBundle.length > 0 ? this.calculateBundlePricing(conflictDetails.conflictingBundle, baseFee) : undefined 966 | } 967 | } 968 | 969 | /** 970 | * Gets information about a conflicting bundle. Useful if you're competing 971 | * for well-known MEV and want to know why your bundle didn't land. 972 | * @param targetSignedBundledTransactions signed bundle 973 | * @param targetBlockNumber block in which bundle should be included 974 | * @returns conflict details 975 | */ 976 | public async getConflictingBundleWithoutGasPricing( 977 | targetSignedBundledTransactions: Array, 978 | targetBlockNumber: number 979 | ): Promise { 980 | const [initialSimulation, competingBundles] = await Promise.all([ 981 | this.simulate(targetSignedBundledTransactions, targetBlockNumber, targetBlockNumber - 1), 982 | this.fetchBlocksApi(targetBlockNumber) 983 | ]) 984 | if (competingBundles.latest_block_number <= targetBlockNumber) { 985 | throw new Error('Blocks-api has not processed target block') 986 | } 987 | if ('error' in initialSimulation || initialSimulation.firstRevert !== undefined) { 988 | throw new Error('Target bundle errors at top of block') 989 | } 990 | const blockDetails = competingBundles.blocks[0] 991 | if (blockDetails === undefined) { 992 | return { 993 | initialSimulation, 994 | conflictType: FlashbotsBundleConflictType.NoBundlesInBlock, 995 | conflictingBundle: [] 996 | } 997 | } 998 | const bundleTransactions = blockDetails.transactions 999 | const bundleCount = bundleTransactions[bundleTransactions.length - 1].bundle_index + 1 1000 | const signedPriorBundleTransactions = [] 1001 | for (let currentBundleId = 0; currentBundleId < bundleCount; currentBundleId++) { 1002 | const currentBundleTransactions = bundleTransactions.filter((bundleTransaction) => bundleTransaction.bundle_index === currentBundleId) 1003 | const currentBundleSignedTxs = await Promise.all( 1004 | currentBundleTransactions.map(async (competitorBundleBlocksApiTx) => { 1005 | const tx = await this.genericProvider.getTransaction(competitorBundleBlocksApiTx.transaction_hash) 1006 | if (tx.raw !== undefined) { 1007 | return tx.raw 1008 | } 1009 | if (tx.v !== undefined && tx.r !== undefined && tx.s !== undefined) { 1010 | if (tx.type === 2) { 1011 | delete tx.gasPrice 1012 | } 1013 | return serialize(tx, { 1014 | v: tx.v, 1015 | r: tx.r, 1016 | s: tx.s 1017 | }) 1018 | } 1019 | throw new Error('Could not get raw tx') 1020 | }) 1021 | ) 1022 | signedPriorBundleTransactions.push(...currentBundleSignedTxs) 1023 | const competitorAndTargetBundleSimulation = await this.simulate( 1024 | [...signedPriorBundleTransactions, ...targetSignedBundledTransactions], 1025 | targetBlockNumber, 1026 | targetBlockNumber - 1 1027 | ) 1028 | 1029 | if ('error' in competitorAndTargetBundleSimulation) { 1030 | if (competitorAndTargetBundleSimulation.error.message.startsWith('err: nonce too low:')) { 1031 | return { 1032 | conflictType: FlashbotsBundleConflictType.NonceCollision, 1033 | initialSimulation, 1034 | conflictingBundle: currentBundleTransactions 1035 | } 1036 | } 1037 | throw new Error('Simulation error') 1038 | } 1039 | const targetSimulation = competitorAndTargetBundleSimulation.results.slice(-targetSignedBundledTransactions.length) 1040 | for (let j = 0; j < targetSimulation.length; j++) { 1041 | const targetSimulationTx = targetSimulation[j] 1042 | const initialSimulationTx = initialSimulation.results[j] 1043 | if ('error' in targetSimulationTx || 'error' in initialSimulationTx) { 1044 | if ('error' in targetSimulationTx != 'error' in initialSimulationTx) { 1045 | return { 1046 | conflictType: FlashbotsBundleConflictType.Error, 1047 | initialSimulation, 1048 | conflictingBundle: currentBundleTransactions 1049 | } 1050 | } 1051 | continue 1052 | } 1053 | if (targetSimulationTx.ethSentToCoinbase != initialSimulationTx.ethSentToCoinbase) { 1054 | return { 1055 | conflictType: FlashbotsBundleConflictType.CoinbasePayment, 1056 | initialSimulation, 1057 | conflictingBundle: currentBundleTransactions 1058 | } 1059 | } 1060 | if (targetSimulationTx.gasUsed != initialSimulation.results[j].gasUsed) { 1061 | return { 1062 | conflictType: FlashbotsBundleConflictType.GasUsed, 1063 | initialSimulation, 1064 | conflictingBundle: currentBundleTransactions 1065 | } 1066 | } 1067 | } 1068 | } 1069 | return { 1070 | conflictType: FlashbotsBundleConflictType.NoConflict, 1071 | initialSimulation, 1072 | conflictingBundle: [] 1073 | } 1074 | } 1075 | 1076 | /** Gets information about a block from Flashbots blocks API. */ 1077 | public async fetchBlocksApi(blockNumber: number): Promise { 1078 | return fetchJson(`https://blocks.flashbots.net/v1/blocks?block_number=${blockNumber}`) 1079 | } 1080 | 1081 | private async request(request: string) { 1082 | const connectionInfo = { ...this.connectionInfo } 1083 | connectionInfo.headers = { 1084 | 'X-Flashbots-Signature': `${await this.authSigner.getAddress()}:${await this.authSigner.signMessage(id(request))}`, 1085 | ...this.connectionInfo.headers 1086 | } 1087 | return fetchJson(connectionInfo, request) 1088 | } 1089 | 1090 | private async fetchReceipts(bundledTransactions: Array): Promise> { 1091 | return Promise.all(bundledTransactions.map((bundledTransaction) => this.genericProvider.getTransactionReceipt(bundledTransaction.hash))) 1092 | } 1093 | 1094 | private prepareRelayRequest( 1095 | method: 1096 | | 'eth_callBundle' 1097 | | 'eth_cancelBundle' 1098 | | 'eth_sendBundle' 1099 | | 'eth_sendPrivateTransaction' 1100 | | 'eth_cancelPrivateTransaction' 1101 | | 'flashbots_getUserStats' 1102 | | 'flashbots_getBundleStats' 1103 | | 'flashbots_getUserStatsV2' 1104 | | 'flashbots_getBundleStatsV2', 1105 | params: RpcParams 1106 | ) { 1107 | return { 1108 | method: method, 1109 | params: params, 1110 | id: this._nextId++, 1111 | jsonrpc: '2.0' 1112 | } 1113 | } 1114 | } 1115 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | "lib": [ "es2018" ], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./build", /* Redirect output structure to the directory. */ 16 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | "composite": true, /* Enable project compilation */ 18 | "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "src/" /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | --------------------------------------------------------------------------------