diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 186958a18a..cc080d1ba5 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -20,8 +20,9 @@ use log::LevelFilter; use crate::{ helper::{ - check_file_exists, enter_or_generate_mnemonic, generate_mnemonic, get_address, get_alias, get_bip_path, - get_decision, get_password, import_mnemonic, parse_bip_path, select_secret_manager, SecretManagerChoice, + check_file_exists, enter_address, enter_alias, enter_bip_path, enter_decision, enter_or_generate_mnemonic, + enter_password, generate_mnemonic, import_mnemonic, parse_bip_path, select_or_enter_bip_path, + select_secret_manager, BipPathChoice, SecretManagerChoice, }, println_log_error, println_log_info, }; @@ -181,7 +182,7 @@ pub async fn new_wallet(cli: Cli) -> Result, Error> { LinkedSecretManager::Stronghold { snapshot_exists: true, .. } => { - let password = get_password("Stronghold password", false)?; + let password = enter_password("Stronghold password", false)?; backup_to_stronghold_snapshot_command(&wallet, &password, Path::new(&backup_path)).await?; return Ok(None); } @@ -203,7 +204,7 @@ pub async fn new_wallet(cli: Cli) -> Result, Error> { LinkedSecretManager::Stronghold { snapshot_exists: true, .. } => { - let current_password = get_password("Stronghold password", false)?; + let current_password = enter_password("Stronghold password", false)?; change_password_command(&wallet, current_password).await?; Some(wallet) } @@ -226,6 +227,7 @@ pub async fn new_wallet(cli: Cli) -> Result, Error> { storage_path.display() ); } + // TODO: move this into `init_command`? let secret_manager = create_secret_manager(&init_parameters).await?; let secret_manager_variant = secret_manager.to_string(); let wallet = init_command(storage_path, secret_manager, init_parameters).await?; @@ -327,7 +329,8 @@ pub async fn new_wallet(cli: Cli) -> Result, Error> { let snapshot_path = Path::new(&init_params.stronghold_snapshot_path); if !snapshot_path.exists() { - if get_decision("Create a new wallet with default parameters?")? { + if enter_decision("Create a new wallet with default parameters?")? { + // TODO: move this into `init_command`? let secret_manager = create_secret_manager(&init_params).await?; let secret_manager_variant = secret_manager.to_string(); let wallet = init_command(storage_path, secret_manager, init_params).await?; @@ -375,7 +378,7 @@ pub async fn backup_to_stronghold_snapshot_command( } pub async fn change_password_command(wallet: &Wallet, current_password: Password) -> Result<(), Error> { - let new_password = get_password("New Stronghold password", true)?; + let new_password = enter_password("New Stronghold password", true)?; wallet .change_stronghold_password(current_password, new_password) .await?; @@ -393,8 +396,8 @@ pub async fn init_command( let mut address = init_params.address.map(|s| Bech32Address::from_str(&s)).transpose()?; let mut forced = false; if address.is_none() { - if get_decision("Do you want to set the address of the new wallet?")? { - address.replace(get_address("Set wallet address").await?); + if enter_decision("Do you want to set the address of the new wallet?")? { + address.replace(enter_address()?); } else { forced = true; } @@ -402,17 +405,19 @@ pub async fn init_command( let mut bip_path = init_params.bip_path; if bip_path.is_none() { - if forced || get_decision("Do you want to set the bip path of the new wallet?")? { - bip_path.replace( - get_bip_path("Set bip path (///)").await?, - ); + if forced || enter_decision("Do you want to set the bip path of the new wallet?")? { + bip_path.replace(match select_or_enter_bip_path()? { + BipPathChoice::Iota => parse_bip_path("4218/0/0/0").unwrap(), + BipPathChoice::Shimmer => parse_bip_path("4219/0/0/0").unwrap(), + BipPathChoice::Custom => enter_bip_path()?, + }); } } let mut alias = init_params.alias; if alias.is_none() { - if get_decision("Do you want to set an alias for the new wallet?")? { - alias.replace(get_alias("Set wallet alias").await?); + if enter_decision("Do you want to set an alias for the new wallet?")? { + alias.replace(enter_alias()?); } } @@ -431,7 +436,7 @@ pub async fn migrate_stronghold_snapshot_v2_to_v3_command(path: Option) let snapshot_path = path.as_deref().unwrap_or(DEFAULT_STRONGHOLD_SNAPSHOT_PATH); check_file_exists(snapshot_path.as_ref()).await?; - let password = get_password("Stronghold password", false)?; + let password = enter_password("Stronghold password", false)?; StrongholdAdapter::migrate_snapshot_v2_to_v3(snapshot_path, password, "wallet.rs", 100, None, None)?; println_log_info!("Stronghold snapshot successfully migrated from v2 to v3."); @@ -454,7 +459,7 @@ pub async fn restore_from_stronghold_snapshot_command( let mut builder = Wallet::builder(); let password = if snapshot_path.exists() { - Some(get_password("Stronghold password", false)?) + Some(enter_password("Stronghold password", false)?) } else { None }; @@ -485,7 +490,7 @@ pub async fn restore_from_stronghold_snapshot_command( .finish() .await?; - let password = get_password("Stronghold backup password", false)?; + let password = enter_password("Stronghold backup password", false)?; if let Err(e) = wallet .restore_from_stronghold_snapshot(backup_path.into(), password, None, None) .await @@ -524,7 +529,7 @@ async fn create_secret_manager(init_params: &InitParameters) -> Result Result import_mnemonic(path).await?, None => enter_or_generate_mnemonic().await?, diff --git a/cli/src/helper.rs b/cli/src/helper.rs index ee07feea88..c39d0f9cd4 100644 --- a/cli/src/helper.rs +++ b/cli/src/helper.rs @@ -22,7 +22,7 @@ use crate::{println_log_error, println_log_info}; const DEFAULT_MNEMONIC_FILE_PATH: &str = "./mnemonic.txt"; -pub fn get_password(prompt: &str, confirmation: bool) -> Result { +pub fn enter_password(prompt: &str, confirmation: bool) -> Result { let mut password = dialoguer::Password::new().with_prompt(prompt); if confirmation { @@ -34,7 +34,7 @@ pub fn get_password(prompt: &str, confirmation: bool) -> Result Ok(password.interact()?.into()) } -pub fn get_decision(prompt: &str) -> Result { +pub fn enter_decision(prompt: &str) -> Result { loop { let input = Input::::new() .with_prompt(prompt) @@ -51,67 +51,92 @@ pub fn get_decision(prompt: &str) -> Result { } } -pub async fn get_alias(prompt: &str) -> Result { +pub fn enter_address() -> Result { loop { - let input = Input::::new().with_prompt(prompt).interact_text()?; - if input.is_empty() || !input.is_ascii() { - println_log_error!("Invalid input, please enter a valid alias (non-empty, ASCII)."); - } else { - return Ok(input); + let input = Input::::new() + .with_prompt("Enter a Bech32 wallet address") + .interact_text()?; + match Bech32Address::from_str(&input) { + Ok(address) => { + return Ok(address); + } + Err(err) => { + println_log_error!("Invalid input, please enter a valid Bech32 address: {err}"); + } } } } -pub async fn get_address(prompt: &str) -> Result { +pub fn enter_bip_path() -> Result { loop { - let input = Input::::new().with_prompt(prompt).interact_text()?; - if input.is_empty() || !input.is_ascii() { - println_log_error!("Invalid input, please enter a valid Bech32 address."); - } else { - return Ok(Bech32Address::from_str(&input)?); + let input = Input::::new().with_prompt("Enter a bip path").interact_text()?; + match parse_bip_path(&input) { + Ok(bip_path) => return Ok(bip_path), + Err(err) => { + let s = err.to_string(); + println_log_error!("{s}"); + } } + // println_log_error!("Invalid input, please enter a valid bip path."); } } -pub async fn get_bip_path(prompt: &str) -> Result { - loop { - let input = Input::::new().with_prompt(prompt).interact_text()?; - if input.is_empty() || !input.is_ascii() { - println_log_error!( - "Invalid input, please enter a valid bip path (///)." - ); - } else { - return Ok(parse_bip_path(&input).map_err(|err| eyre!(err))?); - } +pub fn parse_bip_path(input: &str) -> Result { + if input.is_empty() || !input.is_ascii() { + return Err(eyre!( + "invalid BIP path format. Expected: `///`" + )); } -} -pub fn parse_bip_path(arg: &str) -> Result { - let mut bip_path_enc = Vec::with_capacity(4); - for p in arg.split_terminator('/').map(|p| p.trim()) { - match p.parse::() { - Ok(value) => bip_path_enc.push(value), - Err(_) => { - return Err(format!("cannot parse BIP path: {p}")); + let mut segments = Vec::with_capacity(4); + for (i, segment) in input.split_terminator('/').map(|p| p.trim()).enumerate() { + match segment.parse::() { + Ok(s) => segments.push(s), + Err(err) => { + return Err(eyre!("invalid BIP path segment. {i}/`{segment}`: {err}")); } } } - if bip_path_enc.len() != 4 { - return Err( + if segments.len() != 4 { + return Err(eyre!( "invalid BIP path format. Expected: `///`" - .to_string(), - ); + )); } - let bip_path = Bip44::new(bip_path_enc[0]) - .with_account(bip_path_enc[1]) - .with_change(bip_path_enc[2]) - .with_address_index(bip_path_enc[3]); + let bip_path = Bip44::new(segments[0]) + .with_account(segments[1]) + .with_change(segments[2]) + .with_address_index(segments[3]); Ok(bip_path) } +pub fn enter_alias() -> Result { + loop { + let input = Input::::new() + .with_prompt("Enter a wallet alias") + .interact_text()?; + if !input.is_empty() && input.is_ascii() { + return Ok(input); + } else { + println_log_error!("Invalid input, please enter a valid alias (non-empty, ASCII)."); + } + } +} + +pub fn enter_mnemonic() -> Result { + loop { + let mnemonic = Mnemonic::from(Input::::new().with_prompt("Enter a mnemonic").interact_text()?); + match verify_mnemonic(&*mnemonic) { + Ok(_) => return Ok(mnemonic), + Err(err) => { + println_log_error!("Invalid mnemonic. Please enter a bip-39 conform mnemonic: {err}"); + } + } + } +} + pub async fn bytes_from_hex_or_file(hex: Option, file: Option) -> Result>, Error> { Ok(if let Some(hex) = hex { Some(prefix_hex::decode(hex)?) @@ -189,21 +214,6 @@ pub async fn generate_mnemonic( Ok(mnemonic) } -pub fn enter_mnemonic() -> Result { - loop { - let input = Mnemonic::from( - Input::::new() - .with_prompt("Enter your mnemonic") - .interact_text()?, - ); - if verify_mnemonic(&*input).is_err() { - println_log_error!("Invalid mnemonic. Please enter a bip-39 conform mnemonic."); - } else { - return Ok(input); - } - } -} - pub async fn import_mnemonic(path: &str) -> Result { let mut mnemonics = read_mnemonics_from_file(path).await?; if mnemonics.is_empty() { @@ -393,7 +403,7 @@ impl FromStr for SecretManagerChoice { } } -pub async fn select_secret_manager() -> Result { +pub fn select_secret_manager() -> Result { let choices = ["Stronghold", "Ledger Nano", "Ledger Nano Simulator"]; Ok(Select::with_theme(&ColorfulTheme::default()) @@ -403,3 +413,45 @@ pub async fn select_secret_manager() -> Result { .interact_on(&Term::stderr())? .into()) } + +#[derive(Copy, Clone, Debug, clap::ValueEnum)] +pub enum BipPathChoice { + Iota, + Shimmer, + Custom, +} + +impl From for BipPathChoice { + fn from(value: usize) -> Self { + match value { + 0 => Self::Iota, + 1 => Self::Shimmer, + 2 => Self::Custom, + _ => panic!("invalid bip path choice index"), + } + } +} + +impl FromStr for BipPathChoice { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "iota" => Ok(Self::Iota), + "shimmer" => Ok(Self::Shimmer), + "custom" => Ok(Self::Custom), + _ => Err("invalid bip path specifier [iota|shimmer|custom]"), + } + } +} + +pub fn select_or_enter_bip_path() -> Result { + let choices = ["4218/0/0/0", "4219/0/0/0", "Custom"]; + + Ok(Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Select bip path") + .items(&choices) + .default(0) + .interact_on(&Term::stderr())? + .into()) +} diff --git a/cli/src/wallet_cli/mod.rs b/cli/src/wallet_cli/mod.rs index 3302528c89..a287d8a158 100644 --- a/cli/src/wallet_cli/mod.rs +++ b/cli/src/wallet_cli/mod.rs @@ -35,7 +35,7 @@ use rustyline::{error::ReadlineError, history::MemHistory, Config, Editor}; use self::completer::WalletCommandHelper; use crate::{ - helper::{bytes_from_hex_or_file, get_password, to_utc_date_time}, + helper::{bytes_from_hex_or_file, enter_password, to_utc_date_time}, println_log_error, println_log_info, }; @@ -1411,7 +1411,7 @@ async fn ensure_password(wallet: &Wallet) -> Result<(), Error> { if matches!(*wallet.secret_manager().read().await, SecretManager::Stronghold(_)) && !wallet.is_stronghold_password_available().await? { - let password = get_password("Stronghold password", false)?; + let password = enter_password("Stronghold password", false)?; wallet.set_stronghold_password(password).await?; }