11#![ cfg( feature = "flashbots" ) ]
22
33use 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+ } ;
2233use url:: Url ;
2334
2435static 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" ]
5873async 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