├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── dependabot.yml ├── .gitignore ├── .mocharc.json ├── LICENSE ├── README.md ├── bin ├── dev ├── dev.cmd ├── run └── run.cmd ├── install.sh ├── package.json ├── src ├── commands │ ├── bundleCache.ts │ ├── cancelPrivateTransaction.ts │ ├── getBundleStats.ts │ ├── getConflictingBundle.ts │ ├── getUserStats.ts │ ├── sendBundle.ts │ ├── sendPrivateTransaction.ts │ ├── simulateBundle.ts │ └── uuid.ts ├── index.ts └── lib │ ├── constants.ts │ ├── error.ts │ └── flashbots.ts ├── test ├── commands │ ├── cacheTx.test.ts │ ├── getBundleStats.test.ts │ ├── getConflictingBundle.test.ts │ ├── getUserStats.test.ts │ ├── hello │ │ ├── index.test.ts │ │ └── world.test.ts │ ├── sendBundle.test.ts │ ├── simulateBundle.test.ts │ └── uuid.test.ts ├── helpers │ └── init.js └── tsconfig.json ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | orbs: 3 | node: circleci/node@5.0 4 | jobs: 5 | install-node-example: 6 | docker: 7 | - image: 'cimg/base:stable' 8 | steps: 9 | - checkout 10 | - node/install: 11 | install-yarn: true 12 | node-version: '16.13' 13 | - run: yarn install && yarn build 14 | workflows: 15 | test_my_app: 16 | jobs: 17 | - install-node-example 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "oclif", 4 | "oclif-typescript" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | versioning-strategy: increase 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | labels: 9 | - "dependencies" 10 | open-pull-requests-limit: 100 11 | pull-request-branch-name: 12 | separator: "-" 13 | ignore: 14 | - dependency-name: "fs-extra" 15 | - dependency-name: "*" 16 | update-types: ["version-update:semver-major"] 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /lib 6 | /package-lock.json 7 | /tmp 8 | node_modules 9 | oclif.manifest.json 10 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "test/helpers/init.js", 4 | "ts-node/register" 5 | ], 6 | "watch-extensions": [ 7 | "ts" 8 | ], 9 | "recursive": true, 10 | "reporter": "spec", 11 | "timeout": 60000 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Salesforce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flashbots-cli 2 | 3 | Flashbots CLI Tool 4 | 5 | [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io) 6 | [![Version](https://img.shields.io/npm/v/flashbots-cli.svg)](https://npmjs.org/package/flashbots-cli) 7 | [![CircleCI](https://circleci.com/gh/zeroXbrock/flashbots-cli/tree/main.svg?style=shield)](https://circleci.com/gh/zeroXbrock/flashbots-cli/tree/main) 8 | [![Downloads/week](https://img.shields.io/npm/dw/oclif-hello-world.svg)](https://npmjs.org/package/flashbots-cli) 9 | [![License](https://img.shields.io/npm/l/flashbots-cli.svg)](https://github.com/zeroXbrock/flashbots-cli/blob/main/package.json) 10 | 11 | `flashbots-cli` is a quick-and-easy solution for interacting with the flashbots relay. All methods implemented in the [`ethers-provider-flashbots-bundle`](https://github.com/flashbots/ethers-provider-flashbots-bundle) project are implemented here (and more coming soon). 12 | 13 | ## install 14 | 15 | ### via npm (global install) 16 | 17 | ```sh 18 | npm install -g flashbots-cli@latest 19 | flashbots --help 20 | ``` 21 | 22 | ### via npx (one-off usage) 23 | 24 | ```sh 25 | npx flashbots-cli@latest --help 26 | 27 | # if you wanna get fancy 28 | alias fb='npx flashbots-cli@latest' 29 | fb --help 30 | ``` 31 | 32 | ### install from source (hacker mode) 33 | 34 | ```sh 35 | # if you use zsh 36 | curl https://raw.githubusercontent.com/zeroXbrock/flashbots-cli/main/install.sh | /bin/zsh 37 | 38 | # if you use bash 39 | curl https://raw.githubusercontent.com/zeroXbrock/flashbots-cli/main/install.sh | /bin/bash 40 | ``` 41 | 42 | ## use 43 | 44 | **It is strongly recommended** to set `FB_AUTH_SIGNER` in your environment to a private key you control. It's used to sign bundles, authenticate requests, and earn reputation with Flashbots. Earning reputation can put you in the high priority queue for bundle submissions. 45 | 46 | Here's an example using the hardhat 0 account: 47 | 48 | ```sh 49 | # set in terminal per-session 50 | export FB_AUTH_SIGNER=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 51 | 52 | # recommended: set permanently in your terminal's profile (I use zsh) 53 | echo 'export FB_AUTH_SIGNER=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' >> ~/.zshrc 54 | ``` 55 | 56 | ```sh 57 | # flashbots 58 | # flashbots help [command] 59 | # flashbots --help 60 | 61 | flashbots help 62 | ``` 63 | 64 | Result: 65 | 66 | ```txt 67 | Flashbots CLI tool. 68 | 69 | VERSION 70 | flashbots-cli/x.y.z linux-x64 node-v16.1.0 71 | 72 | USAGE 73 | $ flashbots [COMMAND] 74 | 75 | COMMANDS 76 | bundleCache Add txs to a bundle one at a time. 77 | getBundleStats Get info about a bundle. 78 | getConflictingBundle Get information about competing bundles. 79 | getUserStats Get Flashbots reputation data for your account. 80 | help Display help for flashbots. 81 | sendBundle Send a bundle to the Flashbots relay. 82 | simulateBundle Simulate a bundle. 83 | uuid Generate a random UUID. 84 | ``` 85 | 86 | Get command usage 87 | 88 | ```sh 89 | # flashbots help [COMMAND] 90 | # flashbots --help [COMMAND] 91 | # flashbots [COMMAND] --help 92 | 93 | flashbots simulateBundle --help 94 | ``` 95 | 96 | Result 97 | 98 | ```txt 99 | Simulate a bundle. 100 | 101 | USAGE 102 | $ flashbots simulateBundle [BUNDLE_TXS] [-b ] [--state-block-tag 103 | ] [--block-timestamp ] 104 | 105 | ARGUMENTS 106 | BUNDLE_TXS JSON string-encoded array of raw signed transactions (0x-prefixed) 107 | 108 | FLAGS 109 | -b, --target-block= Block to target for bundle submission 110 | --block-timestamp= Timestamp to execute simulation against 111 | --state-block-tag= Block tag which specifies block state to execute 112 | simulation against 113 | 114 | DESCRIPTION 115 | Simulate a bundle. 116 | 117 | EXAMPLES 118 | # Simulate a bundle in the current block 119 | 120 | $ flashbots simulateBundle '["0x02f8d37c496de...", \ 121 | "0x02f8d38a7bf47..."]' 122 | 123 | # Simulate a bundle against a specific timestamp 124 | 125 | $ flashbots simulateBundle '["0x02f8d37c496de...", \ 126 | "0x02f8d38a7bf47..."]' --block-timestamp 1652859017 127 | 128 | # Simulate a bundle against a block tag 129 | 130 | $ flashbots simulateBundle '["0x02f8d37c496de...", \ 131 | "0x02f8d38a7bf47..."]' --state-block-tag latest 132 | ``` 133 | 134 | ## examples 135 | 136 | ### `bundleCache` 137 | 138 | `bundleCache` allows you to add raw signed transactions to a queue one at a time. This is commonly used for whitehat scenarios where you want to build a bundle using transactions that are signed by a private key you don't have. 139 | 140 | **Add first transaction to bundle:** 141 | 142 | ```sh 143 | # raw signed tx #1 144 | tx1='0xf87009851010b8720083010d8894eaa314eb4cc5a16458b17a94e759252f4fda9ea4808b6c617a792077616e6b65721ba026314be2b42cda8015133376447bd6ab93bc08d367fe7d1b57f270256f5e5e04a00595467676caa58aacc709a4c1c987cf2f18f10a527797c4730e23a320d034ce' 145 | 146 | # flashbots-cli includes a `uuid` command to generate UUIDs for this 147 | bundleId=$(flashbots uuid) 148 | echo $bundleId 149 | cd8578e0-7c7a-40ce-b60d-190f084a87ee 150 | 151 | flashbots bundleCache $bundleId $tx1 152 | ``` 153 | 154 | Result: 155 | 156 | ```json 157 | {"nonce":9,"gasPrice":{"type":"BigNumber","hex":"0x1010b87200"},"gasLimit":{"type":"BigNumber","hex":"0x010d88"},"to":"0xeAa314Eb4Cc5A16458B17a94e759252f4FDA9eA4","value":{"type":"BigNumber","hex":"0x00"},"data":"0x6c617a792077616e6b6572","chainId":0,"v":27,"r":"0x26314be2b42cda8015133376447bd6ab93bc08d367fe7d1b57f270256f5e5e04","s":"0x0595467676caa58aacc709a4c1c987cf2f18f10a527797c4730e23a320d034ce","from":"0x13babe07e11ff6f2276B97735131c867D793615b","hash":"0x84809aa44167e877b4b95f74b4b7f022ce04114be0c37f39ca858c438abbfb75","type":null,"confirmations":0} 158 | Cached tx in bundle cd8578e0-7c7a-40ce-b60d-190f084a87ee 159 | ``` 160 | 161 | **Add second tranaction to bundle:** 162 | 163 | ```sh 164 | # raw signed tx #2 165 | tx2='0xf8700a851010b8720083010d8894eaa314eb4cc5a16458b17a94e759252f4fda9ea4808b6c617a792077616e6b65721ba0ef7bdce42d5e3c8ef515d1afaafecd952ac1f2adc97f1c3a76807a0b9ac21784a03a5589f444cf5e5b09456ab599544ac6a6e831fd6a1e9c0b596db0a3cc085edb' 166 | 167 | flashbots bundleCache $bundleId $tx2 168 | ``` 169 | 170 | Result: 171 | 172 | ```json 173 | {"nonce":10,"gasPrice":{"type":"BigNumber","hex":"0x1010b87200"},"gasLimit":{"type":"BigNumber","hex":"0x010d88"},"to":"0xeAa314Eb4Cc5A16458B17a94e759252f4FDA9eA4","value":{"type":"BigNumber","hex":"0x00"},"data":"0x6c617a792077616e6b6572","chainId":0,"v":27,"r":"0xef7bdce42d5e3c8ef515d1afaafecd952ac1f2adc97f1c3a76807a0b9ac21784","s":"0x3a5589f444cf5e5b09456ab599544ac6a6e831fd6a1e9c0b596db0a3cc085edb","from":"0x13babe07e11ff6f2276B97735131c867D793615b","hash":"0x5346ddf789eb33c2e0a07bacbc53cf3f0766608b50cefcea64684586a650b6aa","type":null,"confirmations":0} 174 | Cached tx in bundle cd8578e0-7c7a-40ce-b60d-190f084a87ee 175 | ``` 176 | 177 | **Get entire cached bundle:** 178 | 179 | ```sh 180 | flashbots bundleCache $bundleId 181 | ``` 182 | 183 | Result: 184 | 185 | ```txt 186 | {"bundleId":"cd8578e0-7c7a-40ce-b60d-190f084a87ee","rawTxs":["0xf8700a851010b8720083010d8894eaa314eb4cc5a16458b17a94e759252f4fda9ea4808b6c617a792077616e6b65721ba0ef7bdce42d5e3c8ef515d1afaafecd952ac1f2adc97f1c3a76807a0b9ac21784a03a5589f444cf5e5b09456ab599544ac6a6e831fd6a1e9c0b596db0a3cc085edb","0xf87009851010b8720083010d8894eaa314eb4cc5a16458b17a94e759252f4fda9ea4808b6c617a792077616e6b65721ba026314be2b42cda8015133376447bd6ab93bc08d367fe7d1b57f270256f5e5e04a00595467676caa58aacc709a4c1c987cf2f18f10a527797c4730e23a320d034ce"]} 187 | ``` 188 | 189 | ### `getBundleStats` 190 | 191 | `getBundleStats` retrieves information about a bundle's inclusion from simulation through submission. 192 | 193 | ```sh 194 | bundleHash='0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234' 195 | targetBlock='13509887' 196 | 197 | flashbots getBundleStats $bundleHash $targetBlock 198 | ``` 199 | 200 | Result: 201 | 202 | ```json 203 | { 204 | "isSimulated": true, 205 | "isSentToMiners": true, 206 | "isHighPriority": true, 207 | "simulatedAt": "2021-10-29T04:00:50.526Z", 208 | "submittedAt": "2021-10-29T04:00:50.472Z", 209 | "sentToMinersAt": "2021-10-29T04:00:50.546Z" 210 | } 211 | ``` 212 | 213 | ### `getConflictingBundle` 214 | 215 | `getConflictingBundle` retrieves information about competing bundle(s). 216 | 217 | ```sh 218 | signedTxs='["0xf87009851010b8720083010d8894eaa314eb4cc5a16458b17a94e759252f4fda9ea4808b6c617a792077616e6b65721ba026314be2b42cda8015133376447bd6ab93bc08d367fe7d1b57f270256f5e5e04a00595467676caa58aacc709a4c1c987cf2f18f10a527797c4730e23a320d034ce", "0xf8700a851010b8720083010d8894eaa314eb4cc5a16458b17a94e759252f4fda9ea4808b6c617a792077616e6b65721ba0ef7bdce42d5e3c8ef515d1afaafecd952ac1f2adc97f1c3a76807a0b9ac21784a03a5589f444cf5e5b09456ab599544ac6a6e831fd6a1e9c0b596db0a3cc085edb"]' 219 | targetBlock=13140328 220 | 221 | flashbots getConflictingBundle $signedTxs $targetBlock 222 | ``` 223 | 224 | Result: 225 | 226 | ```json 227 | { 228 | "conflictType": FlashbotsBundleConflictType.NonceCollision, 229 | "initialSimulation": { 230 | "totalGasUsed": 205860, 231 | "bundleHash": "0x1720ea33d96dca026dddd5689f8cad21966988348ced04e9054a0dca5d60f1d4", 232 | "coinbaseDiff": BigNumber(0x0176750858d000), 233 | }, 234 | "results": [...] 235 | }, 236 | "targetBundleGasPricing": { 237 | "gasUsed": 205860, 238 | "txCount": 1, 239 | "gasFeesPaidBySearcher": BigNumber(0x0176750858d000), 240 | "priorityFeesReceivedByMiner": BigNumber(0x52efd8d80dbc24), 241 | "ethSentToCoinbase": BigNumber.from(0x00), 242 | "effectiveGasPriceToSearcher": BigNumber(0x77359400), 243 | "effectivePriorityFeeToMiner": BigNumber(0x1a6734f601) 244 | }, 245 | "conflictingBundleGasPricing": { 246 | "gasUsed": 396462, 247 | "txCount": 3, 248 | "gasFeesPaidBySearcher": BigNumber(0xc4c3c97ce1bff8b4), 249 | "priorityFeesReceivedByMiner": BigNumber(0xc4213e4d7ad82006), 250 | "ethSentToCoinbase": BigNumber(0xc4c2663d3b804731), 251 | "effectiveGasPriceToSearcher": BigNumber(0x410ce509aa1e), 252 | "effectivePriorityFeeToMiner": BigNumber(0x40f2069f201d) 253 | }, 254 | "conflictingBundle": [ 255 | { 256 | "transaction_hash": "0x23a33038289dda1b6e722835d2b9388cb41d96d085c19ca6b71bb3e9697e6692", 257 | "tx_index": 0, 258 | "bundle_type": "flashbots", 259 | "bundle_index": 0, 260 | "block_number": 13140328, 261 | "eoa_address": "0x38563699560e4512c7574C8cC5Cf89fd43923BcA", 262 | "to_address": "0x000000000035B5e5ad9019092C665357240f594e", 263 | "gas_used": 100893, 264 | "gas_price": "0", 265 | "coinbase_transfer": "0", 266 | "total_miner_reward": "0" 267 | }, 268 | ... 269 | ] 270 | } 271 | ``` 272 | 273 | ### `getUserStats` 274 | 275 | `getUserStats` retrieves Flashbots reputation information about an account you control. 276 | 277 | ```sh 278 | # using FB_AUTH_SIGNER from environment 279 | flashbots getUserStats 280 | ``` 281 | 282 | Optionally override your environment key: 283 | 284 | ```sh 285 | # Get user stats about hardhat account 0 286 | authSignerKey=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 287 | 288 | flashbots getUserStats $authSignerKey 289 | ``` 290 | 291 | Result: 292 | 293 | ```txt 294 | {"is_high_priority":true,"all_time_miner_payments":"271021997564860145835","all_time_gas_simulated":"28405149806","last_7d_miner_payments":"18728986348907349342","last_7d_gas_simulated":"4798626009","last_1d_miner_payments":"5360668420920718","last_1d_gas_simulated":"4917660"} 295 | ``` 296 | 297 | ### `sendBundle` 298 | 299 | `sendBundle` sends a bundle of raw signed transactions to flashbots. By default, we target the next available block (`getBlockNumber() + 1`) and sign the bundle with the key at the environment variable FB_AUTH_SIGNER. If this variable is not set, a random key will be generated and used to send the bundle. 300 | 301 | See `flashbots help sendBundle` for optional settings. 302 | 303 | ```sh 304 | signedTxs='["0xf87009851010b8720083010d8894eaa314eb4cc5a16458b17a94e759252f4fda9ea4808b6c617a792077616e6b65721ba026314be2b42cda8015133376447bd6ab93bc08d367fe7d1b57f270256f5e5e04a00595467676caa58aacc709a4c1c987cf2f18f10a527797c4730e23a320d034ce", "0xf8700a851010b8720083010d8894eaa314eb4cc5a16458b17a94e759252f4fda9ea4808b6c617a792077616e6b65721ba0ef7bdce42d5e3c8ef515d1afaafecd952ac1f2adc97f1c3a76807a0b9ac21784a03a5589f444cf5e5b09456ab599544ac6a6e831fd6a1e9c0b596db0a3cc085edb"]' 305 | 306 | flashbots sendBundle $signedTxs 307 | ``` 308 | 309 | Result: 310 | 311 | ```json 312 | Targeting block 14802559 for bundle submission. 313 | 314 | {"bundleTransactions":[{"signedTransaction":"0xf87009851010b8720083010d8894eaa314eb4cc5a16458b17a94e759252f4fda9ea4808b6c617a792077616e6b65721ba026314be2b42cda8015133376447bd6ab93bc08d367fe7d1b57f270256f5e5e04a00595467676caa58aacc709a4c1c987cf2f18f10a527797c4730e23a320d034ce","hash":"0x84809aa44167e877b4b95f74b4b7f022ce04114be0c37f39ca858c438abbfb75","account":"0x13babe07e11ff6f2276B97735131c867D793615b","nonce":9},{"signedTransaction":"0xf8700a851010b8720083010d8894eaa314eb4cc5a16458b17a94e759252f4fda9ea4808b6c617a792077616e6b65721ba0ef7bdce42d5e3c8ef515d1afaafecd952ac1f2adc97f1c3a76807a0b9ac21784a03a5589f444cf5e5b09456ab599544ac6a6e831fd6a1e9c0b596db0a3cc085edb","hash":"0x5346ddf789eb33c2e0a07bacbc53cf3f0766608b50cefcea64684586a650b6aa","account":"0x13babe07e11ff6f2276B97735131c867D793615b","nonce":10}],"bundleHash":"0x57b0f5e51b924a9d88a59b2d4f0c4fd2c26c80a130d982c9bc4e97d3001ff054"} 315 | ``` 316 | 317 | ### `simulateBundle` 318 | 319 | `simulateBundle` simulates a bundle and prints information about the bundle's inclusion viability. 320 | 321 | ```sh 322 | signedTxs='["0xf87009851010b8720083010d8894eaa314eb4cc5a16458b17a94e759252f4fda9ea4808b6c617a792077616e6b65721ba026314be2b42cda8015133376447bd6ab93bc08d367fe7d1b57f270256f5e5e04a00595467676caa58aacc709a4c1c987cf2f18f10a527797c4730e23a320d034ce", "0xf8700a851010b8720083010d8894eaa314eb4cc5a16458b17a94e759252f4fda9ea4808b6c617a792077616e6b65721ba0ef7bdce42d5e3c8ef515d1afaafecd952ac1f2adc97f1c3a76807a0b9ac21784a03a5589f444cf5e5b09456ab599544ac6a6e831fd6a1e9c0b596db0a3cc085edb"]' 323 | 324 | flashbots simulateBundle $signedTxs 325 | ``` 326 | 327 | Result: 328 | 329 | ```json 330 | Targeting block 14802588 for bundle simulation. 331 | {"bundleHash":"0x57b0f5e51b924a9d88a59b2d4f0c4fd2c26c80a130d982c9bc4e97d3001ff054","coinbaseDiff":{"type":"BigNumber","hex":"0x05db54a1c878e0"},"results":[{"coinbaseDiff":"824265710779504","ethSentToCoinbase":"0","fromAddress":"0x13babe07e11ff6f2276B97735131c867D793615b","gasFees":"824265710779504","gasPrice":"38924523554","gasUsed":21176,"toAddress":"0xeAa314Eb4Cc5A16458B17a94e759252f4FDA9eA4","txHash":"0x84809aa44167e877b4b95f74b4b7f022ce04114be0c37f39ca858c438abbfb75","value":"0x"},{"coinbaseDiff":"824265710779504","ethSentToCoinbase":"0","fromAddress":"0x13babe07e11ff6f2276B97735131c867D793615b","gasFees":"824265710779504","gasPrice":"38924523554","gasUsed":21176,"toAddress":"0xeAa314Eb4Cc5A16458B17a94e759252f4FDA9eA4","txHash":"0x5346ddf789eb33c2e0a07bacbc53cf3f0766608b50cefcea64684586a650b6aa","value":"0x"}],"totalGasUsed":42352} 332 | ``` 333 | 334 | ### `sendPrivateTransaction` 335 | 336 | `sendPrivateTransaction` sends a single signed transaction to Flashbots. Flashbots will try to get the transaction included for a maximum of 25 blocks. 337 | 338 | ```sh 339 | signedTx=0xf87080853010b872008a010d8894eaa314db4cc5a16458b17a94e759252a4fda9ea4808b6c6a7a792f77616e6b65721ba05bf41e534c768499a3a84567e9acca5fdec73164d54b0cd21e4cdd94a11af29ba0591f06662aa673b645378774ee4a665fdf8a7ec8e0418ec0ac0d24f915bc8526 340 | flashbots sendPrivateTransaction $signedTx 341 | ``` 342 | 343 | Result: 344 | 345 | ```json 346 | {"transaction":{"signedTransaction":"0xf87080853010b872008a010d8894eaa314db4cc5a16458b17a94e759252a4fda9ea4808b6c6a7a792f77616e6b65721ba05bf41e534c768499a3a84567e9acca5fdec73164d54b0cd21e4cdd94a11af29ba0591f06662aa673b645378774ee4a665fdf8a7ec8e0418ec0ac0d24f915bc8526","hash":"0x1030b9b9c685b0c63543ce2b14d286b3dcc82852a9d3404e3f1aaaf5108bb73c","account":"0x0000000000092DD1482686a414A08e64fF1463C2","nonce":0}} 347 | ``` 348 | 349 | Send with options: 350 | 351 | ```sh 352 | signedTx=0xf87080853010b872008a010d8894eaa314db4cc5a16458b17a94e759252a4fda9ea4808b6c6a7a792f77616e6b65721ba05bf41e534c768499a3a84567e9acca5fdec73164d54b0cd21e4cdd94a11af29ba0591f06662aa673b645378774ee4a665fdf8a7ec8e0418ec0ac0d24f915bc8526 353 | flashbots sendPrivateTransaction $signedTx --max-block-number 15161558 --simulation-timestamp 1658080039 354 | ``` 355 | 356 | Result: 357 | 358 | ```json 359 | {"transaction":{"signedTransaction":"0xf87080853010b872008a010d8894eaa314db4cc5a16458b17a94e759252a4fda9ea4808b6c6a7a792f77616e6b65721ba05bf41e534c768499a3a84567e9acca5fdec73164d54b0cd21e4cdd94a11af29ba0591f06662aa673b645378774ee4a665fdf8a7ec8e0418ec0ac0d24f915bc8526","hash":"0x1030b9b9c685b0c63543ce2b14d286b3dcc82852a9d3404e3f1aaaf5108bb73c","account":"0x0000000000092DD1482686a414A08e64fF1463C2","nonce":0}} 360 | Waiting for inclusion... 361 | Private transaction successfully mined. 362 | ``` 363 | 364 | ### `cancelPrivateTransaction` 365 | 366 | `cancelPrivateTransaction` cancels a pending private transaction. 367 | 368 | ```sh 369 | txHash=0x1030b9b9c685b0c63543ce2b14d286b3dcc82852a9d3404e3f1aaaf5108bb73c 370 | flashbots cancelPrivateTransaction $txHash 371 | ``` 372 | 373 | Result: 374 | 375 | ```txt 376 | Transaction cancelled (0x1030b9b9c685b0c63543ce2b14d286b3dcc82852a9d3404e3f1aaaf5108bb73c) 377 | ``` 378 | 379 | ## development 380 | 381 | ### run local build 382 | 383 | ```sh 384 | ./bin/dev help 385 | ``` 386 | 387 | ## features 388 | 389 | - [x] `bundleCache` 390 | - [x] `getBundleStats` 391 | - [x] `getConflictingBundle` 392 | - [x] `getUserStats` 393 | - [x] `sendBundle` 394 | - [x] `simulateBundle` 395 | - [x] `uuid` 396 | - [x] `sendPrivateTransaction` 397 | - [ ] fast mode option 398 | - [x] `cancelPrivateTransaction` 399 | - [ ] Goerli support 400 | - [ ] auto-complete 401 | - [ ] sign, simulate, & send unsigned txs 402 | - [ ] bundle status polling (equivalent of `(await sendBundle()).wait()`) 403 | 404 | Got a big 🧠 idea? Drop it in the [issues](https://github.com/zeroXbrock/flashbots-cli/issues) and tag the maintainer! Or if you can code it yourself, drop a PR! Contributions welcome 🤝 405 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core') 4 | 5 | const path = require('path') 6 | const project = path.join(__dirname, '..', 'tsconfig.json') 7 | 8 | // In dev mode -> use ts-node and dev plugins 9 | process.env.NODE_ENV = 'development' 10 | 11 | require('ts-node').register({project}) 12 | 13 | // In dev mode, always show stack traces 14 | oclif.settings.debug = true; 15 | 16 | // Start the CLI 17 | oclif.run().then(oclif.flush).catch(oclif.Errors.handle) 18 | -------------------------------------------------------------------------------- /bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\dev" %* -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core') 4 | 5 | oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle')) 6 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | installLocation='/usr/local/lib/flashbots-cli' 2 | 3 | rm -rf ./flashbots-cli 2>/dev/null 4 | sudo rm -rf $installLocation 2>/dev/null 5 | git clone https://github.com/zeroXbrock/flashbots-cli.git 6 | cd flashbots-cli 7 | yarn install && yarn build 8 | cd .. 9 | 10 | sudo mv ./flashbots-cli $installLocation 11 | 12 | aliasStmt="alias flashbots=\"${installLocation}/bin/run\"" 13 | 14 | if [ -f ~/.zshrc ]; then 15 | # assume Zsh 16 | echo $aliasStmt >> ~/.zshrc 17 | sourceStmt='source ~/.zshrc' 18 | echo "Reload your terminal or run 'source ~/.zshrc' to load flashbots-cli" 19 | elif [ -f ~/.bashrc ]; then 20 | # assume Bash 21 | echo $aliasStmt >> ~/.bashrc 22 | echo "Reload your terminal or run 'source ~/.bashrc' to load flashbots-cli" 23 | else 24 | # assume something else 25 | echo "Couldn't find your terminal profile (.zshrc or .bashrc)." 26 | echo "Add the following line to your profile file:" 27 | echo 28 | echo $aliasStmt 29 | fi 30 | 31 | echo "Run 'flashbots' to see a list of commands." 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flashbots-cli", 3 | "version": "1.0.2", 4 | "description": "Flashbots CLI tool.", 5 | "author": "zeroXbrock", 6 | "bin": { 7 | "flashbots": "./bin/run" 8 | }, 9 | "homepage": "https://github.com/zeroXbrock/flashbots-cli", 10 | "license": "MIT", 11 | "main": "dist/index.js", 12 | "repository": "zeroXbrock/flashbots-cli", 13 | "files": [ 14 | "/bin", 15 | "/dist", 16 | "/npm-shrinkwrap.json", 17 | "/oclif.manifest.json" 18 | ], 19 | "dependencies": { 20 | "@flashbots/ethers-provider-bundle": "0.5.0", 21 | "@oclif/core": "^1", 22 | "@oclif/plugin-help": "^5", 23 | "@oclif/plugin-not-found": "^2.3.1", 24 | "@oclif/plugin-warn-if-update-available": "^2.0.4", 25 | "axios": "^0.27.2", 26 | "ethers": "^5.6.6" 27 | }, 28 | "devDependencies": { 29 | "@oclif/test": "^2", 30 | "@types/chai": "^4", 31 | "@types/mocha": "^9.0.0", 32 | "@types/node": "^16.9.4", 33 | "bump": "^0.2.5", 34 | "chai": "^4", 35 | "eslint": "^7.32.0", 36 | "eslint-config-oclif": "^4", 37 | "eslint-config-oclif-typescript": "^1.0.2", 38 | "globby": "^11", 39 | "mocha": "^9", 40 | "oclif": "^3", 41 | "shx": "^0.3.3", 42 | "ts-node": "^10.2.1", 43 | "tslib": "^2.3.1", 44 | "typescript": "^4.4.3" 45 | }, 46 | "oclif": { 47 | "bin": "flashbots", 48 | "dirname": "flashbots", 49 | "commands": "./dist/commands", 50 | "plugins": [ 51 | "@oclif/plugin-help", 52 | "@oclif/plugin-not-found", 53 | "@oclif/plugin-warn-if-update-available" 54 | ] 55 | }, 56 | "scripts": { 57 | "prepublishOnly": "npx bump", 58 | "build": "shx rm -rf dist && tsc -b", 59 | "lint": "eslint . --ext .ts --config .eslintrc", 60 | "postpack": "shx rm -f oclif.manifest.json", 61 | "posttest": "yarn lint", 62 | "prepack": "yarn build && oclif manifest && oclif readme", 63 | "test": "mocha --forbid-only \"test/**/*.test.ts\"", 64 | "version": "oclif readme && git add README.md" 65 | }, 66 | "engines": { 67 | "node": ">=12.0.0" 68 | }, 69 | "bugs": "https://github.com/zeroXbrock/flashbots-cli/issues", 70 | "keywords": [ 71 | "flashbots", 72 | "oclif", 73 | "cli" 74 | ], 75 | "types": "dist/index.d.ts" 76 | } -------------------------------------------------------------------------------- /src/commands/bundleCache.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Command } from '@oclif/core' 3 | import { providers } from 'ethers' 4 | 5 | // lib 6 | import { FLASHBOTS_PROTECT_URL } from '../lib/constants' 7 | import { handleGenericError } from '../lib/error' 8 | 9 | export default class BundleCache extends Command { 10 | static description = `Add txs to a bundle one at a time. 11 | Specify [BUNDLE_ID] AND [RAW_TX] to add a new transaction to a bundle. 12 | To get the current cached bundle, only set [BUNDLE_ID].` 13 | 14 | static examples = [ 15 | `# Add a transaction to a bundle 16 | <%= config.bin %> <%= command.id %> 5b479f88-01ca 0x02f8d30182024...`, 17 | `# Get transactions in a bundle 18 | <%= config.bin %> <%= command.id %> 5b479f88-01ca`, 19 | ] 20 | 21 | static args = [ 22 | {name: 'bundle_id', description: 'Unique ID to identify your bundle (UUIDv4 recommended)', required: true}, 23 | {name: 'raw_tx', description: 'Raw signed transaction (0x-prefixed hex data)'} 24 | ] 25 | 26 | async catch(e: any) { 27 | handleGenericError(e, this) 28 | } 29 | 30 | public async run(): Promise { 31 | const {args} = await this.parse(BundleCache) 32 | 33 | if (args.raw_tx) { 34 | // add tx to bundle 35 | const provider = new providers.JsonRpcProvider(`${FLASHBOTS_PROTECT_URL}?bundle=${args.bundle_id}`) 36 | const res = await provider.sendTransaction(args.raw_tx) 37 | this.log(JSON.stringify(res)) 38 | this.log(`Cached tx in bundle ${args.bundle_id}`) 39 | } else { 40 | // get bundle 41 | const res = await axios.get(`${FLASHBOTS_PROTECT_URL}/bundle?id=${args.bundle_id}`) 42 | this.log(JSON.stringify(res.data)) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/commands/cancelPrivateTransaction.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/core' 2 | 3 | // lib 4 | import { handleGenericError } from '../lib/error' 5 | import { getFlashbotsProvider } from '../lib/flashbots' 6 | 7 | export default class CancelPrivateTransaction extends Command { 8 | static description = 'Cancel a pending private transaction.' 9 | 10 | static examples = [ 11 | `# Cancel a pending private transaction 12 | <%= config.bin %> <%= command.id %> 0x1030b9b9c685b0c63543ce2b14d286b3dcc82852a9d3404e3f1aaaf5108bb73c`, 13 | ] 14 | 15 | static args = [ 16 | {name: 'tx_hash', description: "Transaction hash of private tx to be cancelled", required: true}, 17 | ] 18 | 19 | async catch(e: any) { 20 | handleGenericError(e, this) 21 | } 22 | 23 | public async run(): Promise { 24 | const {args} = await this.parse(CancelPrivateTransaction) 25 | 26 | const flashbotsProvider = await getFlashbotsProvider() 27 | try { 28 | await flashbotsProvider.cancelPrivateTransaction(args.tx_hash) 29 | this.log(`Transaction cancelled (${args.tx_hash})`) 30 | } catch (e) { 31 | if (e instanceof Error) { 32 | if (e.message.includes("tx not found")) { 33 | console.error("tx not found") 34 | } else { 35 | console.error(e.message) 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/getBundleStats.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/core' 2 | 3 | // lib 4 | import { handleGenericError } from '../lib/error' 5 | import { getFlashbotsProvider } from '../lib/flashbots' 6 | 7 | export default class GetBundleStats extends Command { 8 | static description = 'Get info about a bundle.' 9 | 10 | static examples = [ 11 | `# Get bundle stats for block 14798035 12 | <%= config.bin %> <%= command.id %> 0x47b1f5ea1b924acd88459b2e4f0ccfd2326ca0a134b982c1bc4a97e9002ff051 14798035`, 13 | ] 14 | 15 | static args = [ 16 | {name: 'bundle_hash', description: "Bundle hash returned by simulateBundle", required: true}, 17 | {name: 'target_block', description: "Block in which the bundle was targeted/executed", required: true}, 18 | ] 19 | 20 | async catch(e: any) { 21 | handleGenericError(e, this) 22 | } 23 | 24 | public async run(): Promise { 25 | const {args} = await this.parse(GetBundleStats) 26 | 27 | const flashbotsProvider = await getFlashbotsProvider() 28 | const res = await flashbotsProvider.getBundleStats(args.bundle_hash, args.target_block) 29 | this.log(JSON.stringify(res)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commands/getConflictingBundle.ts: -------------------------------------------------------------------------------- 1 | import { Command, Flags } from '@oclif/core' 2 | 3 | // lib 4 | import { handleGenericError } from '../lib/error' 5 | import { getFlashbotsProvider } from '../lib/flashbots' 6 | 7 | export default class GetConflictingBundle extends Command { 8 | static description = 'Get information about competing bundles.' 9 | 10 | static examples = [ 11 | `# Get conflicting bundles (if any) 12 | <%= config.bin %> <%= command.id %> '["0x02f8d37c496de...", "0x02f8d38a7bf47..."]' 14797973`, 13 | `# Get conflicting bundles (if any) and skip gas pricing calculations 14 | <%= config.bin %> <%= command.id %> -g '["0x02f8d37c496de...", "0x02f8d38a7bf47..."]' 14797973`, 15 | ] 16 | 17 | static flags = { 18 | 'ignore-gas-pricing': Flags.boolean({char: 'g', description: "Gets conflicting bundle(s) without calculating gas pricing data"}), 19 | } 20 | 21 | static args = [ 22 | {name: 'bundle_txs', description: 'JSON string-encoded array of raw signed transactions (0x-prefixed)', required: true}, 23 | {name: 'target_block', description: 'Block in which the conflicting bundle was mined', required: true}, 24 | ] 25 | 26 | async catch(e: any) { 27 | handleGenericError(e, this) 28 | } 29 | 30 | public async run(): Promise { 31 | const {args, flags} = await this.parse(GetConflictingBundle) 32 | const flashbotsProvider = await getFlashbotsProvider() 33 | let res: any; 34 | if (flags['ignore-gas-pricing']) { 35 | res = flashbotsProvider.getConflictingBundleWithoutGasPricing(JSON.parse(args.bundle_txs), parseInt(args.target_block)) 36 | } else { 37 | res = flashbotsProvider.getConflictingBundle(JSON.parse(args.bundle_txs), parseInt(args.target_block)) 38 | } 39 | this.log(JSON.stringify(res)) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/getUserStats.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/core' 2 | 3 | // lib 4 | import { handleGenericError } from '../lib/error' 5 | import { getFlashbotsProvider } from '../lib/flashbots' 6 | 7 | export default class GetUserStats extends Command { 8 | static description = `Get Flashbots reputation data for your account. 9 | You may optionally specify a private key, otherwise the environment variable FB_AUTH_SIGNER will be used. 10 | Note: your private key is not sent over the web. It is only used to sign a message which proves you own the account.` 11 | 12 | static examples = [ 13 | `# Get user stats about hardhat account 0 14 | <%= config.bin %> <%= command.id %> 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`, 15 | ] 16 | 17 | static args = [{name: 'signer_key', description: "Private key used to sign bundles", required: false}] 18 | 19 | async catch(e: any) { 20 | handleGenericError(e, this) 21 | } 22 | 23 | public async run(): Promise { 24 | const {args} = await this.parse(GetUserStats) 25 | const flashbotsProvider = await getFlashbotsProvider(args.signer_key) 26 | const res = await flashbotsProvider.getUserStats() 27 | this.log(JSON.stringify(res)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/sendBundle.ts: -------------------------------------------------------------------------------- 1 | import { Command, Flags } from '@oclif/core' 2 | 3 | // lib 4 | import { handleGenericError } from '../lib/error' 5 | import { getFlashbotsProvider, getStandardProvider } from '../lib/flashbots' 6 | 7 | export default class SendBundle extends Command { 8 | static description = `Send a bundle to the Flashbots relay. 9 | All flags are optional.` 10 | static exampleBundle = `'["0x02f8d37c496de...", "0x02f8d38a7bf47..."]'` 11 | static examples = [ 12 | `# Send a bundle in the next available block 13 | <%= config.bin %> <%= command.id %> ${this.exampleBundle}`, 14 | `# Send a bundle with specific timestamp(s) 15 | <%= config.bin %> <%= command.id %> ${this.exampleBundle} --min-timestamp 1652856500 --max-timestamp 1652856700`, 16 | `# Send a bundle with reverting transactions 17 | <%= config.bin %> <%= command.id %> ${this.exampleBundle} --reverting-tx 0x60929406276058bc757bc84a576857e6a6118566690fd3604d4f1cdb5ebd89d3 --reverting-tx 0x548efb5c5de7df6d76f8aca4391def510fa575c81dd588d2ff4e8fc5a7c4eb79`, 18 | ] 19 | 20 | static flags = { 21 | 'target-block': Flags.integer({char: 'b', description: 'Block to target for bundle submission (default latest+1)'}), 22 | 'min-timestamp': Flags.integer({description: 'Minimum timestamp at which this bundle can be included'}), 23 | 'max-timestamp': Flags.integer({description: 'Maximum timestamp at which this bundle can be included'}), 24 | 'reverting-tx': Flags.string({description: 'Tx hash that is allowed to revert, can be set multiple times', multiple: true}), 25 | } 26 | 27 | static args = [{name: 'bundle_txs', description: 'JSON string-encoded array of raw signed transactions (0x-prefixed)', required: true}] 28 | 29 | async catch(e: any) { 30 | handleGenericError(e, this) 31 | } 32 | 33 | public async run(): Promise { 34 | const {args, flags} = await this.parse(SendBundle) 35 | 36 | const flashbotsProvider = await getFlashbotsProvider() 37 | 38 | let targetBlock: number; 39 | if (flags['target-block']) { 40 | targetBlock = flags['target-block'] 41 | } else { 42 | const provider = getStandardProvider() 43 | targetBlock = (await provider.getBlockNumber()) + 1 44 | } 45 | this.log(`Targeting block ${targetBlock} for bundle submission.`) 46 | 47 | const optionalArgs = { 48 | minTimestamp: flags['min-timestamp'], 49 | maxTimestamp: flags['max-timestamp'], 50 | revertingTxHashes: flags['reverting-tx'], 51 | } 52 | 53 | const res: any = await flashbotsProvider.sendRawBundle(JSON.parse(args.bundle_txs), targetBlock, optionalArgs) 54 | if (res['error']) { 55 | this.error(JSON.stringify(res.error)) 56 | } 57 | this.log(JSON.stringify(res)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/commands/sendPrivateTransaction.ts: -------------------------------------------------------------------------------- 1 | import { FlashbotsBundleRawTransaction, FlashbotsPrivateTransactionResponse, FlashbotsTransactionResolution, RelayResponseError } from '@flashbots/ethers-provider-bundle' 2 | import { Command, Flags } from '@oclif/core' 3 | 4 | // lib 5 | import { handleGenericError } from '../lib/error' 6 | import { getFlashbotsProvider } from '../lib/flashbots' 7 | 8 | export default class SendPrivateTransaction extends Command { 9 | static description = 'Send a private transaction to Flashbots miners.' 10 | 11 | static examples = [ 12 | `signedTx=0xf87080853010b8720083010d8894eaa314eb4cc5a16458ba7a94e759252f4fda9ea4808b6c6a7a792077616e6b657f1ba05bf41e534c768493a3a84567e9acca5fdec73164d54b0cd21e4cdd94a11af29ba0591f06662aa673b645378774ee4a665fdf8a7ec8e0418ec0ac0d24f915bc8516`, 13 | `# Send a private transaction for the next 25 blocks 14 | <%= config.bin %> <%= command.id %> $signedTx`, 15 | `# Send a private transaction up to block 15161558 (note: 25 blocks is still the max duration) 16 | <%= config.bin %> <%= command.id %> $signedTx --max-block-number 15161558`, 17 | `# Send a private transaction with a custom simulation timestamp 18 | <%= config.bin %> <%= command.id %> $signedTx --simulation-timestamp 1658080039`, 19 | ] 20 | 21 | static args = [ 22 | {name: 'signed_transaction', description: "Raw signed transaction to send", required: true}, 23 | ] 24 | 25 | static flags = { 26 | 'max-block-number': Flags.integer({char: 'b', description: 'Highest block number to allow transaction to be included'}), 27 | 'simulation-timestamp': Flags.integer({char: 't', description: 'Timestamp to use for transaction simulation'}), 28 | } 29 | 30 | async catch(e: any) { 31 | handleGenericError(e, this) 32 | } 33 | 34 | public async run(): Promise { 35 | const {args, flags} = await this.parse(SendPrivateTransaction) 36 | 37 | const flashbotsProvider = await getFlashbotsProvider() 38 | const tx: FlashbotsBundleRawTransaction = { 39 | signedTransaction: args.signed_transaction, 40 | } 41 | 42 | const res = await flashbotsProvider.sendPrivateTransaction(tx, { 43 | maxBlockNumber: flags['max-block-number'], 44 | simulationTimestamp: flags['simulation-timestamp'], 45 | }) 46 | const resErr = res as RelayResponseError 47 | const resSuccess = res as FlashbotsPrivateTransactionResponse 48 | if (resErr.error) { 49 | console.error(resErr) 50 | } else { 51 | const simRes = await resSuccess.simulate() 52 | if ('error' in simRes) { 53 | console.error(JSON.stringify(simRes)) 54 | } else { 55 | console.log(resSuccess.transaction) 56 | console.log("Waiting for inclusion...") 57 | const waitRes = await resSuccess.wait() 58 | if (waitRes === FlashbotsTransactionResolution.TransactionIncluded) { 59 | this.log("Private transaction successfully mined.") 60 | } else if (waitRes === FlashbotsTransactionResolution.TransactionDropped) { 61 | this.log("Private transaction was not mined and has been removed from the system.") 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/commands/simulateBundle.ts: -------------------------------------------------------------------------------- 1 | import { Command, Flags } from '@oclif/core' 2 | 3 | // lib 4 | import { handleGenericError } from '../lib/error' 5 | import { getFlashbotsProvider, getStandardProvider } from '../lib/flashbots' 6 | 7 | export default class SimulateBundle extends Command { 8 | static description = 'Simulate a bundle.' 9 | 10 | static examples = [ 11 | `# Simulate a bundle in the current block 12 | <%= config.bin %> <%= command.id %> '["0x02f8d37c496de...", "0x02f8d38a7bf47..."]'`, 13 | `# Simulate a bundle against a specific timestamp 14 | <%= config.bin %> <%= command.id %> '["0x02f8d37c496de...", "0x02f8d38a7bf47..."]' --block-timestamp 1652859017`, 15 | `# Simulate a bundle against a block tag 16 | <%= config.bin %> <%= command.id %> '["0x02f8d37c496de...", "0x02f8d38a7bf47..."]' --state-block-tag latest`, 17 | ] 18 | 19 | static flags = { 20 | 'target-block': Flags.integer({char: 'b', description: 'Block to target for bundle submission'}), 21 | 'state-block-tag': Flags.string({description: 'Block tag which specifies block state to execute simulation against'}), 22 | 'block-timestamp': Flags.integer({description: 'Timestamp to execute simulation against'}), 23 | } 24 | 25 | static args = [ 26 | {name: 'bundle_txs', description: "JSON string-encoded array of raw signed transactions (0x-prefixed)", required: true}, 27 | ] 28 | 29 | async catch(e: any) { 30 | handleGenericError(e, this) 31 | } 32 | 33 | public async run(): Promise { 34 | const {args, flags} = await this.parse(SimulateBundle) 35 | const flashbotsProvider = await getFlashbotsProvider() 36 | 37 | let targetBlock: number; 38 | if (flags['target-block']) { 39 | targetBlock = flags['target-block'] 40 | } else { 41 | const provider = getStandardProvider() 42 | targetBlock = await provider.getBlockNumber() 43 | } 44 | this.log(`Targeting block ${targetBlock} for bundle simulation.`) 45 | 46 | const res: any = await flashbotsProvider.simulate(JSON.parse(args.bundle_txs), targetBlock, flags['state-block-tag'], flags['block-timestamp']) 47 | if (res.error) { 48 | this.error(JSON.stringify(res.error)) 49 | } else { 50 | this.log(JSON.stringify(res)) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/commands/uuid.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/core' 2 | import { randomUUID } from 'crypto' 3 | 4 | // lib 5 | import { handleGenericError } from '../lib/error' 6 | 7 | export default class Uuid extends Command { 8 | static description = 'Generate a random UUID.' 9 | 10 | async catch(e: any) { 11 | handleGenericError(e, this) 12 | } 13 | 14 | public async run(): Promise { 15 | this.log(randomUUID()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {run} from '@oclif/core' 2 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const FLASHBOTS_PROTECT_URL="https://rpc.flashbots.net" 2 | -------------------------------------------------------------------------------- /src/lib/error.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@oclif/core"; 2 | 3 | export const handleGenericError = (e: any, context: Command) => { 4 | if (e.reason && e.value) 5 | context.error(`${e.reason}: ${e.value}`) 6 | else { 7 | context.error(e) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/flashbots.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider } from '@ethersproject/providers' 2 | import { FlashbotsBundleProvider } from '@flashbots/ethers-provider-bundle' 3 | import { Wallet, providers } from 'ethers' 4 | import { FLASHBOTS_PROTECT_URL } from './constants' 5 | 6 | const ENV_AUTH_SIGNER = process.env.FB_AUTH_SIGNER ? (() => { 7 | const wallet = new Wallet(process.env.FB_AUTH_SIGNER) 8 | console.log(`Auth Signer: ${wallet.address}`) 9 | return wallet 10 | })() : (() => { 11 | console.warn("FB_AUTH_SIGNER is not set in your environment. Using a random wallet.") 12 | return Wallet.createRandom() 13 | })() 14 | 15 | export const getStandardProvider = (): JsonRpcProvider => { 16 | return new providers.JsonRpcProvider({url: FLASHBOTS_PROTECT_URL}, 1) 17 | } 18 | 19 | export const getFlashbotsProvider = async (authSignerKeyOverride?: string): Promise => { 20 | const provider = getStandardProvider() 21 | const signer = authSignerKeyOverride ? new Wallet(authSignerKeyOverride) : ENV_AUTH_SIGNER 22 | return await FlashbotsBundleProvider.create(provider, signer) 23 | } 24 | -------------------------------------------------------------------------------- /test/commands/cacheTx.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('cacheTx', () => { 4 | test 5 | .stdout() 6 | .command(['cacheTx']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['cacheTx', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/getBundleStats.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('getBundleStats', () => { 4 | test 5 | .stdout() 6 | .command(['getBundleStats']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['getBundleStats', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/getConflictingBundle.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('getConflictingBundle', () => { 4 | test 5 | .stdout() 6 | .command(['getConflictingBundle']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['getConflictingBundle', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/getUserStats.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('getUserStats', () => { 4 | test 5 | .stdout() 6 | .command(['getUserStats']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['getUserStats', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/hello/index.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('hello', () => { 4 | test 5 | .stdout() 6 | .command(['hello', 'friend', '--from=oclif']) 7 | .it('runs hello cmd', ctx => { 8 | expect(ctx.stdout).to.contain('hello friend from oclif!') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/commands/hello/world.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('hello world', () => { 4 | test 5 | .stdout() 6 | .command(['hello:world']) 7 | .it('runs hello world cmd', ctx => { 8 | expect(ctx.stdout).to.contain('hello world!') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/commands/sendBundle.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('sendBundle', () => { 4 | test 5 | .stdout() 6 | .command(['sendBundle']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['sendBundle', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/simulateBundle.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('simulateBundle', () => { 4 | test 5 | .stdout() 6 | .command(['simulateBundle']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['simulateBundle', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/uuid.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('uuid', () => { 4 | test 5 | .stdout() 6 | .command(['uuid']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['uuid', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/helpers/init.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json') 3 | process.env.NODE_ENV = 'development' 4 | 5 | global.oclif = global.oclif || {} 6 | global.oclif.columns = 80 7 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [ 7 | {"path": ".."} 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "module": "commonjs", 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "strict": true, 9 | "target": "es2019" 10 | }, 11 | "include": [ 12 | "src/**/*" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------