Skip to content

Commit e3f3a9c

Browse files
committed
adds more flashbots integration tests
- adds tests for sending and confirming a bundle on sepolia and mainnet
1 parent f93ca34 commit e3f3a9c

File tree

2 files changed

+219
-35
lines changed

2 files changed

+219
-35
lines changed

src/utils/flashbots.rs

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
//! A generic Flashbots bundle API wrapper.
22
use crate::utils::signer::LocalOrAws;
33
use alloy::{
4-
primitives::{keccak256, BlockNumber},
4+
primitives::keccak256,
55
rpc::{
66
json_rpc::{Id, Response, ResponsePayload, RpcRecv, RpcSend},
7-
types::mev::{EthBundleHash, MevSendBundle, SimBundleResponse},
7+
types::mev::{BundleItem, EthBundleHash, Inclusion, MevSendBundle, SimBundleResponse},
88
},
99
signers::Signer,
1010
};
1111
use init4_from_env_derive::FromEnv;
1212
use reqwest::header::CONTENT_TYPE;
13-
use serde_json::json;
1413
use std::borrow::Cow;
1514

1615
/// Configuration for the Flashbots provider.
@@ -80,21 +79,7 @@ impl Flashbots {
8079
/// Simulate a bundle via `mev_simBundle`.
8180
pub async fn simulate_bundle(&self, bundle: &MevSendBundle) -> eyre::Result<()> {
8281
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-
82+
dbg!("sim bundle response ###", resp);
9883
Ok(())
9984
}
10085

@@ -139,7 +124,6 @@ impl Flashbots {
139124
async fn compute_signature(&self, body_bz: &[u8]) -> Result<String, eyre::Error> {
140125
let payload = keccak256(body_bz).to_string();
141126
let signature = self.signer.sign_message(payload.as_ref()).await?;
142-
dbg!(signature.to_string());
143127
let address = self.signer.address();
144128
let value = format!("{address}:{signature}");
145129
Ok(value)

tests/flashbots.rs

Lines changed: 216 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![cfg(feature = "flashbots")]
22

33
use alloy::{
4+
consensus::constants::GWEI_TO_WEI,
45
eips::Encodable2718,
56
network::EthereumWallet,
67
primitives::{B256, U256},
@@ -12,29 +13,40 @@ use alloy::{
1213
Identity, Provider, ProviderBuilder, SendableTx,
1314
},
1415
rpc::types::{
15-
mev::{BundleItem, MevSendBundle, ProtocolVersion},
16+
mev::{BundleItem, Inclusion, MevSendBundle, Privacy, ProtocolVersion},
1617
TransactionRequest,
1718
},
1819
signers::{local::PrivateKeySigner, Signer},
1920
};
20-
use init4_bin_base::utils::{flashbots::Flashbots, signer::LocalOrAws};
21-
use std::sync::LazyLock;
21+
use init4_bin_base::{
22+
deps::tracing::debug,
23+
deps::tracing_subscriber::{
24+
fmt, layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter, Layer,
25+
},
26+
utils::{flashbots::Flashbots, signer::LocalOrAws},
27+
};
28+
use std::{
29+
env,
30+
sync::LazyLock,
31+
time::{Duration, Instant},
32+
};
2233
use url::Url;
2334

2435
static FLASHBOTS_URL: LazyLock<Url> = LazyLock::new(|| {
25-
Url::parse("https://relay-sepolia.flashbots.net:443").expect("valid flashbots url")
36+
Url::parse("https://relay-sepolia.flashbots.net").expect("valid flashbots url")
2637
});
27-
static BUILDER_KEY: LazyLock<LocalOrAws> = LazyLock::new(|| {
38+
39+
static DEFAULT_BUILDER_KEY: LazyLock<LocalOrAws> = LazyLock::new(|| {
2840
LocalOrAws::Local(PrivateKeySigner::from_bytes(&B256::repeat_byte(0x02)).unwrap())
2941
});
30-
static TEST_PROVIDER: LazyLock<Flashbots> = LazyLock::new(get_test_provider);
3142

32-
fn get_test_provider() -> Flashbots {
33-
Flashbots::new(FLASHBOTS_URL.clone(), BUILDER_KEY.clone())
43+
static TEST_PROVIDER: LazyLock<Flashbots> = LazyLock::new(get_default_test_provider);
44+
45+
fn get_default_test_provider() -> Flashbots {
46+
Flashbots::new(FLASHBOTS_URL.clone(), DEFAULT_BUILDER_KEY.clone())
3447
}
3548

36-
#[allow(clippy::type_complexity)]
37-
fn get_sepolia() -> FillProvider<
49+
type SepoliaProvider = FillProvider<
3850
JoinFill<
3951
JoinFill<
4052
Identity,
@@ -43,9 +55,12 @@ fn get_sepolia() -> FillProvider<
4355
WalletFiller<EthereumWallet>,
4456
>,
4557
alloy::providers::RootProvider,
46-
> {
58+
>;
59+
60+
#[allow(clippy::type_complexity)]
61+
fn get_sepolia(builder_key: LocalOrAws) -> SepoliaProvider {
4762
ProviderBuilder::new()
48-
.wallet(BUILDER_KEY.clone())
63+
.wallet(builder_key.clone())
4964
.connect_http(
5065
"https://ethereum-sepolia-rpc.publicnode.com"
5166
.parse()
@@ -57,13 +72,13 @@ fn get_sepolia() -> FillProvider<
5772
#[ignore = "integration test"]
5873
async fn test_simulate_valid_bundle_sepolia() {
5974
let flashbots = &*TEST_PROVIDER;
60-
let sepolia = get_sepolia();
75+
let sepolia = get_sepolia(DEFAULT_BUILDER_KEY.clone());
6176

6277
let req = TransactionRequest::default()
63-
.to(BUILDER_KEY.address())
78+
.to(DEFAULT_BUILDER_KEY.address())
6479
.value(U256::from(1u64))
6580
.gas_limit(51_000)
66-
.from(BUILDER_KEY.address());
81+
.from(DEFAULT_BUILDER_KEY.address());
6782
let SendableTx::Envelope(tx) = sepolia.fill(req).await.unwrap() else {
6883
panic!("expected filled tx");
6984
};
@@ -78,7 +93,7 @@ async fn test_simulate_valid_bundle_sepolia() {
7893

7994
let bundle_body = vec![BundleItem::Tx {
8095
tx: tx_bytes,
81-
can_revert: true,
96+
can_revert: false,
8297
}];
8398
let bundle = MevSendBundle::new(latest_block, Some(0), ProtocolVersion::V0_1, bundle_body);
8499

@@ -94,3 +109,188 @@ async fn test_simulate_valid_bundle_sepolia() {
94109
"unexpected error: {err}"
95110
);
96111
}
112+
113+
#[tokio::test]
114+
#[ignore = "integration test"]
115+
async fn test_send_valid_bundle_sepolia() {
116+
setup_logging();
117+
118+
let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set");
119+
let builder_key = LocalOrAws::load(&raw_key, Some(11155111))
120+
.await
121+
.expect("failed to load builder key");
122+
123+
let flashbots = Flashbots::new(FLASHBOTS_URL.clone(), builder_key.clone());
124+
let sepolia = get_sepolia(builder_key.clone());
125+
126+
let req = TransactionRequest::default()
127+
.to(builder_key.address())
128+
.value(U256::from(1u64))
129+
.gas_limit(21_000)
130+
.max_fee_per_gas((50 * GWEI_TO_WEI).into())
131+
.max_priority_fee_per_gas((2 * GWEI_TO_WEI).into())
132+
.from(builder_key.address());
133+
134+
sepolia.estimate_gas(req.clone()).await.unwrap();
135+
136+
let SendableTx::Envelope(tx) = sepolia.fill(req.clone()).await.unwrap() else {
137+
panic!("expected filled tx");
138+
};
139+
let tx_bytes = tx.encoded_2718().into();
140+
141+
let latest_block = sepolia
142+
.get_block_by_number(alloy::eips::BlockNumberOrTag::Latest)
143+
.await
144+
.unwrap()
145+
.unwrap()
146+
.number();
147+
// Give ourselves a buffer: target a couple blocks out to avoid timing edges
148+
let target_block = latest_block + 1;
149+
150+
// Assemble the bundle and target it to the latest block
151+
let bundle_body = vec![BundleItem::Tx {
152+
tx: tx_bytes,
153+
can_revert: false,
154+
}];
155+
let mut bundle = MevSendBundle::new(
156+
target_block,
157+
Some(target_block + 5),
158+
ProtocolVersion::V0_1,
159+
bundle_body,
160+
);
161+
bundle.inclusion = Inclusion::at_block(target_block);
162+
// bundle.privacy = Some(Privacy::default().with_builders(Some(vec![
163+
// "flashbots".to_string(),
164+
// "rsync".to_string(),
165+
// "Titan".to_string(),
166+
// "beaverbuild.org".to_string(),
167+
// ])));
168+
169+
dbg!(latest_block);
170+
dbg!(&bundle.inclusion.block_number(), &bundle.inclusion.max_block_number());
171+
172+
flashbots.simulate_bundle(&bundle).await.unwrap();
173+
174+
let bundle_resp = flashbots.send_bundle(&bundle).await.unwrap();
175+
assert!(bundle_resp.bundle_hash != B256::ZERO);
176+
dbg!(bundle_resp);
177+
178+
assert_tx_included(&sepolia, tx.hash().clone(), 15).await;
179+
}
180+
181+
#[tokio::test]
182+
#[ignore = "integration test"]
183+
async fn test_send_valid_bundle_mainnet() {
184+
setup_logging();
185+
186+
let raw_key = env::var("BUILDER_KEY").expect("BUILDER_KEY must be set");
187+
188+
let builder_key = LocalOrAws::load(&raw_key, None)
189+
.await
190+
.expect("failed to load builder key");
191+
debug!(builder_key_address = ?builder_key.address(), "loaded builder key");
192+
193+
let flashbots = Flashbots::new(
194+
Url::parse("https://relay.flashbots.net").unwrap(),
195+
builder_key.clone(),
196+
);
197+
debug!(?flashbots.relay_url, "created flashbots provider");
198+
199+
let mainnet = ProviderBuilder::new()
200+
.wallet(builder_key.clone())
201+
.connect_http("https://cloudflare-eth.com".parse().unwrap());
202+
203+
// Build a valid transaction to bundle
204+
let req = TransactionRequest::default()
205+
.to(builder_key.address())
206+
.value(U256::from(1u64))
207+
.gas_limit(21_000)
208+
.max_fee_per_gas((50 * GWEI_TO_WEI).into())
209+
.max_priority_fee_per_gas((2 * GWEI_TO_WEI).into())
210+
.from(builder_key.address());
211+
dbg!(req.clone());
212+
213+
// Estimate gas will fail if this wallet isn't properly funded for this TX.
214+
let gas_estimates = mainnet.estimate_gas(req.clone()).await.unwrap();
215+
dbg!(gas_estimates);
216+
217+
let SendableTx::Envelope(tx) = mainnet.fill(req.clone()).await.unwrap() else {
218+
panic!("expected filled tx");
219+
};
220+
dbg!(req.clone());
221+
222+
let tx_bytes = tx.encoded_2718().into();
223+
dbg!(tx.hash());
224+
225+
// Fetch latest block info to build a valid target block for the bundle
226+
let latest_block = mainnet
227+
.get_block_by_number(alloy::eips::BlockNumberOrTag::Latest)
228+
.await
229+
.unwrap()
230+
.unwrap()
231+
.number();
232+
let target_block = latest_block + 1;
233+
234+
// Assemble the bundle and target it to the latest block
235+
let bundle_body = vec![BundleItem::Tx {
236+
tx: tx_bytes,
237+
can_revert: false,
238+
}];
239+
let mut bundle = MevSendBundle::new(target_block, None, ProtocolVersion::V0_1, bundle_body);
240+
bundle.inclusion = Inclusion::at_block(target_block);
241+
bundle.privacy = Some(Privacy::default().with_builders(Some(vec!["flashbots".to_string()])));
242+
243+
let resp = flashbots
244+
.send_bundle(&bundle)
245+
.await
246+
.expect("should send bundle");
247+
dbg!(&resp);
248+
249+
assert!(resp.bundle_hash != B256::ZERO);
250+
}
251+
252+
/// Asserts that a tx was included in Sepolia within `deadline` seconds.
253+
async fn assert_tx_included(sepolia: &SepoliaProvider, tx_hash: B256, deadline: u64) {
254+
let now = Instant::now();
255+
let deadline = now + Duration::from_secs(deadline);
256+
let mut found = false;
257+
258+
loop {
259+
let n = Instant::now();
260+
if n >= deadline {
261+
break;
262+
}
263+
264+
match sepolia.get_transaction_by_hash(tx_hash).await {
265+
Ok(Some(_tx)) => {
266+
found = true;
267+
break;
268+
}
269+
Ok(None) => {
270+
// Not yet present; wait and retry
271+
dbg!("transaction not yet seen");
272+
tokio::time::sleep(Duration::from_secs(1)).await;
273+
}
274+
Err(err) => {
275+
// Transient error querying the provider; log and retry
276+
eprintln!("warning: error querying tx: {}", err);
277+
tokio::time::sleep(Duration::from_secs(1)).await;
278+
}
279+
}
280+
}
281+
282+
assert!(
283+
found,
284+
"transaction was not seen by the provider within {:?} seconds",
285+
deadline
286+
);
287+
}
288+
289+
/// Initializes logger for printing during testing
290+
pub fn setup_logging() {
291+
// Initialize logging
292+
let filter = EnvFilter::from_default_env();
293+
let fmt = fmt::layer().with_filter(filter);
294+
let registry = registry().with(fmt);
295+
let _ = registry.try_init();
296+
}

0 commit comments

Comments
 (0)