wallet + ui: optimize wallet synchronization, add wallet repairing, fix wallet loading content, and wallet status, fix wallet settings panel width update translations

This commit is contained in:
ardocrat 2023-08-13 21:09:04 +03:00
parent a0930d4bf3
commit 09d3835082
14 changed files with 1056 additions and 269 deletions

View file

@ -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

View file

@ -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: Получить

View file

@ -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"))

View file

@ -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::<u64>().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::<u64>() {
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);
});
}
}

View file

@ -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 {

View file

@ -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 {
}

View file

@ -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;

View file

@ -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) {
}
}

View file

@ -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<dyn WalletTab>,
pub current_tab: Box<dyn WalletTab>,
}
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 {

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
});
});
});
}
}

View file

@ -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<WalletInstance>,
/// Wallet configuration.
pub config: WalletConfig,
/// Wallet instance, initializing on wallet opening and clearing on wallet closing.
instance: Option<WalletInstance>,
/// [`WalletInstance`] external connection id applied after opening.
instance_ext_conn_id: Option<i64>,
/// Wallet updating thread.
thread: Arc<RwLock<Option<Thread>>>,
/// Wallet sync thread.
sync_thread: Arc<RwLock<Option<Thread>>>,
/// Flag to check if wallet reopening is needed.
reopen: Arc<AtomicBool>,
@ -52,33 +55,41 @@ pub struct Wallet {
closing: Arc<AtomicBool>,
/// Error on wallet loading.
load_error: Arc<AtomicBool>,
/// Info loading progress in percents
info_load_progress: Arc<AtomicU8>,
/// Transactions loading progress in percents
txs_load_progress: Arc<AtomicU8>,
sync_error: Arc<AtomicBool>,
/// Info loading progress in percents.
info_sync_progress: Arc<AtomicU8>,
/// Transactions loading progress in percents.
txs_sync_progress: Arc<AtomicU8>,
/// Wallet data.
data: Arc<RwLock<Option<WalletData>>>,
/// Attempts amount to update wallet data.
data_update_attempts: Arc<AtomicU8>
sync_attempts: Arc<AtomicU8>,
/// Flag to check if wallet repairing and restoring missing outputs is needed.
repair_needed: Arc<AtomicBool>,
/// Wallet repair progress in percents.
repair_progress: Arc<AtomicU8>
}
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<i64> {
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<WalletData> {
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<SecretKey>) -> Thread {
let wallet_update = wallet.clone();
/// Launch thread to sync wallet data from node.
fn start_sync(wallet: Wallet, keychain: Option<SecretKey>) -> 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<SecretKey>) -> 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<SecretKey>) {
println!("UPDATE start, attempts: {}", wallet.get_data_update_attempts());
/// Retrieve [`WalletData`] from node.
fn sync_wallet_data(wallet: &Wallet, keychain: Option<SecretKey>) {
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::<StatusMessage>();
// 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<SecretKey>) {
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::<StatusMessage>();
// 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<SecretKey>) {
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);
}
}
}
/// Scan wallet's outputs, repairing and restoring missing outputs if required.
fn scan_wallet(wallet: &Wallet, keychain: Option<SecretKey>) {
println!("repair the wallet");
let (info_tx, info_rx) = mpsc::channel::<StatusMessage>();
// 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);
}