diff --git a/locales/en.yml b/locales/en.yml index fab5930..b3a21a4 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -2,11 +2,11 @@ copy: Copy paste: Paste continue: Continue complete: Complete -closing: Closing -loading: Loading loading_error: Loading error retry: Retry close: Close +change: Change +show: Show wallets: title: Wallets create_desc: Create or import existing wallet from saved recovery phrase. @@ -14,9 +14,12 @@ wallets: name: 'Name:' pass: 'Password:' pass_empty: Enter password from the wallet + current_pass: 'Current password:' + new_pass: 'New password:' + min_tx_conf_count: 'Minimum amount of confirmations for transactions:' create: Create recover: Restore - saved_phrase: Saved phrase + recovery_phrase: Recovery phrase words_count: 'Words count:' enter_word: 'Enter word #%{number}:' not_valid_word: Entered word is not valid @@ -35,10 +38,22 @@ wallets: locked: Locked unlocked: Unlocked enable_node: 'Enable integrated node to use the wallet or change connection settings by selecting %{settings} at the bottom of the screen.' + node_loading: 'Wallet will be loaded after integrated node synchronization, you can change connection settings by selecting %{settings} at the bottom of the screen.' + loading: Loading + closing: Closing + checking: Checking wallet_loading: Loading wallet wallet_closing: Closing wallet + wallet_checking: Checking wallet tx_loading: Loading transactions - 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.' + recovery: Recovery + repair_wallet: Repair wallet + repair_desc: Check a wallet, repairing and restoring missing outputs if required. This operation will take time. + repair_unavailable: You need an active connection to the node and completed wallet synchronization. + delete: Delete wallet + delete_conf: 'Are you sure you want to remove the wallet? Enter password to confirm:' + delete_desc: Make sure you have saved your recovery phrase to access funds in the future. + wallet_loading_err: 'An error occurred during synchronization of 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 261ffdc..5b49964 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -2,11 +2,11 @@ copy: Копировать paste: Вставить continue: Продолжить complete: Завершить -closing: Закрывается -loading: Загружается loading_error: Ошибка загрузки retry: Повторить close: Закрыть +change: Изменить +show: Показать wallets: title: Кошельки create_desc: Создайте или импортируйте существующий кошелёк из сохранённой фразы восстановления. @@ -14,9 +14,12 @@ wallets: name: 'Название:' pass: 'Пароль:' pass_empty: Введите пароль от кошелька + current_pass: 'Текущий пароль:' + new_pass: 'Новый пароль:' + min_tx_conf_count: 'Минимальное количество подтверждений для транзакций:' create: Создать recover: Восстановить - saved_phrase: Сохранённая фраза + recovery_phrase: Фраза восстановления words_count: 'Количество слов:' enter_word: 'Введите слово #%{number}:' not_valid_word: Введено недопустимое слово @@ -35,10 +38,22 @@ wallets: locked: Заблокирован unlocked: Разблокирован enable_node: 'Чтобы использовать кошелёк, включите встроенный узел или измените настройки подключения, выбрав %{settings} внизу экрана.' + node_loading: Кошелёк будет загружен после синхронизации встроенного узла, вы можете изменить настройки подключения, выбрав %{settings} внизу экрана. + loading: Загружается + closing: Закрывается + checking: Проверяется wallet_loading: Загрузка кошелька wallet_closing: Закрытие кошелька + wallet_checking: Проверка кошелька tx_loading: Загрузка транзакций - wallet_loading_err: 'Во время загрузки кошелька произошла ошибка, вы можете повторить попытку или изменить настройки подключения, выбрав %{settings} внизу экрана.' + recovery: Восстановление + repair_wallet: Починить кошелёк + repair_desc: Проверить кошелёк, исправляя и восстанавливая недостающие выходы, если это необходимо. Эта операция займёт время. + repair_unavailable: Необходимо активное подключение к узлу и завершённая синхронизация кошелька. + delete: Удалить кошелёк + delete_conf: 'Вы уверены, что хотите удалить кошелек? Введите пароль для подтверждения:' + delete_desc: Убедитесь, что вы сохранили вашу фразу восстановления, чтобы получить доступ к средствам в будущем. + wallet_loading_err: 'Во время синхронизации кошелька произошла ошибка, вы можете повторить попытку или изменить настройки подключения, выбрав %{settings} внизу экрана.' wallet: Кошелёк send: Отправить receive: Получить diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index f7f65d8..3cb7a44 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -29,7 +29,7 @@ use crate::wallet::{ConnectionsConfig, ExternalConnection, Wallet, WalletList}; /// Wallets content. pub struct WalletsContent { - /// Loaded list of wallets. + /// List of wallets. wallets: WalletList, /// Password to open wallet for [`Modal`]. @@ -415,8 +415,8 @@ impl WalletsContent { }); } - // Show button to close opened wallet if wallet is not loading. - if !wallet.is_closing() { + // Show button to close opened wallet. + if !wallet.is_closing() { View::item_button(ui, if !is_selected { Rounding::none() } else { @@ -437,7 +437,7 @@ impl WalletsContent { View::ellipsize_text(ui, wallet.config.name.to_owned(), 18.0, name_color); // Setup wallet connection text. - let conn_text = if let Some(id) = wallet.config.ext_conn_id { + let conn_text = if let Some(id) = wallet.get_current_ext_conn_id() { let ext_conn_url = match ConnectionsConfig::ext_conn(id) { None => ExternalConnection::DEFAULT_MAIN_URL.to_string(), Some(ext_conn) => ext_conn.url @@ -451,13 +451,41 @@ impl WalletsContent { // Setup wallet status text. let status_text = if wallet.is_open() { - if wallet.is_closing() { - format!("{} {}", SPINNER, t!("wallets.wallet_closing")) - } else if wallet.get_data().is_none() { - if wallet.load_error() { - format!("{} {}", WARNING_CIRCLE, t!("loading_error")) + if wallet.sync_error() { + format!("{} {}", WARNING_CIRCLE, t!("loading_error")) + } else if wallet.is_closing() { + format!("{} {}", SPINNER, t!("wallets.closing")) + } else if wallet.is_repairing() { + let repair_progress = wallet.repairing_progress(); + if repair_progress == 0 { + format!("{} {}", SPINNER, t!("wallets.checking")) } else { - format!("{} {}", SPINNER, t!("loading")) + format!("{} {}: {}%", + SPINNER, + t!("wallets.checking"), + repair_progress) + } + } else if wallet.get_data().is_none() { + let info_progress = wallet.info_sync_progress(); + if info_progress != 100 { + if info_progress == 0 { + format!("{} {}", SPINNER, t!("wallets.loading")) + } else { + format!("{} {}: {}%", + SPINNER, + t!("wallets.loading"), + info_progress) + } + } else { + let tx_progress = wallet.txs_sync_progress(); + if tx_progress == 0 { + t!("wallets.tx_loading") + } else { + format!("{} {}: {}%", + SPINNER, + t!("wallets.tx_loading"), + tx_progress) + } } } else { format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked")) diff --git a/src/gui/views/wallets/setup/common.rs b/src/gui/views/wallets/setup/common.rs new file mode 100644 index 0000000..8fd81f7 --- /dev/null +++ b/src/gui/views/wallets/setup/common.rs @@ -0,0 +1,445 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use egui::{Align, Id, Layout, RichText, TextStyle, Widget}; + +use crate::gui::Colors; +use crate::gui::icons::{CLOCK_COUNTDOWN, EYE, EYE_SLASH, PASSWORD, PENCIL}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, View}; +use crate::gui::views::types::ModalPosition; +use crate::wallet::Wallet; + +/// Common wallet setup content. +pub struct CommonSetup { + /// Wallet name [`Modal`] value. + name_edit: String, + + /// Flag to check if password change [`Modal`] was opened at first time to focus input field. + first_edit_pass_opening: bool, + /// Flag to check if wrong password was entered. + wrong_pass: bool, + /// Current wallet password [`Modal`] value. + current_pass_edit: String, + /// Flag to show/hide old password at [`egui::TextEdit`] field. + hide_current_pass: bool, + /// New wallet password [`Modal`] value. + new_pass_edit: String, + /// Flag to show/hide new password at [`egui::TextEdit`] field. + hide_new_pass: bool, + + /// Minimum confirmations number value. + min_confirmations_edit: String +} + +/// Identifier for wallet name [`Modal`]. +const NAME_EDIT_MODAL: &'static str = "wallet_name_edit_modal"; +/// Identifier for wallet password [`Modal`]. +const PASS_EDIT_MODAL: &'static str = "wallet_pass_edit_modal"; +/// Identifier for minimum confirmations [`Modal`]. +const MIN_CONFIRMATIONS_EDIT_MODAL: &'static str = "wallet_min_conf_edit_modal"; + +impl Default for CommonSetup { + fn default() -> Self { + Self { + name_edit: "".to_string(), + first_edit_pass_opening: true, + wrong_pass: false, + current_pass_edit: "".to_string(), + hide_current_pass: true, + new_pass_edit: "".to_string(), + hide_new_pass: true, + min_confirmations_edit: "".to_string() + } + } +} + +impl CommonSetup { + pub fn ui(&mut self, + ui: &mut egui::Ui, + _: &mut eframe::Frame, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + // Draw modal content for this ui container. + self.modal_content_ui(ui, wallet, cb); + + ui.vertical_centered(|ui| { + // Show wallet name. + ui.add_space(2.0); + ui.label(RichText::new(t!("wallets.name")).size(16.0).color(Colors::GRAY)); + ui.add_space(2.0); + ui.label(RichText::new(wallet.config.name.clone()).size(16.0).color(Colors::BLACK)); + ui.add_space(8.0); + + // Show wallet name setup. + let name_text = format!("{} {}", PENCIL, t!("change")); + View::button(ui, name_text, Colors::BUTTON, || { + self.name_edit = wallet.config.name.clone(); + // Show wallet name modal. + Modal::new(NAME_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.wallet")) + .show(); + cb.show_keyboard(); + }); + + ui.add_space(12.0); + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(6.0); + ui.label(RichText::new(t!("wallets.pass")).size(16.0).color(Colors::GRAY)); + ui.add_space(6.0); + + // Show wallet password setup. + let pass_text = format!("{} {}", PASSWORD, t!("change")); + View::button(ui, pass_text, Colors::BUTTON, || { + // Setup modal values. + self.first_edit_pass_opening = true; + self.current_pass_edit = "".to_string(); + self.new_pass_edit = "".to_string(); + self.hide_current_pass = true; + self.hide_new_pass = true; + self.wrong_pass = false; + // Show wallet password modal. + Modal::new(PASS_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.wallet")) + .show(); + cb.show_keyboard(); + }); + + ui.add_space(12.0); + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(6.0); + ui.label(RichText::new(t!("wallets.min_tx_conf_count")).size(16.0).color(Colors::GRAY)); + ui.add_space(6.0); + + // Show minimum amount of confirmations value setup. + let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, wallet.config.min_confirmations); + View::button(ui, min_conf_text, Colors::BUTTON, || { + self.min_confirmations_edit = wallet.config.min_confirmations.to_string(); + // Show minimum amount of confirmations value modal. + Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("network_settings.change_value")) + .show(); + cb.show_keyboard(); + }); + + ui.add_space(12.0); + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(4.0); + }); + } + + /// Draw modal content for current ui container. + fn modal_content_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + match Modal::opened() { + None => {} + Some(id) => { + match id { + NAME_EDIT_MODAL => { + Modal::ui(ui.ctx(), |ui, modal| { + self.name_modal_ui(ui, wallet, modal, cb); + }); + } + PASS_EDIT_MODAL => { + Modal::ui(ui.ctx(), |ui, modal| { + self.pass_modal_ui(ui, wallet, modal, cb); + }); + } + MIN_CONFIRMATIONS_EDIT_MODAL => { + Modal::ui(ui.ctx(), |ui, modal| { + self.min_conf_modal_ui(ui, wallet, modal, cb); + }); + } + _ => {} + } + } + } + } + + /// Draw wallet name [`Modal`] content. + fn name_modal_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("wallets.name")) + .size(17.0) + .color(Colors::GRAY)); + ui.add_space(8.0); + + // Draw wallet name edit. + let text_edit_resp = egui::TextEdit::singleline(&mut self.name_edit) + .id(Id::from(modal.id).with(wallet.config.id)) + .font(TextStyle::Heading) + .desired_width(ui.available_width()) + .cursor_at_end(true) + .ui(ui); + text_edit_resp.request_focus(); + if text_edit_resp.clicked() { + cb.show_keyboard(); + } + ui.add_space(12.0); + }); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::WHITE, || { + // Close modal. + cb.hide_keyboard(); + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Save button callback. + let mut on_save = || { + if !self.name_edit.is_empty() { + wallet.config.name = self.name_edit.clone(); + wallet.config.save(); + cb.hide_keyboard(); + modal.close(); + } + }; + + View::on_enter_key(ui, || { + (on_save)(); + }); + + View::button(ui, t!("modal.save"), Colors::WHITE, on_save); + }); + }); + ui.add_space(6.0); + }); + } + + /// Draw wallet pass [`Modal`] content. + fn pass_modal_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("wallets.current_pass")) + .size(17.0) + .color(Colors::GRAY)); + ui.add_space(6.0); + + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(34.0); + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Draw button to show/hide current password. + let eye_icon = if self.hide_current_pass { EYE } else { EYE_SLASH }; + View::button(ui, eye_icon.to_string(), Colors::WHITE, || { + self.hide_current_pass = !self.hide_current_pass; + }); + + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + // Draw current wallet password text edit. + let old_pass_resp = egui::TextEdit::singleline(&mut self.current_pass_edit) + .id(Id::from(modal.id).with(wallet.config.id).with("old_pass")) + .font(TextStyle::Heading) + .desired_width(ui.available_width()) + .cursor_at_end(true) + .password(self.hide_current_pass) + .ui(ui); + if old_pass_resp.clicked() { + cb.show_keyboard(); + } + + // Setup focus on input field on first modal opening. + if self.first_edit_pass_opening { + self.first_edit_pass_opening = false; + old_pass_resp.request_focus(); + } + }); + }); + ui.add_space(6.0); + + ui.label(RichText::new(t!("wallets.new_pass")) + .size(17.0) + .color(Colors::GRAY)); + ui.add_space(6.0); + + let mut new_rect = ui.available_rect_before_wrap(); + new_rect.set_height(34.0); + ui.allocate_ui_with_layout(new_rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Draw button to show/hide new password. + let eye_icon = if self.hide_new_pass { EYE } else { EYE_SLASH }; + View::button(ui, eye_icon.to_string(), Colors::WHITE, || { + self.hide_new_pass = !self.hide_new_pass; + }); + + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + // Draw new wallet password text edit. + let new_pass_resp = egui::TextEdit::singleline(&mut self.new_pass_edit) + .id(Id::from(modal.id).with(wallet.config.id).with("new_pass")) + .font(TextStyle::Heading) + .desired_width(ui.available_width()) + .cursor_at_end(true) + .password(self.hide_new_pass) + .ui(ui); + if new_pass_resp.clicked() { + cb.show_keyboard(); + } + }); + }); + + // Show information when password is empty. + if self.current_pass_edit.is_empty() || self.new_pass_edit.is_empty() { + ui.add_space(8.0); + ui.label(RichText::new(t!("wallets.pass_empty")) + .size(17.0) + .color(Colors::INACTIVE_TEXT)); + } else if self.wrong_pass { + ui.add_space(8.0); + ui.label(RichText::new(t!("wallets.wrong_pass")) + .size(17.0) + .color(Colors::RED)); + } + ui.add_space(10.0); + }); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::WHITE, || { + // Close modal. + cb.hide_keyboard(); + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Callback for button to continue. + let mut on_continue = || { + if self.new_pass_edit.is_empty() { + return; + } + let old_pass = self.current_pass_edit.clone(); + let new_pass = self.new_pass_edit.clone(); + match wallet.change_password(old_pass, new_pass) { + Ok(_) => { + // Clear values. + self.first_edit_pass_opening = true; + self.current_pass_edit = "".to_string(); + self.new_pass_edit = "".to_string(); + self.hide_current_pass = true; + self.hide_new_pass = true; + self.wrong_pass = false; + // Close modal. + cb.hide_keyboard(); + modal.close(); + } + Err(_) => self.wrong_pass = true + } + }; + + // Continue on Enter key press. + View::on_enter_key(ui, || { + (on_continue)(); + }); + + View::button(ui, t!("change"), Colors::WHITE, on_continue); + }); + }); + ui.add_space(6.0); + }); + } + + /// Draw wallet name [`Modal`] content. + fn min_conf_modal_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("wallets.min_tx_conf_count")) + .size(17.0) + .color(Colors::GRAY)); + ui.add_space(8.0); + + // Minimum amount of confirmations text edit. + let text_edit_resp = egui::TextEdit::singleline(&mut self.min_confirmations_edit) + .id(Id::from(modal.id)) + .font(TextStyle::Heading) + .desired_width(48.0) + .cursor_at_end(true) + .ui(ui); + text_edit_resp.request_focus(); + if text_edit_resp.clicked() { + cb.show_keyboard(); + } + + // Show error when specified value is not valid or reminder to restart enabled node. + if self.min_confirmations_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label(RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::RED)); + } + ui.add_space(12.0); + }); + + // Show modal buttons. + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::WHITE, || { + // Close modal. + cb.hide_keyboard(); + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Save button callback. + let mut on_save = || { + if let Ok(min_conf) = self.min_confirmations_edit.parse::() { + wallet.config.min_confirmations = min_conf; + cb.hide_keyboard(); + modal.close(); + } + }; + + View::on_enter_key(ui, || { + (on_save)(); + }); + + View::button(ui, t!("modal.save"), Colors::WHITE, on_save); + }); + }); + ui.add_space(6.0); + }); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/setup/connection.rs b/src/gui/views/wallets/setup/connection.rs index 9aa30b4..3aa4ee3 100644 --- a/src/gui/views/wallets/setup/connection.rs +++ b/src/gui/views/wallets/setup/connection.rs @@ -16,7 +16,7 @@ use egui::{Align, Id, Layout, RichText, Rounding, TextStyle, Widget}; use url::Url; use crate::gui::Colors; -use crate::gui::icons::{CHECK, CHECK_CIRCLE, CHECK_FAT, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE, GLOBE_SIMPLE, POWER, X_CIRCLE}; +use crate::gui::icons::{CHECK, CHECK_CIRCLE, CHECK_FAT, COMPUTER_TOWER, DOTS_THREE_CIRCLE, GLOBE, PLUS_CIRCLE, POWER, X_CIRCLE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, View}; use crate::gui::views::types::{ModalContainer, ModalPosition}; @@ -164,7 +164,7 @@ impl ConnectionSetup { ui.add_space(6.0); // Show button to add new external node connection. - let add_node_text = format!("{} {}", GLOBE_SIMPLE, t!("wallets.add_node")); + let add_node_text = format!("{} {}", PLUS_CIRCLE, t!("wallets.add_node")); View::button(ui, add_node_text, Colors::GOLD, || { self.show_add_ext_conn_modal(cb); }); @@ -203,16 +203,15 @@ impl ConnectionSetup { View::item_button(ui, View::item_rounding(0, 1, true), CHECK, None, || { self.method = ConnectionMethod::Integrated; }); + } else { + ui.add_space(14.0); + ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::GREEN)); + ui.add_space(14.0); } if !Node::is_running() { // Draw button to start integrated node. - let rounding = if is_current_method { - View::item_rounding(0, 1, true) - } else { - Rounding::none() - }; - View::item_button(ui, rounding, POWER, Some(Colors::GREEN), || { + View::item_button(ui, Rounding::none(), POWER, Some(Colors::GREEN), || { Node::start(); }); } @@ -273,7 +272,7 @@ impl ConnectionSetup { }); } else { ui.add_space(12.0); - ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::TITLE)); + ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::GREEN)); } let layout_size = ui.available_size(); @@ -285,7 +284,6 @@ impl ConnectionSetup { let conn_text = format!("{} {}", COMPUTER_TOWER, conn.url); View::ellipsize_text(ui, conn_text, 15.0, Colors::TITLE); ui.add_space(1.0); - // Setup connection status text. let status_text = if let Some(available) = conn.available { if available { diff --git a/src/gui/views/wallets/setup/main.rs b/src/gui/views/wallets/setup/main.rs deleted file mode 100644 index 68632ff..0000000 --- a/src/gui/views/wallets/setup/main.rs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2023 The Grim Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Main wallet setup content. -pub struct MainSetup { - -} \ No newline at end of file diff --git a/src/gui/views/wallets/setup/mod.rs b/src/gui/views/wallets/setup/mod.rs index a89544c..6391118 100644 --- a/src/gui/views/wallets/setup/mod.rs +++ b/src/gui/views/wallets/setup/mod.rs @@ -15,8 +15,8 @@ mod connection; pub use connection::ConnectionSetup; -mod main; -pub use main::MainSetup; +mod common; +pub use common::CommonSetup; mod recovery; pub use recovery::RecoverySetup; \ No newline at end of file diff --git a/src/gui/views/wallets/setup/recovery.rs b/src/gui/views/wallets/setup/recovery.rs index c4ea21e..c163af6 100644 --- a/src/gui/views/wallets/setup/recovery.rs +++ b/src/gui/views/wallets/setup/recovery.rs @@ -12,7 +12,170 @@ // See the License for the specific language governing permissions and // limitations under the License. +use egui::RichText; +use grin_chain::SyncStatus; +use crate::gui::Colors; +use crate::gui::icons::{EYE, STETHOSCOPE, TRASH, WRENCH}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, View}; +use crate::gui::views::types::ModalPosition; +use crate::node::Node; +use crate::wallet::Wallet; + /// Wallet recovery setup content. pub struct RecoverySetup { + /// Wallet password [`Modal`] value. + pass_edit: String, + /// Flag to check if wrong password was entered. + wrong_pass: bool, + /// Flag to show recovery phrase when password check was passed. + show_recovery_phrase: bool, +} +/// Identifier for recovery phrase [`Modal`]. +const RECOVERY_PHRASE_MODAL: &'static str = "recovery_phrase_modal"; +/// Identifier to confirm wallet deletion [`Modal`]. +const DELETE_CONFIRMATION_MODAL: &'static str = "delete_wallet_confirmation_modal"; + +impl Default for RecoverySetup { + fn default() -> Self { + Self { + wrong_pass: false, + pass_edit: "".to_string(), + show_recovery_phrase: false, + } + } +} + +impl RecoverySetup { + pub fn ui(&mut self, + ui: &mut egui::Ui, + _: &mut eframe::Frame, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + // Draw modal content for this ui container. + self.modal_content_ui(ui, wallet, cb); + + ui.add_space(10.0); + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(6.0); + View::sub_title(ui, format!("{} {}", WRENCH, t!("wallets.recovery"))); + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(4.0); + + ui.vertical_centered(|ui| { + let integrated_node = wallet.get_current_ext_conn_id().is_none(); + let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync); + + if wallet.sync_error() || (integrated_node && !integrated_node_ready) { + ui.add_space(8.0); + ui.label(RichText::new(t!("wallets.repair_unavailable")) + .size(16.0) + .color(Colors::RED)); + } else if wallet.is_repairing() { + ui.add_space(8.0); + View::small_loading_spinner(ui); + ui.add_space(1.0); + } else { + ui.add_space(6.0); + // Draw button to repair the wallet. + let repair_text = format!("{} {}", STETHOSCOPE, t!("wallets.repair_wallet")); + View::button(ui, repair_text, Colors::GOLD, || { + wallet.repair(); + }); + } + + ui.add_space(6.0); + ui.label(RichText::new(t!("wallets.repair_desc")) + .size(16.0) + .color(Colors::INACTIVE_TEXT)); + + ui.add_space(6.0); + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(6.0); + + let recovery_phrase_text = format!("{}:", t!("wallets.recovery_phrase")); + ui.label(RichText::new(recovery_phrase_text).size(16.0).color(Colors::GRAY)); + ui.add_space(6.0); + + // Draw button to show recovery phrase. + let repair_text = format!("{} {}", EYE, t!("show")); + View::button(ui, repair_text, Colors::BUTTON, || { + // Setup modal values. + self.pass_edit = "".to_string(); + self.wrong_pass = false; + self.show_recovery_phrase = false; + // Show recovery phrase modal. + Modal::new(RECOVERY_PHRASE_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.recovery_phrase")) + .show(); + cb.show_keyboard(); + }); + + ui.add_space(12.0); + View::horizontal_line(ui, Colors::ITEM_STROKE); + ui.add_space(6.0); + ui.label(RichText::new(t!("wallets.delete_desc")).size(16.0).color(Colors::GRAY)); + ui.add_space(6.0); + + // Draw button to delete the wallet. + let delete_text = format!("{} {}", TRASH, t!("wallets.delete")); + View::button(ui, delete_text, Colors::GOLD, || { + // Setup modal values. + self.pass_edit = "".to_string(); + self.wrong_pass = false; + // Show wallet deletion confirmation modal. + Modal::new(DELETE_CONFIRMATION_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("modal.confirmation")) + .show(); + cb.show_keyboard(); + }); + ui.add_space(8.0); + }); + } + + /// Draw modal content for current ui container. + fn modal_content_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + match Modal::opened() { + None => {} + Some(id) => { + match id { + RECOVERY_PHRASE_MODAL => { + Modal::ui(ui.ctx(), |ui, modal| { + self.recovery_phrase_modal_ui(ui, wallet, modal, cb); + }); + } + DELETE_CONFIRMATION_MODAL => { + Modal::ui(ui.ctx(), |ui, modal| { + self.delete_confirmation_modal_ui(ui, wallet, modal, cb); + }); + } + _ => {} + } + } + } + } + + /// Draw recovery phrase [`Modal`] content. + fn recovery_phrase_modal_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + + } + + /// Draw recovery phrase [`Modal`] content. + fn delete_confirmation_modal_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + + } } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 44d1a01..b018323 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -14,7 +14,7 @@ use std::time::Duration; -use egui::{Margin, RichText, ScrollArea}; +use egui::{Margin, RichText}; use grin_chain::SyncStatus; use crate::AppConfig; @@ -30,7 +30,7 @@ use crate::wallet::Wallet; /// Selected and opened wallet content. pub struct WalletContent { /// Current tab content to show. - current_tab: Box, + pub current_tab: Box, } impl Default for WalletContent { @@ -46,8 +46,6 @@ impl WalletContent { wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { // Show wallet tabs panel. - let not_show_tabs = - wallet.is_closing() || (wallet.get_data().is_none() && !wallet.load_error()); egui::TopBottomPanel::bottom("wallet_tabs") .frame(egui::Frame { fill: Colors::FILL, @@ -59,7 +57,7 @@ impl WalletContent { }, ..Default::default() }) - .show_animated_inside(ui, !not_show_tabs, |ui| { + .show_animated_inside(ui, !Self::block_navigation_on_sync(wallet), |ui| { ui.vertical_centered(|ui| { // Setup tabs width. let available_width = ui.available_width(); @@ -85,38 +83,16 @@ impl WalletContent { inner_margin: Margin { left: View::far_left_inset_margin(ui) + 4.0, right: View::get_right_inset() + 4.0, - top: 4.0, + top: 3.0, bottom: 4.0, }, ..Default::default() }) .show_inside(ui, |ui| { - let scroll_id = format!("wallet_tab_{}_{}", - wallet.config.id, - self.current_tab.get_type().id()); - ScrollArea::vertical() - .id_source(scroll_id) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.vertical_centered(|ui| { - // Setup tab content width. - let available_width = ui.available_width(); - if available_width == 0.0 { - return; - } - let mut rect = ui.available_rect_before_wrap(); - let width = f32::min(available_width, Root::SIDE_PANEL_WIDTH * 1.3); - rect.set_width(width); - - // Draw current tab content. - ui.allocate_ui(rect.size(), |ui| { - self.current_tab.ui(ui, frame, wallet, cb); - }); - }); - }); + self.current_tab.ui(ui, frame, wallet, cb); }); - // Refresh content after 1 second for loaded wallet. + // Refresh content after 1 second for synced wallet. if wallet.get_data().is_some() { ui.ctx().request_repaint_after(Duration::from_millis(1000)); } else { @@ -159,12 +135,15 @@ impl WalletContent { }); } - /// 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.is_closing() { - Self::loading_progress_ui(ui, wallet); + /// Draw content when wallet is syncing and not ready to use, returns `true` at this case. + pub fn sync_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, wallet: &Wallet) -> bool { + if wallet.is_repairing() && !wallet.sync_error() { + Self::sync_progress_ui(ui, wallet); return true; - } else if wallet.config.ext_conn_id.is_none() { + } else if wallet.is_closing() { + Self::sync_progress_ui(ui, wallet); + return true; + } else if wallet.get_current_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, 108.0, |ui| { @@ -182,48 +161,70 @@ impl WalletContent { } }); return true - } else if wallet.load_error() + } else if wallet.sync_error() && Node::get_sync_status() == Some(SyncStatus::NoSync) { - Self::loading_error_ui(ui, wallet); + Self::sync_error_ui(ui, wallet); return true; } else if wallet.get_data().is_none() { - Self::loading_progress_ui(ui, wallet); + Self::sync_progress_ui(ui, wallet); return true; } + } else if wallet.sync_error() { + Self::sync_error_ui(ui, wallet); + return true; } else if wallet.get_data().is_none() { - if wallet.load_error() { - Self::loading_error_ui(ui, wallet); - } else { - Self::loading_progress_ui(ui, wallet); - } + Self::sync_progress_ui(ui, wallet); return true; } false } - /// Draw wallet loading error content. - fn loading_error_ui(ui: &mut egui::Ui, wallet: &Wallet) { + /// Draw wallet sync error content. + fn sync_error_ui(ui: &mut egui::Ui, wallet: &Wallet) { View::center_content(ui, 108.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_load_error(false); + wallet.retry_sync(); }); }); } - /// Draw wallet loading progress content. - pub fn loading_progress_ui(ui: &mut egui::Ui, wallet: &Wallet) { + /// Check when to block tabs navigation on sync progress. + pub fn block_navigation_on_sync(wallet: &Wallet) -> bool { + let sync_error = wallet.sync_error(); + let integrated_node = wallet.get_current_ext_conn_id().is_none(); + let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync); + let sync_after_opening = wallet.get_data().is_none() && !wallet.sync_error(); + // Block navigation if wallet is repairing and integrated node is not launching, + // or wallet is closing or syncing after opening when there is no data to show. + (wallet.is_repairing() && (integrated_node_ready || !integrated_node) && !sync_error) + || wallet.is_closing() || (sync_after_opening && !integrated_node) + } + + /// Draw wallet sync progress content. + pub fn sync_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. + // Setup sync progress text. let text = { - let info_progress = wallet.info_load_progress(); + let integrated_node = wallet.get_current_ext_conn_id().is_none(); + let integrated_node_ready = Node::get_sync_status() == Some(SyncStatus::NoSync); + let info_progress = wallet.info_sync_progress(); if wallet.is_closing() { t!("wallets.wallet_closing") + } else if integrated_node && !integrated_node_ready { + t!("wallets.node_loading", "settings" => GEAR_FINE) + } else if wallet.is_repairing() { + let repair_progress = wallet.repairing_progress(); + if repair_progress == 0 { + t!("wallets.wallet_checking") + } else { + format!("{}: {}%", t!("wallets.wallet_checking"), repair_progress) + } } else if info_progress != 100 { if info_progress == 0 { t!("wallets.wallet_loading") @@ -231,7 +232,7 @@ impl WalletContent { format!("{}: {}%", t!("wallets.wallet_loading"), info_progress) } } else { - let tx_progress = wallet.txs_load_progress(); + let tx_progress = wallet.txs_sync_progress(); if tx_progress == 0 { t!("wallets.tx_loading") } else { diff --git a/src/gui/views/wallets/wallet/info.rs b/src/gui/views/wallets/wallet/info.rs index 9013ac8..7d1dc6c 100644 --- a/src/gui/views/wallets/wallet/info.rs +++ b/src/gui/views/wallets/wallet/info.rs @@ -32,7 +32,7 @@ impl WalletTab for WalletInfo { frame: &mut eframe::Frame, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { - if WalletContent::loading_ui(ui, frame, wallet) { + if WalletContent::sync_ui(ui, frame, wallet) { return; } } diff --git a/src/gui/views/wallets/wallet/receive.rs b/src/gui/views/wallets/wallet/receive.rs index f844533..e4f4b2b 100644 --- a/src/gui/views/wallets/wallet/receive.rs +++ b/src/gui/views/wallets/wallet/receive.rs @@ -31,7 +31,7 @@ impl WalletTab for WalletReceive { frame: &mut eframe::Frame, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { - if WalletContent::loading_ui(ui, frame, wallet) { + if WalletContent::sync_ui(ui, frame, wallet) { return; } } diff --git a/src/gui/views/wallets/wallet/send.rs b/src/gui/views/wallets/wallet/send.rs index d8c9c1a..ecd9586 100644 --- a/src/gui/views/wallets/wallet/send.rs +++ b/src/gui/views/wallets/wallet/send.rs @@ -31,7 +31,7 @@ impl WalletTab for WalletSend { frame: &mut eframe::Frame, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { - if WalletContent::loading_ui(ui, frame, wallet) { + if WalletContent::sync_ui(ui, frame, wallet) { return; } } diff --git a/src/gui/views/wallets/wallet/settings.rs b/src/gui/views/wallets/wallet/settings.rs index 3381547..c78a16f 100644 --- a/src/gui/views/wallets/wallet/settings.rs +++ b/src/gui/views/wallets/wallet/settings.rs @@ -12,24 +12,33 @@ // See the License for the specific language governing permissions and // limitations under the License. +use egui::{Id, ScrollArea}; + use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::wallets::setup::ConnectionSetup; +use crate::gui::views::Root; +use crate::gui::views::wallets::setup::{CommonSetup, ConnectionSetup, RecoverySetup}; use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::wallet::WalletContent; use crate::wallet::{ExternalConnection, Wallet}; /// Wallet settings tab content. pub struct WalletSettings { + /// Common setup content. + common_setup: CommonSetup, /// Connection setup content. - conn_setup: ConnectionSetup + conn_setup: ConnectionSetup, + /// Recovery setup content. + recovery_setup: RecoverySetup } impl Default for WalletSettings { fn default() -> Self { - // Check external connections availability on first tab opening. + // Check external connections availability on opening. ExternalConnection::start_ext_conn_availability_check(); Self { + common_setup: CommonSetup::default(), conn_setup: ConnectionSetup::default(), + recovery_setup: RecoverySetup::default() } } } @@ -44,12 +53,36 @@ impl WalletTab for WalletSettings { frame: &mut eframe::Frame, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { - // Show progress if wallet is loading after opening without an error. - if wallet.is_closing() || (wallet.get_data().is_none() && !wallet.load_error()) { - WalletContent::loading_progress_ui(ui, wallet); + // Show loading progress if navigation is blocked. + if WalletContent::block_navigation_on_sync(wallet) { + WalletContent::sync_progress_ui(ui, wallet); return; } - self.conn_setup.wallet_ui(ui, frame, wallet, cb); + ScrollArea::vertical() + .id_source(Id::from("wallet_settings_scroll").with(wallet.config.id)) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.vertical_centered(|ui| { + // Setup tab content width. + let available_width = ui.available_width(); + if available_width == 0.0 { + return; + } + let mut rect = ui.available_rect_before_wrap(); + let width = f32::min(available_width, Root::SIDE_PANEL_WIDTH * 1.3); + rect.set_width(width); + + // Draw current tab content. + ui.allocate_ui(rect.size(), |ui| { + // Show common wallet setup. + self.common_setup.ui(ui, frame, wallet, cb); + // Show wallet connections setup. + self.conn_setup.wallet_ui(ui, frame, wallet, cb); + // Show wallet recovery setup. + self.recovery_setup.ui(ui, frame, wallet, cb); + }); + }); + }); } } \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 4a2a97d..ec2946e 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -24,6 +24,7 @@ use grin_core::global; use grin_keychain::{ExtKeychain, Keychain}; use grin_util::secp::SecretKey; use grin_util::types::ZeroingString; +use grin_wallet_api::Owner; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; use grin_wallet_libwallet::{Error, NodeClient, StatusMessage, WalletInst, WalletLCProvider}; use grin_wallet_libwallet::api_impl::owner::{retrieve_summary_info, retrieve_txs}; @@ -36,13 +37,15 @@ use crate::wallet::types::{ConnectionMethod, WalletData, WalletInstance}; /// Contains wallet instance, configuration and state, handles wallet commands. #[derive(Clone)] pub struct Wallet { - /// Wallet instance, initializing on wallet opening and clearing on wallet closing. - instance: Option, /// Wallet configuration. pub config: WalletConfig, + /// Wallet instance, initializing on wallet opening and clearing on wallet closing. + instance: Option, + /// [`WalletInstance`] external connection id applied after opening. + instance_ext_conn_id: Option, - /// Wallet updating thread. - thread: Arc>>, + /// Wallet sync thread. + sync_thread: Arc>>, /// Flag to check if wallet reopening is needed. reopen: Arc, @@ -52,33 +55,41 @@ pub struct Wallet { closing: Arc, /// Error on wallet loading. - load_error: Arc, - /// Info loading progress in percents - info_load_progress: Arc, - /// Transactions loading progress in percents - txs_load_progress: Arc, + sync_error: Arc, + /// Info loading progress in percents. + info_sync_progress: Arc, + /// Transactions loading progress in percents. + txs_sync_progress: Arc, /// Wallet data. data: Arc>>, /// Attempts amount to update wallet data. - data_update_attempts: Arc + sync_attempts: Arc, + + /// Flag to check if wallet repairing and restoring missing outputs is needed. + repair_needed: Arc, + /// Wallet repair progress in percents. + repair_progress: Arc } impl Wallet { /// Create new [`Wallet`] instance with provided [`WalletConfig`]. fn new(config: WalletConfig) -> Self { Self { - instance: None, config, - thread: Arc::from(RwLock::new(None)), + instance: None, + instance_ext_conn_id: None, + sync_thread: Arc::from(RwLock::new(None)), reopen: Arc::new(AtomicBool::new(false)), is_open: Arc::from(AtomicBool::new(false)), closing: Arc::new(AtomicBool::new(false)), - load_error: Arc::from(AtomicBool::new(false)), - info_load_progress: Arc::from(AtomicU8::new(0)), - txs_load_progress: Arc::from(AtomicU8::new(0)), + sync_error: Arc::from(AtomicBool::new(false)), + info_sync_progress: Arc::from(AtomicU8::new(0)), + txs_sync_progress: Arc::from(AtomicU8::new(0)), data: Arc::from(RwLock::new(None)), - data_update_attempts: Arc::new(AtomicU8::new(0)) + sync_attempts: Arc::new(AtomicU8::new(0)), + repair_needed: Arc::new(AtomicBool::new(false)), + repair_progress: Arc::new(AtomicU8::new(0)), } } @@ -166,26 +177,34 @@ impl Wallet { Ok(Arc::new(Mutex::new(wallet))) } - /// Open the wallet and start update the data at separate thread. + /// Open the wallet and start [`WalletData`] sync at separate thread. pub fn open(&mut self, password: String) -> Result<(), Error> { if self.is_open() { return Err(Error::GenericError("Already opened".to_string())); } - // Create new wallet instance. - let instance = Self::create_wallet_instance(self.config.clone())?; - self.instance = Some(instance.clone()); + // Create new wallet instance if sync thread was stopped. + if self.sync_thread.write().unwrap().is_none() { + let new_instance = Self::create_wallet_instance(self.config.clone())?; + self.instance = Some(new_instance); + self.instance_ext_conn_id = self.config.ext_conn_id; + } // Open the wallet. + let instance = self.instance.clone().unwrap(); let mut wallet_lock = instance.lock(); let lc = wallet_lock.lc_provider()?; match lc.open_wallet(None, ZeroingString::from(password), false, false) { Ok(keychain) => { - // Start data updating if thread was not launched. - let mut thread_w = self.thread.write().unwrap(); + // Reset an error on opening. + self.set_sync_error(false); + self.reset_sync_attempts(); + + // Start new synchronization thread or wake up existing one. + let mut thread_w = self.sync_thread.write().unwrap(); if thread_w.is_none() { - println!("create new thread"); - let thread = start_wallet(self.clone(), keychain.clone()); + // Start wallet synchronization. + let thread = start_sync(self.clone(), keychain.clone()); *thread_w = Some(thread); } else { println!("unfreeze thread"); @@ -201,51 +220,14 @@ impl Wallet { Ok(()) } - /// Set wallet reopen status. - pub fn set_reopen(&self, reopen: bool) { - self.reopen.store(reopen, Ordering::Relaxed); - } - - /// Check if wallet reopen is needed. - pub fn reopen_needed(&self) -> bool { - self.reopen.load(Ordering::Relaxed) - } - - /// Get wallet transactions loading progress. - pub fn txs_load_progress(&self) -> u8 { - self.txs_load_progress.load(Ordering::Relaxed) - } - - /// Get wallet info loading progress. - pub fn info_load_progress(&self) -> u8 { - self.info_load_progress.load(Ordering::Relaxed) - } - - /// Check if wallet had an error on loading. - pub fn load_error(&self) -> bool { - self.load_error.load(Ordering::Relaxed) - } - - /// Set an error for wallet on loading. - pub fn set_load_error(&self, error: bool) { - self.load_error.store(error, Ordering::Relaxed); - } - - /// Get wallet data update attempts. - fn get_data_update_attempts(&self) -> u8 { - self.data_update_attempts.load(Ordering::Relaxed) - } - - /// Increment wallet data update attempts. - fn increment_data_update_attempts(&self) { - let mut attempts = self.get_data_update_attempts(); - attempts += 1; - self.data_update_attempts.store(attempts, Ordering::Relaxed); - } - - /// Reset wallet data update attempts. - fn reset_data_update_attempts(&self) { - self.data_update_attempts.store(0, Ordering::Relaxed); + /// Get current external connection id applied to [`WalletInstance`] + /// after opening if sync is running or take it from configuration. + pub fn get_current_ext_conn_id(&self) -> Option { + if self.sync_thread.read().unwrap().is_some() { + self.instance_ext_conn_id + } else { + self.config.ext_conn_id + } } /// Check if wallet was open. @@ -270,33 +252,76 @@ impl Wallet { let instance = wallet_close.instance.clone().unwrap(); thread::spawn(move || { // Close the wallet. - let mut wallet_lock = instance.lock(); - let lc = wallet_lock.lc_provider().unwrap(); - let _ = lc.close_wallet(None); - wallet_close.instance = None; - - // Clear wallet info. - let mut w_data = wallet_close.data.write().unwrap(); - *w_data = None; - - // Reset wallet loading values. - wallet_close.info_load_progress.store(0, Ordering::Relaxed); - wallet_close.txs_load_progress.store(0, Ordering::Relaxed); - wallet_close.set_load_error(false); - wallet_close.reset_data_update_attempts(); + { + let mut wallet_lock = instance.lock(); + let lc = wallet_lock.lc_provider().unwrap(); + let _ = lc.close_wallet(None); + } // Mark wallet as not opened. wallet_close.closing.store(false, Ordering::Relaxed); wallet_close.is_open.store(false, Ordering::Relaxed); // Wake up wallet thread. - let thread_r = wallet_close.thread.read().unwrap(); + let thread_r = wallet_close.sync_thread.read().unwrap(); if let Some(thread) = thread_r.as_ref() { thread.unpark(); } }); } + /// Set wallet reopen status. + pub fn set_reopen(&self, reopen: bool) { + self.reopen.store(reopen, Ordering::Relaxed); + } + + /// Check if wallet reopen is needed. + pub fn reopen_needed(&self) -> bool { + self.reopen.load(Ordering::Relaxed) + } + + /// Get wallet transactions synchronization progress. + pub fn txs_sync_progress(&self) -> u8 { + self.txs_sync_progress.load(Ordering::Relaxed) + } + + /// Get wallet info synchronization progress. + pub fn info_sync_progress(&self) -> u8 { + self.info_sync_progress.load(Ordering::Relaxed) + } + + /// Check if wallet had an error on synchronization. + pub fn sync_error(&self) -> bool { + self.sync_error.load(Ordering::Relaxed) + } + + /// Retry synchronization on error. + pub fn retry_sync(&self) { + self.set_sync_error(false); + } + + /// Set an error for wallet on synchronization. + fn set_sync_error(&self, error: bool) { + self.sync_error.store(error, Ordering::Relaxed); + } + + /// Get current wallet synchronization attempts before setting an error. + fn get_sync_attempts(&self) -> u8 { + self.sync_attempts.load(Ordering::Relaxed) + } + + /// Increment wallet synchronization attempts before setting an error. + fn increment_sync_attempts(&self) { + let mut attempts = self.get_sync_attempts(); + attempts += 1; + self.sync_attempts.store(attempts, Ordering::Relaxed); + } + + /// Reset wallet synchronization attempts. + fn reset_sync_attempts(&self) { + self.sync_attempts.store(0, Ordering::Relaxed); + } + /// Get wallet data. pub fn get_data(&self) -> Option { let r_data = self.data.read().unwrap(); @@ -310,32 +335,66 @@ impl Wallet { let lc = wallet_lock.lc_provider()?; lc.change_password(None, ZeroingString::from(old), ZeroingString::from(new)) } + + /// Initiate wallet repair by scanning its outputs. + pub fn repair(&self) { + self.repair_needed.store(true, Ordering::Relaxed); + // Wake up wallet thread. + let thread_r = self.sync_thread.read().unwrap(); + if let Some(thread) = thread_r.as_ref() { + thread.unpark(); + } + } + + /// Check if wallet is repairing. + pub fn is_repairing(&self) -> bool { + self.repair_needed.load(Ordering::Relaxed) + } + + /// Get wallet repairing progress. + pub fn repairing_progress(&self) -> u8 { + self.repair_progress.load(Ordering::Relaxed) + } + + pub fn delete_wallet(&self) { + + } } -/// Delay in seconds to update wallet data every minute as average block time. -const DATA_UPDATE_DELAY: Duration = Duration::from_millis(60 * 1000); +/// Delay in seconds to sync [`WalletData`] (60 seconds as average block time). +const SYNC_DELAY: Duration = Duration::from_millis(60 * 1000); -/// Number of attempts to update data after wallet opening before setting an error. -const DATA_UPDATE_ATTEMPTS: u8 = 10; +/// Number of attempts to sync [`WalletData`] before setting an error. +const SYNC_ATTEMPTS: u8 = 10; -/// Launch thread to update wallet data. -fn start_wallet(wallet: Wallet, keychain: Option) -> Thread { - let wallet_update = wallet.clone(); +/// Launch thread to sync wallet data from node. +fn start_sync(wallet: Wallet, keychain: Option) -> Thread { + // Reset progress values. + wallet.info_sync_progress.store(0, Ordering::Relaxed); + wallet.txs_sync_progress.store(0, Ordering::Relaxed); + wallet.repair_progress.store(0, Ordering::Relaxed); + + println!("create new thread"); thread::spawn(move || loop { println!("start new cycle"); - // Stop updating if wallet was closed. - if !wallet_update.is_open() { + // Stop syncing if wallet was closed. + if !wallet.is_open() { println!("finishing thread at start"); - let mut thread_w = wallet_update.thread.write().unwrap(); + // Clear thread instance. + let mut thread_w = wallet.sync_thread.write().unwrap(); *thread_w = None; + + // Clear wallet info. + let mut w_data = wallet.data.write().unwrap(); + *w_data = None; println!("finish at start complete"); return; } - // Set an error when required integrated node is not enabled and - // skip next cycle of update when node sync is not finished. - if wallet_update.config.ext_conn_id.is_none() { - wallet_update.set_load_error(!Node::is_running() || Node::is_stopping()); + // Set an error when required integrated node is not enabled + // and skip cycle when node sync is not finished. + if wallet.get_current_ext_conn_id().is_none() { + wallet.set_sync_error(!Node::is_running() || Node::is_stopping()); if !Node::is_running() || Node::get_sync_status() != Some(SyncStatus::NoSync) { println!("integrated node wait"); thread::park_timeout(Duration::from_millis(1000)); @@ -343,51 +402,60 @@ fn start_wallet(wallet: Wallet, keychain: Option) -> Thread { } } - // Update wallet data if there is no error. - if !wallet_update.load_error() { - update_wallet_data(&wallet_update, keychain.clone()); + // Scan outputs if repair is needed or sync data if there is no error. + if !wallet.sync_error() { + if wallet.is_repairing() { + scan_wallet(&wallet, keychain.clone()) + } else { + sync_wallet_data(&wallet, keychain.clone()); + } } - // Stop updating if wallet was closed. - if !wallet_update.is_open() { + // Stop sync if wallet was closed. + if !wallet.is_open() { println!("finishing thread after updating"); - let mut thread_w = wallet_update.thread.write().unwrap(); + // Clear thread instance. + let mut thread_w = wallet.sync_thread.write().unwrap(); *thread_w = None; + + // Clear wallet info. + let mut w_data = wallet.data.write().unwrap(); + *w_data = None; println!("finishing after updating complete"); return; } - // Repeat after default delay or after 1 second if update was not success. - let delay = if wallet_update.load_error() - || wallet_update.get_data_update_attempts() != 0 { + // Repeat after default delay or after 1 second if sync was not success. + let delay = if wallet.sync_error() + || wallet.get_sync_attempts() != 0 { Duration::from_millis(1000) } else { - DATA_UPDATE_DELAY + SYNC_DELAY }; println!("park for {}", delay.as_millis()); thread::park_timeout(delay); }).thread().clone() } -/// Handle [`WalletCommand::UpdateData`] command to update [`WalletData`]. -fn update_wallet_data(wallet: &Wallet, keychain: Option) { - println!("UPDATE start, attempts: {}", wallet.get_data_update_attempts()); +/// Retrieve [`WalletData`] from node. +fn sync_wallet_data(wallet: &Wallet, keychain: Option) { + println!("SYNC start, attempts: {}", wallet.get_sync_attempts()); - let wallet_scan = wallet.clone(); + let wallet_info = wallet.clone(); let (info_tx, info_rx) = mpsc::channel::(); - // Update info loading progress at separate thread. + // Update info sync progress at separate thread. thread::spawn(move || { while let Ok(m) = info_rx.recv() { - println!("UPDATE INFO MESSAGE"); + println!("SYNC INFO MESSAGE"); match m { StatusMessage::UpdatingOutputs(_) => {} StatusMessage::UpdatingTransactions(_) => {} StatusMessage::FullScanWarn(_) => {} StatusMessage::Scanning(_, progress) => { - wallet_scan.info_load_progress.store(progress, Ordering::Relaxed); + wallet_info.info_sync_progress.store(progress, Ordering::Relaxed); } StatusMessage::ScanningComplete(_) => { - wallet_scan.info_load_progress.store(100, Ordering::Relaxed); + wallet_info.info_sync_progress.store(100, Ordering::Relaxed); } StatusMessage::UpdateWarning(_) => {} } @@ -406,37 +474,25 @@ fn update_wallet_data(wallet: &Wallet, keychain: Option) { Ok(info) => { // Do not retrieve txs if wallet was closed. if !wallet.is_open() { - println!("UPDATE stop at retrieve_summary_info"); return; } - - // Add attempt if scanning was not complete - // or set an error on initial request. - if wallet.info_load_progress() != 100 { - println!("UPDATE retrieve_summary_info was not completed"); - if wallet.get_data().is_none() { - wallet.set_load_error(true); - } else { - wallet.increment_data_update_attempts(); - } - } else { - println!("UPDATE before retrieve_txs"); - + // Retrieve txs if retrieving info was success. + if wallet.info_sync_progress() == 100 { let wallet_txs = wallet.clone(); let (txs_tx, txs_rx) = mpsc::channel::(); - // Update txs loading progress at separate thread. + // Update txs sync progress at separate thread. thread::spawn(move || { while let Ok(m) = txs_rx.recv() { - println!("UPDATE TXS MESSAGE"); + println!("SYNC TXS MESSAGE"); match m { StatusMessage::UpdatingOutputs(_) => {} StatusMessage::UpdatingTransactions(_) => {} StatusMessage::FullScanWarn(_) => {} StatusMessage::Scanning(_, progress) => { - wallet_txs.txs_load_progress.store(progress, Ordering::Relaxed); + wallet_txs.txs_sync_progress.store(progress, Ordering::Relaxed); } StatusMessage::ScanningComplete(_) => { - wallet_txs.txs_load_progress.store(100, Ordering::Relaxed); + wallet_txs.txs_sync_progress.store(100, Ordering::Relaxed); } StatusMessage::UpdateWarning(_) => {} } @@ -454,53 +510,104 @@ fn update_wallet_data(wallet: &Wallet, keychain: Option) { None ) { Ok(txs) => { - // Do not update data if wallet was closed. + // Do not sync data if wallet was closed. if !wallet.is_open() { return; } - - // Add attempt if retrieving was not complete - // or set an error on initial request. - if wallet.txs_load_progress() != 100 { - if wallet.get_data().is_none() { - wallet.set_load_error(true); - } else { - wallet.increment_data_update_attempts(); - } - } else { + // Save data if loading was completed. + if wallet.txs_sync_progress() == 100 { + // Reset attempts. + wallet.reset_sync_attempts(); // Set wallet data. let mut w_data = wallet.data.write().unwrap(); *w_data = Some(WalletData { info: info.1, txs: txs.1 }); - - // Reset attempts. - wallet.reset_data_update_attempts(); + return; } } Err(e) => { println!("error on retrieve_txs {}", e); - // Increment attempts value in case of error. - wallet.increment_data_update_attempts(); } } } } Err(e) => { println!("error on retrieve_summary_info {}", e); - // Increment attempts value in case of error. - wallet.increment_data_update_attempts(); } } } - // Reset progress values. - wallet.info_load_progress.store(0, Ordering::Relaxed); - wallet.txs_load_progress.store(0, Ordering::Relaxed); + // Reset progress. + wallet.info_sync_progress.store(0, Ordering::Relaxed); + wallet.txs_sync_progress.store(0, Ordering::Relaxed); - println!("UPDATE finish, attempts: {}", wallet.get_data_update_attempts()); + // Exit if wallet was closed. + if !wallet.is_open() { + return; + } + + // Set an error if data was not loaded after opening or increment attempts count. + if wallet.get_data().is_none() { + wallet.set_sync_error(true); + } else { + wallet.increment_sync_attempts(); + } + + println!("SYNC cycle finished, attempts: {}", wallet.get_sync_attempts()); // Set an error if maximum number of attempts was reached. - if wallet.get_data_update_attempts() >= DATA_UPDATE_ATTEMPTS { - wallet.reset_data_update_attempts(); - wallet.set_load_error(true); + if wallet.get_sync_attempts() >= SYNC_ATTEMPTS { + wallet.reset_sync_attempts(); + wallet.set_sync_error(true); } -} \ No newline at end of file +} + +/// Scan wallet's outputs, repairing and restoring missing outputs if required. +fn scan_wallet(wallet: &Wallet, keychain: Option) { + println!("repair the wallet"); + let (info_tx, info_rx) = mpsc::channel::(); + // Update scan progress at separate thread. + let wallet_scan = wallet.clone(); + thread::spawn(move || { + while let Ok(m) = info_rx.recv() { + println!("REPAIR WALLET MESSAGE"); + match m { + StatusMessage::UpdatingOutputs(_) => {} + StatusMessage::UpdatingTransactions(_) => {} + StatusMessage::FullScanWarn(_) => {} + StatusMessage::Scanning(_, progress) => { + wallet_scan.repair_progress.store(progress, Ordering::Relaxed); + } + StatusMessage::ScanningComplete(_) => { + wallet_scan.repair_progress.store(100, Ordering::Relaxed); + } + StatusMessage::UpdateWarning(_) => {} + } + } + }); + + // Start wallet scanning. + let api = Owner::new(wallet.instance.clone().unwrap(), Some(info_tx)); + match api.scan(keychain.as_ref(), Some(1), false) { + Ok(()) => { + println!("repair was complete"); + // Set sync error if scanning was not complete and wallet is open. + if wallet.is_open() && wallet.repair_progress.load(Ordering::Relaxed) != 100 { + wallet.set_sync_error(true); + } else { + wallet.repair_needed.store(false, Ordering::Relaxed); + } + } + Err(e) => { + println!("error on repair {}", e); + // Set sync error if wallet is open. + if wallet.is_open() { + wallet.set_sync_error(true); + } else { + wallet.repair_needed.store(false, Ordering::Relaxed); + } + } + } + + // Reset repair progress. + wallet.repair_progress.store(0, Ordering::Relaxed); +}