From cb9e86750c531c43faa5d699e841d0ba541a21c2 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 10 Aug 2024 02:35:42 +0300 Subject: [PATCH] mnemonic: words import and errors check refactoring --- src/gui/views/wallets/creation/creation.rs | 55 +++--- src/gui/views/wallets/creation/mnemonic.rs | 120 ++++++-------- src/wallet/mnemonic.rs | 184 ++++++++++++++++----- src/wallet/types.rs | 9 + src/wallet/wallet.rs | 2 +- 5 files changed, 229 insertions(+), 141 deletions(-) diff --git a/src/gui/views/wallets/creation/creation.rs b/src/gui/views/wallets/creation/creation.rs index b44f091..d8b3cc8 100644 --- a/src/gui/views/wallets/creation/creation.rs +++ b/src/gui/views/wallets/creation/creation.rs @@ -143,26 +143,19 @@ impl WalletCreation { // Setup step description text and availability. let (step_text, mut step_available) = match step { Step::EnterMnemonic => { - let mode = &self.mnemonic_setup.mnemonic.mode; - let text = if mode == &PhraseMode::Generate { - t!("wallets.create_phrase_desc") - } else { - t!("wallets.restore_phrase_desc") + let mode = &self.mnemonic_setup.mnemonic.mode(); + let (text, available) = match mode { + PhraseMode::Generate => (t!("wallets.create_phrase_desc"), true), + PhraseMode::Import => { + let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid(); + (t!("wallets.restore_phrase_desc"), available) + } }; - let available = !self - .mnemonic_setup - .mnemonic - .words - .contains(&String::from("")); (text, available) } Step::ConfirmMnemonic => { let text = t!("wallets.restore_phrase_desc"); - let available = !self - .mnemonic_setup - .mnemonic - .confirm_words - .contains(&String::from("")); + let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid(); (text, available) } Step::SetupConnection => { @@ -170,8 +163,11 @@ impl WalletCreation { } }; - // Show step description or error if entered phrase is not valid. - if self.mnemonic_setup.valid_phrase && self.creation_error.is_none() { + // Show step description or error. + let generate_step = step == Step::EnterMnemonic && + self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate; + if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) || + generate_step { ui.add_space(2.0); ui.label(RichText::new(step_text).size(16.0).color(Colors::gray())); ui.add_space(2.0); @@ -185,6 +181,7 @@ impl WalletCreation { .color(Colors::red())); ui.add_space(10.0); } else { + ui.add_space(2.0); ui.label(RichText::new(&t!("wallets.not_valid_phrase")) .size(16.0) .color(Colors::red())); @@ -225,8 +222,8 @@ impl WalletCreation { } else { let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase()); View::button(ui, paste_text, Colors::white_or_black(false), || { - let data = ZeroingString::from(cb.get_string_from_buffer().trim()); - self.mnemonic_setup.mnemonic.import_text(&data, true); + let data = ZeroingString::from(cb.get_string_from_buffer()); + self.mnemonic_setup.mnemonic.import(&data); }); } ui.add_space(4.0); @@ -241,7 +238,7 @@ impl WalletCreation { /// Draw copy or paste button at [`Step::EnterMnemonic`]. fn copy_or_paste_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - match self.mnemonic_setup.mnemonic.mode { + match self.mnemonic_setup.mnemonic.mode() { PhraseMode::Generate => { // Show copy button. let c_t = format!("{} {}", COPY, t!("copy").to_uppercase()); @@ -253,8 +250,8 @@ impl WalletCreation { // Show paste button. let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase()); View::button(ui, p_t, Colors::white_or_black(false), || { - let data = ZeroingString::from(cb.get_string_from_buffer().trim()); - self.mnemonic_setup.mnemonic.import_text(&data, false); + let data = ZeroingString::from(cb.get_string_from_buffer()); + self.mnemonic_setup.mnemonic.import(&data); }); } } @@ -278,15 +275,10 @@ impl WalletCreation { self.step = if let Some(step) = &self.step { match step { Step::EnterMnemonic => { - if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate { + 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 { - Some(Step::EnterMnemonic) - } + Some(Step::SetupConnection) } } Step::ConfirmMnemonic => { @@ -388,7 +380,10 @@ impl WalletCreation { self.creation_error = None; }, Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic), - Step::SetupConnection => self.step = Some(Step::EnterMnemonic) + Step::SetupConnection => { + self.creation_error = None; + self.step = Some(Step::EnterMnemonic) + } } } } diff --git a/src/gui/views/wallets/creation/mnemonic.rs b/src/gui/views/wallets/creation/mnemonic.rs index bc4013d..b4cdc37 100644 --- a/src/gui/views/wallets/creation/mnemonic.rs +++ b/src/gui/views/wallets/creation/mnemonic.rs @@ -20,21 +20,18 @@ use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{CameraContent, Modal, Content, View}; use crate::gui::views::types::{ModalContainer, ModalPosition, QrScanResult, TextEditOptions}; use crate::wallet::Mnemonic; -use crate::wallet::types::{PhraseMode, PhraseSize}; +use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord}; /// Mnemonic phrase setup content. pub struct MnemonicSetup { /// Current mnemonic phrase. - pub(crate) mnemonic: Mnemonic, - - /// Flag to check if entered phrase was valid. - pub(crate) valid_phrase: bool, + pub mnemonic: Mnemonic, /// Current word number to edit at [`Modal`]. - word_num_edit: usize, + word_index_edit: usize, /// Entered word value for [`Modal`]. word_edit: String, - /// Flag to check if entered word is valid. + /// Flag to check if entered word is valid at [`Modal`]. valid_word_edit: bool, /// Camera content for QR scan [`Modal`]. @@ -56,8 +53,7 @@ impl Default for MnemonicSetup { fn default() -> Self { Self { mnemonic: Mnemonic::default(), - valid_phrase: true, - word_num_edit: 0, + word_index_edit: 0, word_edit: String::from(""), valid_word_edit: true, camera_content: CameraContent::default(), @@ -103,7 +99,7 @@ impl MnemonicSetup { ui.add_space(6.0); // Show words setup. - self.word_list_ui(ui, self.mnemonic.mode == PhraseMode::Import, cb); + self.word_list_ui(ui, self.mnemonic.mode() == PhraseMode::Import, cb); } /// Draw content for phrase confirmation step. @@ -123,7 +119,7 @@ impl MnemonicSetup { /// Draw mode and size setup. fn mode_type_ui(&mut self, ui: &mut egui::Ui) { // Show mode setup. - let mut mode = self.mnemonic.mode.clone(); + let mut mode = self.mnemonic.mode(); ui.columns(2, |columns| { columns[0].vertical_centered(|ui| { let create_mode = PhraseMode::Generate; @@ -136,8 +132,8 @@ impl MnemonicSetup { View::radio_value(ui, &mut mode, import_mode, import_text); }); }); - if mode != self.mnemonic.mode { - self.mnemonic.set_mode(mode) + if mode != self.mnemonic.mode() { + self.mnemonic.set_mode(mode); } ui.add_space(10.0); @@ -150,7 +146,7 @@ impl MnemonicSetup { ui.add_space(6.0); // Show mnemonic phrase size setup. - let mut size = self.mnemonic.size.clone(); + let mut size = self.mnemonic.size(); ui.columns(5, |columns| { for (index, word) in PhraseSize::VALUES.iter().enumerate() { columns[index].vertical_centered(|ui| { @@ -159,29 +155,20 @@ impl MnemonicSetup { }); } }); - if size != self.mnemonic.size { + if size != self.mnemonic.size() { self.mnemonic.set_size(size); } } - /// Draw list of words for mnemonic phrase. - fn word_list_ui(&mut self, ui: &mut egui::Ui, edit_words: bool, cb: &dyn PlatformCallbacks) { + /// Draw grid of words for mnemonic phrase. + fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool, cb: &dyn PlatformCallbacks) { ui.add_space(6.0); ui.scope(|ui| { // Setup spacing between columns. ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0); // Select list of words based on current mode and edit flag. - let words = match self.mnemonic.mode { - PhraseMode::Generate => { - if edit_words { - &self.mnemonic.confirm_words - } else { - &self.mnemonic.words - } - } - PhraseMode::Import => &self.mnemonic.words - }.clone(); + let words = self.mnemonic.words(edit); let mut word_number = 0; let cols = list_columns_count(ui); @@ -192,25 +179,25 @@ impl MnemonicSetup { ui.columns(cols, |columns| { columns[0].horizontal(|ui| { let word = chunk.get(0).unwrap(); - self.word_item_ui(ui, word_number, word, edit_words, cb); + self.word_item_ui(ui, word_number, word, edit, cb); }); columns[1].horizontal(|ui| { word_number += 1; let word = chunk.get(1).unwrap(); - self.word_item_ui(ui, word_number, word, edit_words, cb); + self.word_item_ui(ui, word_number, word, edit, cb); }); if size > 2 { columns[2].horizontal(|ui| { word_number += 1; let word = chunk.get(2).unwrap(); - self.word_item_ui(ui, word_number, word, edit_words, cb); + self.word_item_ui(ui, word_number, word, edit, cb); }); } if size > 3 { columns[3].horizontal(|ui| { word_number += 1; let word = chunk.get(3).unwrap(); - self.word_item_ui(ui, word_number, word, edit_words, cb); + self.word_item_ui(ui, word_number, word, edit, cb); }); } }); @@ -218,7 +205,7 @@ impl MnemonicSetup { ui.columns(cols, |columns| { columns[0].horizontal(|ui| { let word = chunk.get(0).unwrap(); - self.word_item_ui(ui, word_number, word, edit_words, cb); + self.word_item_ui(ui, word_number, word, edit, cb); }); }); } @@ -227,20 +214,24 @@ impl MnemonicSetup { ui.add_space(6.0); } - /// Draw word list item for current mode. + /// Draw word grid item. fn word_item_ui(&mut self, ui: &mut egui::Ui, num: usize, - word: &String, + word: &PhraseWord, edit: bool, cb: &dyn PlatformCallbacks) { + let color = if !word.valid || (word.text.is_empty() && !self.mnemonic.valid()) { + Colors::red() + } else { + Colors::white_or_black(true) + }; if edit { ui.add_space(6.0); View::button(ui, PENCIL.to_string(), Colors::button(), || { - // Setup modal values. - self.word_num_edit = num; - self.word_edit = word.clone(); - self.valid_word_edit = true; + self.word_index_edit = num - 1; + self.word_edit = word.text.clone(); + self.valid_word_edit = word.valid; // Show word edit modal. Modal::new(WORD_INPUT_MODAL) .position(ModalPosition::CenterTop) @@ -248,34 +239,33 @@ impl MnemonicSetup { .show(); cb.show_keyboard(); }); - ui.label(RichText::new(format!("#{} {}", num, word)) + ui.label(RichText::new(format!("#{} {}", num, word.text)) .size(17.0) - .color(Colors::white_or_black(true))); + .color(color)); } else { ui.add_space(12.0); - let text = format!("#{} {}", num, word); - ui.label(RichText::new(text).size(17.0).color(Colors::white_or_black(true))); + let text = format!("#{} {}", num, word.text); + ui.label(RichText::new(text).size(17.0).color(color)); } } - /// Reset mnemonic phrase to default values. + /// Reset mnemonic phrase state to default values. pub fn reset(&mut self) { self.mnemonic = Mnemonic::default(); - self.valid_phrase = true; } /// Draw word input [`Modal`] content. fn word_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { ui.add_space(6.0); ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_num_edit)) + ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_index_edit + 1)) .size(17.0) .color(Colors::gray())); ui.add_space(8.0); // Draw word value text edit. let mut text_edit_opts = TextEditOptions::new( - Id::from(modal.id).with(self.word_num_edit) + Id::from(modal.id).with(self.word_index_edit) ); View::text_edit(ui, cb, &mut self.word_edit, &mut text_edit_opts); @@ -305,38 +295,22 @@ impl MnemonicSetup { columns[1].vertical_centered_justified(|ui| { // Callback to save the word. let mut save = || { - self.word_edit = self.word_edit.trim().to_string(); - - // Check if word is valid. - if !self.mnemonic.is_valid_word(&self.word_edit) { - self.valid_word_edit = false; + // Insert word checking validity. + let word = &self.word_edit.trim().to_string(); + self.valid_word_edit = self.mnemonic.insert(self.word_index_edit, word); + if !self.valid_word_edit { return; } - self.valid_word_edit = true; - - // Select list where to save word. - let words = match self.mnemonic.mode { - PhraseMode::Generate => &mut self.mnemonic.confirm_words, - PhraseMode::Import => &mut self.mnemonic.words - }; - - // Save word at list. - let word_index = self.word_num_edit - 1; - words.remove(word_index); - words.insert(word_index, self.word_edit.clone()); - // Close modal or go to next word to edit. - let close_modal = words.len() == self.word_num_edit - || !words.get(self.word_num_edit).unwrap().is_empty(); + let next_word = self.mnemonic.get(self.word_index_edit + 1); + let close_modal = next_word.is_none() || + (!next_word.as_ref().unwrap().text.is_empty() && + next_word.unwrap().valid); if close_modal { - // Check if entered phrase was valid when all words were entered. - if !self.mnemonic.words.contains(&String::from("")) { - self.valid_phrase = self.mnemonic.is_valid_phrase(); - } cb.hide_keyboard(); modal.close(); } else { - self.word_num_edit += 1; + self.word_index_edit += 1; self.word_edit = String::from(""); } }; @@ -383,8 +357,8 @@ impl MnemonicSetup { self.camera_content.clear_state(); match &result { QrScanResult::Text(text) => { - self.mnemonic.import_text(text, false); - if self.mnemonic.is_valid_phrase() { + self.mnemonic.import(text); + if self.mnemonic.valid() { modal.close(); return; } diff --git a/src/wallet/mnemonic.rs b/src/wallet/mnemonic.rs index fec22b8..e678cf5 100644 --- a/src/wallet/mnemonic.rs +++ b/src/wallet/mnemonic.rs @@ -16,18 +16,20 @@ use grin_keychain::mnemonic::{from_entropy, search, to_entropy}; use grin_util::ZeroingString; use rand::{Rng, thread_rng}; -use crate::wallet::types::{PhraseMode, PhraseSize}; +use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord}; /// Mnemonic phrase container. pub struct Mnemonic { /// Phrase setup mode. - pub(crate) mode: PhraseMode, + mode: PhraseMode, /// Size of phrase based on words count. - pub(crate) size: PhraseSize, + size: PhraseSize, /// Generated words. - pub(crate) words: Vec, + words: Vec, /// Words to confirm the phrase. - pub(crate) confirm_words: Vec + confirmation: Vec, + /// Flag to check if entered phrase if valid. + valid: bool, } impl Default for Mnemonic { @@ -35,43 +37,67 @@ impl Default for Mnemonic { 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 } + let confirmation = Self::empty_words(&size); + Self { mode, size, words, confirmation, valid: true } } } impl Mnemonic { - /// Change mnemonic phrase setup [`PhraseMode`]. + /// Generate words based on provided [`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); + self.confirmation = Self::empty_words(&self.size); + self.valid = true; } - /// Change mnemonic phrase words [`PhraseSize`]. + /// Get current phrase mode. + pub fn mode(&self) -> PhraseMode { + self.mode.clone() + } + + /// Generate words based on provided [`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); + self.confirmation = Self::empty_words(&self.size); + self.valid = true; } - /// Check if provided word is in BIP39 format. - pub fn is_valid_word(&self, word: &String) -> bool { - search(word).is_ok() + /// Get current phrase size. + pub fn size(&self) -> PhraseSize { + self.size.clone() + } + + /// Get words based on current [`PhraseMode`]. + pub fn words(&self, edit: bool) -> Vec { + match self.mode { + PhraseMode::Generate => { + if edit { + &self.confirmation + } else { + &self.words + } + } + PhraseMode::Import => &self.words + }.clone() } /// Check if current phrase is valid. - pub fn is_valid_phrase(&self) -> bool { - to_entropy(self.get_phrase().as_str()).is_ok() + pub fn valid(&self) -> bool { + self.valid } /// Get phrase from words. pub fn get_phrase(&self) -> String { - self.words.iter().map(|x| x.to_string() + " ").collect::() + self.words.iter() + .enumerate() + .map(|(i, x)| if i == 0 { "" } else { " " }.to_owned() + &x.text) + .collect::() } - /// Generate list of words based on provided [`PhraseMode`] and [`PhraseSize`]. - fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec { + /// Generate [`PhraseWord`] list based on provided [`PhraseMode`] and [`PhraseSize`]. + fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec { match mode { PhraseMode::Generate => { let mut rng = thread_rng(); @@ -81,8 +107,14 @@ impl Mnemonic { } from_entropy(&entropy).unwrap() .split(" ") - .map(|s| String::from(s)) - .collect::>() + .map(|s| { + let text = s.to_string(); + PhraseWord { + text, + valid: true, + } + }) + .collect::>() }, PhraseMode::Import => { Self::empty_words(size) @@ -91,39 +123,117 @@ impl Mnemonic { } /// Generate empty list of words based on provided [`PhraseSize`]. - fn empty_words(size: &PhraseSize) -> Vec { + fn empty_words(size: &PhraseSize) -> Vec { let mut words = Vec::with_capacity(size.value()); for _ in 0..size.value() { - words.push(String::from("")) + words.push(PhraseWord { + text: "".to_string(), + valid: true, + }); } words } - /// Set words from provided text if possible. - pub fn import_text(&mut self, text: &ZeroingString, confirmation: bool) { + /// Insert word into provided index and return validation result. + pub fn insert(&mut self, index: usize, word: &String) -> bool { + // Check if word is valid. + let found = search(word).is_ok(); + if !found { + return false; + } + let is_confirmation = self.mode == PhraseMode::Generate; + if is_confirmation { + let w = self.words.get(index).unwrap(); + if word != &w.text { + return false; + } + } + + // Save valid word at list. + let words = if is_confirmation { + &mut self.confirmation + } else { + &mut self.words + }; + words.remove(index); + words.insert(index, PhraseWord { text: word.to_owned(), valid: true }); + + // Validate phrase when all words are entered. + let mut has_empty = false; + let _: Vec<_> = words.iter().map(|w| { + if w.text.is_empty() { + has_empty = true; + } + }).collect(); + if !has_empty { + self.valid = to_entropy(self.get_phrase().as_str()).is_ok(); + } + true + } + + /// Get word from provided index. + pub fn get(&self, index: usize) -> Option { + let words = match self.mode { + PhraseMode::Generate => &self.confirmation, + PhraseMode::Import => &self.words + }; + let word = words.get(index); + if let Some(w) = word { + return Some(PhraseWord { + text: w.text.clone(), + valid: w.valid + }); + } + None + } + + /// Setup phrase from provided text if possible. + pub fn import(&mut self, text: &ZeroingString) { let words_split = text.trim().split(" "); let count = words_split.clone().count(); if let Some(size) = PhraseSize::type_for_value(count) { - if !confirmation { + // Setup phrase size. + let confirm = self.mode == PhraseMode::Generate; + if !confirm { self.size = size; } else if self.size != size { return; } + + // Setup word list. let mut words = vec![]; - words_split.for_each(|word| { - if confirmation && !self.is_valid_word(&word.to_string()) { - words = vec![]; - return; - } - words.push(word.to_string()) + words_split.for_each(|w| { + let mut text = w.to_string(); + text.retain(|c| c.is_alphabetic()); + let valid = search(&text).is_ok(); + words.push(PhraseWord { text, valid }); }); - if confirmation { - if !words.is_empty() { - self.confirm_words = words; + let mut has_invalid = false; + for (i, w) in words.iter().enumerate() { + if !self.insert(i, &w.text) { + has_invalid = true; } - } else { - self.words = words; } + self.valid = !has_invalid; } } + + /// Check if phrase has invalid or empty words. + pub fn has_empty_or_invalid(&self) -> bool { + let words = match self.mode { + PhraseMode::Generate => &self.confirmation, + PhraseMode::Import => &self.words + }; + let mut has_empty = false; + let mut has_invalid = false; + let _: Vec<_> = words.iter().map(|w| { + if w.text.is_empty() { + has_empty = true; + } + if !w.valid { + has_invalid = true; + } + }).collect(); + has_empty || has_invalid + } } \ No newline at end of file diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 7dd594e..49885c0 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -19,6 +19,15 @@ use grin_util::Mutex; use grin_wallet_impls::{DefaultLCProvider, HTTPNodeClient}; use grin_wallet_libwallet::{TxLogEntry, TxLogEntryType, WalletInfo, WalletInst}; +/// Mnemonic phrase word. +#[derive(Clone)] +pub struct PhraseWord { + /// Word text. + pub text: String, + /// Flag to check if word is valid. + pub valid: bool, +} + /// Mnemonic phrase setup mode. #[derive(PartialEq, Clone)] pub enum PhraseMode { diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 90938b9..99d313e 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -140,7 +140,7 @@ impl Wallet { let p = w_lock.lc_provider()?; p.create_wallet(None, Some(ZeroingString::from(mnemonic.get_phrase())), - mnemonic.size.entropy_size(), + mnemonic.size().entropy_size(), ZeroingString::from(password.clone()), false, )?;