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 multisig;
15pub mod preimage;
16pub mod recovery;
17pub mod reversible;
18pub mod runtime;
19pub mod scheduler;
20pub mod send;
21pub mod storage;
22pub mod system;
23pub mod tech_collective;
24pub mod transfers;
25pub mod treasury;
26pub mod wallet;
27pub mod wormhole;
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	/// Multisig commands (multi-signature wallets)
84	#[command(subcommand)]
85	Multisig(multisig::MultisigCommands),
86
87	/// Scheduler commands
88	#[command(subcommand)]
89	Scheduler(scheduler::SchedulerCommands),
90
91	/// Direct interaction with chain storage (Sudo required for set)
92	#[command(subcommand)]
93	Storage(storage::StorageCommands),
94
95	/// Tech Collective management commands
96	#[command(subcommand)]
97	TechCollective(tech_collective::TechCollectiveCommands),
98
99	/// Tech Referenda management commands (for runtime upgrade proposals)
100	#[command(subcommand)]
101	Preimage(preimage::PreimageCommands),
102
103	/// Treasury account info
104	#[command(subcommand)]
105	Treasury(treasury::TreasuryCommands),
106
107	/// Privacy-preserving transfer queries via Subsquid indexer
108	#[command(subcommand)]
109	Transfers(transfers::TransfersCommands),
110
111	/// Runtime management commands (requires root/sudo permissions)
112	#[command(subcommand)]
113	Runtime(runtime::RuntimeCommands),
114
115	/// Generic extrinsic call - call ANY pallet function!
116	Call {
117		/// Pallet name (e.g., "Balances")
118		#[arg(long)]
119		pallet: String,
120
121		/// Call/function name (e.g., "transfer_allow_death")
122		#[arg(short, long)]
123		call: String,
124
125		/// Arguments as JSON array (e.g., '["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
126		/// "1000000000000"]')
127		#[arg(short, long)]
128		args: Option<String>,
129
130		/// Wallet name to sign with
131		#[arg(short, long)]
132		from: String,
133
134		/// Password for the wallet
135		#[arg(short, long)]
136		password: Option<String>,
137
138		/// Read password from file
139		#[arg(long)]
140		password_file: Option<String>,
141
142		/// Optional tip amount to prioritize the transaction
143		#[arg(long)]
144		tip: Option<String>,
145
146		/// Create offline extrinsic without submitting
147		#[arg(long)]
148		offline: bool,
149
150		/// Output the call as hex-encoded data only
151		#[arg(long)]
152		call_data_only: bool,
153	},
154
155	/// Query account balance
156	Balance {
157		/// Account address to query (SS58 format)
158		#[arg(short, long)]
159		address: String,
160	},
161
162	/// Developer utilities and testing tools
163	#[command(subcommand)]
164	Developer(DeveloperCommands),
165
166	/// Query events from blocks
167	Events {
168		/// Block number to query events from (full support)
169		#[arg(long)]
170		block: Option<u32>,
171
172		/// Block hash to query events from (full support)
173		#[arg(long)]
174		block_hash: Option<String>,
175
176		/// Query events from latest block
177		#[arg(long)]
178		latest: bool,
179
180		/// Query events from finalized block (full support)
181		#[arg(long)]
182		finalized: bool,
183
184		/// Filter events by pallet name (e.g., "Balances")
185		#[arg(long)]
186		pallet: Option<String>,
187
188		/// Show raw event data
189		#[arg(long)]
190		raw: bool,
191
192		/// Disable event decoding (decoding is enabled by default)
193		#[arg(long)]
194		no_decode: bool,
195	},
196
197	/// Query system information
198	System {
199		/// Show runtime version information
200		#[arg(long)]
201		runtime: bool,
202
203		/// Show metadata statistics
204		#[arg(long)]
205		metadata: bool,
206
207		/// Show available JSON-RPC methods exposed by the node
208		#[arg(long)]
209		rpc_methods: bool,
210	},
211
212	/// Explore chain metadata and available pallets/calls
213	Metadata {
214		/// Skip displaying documentation for calls
215		#[arg(long)]
216		no_docs: bool,
217
218		/// Show only metadata statistics
219		#[arg(long)]
220		stats_only: bool,
221
222		/// Filter by specific pallet name
223		#[arg(long)]
224		pallet: Option<String>,
225	},
226
227	/// Show version information
228	Version,
229
230	/// Check compatibility with the connected node
231	CompatibilityCheck,
232
233	/// Block management and analysis commands
234	#[command(subcommand)]
235	Block(block::BlockCommands),
236
237	/// Wormhole proof generation and verification
238	#[command(subcommand)]
239	Wormhole(wormhole::WormholeCommands),
240
241	/// Send random amounts to multiple addresses (total is distributed randomly)
242	Multisend {
243		/// Wallet name to send from
244		#[arg(short, long)]
245		from: String,
246
247		/// File containing addresses (JSON array: ["addr1", "addr2", ...])
248		#[arg(long, conflicts_with = "addresses")]
249		addresses_file: Option<String>,
250
251		/// Comma-separated list of recipient addresses
252		#[arg(long, value_delimiter = ',', conflicts_with = "addresses_file")]
253		addresses: Option<Vec<String>>,
254
255		/// Total amount to distribute across all recipients (e.g., "1000", "100.5")
256		#[arg(long)]
257		total: String,
258
259		/// Minimum amount per recipient (e.g., "10", "1.5")
260		#[arg(long)]
261		min: String,
262
263		/// Maximum amount per recipient (e.g., "100", "50.5")
264		#[arg(long)]
265		max: String,
266
267		/// Password for the wallet (or use environment variables)
268		#[arg(short, long)]
269		password: Option<String>,
270
271		/// Read password from file (for scripting)
272		#[arg(long)]
273		password_file: Option<String>,
274
275		/// Optional tip amount to prioritize the transaction (e.g., "1", "0.5")
276		#[arg(long)]
277		tip: Option<String>,
278
279		/// Skip confirmation prompt (for scripting)
280		#[arg(long, short = 'y')]
281		yes: bool,
282	},
283}
284
285/// Developer subcommands
286#[derive(Subcommand, Debug)]
287pub enum DeveloperCommands {
288	/// Create standard test wallets (crystal_alice, crystal_bob, crystal_charlie)
289	CreateTestWallets,
290}
291
292/// Execute a CLI command
293pub async fn execute_command(
294	command: Commands,
295	node_url: &str,
296	verbose: bool,
297	execution_mode: common::ExecutionMode,
298) -> crate::error::Result<()> {
299	match command {
300		Commands::Wallet(wallet_cmd) => wallet::handle_wallet_command(wallet_cmd, node_url).await,
301		Commands::Send { from, to, amount, password, password_file, tip, nonce } =>
302			send::handle_send_command(
303				from,
304				to,
305				&amount,
306				node_url,
307				password,
308				password_file,
309				tip,
310				nonce,
311				execution_mode,
312			)
313			.await,
314		Commands::Batch(batch_cmd) =>
315			batch::handle_batch_command(batch_cmd, node_url, execution_mode).await,
316		Commands::Reversible(reversible_cmd) =>
317			reversible::handle_reversible_command(reversible_cmd, node_url, execution_mode).await,
318		Commands::HighSecurity(hs_cmd) =>
319			high_security::handle_high_security_command(hs_cmd, node_url, execution_mode).await,
320		Commands::Recovery(recovery_cmd) =>
321			recovery::handle_recovery_command(recovery_cmd, node_url, execution_mode).await,
322		Commands::Multisig(multisig_cmd) =>
323			multisig::handle_multisig_command(multisig_cmd, node_url, execution_mode).await,
324		Commands::Scheduler(scheduler_cmd) =>
325			scheduler::handle_scheduler_command(scheduler_cmd, node_url, execution_mode).await,
326		Commands::Storage(storage_cmd) =>
327			storage::handle_storage_command(storage_cmd, node_url, execution_mode).await,
328		Commands::TechCollective(tech_collective_cmd) =>
329			tech_collective::handle_tech_collective_command(
330				tech_collective_cmd,
331				node_url,
332				execution_mode,
333			)
334			.await,
335		Commands::Preimage(preimage_cmd) =>
336			preimage::handle_preimage_command(preimage_cmd, node_url, execution_mode).await,
337		Commands::Treasury(treasury_cmd) =>
338			treasury::handle_treasury_command(treasury_cmd, node_url, execution_mode).await,
339		Commands::Transfers(transfers_cmd) =>
340			transfers::handle_transfers_command(transfers_cmd).await,
341		Commands::Runtime(runtime_cmd) =>
342			runtime::handle_runtime_command(runtime_cmd, node_url, execution_mode).await,
343		Commands::Call {
344			pallet,
345			call,
346			args,
347			from,
348			password,
349			password_file,
350			tip,
351			offline,
352			call_data_only,
353		} =>
354			handle_generic_call_command(
355				pallet,
356				call,
357				args,
358				from,
359				password,
360				password_file,
361				tip,
362				offline,
363				call_data_only,
364				node_url,
365				execution_mode,
366			)
367			.await,
368		Commands::Balance { address } => {
369			let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
370
371			// Resolve address (could be wallet name or SS58 address)
372			let resolved_address = common::resolve_address(&address)?;
373
374			let account_data = send::get_account_data(&quantus_client, &resolved_address).await?;
375			let (symbol, decimals) = send::get_chain_properties(&quantus_client).await?;
376
377			let free_fmt = send::format_balance(account_data.free, decimals);
378			let reserved_fmt = send::format_balance(account_data.reserved, decimals);
379			let frozen_fmt = send::format_balance(account_data.frozen, decimals);
380
381			log_print!("๐Ÿ’ฐ {} {}", "Balance".bright_green().bold(), resolved_address.bright_cyan());
382			log_print!("   Free:     {} {}", free_fmt.bright_green(), symbol);
383			log_print!("   Reserved: {} {}", reserved_fmt.bright_yellow(), symbol);
384			log_print!("   Frozen:   {} {}", frozen_fmt.bright_red(), symbol);
385			Ok(())
386		},
387		Commands::Developer(dev_cmd) => handle_developer_command(dev_cmd).await,
388		Commands::Events { block, block_hash, latest: _, finalized, pallet, raw, no_decode } =>
389			events::handle_events_command(
390				block, block_hash, finalized, pallet, raw, !no_decode, node_url,
391			)
392			.await,
393		Commands::System { runtime, metadata, rpc_methods } => {
394			if runtime || metadata || rpc_methods {
395				system::handle_system_extended_command(
396					node_url,
397					runtime,
398					metadata,
399					rpc_methods,
400					verbose,
401				)
402				.await
403			} else {
404				system::handle_system_command(node_url).await
405			}
406		},
407		Commands::Metadata { no_docs, stats_only, pallet } =>
408			metadata::handle_metadata_command(node_url, no_docs, stats_only, pallet).await,
409		Commands::Version => {
410			log_print!("CLI Version: Quantus CLI v{}", env!("CARGO_PKG_VERSION"));
411			Ok(())
412		},
413		Commands::CompatibilityCheck => handle_compatibility_check(node_url).await,
414		Commands::Block(block_cmd) => block::handle_block_command(block_cmd, node_url).await,
415		Commands::Wormhole(wormhole_cmd) =>
416			wormhole::handle_wormhole_command(wormhole_cmd, node_url).await,
417		Commands::Multisend {
418			from,
419			addresses_file,
420			addresses,
421			total,
422			min,
423			max,
424			password,
425			password_file,
426			tip,
427			yes,
428		} =>
429			multisend::handle_multisend_command(
430				from,
431				node_url,
432				addresses_file,
433				addresses,
434				total,
435				min,
436				max,
437				password,
438				password_file,
439				tip,
440				yes,
441				execution_mode,
442			)
443			.await,
444	}
445}
446
447/// Handle generic extrinsic call command
448#[allow(clippy::too_many_arguments)]
449async fn handle_generic_call_command(
450	pallet: String,
451	call: String,
452	args: Option<String>,
453	from: String,
454	password: Option<String>,
455	password_file: Option<String>,
456	tip: Option<String>,
457	offline: bool,
458	call_data_only: bool,
459	node_url: &str,
460	execution_mode: common::ExecutionMode,
461) -> crate::error::Result<()> {
462	// For now, we only support live submission (not offline or call-data-only)
463	if offline {
464		log_error!("โŒ Offline mode is not yet implemented");
465		log_print!("๐Ÿ’ก Currently only live submission is supported");
466		return Ok(());
467	}
468
469	if call_data_only {
470		log_error!("โŒ Call-data-only mode is not yet implemented");
471		log_print!("๐Ÿ’ก Currently only live submission is supported");
472		return Ok(());
473	}
474
475	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
476
477	let args_vec = if let Some(args_str) = args {
478		serde_json::from_str(&args_str).map_err(|e| {
479			crate::error::QuantusError::Generic(format!("Invalid JSON for arguments: {e}"))
480		})?
481	} else {
482		vec![]
483	};
484
485	generic_call::handle_generic_call(
486		&pallet,
487		&call,
488		args_vec,
489		&keypair,
490		tip,
491		node_url,
492		execution_mode,
493	)
494	.await
495}
496
497/// Handle developer subcommands
498pub async fn handle_developer_command(command: DeveloperCommands) -> crate::error::Result<()> {
499	match command {
500		DeveloperCommands::CreateTestWallets => {
501			use crate::wallet::WalletManager;
502
503			log_print!(
504				"๐Ÿงช {} Creating standard test wallets...",
505				"DEVELOPER".bright_magenta().bold()
506			);
507			log_print!("");
508
509			let wallet_manager = WalletManager::new()?;
510
511			// Standard test wallets with well-known names
512			let test_wallets = vec![
513				("crystal_alice", "Alice's test wallet for development"),
514				("crystal_bob", "Bob's test wallet for development"),
515				("crystal_charlie", "Charlie's test wallet for development"),
516			];
517
518			let mut created_count = 0;
519
520			for (name, description) in test_wallets {
521				log_verbose!("Creating wallet: {}", name.bright_green());
522
523				// Create wallet with a default password for testing
524				match wallet_manager.create_developer_wallet(name).await {
525					Ok(wallet_info) => {
526						log_success!("โœ… Created {}", name.bright_green());
527						log_success!("   Address: {}", wallet_info.address.bright_cyan());
528						log_success!("   Description: {}", description.dimmed());
529						created_count += 1;
530					},
531					Err(e) => {
532						log_error!("โŒ Failed to create {}: {}", name.bright_red(), e);
533					},
534				}
535			}
536
537			log_print!("");
538			log_success!("๐ŸŽ‰ Test wallet creation complete!");
539			log_success!("   Created: {} wallets", created_count.to_string().bright_green());
540			log_print!("");
541			log_print!("๐Ÿ’ก {} You can now use these wallets:", "TIP".bright_blue().bold());
542			log_print!("   quantus send --from crystal_alice --to <address> --amount 1000");
543			log_print!("   quantus send --from crystal_bob --to <address> --amount 1000");
544			log_print!("   quantus send --from crystal_charlie --to <address> --amount 1000");
545			log_print!("");
546
547			Ok(())
548		},
549	}
550}
551
552/// Handle compatibility check command
553async fn handle_compatibility_check(node_url: &str) -> crate::error::Result<()> {
554	log_print!("๐Ÿ” Compatibility Check");
555	log_print!("๐Ÿ”— Connecting to: {}", node_url.bright_cyan());
556	log_print!("");
557
558	// Connect to the node
559	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
560
561	// Get runtime version
562	let runtime_version = runtime::get_runtime_version(quantus_client.client()).await?;
563
564	// Get system info for additional details
565	let chain_info = system::get_complete_chain_info(node_url).await?;
566
567	log_print!("๐Ÿ“‹ Version Information:");
568	log_print!("   โ€ข CLI Version: {}", env!("CARGO_PKG_VERSION").bright_green());
569	log_print!(
570		"   โ€ข Runtime Spec Version: {}",
571		runtime_version.spec_version.to_string().bright_yellow()
572	);
573	log_print!(
574		"   โ€ข Runtime Impl Version: {}",
575		runtime_version.impl_version.to_string().bright_blue()
576	);
577	log_print!(
578		"   โ€ข Transaction Version: {}",
579		runtime_version.transaction_version.to_string().bright_magenta()
580	);
581
582	if let Some(name) = &chain_info.chain_name {
583		log_print!("   โ€ข Chain Name: {}", name.bright_cyan());
584	}
585
586	log_print!("");
587
588	// Check compatibility
589	let is_compatible = crate::config::is_runtime_compatible(runtime_version.spec_version);
590
591	log_print!("๐Ÿ” Compatibility Analysis:");
592	log_print!("   โ€ข Supported Runtime Versions: {:?}", crate::config::COMPATIBLE_RUNTIME_VERSIONS);
593	log_print!("   โ€ข Current Runtime Version: {}", runtime_version.spec_version);
594
595	if is_compatible {
596		log_success!("โœ… COMPATIBLE - This CLI version supports the connected node");
597		log_print!("   โ€ข All features should work correctly");
598		log_print!("   โ€ข You can safely use all CLI commands");
599	} else {
600		log_error!("โŒ INCOMPATIBLE - This CLI version may not work with the connected node");
601		log_print!("   โ€ข Some features may not work correctly");
602		log_print!("   โ€ข Consider updating the CLI or connecting to a compatible node");
603		log_print!("   โ€ข Supported versions: {:?}", crate::config::COMPATIBLE_RUNTIME_VERSIONS);
604	}
605
606	log_print!("");
607	log_print!("๐Ÿ’ก Tip: Use 'quantus version' for quick version check");
608	log_print!("๐Ÿ’ก Tip: Use 'quantus system --runtime' for detailed system info");
609
610	Ok(())
611}