diff --git a/Cargo.toml b/Cargo.toml index 463ac70cc8..b58fca980a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,6 +101,7 @@ humantime-serde = "=1.1.1" hyper = "=1.4.1" hyper-tungstenite = "=0.13.0" hyper-util = "=0.1.9" +inquire = "=0.7.5" konst = "=0.3.9" lazy_static = "=1.5.0" lru = "=0.12.4" @@ -114,7 +115,6 @@ parking_lot = "=0.12.3" pin-project = "=1.1.5" rand = { version = "=0.8.5", default-features = false } rand_chacha = { version = "=0.3.1", default-features = false } -requestty = "=0.5.0" reqwest = "=0.12.7" ringbuffer = "=0.15.0" rkyv = { version = "=0.7.39", default-features = false } diff --git a/rusk-wallet/Cargo.toml b/rusk-wallet/Cargo.toml index 81e0c674ed..8101562651 100644 --- a/rusk-wallet/Cargo.toml +++ b/rusk-wallet/Cargo.toml @@ -26,7 +26,6 @@ serde_json = { workspace = true } hex = { workspace = true } tiny-bip39 = { workspace = true } crossterm = { workspace = true } -requestty = { workspace = true } futures = { workspace = true } base64 = { workspace = true } blake3 = { workspace = true } @@ -55,7 +54,7 @@ tracing-subscriber = { workspace = true, features = [ ] } rkyv = { workspace = true } - +inquire = { workspace = true } konst = { workspace = true } [dev-dependencies] diff --git a/rusk-wallet/src/bin/command/history.rs b/rusk-wallet/src/bin/command/history.rs index 3ccc437c0c..389b35459e 100644 --- a/rusk-wallet/src/bin/command/history.rs +++ b/rusk-wallet/src/bin/command/history.rs @@ -8,11 +8,11 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fmt::{self, Display}; -use rusk_wallet::DecodedNote; +use rusk_wallet::{BlockTransaction, DecodedNote, GraphQL}; use execution_core::{dusk, from_dusk, transfer::Transaction}; -use crate::io::{self, GraphQL}; +use crate::io::{self}; use crate::settings::Settings; pub struct TransactionHistory { @@ -94,19 +94,22 @@ pub(crate) async fn transaction_from_notes( let note_hash = decoded_note.note.hash(); // Looking for the transaction which created the note - let note_creator = txs.iter().find(|(t, _, _)| { - t.outputs().iter().any(|n| n.hash().eq(¬e_hash)) - || t.nullifiers() - .iter() - .any(|tx_null| nullifiers.iter().any(|(n, _)| n == tx_null)) - }); + let note_creator = txs.iter_mut().find(|tx_block| { + let tx = &tx_block.tx; - if let Some((t, tx_id, gas_spent)) = note_creator { - let inputs_amount: f64 = t + tx.outputs().iter().any(|note| note.hash().eq(¬e_hash)) + || tx.nullifiers().iter().any(|tx_null| { + nullifiers.iter().any(|(nullifier, _)| nullifier == tx_null) + }) + }); + if let Some(BlockTransaction { tx, id, gas_spent }) = note_creator { + let inputs_amount: f64 = tx .nullifiers() .iter() .filter_map(|input| { - nullifiers.iter().find_map(|n| n.0.eq(input).then_some(n.1)) + nullifiers.iter().find_map(|(nullifier, gas)| { + nullifier.eq(input).then_some(gas) + }) }) .sum::() as f64; @@ -115,15 +118,15 @@ pub(crate) async fn transaction_from_notes( false => TransactionDirection::In, }; - match ret.iter_mut().find(|th| &th.id == tx_id) { + match ret.iter_mut().find(|th| &th.id == id) { Some(tx) => tx.amount += note_amount, None => ret.push(TransactionHistory { direction, height: decoded_note.block_height, amount: note_amount - inputs_amount, - fee: gas_spent * t.gas_price(), - tx: t.clone(), - id: tx_id.clone(), + fee: *gas_spent * tx.gas_price(), + tx: tx.clone(), + id: id.clone(), }), } } else { diff --git a/rusk-wallet/src/bin/interactive.rs b/rusk-wallet/src/bin/interactive.rs index 076d3fd9ff..224336695e 100644 --- a/rusk-wallet/src/bin/interactive.rs +++ b/rusk-wallet/src/bin/interactive.rs @@ -6,8 +6,11 @@ mod command_menu; +use std::fmt::Display; + use bip39::{Language, Mnemonic, MnemonicType}; -use requestty::Question; +use inquire::{InquireError, Select}; + use rusk_wallet::{ currency::Dusk, dat::{DatFileVersion, LATEST_VERSION}, @@ -17,7 +20,7 @@ use rusk_wallet::{ use crate::{ io::{self, prompt}, settings::Settings, - Command, GraphQL, Menu, RunResult, WalletFile, + Command, GraphQL, RunResult, WalletFile, }; /// Run the interactive UX loop with a loaded wallet @@ -27,59 +30,24 @@ pub(crate) async fn run_loop( ) -> anyhow::Result<()> { loop { // let the user choose (or create) a profile - let profile_idx = match menu_profile(wallet)? { - ProfileSelect::Index(profile_idx) => profile_idx, - ProfileSelect::New => { - if wallet.profiles().len() >= MAX_PROFILES { - println!( - "Cannot create more profiles, this wallet only supports up to {MAX_PROFILES} profiles" - ); - std::process::exit(0); - } - - let profile_idx = wallet.add_profile(); - let file_version = wallet.get_file_version()?; - - let password = &settings.password; - // if the version file is old, ask for password and save as - // latest dat file - if file_version.is_old() { - let pwd = prompt::request_auth( - "Updating your wallet data file, please enter your wallet password ", - password, - DatFileVersion::RuskBinaryFileFormat(LATEST_VERSION), - )?; - - wallet.save_to(WalletFile { - path: wallet.file().clone().unwrap().path, - pwd, - })?; - } else { - // else just save - wallet.save()?; - } - - profile_idx - } - ProfileSelect::Exit => std::process::exit(0), - }; + let profile_index = profile_idx(wallet, settings).await?; loop { - let profile = &wallet.profiles()[profile_idx as usize]; + let profile = &wallet.profiles()[profile_index as usize]; prompt::hide_cursor()?; let op = if !wallet.is_online().await { - println!("{}", profile.shielded_address_string()); + println!("\r{}", profile.shielded_address_string()); println!("{}", profile.public_account_string()); println!(); - command_menu::offline(profile_idx, settings) + command_menu::offline(profile_index, settings) } else { // get balance for this profile let moonlight_bal = - wallet.get_moonlight_balance(profile_idx).await?; + wallet.get_moonlight_balance(profile_index).await?; let phoenix_bal = - wallet.get_phoenix_balance(profile_idx).await?; + wallet.get_phoenix_balance(profile_index).await?; let phoenix_spendable = phoenix_bal.spendable.into(); let phoenix_total: Dusk = phoenix_bal.value.into(); @@ -106,7 +74,7 @@ pub(crate) async fn run_loop( println!(); command_menu::online( - profile_idx, + profile_index, wallet, phoenix_spendable, moonlight_bal, @@ -118,8 +86,8 @@ pub(crate) async fn run_loop( prompt::hide_cursor()?; // perform operations with this profile - match op? { - ProfileOp::Run(cmd) => { + match op { + Ok(ProfileOp::Run(cmd)) => { // request confirmation before running if confirm(&cmd, wallet)? { // run command @@ -133,8 +101,8 @@ pub(crate) async fn run_loop( if let RunResult::Tx(hash) = res { let tx_id = hex::encode(hash.to_bytes()); - // Wait for transaction confirmation from - // network + // Wait for transaction confirmation + // from network let gql = GraphQL::new( settings.state.to_string(), io::status::interactive, @@ -149,74 +117,113 @@ pub(crate) async fn run_loop( } } - Err(err) => println!("{err}"), + Err(err) => return Err(err), } } } - ProfileOp::Back => break, - ProfileOp::Stay => continue, - } + Ok(ProfileOp::Stay) => (), + Ok(ProfileOp::Back) => { + break; + } + Err(e) => match e.downcast_ref::() { + Some(InquireError::OperationCanceled) => (), + _ => return Err(e), + }, + }; } } } -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -enum ProfileSelect { - Index(u8), +#[derive(PartialEq, Eq, Debug, Clone)] +enum ProfileSelect<'a> { + Index(u8, &'a Profile), New, - Exit, + Back, +} + +async fn profile_idx( + wallet: &mut Wallet, + settings: &Settings, +) -> anyhow::Result { + match menu_profile(wallet)? { + ProfileSelect::Index(index, _) => Ok(index), + ProfileSelect::New => { + if wallet.profiles().len() >= MAX_PROFILES { + println!( + "Cannot create more profiles, this wallet only supports up to {MAX_PROFILES} profiles" + ); + + return Err(InquireError::OperationCanceled.into()); + } + + let profile_idx = wallet.add_profile(); + let file_version = wallet.get_file_version()?; + + let password = &settings.password; + // if the version file is old, ask for password and save as + // latest dat file + if file_version.is_old() { + let pwd = prompt::request_auth( + "Updating your wallet data file, please enter your wallet password ", + password, + DatFileVersion::RuskBinaryFileFormat(LATEST_VERSION), + )?; + + wallet.save_to(WalletFile { + path: wallet.file().clone().unwrap().path, + pwd, + })?; + } else { + // else just save + wallet.save()?; + } + + Ok(profile_idx) + } + ProfileSelect::Back => Err(InquireError::OperationCanceled.into()), + } } -/// Allows the user to choose an profile from the selected wallet +/// Allows the user to choose a profile from the selected wallet /// to start performing operations. fn menu_profile(wallet: &Wallet) -> anyhow::Result { - let mut profile_menu = Menu::title("Profiles"); - for (profile_idx, profile) in wallet.profiles().iter().enumerate() { - let profile_idx = profile_idx as u8; - let profile_str = format!( - "{}\n {}\n {}", - Profile::index_string(profile_idx), - profile.shielded_address_preview(), - profile.public_account_preview(), - ); - profile_menu = - profile_menu.add(ProfileSelect::Index(profile_idx), profile_str); + let mut menu_items = Vec::new(); + let profiles = wallet.profiles(); + + for (index, profile) in profiles.iter().enumerate() { + menu_items.push(ProfileSelect::Index(index as u8, profile)); } let remaining_profiles = MAX_PROFILES.saturating_sub(wallet.profiles().len()); - let mut action_menu = Menu::new(); // only show the option to create a new profile if we don't already have // `MAX_PROFILES` if remaining_profiles > 0 { - action_menu = action_menu - .separator() - .add(ProfileSelect::New, "New profile") - }; + menu_items.push(ProfileSelect::New); + } + + menu_items.push(ProfileSelect::Back); + + let mut select = Select::new("Your Profiles", menu_items); + + // UNWRAP: Its okay to unwrap because the default help message + // is provided by inquire Select struct + let mut msg = Select::::DEFAULT_HELP_MESSAGE + .unwrap() + .to_owned(); if let Some(rx) = &wallet.state()?.sync_rx { if let Ok(status) = rx.try_recv() { - action_menu = action_menu - .separator() - .separator_msg(format!("Sync Status: {status}")); + msg = format!("Sync Status: {status}"); } else { - action_menu = action_menu - .separator() - .separator_msg("Waiting for Sync to complete.."); + msg = "Waiting for Sync to complete..".to_string(); } } - action_menu = action_menu.separator().add(ProfileSelect::Exit, "Exit"); - - let menu = profile_menu.extend(action_menu); - let questions = Question::select("theme") - .message("Please select a profile") - .choices(menu.clone()) - .build(); + select = select.with_help_message(&msg); - let answer = requestty::prompt_one(questions)?; - Ok(menu.answer(&answer).to_owned()) + Ok(select.prompt()?) } #[derive(PartialEq, Eq, Hash, Debug, Clone)] @@ -227,7 +234,7 @@ enum ProfileOp { } /// Allows the user to load a wallet interactively -pub(crate) fn load_wallet( +pub(crate) async fn load_wallet( wallet_path: &WalletPath, settings: &Settings, file_version: Result, @@ -238,7 +245,7 @@ pub(crate) fn load_wallet( let password = &settings.password; // display main menu - let wallet = match menu_wallet(wallet_found)? { + let wallet = match menu_wallet(wallet_found, settings).await? { MainMenu::Load(path) => { let file_version = file_version?; let mut attempt = 1; @@ -312,38 +319,38 @@ enum MainMenu { /// Allows the user to load an existing wallet, recover a lost one /// or create a new one. -fn menu_wallet(wallet_found: Option) -> anyhow::Result { +async fn menu_wallet( + wallet_found: Option, + settings: &Settings, +) -> anyhow::Result { // create the wallet menu - let mut menu = Menu::new(); + let mut menu_items = Vec::new(); if let Some(wallet_path) = wallet_found { - menu = menu - .separator() - .add(MainMenu::Load(wallet_path), "Access your wallet") - .separator() - .add(MainMenu::Create, "Replace your wallet with a new one") - .add( - MainMenu::Recover, - "Replace your wallet with a lost one using the mnemonic phrase", - ) + menu_items.push(MainMenu::Load(wallet_path)); + menu_items.push(MainMenu::Create); + menu_items.push(MainMenu::Recover); } else { - menu = menu.add(MainMenu::Create, "Create a new wallet").add( - MainMenu::Recover, - "Access a lost wallet using the mnemonic phrase", - ) + menu_items.push(MainMenu::Create); + menu_items.push(MainMenu::Recover); } - // create the action menu - menu = menu.separator().add(MainMenu::Exit, "Exit"); + menu_items.push(MainMenu::Exit); + + let emoji_state = status_emoji(settings.check_state_con().await.is_ok()); + let emoji_prover = status_emoji(settings.check_prover_con().await.is_ok()); + + let state_status = format!("{} State: {}", emoji_state, settings.state); + let prover_status = format!("{} Prover: {}", emoji_prover, settings.prover); + + let menu = format!( + "Welcome\n {state_status}\n {prover_status} \nWhat would you like to do?", + ); // let the user choose an option - let questions = Question::select("theme") - .message("What would you like to do?") - .choices(menu.clone()) - .build(); + let select = Select::new(menu.as_str(), menu_items); - let answer = requestty::prompt_one(questions)?; - Ok(menu.answer(&answer).to_owned()) + Ok(select.prompt()?) } /// Request user confirmation for a transfer transaction @@ -458,3 +465,42 @@ fn confirm(cmd: &Command, wallet: &Wallet) -> anyhow::Result { _ => Ok(true), } } + +fn status_emoji(status: bool) -> String { + if status { + "✅".to_string() + } else { + "❌".to_string() + } +} + +impl<'a> Display for ProfileSelect<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProfileSelect::Index(index, profile) => write!( + f, + "{}\n {}\n {}", + Profile::index_string(*index), + profile.shielded_address_preview(), + profile.public_account_preview(), + ), + ProfileSelect::New => write!(f, "Create a new profile"), + ProfileSelect::Back => write!(f, "Back"), + } + } +} + +impl Display for MainMenu { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MainMenu::Load(path) => { + write!(f, "Load wallet from {}", path.wallet.display()) + } + MainMenu::Create => write!(f, "Create a new wallet"), + MainMenu::Recover => { + write!(f, "Recover a lost wallet using recovery phrase") + } + MainMenu::Exit => write!(f, "Exit"), + } + } +} diff --git a/rusk-wallet/src/bin/interactive/command_menu.rs b/rusk-wallet/src/bin/interactive/command_menu.rs index eb5e595bb6..79f6c62a37 100644 --- a/rusk-wallet/src/bin/interactive/command_menu.rs +++ b/rusk-wallet/src/bin/interactive/command_menu.rs @@ -4,8 +4,10 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. +use std::fmt::Display; + use execution_core::transfer::data::MAX_MEMO_SIZE; -use requestty::Question; +use inquire::{InquireError, Select}; use rusk_wallet::{ currency::Dusk, gas::{ @@ -15,7 +17,7 @@ use rusk_wallet::{ Address, Wallet, MAX_FUNCTION_NAME_SIZE, }; -use crate::{prompt, settings::Settings, Command, Menu, WalletFile}; +use crate::{prompt, settings::Settings, Command, WalletFile}; use super::ProfileOp; @@ -37,6 +39,30 @@ enum MenuItem { Back, } +impl Display for MenuItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MenuItem::History => write!(f, "Show Transactions History"), + MenuItem::Transfer => write!(f, "Transfer Dusk"), + MenuItem::Stake => write!(f, "Stake"), + MenuItem::Unstake => write!(f, "Unstake"), + MenuItem::Withdraw => write!(f, "Withdraw Stake Reward"), + MenuItem::ContractDeploy => write!(f, "Deploy a Contract"), + MenuItem::ContractCall => write!(f, "Call a Contract"), + MenuItem::Unshield => { + write!(f, "Convert Shielded Dusk to Public Dusk") + } + MenuItem::Shield => { + write!(f, "Convert Public Dusk to Shielded Dusk") + } + MenuItem::CalculateContractId => write!(f, "Calculate Contract ID"), + MenuItem::StakeInfo => write!(f, "Stake Info"), + MenuItem::Export => write!(f, "Export Provisioner Key-Pair"), + MenuItem::Back => write!(f, "Back"), + } + } +} + /// Allows the user to choose the operation to perform for the /// selected profile pub(crate) async fn online( @@ -46,33 +72,31 @@ pub(crate) async fn online( moonlight_balance: Dusk, settings: &Settings, ) -> anyhow::Result { - let cmd_menu = Menu::new() - .add(MenuItem::History, "Show Transactions History") - .add(MenuItem::Transfer, "Transfer Dusk") - .add(MenuItem::Unshield, "Convert Shielded Dusk to Public Dusk") - .add(MenuItem::Shield, "Convert Public Dusk to Shielded Dusk") - .add(MenuItem::StakeInfo, "Check Existing Stake") - .add(MenuItem::Stake, "Stake") - .add(MenuItem::Unstake, "Unstake") - .add(MenuItem::Withdraw, "Withdraw Stake Reward") - .add(MenuItem::ContractCall, "Call a Contract") - .add(MenuItem::ContractDeploy, "Deploy a Contract") - .add(MenuItem::CalculateContractId, "Calculate Contract ID") - .add(MenuItem::Export, "Export Provisioner Key-Pair") - .separator() - .add(MenuItem::Back, "Back") - .separator(); - - let q = Question::select("theme") - .message("What do you like to do?") - .choices(cmd_menu.clone()) - .page_size(20) - .build(); - - let answer = requestty::prompt_one(q)?; - let cmd = cmd_menu.answer(&answer).to_owned(); - - let res = match cmd { + let cmd_menu = vec![ + MenuItem::History, + MenuItem::Transfer, + MenuItem::Unshield, + MenuItem::Shield, + MenuItem::StakeInfo, + MenuItem::Stake, + MenuItem::Unstake, + MenuItem::Withdraw, + MenuItem::ContractCall, + MenuItem::ContractDeploy, + MenuItem::CalculateContractId, + MenuItem::Export, + MenuItem::Back, + ]; + + let select = Select::new("What would you like to do?", cmd_menu).prompt(); + + if let Err(InquireError::OperationCanceled) = select { + return Ok(ProfileOp::Back); + } + + let select = select?; + + let res = match select { MenuItem::Transfer => { let rcvr = prompt::request_rcvr_addr("recipient")?; @@ -355,6 +379,7 @@ pub(crate) async fn online( })), MenuItem::Back => ProfileOp::Back, }; + Ok(res) } @@ -364,21 +389,12 @@ pub(crate) fn offline( profile_idx: u8, settings: &Settings, ) -> anyhow::Result { - let cmd_menu = Menu::new() - .separator() - .add(MenuItem::Export, "Export provisioner key-pair") - .separator() - .add(MenuItem::Back, "Back"); + let cmd_menu = vec![MenuItem::Export]; - let q = Question::select("theme") - .message("[OFFLINE] What would you like to do?") - .choices(cmd_menu.clone()) - .build(); + let select = Select::new("[OFFLINE] What would you like to do?", cmd_menu) + .prompt()?; - let answer = requestty::prompt_one(q)?; - let cmd = cmd_menu.answer(&answer).to_owned(); - - let res = match cmd { + let res = match select { MenuItem::Export => ProfileOp::Run(Box::new(Command::Export { profile_idx: Some(profile_idx), name: None, @@ -387,9 +403,9 @@ pub(crate) fn offline( settings.wallet_dir.clone(), )?, })), - MenuItem::Back => ProfileOp::Back, _ => unreachable!(), }; + Ok(res) } diff --git a/rusk-wallet/src/bin/io.rs b/rusk-wallet/src/bin/io.rs index 3b7c578568..57802746df 100644 --- a/rusk-wallet/src/bin/io.rs +++ b/rusk-wallet/src/bin/io.rs @@ -5,10 +5,8 @@ // Copyright (c) DUSK NETWORK. All rights reserved. mod args; -mod gql; pub(crate) mod prompt; pub(crate) mod status; pub(crate) use args::WalletArgs; -pub(crate) use gql::GraphQL; diff --git a/rusk-wallet/src/bin/io/prompt.rs b/rusk-wallet/src/bin/io/prompt.rs index d5354c9c2c..b587e7f86b 100644 --- a/rusk-wallet/src/bin/io/prompt.rs +++ b/rusk-wallet/src/bin/io/prompt.rs @@ -4,6 +4,7 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. +use std::fmt::Display; use std::path::PathBuf; use std::str::FromStr; use std::{io::stdout, println}; @@ -16,16 +17,42 @@ use crossterm::{ use anyhow::Result; use bip39::{ErrorKind, Language, Mnemonic}; use execution_core::stake::MINIMUM_STAKE; -use requestty::{Choice, Question}; -use rusk_wallet::gas::{self, MempoolGasPrices}; +use inquire::ui::RenderConfig; +use inquire::validator::Validation; +use inquire::{ + Confirm, CustomType, InquireError, Password, PasswordDisplayMode, Select, + Text, +}; use rusk_wallet::{ currency::{Dusk, Lux}, dat::DatFileVersion, + gas::{self, MempoolGasPrices}, Address, Error, MAX_CONVERTIBLE, MIN_CONVERTIBLE, }; use sha2::{Digest, Sha256}; +pub(crate) fn ask_pwd(msg: &str) -> Result { + let pwd = Password::new(msg) + .with_display_toggle_enabled() + .without_confirmation() + .with_display_mode(PasswordDisplayMode::Hidden) + .prompt(); + + pwd +} + +pub(crate) fn create_new_password() -> Result { + let pwd = Password::new("password") + .with_display_toggle_enabled() + .with_display_mode(PasswordDisplayMode::Hidden) + .with_custom_confirmation_message("confirm password: ") + .with_custom_confirmation_error_message("The passwords doesn't match") + .prompt(); + + pwd +} + /// Request the user to authenticate with a password pub(crate) fn request_auth( msg: &str, @@ -35,15 +62,7 @@ pub(crate) fn request_auth( let pwd = match password.as_ref() { Some(p) => p.to_string(), - None => { - let q = Question::password("password") - .message(format!("{}:", msg)) - .mask('*') - .build(); - - let a = requestty::prompt_one(q)?; - a.as_string().expect("answer to be a string").into() - } + None => ask_pwd(msg)?, }; Ok(hash(file_version, &pwd)) @@ -56,37 +75,7 @@ pub(crate) fn create_password( ) -> anyhow::Result> { let pwd = match password.as_ref() { Some(p) => p.to_string(), - None => { - let mut pwd = String::from(""); - - let mut pwds_match = false; - while !pwds_match { - // enter password - let q = Question::password("password") - .message("Enter the password for the wallet:") - .mask('*') - .build(); - let a = requestty::prompt_one(q)?; - let pwd1 = a.as_string().expect("answer to be a string"); - - // confirm password - let q = Question::password("password") - .message("Please confirm the password:") - .mask('*') - .build(); - let a = requestty::prompt_one(q)?; - let pwd2 = a.as_string().expect("answer to be a string"); - - // check match - pwds_match = pwd1 == pwd2; - if pwds_match { - pwd = pwd1.to_string() - } else { - println!("Passwords don't match, please try again."); - } - } - pwd - } + None => create_new_password()?, }; Ok(hash(file_version, &pwd)) @@ -98,23 +87,20 @@ where S: std::fmt::Display, { // inform the user about the mnemonic phrase - println!("The following phrase is essential for you to regain access to your wallet\nin case you lose access to this computer."); - println!("Please print it or write it down and store it somewhere safe:"); - println!(); - println!("> {}", phrase); - println!(); + let msg = format!("The following phrase is essential for you to regain access to your wallet\nin case you lose access to this computer. Please print it or write it down and store it somewhere safe.\n> {} \nHave you backed up this phrase?", phrase); // let the user confirm they have backed up their phrase - loop { - let q = requestty::Question::confirm("proceed") - .message("Have you backed up your mnemonic phrase?") - .build(); - - let a = requestty::prompt_one(q)?; - if a.as_bool().expect("answer to be a bool") { - return Ok(()); - } + let confirm = Confirm::new(&msg) + .with_help_message( + "It is important you backup the mnemonic phrase before proceeding", + ) + .prompt()?; + + if !confirm { + confirm_mnemonic_phrase(phrase)? } + + Ok(()) } /// Request the user to input the mnemonic phrase @@ -122,14 +108,9 @@ pub(crate) fn request_mnemonic_phrase() -> anyhow::Result { // let the user input the mnemonic phrase let mut attempt = 1; loop { - let q = Question::input("phrase") - .message("Please enter the mnemonic phrase:") - .build(); + let phrase = Text::new("Please enter the mnemonic phrase").prompt()?; - let a = requestty::prompt_one(q)?; - let phrase = a.as_string().expect("answer to be a string"); - - match Mnemonic::from_phrase(phrase, Language::English) { + match Mnemonic::from_phrase(&phrase, Language::English) { Ok(phrase) => break Ok(phrase.to_string()), Err(err) if attempt > 2 => match err.downcast_ref::() { @@ -146,12 +127,6 @@ pub(crate) fn request_mnemonic_phrase() -> anyhow::Result { } } -fn is_valid_dir(dir: &str) -> bool { - let mut p = std::path::PathBuf::new(); - p.push(dir); - p.is_dir() -} - /// Use sha256 for Rusk Binary Format, and blake for the rest fn hash(file_version: DatFileVersion, pwd: &str) -> Vec { match file_version { @@ -171,95 +146,100 @@ pub(crate) fn request_dir( what_for: &str, profile: PathBuf, ) -> Result { - let q = Question::input("name") - .message(format!("Please enter a directory to {}:", what_for)) - .default(profile.as_os_str().to_str().expect("default dir")) - .validate_on_key(|dir, _| is_valid_dir(dir)) - .validate(|dir, _| { - if is_valid_dir(dir) { - Ok(()) - } else { - Err("Not a valid directory".to_string()) - } - }) - .build(); + let validator = |dir: &str| { + let path = PathBuf::from(dir); + + if path.is_dir() { + Ok(Validation::Valid) + } else { + Ok(Validation::Invalid("Not a valid directory".into())) + } + }; + + 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 p = PathBuf::from(q); - let a = requestty::prompt_one(q)?; - let mut p = std::path::PathBuf::new(); - p.push(a.as_string().expect("answer to be a string")); Ok(p) } /// Asks the user for confirmation pub(crate) fn ask_confirm() -> anyhow::Result { - let q = requestty::Question::confirm("confirm") - .message("Transaction ready. Proceed?") - .build(); - let a = requestty::prompt_one(q)?; - Ok(a.as_bool().expect("answer to be a bool")) + Ok(Confirm::new("Transaction ready. Proceed?") + .with_default(true) + .prompt()?) } /// Asks the user for confirmation before deleting cache pub(crate) fn ask_confirm_erase_cache(msg: &str) -> anyhow::Result { - let q = requestty::Question::confirm("confirm").message(msg).build(); - let a = requestty::prompt_one(q)?; - Ok(a.as_bool().expect("answer to be a bool")) + Ok(Confirm::new(msg).prompt()?) } /// Request a receiver address pub(crate) fn request_rcvr_addr(addr_for: &str) -> anyhow::Result
{ // let the user input the receiver address - let q = Question::input("addr") - .message(format!("Please enter the {} address:", addr_for)) - .validate_on_key(|addr, _| Address::from_str(addr).is_ok()) - .validate(|addr, _| { - if Address::from_str(addr).is_ok() { - Ok(()) - } else { - Err("Please introduce a valid DUSK address".to_string()) - } - }) - .build(); - - let a = requestty::prompt_one(q)?; Ok(Address::from_str( - a.as_string().expect("answer to be a string"), + &Text::new(format!("Please enter the {} address:", addr_for).as_str()) + .with_validator(|addr: &str| { + if Address::from_str(addr).is_ok() { + Ok(Validation::Valid) + } else { + Ok(Validation::Invalid( + "Please introduce a valid DUSK address".into(), + )) + } + }) + .prompt()?, )?) } -/// Checks if the value is larger than the given min and smaller than the -/// min of the balance and `MAX_CONVERTIBLE`. -fn check_valid_denom( - value: f64, - min: Dusk, - balance: Dusk, -) -> Result<(), String> { - let value = Dusk::from(value); - let max = std::cmp::min(balance, MAX_CONVERTIBLE); - match (min..=max).contains(&value) { - true => Ok(()), - false => { - Err(format!("The amount has to be between {} and {}", min, max)) - } - } -} - /// Request an amount of token larger than a given min. fn request_token( action: &str, min: Dusk, balance: Dusk, + default: Option, ) -> anyhow::Result { - let question = requestty::Question::float("amt") - .message(format!("Introduce the amount of DUSK to {}:", action)) - .default(min.into()) - .validate_on_key(|f, _| check_valid_denom(f, min, balance).is_ok()) - .validate(|f, _| check_valid_denom(f, min, balance)) - .build(); + // 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)) { + true => Ok(Validation::Valid), + false => Ok(Validation::Invalid( + format!("The amount has to be between {} and {}", min, max) + .into(), + )), + } + }; - let a = requestty::prompt_one(question)?; + let msg = format!("Introduce dusk amount for {}", action); + + let amount_prompt: CustomType = CustomType { + message: &msg, + starting_input: None, + formatter: &|i| format!("DUSK {}", i), + default_value_formatter: &|i| format!("DUSK {}", i), + default, + validators: vec![Box::new(validator)], + placeholder: Some("123.45"), + error_message: "Please type a valid number.".into(), + help_message: "The number should use a dot as the decimal separator." + .into(), + parser: &|i| match i.parse::() { + Ok(val) => Ok(val), + Err(_) => Err(()), + }, + render_config: RenderConfig::default(), + }; - Ok(a.as_float().expect("answer to be a float").into()) + Ok(amount_prompt.prompt()?.into()) } /// Request a positive amount of tokens @@ -268,7 +248,8 @@ pub(crate) fn request_token_amt( balance: Dusk, ) -> anyhow::Result { let min = MIN_CONVERTIBLE; - request_token(action, min, balance) + + request_token(action, min, balance, None) } /// Request amount of tokens that can be 0 @@ -277,32 +258,31 @@ pub(crate) fn request_optional_token_amt( balance: Dusk, ) -> anyhow::Result { let min = Dusk::from(0); - request_token(action, min, balance) + + request_token(action, min, balance, None) } /// Request amount of tokens that can't be lower than MINIMUM_STAKE pub(crate) fn request_stake_token_amt(balance: Dusk) -> anyhow::Result { let min: Dusk = MINIMUM_STAKE.into(); - request_token("stake", min, balance) + + request_token("stake", min, balance, None) } /// Request gas limit pub(crate) fn request_gas_limit(default_gas_limit: u64) -> anyhow::Result { - let question = requestty::Question::int("amt") - .message("Introduce the gas limit for this transaction:") - .default(default_gas_limit as i64) - .validate_on_key(|n, _| n > (gas::MIN_LIMIT as i64)) - .validate(|n, _| { - if n < gas::MIN_LIMIT as i64 { - Err("Gas limit too low".to_owned()) - } else { - Ok(()) - } - }) - .build(); - - let a = requestty::prompt_one(question)?; - Ok(a.as_int().expect("answer to be an int") as u64) + Ok( + CustomType::::new("Introduce the gas limit for this transaction") + .with_default(default_gas_limit) + .with_validator(|n: &u64| { + if *n < gas::MIN_LIMIT { + Ok(Validation::Invalid("Gas limit too low".into())) + } else { + Ok(Validation::Valid) + } + }) + .prompt()?, + ) } /// Request gas price @@ -316,50 +296,36 @@ pub(crate) fn request_gas_price( min_gas_price }; - let default_gas_price = Dusk::from(default_gas_price).into(); - let min_gas_price = Dusk::from(min_gas_price).into(); - - let question = requestty::Question::float("amt") - .message("Introduce the gas price for this transaction:") - .default(default_gas_price) - .validate_on_key(|f, _| { - check_valid_denom(f, MIN_CONVERTIBLE, MAX_CONVERTIBLE).is_ok() - }) - .validate(|f, _| check_valid_denom(f, MIN_CONVERTIBLE, MAX_CONVERTIBLE)) - .validate(|f, _| { - if f < min_gas_price { - Err("Gas limit too low".to_owned()) - } else { - Ok(()) - } - }) - .build(); - - let a = requestty::prompt_one(question)?; - let price = Dusk::from(a.as_float().expect("answer to be a float")); - Ok(*price) + request_token( + "gas price", + Dusk::from(min_gas_price), + MAX_CONVERTIBLE, + Some(default_gas_price as f64), + ) + .map(|dusk| *dusk) } pub(crate) fn request_str( name: &str, max_length: usize, ) -> anyhow::Result { - let question = requestty::Question::input("string") - .message(format!("Introduce string for {}:", name)) - .validate(|input, _| { - if input.len() > max_length { - Err(format!( - "Input exceeds the maximum length of {} characters", - max_length - )) - } else { - Ok(()) - } - }) - .build(); - - let a = requestty::prompt_one(question)?; - Ok(a.as_string().expect("answer to be a string").to_owned()) + Ok( + Text::new(format!("Introduce string for {}:", name).as_str()) + .with_validator(move |input: &str| { + if input.len() > max_length { + Ok(Validation::Invalid( + format!( + "Input exceeds the maximum length of {} characters", + max_length + ) + .into(), + )) + } else { + Ok(Validation::Valid) + } + }) + .prompt()?, + ) } pub enum TransactionModel { @@ -367,98 +333,64 @@ pub enum TransactionModel { Public, } -impl From<&str> for TransactionModel { - fn from(value: &str) -> Self { - match value { - "Shielded" => TransactionModel::Shielded, - "Public" => TransactionModel::Public, - _ => panic!("Unknown transaction model"), +impl Display for TransactionModel { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + TransactionModel::Shielded => write!(f, "Shielded"), + TransactionModel::Public => write!(f, "Public"), } } } /// Request transaction model to use pub(crate) fn request_transaction_model() -> anyhow::Result { - let question = requestty::Question::select( - "Please specify the transaction model to use", + let choices = vec![TransactionModel::Shielded, TransactionModel::Public]; + + Ok( + Select::new("Please specify the transaction model to use", choices) + .prompt()?, ) - .choices(vec![Choice("Public".into()), "Shielded".into()]) - .build(); - - let a = requestty::prompt_one(question)?; - Ok(a.as_list_item() - .expect("answer must be a list item") - .text - .as_str() - .into()) } /// Request contract WASM file location pub(crate) fn request_contract_code() -> anyhow::Result { - let question = requestty::Question::input("Location of the WASM contract") - .message("Location of the WASM file:") - .validate_on_key(|f, _| PathBuf::from(f).exists()) - .validate(|f, _| { - PathBuf::from(f) - .exists() - .then_some(()) - .ok_or("File not found".to_owned()) - }) - .build(); - - let a = requestty::prompt_one(question)?; - let location = a.as_string().expect("answer to be a string").to_owned(); - - Ok(PathBuf::from(location)) + request_dir("Location of the WASM contract", PathBuf::new()) } pub(crate) fn request_bytes(name: &str) -> anyhow::Result> { - let question = requestty::Question::input("bytes") - .message(format!("Introduce bytes for {}", name)) - .validate_on_key(|f, _| hex::decode(f).is_ok()) - .validate(|f, _| { - hex::decode(f) - .is_ok() - .then_some(()) - .ok_or("Invalid hex string".to_owned()) - }) - .build(); - - let a = requestty::prompt_one(question)?; - let bytes = hex::decode(a.as_string().expect("answer to be a string"))?; + let byte_string = + Text::new(format!("Introduce hex bytes for {}", name).as_str()) + .with_validator(|f: &str| match hex::decode(f) { + Ok(_) => Ok(Validation::Valid), + Err(_) => Ok(Validation::Invalid("Invalid hex string".into())), + }) + .prompt()?; + + let bytes = hex::decode(byte_string)?; Ok(bytes) } pub(crate) fn request_nonce() -> anyhow::Result { - let question = requestty::Question::input("Contract Deployment nonce") - .message("Introduce a number for nonce") - .validate_on_key(|f, _| u64::from_str(f).is_ok()) - .validate(|f, _| { - u64::from_str(f) - .is_ok() - .then_some(()) - .ok_or("Invalid number".to_owned()) - }) - .build(); - - let a = requestty::prompt_one(question)?; - let bytes = u64::from_str(a.as_string().expect("answer to be a string"))?; + let nonce_string = + Text::new("Introduce a number for Contract Deployment nonce") + .with_validator(|f: &str| match u64::from_str(f) { + Ok(_) => Ok(Validation::Valid), + Err(_) => Ok(Validation::Invalid("Invalid u64 nonce".into())), + }) + .prompt()?; + + let bytes = u64::from_str(&nonce_string)?; Ok(bytes) } /// Request Dusk block explorer to be opened pub(crate) fn launch_explorer(url: String) -> Result<()> { - let q = requestty::Question::confirm("launch") - .message("Launch block explorer?") - .build(); - - let a = requestty::prompt_one(q)?; - let open = a.as_bool().expect("answer to be a bool"); - if open { + if Confirm::new("Launch block explorer?").prompt()? { open::that(url)?; } + Ok(()) } diff --git a/rusk-wallet/src/bin/io/status.rs b/rusk-wallet/src/bin/io/status.rs index d24b7148c2..dfceddc9df 100644 --- a/rusk-wallet/src/bin/io/status.rs +++ b/rusk-wallet/src/bin/io/status.rs @@ -4,18 +4,14 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use std::io::{stdout, Write}; -use std::thread; -use std::time::Duration; - use tracing::info; /// Prints an interactive status message -pub(crate) fn interactive(status: &str) { - print!("\r{status: <50}\r"); - let mut stdout = stdout(); - stdout.flush().unwrap(); - thread::sleep(Duration::from_millis(85)); +pub(crate) fn interactive(_status: &str) { + // FIXME: We currently don't print callback + // messages from wallet functions because we + // haven't found a constructive way to do so + // See issue #2962 } /// Logs the status message at info level diff --git a/rusk-wallet/src/bin/main.rs b/rusk-wallet/src/bin/main.rs index 3fedf09ed7..94574d61f8 100644 --- a/rusk-wallet/src/bin/main.rs +++ b/rusk-wallet/src/bin/main.rs @@ -8,14 +8,13 @@ mod command; mod config; mod interactive; mod io; -mod menu; mod settings; pub(crate) use command::{Command, RunResult}; -pub(crate) use menu::Menu; use bip39::{Language, Mnemonic, MnemonicType}; use clap::Parser; +use inquire::InquireError; use rocksdb::ErrorKind; use tracing::{error, info, warn, Level}; @@ -25,12 +24,11 @@ use crate::settings::{LogFormat, Settings}; use rusk_wallet::{ currency::Dusk, dat::{self, LATEST_VERSION}, - Error, Profile, SecureWalletFile, Wallet, WalletPath, EPOCH, + Error, GraphQL, Profile, SecureWalletFile, Wallet, WalletPath, EPOCH, }; use config::Config; -use io::{prompt, status}; -use io::{GraphQL, WalletArgs}; +use io::{prompt, status, WalletArgs}; use std::fs::{self, File}; use std::io::Write; @@ -55,8 +53,8 @@ impl SecureWalletFile for WalletFile { async fn main() -> anyhow::Result<()> { if let Err(err) = exec().await { // display the error message (if any) - match err.downcast_ref::() { - Some(requestty::ErrorKind::Interrupted) => { + match err.downcast_ref::() { + Some(InquireError::OperationInterrupted) => { // TODO: Handle this error properly // See also https://github.com/dusk-network/wallet-cli/issues/104 } @@ -132,10 +130,6 @@ async fn exec() -> anyhow::Result<()> { // get the subcommand, if it is `None` we run the wallet in interactive mode let cmd = args.command.clone(); - // set symbols to ASCII for Windows terminal compatibility - #[cfg(windows)] - requestty::symbols::set(requestty::symbols::ASCII); - // Get the initial settings from the args let settings_builder = Settings::args(args); @@ -207,7 +201,8 @@ async fn exec() -> anyhow::Result<()> { // if `cmd` is `None` we are in interactive mode and need to load the // wallet from file None => { - interactive::load_wallet(&wallet_path, &settings, file_version)? + interactive::load_wallet(&wallet_path, &settings, file_version) + .await? } // else we check if we need to replace the wallet and then load it Some(ref cmd) => match cmd { @@ -413,7 +408,6 @@ async fn exec() -> anyhow::Result<()> { } } - // Gracefully close the wallet wallet.close(); Ok(()) diff --git a/rusk-wallet/src/bin/menu.rs b/rusk-wallet/src/bin/menu.rs deleted file mode 100644 index 9874b614e9..0000000000 --- a/rusk-wallet/src/bin/menu.rs +++ /dev/null @@ -1,96 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. -// -// Copyright (c) DUSK NETWORK. All rights reserved. - -use core::fmt::Debug; -use std::collections::HashMap; -use std::hash::Hash; - -use requestty::question::Choice; -use requestty::{Answer, DefaultSeparator, Separator}; - -#[derive(Clone, Debug)] -pub struct Menu { - items: Vec>, - keys: HashMap, -} - -impl Default for Menu -where - K: Eq + Hash + Debug, -{ - fn default() -> Self { - Self::new() - } -} - -impl Menu -where - K: Eq + Hash + Debug, -{ - pub fn new() -> Self { - Self { - items: vec![], - keys: HashMap::new(), - } - } - - pub fn title(title: T) -> Self - where - T: Into, - { - let title = format!("─ {:─<12}", format!("{} ", title.into())); - let title = Separator(title); - let items = vec![title]; - let keys = HashMap::new(); - - Self { items, keys } - } - - pub fn add(mut self, key: K, item: V) -> Self - where - V: Into>, - { - self.items.push(item.into()); - self.keys.insert(self.items.len() - 1, key); - self - } - - pub fn separator(mut self) -> Self { - self.items.push(DefaultSeparator); - self - } - - pub fn separator_msg>(mut self, msg: S) -> Self { - self.items.push(Separator(msg.into())); - self - } - - pub fn answer(&self, answer: &Answer) -> &K { - let index = answer.as_list_item().unwrap().index; - let key = self.keys.get(&index); - key.unwrap() - } - - pub fn extend(mut self, other: Self) -> Self { - let len = self.items.len(); - - self.items.extend(other.items); - - for (key, val) in other.keys.into_iter() { - self.keys.insert(key + len, val); - } - - self - } -} - -impl IntoIterator for Menu { - type Item = Choice; - type IntoIter = std::vec::IntoIter>; - fn into_iter(self) -> Self::IntoIter { - self.items.into_iter() - } -} diff --git a/rusk-wallet/src/bin/settings.rs b/rusk-wallet/src/bin/settings.rs index 3208936d9e..7a23f767cb 100644 --- a/rusk-wallet/src/bin/settings.rs +++ b/rusk-wallet/src/bin/settings.rs @@ -7,7 +7,7 @@ use crate::config::Network; use crate::io::WalletArgs; -use rusk_wallet::Error; +use rusk_wallet::{Error, RuesHttpClient}; use std::fmt; use std::path::PathBuf; @@ -135,6 +135,18 @@ impl Settings { SettingsBuilder { wallet_dir, args } } + + pub async fn check_state_con(&self) -> Result<(), reqwest::Error> { + RuesHttpClient::new(self.state.as_ref()) + .check_connection() + .await + } + + pub async fn check_prover_con(&self) -> Result<(), reqwest::Error> { + RuesHttpClient::new(self.prover.as_ref()) + .check_connection() + .await + } } impl From<&LogLevel> for Level { diff --git a/rusk-wallet/src/gql.rs b/rusk-wallet/src/gql.rs index cd71ec3c53..00e347c417 100644 --- a/rusk-wallet/src/gql.rs +++ b/rusk-wallet/src/gql.rs @@ -4,8 +4,8 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -//! The graphql endpoint can be queried with this helper struct. -//! The /on/gaphql/query if queried with empty bytes returns the +//! The graphql endpoint can be queried with this helper struct. +//! The /on/gaphql/query if queried with empty bytes returns the //! graphql schema use execution_core::transfer::Transaction; @@ -14,9 +14,6 @@ use tokio::time::{sleep, Duration}; use crate::{Error, RuesHttpClient}; use serde::Deserialize; - - - /// GraphQL is a helper struct that aggregates all queries done /// to the Dusk GraphQL database. /// This helps avoid having helper structs and boilerplate code @@ -27,11 +24,11 @@ pub struct GraphQL { status: fn(&str), } -/// The tx_for_block returns a Vec which contains +/// The tx_for_block returns a Vec which contains /// the execution-core transaction, its id hash and gas spent -pub struct TransactionBlock { - /// The execution transacton struct obtianed from grahpql endpoint - pub tx: Transaction, +pub struct BlockTransaction { + /// The execution-core transaction struct obtained from GraphQL endpoint + pub tx: Transaction, /// The hash of the transaction or the id of the transaction in string utf8 pub id: String, /// Gas amount spent for the transaction @@ -65,7 +62,6 @@ struct SpentTxResponse { /// Transaction status #[derive(Debug)] -#[allow(missing_docs)] pub enum TxStatus { Ok, NotFound, @@ -121,7 +117,7 @@ impl GraphQL { pub async fn txs_for_block( &self, block_height: u64, - ) -> anyhow::Result, GraphQLError> { + ) -> anyhow::Result, GraphQLError> { let query = "query { block(height: ####) { transactions {id, raw, gasSpent, err}}}" .replace("####", block_height.to_string().as_str()); @@ -135,8 +131,12 @@ impl GraphQL { let tx_raw = hex::decode(&spent_tx.raw) .map_err(|_| GraphQLError::TxStatus)?; let ph_tx = Transaction::from_slice(&tx_raw).unwrap(); - - ret.push(TransactionBlock { tx: ph_tx, id: spent_tx.id, gas_spent: spent_tx.gas_spent as u64 }); + + ret.push(BlockTransaction { + tx: ph_tx, + id: spent_tx.id, + gas_spent: spent_tx.gas_spent as u64, + }); } Ok(ret) @@ -200,8 +200,10 @@ async fn test() -> Result<(), Box> { ) .await?; let block_txs = gql.txs_for_block(90).await?; - block_txs.into_iter().for_each(|(t, chain_txid, _)| { - let hash = t.hash(); + block_txs.into_iter().for_each(|tx_block| { + let tx = tx_block.tx; + let chain_txid = tx_block.id; + let hash = tx.hash(); let tx_id = hex::encode(hash.to_bytes()); assert_eq!(chain_txid, tx_id); println!("txid: {tx_id}"); diff --git a/rusk-wallet/src/lib.rs b/rusk-wallet/src/lib.rs index c8e07b1cf5..3f169643c2 100644 --- a/rusk-wallet/src/lib.rs +++ b/rusk-wallet/src/lib.rs @@ -18,6 +18,7 @@ mod cache; mod clients; mod crypto; mod error; +mod gql; mod rues; mod store; mod wallet; @@ -33,6 +34,8 @@ pub use wallet::{ Address, DecodedNote, Profile, SecureWalletFile, Wallet, WalletPath, }; +pub use gql::{BlockTransaction, GraphQL}; + use execution_core::{ dusk, from_dusk, stake::StakeData, diff --git a/rusk-wallet/src/rues.rs b/rusk-wallet/src/rues.rs index d9f9a58acc..a3b88c618c 100644 --- a/rusk-wallet/src/rues.rs +++ b/rusk-wallet/src/rues.rs @@ -9,7 +9,7 @@ use std::time::Duration; use reqwest::{Body, Response}; use rkyv::Archive; -use crate::{gql::GraphQL, Error}; +use crate::Error; /// Supported Rusk version const REQUIRED_RUSK_VERSION: &str = ">=0.8.0"; @@ -59,6 +59,7 @@ impl RuesHttpClient { /// Check rusk connection pub async fn check_connection(&self) -> Result<(), reqwest::Error> { self.client.post(&self.uri).send().await?; + Ok(()) } diff --git a/rusk-wallet/src/wallet/address.rs b/rusk-wallet/src/wallet/address.rs index 2a5a498285..1e55903ca1 100644 --- a/rusk-wallet/src/wallet/address.rs +++ b/rusk-wallet/src/wallet/address.rs @@ -153,6 +153,7 @@ impl fmt::Debug for Address { /// Profile struct containing the addresses used for shielded and public /// transactions as well as for staking operations. +#[derive(Debug, PartialEq, Eq)] pub struct Profile { /// Shielded address for shielded transactions pub shielded_addr: PhoenixPublicKey,