├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── RELEASE.md ├── examples ├── advanced.rs ├── broadcast.rs └── simple.rs └── src ├── bundle.rs ├── jsonrpc.rs ├── lib.rs ├── middleware.rs ├── pending_bundle.rs ├── relay.rs ├── user.rs └── utils.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [master, main] 6 | pull_request: 7 | branches: [master, main] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout sources 14 | uses: actions/checkout@v3 15 | - name: Install toolchain 16 | uses: dtolnay/rust-toolchain@nightly 17 | with: 18 | components: rustfmt, clippy 19 | - uses: Swatinem/rust-cache@v2 20 | with: 21 | cache-on-failure: true 22 | 23 | - name: cargo fmt 24 | uses: actions-rs/cargo@v1 25 | with: 26 | command: fmt 27 | args: --all --check 28 | 29 | - name: cargo clippy 30 | uses: actions-rs/clippy-check@v1 31 | with: 32 | args: --all --all-features 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | test: 36 | name: test 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout sources 40 | uses: actions/checkout@v3 41 | - name: Install toolchain 42 | uses: dtolnay/rust-toolchain@nightly 43 | - uses: Swatinem/rust-cache@v2 44 | with: 45 | cache-on-failure: true 46 | 47 | - name: Install latest nextest release 48 | uses: taiki-e/install-action@nextest 49 | 50 | - name: Run tests 51 | run: cargo nextest run --all-features 52 | 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .idea 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | 9 | ## [Unreleased] 10 | 11 | ## [0.15.0] 12 | 13 | - Fix simulation for broadcaster middleware (#58) 14 | - Added support for `replacementUuid` field in `eth_sendBundle` (#63) 15 | - Made response's data field optional to support some builders other than Flashbots (#76) 16 | 17 | ## [0.14.0] 18 | 19 | ### Added 20 | 21 | - Added a broadcaster middleware (#54) 22 | 23 | ## [0.13.1] 24 | 25 | ### Fixed 26 | 27 | - Check the entire block to determine bundle inclusion. See #47 for more detail. 28 | 29 | ## [0.13.0] 30 | 31 | ### Added 32 | 33 | - Added `add_transaction` and `add_revertible_transaction` (#46) 34 | 35 | ### Changed 36 | 37 | - Bumped `ethers` to `2.0.0` and fixed middleware breaking changes 38 | 39 | ## [0.12.1] 40 | 41 | ### Fixed 42 | 43 | - Handle optional fields in the response of `flashbots_getBundleStatsV2`. 44 | 45 | ## [0.12.0] 46 | 47 | ### Added 48 | 49 | - It is now possible to use your own endpoint for simulating bundles. Bundles will be sent over JSON-RPC to the given endpoint. The JSON-RPC method used is `eth_callBundle`. See `Middleware::set_simulation_relay`. 50 | 51 | ### Changed 52 | 53 | - The library now uses the new `flashbots_getUserStatsV2` and `flashbots_getBundleStatsV2` endpoints. **If you need the old endpoints, pin your version to 0.11.0.** 54 | 55 | ## [0.11.0] 56 | 57 | ### Changed 58 | 59 | - Addd a `rustls` feature flag to use native TLS over OpenSSL 60 | - Bumped `thiserror` to `1.0.37` 61 | - Bumped `chrono` to `0.4.22` 62 | - Bumped `url` to `2.3.1` 63 | - Bumped `async-trait` to `0.1.58` 64 | - Bumped `ethers` to `1.0.0` 65 | 66 | ## [0.10.0] 67 | 68 | - Replaced `anyhow` with `eyre` 69 | - Updated to ethers `^0.17.0` 70 | 71 | ## [0.9.0] 72 | 73 | ### Changed 74 | 75 | - Updated to ethers `^0.13.0` 76 | 77 | ## [0.8.2] 78 | 79 | ### Added 80 | 81 | - `Debug` is now implemented for `UserStats` and `BundleStats` 82 | 83 | ### Changed 84 | 85 | - `PendingBundle::bundle_hash()` is now deprecated 86 | 87 | ## [0.8.1] 88 | 89 | A small patch to fix the documentation on [docs.rs](https://docs.rs). 90 | 91 | ## [0.8.0] 92 | 93 | ### Added 94 | 95 | - The basefee for a simulated block can now be specified. 96 | 97 | ## [0.7.0] 98 | 99 | ### Changed 100 | 101 | - Updated to ethers `^0.6.0` 102 | 103 | ## [0.6.0] 104 | 105 | ### Changed 106 | 107 | - Relaxed version requirement for Ethers - version requirement is now `^0.5.0`. 108 | - Disabled default Ethers features to allow for building on Windows (which lacks IPC support, see https://github.com/gakonst/ethers-rs/issues/393) 109 | 110 | ## [0.5.0] 111 | 112 | ### Added 113 | 114 | - Revert reason is now parsed, if there is any. 115 | 116 | ### Fixed 117 | 118 | - `value` on a simulated transaction was incorrectly assumed to be 119 | the amount of Ether sent in a transaction. It is now correctly 120 | parsed as `Bytes`, since it represents the return data (if any) 121 | of the transaction. 122 | 123 | ## [0.4.0] 124 | 125 | ### Changed 126 | 127 | - Parameters are now validated before bundles are sent to the relay. 128 | Check [the documentation](https://docs.rs/ethers-flashbots/0.4.0/ethers_flashbots/enum.FlashbotsMiddlewareError.html#variant.MissingParameters) for more information. 129 | - Bumped ethers to 0.5.1 130 | 131 | ### Added 132 | 133 | - Added a helper to get the effective gas price of bundles and 134 | bundle transactions (`SimulatedBundle::effective_gas_price` and `SimulatedTransaction::effective_gas_price`). 135 | 136 | ## [0.3.1] 137 | 138 | ### Added 139 | 140 | - Added a way to get stats about bundles (`FlashbotsMiddleware::get_bundle_stats`) 141 | - Added a getter for the bundle hash of a pending bundle 142 | - Added a way to get stats about your searcher identity (`FlashbotsMiddleware::get_user_stats`) 143 | 144 | ## [0.3.0] 145 | 146 | ### Fixes 147 | 148 | - If your bundle contains a transaction that deploys a contract, 149 | the `SimulatedTransaction` will now have a destination (`to`) of 150 | `None` to distinguish this from the zero address. 151 | 152 | ## [0.2.2] 153 | 154 | ### Fixes 155 | 156 | - Handle non-JSONRPC errors from the Flashbots relay 157 | 158 | ## [0.2.1] 159 | 160 | ### Fixes 161 | 162 | - Correctly serializes bundle requests 163 | 164 | ## [0.2.0] 165 | 166 | **NOTE**: This release is unfortunately broken, please update to [0.2.1] 167 | 168 | ### Added 169 | 170 | - You can now wait for bundle inclusions (see `PendingBundle`). 171 | - Added `BundleRequest::transaction_hashes` 172 | 173 | ## [0.1.3] 174 | 175 | **NOTE**: This release is unfortunately broken, please update to [0.2.1] 176 | 177 | ### Added 178 | 179 | - Added a revert reason to simulated transactions 180 | 181 | ## [0.1.2] 182 | 183 | **NOTE**: This release is unfortunately broken, please update to [0.2.1] 184 | 185 | ### Changed 186 | 187 | - Pinned ethers dependencies to specific versions 188 | 189 | ## [0.1.1] 190 | 191 | **NOTE**: This release is unfortunately broken, please update to [0.2.1] 192 | 193 | Initial release. 194 | 195 | 196 | 197 | [Unreleased]: https://github.com/onbjerg/ethers-flashbots/compare/0.15.0...HEAD 198 | [0.15.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.14.0...0.15.0 199 | [0.14.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.13.1...0.14.0 200 | [0.13.1]: https://github.com/onbjerg/ethers-flashbots/compare/0.13.0...0.13.1 201 | [0.13.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.12.1...0.13.0 202 | [0.12.1]: https://github.com/onbjerg/ethers-flashbots/compare/0.12.0...0.12.1 203 | [0.12.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.11.0...0.12.0 204 | [0.11.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.10.0...0.11.0 205 | [0.10.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.9.0...0.10.0 206 | [0.9.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.8.2...0.9.0 207 | [0.8.2]: https://github.com/onbjerg/ethers-flashbots/compare/0.8.1...0.8.2 208 | [0.8.1]: https://github.com/onbjerg/ethers-flashbots/compare/0.8.0...0.8.1 209 | [0.8.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.7.0...0.8.0 210 | [0.7.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.6.0...0.7.0 211 | [0.6.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.5.0...0.6.0 212 | [0.5.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.4.0...0.5.0 213 | [0.4.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.3.1...0.4.0 214 | [0.3.1]: https://github.com/onbjerg/ethers-flashbots/compare/0.3.0...0.3.1 215 | [0.3.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.2.2...0.3.0 216 | [0.2.2]: https://github.com/onbjerg/ethers-flashbots/compare/0.2.1...0.2.2 217 | [0.2.1]: https://github.com/onbjerg/ethers-flashbots/compare/0.2.0...0.2.1 218 | [0.2.0]: https://github.com/onbjerg/ethers-flashbots/compare/0.1.3...0.2.0 219 | [0.1.3]: https://github.com/onbjerg/ethers-flashbots/compare/0.1.2...0.1.3 220 | [0.1.2]: https://github.com/onbjerg/ethers-flashbots/compare/0.1.1...0.1.2 221 | [0.1.1]: https://github.com/onbjerg/ethers-flashbots/compare/97dc88a0...0.1.1 222 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethers-flashbots" 3 | version = "0.15.0" 4 | authors = ["Oliver Nordbjerg "] 5 | license = "MIT" 6 | edition = "2018" 7 | readme = "./README.md" 8 | documentation = "https://docs.rs/ethers-flashbots" 9 | repository = "https://github.com/onbjerg/ethers-flashbots" 10 | homepage = "https://github.com/onbjerg/ethers-flashbots" 11 | description = """ 12 | Flashbots middleware for ethers-rs 13 | """ 14 | 15 | [dependencies] 16 | # Error handling 17 | thiserror = { version = "1.0.37", default-features = false } 18 | 19 | # Serialization/deserialization 20 | serde = "1" 21 | serde_json = "1" 22 | chrono = { version = "0.4.22", features = ["default", "serde"] } 23 | uuid = "1.5" 24 | 25 | # HTTP 26 | url = { version = "2.3.1", default-features = false } 27 | reqwest = { version = "0.12", features = ["json"], default-features = false } 28 | 29 | # Async 30 | async-trait = { version = "0.1.58", default-features = false } 31 | futures-util = "0.3" 32 | futures-core = "0.3" 33 | pin-project = "1" 34 | 35 | # Ethers 36 | ethers = { version = "2.0.0", default-features = false } 37 | 38 | [dev-dependencies] 39 | tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread"] } 40 | ethers = { version = "2.0.0", default-features = false } 41 | eyre = "0.6" 42 | 43 | [features] 44 | default = ['openssl'] 45 | openssl = ['ethers/openssl', 'reqwest/default-tls'] 46 | rustls = ['ethers/rustls', 'reqwest/rustls-tls'] 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Ethers Flashbots 2 | 3 | > ### Deprecated 4 | > 5 | > This package is deprecated alongside ethers-rs, and will no longer be maintained. 6 | 7 | [![CI Status][ci-badge]][ci-url] 8 | [![Crates.io][crates-badge]][crates-url] 9 | [![Docs.rs][docs-badge]][docs-url] 10 | 11 | [ci-badge]: https://github.com/onbjerg/ethers-flashbots/actions/workflows/ci.yml/badge.svg 12 | [ci-url]: https://github.com/onbjerg/ethers-flashbots/actions/workflows/ci.yml 13 | [crates-badge]: https://img.shields.io/crates/v/ethers-flashbots.svg 14 | [crates-url]: https://crates.io/crates/ethers-flashbots 15 | [docs-badge]: https://docs.rs/ethers-flashbots/badge.svg 16 | [docs-url]: https://docs.rs/ethers-flashbots 17 | 18 | An [Ethers](https://github.com/gakonst/ethers-rs) middleware to send transactions as [Flashbots](https://docs.flashbots.net) bundles. 19 | 20 | ### Installation 21 | 22 | Add `ethers-flashbots` to your `Cargo.toml`. 23 | 24 | ```toml 25 | # This is the development version, for the stable release refer 26 | # to crates.io 27 | ethers-flashbots = { git = "https://github.com/onbjerg/ethers-flashbots" } 28 | ``` 29 | 30 | ### Usage 31 | 32 | ```rs 33 | use eyre::Result; 34 | use ethers::core::rand::thread_rng; 35 | use ethers::prelude::*; 36 | use ethers_flashbots::*; 37 | use std::convert::TryFrom; 38 | use url::Url; 39 | 40 | #[tokio::main] 41 | async fn main() -> Result<()> { 42 | // Connect to the network 43 | let provider = Provider::::try_from("https://mainnet.eth.aragon.network")?; 44 | 45 | // This is your searcher identity 46 | let bundle_signer = LocalWallet::new(&mut thread_rng()); 47 | // This signs transactions 48 | let wallet = LocalWallet::new(&mut thread_rng()); 49 | 50 | // Add signer and Flashbots middleware 51 | let client = SignerMiddleware::new( 52 | FlashbotsMiddleware::new( 53 | provider, 54 | Url::parse("https://relay.flashbots.net")?, 55 | bundle_signer, 56 | ), 57 | wallet, 58 | ); 59 | 60 | // Pay Vitalik using a Flashbots bundle! 61 | let tx = TransactionRequest::pay("vitalik.eth", 100); 62 | let pending_tx = client.send_transaction(tx, None).await?; 63 | 64 | // Get the receipt 65 | let receipt = pending_tx 66 | .await? 67 | .ok_or_else(|| eyre::format_err!("tx not included"))?; 68 | let tx = client.get_transaction(receipt.transaction_hash).await?; 69 | 70 | println!("Sent transaction: {}\n", serde_json::to_string(&tx)?); 71 | println!("Receipt: {}\n", serde_json::to_string(&receipt)?); 72 | 73 | Ok(()) 74 | } 75 | ``` 76 | 77 | See [the examples](./examples) for more in-depth examples. 78 | 79 | ### Contributing 80 | 81 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 82 | 83 | Please make sure that the tests and lints pass (`cargo test && cargo clippy -- -D clippy::all && cargo fmt -- --check`). 84 | 85 | Make sure to add your changes to the "Unreleased" section of the changelog. 86 | 87 | ### Donate 88 | 89 | If you would like to support me in my open source journey feel free to send me some Eth or tokens (anything accepted) at bjerg.eth. I appreciate it! 🙇 90 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | ## Release Instructions 2 | 3 | - Update version in `Cargo.toml` 4 | - Move "Unreleased" section in [CHANGELOG.md](./CHANGELOG.md) into a header with the new version, and add a link at the bottom of the CHANGELOG to compares that version to HEAD 5 | - Ensure tests and lints pass 6 | - Commit those changes and tag that commit with the version number (**no `v` prefix!**) 7 | - Push the changes and run `cargo publish` 8 | -------------------------------------------------------------------------------- /examples/advanced.rs: -------------------------------------------------------------------------------- 1 | use ethers::core::{rand::thread_rng, types::transaction::eip2718::TypedTransaction}; 2 | use ethers::prelude::*; 3 | use ethers_flashbots::*; 4 | use eyre::Result; 5 | use std::convert::TryFrom; 6 | use url::Url; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | // Connect to the network 11 | let provider = Provider::::try_from("https://mainnet.eth.aragon.network")?; 12 | 13 | // This is your searcher identity 14 | let bundle_signer = LocalWallet::new(&mut thread_rng()); 15 | // This signs transactions 16 | let wallet = LocalWallet::new(&mut thread_rng()); 17 | 18 | // Add signer and Flashbots middleware 19 | let client = SignerMiddleware::new( 20 | FlashbotsMiddleware::new( 21 | provider, 22 | Url::parse("https://relay.flashbots.net")?, 23 | bundle_signer, 24 | ), 25 | wallet, 26 | ); 27 | 28 | // get last block number 29 | let block_number = client.get_block_number().await?; 30 | 31 | // Build a custom bundle that pays 0x0000000000000000000000000000000000000000 32 | let tx = { 33 | let mut inner: TypedTransaction = TransactionRequest::pay(Address::zero(), 100).into(); 34 | client.fill_transaction(&mut inner, None).await?; 35 | inner 36 | }; 37 | let signature = client.signer().sign_transaction(&tx).await?; 38 | let bundle = BundleRequest::new() 39 | .push_transaction(tx.rlp_signed(&signature)) 40 | .set_block(block_number + 1) 41 | .set_simulation_block(block_number) 42 | .set_simulation_timestamp(0); 43 | 44 | // Simulate it 45 | let simulated_bundle = client.inner().simulate_bundle(&bundle).await?; 46 | println!("Simulated bundle: {:?}", simulated_bundle); 47 | 48 | // Send it 49 | let pending_bundle = client.inner().send_bundle(&bundle).await?; 50 | 51 | // You can also optionally wait to see if the bundle was included 52 | match pending_bundle.await { 53 | Ok(bundle_hash) => println!( 54 | "Bundle with hash {:?} was included in target block", 55 | bundle_hash 56 | ), 57 | Err(PendingBundleError::BundleNotIncluded) => { 58 | println!("Bundle was not included in target block.") 59 | } 60 | Err(e) => println!("An error occured: {}", e), 61 | } 62 | 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /examples/broadcast.rs: -------------------------------------------------------------------------------- 1 | use ethers::core::{rand::thread_rng, types::transaction::eip2718::TypedTransaction}; 2 | use ethers::prelude::*; 3 | use ethers_flashbots::*; 4 | use eyre::Result; 5 | use std::convert::TryFrom; 6 | use url::Url; 7 | 8 | // See https://www.mev.to/builders for up to date builder URLs 9 | static BUILDER_URLS: &[&str] = &[ 10 | "https://builder0x69.io", 11 | "https://rpc.beaverbuild.org", 12 | "https://relay.flashbots.net", 13 | "https://rsync-builder.xyz", 14 | "https://rpc.titanbuilder.xyz", 15 | "https://api.blocknative.com/v1/auction", 16 | "https://mev.api.blxrbdn.com", 17 | "https://eth-builder.com", 18 | "https://builder.gmbit.co/rpc", 19 | "https://buildai.net", 20 | "https://rpc.payload.de", 21 | "https://rpc.lightspeedbuilder.info", 22 | "https://rpc.nfactorial.xyz", 23 | "https://rpc.lokibuilder.xyz", 24 | ]; 25 | 26 | #[tokio::main] 27 | async fn main() -> Result<()> { 28 | // Connect to the network 29 | let provider = Provider::::try_from("https://mainnet.eth.aragon.network")?; 30 | 31 | // This is your searcher identity 32 | let bundle_signer = LocalWallet::new(&mut thread_rng()); 33 | // This signs transactions 34 | let wallet = LocalWallet::new(&mut thread_rng()); 35 | 36 | // Add signer and Flashbots middleware 37 | let client = SignerMiddleware::new( 38 | BroadcasterMiddleware::new( 39 | provider, 40 | BUILDER_URLS 41 | .iter() 42 | .map(|url| Url::parse(url).unwrap()) 43 | .collect(), 44 | Url::parse("https://relay.flashbots.net")?, 45 | bundle_signer, 46 | ), 47 | wallet, 48 | ); 49 | 50 | // get last block number 51 | let block_number = client.get_block_number().await?; 52 | 53 | // Build a custom bundle that pays 0x0000000000000000000000000000000000000000 54 | let tx = { 55 | let mut inner: TypedTransaction = TransactionRequest::pay(Address::zero(), 100).into(); 56 | client.fill_transaction(&mut inner, None).await?; 57 | inner 58 | }; 59 | let signature = client.signer().sign_transaction(&tx).await?; 60 | let bundle = BundleRequest::new() 61 | .push_transaction(tx.rlp_signed(&signature)) 62 | .set_block(block_number + 1) 63 | .set_simulation_block(block_number) 64 | .set_simulation_timestamp(0); 65 | 66 | // Send it 67 | let results = client.inner().send_bundle(&bundle).await?; 68 | 69 | // You can also optionally wait to see if the bundle was included 70 | for result in results { 71 | match result { 72 | Ok(pending_bundle) => match pending_bundle.await { 73 | Ok(bundle_hash) => println!( 74 | "Bundle with hash {:?} was included in target block", 75 | bundle_hash 76 | ), 77 | Err(PendingBundleError::BundleNotIncluded) => { 78 | println!("Bundle was not included in target block.") 79 | } 80 | Err(e) => println!("An error occured: {}", e), 81 | }, 82 | Err(e) => println!("An error occured: {}", e), 83 | } 84 | } 85 | 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use ethers::core::rand::thread_rng; 2 | use ethers::prelude::*; 3 | use ethers_flashbots::*; 4 | use eyre::Result; 5 | use std::convert::TryFrom; 6 | use url::Url; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | // Connect to the network 11 | let provider = Provider::::try_from("https://mainnet.eth.aragon.network")?; 12 | 13 | // This is your searcher identity 14 | let bundle_signer = LocalWallet::new(&mut thread_rng()); 15 | // This signs transactions 16 | let wallet = LocalWallet::new(&mut thread_rng()); 17 | 18 | // Add signer and Flashbots middleware 19 | let client = SignerMiddleware::new( 20 | FlashbotsMiddleware::new( 21 | provider, 22 | Url::parse("https://relay.flashbots.net")?, 23 | bundle_signer, 24 | ), 25 | wallet, 26 | ); 27 | 28 | // Pay Vitalik using a Flashbots bundle! 29 | let tx = TransactionRequest::pay("vitalik.eth", 100); 30 | let pending_tx = client.send_transaction(tx, None).await?; 31 | 32 | // Get the receipt 33 | let receipt = pending_tx 34 | .await? 35 | .ok_or_else(|| eyre::eyre!("tx not included"))?; 36 | let tx = client.get_transaction(receipt.transaction_hash).await?; 37 | 38 | println!("Sent transaction: {}\n", serde_json::to_string(&tx)?); 39 | println!("Receipt: {}\n", serde_json::to_string(&receipt)?); 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /src/bundle.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{deserialize_optional_h160, deserialize_u256, deserialize_u64}; 2 | use chrono::{DateTime, Utc}; 3 | use ethers::core::{ 4 | types::{transaction::response::Transaction, Address, Bytes, TxHash, H256, U256, U64}, 5 | utils::keccak256, 6 | }; 7 | use serde::{Deserialize, Serialize, Serializer}; 8 | use uuid::Uuid; 9 | 10 | /// A bundle hash. 11 | pub type BundleHash = H256; 12 | 13 | /// A transaction that can be added to a bundle. 14 | #[derive(Debug, Clone)] 15 | pub enum BundleTransaction { 16 | /// A pre-signed transaction. 17 | Signed(Box), 18 | /// An RLP encoded signed transaction. 19 | Raw(Bytes), 20 | } 21 | 22 | impl From for BundleTransaction { 23 | fn from(tx: Transaction) -> Self { 24 | Self::Signed(Box::new(tx)) 25 | } 26 | } 27 | 28 | impl From for BundleTransaction { 29 | fn from(tx: Bytes) -> Self { 30 | Self::Raw(tx) 31 | } 32 | } 33 | /// A bundle that can be submitted to a Flashbots relay. 34 | /// 35 | /// The bundle can include your own transactions and transactions from 36 | /// the mempool. 37 | /// 38 | /// Additionally, this bundle can be simulated through a relay if simulation 39 | /// parameters are provided using [`BundleRequest::set_simulation_block`] and 40 | /// [`BundleRequest::set_simulation_timestamp`]. 41 | /// 42 | /// Please note that some parameters are required, and submitting a bundle 43 | /// without them will get it rejected pre-flight. The required parameters 44 | /// include: 45 | /// 46 | /// - At least one transaction ([`BundleRequest::push_transaction`]) 47 | /// - A target block ([`BundleRequest::set_block`]) 48 | #[derive(Clone, Debug, Default, Serialize)] 49 | #[serde(rename_all = "camelCase")] 50 | pub struct BundleRequest { 51 | #[serde(rename = "txs")] 52 | #[serde(serialize_with = "serialize_txs")] 53 | transactions: Vec, 54 | #[serde(rename = "revertingTxHashes")] 55 | #[serde(skip_serializing_if = "Vec::is_empty")] 56 | revertible_transaction_hashes: Vec, 57 | 58 | #[serde(rename = "blockNumber")] 59 | #[serde(skip_serializing_if = "Option::is_none")] 60 | target_block: Option, 61 | 62 | #[serde(skip_serializing_if = "Option::is_none")] 63 | min_timestamp: Option, 64 | 65 | #[serde(skip_serializing_if = "Option::is_none")] 66 | max_timestamp: Option, 67 | 68 | #[serde(rename = "replacementUuid")] 69 | #[serde(skip_serializing_if = "Option::is_none")] 70 | #[serde(serialize_with = "serialize_uuid_as_string")] 71 | uuid: Option, 72 | 73 | #[serde(rename = "stateBlockNumber")] 74 | #[serde(skip_serializing_if = "Option::is_none")] 75 | simulation_block: Option, 76 | 77 | #[serde(skip_serializing_if = "Option::is_none")] 78 | #[serde(rename = "timestamp")] 79 | simulation_timestamp: Option, 80 | 81 | #[serde(skip_serializing_if = "Option::is_none")] 82 | #[serde(rename = "baseFee")] 83 | simulation_basefee: Option, 84 | } 85 | 86 | fn serialize_uuid_as_string(x: &Option, s: S) -> Result 87 | where 88 | S: Serializer, 89 | { 90 | // Don't need to handle None option here as handled by 91 | // #[serde(skip_serializing_if = "Option::is_none")] 92 | s.serialize_str(&x.unwrap().to_string()) 93 | } 94 | 95 | pub fn serialize_txs(txs: &[BundleTransaction], s: S) -> Result 96 | where 97 | S: Serializer, 98 | { 99 | let raw_txs: Vec = txs 100 | .iter() 101 | .map(|tx| match tx { 102 | BundleTransaction::Signed(inner) => inner.rlp(), 103 | BundleTransaction::Raw(inner) => inner.clone(), 104 | }) 105 | .collect(); 106 | 107 | raw_txs.serialize(s) 108 | } 109 | 110 | impl BundleRequest { 111 | /// Creates an empty bundle request. 112 | pub fn new() -> Self { 113 | Default::default() 114 | } 115 | 116 | /// Adds a transaction to the bundle request. 117 | /// 118 | /// Transactions added to the bundle can either be novel transactions, 119 | /// i.e. transactions that you have crafted, or they can be from 120 | /// one of the mempool APIs. 121 | pub fn push_transaction>(mut self, tx: T) -> Self { 122 | self.transactions.push(tx.into()); 123 | self 124 | } 125 | 126 | /// Adds a transaction to the bundle request. 127 | /// 128 | /// This function takes a mutable reference to `self` and adds the specified 129 | /// transaction to the `transactions` vector. The added transaction can either 130 | /// be a novel transaction that you have crafted, or it can be from one of the 131 | /// mempool APIs. 132 | pub fn add_transaction>(&mut self, tx: T) { 133 | self.transactions.push(tx.into()); 134 | } 135 | 136 | /// Adds a revertible transaction to the bundle request. 137 | /// 138 | /// This differs from [`BundleRequest::push_transaction`] in that the bundle will still be 139 | /// considered valid if the transaction reverts. 140 | pub fn push_revertible_transaction>(mut self, tx: T) -> Self { 141 | let tx = tx.into(); 142 | self.transactions.push(tx.clone()); 143 | 144 | let tx_hash: H256 = match tx { 145 | BundleTransaction::Signed(inner) => inner.hash(), 146 | BundleTransaction::Raw(inner) => keccak256(inner).into(), 147 | }; 148 | self.revertible_transaction_hashes.push(tx_hash); 149 | 150 | self 151 | } 152 | 153 | /// Adds a revertible transaction to the bundle request. 154 | /// 155 | /// This function takes a mutable reference to `self` and adds the specified 156 | /// revertible transaction to the `transactions` vector. The added transaction can either 157 | /// be a novel transaction that you have crafted, or it can be from one of the 158 | /// mempool APIs. Unlike the `push_transaction` method, the bundle will still be considered 159 | /// valid even if the added transaction reverts. 160 | pub fn add_revertible_transaction>(&mut self, tx: T) { 161 | let tx = tx.into(); 162 | self.transactions.push(tx.clone()); 163 | 164 | let tx_hash: H256 = match tx { 165 | BundleTransaction::Signed(inner) => inner.hash(), 166 | BundleTransaction::Raw(inner) => keccak256(inner).into(), 167 | }; 168 | self.revertible_transaction_hashes.push(tx_hash); 169 | } 170 | 171 | /// Get a reference to the transactions currently in the bundle request. 172 | pub fn transactions(&self) -> &Vec { 173 | &self.transactions 174 | } 175 | 176 | /// Get a list of transaction hashes in the bundle request. 177 | pub fn transaction_hashes(&self) -> Vec { 178 | self.transactions 179 | .iter() 180 | .map(|tx| match tx { 181 | BundleTransaction::Signed(inner) => keccak256(inner.rlp()).into(), 182 | BundleTransaction::Raw(inner) => keccak256(inner).into(), 183 | }) 184 | .collect() 185 | } 186 | 187 | /// Get a reference to the replacement uuid (if any). 188 | pub fn uuid(&self) -> &Option { 189 | &self.uuid 190 | } 191 | 192 | /// Set the replacement uuid of the bundle. 193 | /// This is used for bundle replacements or cancellations using eth_cancelBundle 194 | pub fn set_uuid(mut self, uuid: Uuid) -> Self { 195 | self.uuid = Some(uuid); 196 | self 197 | } 198 | 199 | /// Get the target block (if any). 200 | pub fn block(&self) -> Option { 201 | self.target_block 202 | } 203 | 204 | /// Set the target block of the bundle. 205 | pub fn set_block(mut self, block: U64) -> Self { 206 | self.target_block = Some(block); 207 | self 208 | } 209 | 210 | /// Get the block that determines the state for bundle simulation (if any). 211 | /// 212 | /// See [`eth_callBundle`][fb_call_bundle] in the Flashbots documentation 213 | /// for more information on bundle simulations. 214 | /// 215 | /// [fb_call_bundle]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_callbundle 216 | pub fn simulation_block(&self) -> Option { 217 | self.simulation_block 218 | } 219 | 220 | /// Set the block that determines the state for bundle simulation. 221 | pub fn set_simulation_block(mut self, block: U64) -> Self { 222 | self.simulation_block = Some(block); 223 | self 224 | } 225 | 226 | /// Get the UNIX timestamp used for bundle simulation (if any). 227 | /// 228 | /// See [`eth_callBundle`][fb_call_bundle] in the Flashbots documentation 229 | /// for more information on bundle simulations. 230 | /// 231 | /// [fb_call_bundle]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_callbundle 232 | pub fn simulation_timestamp(&self) -> Option { 233 | self.simulation_timestamp 234 | } 235 | 236 | /// Set the UNIX timestamp used for bundle simulation. 237 | pub fn set_simulation_timestamp(mut self, timestamp: u64) -> Self { 238 | self.simulation_timestamp = Some(timestamp); 239 | self 240 | } 241 | 242 | /// Get the base gas fee for bundle simulation (if any). 243 | /// 244 | /// See [`eth_callBundle`][fb_call_bundle] in the Flashbots documentation 245 | /// for more information on bundle simulations. 246 | /// 247 | /// [fb_call_bundle]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_callbundle 248 | pub fn simulation_basefee(&self) -> Option { 249 | self.simulation_basefee 250 | } 251 | 252 | /// Set the base gas fee for bundle simulation (if any). 253 | /// Optional: will default to a value chosen by the node if not specified. 254 | pub fn set_simulation_basefee(mut self, basefee: u64) -> Self { 255 | self.simulation_basefee = Some(basefee); 256 | self 257 | } 258 | 259 | /// Get the minimum timestamp for which this bundle is valid (if any), 260 | /// in seconds since the UNIX epoch. 261 | pub fn min_timestamp(&self) -> Option { 262 | self.min_timestamp 263 | } 264 | 265 | /// Set the minimum timestamp for which this bundle is valid (if any), 266 | /// in seconds since the UNIX epoch. 267 | pub fn set_min_timestamp(mut self, timestamp: u64) -> Self { 268 | self.min_timestamp = Some(timestamp); 269 | self 270 | } 271 | 272 | /// Get the maximum timestamp for which this bundle is valid (if any), 273 | /// in seconds since the UNIX epoch. 274 | pub fn max_timestamp(&self) -> Option { 275 | self.max_timestamp 276 | } 277 | 278 | /// Set the maximum timestamp for which this bundle is valid (if any), 279 | /// in seconds since the UNIX epoch. 280 | pub fn set_max_timestamp(mut self, timestamp: u64) -> Self { 281 | self.max_timestamp = Some(timestamp); 282 | self 283 | } 284 | } 285 | 286 | /// Details of a simulated transaction. 287 | /// 288 | /// Details for a transaction that has been simulated as part of 289 | /// a bundle. 290 | #[derive(Debug, Clone, Deserialize)] 291 | pub struct SimulatedTransaction { 292 | /// The transaction hash 293 | #[serde(rename = "txHash")] 294 | pub hash: H256, 295 | /// The difference in coinbase's balance due to this transaction. 296 | /// 297 | /// This includes tips and gas fees for this transaction. 298 | #[serde(rename = "coinbaseDiff")] 299 | #[serde(deserialize_with = "deserialize_u256")] 300 | pub coinbase_diff: U256, 301 | /// The amount of Eth sent to coinbase in this transaction. 302 | #[serde(rename = "ethSentToCoinbase")] 303 | #[serde(deserialize_with = "deserialize_u256")] 304 | pub coinbase_tip: U256, 305 | /// The gas price. 306 | #[serde(rename = "gasPrice")] 307 | #[serde(deserialize_with = "deserialize_u256")] 308 | pub gas_price: U256, 309 | /// The amount of gas used in this transaction. 310 | #[serde(rename = "gasUsed")] 311 | #[serde(deserialize_with = "deserialize_u256")] 312 | pub gas_used: U256, 313 | /// The total gas fees for this transaction. 314 | #[serde(rename = "gasFees")] 315 | #[serde(deserialize_with = "deserialize_u256")] 316 | pub gas_fees: U256, 317 | /// The origin of this transaction. 318 | #[serde(rename = "fromAddress")] 319 | pub from: Address, 320 | /// The destination of this transaction. 321 | /// 322 | /// If this is `None`, then the transaction was to a newly 323 | /// deployed contract. 324 | #[serde(rename = "toAddress")] 325 | #[serde(deserialize_with = "deserialize_optional_h160")] 326 | pub to: Option
, 327 | /// The return value of the transaction. 328 | pub value: Option, 329 | /// The reason this transaction failed (if it did). 330 | pub error: Option, 331 | /// The revert reason for this transaction, if available. 332 | pub revert: Option, 333 | } 334 | 335 | impl SimulatedTransaction { 336 | /// The effective gas price of the transaction, 337 | /// i.e. `coinbase_diff / gas_used`. 338 | pub fn effective_gas_price(&self) -> U256 { 339 | self.coinbase_diff / self.gas_used 340 | } 341 | } 342 | 343 | /// Details of a simulated bundle. 344 | /// 345 | /// The details of a bundle that has been simulated. 346 | #[derive(Debug, Clone, Deserialize)] 347 | pub struct SimulatedBundle { 348 | /// The bundle's hash. 349 | #[serde(rename = "bundleHash")] 350 | pub hash: BundleHash, 351 | /// The difference in coinbase's balance due to this bundle. 352 | /// 353 | /// This includes total gas fees and coinbase tips. 354 | #[serde(rename = "coinbaseDiff")] 355 | #[serde(deserialize_with = "deserialize_u256")] 356 | pub coinbase_diff: U256, 357 | /// The amount of Eth sent to coinbase in this bundle. 358 | #[serde(rename = "ethSentToCoinbase")] 359 | #[serde(deserialize_with = "deserialize_u256")] 360 | pub coinbase_tip: U256, 361 | /// The gas price of the bundle. 362 | #[serde(rename = "bundleGasPrice")] 363 | #[serde(deserialize_with = "deserialize_u256")] 364 | pub gas_price: U256, 365 | /// The total amount of gas used in this bundle. 366 | #[serde(rename = "totalGasUsed")] 367 | #[serde(deserialize_with = "deserialize_u256")] 368 | pub gas_used: U256, 369 | /// The total amount of gas fees in this bundle. 370 | #[serde(rename = "gasFees")] 371 | #[serde(deserialize_with = "deserialize_u256")] 372 | pub gas_fees: U256, 373 | /// The block at which this bundle was simulated. 374 | #[serde(rename = "stateBlockNumber")] 375 | #[serde(deserialize_with = "deserialize_u64")] 376 | pub simulation_block: U64, 377 | /// The simulated transactions in this bundle. 378 | #[serde(rename = "results")] 379 | pub transactions: Vec, 380 | } 381 | 382 | impl SimulatedBundle { 383 | /// The effective gas price of the transaction, 384 | /// i.e. `coinbase_diff / gas_used`. 385 | /// 386 | /// Note that this is also an approximation of the 387 | /// bundle's score. 388 | pub fn effective_gas_price(&self) -> U256 { 389 | self.coinbase_diff / self.gas_used 390 | } 391 | } 392 | 393 | /// Represents stats for a submitted bundle. 394 | /// 395 | /// See [Flashbots docs][fb_getbundlestats] for more information. 396 | /// 397 | /// [fb_getbundlestats]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint/#flashbots_getbundlestats 398 | #[derive(Deserialize, Debug)] 399 | #[serde(rename_all = "camelCase")] 400 | pub struct BundleStats { 401 | /// Whether the bundle is high priority. 402 | pub is_high_priority: bool, 403 | /// Whether the bundle was simulated. 404 | pub is_simulated: bool, 405 | /// When the bundle was simulated 406 | pub simulated_at: Option>, 407 | /// When the bundle was received by the bundle API. 408 | pub received_at: Option>, 409 | /// A list of times at which builders selected the bundle to be included in the target block. 410 | #[serde(default = "Vec::new")] 411 | pub considered_by_builders_at: Vec, 412 | /// A list of times at which builders sealed a block containing the bundle. 413 | #[serde(default = "Vec::new")] 414 | pub sealed_by_builders_at: Vec, 415 | } 416 | 417 | /// A builder log entry is a pairing of a builder's public key and a timestamp at which they 418 | /// performed some operation on a bundle. 419 | #[derive(Deserialize, Debug)] 420 | pub struct BuilderEntry { 421 | /// The public key of the builder. 422 | pub pubkey: Bytes, 423 | /// The timestamp of this log entry. 424 | pub timestamp: Option>, 425 | } 426 | 427 | #[cfg(test)] 428 | mod tests { 429 | use super::*; 430 | use std::str::FromStr; 431 | use uuid::uuid; 432 | 433 | #[test] 434 | fn bundle_serialize() { 435 | let bundle = BundleRequest::new() 436 | .push_transaction(Bytes::from(vec![0x1])) 437 | .push_revertible_transaction(Bytes::from(vec![0x2])) 438 | .set_block(2.into()) 439 | .set_min_timestamp(1000) 440 | .set_max_timestamp(2000) 441 | .set_simulation_timestamp(1000) 442 | .set_simulation_block(1.into()) 443 | .set_simulation_basefee(333333); 444 | 445 | assert_eq!( 446 | &serde_json::to_string(&bundle).unwrap(), 447 | r#"{"txs":["0x01","0x02"],"revertingTxHashes":["0xf2ee15ea639b73fa3db9b34a245bdfa015c260c598b211bf05a1ecc4b3e3b4f2"],"blockNumber":"0x2","minTimestamp":1000,"maxTimestamp":2000,"stateBlockNumber":"0x1","timestamp":1000,"baseFee":333333}"# 448 | ); 449 | } 450 | 451 | #[test] 452 | fn bundle_serialize_add_transactions() { 453 | let mut bundle = BundleRequest::new() 454 | .push_transaction(Bytes::from(vec![0x1])) 455 | .push_revertible_transaction(Bytes::from(vec![0x2])) 456 | .set_block(2.into()) 457 | .set_min_timestamp(1000) 458 | .set_max_timestamp(2000) 459 | .set_simulation_timestamp(1000) 460 | .set_simulation_block(1.into()) 461 | .set_simulation_basefee(333333) 462 | .set_uuid(uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8")); 463 | 464 | bundle.add_transaction(Bytes::from(vec![0x3])); 465 | bundle.add_revertible_transaction(Bytes::from(vec![0x4])); 466 | 467 | assert_eq!( 468 | &serde_json::to_string(&bundle).unwrap(), 469 | r#"{"txs":["0x01","0x02","0x03","0x04"],"revertingTxHashes":["0xf2ee15ea639b73fa3db9b34a245bdfa015c260c598b211bf05a1ecc4b3e3b4f2","0xf343681465b9efe82c933c3e8748c70cb8aa06539c361de20f72eac04e766393"],"blockNumber":"0x2","minTimestamp":1000,"maxTimestamp":2000,"replacementUuid":"67e55044-10b1-426f-9247-bb680e5fe0c8","stateBlockNumber":"0x1","timestamp":1000,"baseFee":333333}"# 470 | ); 471 | } 472 | 473 | #[test] 474 | fn simulated_bundle_deserialize() { 475 | let simulated_bundle: SimulatedBundle = serde_json::from_str( 476 | r#"{ 477 | "bundleGasPrice": "476190476193", 478 | "bundleHash": "0x73b1e258c7a42fd0230b2fd05529c5d4b6fcb66c227783f8bece8aeacdd1db2e", 479 | "coinbaseDiff": "20000000000126000", 480 | "ethSentToCoinbase": "20000000000000000", 481 | "gasFees": "126000", 482 | "results": [ 483 | { 484 | "coinbaseDiff": "10000000000063000", 485 | "ethSentToCoinbase": "10000000000000000", 486 | "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0", 487 | "gasFees": "63000", 488 | "gasPrice": "476190476193", 489 | "gasUsed": 21000, 490 | "toAddress": "0x73625f59CAdc5009Cb458B751b3E7b6b48C06f2C", 491 | "txHash": "0x669b4704a7d993a946cdd6e2f95233f308ce0c4649d2e04944e8299efcaa098a", 492 | "value": "0x", 493 | "error": "execution reverted" 494 | }, 495 | { 496 | "coinbaseDiff": "10000000000063000", 497 | "ethSentToCoinbase": "10000000000000000", 498 | "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0", 499 | "gasFees": "63000", 500 | "gasPrice": "476190476193", 501 | "gasUsed": 21000, 502 | "toAddress": "0x73625f59CAdc5009Cb458B751b3E7b6b48C06f2C", 503 | "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa", 504 | "value": "0x01" 505 | }, 506 | { 507 | "coinbaseDiff": "10000000000063000", 508 | "ethSentToCoinbase": "10000000000000000", 509 | "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0", 510 | "gasFees": "63000", 511 | "gasPrice": "476190476193", 512 | "gasUsed": 21000, 513 | "toAddress": "0x", 514 | "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa", 515 | "value": "0x" 516 | } 517 | ], 518 | "stateBlockNumber": 5221585, 519 | "totalGasUsed": 42000 520 | }"#, 521 | ) 522 | .unwrap(); 523 | 524 | assert_eq!( 525 | simulated_bundle.hash, 526 | H256::from_str("0x73b1e258c7a42fd0230b2fd05529c5d4b6fcb66c227783f8bece8aeacdd1db2e") 527 | .expect("could not deserialize hash") 528 | ); 529 | assert_eq!( 530 | simulated_bundle.coinbase_diff, 531 | U256::from(20000000000126000u64) 532 | ); 533 | assert_eq!( 534 | simulated_bundle.coinbase_tip, 535 | U256::from(20000000000000000u64) 536 | ); 537 | assert_eq!(simulated_bundle.gas_price, U256::from(476190476193u64)); 538 | assert_eq!(simulated_bundle.gas_used, U256::from(42000)); 539 | assert_eq!(simulated_bundle.gas_fees, U256::from(126000)); 540 | assert_eq!(simulated_bundle.simulation_block, U64::from(5221585)); 541 | assert_eq!(simulated_bundle.transactions.len(), 3); 542 | assert_eq!( 543 | simulated_bundle.transactions[0].value, 544 | Some(Bytes::from(vec![])) 545 | ); 546 | assert_eq!( 547 | simulated_bundle.transactions[0].error, 548 | Some("execution reverted".into()) 549 | ); 550 | assert_eq!(simulated_bundle.transactions[1].error, None); 551 | assert_eq!( 552 | simulated_bundle.transactions[1].value, 553 | Some(Bytes::from(vec![0x1])) 554 | ); 555 | assert_eq!(simulated_bundle.transactions[2].to, None); 556 | } 557 | 558 | #[test] 559 | fn simulated_transaction_deserialize() { 560 | let tx: SimulatedTransaction = serde_json::from_str( 561 | r#"{ 562 | "coinbaseDiff": "10000000000063000", 563 | "ethSentToCoinbase": "10000000000000000", 564 | "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0", 565 | "gasFees": "63000", 566 | "gasPrice": "476190476193", 567 | "gasUsed": 21000, 568 | "toAddress": "0x", 569 | "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa", 570 | "error": "execution reverted" 571 | }"#, 572 | ) 573 | .unwrap(); 574 | assert_eq!(tx.error, Some("execution reverted".into())); 575 | 576 | let tx: SimulatedTransaction = serde_json::from_str( 577 | r#"{ 578 | "coinbaseDiff": "10000000000063000", 579 | "ethSentToCoinbase": "10000000000000000", 580 | "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0", 581 | "gasFees": "63000", 582 | "gasPrice": "476190476193", 583 | "gasUsed": 21000, 584 | "toAddress": "0x", 585 | "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa", 586 | "error": "execution reverted", 587 | "revert": "transfer failed" 588 | }"#, 589 | ) 590 | .unwrap(); 591 | 592 | assert_eq!(tx.error, Some("execution reverted".into())); 593 | assert_eq!(tx.revert, Some("transfer failed".into())); 594 | } 595 | 596 | #[test] 597 | fn bundle_stats_deserialize() { 598 | let bundle_stats: BundleStats = serde_json::from_str( 599 | r#"{ 600 | "isHighPriority": true, 601 | "isSimulated": true, 602 | "simulatedAt": "2022-10-06T21:36:06.317Z", 603 | "receivedAt": "2022-10-06T21:36:06.250Z", 604 | "consideredByBuildersAt": [{ 605 | "pubkey": "0x81babeec8c9f2bb9c329fd8a3b176032fe0ab5f3b92a3f44d4575a231c7bd9c31d10b6328ef68ed1e8c02a3dbc8e80f9", 606 | "timestamp": "2022-10-06T21:36:06.343Z" 607 | }, { 608 | "pubkey": "0x81beef03aafd3dd33ffd7deb337407142c80fea2690e5b3190cfc01bde5753f28982a7857c96172a75a234cb7bcb994f", 609 | "timestamp": "2022-10-06T21:36:06.394Z" 610 | }, { 611 | "pubkey": "0xa1dead1e65f0a0eee7b5170223f20c8f0cbf122eac3324d61afbdb33a8885ff8cab2ef514ac2c7698ae0d6289ef27fcf", 612 | "timestamp": "2022-10-06T21:36:06.322Z" 613 | }], 614 | "sealedByBuildersAt": [{ 615 | "pubkey": "0x81beef03aafd3dd33ffd7deb337407142c80fea2690e5b3190cfc01bde5753f28982a7857c96172a75a234cb7bcb994f", 616 | "timestamp": "2022-10-06T21:36:07.742Z" 617 | }] 618 | }"#, 619 | ) 620 | .unwrap(); 621 | 622 | assert!(bundle_stats.is_high_priority); 623 | assert!(bundle_stats.is_simulated); 624 | assert_eq!( 625 | bundle_stats.simulated_at.unwrap().to_rfc3339(), 626 | "2022-10-06T21:36:06.317+00:00" 627 | ); 628 | assert_eq!( 629 | bundle_stats.received_at.unwrap().to_rfc3339(), 630 | "2022-10-06T21:36:06.250+00:00" 631 | ); 632 | 633 | assert_eq!(bundle_stats.considered_by_builders_at.len(), 3); 634 | assert_eq!(bundle_stats.sealed_by_builders_at.len(), 1); 635 | } 636 | } 637 | -------------------------------------------------------------------------------- /src/jsonrpc.rs: -------------------------------------------------------------------------------- 1 | // Code adapted from: https://github.com/althea-net/guac_rs/tree/master/web3/src/jsonrpc 2 | // NOTE: This module only exists since there is no way to use the data structures 3 | // in the `ethers-providers/src/transports/common.rs` from another crate. 4 | use ethers::core::types::U256; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | use std::fmt; 8 | use thiserror::Error; 9 | 10 | /// A JSON-RPC 2.0 error 11 | #[derive(Serialize, Deserialize, Debug, Clone, Error)] 12 | pub struct JsonRpcError { 13 | /// The error code 14 | pub code: i64, 15 | /// The error message 16 | pub message: String, 17 | /// Additional data 18 | pub data: Option, 19 | } 20 | 21 | impl fmt::Display for JsonRpcError { 22 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 23 | write!( 24 | f, 25 | "(code: {}, message: {}, data: {:?})", 26 | self.code, self.message, self.data 27 | ) 28 | } 29 | } 30 | 31 | fn is_zst(_t: &T) -> bool { 32 | std::mem::size_of::() == 0 33 | } 34 | 35 | #[derive(Serialize, Deserialize, Debug)] 36 | /// A JSON-RPC request 37 | pub struct Request<'a, T> { 38 | id: u64, 39 | jsonrpc: &'a str, 40 | method: &'a str, 41 | #[serde(skip_serializing_if = "is_zst")] 42 | params: T, 43 | } 44 | 45 | #[derive(Serialize, Deserialize, Debug)] 46 | /// A JSON-RPC Notifcation 47 | pub struct Notification { 48 | jsonrpc: String, 49 | method: String, 50 | pub params: Subscription, 51 | } 52 | 53 | #[derive(Serialize, Deserialize, Debug)] 54 | pub struct Subscription { 55 | pub subscription: U256, 56 | pub result: R, 57 | } 58 | 59 | impl<'a, T> Request<'a, T> { 60 | /// Creates a new JSON RPC request 61 | pub fn new(id: u64, method: &'a str, params: T) -> Self { 62 | Self { 63 | id, 64 | jsonrpc: "2.0", 65 | method, 66 | params, 67 | } 68 | } 69 | } 70 | 71 | #[derive(Serialize, Deserialize, Debug, Clone)] 72 | pub struct Response { 73 | pub(crate) id: u64, 74 | jsonrpc: String, 75 | #[serde(flatten)] 76 | pub data: ResponseData, 77 | } 78 | 79 | #[derive(Serialize, Deserialize, Debug, Clone)] 80 | #[serde(untagged)] 81 | pub enum ResponseData { 82 | Error { error: JsonRpcError }, 83 | Success { result: Option }, 84 | } 85 | 86 | impl ResponseData { 87 | /// Consume response and return value 88 | pub fn into_result(self) -> Result, JsonRpcError> { 89 | match self { 90 | ResponseData::Success { result } => Ok(result), 91 | ResponseData::Error { error } => Err(error), 92 | } 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | 100 | #[test] 101 | fn deser_response() { 102 | let response: Response = 103 | serde_json::from_str(r#"{"jsonrpc": "2.0", "result": 19, "id": 1}"#).unwrap(); 104 | assert_eq!(response.id, 1); 105 | assert_eq!(response.data.into_result().unwrap(), Some(19)); 106 | } 107 | 108 | #[test] 109 | fn deser_response_without_result() { 110 | let response: Response = 111 | serde_json::from_str(r#"{"jsonrpc": "2.0", "id": 1, "result": null}"#).unwrap(); 112 | assert_eq!(response.id, 1); 113 | assert_eq!(response.data.into_result().unwrap(), None); 114 | } 115 | 116 | #[test] 117 | fn ser_request() { 118 | let request: Request<()> = Request::new(300, "method_name", ()); 119 | assert_eq!( 120 | &serde_json::to_string(&request).unwrap(), 121 | r#"{"id":300,"jsonrpc":"2.0","method":"method_name"}"# 122 | ); 123 | 124 | let request: Request = Request::new(300, "method_name", 1); 125 | assert_eq!( 126 | &serde_json::to_string(&request).unwrap(), 127 | r#"{"id":300,"jsonrpc":"2.0","method":"method_name","params":1}"# 128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Ethers Flashbots 2 | //! 3 | //! Provides an [ethers](https://docs.rs/ethers) compatible middleware for submitting 4 | //! [Flashbots](https://docs.flashbots.net) bundles. 5 | //! 6 | //! In addition to leveraging the standard Ethers middleware API ([`send_transaction`][ethers::providers::Middleware::send_transaction]), 7 | //! custom bundles can be crafted, simulated and submitted. 8 | mod bundle; 9 | pub use bundle::{ 10 | BundleHash, BundleRequest, BundleStats, BundleTransaction, SimulatedBundle, 11 | SimulatedTransaction, 12 | }; 13 | 14 | mod pending_bundle; 15 | pub use pending_bundle::{PendingBundle, PendingBundleError}; 16 | 17 | mod user; 18 | pub use user::UserStats; 19 | 20 | mod middleware; 21 | pub use middleware::{BroadcasterMiddleware, FlashbotsMiddleware, FlashbotsMiddlewareError}; 22 | 23 | mod jsonrpc; 24 | mod relay; 25 | pub use relay::{Relay, RelayError}; 26 | 27 | mod utils; 28 | -------------------------------------------------------------------------------- /src/middleware.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | bundle::{BundleHash, BundleRequest, BundleStats, SimulatedBundle}, 3 | pending_bundle::PendingBundle, 4 | relay::{GetBundleStatsParams, GetUserStatsParams, Relay, RelayError, SendBundleResponse}, 5 | UserStats, 6 | }; 7 | use async_trait::async_trait; 8 | use ethers::{ 9 | core::{ 10 | types::{BlockNumber, Bytes, U64}, 11 | utils::keccak256, 12 | }, 13 | providers::{Middleware, MiddlewareError, PendingTransaction}, 14 | signers::Signer, 15 | }; 16 | use futures_util::future; 17 | use thiserror::Error; 18 | use url::Url; 19 | 20 | /// Errors for the Flashbots middleware. 21 | #[derive(Error, Debug)] 22 | pub enum FlashbotsMiddlewareError { 23 | /// Some parameters were missing. 24 | /// 25 | /// For bundle simulation, check that the following are set: 26 | /// - `simulation_block` 27 | /// - `simulation_timestamp` 28 | /// - `block` 29 | /// 30 | /// For bundle submission, check that the following are set: 31 | /// - `block` 32 | /// 33 | /// Additionally, `min_timestamp` and `max_timestamp` must 34 | /// both be set or unset. 35 | #[error("Some parameters were missing")] 36 | MissingParameters, 37 | /// The relay responded with an error. 38 | #[error(transparent)] 39 | RelayError(#[from] RelayError), 40 | /// An error occured in one of the middlewares. 41 | #[error("{0}")] 42 | MiddlewareError(M::Error), 43 | /// Empty data for bundle simulation request. 44 | #[error("Bundle simulation is not available")] 45 | BundleSimError, 46 | /// Empty data for bundle stats request. 47 | #[error("Bundle stats are not available")] 48 | BundleStatsError, 49 | /// Empty data for user stats request. 50 | #[error("User stats are not available")] 51 | UserStatsError, 52 | } 53 | 54 | impl MiddlewareError for FlashbotsMiddlewareError { 55 | type Inner = M::Error; 56 | 57 | fn from_err(src: M::Error) -> FlashbotsMiddlewareError { 58 | FlashbotsMiddlewareError::MiddlewareError(src) 59 | } 60 | 61 | fn as_inner(&self) -> Option<&Self::Inner> { 62 | match self { 63 | FlashbotsMiddlewareError::MiddlewareError(e) => Some(e), 64 | _ => None, 65 | } 66 | } 67 | } 68 | 69 | /// A middleware used to send bundles to a Flashbots relay. 70 | /// 71 | /// **NOTE**: This middleware does **NOT** sign your transactions. Use 72 | /// another method to sign your transactions, and then forward the signed 73 | /// transactions to the middleware. 74 | /// 75 | /// You can either send custom bundles (see [`BundleRequest`]) or send 76 | /// transactions as you normally would (see [`Middleware::send_transaction`]) from 77 | /// another middleware. 78 | /// 79 | /// If you use [`Middleware::send_transaction`] then a bundle will be constructed 80 | /// for you with the following assumptions: 81 | /// 82 | /// - You do not want to allow the transaction to revert 83 | /// - You do not care to set a minimum or maximum timestamp for the bundle 84 | /// - The block you are targetting with your bundle is the next block 85 | /// - You do not want to simulate the bundle before sending to the relay 86 | /// 87 | /// # Example 88 | /// ``` 89 | /// use ethers::prelude::*; 90 | /// use std::convert::TryFrom; 91 | /// use ethers_flashbots::FlashbotsMiddleware; 92 | /// use url::Url; 93 | /// 94 | /// # async fn foo() -> Result<(), Box> { 95 | /// let provider = Provider::::try_from("http://localhost:8545") 96 | /// .expect("Could not instantiate HTTP provider"); 97 | /// 98 | /// // Used to sign Flashbots relay requests - this is your searcher identity 99 | /// let signer: LocalWallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" 100 | /// .parse()?; 101 | /// 102 | /// // Used to sign transactions 103 | /// let wallet: LocalWallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" 104 | /// .parse()?; 105 | /// 106 | /// // Note: The order is important! You want the signer 107 | /// // middleware to sign your transactions *before* they 108 | /// // are sent to your Flashbots middleware. 109 | /// let mut client = SignerMiddleware::new( 110 | /// FlashbotsMiddleware::new( 111 | /// provider, 112 | /// Url::parse("https://relay.flashbots.net")?, 113 | /// signer 114 | /// ), 115 | /// wallet 116 | /// ); 117 | /// 118 | /// // This transaction will now be send as a Flashbots bundle! 119 | /// let tx = TransactionRequest::pay("vitalik.eth", 100); 120 | /// let pending_tx = client.send_transaction(tx, None).await?; 121 | /// # Ok(()) 122 | /// # } 123 | /// ``` 124 | #[derive(Debug)] 125 | pub struct FlashbotsMiddleware { 126 | inner: M, 127 | relay: Relay, 128 | simulation_relay: Option>, 129 | } 130 | 131 | impl FlashbotsMiddleware { 132 | /// Initialize a new Flashbots middleware. 133 | /// 134 | /// The signer is used to sign requests to the relay. 135 | pub fn new(inner: M, relay_url: impl Into, relay_signer: S) -> Self { 136 | Self { 137 | inner, 138 | relay: Relay::new(relay_url, Some(relay_signer)), 139 | simulation_relay: None, 140 | } 141 | } 142 | 143 | /// Get the relay client used by the middleware. 144 | pub fn relay(&self) -> &Relay { 145 | &self.relay 146 | } 147 | 148 | /// Get the relay client used by the middleware to simulate 149 | /// bundles if set. 150 | pub fn simulation_relay(&self) -> Option<&Relay> { 151 | self.simulation_relay.as_ref() 152 | } 153 | 154 | /// Set a separate relay to use for simulating bundles. 155 | /// 156 | /// This can either be a full Flashbots relay or a node that implements 157 | /// the `eth_callBundle` remote procedure call. 158 | pub fn set_simulation_relay(&mut self, relay_url: impl Into) { 159 | self.simulation_relay = Some(Relay::new(relay_url, None)); 160 | } 161 | 162 | /// Simulate a bundle. 163 | /// 164 | /// See [`eth_callBundle`][fb_callBundle] for more information. 165 | /// 166 | /// [fb_callBundle]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_callbundle 167 | pub async fn simulate_bundle( 168 | &self, 169 | bundle: &BundleRequest, 170 | ) -> Result> { 171 | bundle 172 | .block() 173 | .and(bundle.simulation_block()) 174 | .and(bundle.simulation_timestamp()) 175 | .ok_or(FlashbotsMiddlewareError::MissingParameters)?; 176 | 177 | self.simulation_relay 178 | .as_ref() 179 | .unwrap_or(&self.relay) 180 | .request("eth_callBundle", [bundle]) 181 | .await 182 | .map_err(FlashbotsMiddlewareError::RelayError)? 183 | .ok_or(FlashbotsMiddlewareError::BundleSimError) 184 | } 185 | 186 | /// Send a bundle to the relayer. 187 | /// 188 | /// See [`eth_sendBundle`][fb_sendBundle] for more information. 189 | /// 190 | /// [fb_sendBundle]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_sendbundle 191 | pub async fn send_bundle( 192 | &self, 193 | bundle: &BundleRequest, 194 | ) -> Result::Provider>, FlashbotsMiddlewareError> 195 | { 196 | // The target block must be set 197 | bundle 198 | .block() 199 | .ok_or(FlashbotsMiddlewareError::MissingParameters)?; 200 | 201 | // `min_timestamp` and `max_timestamp` must both either be unset or set. 202 | if bundle.min_timestamp().xor(bundle.max_timestamp()).is_some() { 203 | return Err(FlashbotsMiddlewareError::MissingParameters); 204 | } 205 | 206 | let response: Option = self 207 | .relay 208 | .request("eth_sendBundle", [bundle]) 209 | .await 210 | .map_err(FlashbotsMiddlewareError::RelayError)?; 211 | 212 | match response { 213 | Some(r) => Ok(PendingBundle::new( 214 | r.bundle_hash, 215 | bundle.block().unwrap(), 216 | bundle.transaction_hashes(), 217 | self.provider(), 218 | )), 219 | None => Ok(PendingBundle::new( 220 | None, 221 | bundle.block().unwrap(), 222 | bundle.transaction_hashes(), 223 | self.provider(), 224 | )), 225 | } 226 | } 227 | 228 | /// Get stats for a particular bundle. 229 | pub async fn get_bundle_stats( 230 | &self, 231 | bundle_hash: BundleHash, 232 | block_number: U64, 233 | ) -> Result> { 234 | self.relay 235 | .request( 236 | "flashbots_getBundleStatsV2", 237 | [GetBundleStatsParams { 238 | bundle_hash, 239 | block_number, 240 | }], 241 | ) 242 | .await 243 | .map_err(FlashbotsMiddlewareError::RelayError)? 244 | .ok_or(FlashbotsMiddlewareError::BundleStatsError) 245 | } 246 | 247 | /// Get stats for your searcher identity. 248 | /// 249 | /// Your searcher identity is determined by the signer you 250 | /// constructed the middleware with. 251 | pub async fn get_user_stats(&self) -> Result> { 252 | let latest_block = self 253 | .inner 254 | .get_block_number() 255 | .await 256 | .map_err(FlashbotsMiddlewareError::MiddlewareError)?; 257 | 258 | self.relay 259 | .request( 260 | "flashbots_getUserStatsV2", 261 | [GetUserStatsParams { 262 | block_number: latest_block, 263 | }], 264 | ) 265 | .await 266 | .map_err(FlashbotsMiddlewareError::RelayError)? 267 | .ok_or(FlashbotsMiddlewareError::UserStatsError) 268 | } 269 | } 270 | 271 | #[async_trait] 272 | impl Middleware for FlashbotsMiddleware 273 | where 274 | M: Middleware, 275 | S: Signer, 276 | { 277 | type Error = FlashbotsMiddlewareError; 278 | type Provider = M::Provider; 279 | type Inner = M; 280 | 281 | fn inner(&self) -> &M { 282 | &self.inner 283 | } 284 | 285 | async fn send_raw_transaction<'a>( 286 | &'a self, 287 | tx: Bytes, 288 | ) -> Result, Self::Error> { 289 | let tx_hash = keccak256(&tx); 290 | 291 | // Get the latest block 292 | let latest_block = self 293 | .inner 294 | .get_block(BlockNumber::Latest) 295 | .await 296 | .map_err(FlashbotsMiddlewareError::MiddlewareError)? 297 | .expect("The latest block is pending (this should not happen)"); 298 | 299 | // Construct the bundle, assuming that the target block is the 300 | // next block. 301 | let bundle = BundleRequest::new().push_transaction(tx.clone()).set_block( 302 | latest_block 303 | .number 304 | .expect("The latest block is pending (this should not happen)") 305 | + 1, 306 | ); 307 | 308 | self.send_bundle(&bundle).await?; 309 | 310 | Ok(PendingTransaction::new(tx_hash.into(), self.provider()) 311 | .interval(self.provider().get_interval())) 312 | } 313 | } 314 | 315 | /// A middleware used to broadcast bundles to multiple builders. 316 | /// 317 | /// **NOTE**: This middleware does **NOT** sign your transactions. Use 318 | /// another method to sign your transactions, and then forward the signed 319 | /// transactions to the middleware. 320 | /// 321 | /// You can either send custom bundles (see [`BundleRequest`]) or send 322 | /// transactions as you normally would (see [`Middleware::send_transaction`]) from 323 | /// another middleware. 324 | /// 325 | /// If you use [`Middleware::send_transaction`] then a bundle will be constructed 326 | /// for you with the following assumptions: 327 | /// 328 | /// - You do not want to allow the transaction to revert 329 | /// - You do not care to set a minimum or maximum timestamp for the bundle 330 | /// - The block you are targetting with your bundle is the next block 331 | /// - You do not want to simulate the bundle before sending to the builder 332 | /// 333 | /// # Example 334 | /// ``` 335 | /// use ethers::prelude::*; 336 | /// use std::convert::TryFrom; 337 | /// use ethers_flashbots::BroadcasterMiddleware; 338 | /// use url::Url; 339 | /// 340 | /// # async fn foo() -> Result<(), Box> { 341 | /// let provider = Provider::::try_from("http://localhost:8545") 342 | /// .expect("Could not instantiate HTTP provider"); 343 | /// 344 | /// // Used to sign Flashbots relay requests - this is your searcher identity 345 | /// let signer: LocalWallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" 346 | /// .parse()?; 347 | /// 348 | /// // Used to sign transactions 349 | /// let wallet: LocalWallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" 350 | /// .parse()?; 351 | /// 352 | /// // Note: The order is important! You want the signer 353 | /// // middleware to sign your transactions *before* they 354 | /// // are sent to your Flashbots middleware. 355 | /// let mut client = SignerMiddleware::new( 356 | /// BroadcasterMiddleware::new( 357 | /// provider, 358 | /// vec![Url::parse("https://rpc.titanbuilder.xyz")?, Url::parse("https://relay.flashbots.net")?], 359 | /// Url::parse("https://relay.flashbots.net")?, 360 | /// signer 361 | /// ), 362 | /// wallet 363 | /// ); 364 | /// 365 | /// // This transaction will now be sent as a Flashbots bundle! 366 | /// let tx = TransactionRequest::pay("vitalik.eth", 100); 367 | /// let pending_tx = client.send_transaction(tx, None).await?; 368 | /// # Ok(()) 369 | /// # } 370 | /// ``` 371 | #[derive(Debug)] 372 | pub struct BroadcasterMiddleware { 373 | inner: M, 374 | relays: Vec>, 375 | simulation_relay: Relay, 376 | } 377 | 378 | impl BroadcasterMiddleware { 379 | /// Initialize a new Flashbots middleware. 380 | /// 381 | /// The signer is used to sign requests to the relay. 382 | pub fn new( 383 | inner: M, 384 | relay_urls: Vec, 385 | simulation_relay: impl Into, 386 | relay_signer: S, 387 | ) -> Self 388 | where 389 | S: Clone, 390 | { 391 | Self { 392 | inner, 393 | relays: relay_urls 394 | .into_iter() 395 | .map(|r| Relay::new(r, Some(relay_signer.clone()))) 396 | .collect(), 397 | simulation_relay: Relay::new(simulation_relay, Some(relay_signer)), 398 | } 399 | } 400 | 401 | /// Get the relay client used by the middleware. 402 | pub fn relay(&self) -> &Vec> { 403 | &self.relays 404 | } 405 | 406 | /// Get the relay client used by the middleware to simulate 407 | /// bundles. 408 | pub fn simulation_relay(&self) -> &Relay { 409 | &self.simulation_relay 410 | } 411 | 412 | /// Simulate a bundle. 413 | /// 414 | /// See [`eth_callBundle`][fb_callBundle] for more information. 415 | /// 416 | /// [fb_callBundle]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_callbundle 417 | pub async fn simulate_bundle( 418 | &self, 419 | bundle: &BundleRequest, 420 | ) -> Result> { 421 | bundle 422 | .block() 423 | .and(bundle.simulation_block()) 424 | .and(bundle.simulation_timestamp()) 425 | .ok_or(FlashbotsMiddlewareError::MissingParameters)?; 426 | 427 | self.simulation_relay 428 | .request("eth_callBundle", [bundle]) 429 | .await 430 | .map_err(FlashbotsMiddlewareError::RelayError)? 431 | .ok_or(FlashbotsMiddlewareError::BundleSimError) 432 | } 433 | 434 | /// Broadcast a bundle to the builders. 435 | /// 436 | /// See [`eth_sendBundle`][fb_sendBundle] for more information. 437 | /// 438 | /// [fb_sendBundle]: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_sendbundle 439 | pub async fn send_bundle( 440 | &self, 441 | bundle: &BundleRequest, 442 | ) -> Result< 443 | Vec< 444 | Result< 445 | PendingBundle<'_, ::Provider>, 446 | FlashbotsMiddlewareError, 447 | >, 448 | >, 449 | FlashbotsMiddlewareError, 450 | > { 451 | // The target block must be set 452 | bundle 453 | .block() 454 | .ok_or(FlashbotsMiddlewareError::MissingParameters)?; 455 | 456 | let futures = self 457 | .relays 458 | .iter() 459 | .map(|relay| async move { 460 | let response = relay.request("eth_sendBundle", [bundle]).await; 461 | response 462 | .map(|response: Option| match response { 463 | Some(r) => PendingBundle::new( 464 | r.bundle_hash, 465 | bundle.block().unwrap(), 466 | bundle.transaction_hashes(), 467 | self.provider(), 468 | ), 469 | None => PendingBundle::new( 470 | None, 471 | bundle.block().unwrap(), 472 | bundle.transaction_hashes(), 473 | self.provider(), 474 | ), 475 | }) 476 | .map_err(FlashbotsMiddlewareError::RelayError) 477 | }) 478 | .collect::>(); 479 | 480 | let responses = future::join_all(futures).await; 481 | 482 | Ok(responses) 483 | } 484 | } 485 | 486 | #[async_trait] 487 | impl Middleware for BroadcasterMiddleware 488 | where 489 | M: Middleware, 490 | S: Signer, 491 | { 492 | type Error = FlashbotsMiddlewareError; 493 | type Provider = M::Provider; 494 | type Inner = M; 495 | 496 | fn inner(&self) -> &M { 497 | &self.inner 498 | } 499 | 500 | async fn send_raw_transaction<'a>( 501 | &'a self, 502 | tx: Bytes, 503 | ) -> Result, Self::Error> { 504 | let tx_hash = keccak256(&tx); 505 | 506 | // Get the latest block 507 | let latest_block = self 508 | .inner 509 | .get_block(BlockNumber::Latest) 510 | .await 511 | .map_err(FlashbotsMiddlewareError::MiddlewareError)? 512 | .expect("The latest block is pending (this should not happen)"); 513 | 514 | // Construct the bundle, assuming that the target block is the 515 | // next block. 516 | let bundle = BundleRequest::new().push_transaction(tx.clone()).set_block( 517 | latest_block 518 | .number 519 | .expect("The latest block is pending (this should not happen)") 520 | + 1, 521 | ); 522 | 523 | self.send_bundle(&bundle).await?; 524 | 525 | Ok(PendingTransaction::new(tx_hash.into(), self.provider()) 526 | .interval(self.provider().get_interval())) 527 | } 528 | } 529 | -------------------------------------------------------------------------------- /src/pending_bundle.rs: -------------------------------------------------------------------------------- 1 | use crate::bundle::BundleHash; 2 | use ethers::core::types::{Block, TxHash, U64}; 3 | use ethers::providers::{ 4 | interval, JsonRpcClient, Middleware, Provider, ProviderError, DEFAULT_POLL_INTERVAL, 5 | }; 6 | use futures_core::stream::Stream; 7 | use futures_util::stream::StreamExt; 8 | use pin_project::pin_project; 9 | use std::{ 10 | future::Future, 11 | pin::Pin, 12 | task::{Context, Poll}, 13 | }; 14 | use thiserror::Error; 15 | 16 | /// A pending bundle is one that has been submitted to a relay, 17 | /// but not yet included. 18 | /// 19 | /// You can `await` the pending bundle. When the target block of the 20 | /// bundle has been included in the chain the future will resolve, 21 | /// either with the bundle hash indicating that the bundle was 22 | /// included in the target block, or with an error indicating 23 | /// that the bundle was not included in the target block. 24 | /// 25 | /// To figure out why your bundle was not included, refer to the 26 | /// [Flashbots documentation][fb_debug]. 27 | /// 28 | /// [fb_debug]: https://docs.flashbots.net/flashbots-auction/searchers/faq/#why-didnt-my-transaction-get-included 29 | #[pin_project] 30 | pub struct PendingBundle<'a, P> { 31 | pub bundle_hash: Option, 32 | pub block: U64, 33 | pub transactions: Vec, 34 | provider: &'a Provider

