diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 309883355372549daffa1131bf2fa5a2d69adef0..0be934af2cc5dc511caa4c67531ac12b848aada2 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -3,6 +3,7 @@ mod display; use crate::commands::cesium::compute_g1v1_public_key; use crate::entities::vault_account; use crate::entities::vault_account::{AccountTreeNode, ActiveModel, DbAccountId}; +use crate::inputs::{trim_and_reduce_empty_as_none, validate_derivation_path, validate_vault_name}; use crate::keys::seed_from_cesium; use crate::*; use age::secrecy::Secret; @@ -60,14 +61,14 @@ pub enum Subcommand { g1v1_password: Option<String>, /// Password for encrypting the key (non-interactive mode) - #[clap(short = 'p', long, required = false)] + #[clap(short = 'p', long, required = false, conflicts_with_all=["no_password"])] password: Option<String>, - /// Use empty password (non-interactive mode) + /// Use empty password for encrypting the key (non-interactive mode) #[clap(long, required = false)] no_password: bool, - /// Name for the wallet entry (non-interactive mode) + /// Name for the wallet entry (non-interactive mode) - "" empty string will be considered as None #[clap(short = 'n', long, required = false)] name: Option<String>, }, @@ -83,6 +84,22 @@ pub enum Subcommand { Derive { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, + + /// Derivation path (non-interactive mode) + #[clap(short = 'd', long, required = false)] + derivation_path: Option<String>, + + /// Password to decrypt the <Base> account key (non-interactive mode) + #[clap(short = 'p', long, required = false, requires = "derivation_path", conflicts_with_all=["no_password"])] + password: Option<String>, + + /// Use empty password to decrypt the <Base> account key (non-interactive mode) + #[clap(long, required = false, requires = "derivation_path")] + no_password: bool, + + /// Name for the wallet entry (non-interactive mode) - "" empty string will be considered as None + #[clap(short = 'n', long, required = false, requires = "derivation_path")] + name: Option<String>, }, /// Give a meaningful name to an SS58 Address in the vault Rename { @@ -170,7 +187,7 @@ pub struct AddressOrVaultNameGroup { address: Option<AccountId>, /// Name of an SS58 Address in the vault #[clap(short = 'v')] - name: Option<String>, + vault_name: Option<String>, } pub struct VaultDataToImport { @@ -390,6 +407,10 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE } Subcommand::Derive { address_or_vault_name, + derivation_path, + password, + no_password, + name, } => { let account_tree_node_to_derive = retrieve_account_tree_node(db, address_or_vault_name).await?; @@ -420,8 +441,15 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE println!("The linked <Base> account is {base_account}"); - println!("Enter password to decrypt the <Base> account key"); - let password = inputs::prompt_password()?; + // Handle password from non-interactive mode or ask for it + let password = if no_password { + String::new() + } else if let Some(password) = password { + password + } else { + println!("Enter password to decrypt the <Base> account key"); + inputs::prompt_password()? + }; let account_to_derive_secret_suri = vault_account::compute_suri_account_tree_node( &account_tree_node_to_derive, @@ -429,7 +457,14 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE )?; println!(); - let derivation_path = inputs::prompt_vault_derivation_path()?; + + // Handle derivation_path from non-interactive mode or ask for it + let derivation_path = if let Some(derivation_path) = derivation_path { + validate_derivation_path(derivation_path.clone())?; + derivation_path + } else { + inputs::prompt_vault_derivation_path()? + }; let derivation_secret_suri = format!("{account_to_derive_secret_suri}{derivation_path}"); @@ -451,6 +486,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE &derivation_address, &derivation_path, &account_to_derive.address.to_string(), + name, ) .await?; @@ -742,7 +778,7 @@ pub async fn retrieve_account_tree_node<C>( where C: ConnectionTrait, { - let account_tree_node = if let Some(name_input) = &address_or_vault_name.name { + let account_tree_node = if let Some(name_input) = &address_or_vault_name.vault_name { retrieve_account_tree_node_for_name(db, name_input).await? } else if let Some(address) = &address_or_vault_name.address { let base_account_tree_node = @@ -909,7 +945,8 @@ where ))? { true => { let name = if let Some(name) = name_opt { - Some(name) + validate_vault_name(&name)?; + trim_and_reduce_empty_as_none(name) } else { println!("(Optional) Enter a name for the vault entry (leave empty to remove the name)"); inputs::prompt_vault_name_and_check_availability( @@ -945,7 +982,8 @@ where let encrypted_suri = compute_encrypted_suri(password, vault_data.secret_suri.clone())?; let name = if let Some(name) = name_opt { - Some(name) + validate_vault_name(&name)?; + trim_and_reduce_empty_as_none(name) } else { println!("(Optional) Enter a name for the vault entry"); inputs::prompt_vault_name_and_check_availability(db_tx, None).await? @@ -980,6 +1018,7 @@ pub async fn create_derivation_account<C>( derivation_address: &String, derivation_path: &String, parent_address: &String, + name_opt: Option<String>, ) -> Result<vault_account::Model, GcliError> where C: ConnectionTrait, @@ -1044,14 +1083,17 @@ where let result = inputs::select_action("Your choice?", vec!["1", "2"])?; match result { "2" => { - println!( - "(Optional) Enter a name for the vault entry (leave empty to remove the name)" - ); - let name = inputs::prompt_vault_name_and_check_availability( - db_tx, - existing_vault_account.name.as_ref(), - ) - .await?; + let name = if let Some(name) = name_opt { + validate_vault_name(&name)?; + trim_and_reduce_empty_as_none(name) + } else { + println!("(Optional) Enter a name for the vault entry (leave empty to remove the name)"); + inputs::prompt_vault_name_and_check_availability( + db_tx, + existing_vault_account.name.as_ref(), + ) + .await? + }; // Since links are made based on address / parent(address) we can just edit the existing entry and it should be fine let mut vault_account: ActiveModel = existing_vault_account.into(); @@ -1071,8 +1113,13 @@ where } } } else { - println!("(Optional) Enter a name for the vault entry"); - let name = inputs::prompt_vault_name_and_check_availability(db_tx, None).await?; + let name = if let Some(name) = name_opt { + validate_vault_name(&name)?; + trim_and_reduce_empty_as_none(name) + } else { + println!("(Optional) Enter a name for the vault entry"); + inputs::prompt_vault_name_and_check_availability(db_tx, None).await? + }; let derivation = vault_account::create_derivation_account( db_tx, diff --git a/src/inputs.rs b/src/inputs.rs index ee84a6da655a277a464fa14c5e2c6e2218a5613a..56ad96fdc6339a39d5effabd69f9f14d6c499a49 100644 --- a/src/inputs.rs +++ b/src/inputs.rs @@ -46,15 +46,16 @@ where C: ConnectionTrait, { loop { - let mut text_inquire = inquire::Text::new("Name:").with_validator({ - |input: &str| { - if input.contains('<') || input.contains('>') || input.contains('/') { - return Ok(Validation::Invalid( - "Name cannot contain characters '<', '>', '/'".into(), - )); + let mut text_inquire = inquire::Text::new("Name:").with_validator(|input: &str| { + match validate_vault_name(input) { + Ok(_) => Ok(Validation::Valid), + Err(error) => { + if let GcliError::Input(message) = error { + Ok(Validation::Invalid(ErrorMessage::from(message))) + } else { + Ok(Validation::Invalid("Unknown error".into())) + } } - - Ok(Validation::Valid) } }); @@ -66,11 +67,7 @@ where .prompt() .map_err(|e| GcliError::Input(e.to_string()))?; - let name = if name.trim().is_empty() { - None - } else { - Some(name.trim().to_string()) - }; + let name = trim_and_reduce_empty_as_none(name); let available = vault_account::check_name_available(db, initial_name, name.as_ref()).await?; @@ -86,31 +83,56 @@ where } } +pub fn trim_and_reduce_empty_as_none(name: String) -> Option<String> { + if name.trim().is_empty() { + None + } else { + Some(name.trim().to_string()) + } +} + +pub fn validate_vault_name(vault_name: &str) -> Result<(), GcliError> { + if vault_name.contains('<') || vault_name.contains('>') || vault_name.contains('/') { + return Err(GcliError::Input( + "Name cannot contain characters '<', '>', '/'".into(), + )); + } + + Ok(()) +} + /// Prompt for a derivation path pub fn prompt_vault_derivation_path() -> Result<String, GcliError> { inquire::Text::new("Derivation path:") - .with_validator(|input: &str| { - if !input.starts_with("/") { - Ok(Validation::Invalid( - "derivation path needs to start with one or more '/'".into(), - )) - } else { - match vault::parse_prefix_and_derivation_path_from_suri(input.to_string()) { - Ok(_) => Ok(Validation::Valid), - Err(error) => { - if let GcliError::Input(message) = error { - Ok(Validation::Invalid(ErrorMessage::from(message))) - } else { - Ok(Validation::Invalid("Unknown error".into())) - } + .with_validator( + |input: &str| match validate_derivation_path(input.to_string()) { + Ok(_) => Ok(Validation::Valid), + Err(error) => { + if let GcliError::Input(message) = error { + Ok(Validation::Invalid(ErrorMessage::from(message))) + } else { + Ok(Validation::Invalid("Unknown error".into())) } } - } - }) + }, + ) .prompt() .map_err(|e| GcliError::Input(e.to_string())) } +pub fn validate_derivation_path(derivation_path: String) -> Result<(), GcliError> { + if !derivation_path.starts_with("/") { + Err(GcliError::Input( + "derivation path needs to start with one or more '/'".into(), + )) + } else { + match vault::parse_prefix_and_derivation_path_from_suri(derivation_path.to_string()) { + Ok(_) => Ok(()), + Err(error) => Err(error), + } + } +} + pub fn confirm_action(query: impl ToString) -> Result<bool, GcliError> { inquire::Confirm::new(query.to_string().as_str()) .prompt()