quantus_cli/cli/
block.rs

1//! `quantus block` subcommand - detailed block analysis
2use crate::{
3	chain::client::QuantusClient,
4	cli::{address_format::QuantusSS58, storage},
5	error::QuantusError,
6	log_error, log_print, log_success,
7};
8use clap::Subcommand;
9use colored::Colorize;
10use qp_poseidon::PoseidonHasher;
11use sp_core::crypto::Ss58Codec;
12use std::str::FromStr;
13use subxt::events::EventDetails;
14
15/// Block management and analysis commands
16#[derive(Subcommand, Debug)]
17pub enum BlockCommands {
18	/// Detailed block analysis
19	Analyze {
20		/// Block number to analyze
21		#[arg(long)]
22		number: Option<u32>,
23
24		/// Block hash to analyze (alternative to number)
25		#[arg(long)]
26		hash: Option<String>,
27
28		/// Use latest block if no number/hash specified
29		#[arg(long)]
30		latest: bool,
31
32		/// Show storage statistics for this block
33		#[arg(long)]
34		storage: bool,
35
36		/// Show detailed extrinsic information
37		#[arg(long)]
38		extrinsics: bool,
39
40		/// Show detailed information for ALL extrinsics (not just first 3)
41		#[arg(long)]
42		extrinsics_details: bool,
43
44		/// Show events from this block
45		#[arg(long)]
46		events: bool,
47
48		/// Show all available information
49		#[arg(long)]
50		all: bool,
51	},
52
53	/// List blocks in range with summary info
54	List {
55		/// Start block number
56		#[arg(long)]
57		start: u32,
58		/// End block number
59		#[arg(long)]
60		end: u32,
61		/// Block step (default: 1)
62		#[arg(long)]
63		step: Option<u32>,
64	},
65}
66
67/// Handle block commands
68pub async fn handle_block_command(
69	command: BlockCommands,
70	node_url: &str,
71) -> crate::error::Result<()> {
72	match command {
73		BlockCommands::Analyze {
74			number,
75			hash,
76			latest,
77			storage,
78			extrinsics,
79			extrinsics_details,
80			events,
81			all,
82		} =>
83			handle_block_analyze_command(
84				number,
85				hash,
86				latest,
87				storage,
88				extrinsics,
89				extrinsics_details,
90				events,
91				all,
92				node_url,
93			)
94			.await,
95		BlockCommands::List { start, end, step } =>
96			handle_block_list_command(start, end, step, node_url).await,
97	}
98}
99
100/// Handle detailed block analysis
101async fn handle_block_analyze_command(
102	number: Option<u32>,
103	hash: Option<String>,
104	latest: bool,
105	storage: bool,
106	extrinsics: bool,
107	extrinsics_details: bool,
108	events: bool,
109	all: bool,
110	node_url: &str,
111) -> crate::error::Result<()> {
112	log_print!("🔍 Block Analysis");
113
114	let quantus_client = QuantusClient::new(node_url).await?;
115
116	// Determine which block to analyze
117	let (block_number, block_hash) = if let Some(num) = number {
118		// Convert number to hash using our storage function
119		let hash = storage::resolve_block_hash(&quantus_client, &num.to_string()).await?;
120		(num, hash)
121	} else if let Some(h) = hash {
122		// Parse hash and get block number from storage
123		let parsed_hash = storage::resolve_block_hash(&quantus_client, &h).await?;
124		// Get block number by querying System::Number at that block
125		let storage_at = quantus_client.client().storage().at(parsed_hash);
126		let number_addr = crate::chain::quantus_subxt::api::storage().system().number();
127		let block_num = storage_at.fetch_or_default(&number_addr).await.map_err(|e| {
128			QuantusError::NetworkError(format!("Failed to get block number: {e:?}"))
129		})?;
130		(block_num, parsed_hash)
131	} else if latest {
132		// Use latest block
133		let hash = quantus_client.get_latest_block().await?;
134		let storage_at = quantus_client.client().storage().at(hash);
135		let number_addr = crate::chain::quantus_subxt::api::storage().system().number();
136		let block_num = storage_at.fetch_or_default(&number_addr).await.map_err(|e| {
137			QuantusError::NetworkError(format!("Failed to get latest block number: {e:?}"))
138		})?;
139		(block_num, hash)
140	} else {
141		return Err(QuantusError::Generic("Must specify --number, --hash, or --latest".to_string()));
142	};
143
144	log_print!("📦 Block #{} - {:#x}", block_number, block_hash);
145	log_print!("");
146
147	// Get basic block information using RPC
148	use jsonrpsee::core::client::ClientT;
149	let block_data: serde_json::Value = quantus_client
150		.rpc_client()
151		.request("chain_getBlock", [format!("{block_hash:#x}")])
152		.await
153		.map_err(|e| QuantusError::NetworkError(format!("Failed to get block data: {e:?}")))?;
154
155	// Show basic block header information
156	show_block_header(&block_data)?;
157
158	// Show storage statistics if requested or --all
159	if storage || all {
160		show_storage_statistics(&quantus_client, block_hash).await?;
161	}
162
163	// Show events if requested or --all
164	if events || all {
165		show_block_events(block_number, node_url).await?;
166	}
167
168	// Show extrinsic details if requested or --all
169	if extrinsics || all {
170		show_extrinsic_details(&quantus_client, block_hash, &block_data).await?;
171	}
172
173	// Show detailed information for ALL extrinsics if requested (only when explicitly requested)
174	if extrinsics_details {
175		show_all_extrinsic_details(&quantus_client, block_hash, &block_data).await?;
176	}
177
178	log_success!("✅ Block analysis complete!");
179	log_print!("💡 Use --all to see all information, or combine --storage --events --extrinsics --extrinsics-details");
180
181	Ok(())
182}
183
184/// Show block header information
185fn show_block_header(block_data: &serde_json::Value) -> crate::error::Result<()> {
186	if let Some(block) = block_data.get("block") {
187		if let Some(header) = block.get("header") {
188			log_print!("📋 Block Header:");
189			if let Some(parent_hash) = header.get("parentHash") {
190				log_print!(
191					"   • Parent Hash: {}",
192					parent_hash.as_str().unwrap_or("unknown").bright_blue()
193				);
194			}
195			if let Some(state_root) = header.get("stateRoot") {
196				log_print!(
197					"   • State Root: {}",
198					state_root.as_str().unwrap_or("unknown").bright_green()
199				);
200			}
201			if let Some(extrinsics_root) = header.get("extrinsicsRoot") {
202				log_print!(
203					"   • Extrinsics Root: {}",
204					extrinsics_root.as_str().unwrap_or("unknown").bright_yellow()
205				);
206			}
207
208			// Try to get timestamp from header if available
209			if let Some(timestamp) = header.get("timestamp") {
210				if let Some(timestamp_num) = timestamp.as_u64() {
211					// Convert milliseconds to human readable time
212					let timestamp_secs = timestamp_num / 1000;
213					let datetime = chrono::DateTime::from_timestamp(timestamp_secs as i64, 0);
214					if let Some(dt) = datetime {
215						log_print!(
216							"   • Timestamp: {} ({})",
217							dt.format("%Y-%m-%d %H:%M:%S UTC").to_string().bright_cyan(),
218							timestamp_num.to_string().bright_yellow()
219						);
220					}
221				}
222			}
223		}
224
225		if let Some(extrinsics) = block.get("extrinsics") {
226			if let Some(extrinsics_array) = extrinsics.as_array() {
227				log_print!(
228					"   • Extrinsics Count: {}",
229					extrinsics_array.len().to_string().bright_magenta()
230				);
231
232				// Calculate approximate block size
233				let block_size = serde_json::to_string(block_data).unwrap_or_default().len();
234				log_print!("   • Approximate Size: {} bytes", block_size.to_string().bright_cyan());
235			}
236		}
237	}
238
239	log_print!("");
240	Ok(())
241}
242
243/// Show storage statistics for the block
244async fn show_storage_statistics(
245	quantus_client: &QuantusClient,
246	block_hash: subxt::utils::H256,
247) -> crate::error::Result<()> {
248	log_print!("💾 Storage Statistics:");
249
250	// Account count
251	let account_count =
252		storage::count_storage_entries(quantus_client, "System", "Account", block_hash).await?;
253	log_print!("   • Accounts: {}", account_count.to_string().bright_green());
254
255	// BlockHash count
256	let blockhash_count =
257		storage::count_storage_entries(quantus_client, "System", "BlockHash", block_hash).await?;
258	log_print!("   • Block Hashes: {}", blockhash_count.to_string().bright_blue());
259
260	// Event count
261	let storage_at = quantus_client.client().storage().at(block_hash);
262	let event_count_addr = crate::chain::quantus_subxt::api::storage().system().event_count();
263	let event_count = storage_at
264		.fetch_or_default(&event_count_addr)
265		.await
266		.map_err(|e| {
267			log_error!("Failed to get event count: {:?}", e);
268			e
269		})
270		.unwrap_or_default();
271	log_print!("   • Events: {}", event_count.to_string().bright_yellow());
272
273	// Try to get timestamp from Timestamp::Now storage
274	let timestamp_addr = crate::chain::quantus_subxt::api::storage().timestamp().now();
275	match storage_at.fetch(&timestamp_addr).await {
276		Ok(Some(timestamp)) => {
277			// Convert milliseconds to human readable time
278			let timestamp_secs = timestamp / 1000;
279			let datetime = chrono::DateTime::from_timestamp(timestamp_secs as i64, 0);
280			if let Some(dt) = datetime {
281				log_print!(
282					"   • Block Time: {} ({})",
283					dt.format("%Y-%m-%d %H:%M:%S UTC").to_string().bright_green(),
284					timestamp.to_string().bright_yellow()
285				);
286			}
287		},
288		Ok(None) => {
289			log_print!("   • Block Time: {}", "no timestamp".bright_yellow());
290		},
291		Err(_) => {
292			log_print!("   • Block Time: {}", "unknown".bright_yellow());
293		},
294	}
295
296	log_print!("");
297	Ok(())
298}
299
300/// Show events from the block
301async fn show_block_events(block_number: u32, node_url: &str) -> crate::error::Result<()> {
302	log_print!("📋 Events:");
303
304	// Use the existing events command internally
305	match crate::cli::events::handle_events_command(
306		Some(block_number),
307		None,
308		false,
309		None,
310		false,
311		true,
312		node_url,
313	)
314	.await
315	{
316		Ok(_) => {
317			// Events were already printed by the events command
318		},
319		Err(e) => log_error!("Failed to get events: {}", e),
320	}
321
322	log_print!("");
323	Ok(())
324}
325
326/// Show extrinsic details
327async fn show_extrinsic_details(
328	quantus_client: &QuantusClient,
329	block_hash: subxt::utils::H256,
330	block_data: &serde_json::Value,
331) -> crate::error::Result<()> {
332	if let Some(block) = block_data.get("block") {
333		if let Some(extrinsics) = block.get("extrinsics") {
334			if let Some(extrinsics_array) = extrinsics.as_array() {
335				log_print!("🔧 Extrinsics Details:");
336				log_print!(
337					"   • Total Count: {}",
338					extrinsics_array.len().to_string().bright_green()
339				);
340
341				// Calculate total size of all extrinsics in actual bytes
342				let mut total_size_bytes = 0;
343				let mut total_size_chars = 0;
344				for extrinsic in extrinsics_array.iter() {
345					if let Some(ext_str) = extrinsic.as_str() {
346						total_size_chars += ext_str.len();
347						// Convert hex string to actual bytes
348						if let Some(hex_part) = ext_str.strip_prefix("0x") {
349							// Remove "0x" prefix and convert hex to bytes
350							if hex_part.len() % 2 == 0 {
351								total_size_bytes += hex_part.len() / 2;
352							} else {
353								total_size_bytes += hex_part.len().div_ceil(2);
354							}
355						} else {
356							// If not hex, assume it's already in bytes
357							total_size_bytes += ext_str.len();
358						}
359					}
360				}
361				log_print!(
362					"   • Total Size: {:.1} KB ({} chars)",
363					total_size_bytes as f64 / 1024.0,
364					total_size_chars.to_string().bright_cyan()
365				);
366
367				// Pre-fetch events and group by extrinsic index
368				let events =
369					quantus_client.client().blocks().at(block_hash).await?.events().await?;
370				let mut events_by_ex_idx: std::collections::BTreeMap<usize, Vec<String>> =
371					std::collections::BTreeMap::new();
372				for ev in events.iter().flatten() {
373					if let subxt::events::Phase::ApplyExtrinsic(ex_idx) = ev.phase() {
374						let msg = format_event_details(&ev);
375						events_by_ex_idx.entry(ex_idx as usize).or_default().push(msg);
376					}
377				}
378
379				// Get extrinsics via subxt for detailed analysis
380				let block = quantus_client.client().blocks().at(block_hash).await?;
381				let extrinsics = block.extrinsics().await?;
382
383				// Get parent block hash for nonce calculation
384				let parent_hash = {
385					use jsonrpsee::core::client::ClientT;
386					let header: serde_json::Value = quantus_client
387						.rpc_client()
388						.request("chain_getHeader", [format!("{block_hash:#x}")])
389						.await
390						.map_err(|e| {
391							crate::error::QuantusError::NetworkError(format!(
392								"Failed to get header: {e:?}"
393							))
394						})?;
395					let parent_hash_str = header["parentHash"].as_str().ok_or_else(|| {
396						crate::error::QuantusError::NetworkError(
397							"Missing parentHash in header".to_string(),
398						)
399					})?;
400					subxt::utils::H256::from_str(parent_hash_str).map_err(|e| {
401						crate::error::QuantusError::NetworkError(format!(
402							"Invalid parent hash: {e:?}"
403						))
404					})?
405				};
406
407				// Track nonce per signer in this block
408				let mut signer_nonce_tracker: std::collections::HashMap<String, u32> =
409					std::collections::HashMap::new();
410
411				// Show first 3 extrinsics with details
412				for (index, extrinsic) in extrinsics_array.iter().take(3).enumerate() {
413					let ext_str = extrinsic.as_str().unwrap_or("unknown");
414					let (ext_size_bytes, preview, hash_hex) = summarize_extrinsic(ext_str);
415					log_print!(
416						"   {}. Hash: {} | Size: {} bytes | Data: {}...",
417						(index + 1).to_string().bright_yellow(),
418						hash_hex.bright_blue(),
419						ext_size_bytes.to_string().bright_cyan(),
420						preview.bright_magenta()
421					);
422
423					// Extract signer and transaction details
424					if let Some(ext_details) = extrinsics.iter().nth(index) {
425						if ext_details.is_signed() {
426							// Extract signer and tip from events
427							let mut signer_from_events: Option<String> = None;
428							let mut tip_from_events: Option<u128> = None;
429
430							if let Some(event_list) = events_by_ex_idx.get(&index) {
431								for event_line in event_list {
432									if event_line.contains("TransactionFeePaid") {
433										// Extract signer (who field)
434										if let Some(who_start) = event_line.find("who: ") {
435											if let Some(who_end) =
436												event_line[who_start + 5..].find(',')
437											{
438												let signer = &event_line
439													[who_start + 5..who_start + 5 + who_end];
440												signer_from_events = Some(signer.to_string());
441											}
442										}
443										// Extract tip
444										if let Some(tip_start) = event_line.find("tip: ") {
445											if let Some(tip_end) =
446												event_line[tip_start + 5..].find(' ')
447											{
448												let tip_str = &event_line
449													[tip_start + 5..tip_start + 5 + tip_end];
450												if let Ok(tip_val) = tip_str.parse::<u128>() {
451													tip_from_events = Some(tip_val);
452												}
453											}
454										}
455									}
456								}
457							}
458
459							if let Some(ref signer) = signer_from_events {
460								log_print!("       • Signer: {}", signer);
461
462								// Calculate nonce from parent block + track per signer
463								if !signer_nonce_tracker.contains_key(signer) {
464									let nonce = get_account_nonce_at_block(
465										quantus_client,
466										signer,
467										parent_hash,
468									)
469									.await
470									.unwrap_or(0);
471									signer_nonce_tracker.insert(signer.clone(), nonce);
472								}
473
474								// Get current nonce for this signer (increments for each tx from
475								// same signer)
476								let current_nonce = *signer_nonce_tracker.get(signer).unwrap();
477								log_print!("       • Nonce: {}", current_nonce);
478
479								// Increment for next transaction from this signer
480								signer_nonce_tracker.insert(signer.clone(), current_nonce + 1);
481							}
482
483							if let Some(tip) = tip_from_events {
484								// Format tip nicely
485								let tip_hei = tip as f64 / 1_000_000_000_000.0;
486								log_print!("       • Tip: {:.6} HEI", tip_hei);
487							}
488						} else {
489							log_print!("       • Unsigned extrinsic");
490						}
491					}
492
493					// Related events (if any)
494					if let Some(list) = events_by_ex_idx.get(&index) {
495						for line in list {
496							log_print!("       ↪ {}", line);
497						}
498					}
499				}
500
501				if extrinsics_array.len() > 3 {
502					log_print!(
503						"   ... and {} more extrinsics",
504						(extrinsics_array.len() - 3).to_string().bright_blue()
505					);
506				}
507			}
508		}
509	}
510	log_print!("");
511	Ok(())
512}
513
514/// Show detailed information for ALL extrinsics
515async fn show_all_extrinsic_details(
516	quantus_client: &QuantusClient,
517	block_hash: subxt::utils::H256,
518	block_data: &serde_json::Value,
519) -> crate::error::Result<()> {
520	if let Some(block) = block_data.get("block") {
521		if let Some(extrinsics) = block.get("extrinsics") {
522			if let Some(extrinsics_array) = extrinsics.as_array() {
523				log_print!("🔧 ALL Extrinsics Details:");
524				log_print!(
525					"   • Total Count: {}",
526					extrinsics_array.len().to_string().bright_green()
527				);
528
529				// Calculate total size of all extrinsics in actual bytes
530				let mut total_size_bytes = 0;
531				let mut total_size_chars = 0;
532				for extrinsic in extrinsics_array.iter() {
533					if let Some(ext_str) = extrinsic.as_str() {
534						total_size_chars += ext_str.len();
535						// Convert hex string to actual bytes
536						if let Some(hex_part) = ext_str.strip_prefix("0x") {
537							// Remove "0x" prefix and convert hex to bytes
538							if hex_part.len() % 2 == 0 {
539								total_size_bytes += hex_part.len() / 2;
540							} else {
541								total_size_bytes += hex_part.len().div_ceil(2);
542							}
543						} else {
544							// If not hex, assume it's already in bytes
545							total_size_bytes += ext_str.len();
546						}
547					}
548				}
549				log_print!(
550					"   • Total Size: {:.1} KB ({} chars)",
551					total_size_bytes as f64 / 1024.0,
552					total_size_chars.to_string().bright_cyan()
553				);
554
555				// Pre-fetch events and group by extrinsic index
556				let events =
557					quantus_client.client().blocks().at(block_hash).await?.events().await?;
558				let mut events_by_ex_idx: std::collections::BTreeMap<usize, Vec<String>> =
559					std::collections::BTreeMap::new();
560				for ev in events.iter().flatten() {
561					if let subxt::events::Phase::ApplyExtrinsic(ex_idx) = ev.phase() {
562						let msg = format_event_details(&ev);
563						events_by_ex_idx.entry(ex_idx as usize).or_default().push(msg);
564					}
565				}
566
567				// Get extrinsics via subxt for detailed analysis
568				let block = quantus_client.client().blocks().at(block_hash).await?;
569				let extrinsics = block.extrinsics().await?;
570
571				// Get parent block hash for nonce calculation
572				let parent_hash = {
573					use jsonrpsee::core::client::ClientT;
574					let header: serde_json::Value = quantus_client
575						.rpc_client()
576						.request("chain_getHeader", [format!("{block_hash:#x}")])
577						.await
578						.map_err(|e| {
579							crate::error::QuantusError::NetworkError(format!(
580								"Failed to get header: {e:?}"
581							))
582						})?;
583					let parent_hash_str = header["parentHash"].as_str().ok_or_else(|| {
584						crate::error::QuantusError::NetworkError(
585							"Missing parentHash in header".to_string(),
586						)
587					})?;
588					subxt::utils::H256::from_str(parent_hash_str).map_err(|e| {
589						crate::error::QuantusError::NetworkError(format!(
590							"Invalid parent hash: {e:?}"
591						))
592					})?
593				};
594
595				// Track nonce per signer in this block
596				let mut signer_nonce_tracker: std::collections::HashMap<String, u32> =
597					std::collections::HashMap::new();
598
599				// Show all extrinsics with details
600				for (index, extrinsic) in extrinsics_array.iter().enumerate() {
601					let ext_str = extrinsic.as_str().unwrap_or("unknown");
602					let (ext_size_bytes, preview, hash_hex) = summarize_extrinsic(ext_str);
603					log_print!(
604						"   {}. Hash: {} | Size: {} bytes | Data: {}...",
605						(index + 1).to_string().bright_yellow(),
606						hash_hex.bright_blue(),
607						ext_size_bytes.to_string().bright_cyan(),
608						preview.bright_magenta()
609					);
610
611					// Extract signer and transaction details
612					if let Some(ext_details) = extrinsics.iter().nth(index) {
613						if ext_details.is_signed() {
614							// Extract signer and tip from events
615							let mut signer_from_events: Option<String> = None;
616							let mut tip_from_events: Option<u128> = None;
617
618							if let Some(event_list) = events_by_ex_idx.get(&index) {
619								for event_line in event_list {
620									if event_line.contains("TransactionFeePaid") {
621										// Extract signer (who field)
622										if let Some(who_start) = event_line.find("who: ") {
623											if let Some(who_end) =
624												event_line[who_start + 5..].find(',')
625											{
626												let signer = &event_line
627													[who_start + 5..who_start + 5 + who_end];
628												signer_from_events = Some(signer.to_string());
629											}
630										}
631										// Extract tip
632										if let Some(tip_start) = event_line.find("tip: ") {
633											if let Some(tip_end) =
634												event_line[tip_start + 5..].find(' ')
635											{
636												let tip_str = &event_line
637													[tip_start + 5..tip_start + 5 + tip_end];
638												if let Ok(tip_val) = tip_str.parse::<u128>() {
639													tip_from_events = Some(tip_val);
640												}
641											}
642										}
643									}
644								}
645							}
646
647							if let Some(ref signer) = signer_from_events {
648								log_print!("       • Signer: {}", signer);
649
650								// Calculate nonce from parent block + track per signer
651								if !signer_nonce_tracker.contains_key(signer) {
652									let nonce = get_account_nonce_at_block(
653										quantus_client,
654										signer,
655										parent_hash,
656									)
657									.await
658									.unwrap_or(0);
659									signer_nonce_tracker.insert(signer.clone(), nonce);
660								}
661
662								// Get current nonce for this signer (increments for each tx from
663								// same signer)
664								let current_nonce = *signer_nonce_tracker.get(signer).unwrap();
665								log_print!("       • Nonce: {}", current_nonce);
666
667								// Increment for next transaction from this signer
668								signer_nonce_tracker.insert(signer.clone(), current_nonce + 1);
669							}
670
671							if let Some(tip) = tip_from_events {
672								// Format tip nicely
673								let tip_hei = tip as f64 / 1_000_000_000_000.0;
674								log_print!("       • Tip: {:.6} HEI", tip_hei);
675							}
676						} else {
677							log_print!("       • Unsigned extrinsic");
678						}
679					}
680
681					// Related events (if any)
682					if let Some(list) = events_by_ex_idx.get(&index) {
683						for line in list {
684							log_print!("       ↪ {}", line);
685						}
686					}
687				}
688			}
689		}
690	}
691	log_print!("");
692	Ok(())
693}
694
695/// Compute summary info for an extrinsic hex string: (size_bytes, preview, hash_hex)
696fn summarize_extrinsic(ext_hex: &str) -> (usize, String, String) {
697	let ext_str = ext_hex;
698	let (bytes, size_bytes) = if let Some(hex_part) = ext_str.strip_prefix("0x") {
699		if let Ok(decoded) = hex::decode(hex_part) {
700			let size = decoded.len();
701			(decoded, size)
702		} else {
703			(ext_str.as_bytes().to_vec(), ext_str.len())
704		}
705	} else {
706		(ext_str.as_bytes().to_vec(), ext_str.len())
707	};
708
709	// Compute extrinsic hash using Poseidon (chain hasher)
710	let h = <PoseidonHasher as sp_runtime::traits::Hash>::hash(&bytes);
711	let hash_hex = format!("{h:#x}");
712
713	let preview = if ext_str.len() > 20 { ext_str[..20].to_string() } else { ext_str.to_string() };
714	(size_bytes, preview, hash_hex)
715}
716
717/// Format event details with typed decoding and nicer AccountId formatting
718fn format_event_details<T: subxt::Config>(event: &EventDetails<T>) -> String {
719	if let Ok(typed) = event.as_root_event::<crate::chain::quantus_subxt::api::Event>() {
720		let formatted = format_event_with_ss58_addresses(&typed);
721		return format!("📝 {}.{} {}", event.pallet_name(), event.variant_name(), formatted);
722	}
723	format!("📝 {}.{}", event.pallet_name(), event.variant_name())
724}
725
726fn format_event_with_ss58_addresses(event: &crate::chain::quantus_subxt::api::Event) -> String {
727	let debug_str = format!("{event:?}");
728	let mut result = debug_str.clone();
729	let mut attempts = 0;
730	while let Some(account_id) = extract_account_id_from_debug(&result) {
731		let ss58_address = format_account_id(&account_id);
732		let account_debug = format!("{account_id:?}");
733		result = result.replace(&account_debug, &ss58_address);
734		attempts += 1;
735		if attempts > 10 {
736			break;
737		}
738	}
739	result
740}
741
742fn extract_account_id_from_debug(debug_str: &str) -> Option<subxt::utils::AccountId32> {
743	if let Some(start) = debug_str.find("AccountId32([") {
744		// "
745		if let Some(end) = debug_str[start..].find("])") {
746			let bytes_str = &debug_str[start + 13..start + end];
747			let bytes: Vec<u8> = bytes_str
748				.split(',')
749				.map(|s| s.trim().parse::<u8>().ok())
750				.collect::<Option<Vec<u8>>>()?;
751			if bytes.len() == 32 {
752				let mut account_bytes = [0u8; 32];
753				account_bytes.copy_from_slice(&bytes);
754				return Some(subxt::utils::AccountId32::from(account_bytes));
755			}
756		}
757	}
758	None
759}
760
761fn format_account_id(account_id: &subxt::utils::AccountId32) -> String {
762	account_id.to_quantus_ss58()
763}
764
765/// Get account nonce at specific block
766async fn get_account_nonce_at_block(
767	quantus_client: &QuantusClient,
768	account_address: &str,
769	block_hash: subxt::utils::H256,
770) -> crate::error::Result<u32> {
771	// Parse the SS58 address to AccountId32 (sp-core)
772	let account_id_sp = sp_core::crypto::AccountId32::from_ss58check(account_address)
773		.map_err(|e| QuantusError::NetworkError(format!("Invalid SS58 address: {e:?}")))?;
774
775	// Convert to subxt_core AccountId32 for storage query
776	let account_bytes: [u8; 32] = *account_id_sp.as_ref();
777	let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_bytes);
778
779	// Use SubXT to query System::Account storage at specific block
780	use crate::chain::quantus_subxt::api;
781	let storage_addr = api::storage().system().account(account_id);
782	let storage_at = quantus_client.client().storage().at(block_hash);
783
784	let account_info = storage_at
785		.fetch_or_default(&storage_addr)
786		.await
787		.map_err(|e| QuantusError::NetworkError(format!("Failed to fetch account info: {e:?}")))?;
788
789	Ok(account_info.nonce)
790}
791
792/// Handle block list command
793pub async fn handle_block_list_command(
794	start: u32,
795	end: u32,
796	step: Option<u32>,
797	node_url: &str,
798) -> crate::error::Result<()> {
799	log_print!(
800		"📦 Listing blocks from {} to {}",
801		start.to_string().bright_green(),
802		end.to_string().bright_green()
803	);
804
805	let step = step.unwrap_or(1);
806	if step > 1 {
807		log_print!("📏 Step: {}", step.to_string().bright_cyan());
808	}
809
810	let quantus_client = QuantusClient::new(node_url).await?;
811	list_blocks_in_range(&quantus_client, start, end, step).await
812}
813
814/// List blocks in range with summary information
815async fn list_blocks_in_range(
816	quantus_client: &QuantusClient,
817	start: u32,
818	end: u32,
819	step: u32,
820) -> crate::error::Result<()> {
821	use jsonrpsee::core::client::ClientT;
822
823	log_print!("🔍 Fetching block information...");
824
825	let mut block_count = 0;
826	let mut total_extrinsics = 0;
827	let mut total_events = 0;
828	let mut total_size = 0;
829	let mut tps_values = Vec::new();
830	let mut previous_timestamp: Option<u64> = None;
831
832	// Progress indicator
833	log_print!("📊 Processing {} blocks...", ((end - start) / step + 1).to_string().bright_cyan());
834
835	// Print table header
836	log_print!("");
837	log_print!(
838		"{:<20} {:<20} {:<12} {:<10} {:<8} {:<8}",
839		"Block".bright_green().bold(),
840		"Time".bright_cyan().bold(),
841		"Extrinsics".bright_blue().bold(),
842		"Events".bright_yellow().bold(),
843		"Size".bright_magenta().bold(),
844		"TPS".bright_red().bold()
845	);
846	log_print!(
847		"{:<20} {:<20} {:<12} {:<10} {:<8} {:<8}",
848		"────────────────────".bright_green(),
849		"────────────────────".bright_cyan(),
850		"────────────".bright_blue(),
851		"──────────".bright_yellow(),
852		"──────".bright_magenta(),
853		"──────".bright_red()
854	);
855
856	for block_num in (start..=end).step_by(step as usize) {
857		// Get block hash for this block number
858		let block_hash: subxt::utils::H256 = quantus_client
859			.rpc_client()
860			.request::<subxt::utils::H256, [u32; 1]>("chain_getBlockHash", [block_num])
861			.await
862			.map_err(|e| {
863				QuantusError::NetworkError(format!(
864					"Failed to get block hash for block {block_num}: {e:?}"
865				))
866			})?;
867
868		// Get block data
869		let block_data: serde_json::Value = quantus_client
870			.rpc_client()
871			.request::<serde_json::Value, [String; 1]>(
872				"chain_getBlock",
873				[format!("{block_hash:#x}")],
874			)
875			.await
876			.map_err(|e| {
877				QuantusError::NetworkError(format!(
878					"Failed to get block data for block {block_num}: {e:?}"
879				))
880			})?;
881
882		// Extract basic info
883		let extrinsics_count = if let Some(block) = block_data.get("block") {
884			if let Some(extrinsics) = block.get("extrinsics") {
885				if let Some(extrinsics_array) = extrinsics.as_array() {
886					extrinsics_array.len()
887				} else {
888					0
889				}
890			} else {
891				0
892			}
893		} else {
894			0
895		};
896
897		// Get timestamp from storage
898		let storage_at = quantus_client.client().storage().at(block_hash);
899		let timestamp_addr = crate::chain::quantus_subxt::api::storage().timestamp().now();
900		let timestamp = storage_at.fetch(&timestamp_addr).await.ok().flatten();
901
902		// Get event count
903		let event_count_addr = crate::chain::quantus_subxt::api::storage().system().event_count();
904		let event_count = storage_at.fetch(&event_count_addr).await.ok().flatten().unwrap_or(0);
905
906		// Calculate block size in KB based on actual data
907		let block_size_bytes = if let Some(block) = block_data.get("block") {
908			if let Some(extrinsics) = block.get("extrinsics") {
909				if let Some(extrinsics_array) = extrinsics.as_array() {
910					// Calculate size from actual extrinsic data
911					let mut total_bytes = 0;
912					for extrinsic in extrinsics_array.iter() {
913						if let Some(ext_str) = extrinsic.as_str() {
914							// Remove "0x" prefix and convert hex to bytes
915							if let Some(hex_part) = ext_str.strip_prefix("0x") {
916								if hex_part.len() % 2 == 0 {
917									total_bytes += hex_part.len() / 2;
918								} else {
919									total_bytes += hex_part.len().div_ceil(2);
920								}
921							} else {
922								total_bytes += ext_str.len();
923							}
924						}
925					}
926					// Add some overhead for block header (approximate)
927					total_bytes + 1024 // ~1KB for header
928				} else {
929					0
930				}
931			} else {
932				0
933			}
934		} else {
935			0
936		};
937		let block_size_kb = block_size_bytes as f64 / 1024.0;
938
939		// Update totals
940		block_count += 1;
941		total_extrinsics += extrinsics_count;
942		total_events += event_count;
943		total_size += block_size_bytes;
944
945		// Calculate TPS (Transactions Per Second)
946		let tps_str = match (timestamp, previous_timestamp) {
947			(Some(ts), Some(prev_ts)) => {
948				let time_diff_ms = ts.saturating_sub(prev_ts);
949				if time_diff_ms > 0 {
950					let time_diff_secs = time_diff_ms as f64 / 1000.0;
951					let tps = extrinsics_count as f64 / time_diff_secs;
952					tps_values.push(tps);
953					format!("{tps:.1}")
954				} else {
955					"N/A".to_string()
956				}
957			},
958			_ => "N/A".to_string(),
959		};
960
961		// Display block info - always show full date
962		let time_str = if let Some(ts) = timestamp {
963			let timestamp_secs = ts / 1000;
964			let datetime = chrono::DateTime::from_timestamp(timestamp_secs as i64, 0);
965			if let Some(dt) = datetime {
966				dt.format("%Y-%m-%d %H:%M:%S").to_string()
967			} else {
968				"unknown".to_string()
969			}
970		} else {
971			"unknown".to_string()
972		};
973
974		log_print!(
975			"📦 {:<18} {:<20} {:<12} {:<10} {:<8} {:<8}",
976			format!("#{block_num}").bright_green(),
977			time_str.bright_cyan(),
978			extrinsics_count.to_string().bright_blue(),
979			event_count.to_string().bright_yellow(),
980			format!("{block_size_kb:.1}K").bright_magenta(),
981			tps_str.bright_red()
982		);
983
984		// Update previous timestamp for next iteration
985		previous_timestamp = timestamp.or(previous_timestamp);
986	}
987
988	// Summary
989	log_print!("");
990	log_print!("📊 Summary:");
991	log_print!("   • Blocks processed: {}", block_count.to_string().bright_green());
992	log_print!("   • Total extrinsics: {}", total_extrinsics.to_string().bright_blue());
993	log_print!("   • Total events: {}", total_events.to_string().bright_yellow());
994	log_print!(
995		"   • Total size: {} KB",
996		format!("{:.1}", total_size as f64 / 1024.0).bright_magenta()
997	);
998	log_print!(
999		"   • Average extrinsics per block: {}",
1000		format!("{:.1}", total_extrinsics as f64 / block_count as f64).bright_cyan()
1001	);
1002	log_print!(
1003		"   • Average events per block: {}",
1004		format!("{:.1}", total_events as f64 / block_count as f64).bright_cyan()
1005	);
1006
1007	// TPS Statistics
1008	if !tps_values.is_empty() {
1009		let max_tps = tps_values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
1010		let min_tps = tps_values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
1011		let avg_tps = tps_values.iter().sum::<f64>() / tps_values.len() as f64;
1012
1013		log_print!("");
1014		log_print!("🚀 TPS Statistics:");
1015		log_print!("   • MAX TPS: {}", format!("{max_tps:.1}").bright_green().bold());
1016		log_print!("   • MIN TPS: {}", format!("{min_tps:.1}").bright_yellow().bold());
1017		log_print!("   • AVG TPS: {}", format!("{avg_tps:.1}").bright_cyan().bold());
1018		log_print!("   • TPS samples: {}", tps_values.len().to_string().bright_magenta());
1019	} else {
1020		log_print!("");
1021		log_print!(
1022			"🚀 TPS Statistics: No TPS data available (need at least 2 blocks with timestamps)"
1023		);
1024	}
1025
1026	Ok(())
1027}