From ab83d7c95caaa8c2b4af04387107b25aee20d045 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Wed, 9 Aug 2023 20:11:44 +0300 Subject: [PATCH] wallet + ui: optimize loading, retry button for non-success loading, update translations --- locales/en.yml | 5 +- locales/ru.yml | 5 +- src/gui/views/wallets/content.rs | 14 +- src/gui/views/wallets/wallet/content.rs | 95 +++++++----- src/gui/views/wallets/wallet/info.rs | 2 +- src/wallet/wallets.rs | 189 ++++++++++++++---------- 6 files changed, 191 insertions(+), 119 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 9e5dd6d..182c84b 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -2,6 +2,9 @@ copy: Copy paste: Paste continue: Continue complete: Complete +loading: Loading +loading_error: Loading error +retry: Retry wallets: title: Wallets create_desc: Create or import existing wallet from saved recovery phrase. @@ -31,7 +34,7 @@ wallets: unlocked: Unlocked enable_node: 'Enable integrated node to use the wallet or change connection settings by selecting %{settings} at the bottom of the screen.' wallet_loading: 'Wallet is loading' - wallet_loading_err: 'An error occurred during loading the wallet, you can change connection settings by selecting %{settings} at the bottom of the screen.' + wallet_loading_err: 'An error occurred during loading the wallet, you can retry or change connection settings by selecting %{settings} at the bottom of the screen.' wallet: Wallet send: Send receive: Receive diff --git a/locales/ru.yml b/locales/ru.yml index 5d804fa..7776cef 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -2,6 +2,9 @@ copy: Копировать paste: Вставить continue: Продолжить complete: Завершить +loading: Загружается +loading_error: Ошибка загрузки +retry: Повторить wallets: title: Кошельки create_desc: Создайте или импортируйте существующий кошелёк из сохранённой фразы восстановления. @@ -31,7 +34,7 @@ wallets: unlocked: Разблокирован enable_node: 'Чтобы использовать кошелёк, включите встроенный узел или измените настройки подключения, выбрав %{settings} внизу экрана.' wallet_loading: 'Кошелёк загружается' - wallet_loading_err: 'Во время загрузки кошелька произошла ошибка, вы можете изменить настройки подключения, выбрав %{settings} внизу экрана.' + wallet_loading_err: 'Во время загрузки кошелька произошла ошибка, вы можете повторить попытку или изменить настройки подключения, выбрав %{settings} внизу экрана.' wallet: Кошелёк send: Отправить receive: Получить diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index efe6377..3c09e66 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -18,7 +18,7 @@ use grin_core::global::ChainTypes; use crate::AppConfig; use crate::gui::Colors; -use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, EYE, EYE_SLASH, FOLDER_LOCK, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE}; +use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, EYE, EYE_SLASH, FOLDER_LOCK, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SPINNER, SUITCASE, WARNING_CIRCLE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, Root, TitlePanel, View}; use crate::gui::views::types::{ModalContainer, ModalPosition, TitleType}; @@ -408,7 +408,15 @@ impl WalletsContent { // Setup wallet status text. let status_text = if wallet.is_open() { - format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked")) + if wallet.get_info().is_none() { + if wallet.loading_error() { + format!("{} {}", WARNING_CIRCLE, t!("loading_error")) + } else { + format!("{} {}", SPINNER, t!("loading")) + } + } else { + format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked")) + } } else { format!("{} {}", FOLDER_LOCK, t!("wallets.locked")) }; @@ -532,7 +540,7 @@ impl WalletsContent { if self.pass_edit.is_empty() { return; } - match self.wallets.launch_selected(self.pass_edit.clone()) { + match self.wallets.open_selected(self.pass_edit.clone()) { Ok(_) => { // Clear values. self.pass_edit = "".to_string(); diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 6032c1b..52b502d 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -15,9 +15,11 @@ use std::time::Duration; use egui::{Margin, RichText}; +use grin_chain::SyncStatus; +use crate::AppConfig; use crate::gui::Colors; -use crate::gui::icons::{DOWNLOAD, GEAR_FINE, POWER, UPLOAD, WALLET}; +use crate::gui::icons::{DOWNLOAD, GEAR_FINE, POWER, REPEAT, UPLOAD, WALLET}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Root, View}; use crate::gui::views::wallets::{WalletInfo, WalletReceive, WalletSend, WalletSettings}; @@ -119,48 +121,63 @@ impl WalletContent { }); } - /// Content to draw when wallet is loading. - pub fn show_loading_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, wallet: &Wallet) -> bool { - if wallet.config.ext_conn_id.is_none() && !Node::is_running() { - let dual_panel_root = Root::is_dual_panel_mode(frame); - View::center_content(ui, if !dual_panel_root { 162.0 } else { 96.0 }, |ui| { - let text = t!("wallets.enable_node", "settings" => GEAR_FINE); - ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT)); - // Show button to enable integrated node at non-dual root panel mode. - if !dual_panel_root { + /// Content to draw when wallet is loading, returns `true` if wallet is not ready. + pub fn loading_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, wallet: &Wallet) -> bool { + if wallet.config.ext_conn_id.is_none() { + if !Node::is_running() || Node::is_stopping() { + let dual_panel_root = Root::is_dual_panel_mode(frame); + View::center_content(ui, if !dual_panel_root { 162.0 } else { 96.0 }, |ui| { + let text = t!("wallets.enable_node", "settings" => GEAR_FINE); + ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT)); ui.add_space(8.0); - let enable_node_text = format!("{} {}", POWER, t!("network.enable_node")); - View::button(ui, enable_node_text, Colors::GOLD, || { - Node::start(); - }); - } - }); - return true; - } else { - if wallet.get_info().is_none() || wallet.loading_progress() < 100 { - if wallet.loading_error() { - View::center_content(ui, 96.0, |ui| { - let text = t!("wallets.wallet_loading_err", "settings" => GEAR_FINE); - ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT)); - }); - } else { - View::center_content(ui, 162.0, |ui| { - View::big_loading_spinner(ui); - ui.add_space(18.0); - // Setup loading progress text. - let progress = wallet.loading_progress(); - let text = if progress == 0 { - t!("wallets.wallet_loading") - } else { - format!("{}: {}%", t!("wallets.wallet_loading"), progress) - }; - ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT)); - }); - } + // Show button to enable integrated node at non-dual root panel mode + // or when network connections are not showing and node is not stopping + if (!dual_panel_root || AppConfig::show_connections_network_panel()) + && !Node::is_stopping() { + let enable_node_text = format!("{} {}", POWER, t!("network.enable_node")); + View::button(ui, enable_node_text, Colors::GOLD, || { + Node::start(); + }); + } + }); + return true + } else if wallet.get_info().is_none() { + Self::progress_ui(ui, wallet); return true; - } else { } + } else if wallet.get_info().is_none() { + // Show error message with button to retry on wallet loading error or loading progress. + if wallet.loading_error() { + View::center_content(ui, 162.0, |ui| { + let text = t!("wallets.wallet_loading_err", "settings" => GEAR_FINE); + ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT)); + ui.add_space(8.0); + let retry_text = format!("{} {}", REPEAT, t!("retry")); + View::button(ui, retry_text, Colors::GOLD, || { + wallet.set_loading_error(false); + }); + }); + } else { + Self::progress_ui(ui, wallet); + } + return true; } false } + + /// Draw wallet loading progress. + fn progress_ui(ui: &mut egui::Ui, wallet: &Wallet) { + View::center_content(ui, 162.0, |ui| { + View::big_loading_spinner(ui); + ui.add_space(18.0); + // Setup loading progress text. + let progress = wallet.loading_progress(); + let text = if progress == 0 { + t!("wallets.wallet_loading") + } else { + format!("{}: {}%", t!("wallets.wallet_loading"), progress) + }; + ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT)); + }); + } } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/info.rs b/src/gui/views/wallets/wallet/info.rs index f814a32..85574e9 100644 --- a/src/gui/views/wallets/wallet/info.rs +++ b/src/gui/views/wallets/wallet/info.rs @@ -32,6 +32,6 @@ impl WalletTab for WalletInfo { frame: &mut eframe::Frame, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - WalletContent::show_loading_ui(ui, frame, wallet); + WalletContent::loading_ui(ui, frame, wallet); } } \ No newline at end of file diff --git a/src/wallet/wallets.rs b/src/wallet/wallets.rs index 1459e61..611237e 100644 --- a/src/wallet/wallets.rs +++ b/src/wallet/wallets.rs @@ -14,10 +14,11 @@ use std::path::PathBuf; use std::sync::{Arc, mpsc, RwLock}; -use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU8, Ordering}; use std::thread; use std::time::Duration; +use grin_chain::SyncStatus; use grin_core::global; use grin_core::global::ChainTypes; use grin_keychain::{ExtKeychain, Keychain}; @@ -29,7 +30,7 @@ use grin_wallet_libwallet::api_impl::owner::retrieve_summary_info; use parking_lot::Mutex; use crate::AppConfig; -use crate::node::NodeConfig; +use crate::node::{Node, NodeConfig}; use crate::wallet::{ConnectionsConfig, ExternalConnection, WalletConfig}; use crate::wallet::types::{ConnectionMethod, WalletInstance}; @@ -51,7 +52,7 @@ impl Default for Wallets { } impl Wallets { - /// Initialize wallets from base directory for provided [`ChainType`]. + /// Initialize [`Wallet`] list from base directory for provided [`ChainType`]. fn init(chain_type: ChainTypes) -> Vec { let mut wallets = Vec::new(); let wallets_dir = WalletConfig::get_base_path(chain_type); @@ -68,7 +69,7 @@ impl Wallets { wallets } - /// Reinitialize wallets for provided [`ChainTypes`]. + /// Reinitialize [`Wallet`] list for provided [`ChainTypes`]. pub fn reinit(&mut self, chain_type: ChainTypes) { for w in self.list.iter_mut() { let _ = w.close(); @@ -82,17 +83,17 @@ impl Wallets { self.list.insert(0, wallet); } - /// Select wallet with provided identifier. + /// Select [`Wallet`] with provided identifier. pub fn select(&mut self, id: Option) { self.selected_id = id; } - /// Check if wallet is selected for provided identifier. + /// Check if [`Wallet`] is selected for provided identifier. pub fn is_selected(&self, id: i64) -> bool { return Some(id) == self.selected_id; } - /// Check if selected wallet is open. + /// Check if selected [`Wallet`] is open. pub fn is_selected_open(&self) -> bool { for w in &self.list { if Some(w.config.id) == self.selected_id { @@ -102,8 +103,8 @@ impl Wallets { false } - /// Open and load selected wallet. - pub fn launch_selected(&mut self, password: String) -> Result<(), Error> { + /// Open selected [`Wallet`]. + pub fn open_selected(&mut self, password: String) -> Result<(), Error> { for w in self.list.iter_mut() { if Some(w.config.id) == self.selected_id { return w.open(password.clone()); @@ -113,33 +114,36 @@ impl Wallets { } } -/// Contains wallet instance and config. +/// Contains wallet instance and handles wallet commands. #[derive(Clone)] pub struct Wallet { /// Wallet instance. instance: WalletInstance, - /// Wallet configuration. pub config: WalletConfig, + /// Identifier for launched wallet thread. + thread_id: Arc, + /// Flag to check if wallet is open. is_open: Arc, /// Error on wallet loading. loading_error: Arc, /// Loading progress in percents - pub loading_progress: Arc, + loading_progress: Arc, /// Wallet balance information. info: Arc>> } impl Wallet { - /// Create wallet from provided instance and config. + /// Instantiate [`Wallet`] from provided [`WalletInstance`] and [`WalletConfig`]. fn new(instance: WalletInstance, config: WalletConfig) -> Self { Self { instance, config, + thread_id: Arc::new(AtomicI64::new(0)), is_open: Arc::from(AtomicBool::new(false)), loading_error: Arc::from(AtomicBool::new(false)), loading_progress: Arc::new(AtomicU8::new(0)), @@ -170,7 +174,7 @@ impl Wallet { Ok(w) } - /// Initialize wallet from provided data path. + /// Initialize [`Wallet`] from provided data path. fn init(data_path: PathBuf) -> Option { let wallet_config = WalletConfig::load(data_path.clone()); if let Some(config) = wallet_config { @@ -181,14 +185,14 @@ impl Wallet { None } - /// Reinitialize wallet instance to apply new config e.g. on change connection settings. + /// Reinitialize [`WalletInstance`] to apply new [`WalletConfig`]. pub fn reinit(&mut self) -> Result<(), Error> { self.close()?; self.instance = Self::create_wallet_instance(self.config.clone())?; Ok(()) } - /// Create wallet instance from provided config. + /// Create [`WalletInstance`] from provided [`WalletConfig`]. fn create_wallet_instance(config: WalletConfig) -> Result { // Assume global chain type has already been initialized. let chain_type = config.chain_type; @@ -222,7 +226,7 @@ impl Wallet { Ok(wallet) } - /// Instantiate wallet from provided node client and config. + /// Instantiate [`WalletInstance`] from provided node client and [`WalletConfig`]. fn inst_wallet( config: WalletConfig, node_client: C, @@ -250,7 +254,7 @@ impl Wallet { match lc.open_wallet(None, ZeroingString::from(password), false, false) { Ok(keychain) => { self.is_open.store(true, Ordering::Relaxed); - // Start commands handling and updating info. + // Start info updating and commands handling. start_wallet(self.clone(), keychain); } Err(e) => return Err(e) @@ -258,7 +262,19 @@ impl Wallet { Ok(()) } - /// Get wallet loading progress. + /// Get wallet thread identifier. + fn get_thread_id(&self) -> i64 { + self.thread_id.load(Ordering::Relaxed) + } + + /// Refresh wallet thread identifier. + fn new_thread_id(&self) -> i64 { + let id = chrono::Utc::now().timestamp(); + self.thread_id.store(id, Ordering::Relaxed); + id + } + + /// Get wallet loading progress after opening. pub fn loading_progress(&self) -> u8 { self.loading_progress.load(Ordering::Relaxed) } @@ -268,23 +284,32 @@ impl Wallet { self.loading_error.load(Ordering::Relaxed) } - /// Check if wallet is open. + /// Set an error for wallet on loading. + pub fn set_loading_error(&self, error: bool) { + self.loading_error.store(error, Ordering::Relaxed); + } + + /// Check if wallet was open. pub fn is_open(&self) -> bool { self.is_open.load(Ordering::Relaxed) } - /// Close wallet. + /// Close the wallet. pub fn close(&mut self) -> Result<(), Error> { if self.is_open() { let mut wallet_lock = self.instance.lock(); let lc = wallet_lock.lc_provider()?; lc.close_wallet(None)?; + // Clear wallet info. + let mut w_info = self.info.write().unwrap(); + *w_info = None; + // Mark wallet as not opened. self.is_open.store(false, Ordering::Relaxed); } Ok(()) } - /// Get wallet info. + /// Get wallet balance info. pub fn get_info(&self) -> Option { let r_info = self.info.read().unwrap(); r_info.clone() @@ -294,71 +319,87 @@ impl Wallet { /// Delay in seconds to update wallet info. const INFO_UPDATE_DELAY: Duration = Duration::from_millis(20 * 1000); -/// Maximum number of attempts to load the wallet on start before stopping the loop. -const MAX_LOADING_ATTEMPTS: i8 = 10; +/// Launch thread for commands handling and info updating after [`INFO_UPDATE_DELAY`]. +fn start_wallet(mut wallet: Wallet, keychain_mask: Option) { + // Setup initial loading values. + wallet.loading_progress.store(0, Ordering::Relaxed); + wallet.set_loading_error(false); -/// Start wallet by launching separate thread to handle commands and updating info after delay. -fn start_wallet(wallet: Wallet, keychain_mask: Option) { // Launch loop at separate thread to update wallet info. - let mut loading_attempts = 1; + let thread_id = wallet.new_thread_id(); thread::spawn(move || loop { - // Stop updating if wallet was closed. - if !wallet.is_open() { + // Stop updating if wallet was closed or thread changed. + if !wallet.is_open() || thread_id != wallet.get_thread_id() { break; } - // Update progress at separate thread. - let wallet_scan = wallet.clone(); - let (tx, rx) = mpsc::channel::(); - thread::spawn(move || { - while let Ok(m) = rx.recv() { - // Stop updating if wallet was closed or maximum. - if !wallet_scan.is_open() || loading_attempts == MAX_LOADING_ATTEMPTS { - return; - } - println!("m: {}", serde_json::to_string::(&m.clone()).unwrap()); - match m { - StatusMessage::UpdatingOutputs(_) => {} - StatusMessage::UpdatingTransactions(_) => {} - StatusMessage::FullScanWarn(_) => {} - StatusMessage::Scanning(_, progress) => { - wallet_scan.loading_progress.store(progress, Ordering::Relaxed); - } - StatusMessage::ScanningComplete(_) => { - wallet_scan.loading_progress.store(100, Ordering::Relaxed); - } - StatusMessage::UpdateWarning(_) => {} - } + // Setup error when required integrated node is not enabled or skip when it's not ready. + if wallet.config.ext_conn_id.is_none() { + if !Node::is_running() { + wallet.set_loading_error(true); + } else if Node::get_sync_status() != Some(SyncStatus::NoSync) { + wallet.set_loading_error(false); + thread::sleep(Duration::from_millis(1000)); + continue; } - }); + } - // Retrieve wallet info. - match retrieve_summary_info( - wallet.instance.clone(), - keychain_mask.as_ref(), - &Some(tx), - true, - wallet.config.min_confirmations - ) { - Ok(info) => { - wallet.loading_error.store(false, Ordering::Relaxed); - let mut w_info = wallet.info.write().unwrap(); - *w_info = Some(info.1); - } - Err(e) => { - println!("Error!: {}", e); - loading_attempts += 1; - wallet.loading_progress.store(0, Ordering::Relaxed); - // Setup an error flag when maximum loading attempts were reached. - if loading_attempts == MAX_LOADING_ATTEMPTS { - wallet.loading_error.store(true, Ordering::Relaxed); - break; + // Update wallet info if it was no loading error after several attempts. + if !wallet.loading_error() { + let wallet_scan = wallet.clone(); + let (tx, rx) = mpsc::channel::(); + // Update loading progress at separate thread. + thread::spawn(move || { + while let Ok(m) = rx.recv() { + // Stop updating if wallet was closed or maximum. + if !wallet_scan.is_open() || wallet_scan.get_thread_id() != thread_id { + return; + } + match m { + StatusMessage::UpdatingOutputs(_) => {} + StatusMessage::UpdatingTransactions(_) => {} + StatusMessage::FullScanWarn(_) => {} + StatusMessage::Scanning(_, progress) => { + wallet_scan.loading_progress.store(progress, Ordering::Relaxed); + } + StatusMessage::ScanningComplete(_) => { + wallet_scan.loading_progress.store(100, Ordering::Relaxed); + } + StatusMessage::UpdateWarning(_) => {} + } + } + }); + + // Retrieve wallet info. + match retrieve_summary_info( + wallet.instance.clone(), + keychain_mask.as_ref(), + &Some(tx), + true, + wallet.config.min_confirmations + ) { + Ok(info) => { + if wallet.loading_progress() != 100 { + wallet.set_loading_error(true); + } else { + let mut w_info = wallet.info.write().unwrap(); + *w_info = Some(info.1); + } + } + Err(_) => { + wallet.set_loading_error(true); + wallet.loading_progress.store(0, Ordering::Relaxed); } } } + // Stop updating if wallet was closed. + if !wallet.is_open() || wallet.get_thread_id() != thread_id { + break; + } + // Repeat after default delay or after 1 second if update was not complete. - let delay = if wallet.loading_progress() == 100 { + let delay = if wallet.loading_progress() == 100 && !wallet.loading_error() { INFO_UPDATE_DELAY } else { Duration::from_millis(1000)