Skip to content

Commit e971033

Browse files
committed
feat: add Flashbots Provider, ported from builder
1 parent 9b9249c commit e971033

File tree

5 files changed

+236
-1
lines changed

5 files changed

+236
-1
lines changed

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ 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.0"
7+
version = "0.12.1"
88
edition = "2021"
99
rust-version = "1.81"
1010
authors = ["init4", "James Prestwich"]
@@ -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,9 +72,11 @@ 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"]
79+
serde_json = ["dep:serde_json"]
7780

7881
[[example]]
7982
name = "oauth"

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: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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 wrapper over a `Provider` that adds Flashbots MEV bundle helpers.
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+
48+
impl Flashbots {
49+
/// Wraps a provider with the URL and returns a new `FlashbotsProvider`.
50+
pub const fn new(relay_url: url::Url, signer: LocalOrAws) -> Self {
51+
Self { relay_url, signer }
52+
}
53+
54+
/// Sends a bundle via `mev_sendBundle`.
55+
pub async fn send_bundle(&self, bundle: &MevSendBundle) -> eyre::Result<EthBundleHash> {
56+
self.raw_call("mev_sendBundle", &[bundle]).await
57+
}
58+
59+
/// Simulate a bundle via `mev_simBundle`.
60+
pub async fn simulate_bundle(&self, bundle: &MevSendBundle) -> eyre::Result<()> {
61+
let resp: SimBundleResponse = self.raw_call("mev_simBundle", &[bundle]).await?;
62+
dbg!("successfully simulated bundle", &resp);
63+
Ok(())
64+
}
65+
66+
/// Fetches the bundle status by hash
67+
pub async fn bundle_status(
68+
&self,
69+
hash: EthBundleHash,
70+
block_number: BlockNumber,
71+
) -> eyre::Result<()> {
72+
let params = json!({ "bundleHash": hash, "blockNumber": block_number });
73+
let _resp: serde_json::Value = self
74+
.raw_call("flashbots_getBundleStatsV2", &[params])
75+
.await?;
76+
77+
Ok(())
78+
}
79+
80+
/// Makes a raw JSON-RPC call with the Flashbots signature header to the method with the given params.
81+
async fn raw_call<Params: RpcSend, Payload: RpcRecv>(
82+
&self,
83+
method: &str,
84+
params: &Params,
85+
) -> eyre::Result<Payload> {
86+
let req = alloy::rpc::json_rpc::Request::new(
87+
Cow::Owned(method.to_string()),
88+
Id::Number(1),
89+
params,
90+
);
91+
let body_bz = serde_json::to_vec(&req)?;
92+
drop(req);
93+
94+
let value = self.compute_signature(&body_bz).await?;
95+
96+
let client = reqwest::Client::new();
97+
let resp = client
98+
.post(self.relay_url.as_str())
99+
.header(CONTENT_TYPE, "application/json")
100+
.header("X-Flashbots-Signature", value)
101+
.body(body_bz)
102+
.send()
103+
.await?;
104+
105+
let resp: Response<Payload> = resp.json().await?;
106+
107+
match resp.payload {
108+
ResponsePayload::Success(payload) => Ok(payload),
109+
ResponsePayload::Failure(err) => {
110+
eyre::bail!("flashbots error: {err}");
111+
}
112+
}
113+
}
114+
115+
/// Builds an EIP-191 signature for the given body bytes.
116+
async fn compute_signature(&self, body_bz: &[u8]) -> Result<String, eyre::Error> {
117+
let payload = keccak256(body_bz).to_string();
118+
let signature = self.signer.sign_message(payload.as_ref()).await?;
119+
dbg!(signature.to_string());
120+
let address = self.signer.address();
121+
let value = format!("{address}:{signature}");
122+
Ok(value)
123+
}
124+
}

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: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
const FLASHBOTS_URL: LazyLock<Url> = LazyLock::new(|| {
25+
Url::parse("https://relay-sepolia.flashbots.net:443").expect("valid flashbots url")
26+
});
27+
const BUILDER_KEY: LazyLock<LocalOrAws> = LazyLock::new(|| {
28+
LocalOrAws::Local(PrivateKeySigner::from_bytes(&B256::repeat_byte(0x02)).unwrap())
29+
});
30+
const 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+
fn get_sepolia() -> FillProvider<
37+
JoinFill<
38+
JoinFill<
39+
Identity,
40+
JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
41+
>,
42+
WalletFiller<EthereumWallet>,
43+
>,
44+
alloy::providers::RootProvider,
45+
> {
46+
ProviderBuilder::new()
47+
.wallet(BUILDER_KEY.clone())
48+
.connect_http(
49+
"https://ethereum-sepolia-rpc.publicnode.com"
50+
.parse()
51+
.unwrap(),
52+
)
53+
}
54+
55+
#[tokio::test]
56+
// #[ignore = "integration test"]
57+
async fn test_simulate_valid_bundle_sepolia() {
58+
let flashbots = &*TEST_PROVIDER;
59+
let sepolia = get_sepolia();
60+
61+
let req = TransactionRequest::default()
62+
.to(BUILDER_KEY.address())
63+
.value(U256::from(1u64))
64+
.gas_limit(51_000)
65+
.from(BUILDER_KEY.address());
66+
let SendableTx::Envelope(tx) = sepolia.fill(req).await.unwrap() else {
67+
panic!("expected filled tx");
68+
};
69+
let tx_bytes = tx.encoded_2718().into();
70+
71+
let latest_block = sepolia
72+
.get_block_by_number(alloy::eips::BlockNumberOrTag::Latest)
73+
.await
74+
.unwrap()
75+
.unwrap()
76+
.number();
77+
78+
let bundle_body = vec![BundleItem::Tx {
79+
tx: tx_bytes,
80+
can_revert: true,
81+
}];
82+
let bundle = MevSendBundle::new(latest_block, Some(0), ProtocolVersion::V0_1, bundle_body);
83+
84+
let err = flashbots
85+
.simulate_bundle(&bundle)
86+
.await
87+
.unwrap_err()
88+
.to_string();
89+
// If we have hit this point, we have succesfully authed to the flashbots
90+
// api via header
91+
assert!(
92+
err.contains("insufficient funds for gas"),
93+
"unexpected error: {err}"
94+
);
95+
}

0 commit comments

Comments
 (0)