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