Skip to main content

quantus_cli/cli/
mod.rs

1use crate::{log_error, log_print, log_success, log_verbose};
2use clap::Subcommand;
3use colored::Colorize;
4
5pub mod address_format;
6pub mod batch;
7pub mod block;
8pub mod common;
9pub mod events;
10pub mod generic_call;
11pub mod high_security;
12pub mod metadata;
13pub mod multisend;
14pub mod preimage;
15pub mod recovery;
16pub mod reversible;
17pub mod runtime;
18pub mod scheduler;
19pub mod send;
20pub mod storage;
21pub mod system;
22pub mod tech_collective;
23pub mod transfers;
24pub mod treasury;
25pub mod wallet;
26pub mod wormhole;
27
28/// Main CLI commands
29#[derive(Subcommand, Debug)]
30pub enum Commands {
31	/// Wallet management commands
32	#[command(subcommand)]
33	Wallet(wallet::WalletCommands),
34
35	/// Send tokens to another account
36	Send {
37		/// The recipient's account address
38		#[arg(short, long)]
39		to: String,
40
41		/// Amount to send (e.g., "10", "10.5", "0.0001")
42		#[arg(short, long)]
43		amount: String,
44
45		/// Wallet name to send from
46		#[arg(short, long)]
47		from: String,
48
49		/// Password for the wallet (or use environment variables)
50		#[arg(short, long)]
51		password: Option<String>,
52
53		/// Read password from file (for scripting)
54		#[arg(long)]
55		password_file: Option<String>,
56
57		/// Optional tip amount to prioritize the transaction (e.g., "1", "0.5")
58		#[arg(long)]
59		tip: Option<String>,
60
61		/// Manual nonce override (use with caution - must be exact next nonce for account)
62		#[arg(long)]
63		nonce: Option<u32>,
64	},
65
66	/// Batch transfer commands and configuration
67	#[command(subcommand)]
68	Batch(batch::BatchCommands),
69
70	/// Reversible transfer commands
71	#[command(subcommand)]
72	Reversible(reversible::ReversibleCommands),
73
74	/// High-Security commands (reversible account settings)
75	#[command(subcommand)]
76	HighSecurity(high_security::HighSecurityCommands),
77
78	/// Recovery commands
79	#[command(subcommand)]
80	Recovery(recovery::RecoveryCommands),
81
82	/// Scheduler commands
83	#[command(subcommand)]
84	Scheduler(scheduler::SchedulerCommands),
85
86	/// Direct interaction with chain storage (Sudo required for set)
87	#[command(subcommand)]
88	Storage(storage::StorageCommands),
89
90	/// Tech Collective management commands
91	#[command(subcommand)]
92	TechCollective(tech_collective::TechCollectiveCommands),
93
94	/// Tech Referenda management commands (for runtime upgrade proposals)
95	#[command(subcommand)]
96	Preimage(preimage::PreimageCommands),
97
98	/// Treasury management commands
99	#[command(subcommand)]
100	Treasury(treasury::TreasuryCommands),
101
102	/// Privacy-preserving transfer queries via Subsquid indexer
103	#[command(subcommand)]
104	Transfers(transfers::TransfersCommands),
105
106	/// Runtime management commands (requires root/sudo permissions)
107	#[command(subcommand)]
108	Runtime(runtime::RuntimeCommands),
109
110	/// Generic extrinsic call - call ANY pallet function!
111	Call {
112		/// Pallet name (e.g., "Balances")
113		#[arg(long)]
114		pallet: String,
115
116		/// Call/function name (e.g., "transfer_allow_death")
117		#[arg(short, long)]
118		call: String,
119
120		/// Arguments as JSON array (e.g., '["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
121		/// "1000000000000"]')
122		#[arg(short, long)]
123		args: Option<String>,
124
125		/// Wallet name to sign with
126		#[arg(short, long)]
127		from: String,
128
129		/// Password for the wallet
130		#[arg(short, long)]
131		password: Option<String>,
132
133		/// Read password from file
134		#[arg(long)]
135		password_file: Option<String>,
136
137		/// Optional tip amount to prioritize the transaction
138		#[arg(long)]
139		tip: Option<String>,
140
141		/// Create offline extrinsic without submitting
142		#[arg(long)]
143		offline: bool,
144
145		/// Output the call as hex-encoded data only
146		#[arg(long)]
147		call_data_only: bool,
148	},
149
150	/// Query account balance
151	Balance {
152		/// Account address to query (SS58 format)
153		#[arg(short, long)]
154		address: String,
155	},
156
157	/// Developer utilities and testing tools
158	#[command(subcommand)]
159	Developer(DeveloperCommands),
160
161	/// Query events from blocks
162	Events {
163		/// Block number to query events from (full support)
164		#[arg(long)]
165		block: Option<u32>,
166
167		/// Block hash to query events from (full support)
168		#[arg(long)]
169		block_hash: Option<String>,
170
171		/// Query events from latest block
172		#[arg(long)]
173		latest: bool,
174
175		/// Query events from finalized block (full support)
176		#[arg(long)]
177		finalized: bool,
178
179		/// Filter events by pallet name (e.g., "Balances")
180		#[arg(long)]
181		pallet: Option<String>,
182
183		/// Show raw event data
184		#[arg(long)]
185		raw: bool,
186
187		/// Disable event decoding (decoding is enabled by default)
188		#[arg(long)]
189		no_decode: bool,
190	},
191
192	/// Query system information
193	System {
194		/// Show runtime version information
195		#[arg(long)]
196		runtime: bool,
197
198		/// Show metadata statistics
199		#[arg(long)]
200		metadata: bool,
201
202		/// Show available JSON-RPC methods exposed by the node
203		#[arg(long)]
204		rpc_methods: bool,
205	},
206
207	/// Explore chain metadata and available pallets/calls
208	Metadata {
209		/// Skip displaying documentation for calls
210		#[arg(long)]
211		no_docs: bool,
212
213		/// Show only metadata statistics
214		#[arg(long)]
215		stats_only: bool,
216
217		/// Filter by specific pallet name
218		#[arg(long)]
219		pallet: Option<String>,
220	},
221
222	/// Show version information
223	Version,
224
225	/// Check compatibility with the connected node
226	CompatibilityCheck,
227
228	/// Block management and analysis commands
229	#[command(subcommand)]
230	Block(block::BlockCommands),
231
232	/// Wormhole proof generation and verification
233	#[command(subcommand)]
234	Wormhole(wormhole::WormholeCommands),
235
236	/// Send random amounts to multiple addresses (total is distributed randomly)
237	Multisend {
238		/// Wallet name to send from
239		#[arg(short, long)]
240		from: String,
241
242		/// File containing addresses (JSON array: ["addr1", "addr2", ...])
243		#[arg(long, conflicts_with = "addresses")]
244		addresses_file: Option<String>,
245
246		/// Comma-separated list of recipient addresses
247		#[arg(long, value_delimiter = ',', conflicts_with = "addresses_file")]
248		addresses: Option<Vec<String>>,
249
250		/// Total amount to distribute across all recipients (e.g., "1000", "100.5")
251		#[arg(long)]
252		total: String,
253
254		/// Minimum amount per recipient (e.g., "10", "1.5")
255		#[arg(long)]
256		min: String,
257
258		/// Maximum amount per recipient (e.g., "100", "50.5")
259		#[arg(long)]
260		max: String,
261
262		/// Password for the wallet (or use environment variables)
263		#[arg(short, long)]
264		password: Option<String>,
265
266		/// Read password from file (for scripting)
267		#[arg(long)]
268		password_file: Option<String>,
269
270		/// Optional tip amount to prioritize the transaction (e.g., "1", "0.5")
271		#[arg(long)]
272		tip: Option<String>,
273
274		/// Skip confirmation prompt (for scripting)
275		#[arg(long, short = 'y')]
276		yes: bool,
277	},
278}
279
280/// Developer subcommands
281#[derive(Subcommand, Debug)]
282pub enum DeveloperCommands {
283	/// Create standard test wallets (crystal_alice, crystal_bob, crystal_charlie)
284	CreateTestWallets,
285
286	/// Build wormhole circuit binaries and copy to CLI and chain directories
287	BuildCircuits {
288		/// Path to qp-zk-circuits repository (default: ../qp-zk-circuits)
289		#[arg(long, default_value = "../qp-zk-circuits")]
290		circuits_path: String,
291
292		/// Path to chain repository (default: ../chain)
293		#[arg(long, default_value = "../chain")]
294		chain_path: String,
295
296		/// Number of leaf proofs aggregated into a single proof
297		#[arg(long)]
298		num_leaf_proofs: usize,
299
300		/// Skip copying to chain directory
301		#[arg(long)]
302		skip_chain: bool,
303	},
304}
305
306/// Execute a CLI command
307pub async fn execute_command(
308	command: Commands,
309	node_url: &str,
310	verbose: bool,
311	execution_mode: common::ExecutionMode,
312) -> crate::error::Result<()> {
313	match command {
314		Commands::Wallet(wallet_cmd) => wallet::handle_wallet_command(wallet_cmd, node_url).await,
315		Commands::Send { from, to, amount, password, password_file, tip, nonce } =>
316			send::handle_send_command(
317				from,
318				to,
319				&amount,
320				node_url,
321				password,
322				password_file,
323				tip,
324				nonce,
325				execution_mode,
326			)
327			.await,
328		Commands::Batch(batch_cmd) =>
329			batch::handle_batch_command(batch_cmd, node_url, execution_mode).await,
330		Commands::Reversible(reversible_cmd) =>
331			reversible::handle_reversible_command(reversible_cmd, node_url, execution_mode).await,
332		Commands::HighSecurity(hs_cmd) =>
333			high_security::handle_high_security_command(hs_cmd, node_url, execution_mode).await,
334		Commands::Recovery(recovery_cmd) =>
335			recovery::handle_recovery_command(recovery_cmd, node_url, execution_mode).await,
336		Commands::Scheduler(scheduler_cmd) =>
337			scheduler::handle_scheduler_command(scheduler_cmd, node_url, execution_mode).await,
338		Commands::Storage(storage_cmd) =>
339			storage::handle_storage_command(storage_cmd, node_url, execution_mode).await,
340		Commands::TechCollective(tech_collective_cmd) =>
341			tech_collective::handle_tech_collective_command(
342				tech_collective_cmd,
343				node_url,
344				execution_mode,
345			)
346			.await,
347		Commands::Preimage(preimage_cmd) =>
348			preimage::handle_preimage_command(preimage_cmd, node_url, execution_mode).await,
349		Commands::Treasury(treasury_cmd) =>
350			treasury::handle_treasury_command(treasury_cmd, node_url, execution_mode).await,
351		Commands::Transfers(transfers_cmd) =>
352			transfers::handle_transfers_command(transfers_cmd).await,
353		Commands::Runtime(runtime_cmd) =>
354			runtime::handle_runtime_command(runtime_cmd, node_url, execution_mode).await,
355		Commands::Call {
356			pallet,
357			call,
358			args,
359			from,
360			password,
361			password_file,
362			tip,
363			offline,
364			call_data_only,
365		} =>
366			handle_generic_call_command(
367				pallet,
368				call,
369				args,
370				from,
371				password,
372				password_file,
373				tip,
374				offline,
375				call_data_only,
376				node_url,
377				execution_mode,
378			)
379			.await,
380		Commands::Balance { address } => {
381			let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
382
383			// Resolve address (could be wallet name or SS58 address)
384			let resolved_address = common::resolve_address(&address)?;
385
386			let balance = send::get_balance(&quantus_client, &resolved_address).await?;
387			let formatted_balance =
388				send::format_balance_with_symbol(&quantus_client, balance).await?;
389			log_print!("๐Ÿ’ฐ Balance: {}", formatted_balance);
390			Ok(())
391		},
392		Commands::Developer(dev_cmd) => handle_developer_command(dev_cmd).await,
393		Commands::Events { block, block_hash, latest: _, finalized, pallet, raw, no_decode } =>
394			events::handle_events_command(
395				block, block_hash, finalized, pallet, raw, !no_decode, node_url,
396			)
397			.await,
398		Commands::System { runtime, metadata, rpc_methods } => {
399			if runtime || metadata || rpc_methods {
400				system::handle_system_extended_command(
401					node_url,
402					runtime,
403					metadata,
404					rpc_methods,
405					verbose,
406				)
407				.await
408			} else {
409				system::handle_system_command(node_url).await
410			}
411		},
412		Commands::Metadata { no_docs, stats_only, pallet } =>
413			metadata::handle_metadata_command(node_url, no_docs, stats_only, pallet).await,
414		Commands::Version => {
415			log_print!("CLI Version: Quantus CLI v{}", env!("CARGO_PKG_VERSION"));
416			Ok(())
417		},
418		Commands::CompatibilityCheck => handle_compatibility_check(node_url).await,
419		Commands::Block(block_cmd) => block::handle_block_command(block_cmd, node_url).await,
420		Commands::Wormhole(wormhole_cmd) =>
421			wormhole::handle_wormhole_command(wormhole_cmd, node_url).await,
422		Commands::Multisend {
423			from,
424			addresses_file,
425			addresses,
426			total,
427			min,
428			max,
429			password,
430			password_file,
431			tip,
432			yes,
433		} =>
434			multisend::handle_multisend_command(
435				from,
436				node_url,
437				addresses_file,
438				addresses,
439				total,
440				min,
441				max,
442				password,
443				password_file,
444				tip,
445				yes,
446				execution_mode,
447			)
448			.await,
449	}
450}
451
452/// Handle generic extrinsic call command
453#[allow(clippy::too_many_arguments)]
454async fn handle_generic_call_command(
455	pallet: String,
456	call: String,
457	args: Option<String>,
458	from: String,
459	password: Option<String>,
460	password_file: Option<String>,
461	tip: Option<String>,
462	offline: bool,
463	call_data_only: bool,
464	node_url: &str,
465	execution_mode: common::ExecutionMode,
466) -> crate::error::Result<()> {
467	// For now, we only support live submission (not offline or call-data-only)
468	if offline {
469		log_error!("โŒ Offline mode is not yet implemented");
470		log_print!("๐Ÿ’ก Currently only live submission is supported");
471		return Ok(());
472	}
473
474	if call_data_only {
475		log_error!("โŒ Call-data-only mode is not yet implemented");
476		log_print!("๐Ÿ’ก Currently only live submission is supported");
477		return Ok(());
478	}
479
480	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
481
482	let args_vec = if let Some(args_str) = args {
483		serde_json::from_str(&args_str).map_err(|e| {
484			crate::error::QuantusError::Generic(format!("Invalid JSON for arguments: {e}"))
485		})?
486	} else {
487		vec![]
488	};
489
490	generic_call::handle_generic_call(
491		&pallet,
492		&call,
493		args_vec,
494		&keypair,
495		tip,
496		node_url,
497		execution_mode,
498	)
499	.await
500}
501
502/// Handle developer subcommands
503pub async fn handle_developer_command(command: DeveloperCommands) -> crate::error::Result<()> {
504	match command {
505		DeveloperCommands::CreateTestWallets => {
506			use crate::wallet::WalletManager;
507
508			log_print!(
509				"๐Ÿงช {} Creating standard test wallets...",
510				"DEVELOPER".bright_magenta().bold()
511			);
512			log_print!("");
513
514			let wallet_manager = WalletManager::new()?;
515
516			// Standard test wallets with well-known names
517			let test_wallets = vec![
518				("crystal_alice", "Alice's test wallet for development"),
519				("crystal_bob", "Bob's test wallet for development"),
520				("crystal_charlie", "Charlie's test wallet for development"),
521			];
522
523			let mut created_count = 0;
524
525			for (name, description) in test_wallets {
526				log_verbose!("Creating wallet: {}", name.bright_green());
527
528				// Create wallet with a default password for testing
529				match wallet_manager.create_developer_wallet(name).await {
530					Ok(wallet_info) => {
531						log_success!("โœ… Created {}", name.bright_green());
532						log_success!("   Address: {}", wallet_info.address.bright_cyan());
533						log_success!("   Description: {}", description.dimmed());
534						created_count += 1;
535					},
536					Err(e) => {
537						log_error!("โŒ Failed to create {}: {}", name.bright_red(), e);
538					},
539				}
540			}
541
542			log_print!("");
543			log_success!("๐ŸŽ‰ Test wallet creation complete!");
544			log_success!("   Created: {} wallets", created_count.to_string().bright_green());
545			log_print!("");
546			log_print!("๐Ÿ’ก {} You can now use these wallets:", "TIP".bright_blue().bold());
547			log_print!("   quantus send --from crystal_alice --to <address> --amount 1000");
548			log_print!("   quantus send --from crystal_bob --to <address> --amount 1000");
549			log_print!("   quantus send --from crystal_charlie --to <address> --amount 1000");
550			log_print!("");
551
552			Ok(())
553		},
554		DeveloperCommands::BuildCircuits {
555			circuits_path,
556			chain_path,
557			num_leaf_proofs,
558			skip_chain,
559		} => build_wormhole_circuits(&circuits_path, &chain_path, num_leaf_proofs, skip_chain).await,
560	}
561}
562
563/// Build wormhole circuit binaries and copy them to the appropriate locations
564async fn build_wormhole_circuits(
565	circuits_path: &str,
566	chain_path: &str,
567	num_leaf_proofs: usize,
568	skip_chain: bool,
569) -> crate::error::Result<()> {
570	use std::{path::Path, process::Command};
571
572	log_print!("Building ZK circuit binaries (num_leaf_proofs={})", num_leaf_proofs);
573	log_print!("");
574
575	let circuits_dir = Path::new(circuits_path);
576	let chain_dir = Path::new(chain_path);
577
578	// Verify circuits directory exists
579	if !circuits_dir.exists() {
580		return Err(crate::error::QuantusError::Generic(format!(
581			"Circuits directory not found: {}",
582			circuits_path
583		)));
584	}
585
586	// Step 1: Build the circuit builder
587	log_print!("Step 1/4: Building circuit builder...");
588	let build_output = Command::new("cargo")
589		.args(["build", "--release", "-p", "qp-wormhole-circuit-builder"])
590		.current_dir(circuits_dir)
591		.output()
592		.map_err(|e| {
593			crate::error::QuantusError::Generic(format!("Failed to run cargo build: {}", e))
594		})?;
595
596	if !build_output.status.success() {
597		let stderr = String::from_utf8_lossy(&build_output.stderr);
598		return Err(crate::error::QuantusError::Generic(format!(
599			"Circuit builder compilation failed:\n{}",
600			stderr
601		)));
602	}
603	log_success!("   Done");
604
605	// Step 2: Run the circuit builder to generate binaries
606	log_print!("Step 2/4: Generating circuit binaries (this may take a while)...");
607	let builder_path = circuits_dir.join("target/release/qp-wormhole-circuit-builder");
608	let run_output = Command::new(&builder_path)
609		.args(["--num-leaf-proofs", &num_leaf_proofs.to_string()])
610		.current_dir(circuits_dir)
611		.output()
612		.map_err(|e| {
613			crate::error::QuantusError::Generic(format!("Failed to run circuit builder: {}", e))
614		})?;
615
616	if !run_output.status.success() {
617		let stderr = String::from_utf8_lossy(&run_output.stderr);
618		return Err(crate::error::QuantusError::Generic(format!(
619			"Circuit builder failed:\n{}",
620			stderr
621		)));
622	}
623	log_success!("   Done");
624
625	// Step 3: Copy binaries to CLI and touch aggregator to force recompile
626	log_print!("Step 3/4: Copying binaries to CLI...");
627	let source_bins = circuits_dir.join("generated-bins");
628	let cli_bins = Path::new("generated-bins");
629
630	let cli_bin_files = [
631		"common.bin",
632		"verifier.bin",
633		"prover.bin",
634		"dummy_proof.bin",
635		"aggregated_common.bin",
636		"aggregated_verifier.bin",
637		"config.json",
638	];
639
640	for file in &cli_bin_files {
641		let src = source_bins.join(file);
642		let dst = cli_bins.join(file);
643		std::fs::copy(&src, &dst).map_err(|e| {
644			crate::error::QuantusError::Generic(format!("Failed to copy {} to CLI: {}", file, e))
645		})?;
646		log_verbose!("   Copied {}", file);
647	}
648
649	// Touch aggregator lib.rs to force cargo to recompile it
650	let aggregator_lib = circuits_dir.join("wormhole/aggregator/src/lib.rs");
651	if aggregator_lib.exists() {
652		if let Ok(file) = std::fs::OpenOptions::new().write(true).open(&aggregator_lib) {
653			let _ = file.set_modified(std::time::SystemTime::now());
654		}
655	}
656	log_success!("   Done");
657
658	// Step 4: Copy binaries to chain directory (if not skipped)
659	if !skip_chain {
660		log_print!("Step 4/4: Copying binaries to chain...");
661
662		if !chain_dir.exists() {
663			log_error!("   Chain directory not found: {}", chain_path);
664			log_print!("   Use --skip-chain to skip this step");
665		} else {
666			let chain_bins = chain_dir.join("pallets/wormhole");
667
668			let chain_bin_files =
669				["aggregated_common.bin", "aggregated_verifier.bin", "config.json"];
670
671			for file in &chain_bin_files {
672				let src = source_bins.join(file);
673				let dst = chain_bins.join(file);
674				std::fs::copy(&src, &dst).map_err(|e| {
675					crate::error::QuantusError::Generic(format!(
676						"Failed to copy {} to chain: {}",
677						file, e
678					))
679				})?;
680				log_verbose!("   Copied {}", file);
681			}
682
683			// Touch pallet lib.rs to force cargo to recompile it
684			let pallet_lib = chain_bins.join("src/lib.rs");
685			if pallet_lib.exists() {
686				if let Ok(file) = std::fs::OpenOptions::new().write(true).open(&pallet_lib) {
687					let _ = file.set_modified(std::time::SystemTime::now());
688				}
689			}
690			log_success!("   Done");
691		}
692	} else {
693		log_print!("Step 4/4: Skipping chain copy (--skip-chain)");
694	}
695
696	log_print!("");
697	log_success!("Circuit build complete!");
698	log_print!("");
699	if !skip_chain {
700		log_print!("{}", "Next steps:".bright_blue().bold());
701		log_print!("  1. Rebuild chain: cd {} && cargo build --release", chain_path);
702		log_print!("  2. Restart the chain node");
703		log_print!("");
704	}
705
706	Ok(())
707}
708
709/// Handle compatibility check command
710async fn handle_compatibility_check(node_url: &str) -> crate::error::Result<()> {
711	log_print!("๐Ÿ” Compatibility Check");
712	log_print!("๐Ÿ”— Connecting to: {}", node_url.bright_cyan());
713	log_print!("");
714
715	// Connect to the node
716	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
717
718	// Get runtime version
719	let runtime_version = runtime::get_runtime_version(quantus_client.client()).await?;
720
721	// Get system info for additional details
722	let chain_info = system::get_complete_chain_info(node_url).await?;
723
724	log_print!("๐Ÿ“‹ Version Information:");
725	log_print!("   โ€ข CLI Version: {}", env!("CARGO_PKG_VERSION").bright_green());
726	log_print!(
727		"   โ€ข Runtime Spec Version: {}",
728		runtime_version.spec_version.to_string().bright_yellow()
729	);
730	log_print!(
731		"   โ€ข Runtime Impl Version: {}",
732		runtime_version.impl_version.to_string().bright_blue()
733	);
734	log_print!(
735		"   โ€ข Transaction Version: {}",
736		runtime_version.transaction_version.to_string().bright_magenta()
737	);
738
739	if let Some(name) = &chain_info.chain_name {
740		log_print!("   โ€ข Chain Name: {}", name.bright_cyan());
741	}
742
743	log_print!("");
744
745	// Check compatibility
746	let is_compatible = crate::config::is_runtime_compatible(runtime_version.spec_version);
747
748	log_print!("๐Ÿ” Compatibility Analysis:");
749	log_print!("   โ€ข Supported Runtime Versions: {:?}", crate::config::COMPATIBLE_RUNTIME_VERSIONS);
750	log_print!("   โ€ข Current Runtime Version: {}", runtime_version.spec_version);
751
752	if is_compatible {
753		log_success!("โœ… COMPATIBLE - This CLI version supports the connected node");
754		log_print!("   โ€ข All features should work correctly");
755		log_print!("   โ€ข You can safely use all CLI commands");
756	} else {
757		log_error!("โŒ INCOMPATIBLE - This CLI version may not work with the connected node");
758		log_print!("   โ€ข Some features may not work correctly");
759		log_print!("   โ€ข Consider updating the CLI or connecting to a compatible node");
760		log_print!("   โ€ข Supported versions: {:?}", crate::config::COMPATIBLE_RUNTIME_VERSIONS);
761	}
762
763	log_print!("");
764	log_print!("๐Ÿ’ก Tip: Use 'quantus version' for quick version check");
765	log_print!("๐Ÿ’ก Tip: Use 'quantus system --runtime' for detailed system info");
766
767	Ok(())
768}