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 preimage;
14pub mod recovery;
15pub mod referenda;
16pub mod referenda_decode;
17pub mod reversible;
18pub mod runtime;
19pub mod scheduler;
20pub mod send;
21pub mod storage;
22pub mod system;
23pub mod tech_collective;
24pub mod tech_referenda;
25pub mod treasury;
26pub mod wallet;
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	#[command(subcommand)]
98	TechReferenda(tech_referenda::TechReferendaCommands),
99
100	/// Standard Referenda management commands (public governance)
101	#[command(subcommand)]
102	Referenda(referenda::ReferendaCommands),
103
104	/// Treasury management commands
105	#[command(subcommand)]
106	Treasury(treasury::TreasuryCommands),
107
108	/// Runtime management commands (requires root/sudo permissions)
109	#[command(subcommand)]
110	Runtime(runtime::RuntimeCommands),
111
112	/// Generic extrinsic call - call ANY pallet function!
113	Call {
114		/// Pallet name (e.g., "Balances")
115		#[arg(long)]
116		pallet: String,
117
118		/// Call/function name (e.g., "transfer_allow_death")
119		#[arg(short, long)]
120		call: String,
121
122		/// Arguments as JSON array (e.g., '["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
123		/// "1000000000000"]')
124		#[arg(short, long)]
125		args: Option<String>,
126
127		/// Wallet name to sign with
128		#[arg(short, long)]
129		from: String,
130
131		/// Password for the wallet
132		#[arg(short, long)]
133		password: Option<String>,
134
135		/// Read password from file
136		#[arg(long)]
137		password_file: Option<String>,
138
139		/// Optional tip amount to prioritize the transaction
140		#[arg(long)]
141		tip: Option<String>,
142
143		/// Create offline extrinsic without submitting
144		#[arg(long)]
145		offline: bool,
146
147		/// Output the call as hex-encoded data only
148		#[arg(long)]
149		call_data_only: bool,
150	},
151
152	/// Query account balance
153	Balance {
154		/// Account address to query (SS58 format)
155		#[arg(short, long)]
156		address: String,
157	},
158
159	/// Developer utilities and testing tools
160	#[command(subcommand)]
161	Developer(DeveloperCommands),
162
163	/// Query events from blocks
164	Events {
165		/// Block number to query events from (full support)
166		#[arg(long)]
167		block: Option<u32>,
168
169		/// Block hash to query events from (full support)
170		#[arg(long)]
171		block_hash: Option<String>,
172
173		/// Query events from latest block
174		#[arg(long)]
175		latest: bool,
176
177		/// Query events from finalized block (full support)
178		#[arg(long)]
179		finalized: bool,
180
181		/// Filter events by pallet name (e.g., "Balances")
182		#[arg(long)]
183		pallet: Option<String>,
184
185		/// Show raw event data
186		#[arg(long)]
187		raw: bool,
188
189		/// Disable event decoding (decoding is enabled by default)
190		#[arg(long)]
191		no_decode: bool,
192	},
193
194	/// Query system information
195	System {
196		/// Show runtime version information
197		#[arg(long)]
198		runtime: bool,
199
200		/// Show metadata statistics
201		#[arg(long)]
202		metadata: bool,
203
204		/// Show available JSON-RPC methods exposed by the node
205		#[arg(long)]
206		rpc_methods: bool,
207	},
208
209	/// Explore chain metadata and available pallets/calls
210	Metadata {
211		/// Skip displaying documentation for calls
212		#[arg(long)]
213		no_docs: bool,
214
215		/// Show only metadata statistics
216		#[arg(long)]
217		stats_only: bool,
218
219		/// Filter by specific pallet name
220		#[arg(long)]
221		pallet: Option<String>,
222	},
223
224	/// Show version information
225	Version,
226
227	/// Check compatibility with the connected node
228	CompatibilityCheck,
229
230	/// Block management and analysis commands
231	#[command(subcommand)]
232	Block(block::BlockCommands),
233}
234
235/// Developer subcommands
236#[derive(Subcommand, Debug)]
237pub enum DeveloperCommands {
238	/// Create standard test wallets (crystal_alice, crystal_bob, crystal_charlie)
239	CreateTestWallets,
240}
241
242/// Execute a CLI command
243pub async fn execute_command(
244	command: Commands,
245	node_url: &str,
246	verbose: bool,
247	finalized: bool,
248) -> crate::error::Result<()> {
249	match command {
250		Commands::Wallet(wallet_cmd) => wallet::handle_wallet_command(wallet_cmd, node_url).await,
251		Commands::Send { from, to, amount, password, password_file, tip, nonce } =>
252			send::handle_send_command(
253				from,
254				to,
255				&amount,
256				node_url,
257				password,
258				password_file,
259				tip,
260				nonce,
261				finalized,
262			)
263			.await,
264		Commands::Batch(batch_cmd) =>
265			batch::handle_batch_command(batch_cmd, node_url, finalized).await,
266		Commands::Reversible(reversible_cmd) =>
267			reversible::handle_reversible_command(reversible_cmd, node_url, finalized).await,
268		Commands::HighSecurity(hs_cmd) =>
269			high_security::handle_high_security_command(hs_cmd, node_url, finalized).await,
270		Commands::Recovery(recovery_cmd) =>
271			recovery::handle_recovery_command(recovery_cmd, node_url, finalized).await,
272		Commands::Scheduler(scheduler_cmd) =>
273			scheduler::handle_scheduler_command(scheduler_cmd, node_url, finalized).await,
274		Commands::Storage(storage_cmd) =>
275			storage::handle_storage_command(storage_cmd, node_url, finalized).await,
276		Commands::TechCollective(tech_collective_cmd) =>
277			tech_collective::handle_tech_collective_command(
278				tech_collective_cmd,
279				node_url,
280				finalized,
281			)
282			.await,
283		Commands::Preimage(preimage_cmd) =>
284			preimage::handle_preimage_command(preimage_cmd, node_url, finalized).await,
285		Commands::TechReferenda(tech_referenda_cmd) =>
286			tech_referenda::handle_tech_referenda_command(tech_referenda_cmd, node_url, finalized)
287				.await,
288		Commands::Referenda(referenda_cmd) =>
289			referenda::handle_referenda_command(referenda_cmd, node_url, finalized).await,
290		Commands::Treasury(treasury_cmd) =>
291			treasury::handle_treasury_command(treasury_cmd, node_url, finalized).await,
292		Commands::Runtime(runtime_cmd) =>
293			runtime::handle_runtime_command(runtime_cmd, node_url, finalized).await,
294		Commands::Call {
295			pallet,
296			call,
297			args,
298			from,
299			password,
300			password_file,
301			tip,
302			offline,
303			call_data_only,
304		} =>
305			handle_generic_call_command(
306				pallet,
307				call,
308				args,
309				from,
310				password,
311				password_file,
312				tip,
313				offline,
314				call_data_only,
315				node_url,
316				finalized,
317			)
318			.await,
319		Commands::Balance { address } => {
320			let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
321
322			// Resolve address (could be wallet name or SS58 address)
323			let resolved_address = common::resolve_address(&address)?;
324
325			let balance = send::get_balance(&quantus_client, &resolved_address).await?;
326			let formatted_balance =
327				send::format_balance_with_symbol(&quantus_client, balance).await?;
328			log_print!("๐Ÿ’ฐ Balance: {}", formatted_balance);
329			Ok(())
330		},
331		Commands::Developer(dev_cmd) => match dev_cmd {
332			DeveloperCommands::CreateTestWallets => {
333				let _ = crate::cli::handle_developer_command(DeveloperCommands::CreateTestWallets)
334					.await;
335				Ok(())
336			},
337		},
338		Commands::Events { block, block_hash, latest: _, finalized, pallet, raw, no_decode } =>
339			events::handle_events_command(
340				block, block_hash, finalized, pallet, raw, !no_decode, node_url,
341			)
342			.await,
343		Commands::System { runtime, metadata, rpc_methods } => {
344			if runtime || metadata || rpc_methods {
345				system::handle_system_extended_command(
346					node_url,
347					runtime,
348					metadata,
349					rpc_methods,
350					verbose,
351				)
352				.await
353			} else {
354				system::handle_system_command(node_url).await
355			}
356		},
357		Commands::Metadata { no_docs, stats_only, pallet } =>
358			metadata::handle_metadata_command(node_url, no_docs, stats_only, pallet).await,
359		Commands::Version => {
360			log_print!("CLI Version: Quantus CLI v{}", env!("CARGO_PKG_VERSION"));
361			Ok(())
362		},
363		Commands::CompatibilityCheck => handle_compatibility_check(node_url).await,
364		Commands::Block(block_cmd) => block::handle_block_command(block_cmd, node_url).await,
365	}
366}
367
368/// Handle generic extrinsic call command
369#[allow(clippy::too_many_arguments)]
370async fn handle_generic_call_command(
371	pallet: String,
372	call: String,
373	args: Option<String>,
374	from: String,
375	password: Option<String>,
376	password_file: Option<String>,
377	tip: Option<String>,
378	offline: bool,
379	call_data_only: bool,
380	node_url: &str,
381	finalized: bool,
382) -> crate::error::Result<()> {
383	// For now, we only support live submission (not offline or call-data-only)
384	if offline {
385		log_error!("โŒ Offline mode is not yet implemented");
386		log_print!("๐Ÿ’ก Currently only live submission is supported");
387		return Ok(());
388	}
389
390	if call_data_only {
391		log_error!("โŒ Call-data-only mode is not yet implemented");
392		log_print!("๐Ÿ’ก Currently only live submission is supported");
393		return Ok(());
394	}
395
396	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
397
398	let args_vec = if let Some(args_str) = args {
399		serde_json::from_str(&args_str).map_err(|e| {
400			crate::error::QuantusError::Generic(format!("Invalid JSON for arguments: {e}"))
401		})?
402	} else {
403		vec![]
404	};
405
406	generic_call::handle_generic_call(&pallet, &call, args_vec, &keypair, tip, node_url, finalized)
407		.await
408}
409
410/// Handle developer subcommands
411pub async fn handle_developer_command(command: DeveloperCommands) -> crate::error::Result<()> {
412	match command {
413		DeveloperCommands::CreateTestWallets => {
414			use crate::wallet::WalletManager;
415
416			log_print!(
417				"๐Ÿงช {} Creating standard test wallets...",
418				"DEVELOPER".bright_magenta().bold()
419			);
420			log_print!("");
421
422			let wallet_manager = WalletManager::new()?;
423
424			// Standard test wallets with well-known names
425			let test_wallets = vec![
426				("crystal_alice", "Alice's test wallet for development"),
427				("crystal_bob", "Bob's test wallet for development"),
428				("crystal_charlie", "Charlie's test wallet for development"),
429			];
430
431			let mut created_count = 0;
432
433			for (name, description) in test_wallets {
434				log_verbose!("Creating wallet: {}", name.bright_green());
435
436				// Create wallet with a default password for testing
437				match wallet_manager.create_developer_wallet(name).await {
438					Ok(wallet_info) => {
439						log_success!("โœ… Created {}", name.bright_green());
440						log_success!("   Address: {}", wallet_info.address.bright_cyan());
441						log_success!("   Description: {}", description.dimmed());
442						created_count += 1;
443					},
444					Err(e) => {
445						log_error!("โŒ Failed to create {}: {}", name.bright_red(), e);
446					},
447				}
448			}
449
450			log_print!("");
451			log_success!("๐ŸŽ‰ Test wallet creation complete!");
452			log_success!("   Created: {} wallets", created_count.to_string().bright_green());
453			log_print!("");
454			log_print!("๐Ÿ’ก {} You can now use these wallets:", "TIP".bright_blue().bold());
455			log_print!("   quantus send --from crystal_alice --to <address> --amount 1000");
456			log_print!("   quantus send --from crystal_bob --to <address> --amount 1000");
457			log_print!("   quantus send --from crystal_charlie --to <address> --amount 1000");
458			log_print!("");
459
460			Ok(())
461		},
462	}
463}
464
465/// Handle compatibility check command
466async fn handle_compatibility_check(node_url: &str) -> crate::error::Result<()> {
467	log_print!("๐Ÿ” Compatibility Check");
468	log_print!("๐Ÿ”— Connecting to: {}", node_url.bright_cyan());
469	log_print!("");
470
471	// Connect to the node
472	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
473
474	// Get runtime version
475	let runtime_version = runtime::get_runtime_version(quantus_client.client()).await?;
476
477	// Get system info for additional details
478	let chain_info = system::get_complete_chain_info(node_url).await?;
479
480	log_print!("๐Ÿ“‹ Version Information:");
481	log_print!("   โ€ข CLI Version: {}", env!("CARGO_PKG_VERSION").bright_green());
482	log_print!(
483		"   โ€ข Runtime Spec Version: {}",
484		runtime_version.spec_version.to_string().bright_yellow()
485	);
486	log_print!(
487		"   โ€ข Runtime Impl Version: {}",
488		runtime_version.impl_version.to_string().bright_blue()
489	);
490	log_print!(
491		"   โ€ข Transaction Version: {}",
492		runtime_version.transaction_version.to_string().bright_magenta()
493	);
494
495	if let Some(name) = &chain_info.chain_name {
496		log_print!("   โ€ข Chain Name: {}", name.bright_cyan());
497	}
498
499	log_print!("");
500
501	// Check compatibility
502	let is_compatible = crate::config::is_runtime_compatible(runtime_version.spec_version);
503
504	log_print!("๐Ÿ” Compatibility Analysis:");
505	log_print!("   โ€ข Supported Runtime Versions: {:?}", crate::config::COMPATIBLE_RUNTIME_VERSIONS);
506	log_print!("   โ€ข Current Runtime Version: {}", runtime_version.spec_version);
507
508	if is_compatible {
509		log_success!("โœ… COMPATIBLE - This CLI version supports the connected node");
510		log_print!("   โ€ข All features should work correctly");
511		log_print!("   โ€ข You can safely use all CLI commands");
512	} else {
513		log_error!("โŒ INCOMPATIBLE - This CLI version may not work with the connected node");
514		log_print!("   โ€ข Some features may not work correctly");
515		log_print!("   โ€ข Consider updating the CLI or connecting to a compatible node");
516		log_print!("   โ€ข Supported versions: {:?}", crate::config::COMPATIBLE_RUNTIME_VERSIONS);
517	}
518
519	log_print!("");
520	log_print!("๐Ÿ’ก Tip: Use 'quantus version' for quick version check");
521	log_print!("๐Ÿ’ก Tip: Use 'quantus system --runtime' for detailed system info");
522
523	Ok(())
524}