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).await,
276		Commands::TechCollective(tech_collective_cmd) =>
277			tech_collective::handle_tech_collective_command(tech_collective_cmd, node_url).await,
278		Commands::Preimage(preimage_cmd) =>
279			preimage::handle_preimage_command(preimage_cmd, node_url, finalized).await,
280		Commands::TechReferenda(tech_referenda_cmd) =>
281			tech_referenda::handle_tech_referenda_command(tech_referenda_cmd, node_url).await,
282		Commands::Referenda(referenda_cmd) =>
283			referenda::handle_referenda_command(referenda_cmd, node_url).await,
284		Commands::Treasury(treasury_cmd) =>
285			treasury::handle_treasury_command(treasury_cmd, node_url).await,
286		Commands::Runtime(runtime_cmd) =>
287			runtime::handle_runtime_command(runtime_cmd, node_url).await,
288		Commands::Call {
289			pallet,
290			call,
291			args,
292			from,
293			password,
294			password_file,
295			tip,
296			offline,
297			call_data_only,
298		} =>
299			handle_generic_call_command(
300				pallet,
301				call,
302				args,
303				from,
304				password,
305				password_file,
306				tip,
307				offline,
308				call_data_only,
309				node_url,
310				finalized,
311			)
312			.await,
313		Commands::Balance { address } => {
314			let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
315
316			// Resolve address (could be wallet name or SS58 address)
317			let resolved_address = common::resolve_address(&address)?;
318
319			let balance = send::get_balance(&quantus_client, &resolved_address).await?;
320			let formatted_balance =
321				send::format_balance_with_symbol(&quantus_client, balance).await?;
322			log_print!("๐Ÿ’ฐ Balance: {}", formatted_balance);
323			Ok(())
324		},
325		Commands::Developer(dev_cmd) => match dev_cmd {
326			DeveloperCommands::CreateTestWallets => {
327				let _ = crate::cli::handle_developer_command(DeveloperCommands::CreateTestWallets)
328					.await;
329				Ok(())
330			},
331		},
332		Commands::Events { block, block_hash, latest: _, finalized, pallet, raw, no_decode } =>
333			events::handle_events_command(
334				block, block_hash, finalized, pallet, raw, !no_decode, node_url,
335			)
336			.await,
337		Commands::System { runtime, metadata, rpc_methods } => {
338			if runtime || metadata || rpc_methods {
339				system::handle_system_extended_command(
340					node_url,
341					runtime,
342					metadata,
343					rpc_methods,
344					verbose,
345				)
346				.await
347			} else {
348				system::handle_system_command(node_url).await
349			}
350		},
351		Commands::Metadata { no_docs, stats_only, pallet } =>
352			metadata::handle_metadata_command(node_url, no_docs, stats_only, pallet).await,
353		Commands::Version => {
354			log_print!("CLI Version: Quantus CLI v{}", env!("CARGO_PKG_VERSION"));
355			Ok(())
356		},
357		Commands::CompatibilityCheck => handle_compatibility_check(node_url).await,
358		Commands::Block(block_cmd) => block::handle_block_command(block_cmd, node_url).await,
359	}
360}
361
362/// Handle generic extrinsic call command
363#[allow(clippy::too_many_arguments)]
364async fn handle_generic_call_command(
365	pallet: String,
366	call: String,
367	args: Option<String>,
368	from: String,
369	password: Option<String>,
370	password_file: Option<String>,
371	tip: Option<String>,
372	offline: bool,
373	call_data_only: bool,
374	node_url: &str,
375	finalized: bool,
376) -> crate::error::Result<()> {
377	// For now, we only support live submission (not offline or call-data-only)
378	if offline {
379		log_error!("โŒ Offline mode is not yet implemented");
380		log_print!("๐Ÿ’ก Currently only live submission is supported");
381		return Ok(());
382	}
383
384	if call_data_only {
385		log_error!("โŒ Call-data-only mode is not yet implemented");
386		log_print!("๐Ÿ’ก Currently only live submission is supported");
387		return Ok(());
388	}
389
390	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
391
392	let args_vec = if let Some(args_str) = args {
393		serde_json::from_str(&args_str).map_err(|e| {
394			crate::error::QuantusError::Generic(format!("Invalid JSON for arguments: {e}"))
395		})?
396	} else {
397		vec![]
398	};
399
400	generic_call::handle_generic_call(&pallet, &call, args_vec, &keypair, tip, node_url, finalized)
401		.await
402}
403
404/// Handle developer subcommands
405pub async fn handle_developer_command(command: DeveloperCommands) -> crate::error::Result<()> {
406	match command {
407		DeveloperCommands::CreateTestWallets => {
408			use crate::wallet::WalletManager;
409
410			log_print!(
411				"๐Ÿงช {} Creating standard test wallets...",
412				"DEVELOPER".bright_magenta().bold()
413			);
414			log_print!("");
415
416			let wallet_manager = WalletManager::new()?;
417
418			// Standard test wallets with well-known names
419			let test_wallets = vec![
420				("crystal_alice", "Alice's test wallet for development"),
421				("crystal_bob", "Bob's test wallet for development"),
422				("crystal_charlie", "Charlie's test wallet for development"),
423			];
424
425			let mut created_count = 0;
426
427			for (name, description) in test_wallets {
428				log_verbose!("Creating wallet: {}", name.bright_green());
429
430				// Create wallet with a default password for testing
431				match wallet_manager.create_developer_wallet(name).await {
432					Ok(wallet_info) => {
433						log_success!("โœ… Created {}", name.bright_green());
434						log_success!("   Address: {}", wallet_info.address.bright_cyan());
435						log_success!("   Description: {}", description.dimmed());
436						created_count += 1;
437					},
438					Err(e) => {
439						log_error!("โŒ Failed to create {}: {}", name.bright_red(), e);
440					},
441				}
442			}
443
444			log_print!("");
445			log_success!("๐ŸŽ‰ Test wallet creation complete!");
446			log_success!("   Created: {} wallets", created_count.to_string().bright_green());
447			log_print!("");
448			log_print!("๐Ÿ’ก {} You can now use these wallets:", "TIP".bright_blue().bold());
449			log_print!("   quantus send --from crystal_alice --to <address> --amount 1000");
450			log_print!("   quantus send --from crystal_bob --to <address> --amount 1000");
451			log_print!("   quantus send --from crystal_charlie --to <address> --amount 1000");
452			log_print!("");
453
454			Ok(())
455		},
456	}
457}
458
459/// Handle compatibility check command
460async fn handle_compatibility_check(node_url: &str) -> crate::error::Result<()> {
461	log_print!("๐Ÿ” Compatibility Check");
462	log_print!("๐Ÿ”— Connecting to: {}", node_url.bright_cyan());
463	log_print!("");
464
465	// Connect to the node
466	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
467
468	// Get runtime version
469	let runtime_version = runtime::get_runtime_version(quantus_client.client()).await?;
470
471	// Get system info for additional details
472	let chain_info = system::get_complete_chain_info(node_url).await?;
473
474	log_print!("๐Ÿ“‹ Version Information:");
475	log_print!("   โ€ข CLI Version: {}", env!("CARGO_PKG_VERSION").bright_green());
476	log_print!(
477		"   โ€ข Runtime Spec Version: {}",
478		runtime_version.spec_version.to_string().bright_yellow()
479	);
480	log_print!(
481		"   โ€ข Runtime Impl Version: {}",
482		runtime_version.impl_version.to_string().bright_blue()
483	);
484	log_print!(
485		"   โ€ข Transaction Version: {}",
486		runtime_version.transaction_version.to_string().bright_magenta()
487	);
488
489	if let Some(name) = &chain_info.chain_name {
490		log_print!("   โ€ข Chain Name: {}", name.bright_cyan());
491	}
492
493	log_print!("");
494
495	// Check compatibility
496	let is_compatible = crate::config::is_runtime_compatible(runtime_version.spec_version);
497
498	log_print!("๐Ÿ” Compatibility Analysis:");
499	log_print!("   โ€ข Supported Runtime Versions: {:?}", crate::config::COMPATIBLE_RUNTIME_VERSIONS);
500	log_print!("   โ€ข Current Runtime Version: {}", runtime_version.spec_version);
501
502	if is_compatible {
503		log_success!("โœ… COMPATIBLE - This CLI version supports the connected node");
504		log_print!("   โ€ข All features should work correctly");
505		log_print!("   โ€ข You can safely use all CLI commands");
506	} else {
507		log_error!("โŒ INCOMPATIBLE - This CLI version may not work with the connected node");
508		log_print!("   โ€ข Some features may not work correctly");
509		log_print!("   โ€ข Consider updating the CLI or connecting to a compatible node");
510		log_print!("   โ€ข Supported versions: {:?}", crate::config::COMPATIBLE_RUNTIME_VERSIONS);
511	}
512
513	log_print!("");
514	log_print!("๐Ÿ’ก Tip: Use 'quantus version' for quick version check");
515	log_print!("๐Ÿ’ก Tip: Use 'quantus system --runtime' for detailed system info");
516
517	Ok(())
518}