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 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 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 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 #[arg(short, long, default_value = "10")]
125 timeout: u64,
126
127 #[arg(short, long, default_value = "1")]
129 wait_init: u64,
130
131 #[arg(short = 'f', long, default_value = "0")]
133 flashbots_wait: u64,
134
135 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 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 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 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
212async 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 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 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 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 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 info!("Creating blockchain and initial state");
267 let blockchain = Blockchain::new_with_config(1, false); let blockchain_state = BlockchainState::<KabuDB, EthPrimitives>::new_with_market_state(market_state_instance);
273
274 let market_instance = blockchain.market();
276
277 {
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 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 let config = KabuConfig {
314 remote_node: None, signers: Vec::new(),
316 builders: Vec::new(),
317 multicaller_address,
318 influxdb: None,
319 webserver: None,
320 database: None, };
322
323 let backrun_config = BackrunConfig::new_dumb();
325
326 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 info!("Test runner using no database pool");
342
343 let mev_channels = MevComponentChannels::<KabuDB>::default();
345
346 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 let evm_config = EthEvmConfig::mainnet();
354
355 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); let mut context =
367 KabuContext::new(reth_provider.clone(), client.clone(), evm_config, blockchain.clone(), blockchain_state.clone(), node_config);
368
369 context.channels = mev_channels.clone();
371
372 let task_manager = TaskManager::new(tokio::runtime::Handle::current());
374 let task_executor = task_manager.executor();
375
376 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 let signer = signers.write().await.add_privkey(alloy_primitives::Bytes::from(priv_key));
384
385 let nonce = client.get_transaction_count(signer.address()).await?;
387 let balance = client.get_balance(signer.address()).await?;
388
389 account_state.write().await.add_account(signer.address()).set_nonce(nonce).set_balance(Address::default(), balance); 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 let _handle = NodeBuilder::new()
403 .with_context(context)
404 .with_components(KabuEthereumNode::components().build())
405 .launch(task_executor.clone())
406 .await?;
407
408 println!("[{}] Waiting for component initialization...", Local::now().format("%H:%M:%S.%3f"));
410 tokio::time::sleep(Duration::from_millis(500)).await;
411
412 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 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 match UniswapV2Pool::fetch_pool_data(client.clone(), pool_config.address).await {
427 Ok(pool) => {
428 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 match UniswapV3Pool::fetch_pool_data(client.clone(), pool_config.address).await {
463 Ok(pool) => {
464 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 let market_events_channel = mev_channels.market_events.clone();
508 let mempool_events_channel = mev_channels.mempool_events.clone();
509 let mempool_instance = blockchain.mempool();
511
512 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 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 let bundle_state = BundleState::default();
535
536 let execution_outcome = reth_execution_types::ExecutionOutcome::<Receipt> {
537 bundle: bundle_state,
538 receipts: vec![vec![]], first_block: block_header.number,
540 requests: vec![], };
542
543 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 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 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 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 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); 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 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 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 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}