├── .github └── workflows │ ├── ci.yml │ └── unit.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── crates ├── mev-share-backend │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── rpc.rs │ │ └── service.rs ├── mev-share-rpc-api │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── auth.rs │ │ ├── eth.rs │ │ ├── flashbots.rs │ │ ├── lib.rs │ │ ├── mev.rs │ │ └── types.rs ├── mev-share-sse │ ├── Cargo.toml │ └── src │ │ ├── client.rs │ │ ├── lib.rs │ │ └── server.rs └── mev-share │ ├── Cargo.toml │ └── src │ └── lib.rs ├── examples ├── Cargo.toml ├── rpc-client-onchain.rs ├── rpc-client.rs ├── rpc-sim-service.rs ├── sse-server.rs └── sse.rs └── rustfmt.toml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | merge_group: 4 | push: 5 | branches: 6 | - main 7 | 8 | 9 | env: 10 | RUSTFLAGS: -D warnings 11 | CARGO_TERM_COLOR: always 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 15 | cancel-in-progress: true 16 | 17 | name: ci 18 | jobs: 19 | lint: 20 | name: code lint 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout sources 24 | uses: actions/checkout@v3 25 | - name: Install toolchain 26 | uses: dtolnay/rust-toolchain@nightly 27 | with: 28 | components: rustfmt, clippy 29 | - uses: Swatinem/rust-cache@v2 30 | with: 31 | cache-on-failure: true 32 | 33 | - name: cargo fmt 34 | uses: actions-rs/cargo@v1 35 | with: 36 | command: fmt 37 | args: --all --check 38 | 39 | - name: cargo clippy 40 | uses: actions-rs/clippy-check@v1 41 | with: 42 | args: --all --all-features 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | doc-lint: 46 | name: doc lint 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v3 50 | - name: Install toolchain 51 | uses: dtolnay/rust-toolchain@stable 52 | - uses: Swatinem/rust-cache@v2 53 | - name: Check if documentation builds 54 | run: RUSTDOCFLAGS="-D warnings" cargo doc --all --no-deps --all-features --document-private-items 55 | 56 | lint-success: 57 | if: always() 58 | name: lint success 59 | runs-on: ubuntu-latest 60 | needs: [lint, doc-lint, grafana-lint] 61 | steps: 62 | - name: Decide whether the needed jobs succeeded or failed 63 | uses: re-actors/alls-green@release/v1 64 | with: 65 | jobs: ${{ toJSON(needs) }} -------------------------------------------------------------------------------- /.github/workflows/unit.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | merge_group: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | RUSTFLAGS: -D warnings 10 | CARGO_TERM_COLOR: always 11 | SEED: rustethereumethereumrust 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 15 | cancel-in-progress: true 16 | 17 | name: unit 18 | jobs: 19 | test: 20 | name: test (partition ${{ matrix.partition }}/${{ strategy.job-total }}) 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | partition: [1, 2] 25 | steps: 26 | - name: Checkout sources 27 | uses: actions/checkout@v3 28 | - name: Install toolchain 29 | uses: dtolnay/rust-toolchain@stable 30 | - uses: Swatinem/rust-cache@v2 31 | with: 32 | cache-on-failure: true 33 | 34 | - name: Install latest nextest release 35 | uses: taiki-e/install-action@nextest 36 | 37 | - name: Run tests 38 | run: | 39 | cargo nextest run \ 40 | --locked --workspace --all-features \ 41 | --partition hash:${{ matrix.partition }}/${{ strategy.job-total }} \ 42 | -E 'kind(lib)' -E 'kind(bin)' -E 'kind(proc-macro)' 43 | 44 | doc-test: 45 | name: rustdoc 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v3 49 | - name: Install toolchain 50 | uses: dtolnay/rust-toolchain@stable 51 | - uses: Swatinem/rust-cache@v2 52 | - name: Run doctests 53 | run: cargo test --doc --all --all-features -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | 13 | # Generated by Intellij-based IDEs. 14 | .idea 15 | 16 | # Generated by MacOS 17 | .DS_Store 18 | 19 | # Release artifacts 20 | dist/ 21 | 22 | # VSCode 23 | .vscode -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/mev-share-backend", 4 | "crates/mev-share-sse", 5 | "crates/mev-share-rpc-api", 6 | "crates/mev-share", 7 | 8 | # Internal 9 | "examples", 10 | ] 11 | 12 | # Explicitly set the resolver to version 2, which is the default for packages with edition >= 2021 13 | # https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html 14 | resolver = "2" 15 | 16 | [workspace.package] 17 | version = "0.1.4" 18 | edition = "2021" 19 | rust-version = "1.70" 20 | authors = ["mev-share-rs devs"] 21 | license = "MIT OR Apache-2.0" 22 | homepage = "https://github.com/paradigmxyz/mev-share-rs" 23 | repository = "https://github.com/paradigmxyz/mev-share-rs" 24 | 25 | [workspace.dependencies] 26 | ## workspace crates 27 | mev-share = { path = "crates/mev-share" } 28 | mev-share-sse = { path = "crates/mev-share-sse" } 29 | mev-share-rpc-api = { path = "crates/mev-share-rpc-api" } 30 | 31 | ## eth 32 | alloy-primitives = { version = "1.0.0", features = ["serde"] } 33 | alloy-rpc-types-mev = ">= 1.0" 34 | ethers-core = { version = "2.0", default-features = false } 35 | ethers-signers = "2.0" 36 | ethers-providers = "2.0" 37 | 38 | ## net 39 | http = "0.2.9" 40 | tower = "0.5" 41 | hyper = "0.14" 42 | jsonrpsee = "0.20" 43 | 44 | ## async 45 | futures-util = "0.3" 46 | tokio = "1" 47 | async-trait = "0.1" 48 | 49 | ## misc 50 | serde_json = "1.0" 51 | serde = { version = "1.0", features = ["derive"] } 52 | tracing = "0.1" 53 | thiserror = "2.0" 54 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Matthias Seitz 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 | # mev-share-rs 2 | 3 | Rust utils for [MEV-share](https://github.com/flashbots/mev-share). 4 | 5 | ## Server-Sent-Events 6 | 7 | Subscribe to MEV-Share [event stream](https://github.com/flashbots/mev-share/tree/main/specs/events) as follows: 8 | 9 | ```rs 10 | use futures_util::StreamExt; 11 | use mev_share_sse::EventClient; 12 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 13 | 14 | #[tokio::main] 15 | async fn main() { 16 | tracing_subscriber::registry().with(fmt::layer()).with(EnvFilter::from_default_env()).init(); 17 | 18 | let mainnet_sse = "https://mev-share.flashbots.net"; 19 | let client = EventClient::default(); 20 | let mut stream = client.events(mainnet_sse).await.unwrap(); 21 | println!("Subscribed to {}", stream.endpoint()); 22 | 23 | while let Some(event) = stream.next().await { 24 | dbg!(&event); 25 | } 26 | } 27 | ``` 28 | 29 | ## Sending bundles 30 | 31 | Send [MEV-Share bundles](https://github.com/flashbots/mev-share/tree/main/specs/bundles) as follows: 32 | 33 | ```rs 34 | //! Basic RPC api example 35 | 36 | use jsonrpsee::http_client::{transport::Error as HttpError, HttpClientBuilder}; 37 | use mev_share_rpc_api::{BundleItem, FlashbotsSignerLayer, MevApiClient, SendBundleRequest}; 38 | use tower::ServiceBuilder; 39 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 40 | 41 | use ethers_core::{ 42 | rand::thread_rng, 43 | types::{TransactionRequest, H256}, 44 | }; 45 | use ethers_signers::{LocalWallet, Signer}; 46 | 47 | #[tokio::main] 48 | async fn main() { 49 | tracing_subscriber::registry().with(fmt::layer()).with(EnvFilter::from_default_env()).init(); 50 | 51 | // The signer used to authenticate bundles 52 | let fb_signer = LocalWallet::new(&mut thread_rng()); 53 | 54 | // The signer used to sign our transactions 55 | let tx_signer = LocalWallet::new(&mut thread_rng()); 56 | 57 | // Set up flashbots-style auth middleware 58 | let signing_middleware = FlashbotsSignerLayer::new(fb_signer); 59 | let service_builder = ServiceBuilder::new() 60 | // map signer errors to http errors 61 | .map_err(|e| HttpError::Http(e)) 62 | .layer(signing_middleware); 63 | 64 | // Set up the rpc client 65 | let url = "https://relay.flashbots.net:443"; 66 | let client = HttpClientBuilder::default() 67 | .set_middleware(service_builder) 68 | .build(url) 69 | .expect("Failed to create http client"); 70 | 71 | // Hash of the transaction we are trying to backrun 72 | let tx_hash = H256::random(); 73 | 74 | // Our own tx that we want to include in the bundle 75 | let tx = TransactionRequest::pay("vitalik.eth", 100); 76 | let signature = tx_signer.sign_transaction(&tx.clone().into()).await.unwrap(); 77 | let bytes = tx.rlp_signed(&signature); 78 | 79 | // Build bundle 80 | let mut bundle_body = Vec::new(); 81 | bundle_body.push(BundleItem::Hash { hash: tx_hash }); 82 | bundle_body.push(BundleItem::Tx { tx: bytes, can_revert: false }); 83 | 84 | let bundle = SendBundleRequest { bundle_body, ..Default::default() }; 85 | 86 | // Send bundle 87 | let resp = client.send_bundle(bundle.clone()).await; 88 | println!("Got a bundle response: {:?}", resp); 89 | 90 | // Simulate bundle 91 | let sim_res = client.sim_bundle(bundle, Default::default()).await; 92 | println!("Got a simulation response: {:?}", sim_res); 93 | } 94 | ``` 95 | 96 | 97 | ## License 98 | 99 | Licensed under either of these: 100 | 101 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 102 | https://www.apache.org/licenses/LICENSE-2.0) 103 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 104 | https://opensource.org/licenses/MIT) 105 | -------------------------------------------------------------------------------- /crates/mev-share-backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mev-share-backend" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | description = """ 11 | MEV-share backend support 12 | """ 13 | 14 | [dependencies] 15 | ## eth 16 | mev-share-rpc-api = { path = "../mev-share-rpc-api", features = ["client"] } 17 | 18 | ## async 19 | pin-project-lite = "0.2" 20 | tokio = { workspace = true, features = ["sync"] } 21 | futures-util.workspace = true 22 | 23 | ## misc 24 | thiserror.workspace = true 25 | tracing.workspace = true 26 | -------------------------------------------------------------------------------- /crates/mev-share-backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs, unreachable_pub, unused_crate_dependencies)] 2 | #![deny(unused_must_use, rust_2018_idioms)] 3 | #![doc(test( 4 | no_crate_inject, 5 | attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) 6 | ))] 7 | 8 | //! MEV-Share simulation backend abstractions. 9 | 10 | use mev_share_rpc_api::{SendBundleRequest, SimBundleOverrides, SimBundleResponse}; 11 | use std::{error::Error, future::Future}; 12 | 13 | mod rpc; 14 | mod service; 15 | pub use rpc::RpcSimulator; 16 | pub use service::*; 17 | 18 | /// A type that can start a bundle simulation. 19 | pub trait BundleSimulator: Unpin + Send + Sync { 20 | /// An in progress bundle simulation. 21 | type Simulation: Future + Unpin + Send; 22 | 23 | /// Starts a bundle simulation. 24 | fn simulate_bundle( 25 | &self, 26 | bundle: SendBundleRequest, 27 | sim_overrides: SimBundleOverrides, 28 | ) -> Self::Simulation; 29 | } 30 | 31 | /// Errors that can occur when simulating a bundle. 32 | #[derive(Debug)] 33 | pub enum BundleSimulationOutcome { 34 | /// The simulation was successful. 35 | Success(SimBundleResponse), 36 | /// The simulation failed and is not recoverable. 37 | Fatal(Box), 38 | /// The simulation failed and should be rescheduled. 39 | Reschedule(Box), 40 | } 41 | 42 | /// A simulated bundle. 43 | #[derive(Debug, Clone)] 44 | pub struct SimulatedBundle { 45 | /// The request object that was used for simulation. 46 | pub request: SendBundleRequest, 47 | /// The overrides that were used for simulation. 48 | pub overrides: SimBundleOverrides, 49 | /// The response from the simulation.simulation 50 | pub response: SimBundleResponse, 51 | /// The number of retries that were used for simulation. 52 | pub retries: usize, 53 | } 54 | 55 | impl SimulatedBundle { 56 | /// Returns true if the simulation was successful. 57 | pub fn is_success(&self) -> bool { 58 | self.response.success 59 | } 60 | 61 | /// Returns the profit of the simulation. 62 | pub fn profit(&self) -> u64 { 63 | self.response.profit.as_u64() 64 | } 65 | 66 | /// Returns the gas used by the simulation. 67 | pub fn gas_used(&self) -> u64 { 68 | self.response.gas_used.as_u64() 69 | } 70 | 71 | /// Returns the mev gas price of the simulation. 72 | pub fn mev_gas_price(&self) -> u64 { 73 | self.response.mev_gas_price.as_u64() 74 | } 75 | 76 | /// 77 | pub fn extract_hints(&self) {} 78 | } 79 | -------------------------------------------------------------------------------- /crates/mev-share-backend/src/rpc.rs: -------------------------------------------------------------------------------- 1 | //! RPC simulator implementation. 2 | 3 | use crate::{BundleSimulationOutcome, BundleSimulator}; 4 | use mev_share_rpc_api::{clients::MevApiClient, jsonrpsee, SendBundleRequest, SimBundleOverrides}; 5 | use std::{fmt, fmt::Formatter, future::Future, pin::Pin, sync::Arc}; 6 | 7 | /// A [BundleSimulator] that sends bundles via RPC, see also: [MevApiClient]. 8 | #[derive(Clone)] 9 | pub struct RpcSimulator { 10 | client: Client, 11 | is_err_recoverable: 12 | Arc bool + Unpin + Send + Sync + 'static>, 13 | } 14 | 15 | impl RpcSimulator { 16 | /// Creates a new RPC simulator. 17 | pub fn new(client: Client) -> Self { 18 | Self::with_recoverable_fn(client, is_nonce_too_low) 19 | } 20 | 21 | /// Creates a new RPC simulator with a custom recoverable error function. 22 | /// 23 | /// By default the error is recoverable if it contains the "nonce too low" error. 24 | pub fn with_recoverable_fn(client: Client, f: F) -> Self 25 | where 26 | F: Fn(&jsonrpsee::core::Error) -> bool + Unpin + Send + Sync + 'static, 27 | { 28 | Self { client, is_err_recoverable: Arc::new(f) } 29 | } 30 | } 31 | 32 | impl BundleSimulator for RpcSimulator 33 | where 34 | Client: MevApiClient + Unpin + Clone + Send + Sync + 'static, 35 | { 36 | type Simulation = Pin + Send>>; 37 | 38 | fn simulate_bundle( 39 | &self, 40 | bundle: SendBundleRequest, 41 | sim_overrides: SimBundleOverrides, 42 | ) -> Self::Simulation { 43 | let this = self.clone(); 44 | Box::pin(async move { 45 | match this.client.sim_bundle(bundle, sim_overrides).await { 46 | Ok(res) => BundleSimulationOutcome::Success(res), 47 | Err(err) => { 48 | if (this.is_err_recoverable)(&err) { 49 | BundleSimulationOutcome::Reschedule(Box::new(err)) 50 | } else { 51 | BundleSimulationOutcome::Fatal(Box::new(err)) 52 | } 53 | } 54 | } 55 | }) 56 | } 57 | } 58 | 59 | impl fmt::Debug for RpcSimulator { 60 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 61 | f.debug_struct("RpcSimulator") 62 | .field("client", &"...") 63 | .field("is_err_recoverable", &"...") 64 | .finish() 65 | } 66 | } 67 | 68 | /// whether the error message contains a nonce too low error 69 | /// 70 | /// This is the default recoverable error for RPC simulator 71 | pub(crate) fn is_nonce_too_low(err: &jsonrpsee::core::Error) -> bool { 72 | err.to_string().contains("nonce too low") 73 | } 74 | -------------------------------------------------------------------------------- /crates/mev-share-backend/src/service.rs: -------------------------------------------------------------------------------- 1 | //! Service implementation for queueing simulation stops. 2 | 3 | use std::{ 4 | error::Error, 5 | fmt, 6 | future::Future, 7 | pin::Pin, 8 | sync::{atomic::AtomicU64, Arc}, 9 | task::{ready, Context, Poll}, 10 | time::Instant, 11 | }; 12 | 13 | use crate::{BundleSimulationOutcome, BundleSimulator, SimulatedBundle}; 14 | use futures_util::{stream::FuturesUnordered, Stream, StreamExt}; 15 | use mev_share_rpc_api::{jsonrpsee, SendBundleRequest, SimBundleOverrides}; 16 | use pin_project_lite::pin_project; 17 | use tokio::sync::{mpsc, oneshot}; 18 | use tracing::debug; 19 | 20 | /// Frontend type that can communicate with [BundleSimulatorService]. 21 | #[derive(Debug, Clone)] 22 | pub struct BundleSimulatorHandle { 23 | to_service: mpsc::UnboundedSender, 24 | inner: Arc, 25 | } 26 | 27 | #[derive(Debug)] 28 | struct BundleSimulatorInner { 29 | /// tracks the number of queued jobs. 30 | queued_jobs: AtomicU64, 31 | /// The current block number. 32 | current_block_number: AtomicU64, 33 | } 34 | 35 | // === impl BundleSimulatorHandle === 36 | 37 | impl BundleSimulatorHandle { 38 | /// Returns the number of queued jobs. 39 | pub fn queued_jobs(&self) -> u64 { 40 | self.inner.queued_jobs.load(std::sync::atomic::Ordering::Relaxed) 41 | } 42 | 43 | /// Updates the current block number. 44 | pub fn update_block_number(&self, block_number: u64) { 45 | self.to_service.send(BundleSimulatorMessage::UpdateBlockNumber(block_number)).ok(); 46 | } 47 | 48 | /// Clears all queued jobs. 49 | pub fn clear_queue(&self) { 50 | self.to_service.send(BundleSimulatorMessage::ClearQueue).ok(); 51 | } 52 | 53 | /// Adds a new bundle simulation to the queue with [SimulationPriority::High]. 54 | /// 55 | /// Returns an error when the service failed to queue in the simulation. 56 | pub fn send_bundle_simulation_high_prio( 57 | &self, 58 | request: SendBundleRequest, 59 | overrides: SimBundleOverrides, 60 | ) -> Result<(), AddSimulationErr> { 61 | self.send_bundle_simulation_with_prio(request, overrides, SimulationPriority::High) 62 | } 63 | /// Adds a new bundle simulation to the queue with [SimulationPriority::Normal]. 64 | /// 65 | /// Returns an error when the service failed to queue in the simulation. 66 | pub fn send_bundle_simulation( 67 | &self, 68 | request: SendBundleRequest, 69 | overrides: SimBundleOverrides, 70 | ) -> Result<(), AddSimulationErr> { 71 | self.send_bundle_simulation_with_prio(request, overrides, SimulationPriority::Normal) 72 | } 73 | 74 | /// Adds a new bundle simulation to the queue. 75 | /// 76 | /// Returns an error when the service failed to queue in the simulation. 77 | pub fn send_bundle_simulation_with_prio( 78 | &self, 79 | request: SendBundleRequest, 80 | overrides: SimBundleOverrides, 81 | priority: SimulationPriority, 82 | ) -> Result<(), AddSimulationErr> { 83 | let (tx, ..) = oneshot::channel(); 84 | self.to_service.send(BundleSimulatorMessage::AddSimulation { 85 | request: SimulationRequest { 86 | request, 87 | priority, 88 | overrides, 89 | backed_off_until: None, 90 | retries: 0, 91 | }, 92 | tx, 93 | })?; 94 | Ok(()) 95 | } 96 | 97 | /// Adds a new bundle simulation to the queue with [SimulationPriority::High]. 98 | /// 99 | /// Returns an error when the service failed to queue in the simulation. 100 | pub async fn add_bundle_simulation_high_prio( 101 | &self, 102 | request: SendBundleRequest, 103 | overrides: SimBundleOverrides, 104 | ) -> Result<(), AddSimulationErr> { 105 | self.add_bundle_simulation_with_prio(request, overrides, SimulationPriority::High).await 106 | } 107 | 108 | /// Adds a new bundle simulation to the queue with [SimulationPriority::Normal]. 109 | /// 110 | /// Returns an error when the service failed to queue in the simulation. 111 | pub async fn add_bundle_simulation( 112 | &self, 113 | request: SendBundleRequest, 114 | overrides: SimBundleOverrides, 115 | ) -> Result<(), AddSimulationErr> { 116 | self.add_bundle_simulation_with_prio(request, overrides, SimulationPriority::Normal).await 117 | } 118 | 119 | /// Adds a new bundle simulation to the queue. 120 | /// 121 | /// Returns an error when the service failed to queue in the simulation. 122 | pub async fn add_bundle_simulation_with_prio( 123 | &self, 124 | request: SendBundleRequest, 125 | overrides: SimBundleOverrides, 126 | priority: SimulationPriority, 127 | ) -> Result<(), AddSimulationErr> { 128 | let (tx, rx) = oneshot::channel(); 129 | self.to_service.send(BundleSimulatorMessage::AddSimulation { 130 | request: SimulationRequest { 131 | request, 132 | priority, 133 | overrides, 134 | backed_off_until: None, 135 | retries: 0, 136 | }, 137 | tx, 138 | })?; 139 | 140 | rx.await.map_err(|_| AddSimulationErr::ServiceUnavailable)? 141 | } 142 | 143 | /// Returns a new listener for simulation events. 144 | pub fn events(&self) -> SimulationEventStream { 145 | let (tx, rx) = mpsc::unbounded_channel(); 146 | let _ = self.to_service.send(BundleSimulatorMessage::AddEventListener(tx)); 147 | SimulationEventStream { inner: rx } 148 | } 149 | } 150 | 151 | /// Provides a service for simulating bundles. 152 | #[must_use = "futures do nothing unless you `.await` or poll them"] 153 | pub struct BundleSimulatorService { 154 | /// Creates new simulations. 155 | simulator: Sim, 156 | /// The current simulations. 157 | simulations: FuturesUnordered>, 158 | high_priority_queue: Vec, 159 | normal_priority_queue: Vec, 160 | /// incoming messages from the handle. 161 | from_handle: mpsc::UnboundedReceiver, 162 | /// Copy of the handle sender to keep the channel open. 163 | to_service: mpsc::UnboundedSender, 164 | /// Shared internals 165 | inner: Arc, 166 | /// Listeners for simulation events. 167 | listeners: Vec>, 168 | /// How this service is configured. 169 | config: BundleSimulatorServiceConfig, 170 | } 171 | 172 | impl BundleSimulatorService { 173 | /// Creates a new [BundleSimulatorService] with the given simulator. 174 | pub fn new( 175 | current_block_number: u64, 176 | simulator: Sim, 177 | config: BundleSimulatorServiceConfig, 178 | ) -> Self { 179 | let (to_service, from_handle) = mpsc::unbounded_channel(); 180 | let inner = Arc::new(BundleSimulatorInner { 181 | queued_jobs: AtomicU64::new(0), 182 | current_block_number: AtomicU64::new(current_block_number), 183 | }); 184 | Self { 185 | simulator, 186 | simulations: Default::default(), 187 | high_priority_queue: Default::default(), 188 | normal_priority_queue: Default::default(), 189 | from_handle, 190 | to_service, 191 | inner, 192 | listeners: vec![], 193 | config, 194 | } 195 | } 196 | 197 | /// Returns a new handle to this service 198 | pub fn handle(&self) -> BundleSimulatorHandle { 199 | BundleSimulatorHandle { 200 | to_service: self.to_service.clone(), 201 | inner: Arc::clone(&self.inner), 202 | } 203 | } 204 | 205 | fn current_block_number(&self) -> u64 { 206 | self.inner.current_block_number.load(std::sync::atomic::Ordering::Relaxed) 207 | } 208 | 209 | /// Notifies all listeners of the given event. 210 | fn notify_listeners(&mut self, event: SimulationEvent) { 211 | self.listeners.retain(|l| l.send(event.clone()).is_ok()); 212 | } 213 | 214 | fn has_high_priority_capacity(&self) -> bool { 215 | self.high_priority_queue.len() < self.config.max_high_prio_queue_len 216 | } 217 | 218 | fn has_normal_priority_capacity(&self) -> bool { 219 | self.normal_priority_queue.len() < self.config.max_normal_prio_queue_len 220 | } 221 | 222 | fn pop_next_ready(&mut self, next_block: u64, now: Instant) -> Option { 223 | loop { 224 | let mut outdated = false; 225 | let pos = self 226 | .high_priority_queue 227 | .iter() 228 | .chain(self.normal_priority_queue.iter()) 229 | // skip requests not ready for inclusion 230 | .position(|req| { 231 | if !req.is_ready_at(now) { 232 | return false 233 | } 234 | if req.exceeds_target_block(next_block) { 235 | outdated = true; 236 | return true 237 | } 238 | req.is_min_block(next_block) 239 | }); 240 | 241 | if let Some(mut pos) = pos { 242 | let item = if pos < self.high_priority_queue.len() { 243 | self.high_priority_queue.remove(pos) 244 | } else { 245 | pos -= self.high_priority_queue.len(); 246 | self.normal_priority_queue.remove(pos) 247 | }; 248 | 249 | if outdated { 250 | self.notify_listeners(SimulationEvent::OutdatedRequest { 251 | request: item.request, 252 | overrides: item.overrides, 253 | next_block, 254 | }); 255 | continue 256 | } 257 | 258 | return Some(item) 259 | } 260 | 261 | return None 262 | } 263 | } 264 | 265 | /// Returns a [SimulationRequest] that is ready to be simulated. 266 | fn pop_best_requests(&mut self, now: Instant) -> Vec { 267 | let mut requests = vec![]; 268 | let capacity = 269 | self.config.max_concurrent_simulations.saturating_sub(self.simulations.len()); 270 | if capacity == 0 { 271 | return requests 272 | } 273 | 274 | let current_block = self.current_block_number(); 275 | let next_block = current_block + 1; 276 | 277 | while requests.len() != capacity { 278 | if let Some(req) = self.pop_next_ready(next_block, now) { 279 | requests.push(req); 280 | } else { 281 | break 282 | } 283 | } 284 | 285 | requests 286 | } 287 | 288 | /// Processes a new message from the handle. 289 | fn on_message(&mut self, msg: BundleSimulatorMessage) { 290 | debug!("Received message: {:?}", msg); 291 | match msg { 292 | BundleSimulatorMessage::UpdateBlockNumber(num) => { 293 | let current_block_number = self.current_block_number(); 294 | if num != current_block_number { 295 | let old = current_block_number; 296 | self.inner 297 | .current_block_number 298 | .store(num, std::sync::atomic::Ordering::Relaxed); 299 | self.notify_listeners(SimulationEvent::BlockNumberUpdated { 300 | old, 301 | current: current_block_number, 302 | }); 303 | } 304 | } 305 | BundleSimulatorMessage::ClearQueue => { 306 | self.high_priority_queue.clear(); 307 | self.normal_priority_queue.clear(); 308 | } 309 | BundleSimulatorMessage::AddEventListener(tx) => { 310 | self.listeners.push(tx); 311 | } 312 | BundleSimulatorMessage::AddSimulation { request, tx } => { 313 | if request.priority.is_high() { 314 | if self.has_high_priority_capacity() { 315 | self.high_priority_queue.push(request); 316 | tx.send(Ok(())).ok(); 317 | } else { 318 | tx.send(Err(AddSimulationErr::QueueFull)).ok(); 319 | } 320 | } else if self.has_normal_priority_capacity() { 321 | self.normal_priority_queue.push(request); 322 | tx.send(Ok(())).ok(); 323 | } else { 324 | tx.send(Err(AddSimulationErr::QueueFull)).ok(); 325 | } 326 | } 327 | } 328 | } 329 | 330 | /// Processes a finished simulation. 331 | fn on_simulation_outcome( 332 | &mut self, 333 | outcome: BundleSimulationOutcome, 334 | mut sim: SimulationRequest, 335 | ) { 336 | debug!("Received simulation outcome: {:?}", outcome); 337 | match outcome { 338 | BundleSimulationOutcome::Success(resp) => { 339 | let SimulationRequest { retries, request, overrides, .. } = sim; 340 | self.notify_listeners(SimulationEvent::SimulatedBundle(Ok(SimulatedBundle { 341 | request, 342 | overrides, 343 | response: resp, 344 | retries, 345 | }))); 346 | } 347 | BundleSimulationOutcome::Fatal(error) => { 348 | let err = SimulatedBundleErrorInner { error, sim }; 349 | self.notify_listeners(SimulationEvent::SimulatedBundle(Err( 350 | SimulatedBundleError { inner: Arc::new(err) }, 351 | ))); 352 | } 353 | BundleSimulationOutcome::Reschedule(_error) => { 354 | // reschedule the simulation. 355 | sim.retries += 1; 356 | if sim.retries > self.config.max_retries { 357 | self.notify_listeners(SimulationEvent::ExceededMaxRetries { 358 | request: sim.request, 359 | overrides: sim.overrides, 360 | }); 361 | return 362 | } 363 | if sim.priority.is_high() { 364 | self.high_priority_queue.push(sim); 365 | } else { 366 | self.normal_priority_queue.push(sim); 367 | } 368 | } 369 | } 370 | } 371 | } 372 | 373 | impl Future for BundleSimulatorService 374 | where 375 | Sim: BundleSimulator, 376 | { 377 | type Output = (); 378 | 379 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 380 | let this = self.get_mut(); 381 | 382 | loop { 383 | // drain incoming messages from the handle. 384 | while let Poll::Ready(Some(msg)) = this.from_handle.poll_recv(cx) { 385 | this.on_message(msg); 386 | } 387 | 388 | // process all completed simulations. 389 | while let Poll::Ready(Some((outcome, inner))) = this.simulations.poll_next_unpin(cx) { 390 | this.on_simulation_outcome(outcome, inner); 391 | } 392 | 393 | // try to queue in a new simulation request 394 | let ready = this.pop_best_requests(Instant::now()); 395 | let no_more_work = ready.is_empty(); 396 | for req in ready { 397 | let sim = 398 | this.simulator.simulate_bundle(req.request.clone(), req.overrides.clone()); 399 | let sim = Simulation::new(sim, req); 400 | debug!("Starting new simulation"); 401 | this.simulations.push(sim); 402 | } 403 | 404 | if no_more_work { 405 | break 406 | } 407 | } 408 | 409 | Poll::Pending 410 | } 411 | } 412 | 413 | /// Config values for the [BundleSimulatorService]. 414 | #[derive(Debug, Clone)] 415 | pub struct BundleSimulatorServiceConfig { 416 | /// Maximum number of retries for a simulation. 417 | pub max_retries: usize, 418 | /// Maximum number of _unprocessed_ jobs in the normal priority queue. 419 | pub max_normal_prio_queue_len: usize, 420 | /// Maximum number of _unprocessed_ jobs in the high priority queue. 421 | pub max_high_prio_queue_len: usize, 422 | /// Maximum number of concurrently active simulations. 423 | pub max_concurrent_simulations: usize, 424 | } 425 | 426 | impl Default for BundleSimulatorServiceConfig { 427 | fn default() -> Self { 428 | Self { 429 | max_retries: 30, 430 | max_normal_prio_queue_len: 1024, 431 | max_high_prio_queue_len: 2048, 432 | max_concurrent_simulations: 32, 433 | } 434 | } 435 | } 436 | 437 | pin_project! { 438 | /// A simulation future 439 | struct Simulation { 440 | #[pin] 441 | sim: Sim, 442 | inner: Option 443 | } 444 | } 445 | 446 | impl Simulation { 447 | /// Creates a new simulation. 448 | fn new(sim: Sim, inner: SimulationRequest) -> Self { 449 | Self { sim, inner: Some(inner) } 450 | } 451 | } 452 | 453 | impl Future for Simulation 454 | where 455 | Sim: Future, 456 | { 457 | type Output = (Sim::Output, SimulationRequest); 458 | 459 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 460 | let this = self.project(); 461 | let out = ready!(this.sim.poll(cx)); 462 | let inner = this.inner.take().expect("Simulation polled after completion"); 463 | Poll::Ready((out, inner)) 464 | } 465 | } 466 | 467 | /// Error thrown when adding a simulation fails. 468 | #[derive(Debug, thiserror::Error)] 469 | pub enum AddSimulationError { 470 | /// Thrown when too many jobs are queued. 471 | #[error("too many jobs: {0}")] 472 | TooManyJobs(u64), 473 | } 474 | 475 | /// Message type passed to [BundleSimulatorService]. 476 | #[derive(Debug)] 477 | enum BundleSimulatorMessage { 478 | /// Clear all ongoing jobs. 479 | ClearQueue, 480 | /// Set current block number 481 | UpdateBlockNumber(u64), 482 | /// Add a new simulation job 483 | AddSimulation { request: SimulationRequest, tx: oneshot::Sender> }, 484 | /// Queues in a new event listener. 485 | AddEventListener(mpsc::UnboundedSender), 486 | } 487 | 488 | /// Events emitted by the simulation service. 489 | #[derive(Debug, Clone)] 490 | pub enum SimulationEvent { 491 | /// Result of a simulated bundle. 492 | SimulatedBundle(Result), 493 | /// A request has been dropped because it's max number exceeds the next block 494 | OutdatedRequest { 495 | /// The request object that was used for simulation. 496 | request: SendBundleRequest, 497 | /// The overrides that were used for simulation. 498 | overrides: SimBundleOverrides, 499 | /// Currently tracked next block 500 | next_block: u64, 501 | }, 502 | /// A request has been dropped because it's max number of retries has been exceeded. 503 | ExceededMaxRetries { 504 | /// The request object that was used for simulation. 505 | request: SendBundleRequest, 506 | /// The overrides that were used for simulation. 507 | overrides: SimBundleOverrides, 508 | }, 509 | /// Updated block number 510 | BlockNumberUpdated { 511 | /// replaced block number. 512 | old: u64, 513 | /// new block number. 514 | current: u64, 515 | }, 516 | } 517 | 518 | pin_project! { 519 | /// A stream that yields simulation events. 520 | pub struct SimulationEventStream { 521 | #[pin] 522 | inner: mpsc::UnboundedReceiver, 523 | } 524 | } 525 | 526 | impl SimulationEventStream { 527 | /// Only yields simulation results 528 | pub fn results(self) -> SimulationResultStream { 529 | SimulationResultStream { inner: self.inner } 530 | } 531 | } 532 | 533 | impl Stream for SimulationEventStream { 534 | type Item = SimulationEvent; 535 | 536 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 537 | self.project().inner.poll_recv(cx) 538 | } 539 | } 540 | 541 | pin_project! { 542 | /// A stream that yields outcome of simulations. 543 | pub struct SimulationResultStream { 544 | #[pin] 545 | inner: mpsc::UnboundedReceiver, 546 | } 547 | } 548 | 549 | impl Stream for SimulationResultStream { 550 | type Item = Result; 551 | 552 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 553 | loop { 554 | return match ready!(self.as_mut().project().inner.poll_recv(cx)) { 555 | None => Poll::Ready(None), 556 | Some(SimulationEvent::SimulatedBundle(res)) => Poll::Ready(Some(res)), 557 | _ => continue, 558 | } 559 | } 560 | } 561 | } 562 | 563 | /// Failed to simulate a bundle. 564 | #[derive(Debug, Clone)] 565 | pub struct SimulatedBundleError { 566 | inner: Arc, 567 | } 568 | 569 | impl SimulatedBundleError { 570 | /// Returns the simulation request that failed. 571 | pub fn request(&self) -> &SimulationRequest { 572 | &self.inner.sim 573 | } 574 | 575 | /// Consumes the type and returns the simulation request that failed. 576 | pub fn into_request(self) -> SimulationRequest { 577 | match Arc::try_unwrap(self.inner) { 578 | Ok(res) => res.sim, 579 | Err(inner) => inner.sim.clone(), 580 | } 581 | } 582 | 583 | /// Attempts to downcast the error to a jsonrpsee error. 584 | pub fn as_rpc_error(&self) -> Option<&jsonrpsee::core::Error> { 585 | self.inner.error.downcast_ref() 586 | } 587 | } 588 | 589 | impl fmt::Display for SimulatedBundleError { 590 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 591 | self.inner.error.fmt(f) 592 | } 593 | } 594 | 595 | impl Error for SimulatedBundleError { 596 | fn source(&self) -> Option<&(dyn Error + 'static)> { 597 | Some(&*self.inner.error) 598 | } 599 | } 600 | 601 | #[derive(Debug)] 602 | struct SimulatedBundleErrorInner { 603 | error: Box, 604 | sim: SimulationRequest, 605 | } 606 | 607 | /// How to queue in a simulation. 608 | #[derive(Debug, Clone, Default)] 609 | pub enum SimulationPriority { 610 | /// The simulation is not urgent. 611 | #[default] 612 | Normal, 613 | /// The simulation should be prioritized. 614 | High, 615 | } 616 | 617 | impl SimulationPriority { 618 | /// Returns whether the priority is high. 619 | pub fn is_high(&self) -> bool { 620 | matches!(self, Self::High) 621 | } 622 | 623 | /// Returns whether the priority is normal. 624 | pub fn is_normal(&self) -> bool { 625 | matches!(self, Self::Normal) 626 | } 627 | } 628 | 629 | /// A request to simulate a bundle. 630 | #[derive(Debug, Clone)] 631 | pub struct SimulationRequest { 632 | /// How often this has been retried 633 | pub retries: usize, 634 | /// The request object that was used for simulation. 635 | pub request: SendBundleRequest, 636 | /// The overrides that were used for simulation. 637 | pub overrides: SimBundleOverrides, 638 | /// The priority of the simulation. 639 | pub priority: SimulationPriority, 640 | /// The timestamp when the request can be resent again. 641 | pub backed_off_until: Option, 642 | } 643 | 644 | impl SimulationRequest { 645 | fn is_min_block(&self, block: u64) -> bool { 646 | block > self.request.inclusion.block_number() 647 | } 648 | 649 | fn is_ready_at(&self, now: Instant) -> bool { 650 | self.backed_off_until.map_or(true, |backoff| now > backoff) 651 | } 652 | 653 | fn exceeds_target_block(&self, block: u64) -> bool { 654 | self.request.inclusion.max_block_number().map(|target| block > target).unwrap_or_default() 655 | } 656 | } 657 | 658 | /// Errors that can occur when adding a simulation job. 659 | #[derive(Debug, thiserror::Error)] 660 | pub enum AddSimulationErr { 661 | /// Thrown when the queue is full 662 | #[error("queue full")] 663 | QueueFull, 664 | /// Thrown when the service is unavailable (dropped). 665 | #[error("simulation service unavailable")] 666 | ServiceUnavailable, 667 | } 668 | 669 | impl From> for AddSimulationErr { 670 | fn from(_: mpsc::error::SendError) -> Self { 671 | Self::ServiceUnavailable 672 | } 673 | } 674 | -------------------------------------------------------------------------------- /crates/mev-share-rpc-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mev-share-rpc-api" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | description = """ 11 | MEV-share RPC API trait definitions 12 | """ 13 | 14 | [dependencies] 15 | ## eth 16 | ethers-core.workspace = true 17 | ethers-signers.workspace = true 18 | 19 | ## misc 20 | jsonrpsee = { workspace = true, features = ["server", "macros"] } 21 | serde.workspace = true 22 | serde_json.workspace = true 23 | http.workspace = true 24 | hyper = { workspace = true, features = ["stream"] } 25 | tower.workspace = true 26 | futures-util.workspace = true 27 | async-trait.workspace = true 28 | 29 | 30 | [dev-dependencies] 31 | serde_json.workspace = true 32 | tokio = { workspace = true, features = ["full"] } 33 | 34 | 35 | [features] 36 | default = ["client"] 37 | client = ["jsonrpsee/client"] 38 | server = ["jsonrpsee/server"] 39 | -------------------------------------------------------------------------------- /crates/mev-share-rpc-api/README.md: -------------------------------------------------------------------------------- 1 | # mev-share-rpc-api 2 | 3 | RPC API for [mev-share-rs](https://github.com/flashbots/mev-share/) 4 | 5 | Includes server and client trait bindings. -------------------------------------------------------------------------------- /crates/mev-share-rpc-api/src/auth.rs: -------------------------------------------------------------------------------- 1 | //! A layer responsible for implementing flashbots-style authentication 2 | //! by signing the request body with a private key and adding the signature 3 | //! to the request headers. 4 | 5 | use std::{ 6 | error::Error, 7 | task::{Context, Poll}, 8 | }; 9 | 10 | use ethers_core::{types::H256, utils::keccak256}; 11 | use ethers_signers::Signer; 12 | use futures_util::future::BoxFuture; 13 | 14 | use http::{header::HeaderValue, HeaderName, Request}; 15 | use hyper::Body; 16 | 17 | use tower::{Layer, Service}; 18 | 19 | static FLASHBOTS_HEADER: HeaderName = HeaderName::from_static("x-flashbots-signature"); 20 | 21 | /// Layer that applies [`FlashbotsSigner`] which adds a request header with a signed payload. 22 | #[derive(Clone)] 23 | pub struct FlashbotsSignerLayer { 24 | signer: S, 25 | } 26 | 27 | impl FlashbotsSignerLayer { 28 | /// Creates a new [`FlashbotsSignerLayer`] with the given signer. 29 | pub fn new(signer: S) -> Self { 30 | FlashbotsSignerLayer { signer } 31 | } 32 | } 33 | 34 | impl Layer for FlashbotsSignerLayer { 35 | type Service = FlashbotsSigner; 36 | 37 | fn layer(&self, inner: I) -> Self::Service { 38 | FlashbotsSigner { signer: self.signer.clone(), inner } 39 | } 40 | } 41 | 42 | /// Middleware that signs the request body and adds the signature to the x-flashbots-signature 43 | /// header. For more info, see 44 | #[derive(Clone)] 45 | pub struct FlashbotsSigner { 46 | signer: S, 47 | inner: I, 48 | } 49 | 50 | impl Service> for FlashbotsSigner 51 | where 52 | I: Service> + Clone + Send + 'static, 53 | I::Future: Send, 54 | I::Error: Into> + 'static, 55 | S: Signer + Clone + Send + 'static, 56 | { 57 | type Response = I::Response; 58 | type Error = Box; 59 | type Future = BoxFuture<'static, Result>; 60 | 61 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 62 | self.inner.poll_ready(cx).map_err(Into::into) 63 | } 64 | 65 | fn call(&mut self, request: Request) -> Self::Future { 66 | let clone = self.inner.clone(); 67 | // wait for service to be ready 68 | let mut inner = std::mem::replace(&mut self.inner, clone); 69 | let signer = self.signer.clone(); 70 | 71 | let (mut parts, body) = request.into_parts(); 72 | 73 | // if method is not POST, return an error. 74 | if parts.method != http::Method::POST { 75 | return Box::pin(async move { 76 | Err(format!("Invalid method: {}", parts.method.as_str()).into()) 77 | }) 78 | } 79 | 80 | // if content-type is not json, or signature already exists, just pass through the request 81 | let is_json = parts 82 | .headers 83 | .get(http::header::CONTENT_TYPE) 84 | .map(|v| v == HeaderValue::from_static("application/json")) 85 | .unwrap_or(false); 86 | let has_sig = parts.headers.contains_key(FLASHBOTS_HEADER.clone()); 87 | 88 | if !is_json || has_sig { 89 | return Box::pin(async move { 90 | let request = Request::from_parts(parts, body); 91 | inner.call(request).await.map_err(Into::into) 92 | }) 93 | } 94 | 95 | // otherwise, sign the request body and add the signature to the header 96 | Box::pin(async move { 97 | let body_bytes = hyper::body::to_bytes(body).await?; 98 | 99 | // sign request body and insert header 100 | let signature = signer 101 | .sign_message(format!("0x{:x}", H256::from(keccak256(body_bytes.as_ref())))) 102 | .await?; 103 | 104 | let header_val = 105 | HeaderValue::from_str(&format!("{:?}:0x{}", signer.address(), signature)) 106 | .expect("Header contains invalid characters"); 107 | parts.headers.insert(FLASHBOTS_HEADER.clone(), header_val); 108 | 109 | let request = Request::from_parts(parts, Body::from(body_bytes.clone())); 110 | inner.call(request).await.map_err(Into::into) 111 | }) 112 | } 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use super::*; 118 | use ethers_core::rand::thread_rng; 119 | use ethers_signers::LocalWallet; 120 | use http::Response; 121 | use hyper::Body; 122 | use std::convert::Infallible; 123 | use tower::{service_fn, ServiceExt}; 124 | 125 | #[tokio::test] 126 | async fn test_signature() { 127 | let fb_signer = LocalWallet::new(&mut thread_rng()); 128 | 129 | // mock service that returns the request headers 130 | let svc = FlashbotsSigner { 131 | signer: fb_signer.clone(), 132 | inner: service_fn(|_req: Request| async { 133 | let (parts, _) = _req.into_parts(); 134 | 135 | let mut res = Response::builder(); 136 | for (k, v) in parts.headers.iter() { 137 | res = res.header(k, v); 138 | } 139 | let res = res.body(Body::empty()).unwrap(); 140 | Ok::<_, Infallible>(res) 141 | }), 142 | }; 143 | 144 | // build request 145 | let bytes = vec![1u8; 32]; 146 | let req = Request::builder() 147 | .method(http::Method::POST) 148 | .header(http::header::CONTENT_TYPE, "application/json") 149 | .body(Body::from(bytes.clone())) 150 | .unwrap(); 151 | 152 | let res = svc.oneshot(req).await.unwrap(); 153 | 154 | let header = res.headers().get("x-flashbots-signature").unwrap(); 155 | let header = header.to_str().unwrap(); 156 | let header = header.split(":0x").collect::>(); 157 | let header_address = header[0]; 158 | let header_signature = header[1]; 159 | 160 | let signer_address = format!("{:?}", fb_signer.address()); 161 | let expected_signature = fb_signer 162 | .sign_message(format!("0x{:x}", H256::from(keccak256(bytes.clone())))) 163 | .await 164 | .unwrap() 165 | .to_string(); 166 | 167 | // verify that the header contains expected address and signature 168 | assert_eq!(header_address, signer_address); 169 | assert_eq!(header_signature, expected_signature); 170 | } 171 | 172 | #[tokio::test] 173 | async fn test_skips_non_json() { 174 | let fb_signer = LocalWallet::new(&mut thread_rng()); 175 | 176 | // mock service that returns the request headers 177 | let svc = FlashbotsSigner { 178 | signer: fb_signer.clone(), 179 | inner: service_fn(|_req: Request| async { 180 | let (parts, _) = _req.into_parts(); 181 | 182 | let mut res = Response::builder(); 183 | for (k, v) in parts.headers.iter() { 184 | res = res.header(k, v); 185 | } 186 | let res = res.body(Body::empty()).unwrap(); 187 | Ok::<_, Infallible>(res) 188 | }), 189 | }; 190 | 191 | // build plain text request 192 | let bytes = vec![1u8; 32]; 193 | let req = Request::builder() 194 | .method(http::Method::POST) 195 | .header(http::header::CONTENT_TYPE, "text/plain") 196 | .body(Body::from(bytes.clone())) 197 | .unwrap(); 198 | 199 | let res = svc.oneshot(req).await.unwrap(); 200 | 201 | // response should not contain a signature header 202 | let header = res.headers().get("x-flashbots-signature"); 203 | assert!(header.is_none()); 204 | } 205 | 206 | #[tokio::test] 207 | async fn test_returns_error_when_not_post() { 208 | let fb_signer = LocalWallet::new(&mut thread_rng()); 209 | 210 | // mock service that returns the request headers 211 | let svc = FlashbotsSigner { 212 | signer: fb_signer.clone(), 213 | inner: service_fn(|_req: Request| async { 214 | let (parts, _) = _req.into_parts(); 215 | 216 | let mut res = Response::builder(); 217 | for (k, v) in parts.headers.iter() { 218 | res = res.header(k, v); 219 | } 220 | let res = res.body(Body::empty()).unwrap(); 221 | Ok::<_, Infallible>(res) 222 | }), 223 | }; 224 | 225 | // build plain text request 226 | let bytes = vec![1u8; 32]; 227 | let req = Request::builder() 228 | .method(http::Method::GET) 229 | .header(http::header::CONTENT_TYPE, "application/json") 230 | .body(Body::from(bytes.clone())) 231 | .unwrap(); 232 | 233 | let res = svc.oneshot(req).await; 234 | 235 | // should be an error 236 | assert!(res.is_err()); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /crates/mev-share-rpc-api/src/eth.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | CancelBundleRequest, CancelPrivateTransactionRequest, EthBundleHash, EthCallBundleResponse, 3 | EthCallBundleTransactionResult, EthSendBundle, PrivateTransactionRequest, 4 | }; 5 | use ethers_core::types::{Bytes, H256}; 6 | 7 | // re-export the rpc server trait 8 | #[cfg(feature = "server")] 9 | pub use rpc::EthBundleApiServer; 10 | 11 | /// jsonrpsee generated code. 12 | /// 13 | /// This hides the generated client trait which is replaced by the `EthBundleApiClient` trait. 14 | mod rpc { 15 | use crate::{ 16 | CancelBundleRequest, CancelPrivateTransactionRequest, EthBundleHash, EthCallBundleResponse, 17 | EthCallBundleTransactionResult, EthSendBundle, PrivateTransactionRequest, 18 | }; 19 | use ethers_core::types::{Bytes, H256}; 20 | use jsonrpsee::proc_macros::rpc; 21 | 22 | /// Eth bundle rpc interface. 23 | /// 24 | /// See also 25 | #[cfg_attr(not(feature = "client"), rpc(server, namespace = "eth"))] 26 | #[cfg_attr(all(feature = "client", feature = "server"), rpc(server, client, namespace = "eth"))] 27 | #[cfg_attr(not(feature = "server"), rpc(client, namespace = "eth"))] 28 | #[async_trait::async_trait] 29 | pub trait EthBundleApi { 30 | /// `eth_sendBundle` can be used to send your bundles to the builder. 31 | #[method(name = "sendBundle")] 32 | async fn send_bundle( 33 | &self, 34 | bundle: EthSendBundle, 35 | ) -> jsonrpsee::core::RpcResult; 36 | 37 | /// `eth_callBundle` can be used to simulate a bundle against a specific block number, 38 | /// including simulating a bundle at the top of the next block. 39 | #[method(name = "callBundle")] 40 | async fn call_bundle( 41 | &self, 42 | request: EthCallBundleResponse, 43 | ) -> jsonrpsee::core::RpcResult; 44 | 45 | /// `eth_cancelBundle` is used to prevent a submitted bundle from being included on-chain. See [bundle cancellations](https://docs.flashbots.net/flashbots-auction/searchers/advanced/bundle-cancellations) for more information. 46 | #[method(name = "cancelBundle")] 47 | async fn cancel_bundle( 48 | &self, 49 | request: CancelBundleRequest, 50 | ) -> jsonrpsee::core::RpcResult<()>; 51 | 52 | /// `eth_sendPrivateTransaction` is used to send a single transaction to Flashbots. Flashbots will attempt to build a block including the transaction for the next 25 blocks. See [Private Transactions](https://docs.flashbots.net/flashbots-protect/additional-documentation/eth-sendPrivateTransaction) for more info. 53 | #[method(name = "sendPrivateTransaction")] 54 | async fn send_private_transaction( 55 | &self, 56 | request: PrivateTransactionRequest, 57 | ) -> jsonrpsee::core::RpcResult; 58 | 59 | /// The `eth_sendPrivateRawTransaction` method can be used to send private transactions to 60 | /// the RPC endpoint. Private transactions are protected from frontrunning and kept 61 | /// private until included in a block. A request to this endpoint needs to follow 62 | /// the standard eth_sendRawTransaction 63 | #[method(name = "sendPrivateRawTransaction")] 64 | async fn send_private_raw_transaction( 65 | &self, 66 | bytes: Bytes, 67 | ) -> jsonrpsee::core::RpcResult; 68 | 69 | /// The `eth_cancelPrivateTransaction` method stops private transactions from being 70 | /// submitted for future blocks. 71 | /// 72 | /// A transaction can only be cancelled if the request is signed by the same key as the 73 | /// eth_sendPrivateTransaction call submitting the transaction in first place. 74 | #[method(name = "cancelPrivateTransaction")] 75 | async fn cancel_private_transaction( 76 | &self, 77 | request: CancelPrivateTransactionRequest, 78 | ) -> jsonrpsee::core::RpcResult; 79 | } 80 | } 81 | 82 | /// An object safe version of the `EthBundleApiClient` trait. 83 | #[cfg(feature = "client")] 84 | #[async_trait::async_trait] 85 | pub trait EthBundleApiClient { 86 | /// `eth_sendBundle` can be used to send your bundles to the builder. 87 | async fn send_bundle( 88 | &self, 89 | bundle: EthSendBundle, 90 | ) -> Result; 91 | 92 | /// `eth_callBundle` can be used to simulate a bundle against a specific block number, including 93 | /// simulating a bundle at the top of the next block. 94 | async fn call_bundle( 95 | &self, 96 | request: EthCallBundleResponse, 97 | ) -> Result; 98 | 99 | /// `eth_cancelBundle` is used to prevent a submitted bundle from being included on-chain. See [bundle cancellations](https://docs.flashbots.net/flashbots-auction/searchers/advanced/bundle-cancellations) for more information. 100 | async fn cancel_bundle( 101 | &self, 102 | request: CancelBundleRequest, 103 | ) -> Result<(), jsonrpsee::core::Error>; 104 | 105 | /// `eth_sendPrivateTransaction` is used to send a single transaction to Flashbots. Flashbots will attempt to build a block including the transaction for the next 25 blocks. See [Private Transactions](https://docs.flashbots.net/flashbots-protect/additional-documentation/eth-sendPrivateTransaction) for more info. 106 | async fn send_private_transaction( 107 | &self, 108 | request: PrivateTransactionRequest, 109 | ) -> Result; 110 | 111 | /// The `eth_sendPrivateRawTransaction` method can be used to send private transactions to the 112 | /// RPC endpoint. Private transactions are protected from frontrunning and kept private until 113 | /// included in a block. A request to this endpoint needs to follow the standard 114 | /// eth_sendRawTransaction 115 | async fn send_private_raw_transaction( 116 | &self, 117 | bytes: Bytes, 118 | ) -> Result; 119 | 120 | /// The `eth_cancelPrivateTransaction` method stops private transactions from being submitted 121 | /// for future blocks. 122 | /// 123 | /// A transaction can only be cancelled if the request is signed by the same key as the 124 | /// eth_sendPrivateTransaction call submitting the transaction in first place. 125 | async fn cancel_private_transaction( 126 | &self, 127 | request: CancelPrivateTransactionRequest, 128 | ) -> Result; 129 | } 130 | 131 | #[cfg(feature = "client")] 132 | #[async_trait::async_trait] 133 | impl EthBundleApiClient for T 134 | where 135 | T: rpc::EthBundleApiClient + Sync, 136 | { 137 | async fn send_bundle( 138 | &self, 139 | bundle: EthSendBundle, 140 | ) -> Result { 141 | rpc::EthBundleApiClient::send_bundle(self, bundle).await 142 | } 143 | 144 | async fn call_bundle( 145 | &self, 146 | request: EthCallBundleResponse, 147 | ) -> Result { 148 | rpc::EthBundleApiClient::call_bundle(self, request).await 149 | } 150 | 151 | async fn cancel_bundle( 152 | &self, 153 | request: CancelBundleRequest, 154 | ) -> Result<(), jsonrpsee::core::Error> { 155 | rpc::EthBundleApiClient::cancel_bundle(self, request).await 156 | } 157 | 158 | async fn send_private_transaction( 159 | &self, 160 | request: PrivateTransactionRequest, 161 | ) -> Result { 162 | rpc::EthBundleApiClient::send_private_transaction(self, request).await 163 | } 164 | 165 | async fn send_private_raw_transaction( 166 | &self, 167 | bytes: Bytes, 168 | ) -> Result { 169 | rpc::EthBundleApiClient::send_private_raw_transaction(self, bytes).await 170 | } 171 | 172 | async fn cancel_private_transaction( 173 | &self, 174 | request: CancelPrivateTransactionRequest, 175 | ) -> Result { 176 | rpc::EthBundleApiClient::cancel_private_transaction(self, request).await 177 | } 178 | } 179 | 180 | #[cfg(all(test, feature = "client"))] 181 | mod tests { 182 | use super::*; 183 | 184 | #[allow(dead_code)] 185 | struct Client { 186 | inner: Box, 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /crates/mev-share-rpc-api/src/flashbots.rs: -------------------------------------------------------------------------------- 1 | use crate::{BundleStats, UserStats}; 2 | use ethers_core::types::{H256, U64}; 3 | 4 | // re-export the rpc server trait 5 | #[cfg(feature = "server")] 6 | pub use rpc::FlashbotsApiServer; 7 | 8 | /// Generates a client using [jsonrpsee_proc_macros]. 9 | /// 10 | /// This hides the generated client trait which is replaced by the [`super::FlashbotsApiClient`] 11 | /// trait. 12 | /// 13 | /// [jsonrpsee_proc_macros]: https://docs.rs/jsonrpsee-proc-macros/0.20.0/jsonrpsee_proc_macros/attr.rpc.html 14 | mod rpc { 15 | use crate::{BundleStats, UserStats}; 16 | use ethers_core::types::{H256, U64}; 17 | use jsonrpsee::proc_macros::rpc; 18 | use serde::{Deserialize, Serialize}; 19 | 20 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 21 | #[serde(rename_all = "camelCase")] 22 | pub struct GetUserStatsRequest { 23 | pub block_number: U64, 24 | } 25 | 26 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 27 | #[serde(rename_all = "camelCase")] 28 | 29 | pub struct GetBundleStatsRequest { 30 | pub bundle_hash: H256, 31 | pub block_number: U64, 32 | } 33 | 34 | /// Flashbots RPC interface. 35 | #[cfg_attr(not(feature = "client"), rpc(server, namespace = "flashbots"))] 36 | #[cfg_attr( 37 | all(feature = "client", feature = "server"), 38 | rpc(server, client, namespace = "flashbots") 39 | )] 40 | #[cfg_attr(not(feature = "server"), rpc(client, namespace = "flashbots"))] 41 | #[async_trait::async_trait] 42 | pub trait FlashbotsApi { 43 | /// See [`super::FlashbotsApiClient::get_user_stats`] 44 | #[method(name = "getUserStatsV2")] 45 | async fn get_user_stats( 46 | &self, 47 | request: GetUserStatsRequest, 48 | ) -> jsonrpsee::core::RpcResult; 49 | 50 | /// See [`super::FlashbotsApiClient::get_user_stats`] 51 | #[method(name = "getBundleStatsV2")] 52 | async fn get_bundle_stats( 53 | &self, 54 | request: GetBundleStatsRequest, 55 | ) -> jsonrpsee::core::RpcResult; 56 | } 57 | } 58 | 59 | /// An object safe version of the `FlashbotsApiClient` trait. 60 | #[cfg(feature = "client")] 61 | #[async_trait::async_trait] 62 | pub trait FlashbotsApiClient { 63 | /// Returns a quick summary of how a searcher is performing in the Flashbots ecosystem, 64 | /// including their reputation-based priority. 65 | /// 66 | /// Note: It is currently updated once every hour. 67 | /// 68 | /// # Arguments 69 | /// 70 | /// * `block_number` - A recent block number, in order to prevent replay attacks. Must be within 71 | /// 20 blocks of the current chain tip. 72 | async fn get_user_stats(&self, block_number: U64) -> Result; 73 | 74 | /// Returns stats for a single bundle. 75 | /// 76 | /// You must provide a blockNumber and the bundleHash, and the signing address must be the 77 | /// same as the one who submitted the bundle. 78 | /// 79 | /// # Arguments 80 | /// 81 | /// * `block_hash` - Returned by the Flashbots API when calling 82 | /// `eth_sendBundle`/`mev_sendBundle`. [`crate::SendBundleResponse`]. 83 | /// * `block_number` - The block number the bundle was targeting. See [`crate::Inclusion`]. 84 | async fn get_bundle_stats( 85 | &self, 86 | bundle_hash: H256, 87 | block_number: U64, 88 | ) -> Result; 89 | } 90 | 91 | #[cfg(feature = "client")] 92 | #[async_trait::async_trait] 93 | impl FlashbotsApiClient for T 94 | where 95 | T: rpc::FlashbotsApiClient + Sync, 96 | { 97 | /// See [`FlashbotsApiClient::get_user_stats`] 98 | async fn get_user_stats(&self, block_number: U64) -> Result { 99 | self.get_user_stats(rpc::GetUserStatsRequest { block_number }).await 100 | } 101 | 102 | /// See [`FlashbotsApiClient::get_user_stats`] 103 | async fn get_bundle_stats( 104 | &self, 105 | bundle_hash: H256, 106 | block_number: U64, 107 | ) -> Result { 108 | self.get_bundle_stats(rpc::GetBundleStatsRequest { bundle_hash, block_number }).await 109 | } 110 | } 111 | 112 | #[cfg(all(test, feature = "client"))] 113 | mod tests { 114 | use super::*; 115 | use crate::FlashbotsSignerLayer; 116 | use ethers_core::rand::thread_rng; 117 | use ethers_signers::LocalWallet; 118 | use jsonrpsee::http_client::{transport, HttpClientBuilder}; 119 | 120 | struct Client { 121 | inner: Box, 122 | } 123 | 124 | #[allow(dead_code)] 125 | async fn assert_flashbots_api_box() { 126 | let fb_signer = LocalWallet::new(&mut thread_rng()); 127 | let http = HttpClientBuilder::default() 128 | .set_middleware( 129 | tower::ServiceBuilder::new() 130 | .map_err(transport::Error::Http) 131 | .layer(FlashbotsSignerLayer::new(fb_signer.clone())), 132 | ) 133 | .build("http://localhost:3030") 134 | .unwrap(); 135 | let client = Client { inner: Box::new(http) }; 136 | client.inner.get_user_stats(Default::default()).await.unwrap(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /crates/mev-share-rpc-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs, unreachable_pub, unused_crate_dependencies)] 2 | #![deny(unused_must_use, rust_2018_idioms)] 3 | #![doc(test( 4 | no_crate_inject, 5 | attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) 6 | ))] 7 | 8 | //! MEV-Share RPC interface definitions 9 | 10 | /// `mev` namespace 11 | mod mev; 12 | 13 | /// `eth` namespace extension for bundles 14 | mod eth; 15 | 16 | /// `flashbots` namespace 17 | mod flashbots; 18 | 19 | /// type bindings 20 | mod types; 21 | pub use types::*; 22 | 23 | /// flashbots-style auth 24 | mod auth; 25 | 26 | /// re-export of all server traits 27 | #[cfg(feature = "server")] 28 | pub use servers::*; 29 | 30 | /// Aggregates all server traits. 31 | #[cfg(feature = "server")] 32 | #[doc(hidden)] 33 | pub mod servers { 34 | pub use crate::{eth::EthBundleApiServer, flashbots::FlashbotsApiServer, mev::MevApiServer}; 35 | } 36 | 37 | /// re-export jsonrpsee for convenience 38 | #[cfg(feature = "client")] 39 | pub use jsonrpsee; 40 | 41 | /// re-export of all client traits 42 | #[cfg(feature = "client")] 43 | pub use clients::*; 44 | 45 | /// Aggregates all client traits. 46 | #[cfg(feature = "client")] 47 | #[doc(hidden)] 48 | pub mod clients { 49 | pub use crate::{ 50 | auth::{FlashbotsSigner, FlashbotsSignerLayer}, 51 | eth::EthBundleApiClient, 52 | flashbots::FlashbotsApiClient, 53 | mev::MevApiClient, 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /crates/mev-share-rpc-api/src/mev.rs: -------------------------------------------------------------------------------- 1 | use crate::{SendBundleRequest, SendBundleResponse, SimBundleOverrides, SimBundleResponse}; 2 | 3 | // re-export the rpc server trait 4 | #[cfg(feature = "server")] 5 | pub use rpc::MevApiServer; 6 | 7 | /// jsonrpsee generated code. 8 | /// 9 | /// This hides the generated client trait which is replaced by the `MevApiClient` trait. 10 | mod rpc { 11 | use crate::{SendBundleRequest, SendBundleResponse, SimBundleOverrides, SimBundleResponse}; 12 | use jsonrpsee::proc_macros::rpc; 13 | 14 | /// Mev rpc interface. 15 | #[cfg_attr(not(feature = "client"), rpc(server, namespace = "mev"))] 16 | #[cfg_attr(all(feature = "client", feature = "server"), rpc(server, client, namespace = "mev"))] 17 | #[cfg_attr(not(feature = "server"), rpc(client, namespace = "mev"))] 18 | #[async_trait::async_trait] 19 | pub trait MevApi { 20 | /// Submitting bundles to the relay. It takes in a bundle and provides a bundle hash as a 21 | /// return value. 22 | #[method(name = "sendBundle")] 23 | async fn send_bundle( 24 | &self, 25 | request: SendBundleRequest, 26 | ) -> jsonrpsee::core::RpcResult; 27 | 28 | /// Similar to `mev_sendBundle` but instead of submitting a bundle to the relay, it returns 29 | /// a simulation result. Only fully matched bundles can be simulated. 30 | #[method(name = "simBundle")] 31 | async fn sim_bundle( 32 | &self, 33 | bundle: SendBundleRequest, 34 | sim_overrides: SimBundleOverrides, 35 | ) -> jsonrpsee::core::RpcResult; 36 | } 37 | } 38 | 39 | /// An object safe version of the `MevApiClient` trait. 40 | #[cfg(feature = "client")] 41 | #[async_trait::async_trait] 42 | pub trait MevApiClient { 43 | /// Submitting bundles to the relay. It takes in a bundle and provides a bundle hash as a return 44 | /// value. 45 | async fn send_bundle( 46 | &self, 47 | request: SendBundleRequest, 48 | ) -> Result; 49 | 50 | /// Similar to `mev_sendBundle` but instead of submitting a bundle to the relay, it returns a 51 | /// simulation result. Only fully matched bundles can be simulated. 52 | async fn sim_bundle( 53 | &self, 54 | bundle: SendBundleRequest, 55 | sim_overrides: SimBundleOverrides, 56 | ) -> Result; 57 | } 58 | 59 | #[cfg(feature = "client")] 60 | #[async_trait::async_trait] 61 | impl MevApiClient for T 62 | where 63 | T: rpc::MevApiClient + Sync, 64 | { 65 | async fn send_bundle( 66 | &self, 67 | request: SendBundleRequest, 68 | ) -> Result { 69 | rpc::MevApiClient::send_bundle(self, request).await 70 | } 71 | 72 | async fn sim_bundle( 73 | &self, 74 | bundle: SendBundleRequest, 75 | sim_overrides: SimBundleOverrides, 76 | ) -> Result { 77 | rpc::MevApiClient::sim_bundle(self, bundle, sim_overrides).await 78 | } 79 | } 80 | 81 | #[cfg(all(test, feature = "client"))] 82 | mod tests { 83 | use super::*; 84 | use crate::FlashbotsSignerLayer; 85 | use ethers_core::rand::thread_rng; 86 | use ethers_signers::LocalWallet; 87 | use jsonrpsee::http_client::{transport, HttpClientBuilder}; 88 | 89 | struct Client { 90 | inner: Box, 91 | } 92 | 93 | #[allow(dead_code)] 94 | async fn assert_mev_api_box() { 95 | let fb_signer = LocalWallet::new(&mut thread_rng()); 96 | let http = HttpClientBuilder::default() 97 | .set_middleware( 98 | tower::ServiceBuilder::new() 99 | .map_err(transport::Error::Http) 100 | .layer(FlashbotsSignerLayer::new(fb_signer.clone())), 101 | ) 102 | .build("http://localhost:3030") 103 | .unwrap(); 104 | let client = Client { inner: Box::new(http) }; 105 | client 106 | .inner 107 | .send_bundle(SendBundleRequest { 108 | protocol_version: Default::default(), 109 | inclusion: Default::default(), 110 | bundle_body: vec![], 111 | validity: None, 112 | privacy: None, 113 | }) 114 | .await 115 | .unwrap(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/mev-share-rpc-api/src/types.rs: -------------------------------------------------------------------------------- 1 | //! MEV-share bundle type bindings 2 | #![allow(missing_docs)] 3 | use ethers_core::types::{Address, BlockId, BlockNumber, Bytes, Log, TxHash, H256, U256, U64}; 4 | use serde::{ 5 | ser::{SerializeSeq, Serializer}, 6 | Deserialize, Deserializer, Serialize, 7 | }; 8 | 9 | /// A bundle of transactions to send to the matchmaker. 10 | /// 11 | /// Note: this is for `mev_sendBundle` and not `eth_sendBundle`. 12 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct SendBundleRequest { 15 | /// The version of the MEV-share API to use. 16 | #[serde(rename = "version")] 17 | pub protocol_version: ProtocolVersion, 18 | /// Data used by block builders to check if the bundle should be considered for inclusion. 19 | #[serde(rename = "inclusion")] 20 | pub inclusion: Inclusion, 21 | /// The transactions to include in the bundle. 22 | #[serde(rename = "body")] 23 | pub bundle_body: Vec, 24 | /// Requirements for the bundle to be included in the block. 25 | #[serde(rename = "validity", skip_serializing_if = "Option::is_none")] 26 | pub validity: Option, 27 | /// Preferences on what data should be shared about the bundle and its transactions 28 | #[serde(rename = "privacy", skip_serializing_if = "Option::is_none")] 29 | pub privacy: Option, 30 | } 31 | 32 | /// Data used by block builders to check if the bundle should be considered for inclusion. 33 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct Inclusion { 36 | /// The first block the bundle is valid for. 37 | pub block: U64, 38 | /// The last block the bundle is valid for. 39 | #[serde(skip_serializing_if = "Option::is_none")] 40 | pub max_block: Option, 41 | } 42 | 43 | impl Inclusion { 44 | /// Creates a new inclusion with the given min block.. 45 | pub fn at_block(block: u64) -> Self { 46 | Self { block: U64::from(block), max_block: None } 47 | } 48 | 49 | /// Returns the block number of the first block the bundle is valid for. 50 | #[inline] 51 | pub fn block_number(&self) -> u64 { 52 | self.block.as_u64() 53 | } 54 | 55 | /// Returns the block number of the last block the bundle is valid for. 56 | #[inline] 57 | pub fn max_block_number(&self) -> Option { 58 | self.max_block.as_ref().map(|b| b.as_u64()) 59 | } 60 | } 61 | 62 | /// A bundle tx, which can either be a transaction hash, or a full tx. 63 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] 64 | #[serde(untagged)] 65 | #[serde(rename_all = "camelCase")] 66 | pub enum BundleItem { 67 | /// The hash of either a transaction or bundle we are trying to backrun. 68 | Hash { 69 | /// Tx hash. 70 | hash: TxHash, 71 | }, 72 | /// A new signed transaction. 73 | #[serde(rename_all = "camelCase")] 74 | Tx { 75 | /// Bytes of the signed transaction. 76 | tx: Bytes, 77 | /// If true, the transaction can revert without the bundle being considered invalid. 78 | can_revert: bool, 79 | }, 80 | } 81 | 82 | /// Requirements for the bundle to be included in the block. 83 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] 84 | #[serde(rename_all = "camelCase")] 85 | pub struct Validity { 86 | /// Specifies the minimum percent of a given bundle's earnings to redistribute 87 | /// for it to be included in a builder's block. 88 | #[serde(skip_serializing_if = "Option::is_none")] 89 | pub refund: Option>, 90 | /// Specifies what addresses should receive what percent of the overall refund for this bundle, 91 | /// if it is enveloped by another bundle (eg. a searcher backrun). 92 | #[serde(skip_serializing_if = "Option::is_none")] 93 | pub refund_config: Option>, 94 | } 95 | 96 | /// Specifies the minimum percent of a given bundle's earnings to redistribute 97 | /// for it to be included in a builder's block. 98 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] 99 | #[serde(rename_all = "camelCase")] 100 | pub struct Refund { 101 | /// The index of the transaction in the bundle. 102 | pub body_idx: u64, 103 | /// The minimum percent of the bundle's earnings to redistribute. 104 | pub percent: u64, 105 | } 106 | 107 | /// Specifies what addresses should receive what percent of the overall refund for this bundle, 108 | /// if it is enveloped by another bundle (eg. a searcher backrun). 109 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] 110 | #[serde(rename_all = "camelCase")] 111 | pub struct RefundConfig { 112 | /// The address to refund. 113 | pub address: Address, 114 | /// The minimum percent of the bundle's earnings to redistribute. 115 | pub percent: u64, 116 | } 117 | 118 | /// Preferences on what data should be shared about the bundle and its transactions 119 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] 120 | #[serde(rename_all = "camelCase")] 121 | pub struct Privacy { 122 | /// Hints on what data should be shared about the bundle and its transactions 123 | #[serde(skip_serializing_if = "Option::is_none")] 124 | pub hints: Option, 125 | /// Names of the builders that should be allowed to see the bundle/transaction. 126 | /// https://github.com/flashbots/dowg/blob/main/builder-registrations.json 127 | #[serde(skip_serializing_if = "Option::is_none")] 128 | pub builders: Option>, 129 | } 130 | 131 | /// Hints on what data should be shared about the bundle and its transactions 132 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 133 | pub struct PrivacyHint { 134 | /// The calldata of the bundle's transactions should be shared. 135 | pub calldata: bool, 136 | /// The address of the bundle's transactions should be shared. 137 | pub contract_address: bool, 138 | /// The logs of the bundle's transactions should be shared. 139 | pub logs: bool, 140 | /// The function selector of the bundle's transactions should be shared. 141 | pub function_selector: bool, 142 | /// The hash of the bundle's transactions should be shared. 143 | pub hash: bool, 144 | /// The hash of the bundle should be shared. 145 | pub tx_hash: bool, 146 | } 147 | 148 | impl PrivacyHint { 149 | pub fn with_calldata(mut self) -> Self { 150 | self.calldata = true; 151 | self 152 | } 153 | 154 | pub fn with_contract_address(mut self) -> Self { 155 | self.contract_address = true; 156 | self 157 | } 158 | 159 | pub fn with_logs(mut self) -> Self { 160 | self.logs = true; 161 | self 162 | } 163 | 164 | pub fn with_function_selector(mut self) -> Self { 165 | self.function_selector = true; 166 | self 167 | } 168 | 169 | pub fn with_hash(mut self) -> Self { 170 | self.hash = true; 171 | self 172 | } 173 | 174 | pub fn with_tx_hash(mut self) -> Self { 175 | self.tx_hash = true; 176 | self 177 | } 178 | 179 | pub fn has_calldata(&self) -> bool { 180 | self.calldata 181 | } 182 | 183 | pub fn has_contract_address(&self) -> bool { 184 | self.contract_address 185 | } 186 | 187 | pub fn has_logs(&self) -> bool { 188 | self.logs 189 | } 190 | 191 | pub fn has_function_selector(&self) -> bool { 192 | self.function_selector 193 | } 194 | 195 | pub fn has_hash(&self) -> bool { 196 | self.hash 197 | } 198 | 199 | pub fn has_tx_hash(&self) -> bool { 200 | self.tx_hash 201 | } 202 | 203 | fn num_hints(&self) -> usize { 204 | let mut num_hints = 0; 205 | if self.calldata { 206 | num_hints += 1; 207 | } 208 | if self.contract_address { 209 | num_hints += 1; 210 | } 211 | if self.logs { 212 | num_hints += 1; 213 | } 214 | if self.function_selector { 215 | num_hints += 1; 216 | } 217 | if self.hash { 218 | num_hints += 1; 219 | } 220 | if self.tx_hash { 221 | num_hints += 1; 222 | } 223 | num_hints 224 | } 225 | } 226 | 227 | impl Serialize for PrivacyHint { 228 | fn serialize(&self, serializer: S) -> Result { 229 | let mut seq = serializer.serialize_seq(Some(self.num_hints()))?; 230 | if self.calldata { 231 | seq.serialize_element("calldata")?; 232 | } 233 | if self.contract_address { 234 | seq.serialize_element("contract_address")?; 235 | } 236 | if self.logs { 237 | seq.serialize_element("logs")?; 238 | } 239 | if self.function_selector { 240 | seq.serialize_element("function_selector")?; 241 | } 242 | if self.hash { 243 | seq.serialize_element("hash")?; 244 | } 245 | if self.tx_hash { 246 | seq.serialize_element("tx_hash")?; 247 | } 248 | seq.end() 249 | } 250 | } 251 | 252 | impl<'de> Deserialize<'de> for PrivacyHint { 253 | fn deserialize>(deserializer: D) -> Result { 254 | let hints = Vec::::deserialize(deserializer)?; 255 | let mut privacy_hint = PrivacyHint::default(); 256 | for hint in hints { 257 | match hint.as_str() { 258 | "calldata" => privacy_hint.calldata = true, 259 | "contract_address" => privacy_hint.contract_address = true, 260 | "logs" => privacy_hint.logs = true, 261 | "function_selector" => privacy_hint.function_selector = true, 262 | "hash" => privacy_hint.hash = true, 263 | "tx_hash" => privacy_hint.tx_hash = true, 264 | _ => return Err(serde::de::Error::custom("invalid privacy hint")), 265 | } 266 | } 267 | Ok(privacy_hint) 268 | } 269 | } 270 | 271 | /// Response from the matchmaker after sending a bundle. 272 | #[derive(Deserialize, Debug, Serialize, Clone, PartialEq, Eq)] 273 | #[serde(rename_all = "camelCase")] 274 | pub struct SendBundleResponse { 275 | /// Hash of the bundle bodies. 276 | pub bundle_hash: H256, 277 | } 278 | 279 | /// The version of the MEV-share API to use. 280 | #[derive(Deserialize, Debug, Serialize, Clone, Default, PartialEq, Eq)] 281 | pub enum ProtocolVersion { 282 | #[default] 283 | #[serde(rename = "beta-1")] 284 | /// The beta-1 version of the API. 285 | Beta1, 286 | /// The 0.1 version of the API. 287 | #[serde(rename = "v0.1")] 288 | V0_1, 289 | } 290 | 291 | /// Optional fields to override simulation state. 292 | #[derive(Deserialize, Debug, Serialize, Clone, Default, PartialEq, Eq)] 293 | #[serde(rename_all = "camelCase")] 294 | pub struct SimBundleOverrides { 295 | /// Block used for simulation state. Defaults to latest block. 296 | /// Block header data will be derived from parent block by default. 297 | /// Specify other params to override the default values. 298 | #[serde(skip_serializing_if = "Option::is_none")] 299 | pub parent_block: Option, 300 | /// Block number used for simulation, defaults to parentBlock.number + 1 301 | #[serde(skip_serializing_if = "Option::is_none")] 302 | pub block_number: Option, 303 | /// Coinbase used for simulation, defaults to parentBlock.coinbase 304 | #[serde(skip_serializing_if = "Option::is_none")] 305 | pub coinbase: Option
, 306 | /// Timestamp used for simulation, defaults to parentBlock.timestamp + 12 307 | #[serde(skip_serializing_if = "Option::is_none")] 308 | pub timestamp: Option, 309 | /// Gas limit used for simulation, defaults to parentBlock.gasLimit 310 | #[serde(skip_serializing_if = "Option::is_none")] 311 | pub gas_limit: Option, 312 | /// Base fee used for simulation, defaults to parentBlock.baseFeePerGas 313 | #[serde(skip_serializing_if = "Option::is_none")] 314 | pub base_fee: Option, 315 | /// Timeout in seconds, defaults to 5 316 | #[serde(skip_serializing_if = "Option::is_none")] 317 | pub timeout: Option, 318 | } 319 | 320 | /// Response from the matchmaker after sending a simulation request. 321 | #[derive(Deserialize, Debug, Serialize, Clone, PartialEq, Eq)] 322 | #[serde(rename_all = "camelCase")] 323 | pub struct SimBundleResponse { 324 | /// Whether the simulation was successful. 325 | pub success: bool, 326 | /// Error message if the simulation failed. 327 | #[serde(skip_serializing_if = "Option::is_none")] 328 | pub error: Option, 329 | /// The block number of the simulated block. 330 | pub state_block: U64, 331 | /// The gas price of the simulated block. 332 | pub mev_gas_price: U64, 333 | /// The profit of the simulated block. 334 | pub profit: U64, 335 | /// The refundable value of the simulated block. 336 | pub refundable_value: U64, 337 | /// The gas used by the simulated block. 338 | pub gas_used: U64, 339 | /// Logs returned by mev_simBundle. 340 | #[serde(skip_serializing_if = "Option::is_none")] 341 | pub logs: Option>, 342 | } 343 | 344 | /// Logs returned by mev_simBundle. 345 | #[derive(Deserialize, Debug, Serialize, Clone, PartialEq, Eq)] 346 | #[serde(rename_all = "camelCase")] 347 | pub struct SimBundleLogs { 348 | /// Logs for transactions in bundle. 349 | #[serde(skip_serializing_if = "Option::is_none")] 350 | pub tx_logs: Option>, 351 | /// Logs for bundles in bundle. 352 | #[serde(skip_serializing_if = "Option::is_none")] 353 | pub bundle_logs: Option>, 354 | } 355 | 356 | impl SendBundleRequest { 357 | /// Create a new bundle request. 358 | pub fn new( 359 | block_num: U64, 360 | max_block: Option, 361 | protocol_version: ProtocolVersion, 362 | bundle_body: Vec, 363 | ) -> Self { 364 | Self { 365 | protocol_version, 366 | inclusion: Inclusion { block: block_num, max_block }, 367 | bundle_body, 368 | validity: None, 369 | privacy: None, 370 | } 371 | } 372 | } 373 | 374 | /// Request for `eth_cancelBundle` 375 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 376 | #[serde(rename_all = "camelCase")] 377 | pub struct CancelBundleRequest { 378 | /// Bundle hash of the bundle to be canceled 379 | pub bundle_hash: String, 380 | } 381 | 382 | /// Request for `eth_sendPrivateTransaction` 383 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 384 | #[serde(rename_all = "camelCase")] 385 | pub struct PrivateTransactionRequest { 386 | /// raw signed transaction 387 | pub tx: Bytes, 388 | /// Hex-encoded number string, optional. Highest block number in which the transaction should 389 | /// be included. 390 | #[serde(skip_serializing_if = "Option::is_none")] 391 | pub max_block_number: Option, 392 | #[serde(default, skip_serializing_if = "PrivateTransactionPreferences::is_empty")] 393 | pub preferences: PrivateTransactionPreferences, 394 | } 395 | 396 | /// Additional preferences for `eth_sendPrivateTransaction` 397 | #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] 398 | pub struct PrivateTransactionPreferences { 399 | /// Requirements for the bundle to be included in the block. 400 | #[serde(skip_serializing_if = "Option::is_none")] 401 | pub validity: Option, 402 | /// Preferences on what data should be shared about the bundle and its transactions 403 | #[serde(skip_serializing_if = "Option::is_none")] 404 | pub privacy: Option, 405 | } 406 | 407 | impl PrivateTransactionPreferences { 408 | /// Returns true if the preferences are empty. 409 | pub fn is_empty(&self) -> bool { 410 | self.validity.is_none() && self.privacy.is_none() 411 | } 412 | } 413 | 414 | /// Request for `eth_cancelPrivateTransaction` 415 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 416 | #[serde(rename_all = "camelCase")] 417 | pub struct CancelPrivateTransactionRequest { 418 | /// Transaction hash of the transaction to be canceled 419 | pub tx_hash: H256, 420 | } 421 | 422 | // TODO(@optimiz-r): Revisit after is closed. 423 | /// Response for `flashbots_getBundleStatsV2` represents stats for a single bundle 424 | /// 425 | /// Note: this is V2: 426 | /// 427 | /// Timestamp format: "2022-10-06T21:36:06.322Z" 428 | #[derive(Default, Debug, Clone, PartialEq, Eq)] 429 | pub enum BundleStats { 430 | /// The relayer has not yet seen the bundle. 431 | #[default] 432 | Unknown, 433 | /// The relayer has seen the bundle, but has not simulated it yet. 434 | Seen(StatsSeen), 435 | /// The relayer has seen the bundle and has simulated it. 436 | Simulated(StatsSimulated), 437 | } 438 | 439 | impl Serialize for BundleStats { 440 | fn serialize(&self, serializer: S) -> Result { 441 | match self { 442 | BundleStats::Unknown => serde_json::json!({"isSimulated": false}).serialize(serializer), 443 | BundleStats::Seen(stats) => stats.serialize(serializer), 444 | BundleStats::Simulated(stats) => stats.serialize(serializer), 445 | } 446 | } 447 | } 448 | 449 | impl<'de> Deserialize<'de> for BundleStats { 450 | fn deserialize(deserializer: D) -> Result 451 | where 452 | D: Deserializer<'de>, 453 | { 454 | let map = serde_json::Map::deserialize(deserializer)?; 455 | 456 | if map.get("receivedAt").is_none() { 457 | Ok(BundleStats::Unknown) 458 | } else if map["isSimulated"] == false { 459 | StatsSeen::deserialize(serde_json::Value::Object(map)) 460 | .map(BundleStats::Seen) 461 | .map_err(serde::de::Error::custom) 462 | } else { 463 | StatsSimulated::deserialize(serde_json::Value::Object(map)) 464 | .map(BundleStats::Simulated) 465 | .map_err(serde::de::Error::custom) 466 | } 467 | } 468 | } 469 | 470 | /// Response for `flashbots_getBundleStatsV2` represents stats for a single bundle 471 | /// 472 | /// Note: this is V2: 473 | /// 474 | /// Timestamp format: "2022-10-06T21:36:06.322Z 475 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 476 | #[serde(rename_all = "camelCase")] 477 | pub struct StatsSeen { 478 | /// boolean representing if this searcher has a high enough reputation to be in the high 479 | /// priority queue 480 | pub is_high_priority: bool, 481 | /// representing whether the bundle gets simulated. All other fields will be omitted except 482 | /// simulated field if API didn't receive bundle 483 | pub is_simulated: bool, 484 | /// time at which the bundle API received the bundle 485 | pub received_at: String, 486 | } 487 | 488 | /// Response for `flashbots_getBundleStatsV2` represents stats for a single bundle 489 | /// 490 | /// Note: this is V2: 491 | /// 492 | /// Timestamp format: "2022-10-06T21:36:06.322Z 493 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 494 | #[serde(rename_all = "camelCase")] 495 | pub struct StatsSimulated { 496 | /// boolean representing if this searcher has a high enough reputation to be in the high 497 | /// priority queue 498 | pub is_high_priority: bool, 499 | /// representing whether the bundle gets simulated. All other fields will be omitted except 500 | /// simulated field if API didn't receive bundle 501 | pub is_simulated: bool, 502 | /// time at which the bundle gets simulated 503 | pub simulated_at: String, 504 | /// time at which the bundle API received the bundle 505 | pub received_at: String, 506 | /// indicates time at which each builder selected the bundle to be included in the target 507 | /// block 508 | #[serde(default = "Vec::new")] 509 | pub considered_by_builders_at: Vec, 510 | /// indicates time at which each builder sealed a block containing the bundle 511 | #[serde(default = "Vec::new")] 512 | pub sealed_by_builders_at: Vec, 513 | } 514 | 515 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 516 | #[serde(rename_all = "camelCase")] 517 | pub struct ConsideredByBuildersAt { 518 | pub pubkey: String, 519 | pub timestamp: String, 520 | } 521 | 522 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 523 | #[serde(rename_all = "camelCase")] 524 | pub struct SealedByBuildersAt { 525 | pub pubkey: String, 526 | pub timestamp: String, 527 | } 528 | 529 | /// Response for `flashbots_getUserStatsV2` represents stats for a searcher. 530 | /// 531 | /// Note: this is V2: 532 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 533 | #[serde(rename_all = "camelCase")] 534 | pub struct UserStats { 535 | /// Represents whether this searcher has a high enough reputation to be in the high priority 536 | /// queue. 537 | pub is_high_priority: bool, 538 | /// The total amount paid to validators over all time. 539 | #[serde(with = "u256_numeric_string")] 540 | pub all_time_validator_payments: U256, 541 | /// The total amount of gas simulated across all bundles submitted to Flashbots. 542 | /// This is the actual gas used in simulations, not gas limit. 543 | #[serde(with = "u256_numeric_string")] 544 | pub all_time_gas_simulated: U256, 545 | /// The total amount paid to validators the last 7 days. 546 | #[serde(with = "u256_numeric_string")] 547 | pub last_7d_validator_payments: U256, 548 | /// The total amount of gas simulated across all bundles submitted to Flashbots in the last 7 549 | /// days. This is the actual gas used in simulations, not gas limit. 550 | #[serde(with = "u256_numeric_string")] 551 | pub last_7d_gas_simulated: U256, 552 | /// The total amount paid to validators the last day. 553 | #[serde(with = "u256_numeric_string")] 554 | pub last_1d_validator_payments: U256, 555 | /// The total amount of gas simulated across all bundles submitted to Flashbots in the last 556 | /// day. This is the actual gas used in simulations, not gas limit. 557 | #[serde(with = "u256_numeric_string")] 558 | pub last_1d_gas_simulated: U256, 559 | } 560 | 561 | /// Bundle of transactions for `eth_sendBundle` 562 | /// 563 | /// Note: this is for `eth_sendBundle` and not `mev_sendBundle` 564 | /// 565 | /// 566 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 567 | #[serde(rename_all = "camelCase")] 568 | pub struct EthSendBundle { 569 | /// A list of hex-encoded signed transactions 570 | pub txs: Vec, 571 | /// hex-encoded block number for which this bundle is valid 572 | pub block_number: U64, 573 | /// unix timestamp when this bundle becomes active 574 | #[serde(skip_serializing_if = "Option::is_none")] 575 | pub min_timestamp: Option, 576 | /// unix timestamp how long this bundle stays valid 577 | #[serde(skip_serializing_if = "Option::is_none")] 578 | pub max_timestamp: Option, 579 | /// list of hashes of possibly reverting txs 580 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 581 | pub reverting_tx_hashes: Vec, 582 | /// UUID that can be used to cancel/replace this bundle 583 | #[serde(rename = "replacementUuid", skip_serializing_if = "Option::is_none")] 584 | pub replacement_uuid: Option, 585 | } 586 | 587 | /// Response from the matchmaker after sending a bundle. 588 | #[derive(Deserialize, Debug, Serialize, Clone, PartialEq, Eq)] 589 | #[serde(rename_all = "camelCase")] 590 | pub struct EthBundleHash { 591 | /// Hash of the bundle bodies. 592 | pub bundle_hash: H256, 593 | } 594 | 595 | /// Bundle of transactions for `eth_callBundle` 596 | /// 597 | /// 598 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 599 | #[serde(rename_all = "camelCase")] 600 | pub struct EthCallBundle { 601 | /// A list of hex-encoded signed transactions 602 | pub txs: Vec, 603 | /// hex encoded block number for which this bundle is valid on 604 | pub block_number: U64, 605 | /// Either a hex encoded number or a block tag for which state to base this simulation on 606 | pub state_block_number: BlockNumber, 607 | /// the timestamp to use for this bundle simulation, in seconds since the unix epoch 608 | #[serde(skip_serializing_if = "Option::is_none")] 609 | pub timestamp: Option, 610 | } 611 | 612 | /// Response for `eth_callBundle` 613 | #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 614 | #[serde(rename_all = "camelCase")] 615 | pub struct EthCallBundleResponse { 616 | #[serde(with = "u256_numeric_string")] 617 | pub bundle_gas_price: U256, 618 | pub bundle_hash: String, 619 | #[serde(with = "u256_numeric_string")] 620 | pub coinbase_diff: U256, 621 | #[serde(with = "u256_numeric_string")] 622 | pub eth_sent_to_coinbase: U256, 623 | #[serde(with = "u256_numeric_string")] 624 | pub gas_fees: U256, 625 | pub results: Vec, 626 | pub state_block_number: u64, 627 | pub total_gas_used: u64, 628 | } 629 | 630 | /// Result of a single transaction in a bundle for `eth_callBundle` 631 | #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 632 | #[serde(rename_all = "camelCase")] 633 | pub struct EthCallBundleTransactionResult { 634 | #[serde(with = "u256_numeric_string")] 635 | pub coinbase_diff: U256, 636 | #[serde(with = "u256_numeric_string")] 637 | pub eth_sent_to_coinbase: U256, 638 | pub from_address: Address, 639 | #[serde(with = "u256_numeric_string")] 640 | pub gas_fees: U256, 641 | #[serde(with = "u256_numeric_string")] 642 | pub gas_price: U256, 643 | pub gas_used: u64, 644 | pub to_address: Address, 645 | pub tx_hash: H256, 646 | pub value: Bytes, 647 | } 648 | 649 | mod u256_numeric_string { 650 | use ethers_core::types::{serde_helpers::StringifiedNumeric, U256}; 651 | use serde::{de, Deserialize, Serializer}; 652 | 653 | pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result 654 | where 655 | D: de::Deserializer<'de>, 656 | { 657 | let num = StringifiedNumeric::deserialize(deserializer)?; 658 | num.try_into().map_err(de::Error::custom) 659 | } 660 | 661 | pub(crate) fn serialize(val: &U256, serializer: S) -> Result 662 | where 663 | S: Serializer, 664 | { 665 | let val: u128 = (*val).try_into().map_err(serde::ser::Error::custom)?; 666 | serializer.serialize_str(&val.to_string()) 667 | } 668 | } 669 | 670 | #[cfg(test)] 671 | mod tests { 672 | use super::*; 673 | use ethers_core::types::Bytes; 674 | use std::str::FromStr; 675 | 676 | #[test] 677 | fn can_deserialize_simple() { 678 | let str = r#" 679 | [{ 680 | "version": "v0.1", 681 | "inclusion": { 682 | "block": "0x1" 683 | }, 684 | "body": [{ 685 | "tx": "0x02f86b0180843b9aca00852ecc889a0082520894c87037874aed04e51c29f582394217a0a2b89d808080c080a0a463985c616dd8ee17d7ef9112af4e6e06a27b071525b42182fe7b0b5c8b4925a00af5ca177ffef2ff28449292505d41be578bebb77110dfc09361d2fb56998260", 686 | "canRevert": false 687 | }] 688 | }] 689 | "#; 690 | let res: Result, _> = serde_json::from_str(str); 691 | assert!(res.is_ok()); 692 | } 693 | 694 | #[test] 695 | fn can_deserialize_complex() { 696 | let str = r#" 697 | [{ 698 | "version": "v0.1", 699 | "inclusion": { 700 | "block": "0x1" 701 | }, 702 | "body": [{ 703 | "tx": "0x02f86b0180843b9aca00852ecc889a0082520894c87037874aed04e51c29f582394217a0a2b89d808080c080a0a463985c616dd8ee17d7ef9112af4e6e06a27b071525b42182fe7b0b5c8b4925a00af5ca177ffef2ff28449292505d41be578bebb77110dfc09361d2fb56998260", 704 | "canRevert": false 705 | }], 706 | "privacy": { 707 | "hints": [ 708 | "calldata" 709 | ] 710 | }, 711 | "validity": { 712 | "refundConfig": [ 713 | { 714 | "address": "0x8EC1237b1E80A6adf191F40D4b7D095E21cdb18f", 715 | "percent": 100 716 | } 717 | ] 718 | } 719 | }] 720 | "#; 721 | let res: Result, _> = serde_json::from_str(str); 722 | assert!(res.is_ok()); 723 | } 724 | 725 | #[test] 726 | fn can_serialize_complex() { 727 | let str = r#" 728 | [{ 729 | "version": "v0.1", 730 | "inclusion": { 731 | "block": "0x1" 732 | }, 733 | "body": [{ 734 | "tx": "0x02f86b0180843b9aca00852ecc889a0082520894c87037874aed04e51c29f582394217a0a2b89d808080c080a0a463985c616dd8ee17d7ef9112af4e6e06a27b071525b42182fe7b0b5c8b4925a00af5ca177ffef2ff28449292505d41be578bebb77110dfc09361d2fb56998260", 735 | "canRevert": false 736 | }], 737 | "privacy": { 738 | "hints": [ 739 | "calldata" 740 | ] 741 | }, 742 | "validity": { 743 | "refundConfig": [ 744 | { 745 | "address": "0x8EC1237b1E80A6adf191F40D4b7D095E21cdb18f", 746 | "percent": 100 747 | } 748 | ] 749 | } 750 | }] 751 | "#; 752 | let bundle_body = vec![BundleItem::Tx { 753 | tx: Bytes::from_str("0x02f86b0180843b9aca00852ecc889a0082520894c87037874aed04e51c29f582394217a0a2b89d808080c080a0a463985c616dd8ee17d7ef9112af4e6e06a27b071525b42182fe7b0b5c8b4925a00af5ca177ffef2ff28449292505d41be578bebb77110dfc09361d2fb56998260").unwrap(), 754 | can_revert: false, 755 | }]; 756 | 757 | let validity = Some(Validity { 758 | refund_config: Some(vec![RefundConfig { 759 | address: "0x8EC1237b1E80A6adf191F40D4b7D095E21cdb18f".parse().unwrap(), 760 | percent: 100, 761 | }]), 762 | ..Default::default() 763 | }); 764 | 765 | let privacy = Some(Privacy { 766 | hints: Some(PrivacyHint { calldata: true, ..Default::default() }), 767 | ..Default::default() 768 | }); 769 | 770 | let bundle = SendBundleRequest { 771 | protocol_version: ProtocolVersion::V0_1, 772 | inclusion: Inclusion { block: 1.into(), max_block: None }, 773 | bundle_body, 774 | validity, 775 | privacy, 776 | }; 777 | let expected = serde_json::from_str::>(str).unwrap(); 778 | assert_eq!(bundle, expected[0]); 779 | } 780 | 781 | #[test] 782 | fn can_serialize_privacy_hint() { 783 | let hint = PrivacyHint { 784 | calldata: true, 785 | contract_address: true, 786 | logs: true, 787 | function_selector: true, 788 | hash: true, 789 | tx_hash: true, 790 | }; 791 | let expected = 792 | r#"["calldata","contract_address","logs","function_selector","hash","tx_hash"]"#; 793 | let actual = serde_json::to_string(&hint).unwrap(); 794 | assert_eq!(actual, expected); 795 | } 796 | 797 | #[test] 798 | fn can_deserialize_privacy_hint() { 799 | let hint = PrivacyHint { 800 | calldata: true, 801 | contract_address: false, 802 | logs: true, 803 | function_selector: false, 804 | hash: true, 805 | tx_hash: false, 806 | }; 807 | let expected = r#"["calldata","logs","hash"]"#; 808 | let actual: PrivacyHint = serde_json::from_str(expected).unwrap(); 809 | assert_eq!(actual, hint); 810 | } 811 | 812 | #[test] 813 | fn can_dererialize_sim_response() { 814 | let expected = r#" 815 | { 816 | "success": true, 817 | "stateBlock": "0x8b8da8", 818 | "mevGasPrice": "0x74c7906005", 819 | "profit": "0x4bc800904fc000", 820 | "refundableValue": "0x4bc800904fc000", 821 | "gasUsed": "0xa620", 822 | "logs": [{},{}] 823 | } 824 | "#; 825 | let actual: SimBundleResponse = serde_json::from_str(expected).unwrap(); 826 | assert!(actual.success); 827 | } 828 | 829 | #[test] 830 | fn can_deserialize_eth_call_resp() { 831 | let s = r#"{ 832 | "bundleGasPrice": "476190476193", 833 | "bundleHash": "0x73b1e258c7a42fd0230b2fd05529c5d4b6fcb66c227783f8bece8aeacdd1db2e", 834 | "coinbaseDiff": "20000000000126000", 835 | "ethSentToCoinbase": "20000000000000000", 836 | "gasFees": "126000", 837 | "results": [ 838 | { 839 | "coinbaseDiff": "10000000000063000", 840 | "ethSentToCoinbase": "10000000000000000", 841 | "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0", 842 | "gasFees": "63000", 843 | "gasPrice": "476190476193", 844 | "gasUsed": 21000, 845 | "toAddress": "0x73625f59CAdc5009Cb458B751b3E7b6b48C06f2C", 846 | "txHash": "0x669b4704a7d993a946cdd6e2f95233f308ce0c4649d2e04944e8299efcaa098a", 847 | "value": "0x" 848 | }, 849 | { 850 | "coinbaseDiff": "10000000000063000", 851 | "ethSentToCoinbase": "10000000000000000", 852 | "fromAddress": "0x02A727155aeF8609c9f7F2179b2a1f560B39F5A0", 853 | "gasFees": "63000", 854 | "gasPrice": "476190476193", 855 | "gasUsed": 21000, 856 | "toAddress": "0x73625f59CAdc5009Cb458B751b3E7b6b48C06f2C", 857 | "txHash": "0xa839ee83465657cac01adc1d50d96c1b586ed498120a84a64749c0034b4f19fa", 858 | "value": "0x" 859 | } 860 | ], 861 | "stateBlockNumber": 5221585, 862 | "totalGasUsed": 42000 863 | }"#; 864 | 865 | let _call = serde_json::from_str::(s).unwrap(); 866 | } 867 | 868 | #[test] 869 | fn can_serialize_deserialize_bundle_stats() { 870 | let fixtures = [ 871 | ( 872 | r#"{ 873 | "isSimulated": false 874 | }"#, 875 | BundleStats::Unknown, 876 | ), 877 | ( 878 | r#"{ 879 | "isHighPriority": false, 880 | "isSimulated": false, 881 | "receivedAt": "476190476193" 882 | }"#, 883 | BundleStats::Seen(StatsSeen { 884 | is_high_priority: false, 885 | is_simulated: false, 886 | received_at: "476190476193".to_string(), 887 | }), 888 | ), 889 | ( 890 | r#"{ 891 | "isHighPriority": true, 892 | "isSimulated": true, 893 | "simulatedAt": "111", 894 | "receivedAt": "222", 895 | "consideredByBuildersAt":[], 896 | "sealedByBuildersAt": [ 897 | { 898 | "pubkey": "333", 899 | "timestamp": "444" 900 | }, 901 | { 902 | "pubkey": "555", 903 | "timestamp": "666" 904 | } 905 | ] 906 | }"#, 907 | BundleStats::Simulated(StatsSimulated { 908 | is_high_priority: true, 909 | is_simulated: true, 910 | simulated_at: String::from("111"), 911 | received_at: String::from("222"), 912 | considered_by_builders_at: vec![], 913 | sealed_by_builders_at: vec![ 914 | SealedByBuildersAt { 915 | pubkey: String::from("333"), 916 | timestamp: String::from("444"), 917 | }, 918 | SealedByBuildersAt { 919 | pubkey: String::from("555"), 920 | timestamp: String::from("666"), 921 | }, 922 | ], 923 | }), 924 | ), 925 | ]; 926 | 927 | let strip_whitespaces = 928 | |input: &str| input.chars().filter(|&c| !c.is_whitespace()).collect::(); 929 | 930 | for (serialized, deserialized) in fixtures { 931 | // Check de-serialization 932 | let deserialized_expected = serde_json::from_str::(serialized).unwrap(); 933 | assert_eq!(deserialized, deserialized_expected); 934 | 935 | // Check serialization 936 | let serialized_expected = &serde_json::to_string(&deserialized).unwrap(); 937 | assert_eq!(strip_whitespaces(serialized), strip_whitespaces(serialized_expected)); 938 | } 939 | } 940 | } 941 | -------------------------------------------------------------------------------- /crates/mev-share-sse/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mev-share-sse" 3 | version = "0.5.1" 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | license.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | description = """ 10 | MEV-share Server Side Events support in rust 11 | """ 12 | 13 | [dependencies] 14 | alloy-rpc-types-mev.workspace = true 15 | 16 | ## async 17 | async-sse = "5.1" 18 | reqwest = { version = "0.12", default-features = false, features = [ 19 | "stream", 20 | "json", 21 | ] } 22 | futures-util = { workspace = true, features = ["io"] } 23 | http-types = { version = "2.12", default-features = false } 24 | tower = { workspace = true, optional = true } 25 | hyper = { workspace = true, features = ["stream"], optional = true } 26 | tokio-util = { version = "0.7", features = ["compat"], optional = true } 27 | tokio-stream = { version = "0.1", features = ["sync"], optional = true } 28 | 29 | ## misc 30 | bytes = "1.4" 31 | pin-project-lite = "0.2" 32 | thiserror.workspace = true 33 | http = { workspace = true, optional = true } 34 | serde.workspace = true 35 | serde_json.workspace = true 36 | tokio = { workspace = true, features = ["time"] } 37 | tracing.workspace = true 38 | 39 | [features] 40 | default = ["rustls"] 41 | rustls = ["reqwest/rustls-tls"] 42 | native-tls = ["reqwest/native-tls"] 43 | server = ["hyper", "http", "tokio-stream", "tokio-util", "tower"] 44 | 45 | [dev-dependencies] 46 | tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] } 47 | tracing-subscriber = { version = "0.3", default-features = false, features = [ 48 | "env-filter", 49 | "fmt", 50 | ] } 51 | -------------------------------------------------------------------------------- /crates/mev-share-sse/src/client.rs: -------------------------------------------------------------------------------- 1 | //! Server-sent events (SSE) support. 2 | 3 | use alloy_rpc_types_mev::mevshare::{Event, EventHistory, EventHistoryInfo, EventHistoryParams}; 4 | use async_sse::Decoder; 5 | use bytes::Bytes; 6 | use futures_util::{ 7 | stream::{IntoAsyncRead, MapErr, MapOk}, 8 | Stream, TryFutureExt, TryStreamExt, 9 | }; 10 | use pin_project_lite::pin_project; 11 | use reqwest::header::{self, HeaderValue}; 12 | use serde::{de::DeserializeOwned, Serialize}; 13 | use std::{ 14 | future::Future, 15 | io, 16 | pin::Pin, 17 | task::{ready, Context, Poll}, 18 | time::Duration, 19 | }; 20 | use tracing::{debug, trace, warn}; 21 | 22 | type TryIo = fn(reqwest::Error) -> io::Error; 23 | type TryOk = fn(async_sse::Event) -> serde_json::Result>; 24 | type ReqStream = Pin> + Send>>; 25 | type SseDecoderStream = MapOk>>, TryOk>; 26 | 27 | /// The client for SSE. 28 | /// 29 | /// This is a simple wrapper around [reqwest::Client] that provides subscription function for SSE. 30 | #[derive(Debug, Clone)] 31 | pub struct EventClient { 32 | client: reqwest::Client, 33 | max_retries: Option, 34 | } 35 | 36 | // === impl EventClient === 37 | 38 | impl EventClient { 39 | /// Create a new client with the given reqwest client. 40 | /// 41 | /// ``` 42 | /// use mev_share_sse::EventClient; 43 | /// let client = EventClient::new(reqwest::Client::new()); 44 | /// ``` 45 | pub fn new(client: reqwest::Client) -> Self { 46 | Self { client, max_retries: None } 47 | } 48 | 49 | /// Set the maximum number of retries. 50 | pub fn with_max_retries(mut self, max_retries: u64) -> Self { 51 | self.set_max_retries(max_retries); 52 | self 53 | } 54 | 55 | /// Set the maximum number of retries. 56 | pub fn set_max_retries(&mut self, max_retries: u64) { 57 | self.max_retries = Some(max_retries); 58 | } 59 | 60 | /// Returns the maximum number of retries. 61 | pub fn max_retries(&self) -> Option { 62 | self.max_retries 63 | } 64 | 65 | /// Subscribe to the MEV-share SSE endpoint. 66 | /// 67 | /// This connects to the endpoint and returns a stream of `T` items. 68 | /// 69 | /// See [EventClient::events] for a more convenient way to subscribe to [Event] streams. 70 | pub async fn subscribe( 71 | &self, 72 | endpoint: &str, 73 | ) -> reqwest::Result> { 74 | let st = new_stream(&self.client, endpoint, None::<()>).await?; 75 | 76 | let endpoint = endpoint.to_string(); 77 | let inner = 78 | EventStreamInner { num_retries: 0, endpoint, client: self.clone(), query: None }; 79 | let st = EventStream { inner, state: Some(State::Active(Box::pin(st))) }; 80 | 81 | Ok(st) 82 | } 83 | 84 | /// Subscribe to the MEV-share SSE endpoint with additional query params. 85 | /// 86 | /// This connects to the endpoint and returns a stream of `T` items. 87 | /// 88 | /// See [EventClient::events] for a more convenient way to subscribe to [Event] streams. 89 | pub async fn subscribe_with_query( 90 | &self, 91 | endpoint: &str, 92 | query: S, 93 | ) -> reqwest::Result> { 94 | let query = Some(serde_json::to_value(query).expect("serialization failed")); 95 | let st = new_stream(&self.client, endpoint, query.as_ref()).await?; 96 | 97 | let endpoint = endpoint.to_string(); 98 | let inner = EventStreamInner { num_retries: 0, endpoint, client: self.clone(), query }; 99 | let st = EventStream { inner, state: Some(State::Active(Box::pin(st))) }; 100 | 101 | Ok(st) 102 | } 103 | 104 | /// Subscribe to a a stream of [Event]s. 105 | /// 106 | /// This is a convenience function for [EventClient::subscribe]. 107 | /// 108 | /// # Example 109 | /// 110 | /// ``` 111 | /// use futures_util::StreamExt; 112 | /// use mev_share_sse::EventClient; 113 | /// # async fn demo() { 114 | /// let client = EventClient::default(); 115 | /// let mut stream = client.events("https://mev-share.flashbots.net").await.unwrap(); 116 | /// while let Some(event) = stream.next().await { 117 | /// dbg!(&event); 118 | /// } 119 | /// # } 120 | /// ``` 121 | pub async fn events(&self, endpoint: &str) -> reqwest::Result> { 122 | self.subscribe(endpoint).await 123 | } 124 | 125 | /// Gets past events that were broadcast via the SSE event stream. 126 | /// 127 | /// Such as `https://mev-share.flashbots.net/api/v1/history`. 128 | /// 129 | /// # Example 130 | /// 131 | /// ``` 132 | /// use mev_share_sse::EventClient; 133 | /// use mev_share_sse::EventHistoryParams; 134 | /// # async fn demo() { 135 | /// let client = EventClient::default(); 136 | /// let params = EventHistoryParams::default(); 137 | /// let history = client.event_history("https://mev-share.flashbots.net/api/v1/history", params).await.unwrap(); 138 | /// dbg!(&history); 139 | /// # } 140 | /// ``` 141 | pub async fn event_history( 142 | &self, 143 | endpoint: &str, 144 | params: EventHistoryParams, 145 | ) -> reqwest::Result> { 146 | self.client.get(endpoint).query(¶ms).send().await?.json().await 147 | } 148 | 149 | /// Gets information about the event history endpoint 150 | /// 151 | /// Such as `https://mev-share.flashbots.net/api/v1/history/info`. 152 | /// 153 | /// # Example 154 | /// 155 | /// ``` 156 | /// use mev_share_sse::EventClient; 157 | /// # async fn demo() { 158 | /// let client = EventClient::default(); 159 | /// let info = client.event_history_info("https://mev-share.flashbots.net/api/v1/history/info").await.unwrap(); 160 | /// dbg!(&info); 161 | /// # } 162 | /// ``` 163 | pub async fn event_history_info(&self, endpoint: &str) -> reqwest::Result { 164 | self.get_json(endpoint).await 165 | } 166 | 167 | async fn get_json(&self, endpoint: &str) -> reqwest::Result { 168 | self.client.get(endpoint).send().await?.json().await 169 | } 170 | } 171 | 172 | impl Default for EventClient { 173 | fn default() -> Self { 174 | Self::new(Default::default()) 175 | } 176 | } 177 | 178 | /// A stream of SSE items 179 | #[must_use = "streams do nothing unless polled"] 180 | pub struct EventStream { 181 | inner: EventStreamInner, 182 | /// State the stream is in 183 | state: Option>, 184 | } 185 | 186 | // === impl EventStream === 187 | 188 | impl EventStream { 189 | /// The endpoint this stream is connected to. 190 | pub fn endpoint(&self) -> &str { 191 | &self.inner.endpoint 192 | } 193 | 194 | /// Resets all retry attempts 195 | pub fn reset_retries(&mut self) { 196 | self.inner.num_retries = 0; 197 | } 198 | } 199 | 200 | impl EventStream { 201 | /// Retries the stream by establishing a new connection. 202 | pub async fn retry(&mut self) -> Result<(), SseError> { 203 | let st = self.inner.retry().await?; 204 | self.state = Some(State::Active(Box::pin(st))); 205 | Ok(()) 206 | } 207 | 208 | /// Retries the stream by establishing a new connection using the given endpoint. 209 | pub async fn retry_with(&mut self, endpoint: impl Into) -> Result<(), SseError> { 210 | self.inner.endpoint = endpoint.into(); 211 | let st = self.inner.retry().await?; 212 | self.state = Some(State::Active(Box::pin(st))); 213 | Ok(()) 214 | } 215 | } 216 | 217 | impl Stream for EventStream { 218 | type Item = Result; 219 | 220 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 221 | let this = self.get_mut(); 222 | let mut res = Poll::Pending; 223 | 224 | loop { 225 | match this.state.take().expect("EventStream polled after completion") { 226 | State::End => return Poll::Ready(None), 227 | State::Retry(mut fut) => match fut.as_mut().poll(cx) { 228 | Poll::Ready(Ok(st)) => { 229 | this.state = Some(State::Active(Box::pin(st))); 230 | continue 231 | } 232 | Poll::Ready(Err(err)) => { 233 | this.state = Some(State::End); 234 | return Poll::Ready(Some(Err(err))) 235 | } 236 | Poll::Pending => { 237 | this.state = Some(State::Retry(fut)); 238 | return Poll::Pending 239 | } 240 | }, 241 | State::Active(mut st) => { 242 | // Active state 243 | match st.as_mut().poll_next(cx) { 244 | Poll::Ready(None) => { 245 | this.state = Some(State::End); 246 | debug!("stream finished"); 247 | return Poll::Ready(None) 248 | } 249 | Poll::Ready(Some(Ok(maybe_event))) => match maybe_event { 250 | EventOrRetry::Event(event) => { 251 | res = Poll::Ready(Some(Ok(event))); 252 | } 253 | EventOrRetry::Retry(duration) => { 254 | let mut client = this.inner.clone(); 255 | let fut = Box::pin(async move { 256 | tokio::time::sleep(duration).await; 257 | client.retry().await 258 | }); 259 | this.state = Some(State::Retry(fut)); 260 | continue 261 | } 262 | }, 263 | Poll::Ready(Some(Err(err))) => { 264 | warn!(?err, "active stream error"); 265 | res = Poll::Ready(Some(Err(err))); 266 | } 267 | Poll::Pending => {} 268 | } 269 | this.state = Some(State::Active(st)); 270 | break 271 | } 272 | } 273 | } 274 | 275 | res 276 | } 277 | } 278 | 279 | impl std::fmt::Debug for EventStream { 280 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 281 | f.debug_struct("EventStream") 282 | .field("endpoint", &self.inner.endpoint) 283 | .field("num_retries", &self.inner.num_retries) 284 | .field("client", &self.inner.client.client) 285 | .finish_non_exhaustive() 286 | } 287 | } 288 | 289 | enum State { 290 | End, 291 | Retry(Pin, SseError>> + Send>>), 292 | Active(Pin>>), 293 | } 294 | 295 | #[derive(Clone)] 296 | struct EventStreamInner { 297 | num_retries: u64, 298 | endpoint: String, 299 | client: EventClient, 300 | query: Option, 301 | } 302 | 303 | // === impl EventStreamInner === 304 | 305 | impl EventStreamInner { 306 | /// Create a new subscription stream. 307 | async fn retry(&mut self) -> Result, SseError> { 308 | self.num_retries += 1; 309 | if let Some(max_retries) = self.client.max_retries { 310 | if self.num_retries > max_retries { 311 | return Err(SseError::MaxRetriesExceeded(max_retries)) 312 | } 313 | } 314 | debug!(retries = self.num_retries, "retrying SSE stream"); 315 | new_stream(&self.client.client, &self.endpoint, self.query.as_ref()) 316 | .map_err(SseError::RetryError) 317 | .await 318 | } 319 | } 320 | 321 | pin_project! { 322 | /// A stream of SSE events. 323 | struct ActiveEventStream { 324 | #[pin] 325 | st: SseDecoderStream 326 | } 327 | } 328 | 329 | impl Stream for ActiveEventStream { 330 | type Item = Result, SseError>; 331 | 332 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 333 | let this = self.project(); 334 | match ready!(this.st.poll_next(cx)) { 335 | None => { 336 | // Stream ended 337 | Poll::Ready(None) 338 | } 339 | Some(res) => { 340 | // Stream has a new event 341 | let item = match res { 342 | Ok(Ok(e)) => Ok(e), 343 | Ok(Err(e)) => Err(SseError::SerdeJsonError(e)), 344 | Err(e) => Err(SseError::Http(e)), 345 | }; 346 | Poll::Ready(Some(item)) 347 | } 348 | } 349 | } 350 | } 351 | 352 | /// Creates a new SSE stream. 353 | async fn new_stream( 354 | client: &reqwest::Client, 355 | endpoint: &str, 356 | query: Option, 357 | ) -> reqwest::Result> { 358 | let mut builder = client 359 | .get(endpoint) 360 | .header(header::ACCEPT, HeaderValue::from_static("text/event-stream")) 361 | .header(header::CACHE_CONTROL, HeaderValue::from_static("no-cache")); 362 | if let Some(query) = query { 363 | builder = builder.query(&query); 364 | } 365 | let resp = builder.send().await?; 366 | let map_io_err: TryIo = |e| io::Error::new(io::ErrorKind::Other, e); 367 | let o: TryOk<_> = |e| match e { 368 | async_sse::Event::Message(msg) => { 369 | trace!( 370 | message = ?String::from_utf8_lossy(msg.data()), 371 | "received message" 372 | ); 373 | serde_json::from_slice::(msg.data()).map(EventOrRetry::Event) 374 | } 375 | async_sse::Event::Retry(duration) => Ok(EventOrRetry::Retry(duration)), 376 | }; 377 | let event_stream: ReqStream = Box::pin(resp.bytes_stream()); 378 | let st = async_sse::decode(event_stream.map_err(map_io_err).into_async_read()).map_ok(o); 379 | Ok(ActiveEventStream { st }) 380 | } 381 | 382 | enum EventOrRetry { 383 | Retry(Duration), 384 | Event(T), 385 | } 386 | 387 | /// Error variants that can occur while handling an SSE subscription, 388 | #[derive(Debug, thiserror::Error)] 389 | pub enum SseError { 390 | /// Failed to deserialize the SSE event data. 391 | #[error("Failed to deserialize event: {0}")] 392 | SerdeJsonError(serde_json::Error), 393 | /// Connection related error 394 | #[error("{0}")] 395 | Http(http_types::Error), 396 | /// Request related error 397 | #[error("Failed to establish a retry connection: {0}")] 398 | RetryError(reqwest::Error), 399 | /// Request related error 400 | #[error("Exceeded all retries: {0}")] 401 | MaxRetriesExceeded(u64), 402 | } 403 | 404 | #[cfg(test)] 405 | mod tests { 406 | use super::*; 407 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 408 | 409 | const HISTORY_V1: &str = "https://mev-share.flashbots.net/api/v1/history"; 410 | const HISTORY_INFO_V1: &str = "https://mev-share.flashbots.net/api/v1/history/info"; 411 | 412 | fn init_tracing() { 413 | let _ = tracing_subscriber::registry() 414 | .with(fmt::layer()) 415 | .with(EnvFilter::from_default_env()) 416 | .try_init(); 417 | } 418 | 419 | #[tokio::test] 420 | #[ignore] 421 | async fn get_event_history_info() { 422 | init_tracing(); 423 | let client = EventClient::default(); 424 | let _info = client.event_history_info(HISTORY_INFO_V1).await.unwrap(); 425 | } 426 | 427 | #[tokio::test] 428 | #[ignore] 429 | async fn get_event_history() { 430 | init_tracing(); 431 | let client = EventClient::default(); 432 | let _history = client.event_history(HISTORY_V1, Default::default()).await.unwrap(); 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /crates/mev-share-sse/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs, unreachable_pub)] 2 | #![deny(unused_must_use, rust_2018_idioms)] 3 | #![doc(test( 4 | no_crate_inject, 5 | attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) 6 | ))] 7 | #![cfg_attr(not(test), warn(unused_crate_dependencies))] 8 | 9 | //! Rust client implementation for the [MEV-share protocol](https://github.com/flashbots/mev-share) 10 | 11 | pub mod client; 12 | pub use client::EventClient; 13 | 14 | #[cfg(feature = "server")] 15 | pub mod server; 16 | 17 | pub use alloy_rpc_types_mev::mevshare::*; 18 | -------------------------------------------------------------------------------- /crates/mev-share-sse/src/server.rs: -------------------------------------------------------------------------------- 1 | //! SSE server side support. 2 | 3 | use futures_util::{Stream, StreamExt}; 4 | use http::{ 5 | header::{CACHE_CONTROL, CONTENT_TYPE}, 6 | StatusCode, 7 | }; 8 | use hyper::{Body, Request, Response}; 9 | use serde::Serialize; 10 | use std::{ 11 | convert::Infallible, 12 | error::Error, 13 | fmt, 14 | future::Future, 15 | pin::Pin, 16 | sync::Arc, 17 | task::{ready, Context, Poll}, 18 | }; 19 | use tokio::sync::broadcast; 20 | use tokio_stream::wrappers::BroadcastStream; 21 | use tokio_util::{compat::FuturesAsyncReadCompatExt, io::ReaderStream}; 22 | use tower::{Layer, Service}; 23 | use tracing::debug; 24 | 25 | /// A helper type that can be used to create a [`SseBroadcastLayer`]. 26 | /// 27 | /// This will broadcast serialized json messages to all subscribers. 28 | #[derive(Clone, Debug)] 29 | pub struct SseBroadcaster { 30 | /// The sender to emit broadcast messages. 31 | sender: broadcast::Sender, 32 | } 33 | 34 | impl SseBroadcaster { 35 | /// Creates a new [`SseBroadcaster`] with the given sender. 36 | pub fn new(sender: broadcast::Sender) -> Self { 37 | Self { sender } 38 | } 39 | 40 | /// Creates a new receiver that's ready to be used. 41 | /// 42 | /// This is intended as the Fn for the [SseBroadcastLayer]. 43 | pub fn ready_stream( 44 | &self, 45 | ) -> futures_util::future::Ready>> { 46 | futures_util::future::ready(Ok(self.stream())) 47 | } 48 | 49 | /// Returns a new stream of [SSeBroadcastMessage]. 50 | pub fn stream(&self) -> SseBroadcastStream { 51 | SseBroadcastStream { st: BroadcastStream::new(self.subscribe()) } 52 | } 53 | 54 | /// Creates a new Receiver handle that will receive values sent after this call to subscribe. 55 | pub fn subscribe(&self) -> broadcast::Receiver { 56 | self.sender.subscribe() 57 | } 58 | 59 | /// Sends a message to all subscribers. 60 | /// 61 | /// See also [`Sender::send`](broadcast::Sender::send) 62 | pub fn send(&self, msg: &T) -> Result { 63 | let msg = SSeBroadcastMessage(Arc::from(serde_json::to_string(msg)?)); 64 | self.sender.send(msg).map_err(|err| SseSendError::ChannelClosed(err.0.as_ref().to_string())) 65 | } 66 | } 67 | 68 | /// Error returned by [`SseBroadcastLayer`]. 69 | #[derive(Debug, thiserror::Error)] 70 | pub enum SseSendError { 71 | /// Failed to serialize the message before sending. 72 | #[error("failed to serialize message: {0}")] 73 | Json(#[from] serde_json::Error), 74 | /// Failed to send the message. 75 | #[error("failed to send message because broadcast channel closed")] 76 | ChannelClosed(String), 77 | } 78 | 79 | /// Helper new type to make Arc AsRef str. 80 | /// 81 | /// Note: This is a workaround for the fact that Arc does not implement AsRef. 82 | #[derive(Clone, Debug)] 83 | pub struct SSeBroadcastMessage(Arc); 84 | 85 | impl AsRef for SSeBroadcastMessage { 86 | fn as_ref(&self) -> &str { 87 | self.0.as_ref() 88 | } 89 | } 90 | 91 | impl fmt::Display for SSeBroadcastMessage { 92 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 93 | self.0.fmt(f) 94 | } 95 | } 96 | 97 | impl Serialize for SSeBroadcastMessage { 98 | fn serialize(&self, serializer: S) -> Result { 99 | self.as_ref().serialize(serializer) 100 | } 101 | } 102 | 103 | /// A Stream that emits SSE messages. 104 | #[must_use = "streams do nothing unless polled"] 105 | #[derive(Debug)] 106 | pub struct SseBroadcastStream { 107 | st: BroadcastStream, 108 | } 109 | 110 | impl Stream for SseBroadcastStream { 111 | type Item = SSeBroadcastMessage; 112 | 113 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 114 | let this = self.get_mut(); 115 | loop { 116 | match ready!(this.st.poll_next_unpin(cx)) { 117 | None => return Poll::Ready(None), 118 | Some(Ok(item)) => return Poll::Ready(Some(item)), 119 | Some(Err(err)) => { 120 | debug!("broadcast stream is lagging: {err}"); 121 | continue 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | /// A service impl that handles SSE requests. 129 | /// 130 | /// 131 | /// # Example 132 | /// 133 | /// ``` 134 | /// use mev_share_sse::server::{SseBroadcaster, SseBroadcastService}; 135 | /// let (tx, _rx) = tokio::sync::broadcast::channel(1000); 136 | /// let broadcaster = SseBroadcaster::new(tx); 137 | /// let svc = SseBroadcastService::new(move || broadcaster.ready_stream()); 138 | /// ``` 139 | #[derive(Debug, Clone)] 140 | pub struct SseBroadcastService { 141 | handler: F, 142 | } 143 | 144 | impl SseBroadcastService { 145 | /// Creates a new [`SseBroadcastService`] with the given handler. 146 | pub fn new(handler: F) -> Self { 147 | Self { handler } 148 | } 149 | } 150 | 151 | impl Service> for SseBroadcastService 152 | where 153 | F: Fn() -> R, 154 | R: Future>> + Send + 'static, 155 | St: Stream + Send + Unpin + 'static, 156 | Item: AsRef + Send + Sync + 'static, 157 | { 158 | type Response = Response; 159 | type Error = Box; 160 | type Future = 161 | Pin> + Send + 'static>>; 162 | 163 | fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { 164 | Poll::Ready(Ok(())) 165 | } 166 | 167 | fn call(&mut self, _: Request) -> Self::Future { 168 | // acquire the receiver and perform the upgrade 169 | let get_receiver = (self.handler)(); 170 | let fut = async { 171 | let st = match get_receiver.await { 172 | Ok(st) => st, 173 | Err(err) => { 174 | return Ok(Response::builder() 175 | .status(StatusCode::INTERNAL_SERVER_ERROR) 176 | .body(Body::from(err.to_string())) 177 | .expect("failed to build response")) 178 | } 179 | }; 180 | let (sender, encoder) = async_sse::encode(); 181 | 182 | tokio::task::spawn(async move { 183 | let mut st = st; 184 | while let Some(data) = st.next().await { 185 | let _ = sender.send(None, data.as_ref(), None).await; 186 | } 187 | }); 188 | 189 | // Perform the handshake as described here: 190 | // 191 | Response::builder() 192 | .status(StatusCode::OK) 193 | .header(CACHE_CONTROL, "no-cache") 194 | .header(CONTENT_TYPE, "text/event-stream") 195 | .body(Body::wrap_stream(ReaderStream::new(encoder.compat()))) 196 | .map_err(|err| err.into()) 197 | }; 198 | 199 | Box::pin(fut) 200 | } 201 | } 202 | 203 | /// Layer that can create [`SseBroadcastProxyService`]. 204 | #[derive(Clone)] 205 | pub struct SseBroadcastLayer { 206 | /// How to create a listener 207 | handler: F, 208 | /// The uri path to intercept 209 | path: Arc, 210 | } 211 | 212 | impl SseBroadcastLayer { 213 | /// Creates a new [`SseBroadcastLayer`] with the given handler and the path to listen on. 214 | pub fn new(path: impl Into, handler: F) -> Self { 215 | Self { path: Arc::new(path.into()), handler } 216 | } 217 | } 218 | 219 | impl Layer for SseBroadcastLayer 220 | where 221 | F: Clone, 222 | { 223 | type Service = SseBroadcastProxyService; 224 | 225 | fn layer(&self, inner: S) -> Self::Service { 226 | SseBroadcastProxyService { 227 | inner, 228 | svc: SseBroadcastService::new(self.handler.clone()), 229 | path: Arc::clone(&self.path), 230 | } 231 | } 232 | } 233 | 234 | /// A service that will stream messages into a http response. 235 | /// 236 | /// Note: This will not set sse id's and will use the default "message" as event name. 237 | #[derive(Debug)] 238 | pub struct SseBroadcastProxyService { 239 | path: Arc, 240 | inner: S, 241 | svc: SseBroadcastService, 242 | } 243 | 244 | impl SseBroadcastProxyService { 245 | /// Returns the path this service is listening on. 246 | pub fn path(&self) -> &str { 247 | self.path.as_str() 248 | } 249 | } 250 | 251 | impl Service> for SseBroadcastProxyService 252 | where 253 | S: Service, Response = Response, Error = Infallible>, 254 | S::Response: 'static, 255 | S::Error: Into> + 'static, 256 | S::Future: Send + 'static, 257 | F: Fn() -> R, 258 | R: Future>> + Send + 'static, 259 | St: Stream + Send + Unpin + 'static, 260 | Item: AsRef + Send + Sync + 'static, 261 | { 262 | type Response = S::Response; 263 | type Error = Box; 264 | type Future = 265 | Pin> + Send + 'static>>; 266 | 267 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 268 | self.inner.poll_ready(cx).map_err(Into::into) 269 | } 270 | 271 | fn call(&mut self, request: Request) -> Self::Future { 272 | // check if the request is for the path we are listening on for SSE 273 | if self.path.as_str() == request.uri() { 274 | return self.svc.call(request) 275 | } 276 | 277 | // delegate to the inner service if path does not match 278 | let fut = self.inner.call(request); 279 | 280 | Box::pin(async move { fut.await.map_err(Into::into) }) 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /crates/mev-share/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mev-share" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | description = """ 11 | MEV-share client in rust 12 | """ 13 | 14 | [dependencies] 15 | mev-share-rpc-api.workspace = true 16 | mev-share-sse = { workspace = true, features = ["server"] } 17 | -------------------------------------------------------------------------------- /crates/mev-share/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # mev-share-rs 2 | //! 3 | //! A client library for MEV-Share written in rust. 4 | 5 | #![warn(missing_docs, unreachable_pub, unused_crate_dependencies)] 6 | #![deny(unused_must_use, rust_2018_idioms)] 7 | #![doc(test( 8 | no_crate_inject, 9 | attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) 10 | ))] 11 | 12 | #[doc(inline)] 13 | pub use mev_share_rpc_api as rpc; 14 | #[doc(inline)] 15 | pub use mev_share_sse as sse; 16 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "examples" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [dependencies] 8 | mev-share-sse = { path = "../crates/mev-share-sse", features = ["server"] } 9 | mev-share-rpc-api = { path = "../crates/mev-share-rpc-api", features = [ 10 | "client", 11 | "server", 12 | ] } 13 | mev-share-backend = { path = "../crates/mev-share-backend" } 14 | 15 | ## async 16 | tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] } 17 | futures-util.workspace = true 18 | jsonrpsee = { workspace = true, features = ["client", "async-client"] } 19 | tower.workspace = true 20 | 21 | ## eth 22 | alloy-primitives.workspace = true 23 | ethers-core.workspace = true 24 | ethers-providers.workspace = true 25 | ethers-signers.workspace = true 26 | ethers-middleware = "2.0" 27 | 28 | ## server 29 | hyper = { workspace = true, features = ["full"] } 30 | 31 | ## misc 32 | tracing.workspace = true 33 | tracing-subscriber = { version = "0.3", default-features = false, features = [ 34 | "env-filter", 35 | "fmt", 36 | ] } 37 | anyhow = "1.0" 38 | 39 | [[example]] 40 | name = "sse" 41 | path = "sse.rs" 42 | 43 | [[example]] 44 | name = "sse-server" 45 | path = "sse-server.rs" 46 | 47 | [[example]] 48 | name = "rpc-client" 49 | path = "rpc-client.rs" 50 | 51 | [[example]] 52 | name = "rpc-sim-service" 53 | path = "rpc-sim-service.rs" 54 | 55 | [[example]] 56 | name = "rpc-client-onchain" 57 | path = "rpc-client-onchain.rs" 58 | -------------------------------------------------------------------------------- /examples/rpc-client-onchain.rs: -------------------------------------------------------------------------------- 1 | //! Basic RPC api example 2 | 3 | use anyhow::Result; 4 | use ethers_core::{ 5 | rand::thread_rng, 6 | types::{ 7 | transaction::eip2718::TypedTransaction, Bytes, Chain, Eip1559TransactionRequest, H256, 8 | }, 9 | }; 10 | use ethers_middleware::MiddlewareBuilder; 11 | use ethers_providers::{Middleware, Provider}; 12 | use ethers_signers::{LocalWallet, Signer}; 13 | use jsonrpsee::{ 14 | core::async_trait, 15 | http_client::{transport::Error as HttpError, HttpClientBuilder}, 16 | }; 17 | use mev_share_rpc_api::{ 18 | BundleItem, FlashbotsApiClient, FlashbotsSignerLayer, Inclusion, MevApiClient, Privacy, 19 | PrivacyHint, PrivateTransactionPreferences, PrivateTransactionRequest, SendBundleRequest, 20 | }; 21 | use std::{env, str::FromStr}; 22 | use tokio::time::Duration; 23 | use tower::ServiceBuilder; 24 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 25 | 26 | /// Sends a transaction to the Goerli mempool and send a request to the Goerli MEV-wShare deployment 27 | /// to backrun it. 28 | /// 29 | /// # Usage 30 | /// 31 | /// Run: 32 | /// 33 | /// ```sh 34 | /// export ETH_RPC_URL="..." 35 | /// export SENDER_PRIVATE_KEY="..." 36 | /// cargo run --example rpc-client-onchain 37 | /// ``` 38 | /// 39 | /// where: 40 | /// 41 | /// - `ETH_RPC_URL` is a Goerli RPC provider (that needs to answer to `eth_getBlockNumber` 42 | /// requests in order to get the target block for our bundle) 43 | /// - `SENDER_PRIVATE_KEY` is the private key of a wallet with some ETH on Goerli to send 44 | /// transactions. 45 | #[tokio::main] 46 | async fn main() -> Result<()> { 47 | tracing_subscriber::registry().with(fmt::layer()).with(EnvFilter::from_default_env()).init(); 48 | 49 | // The signer used to authenticate bundles 50 | let fb_signer = LocalWallet::new(&mut thread_rng()); 51 | 52 | // The signer used to sign our transactions 53 | let tx_signer = 54 | LocalWallet::from_str(&env::var("SENDER_PRIVATE_KEY")?)?.with_chain_id(Chain::Goerli); 55 | 56 | // Set up flashbots-style auth middleware 57 | let signing_middleware = FlashbotsSignerLayer::new(fb_signer); 58 | let service_builder = ServiceBuilder::new() 59 | // map signer errors to http errors 60 | .map_err(HttpError::Http) 61 | .layer(signing_middleware); 62 | 63 | // Set up the rpc client 64 | let url = "https://relay-goerli.flashbots.net:443"; 65 | let client = HttpClientBuilder::default().set_middleware(service_builder).build(url)?; 66 | 67 | // Set up the eth client 68 | let eth_rpc_url = std::env::var("ETH_RPC_URL").expect("ETH_RPC_URL must be set"); 69 | let eth_client = Provider::try_from(eth_rpc_url)? 70 | .with_signer(tx_signer.clone()) 71 | .nonce_manager(tx_signer.address()); 72 | 73 | // Set up a private tx we will try to backrun 74 | let tx_to_backrun = Eip1559TransactionRequest::new() 75 | .to(tx_signer.address()) 76 | .value(100) 77 | .data(b"im backrunniiiiiing") 78 | .fill(ð_client) 79 | .await? 80 | .sign(&tx_signer) 81 | .await?; 82 | 83 | // Build private tx request 84 | let private_tx = PrivateTransactionRequest { 85 | tx: tx_to_backrun, 86 | max_block_number: None, 87 | preferences: PrivateTransactionPreferences { 88 | privacy: Some(Privacy { 89 | hints: Some( 90 | PrivacyHint::default() 91 | .with_calldata() 92 | .with_contract_address() 93 | .with_function_selector(), 94 | ), 95 | ..Default::default() 96 | }), 97 | ..Default::default() 98 | }, 99 | }; 100 | 101 | // Send a private tx 102 | let tx_to_backrun_hash = 103 | mev_share_rpc_api::EthBundleApiClient::send_private_transaction(&client, private_tx) 104 | .await?; 105 | 106 | println!("Sent tx to backrun, hash: {tx_to_backrun_hash:?}"); 107 | 108 | // Our own tx that we want to include in the bundle 109 | let backrun_tx = Eip1559TransactionRequest::new() 110 | .to(tx_signer.address()) 111 | .value(100) 112 | .data(b"im backrunniiiiiing") 113 | .fill(ð_client) 114 | .await? 115 | .sign(&tx_signer) 116 | .await?; 117 | 118 | // Build bundle 119 | let current_block = eth_client.get_block_number().await?; 120 | let target_block = current_block + 2; 121 | let bundle_body = vec![ 122 | BundleItem::Hash { hash: tx_to_backrun_hash }, 123 | BundleItem::Tx { tx: backrun_tx, can_revert: false }, 124 | ]; 125 | let bundle_request = SendBundleRequest { 126 | protocol_version: mev_share_rpc_api::ProtocolVersion::V0_1, 127 | bundle_body, 128 | inclusion: Inclusion { block: target_block, ..Default::default() }, 129 | ..Default::default() 130 | }; 131 | 132 | // Send bundle 133 | let bundle_send_res = client.send_bundle(bundle_request.clone()).await?; 134 | println!("`mev_sendBundle`: {bundle_send_res:?}"); 135 | 136 | let wait_duration = Duration::from_secs(10); 137 | println!("checking bundle stats in {wait_duration:?}..."); 138 | tokio::time::sleep(wait_duration).await; 139 | 140 | // Get bundle stats 141 | let bundle_stats = client.get_bundle_stats(H256::random(), target_block).await; 142 | println!("`flashbots_getBundleStatsV2`: {:?}", bundle_stats); 143 | 144 | // Get user stats 145 | let user_stats = client.get_user_stats(current_block).await; 146 | println!("`flashbots_getUserStatsV2`: {:?}", user_stats); 147 | 148 | Ok(()) 149 | } 150 | 151 | /* Helpers for building and signing transactions. */ 152 | 153 | #[async_trait] 154 | trait TxFill { 155 | async fn fill(mut self, eth_client: &P) -> Result; 156 | } 157 | 158 | #[async_trait] 159 | impl TxFill for Eip1559TransactionRequest { 160 | async fn fill(mut self, eth_client: &P) -> Result { 161 | let mut tx: TypedTransaction = self.into(); 162 | eth_client 163 | .fill_transaction(&mut tx, None) 164 | .await 165 | .expect("Failed to fill backrun transaction"); 166 | Ok(tx) 167 | } 168 | } 169 | 170 | #[async_trait] 171 | trait TxSign { 172 | async fn sign(&self, signer: &S) -> Result 173 | where 174 | S::Error: 'static; 175 | } 176 | 177 | #[async_trait] 178 | impl TxSign for TypedTransaction { 179 | async fn sign(&self, signer: &S) -> Result 180 | where 181 | S::Error: 'static, 182 | { 183 | let signature = signer.sign_transaction(self).await?; 184 | Ok(self.rlp_signed(&signature)) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /examples/rpc-client.rs: -------------------------------------------------------------------------------- 1 | //! Basic RPC api example 2 | 3 | use jsonrpsee::http_client::{transport::Error as HttpError, HttpClientBuilder}; 4 | use mev_share_rpc_api::{ 5 | BundleItem, FlashbotsApiClient, FlashbotsSignerLayer, MevApiClient, SendBundleRequest, 6 | }; 7 | use tower::ServiceBuilder; 8 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 9 | 10 | use ethers_core::{ 11 | rand::thread_rng, 12 | types::{TransactionRequest, H256}, 13 | }; 14 | use ethers_signers::{LocalWallet, Signer}; 15 | 16 | #[tokio::main] 17 | async fn main() { 18 | tracing_subscriber::registry().with(fmt::layer()).with(EnvFilter::from_default_env()).init(); 19 | 20 | // The signer used to authenticate bundles 21 | let fb_signer = LocalWallet::new(&mut thread_rng()); 22 | 23 | // The signer used to sign our transactions 24 | let tx_signer = LocalWallet::new(&mut thread_rng()); 25 | 26 | // Set up flashbots-style auth middleware 27 | let signing_middleware = FlashbotsSignerLayer::new(fb_signer); 28 | let service_builder = ServiceBuilder::new() 29 | // map signer errors to http errors 30 | .map_err(HttpError::Http) 31 | .layer(signing_middleware); 32 | 33 | // Set up the rpc client 34 | let url = "https://relay.flashbots.net:443"; 35 | let client = HttpClientBuilder::default() 36 | .set_middleware(service_builder) 37 | .build(url) 38 | .expect("Failed to create http client"); 39 | 40 | // Hash of the transaction we are trying to backrun 41 | let tx_hash = H256::random(); 42 | 43 | // Our own tx that we want to include in the bundle 44 | let tx = TransactionRequest::pay("vitalik.eth", 100); 45 | let signature = tx_signer.sign_transaction(&tx.clone().into()).await.unwrap(); 46 | let bytes = tx.rlp_signed(&signature); 47 | 48 | // Build bundle 49 | let bundle_body = 50 | vec![BundleItem::Hash { hash: tx_hash }, BundleItem::Tx { tx: bytes, can_revert: false }]; 51 | 52 | let bundle = SendBundleRequest { bundle_body, ..Default::default() }; 53 | 54 | // Send bundle 55 | let send_res = client.send_bundle(bundle.clone()).await; 56 | println!("Got a bundle response: {:?}", send_res); 57 | 58 | // Simulate bundle 59 | let sim_res = client.sim_bundle(bundle, Default::default()).await; 60 | println!("Got a simulation response: {:?}", sim_res); 61 | 62 | // Get bundle stats 63 | if let Ok(bundle) = send_res { 64 | let bundle_stats = client.get_bundle_stats(bundle.bundle_hash, Default::default()).await; 65 | println!("Got a `flashbots_getBundleStatsV2` response: {:?}", bundle_stats); 66 | } 67 | 68 | // Get user stats 69 | let user_stats = client.get_user_stats(Default::default()).await; 70 | println!("Got a `flashbots_getUserStatsV2` response: {:?}", user_stats); 71 | } 72 | -------------------------------------------------------------------------------- /examples/rpc-sim-service.rs: -------------------------------------------------------------------------------- 1 | //! Simulation queue example 2 | 3 | use futures_util::StreamExt; 4 | 5 | use jsonrpsee::http_client::HttpClientBuilder; 6 | use mev_share_rpc_api::{Inclusion, SendBundleRequest}; 7 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 8 | 9 | use ethers_providers::Middleware; 10 | 11 | use mev_share_backend::{BundleSimulatorService, RpcSimulator}; 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | tracing_subscriber::registry().with(fmt::layer()).with(EnvFilter::from_default_env()).init(); 16 | 17 | let eth_rpc_url = std::env::var("ETH_RPC_URL").expect("ETH_RPC_URL must be set"); 18 | // Set up the rpc client 19 | let url = "https://relay.flashbots.net:443"; 20 | let client = HttpClientBuilder::default().build(url).expect("Failed to create http client"); 21 | let sim_client = RpcSimulator::new(client); 22 | 23 | let eth_client = 24 | ethers_providers::Provider::try_from(eth_rpc_url).expect("could not parse ETH_RPC_URL"); 25 | let current_block = 26 | eth_client.get_block_number().await.expect("could not get block number").as_u64(); 27 | 28 | let sim = BundleSimulatorService::new(current_block, sim_client, Default::default()); 29 | 30 | let handle = sim.handle(); 31 | 32 | // subscribe to all bundle simulation results 33 | let mut sim_results = handle.events().results(); 34 | 35 | // spawn the simulation service 36 | let sim = tokio::task::spawn(sim); 37 | 38 | handle 39 | .add_bundle_simulation_with_prio( 40 | SendBundleRequest { 41 | protocol_version: Default::default(), 42 | inclusion: Inclusion::at_block(current_block - 1), 43 | bundle_body: vec![], 44 | validity: None, 45 | privacy: None, 46 | }, 47 | Default::default(), 48 | Default::default(), 49 | ) 50 | .await 51 | .unwrap(); 52 | 53 | let result = sim_results.next().await.unwrap(); 54 | 55 | dbg!(&result); 56 | 57 | sim.await.unwrap(); 58 | } 59 | -------------------------------------------------------------------------------- /examples/sse-server.rs: -------------------------------------------------------------------------------- 1 | //! SSE server example 2 | use alloy_primitives::B256; 3 | use futures_util::StreamExt; 4 | use hyper::{service::make_service_fn, Server}; 5 | use mev_share_sse::{Event, EventClient}; 6 | use std::net::SocketAddr; 7 | use tokio::sync::mpsc; 8 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 9 | 10 | use mev_share_sse::server::{SseBroadcastService, SseBroadcaster}; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<(), Box> { 14 | tracing_subscriber::registry().with(fmt::layer()).with(EnvFilter::from_default_env()).init(); 15 | 16 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 17 | 18 | let (tx, _rx) = tokio::sync::broadcast::channel(1000); 19 | let broadcaster = SseBroadcaster::new(tx); 20 | 21 | let b = broadcaster.clone(); 22 | 23 | let svc = SseBroadcastService::new(move || b.ready_stream()); 24 | 25 | let make_svc = make_service_fn(move |_| { 26 | let svc = svc.clone(); 27 | async { Ok::<_, hyper::Error>(svc) } 28 | }); 29 | 30 | let server = Server::bind(&addr).serve(make_svc); 31 | 32 | tracing::debug!("listening on {}", addr); 33 | 34 | let server = tokio::spawn(server); 35 | 36 | let (tx, mut rx) = mpsc::unbounded_channel(); 37 | let client = EventClient::default(); 38 | let mut stream = client.events(&format!("http://{addr}")).await.unwrap(); 39 | tokio::spawn(async move { 40 | println!("Subscribed to {}", stream.endpoint()); 41 | while let Some(event) = stream.next().await { 42 | let event = event.unwrap(); 43 | println!("Received event from server: {:?}", event); 44 | let _ = tx.send(event); 45 | } 46 | }); 47 | 48 | let event = Event { hash: B256::ZERO, transactions: vec![], logs: vec![] }; 49 | 50 | broadcaster.send(&event).unwrap(); 51 | 52 | let received = rx.recv().await.unwrap(); 53 | assert_eq!(event, received); 54 | println!("Matching response"); 55 | 56 | server.await??; 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /examples/sse.rs: -------------------------------------------------------------------------------- 1 | //! Basic SSE example 2 | use futures_util::StreamExt; 3 | use mev_share_sse::EventClient; 4 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | tracing_subscriber::registry().with(fmt::layer()).with(EnvFilter::from_default_env()).init(); 9 | 10 | let mainnet_sse = "https://mev-share.flashbots.net"; 11 | let client = EventClient::default(); 12 | let mut stream = client.events(mainnet_sse).await.unwrap(); 13 | println!("Subscribed to {}", stream.endpoint()); 14 | 15 | while let Some(event) = stream.next().await { 16 | dbg!(&event); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | imports_granularity = "Crate" 3 | use_small_heuristics = "Max" 4 | comment_width = 100 5 | wrap_comments = true 6 | binop_separator = "Back" 7 | trailing_comma = "Vertical" 8 | trailing_semicolon = false 9 | use_field_init_shorthand = true --------------------------------------------------------------------------------