Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ name = "init4-bin-base"
description = "Internal utilities for binaries produced by the init4 team"
keywords = ["init4", "bin", "base"]

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

[dev-dependencies]
ajj = "0.3.1"
Expand All @@ -71,6 +72,7 @@ tokio = { version = "1.43.0", features = ["macros"] }
[features]
default = ["alloy", "rustls"]
alloy = ["dep:alloy"]
flashbots = ["alloy", "aws", "alloy?/json-rpc", "dep:eyre", "dep:reqwest", "dep:serde_json"]
aws = ["alloy", "alloy?/signer-aws", "dep:async-trait", "dep:aws-config", "dep:aws-sdk-kms"]
perms = ["dep:oauth2", "dep:tokio", "dep:reqwest", "dep:signet-tx-cache", "dep:eyre", "dep:axum", "dep:tower"]
rustls = ["dep:rustls", "rustls/aws-lc-rs"]
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ pub mod utils {
/// slot.
pub mod calc;

/// A simple interface to flashbots RPC endpoints.
#[cfg(feature = "flashbots")]
pub mod flashbots;

/// [`FromEnv`], [`FromEnvVar`] traits and related utilities.
///
/// [`FromEnv`]: from_env::FromEnv
Expand Down
147 changes: 147 additions & 0 deletions src/utils/flashbots.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
//! A generic Flashbots bundle API wrapper.
use crate::utils::signer::LocalOrAws;
use alloy::{
primitives::{keccak256, BlockNumber},
rpc::{
json_rpc::{Id, Response, ResponsePayload, RpcRecv, RpcSend},
types::mev::{EthBundleHash, MevSendBundle, SimBundleResponse},
},
signers::Signer,
};
use init4_from_env_derive::FromEnv;
use reqwest::header::CONTENT_TYPE;
use serde_json::json;
use std::borrow::Cow;

/// Configuration for the Flashbots provider.
#[derive(Debug, Clone, FromEnv)]
#[from_env(crate)]
pub struct FlashbotsConfig {
/// Flashbots endpoint for privately submitting rollup blocks.
#[from_env(
var = "FLASHBOTS_ENDPOINT",
desc = "Flashbots endpoint for privately submitting rollup blocks",
optional
)]
pub flashbots_endpoint: Option<url::Url>,
}

impl FlashbotsConfig {
/// Make a [`Flashbots`] instance from this config, using the specified signer.
pub fn build(&self, signer: LocalOrAws) -> Option<Flashbots> {
self.flashbots_endpoint
.as_ref()
.map(|url| Flashbots::new(url.clone(), signer))
}
}

/// A basic provider for common Flashbots Relay endpoints.
#[derive(Debug)]
pub struct Flashbots {
/// The base URL for the Flashbots API.
pub relay_url: url::Url,

/// Signer is loaded once at startup.
signer: LocalOrAws,

/// The reqwest client to use for requests.
client: reqwest::Client,
}

impl Flashbots {
/// Instantiate a new provider from the URL and signer.
pub fn new(relay_url: url::Url, signer: LocalOrAws) -> Self {
Self {
relay_url,
client: Default::default(),
signer,
}
}

/// Instantiate a new provider from the URL and signer, with a specific
/// Reqwest client.
pub const fn new_with_client(
relay_url: url::Url,
signer: LocalOrAws,
client: reqwest::Client,
) -> Self {
Self {
relay_url,
client,
signer,
}
}

/// Sends a bundle via `mev_sendBundle`.
pub async fn send_bundle(&self, bundle: &MevSendBundle) -> eyre::Result<EthBundleHash> {
self.raw_call("mev_sendBundle", &[bundle]).await
}

/// Simulate a bundle via `mev_simBundle`.
pub async fn simulate_bundle(&self, bundle: &MevSendBundle) -> eyre::Result<()> {
let resp: SimBundleResponse = self.raw_call("mev_simBundle", &[bundle]).await?;
dbg!("successfully simulated bundle", &resp);
Ok(())
}

/// Fetch the bundle status by hash.
pub async fn bundle_status(
&self,
hash: EthBundleHash,
block_number: BlockNumber,
) -> eyre::Result<()> {
let params = json!({ "bundleHash": hash, "blockNumber": block_number });
let _resp: serde_json::Value = self
.raw_call("flashbots_getBundleStatsV2", &[params])
.await?;

Ok(())
}

/// Make a raw JSON-RPC call with the Flashbots signature header to the
/// method with the given params.
async fn raw_call<Params: RpcSend, Payload: RpcRecv>(
&self,
method: &str,
params: &Params,
) -> eyre::Result<Payload> {
let req = alloy::rpc::json_rpc::Request::new(
Cow::Owned(method.to_string()),
Id::Number(1),
params,
);
let body_bz = serde_json::to_vec(&req)?;
drop(req);

let value = self.compute_signature(&body_bz).await?;

let resp = self
.client
.post(self.relay_url.as_str())
.header(CONTENT_TYPE, "application/json")
.header("X-Flashbots-Signature", value)
.body(body_bz)
.send()
.await?;

let resp: Response<Payload> = resp.json().await?;

match resp.payload {
ResponsePayload::Success(payload) => Ok(payload),
ResponsePayload::Failure(err) => {
eyre::bail!("flashbots error: {err}");
}
}
}

/// Builds an EIP-191 signature for the given body bytes. This signature is
/// used to authenticate to the relay API via a header
async fn compute_signature(&self, body_bz: &[u8]) -> Result<String, eyre::Error> {
let payload = keccak256(body_bz).to_string();
let signature = self.signer.sign_message(payload.as_ref()).await?;
dbg!(signature.to_string());
let address = self.signer.address();
let value = format!("{address}:{signature}");
Ok(value)
}
}
9 changes: 9 additions & 0 deletions src/utils/signer.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::utils::from_env::FromEnv;
use alloy::{
consensus::SignableTransaction,
network::{Ethereum, EthereumWallet, IntoWallet},
primitives::{Address, ChainId, B256},
signers::{
aws::{AwsSigner, AwsSignerError},
Expand Down Expand Up @@ -169,3 +170,11 @@ impl alloy::signers::Signer<Signature> for LocalOrAws {
}
}
}

