Skip to content

Commit

Permalink
bip path options
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex6323 committed Mar 20, 2024
1 parent 84e432a commit 8bf3ad6
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 77 deletions.
43 changes: 24 additions & 19 deletions cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -181,7 +182,7 @@ pub async fn new_wallet(cli: Cli) -> Result<Option<Wallet>, 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);
}
Expand All @@ -203,7 +204,7 @@ pub async fn new_wallet(cli: Cli) -> Result<Option<Wallet>, 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)
}
Expand All @@ -226,6 +227,7 @@ pub async fn new_wallet(cli: Cli) -> Result<Option<Wallet>, 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?;
Expand Down Expand Up @@ -327,7 +329,8 @@ pub async fn new_wallet(cli: Cli) -> Result<Option<Wallet>, 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?;
Expand Down Expand Up @@ -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?;
Expand All @@ -393,26 +396,28 @@ 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;
}
}

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 (<coin_type>/<account_index>/<change_address>/<address_index>)").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()?);
}
}

Expand All @@ -431,7 +436,7 @@ pub async fn migrate_stronghold_snapshot_v2_to_v3_command(path: Option<String>)
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.");
Expand All @@ -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
};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -524,7 +529,7 @@ async fn create_secret_manager(init_params: &InitParameters) -> Result<SecretMan
let choice = if let Some(choice) = &init_params.secret_manager {
*choice
} else {
select_secret_manager().await?
select_secret_manager()?
};

Ok(match choice {
Expand All @@ -535,7 +540,7 @@ async fn create_secret_manager(init_params: &InitParameters) -> Result<SecretMan
bail!("cannot initialize: {} already exists", snapshot_path.display());
}

let password = get_password("Stronghold password", true)?;
let password = enter_password("Stronghold password", true)?;
let mnemonic = match &init_params.mnemonic_file_path {
Some(path) => import_mnemonic(path).await?,
None => enter_or_generate_mnemonic().await?,
Expand Down
164 changes: 108 additions & 56 deletions cli/src/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Password, Error> {
pub fn enter_password(prompt: &str, confirmation: bool) -> Result<Password, Error> {
let mut password = dialoguer::Password::new().with_prompt(prompt);

if confirmation {
Expand All @@ -34,7 +34,7 @@ pub fn get_password(prompt: &str, confirmation: bool) -> Result<Password, Error>
Ok(password.interact()?.into())
}

pub fn get_decision(prompt: &str) -> Result<bool, Error> {
pub fn enter_decision(prompt: &str) -> Result<bool, Error> {
loop {
let input = Input::<String>::new()
.with_prompt(prompt)
Expand All @@ -51,67 +51,92 @@ pub fn get_decision(prompt: &str) -> Result<bool, Error> {
}
}

pub async fn get_alias(prompt: &str) -> Result<String, Error> {
pub fn enter_address() -> Result<Bech32Address, Error> {
loop {
let input = Input::<String>::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::<String>::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<Bech32Address, Error> {
pub fn enter_bip_path() -> Result<Bip44, Error> {
loop {
let input = Input::<String>::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::<String>::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<Bip44, Error> {
loop {
let input = Input::<String>::new().with_prompt(prompt).interact_text()?;
if input.is_empty() || !input.is_ascii() {
println_log_error!(
"Invalid input, please enter a valid bip path (<coin_type>/<account_index>/<change_address>/<address_index>)."
);
} else {
return Ok(parse_bip_path(&input).map_err(|err| eyre!(err))?);
}
pub fn parse_bip_path(input: &str) -> Result<Bip44, Error> {
if input.is_empty() || !input.is_ascii() {
return Err(eyre!(
"invalid BIP path format. Expected: `<coin_type>/<account_index>/<change_address>/<address_index>`"
));
}
}

pub fn parse_bip_path(arg: &str) -> Result<Bip44, String> {
let mut bip_path_enc = Vec::with_capacity(4);
for p in arg.split_terminator('/').map(|p| p.trim()) {
match p.parse::<u32>() {
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::<u32>() {
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: `<coin_type>/<account_index>/<change_address>/<address_index>`"
.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<String, Error> {
loop {
let input = Input::<String>::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<Mnemonic, Error> {
loop {
let mnemonic = Mnemonic::from(Input::<String>::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<String>, file: Option<String>) -> Result<Option<Vec<u8>>, Error> {
Ok(if let Some(hex) = hex {
Some(prefix_hex::decode(hex)?)
Expand Down Expand Up @@ -189,21 +214,6 @@ pub async fn generate_mnemonic(
Ok(mnemonic)
}

pub fn enter_mnemonic() -> Result<Mnemonic, Error> {
loop {
let input = Mnemonic::from(
Input::<String>::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<Mnemonic, Error> {
let mut mnemonics = read_mnemonics_from_file(path).await?;
if mnemonics.is_empty() {
Expand Down Expand Up @@ -393,7 +403,7 @@ impl FromStr for SecretManagerChoice {
}
}

pub async fn select_secret_manager() -> Result<SecretManagerChoice, Error> {
pub fn select_secret_manager() -> Result<SecretManagerChoice, Error> {
let choices = ["Stronghold", "Ledger Nano", "Ledger Nano Simulator"];

Ok(Select::with_theme(&ColorfulTheme::default())
Expand All @@ -403,3 +413,45 @@ pub async fn select_secret_manager() -> Result<SecretManagerChoice, Error> {
.interact_on(&Term::stderr())?
.into())
}

#[derive(Copy, Clone, Debug, clap::ValueEnum)]
pub enum BipPathChoice {
Iota,
Shimmer,
Custom,
}

impl From<usize> 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<Self, Self::Err> {
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<BipPathChoice, Error> {
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())
}
4 changes: 2 additions & 2 deletions cli/src/wallet_cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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?;
}

Expand Down

0 comments on commit 8bf3ad6

Please sign in to comment.