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 wallet: Ledger Nano support #1606

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4f30cbd
finish impl
Alex6323 Nov 10, 2023
8c40009
changelog
Alex6323 Nov 10, 2023
790b5c9
Add Ledger Nano simulator choice
Alex6323 Nov 10, 2023
6316084
panic instead of unreachable
Alex6323 Nov 14, 2023
29f7d4f
Merge branch 'develop' into feat/cli/ledger-nano-support
Alex6323 Nov 14, 2023
b63bb3a
panic message
Alex6323 Nov 15, 2023
88ccfd2
full ledger nano support
Alex6323 Nov 15, 2023
22534e2
Merge branch 'develop' into feat/cli/ledger-nano-support
Alex6323 Nov 17, 2023
9ff617b
Merge branch 'develop' into feat/cli/ledger-nano-support
thibault-martinez Nov 24, 2023
374cf32
Merge branch 'develop' into feat/cli/ledger-nano-support
Thoralf-M Nov 29, 2023
1af13b9
Merge branch 'develop' into feat/cli/ledger-nano-support
Alex6323 Dec 11, 2023
2146605
Merge branch 'develop' into feat/cli/ledger-nano-support
Alex6323 Jan 8, 2024
6572ef5
fix usability issues
Alex6323 Jan 10, 2024
6be0c25
fix backup/restore
Alex6323 Jan 10, 2024
11b641a
clean up failed restore
Alex6323 Jan 10, 2024
fff53e4
Merge branch 'develop' into feat/cli/ledger-nano-support
Alex6323 Jan 10, 2024
46aa65e
PR suggestion
Alex6323 Jan 11, 2024
3ad1634
small cleanup
Alex6323 Jan 11, 2024
b32d194
Update year (how time flies :see_no_evil:)
Alex6323 Jan 11, 2024
7813b6d
revert breaking change
Alex6323 Jan 12, 2024
c77ee0d
fix fs cleanup after failed restore
Alex6323 Jan 12, 2024
3cbd368
getter
Alex6323 Jan 16, 2024
aaa57c8
derive ValueEnum
Alex6323 Jan 16, 2024
467d9a6
optional secret manager choice
Alex6323 Jan 16, 2024
edb77d1
remove unnecessary password input
Alex6323 Jan 16, 2024
03a4e9e
create initial account for init
Alex6323 Jan 16, 2024
b4e0867
ensure set stronghold password (lazily)
Alex6323 Jan 16, 2024
c1bbf5f
fix
Alex6323 Jan 16, 2024
b09cc16
changelog
Alex6323 Jan 17, 2024
f877a07
Merge branch 'develop' into feat/cli/ledger-nano-support
Alex6323 Jan 22, 2024
eeb85cb
Merge branch 'develop' into feat/cli/ledger-nano-support
thibault-martinez Jan 22, 2024
62b57ba
Edit changelog
thibault-martinez Jan 23, 2024
e0439de
Bump version and changelog
thibault-martinez Jan 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Security -->

## 1.3.0 - 2023-xx-xx
Alex6323 marked this conversation as resolved.
Show resolved Hide resolved

### Added

- Ledger Nano support;

## 1.2.0 - 2023-10-26

### Added
Expand Down
1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ iota-sdk = { path = "../sdk", default-features = false, features = [
"rocksdb",
"stronghold",
"participation",
"ledger_nano",
] }

