Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ signet-constants = { version = "0.11.1" }
signet-tx-cache = { version = "0.11.1", optional = true }

# alloy
alloy = { version = "1.0.35", optional = true, default-features = false, features = ["std", "signer-local", "consensus", "network"] }
alloy = { version = "1.0.35", optional = true, default-features = false, features = ["std", "signer-local", "consensus", "network", "provider-mev-api"] }

# Tracing
tracing = "0.1.40"
Expand Down
24 changes: 5 additions & 19 deletions src/utils/flashbots.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! A generic Flashbots bundle API wrapper.
use crate::utils::signer::LocalOrAws;
use alloy::{
primitives::{keccak256, BlockNumber},
primitives::keccak256,
rpc::{
json_rpc::{Id, Response, ResponsePayload, RpcRecv, RpcSend},
types::mev::{EthBundleHash, MevSendBundle, SimBundleResponse},
Expand All @@ -10,7 +10,6 @@ use alloy::{
};
use init4_from_env_derive::FromEnv;
use reqwest::header::CONTENT_TYPE;
use serde_json::json;
use std::borrow::Cow;

/// Configuration for the Flashbots provider.
Expand Down Expand Up @@ -74,27 +73,15 @@ impl Flashbots {

/// Sends a bundle via `mev_sendBundle`.
pub async fn send_bundle(&self, bundle: &MevSendBundle) -> eyre::Result<EthBundleHash> {
self.raw_call("mev_sendBundle", &[bundle]).await
let resp = self.raw_call("mev_sendBundle", &[bundle]).await?;
dbg!("sim bundle response", &resp);
Ok(resp)
}

/// 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?;

dbg!("send bundle response ###", resp);
Ok(())
}

Expand Down Expand Up @@ -139,7 +126,6 @@ impl Flashbots {
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)
Expand Down
273 changes: 225 additions & 48 deletions tests/flashbots.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,56 @@
#![cfg(feature = "flashbots")]

use alloy::{
eips::Encodable2718,
consensus::constants::GWEI_TO_WEI,
eips::{BlockId, Encodable2718},
network::EthereumWallet,
primitives::{B256, U256},
providers::{
ext::MevApi,
fillers::{
BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller,
WalletFiller,
},
Identity, Provider, ProviderBuilder, SendableTx,
},
rpc::types::{
mev::{BundleItem, MevSendBundle, ProtocolVersion},
mev::{EthCallBundle, EthSendBundle},
TransactionRequest,
},
signers::{local::PrivateKeySigner, Signer},
signers::Signer,
};
use init4_bin_base::{
deps::tracing_subscriber::{
fmt, layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter, Layer,
},
utils::signer::LocalOrAws,
};
use std::{
env,
sync::LazyLock,
time::{Duration, Instant},
};
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")
/// Hoodi endpoints
static TITANBUILDER_HOODI_RPC: LazyLock<Url> = LazyLock::new(|| {
Url::parse("https://rpc-hoodi.titanbuilder.xyz/").expect("valid flashbots url")
});
static BUILDER_KEY: LazyLock<LocalOrAws> = LazyLock::new(|| {
LocalOrAws::Local(PrivateKeySigner::from_bytes(&B256::repeat_byte(0x02)).unwrap())

static HOODI_HOST_RPC: LazyLock<Url> = LazyLock::new(|| {
Url::parse("https://ethereum-hoodi-rpc.publicnode.com").expect("valid hoodi url")
});
static TEST_PROVIDER: LazyLock<Flashbots> = LazyLock::new(get_test_provider);

fn get_test_provider() -> Flashbots {
Flashbots::new(FLASHBOTS_URL.clone(), BUILDER_KEY.clone())
}
/// Pecorino endpoints
static PECORINO_RBUILDER: LazyLock<Url> = LazyLock::new(|| {
Url::parse("https://host-builder-rpc.pecorino.signet.sh").expect("valid pecorino rbuilder url")
});

#[allow(clippy::type_complexity)]
fn get_sepolia() -> FillProvider<
static PECORINO_HOST_RPC: LazyLock<Url> = LazyLock::new(|| {
Url::parse("https://host-rpc.pecorino.signet.sh").expect("valid pecorino url")
});

type HoodiProvider = FillProvider<
JoinFill<
JoinFill<
Identity,
Expand All @@ -43,54 +59,215 @@ fn get_sepolia() -> FillProvider<
WalletFiller<EthereumWallet>,
>,
alloy::providers::RootProvider,
> {
>;

#[allow(clippy::type_complexity)]
fn get_hoodi_host(builder_key: LocalOrAws) -> HoodiProvider {
ProviderBuilder::new()
.wallet(BUILDER_KEY.clone())
.connect_http(
"https://ethereum-sepolia-rpc.publicnode.com"
.parse()
.unwrap(),
)
.wallet(builder_key.clone())
.connect_http(HOODI_HOST_RPC.clone())
}

#[tokio::test]
#[ignore = "integration test"]
async fn test_send_valid_bundle_hoodi() {
setup_logging();

let key_from_env = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set");
let builder_key = LocalOrAws::load(&key_from_env, Some(560048))
.await
.expect("failed to load builder key");

let flashbots = ProviderBuilder::new()
.wallet(builder_key.clone())
.connect_http(TITANBUILDER_HOODI_RPC.clone());

let hoodi = get_hoodi_host(builder_key.clone());

let req = TransactionRequest::default()
.to(builder_key.address())
.value(U256::from(0u64))
.gas_limit(21_000)
.max_fee_per_gas((50 * GWEI_TO_WEI).into())
.max_priority_fee_per_gas((2 * GWEI_TO_WEI).into())
.from(builder_key.address());

let SendableTx::Envelope(tx) = hoodi.fill(req.clone()).await.unwrap() else {
panic!("expected filled tx");
};

let block = hoodi.get_block(BlockId::latest()).await.unwrap().unwrap();
let target_block = block.number() + 1;

let bundle = EthSendBundle {
txs: vec![tx.encoded_2718().into()],
block_number: target_block,
..Default::default()
};

let result = flashbots
.send_bundle(bundle)
.with_auth(builder_key.clone())
.await;
dbg!(result.as_ref().unwrap());
assert!(result.is_ok(), "should send bundle: {:#?}", result);
assert!(result.unwrap().is_some(), "should have bundle hash");
// assert_tx_included(&hoodi, tx.tx_hash().clone(), 120).await;
}

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

let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set");
let builder_key = LocalOrAws::load(&raw_key, Some(3151908))
.await
.expect("failed to load builder key");

let flashbots = ProviderBuilder::new()
.wallet(builder_key.clone())
.connect_http(PECORINO_RBUILDER.clone());

let pecorino = ProviderBuilder::new()
.wallet(builder_key.clone())
.connect_http(PECORINO_HOST_RPC.clone());

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 {
.to(builder_key.address())
.value(U256::from(0u64))
.gas_limit(21_000)
.max_fee_per_gas((50 * GWEI_TO_WEI).into())
.max_priority_fee_per_gas((2 * GWEI_TO_WEI).into())
.from(builder_key.address());

let SendableTx::Envelope(tx) = pecorino.fill(req.clone()).await.unwrap() else {
panic!("expected filled tx");
};
let tx_bytes = tx.encoded_2718().into();
dbg!("prepared transaction request", tx.clone());

let latest_block = sepolia
.get_block_by_number(alloy::eips::BlockNumberOrTag::Latest)
let block = pecorino
.get_block(BlockId::latest())
.await
.unwrap()
.unwrap()
.number();
.unwrap();
let target_block = block.number() + 1;
dbg!("preparing bundle for", target_block);

let bundle = EthCallBundle {
txs: vec![tx.encoded_2718().into()],
block_number: target_block,
..Default::default()
};

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);
// FAIL: This test currently fails - why?
// thread 'test_sim_bundle_pecorino' panicked at tests/flashbots.rs:610:17:
// called `Result::unwrap()` on an `Err` value: ErrorResp(ErrorPayload { code: -32601, message: "Method not found", data: None })
let result = flashbots
.call_bundle(bundle)
.with_auth(builder_key.clone())
.await;
dbg!(result.unwrap());
}

let err = flashbots
.simulate_bundle(&bundle)
#[tokio::test]
#[ignore = "integration test"]
async fn test_send_bundle_pecorino() {
setup_logging();

let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set");
let builder_key = LocalOrAws::load(&raw_key, Some(3151908))
.await
.unwrap_err()
.to_string();
// If we have hit this point, we have succesfully authed to the flashbots
// api via header
.expect("failed to load builder key");

let flashbots = ProviderBuilder::new()
.wallet(builder_key.clone())
.connect_http(PECORINO_RBUILDER.clone());

let pecorino = ProviderBuilder::new()
.wallet(builder_key.clone())
.connect_http("https://rpc.pecorino.signet.sh".parse().unwrap());

let req = TransactionRequest::default()
.to(builder_key.address())
.value(U256::from(0u64))
.gas_limit(21_000)
.max_fee_per_gas((50 * GWEI_TO_WEI).into())
.max_priority_fee_per_gas((2 * GWEI_TO_WEI).into())
.from(builder_key.address());

let SendableTx::Envelope(tx) = pecorino.fill(req.clone()).await.unwrap() else {
panic!("expected filled tx");
};
dbg!("prepared transaction request", tx.clone());

let block = pecorino
.get_block(BlockId::latest())
.await
.unwrap()
.unwrap();
let target_block = block.number() + 1;
dbg!("preparing bundle for", target_block);

let bundle = EthSendBundle {
txs: vec![tx.encoded_2718().into()],
block_number: target_block,
..Default::default()
};

let result = flashbots
.send_bundle(bundle)
.with_auth(builder_key.clone())
.await;
dbg!(result.as_ref().unwrap());
assert!(result.is_ok(), "should send bundle: {:#?}", result);
assert!(result.unwrap().is_some(), "should have bundle hash");
}

/// Asserts that a tx was included in Sepolia within `deadline` seconds.
async fn assert_tx_included(sepolia: &HoodiProvider, tx_hash: B256, deadline: u64) {
let now = Instant::now();
let deadline = now + Duration::from_secs(deadline);
let mut found = false;

loop {
let n = Instant::now();
if n >= deadline {
break;
}

match sepolia.get_transaction_by_hash(tx_hash).await {
Ok(Some(_tx)) => {
found = true;
break;
}
Ok(None) => {
// Not yet present; wait and retry
dbg!("transaction not yet seen");
tokio::time::sleep(Duration::from_secs(1)).await;
}
Err(err) => {
// Transient error querying the provider; log and retry
eprintln!("warning: error querying tx: {}", err);
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}

assert!(
err.contains("insufficient funds for gas"),
"unexpected error: {err}"
found,
"transaction was not seen by the provider within {:?} seconds",
deadline
);
}

/// Initializes logger for printing during testing
pub fn setup_logging() {
let filter = EnvFilter::from_default_env();
let fmt = fmt::layer().with_filter(filter);
let registry = registry().with(fmt);
let _ = registry.try_init();
}
Loading