kabu_backtest_runner/
main.rs

1use crate::flashbots_mock::BundleRequest;
2use crate::flashbots_mock::mount_flashbots_mock;
3use crate::test_config::TestConfig;
4use alloy_network::Ethereum;
5use alloy_primitives::{Address, TxHash, U256, address};
6use alloy_provider::Provider;
7use alloy_provider::network::eip2718::Encodable2718;
8use alloy_rpc_types::{BlockId, BlockNumberOrTag};
9use alloy_rpc_types_eth::TransactionTrait;
10use chrono::Local;
11use clap::Parser;
12use eyre::{OptionExt, Result, eyre};
13use kabu::core::blockchain::Blockchain;
14use kabu::core::blockchain::BlockchainState;
15use kabu::core::config::KabuConfig;
16use kabu::defi::address_book::TokenAddressEth;
17use kabu::defi::pools::PoolsLoadingConfig;
18use kabu::defi::pools::{UniswapV2Pool, UniswapV3Pool};
19use kabu::evm::db::{AlloyDB, KabuDB};
20use kabu::evm::utils::NWETH;
21use kabu::execution::multicaller::{MulticallerDeployer, MulticallerSwapEncoder};
22use kabu::node::debug_provider::AnvilDebugProviderFactory;
23use kabu::node::reth_api::KabuRethProviderWrapper;
24use kabu::strategy::backrun::BackrunConfig;
25use kabu::types::blockchain::{ChainParameters, debug_trace_block};
26use kabu::types::entities::LoomTxSigner;
27use kabu::types::events::{MarketEvents, MempoolEvents, SwapComposeMessage};
28use kabu::types::market::{MarketState, PoolClass, Token};
29use kabu::types::market::{Pool, PoolWrapper, RequiredStateReader};
30use kabu::types::swap::Swap;
31use kabu_core_components::MevComponentChannels;
32use kabu_core_node::{KabuContext, KabuEthereumNode, NodeBuilder, NodeConfig};
33use reth::chainspec::MAINNET;
34use reth_node_ethereum::{EthEvmConfig, EthereumNode};
35use reth_storage_rpc_provider::{RpcBlockchainProvider, RpcBlockchainProviderConfig};
36use reth_tasks::TaskManager;
37use std::env;
38use std::fmt::{Display, Formatter};
39use std::process::exit;
40use std::time::{Duration, Instant};
41use tracing::{debug, error, info, warn};
42use tracing_subscriber::layer::SubscriberExt;
43use tracing_subscriber::util::SubscriberInitExt;
44use tracing_subscriber::{EnvFilter, Layer, fmt};
45use wiremock::MockServer;
46
47mod flashbots_mock;
48mod test_config;
49
50use reth::primitives::{EthPrimitives, TxTy};
51use reth::rpc::compat::{TryFromBlockResponse, TryFromTransactionResponse};
52use reth_ethereum_primitives::Receipt;
53use reth_primitives::Block;
54use reth_primitives_traits::block::RecoveredBlock;
55use reth_primitives_traits::{Block as BlockTrait, BlockTy, SignerRecoverable};
56use revm::database::BundleState;
57use std::io::{self, Write};
58
59#[derive(Clone, Default, Debug)]
60struct Stat {
61    found_counter: usize,
62    sign_counter: usize,
63    best_profit_eth: U256,
64    best_swap: Option<Swap>,
65    start_time: Option<Instant>,
66}
67
68impl Display for Stat {
69    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
70        // Runtime
71        if let Some(start) = self.start_time {
72            let elapsed = start.elapsed();
73            writeln!(f, "  Runtime: {:.3}s", elapsed.as_secs_f64())?;
74        }
75
76        // Summary
77        writeln!(f, "  Opportunities Found: {}", self.found_counter)?;
78        writeln!(
79            f,
80            "  Opportunities Verified: {} ({:.1}%)",
81            self.sign_counter,
82            if self.found_counter > 0 { (self.sign_counter as f64 / self.found_counter as f64) * 100.0 } else { 0.0 }
83        )?;
84
85        // Best arbitrage
86        match &self.best_swap {
87            Some(swap) => {
88                writeln!(f, "\n  Best Arbitrage:")?;
89                match swap.get_first_token() {
90                    Some(token) => {
91                        writeln!(f, "    Token Profit: {} {}", token.to_float(swap.arb_profit()), token.get_symbol())?;
92                    }
93                    None => {
94                        writeln!(f, "    Raw Profit: {}", swap.arb_profit())?;
95                    }
96                }
97                writeln!(f, "    ETH Profit: {} ETH", NWETH::to_float(swap.arb_profit_eth()))?;
98                writeln!(f, "    Path: {swap}")?;
99            }
100            None => {
101                writeln!(f, "\n  No profitable arbitrage found")?;
102            }
103        }
104
105        Ok(())
106    }
107}
108
109#[allow(dead_code)]
110fn parse_tx_hashes(tx_hash_vec: Vec<&str>) -> Result<Vec<TxHash>> {
111    let mut ret: Vec<TxHash> = Vec::new();
112    for tx_hash in tx_hash_vec {
113        ret.push(tx_hash.parse()?);
114    }
115    Ok(ret)
116}
117
118use std::ffi::OsStr;
119use std::path::PathBuf;
120
121#[derive(Parser, Debug)]
122struct Commands {
123    /// Timeout in seconds after the test fails
124    #[arg(short, long, default_value = "10")]
125    timeout: u64,
126
127    /// Wait xx seconds before start re-broadcasting
128    #[arg(short, long, default_value = "1")]
129    wait_init: u64,
130
131    /// Flashbots collection wait time in seconds
132    #[arg(short = 'f', long, default_value = "0")]
133    flashbots_wait: u64,
134
135    /// Path to test file or directory containing tests
136    path: PathBuf,
137}
138
139#[tokio::main]
140async fn main() -> Result<()> {
141    dotenvy::from_filename(".env.test").ok();
142    let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| "debug,alloy_rpc_client=off,kabu_multicaller=trace".into());
143    let fmt_layer = fmt::Layer::default().with_thread_ids(true).with_file(false).with_line_number(true).with_filter(env_filter);
144
145    tracing_subscriber::registry().with(fmt_layer).init();
146
147    let args = Commands::parse();
148
149    if args.path.is_file() {
150        // Run single test
151        println!("[{}] Running single test: {}", Local::now().format("%H:%M:%S.%3f"), args.path.display());
152        execute_test(args.path.clone(), &args).await?;
153    } else if args.path.is_dir() {
154        // Find and run multiple tests
155        let mut test_files = Vec::new();
156        for entry in std::fs::read_dir(&args.path)? {
157            let path = entry?.path();
158            if path.extension() == Some(OsStr::new("toml"))
159                && path.file_name().and_then(|n| n.to_str()).map(|n| n.starts_with("test_")).unwrap_or(false)
160            {
161                test_files.push(path);
162            }
163        }
164        test_files.sort();
165
166        println!("[{}] Found {} tests in {}", Local::now().format("%H:%M:%S.%3f"), test_files.len(), args.path.display());
167
168        let mut passed = 0;
169        let mut failed = 0;
170        let mut skipped = 0;
171
172        for test_file in test_files {
173            // Quick check if test is disabled
174            let test_config = TestConfig::from_file(test_file.to_string_lossy().to_string()).await?;
175
176            if test_config.settings.disabled {
177                skipped += 1;
178                println!("\n[{}] SKIPPED: {} (disabled)", Local::now().format("%H:%M:%S.%3f"), test_file.display());
179                continue;
180            }
181
182            println!("\n{}", "=".repeat(70));
183            println!("[{}] Running: {}", Local::now().format("%H:%M:%S.%3f"), test_file.display());
184            println!("{}", "=".repeat(70));
185
186            match execute_test(test_file, &args).await {
187                Ok(()) => {
188                    passed += 1;
189                    println!("[{}] ✓ Test passed", Local::now().format("%H:%M:%S.%3f"));
190                }
191                Err(e) => {
192                    failed += 1;
193                    eprintln!("[{}] ✗ Test failed: {}", Local::now().format("%H:%M:%S.%3f"), e);
194                }
195            }
196        }
197
198        println!("\n{}", "=".repeat(70));
199        println!("[{}] TEST SUMMARY: {} passed, {} failed, {} skipped", Local::now().format("%H:%M:%S.%3f"), passed, failed, skipped);
200        println!("{}", "=".repeat(70));
201
202        if failed > 0 {
203            std::process::exit(1);
204        }
205    } else {
206        return Err(eyre!("Path does not exist: {}", args.path.display()));
207    }
208
209    Ok(())
210}
211
212// Complete test execution - everything happens here for isolation
213async fn execute_test(test_path: PathBuf, args: &Commands) -> Result<()> {
214    let test_config = TestConfig::from_file(test_path.to_string_lossy().to_string()).await?;
215
216    // Check if test is disabled
217    if test_config.settings.disabled {
218        println!("[{}] SKIPPED: Test disabled - {}", Local::now().format("%H:%M:%S.%3f"), test_path.display());
219        return Ok(());
220    }
221
222    println!("[{}] TEST STARTED: {}", Local::now().format("%H:%M:%S.%3f"), test_path.display());
223    println!("[{}] Block: {} | Timeout: {}s", Local::now().format("%H:%M:%S.%3f"), test_config.settings.block, args.timeout);
224
225    // Create a fresh Anvil instance for this test
226    let node_url = env::var("MAINNET_WS")?;
227    let client = AnvilDebugProviderFactory::from_node_on_block(node_url, test_config.settings.block).await?;
228    let priv_key = client.privkey()?.to_bytes().to_vec();
229
230    let mut mock_server: Option<MockServer> = None;
231    if test_config.modules.flashbots {
232        // Start flashbots mock server
233        mock_server = Some(MockServer::start().await);
234        mount_flashbots_mock(mock_server.as_ref().unwrap()).await;
235    }
236
237    let multicaller_address = MulticallerDeployer::new()
238        .set_code(client.clone(), address!("FCfCfcfC0AC30164AFdaB927F441F2401161F358"))
239        .await?
240        .address()
241        .ok_or_eyre("MULTICALLER_NOT_DEPLOYED")?;
242    info!("Multicaller deployed at {:?}", multicaller_address);
243
244    let multicaller_encoder = MulticallerSwapEncoder::default_with_address(multicaller_address);
245
246    let block_number = client.get_block_number().await?;
247    info!("Current block_number={}", block_number);
248
249    let block_response = client.get_block(block_number.into()).await?.unwrap();
250    let block = <BlockTy<EthPrimitives> as TryFromBlockResponse<Ethereum>>::from_block_response(block_response)?;
251    let block_header = block.header().clone();
252    info!("Current block_header={:?}", block_header);
253
254    let block_response = client.get_block(block_number.into()).await?.unwrap();
255
256    let block_header_with_txes = <BlockTy<EthPrimitives> as TryFromBlockResponse<Ethereum>>::from_block_response(block_response)?;
257
258    // Create AlloyDB connected to the forked Anvil instance
259    let alloy_db =
260        AlloyDB::new(client.clone(), BlockId::Number(BlockNumberOrTag::Number(block_number))).ok_or_eyre("Failed to create AlloyDB")?;
261    let state_db = KabuDB::new().with_ext_db(alloy_db);
262    let market_state_instance = MarketState::new(state_db);
263
264    // Add default tokens for price actor
265
266    info!("Creating blockchain and initial state");
267    // Create Blockchain instance which manages all channels
268    // Disable influxdb for test runner
269    let blockchain = Blockchain::new_with_config(1, false); // Chain ID 1 for mainnet, influxdb disabled
270
271    // Create blockchain state
272    let blockchain_state = BlockchainState::<KabuDB, EthPrimitives>::new_with_market_state(market_state_instance);
273
274    // Get references we need for setup
275    let market_instance = blockchain.market();
276
277    // Update the market with our test tokens
278    {
279        let mut market_guard = market_instance.write().await;
280        market_guard.add_token(Token::new_with_data(TokenAddressEth::USDC, Some("USDC".to_string()), None, Some(6), true, false));
281        market_guard.add_token(Token::new_with_data(TokenAddressEth::USDT, Some("USDT".to_string()), None, Some(6), true, false));
282        market_guard.add_token(Token::new_with_data(TokenAddressEth::WBTC, Some("WBTC".to_string()), None, Some(8), true, false));
283        market_guard.add_token(Token::new_with_data(TokenAddressEth::DAI, Some("DAI".to_string()), None, Some(18), true, false));
284    }
285
286    // Update latest block with current block info
287    let (_, _post) = debug_trace_block(client.clone(), BlockId::Number(BlockNumberOrTag::Number(block_number)), true).await?;
288
289    info!("Starting initialize signers actor");
290
291    for (token_name, token_config) in test_config.tokens {
292        let symbol = token_config.symbol.unwrap_or(token_config.address.to_checksum(None));
293        let name = token_config.name.unwrap_or(symbol.clone());
294        let token = Token::new_with_data(
295            token_config.address,
296            Some(symbol),
297            Some(name),
298            Some(token_config.decimals.map_or(18, |x| x)),
299            token_config.basic.unwrap_or_default(),
300            token_config.middle.unwrap_or_default(),
301        );
302        if let Some(price_float) = token_config.price {
303            let price_u256 = NWETH::from_float(price_float) * token.get_exp() / NWETH::get_exp();
304            debug!("Setting price : {} -> {} ({})", token_name, price_u256, price_u256.to::<u128>());
305
306            token.set_eth_price(Some(price_u256));
307        };
308
309        market_instance.write().await.add_token(token);
310    }
311
312    // Create config for test environment
313    let config = KabuConfig {
314        remote_node: None, // No remote node needed for test environment
315        signers: Vec::new(),
316        builders: Vec::new(),
317        multicaller_address,
318        influxdb: None,
319        webserver: None,
320        database: None, // Database is optional for test runner
321    };
322
323    // Create BackrunConfig from test settings
324    let backrun_config = BackrunConfig::new_dumb();
325
326    // Configure which pools to load based on test config
327    let mut pools_config = PoolsLoadingConfig::disable_all(PoolsLoadingConfig::default());
328    for pool_config in test_config.pools.values() {
329        match pool_config.class {
330            PoolClass::UniswapV2 => pools_config = pools_config.enable(PoolClass::UniswapV2),
331            PoolClass::UniswapV3 => pools_config = pools_config.enable(PoolClass::UniswapV3),
332            PoolClass::Curve => pools_config = pools_config.enable(PoolClass::Curve),
333            _ => {}
334        }
335    }
336
337    println!("[{}] Building Kabu MEV components...", Local::now().format("%H:%M:%S.%3f"));
338    info!("Building Kabu MEV components with KabuEthereumNode");
339
340    // For test runner, we don't need a database pool
341    info!("Test runner using no database pool");
342
343    // Create channels first so we can use them throughout the test
344    let mev_channels = MevComponentChannels::<KabuDB>::default();
345
346    // Create RpcBlockchainProvider for test environment and wrap it with canonical state support
347    let rpc_config = RpcBlockchainProviderConfig { compute_state_root: false, reth_rpc_support: false };
348    let inner_provider =
349        RpcBlockchainProvider::<_, EthereumNode, Ethereum>::new_with_config(client.clone(), rpc_config).with_chain_spec(MAINNET.clone());
350    let reth_provider = KabuRethProviderWrapper::new(inner_provider);
351
352    // Create EVM config
353    let evm_config = EthEvmConfig::mainnet();
354
355    // Create node configuration
356    let node_config = NodeConfig::new()
357        .chain_id(1)
358        .config(config.clone())
359        .backrun_config(backrun_config.clone())
360        .multicaller_address(multicaller_address)
361        .swap_encoder(multicaller_encoder.clone())
362        .pools_config(pools_config.clone())
363        .enable_web_server(false); // Disable web server for test runner
364
365    // Create context with all providers and custom channels for testing
366    let mut context =
367        KabuContext::new(reth_provider.clone(), client.clone(), evm_config, blockchain.clone(), blockchain_state.clone(), node_config);
368
369    // Override channels with our test channels
370    context.channels = mev_channels.clone();
371
372    // Create TaskExecutor
373    let task_manager = TaskManager::new(tokio::runtime::Handle::current());
374    let task_executor = task_manager.executor();
375
376    // Initialize test signer BEFORE launching components
377    println!("[{}] Initializing test signer...", Local::now().format("%H:%M:%S.%3f"));
378    {
379        let signers = mev_channels.signers.clone();
380        let account_state = mev_channels.account_state.clone();
381
382        // Add the private key directly
383        let signer = signers.write().await.add_privkey(alloy_primitives::Bytes::from(priv_key));
384
385        // Get the actual nonce and balance from Anvil
386        let nonce = client.get_transaction_count(signer.address()).await?;
387        let balance = client.get_balance(signer.address()).await?;
388
389        // Set the correct account state
390        account_state.write().await.add_account(signer.address()).set_nonce(nonce).set_balance(Address::default(), balance); // ETH balance
391
392        println!(
393            "[{}] Test signer initialized: {:?} (nonce={}, balance={})",
394            Local::now().format("%H:%M:%S.%3f"),
395            signer.address(),
396            nonce,
397            balance
398        );
399    }
400
401    // Build and launch components - now SignersComponent will have signers configured
402    let _handle = NodeBuilder::new()
403        .with_context(context)
404        .with_components(KabuEthereumNode::components().build())
405        .launch(task_executor.clone())
406        .await?;
407
408    // Give components time to initialize
409    println!("[{}] Waiting for component initialization...", Local::now().format("%H:%M:%S.%3f"));
410    tokio::time::sleep(Duration::from_millis(500)).await;
411
412    // Manually load test pools since automatic loader may not work in test environment
413    let total_pools = test_config.pools.len();
414    if total_pools > 0 {
415        println!("[{}] Loading {} test pools manually...", Local::now().format("%H:%M:%S.%3f"), total_pools);
416
417        let mut pools_loaded = 0;
418
419        // Get access to the market state to update the state DB
420        let market_state_db = blockchain_state.market_state_commit();
421
422        for (pool_name, pool_config) in &test_config.pools {
423            match pool_config.class {
424                PoolClass::UniswapV2 => {
425                    // For UniswapV2, we need to fetch pool data from chain
426                    match UniswapV2Pool::fetch_pool_data(client.clone(), pool_config.address).await {
427                        Ok(pool) => {
428                            // Load pool state into state DB
429                            match pool.get_state_required() {
430                                Ok(state_required) => {
431                                    match RequiredStateReader::fetch_calls_and_slots(client.clone(), state_required, Some(block_number))
432                                        .await
433                                    {
434                                        Ok(state_update) => {
435                                            market_state_db.write().await.state_db.apply_geth_update(state_update);
436                                            debug!("Applied state update for UniswapV2 pool {}", pool_name);
437                                        }
438                                        Err(e) => {
439                                            error!("Failed to fetch state for UniswapV2 pool {}: {}", pool_name, e);
440                                        }
441                                    }
442                                }
443                                Err(e) => {
444                                    error!("Failed to get required state for UniswapV2 pool {}: {}", pool_name, e);
445                                }
446                            }
447
448                            if let Err(e) = market_instance.write().await.add_pool(PoolWrapper::from(pool)) {
449                                error!("Failed to add UniswapV2 pool to market: {}", e);
450                            } else {
451                                pools_loaded += 1;
452                                println!("  ✓ Loaded {} (UniswapV2) at {}", pool_name, pool_config.address);
453                            }
454                        }
455                        Err(e) => {
456                            error!("Failed to load UniswapV2 pool {}: {}", pool_name, e);
457                        }
458                    }
459                }
460                PoolClass::UniswapV3 => {
461                    // For UniswapV3, we need to fetch pool data from chain
462                    match UniswapV3Pool::fetch_pool_data(client.clone(), pool_config.address).await {
463                        Ok(pool) => {
464                            // Load pool state into state DB
465                            match pool.get_state_required() {
466                                Ok(state_required) => {
467                                    match RequiredStateReader::fetch_calls_and_slots(client.clone(), state_required, Some(block_number))
468                                        .await
469                                    {
470                                        Ok(state_update) => {
471                                            market_state_db.write().await.state_db.apply_geth_update(state_update);
472                                            debug!("Applied state update for UniswapV3 pool {}", pool_name);
473                                        }
474                                        Err(e) => {
475                                            error!("Failed to fetch state for UniswapV3 pool {}: {}", pool_name, e);
476                                        }
477                                    }
478                                }
479                                Err(e) => {
480                                    error!("Failed to get required state for UniswapV3 pool {}: {}", pool_name, e);
481                                }
482                            }
483
484                            if let Err(e) = market_instance.write().await.add_pool(PoolWrapper::from(pool)) {
485                                error!("Failed to add UniswapV3 pool to market: {}", e);
486                            } else {
487                                pools_loaded += 1;
488                                println!("  ✓ Loaded {} (UniswapV3) at {}", pool_name, pool_config.address);
489                            }
490                        }
491                        Err(e) => {
492                            error!("Failed to load UniswapV3 pool {}: {}", pool_name, e);
493                        }
494                    }
495                }
496                _ => {
497                    warn!("Unsupported pool class {:?} for {}", pool_config.class, pool_name);
498                }
499            }
500        }
501
502        println!("[{}] Loaded {}/{} test pools", Local::now().format("%H:%M:%S.%3f"), pools_loaded, total_pools);
503    }
504
505    // Get references for event sending
506    // Use channels from MevComponentChannels (these are what components are listening to)
507    let market_events_channel = mev_channels.market_events.clone();
508    let mempool_events_channel = mev_channels.mempool_events.clone();
509    // But mempool instance comes from blockchain
510    let mempool_instance = blockchain.mempool();
511
512    // #### Blockchain events
513    // we need to wait for all components to start. For the CI it can be a bit longer
514    let components_wait = args.wait_init;
515    println!("[{}] Waiting {}s for all components to initialize...", Local::now().format("%H:%M:%S.%3f"), components_wait);
516
517    // Show countdown
518    for i in (1..=components_wait).rev() {
519        print!("\r[{}] Starting in {}s... ", Local::now().format("%H:%M:%S.%3f"), i);
520        io::stdout().flush().unwrap();
521        tokio::time::sleep(Duration::from_secs(1)).await;
522    }
523    println!("\r[{}] Components ready!              ", Local::now().format("%H:%M:%S.%3f"));
524
525    let next_block_base_fee = ChainParameters::ethereum().calc_next_block_base_fee(
526        block_header.gas_used,
527        block_header.gas_limit,
528        block_header.base_fee_per_gas.unwrap_or_default(),
529    );
530
531    // Send canonical state notification for the block
532    // Convert the block and state to the format needed for canonical notification
533    // Create an empty BundleState for testing
534    let bundle_state = BundleState::default();
535
536    let execution_outcome = reth_execution_types::ExecutionOutcome::<Receipt> {
537        bundle: bundle_state,
538        receipts: vec![vec![]], // Empty receipts for now
539        first_block: block_header.number,
540        requests: vec![], // requests
541    };
542
543    // We need to recover senders for the block
544    // For backtest, we can use empty senders since we're not validating signatures
545    // Convert alloy Block to reth Block
546    let reth_block = Block {
547        header: block_header.clone(),
548        body: alloy_consensus::BlockBody {
549            transactions: block_header_with_txes.body().transactions.clone(),
550            ommers: vec![],
551            withdrawals: block_header_with_txes.body().withdrawals.clone(),
552        },
553    };
554
555    // Create RecoveredBlock with empty senders for testing
556    let senders = vec![Address::ZERO; reth_block.body.transactions.len()];
557    let recovered_block = RecoveredBlock::new_unhashed(reth_block, senders);
558
559    if let Err(e) = reth_provider.send_canonical_commit(recovered_block, execution_outcome) {
560        error!("Failed to send canonical state notification: {}", e);
561    } else {
562        info!(
563            "Sent canonical state notification for block {} with {} transactions",
564            block_header.number,
565            block_header_with_txes.body().transactions.len()
566        );
567    }
568
569    // Sending block header update message for market events
570    if let Err(e) = market_events_channel.send(MarketEvents::BlockHeaderUpdate {
571        block_number: block_header.number,
572        block_hash: block_header.hash_slow(),
573        timestamp: block_header.timestamp,
574        base_fee: block_header.base_fee_per_gas.unwrap_or_default(),
575        next_base_fee: next_block_base_fee,
576    }) {
577        error!("Failed to send BlockHeaderUpdate: {}", e);
578    } else {
579        info!("Sent BlockHeaderUpdate for block {}", block_header.number);
580    }
581
582    // #### RE-BROADCASTER
583    //starting broadcasting transactions from eth to anvil
584    let client_clone = client.clone();
585    let mempool_events_channel_clone = mempool_events_channel.clone();
586    let mempool_instance_clone = mempool_instance.clone();
587    let market_events_channel_clone = market_events_channel.clone();
588    let block_hash = block_header.hash_slow();
589    tokio::spawn(async move {
590        info!("Re-broadcaster task started");
591
592        let total_txs = test_config.txs.len();
593        if total_txs > 0 {
594            info!("Re-broadcasting {} transactions", total_txs);
595        }
596
597        // Add delay to ensure components are ready
598        tokio::time::sleep(Duration::from_millis(100)).await;
599
600        for (_, tx_config) in test_config.txs.iter() {
601            debug!("Fetching original tx {}", tx_config.hash);
602            let Some(transaction_response) = client_clone.get_transaction_by_hash(tx_config.hash).await.unwrap() else {
603                panic!("Cannot get tx: {}", tx_config.hash);
604            };
605
606            let tx =
607                <TxTy<EthPrimitives> as TryFromTransactionResponse<Ethereum>>::from_transaction_response(transaction_response).unwrap();
608
609            let from = tx.recover_signer().unwrap();
610            let to = tx.to().unwrap_or_default();
611
612            match tx_config.send.to_lowercase().as_str() {
613                "mempool" => {
614                    let mut mempool_guard = mempool_instance_clone.write().await;
615                    let tx_hash: TxHash = *tx.tx_hash();
616
617                    mempool_guard.add_tx(tx.clone());
618                    drop(mempool_guard); // Release lock before sending event
619
620                    if let Err(e) = mempool_events_channel_clone.send(MempoolEvents::MempoolActualTxUpdate { tx_hash }) {
621                        error!("Failed to send mempool event: {}", e);
622                    } else {
623                        info!("Sent mempool event for tx {}", tx_hash);
624                    }
625                }
626                "block" => match client_clone.send_raw_transaction(tx.encoded_2718().as_slice()).await {
627                    Ok(p) => {
628                        debug!("Transaction sent {}", p.tx_hash());
629                    }
630                    Err(e) => {
631                        error!("Error sending transaction : {e}");
632                    }
633                },
634                _ => {
635                    debug!("Incorrect action {} for : hash {} from {} to {}  ", tx_config.send, tx.tx_hash(), from, to);
636                }
637            }
638        }
639
640        // Send block state update after all transactions are added to mempool
641        tokio::time::sleep(Duration::from_millis(100)).await;
642        if let Err(e) = market_events_channel_clone.send(MarketEvents::BlockStateUpdate { block_hash }) {
643            error!("Failed to send block state update: {}", e);
644        } else {
645            info!("Sent block state update for block {}", block_hash);
646        }
647    });
648
649    let test_start_time = Instant::now();
650    println!("[{}] Waiting for arbitrage opportunities...", Local::now().format("%H:%M:%S.%3f"));
651
652    let mut tx_compose_sub = mev_channels.swap_compose.subscribe();
653
654    let mut stat = Stat { start_time: Some(test_start_time), ..Default::default() };
655    let timeout_duration = Duration::from_secs(args.timeout);
656    let mut last_update = Instant::now();
657    let update_interval = Duration::from_secs(5);
658
659    loop {
660        tokio::select! {
661            msg = tx_compose_sub.recv() => {
662                match msg {
663                    Ok(msg) => match msg.inner {
664                        SwapComposeMessage::Ready(ready_message) => {
665                            debug!(swap=%ready_message.swap, "Ready message");
666                            stat.sign_counter += 1;
667                            println!(
668                                "[{}] Verified swap #{} - Profit: {} ETH",
669                                Local::now().format("%H:%M:%S.%3f"),
670                                stat.sign_counter,
671                                NWETH::to_float(ready_message.swap.arb_profit_eth())
672                            );
673
674                            if stat.best_profit_eth < ready_message.swap.arb_profit_eth() {
675                                stat.best_profit_eth = ready_message.swap.arb_profit_eth();
676                                stat.best_swap = Some(ready_message.swap.clone());
677                            }
678
679                            if let Some(swaps_ok) = test_config.assertions.swaps_ok
680                                && stat.sign_counter >= swaps_ok  {
681                                    break;
682                                }
683                        }
684                        SwapComposeMessage::Prepare(encode_message) => {
685                            debug!(swap=%encode_message.swap, "Prepare message");
686                            stat.found_counter += 1;
687                            if stat.found_counter == 1 {
688                                println!("[{}] Found first arbitrage opportunity!", Local::now().format("%H:%M:%S.%3f"));
689                            }
690                        }
691                        _ => {}
692                    },
693                    Err(error) => {
694                        error!(%error, "tx_compose_sub.recv")
695                    }
696                }
697            }
698            _ = tokio::time::sleep(update_interval) => {
699                if last_update.elapsed() >= update_interval {
700                    let elapsed = test_start_time.elapsed().as_secs();
701                    let remaining = timeout_duration.as_secs().saturating_sub(elapsed);
702                    println!(
703                        "[{}] Status: {} found, {} verified | Elapsed: {}s, Remaining: {}s",
704                        Local::now().format("%H:%M:%S.%3f"),
705                        stat.found_counter,
706                        stat.sign_counter,
707                        elapsed,
708                        remaining
709                    );
710                    last_update = Instant::now();
711
712                    if elapsed >= timeout_duration.as_secs() {
713                        println!("[{}] Test timeout reached ({}s)", Local::now().format("%H:%M:%S.%3f"), timeout_duration.as_secs());
714                        break;
715                    }
716                }
717            }
718        }
719    }
720    if test_config.modules.flashbots && args.flashbots_wait > 0 {
721        // wait for flashbots mock server to receive all requests
722        println!(
723            "[{}] Waiting {}s for Flashbots mock server to collect requests...",
724            Local::now().format("%H:%M:%S.%3f"),
725            args.flashbots_wait
726        );
727        for i in (1..=args.flashbots_wait).rev() {
728            print!("\r[{}] Flashbots collection: {}s remaining... ", Local::now().format("%H:%M:%S.%3f"), i);
729            io::stdout().flush().unwrap();
730            tokio::time::sleep(Duration::from_secs(1)).await;
731        }
732        println!("\r[{}] Flashbots collection complete!                    ", Local::now().format("%H:%M:%S.%3f"));
733        if let Some(last_requests) = mock_server.unwrap().received_requests().await {
734            if last_requests.is_empty() {
735                println!("[{}] Flashbots: No requests received", Local::now().format("%H:%M:%S.%3f"))
736            } else {
737                println!("[{}] Flashbots: {} requests received", Local::now().format("%H:%M:%S.%3f"), last_requests.len());
738                for request in last_requests {
739                    let bundle_request: BundleRequest = serde_json::from_slice(&request.body)?;
740                    println!(
741                        "[{}]   Bundles: {} | Blocks: {:?} | Txs: {:?}",
742                        Local::now().format("%H:%M:%S.%3f"),
743                        bundle_request.params.len(),
744                        bundle_request.params.iter().map(|b| b.target_block).collect::<Vec<_>>(),
745                        bundle_request.params.iter().map(|b| b.transactions.len()).collect::<Vec<_>>()
746                    );
747                    // print all transactions
748                    for bundle in bundle_request.params {
749                        println!("[{}]     Bundle: {} tx(s)", Local::now().format("%H:%M:%S.%3f"), bundle.transactions.len());
750                    }
751                }
752            }
753        } else {
754            println!("[{}] Flashbots: Recording disabled", Local::now().format("%H:%M:%S.%3f"))
755        }
756    }
757
758    println!("\n[{}] TEST RESULTS\n{}", Local::now().format("%H:%M:%S.%3f"), "=".repeat(50));
759    println!("{stat}");
760    println!("{}", "=".repeat(50));
761
762    if let Some(swaps_encoded) = test_config.assertions.swaps_encoded {
763        if swaps_encoded > stat.found_counter {
764            println!(
765                "\n[{}] FAILED: Encoded swaps\n  Expected: >= {}\n  Actual: {}",
766                Local::now().format("%H:%M:%S.%3f"),
767                swaps_encoded,
768                stat.found_counter
769            );
770            exit(1)
771        } else {
772            println!("[{}] PASSED: Encoded swaps ({})", Local::now().format("%H:%M:%S.%3f"), stat.found_counter);
773        }
774    }
775    if let Some(swaps_ok) = test_config.assertions.swaps_ok {
776        if swaps_ok > stat.sign_counter {
777            println!(
778                "\n[{}] FAILED: Verified swaps\n  Expected: >= {}\n  Actual: {}",
779                Local::now().format("%H:%M:%S.%3f"),
780                swaps_ok,
781                stat.sign_counter
782            );
783            exit(1)
784        } else {
785            println!("[{}] PASSED: Verified swaps ({})", Local::now().format("%H:%M:%S.%3f"), stat.sign_counter);
786        }
787    }
788    if let Some(best_profit) = test_config.assertions.best_profit_eth {
789        if NWETH::from_float(best_profit) > stat.best_profit_eth {
790            println!(
791                "\n[{}] FAILED: Best profit\n  Expected: >= {} ETH\n  Actual: {} ETH",
792                Local::now().format("%H:%M:%S.%3f"),
793                best_profit,
794                NWETH::to_float(stat.best_profit_eth)
795            );
796            exit(1)
797        } else {
798            println!("[{}] PASSED: Best profit ({} ETH)", Local::now().format("%H:%M:%S.%3f"), NWETH::to_float(stat.best_profit_eth));
799        }
800    }
801
802    Ok(())
803}