Skip to content

Commit f22abd3

Browse files
authored
feat: add Flashbots Provider, ported from builder (#71)
* feat: add Flashbots Provider, ported from builder * fix: statics * lint: clippy
1 parent c0c5508 commit f22abd3

File tree

5 files changed

+260
-2
lines changed

5 files changed

+260
-2
lines changed

Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ name = "init4-bin-base"
44
description = "Internal utilities for binaries produced by the init4 team"
55
keywords = ["init4", "bin", "base"]
66

7-
version = "0.12.3"
7+
version = "0.12.4"
88
edition = "2021"
9-
rust-version = "1.81"
9+
rust-version = "1.83"
1010
authors = ["init4", "James Prestwich", "evalir"]
1111
license = "MIT OR Apache-2.0"
1212
homepage = "https://github.com/init4tech/bin-base"
@@ -59,6 +59,7 @@ aws-config = { version = "1.1.7", optional = true }
5959
aws-sdk-kms = { version = "1.15.0", optional = true }
6060
reqwest = { version = "0.12.15", optional = true }
6161
rustls = { version = "0.23.31", optional = true }
62+
serde_json = { version = "1.0.145", optional = true }
6263

6364
[dev-dependencies]
6465
ajj = "0.3.1"
@@ -71,6 +72,7 @@ tokio = { version = "1.43.0", features = ["macros"] }
7172
[features]
7273
default = ["alloy", "rustls"]
7374
alloy = ["dep:alloy"]
75+
flashbots = ["alloy", "aws", "alloy?/json-rpc", "dep:eyre", "dep:reqwest", "dep:serde_json"]
7476
aws = ["alloy", "alloy?/signer-aws", "dep:async-trait", "dep:aws-config", "dep:aws-sdk-kms"]
7577
perms = ["dep:oauth2", "dep:tokio", "dep:reqwest", "dep:signet-tx-cache", "dep:eyre", "dep:axum", "dep:tower"]
7678
rustls = ["dep:rustls", "rustls/aws-lc-rs"]

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ pub mod utils {
2222
/// slot.
2323
pub mod calc;
2424

25+
/// A simple interface to flashbots RPC endpoints.
26+
#[cfg(feature = "flashbots")]
27+
pub mod flashbots;
28+
2529
/// [`FromEnv`], [`FromEnvVar`] traits and related utilities.
2630
///
2731
/// [`FromEnv`]: from_env::FromEnv

src/utils/flashbots.rs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
//! A generic Flashbots bundle API wrapper.
2+
use crate::utils::signer::LocalOrAws;
3+
use alloy::{
4+
primitives::{keccak256, BlockNumber},
5+
rpc::{
6+
json_rpc::{Id, Response, ResponsePayload, RpcRecv, RpcSend},
7+
types::mev::{EthBundleHash, MevSendBundle, SimBundleResponse},
8+
},
9+
signers::Signer,
10+
};
11+
use init4_from_env_derive::FromEnv;
12+
use reqwest::header::CONTENT_TYPE;
13+
use serde_json::json;
14+
use std::borrow::Cow;
15+
16+
/// Configuration for the Flashbots provider.
17+
#[derive(Debug, Clone, FromEnv)]
18+
#[from_env(crate)]
19+
pub struct FlashbotsConfig {
20+
/// Flashbots endpoint for privately submitting rollup blocks.
21+
#[from_env(
22+
var = "FLASHBOTS_ENDPOINT",
23+
desc = "Flashbots endpoint for privately submitting rollup blocks",
24+
optional
25+
)]
26+
pub flashbots_endpoint: Option<url::Url>,
27+
}
28+
29+
impl FlashbotsConfig {
30+
/// Make a [`Flashbots`] instance from this config, using the specified signer.
31+
pub fn build(&self, signer: LocalOrAws) -> Option<Flashbots> {
32+
self.flashbots_endpoint
33+
.as_ref()
34+
.map(|url| Flashbots::new(url.clone(), signer))
35+
}
36+
}
37+
38+
/// A basic provider for common Flashbots Relay endpoints.
39+
#[derive(Debug)]
40+
pub struct Flashbots {
41+
/// The base URL for the Flashbots API.
42+
pub relay_url: url::Url,
43+
44+
/// Signer is loaded once at startup.
45+
signer: LocalOrAws,
46+
47+
/// The reqwest client to use for requests.
48+
client: reqwest::Client,
49+
}
50+
51+
impl Flashbots {
52+
/// Instantiate a new provider from the URL and signer.
53+
pub fn new(relay_url: url::Url, signer: LocalOrAws) -> Self {
54+
Self {
55+
relay_url,
56+
client: Default::default(),
57+
signer,
58+
}
59+
}
60+
61+
/// Instantiate a new provider from the URL and signer, with a specific
62+
/// Reqwest client.
63+
pub const fn new_with_client(
64+
relay_url: url::Url,
65+
signer: LocalOrAws,
66+
client: reqwest::Client,
67+
) -> Self {
68+
Self {
69+
relay_url,
70+
client,
71+
signer,
72+
}
73+
}
74+
75+
/// Sends a bundle via `mev_sendBundle`.
76+
pub async fn send_bundle(&self, bundle: &MevSendBundle) -> eyre::Result<EthBundleHash> {
77+
self.raw_call("mev_sendBundle", &[bundle]).await
78+
}
79+
80+
/// Simulate a bundle via `mev_simBundle`.
81+
pub async fn simulate_bundle(&self, bundle: &MevSendBundle) -> eyre::Result<()> {
82+
let resp: SimBundleResponse = self.raw_call("mev_simBundle", &[bundle]).await?;
83+
dbg!("successfully simulated bundle", &resp);
84+
Ok(())
85+
}
86+
87+
/// Fetch the bundle status by hash.
88+
pub async fn bundle_status(
89+
&self,
90+
hash: EthBundleHash,
91+
block_number: BlockNumber,
92+
) -> eyre::Result<()> {
93+
let params = json!({ "bundleHash": hash, "blockNumber": block_number });
94+
let _resp: serde_json::Value = self
95+
.raw_call("flashbots_getBundleStatsV2", &[params])
96+
.await?;
97+
98+
Ok(())
99+
}
100+
101+
/// Make a raw JSON-RPC call with the Flashbots signature header to the
102+
/// method with the given params.
103+
async fn raw_call<Params: RpcSend, Payload: RpcRecv>(
104+
&self,
105+
method: &str,
106+
params: &Params,
107+
) -> eyre::Result<Payload> {
108+
let req = alloy::rpc::json_rpc::Request::new(
109+
Cow::Owned(method.to_string()),
110+
Id::Number(1),
111+
params,
112+
);
113+
let body_bz = serde_json::to_vec(&req)?;
114+
drop(req);
115+
116+
let value = self.compute_signature(&body_bz).await?;
117+
118+
let resp = self
119+
.client
120+
.post(self.relay_url.as_str())
121+
.header(CONTENT_TYPE, "application/json")
122+
.header("X-Flashbots-Signature", value)
123+
.body(body_bz)
124+
.send()
125+
.await?;
126+
127+
let resp: Response<Payload> = resp.json().await?;
128+
129+
match resp.payload {
130+
ResponsePayload::Success(payload) => Ok(payload),
131+
ResponsePayload::Failure(err) => {
132+
eyre::bail!("flashbots error: {err}");
133+
}
134+
}
135+
}
136+
137+
/// Builds an EIP-191 signature for the given body bytes. This signature is
138+
/// used to authenticate to the relay API via a header
139+
async fn compute_signature(&self, body_bz: &[u8]) -> Result<String, eyre::Error> {
140+
let payload = keccak256(body_bz).to_string();
141+
let signature = self.signer.sign_message(payload.as_ref()).await?;
142+
dbg!(signature.to_string());
143+
let address = self.signer.address();
144+
let value = format!("{address}:{signature}");
145+
Ok(value)
146+
}
147+
}

