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
94 changes: 49 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,17 @@ 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: `a/b/c/d` where `a` = coin type, `b` = account index, `c` = address index, `d` =
/// internal.
Alex6323 marked this conversation as resolved.
Show resolved Hide resolved
#[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 +87,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 +230,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 +332,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 +350,16 @@ 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);
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
println_log_info!("Network name: {}", wallet.client().get_network_name().await?);
Ok(())
}

pub async fn backup_to_stronghold_snapshot_command(
wallet: &Wallet,
password: &Password,
Expand Down Expand Up @@ -400,23 +390,37 @@ 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()?;
if address.is_none() {
if get_decision("Do you want to assign an existing address to your new wallet?")? {
address.replace(get_address("Set wallet address").await?);
}
}

let mut bip_path = init_params.bip_path;
if bip_path.is_none() {
if get_decision("Do you want to assign a valid address bip path to your new wallet?")? {
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
bip_path.replace(
get_bip_path("Set address bip path (format: a/b/c/d)")
.await
.expect("todo"),
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
);
}
}

let mut alias = init_params.alias;
if alias.is_none() {
if get_decision("Do you want to assign an alias name to your 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
50 changes: 49 additions & 1 deletion 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 @@ -61,6 +62,53 @@ pub async fn get_alias(prompt: &str) -> Result<String, Error> {
}
}

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 choose a non-empty address consisting of ASCII characters.");
} 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 choose a non-empty address consisting of ASCII characters.");
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
} else {
return Ok(parse_bip_path(&input).expect("todo"));
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

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