|
1 | 1 | //! A generic Flashbots bundle API wrapper. |
| 2 | +use std::io::Read; |
| 3 | + |
2 | 4 | use crate::config::BuilderConfig; |
3 | 5 | use alloy::{ |
4 | | - primitives::BlockNumber, |
| 6 | + primitives::{BlockNumber, keccak256, ruint::aliases::B256}, |
5 | 7 | rpc::types::mev::{EthBundleHash, MevSendBundle}, |
| 8 | + signers::Signer, |
6 | 9 | }; |
| 10 | +use axum::body; |
7 | 11 | use eyre::Context as _; |
8 | | -use eyre::eyre; |
9 | | -use init4_bin_base::deps::tracing::debug; |
10 | | -use reqwest::Client as HttpClient; |
| 12 | + |
| 13 | +use reqwest::header::CONTENT_TYPE; |
11 | 14 | use serde_json::json; |
12 | 15 |
|
| 16 | +use init4_bin_base::utils::signer::LocalOrAws; |
| 17 | + |
13 | 18 | /// A wrapper over a `Provider` that adds Flashbots MEV bundle helpers. |
14 | 19 | #[derive(Debug)] |
15 | | -pub struct FlashbotsProvider { |
| 20 | +pub struct Flashbots { |
16 | 21 | /// The base URL for the Flashbots API. |
17 | 22 | pub relay_url: url::Url, |
18 | | - /// Inner HTTP client used for JSON-RPC requests to the relay. |
19 | | - pub inner: HttpClient, |
20 | 23 | /// Builder configuration for the task. |
21 | 24 | pub config: BuilderConfig, |
| 25 | + /// Signer is loaded once at startup. |
| 26 | + signer: LocalOrAws, |
22 | 27 | } |
23 | 28 |
|
24 | | -impl FlashbotsProvider { |
| 29 | +impl Flashbots { |
25 | 30 | /// Wraps a provider with the URL and returns a new `FlashbotsProvider`. |
26 | | - pub fn new(config: &BuilderConfig) -> Self { |
| 31 | + pub async fn new(config: &BuilderConfig) -> Self { |
27 | 32 | let relay_url = |
28 | 33 | config.flashbots_endpoint.as_ref().expect("Flashbots endpoint must be set").clone(); |
29 | | - Self { relay_url, inner: HttpClient::new(), config: config.clone() } |
| 34 | + |
| 35 | + let signer = |
| 36 | + config.connect_builder_signer().await.expect("Local or AWS signer must be set"); |
| 37 | + |
| 38 | + Self { relay_url, config: config.clone(), signer } |
30 | 39 | } |
31 | 40 |
|
32 | | - /// Submit the prepared Flashbots bundle to the relay via `mev_sendBundle`. |
| 41 | + /// Sends a bundle via `mev_sendBundle`. |
33 | 42 | pub async fn send_bundle(&self, bundle: MevSendBundle) -> eyre::Result<EthBundleHash> { |
34 | | - // NB: The Flashbots relay expects a single parameter which is the bundle object. |
35 | | - // Alloy's `raw_request` accepts any serializable params; wrapping in a 1-tuple is fine. |
36 | | - // We POST a JSON-RPC request to the relay URL using our inner HTTP client. |
37 | | - let body = |
38 | | - json!({ "jsonrpc": "2.0", "id": 1, "method": "mev_sendBundle", "params": [bundle] }); |
39 | | - let resp = self |
40 | | - .inner |
41 | | - .post(self.relay_url.as_str()) |
42 | | - .json(&body) |
43 | | - .send() |
44 | | - .await |
45 | | - .wrap_err("mev_sendBundle HTTP request failed")?; |
46 | | - |
47 | | - let v: serde_json::Value = |
48 | | - resp.json().await.wrap_err("failed to parse mev_sendBundle response")?; |
49 | | - if let Some(err) = v.get("error") { |
50 | | - return Err(eyre!("mev_sendBundle error: {}", err)); |
51 | | - } |
52 | | - let result = v.get("result").ok_or_else(|| eyre!("mev_sendBundle missing result"))?; |
53 | | - let hash: EthBundleHash = serde_json::from_value(result.clone()) |
54 | | - .wrap_err("failed to deserialize mev_sendBundle result")?; |
55 | | - debug!(?hash, "mev_sendBundle response"); |
| 43 | + let params = serde_json::to_value(bundle)?; |
| 44 | + let v = self.raw_call("mev_sendBundle", params).await?; |
| 45 | + let hash: EthBundleHash = |
| 46 | + serde_json::from_value(v.get("result").cloned().unwrap_or(serde_json::Value::Null))?; |
56 | 47 | Ok(hash) |
57 | 48 | } |
58 | 49 |
|
59 | 50 | /// Simulate a bundle via `mev_simBundle`. |
60 | 51 | pub async fn simulate_bundle(&self, bundle: MevSendBundle) -> eyre::Result<()> { |
61 | | - let body = |
62 | | - json!({ "jsonrpc": "2.0", "id": 1, "method": "mev_simBundle", "params": [bundle] }); |
63 | | - let resp = self |
64 | | - .inner |
65 | | - .post(self.relay_url.as_str()) |
66 | | - .json(&body) |
67 | | - .send() |
68 | | - .await |
69 | | - .wrap_err("mev_simBundle HTTP request failed")?; |
70 | | - |
71 | | - let v: serde_json::Value = |
72 | | - resp.json().await.wrap_err("failed to parse mev_simBundle response")?; |
73 | | - if let Some(err) = v.get("error") { |
74 | | - return Err(eyre!("mev_simBundle error: {}", err)); |
75 | | - } |
76 | | - debug!(?v, "mev_simBundle response"); |
| 52 | + let params = serde_json::to_value(bundle)?; |
| 53 | + let _ = self.raw_call("mev_simBundle", params).await?; |
77 | 54 | Ok(()) |
78 | 55 | } |
79 | 56 |
|
80 | | - /// Check that status of a bundle |
| 57 | + /// Fetches the bundle status by hash |
81 | 58 | pub async fn bundle_status( |
82 | 59 | &self, |
83 | 60 | _hash: EthBundleHash, |
84 | 61 | block_number: BlockNumber, |
85 | 62 | ) -> eyre::Result<()> { |
86 | 63 | let params = json!({ "bundleHash": _hash, "blockNumber": block_number }); |
87 | | - let body = json!({ "jsonrpc": "2.0", "id": 1, "method": "flashbots_getBundleStatsV2", "params": [params] }); |
88 | | - let resp = self |
89 | | - .inner |
| 64 | + let _ = self.raw_call("flashbots_getBundleStatsV2", params).await?; |
| 65 | + Ok(()) |
| 66 | + } |
| 67 | + |
| 68 | + /// Makes a raw JSON-RPC call with the Flashbots signature header to the method with the given params. |
| 69 | + async fn raw_call( |
| 70 | + &self, |
| 71 | + method: &str, |
| 72 | + params: serde_json::Value, |
| 73 | + ) -> eyre::Result<serde_json::Value> { |
| 74 | + let params = match params { |
| 75 | + serde_json::Value::Array(_) => params, |
| 76 | + other => serde_json::Value::Array(vec![other]), |
| 77 | + }; |
| 78 | + |
| 79 | + let body = json!({"jsonrpc":"2.0","id":1,"method":method,"params":params}); |
| 80 | + let body_bz = serde_json::to_vec(&body)?; |
| 81 | + |
| 82 | + let payload = format!("0x{:x}", keccak256(body_bz.clone())); |
| 83 | + let signature = self.signer.sign_message(payload.as_ref()).await?; |
| 84 | + dbg!(signature.to_string()); |
| 85 | + |
| 86 | + let address = self.signer.address(); |
| 87 | + let value = format!("{}:{}", address, signature); |
| 88 | + dbg!(value.clone()); |
| 89 | + |
| 90 | + let client = reqwest::Client::new(); |
| 91 | + let resp = client |
90 | 92 | .post(self.relay_url.as_str()) |
91 | | - .json(&body) |
| 93 | + .header(CONTENT_TYPE, "application/json") |
| 94 | + .header("X-Flashbots-Signature", value) |
| 95 | + .body(body_bz) |
92 | 96 | .send() |
93 | | - .await |
94 | | - .wrap_err("flashbots_getBundleStatsV2 HTTP request failed")?; |
95 | | - |
| 97 | + .await?; |
| 98 | + let text = resp.text().await?; |
96 | 99 | let v: serde_json::Value = |
97 | | - resp.json().await.wrap_err("failed to parse flashbots_getBundleStatsV2 response")?; |
| 100 | + serde_json::from_str(&text).wrap_err("failed to parse flashbots JSON")?; |
98 | 101 | if let Some(err) = v.get("error") { |
99 | | - return Err(eyre!("flashbots_getBundleStatsV2 error: {}", err)); |
| 102 | + eyre::bail!("flashbots error: {err}"); |
100 | 103 | } |
101 | | - debug!(?v, "flashbots_getBundleStatsV2 response"); |
102 | | - Ok(()) |
| 104 | + Ok(v) |
103 | 105 | } |
104 | 106 | } |
105 | 107 |
|
106 | | -// (no additional helpers) |
| 108 | +// Raw Flashbots JSON-RPC call with header signing via reqwest. |
| 109 | +impl Flashbots {} |
0 commit comments