From 923789fefa8bb05a2c9756daa70f33da3b7d9242 Mon Sep 17 00:00:00 2001 From: Arthur Welf Date: Mon, 18 Nov 2024 18:03:09 +0100 Subject: [PATCH] rusk-wallet: Remove all panics in the codebase --- rusk-wallet/src/bin/command.rs | 7 +-- rusk-wallet/src/bin/command/history.rs | 3 +- rusk-wallet/src/bin/config.rs | 33 +++++++----- rusk-wallet/src/bin/interactive.rs | 29 +++++------ rusk-wallet/src/bin/io/prompt.rs | 37 +++++++------- rusk-wallet/src/bin/main.rs | 4 +- rusk-wallet/src/bin/settings.rs | 16 +++--- rusk-wallet/src/cache.rs | 16 ++++-- rusk-wallet/src/clients.rs | 16 ++++-- rusk-wallet/src/clients/sync.rs | 2 +- rusk-wallet/src/currency.rs | 69 +++++++++++++++----------- rusk-wallet/src/error.rs | 39 +++++++++++++++ rusk-wallet/src/gql.rs | 39 +++++++-------- rusk-wallet/src/lib.rs | 28 +++++++---- rusk-wallet/src/rues.rs | 15 ++++-- rusk-wallet/src/wallet.rs | 33 +++++++----- 16 files changed, 241 insertions(+), 145 deletions(-) diff --git a/rusk-wallet/src/bin/command.rs b/rusk-wallet/src/bin/command.rs index 3f17df8843..0664abd9e8 100644 --- a/rusk-wallet/src/bin/command.rs +++ b/rusk-wallet/src/bin/command.rs @@ -19,7 +19,7 @@ use rusk_wallet::{ Gas, DEFAULT_LIMIT_CALL, DEFAULT_LIMIT_DEPLOYMENT, DEFAULT_LIMIT_TRANSFER, DEFAULT_PRICE, MIN_PRICE_DEPLOYMENT, }, - Address, Error, Profile, Wallet, EPOCH, MAX_PROFILES, + get_max_profiles, Address, Error, Profile, Wallet, EPOCH, }; use wallet_core::BalanceInfo; @@ -328,10 +328,11 @@ impl Command { } } Command::Profiles { new } => { + let max_profiles = get_max_profiles()?; if new { - if wallet.profiles().len() >= MAX_PROFILES { + if wallet.profiles().len() >= max_profiles { println!( - "Cannot create more profiles, this wallet only supports up to {MAX_PROFILES} profiles. You have {} profiles already.", wallet.profiles().len() + "Cannot create more profiles, this wallet only supports up to {max_profiles} profiles. You have {} profiles already.", wallet.profiles().len() ); std::process::exit(0); } diff --git a/rusk-wallet/src/bin/command/history.rs b/rusk-wallet/src/bin/command/history.rs index 389b35459e..87fb6e3391 100644 --- a/rusk-wallet/src/bin/command/history.rs +++ b/rusk-wallet/src/bin/command/history.rs @@ -66,7 +66,8 @@ pub(crate) async fn transaction_from_notes( ) -> anyhow::Result> { notes.sort_by(|a, b| a.note.pos().cmp(b.note.pos())); let mut ret: Vec = vec![]; - let gql = GraphQL::new(settings.state.to_string(), io::status::interactive); + let gql = + GraphQL::new(settings.state.to_string(), io::status::interactive)?; let nullifiers = notes .iter() diff --git a/rusk-wallet/src/bin/config.rs b/rusk-wallet/src/bin/config.rs index 6d67780377..1fdadbec2e 100644 --- a/rusk-wallet/src/bin/config.rs +++ b/rusk-wallet/src/bin/config.rs @@ -9,6 +9,8 @@ use std::collections::HashMap; use std::path::Path; use url::Url; +use crate::Error; + #[derive(Debug, Deserialize, Clone)] #[allow(dead_code)] pub(crate) struct Network { @@ -38,22 +40,29 @@ fn read_to_string>(path: P) -> io::Result> { impl Config { /// Attempt to load configuration from file - pub fn load(profile: &Path) -> anyhow::Result { + pub fn load(profile: &Path) -> Result { let profile = profile.join("config.toml"); - let mut global_config = dirs::home_dir().expect("OS not supported"); - global_config.push(".config"); - global_config.push(env!("CARGO_BIN_NAME")); - global_config.push("config.toml"); + let mut global_config = dirs::home_dir(); + + match global_config { + Some(ref mut path) => { + path.push(".config"); + path.push(env!("CARGO_BIN_NAME")); + path.push("config.toml"); - let contents = read_to_string(profile)? - .or(read_to_string(&global_config)?) - .unwrap_or_else(|| { - include_str!("../../default.config.toml").to_string() - }); + let contents = read_to_string(profile)? + .or(read_to_string(&path)?) + .unwrap_or_else(|| { + include_str!("../../default.config.toml").to_string() + }); - let network: Network = toml::from_str(&contents)?; + let network: Network = toml::from_str(&contents) + .map_err(|_| Error::NetworkNotFound)?; - Ok(Config { network }) + Ok(Config { network }) + } + None => Err(Error::OsNotSupported), + } } } diff --git a/rusk-wallet/src/bin/interactive.rs b/rusk-wallet/src/bin/interactive.rs index 224336695e..05f7c8157d 100644 --- a/rusk-wallet/src/bin/interactive.rs +++ b/rusk-wallet/src/bin/interactive.rs @@ -14,7 +14,7 @@ use inquire::{InquireError, Select}; use rusk_wallet::{ currency::Dusk, dat::{DatFileVersion, LATEST_VERSION}, - Address, Error, Profile, Wallet, WalletPath, MAX_PROFILES, + get_max_profiles, Address, Error, Profile, Wallet, WalletPath, }; use crate::{ @@ -106,7 +106,7 @@ pub(crate) async fn run_loop( let gql = GraphQL::new( settings.state.to_string(), io::status::interactive, - ); + )?; gql.wait_for(&tx_id).await?; if let Some(explorer) = &settings.explorer { @@ -148,9 +148,10 @@ async fn profile_idx( match menu_profile(wallet)? { ProfileSelect::Index(index, _) => Ok(index), ProfileSelect::New => { - if wallet.profiles().len() >= MAX_PROFILES { + let max_profiles = get_max_profiles()?; + if wallet.profiles().len() >= max_profiles { println!( - "Cannot create more profiles, this wallet only supports up to {MAX_PROFILES} profiles" + "Cannot create more profiles, this wallet only supports up to {max_profiles} profiles" ); return Err(InquireError::OperationCanceled.into()); @@ -169,6 +170,8 @@ async fn profile_idx( DatFileVersion::RuskBinaryFileFormat(LATEST_VERSION), )?; + // UNWRAP: we can safely unwrap here because we know the file is + // not None since we've checked the file version wallet.save_to(WalletFile { path: wallet.file().clone().unwrap().path, pwd, @@ -194,8 +197,10 @@ fn menu_profile(wallet: &Wallet) -> anyhow::Result { menu_items.push(ProfileSelect::Index(index as u8, profile)); } + let max_profiles = get_max_profiles()?; + let remaining_profiles = - MAX_PROFILES.saturating_sub(wallet.profiles().len()); + max_profiles.saturating_sub(wallet.profiles().len()); // only show the option to create a new profile if we don't already have // `MAX_PROFILES` @@ -364,7 +369,7 @@ fn confirm(cmd: &Command, wallet: &Wallet) -> anyhow::Result { gas_price, memo, } => { - let sender = sender.as_ref().expect("sender to be a valid address"); + let sender = sender.as_ref().ok_or(Error::BadAddress)?; sender.same_transaction_model(rcvr)?; let max_fee = gas_limit * gas_price; println!(" > Pay with {}", sender.preview()); @@ -385,8 +390,7 @@ fn confirm(cmd: &Command, wallet: &Wallet) -> anyhow::Result { gas_limit, gas_price, } => { - let sender = - address.as_ref().expect("address to be a valid address"); + let sender = address.as_ref().ok_or(Error::BadAddress)?; let max_fee = gas_limit * gas_price; let stake_to = wallet.public_address(wallet.find_index(sender)?)?; println!(" > Pay with {}", sender.preview()); @@ -403,8 +407,7 @@ fn confirm(cmd: &Command, wallet: &Wallet) -> anyhow::Result { gas_limit, gas_price, } => { - let sender = - address.as_ref().expect("address to be a valid address"); + let sender = address.as_ref().ok_or(Error::BadAddress)?; let unstake_from = wallet.public_address(wallet.find_index(sender)?)?; let max_fee = gas_limit * gas_price; @@ -424,8 +427,7 @@ fn confirm(cmd: &Command, wallet: &Wallet) -> anyhow::Result { gas_limit, gas_price, } => { - let sender = - address.as_ref().expect("address to be a valid address"); + let sender = address.as_ref().ok_or(Error::BadAddress)?; let max_fee = gas_limit * gas_price; let withdraw_from = wallet.public_address(wallet.find_index(sender)?)?; @@ -447,8 +449,7 @@ fn confirm(cmd: &Command, wallet: &Wallet) -> anyhow::Result { gas_limit, gas_price, } => { - let sender = - address.as_ref().expect("address to be a valid address"); + let sender = address.as_ref().ok_or(Error::BadAddress)?; let code_len = code.metadata()?.len(); let max_fee = gas_limit * gas_price; diff --git a/rusk-wallet/src/bin/io/prompt.rs b/rusk-wallet/src/bin/io/prompt.rs index 68f66941d0..a6acd0f260 100644 --- a/rusk-wallet/src/bin/io/prompt.rs +++ b/rusk-wallet/src/bin/io/prompt.rs @@ -36,7 +36,7 @@ pub(crate) fn ask_pwd(msg: &str) -> Result { let pwd = Password::new(msg) .with_display_toggle_enabled() .without_confirmation() - .with_display_mode(PasswordDisplayMode::Hidden) + .with_display_mode(PasswordDisplayMode::Masked) .prompt(); pwd @@ -157,12 +157,14 @@ pub(crate) fn request_dir( } }; - let q = Text::new( - format!("Please enter a directory to {}:", what_for).as_str(), - ) - .with_default(profile.to_str().unwrap()) - .with_validator(validator) - .prompt()?; + let msg = format!("Please enter a directory to {}:", what_for); + let q = match profile.to_str() { + Some(p) => Text::new(msg.as_str()) + .with_default(p) + .with_validator(validator) + .prompt(), + None => Text::new(msg.as_str()).with_validator(validator).prompt(), + }?; let p = PathBuf::from(q); @@ -205,13 +207,13 @@ fn request_token( min: Dusk, balance: Dusk, default: Option, -) -> anyhow::Result { +) -> Result { // Checks if the value is larger than the given min and smaller than the // min of the balance and `MAX_CONVERTIBLE`. let validator = move |value: &f64| { let max = std::cmp::min(balance, MAX_CONVERTIBLE); - match (min..=max).contains(&Dusk::from(*value)) { + match (min..=max).contains(&Dusk::try_from(*value)?) { true => Ok(Validation::Valid), false => Ok(Validation::Invalid( format!("The amount has to be between {} and {}", min, max) @@ -240,34 +242,34 @@ fn request_token( render_config: RenderConfig::default(), }; - Ok(amount_prompt.prompt()?.into()) + amount_prompt.prompt()?.try_into() } /// Request a positive amount of tokens pub(crate) fn request_token_amt( action: &str, balance: Dusk, -) -> anyhow::Result { +) -> Result { let min = MIN_CONVERTIBLE; - request_token(action, min, balance, None) + request_token(action, min, balance, None).map_err(Error::from) } /// Request amount of tokens that can be 0 pub(crate) fn request_optional_token_amt( action: &str, balance: Dusk, -) -> anyhow::Result { +) -> Result { let min = Dusk::from(0); - request_token(action, min, balance, None) + request_token(action, min, balance, None).map_err(Error::from) } /// Request amount of tokens that can't be lower than MINIMUM_STAKE -pub(crate) fn request_stake_token_amt(balance: Dusk) -> anyhow::Result { +pub(crate) fn request_stake_token_amt(balance: Dusk) -> Result { let min: Dusk = MINIMUM_STAKE.into(); - request_token("stake", min, balance, None) + request_token("stake", min, balance, None).map_err(Error::from) } /// Request gas limit @@ -290,7 +292,7 @@ pub(crate) fn request_gas_limit(default_gas_limit: u64) -> anyhow::Result { pub(crate) fn request_gas_price( min_gas_price: Lux, mempool_gas_prices: MempoolGasPrices, -) -> anyhow::Result { +) -> Result { let default_gas_price = if mempool_gas_prices.average > min_gas_price { mempool_gas_prices.average } else { @@ -304,6 +306,7 @@ pub(crate) fn request_gas_price( Some(default_gas_price as f64), ) .map(|dusk| *dusk) + .map_err(Error::from) } pub(crate) fn request_str( diff --git a/rusk-wallet/src/bin/main.rs b/rusk-wallet/src/bin/main.rs index 94574d61f8..3fc0e6c5a5 100644 --- a/rusk-wallet/src/bin/main.rs +++ b/rusk-wallet/src/bin/main.rs @@ -131,7 +131,7 @@ async fn exec() -> anyhow::Result<()> { let cmd = args.command.clone(); // Get the initial settings from the args - let settings_builder = Settings::args(args); + let settings_builder = Settings::args(args)?; // Obtain the wallet dir from the settings let wallet_dir = settings_builder.wallet_dir().clone(); @@ -357,7 +357,7 @@ async fn exec() -> anyhow::Result<()> { let tx_id = hex::encode(hash.to_bytes()); // Wait for transaction confirmation from network - let gql = GraphQL::new(settings.state, status::headless); + let gql = GraphQL::new(settings.state, status::headless)?; gql.wait_for(&tx_id).await?; println!("{tx_id}"); diff --git a/rusk-wallet/src/bin/settings.rs b/rusk-wallet/src/bin/settings.rs index 7a23f767cb..b5c7e15c15 100644 --- a/rusk-wallet/src/bin/settings.rs +++ b/rusk-wallet/src/bin/settings.rs @@ -123,29 +123,31 @@ impl SettingsBuilder { } impl Settings { - pub fn args(args: WalletArgs) -> SettingsBuilder { + pub fn args(args: WalletArgs) -> Result { let wallet_dir = if let Some(path) = &args.wallet_dir { path.clone() } else { - let mut path = dirs::home_dir().expect("OS not supported"); + let mut path = dirs::home_dir().ok_or(Error::OsNotSupported)?; path.push(".dusk"); path.push(env!("CARGO_BIN_NAME")); path }; - SettingsBuilder { wallet_dir, args } + Ok(SettingsBuilder { wallet_dir, args }) } - pub async fn check_state_con(&self) -> Result<(), reqwest::Error> { - RuesHttpClient::new(self.state.as_ref()) + pub async fn check_state_con(&self) -> Result<(), Error> { + RuesHttpClient::new(self.state.as_ref())? .check_connection() .await + .map_err(Error::from) } - pub async fn check_prover_con(&self) -> Result<(), reqwest::Error> { - RuesHttpClient::new(self.prover.as_ref()) + pub async fn check_prover_con(&self) -> Result<(), Error> { + RuesHttpClient::new(self.prover.as_ref())? .check_connection() .await + .map_err(Error::from) } } diff --git a/rusk-wallet/src/cache.rs b/rusk-wallet/src/cache.rs index dbd8844c90..0f005f03fc 100644 --- a/rusk-wallet/src/cache.rs +++ b/rusk-wallet/src/cache.rs @@ -131,7 +131,7 @@ impl Cache { let to_move = self .db .get_cf(&cf, key)? - .expect("Note must exists to be moved"); + .ok_or(Error::CacheDatabaseCorrupted)?; self.db.put_cf(&spent_cf, key, to_move)?; self.db.delete_cf(&cf, n.to_bytes())?; } @@ -148,11 +148,17 @@ impl Cache { /// Returns the last position of inserted notes. If no note has ever been /// inserted it returns None. pub(crate) fn last_pos(&self) -> Result, Error> { - Ok(self.db.get(b"last_pos")?.map(|x| { - let buff: [u8; 8] = x.try_into().expect("Invalid u64 in cache db"); + let last_pos = self.db.get(b"last_pos")?; - u64::from_be_bytes(buff) - })) + match last_pos { + Some(x) => { + let buff = + x.try_into().map_err(|_| Error::CacheDatabaseCorrupted)?; + + Ok(Some(u64::from_be_bytes(buff))) + } + None => Ok(None), + } } /// Returns an iterator over all unspent notes nullifier for the given pk. diff --git a/rusk-wallet/src/clients.rs b/rusk-wallet/src/clients.rs index 62a4c20e7f..15a47c06b6 100644 --- a/rusk-wallet/src/clients.rs +++ b/rusk-wallet/src/clients.rs @@ -37,7 +37,7 @@ use self::sync::sync_db; use super::{cache::Cache, *}; -use crate::{store::LocalStore, Error, MAX_PROFILES}; +use crate::{get_max_profiles, store::LocalStore, Error}; const TRANSFER_CONTRACT: &str = "0100000000000000000000000000000000000000000000000000000000000000"; @@ -85,7 +85,7 @@ impl State { prover: RuesHttpClient, store: LocalStore, ) -> Result { - let cfs = (0..MAX_PROFILES) + let cfs = (0..get_max_profiles()?) .flat_map(|i| { let pk: PhoenixPublicKey = derive_phoenix_pk(store.get_seed(), i as u8); @@ -119,9 +119,15 @@ impl State { } pub(crate) fn cache(&self) -> Arc { - let state = self.cache.lock().unwrap(); - - Arc::clone(&state) + let state = self.cache.lock(); + + // We can get an error if the thread holding the lock panicked while + // holding the lock. In this case, we can recover the guard from the + // poison error and return the guard to the caller. + match state { + Ok(guard) => Arc::clone(&guard), + Err(poisoned) => Arc::clone(&poisoned.into_inner()), + } } pub async fn register_sync(&mut self) -> Result<(), Error> { diff --git a/rusk-wallet/src/clients/sync.rs b/rusk-wallet/src/clients/sync.rs index d5a44e5a1b..bf9f2cd51f 100644 --- a/rusk-wallet/src/clients/sync.rs +++ b/rusk-wallet/src/clients/sync.rs @@ -21,7 +21,7 @@ pub(crate) async fn sync_db( let seed = store.get_seed(); let keys: Vec<(PhoenixSecretKey, PhoenixViewKey, PhoenixPublicKey)> = (0 - ..MAX_PROFILES) + ..get_max_profiles()?) .map(|i| { let i = i as u8; ( diff --git a/rusk-wallet/src/currency.rs b/rusk-wallet/src/currency.rs index 29f7bf559c..2cc6126c69 100644 --- a/rusk-wallet/src/currency.rs +++ b/rusk-wallet/src/currency.rs @@ -151,12 +151,15 @@ impl PartialOrd for Dusk { /// Convenient conversion of primitives to and from Dusk /// Floats are used directly as Dusk value -impl From for Dusk { - fn from(val: f64) -> Self { +impl TryFrom for Dusk { + type Error = Error; + fn try_from(val: f64) -> Result { if val < 0.0 { - panic!("Dusk type does not support negative values"); + return Err(Error::Conversion( + "Dusk type does not support negative values".to_string(), + )); } - Self(dusk(val)) + Ok(Self(dusk(val))) } } @@ -181,10 +184,17 @@ impl From for Dusk { /// Strings are parsed as Dusk values (floats) impl FromStr for Dusk { - type Err = ParseFloatError; + type Err = Error; fn from_str(s: &str) -> Result { - f64::from_str(s).map(Dusk::from) + let parse_result = f64::from_str(s).map_err(|e: ParseFloatError| { + Error::Conversion(format!( + "Failed to parse Dusk from string: {}", + e + )) + })?; + + Dusk::try_from(parse_result) } } @@ -211,20 +221,20 @@ mod tests { #[test] fn basics() { - let one = Dusk::from(1.0); - let dec = Dusk::from(2.25); + let one = Dusk::try_from(1.0).unwrap(); + let dec = Dusk::try_from(2.25).unwrap(); assert_eq!(one, 1.0); assert_eq!(dec, 2.25); assert_eq!(Dusk::MIN, 0); - assert_eq!(Dusk::MIN, Dusk::from(0.0)); + assert_eq!(Dusk::MIN, Dusk::try_from(0.0).unwrap()); } #[test] fn compare_dusk() { - let one = Dusk::from(1.0); - let two = Dusk::from(2.0); - let dec_a = Dusk::from(0.00025); - let dec_b = Dusk::from(0.00190); + let one = Dusk::try_from(1.0).unwrap(); + let two = Dusk::try_from(2.0).unwrap(); + let dec_a = Dusk::try_from(0.00025).unwrap(); + let dec_b = Dusk::try_from(0.00190).unwrap(); assert!(one == one); assert!(one != two); assert!(one < two); @@ -236,22 +246,22 @@ mod tests { #[test] fn ops_dusk_dusk() { - let one = Dusk::from(1.0); - let two = Dusk::from(2.0); - let three = Dusk::from(3.0); + let one = Dusk::try_from(1.0).unwrap(); + let two = Dusk::try_from(2.0).unwrap(); + let three = Dusk::try_from(3.0).unwrap(); assert_eq!(one + two, three); assert_eq!(three - two, one); assert_eq!(one * one, one); assert_eq!(two * one, two); assert_eq!(two / one, two); - let point_five = Dusk::from(0.5); + let point_five = Dusk::try_from(0.5).unwrap(); assert_eq!(one / two, point_five); - assert_eq!(point_five * point_five, Dusk::from(0.25)) + assert_eq!(point_five * point_five, Dusk::try_from(0.25).unwrap()); } #[test] fn ops_dusk_lux() { - let one = Dusk::from(1.0); + let one = Dusk::try_from(1.0).unwrap(); let one_dusk = 1000000000; assert_eq!(one + one_dusk, 2.0); assert_eq!(one - one_dusk, 0.0); @@ -262,43 +272,42 @@ mod tests { #[test] fn conversions() { let my_float = 35.049; - let dusk: Dusk = my_float.into(); + let dusk: Dusk = my_float.try_into().unwrap(); assert_eq!(dusk, my_float); let one_dusk = 1_000_000_000u64; - let dusk: Dusk = one_dusk.into(); + let dusk: Dusk = one_dusk.try_into().unwrap(); assert_eq!(dusk, 1.0); assert_eq!(*dusk, one_dusk); let dusk = Dusk::from_str("69.420").unwrap(); assert_eq!(dusk, 69.420); - let float: f64 = dusk.into(); + let float: f64 = dusk.try_into().unwrap(); assert_eq!(float, 69.420); let borrowed = &Dusk(one_dusk); - let float: f64 = borrowed.into(); + let float: f64 = borrowed.try_into().unwrap(); assert_eq!(float, 1.0); let zero = 0; - assert_eq!(Dusk::from(zero), 0); + assert_eq!(Dusk::try_from(zero).unwrap(), 0); let zero = 0.0; - assert_eq!(Dusk::from(zero), 0.0); + assert_eq!(Dusk::try_from(zero).unwrap(), 0.0); } #[test] #[should_panic] fn overflow() { - let ten = Dusk::from(10.0); + let ten = Dusk::try_from(10.0).unwrap(); let _ = Dusk::MAX + ten; } #[test] - #[should_panic] fn negative_dusk() { - let _ = Dusk::from(-1.0); + assert!(Dusk::try_from(-1.0).is_err()); } #[test] #[should_panic] fn negative_result() { - let one = Dusk::from(1.0); - let two = Dusk::from(2.0); + let one = Dusk::try_from(1.0).unwrap(); + let two = Dusk::try_from(2.0).unwrap(); let _ = one - two; } } diff --git a/rusk-wallet/src/error.rs b/rusk-wallet/src/error.rs index 8360debfd9..802c4e763d 100644 --- a/rusk-wallet/src/error.rs +++ b/rusk-wallet/src/error.rs @@ -4,10 +4,13 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. +use inquire::InquireError; use rand::Error as RngError; use std::io; use std::str::Utf8Error; +use crate::gql::GraphQLError; + /// Errors returned by this library #[derive(Debug, thiserror::Error)] pub enum Error { @@ -35,6 +38,9 @@ pub enum Error { /// Rkyv errors #[error("A serialization error occurred.")] Rkyv, + /// Error creating HTTP client + #[error("Cannot create HTTP client")] + HttpClient, /// Reqwest errors #[error("A request error occurred: {0}")] Reqwest(#[from] reqwest::Error), @@ -57,6 +63,9 @@ pub enum Error { /// The note wasn't found in the note-tree of the transfer-contract #[error("Note wasn't found in transfer-contract")] NoteNotFound, + /// The note couldn't be decrypted with the provided ViewKey + #[error("Note couldn't be decrypted with the provided ViewKey")] + WrongViewKey, /// Not enough gas to perform this transaction #[error("Not enough gas to perform this transaction")] NotEnoughGas, @@ -75,12 +84,18 @@ pub enum Error { /// Address does not belong to this wallet #[error("Address does not belong to this wallet")] AddressNotOwned, + /// No menu item selected + #[error("No menu item selected")] + NoMenuItemSelected, /// Mnemonic phrase is not valid #[error("Invalid mnemonic phrase")] InvalidMnemonicPhrase, /// Path provided is not a directory #[error("Path provided is not a directory")] NotDirectory, + /// Cannot get the path to the $HOME directory + #[error("OS not supported")] + OsNotSupported, /// Wallet file content is not valid #[error("Wallet file content is not valid")] WalletFileCorrupted, @@ -137,6 +152,18 @@ pub enum Error { /// Contract file location not found #[error("Invalid WASM contract path provided")] InvalidWasmContractPath, + /// Invalid environment variable value + #[error("Invalid environment variable value {0}")] + InvalidEnvVar(String), + /// Conversion error + #[error("Conversion error: {0}")] + Conversion(String), + /// GraphQL error + #[error("GraphQL error: {0}")] + GraphQLError(GraphQLError), + /// Inquire error + #[error("Inquire error: {0}")] + InquireError(String), } impl From for Error { @@ -178,3 +205,15 @@ impl From for Error { Self::RocksDB(e) } } + +impl From for Error { + fn from(e: GraphQLError) -> Self { + Self::GraphQLError(e) + } +} + +impl From for Error { + fn from(e: InquireError) -> Self { + Self::InquireError(e.to_string()) + } +} diff --git a/rusk-wallet/src/gql.rs b/rusk-wallet/src/gql.rs index 00e347c417..b6fe91e86d 100644 --- a/rusk-wallet/src/gql.rs +++ b/rusk-wallet/src/gql.rs @@ -70,11 +70,14 @@ pub enum TxStatus { impl GraphQL { /// Create a new GraphQL wallet client - pub fn new>(url: S, status: fn(&str)) -> Self { - Self { - client: RuesHttpClient::new(url), + pub fn new>( + url: S, + status: fn(&str), + ) -> Result { + Ok(Self { + client: RuesHttpClient::new(url)?, status, - } + }) } /// Wait for a transaction to be confirmed (included in a block) @@ -97,10 +100,7 @@ impl GraphQL { } /// Obtain transaction status - async fn tx_status( - &self, - tx_id: &str, - ) -> anyhow::Result { + async fn tx_status(&self, tx_id: &str) -> Result { let query = "query { tx(hash: \"####\") { id, err }}".replace("####", tx_id); let response = self.query(&query).await?; @@ -117,7 +117,7 @@ impl GraphQL { pub async fn txs_for_block( &self, block_height: u64, - ) -> anyhow::Result, GraphQLError> { + ) -> Result, Error> { let query = "query { block(height: ####) { transactions {id, raw, gasSpent, err}}}" .replace("####", block_height.to_string().as_str()); @@ -130,8 +130,8 @@ impl GraphQL { for spent_tx in block.transactions { let tx_raw = hex::decode(&spent_tx.raw) .map_err(|_| GraphQLError::TxStatus)?; - let ph_tx = Transaction::from_slice(&tx_raw).unwrap(); - + let ph_tx = Transaction::from_slice(&tx_raw) + .map_err(|_| GraphQLError::BytesError)?; ret.push(BlockTransaction { tx: ph_tx, id: spent_tx.id, @@ -153,24 +153,21 @@ impl GraphQL { pub enum GraphQLError { /// Generic errors #[error("Error fetching data from the node: {0}")] - Generic(Error), + Generic(serde_json::Error), /// Failed to fetch transaction status #[error("Failed to obtain transaction status")] TxStatus, #[error("Failed to obtain block info")] /// Failed to obtain block info BlockInfo, -} - -impl From for GraphQLError { - fn from(e: Error) -> Self { - Self::Generic(e) - } + /// Bytes decoding errors + #[error("A deserialization error occurred")] + BytesError, } impl From for GraphQLError { fn from(e: serde_json::Error) -> Self { - Self::Generic(e.into()) + Self::Generic(e) } } @@ -185,14 +182,14 @@ impl GraphQL { #[ignore = "Leave it here just for manual tests"] #[tokio::test] -async fn test() -> Result<(), Box> { +async fn test() -> Result<(), Error> { let gql = GraphQL { status: |s| { println!("{s}"); }, client: RuesHttpClient::new( "http://testnet.nodes.dusk.network:9500/graphql", - ), + )?, }; let _ = gql .tx_status( diff --git a/rusk-wallet/src/lib.rs b/rusk-wallet/src/lib.rs index 3f169643c2..5ee959893b 100644 --- a/rusk-wallet/src/lib.rs +++ b/rusk-wallet/src/lib.rs @@ -56,20 +56,28 @@ pub const MAX_CONVERTIBLE: Dusk = Dusk::MAX; pub const MIN_CONVERTIBLE: Dusk = Dusk::new(1); /// The length of an epoch in blocks pub const EPOCH: u64 = 2160; -/// Max addresses the wallet can store -pub const MAX_PROFILES: usize = get_max_profiles(); - +/// Default WALLET_MAX_PROFILES value if not set in the environment variable const DEFAULT_MAX_PROFILES: usize = 2; -const fn get_max_profiles() -> usize { +/// Max addresses the wallet can store +/// +/// This function reads the environment variable `WALLET_MAX_PROFILES` and +/// returns the value if it is set and valid. If the variable is not set, the +/// function returns the default value. +/// +/// The function can fail if the value set in the environment variable is +/// invalid (less than 1 or greater than 255) +pub fn get_max_profiles() -> Result { match option_env!("WALLET_MAX_PROFILES") { Some(v) => match konst::primitive::parse_usize(v) { - Ok(e) if e > 255 => { - panic!("WALLET_MAX_PROFILES must be lower or equal to 255") - } - Ok(e) if e > 0 => e, - _ => panic!("Invalid WALLET_MAX_PROFILES"), + Ok(e) if e > 255 => Err(Error::InvalidEnvVar( + "WALLET_MAX_PROFILES must be lower or equal to 255".to_string(), + )), + Ok(e) if e > 0 => Ok(e), + _ => Err(Error::InvalidEnvVar( + "Invalid WALLET_MAX_PROFILES".to_string(), + )), }, - None => DEFAULT_MAX_PROFILES, + None => Ok(DEFAULT_MAX_PROFILES), } } diff --git a/rusk-wallet/src/rues.rs b/rusk-wallet/src/rues.rs index a3b88c618c..8c8008b5c4 100644 --- a/rusk-wallet/src/rues.rs +++ b/rusk-wallet/src/rues.rs @@ -26,13 +26,18 @@ pub struct RuesHttpClient { impl RuesHttpClient { /// Create a new HTTP Client - pub fn new>(uri: S) -> Self { + pub fn new>(uri: S) -> Result { let client = reqwest::ClientBuilder::new() .connect_timeout(Duration::from_secs(30)) - .build() - .expect("Client to be created"); - let uri = uri.into(); - Self { uri, client } + .build(); + + match client { + Ok(client) => Ok(Self { + uri: uri.into(), + client, + }), + Err(_) => Err(Error::HttpClient), + } } /// Utility for querying the rusk VM diff --git a/rusk-wallet/src/wallet.rs b/rusk-wallet/src/wallet.rs index dd2ea4858a..46b036173d 100644 --- a/rusk-wallet/src/wallet.rs +++ b/rusk-wallet/src/wallet.rs @@ -95,7 +95,10 @@ impl Wallet { // derive the mnemonic seed let seed = Seed::new(&mnemonic, ""); // Takes the mnemonic seed as bytes - let seed_bytes = seed.as_bytes().try_into().unwrap(); + let seed_bytes = seed + .as_bytes() + .try_into() + .map_err(|_| Error::InvalidMnemonicPhrase)?; // Generate the default address at index 0 let profiles = vec![Profile { @@ -233,8 +236,8 @@ impl Wallet { status: fn(&str), ) -> Result<(), Error> { // attempt connection - let http_state = RuesHttpClient::new(rusk_addr); - let http_prover = RuesHttpClient::new(prov_addr); + let http_state = RuesHttpClient::new(rusk_addr)?; + let http_prover = RuesHttpClient::new(prov_addr)?; let state_status = http_state.check_connection().await; let prover_status = http_prover.check_connection().await; @@ -302,15 +305,21 @@ impl Wallet { ); let history = live_notes .chain(spent_notes) - .map(|(nullified_by, note, block_height)| { - let amount = note.value(Some(&vk)).unwrap(); - DecodedNote { - note, - amount, - block_height, - nullified_by, - } - }) + .flat_map( + |(nullified_by, note, block_height)| -> Result<_, Error> { + let amount = note.value(Some(&vk)); + if let Ok(amount) = amount { + Ok(DecodedNote { + note, + amount, + block_height, + nullified_by, + }) + } else { + Err(Error::WrongViewKey) + } + }, + ) .collect(); Ok(history)