From 13f00d9b267c6908a4a489e386706a70d1bfa65e Mon Sep 17 00:00:00 2001 From: dylan Date: Thu, 25 Sep 2025 12:42:21 -0600 Subject: [PATCH 1/6] adds more flashbots integration tests - adds tests for sending and confirming a bundle on sepolia and mainnet --- src/utils/flashbots.rs | 20 +--- tests/flashbots.rs | 232 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 218 insertions(+), 34 deletions(-) diff --git a/src/utils/flashbots.rs b/src/utils/flashbots.rs index 9d8cc11..3361590 100644 --- a/src/utils/flashbots.rs +++ b/src/utils/flashbots.rs @@ -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}, @@ -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. @@ -80,21 +79,7 @@ impl Flashbots { /// 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!("sim bundle response ###", resp); Ok(()) } @@ -139,7 +124,6 @@ impl Flashbots { async fn compute_signature(&self, body_bz: &[u8]) -> Result { 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) diff --git a/tests/flashbots.rs b/tests/flashbots.rs index e5bb202..82b473e 100644 --- a/tests/flashbots.rs +++ b/tests/flashbots.rs @@ -1,6 +1,7 @@ #![cfg(feature = "flashbots")] use alloy::{ + consensus::constants::GWEI_TO_WEI, eips::Encodable2718, network::EthereumWallet, primitives::{B256, U256}, @@ -12,29 +13,40 @@ use alloy::{ Identity, Provider, ProviderBuilder, SendableTx, }, rpc::types::{ - mev::{BundleItem, MevSendBundle, ProtocolVersion}, + mev::{BundleItem, Inclusion, MevSendBundle, Privacy, ProtocolVersion}, TransactionRequest, }, signers::{local::PrivateKeySigner, Signer}, }; -use init4_bin_base::utils::{flashbots::Flashbots, signer::LocalOrAws}; -use std::sync::LazyLock; +use init4_bin_base::{ + deps::tracing::debug, + deps::tracing_subscriber::{ + fmt, layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter, Layer, + }, + utils::{flashbots::Flashbots, signer::LocalOrAws}, +}; +use std::{ + env, + sync::LazyLock, + time::{Duration, Instant}, +}; use url::Url; static FLASHBOTS_URL: LazyLock = LazyLock::new(|| { - Url::parse("https://relay-sepolia.flashbots.net:443").expect("valid flashbots url") + Url::parse("https://relay-sepolia.flashbots.net").expect("valid flashbots url") }); -static BUILDER_KEY: LazyLock = LazyLock::new(|| { + +static DEFAULT_BUILDER_KEY: LazyLock = LazyLock::new(|| { LocalOrAws::Local(PrivateKeySigner::from_bytes(&B256::repeat_byte(0x02)).unwrap()) }); -static TEST_PROVIDER: LazyLock = LazyLock::new(get_test_provider); -fn get_test_provider() -> Flashbots { - Flashbots::new(FLASHBOTS_URL.clone(), BUILDER_KEY.clone()) +static TEST_PROVIDER: LazyLock = LazyLock::new(get_default_test_provider); + +fn get_default_test_provider() -> Flashbots { + Flashbots::new(FLASHBOTS_URL.clone(), DEFAULT_BUILDER_KEY.clone()) } -#[allow(clippy::type_complexity)] -fn get_sepolia() -> FillProvider< +type SepoliaProvider = FillProvider< JoinFill< JoinFill< Identity, @@ -43,9 +55,12 @@ fn get_sepolia() -> FillProvider< WalletFiller, >, alloy::providers::RootProvider, -> { +>; + +#[allow(clippy::type_complexity)] +fn get_sepolia(builder_key: LocalOrAws) -> SepoliaProvider { ProviderBuilder::new() - .wallet(BUILDER_KEY.clone()) + .wallet(builder_key.clone()) .connect_http( "https://ethereum-sepolia-rpc.publicnode.com" .parse() @@ -57,13 +72,13 @@ fn get_sepolia() -> FillProvider< #[ignore = "integration test"] async fn test_simulate_valid_bundle_sepolia() { let flashbots = &*TEST_PROVIDER; - let sepolia = get_sepolia(); + let sepolia = get_sepolia(DEFAULT_BUILDER_KEY.clone()); let req = TransactionRequest::default() - .to(BUILDER_KEY.address()) + .to(DEFAULT_BUILDER_KEY.address()) .value(U256::from(1u64)) .gas_limit(51_000) - .from(BUILDER_KEY.address()); + .from(DEFAULT_BUILDER_KEY.address()); let SendableTx::Envelope(tx) = sepolia.fill(req).await.unwrap() else { panic!("expected filled tx"); }; @@ -78,7 +93,7 @@ async fn test_simulate_valid_bundle_sepolia() { let bundle_body = vec![BundleItem::Tx { tx: tx_bytes, - can_revert: true, + can_revert: false, }]; let bundle = MevSendBundle::new(latest_block, Some(0), ProtocolVersion::V0_1, bundle_body); @@ -94,3 +109,188 @@ async fn test_simulate_valid_bundle_sepolia() { "unexpected error: {err}" ); } + +#[tokio::test] +#[ignore = "integration test"] +async fn test_send_valid_bundle_sepolia() { + setup_logging(); + + let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); + let builder_key = LocalOrAws::load(&raw_key, Some(11155111)) + .await + .expect("failed to load builder key"); + + let flashbots = Flashbots::new(FLASHBOTS_URL.clone(), builder_key.clone()); + let sepolia = get_sepolia(builder_key.clone()); + + let req = TransactionRequest::default() + .to(builder_key.address()) + .value(U256::from(1u64)) + .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()); + + sepolia.estimate_gas(req.clone()).await.unwrap(); + + let SendableTx::Envelope(tx) = sepolia.fill(req.clone()).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(); + // Give ourselves a buffer: target a couple blocks out to avoid timing edges + let target_block = latest_block + 1; + + // Assemble the bundle and target it to the latest block + let bundle_body = vec![BundleItem::Tx { + tx: tx_bytes, + can_revert: false, + }]; + let mut bundle = MevSendBundle::new( + target_block, + Some(target_block + 5), + ProtocolVersion::V0_1, + bundle_body, + ); + bundle.inclusion = Inclusion::at_block(target_block); + // bundle.privacy = Some(Privacy::default().with_builders(Some(vec![ + // "flashbots".to_string(), + // "rsync".to_string(), + // "Titan".to_string(), + // "beaverbuild.org".to_string(), + // ]))); + + dbg!(latest_block); + dbg!(&bundle.inclusion.block_number(), &bundle.inclusion.max_block_number()); + + flashbots.simulate_bundle(&bundle).await.unwrap(); + + let bundle_resp = flashbots.send_bundle(&bundle).await.unwrap(); + assert!(bundle_resp.bundle_hash != B256::ZERO); + dbg!(bundle_resp); + + assert_tx_included(&sepolia, tx.hash().clone(), 15).await; +} + +#[tokio::test] +#[ignore = "integration test"] +async fn test_send_valid_bundle_mainnet() { + setup_logging(); + + let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); + + let builder_key = LocalOrAws::load(&raw_key, None) + .await + .expect("failed to load builder key"); + debug!(builder_key_address = ?builder_key.address(), "loaded builder key"); + + let flashbots = Flashbots::new( + Url::parse("https://relay.flashbots.net").unwrap(), + builder_key.clone(), + ); + debug!(?flashbots.relay_url, "created flashbots provider"); + + let mainnet = ProviderBuilder::new() + .wallet(builder_key.clone()) + .connect_http("https://cloudflare-eth.com".parse().unwrap()); + + // Build a valid transaction to bundle + let req = TransactionRequest::default() + .to(builder_key.address()) + .value(U256::from(1u64)) + .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()); + dbg!(req.clone()); + + // Estimate gas will fail if this wallet isn't properly funded for this TX. + let gas_estimates = mainnet.estimate_gas(req.clone()).await.unwrap(); + dbg!(gas_estimates); + + let SendableTx::Envelope(tx) = mainnet.fill(req.clone()).await.unwrap() else { + panic!("expected filled tx"); + }; + dbg!(req.clone()); + + let tx_bytes = tx.encoded_2718().into(); + dbg!(tx.hash()); + + // Fetch latest block info to build a valid target block for the bundle + let latest_block = mainnet + .get_block_by_number(alloy::eips::BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap() + .number(); + let target_block = latest_block + 1; + + // Assemble the bundle and target it to the latest block + let bundle_body = vec![BundleItem::Tx { + tx: tx_bytes, + can_revert: false, + }]; + let mut bundle = MevSendBundle::new(target_block, None, ProtocolVersion::V0_1, bundle_body); + bundle.inclusion = Inclusion::at_block(target_block); + bundle.privacy = Some(Privacy::default().with_builders(Some(vec!["flashbots".to_string()]))); + + let resp = flashbots + .send_bundle(&bundle) + .await + .expect("should send bundle"); + dbg!(&resp); + + assert!(resp.bundle_hash != B256::ZERO); +} + +/// Asserts that a tx was included in Sepolia within `deadline` seconds. +async fn assert_tx_included(sepolia: &SepoliaProvider, 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!( + found, + "transaction was not seen by the provider within {:?} seconds", + deadline + ); +} + +/// Initializes logger for printing during testing +pub fn setup_logging() { + // Initialize logging + let filter = EnvFilter::from_default_env(); + let fmt = fmt::layer().with_filter(filter); + let registry = registry().with(fmt); + let _ = registry.try_init(); +} From 1beaeb0158dd628fb61edf3a41c68a0faf028304 Mon Sep 17 00:00:00 2001 From: dylan Date: Thu, 25 Sep 2025 16:48:59 -0600 Subject: [PATCH 2/6] wip: flashbots still not landing txs or bundles :( --- Cargo.toml | 2 +- src/utils/flashbots.rs | 6 +- tests/flashbots.rs | 256 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 255 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f5f6ded..f7ef79c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/utils/flashbots.rs b/src/utils/flashbots.rs index 3361590..3226097 100644 --- a/src/utils/flashbots.rs +++ b/src/utils/flashbots.rs @@ -73,13 +73,15 @@ impl Flashbots { /// Sends a bundle via `mev_sendBundle`. pub async fn send_bundle(&self, bundle: &MevSendBundle) -> eyre::Result { - 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!("sim bundle response ###", resp); + dbg!("send bundle response ###", resp); Ok(()) } diff --git a/tests/flashbots.rs b/tests/flashbots.rs index 82b473e..6b8f775 100644 --- a/tests/flashbots.rs +++ b/tests/flashbots.rs @@ -2,10 +2,11 @@ use alloy::{ consensus::constants::GWEI_TO_WEI, - eips::Encodable2718, + eips::{BlockId, Encodable2718}, network::EthereumWallet, primitives::{B256, U256}, providers::{ + ext::MevApi, fillers::{ BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller, @@ -13,11 +14,15 @@ use alloy::{ Identity, Provider, ProviderBuilder, SendableTx, }, rpc::types::{ - mev::{BundleItem, Inclusion, MevSendBundle, Privacy, ProtocolVersion}, + mev::{ + BundleItem, EthCallBundle, EthSendBundle, EthSendPrivateTransaction, Inclusion, + MevSendBundle, Privacy, PrivateTransactionPreferences, ProtocolVersion, + }, TransactionRequest, }, signers::{local::PrivateKeySigner, Signer}, }; +use eyre::Context; use init4_bin_base::{ deps::tracing::debug, deps::tracing_subscriber::{ @@ -58,7 +63,7 @@ type SepoliaProvider = FillProvider< >; #[allow(clippy::type_complexity)] -fn get_sepolia(builder_key: LocalOrAws) -> SepoliaProvider { +fn get_sepolia_host(builder_key: LocalOrAws) -> SepoliaProvider { ProviderBuilder::new() .wallet(builder_key.clone()) .connect_http( @@ -72,7 +77,7 @@ fn get_sepolia(builder_key: LocalOrAws) -> SepoliaProvider { #[ignore = "integration test"] async fn test_simulate_valid_bundle_sepolia() { let flashbots = &*TEST_PROVIDER; - let sepolia = get_sepolia(DEFAULT_BUILDER_KEY.clone()); + let sepolia = get_sepolia_host(DEFAULT_BUILDER_KEY.clone()); let req = TransactionRequest::default() .to(DEFAULT_BUILDER_KEY.address()) @@ -121,7 +126,7 @@ async fn test_send_valid_bundle_sepolia() { .expect("failed to load builder key"); let flashbots = Flashbots::new(FLASHBOTS_URL.clone(), builder_key.clone()); - let sepolia = get_sepolia(builder_key.clone()); + let sepolia = get_sepolia_host(builder_key.clone()); let req = TransactionRequest::default() .to(builder_key.address()) @@ -159,6 +164,7 @@ async fn test_send_valid_bundle_sepolia() { bundle_body, ); bundle.inclusion = Inclusion::at_block(target_block); + // bundle.privacy = Some(Privacy::default().with_builders(Some(vec![ // "flashbots".to_string(), // "rsync".to_string(), @@ -167,7 +173,10 @@ async fn test_send_valid_bundle_sepolia() { // ]))); dbg!(latest_block); - dbg!(&bundle.inclusion.block_number(), &bundle.inclusion.max_block_number()); + dbg!( + &bundle.inclusion.block_number(), + &bundle.inclusion.max_block_number() + ); flashbots.simulate_bundle(&bundle).await.unwrap(); @@ -294,3 +303,238 @@ pub fn setup_logging() { let registry = registry().with(fmt); let _ = registry.try_init(); } + +#[tokio::test] +#[ignore = "integration test"] +async fn test_alloy_flashbots_sepolia() { + setup_logging(); + + let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); + let builder_key = LocalOrAws::load(&raw_key, Some(11155111)) + .await + .expect("failed to load builder key"); + + let flashbots = ProviderBuilder::new() + .wallet(builder_key.clone()) + .connect_http("https://relay-sepolia.flashbots.net".parse().unwrap()); + + let sepolia_host = get_sepolia_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 block = sepolia_host + .get_block(BlockId::latest()) + .await + .unwrap() + .unwrap(); + let target_block = block.number() + 1; + dbg!("preparing bundle for", target_block); + + let SendableTx::Envelope(tx) = sepolia_host.fill(req.clone()).await.unwrap() else { + panic!("expected filled tx"); + }; + dbg!("prepared transaction request", tx.clone()); + let tx_bytes = tx.encoded_2718(); + + let bundle = EthSendBundle { + txs: vec![tx_bytes.clone().into()], + block_number: target_block, + min_timestamp: None, + max_timestamp: None, + reverting_tx_hashes: vec![], + replacement_uuid: None, + dropping_tx_hashes: vec![], + refund_percent: None, + refund_recipient: None, + refund_tx_hashes: vec![], + ..Default::default() + }; + + let call_bundle = EthCallBundle { + txs: vec![tx_bytes.clone().into()], + block_number: target_block, + ..Default::default() + }; + let sim = flashbots + .call_bundle(call_bundle) + .with_auth(builder_key.clone()); + dbg!(sim.await.unwrap()); + + let result = flashbots.send_bundle(bundle).with_auth(builder_key.clone()); + dbg!(result.await.unwrap()); +} + +#[tokio::test] +#[ignore = "integration test"] +async fn test_mev_endpoints() { + setup_logging(); + + let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); + let builder_key = LocalOrAws::load(&raw_key, Some(11155111)) + .await + .expect("failed to load builder key"); + + let flashbots = ProviderBuilder::new() + .wallet(builder_key.clone()) + .connect_http("https://relay-sepolia.flashbots.net".parse().unwrap()); + + let old_flashbots = Flashbots::new( + "https://relay-sepolia.flashbots.net".parse().unwrap(), + builder_key.clone(), + ); + + let sepolia_host = get_sepolia_host(builder_key.clone()); + + let block = sepolia_host + .get_block(BlockId::latest()) + .await + .unwrap() + .unwrap(); + let target_block = block.number() + 1; + dbg!("preparing bundle for", target_block); + + 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) = sepolia_host.fill(req.clone()).await.unwrap() else { + panic!("expected filled tx"); + }; + dbg!("prepared transaction request", tx.clone()); + let tx_bytes = tx.encoded_2718(); + + let bundle = MevSendBundle::new( + target_block, + None, + ProtocolVersion::V0_1, + vec![BundleItem::Tx { + tx: tx_bytes.clone().into(), + can_revert: false, + }], + ); + dbg!("bundle contents", &bundle); + + let _ = old_flashbots.simulate_bundle(&bundle).await.unwrap(); + + let result = flashbots + .send_mev_bundle(bundle) + .with_auth(builder_key.clone()); + dbg!("send mev bundle:", result.await.unwrap()); + + let result = flashbots + .send_private_transaction(EthSendPrivateTransaction { + tx: tx_bytes.into(), + max_block_number: Some(target_block + 5), + preferences: PrivateTransactionPreferences::default(), + }) + .with_auth(builder_key.clone()); + dbg!("send private transaction", result.await.unwrap()); +} + +#[tokio::test] +#[ignore = "integration test"] +async fn test_alloy_flashbots_mainnet() { + setup_logging(); + + let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); + let builder_key = LocalOrAws::load(&raw_key, Some(11155111)) + .await + .expect("failed to load builder key"); + + let flashbots = ProviderBuilder::new() + .wallet(builder_key.clone()) + .connect_http("https://relay-sepolia.flashbots.net".parse().unwrap()); + + let sepolia_host = get_sepolia_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 block = sepolia_host + .get_block(BlockId::latest()) + .await + .unwrap() + .unwrap(); + let target_block = block.number() + 1; + dbg!("preparing bundle for", target_block); + + let target_block = block.number() + 1; + dbg!("preparing bundle for", target_block); + + let SendableTx::Envelope(tx) = sepolia_host.fill(req.clone()).await.unwrap() else { + panic!("expected filled tx"); + }; + dbg!("prepared transaction request", tx.clone()); + let tx_bytes = tx.encoded_2718(); + + let bundle = EthSendBundle { + txs: vec![tx_bytes.clone().into()], + block_number: target_block, + ..Default::default() + }; + + let call_bundle = EthCallBundle { + txs: vec![tx_bytes.clone().into()], + block_number: target_block, + ..Default::default() + }; + + let sim = flashbots + .call_bundle(call_bundle) + .with_auth(builder_key.clone()); + dbg!(sim.await.unwrap()); + + let result = flashbots.send_bundle(bundle).with_auth(builder_key.clone()); + dbg!(result.await.unwrap()); +} + +#[tokio::test] +#[ignore = "integration test"] +pub async fn test_send_single_tx_sepolia() { + setup_logging(); + + let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); + let builder_key = LocalOrAws::load(&raw_key, Some(11155111)) + .await + .expect("failed to load builder key"); + + let sepolia_host = get_sepolia_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) = sepolia_host.fill(req.clone()).await.unwrap() else { + panic!("expected filled tx"); + }; + dbg!("prepared transaction request", tx.clone()); + let tx_bytes = tx.encoded_2718(); + + let pending_tx = sepolia_host + .send_raw_transaction(&tx_bytes) + .await + .expect("should send tx") + .watch() + .await + .unwrap(); + dbg!(pending_tx); +} From 238aa6fcc414f990aac4b57a64118a41aed85e88 Mon Sep 17 00:00:00 2001 From: dylan Date: Fri, 26 Sep 2025 10:29:37 -0600 Subject: [PATCH 3/6] some tidying while continuing testing --- tests/flashbots.rs | 131 +++++++++++++++++++-------------------------- 1 file changed, 56 insertions(+), 75 deletions(-) diff --git a/tests/flashbots.rs b/tests/flashbots.rs index 6b8f775..3636909 100644 --- a/tests/flashbots.rs +++ b/tests/flashbots.rs @@ -22,7 +22,6 @@ use alloy::{ }, signers::{local::PrivateKeySigner, Signer}, }; -use eyre::Context; use init4_bin_base::{ deps::tracing::debug, deps::tracing_subscriber::{ @@ -258,52 +257,6 @@ async fn test_send_valid_bundle_mainnet() { assert!(resp.bundle_hash != B256::ZERO); } -/// Asserts that a tx was included in Sepolia within `deadline` seconds. -async fn assert_tx_included(sepolia: &SepoliaProvider, 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!( - found, - "transaction was not seen by the provider within {:?} seconds", - deadline - ); -} - -/// Initializes logger for printing during testing -pub fn setup_logging() { - // Initialize logging - let filter = EnvFilter::from_default_env(); - let fmt = fmt::layer().with_filter(filter); - let registry = registry().with(fmt); - let _ = registry.try_init(); -} - #[tokio::test] #[ignore = "integration test"] async fn test_alloy_flashbots_sepolia() { @@ -325,7 +278,7 @@ async fn test_alloy_flashbots_sepolia() { .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()) + .max_priority_fee_per_gas((20 * GWEI_TO_WEI).into()) .from(builder_key.address()); let block = sepolia_host @@ -345,14 +298,6 @@ async fn test_alloy_flashbots_sepolia() { let bundle = EthSendBundle { txs: vec![tx_bytes.clone().into()], block_number: target_block, - min_timestamp: None, - max_timestamp: None, - reverting_tx_hashes: vec![], - replacement_uuid: None, - dropping_tx_hashes: vec![], - refund_percent: None, - refund_recipient: None, - refund_tx_hashes: vec![], ..Default::default() }; @@ -372,7 +317,7 @@ async fn test_alloy_flashbots_sepolia() { #[tokio::test] #[ignore = "integration test"] -async fn test_mev_endpoints() { +async fn test_mev_endpoints_sepolia() { setup_logging(); let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); @@ -384,6 +329,7 @@ async fn test_mev_endpoints() { .wallet(builder_key.clone()) .connect_http("https://relay-sepolia.flashbots.net".parse().unwrap()); + // TEMP: Keeping this around because alloy flashbots doesn't have a simulate endpoint for `mev_simBundle`. let old_flashbots = Flashbots::new( "https://relay-sepolia.flashbots.net".parse().unwrap(), builder_key.clone(), @@ -404,7 +350,7 @@ async fn test_mev_endpoints() { .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()) + .max_priority_fee_per_gas((20 * GWEI_TO_WEI).into()) .from(builder_key.address()); let SendableTx::Envelope(tx) = sepolia_host.fill(req.clone()).await.unwrap() else { @@ -430,15 +376,6 @@ async fn test_mev_endpoints() { .send_mev_bundle(bundle) .with_auth(builder_key.clone()); dbg!("send mev bundle:", result.await.unwrap()); - - let result = flashbots - .send_private_transaction(EthSendPrivateTransaction { - tx: tx_bytes.into(), - max_block_number: Some(target_block + 5), - preferences: PrivateTransactionPreferences::default(), - }) - .with_auth(builder_key.clone()); - dbg!("send private transaction", result.await.unwrap()); } #[tokio::test] @@ -453,9 +390,11 @@ async fn test_alloy_flashbots_mainnet() { let flashbots = ProviderBuilder::new() .wallet(builder_key.clone()) - .connect_http("https://relay-sepolia.flashbots.net".parse().unwrap()); + .connect_http("https://relay.flashbots.net".parse().unwrap()); - let sepolia_host = get_sepolia_host(builder_key.clone()); + let mainnet = ProviderBuilder::new() + .wallet(builder_key.clone()) + .connect_http("https://ethereum-rpc.publicnode.com".parse().unwrap()); let req = TransactionRequest::default() .to(builder_key.address()) @@ -465,18 +404,14 @@ async fn test_alloy_flashbots_mainnet() { .max_priority_fee_per_gas((2 * GWEI_TO_WEI).into()) .from(builder_key.address()); - let block = sepolia_host - .get_block(BlockId::latest()) - .await - .unwrap() - .unwrap(); + let block = mainnet.get_block(BlockId::latest()).await.unwrap().unwrap(); let target_block = block.number() + 1; dbg!("preparing bundle for", target_block); let target_block = block.number() + 1; dbg!("preparing bundle for", target_block); - let SendableTx::Envelope(tx) = sepolia_host.fill(req.clone()).await.unwrap() else { + let SendableTx::Envelope(tx) = mainnet.fill(req.clone()).await.unwrap() else { panic!("expected filled tx"); }; dbg!("prepared transaction request", tx.clone()); @@ -538,3 +473,49 @@ pub async fn test_send_single_tx_sepolia() { .unwrap(); dbg!(pending_tx); } + +/// Asserts that a tx was included in Sepolia within `deadline` seconds. +async fn assert_tx_included(sepolia: &SepoliaProvider, 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!( + found, + "transaction was not seen by the provider within {:?} seconds", + deadline + ); +} + +/// Initializes logger for printing during testing +pub fn setup_logging() { + // Initialize logging + let filter = EnvFilter::from_default_env(); + let fmt = fmt::layer().with_filter(filter); + let registry = registry().with(fmt); + let _ = registry.try_init(); +} From 7b45a3423066ee655b222bc5dea2cbe3d0f78d1f Mon Sep 17 00:00:00 2001 From: dylan Date: Fri, 26 Sep 2025 10:29:58 -0600 Subject: [PATCH 4/6] more tidy --- tests/flashbots.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/flashbots.rs b/tests/flashbots.rs index 3636909..505da68 100644 --- a/tests/flashbots.rs +++ b/tests/flashbots.rs @@ -15,8 +15,8 @@ use alloy::{ }, rpc::types::{ mev::{ - BundleItem, EthCallBundle, EthSendBundle, EthSendPrivateTransaction, Inclusion, - MevSendBundle, Privacy, PrivateTransactionPreferences, ProtocolVersion, + BundleItem, EthCallBundle, EthSendBundle, Inclusion, MevSendBundle, Privacy, + ProtocolVersion, }, TransactionRequest, }, From 19267686a7495409850edfd6593dde41c165eb28 Mon Sep 17 00:00:00 2001 From: dylan Date: Tue, 30 Sep 2025 14:00:05 -0600 Subject: [PATCH 5/6] adds hoodi tests scaffolding --- tests/flashbots.rs | 102 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 96 insertions(+), 6 deletions(-) diff --git a/tests/flashbots.rs b/tests/flashbots.rs index 505da68..821dac4 100644 --- a/tests/flashbots.rs +++ b/tests/flashbots.rs @@ -406,15 +406,10 @@ async fn test_alloy_flashbots_mainnet() { let block = mainnet.get_block(BlockId::latest()).await.unwrap().unwrap(); let target_block = block.number() + 1; - dbg!("preparing bundle for", target_block); - - let target_block = block.number() + 1; - dbg!("preparing bundle for", target_block); - let SendableTx::Envelope(tx) = mainnet.fill(req.clone()).await.unwrap() else { panic!("expected filled tx"); }; - dbg!("prepared transaction request", tx.clone()); + dbg!("prepared transaction request", tx.clone(), target_block); let tx_bytes = tx.encoded_2718(); let bundle = EthSendBundle { @@ -474,6 +469,101 @@ pub async fn test_send_single_tx_sepolia() { dbg!(pending_tx); } +#[tokio::test] +#[ignore = "integration test"] +pub async fn test_send_single_tx_hoodi() { + setup_logging(); + + let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); + let builder_key = LocalOrAws::load(&raw_key, Some(560048)) + .await + .expect("failed to load builder key"); + + 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 hoodi = ProviderBuilder::new() + .wallet(builder_key.clone()) + .connect_http("https://ethereum-hoodi-rpc.publicnode.com".parse().unwrap()); + + let SendableTx::Envelope(tx) = hoodi.fill(req.clone()).await.unwrap() else { + panic!("expected filled tx"); + }; + dbg!("prepared transaction request", tx.clone()); + let tx_bytes = tx.encoded_2718(); + + let pending_tx = hoodi + .send_raw_transaction(&tx_bytes) + .await + .expect("should send tx") + .watch() + .await + .unwrap(); + dbg!(pending_tx); +} + +#[tokio::test] +#[ignore = "integration test"] +async fn test_send_valid_bundle_hoodi() { + setup_logging(); + + let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); + let builder_key = LocalOrAws::load(&raw_key, Some(560048)) + .await + .expect("failed to load builder key"); + + let flashbots = ProviderBuilder::new() + .wallet(builder_key.clone()) + .connect_http("https://boost-relay-hoodi.flashbots.net".parse().unwrap()); + + let hoodi = ProviderBuilder::new() + .wallet(builder_key.clone()) + .connect_http("https://ethereum-hoodi-rpc.publicnode.com".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) = hoodi.fill(req.clone()).await.unwrap() else { + panic!("expected filled tx"); + }; + dbg!("prepared transaction request", tx.clone()); + let tx_bytes = tx.encoded_2718(); + + let block = hoodi.get_block(BlockId::latest()).await.unwrap().unwrap(); + let target_block = block.number() + 1; + dbg!("preparing bundle for", target_block); + + // let call_bundle = EthCallBundle { + // txs: vec![tx_bytes.clone().into()], + // block_number: target_block, + // ..Default::default() + // }; + + // let sim = flashbots + // .call_bundle(call_bundle) + // .with_auth(builder_key.clone()); + // dbg!(sim.await.unwrap()); + + let bundle = EthSendBundle { + txs: vec![tx_bytes.clone().into()], + block_number: target_block, + ..Default::default() + }; + + let result = flashbots.send_bundle(bundle).with_auth(builder_key.clone()); + dbg!(result.await.unwrap()); +} + /// Asserts that a tx was included in Sepolia within `deadline` seconds. async fn assert_tx_included(sepolia: &SepoliaProvider, tx_hash: B256, deadline: u64) { let now = Instant::now(); From 8adaeabd4e091c69d57c4fd024b580a08e641ea8 Mon Sep 17 00:00:00 2001 From: dylan Date: Wed, 1 Oct 2025 17:50:45 -0600 Subject: [PATCH 6/6] update and modify for hoodi --- tests/flashbots.rs | 506 ++++++++------------------------------------- 1 file changed, 84 insertions(+), 422 deletions(-) diff --git a/tests/flashbots.rs b/tests/flashbots.rs index 821dac4..e25c93b 100644 --- a/tests/flashbots.rs +++ b/tests/flashbots.rs @@ -14,20 +14,16 @@ use alloy::{ Identity, Provider, ProviderBuilder, SendableTx, }, rpc::types::{ - mev::{ - BundleItem, EthCallBundle, EthSendBundle, Inclusion, MevSendBundle, Privacy, - ProtocolVersion, - }, + mev::{EthCallBundle, EthSendBundle}, TransactionRequest, }, - signers::{local::PrivateKeySigner, Signer}, + signers::Signer, }; use init4_bin_base::{ - deps::tracing::debug, deps::tracing_subscriber::{ fmt, layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter, Layer, }, - utils::{flashbots::Flashbots, signer::LocalOrAws}, + utils::signer::LocalOrAws, }; use std::{ env, @@ -36,21 +32,25 @@ use std::{ }; use url::Url; -static FLASHBOTS_URL: LazyLock = LazyLock::new(|| { - Url::parse("https://relay-sepolia.flashbots.net").expect("valid flashbots url") +/// Hoodi endpoints +static TITANBUILDER_HOODI_RPC: LazyLock = LazyLock::new(|| { + Url::parse("https://rpc-hoodi.titanbuilder.xyz/").expect("valid flashbots url") }); -static DEFAULT_BUILDER_KEY: LazyLock = LazyLock::new(|| { - LocalOrAws::Local(PrivateKeySigner::from_bytes(&B256::repeat_byte(0x02)).unwrap()) +static HOODI_HOST_RPC: LazyLock = LazyLock::new(|| { + Url::parse("https://ethereum-hoodi-rpc.publicnode.com").expect("valid hoodi url") }); -static TEST_PROVIDER: LazyLock = LazyLock::new(get_default_test_provider); +/// Pecorino endpoints +static PECORINO_RBUILDER: LazyLock = LazyLock::new(|| { + Url::parse("https://host-builder-rpc.pecorino.signet.sh").expect("valid pecorino rbuilder url") +}); -fn get_default_test_provider() -> Flashbots { - Flashbots::new(FLASHBOTS_URL.clone(), DEFAULT_BUILDER_KEY.clone()) -} +static PECORINO_HOST_RPC: LazyLock = LazyLock::new(|| { + Url::parse("https://host-rpc.pecorino.signet.sh").expect("valid pecorino url") +}); -type SepoliaProvider = FillProvider< +type HoodiProvider = FillProvider< JoinFill< JoinFill< Identity, @@ -62,339 +62,79 @@ type SepoliaProvider = FillProvider< >; #[allow(clippy::type_complexity)] -fn get_sepolia_host(builder_key: LocalOrAws) -> SepoliaProvider { +fn get_hoodi_host(builder_key: LocalOrAws) -> HoodiProvider { 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_host(DEFAULT_BUILDER_KEY.clone()); - - let req = TransactionRequest::default() - .to(DEFAULT_BUILDER_KEY.address()) - .value(U256::from(1u64)) - .gas_limit(51_000) - .from(DEFAULT_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: false, - }]; - 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}" - ); + .connect_http(HOODI_HOST_RPC.clone()) } - -#[tokio::test] -#[ignore = "integration test"] -async fn test_send_valid_bundle_sepolia() { - setup_logging(); - - let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); - let builder_key = LocalOrAws::load(&raw_key, Some(11155111)) - .await - .expect("failed to load builder key"); - - let flashbots = Flashbots::new(FLASHBOTS_URL.clone(), builder_key.clone()); - let sepolia = get_sepolia_host(builder_key.clone()); - - let req = TransactionRequest::default() - .to(builder_key.address()) - .value(U256::from(1u64)) - .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()); - - sepolia.estimate_gas(req.clone()).await.unwrap(); - - let SendableTx::Envelope(tx) = sepolia.fill(req.clone()).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(); - // Give ourselves a buffer: target a couple blocks out to avoid timing edges - let target_block = latest_block + 1; - - // Assemble the bundle and target it to the latest block - let bundle_body = vec![BundleItem::Tx { - tx: tx_bytes, - can_revert: false, - }]; - let mut bundle = MevSendBundle::new( - target_block, - Some(target_block + 5), - ProtocolVersion::V0_1, - bundle_body, - ); - bundle.inclusion = Inclusion::at_block(target_block); - - // bundle.privacy = Some(Privacy::default().with_builders(Some(vec![ - // "flashbots".to_string(), - // "rsync".to_string(), - // "Titan".to_string(), - // "beaverbuild.org".to_string(), - // ]))); - - dbg!(latest_block); - dbg!( - &bundle.inclusion.block_number(), - &bundle.inclusion.max_block_number() - ); - - flashbots.simulate_bundle(&bundle).await.unwrap(); - - let bundle_resp = flashbots.send_bundle(&bundle).await.unwrap(); - assert!(bundle_resp.bundle_hash != B256::ZERO); - dbg!(bundle_resp); - - assert_tx_included(&sepolia, tx.hash().clone(), 15).await; -} - -#[tokio::test] -#[ignore = "integration test"] -async fn test_send_valid_bundle_mainnet() { - setup_logging(); - - let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); - - let builder_key = LocalOrAws::load(&raw_key, None) - .await - .expect("failed to load builder key"); - debug!(builder_key_address = ?builder_key.address(), "loaded builder key"); - - let flashbots = Flashbots::new( - Url::parse("https://relay.flashbots.net").unwrap(), - builder_key.clone(), - ); - debug!(?flashbots.relay_url, "created flashbots provider"); - - let mainnet = ProviderBuilder::new() - .wallet(builder_key.clone()) - .connect_http("https://cloudflare-eth.com".parse().unwrap()); - - // Build a valid transaction to bundle - let req = TransactionRequest::default() - .to(builder_key.address()) - .value(U256::from(1u64)) - .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()); - dbg!(req.clone()); - - // Estimate gas will fail if this wallet isn't properly funded for this TX. - let gas_estimates = mainnet.estimate_gas(req.clone()).await.unwrap(); - dbg!(gas_estimates); - - let SendableTx::Envelope(tx) = mainnet.fill(req.clone()).await.unwrap() else { - panic!("expected filled tx"); - }; - dbg!(req.clone()); - - let tx_bytes = tx.encoded_2718().into(); - dbg!(tx.hash()); - - // Fetch latest block info to build a valid target block for the bundle - let latest_block = mainnet - .get_block_by_number(alloy::eips::BlockNumberOrTag::Latest) - .await - .unwrap() - .unwrap() - .number(); - let target_block = latest_block + 1; - - // Assemble the bundle and target it to the latest block - let bundle_body = vec![BundleItem::Tx { - tx: tx_bytes, - can_revert: false, - }]; - let mut bundle = MevSendBundle::new(target_block, None, ProtocolVersion::V0_1, bundle_body); - bundle.inclusion = Inclusion::at_block(target_block); - bundle.privacy = Some(Privacy::default().with_builders(Some(vec!["flashbots".to_string()]))); - - let resp = flashbots - .send_bundle(&bundle) - .await - .expect("should send bundle"); - dbg!(&resp); - - assert!(resp.bundle_hash != B256::ZERO); -} - + #[tokio::test] #[ignore = "integration test"] -async fn test_alloy_flashbots_sepolia() { +async fn test_send_valid_bundle_hoodi() { setup_logging(); - let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); - let builder_key = LocalOrAws::load(&raw_key, Some(11155111)) + 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("https://relay-sepolia.flashbots.net".parse().unwrap()); + .connect_http(TITANBUILDER_HOODI_RPC.clone()); - let sepolia_host = get_sepolia_host(builder_key.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((20 * GWEI_TO_WEI).into()) + .max_priority_fee_per_gas((2 * GWEI_TO_WEI).into()) .from(builder_key.address()); - let block = sepolia_host - .get_block(BlockId::latest()) - .await - .unwrap() - .unwrap(); - let target_block = block.number() + 1; - dbg!("preparing bundle for", target_block); - - let SendableTx::Envelope(tx) = sepolia_host.fill(req.clone()).await.unwrap() else { + let SendableTx::Envelope(tx) = hoodi.fill(req.clone()).await.unwrap() else { panic!("expected filled tx"); }; - dbg!("prepared transaction request", tx.clone()); - let tx_bytes = tx.encoded_2718(); - let bundle = EthSendBundle { - txs: vec![tx_bytes.clone().into()], - block_number: target_block, - ..Default::default() - }; + let block = hoodi.get_block(BlockId::latest()).await.unwrap().unwrap(); + let target_block = block.number() + 1; - let call_bundle = EthCallBundle { - txs: vec![tx_bytes.clone().into()], + let bundle = EthSendBundle { + txs: vec![tx.encoded_2718().into()], block_number: target_block, ..Default::default() }; - let sim = flashbots - .call_bundle(call_bundle) - .with_auth(builder_key.clone()); - dbg!(sim.await.unwrap()); - - let result = flashbots.send_bundle(bundle).with_auth(builder_key.clone()); - dbg!(result.await.unwrap()); -} - -#[tokio::test] -#[ignore = "integration test"] -async fn test_mev_endpoints_sepolia() { - setup_logging(); - - let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); - let builder_key = LocalOrAws::load(&raw_key, Some(11155111)) - .await - .expect("failed to load builder key"); - - let flashbots = ProviderBuilder::new() - .wallet(builder_key.clone()) - .connect_http("https://relay-sepolia.flashbots.net".parse().unwrap()); - - // TEMP: Keeping this around because alloy flashbots doesn't have a simulate endpoint for `mev_simBundle`. - let old_flashbots = Flashbots::new( - "https://relay-sepolia.flashbots.net".parse().unwrap(), - builder_key.clone(), - ); - - let sepolia_host = get_sepolia_host(builder_key.clone()); - - let block = sepolia_host - .get_block(BlockId::latest()) - .await - .unwrap() - .unwrap(); - let target_block = block.number() + 1; - dbg!("preparing bundle for", target_block); - - 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((20 * GWEI_TO_WEI).into()) - .from(builder_key.address()); - - let SendableTx::Envelope(tx) = sepolia_host.fill(req.clone()).await.unwrap() else { - panic!("expected filled tx"); - }; - dbg!("prepared transaction request", tx.clone()); - let tx_bytes = tx.encoded_2718(); - - let bundle = MevSendBundle::new( - target_block, - None, - ProtocolVersion::V0_1, - vec![BundleItem::Tx { - tx: tx_bytes.clone().into(), - can_revert: false, - }], - ); - dbg!("bundle contents", &bundle); - - let _ = old_flashbots.simulate_bundle(&bundle).await.unwrap(); let result = flashbots - .send_mev_bundle(bundle) - .with_auth(builder_key.clone()); - dbg!("send mev bundle:", result.await.unwrap()); + .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_alloy_flashbots_mainnet() { +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(11155111)) + 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("https://relay.flashbots.net".parse().unwrap()); + .connect_http(PECORINO_RBUILDER.clone()); - let mainnet = ProviderBuilder::new() + let pecorino = ProviderBuilder::new() .wallet(builder_key.clone()) - .connect_http("https://ethereum-rpc.publicnode.com".parse().unwrap()); + .connect_http(PECORINO_HOST_RPC.clone()); let req = TransactionRequest::default() .to(builder_key.address()) @@ -404,126 +144,52 @@ async fn test_alloy_flashbots_mainnet() { .max_priority_fee_per_gas((2 * GWEI_TO_WEI).into()) .from(builder_key.address()); - let block = mainnet.get_block(BlockId::latest()).await.unwrap().unwrap(); - let target_block = block.number() + 1; - let SendableTx::Envelope(tx) = mainnet.fill(req.clone()).await.unwrap() else { - panic!("expected filled tx"); - }; - dbg!("prepared transaction request", tx.clone(), target_block); - let tx_bytes = tx.encoded_2718(); - - let bundle = EthSendBundle { - txs: vec![tx_bytes.clone().into()], - block_number: target_block, - ..Default::default() - }; - - let call_bundle = EthCallBundle { - txs: vec![tx_bytes.clone().into()], - block_number: target_block, - ..Default::default() - }; - - let sim = flashbots - .call_bundle(call_bundle) - .with_auth(builder_key.clone()); - dbg!(sim.await.unwrap()); - - let result = flashbots.send_bundle(bundle).with_auth(builder_key.clone()); - dbg!(result.await.unwrap()); -} - -#[tokio::test] -#[ignore = "integration test"] -pub async fn test_send_single_tx_sepolia() { - setup_logging(); - - let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); - let builder_key = LocalOrAws::load(&raw_key, Some(11155111)) - .await - .expect("failed to load builder key"); - - let sepolia_host = get_sepolia_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) = sepolia_host.fill(req.clone()).await.unwrap() else { + let SendableTx::Envelope(tx) = pecorino.fill(req.clone()).await.unwrap() else { panic!("expected filled tx"); }; dbg!("prepared transaction request", tx.clone()); - let tx_bytes = tx.encoded_2718(); - let pending_tx = sepolia_host - .send_raw_transaction(&tx_bytes) - .await - .expect("should send tx") - .watch() + let block = pecorino + .get_block(BlockId::latest()) .await + .unwrap() .unwrap(); - dbg!(pending_tx); -} - -#[tokio::test] -#[ignore = "integration test"] -pub async fn test_send_single_tx_hoodi() { - setup_logging(); - - let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set"); - let builder_key = LocalOrAws::load(&raw_key, Some(560048)) - .await - .expect("failed to load builder key"); - - 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 hoodi = ProviderBuilder::new() - .wallet(builder_key.clone()) - .connect_http("https://ethereum-hoodi-rpc.publicnode.com".parse().unwrap()); + let target_block = block.number() + 1; + dbg!("preparing bundle for", target_block); - let SendableTx::Envelope(tx) = hoodi.fill(req.clone()).await.unwrap() else { - panic!("expected filled tx"); + let bundle = EthCallBundle { + txs: vec![tx.encoded_2718().into()], + block_number: target_block, + ..Default::default() }; - dbg!("prepared transaction request", tx.clone()); - let tx_bytes = tx.encoded_2718(); - let pending_tx = hoodi - .send_raw_transaction(&tx_bytes) - .await - .expect("should send tx") - .watch() - .await - .unwrap(); - dbg!(pending_tx); + // 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()); } #[tokio::test] #[ignore = "integration test"] -async fn test_send_valid_bundle_hoodi() { +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(560048)) + 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("https://boost-relay-hoodi.flashbots.net".parse().unwrap()); + .connect_http(PECORINO_RBUILDER.clone()); - let hoodi = ProviderBuilder::new() + let pecorino = ProviderBuilder::new() .wallet(builder_key.clone()) - .connect_http("https://ethereum-hoodi-rpc.publicnode.com".parse().unwrap()); + .connect_http("https://rpc.pecorino.signet.sh".parse().unwrap()); let req = TransactionRequest::default() .to(builder_key.address()) @@ -533,39 +199,36 @@ async fn test_send_valid_bundle_hoodi() { .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 { + let SendableTx::Envelope(tx) = pecorino.fill(req.clone()).await.unwrap() else { panic!("expected filled tx"); }; dbg!("prepared transaction request", tx.clone()); - let tx_bytes = tx.encoded_2718(); - let block = hoodi.get_block(BlockId::latest()).await.unwrap().unwrap(); + let block = pecorino + .get_block(BlockId::latest()) + .await + .unwrap() + .unwrap(); let target_block = block.number() + 1; dbg!("preparing bundle for", target_block); - // let call_bundle = EthCallBundle { - // txs: vec![tx_bytes.clone().into()], - // block_number: target_block, - // ..Default::default() - // }; - - // let sim = flashbots - // .call_bundle(call_bundle) - // .with_auth(builder_key.clone()); - // dbg!(sim.await.unwrap()); - let bundle = EthSendBundle { - txs: vec![tx_bytes.clone().into()], + txs: vec![tx.encoded_2718().into()], block_number: target_block, ..Default::default() }; - - let result = flashbots.send_bundle(bundle).with_auth(builder_key.clone()); - dbg!(result.await.unwrap()); + + 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: &SepoliaProvider, tx_hash: B256, deadline: u64) { +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; @@ -603,7 +266,6 @@ async fn assert_tx_included(sepolia: &SepoliaProvider, tx_hash: B256, deadline: /// Initializes logger for printing during testing pub fn setup_logging() { - // Initialize logging let filter = EnvFilter::from_default_env(); let fmt = fmt::layer().with_filter(filter); let registry = registry().with(fmt);