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#[derive(Subcommand, Debug)]
30pub enum Commands {
31 #[command(subcommand)]
33 Wallet(wallet::WalletCommands),
34
35 Send {
37 #[arg(short, long)]
39 to: String,
40
41 #[arg(short, long)]
43 amount: String,
44
45 #[arg(short, long)]
47 from: String,
48
49 #[arg(short, long)]
51 password: Option<String>,
52
53 #[arg(long)]
55 password_file: Option<String>,
56
57 #[arg(long)]
59 tip: Option<String>,
60
61 #[arg(long)]
63 nonce: Option<u32>,
64 },
65
66 #[command(subcommand)]
68 Batch(batch::BatchCommands),
69
70 #[command(subcommand)]
72 Reversible(reversible::ReversibleCommands),
73
74 #[command(subcommand)]
76 HighSecurity(high_security::HighSecurityCommands),
77
78 #[command(subcommand)]
80 Recovery(recovery::RecoveryCommands),
81
82 #[command(subcommand)]
84 Scheduler(scheduler::SchedulerCommands),
85
86 #[command(subcommand)]
88 Storage(storage::StorageCommands),
89
90 #[command(subcommand)]
92 TechCollective(tech_collective::TechCollectiveCommands),
93
94 #[command(subcommand)]
96 Preimage(preimage::PreimageCommands),
97
98 #[command(subcommand)]
100 Treasury(treasury::TreasuryCommands),
101
102 #[command(subcommand)]
104 Transfers(transfers::TransfersCommands),
105
106 #[command(subcommand)]
108 Runtime(runtime::RuntimeCommands),
109
110 Call {
112 #[arg(long)]
114 pallet: String,
115
116 #[arg(short, long)]
118 call: String,
119
120 #[arg(short, long)]
123 args: Option<String>,
124
125 #[arg(short, long)]
127 from: String,
128
129 #[arg(short, long)]
131 password: Option<String>,
132
133 #[arg(long)]
135 password_file: Option<String>,
136
137 #[arg(long)]
139 tip: Option<String>,
140
141 #[arg(long)]
143 offline: bool,
144
145 #[arg(long)]
147 call_data_only: bool,
148 },
149
150 Balance {
152 #[arg(short, long)]
154 address: String,
155 },
156
157 #[command(subcommand)]
159 Developer(DeveloperCommands),
160
161 Events {
163 #[arg(long)]
165 block: Option<u32>,
166
167 #[arg(long)]
169 block_hash: Option<String>,
170
171 #[arg(long)]
173 latest: bool,
174
175 #[arg(long)]
177 finalized: bool,
178
179 #[arg(long)]
181 pallet: Option<String>,
182
183 #[arg(long)]
185 raw: bool,
186
187 #[arg(long)]
189 no_decode: bool,
190 },
191
192 System {
194 #[arg(long)]
196 runtime: bool,
197
198 #[arg(long)]
200 metadata: bool,
201
202 #[arg(long)]
204 rpc_methods: bool,
205 },
206
207 Metadata {
209 #[arg(long)]
211 no_docs: bool,
212
213 #[arg(long)]
215 stats_only: bool,
216
217 #[arg(long)]
219 pallet: Option<String>,
220 },
221
222 Version,
224
225 CompatibilityCheck,
227
228 #[command(subcommand)]
230 Block(block::BlockCommands),
231
232 #[command(subcommand)]
234 Wormhole(wormhole::WormholeCommands),
235
236 Multisend {
238 #[arg(short, long)]
240 from: String,
241
242 #[arg(long, conflicts_with = "addresses")]
244 addresses_file: Option<String>,
245
246 #[arg(long, value_delimiter = ',', conflicts_with = "addresses_file")]
248 addresses: Option<Vec<String>>,
249
250 #[arg(long)]
252 total: String,
253
254 #[arg(long)]
256 min: String,
257
258 #[arg(long)]
260 max: String,
261
262 #[arg(short, long)]
264 password: Option<String>,
265
266 #[arg(long)]
268 password_file: Option<String>,
269
270 #[arg(long)]
272 tip: Option<String>,
273
274 #[arg(long, short = 'y')]
276 yes: bool,
277 },
278}
279
280#[derive(Subcommand, Debug)]
282pub enum DeveloperCommands {
283 CreateTestWallets,
285
286 BuildCircuits {
288 #[arg(long, default_value = "../qp-zk-circuits")]
290 circuits_path: String,
291
292 #[arg(long, default_value = "../chain")]
294 chain_path: String,
295
296 #[arg(long)]
298 num_leaf_proofs: usize,
299
300 #[arg(long)]
302 skip_chain: bool,
303 },
304}
305
306pub 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 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#[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 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
502pub 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 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 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
563async 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 if !circuits_dir.exists() {
580 return Err(crate::error::QuantusError::Generic(format!(
581 "Circuits directory not found: {}",
582 circuits_path
583 )));
584 }
585
586 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 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 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 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 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 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
709async 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 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
717
718 let runtime_version = runtime::get_runtime_version(quantus_client.client()).await?;
720
721 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 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}