impl IntoWallet<Ethereum> for LocalOrAws {
type NetworkWallet = EthereumWallet;

fn into_wallet(self) -> Self::NetworkWallet {
EthereumWallet::from(self)
}
}
96 changes: 96 additions & 0 deletions tests/flashbots.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#![cfg(feature = "flashbots")]

use alloy::{
eips::Encodable2718,
network::EthereumWallet,
primitives::{B256, U256},
providers::{
fillers::{
BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller,
WalletFiller,
},
Identity, Provider, ProviderBuilder, SendableTx,
},
rpc::types::{
mev::{BundleItem, MevSendBundle, ProtocolVersion},
TransactionRequest,
},
signers::{local::PrivateKeySigner, Signer},
};
use init4_bin_base::utils::{flashbots::Flashbots, signer::LocalOrAws};
use std::sync::LazyLock;
use url::Url;

static FLASHBOTS_URL: LazyLock<Url> = LazyLock::new(|| {
Url::parse("https://relay-sepolia.flashbots.net:443").expect("valid flashbots url")
});
static BUILDER_KEY: LazyLock<LocalOrAws> = LazyLock::new(|| {
LocalOrAws::Local(PrivateKeySigner::from_bytes(&B256::repeat_byte(0x02)).unwrap())
});
static TEST_PROVIDER: LazyLock<Flashbots> = LazyLock::new(get_test_provider);

fn get_test_provider() -> Flashbots {
Flashbots::new(FLASHBOTS_URL.clone(), BUILDER_KEY.clone())
}

#[allow(clippy::type_complexity)]
fn get_sepolia() -> FillProvider<
JoinFill<
JoinFill<
Identity,
JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
>,
WalletFiller<EthereumWallet>,
>,
alloy::providers::RootProvider,
> {
ProviderBuilder::new()
.wallet(BUILDER_KEY.clone())
.connect_http(
"https://ethereum-sepolia-rpc.publicnode.com"
.parse()
.unwrap(),
)
}

#[tokio::test]
#[ignore = "integration test"]
async fn test_simulate_valid_bundle_sepolia() {
let flashbots = &*TEST_PROVIDER;
let sepolia = get_sepolia();

let req = TransactionRequest::default()
.to(BUILDER_KEY.address())
.value(U256::from(1u64))
.gas_limit(51_000)
.from(BUILDER_KEY.address());
let SendableTx::Envelope(tx) = sepolia.fill(req).await.unwrap() else {
panic!("expected filled tx");
};
let tx_bytes = tx.encoded_2718().into();

let latest_block = sepolia
.get_block_by_number(alloy::eips::BlockNumberOrTag::Latest)
.await
.unwrap()
.unwrap()
.number();

let bundle_body = vec![BundleItem::Tx {
tx: tx_bytes,
can_revert: true,
}];
let bundle = MevSendBundle::new(latest_block, Some(0), ProtocolVersion::V0_1, bundle_body);

let err = flashbots
.simulate_bundle(&bundle)
.await
.unwrap_err()
.to_string();
// If we have hit this point, we have succesfully authed to the flashbots
// api via header
assert!(
err.contains("insufficient funds for gas"),
"unexpected error: {err}"
);
}