, 35 | state: PendingBundleState<'a>, 36 | interval: Box + Send + Unpin>, 37 | } 38 | 39 | impl<'a, P: JsonRpcClient> PendingBundle<'a, P> { 40 | pub fn new( 41 | bundle_hash: Option, 42 | block: U64, 43 | transactions: Vec, 44 | provider: &'a Provider

, 45 | ) -> Self { 46 | Self { 47 | bundle_hash, 48 | block, 49 | transactions, 50 | provider, 51 | state: PendingBundleState::PausedGettingBlock, 52 | interval: Box::new(interval(DEFAULT_POLL_INTERVAL)), 53 | } 54 | } 55 | 56 | /// Get the bundle hash for this pending bundle. 57 | #[deprecated(note = "use the bundle_hash field instead")] 58 | pub fn bundle_hash(&self) -> Option { 59 | self.bundle_hash 60 | } 61 | } 62 | 63 | impl<'a, P: JsonRpcClient> Future for PendingBundle<'a, P> { 64 | type Output = Result, PendingBundleError>; 65 | 66 | fn poll(self: Pin<&mut Self>, ctx: &mut Context) -> Poll { 67 | let this = self.project(); 68 | 69 | match this.state { 70 | PendingBundleState::PausedGettingBlock => { 71 | futures_util::ready!(this.interval.poll_next_unpin(ctx)); 72 | let fut = Box::pin(this.provider.get_block(*this.block)); 73 | *this.state = PendingBundleState::GettingBlock(fut); 74 | ctx.waker().wake_by_ref(); 75 | } 76 | PendingBundleState::GettingBlock(fut) => { 77 | let block_res = futures_util::ready!(fut.as_mut().poll(ctx)); 78 | 79 | // If the provider errors, we try again after some interval. 80 | if block_res.is_err() { 81 | *this.state = PendingBundleState::PausedGettingBlock; 82 | ctx.waker().wake_by_ref(); 83 | return Poll::Pending; 84 | } 85 | 86 | let block_opt = block_res.unwrap(); 87 | // If the block doesn't exist yet, we try again after some interval. 88 | if block_opt.is_none() { 89 | *this.state = PendingBundleState::PausedGettingBlock; 90 | ctx.waker().wake_by_ref(); 91 | return Poll::Pending; 92 | } 93 | 94 | let block = block_opt.unwrap(); 95 | // If the block is pending, we try again after some interval. 96 | if block.number.is_none() { 97 | *this.state = PendingBundleState::PausedGettingBlock; 98 | ctx.waker().wake_by_ref(); 99 | return Poll::Pending; 100 | } 101 | 102 | // Check if all transactions of the bundle are present in the block 103 | let included: bool = this 104 | .transactions 105 | .iter() 106 | .all(|tx_hash| block.transactions.contains(tx_hash)); 107 | 108 | *this.state = PendingBundleState::Completed; 109 | if included { 110 | return Poll::Ready(Ok(*this.bundle_hash)); 111 | } else { 112 | return Poll::Ready(Err(PendingBundleError::BundleNotIncluded)); 113 | } 114 | } 115 | PendingBundleState::Completed => { 116 | panic!("polled pending bundle future after completion") 117 | } 118 | } 119 | 120 | Poll::Pending 121 | } 122 | } 123 | 124 | /// Errors for pending bundles. 125 | #[derive(Error, Debug)] 126 | pub enum PendingBundleError { 127 | /// The bundle was not included in the target block. 128 | #[error("Bundle was not included in target block")] 129 | BundleNotIncluded, 130 | /// An error occured while interacting with the RPC endpoint. 131 | #[error(transparent)] 132 | ProviderError(#[from] ProviderError), 133 | } 134 | 135 | type PinBoxFut<'a, T> = Pin> + Send + 'a>>; 136 | 137 | enum PendingBundleState<'a> { 138 | /// Waiting for an interval before calling API again 139 | PausedGettingBlock, 140 | 141 | /// Polling the blockchain to get block information 142 | GettingBlock(PinBoxFut<'a, Option>>), 143 | 144 | /// Future has completed 145 | Completed, 146 | } 147 | -------------------------------------------------------------------------------- /src/relay.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | bundle::BundleHash, 3 | jsonrpc::{JsonRpcError, Request, Response}, 4 | }; 5 | use ethers::core::{ 6 | types::{H256, U64}, 7 | utils::keccak256, 8 | }; 9 | use ethers::signers::Signer; 10 | use reqwest::{Client, Error as ReqwestError}; 11 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 12 | use std::sync::atomic::{AtomicU64, Ordering}; 13 | use thiserror::Error; 14 | use url::Url; 15 | 16 | /// A Flashbots relay client. 17 | /// 18 | /// The client automatically signs every request and sets the Flashbots 19 | /// authorization header appropriately with the given signer. 20 | /// 21 | /// **Note**: You probably do not want to use this directly, unless 22 | /// you want to interact directly with the Relay. Most users should use 23 | /// [`FlashbotsMiddleware`](crate::FlashbotsMiddleware) instead. 24 | #[derive(Debug)] 25 | pub struct Relay { 26 | id: AtomicU64, 27 | client: Client, 28 | url: Url, 29 | signer: Option, 30 | } 31 | 32 | /// Errors for relay requests. 33 | #[derive(Error, Debug)] 34 | pub enum RelayError { 35 | /// The request failed. 36 | #[error(transparent)] 37 | RequestError(#[from] ReqwestError), 38 | /// The request could not be parsed. 39 | #[error(transparent)] 40 | JsonRpcError(#[from] JsonRpcError), 41 | /// The request parameters were invalid. 42 | #[error("Client error: {text}")] 43 | ClientError { text: String }, 44 | /// The request could not be serialized. 45 | #[error(transparent)] 46 | RequestSerdeJson(#[from] serde_json::Error), 47 | /// The request could not be signed. 48 | #[error(transparent)] 49 | SignerError(#[from(S::Error)] S::Error), 50 | /// The response could not be deserialized. 51 | #[error("Deserialization error: {err}. Response: {text}")] 52 | ResponseSerdeJson { 53 | err: serde_json::Error, 54 | text: String, 55 | }, 56 | } 57 | 58 | impl Relay { 59 | /// Initializes a new relay client. 60 | pub fn new(url: impl Into, signer: Option) -> Self { 61 | Self { 62 | id: AtomicU64::new(0), 63 | client: Client::new(), 64 | url: url.into(), 65 | signer, 66 | } 67 | } 68 | 69 | /// Sends a request with the provided method to the relay, with the 70 | /// parameters serialized as JSON. 71 | pub async fn request( 72 | &self, 73 | method: &str, 74 | params: T, 75 | ) -> Result, RelayError> { 76 | let next_id = self.id.load(Ordering::SeqCst) + 1; 77 | self.id.store(next_id, Ordering::SeqCst); 78 | 79 | let payload = Request::new(next_id, method, params); 80 | 81 | let mut req = self.client.post(self.url.as_ref()); 82 | 83 | if let Some(signer) = &self.signer { 84 | let signature = signer 85 | .sign_message(format!( 86 | "0x{:x}", 87 | H256::from(keccak256( 88 | serde_json::to_string(&payload) 89 | .map_err(RelayError::RequestSerdeJson)? 90 | .as_bytes() 91 | )) 92 | )) 93 | .await 94 | .map_err(RelayError::SignerError)?; 95 | 96 | req = req.header( 97 | "X-Flashbots-Signature", 98 | format!("{:?}:0x{}", signer.address(), signature), 99 | ); 100 | } 101 | 102 | let res = req.json(&payload).send().await?; 103 | let status = res.error_for_status_ref(); 104 | 105 | match status { 106 | Err(err) => { 107 | let text = res.text().await?; 108 | let status_code = err.status().unwrap(); 109 | if status_code.is_client_error() { 110 | // Client error (400-499) 111 | Err(RelayError::ClientError { text }) 112 | } else { 113 | // Internal server error (500-599) 114 | Err(RelayError::RequestError(err)) 115 | } 116 | } 117 | Ok(_) => { 118 | let text = res.text().await?; 119 | let res: Response = serde_json::from_str(&text) 120 | .map_err(|err| RelayError::ResponseSerdeJson { err, text })?; 121 | 122 | Ok(res.data.into_result()?) 123 | } 124 | } 125 | } 126 | } 127 | 128 | impl Clone for Relay { 129 | fn clone(&self) -> Self { 130 | Self { 131 | id: AtomicU64::new(0), 132 | client: self.client.clone(), 133 | url: self.url.clone(), 134 | signer: self.signer.clone(), 135 | } 136 | } 137 | } 138 | 139 | #[derive(Deserialize)] 140 | #[serde(rename_all = "camelCase")] 141 | pub(crate) struct SendBundleResponse { 142 | pub(crate) bundle_hash: Option, 143 | } 144 | 145 | #[derive(Serialize)] 146 | #[serde(rename_all = "camelCase")] 147 | pub(crate) struct GetBundleStatsParams { 148 | pub(crate) bundle_hash: BundleHash, 149 | pub(crate) block_number: U64, 150 | } 151 | 152 | #[derive(Serialize)] 153 | #[serde(rename_all = "camelCase")] 154 | pub(crate) struct GetUserStatsParams { 155 | pub(crate) block_number: U64, 156 | } 157 | -------------------------------------------------------------------------------- /src/user.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::deserialize_u256; 2 | use ethers::core::types::U256; 3 | use serde::Deserialize; 4 | 5 | /// Represents stats for a searcher. 6 | #[derive(Deserialize, Debug)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct UserStats { 9 | /// Whether the searcher is high priority or not. 10 | pub is_high_priority: bool, 11 | /// The total amount of payments made to validators. 12 | #[serde(deserialize_with = "deserialize_u256")] 13 | pub all_time_validator_payments: U256, 14 | /// The total amount of gas simulated in bundles. 15 | #[serde(deserialize_with = "deserialize_u256")] 16 | pub all_time_gas_simulated: U256, 17 | /// The total amount of payments made to validators in the last 7 days. 18 | #[serde(deserialize_with = "deserialize_u256")] 19 | pub last_7d_validator_payments: U256, 20 | /// The total amount of gas simulated in bundles the last 7 days. 21 | #[serde(deserialize_with = "deserialize_u256")] 22 | pub last_7d_gas_simulated: U256, 23 | /// The total amount of payments made to validators in the last day. 24 | #[serde(deserialize_with = "deserialize_u256")] 25 | pub last_1d_validator_payments: U256, 26 | /// The total amount of gas simulated in bundles in the last day. 27 | #[serde(deserialize_with = "deserialize_u256")] 28 | pub last_1d_gas_simulated: U256, 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use super::*; 34 | 35 | #[test] 36 | fn user_stats_deserialize() { 37 | let user_stats: UserStats = serde_json::from_str( 38 | r#"{ 39 | "isHighPriority": true, 40 | "allTimeValidatorPayments": "1280749594841588639", 41 | "allTimeGasSimulated": "30049470846", 42 | "last7dValidatorPayments": "1280749594841588639", 43 | "last7dGasSimulated": "30049470846", 44 | "last1dValidatorPayments": "142305510537954293", 45 | "last1dGasSimulated": "2731770076" 46 | }"#, 47 | ) 48 | .unwrap(); 49 | 50 | assert!(user_stats.is_high_priority); 51 | assert_eq!( 52 | user_stats.all_time_validator_payments, 53 | U256::from_dec_str("1280749594841588639").unwrap() 54 | ); 55 | assert_eq!( 56 | user_stats.all_time_gas_simulated, 57 | U256::from_dec_str("30049470846").unwrap() 58 | ); 59 | assert_eq!( 60 | user_stats.last_7d_validator_payments, 61 | U256::from_dec_str("1280749594841588639").unwrap() 62 | ); 63 | assert_eq!( 64 | user_stats.last_7d_gas_simulated, 65 | U256::from_dec_str("30049470846").unwrap() 66 | ); 67 | assert_eq!( 68 | user_stats.last_1d_validator_payments, 69 | U256::from_dec_str("142305510537954293").unwrap() 70 | ); 71 | assert_eq!( 72 | user_stats.last_1d_gas_simulated, 73 | U256::from_dec_str("2731770076").unwrap() 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use ethers::core::types::{H160, U256, U64}; 2 | use serde::{de, Deserialize}; 3 | use serde_json::Value; 4 | use std::str::FromStr; 5 | 6 | pub fn deserialize_u64<'de, D>(deserializer: D) -> Result 7 | where 8 | D: de::Deserializer<'de>, 9 | { 10 | Ok(match Value::deserialize(deserializer)? { 11 | Value::String(s) => { 12 | if s.as_str() == "0x" { 13 | return Ok(U64::zero()); 14 | } 15 | 16 | if s.as_str().starts_with("0x") { 17 | U64::from_str_radix(s.as_str(), 16).map_err(de::Error::custom)? 18 | } else { 19 | U64::from_dec_str(s.as_str()).map_err(de::Error::custom)? 20 | } 21 | } 22 | Value::Number(num) => U64::from( 23 | num.as_u64() 24 | .ok_or_else(|| de::Error::custom("Invalid number"))?, 25 | ), 26 | _ => return Err(de::Error::custom("wrong type")), 27 | }) 28 | } 29 | 30 | pub fn deserialize_u256<'de, D>(deserializer: D) -> Result 31 | where 32 | D: de::Deserializer<'de>, 33 | { 34 | Ok(match Value::deserialize(deserializer)? { 35 | Value::String(s) => { 36 | if s.as_str() == "0x" { 37 | return Ok(U256::zero()); 38 | } 39 | 40 | if s.as_str().starts_with("0x") { 41 | U256::from_str_radix(s.as_str(), 16).map_err(de::Error::custom)? 42 | } else { 43 | U256::from_dec_str(s.as_str()).map_err(de::Error::custom)? 44 | } 45 | } 46 | Value::Number(num) => U256::from( 47 | num.as_u64() 48 | .ok_or_else(|| de::Error::custom("Invalid number"))?, 49 | ), 50 | _ => return Err(de::Error::custom("wrong type")), 51 | }) 52 | } 53 | 54 | pub fn deserialize_optional_h160<'de, D>(deserializer: D) -> Result, D::Error> 55 | where 56 | D: de::Deserializer<'de>, 57 | { 58 | Ok(match Value::deserialize(deserializer)? { 59 | Value::String(s) => { 60 | if s.as_str() == "0x" { 61 | return Ok(None); 62 | } 63 | 64 | Some(H160::from_str(s.as_str()).map_err(de::Error::custom)?) 65 | } 66 | _ => return Err(de::Error::custom("expected a hexadecimal string")), 67 | }) 68 | } 69 | --------------------------------------------------------------------------------