Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: improve wallet creation #2183

Merged
merged 18 commits into from
Mar 21, 2024
Merged
96 changes: 51 additions & 45 deletions cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ use log::LevelFilter;

use crate::{
helper::{
check_file_exists, enter_or_generate_mnemonic, generate_mnemonic, get_alias, get_decision, get_password,
import_mnemonic, select_secret_manager, SecretManagerChoice,
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,
},
println_log_error, println_log_info,
};
Expand Down Expand Up @@ -67,12 +67,16 @@ pub struct InitParameters {
/// Set the node to connect to with this wallet.
#[arg(short, long, value_name = "URL", env = "NODE_URL", default_value = DEFAULT_NODE_URL)]
pub node_url: String,
/// Set the BIP path, `4219/0/0/0` if not provided.
#[arg(short, long, value_parser = parse_bip_path, default_value = "4219/0/0/0")]
/// Set the BIP path. If not provided a bip path has to be provided interactively on first launch.
/// The expected format is: `<coin_type>/<account_index>/<change_address>/<address_index>`.
#[arg(short, long, value_parser = parse_bip_path)]
pub bip_path: Option<Bip44>,
/// Set the Bech32-encoded wallet address.
#[arg(short, long)]
pub address: Option<String>,
/// Set the wallet alias name.
#[arg(short = 'l', long)]
thibault-martinez marked this conversation as resolved.
Show resolved Hide resolved
pub alias: Option<String>,
}

impl Default for InitParameters {
Expand All @@ -82,37 +86,13 @@ impl Default for InitParameters {
stronghold_snapshot_path: DEFAULT_STRONGHOLD_SNAPSHOT_PATH.to_string(),
mnemonic_file_path: None,
node_url: DEFAULT_NODE_URL.to_string(),
bip_path: Some(Bip44::new(SHIMMER_COIN_TYPE)),
bip_path: None,
address: None,
alias: None,
}
}
}

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}"));
}
}
}

if bip_path_enc.len() != 4 {
return Err(
"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]);

Ok(bip_path)
}

#[derive(Debug, Clone, Subcommand)]
pub enum CliCommand {
/// Create a backup file. Currently only Stronghold backup is supported.
Expand Down Expand Up @@ -249,8 +229,7 @@ pub async fn new_wallet(cli: Cli) -> Result<Option<Wallet>, Error> {
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?;
println_log_info!("Created new wallet with '{}' secret manager.", secret_manager_variant);

print_create_wallet_summary(&wallet, &secret_manager_variant).await?;
Some(wallet)
}
CliCommand::MigrateStrongholdSnapshotV2ToV3 { path } => {
Expand Down Expand Up @@ -352,7 +331,7 @@ pub async fn new_wallet(cli: Cli) -> Result<Option<Wallet>, Error> {
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?;
println_log_info!("Created new wallet with '{}' secret manager.", secret_manager_variant);
print_create_wallet_summary(&wallet, &secret_manager_variant).await?;
Some(wallet)
} else {
Cli::print_help()?;
Expand All @@ -370,6 +349,17 @@ pub async fn new_wallet(cli: Cli) -> Result<Option<Wallet>, Error> {
})
}

async fn print_create_wallet_summary(wallet: &Wallet, secret_manager_variant: &str) -> Result<(), Error> {
println_log_info!("Created new wallet with the following parameters:",);
println_log_info!(" Secret manager: {secret_manager_variant}");
println_log_info!(" Wallet address: {}", wallet.address().await);
println_log_info!(" Wallet bip path: {:?}", wallet.bip_path().await);
println_log_info!(" Wallet alias: {:?}", wallet.alias().await);
println_log_info!(" Network name: {}", wallet.client().get_network_name().await?);
println_log_info!(" Node url: {}", wallet.client().get_node_info().await?.url);
Ok(())
}

pub async fn backup_to_stronghold_snapshot_command(
wallet: &Wallet,
password: &Password,
Expand Down Expand Up @@ -400,23 +390,39 @@ pub async fn init_command(
secret_manager: SecretManager,
init_params: InitParameters,
) -> Result<Wallet, Error> {
let alias = if get_decision("Do you want to assign an alias to your wallet?")? {
Some(get_alias("New wallet alias").await?)
} else {
None
};
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?);
} 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 (format=<coin_type>/<account_index>/<change_address>/<address_index>)")
Alex6323 marked this conversation as resolved.
Show resolved Hide resolved
.await?,
);
}
}

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?);
}
}

Ok(Wallet::builder()
.with_secret_manager(secret_manager)
.with_client_options(ClientOptions::new().with_node(init_params.node_url.as_str())?)
.with_storage_path(storage_path.to_str().expect("invalid unicode"))
.with_address(
init_params
.address
.map(|addr| Bech32Address::from_str(&addr))
.transpose()?,
)
.with_bip_path(init_params.bip_path)
.with_address(address)
.with_bip_path(bip_path)
.with_alias(alias)
.finish()
.await?)
Expand Down
54 changes: 52 additions & 2 deletions cli/src/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use dialoguer::{console::Term, theme::ColorfulTheme, Input, Select};
use eyre::{bail, eyre, Error};
use iota_sdk::{
client::{utils::Password, verify_mnemonic},
crypto::keys::bip39::Mnemonic,
crypto::keys::{bip39::Mnemonic, bip44::Bip44},
types::block::address::Bech32Address,
};
use tokio::{
fs::{self, OpenOptions},
Expand Down Expand Up @@ -54,13 +55,62 @@ pub async fn get_alias(prompt: &str) -> Result<String, 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 choose a non-empty alias consisting of ASCII characters.");
println_log_error!("Invalid input, please enter a valid alias (non-empty, ASCII).");
} else {
return Ok(input);
}
}
}

pub async fn get_address(prompt: &str) -> 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 Bech32 address.");
} else {
return Ok(Bech32Address::from_str(&input)?);
}
}
}

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>."
Alex6323 marked this conversation as resolved.
Show resolved Hide resolved
);
} else {
return Ok(parse_bip_path(&input).map_err(|err| eyre!(err))?);
}
}
}

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}"));
}
}
}

if bip_path_enc.len() != 4 {
return Err(
"invalid BIP path format. Expected: `coin_type/account_index/change_address/address_index`".to_string(),
Alex6323 marked this conversation as resolved.
Show resolved Hide resolved
);
}

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]);

Ok(bip_path)
}

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
Loading