src/utils/signer.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::utils::from_env::FromEnv;
22
use alloy::{
33
consensus::SignableTransaction,
4+
network::{Ethereum, EthereumWallet, IntoWallet},
45
primitives::{Address, ChainId, B256},
56
signers::{
67
aws::{AwsSigner, AwsSignerError},
@@ -169,3 +170,11 @@ impl alloy::signers::Signer<Signature> for LocalOrAws {
169170
}
170171
}
171172
}
173+
174+
impl IntoWallet<Ethereum> for LocalOrAws {
175+
type NetworkWallet = EthereumWallet;
176+
177+
fn into_wallet(self) -> Self::NetworkWallet {
178+
EthereumWallet::from(self)
179+
}
180+
}

tests/flashbots.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#![cfg(feature = "flashbots")]
2+
3+
use alloy::{
4+
eips::Encodable2718,
5+
network::EthereumWallet,
6+
primitives::{B256, U256},
7+
providers::{
8+
fillers::{
9+
BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller,
10+
WalletFiller,
11+
},
12+
Identity, Provider, ProviderBuilder, SendableTx,
13+
},
14+
rpc::types::{
15+
mev::{BundleItem, MevSendBundle, ProtocolVersion},
16+
TransactionRequest,
17+
},
18+
signers::{local::PrivateKeySigner, Signer},
19+
};
20+
use init4_bin_base::utils::{flashbots::Flashbots, signer::LocalOrAws};
21+
use std::sync::LazyLock;
22+
use url::Url;
23+
24+
static FLASHBOTS_URL: LazyLock<Url> = LazyLock::new(|| {
25+
Url::parse("https://relay-sepolia.flashbots.net:443").expect("valid flashbots url")
26+
});
27+
static BUILDER_KEY: LazyLock<LocalOrAws> = LazyLock::new(|| {
28+
LocalOrAws::Local(PrivateKeySigner::from_bytes(&B256::repeat_byte(0x02)).unwrap())
29+
});
30+
static TEST_PROVIDER: LazyLock<Flashbots> = LazyLock::new(get_test_provider);
31+
32+
fn get_test_provider() -> Flashbots {
33+
Flashbots::new(FLASHBOTS_URL.clone(), BUILDER_KEY.clone())
34+
}
35+
36+
#[allow(clippy::type_complexity)]
37+
fn get_sepolia() -> FillProvider<
38+
JoinFill<
39+
JoinFill<
40+
Identity,
41+
JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
42+
>,
43+
WalletFiller<EthereumWallet>,
44+
>,
45+
alloy::providers::RootProvider,
46+
> {
47+
ProviderBuilder::new()
48+
.wallet(BUILDER_KEY.clone())
49+
.connect_http(
50+
"https://ethereum-sepolia-rpc.publicnode.com"
51+
.parse()
52+
.unwrap(),
53+
)
54+
}
55+
56+
#[tokio::test]
57+
#[ignore = "integration test"]
58+
async fn test_simulate_valid_bundle_sepolia() {
59+
let flashbots = &*TEST_PROVIDER;
60+
let sepolia = get_sepolia();
61+
62+
let req = TransactionRequest::default()
63+
.to(BUILDER_KEY.address())
64+
.value(U256::from(1u64))
65+
.gas_limit(51_000)
66+
.from(BUILDER_KEY.address());
67+
let SendableTx::Envelope(tx) = sepolia.fill(req).await.unwrap() else {
68+
panic!("expected filled tx");
69+
};
70+
let tx_bytes = tx.encoded_2718().into();
71+
72+
let latest_block = sepolia
73+
.get_block_by_number(alloy::eips::BlockNumberOrTag::Latest)
74+
.await
75+
.unwrap()
76+
.unwrap()
77+
.number();
78+
79+
let bundle_body = vec![BundleItem::Tx {
80+
tx: tx_bytes,
81+
can_revert: true,
82+
}];
83+
let bundle = MevSendBundle::new(latest_block, Some(0), ProtocolVersion::V0_1, bundle_body);
84+
85+
let err = flashbots
86+
.simulate_bundle(&bundle)
87+
.await
88+
.unwrap_err()
89+
.to_string();
90+
// If we have hit this point, we have succesfully authed to the flashbots
91+
// api via header
92+
assert!(
93+
err.contains("insufficient funds for gas"),
94+
"unexpected error: {err}"
95+
);
96+
}

0 commit comments

Comments
 (0)