chrono = { version = "0.4.31", default-features = false, features = ["std"] }
Expand Down
162 changes: 75 additions & 87 deletions cli/src/command/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use clap::{builder::BoolishValueParser, Args, CommandFactory, Parser, Subcommand
use iota_sdk::{
client::{
constants::SHIMMER_COIN_TYPE,
secret::{stronghold::StrongholdSecretManager, SecretManager},
secret::{ledger_nano::LedgerSecretManager, stronghold::StrongholdSecretManager, SecretManager},
stronghold::StrongholdAdapter,
utils::Password,
},
Expand All @@ -17,11 +17,12 @@ use log::LevelFilter;

use crate::{
error::Error,
helper::{check_file_exists, enter_or_generate_mnemonic, generate_mnemonic, get_password, import_mnemonic},
helper::{check_file_exists, generate_mnemonic, get_password, SecretManagerChoice},
println_log_error, println_log_info,
};

const DEFAULT_LOG_LEVEL: &str = "debug";
const DEFAULT_SECRET_MANAGER: &str = "stronghold";
const DEFAULT_NODE_URL: &str = "https://api.testnet.shimmer.network";
const DEFAULT_STRONGHOLD_SNAPSHOT_PATH: &str = "./stardust-cli-wallet.stronghold";
const DEFAULT_WALLET_DATABASE_PATH: &str = "./stardust-cli-wallet-db";
Expand All @@ -32,7 +33,11 @@ pub struct WalletCli {
/// Set the path to the wallet database.
#[arg(long, value_name = "PATH", env = "WALLET_DATABASE_PATH", default_value = DEFAULT_WALLET_DATABASE_PATH)]
pub wallet_db_path: String,
/// Set the path to the stronghold snapshot file.
/// Set the secret manager to use.
#[arg(short, long, value_name = "SECRET_MANAGER", default_value = DEFAULT_SECRET_MANAGER)]
pub secret_manager: SecretManagerChoice,
/// Set the path to the stronghold snapshot file. Ignored if the <SECRET_MANAGER> is not a Stronghold secret
/// manager.
#[arg(long, value_name = "PATH", env = "STRONGHOLD_SNAPSHOT_PATH", default_value = DEFAULT_STRONGHOLD_SNAPSHOT_PATH)]
pub stronghold_snapshot_path: String,
/// Set the account to enter.
Expand Down Expand Up @@ -108,10 +113,30 @@ pub enum WalletCommand {
Sync,
}

impl WalletCommand {
pub fn is_unlock_command(&self) -> bool {
match self {
Self::Accounts
| Self::Backup { .. }
| Self::ChangePassword { .. }
| Self::NewAccount { .. }
| Self::NodeInfo
| Self::SetNodeUrl { .. }
| Self::SetPow { .. }
| Self::Sync => true,
Self::Init(_)
| Self::MigrateStrongholdSnapshotV2ToV3 { .. }
| Self::Mnemonic { .. }
| Self::Restore { .. } => false,
}
}
}

#[derive(Debug, Clone, Args)]
pub struct InitParameters {
// TODO: remove this field to make `InitParameters` independ from the secret manager being used?
Alex6323 marked this conversation as resolved.
Show resolved Hide resolved
/// Set the path to a file containing mnemonics. If empty, a mnemonic has to be entered or will be randomly
/// generated.
/// generated. Only used by some secret managers.
#[arg(short, long, value_name = "PATH")]
pub mnemonic_file_path: Option<String>,
/// Set the node to connect to with this wallet.
Expand All @@ -132,9 +157,7 @@ impl Default for InitParameters {
}
}

pub async fn accounts_command(storage_path: &Path, snapshot_path: &Path) -> Result<(), Error> {
let password = get_password("Stronghold password", false)?;
let wallet = unlock_wallet(storage_path, snapshot_path, password).await?;
pub async fn accounts_command(wallet: &Wallet) -> Result<(), Error> {
let accounts = wallet.get_accounts().await?;

println!("INDEX\tALIAS");
Expand All @@ -146,60 +169,36 @@ pub async fn accounts_command(storage_path: &Path, snapshot_path: &Path) -> Resu
Ok(())
}

pub async fn backup_command(storage_path: &Path, snapshot_path: &Path, backup_path: &Path) -> Result<(), Error> {
let password = get_password("Stronghold password", !snapshot_path.exists())?;
let wallet = unlock_wallet(storage_path, snapshot_path, password.clone()).await?;
wallet.backup(backup_path.into(), password).await?;
// TODO: should we allow the backup to have a different password?
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
// TODO: allow backup wallet with ledger nano?
pub async fn backup_command_stronghold(wallet: &Wallet, password: &Password, backup_path: &Path) -> Result<(), Error> {
wallet.backup(backup_path.into(), password.clone()).await?;

println_log_info!("Wallet has been backed up to \"{}\".", backup_path.display());

Ok(())
}

pub async fn change_password_command(storage_path: &Path, snapshot_path: &Path) -> Result<Wallet, Error> {
let password = get_password("Stronghold password", !snapshot_path.exists())?;
let wallet = unlock_wallet(storage_path, snapshot_path, password.clone()).await?;
let new_password = get_password("Stronghold new password", true)?;
wallet.change_stronghold_password(password, new_password).await?;
pub async fn change_password_command(wallet: &Wallet, current_password: Password) -> Result<(), Error> {
let new_password = get_password("New Stronghold password", true)?;
wallet
.change_stronghold_password(current_password, new_password)
.await?;

println_log_info!("The password has been changed");

Ok(wallet)
Ok(())
}

pub async fn init_command(
storage_path: &Path,
snapshot_path: &Path,
secret_manager: SecretManager,
parameters: InitParameters,
) -> Result<Wallet, Error> {
if storage_path.exists() {
return Err(Error::Miscellaneous(format!(
"cannot initialize: {} already exists",
storage_path.display()
)));
}
if snapshot_path.exists() {
return Err(Error::Miscellaneous(format!(
"cannot initialize: {} already exists",
snapshot_path.display()
)));
}
let password = get_password("Stronghold password", true)?;
let mnemonic = match parameters.mnemonic_file_path {
Some(path) => import_mnemonic(&path).await?,
None => enter_or_generate_mnemonic().await?,
};

let secret_manager = StrongholdSecretManager::builder()
.password(password)
.build(snapshot_path)?;
secret_manager.store_mnemonic(mnemonic).await?;
let secret_manager = SecretManager::Stronghold(secret_manager);

Ok(Wallet::builder()
.with_secret_manager(secret_manager)
.with_client_options(ClientOptions::new().with_node(parameters.node_url.as_str())?)
.with_storage_path(storage_path.to_str().expect("invalid unicode"))
.with_storage_path(storage_path.to_str().expect("invalid wallet db path"))
.with_coin_type(parameters.coin_type)
.finish()
.await?)
Expand All @@ -222,29 +221,25 @@ pub async fn mnemonic_command(output_file_name: Option<String>, output_stdout: O
Ok(())
}

pub async fn new_account_command(
storage_path: &Path,
snapshot_path: &Path,
alias: Option<String>,
) -> Result<(Wallet, AccountIdentifier), Error> {
let password = get_password("Stronghold password", !snapshot_path.exists())?;
let wallet = unlock_wallet(storage_path, snapshot_path, password).await?;

pub async fn new_account_command(wallet: &Wallet, alias: Option<String>) -> Result<AccountIdentifier, Error> {
let alias = add_account(&wallet, alias).await?;

Ok((wallet, alias))
Ok(alias)
}

pub async fn node_info_command(storage_path: &Path) -> Result<Wallet, Error> {
let wallet = unlock_wallet(storage_path, None, None).await?;
pub async fn node_info_command(wallet: &Wallet) -> Result<(), Error> {
let node_info = wallet.client().get_info().await?;

println_log_info!("Current node info: {}", serde_json::to_string_pretty(&node_info)?);

Ok(wallet)
Ok(())
}

pub async fn restore_command(storage_path: &Path, snapshot_path: &Path, backup_path: &Path) -> Result<Wallet, Error> {
pub async fn restore_command_stronghold(
storage_path: &Path,
snapshot_path: &Path,
backup_path: &Path,
) -> Result<Wallet, Error> {
check_file_exists(backup_path).await?;

let mut builder = Wallet::builder();
Expand Down Expand Up @@ -282,22 +277,13 @@ pub async fn restore_command(storage_path: &Path, snapshot_path: &Path, backup_p
Ok(wallet)
}

pub async fn set_node_url_command(storage_path: &Path, snapshot_path: &Path, url: String) -> Result<Wallet, Error> {
let password = get_password("Stronghold password", !snapshot_path.exists())?;
let wallet = unlock_wallet(storage_path, snapshot_path, password).await?;
pub async fn set_node_url_command(wallet: &Wallet, url: String) -> Result<(), Error> {
wallet.set_client_options(ClientOptions::new().with_node(&url)?).await?;

Ok(wallet)
Ok(())
}

pub async fn set_pow_command(
storage_path: &Path,
snapshot_path: &Path,
local_pow: bool,
worker_count: Option<usize>,
) -> Result<Wallet, Error> {
let password = get_password("Stronghold password", !snapshot_path.exists())?;
let wallet = unlock_wallet(storage_path, snapshot_path, password).await?;
pub async fn set_pow_command(wallet: &Wallet, local_pow: bool, worker_count: Option<usize>) -> Result<(), Error> {
// Need to get the current node, so it's not removed
let node = wallet.client().get_node().await?;
let client_options = ClientOptions::new()
Expand All @@ -306,38 +292,40 @@ pub async fn set_pow_command(
.with_pow_worker_count(worker_count);
wallet.set_client_options(client_options).await?;

Ok(wallet)
Ok(())
}

pub async fn sync_command(storage_path: &Path, snapshot_path: &Path) -> Result<Wallet, Error> {
let password = get_password("Stronghold password", !snapshot_path.exists())?;
let wallet = unlock_wallet(storage_path, snapshot_path, password).await?;
pub async fn sync_command(wallet: &Wallet) -> Result<(), Error> {
let total_balance = wallet.sync(None).await?;

println_log_info!("Synchronized all accounts: {:?}", total_balance);

Ok(wallet)
Ok(())
}

pub async fn unlock_wallet(
pub async fn unlock_wallet_stronghold(
storage_path: &Path,
snapshot_path: impl Into<Option<&Path>> + Send,
password: impl Into<Option<Password>> + Send,
snapshot_path: &Path,
password: Password,
) -> Result<Wallet, Error> {
let secret_manager = if let Some(password) = password.into() {
let snapshot_path = snapshot_path.into();
Some(SecretManager::Stronghold(
StrongholdSecretManager::builder()
.password(password)
.build(snapshot_path.ok_or(Error::Miscellaneous("Snapshot file path is not given".to_string()))?)?,
))
} else {
None
};
let secret_manager = SecretManager::Stronghold(
StrongholdSecretManager::builder()
.password(password)
.build(snapshot_path)?,
);

unlock_wallet_inner(storage_path, secret_manager).await
}

pub async fn unlock_wallet_ledgernano(storage_path: &Path, is_simulator: bool) -> Result<Wallet, Error> {
let secret_manager = SecretManager::LedgerNano(LedgerSecretManager::new(is_simulator));
unlock_wallet_inner(storage_path, secret_manager).await
}

async fn unlock_wallet_inner(storage_path: &Path, secret_manager: SecretManager) -> Result<Wallet, Error> {
let maybe_wallet = Wallet::builder()
.with_secret_manager(secret_manager)
.with_storage_path(storage_path.to_str().expect("invalid unicode"))
.with_storage_path(storage_path.to_str().expect("invalid wallet db path"))
.finish()
.await;

Expand Down
45 changes: 44 additions & 1 deletion cli/src/helper.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2020-2022 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use core::str::FromStr;
use std::path::Path;

use chrono::{DateTime, NaiveDateTime, Utc};
Expand Down Expand Up @@ -105,7 +106,7 @@ pub async fn enter_or_generate_mnemonic() -> Result<Mnemonic, Error> {
let mnemonic = match selected_choice {
0 => generate_mnemonic(None, None).await?,
1 => enter_mnemonic()?,
_ => unreachable!(),
_ => panic!("invalid choice index"),
};

Ok(mnemonic)
Expand Down Expand Up @@ -343,3 +344,45 @@ pub async fn check_file_exists(path: &Path) -> Result<(), Error> {
}
Ok(())
}

#[derive(Clone, Debug)]
pub enum SecretManagerChoice {
Stronghold,
LedgerNano,
LedgerNanoSimulator,
}

impl From<usize> for SecretManagerChoice {
fn from(value: usize) -> Self {
match value {
0 => Self::Stronghold,
1 => Self::LedgerNano,
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
2 => Self::LedgerNanoSimulator,
_ => panic!("invalid secret manager choice index"),
}
}
}

impl FromStr for SecretManagerChoice {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"stronghold" => Ok(Self::Stronghold),
"ledger-nano" => Ok(Self::LedgerNano),
"ledger-nano-sim" => Ok(Self::LedgerNanoSimulator),
_ => Err("invalid secret manager specifier [stronghold|ledger-nano|ledger-nano-sim]"),
}
}
}

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

Ok(Select::with_theme(&ColorfulTheme::default())
.with_prompt("Select secret manager")
.items(&choices)
.default(0)
.interact_on(&Term::stderr())?
.into())
}
Loading
Loading