diff --git a/Cargo.lock b/Cargo.lock index 162ffd2..0f8177f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2267,7 +2267,6 @@ dependencies = [ "uuid", "wgpu", "winit", - "zeroize", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5688613..e2ed436 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,6 @@ lazy_static = "1.4.0" toml = "0.7.4" serde = "1" pnet = "0.34.0" -zeroize = "1.6.0" url = "2.4.0" parking_lot = "0.10.2" uuid = { version = "0.8.2", features = ["serde", "v4"] } diff --git a/locales/en.yml b/locales/en.yml index b7d6645..8a268c3 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -29,6 +29,7 @@ wallets: wrong_pass: Entered password is wrong locked: Locked unlocked: Unlocked + enable_node_required: 'Enable integrated node to use the wallet or change connection settings by selecting %{settings} at the bottom of the screen.' network: self: Network node: Integrated node diff --git a/locales/ru.yml b/locales/ru.yml index 222301f..18a7d60 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -29,6 +29,7 @@ wallets: wrong_pass: Введён неправильный пароль locked: Заблокирован unlocked: Разблокирован + enable_node_required: 'Чтобы использовать кошелёк, включите встроенный узел или измените настройки подключения, выбрав %{settings} внизу экрана.' network: self: Сеть node: Встроенный узел diff --git a/src/gui/views/modal.rs b/src/gui/views/modal.rs index b6f098a..08c8433 100644 --- a/src/gui/views/modal.rs +++ b/src/gui/views/modal.rs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::RwLock; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::RwLock; use egui::{Align2, RichText, Rounding, Stroke, Vec2}; use egui::epaint::RectShape; @@ -167,17 +167,17 @@ impl Modal { } /// Draw opened [`Modal`] content. - pub fn ui(ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) { + pub fn ui(ctx: &egui::Context, add_content: impl FnOnce(&mut egui::Ui, &Modal)) { if let Some(modal) = &MODAL_STATE.read().unwrap().modal { if modal.is_open() { - modal.window_ui(ui, add_content); + modal.window_ui(ctx, add_content); } } } /// Draw [`egui::Window`] with provided content. - fn window_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) { - let rect = ui.ctx().screen_rect(); + fn window_ui(&self, ctx: &egui::Context, add_content: impl FnOnce(&mut egui::Ui, &Modal)) { + let rect = ctx.screen_rect(); egui::Window::new("modal_bg_window") .title_bar(false) .resizable(false) @@ -187,13 +187,13 @@ impl Modal { fill: Colors::SEMI_TRANSPARENT, ..Default::default() }) - .show(ui.ctx(), |ui| { + .show(ctx, |ui| { ui.set_min_size(rect.size()); }); // Setup width of modal content. let side_insets = View::get_left_inset() + View::get_right_inset(); - let available_width = ui.available_width() - (side_insets + Self::DEFAULT_MARGIN); + let available_width = rect.width() - (side_insets + Self::DEFAULT_MARGIN); let width = f32::min(available_width, Self::DEFAULT_WIDTH); // Show main content Window at given position. @@ -209,7 +209,7 @@ impl Modal { fill: Colors::YELLOW, ..Default::default() }) - .show(ui.ctx(), |ui| { + .show(ctx, |ui| { if self.title.is_some() { self.title_ui(ui); } @@ -217,7 +217,7 @@ impl Modal { }).unwrap().response.layer_id; // Always show main content Window above background Window. - ui.ctx().move_to_top(layer_id); + ctx.move_to_top(layer_id); } diff --git a/src/gui/views/network/content.rs b/src/gui/views/network/content.rs index 2b838df..dd8b4eb 100644 --- a/src/gui/views/network/content.rs +++ b/src/gui/views/network/content.rs @@ -85,7 +85,7 @@ impl NetworkContent { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { // Show modal content for current ui container. if self.can_draw_modal() { - Modal::ui(ui, |ui, modal| { + Modal::ui(ui.ctx(), |ui, modal| { self.current_tab.as_mut().on_modal_ui(ui, modal, cb); }); } diff --git a/src/gui/views/root.rs b/src/gui/views/root.rs index 9bcdbd1..b33916c 100644 --- a/src/gui/views/root.rs +++ b/src/gui/views/root.rs @@ -149,7 +149,7 @@ impl Root { /// Draw exit confirmation modal content. fn exit_modal_content(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { - Modal::ui(ui, |ui, modal| { + Modal::ui(ui.ctx(), |ui, modal| { if self.show_exit_progress { if !Node::is_running() { self.exit(frame); diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index d241aec..8d8a6f6 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -14,18 +14,25 @@ use egui::{Align, Align2, Layout, Margin, RichText, Rounding, ScrollArea, TextStyle, Widget}; use egui_extras::{Size, StripBuilder}; +use grin_core::global::ChainTypes; +use crate::AppConfig; use crate::gui::Colors; use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, EYE, EYE_SLASH, FOLDER_LOCK, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, ModalContainer, ModalPosition, Root, TitlePanel, TitleType, View}; use crate::gui::views::wallets::creation::{MnemonicSetup, WalletCreation}; use crate::gui::views::wallets::setup::ConnectionSetup; -use crate::gui::views::wallets::wallet::WalletContent; +use crate::gui::views::wallets::WalletContent; use crate::wallet::{Wallet, Wallets}; /// Wallets content. pub struct WalletsContent { + /// Chain type for loaded wallets. + chain_type: ChainTypes, + /// Loaded list of wallets. + wallets: Wallets, + /// Password to open wallet for [`Modal`]. pass_edit: String, /// Flag to show/hide password at [`egui::TextEdit`] field. @@ -48,6 +55,8 @@ pub struct WalletsContent { impl Default for WalletsContent { fn default() -> Self { Self { + chain_type: AppConfig::chain_type(), + wallets: Wallets::default(), pass_edit: "".to_string(), hide_pass: true, wrong_pass: false, @@ -72,12 +81,12 @@ impl ModalContainer for WalletsContent { impl WalletsContent { /// Identifier for wallet opening [`Modal`]. - pub const OPEN_WALLET_MODAL: &'static str = "open_wallet_modal"; + const OPEN_WALLET_MODAL: &'static str = "open_wallet_modal"; pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { // Show modal content for current ui container. if self.can_draw_modal() { - Modal::ui(ui, |ui, modal| { + Modal::ui(ui.ctx(), |ui, modal| { match modal.id { Self::OPEN_WALLET_MODAL => { self.open_wallet_modal_ui(ui, modal, cb); @@ -96,17 +105,17 @@ impl WalletsContent { }); } - // Get wallets. - let wallets = Wallets::list(); - let empty_list = wallets.is_empty(); + // Setup list of wallets if chain type was changed. + let chain_type = AppConfig::chain_type(); + if self.chain_type != chain_type { + self.wallets.reinit(&chain_type); + self.chain_type = chain_type; + } + let empty_list = self.wallets.list.is_empty(); // Setup wallet content flags. let create_wallet = self.creation_content.can_go_back(); - let show_wallet = if let Some(id) = Wallets::selected_id() { - Wallets::is_open(id) - } else { - false - }; + let show_wallet = self.wallets.is_selected_open(); // Setup panels parameters. let dual_panel = is_dual_panel_mode(ui, frame); @@ -135,15 +144,17 @@ impl WalletsContent { if available_width_zero { return; } - if create_wallet || !show_wallet { - // Show wallet creation content - self.creation_content.ui(ui, cb); + // Show wallet creation content. + self.creation_content.ui(ui, cb, |wallet| { + // Add created wallet to list. + self.wallets.add(wallet); + }); } else { - for wallet in wallets.iter() { + for mut wallet in self.wallets.list.clone() { // Show content for selected wallet. - if Some(wallet.config.id) == Wallets::selected_id() { - self.wallet_content.ui(ui, frame, &wallet, cb); + if self.wallets.is_selected(wallet.config.id) { + self.wallet_content.ui(ui, frame, &mut wallet, cb); break; } } @@ -189,7 +200,7 @@ impl WalletsContent { = (!show_wallet && !dual_panel) || (dual_panel && show_wallet); // Show list of wallets. - let scroll = self.list_ui(ui, dual_panel, show_creation_btn, &wallets, cb); + let scroll = self.list_ui(ui, dual_panel, show_creation_btn, cb); if show_creation_btn { // Setup right margin for button. @@ -221,7 +232,7 @@ impl WalletsContent { TitlePanel::ui(title_content, |ui, frame| { if show_wallet && !dual_panel { View::title_button(ui, ARROW_LEFT, || { - Wallets::select(None); + self.wallets.select(None); }); } else if create_wallet { View::title_button(ui, ARROW_LEFT, || { @@ -277,7 +288,6 @@ impl WalletsContent { ui: &mut egui::Ui, dual_panel: bool, show_creation_btn: bool, - wallets: &Vec, cb: &dyn PlatformCallbacks) -> bool { let mut scroller_showing = false; ui.scope(|ui| { @@ -303,18 +313,14 @@ impl WalletsContent { rect.set_width(width); ui.allocate_ui(rect.size(), |ui| { - for (index, w) in wallets.iter().enumerate() { + for mut wallet in self.wallets.list.clone() { // Draw wallet list item. - self.wallet_item_ui(ui, w, cb); - // Add space after last item. - let last_item = index == wallets.len() - 1; - if !last_item { - ui.add_space(5.0); - } - // Add space for wallet creation button. - if show_creation_btn && last_item { - ui.add_space(57.0); - } + self.wallet_item_ui(ui, &mut wallet, cb); + ui.add_space(5.0); + } + // Add space for wallet creation button. + if show_creation_btn { + ui.add_space(52.0); } }); }); @@ -326,11 +332,13 @@ impl WalletsContent { } /// Draw wallet list item. - fn wallet_item_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + fn wallet_item_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { let id = wallet.config.id; - let is_selected = Some(id) == Wallets::selected_id(); - let is_open = Wallets::is_open(id); - let is_current = is_open && is_selected; + let is_selected = self.wallets.is_selected(id); + let is_current = wallet.is_open() && is_selected; // Draw round background. let mut rect = ui.available_rect_before_wrap(); @@ -348,21 +356,21 @@ impl WalletsContent { ui.style_mut().visuals.widgets.hovered.rounding = Rounding::same(8.0); ui.style_mut().visuals.widgets.active.rounding = Rounding::same(8.0); - if !is_open { + if !wallet.is_open() { // Show button to open closed wallet. View::item_button(ui, [false, true], FOLDER_OPEN, || { - Wallets::select(Some(id)); + self.wallets.select(Some(id)); self.show_open_wallet_modal(cb); }); } else if !is_selected { // Show button to select opened wallet. View::item_button(ui, [false, true], CARET_RIGHT, || { - Wallets::select(Some(id)); + self.wallets.select(Some(id)); }); // Show button to close opened wallet. View::item_button(ui, [false, false], LOCK_KEY, || { - Wallets::close(id).unwrap() + let _ = wallet.close(); }); } @@ -386,7 +394,7 @@ impl WalletsContent { ui.add_space(1.0); // Setup wallet status text. - let status_text = if Wallets::is_open(id) { + let status_text = if wallet.is_open() { format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked")) } else { format!("{} {}", FOLDER_LOCK, t!("wallets.locked")) @@ -497,23 +505,18 @@ impl WalletsContent { ui.columns(2, |columns| { columns[0].vertical_centered_justified(|ui| { View::button(ui, t!("modal.cancel"), Colors::WHITE, || { - // Clear values. - self.pass_edit = "".to_string(); - self.wrong_pass = false; - self.hide_pass = true; // Close modal. cb.hide_keyboard(); modal.close(); }); }); columns[1].vertical_centered_justified(|ui| { - // Callback for continue button. + // Callback for button to continue. let mut on_continue = || { if self.pass_edit.is_empty() { return; } - let selected_id = Wallets::selected_id().unwrap(); - match Wallets::open(selected_id, self.pass_edit.clone()) { + match self.wallets.open_selected(self.pass_edit.clone()) { Ok(_) => { // Clear values. self.pass_edit = "".to_string(); @@ -526,6 +529,7 @@ impl WalletsContent { Err(_) => self.wrong_pass = true } }; + // Continue on Enter key press. View::on_enter_key(ui, || { (on_continue)(); diff --git a/src/gui/views/wallets/creation/creation.rs b/src/gui/views/wallets/creation/creation.rs index 060bc9a..e12f0b8 100644 --- a/src/gui/views/wallets/creation/creation.rs +++ b/src/gui/views/wallets/creation/creation.rs @@ -21,9 +21,10 @@ use crate::gui::icons::{CHECK, EYE, EYE_SLASH, FOLDER_PLUS, SHARE_FAT}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, ModalPosition, View}; use crate::gui::views::wallets::creation::MnemonicSetup; -use crate::gui::views::wallets::creation::types::{PhraseMode, Step}; +use crate::gui::views::wallets::creation::types::Step; use crate::gui::views::wallets::setup::ConnectionSetup; -use crate::wallet::Wallets; +use crate::wallet::types::PhraseMode; +use crate::wallet::Wallet; /// Wallet creation content. pub struct WalletCreation { @@ -61,7 +62,7 @@ impl Default for WalletCreation { logo: RetainedImage::from_image_bytes( "logo.png", include_bytes!("../../../../../img/logo.png"), - ).unwrap(), + ).unwrap() } } } @@ -70,7 +71,10 @@ impl WalletCreation { /// Wallet name/password input modal identifier. pub const NAME_PASS_MODAL: &'static str = "name_pass_modal"; - pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + pub fn ui(&mut self, + ui: &mut egui::Ui, + cb: &dyn PlatformCallbacks, + on_create: impl FnOnce(Wallet)) { // Show wallet creation step description and confirmation panel. if self.step.is_some() { egui::TopBottomPanel::bottom("wallet_creation_step_panel") @@ -87,7 +91,7 @@ impl WalletCreation { }) .show_inside(ui, |ui| { ui.vertical_centered(|ui| { - self.step_control_ui(ui); + self.step_control_ui(ui, on_create); }); }); } @@ -109,8 +113,16 @@ impl WalletCreation { }); } + /// Reset wallet creation to default values. + fn reset(&mut self) { + self.step = None; + self.name_edit = String::from(""); + self.pass_edit = String::from(""); + self.mnemonic_setup.reset(); + } + /// Draw [`Step`] description and confirmation control. - fn step_control_ui(&mut self, ui: &mut egui::Ui) { + fn step_control_ui(&mut self, ui: &mut egui::Ui, on_create: impl FnOnce(Wallet)) { if let Some(step) = &self.step { // Setup step description text and availability. let (step_text, mut step_available) = match step { @@ -164,7 +176,7 @@ impl WalletCreation { ui.add_space(4.0); // Show button. View::button(ui, next_text.to_uppercase(), color, || { - self.forward(); + self.forward(Some(on_create)); }); ui.add_space(4.0); } @@ -223,13 +235,7 @@ impl WalletCreation { None => {} Some(step) => { match step { - Step::EnterMnemonic => { - // Clear values if it needs to go back on first step. - self.step = None; - self.name_edit = String::from(""); - self.pass_edit = String::from(""); - self.mnemonic_setup.reset(); - } + Step::EnterMnemonic => self.reset(), Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic), Step::SetupConnection => self.step = Some(Step::EnterMnemonic) } @@ -238,37 +244,41 @@ impl WalletCreation { } /// Go to the next wallet creation [`Step`]. - fn forward(&mut self) { - self.step = match &self.step { - None => Some(Step::EnterMnemonic), - Some(step) => { - match step { - Step::EnterMnemonic => { - if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate { - Some(Step::ConfirmMnemonic) + fn forward(&mut self, on_create: Option) { + self.step = if let Some(step) = &self.step { + match step { + Step::EnterMnemonic => { + if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate { + Some(Step::ConfirmMnemonic) + } else { + // Check if entered phrase was valid. + if self.mnemonic_setup.valid_phrase { + Some(Step::SetupConnection) } else { - // Check if entered phrase was valid. - if self.mnemonic_setup.valid_phrase { - Some(Step::SetupConnection) - } else { - Some(Step::EnterMnemonic) - } + Some(Step::EnterMnemonic) } } - Step::ConfirmMnemonic => Some(Step::SetupConnection), - Step::SetupConnection => { - // Create wallet at last step. - Wallets::create_wallet( - self.name_edit.clone(), - self.pass_edit.clone(), - self.mnemonic_setup.mnemonic.get_phrase(), - self.network_setup.get_ext_conn_url() - ).unwrap(); - None - } + } + Step::ConfirmMnemonic => Some(Step::SetupConnection), + Step::SetupConnection => { + // Create wallet at last step. + let name = self.name_edit.clone(); + let pass = self.pass_edit.clone(); + let phrase = self.mnemonic_setup.mnemonic.get_phrase(); + let ext_conn = self.network_setup.get_ext_conn_url(); + let wallet = Wallet::create(name, pass.clone(), phrase, ext_conn).unwrap(); + // Open created wallet. + wallet.open(pass).unwrap(); + // Pass created wallet to callback. + (on_create.unwrap())(wallet); + // Reset creation data. + self.reset(); + None } } - } + } else { + Some(Step::EnterMnemonic) + }; } /// Start wallet creation from showing [`Modal`] to enter name and password. @@ -379,7 +389,7 @@ impl WalletCreation { if self.name_edit.is_empty() || self.pass_edit.is_empty() { return; } - self.forward(); + self.step = Some(Step::EnterMnemonic); cb.hide_keyboard(); modal.close(); }; diff --git a/src/gui/views/wallets/creation/mnemonic.rs b/src/gui/views/wallets/creation/mnemonic.rs index 46db431..2e319bd 100644 --- a/src/gui/views/wallets/creation/mnemonic.rs +++ b/src/gui/views/wallets/creation/mnemonic.rs @@ -18,7 +18,8 @@ use crate::gui::Colors; use crate::gui::icons::PENCIL; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, ModalPosition, Root, View}; -use crate::gui::views::wallets::creation::types::{Mnemonic, PhraseMode, PhraseSize}; +use crate::wallet::Mnemonic; +use crate::wallet::types::{PhraseMode, PhraseSize}; /// Mnemonic phrase setup content. pub struct MnemonicSetup { diff --git a/src/gui/views/wallets/creation/types.rs b/src/gui/views/wallets/creation/types.rs index 0551516..b3a7504 100644 --- a/src/gui/views/wallets/creation/types.rs +++ b/src/gui/views/wallets/creation/types.rs @@ -12,10 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use grin_keychain::mnemonic::{from_entropy, search, to_entropy}; -use rand::{Rng, thread_rng}; -use zeroize::{Zeroize, ZeroizeOnDrop}; - /// Wallet creation step. #[derive(PartialEq)] pub enum Step { @@ -25,141 +21,4 @@ pub enum Step { ConfirmMnemonic, /// Wallet connection setup. SetupConnection -} - -/// Mnemonic phrase setup mode. -/// Will be completely cleaned from memory on drop. -#[derive(PartialEq, Clone, Zeroize, ZeroizeOnDrop)] -pub enum PhraseMode { - /// Generate new mnemonic phrase. - Generate, - /// Import existing mnemonic phrase. - Import -} - -/// Mnemonic phrase size based on words count. -/// Will be completely cleaned from memory on drop. -#[derive(PartialEq, Clone, Zeroize, ZeroizeOnDrop)] -pub enum PhraseSize { Words12, Words15, Words18, Words21, Words24 } - -impl PhraseSize { - pub const VALUES: [PhraseSize; 5] = [ - PhraseSize::Words12, - PhraseSize::Words15, - PhraseSize::Words18, - PhraseSize::Words21, - PhraseSize::Words24 - ]; - - /// Gen words count number. - pub fn value(&self) -> usize { - match *self { - PhraseSize::Words12 => 12, - PhraseSize::Words15 => 15, - PhraseSize::Words18 => 18, - PhraseSize::Words21 => 21, - PhraseSize::Words24 => 24 - } - } - - /// Gen entropy size for current phrase size. - pub fn entropy_size(&self) -> usize { - match *self { - PhraseSize::Words12 => 16, - PhraseSize::Words15 => 20, - PhraseSize::Words18 => 24, - PhraseSize::Words21 => 28, - PhraseSize::Words24 => 32 - } - } -} - -/// Mnemonic phrase container. -/// Will be completely cleaned from memory on drop. -#[derive(Zeroize, ZeroizeOnDrop)] -pub struct Mnemonic { - /// Phrase setup mode. - pub(crate) mode: PhraseMode, - /// Size of phrase based on words count. - pub(crate) size: PhraseSize, - /// Generated words. - pub(crate) words: Vec, - /// Words to confirm the phrase. - pub(crate) confirm_words: Vec -} - -impl Default for Mnemonic { - fn default() -> Self { - let size = PhraseSize::Words24; - let mode = PhraseMode::Generate; - let words = Self::generate_words(&mode, &size); - let confirm_words = Self::empty_words(&size); - Self { mode, size, words, confirm_words } - } -} - -impl Mnemonic { - /// Change mnemonic phrase setup [`PhraseMode`]. - pub fn set_mode(&mut self, mode: PhraseMode) { - self.mode = mode; - self.words = Self::generate_words(&self.mode, &self.size); - self.confirm_words = Self::empty_words(&self.size); - } - - /// Change mnemonic phrase words [`PhraseSize`]. - pub fn set_size(&mut self, size: PhraseSize) { - self.size = size; - self.words = Self::generate_words(&self.mode, &self.size); - self.confirm_words = Self::empty_words(&self.size); - } - - /// Check if provided word is in BIP39 format and equal to non-empty generated word at index. - pub fn is_valid_word(&self, word: &String, index: usize) -> bool { - let valid = search(word).is_ok(); - let equal = if let Some(gen_word) = self.words.get(index) { - gen_word.is_empty() || gen_word == word - } else { - false - }; - valid && equal - } - - /// Check if current phrase is valid. - pub fn is_valid_phrase(&self) -> bool { - to_entropy(self.get_phrase().as_str()).is_ok() - } - - /// Get phrase from words. - pub fn get_phrase(&self) -> String { - self.words.iter().map(|x| x.to_string() + " ").collect::() - } - - /// Generate list of words based on provided [`PhraseMode`] and [`PhraseSize`]. - fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec { - match mode { - PhraseMode::Generate => { - let mut rng = thread_rng(); - let mut entropy: Vec = Vec::with_capacity(size.entropy_size()); - for _ in 0..size.entropy_size() { - entropy.push(rng.gen()); - } - from_entropy(&entropy).unwrap() - .split(" ") - .map(|s| String::from(s)) - .collect::>() - }, - PhraseMode::Import => { - Self::empty_words(size) - } - } - } - - /// Generate empty list of words based on provided [`PhraseSize`]. - fn empty_words(size: &PhraseSize) -> Vec { - let mut words = Vec::with_capacity(size.value()); - for _ in 0..size.value() { - words.push(String::from("")) - } - words - } } \ No newline at end of file diff --git a/src/gui/views/wallets/mod.rs b/src/gui/views/wallets/mod.rs index a5dcfe2..72adf5f 100644 --- a/src/gui/views/wallets/mod.rs +++ b/src/gui/views/wallets/mod.rs @@ -13,8 +13,10 @@ // limitations under the License. mod creation; -mod wallet; mod setup; mod content; -pub use content::*; \ No newline at end of file +pub use content::*; + +mod wallet; +use wallet::WalletContent; \ 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 bd78c4a..4dda7bb 100644 --- a/src/gui/views/wallets/setup/connection.rs +++ b/src/gui/views/wallets/setup/connection.rs @@ -15,13 +15,12 @@ use egui::{Id, RichText, ScrollArea, TextStyle, Widget}; use url::Url; -use crate::AppConfig; use crate::gui::Colors; use crate::gui::icons::{GLOBE, GLOBE_SIMPLE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, ModalPosition, View}; use crate::gui::views::wallets::setup::ConnectionMethod; -use crate::wallet::ExternalConnection; +use crate::wallet::{ConnectionsConfig, ExternalConnection}; /// Wallet node connection method setup content. pub struct ConnectionSetup { @@ -106,7 +105,7 @@ impl ConnectionSetup { ui.add_space(12.0); // Show external nodes URLs selection. - for conn in AppConfig::external_connections() { + for conn in ConnectionsConfig::external_connections() { View::radio_value(ui, &mut self.method, ConnectionMethod::External(conn.url.clone()), @@ -196,7 +195,7 @@ impl ConnectionSetup { Some(self.ext_node_secret_edit.to_owned()) }; let ext_conn = ExternalConnection::new(url.clone(), secret); - AppConfig::add_external_connection(ext_conn); + ConnectionsConfig::add_external_connection(ext_conn); // Set added method as current. self.method = ConnectionMethod::External(url); diff --git a/src/gui/views/wallets/wallet.rs b/src/gui/views/wallets/wallet/content.rs similarity index 97% rename from src/gui/views/wallets/wallet.rs rename to src/gui/views/wallets/wallet/content.rs index 47a5b4c..49d25ff 100644 --- a/src/gui/views/wallets/wallet.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -34,7 +34,7 @@ impl WalletContent { pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, - wallet: &Wallet, + wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { // Show wallet content. egui::CentralPanel::default() @@ -54,4 +54,6 @@ impl WalletContent { //TODO: wallet content }); } + + } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/mod.rs b/src/gui/views/wallets/wallet/mod.rs new file mode 100644 index 0000000..23bedab --- /dev/null +++ b/src/gui/views/wallets/wallet/mod.rs @@ -0,0 +1,21 @@ +// 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. + +mod content; +mod txs; +mod send; +mod settings; +mod receive; + +pub use content::WalletContent; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/receive.rs b/src/gui/views/wallets/wallet/receive.rs new file mode 100644 index 0000000..aa04af4 --- /dev/null +++ b/src/gui/views/wallets/wallet/receive.rs @@ -0,0 +1,13 @@ +// 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. \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/send.rs b/src/gui/views/wallets/wallet/send.rs new file mode 100644 index 0000000..2035f55 --- /dev/null +++ b/src/gui/views/wallets/wallet/send.rs @@ -0,0 +1,13 @@ +// 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. diff --git a/src/gui/views/wallets/wallet/settings.rs b/src/gui/views/wallets/wallet/settings.rs new file mode 100644 index 0000000..aa04af4 --- /dev/null +++ b/src/gui/views/wallets/wallet/settings.rs @@ -0,0 +1,13 @@ +// 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. \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/txs.rs b/src/gui/views/wallets/wallet/txs.rs new file mode 100644 index 0000000..2035f55 --- /dev/null +++ b/src/gui/views/wallets/wallet/txs.rs @@ -0,0 +1,13 @@ +// 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. diff --git a/src/settings.rs b/src/settings.rs index c0b7898..7395833 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -24,7 +24,6 @@ use serde::{Deserialize, Serialize}; use serde::de::DeserializeOwned; use crate::node::NodeConfig; -use crate::wallet::{ExternalConnection, Wallets}; lazy_static! { /// Static settings state to be accessible globally. @@ -41,36 +40,18 @@ pub struct AppConfig { pub auto_start_node: bool, /// Chain type for node and wallets. chain_type: ChainTypes, - /// URLs of external connections for wallets. - external_connections: Vec } impl Default for AppConfig { fn default() -> Self { Self { auto_start_node: false, - chain_type: ChainTypes::default(), - external_connections: vec![ - ExternalConnection::default() - ], + chain_type: ChainTypes::default() } } } impl AppConfig { - /// Initialize application config from the file. - pub fn init() -> Self { - let path = Settings::get_config_path(APP_CONFIG_FILE_NAME, None); - let parsed = Settings::read_from_file::(path.clone()); - if !path.exists() || parsed.is_err() { - let default_config = AppConfig::default(); - Settings::write_to_file(&default_config, path); - default_config - } else { - parsed.unwrap() - } - } - /// Save app config to file. pub fn save(&self) { Settings::write_to_file(self, Settings::get_config_path(APP_CONFIG_FILE_NAME, None)); @@ -93,8 +74,6 @@ impl AppConfig { w_node_config.node = node_config.node; w_node_config.peers = node_config.peers; } - // Reload wallets. - Wallets::reload(chain_type); } } @@ -117,42 +96,6 @@ impl AppConfig { w_app_config.auto_start_node = !autostart; w_app_config.save(); } - - /// Get external connections for the wallet. - pub fn external_connections() -> Vec { - let r_config = Settings::app_config_to_read(); - r_config.external_connections.clone() - } - - /// Save external connection for the wallet in app config. - pub fn add_external_connection(conn: ExternalConnection) { - let mut w_config = Settings::app_config_to_update(); - let mut exists = false; - for mut c in w_config.external_connections.iter_mut() { - // Update connection if URL exists. - if c.url == conn.url { - c.secret = conn.secret.clone(); - exists = true; - break; - } - } - // Create new connection if URL not exists. - if !exists { - w_config.external_connections.insert(0, conn); - } - w_config.save(); - } - - /// Get external node connection secret from provided URL. - pub fn get_external_connection_secret(url: String) -> Option { - let r_config = Settings::app_config_to_read(); - for c in &r_config.external_connections { - if c.url == url { - return c.secret.clone(); - } - } - None - } } /// Main application directory name. @@ -169,13 +112,27 @@ pub struct Settings { impl Settings { /// Initialize settings with app and node configs. fn init() -> Self { - let app_config = AppConfig::init(); + let path = Settings::get_config_path(APP_CONFIG_FILE_NAME, None); + let app_config = Self::init_config::(path); Self { node_config: Arc::new(RwLock::new(NodeConfig::for_chain_type(&app_config.chain_type))), app_config: Arc::new(RwLock::new(app_config)), } } + /// Initialize config from provided file path or load default if file not exists. + pub fn init_config(path: PathBuf) -> T { + let parsed = Self::read_from_file::(path.clone()); + if !path.exists() || !parsed.is_err() { + let default_config = T::default(); + Settings::write_to_file(&default_config, path); + default_config + } else { + parsed.unwrap() + } + } + + /// Get node config to read values. pub fn node_config_to_read() -> RwLockReadGuard<'static, NodeConfig> { SETTINGS_STATE.node_config.read().unwrap() diff --git a/src/wallet/config.rs b/src/wallet/config.rs index 2913cdb..1102980 100644 --- a/src/wallet/config.rs +++ b/src/wallet/config.rs @@ -19,7 +19,6 @@ use grin_core::global::ChainTypes; use serde_derive::{Deserialize, Serialize}; use crate::{AppConfig, Settings}; -use crate::wallet::Wallets; /// Wallet configuration. #[derive(Serialize, Deserialize, Clone)] @@ -36,6 +35,8 @@ pub struct WalletConfig { /// Wallet configuration file name. const CONFIG_FILE_NAME: &'static str = "grim-wallet.toml"; +/// Base wallets directory name. +pub const BASE_DIR_NAME: &'static str = "wallets"; impl WalletConfig { /// Create wallet config. @@ -59,9 +60,20 @@ impl WalletConfig { None } + /// Get wallets base directory path for provided [`ChainTypes`]. + pub fn get_base_path(chain_type: &ChainTypes) -> PathBuf { + let mut wallets_path = Settings::get_base_path(Some(chain_type.shortname())); + wallets_path.push(BASE_DIR_NAME); + // Create wallets base directory if it doesn't exist. + if !wallets_path.exists() { + let _ = fs::create_dir_all(wallets_path.clone()); + } + wallets_path + } + /// Get config file path for provided [`ChainTypes`] and wallet identifier. fn get_config_file_path(chain_type: &ChainTypes, id: i64) -> PathBuf { - let mut config_path = Wallets::get_base_path(chain_type); + let mut config_path = Self::get_base_path(chain_type); config_path.push(id.to_string()); // Create if the config path doesn't exist. if !config_path.exists() { @@ -74,7 +86,7 @@ impl WalletConfig { /// Get current wallet data path. pub fn get_data_path(&self) -> String { let chain_type = AppConfig::chain_type(); - let mut config_path = Wallets::get_base_path(&chain_type); + let mut config_path = Self::get_base_path(&chain_type); config_path.push(self.id.to_string()); config_path.to_str().unwrap().to_string() } diff --git a/src/wallet/connections/config.rs b/src/wallet/connections/config.rs new file mode 100644 index 0000000..ff86f54 --- /dev/null +++ b/src/wallet/connections/config.rs @@ -0,0 +1,93 @@ +// 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 std::sync::{Arc, RwLock}; + +use lazy_static::lazy_static; +use serde_derive::{Deserialize, Serialize}; + +use crate::Settings; +use crate::wallet::ExternalConnection; + +lazy_static! { + /// Static settings state to be accessible globally. + static ref CONNECTIONS_STATE: Arc> = Arc::new( + RwLock::new( + Settings::init_config(Settings::get_config_path(CONFIG_FILE_NAME, None)) + ) + ); +} + +/// Wallet connections configuration. +#[derive(Serialize, Deserialize, Clone)] +pub struct ConnectionsConfig { + /// URLs of external connections for wallets. + external: Vec +} + +/// Wallet configuration file name. +const CONFIG_FILE_NAME: &'static str = "connections.toml"; + +impl Default for ConnectionsConfig { + fn default() -> Self { + Self { + external: vec![ + ExternalConnection::default() + ], + } + } +} + +impl ConnectionsConfig { + /// Save connections config to file. + pub fn save(&self) { + Settings::write_to_file(self, Settings::get_config_path(CONFIG_FILE_NAME, None)); + } + + /// Get external connections for the wallet. + pub fn external_connections() -> Vec { + let r_config = CONNECTIONS_STATE.read().unwrap(); + r_config.external.clone() + } + + /// Save external connection for the wallet in app config. + pub fn add_external_connection(conn: ExternalConnection) { + let mut w_config = CONNECTIONS_STATE.write().unwrap(); + let mut exists = false; + for mut c in w_config.external.iter_mut() { + // Update connection if URL exists. + if c.url == conn.url { + c.secret = conn.secret.clone(); + exists = true; + break; + } + } + // Create new connection if URL not exists. + if !exists { + w_config.external.insert(0, conn); + } + w_config.save(); + } + + /// Get external node connection secret from provided URL. + pub fn get_external_connection_secret(url: String) -> Option { + let r_config = CONNECTIONS_STATE.read().unwrap(); + for c in &r_config.external { + if c.url == url { + return c.secret.clone(); + } + } + None + } +} \ No newline at end of file diff --git a/src/wallet/connections/external.rs b/src/wallet/connections/external.rs new file mode 100644 index 0000000..7cafd3a --- /dev/null +++ b/src/wallet/connections/external.rs @@ -0,0 +1,39 @@ +// 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 serde_derive::{Deserialize, Serialize}; + +/// External node connection for the wallet. +#[derive(Serialize, Deserialize, Clone)] +pub struct ExternalConnection { + /// Node URL. + pub url: String, + /// Optional API secret key. + pub secret: Option +} + +impl ExternalConnection { + /// Default external node URL. + const DEFAULT_EXTERNAL_NODE_URL: &'static str = "https://grinnnode.live:3413"; + + pub fn new(url: String, secret: Option) -> Self { + Self { url, secret } + } +} + +impl Default for ExternalConnection { + fn default() -> Self { + Self { url: Self::DEFAULT_EXTERNAL_NODE_URL.to_string(), secret: None } + } +} \ No newline at end of file diff --git a/src/wallet/connections/mod.rs b/src/wallet/connections/mod.rs new file mode 100644 index 0000000..b4e5ca1 --- /dev/null +++ b/src/wallet/connections/mod.rs @@ -0,0 +1,19 @@ +// 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. + +mod external; +pub use external::ExternalConnection; + +mod config; +pub use config::ConnectionsConfig; \ No newline at end of file diff --git a/src/wallet/mnemonic.rs b/src/wallet/mnemonic.rs new file mode 100644 index 0000000..78ab853 --- /dev/null +++ b/src/wallet/mnemonic.rs @@ -0,0 +1,106 @@ +// 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 grin_keychain::mnemonic::{from_entropy, search, to_entropy}; +use rand::{Rng, thread_rng}; + +use crate::wallet::types::{PhraseMode, PhraseSize}; + +/// Mnemonic phrase container. +pub struct Mnemonic { + /// Phrase setup mode. + pub(crate) mode: PhraseMode, + /// Size of phrase based on words count. + pub(crate) size: PhraseSize, + /// Generated words. + pub(crate) words: Vec, + /// Words to confirm the phrase. + pub(crate) confirm_words: Vec +} + +impl Default for Mnemonic { + fn default() -> Self { + let size = PhraseSize::Words24; + let mode = PhraseMode::Generate; + let words = Self::generate_words(&mode, &size); + let confirm_words = Self::empty_words(&size); + Self { mode, size, words, confirm_words } + } +} + +impl Mnemonic { + /// Change mnemonic phrase setup [`PhraseMode`]. + pub fn set_mode(&mut self, mode: PhraseMode) { + self.mode = mode; + self.words = Self::generate_words(&self.mode, &self.size); + self.confirm_words = Self::empty_words(&self.size); + } + + /// Change mnemonic phrase words [`PhraseSize`]. + pub fn set_size(&mut self, size: PhraseSize) { + self.size = size; + self.words = Self::generate_words(&self.mode, &self.size); + self.confirm_words = Self::empty_words(&self.size); + } + + /// Check if provided word is in BIP39 format and equal to non-empty generated word at index. + pub fn is_valid_word(&self, word: &String, index: usize) -> bool { + let valid = search(word).is_ok(); + let equal = if let Some(gen_word) = self.words.get(index) { + gen_word.is_empty() || gen_word == word + } else { + false + }; + valid && equal + } + + /// Check if current phrase is valid. + pub fn is_valid_phrase(&self) -> bool { + to_entropy(self.get_phrase().as_str()).is_ok() + } + + /// Get phrase from words. + pub fn get_phrase(&self) -> String { + self.words.iter().map(|x| x.to_string() + " ").collect::() + } + + /// Generate list of words based on provided [`PhraseMode`] and [`PhraseSize`]. + fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec { + match mode { + PhraseMode::Generate => { + let mut rng = thread_rng(); + let mut entropy: Vec = Vec::with_capacity(size.entropy_size()); + for _ in 0..size.entropy_size() { + entropy.push(rng.gen()); + } + from_entropy(&entropy).unwrap() + .split(" ") + .map(|s| String::from(s)) + .collect::>() + }, + PhraseMode::Import => { + Self::empty_words(size) + } + } + } + + /// Generate empty list of words based on provided [`PhraseSize`]. + fn empty_words(size: &PhraseSize) -> Vec { + let mut words = Vec::with_capacity(size.value()); + for _ in 0..size.value() { + words.push(String::from("")) + } + words + } +} \ No newline at end of file diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index f462b3f..193931d 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -12,16 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod types; pub mod updater; pub mod selection; pub mod tx; pub mod keys; +mod mnemonic; +pub use mnemonic::Mnemonic; + +mod connections; +pub use connections::*; + mod wallets; -pub use wallets::{Wallet, Wallets}; +pub use wallets::*; mod config; -pub use config::*; - -mod types; -pub use types::*; \ No newline at end of file +pub use config::*; \ No newline at end of file diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 7cafd3a..26914e3 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -12,28 +12,47 @@ // See the License for the specific language governing permissions and // limitations under the License. -use serde_derive::{Deserialize, Serialize}; - -/// External node connection for the wallet. -#[derive(Serialize, Deserialize, Clone)] -pub struct ExternalConnection { - /// Node URL. - pub url: String, - /// Optional API secret key. - pub secret: Option +/// Mnemonic phrase setup mode. +#[derive(PartialEq, Clone)] +pub enum PhraseMode { + /// Generate new mnemonic phrase. + Generate, + /// Import existing mnemonic phrase. + Import } -impl ExternalConnection { - /// Default external node URL. - const DEFAULT_EXTERNAL_NODE_URL: &'static str = "https://grinnnode.live:3413"; +/// Mnemonic phrase size based on words count. +#[derive(PartialEq, Clone)] +pub enum PhraseSize { Words12, Words15, Words18, Words21, Words24 } - pub fn new(url: String, secret: Option) -> Self { - Self { url, secret } +impl PhraseSize { + pub const VALUES: [PhraseSize; 5] = [ + PhraseSize::Words12, + PhraseSize::Words15, + PhraseSize::Words18, + PhraseSize::Words21, + PhraseSize::Words24 + ]; + + /// Gen words count number. + pub fn value(&self) -> usize { + match *self { + PhraseSize::Words12 => 12, + PhraseSize::Words15 => 15, + PhraseSize::Words18 => 18, + PhraseSize::Words21 => 21, + PhraseSize::Words24 => 24 + } } -} -impl Default for ExternalConnection { - fn default() -> Self { - Self { url: Self::DEFAULT_EXTERNAL_NODE_URL.to_string(), secret: None } + /// Gen entropy size for current phrase size. + pub fn entropy_size(&self) -> usize { + match *self { + PhraseSize::Words12 => 16, + PhraseSize::Words15 => 20, + PhraseSize::Words18 => 24, + PhraseSize::Words21 => 28, + PhraseSize::Words24 => 32 + } } } \ No newline at end of file diff --git a/src/wallet/wallets.rs b/src/wallet/wallets.rs index 247716d..1124baf 100644 --- a/src/wallet/wallets.rs +++ b/src/wallet/wallets.rs @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::BTreeSet; -use std::fs; +use std::{cmp, thread}; use std::path::PathBuf; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use grin_core::global; use grin_core::global::ChainTypes; @@ -23,68 +23,40 @@ use grin_keychain::{ExtKeychain, Identifier, Keychain}; use grin_util::types::ZeroingString; use grin_wallet_api::{Foreign, ForeignCheckMiddlewareFn, Owner}; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; -use grin_wallet_libwallet::{Error, NodeClient, NodeVersionInfo, OutputStatus, Slate, slate_versions, SlatepackArmor, Slatepacker, SlatepackerArgs, TxLogEntry, wallet_lock, WalletBackend, WalletInfo, WalletInst, WalletLCProvider}; -use grin_wallet_libwallet::Error::GenericError; -use lazy_static::lazy_static; +use grin_wallet_libwallet::{Error, NodeClient, NodeVersionInfo, OutputStatus, scan, Slate, slate_versions, SlatepackArmor, Slatepacker, SlatepackerArgs, TxLogEntry, wallet_lock, WalletBackend, WalletInfo, WalletInst, WalletLCProvider}; use log::debug; use parking_lot::Mutex; use uuid::Uuid; -use crate::{AppConfig, Settings}; +use crate::AppConfig; use crate::node::NodeConfig; +use crate::wallet::{ConnectionsConfig, WalletConfig}; use crate::wallet::selection::lock_tx_context; use crate::wallet::tx::{add_inputs_to_slate, new_tx_slate}; use crate::wallet::updater::{cancel_tx, refresh_output_state, retrieve_txs}; -use crate::wallet::WalletConfig; -lazy_static! { - /// Global wallets state. - static ref WALLETS_STATE: Arc> = Arc::new(RwLock::new(Wallets::init())); -} - -/// Manages [`Wallet`] list and state. +/// [`Wallet`] list wrapper. pub struct Wallets { /// List of wallets. - list: Vec, + pub(crate) list: Vec, /// Selected [`Wallet`] identifier. selected_id: Option, - /// Identifiers of opened wallets. - opened_ids: BTreeSet +} + +impl Default for Wallets { + fn default() -> Self { + Self { + list: Self::init(&AppConfig::chain_type()), + selected_id: None + } + } } impl Wallets { - /// Base wallets directory name. - pub const BASE_DIR_NAME: &'static str = "wallets"; - - /// Initialize manager by loading list of wallets into state. - fn init() -> Self { - Self { - list: Self::load_wallets(&AppConfig::chain_type()), - selected_id: None, - opened_ids: BTreeSet::default() - } - } - - /// Create new wallet and add it to state. - pub fn create_wallet( - name: String, - password: String, - mnemonic: String, - external_node_url: Option - )-> Result<(), Error> { - let wallet = Wallet::create(name, password, mnemonic, external_node_url)?; - let mut w_state = WALLETS_STATE.write().unwrap(); - let id = wallet.config.id; - w_state.opened_ids.insert(id); - w_state.selected_id = Some(id); - w_state.list.insert(0, wallet); - Ok(()) - } - - /// Load wallets for provided [`ChainType`]. - fn load_wallets(chain_type: &ChainTypes) -> Vec { + /// Initialize wallets from base directory for provided [`ChainType`]. + fn init(chain_type: &ChainTypes) -> Vec { let mut wallets = Vec::new(); - let wallets_dir = Self::get_base_path(chain_type); + let wallets_dir = WalletConfig::get_base_path(chain_type); // Load wallets from base directory. for dir in wallets_dir.read_dir().unwrap() { let wallet_dir = dir.unwrap().path(); @@ -98,89 +70,118 @@ impl Wallets { wallets } - /// Get list of wallets. - pub fn list() -> Vec { - let r_state = WALLETS_STATE.read().unwrap(); - r_state.list.clone() + /// Reinitialize wallets for provided [`ChainTypes`]. + pub fn reinit(&mut self, chain_type: &ChainTypes) { + self.list = Self::init(chain_type); } - /// Select [`Wallet`] with provided identifier. - pub fn select(id: Option) { - let mut w_state = WALLETS_STATE.write().unwrap(); - w_state.selected_id = id; + /// Add created [`Wallet`] to the list. + pub fn add(&mut self, wallet: Wallet) { + self.selected_id = Some(wallet.config.id); + self.list.insert(0, wallet); } - /// Get selected [`Wallet`] identifier. - pub fn selected_id() -> Option { - let r_state = WALLETS_STATE.read().unwrap(); - r_state.selected_id + /// Select wallet with provided identifier. + pub fn select(&mut self, id: Option) { + self.selected_id = id; } - /// Open [`Wallet`] with provided identifier and password. - pub fn open(id: i64, password: String) -> Result<(), Error> { - let list = Self::list(); - let mut w_state = WALLETS_STATE.write().unwrap(); - for mut w in list { - if w.config.id == id { - w.open(password)?; - break; + /// Check if wallet is selected for provided identifier. + pub fn is_selected(&self, id: i64) -> bool { + return Some(id) == self.selected_id; + } + + /// Check if selected wallet is open. + pub fn is_selected_open(&self) -> bool { + for w in &self.list { + if Some(w.config.id) == self.selected_id { + return w.is_open() } } - w_state.opened_ids.insert(id); - Ok(()) + false } - /// Close [`Wallet`] with provided identifier. - pub fn close(id: i64) -> Result<(), Error> { - let list = Self::list(); - let mut w_state = WALLETS_STATE.write().unwrap(); - for mut w in list { - if w.config.id == id { - w.close()?; - break; + /// Open selected wallet. + pub fn open_selected(&mut self, password: String) -> Result<(), Error> { + for mut w in self.list.iter_mut() { + if Some(w.config.id) == self.selected_id { + return w.open(password); } } - w_state.opened_ids.remove(&id); - Ok(()) + Err(Error::GenericError("Wallet is not selected".to_string())) } - /// Check if [`Wallet`] with provided identifier was open. - pub fn is_open(id: i64) -> bool { - let r_state = WALLETS_STATE.read().unwrap(); - r_state.opened_ids.contains(&id) - } - - /// Get wallets base directory path for provided [`ChainTypes`]. - pub fn get_base_path(chain_type: &ChainTypes) -> PathBuf { - let mut wallets_path = Settings::get_base_path(Some(chain_type.shortname())); - wallets_path.push(Self::BASE_DIR_NAME); - // Create wallets base directory if it doesn't exist. - if !wallets_path.exists() { - let _ = fs::create_dir_all(wallets_path.clone()); + /// Load the wallet by scanning available outputs at separate thread. + pub fn load(w: &mut Wallet) { + if !w.is_open() { + return; } - wallets_path - } + let mut wallet = w.clone(); + thread::spawn(move || { + // Get pmmr range output indexes. + match wallet.pmmr_range() { + Ok((mut lowest_index, highest_index)) => { + println!("pmmr_range {} {}", lowest_index, highest_index); + let mut from_index = lowest_index; + loop { + // Scan outputs for last retrieved index. + println!("scan_outputs {} {}", from_index, highest_index); + match wallet.scan_outputs(from_index, highest_index) { + Ok(last_index) => { + println!("last_index {}", last_index); + if lowest_index == 0 { + lowest_index = last_index; + } + if last_index == highest_index { + wallet.loading_progress = 100; + break; + } else { + from_index = last_index; + } - /// Reload list of wallets for provided [`ChainTypes`]. - pub fn reload(chain_type: &ChainTypes) { - let wallets = Self::load_wallets(chain_type); - let mut w_state = WALLETS_STATE.write().unwrap(); - w_state.selected_id = None; - w_state.opened_ids = BTreeSet::default(); - w_state.list = wallets; + // Update loading progress. + let range = highest_index - lowest_index; + let progress = last_index - lowest_index; + wallet.loading_progress = cmp::min( + (progress / range) as u8 * 100, + 99 + ); + println!("progress {}", wallet.loading_progress); + } + Err(e) => { + wallet.loading_error = Some(e); + break; + } + } + } + wallet.is_loaded.store(true, Ordering::Relaxed); + } + Err(e) => { + wallet.loading_error = Some(e); + } + } + }); } } -/// Wallet instance and config wrapper. +/// Contains wallet instance and config. #[derive(Clone)] pub struct Wallet { /// Wallet instance. instance: WalletInstance, - /// Wallet data path. - path: String, /// Wallet configuration. pub(crate) config: WalletConfig, + + /// Flag to check if wallet is open. + is_open: Arc, + + /// Flag to check if wallet is loaded and ready to use. + is_loaded: Arc, + /// Error on wallet loading. + loading_error: Option, + /// Loading progress in percents + loading_progress: u8, } /// Wallet instance type. @@ -198,37 +199,38 @@ type WalletInstance = Arc< >; impl Wallet { - /// Create new wallet, make it open and selected. - fn create( + /// Create wallet from provided instance and config. + fn new(instance: WalletInstance, config: WalletConfig) -> Self { + Self { + instance, + config, + is_loaded: Arc::new(AtomicBool::new(false)), + is_open: Arc::new(AtomicBool::new(false)), + loading_error: None, + loading_progress: 0, + } + } + + /// Create new wallet. + pub fn create( name: String, password: String, mnemonic: String, external_node_url: Option ) -> Result { let config = WalletConfig::create(name, external_node_url); - let wallet = Self::create_wallet_instance(config.clone())?; - let w = Wallet { - instance: wallet, - path: config.get_data_path(), - config, - }; - + let instance = Self::create_wallet_instance(config.clone())?; + let w = Wallet::new(instance, config); { let mut w_lock = w.instance.lock(); let p = w_lock.lc_provider()?; - - // Create wallet. p.create_wallet(None, Some(ZeroingString::from(mnemonic.clone())), mnemonic.len(), - ZeroingString::from(password.clone()), + ZeroingString::from(password), false, )?; - - // Open wallet. - p.open_wallet(None, ZeroingString::from(password), false, false)?; } - Ok(w) } @@ -236,14 +238,20 @@ impl Wallet { fn init(data_path: PathBuf) -> Option { let wallet_config = WalletConfig::load(data_path.clone()); if let Some(config) = wallet_config { - let path = data_path.to_str().unwrap().to_string(); if let Ok(instance) = Self::create_wallet_instance(config.clone()) { - return Some(Self { instance, path, config }); + return Some(Wallet::new(instance, config)); } } None } + /// Reinitialize wallet instance to apply new config e.g. on change connection settings. + pub fn reinit(&mut self) -> Result<(), Error> { + self.close()?; + self.instance = Self::create_wallet_instance(self.config.clone())?; + Ok(()) + } + /// Create wallet instance from provided config. fn create_wallet_instance(config: WalletConfig) -> Result { // Assume global chain type has already been initialized. @@ -257,9 +265,11 @@ impl Wallet { // Setup node client. let (node_api_url, node_secret) = if let Some(url) = &config.external_node_url { - (url.to_owned(), None) + (url.to_owned(), ConnectionsConfig::get_external_connection_secret(url.to_owned())) } else { - (NodeConfig::get_api_address(), NodeConfig::get_api_secret()) + let api_url = format!("http://{}", NodeConfig::get_api_address()); + let api_secret = NodeConfig::get_foreign_api_secret(); + (api_url, api_secret) }; let node_client = HTTPNodeClient::new(&node_api_url, node_secret)?; @@ -290,22 +300,69 @@ impl Wallet { Ok(Arc::new(Mutex::new(wallet))) } - /// Open wallet. - fn open(&mut self, password: String) -> Result<(), Error> { - let mut wallet_lock = self.instance.lock(); - let lc = wallet_lock.lc_provider()?; - lc.open_wallet(None, ZeroingString::from(password), false, false)?; - Ok(()) - } - - /// Close wallet. - fn close(&mut self) -> Result<(), Error> { + /// Open wallet instance. + pub fn open(&self, password: String) -> Result<(), Error> { let mut wallet_lock = self.instance.lock(); let lc = wallet_lock.lc_provider()?; lc.close_wallet(None)?; + lc.open_wallet(None, ZeroingString::from(password), false, false)?; + self.is_open.store(true, Ordering::Relaxed); Ok(()) } + /// Check if wallet is open. + pub fn is_open(&self) -> bool { + self.is_open.load(Ordering::Relaxed) + } + + /// Close wallet. + pub fn close(&mut self) -> Result<(), Error> { + if self.is_open() { + let mut wallet_lock = self.instance.lock(); + let lc = wallet_lock.lc_provider()?; + lc.close_wallet(None)?; + self.is_open.store(false, Ordering::Relaxed); + } + Ok(()) + } + + /// Scan wallet outputs to check/repair the wallet. + fn scan_outputs( + &self, + last_retrieved_index: u64, + highest_index: u64 + ) -> Result { + let wallet = self.instance.clone(); + let info = scan( + wallet.clone(), + None, + false, + last_retrieved_index, + highest_index, + &None, + )?; + let result = info.last_pmmr_index; + + let parent_key_id = { + wallet_lock!(wallet.clone(), w); + w.parent_key_id().clone() + }; + { + wallet_lock!(wallet, w); + let mut batch = w.batch(None)?; + batch.save_last_confirmed_height(&parent_key_id, info.height)?; + batch.commit()?; + }; + Ok(result) + } + + /// Get pmmr indices representing the outputs for the wallet. + fn pmmr_range(&self) -> Result<(u64, u64), Error> { + wallet_lock!(self.instance.clone(), w); + let pmmr_range = w.w2n_client().height_range_to_pmmr_indices(0, None)?; + Ok(pmmr_range) + } + /// Create transaction. pub fn tx_create( &self, @@ -463,10 +520,10 @@ impl Wallet { let tx_uuid = Uuid::parse_str(tx_slate_id).unwrap(); let (_, txs) = api.retrieve_txs(None, true, None, Some(tx_uuid.clone()), None)?; if txs[0].confirmed { - return Err(Error::from(GenericError(format!( + return Err(Error::GenericError(format!( "Transaction with id {} is already confirmed. Not posting.", tx_slate_id - )))); + ))); } let stored_tx = api.get_stored_tx(None, None, Some(&tx_uuid))?; match stored_tx { @@ -474,10 +531,10 @@ impl Wallet { api.post_tx(None, &stored_tx, true)?; Ok(()) } - None => Err(Error::from(GenericError(format!( + None => Err(Error::GenericError(format!( "Transaction with id {} does not have transaction data. Not posting.", tx_slate_id - )))), + ))), } }