From 4b48195b75badeeb4ee331d77b5eb94ca5c875fd Mon Sep 17 00:00:00 2001 From: ardocrat Date: Wed, 18 Oct 2023 02:26:22 +0300 Subject: [PATCH] wallet: accounts selection, fix config access from different threads --- locales/en.yml | 2 +- locales/ru.yml | 2 +- src/gui/views/wallets/content.rs | 9 +- src/gui/views/wallets/setup/common.rs | 23 +-- src/gui/views/wallets/setup/connection.rs | 12 +- src/gui/views/wallets/setup/recovery.rs | 2 +- src/gui/views/wallets/wallet/content.rs | 176 +++++++++++++++++---- src/gui/views/wallets/wallet/info.rs | 7 +- src/gui/views/wallets/wallet/receive.rs | 2 +- src/gui/views/wallets/wallet/settings.rs | 2 +- src/wallet/config.rs | 6 - src/wallet/list.rs | 18 ++- src/wallet/types.rs | 20 ++- src/wallet/wallet.rs | 178 ++++++++++++++++------ 14 files changed, 337 insertions(+), 122 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 07a9f59..171d2e4 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -54,7 +54,7 @@ wallets: tx_loading: Loading transactions default_account: Default account create_account: Create account - label: 'Label:' + accounts: Accounts tx_sent: Sent tx_received: Received tx_sending: Sending diff --git a/locales/ru.yml b/locales/ru.yml index d815505..4691227 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -54,7 +54,7 @@ wallets: tx_loading: Загрузка транзакций default_account: Стандартный аккаунт create_account: Создать аккаунт - label: 'Метка:' + accounts: Аккаунты tx_sent: Отправлено tx_received: Получено tx_sending: Отправка diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index 1a7403e..fd676c7 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -145,7 +145,7 @@ impl WalletsContent { let list = self.wallets.mut_list(); for wallet in list { // Show content for selected wallet. - if selected_id == Some(wallet.config.id) { + if selected_id == Some(wallet.get_config().id) { // Setup wallet content width. let mut rect = ui.available_rect_before_wrap(); let mut width = ui.available_width(); @@ -345,7 +345,7 @@ impl WalletsContent { // Check if wallet reopen is needed. if !wallet.is_open() && wallet.reopen_needed() { wallet.set_reopen(false); - self.wallets.select(Some(wallet.config.id)); + self.wallets.select(Some(wallet.get_config().id)); self.show_open_wallet_modal(cb); } @@ -371,7 +371,8 @@ impl WalletsContent { ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - let id = wallet.config.id; + let config = wallet.get_config(); + let id = config.id; let is_selected = self.wallets.selected_id == Some(id); let is_current = wallet.is_open() && is_selected; @@ -422,7 +423,7 @@ impl WalletsContent { ui.add_space(3.0); // Setup wallet name text. let name_color = if is_selected { Colors::BLACK } else { Colors::TITLE }; - View::ellipsize_text(ui, wallet.config.name.to_owned(), 18.0, name_color); + View::ellipsize_text(ui, config.name, 18.0, name_color); // Setup wallet connection text. let conn_text = if let Some(id) = wallet.get_current_ext_conn_id() { diff --git a/src/gui/views/wallets/setup/common.rs b/src/gui/views/wallets/setup/common.rs index d24e0ee..f0c7934 100644 --- a/src/gui/views/wallets/setup/common.rs +++ b/src/gui/views/wallets/setup/common.rs @@ -75,17 +75,18 @@ impl CommonSetup { self.modal_content_ui(ui, wallet, cb); ui.vertical_centered(|ui| { + let wallet_name = wallet.get_config().name; // 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.label(RichText::new(wallet_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(); + self.name_edit = wallet_name; // Show wallet name modal. Modal::new(NAME_EDIT_MODAL) .position(ModalPosition::CenterTop) @@ -125,9 +126,10 @@ impl CommonSetup { ui.add_space(6.0); // Show minimum amount of confirmations value setup. - let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, wallet.config.min_confirmations); + let min_confirmations = wallet.get_config().min_confirmations; + let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, min_confirmations); View::button(ui, min_conf_text, Colors::BUTTON, || { - self.min_confirmations_edit = wallet.config.min_confirmations.to_string(); + self.min_confirmations_edit = min_confirmations.to_string(); // Show minimum amount of confirmations value modal. Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL) .position(ModalPosition::CenterTop) @@ -187,7 +189,7 @@ impl CommonSetup { // Draw wallet name edit. let text_edit_resp = egui::TextEdit::singleline(&mut self.name_edit) - .id(Id::from(modal.id).with(wallet.config.id)) + .id(Id::from(modal.id).with(wallet.get_config().id)) .font(TextStyle::Heading) .desired_width(ui.available_width()) .cursor_at_end(true) @@ -216,8 +218,7 @@ impl CommonSetup { // Save button callback. let mut on_save = || { if !self.name_edit.is_empty() { - wallet.config.name = self.name_edit.clone(); - wallet.config.save(); + wallet.change_name(self.name_edit.clone()); cb.hide_keyboard(); modal.close(); } @@ -240,6 +241,8 @@ impl CommonSetup { wallet: &mut Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) { + let wallet_id = wallet.get_config().id; + ui.add_space(6.0); ui.vertical_centered(|ui| { ui.label(RichText::new(t!("wallets.current_pass")) @@ -260,7 +263,7 @@ impl CommonSetup { 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")) + .id(Id::from(modal.id).with(wallet_id).with("old_pass")) .font(TextStyle::Heading) .desired_width(ui.available_width()) .cursor_at_end(true) @@ -297,7 +300,7 @@ impl CommonSetup { 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")) + .id(Id::from(modal.id).with(wallet_id).with("new_pass")) .font(TextStyle::Heading) .desired_width(ui.available_width()) .cursor_at_end(true) @@ -426,7 +429,7 @@ impl CommonSetup { // Save button callback. let mut on_save = || { if let Ok(min_conf) = self.min_confirmations_edit.parse::() { - wallet.config.min_confirmations = min_conf; + wallet.update_min_confirmations(min_conf); cb.hide_keyboard(); modal.close(); } diff --git a/src/gui/views/wallets/setup/connection.rs b/src/gui/views/wallets/setup/connection.rs index 906ec12..9ae0b5c 100644 --- a/src/gui/views/wallets/setup/connection.rs +++ b/src/gui/views/wallets/setup/connection.rs @@ -103,7 +103,7 @@ impl ConnectionSetup { } // Setup connection value from provided wallet. - match wallet.config.ext_conn_id { + match wallet.get_config().ext_conn_id { None => self.method = ConnectionMethod::Integrated, Some(id) => self.method = ConnectionMethod::External(id) } @@ -112,25 +112,25 @@ impl ConnectionSetup { self.ui(ui, frame, cb); // Setup wallet connection value after change. + let config = wallet.get_config(); let changed = match self.method { ConnectionMethod::Integrated => { - let changed = wallet.config.ext_conn_id.is_some(); + let changed = config.ext_conn_id.is_some(); if changed { - wallet.config.ext_conn_id = None; + wallet.update_ext_conn_id(None); } changed } ConnectionMethod::External(id) => { - let changed = wallet.config.ext_conn_id != Some(id); + let changed = config.ext_conn_id != Some(id); if changed { - wallet.config.ext_conn_id = Some(id); + wallet.update_ext_conn_id(Some(id)); } changed } }; if changed { - wallet.config.save(); // Show reopen confirmation modal. Modal::new(REOPEN_WALLET_CONFIRMATION_MODAL) .position(ModalPosition::Center) diff --git a/src/gui/views/wallets/setup/recovery.rs b/src/gui/views/wallets/setup/recovery.rs index 73803fc..c84e98c 100644 --- a/src/gui/views/wallets/setup/recovery.rs +++ b/src/gui/views/wallets/setup/recovery.rs @@ -204,7 +204,7 @@ impl RecoverySetup { ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { // Draw current wallet password text edit. let pass_resp = egui::TextEdit::singleline(&mut self.pass_edit) - .id(Id::from(modal.id).with(wallet.config.id).with("recovery_phrase")) + .id(Id::from(modal.id).with(wallet.get_config().id).with("recovery_phrase")) .font(TextStyle::Heading) .desired_width(ui.available_width()) .cursor_at_end(true) diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index a062c73..f217f65 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -14,13 +14,13 @@ use std::time::Duration; -use egui::{Align, Id, Layout, Margin, RichText, Rounding, TextStyle, Widget}; +use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea, TextStyle, Widget}; use grin_chain::SyncStatus; use grin_core::core::amount_to_hr_string; use crate::AppConfig; use crate::gui::Colors; -use crate::gui::icons::{DOWNLOAD, FILE_ARCHIVE, GEAR_FINE, LIST, PACKAGE, PLUS, POWER, REPEAT, UPLOAD, WALLET}; +use crate::gui::icons::{CHECK, CHECK_FAT, CREDIT_CARD, DOWNLOAD, FILE_ARCHIVE, GEAR_FINE, LIST, PACKAGE, PLUS, POWER, REPEAT, UPLOAD, WALLET}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, Root, View}; use crate::gui::views::types::ModalPosition; @@ -28,14 +28,17 @@ use crate::gui::views::wallets::{WalletInfo, WalletReceive, WalletSend, WalletSe use crate::gui::views::wallets::types::{WalletTab, WalletTabType}; use crate::node::Node; use crate::wallet::{Wallet, WalletConfig}; -use crate::wallet::types::WalletData; +use crate::wallet::types::{GRIN, WalletAccount, WalletData}; /// Selected and opened wallet content. pub struct WalletContent { + /// List of wallet accounts for [`Modal`]. + accounts: Vec, + /// Account label [`Modal`] value. - pub account_label_edit: String, + account_label_edit: String, /// Flag to check if error occurred during account creation at [`Modal`]. - pub account_creation_error: bool, + account_creation_error: bool, /// Current tab content to show. pub current_tab: Box, @@ -44,6 +47,7 @@ pub struct WalletContent { impl Default for WalletContent { fn default() -> Self { Self { + accounts: vec![], account_label_edit: "".to_string(), account_creation_error: false, current_tab: Box::new(WalletInfo::default()) @@ -53,6 +57,8 @@ impl Default for WalletContent { /// Identifier for account creation [`Modal`]. const CREATE_ACCOUNT_MODAL: &'static str = "create_account_modal"; +/// Identifier for account list [`Modal`]. +const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal"; impl WalletContent { pub fn ui(&mut self, @@ -90,7 +96,7 @@ impl WalletContent { ui.vertical_centered(|ui| { // Draw wallet tabs. View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.35, |ui| { - Self::account_ui(ui, data.as_ref().unwrap(), &wallet.config.account, cb); + self.account_ui(ui, wallet, data.unwrap(), cb); }); }); }); @@ -149,6 +155,11 @@ impl WalletContent { Modal::ui(ui.ctx(), |ui, modal| { self.create_account_modal_ui(ui, wallet, modal, cb); }); + }, + ACCOUNT_LIST_MODAL => { + Modal::ui(ui.ctx(), |ui, modal| { + self.account_list_modal_ui(ui, wallet, modal); + }); } _ => {} } @@ -157,9 +168,10 @@ impl WalletContent { } /// Draw wallet account content. - fn account_ui(ui: &mut egui::Ui, - data: &WalletData, - account: &String, + fn account_ui(&mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + data: WalletData, cb: &dyn PlatformCallbacks) { let mut rect = ui.available_rect_before_wrap(); rect.set_height(75.0); @@ -182,8 +194,14 @@ impl WalletContent { }); // Draw button to show list of accounts. - View::item_button(ui, Rounding::none(), LIST, None, || { - //TODO: accounts list modal + View::item_button(ui, Rounding::ZERO, LIST, None, || { + // Load accounts. + self.accounts = wallet.accounts(); + // Show account list modal. + Modal::new(ACCOUNT_LIST_MODAL) + .position(ModalPosition::Center) + .title(t!("wallets.accounts")) + .show(); }); let layout_size = ui.available_size(); @@ -193,18 +211,19 @@ impl WalletContent { ui.add_space(3.0); // Show spendable amount. let amount = amount_to_hr_string(data.info.amount_currently_spendable, false); - let amount_text = format!("{} ツ", amount); + let amount_text = format!("{} {}", amount, GRIN); ui.label(RichText::new(amount_text).size(18.0).color(Colors::BLACK)); + ui.add_space(-2.0); // Show account label. - let default_acc_label = &WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string(); + let account = wallet.get_config().account; + let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string(); let acc_label = if account == default_acc_label { t!("wallets.default_account") } else { account.to_owned() }; let acc_text = format!("{} {}", FILE_ARCHIVE, acc_label); - ui.add_space(-2.0); View::ellipsize_text(ui, acc_text, 15.0, Colors::TEXT); // Show confirmed height. @@ -215,6 +234,43 @@ impl WalletContent { }); } + /// Draw account list [`Modal`] content. + fn account_list_modal_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, modal: &Modal) { + ui.add_space(3.0); + + // Show list of accounts. + let size = self.accounts.len(); + ScrollArea::vertical() + .max_height(300.0) + .id_source("account_list_modal_scroll") + .auto_shrink([true; 2]) + .show_rows(ui, ACCOUNT_ITEM_HEIGHT, size, |ui, row_range| { + for index in row_range { + // Add space before the first item. + if index == 0 { + ui.add_space(4.0); + } + let acc = self.accounts.get(index).unwrap(); + account_item_ui(ui, modal, wallet, acc, index, size); + if index == size - 1 { + ui.add_space(4.0); + } + } + }); + + ui.add_space(2.0); + View::horizontal_line(ui, Colors::STROKE); + ui.add_space(6.0); + + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::WHITE, || { + // Close modal. + modal.close(); + }); + ui.add_space(6.0); + }); + } + /// Draw account creation [`Modal`] content. fn create_account_modal_ui(&mut self, ui: &mut egui::Ui, @@ -230,7 +286,7 @@ impl WalletContent { // Draw account name edit. let text_edit_resp = egui::TextEdit::singleline(&mut self.account_label_edit) - .id(Id::from(modal.id).with(wallet.config.id)) + .id(Id::from(modal.id).with(wallet.get_config().id)) .font(TextStyle::Heading) .desired_width(ui.available_width()) .cursor_at_end(true) @@ -240,15 +296,16 @@ impl WalletContent { cb.show_keyboard(); } ui.add_space(8.0); + + // Show error occurred during account creation.. + if self.account_creation_error { + ui.add_space(2.0); + ui.label(RichText::new(t!("error")) + .size(17.0) + .color(Colors::RED)); + } }); - // Show error occurred during account creation.. - if self.account_creation_error { - ui.add_space(2.0); - ui.label(RichText::new(t!("error")) - .size(17.0) - .color(Colors::RED)); - } ui.add_space(12.0); // Show modal buttons. @@ -270,12 +327,10 @@ impl WalletContent { if !self.account_label_edit.is_empty() { let label = &self.account_label_edit; match wallet.create_account(label) { - Ok(_) => match wallet.set_active_account(label) { - Ok(_) => { - cb.hide_keyboard(); - modal.close(); - } - Err(_) => self.account_creation_error = true + Ok(_) => { + let _ = wallet.set_active_account(label); + cb.hide_keyboard(); + modal.close(); }, Err(_) => self.account_creation_error = true }; @@ -299,7 +354,7 @@ impl WalletContent { // Setup spacing between tabs. ui.style_mut().spacing.item_spacing = egui::vec2(4.0, 0.0); // Setup vertical padding inside tab button. - ui.style_mut().spacing.button_padding = egui::vec2(0.0, 8.0); + ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0); // Draw tab buttons. let current_type = self.current_tab.get_type(); @@ -407,6 +462,7 @@ impl WalletContent { 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 { @@ -436,4 +492,66 @@ impl WalletContent { ui.label(RichText::new(text).size(16.0).color(Colors::INACTIVE_TEXT)); }); } +} + +const ACCOUNT_ITEM_HEIGHT: f32 = 75.0; + +/// Draw account item. +fn account_item_ui(ui: &mut egui::Ui, + modal: &Modal, + wallet: &mut Wallet, + acc: &WalletAccount, + index: usize, + size: usize) { + // Setup layout size. + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(ACCOUNT_ITEM_HEIGHT); + + // Draw round background. + let bg_rect = rect.clone(); + let item_rounding = View::item_rounding(index, size, false); + ui.painter().rect(bg_rect, item_rounding, Colors::FILL, View::ITEM_STROKE); + + ui.vertical(|ui| { + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Draw button to select account. + let is_current_account = wallet.get_config().account == acc.label; + if !is_current_account { + let button_rounding = View::item_rounding(index, size, true); + View::item_button(ui, button_rounding, CHECK, None, || { + let _ = wallet.set_active_account(&acc.label); + modal.close(); + }); + } else { + ui.add_space(12.0); + ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::GREEN)); + } + + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(4.0); + // Show spendable amount. + let amount = amount_to_hr_string(acc.spendable_amount, false); + let amount_text = format!("{} {}", amount, GRIN); + ui.label(RichText::new(amount_text).size(18.0).color(Colors::BLACK)); + ui.add_space(-2.0); + + // Show account name. + let default_acc_label = WalletConfig::DEFAULT_ACCOUNT_LABEL.to_string(); + let acc_label = if acc.label == default_acc_label { + t!("wallets.default_account") + } else { + acc.label.to_owned() + }; + View::ellipsize_text(ui, acc_label, 15.0, Colors::TEXT); + + // Show account BIP32 derivation path. + ui.label(RichText::new(acc.path.to_owned()).size(15.0).color(Colors::GRAY)); + ui.add_space(3.0); + }); + }); + }); + }); } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/info.rs b/src/gui/views/wallets/wallet/info.rs index 4be08de..c9e8704 100644 --- a/src/gui/views/wallets/wallet/info.rs +++ b/src/gui/views/wallets/wallet/info.rs @@ -133,7 +133,7 @@ impl WalletInfo { ui.add_space(3.0); ScrollArea::vertical() .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) - .id_source(Id::from("txs_content").with(wallet.config.id)) + .id_source(Id::from("txs_content").with(wallet.get_config().id)) .auto_shrink([false; 2]) .show_rows(ui, TX_ITEM_HEIGHT, txs_size, |ui, row_range| { ui.add_space(4.0); @@ -244,12 +244,13 @@ fn tx_item_ui(ui: &mut egui::Ui, } } else { let tx_height = tx.kernel_lookup_min_height.unwrap_or(0); + let min_confirmations = wallet.get_config().min_confirmations; match tx.tx_type { TxLogEntryType::ConfirmedCoinbase => { format!("{} {}", CHECK_CIRCLE, t!("wallets.tx_confirmed")) }, TxLogEntryType::TxReceived => { - if last_height - tx_height > wallet.config.min_confirmations { + if last_height - tx_height > min_confirmations { format!("{} {}", ARROW_CIRCLE_DOWN, t!("wallets.tx_received")) } else { format!("{} {}", @@ -258,7 +259,7 @@ fn tx_item_ui(ui: &mut egui::Ui, } }, TxLogEntryType::TxSent => { - if last_height - tx_height > wallet.config.min_confirmations { + if last_height - tx_height > min_confirmations { format!("{} {}", ARROW_CIRCLE_DOWN, t!("wallets.tx_sent")) } else { format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_confirming")) diff --git a/src/gui/views/wallets/wallet/receive.rs b/src/gui/views/wallets/wallet/receive.rs index 8d2c820..9364e7a 100644 --- a/src/gui/views/wallets/wallet/receive.rs +++ b/src/gui/views/wallets/wallet/receive.rs @@ -117,7 +117,7 @@ impl WalletReceive { ui.add_space(3.0); ScrollArea::vertical() .max_height(128.0) - .id_source(Id::from("receive_input").with(wallet.config.id)) + .id_source(Id::from("receive_input").with(wallet.get_config().id)) .auto_shrink([false; 2]) .show(ui, |ui| { ui.add_space(7.0); diff --git a/src/gui/views/wallets/wallet/settings.rs b/src/gui/views/wallets/wallet/settings.rs index 521a087..23d79c9 100644 --- a/src/gui/views/wallets/wallet/settings.rs +++ b/src/gui/views/wallets/wallet/settings.rs @@ -75,7 +75,7 @@ impl WalletTab for WalletSettings { }) .show_inside(ui, |ui| { ScrollArea::vertical() - .id_source(Id::from("wallet_settings_scroll").with(wallet.config.id)) + .id_source(Id::from("wallet_settings_scroll").with(wallet.get_config().id)) .auto_shrink([false; 2]) .show(ui, |ui| { ui.vertical_centered(|ui| { diff --git a/src/wallet/config.rs b/src/wallet/config.rs index ae26a76..63258ba 100644 --- a/src/wallet/config.rs +++ b/src/wallet/config.rs @@ -132,10 +132,4 @@ impl WalletConfig { let config_path = Self::get_config_file_path(self.chain_type, self.id); Settings::write_to_file(self, config_path); } - - /// Save account label value. - pub fn save_account(&mut self, label: &String) { - self.account = label.to_owned(); - self.save(); - } } \ No newline at end of file diff --git a/src/wallet/list.rs b/src/wallet/list.rs index 322e5db..a8d5a45 100644 --- a/src/wallet/list.rs +++ b/src/wallet/list.rs @@ -59,8 +59,8 @@ impl WalletList { } } // Sort wallets by id. - main_wallets.sort_by_key(|w| -w.config.id); - test_wallets.sort_by_key(|w| -w.config.id); + main_wallets.sort_by_key(|w| -w.get_config().id); + test_wallets.sort_by_key(|w| -w.get_config().id); (main_wallets, test_wallets) } @@ -84,7 +84,7 @@ impl WalletList { /// Add created [`Wallet`] to the list. pub fn add(&mut self, wallet: Wallet) { - self.selected_id = Some(wallet.config.id); + self.selected_id = Some(wallet.get_config().id); let list = self.mut_list(); list.insert(0, wallet); } @@ -93,7 +93,7 @@ impl WalletList { pub fn remove(&mut self, id: i64) { let list = self.mut_list(); for (index, wallet) in list.iter().enumerate() { - if wallet.config.id == id { + if wallet.get_config().id == id { list.remove(index); return; } @@ -108,8 +108,9 @@ impl WalletList { /// Get selected [`Wallet`] name. pub fn selected_name(&self) -> String { for w in self.list() { - if Some(w.config.id) == self.selected_id { - return w.config.name.to_owned() + let config = w.get_config(); + if Some(config.id) == self.selected_id { + return config.name.clone() } } t!("wallets.unlocked") @@ -118,7 +119,8 @@ impl WalletList { /// Check if selected [`Wallet`] is open. pub fn is_selected_open(&self) -> bool { for w in self.list() { - if Some(w.config.id) == self.selected_id { + let config = w.get_config(); + if Some(config.id) == self.selected_id { return w.is_open() } } @@ -134,7 +136,7 @@ impl WalletList { pub fn open_selected(&mut self, password: String) -> Result<(), Error> { let selected_id = self.selected_id.clone(); for w in self.mut_list() { - if Some(w.config.id) == selected_id { + if Some(w.get_config().id) == selected_id { return w.open(password.clone()); } } diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 501d971..c659a09 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -19,6 +19,9 @@ use grin_util::Mutex; use grin_wallet_impls::{DefaultLCProvider, HTTPNodeClient}; use grin_wallet_libwallet::{TxLogEntry, WalletInfo, WalletInst}; +/// GRIN coin symbol. +pub const GRIN: &str = "ツ"; + /// Mnemonic phrase setup mode. #[derive(PartialEq, Clone)] pub enum PhraseMode { @@ -87,11 +90,22 @@ pub type WalletInstance = Arc< >, >; -/// Contains wallet data to show. +/// Wallet account data. +#[derive(Clone)] +pub struct WalletAccount { + /// Spendable balance amount. + pub spendable_amount: u64, + /// Account label. + pub label: String, + /// Account BIP32 derivation path. + pub path: String +} + +/// Wallet balance and transactions data. #[derive(Clone)] pub struct WalletData { - /// Wallet balance information. + /// Balance data for current account. pub info: WalletInfo, - /// Wallet transactions. + /// Transactions data. pub txs: Vec } \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 7df7e5e..4994241 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -40,13 +40,13 @@ use grin_wallet_libwallet::api_impl::owner::{cancel_tx, retrieve_summary_info, r use crate::node::{Node, NodeConfig}; use crate::wallet::{ConnectionsConfig, ExternalConnection, WalletConfig}; -use crate::wallet::types::{ConnectionMethod, WalletData, WalletInstance}; +use crate::wallet::types::{ConnectionMethod, WalletAccount, WalletData, WalletInstance}; /// Contains wallet instance, configuration and state, handles wallet commands. #[derive(Clone)] pub struct Wallet { /// Wallet configuration. - pub config: WalletConfig, + config: Arc>, /// Wallet instance, initializing on wallet opening and clearing on wallet closing. instance: Option, /// [`WalletInstance`] external connection id applied after opening. @@ -74,7 +74,10 @@ pub struct Wallet { /// Transactions loading progress in percents. txs_sync_progress: Arc, - /// Wallet data. + /// Wallet accounts. + accounts: Arc>>, + + /// Wallet info to show at ui. data: Arc>>, /// Attempts amount to update wallet data. sync_attempts: Arc, @@ -97,7 +100,7 @@ impl Wallet { /// Create new [`Wallet`] instance with provided [`WalletConfig`]. fn new(config: WalletConfig) -> Self { Self { - config, + config: Arc::new(RwLock::new(config)), instance: None, instance_ext_conn_id: Arc::new(AtomicI64::new(0)), sync_thread: Arc::from(RwLock::new(None)), @@ -109,7 +112,8 @@ impl Wallet { 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)), + accounts: Arc::new(RwLock::new(vec![])), + data: Arc::new(RwLock::new(None)), sync_attempts: Arc::new(AtomicU8::new(0)), repair_needed: Arc::new(AtomicBool::new(false)), repair_progress: Arc::new(AtomicU8::new(0)), @@ -201,6 +205,32 @@ impl Wallet { Ok(Arc::new(Mutex::new(wallet))) } + /// Get wallet config. + pub fn get_config(&self) -> WalletConfig { + self.config.read().unwrap().clone() + } + + /// Change wallet name. + pub fn change_name(&self, name: String) { + let mut w_config = self.config.write().unwrap(); + w_config.name = name; + w_config.save(); + } + + /// Update minimal amount of confirmations. + pub fn update_min_confirmations(&self, min_confirmations: u64) { + let mut w_config = self.config.write().unwrap(); + w_config.min_confirmations = min_confirmations; + w_config.save(); + } + + /// Update external connection identifier. + pub fn update_ext_conn_id(&self, id: Option) { + let mut w_config = self.config.write().unwrap(); + w_config.ext_conn_id = id; + w_config.save(); + } + /// Open the wallet and start [`WalletData`] sync at separate thread. pub fn open(&mut self, password: String) -> Result<(), Error> { if self.is_open() { @@ -209,9 +239,10 @@ impl Wallet { // Create new wallet instance if sync thread was stopped or instance was not created. if self.sync_thread.write().unwrap().is_none() || self.instance.is_none() { - let new_instance = Self::create_wallet_instance(self.config.clone())?; + let config = self.get_config(); + let new_instance = Self::create_wallet_instance(config.clone())?; self.instance = Some(new_instance); - self.instance_ext_conn_id.store(match self.config.ext_conn_id { + self.instance_ext_conn_id.store(match config.ext_conn_id { None => 0, Some(conn_id) => conn_id }, Ordering::Relaxed); @@ -229,7 +260,7 @@ impl Wallet { // Set current account. let wallet_inst = lc.wallet_inst()?; - let label = self.config.account.to_owned(); + let label = self.get_config().account.to_owned(); wallet_inst.set_parent_key_id_by_name(label.as_str())?; // Start new synchronization thread or wake up existing one. @@ -262,7 +293,7 @@ impl Wallet { Some(ext_conn_id) } } else { - self.config.ext_conn_id + self.get_config().ext_conn_id } } @@ -305,7 +336,7 @@ impl Wallet { wallet_close.is_open.store(false, Ordering::Relaxed); // Wake up thread to exit. - wallet_close.refresh(); + wallet_close.sync(); }); } @@ -327,33 +358,33 @@ impl Wallet { /// Set active account from provided label. pub fn set_active_account(&mut self, label: &String) -> Result<(), Error> { - let instance = self.instance.clone().unwrap(); - let mut wallet_lock = instance.lock(); - let lc = wallet_lock.lc_provider()?; - let wallet_inst = lc.wallet_inst()?; - wallet_inst.set_parent_key_id_by_name(label.as_str())?; + let mut api = Owner::new(self.instance.clone().unwrap(), None); + controller::owner_single_use(None, None, Some(&mut api), |api, m| { + api.set_active_account(m, label)?; + Ok(()) + })?; // Save account label into config. - self.config.save_account(label); + let mut w_config = self.config.write().unwrap(); + w_config.account = label.to_owned(); + w_config.save(); // Clear wallet info. let mut w_data = self.data.write().unwrap(); *w_data = None; - // Refresh wallet data. - self.refresh(); + // Reset progress values. + self.info_sync_progress.store(0, Ordering::Relaxed); + self.txs_sync_progress.store(0, Ordering::Relaxed); + + // Sync wallet data. + self.sync(); Ok(()) } /// Get list of accounts for the wallet. - pub fn accounts(&self) -> Vec { - let mut api = Owner::new(self.instance.clone().unwrap(), None); - let mut accounts = vec![]; - let _ = controller::owner_single_use(None, None, Some(&mut api), |api, m| { - accounts = api.accounts(m)?; - Ok(()) - }); - accounts + pub fn accounts(&self) -> Vec { + self.accounts.read().unwrap().clone() } /// Set wallet reopen status. @@ -414,8 +445,8 @@ impl Wallet { r_data.clone() } - /// Wake up wallet thread to refresh wallet info and update statuses. - fn refresh(&self) { + /// Wake up wallet thread to sync wallet data and update statuses. + fn sync(&self) { let thread_r = self.sync_thread.read().unwrap(); if let Some(thread) = thread_r.as_ref() { thread.unpark(); @@ -427,9 +458,9 @@ impl Wallet { let mut api = Owner::new(self.instance.clone().unwrap(), None); match parse_slatepack(&mut api, None, None, Some(message.clone())) { Ok((mut slate, _)) => { + let config = self.get_config(); controller::foreign_single_use(api.wallet_inst.clone(), None, |api| { - let account = self.config.clone().account; - slate = api.receive_tx(&slate, Some(account.as_str()), None)?; + slate = api.receive_tx(&slate, Some(config.account.as_str()), None)?; Ok(()) })?; let mut response = "".to_string(); @@ -439,7 +470,7 @@ impl Wallet { })?; // Create a directory to which slatepack files will be output. - let mut slatepack_dir = self.config.get_slatepacks_path(); + let mut slatepack_dir = config.get_slatepacks_path(); let slatepack_file_name = format!("{}.{}.slatepack", slate.id, slate.state); slatepack_dir.push(slatepack_file_name); @@ -448,8 +479,8 @@ impl Wallet { output.write_all(response.as_bytes())?; output.sync_all()?; - // Refresh wallet info. - self.refresh(); + // Sync wallet info. + self.sync(); Ok(response) } @@ -477,7 +508,7 @@ impl Wallet { thread::spawn(move || { let _ = cancel_tx(instance, None, &None, Some(id), None); // Refresh wallet info to update statuses. - wallet_cancel.refresh(); + wallet_cancel.sync(); }); } @@ -502,7 +533,7 @@ impl Wallet { /// Initiate wallet repair by scanning its outputs. pub fn repair(&self) { self.repair_needed.store(true, Ordering::Relaxed); - self.refresh(); + self.sync(); } /// Check if wallet is repairing. @@ -538,9 +569,7 @@ impl Wallet { Self::close_wallet(&instance); // Remove wallet files. - let mut wallet_lock = instance.lock(); - let _ = wallet_lock.lc_provider().unwrap(); - let _ = fs::remove_dir_all(wallet_delete.config.get_data_path()); + let _ = fs::remove_dir_all(wallet_delete.get_config().get_data_path()); // Mark wallet as not opened and deleted. wallet_delete.closing.store(false, Ordering::Relaxed); @@ -548,7 +577,7 @@ impl Wallet { wallet_delete.deleted.store(true, Ordering::Relaxed); // Wake up thread to exit. - wallet_delete.refresh(); + wallet_delete.sync(); }); } @@ -616,7 +645,7 @@ fn start_sync(mut wallet: Wallet) -> Thread { // 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) + repair_wallet(&wallet) } else { sync_wallet_data(&wallet); } @@ -721,18 +750,22 @@ fn sync_wallet_data(wallet: &Wallet) { None, &Some(info_tx), true, - wallet.config.min_confirmations + wallet.get_config().min_confirmations ) { Ok(info) => { // Do not retrieve txs if wallet was closed. if !wallet.is_open() { return; } - // Retrieve txs if retrieving info was success. + if wallet.info_sync_progress() == 100 { + // Retrieve accounts data. + let last_height = info.1.last_confirmed_height; + update_accounts(wallet, last_height, info.1.amount_currently_spendable); + + // Update txs sync progress at separate thread. let wallet_txs = wallet.clone(); let (txs_tx, txs_rx) = mpsc::channel::(); - // Update txs sync progress at separate thread. thread::spawn(move || { while let Ok(m) = txs_rx.recv() { println!("SYNC TXS MESSAGE"); @@ -828,15 +861,66 @@ fn sync_wallet_data(wallet: &Wallet) { } } +/// Update wallet accounts data. +fn update_accounts(wallet: &Wallet, current_height: u64, current_spendable: u64) { + // Update only current account if list is not empty. + if !wallet.accounts.read().unwrap().is_empty() { + let mut accounts = wallet.accounts.read().unwrap().clone(); + for mut a in accounts.iter_mut() { + if a.label == wallet.get_config().account { + a.spendable_amount = current_spendable; + } + } + // Save accounts data. + let mut w_data = wallet.accounts.write().unwrap(); + *w_data = accounts; + } else { + let mut api = Owner::new(wallet.instance.clone().unwrap(), None); + let _ = controller::owner_single_use(None, None, Some(&mut api), |api, m| { + let mut accounts = vec![]; + for a in api.accounts(m)? { + api.set_active_account(m, a.label.as_str())?; + // Calculate wallet account balance. + let outputs = api.retrieve_outputs(m, true, false, None)?; + let min_confirmations = wallet.get_config().min_confirmations; + let mut spendable_amount = 0; + for out_mapping in outputs.1 { + let out = out_mapping.output; + if out.status == grin_wallet_libwallet::OutputStatus::Unspent { + if !out.is_coinbase || out.lock_height <= current_height + || out.num_confirmations(current_height) >= min_confirmations { + spendable_amount += out.value; + } + } + } + + // Add account to list. + accounts.push(WalletAccount { + spendable_amount, + label: a.label, + path: a.path.to_bip_32_string(), + }) + } + + // Save accounts data. + let mut w_data = wallet.accounts.write().unwrap(); + *w_data = accounts; + + // Set current active account from config. + api.set_active_account(m, wallet.get_config().account.as_str())?; + + Ok(()) + }); + } +} + /// Scan wallet's outputs, repairing and restoring missing outputs if required. -fn scan_wallet(wallet: &Wallet) { - println!("repair the wallet"); +fn repair_wallet(wallet: &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(_) => {} @@ -856,7 +940,6 @@ fn scan_wallet(wallet: &Wallet) { let api = Owner::new(wallet.instance.clone().unwrap(), Some(info_tx)); match api.scan(None, 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); @@ -865,7 +948,6 @@ fn scan_wallet(wallet: &Wallet) { } } Err(e) => { - println!("error on repair {}", e); // Set sync error if wallet is open. if wallet.is_open() { wallet.set_sync_error(true);