diff --git a/core/embed/rust/build.rs b/core/embed/rust/build.rs index b6615f1e12..6de3bd83fc 100644 --- a/core/embed/rust/build.rs +++ b/core/embed/rust/build.rs @@ -313,6 +313,8 @@ fn generate_trezorhal_bindings() { // slip39 .allowlist_function("slip39_word_completion_mask") .allowlist_function("button_sequence_to_word") + .allowlist_var("SLIP39_WORDLIST") + .allowlist_var("SLIP39_WORD_COUNT") // random .allowlist_function("random_uniform") // rgb led diff --git a/core/embed/rust/src/trezorhal/bip39.rs b/core/embed/rust/src/trezorhal/bip39.rs index 1fe4fe9dfa..035ec4e951 100644 --- a/core/embed/rust/src/trezorhal/bip39.rs +++ b/core/embed/rust/src/trezorhal/bip39.rs @@ -1,40 +1,10 @@ -use super::ffi; -use core::cmp::Ordering; -use cstr_core::CStr; - -unsafe fn from_utf8_unchecked<'a>(word: *const cty::c_char) -> &'a str { - // SAFETY: caller must pass a valid 0-terminated UTF-8 string. - // This assumption holds for usage on words of the BIP-39 wordlist. - unsafe { - let word = CStr::from_ptr(word); - core::str::from_utf8_unchecked(word.to_bytes()) - } -} - -/// Compare word from wordlist to a prefix. -/// -/// The comparison returns Less if the word comes lexicographically before all -/// possible words starting with `prefix`, and Greater if it comes after. -/// Equal is returned if the word starts with `prefix`. -unsafe fn prefix_cmp(prefix: &str, word: *const cty::c_char) -> Ordering { - // SAFETY: we assume `word` is a pointer to a 0-terminated string. - for (i, prefix_char) in prefix.as_bytes().iter().enumerate() { - let word_char = unsafe { *(word.add(i)) } as u8; - if word_char == 0 { - // Prefix is longer than word. - return Ordering::Less; - } else if *prefix_char != word_char { - return word_char.cmp(prefix_char); - } - } - Ordering::Equal -} +use super::{ffi, wordlist::Wordlist}; pub fn complete_word(prefix: &str) -> Option<&'static str> { if prefix.is_empty() { None } else { - Wordlist::all().filter_prefix(prefix).iter().next() + Wordlist::bip39().filter_prefix(prefix).iter().next() } } @@ -42,7 +12,7 @@ pub fn options_num(prefix: &str) -> Option { if prefix.is_empty() { None } else { - Some(Wordlist::all().filter_prefix(prefix).len()) + Some(Wordlist::bip39().filter_prefix(prefix).len()) } } @@ -51,201 +21,3 @@ pub fn word_completion_mask(prefix: &str) -> u32 { // passed byte string, making the call safe. unsafe { ffi::mnemonic_word_completion_mask(prefix.as_ptr() as _, prefix.len() as _) } } - -/// Returns all possible letters that form a valid word together with some -/// prefix. -pub fn get_available_letters(prefix: &str) -> impl Iterator { - const CHARS: [char; 26] = [ - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', - 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - ]; - - let mask = word_completion_mask(prefix); - CHARS - .iter() - .filter(move |ch| bitmask_from_char(ch) & mask != 0) - .copied() -} - -fn bitmask_from_char(ch: &char) -> u32 { - 1 << (*ch as u8 - b'a') -} - -pub struct Wordlist(&'static [*const cty::c_char]); - -impl Wordlist { - pub fn all() -> Self { - Self(unsafe { &ffi::BIP39_WORDLIST_ENGLISH }) - } - - pub const fn empty() -> Self { - Self(&[]) - } - - pub fn filter_prefix(&self, prefix: &str) -> Self { - let mut start = 0usize; - let mut end = self.0.len(); - for (i, word) in self.0.iter().enumerate() { - // SAFETY: We assume our slice is an array of 0-terminated strings. - match unsafe { prefix_cmp(prefix, *word) } { - Ordering::Less => { - start = i + 1; - } - Ordering::Greater => { - end = i; - break; - } - _ => {} - } - } - Self(&self.0[start..end]) - } - - pub fn get(&self, index: usize) -> Option<&'static str> { - // SAFETY: we assume every word in the wordlist is a valid 0-terminated UTF-8 - // string. - self.0 - .get(index) - .map(|word| unsafe { from_utf8_unchecked(*word) }) - } - - pub const fn len(&self) -> usize { - self.0.len() - } - - pub fn iter(&self) -> impl Iterator { - self.0 - .iter() - .map(|word| unsafe { from_utf8_unchecked(*word) }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use cstr_core::cstr; - - const BIP39_WORD_COUNT: usize = ffi::BIP39_WORD_COUNT as usize; - - #[test] - fn test_prefix_cmp() { - assert_eq!( - unsafe { prefix_cmp("", cstr!("").as_ptr()) }, - Ordering::Equal - ); - - assert_eq!( - unsafe { prefix_cmp("b", cstr!("").as_ptr()) }, - Ordering::Less - ); - assert_eq!( - unsafe { prefix_cmp("b", cstr!("a").as_ptr()) }, - Ordering::Less - ); - assert_eq!( - unsafe { prefix_cmp("b", cstr!("b").as_ptr()) }, - Ordering::Equal - ); - assert_eq!( - unsafe { prefix_cmp("b", cstr!("below").as_ptr()) }, - Ordering::Equal - ); - assert_eq!( - unsafe { prefix_cmp("b", cstr!("c").as_ptr()) }, - Ordering::Greater - ); - - assert_eq!( - unsafe { prefix_cmp("bartender", cstr!("bar").as_ptr()) }, - Ordering::Less - ); - } - - #[test] - fn test_filter_prefix_empty() { - let words = Wordlist::all().filter_prefix(""); - assert_eq!(words.len(), BIP39_WORD_COUNT); - let iter = words.iter(); - assert_eq!(iter.size_hint(), (BIP39_WORD_COUNT, Some(BIP39_WORD_COUNT))); - } - - #[test] - fn test_filter_prefix() { - let expected_result = vec!["strategy", "street", "strike", "strong", "struggle"]; - let result = Wordlist::all() - .filter_prefix("str") - .iter() - .collect::>(); - assert_eq!(result, expected_result); - } - - #[test] - fn test_filter_prefix_refine() { - let expected_result = vec!["strategy", "street", "strike", "strong", "struggle"]; - let words = Wordlist::all().filter_prefix("st"); - let result_a = words.filter_prefix("str").iter().collect::>(); - let result_b = Wordlist::all() - .filter_prefix("str") - .iter() - .collect::>(); - assert_eq!(result_a, expected_result); - assert_eq!(result_b, expected_result); - - let empty = words.filter_prefix("c"); - assert_eq!(empty.len(), 0); - } - - #[test] - fn test_wordlist_get() { - let words = Wordlist::all(); - assert_eq!(words.get(0), Some("abandon")); - assert_eq!(words.get(BIP39_WORD_COUNT - 1), Some("zoo")); - assert_eq!(words.get(BIP39_WORD_COUNT), None); - assert_eq!(words.get(BIP39_WORD_COUNT + 1), None); - - let filtered = words.filter_prefix("str"); - assert_eq!(filtered.get(0), Some("strategy")); - assert_eq!(filtered.get(filtered.len()), None); - } - - #[test] - fn test_filter_prefix_just_one() { - let expected_result = vec!["stick"]; - let result = Wordlist::all() - .filter_prefix("stick") - .iter() - .collect::>(); - assert_eq!(result, expected_result); - } - - #[test] - fn test_word_completion_mask() { - let result = word_completion_mask("ab"); - assert_eq!(result, 0b101000100100100000001); - let result = word_completion_mask("zoo"); - assert_eq!(result, 0b0); - } - - #[test] - fn test_get_available_letters() { - let expected_result = vec!['a', 'i', 'l', 'o', 's', 'u']; - let result = get_available_letters("ab").collect::>(); - assert_eq!(result, expected_result); - - let expected_result = vec!['a', 'e', 'i', 'o', 'u']; - let result = get_available_letters("str").collect::>(); - assert_eq!(result, expected_result); - - let result = get_available_letters("zoo").collect::>(); - assert_eq!(result.len(), 0); - } - - #[test] - fn test_bitmask_from_char() { - assert_eq!(bitmask_from_char(&'a'), 0b1); - assert_eq!(bitmask_from_char(&'b'), 0b10); - assert_eq!(bitmask_from_char(&'c'), 0b100); - assert_eq!(bitmask_from_char(&'m'), 0b1000000000000); - assert_eq!(bitmask_from_char(&'z'), 0b10000000000000000000000000); - } -} diff --git a/core/embed/rust/src/trezorhal/mod.rs b/core/embed/rust/src/trezorhal/mod.rs index 9696c53dcd..45122be2a5 100644 --- a/core/embed/rust/src/trezorhal/mod.rs +++ b/core/embed/rust/src/trezorhal/mod.rs @@ -15,6 +15,7 @@ pub mod slip39; pub mod storage; pub mod usb; pub mod uzlib; +pub mod wordlist; pub mod buffers; #[cfg(not(feature = "micropython"))] diff --git a/core/embed/rust/src/trezorhal/wordlist.rs b/core/embed/rust/src/trezorhal/wordlist.rs new file mode 100644 index 0000000000..ab2303db32 --- /dev/null +++ b/core/embed/rust/src/trezorhal/wordlist.rs @@ -0,0 +1,199 @@ +use super::ffi; +use core::cmp::Ordering; +use cstr_core::CStr; + +/// Holds all the possible words with the possibility to interact +/// with the "list" - filtering it further, getting their count, etc. +pub struct Wordlist(&'static [*const cty::c_char]); + +impl Wordlist { + /// Initialize BIP39 wordlist. + pub fn bip39() -> Self { + Self(unsafe { &ffi::BIP39_WORDLIST_ENGLISH }) + } + + /// Initialize SLIP39 wordlist. + pub fn slip39() -> Self { + Self(unsafe { &ffi::SLIP39_WORDLIST }) + } + + /// Returns all possible letters that form a valid word together with some + /// prefix. Alphabetically sorted. + pub fn get_available_letters(&self, prefix: &str) -> impl Iterator { + // Fill a "set" of all unique characters, not sorted yet + let mut suffixes: heapless::Vec = heapless::Vec::new(); + for word in self.iter() { + if word.starts_with(prefix) && word.len() > prefix.len() { + let following_char = word.chars().nth(prefix.len()).unwrap(); + if !suffixes.contains(&following_char) { + suffixes.push(following_char).unwrap(); + } + } + } + + suffixes.sort_unstable(); + suffixes.into_iter() + } + + /// Only leaves words that have a specified prefix. Throw away others. + pub fn filter_prefix(&self, prefix: &str) -> Self { + let mut start = 0usize; + let mut end = self.0.len(); + for (i, word) in self.0.iter().enumerate() { + // SAFETY: We assume our slice is an array of 0-terminated strings. + match unsafe { prefix_cmp(prefix, *word) } { + Ordering::Less => { + start = i + 1; + } + Ordering::Greater => { + end = i; + break; + } + _ => {} + } + } + Self(&self.0[start..end]) + } + + /// Get a word at the certain position. + pub fn get(&self, index: usize) -> Option<&'static str> { + // SAFETY: we assume every word in the wordlist is a valid 0-terminated UTF-8 + // string. + self.0 + .get(index) + .map(|word| unsafe { from_utf8_unchecked(*word) }) + } + + /// How many words are currently in the list. + pub const fn len(&self) -> usize { + self.0.len() + } + + /// Iterator of all current words. + pub fn iter(&self) -> impl Iterator { + self.0 + .iter() + .map(|word| unsafe { from_utf8_unchecked(*word) }) + } +} + +/// Compare word from wordlist to a prefix. +/// +/// The comparison returns Less if the word comes lexicographically before all +/// possible words starting with `prefix`, and Greater if it comes after. +/// Equal is returned if the word starts with `prefix`. +unsafe fn prefix_cmp(prefix: &str, word: *const cty::c_char) -> Ordering { + // SAFETY: we assume `word` is a pointer to a 0-terminated string. + for (i, prefix_char) in prefix.as_bytes().iter().enumerate() { + let word_char = unsafe { *(word.add(i)) } as u8; + if word_char == 0 { + // Prefix is longer than word. + return Ordering::Less; + } else if *prefix_char != word_char { + return word_char.cmp(prefix_char); + } + } + Ordering::Equal +} + +unsafe fn from_utf8_unchecked<'a>(word: *const cty::c_char) -> &'a str { + // SAFETY: caller must pass a valid 0-terminated UTF-8 string. + // This assumption holds for usage on words of the BIP-39 wordlist. + unsafe { + let word = CStr::from_ptr(word); + core::str::from_utf8_unchecked(word.to_bytes()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const BIP39_WORD_COUNT: usize = ffi::BIP39_WORD_COUNT as usize; + const SLIP39_WORD_COUNT: usize = ffi::SLIP39_WORD_COUNT as usize; + + #[test] + fn test_filter_prefix_empty() { + let words = Wordlist::bip39().filter_prefix(""); + assert_eq!(words.len(), BIP39_WORD_COUNT); + let iter = words.iter(); + assert_eq!(iter.size_hint(), (BIP39_WORD_COUNT, Some(BIP39_WORD_COUNT))); + + let words = Wordlist::slip39().filter_prefix(""); + assert_eq!(words.len(), SLIP39_WORD_COUNT); + let iter = words.iter(); + assert_eq!( + iter.size_hint(), + (SLIP39_WORD_COUNT, Some(SLIP39_WORD_COUNT)) + ); + } + + #[test] + fn test_filter_prefix() { + let expected_result = vec!["strategy", "street", "strike", "strong", "struggle"]; + let result = Wordlist::bip39() + .filter_prefix("str") + .iter() + .collect::>(); + assert_eq!(result, expected_result); + } + + #[test] + fn test_filter_prefix_refine() { + let expected_result = vec!["strategy", "street", "strike", "strong", "struggle"]; + let words = Wordlist::bip39().filter_prefix("st"); + let result_a = words.filter_prefix("str").iter().collect::>(); + let result_b = Wordlist::bip39() + .filter_prefix("str") + .iter() + .collect::>(); + assert_eq!(result_a, expected_result); + assert_eq!(result_b, expected_result); + + let empty = words.filter_prefix("c"); + assert_eq!(empty.len(), 0); + } + + #[test] + fn test_wordlist_get() { + let words = Wordlist::bip39(); + assert_eq!(words.get(0), Some("abandon")); + assert_eq!(words.get(BIP39_WORD_COUNT - 1), Some("zoo")); + assert_eq!(words.get(BIP39_WORD_COUNT), None); + assert_eq!(words.get(BIP39_WORD_COUNT + 1), None); + + let filtered = words.filter_prefix("str"); + assert_eq!(filtered.get(0), Some("strategy")); + assert_eq!(filtered.get(filtered.len()), None); + } + + #[test] + fn test_filter_prefix_just_one() { + let expected_result = vec!["stick"]; + let result = Wordlist::bip39() + .filter_prefix("stick") + .iter() + .collect::>(); + assert_eq!(result, expected_result); + } + + #[test] + fn test_get_available_letters() { + let expected_result = vec!['a', 'i', 'l', 'o', 's', 'u']; + let result = Wordlist::bip39() + .get_available_letters("ab") + .collect::>(); + assert_eq!(result, expected_result); + + let expected_result = vec!['a', 'e', 'i', 'o', 'u']; + let result = Wordlist::bip39() + .get_available_letters("str") + .collect::>(); + assert_eq!(result, expected_result); + + let result = Wordlist::bip39() + .get_available_letters("zoo") + .collect::>(); + assert_eq!(result.len(), 0); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/mod.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/mod.rs index dba24e2570..4252b61eb9 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/mod.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/mod.rs @@ -1,7 +1,7 @@ -pub mod bip39; pub mod choice; pub mod choice_item; pub mod number_input; pub mod passphrase; pub mod pin; pub mod simple_choice; +pub mod wordlist; diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/bip39.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/wordlist.rs similarity index 79% rename from core/embed/rust/src/ui/model_tr/component/input_methods/bip39.rs rename to core/embed/rust/src/ui/model_tr/component/input_methods/wordlist.rs index 2bc8779003..513e3c2243 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/bip39.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/wordlist.rs @@ -1,5 +1,5 @@ use crate::{ - trezorhal::bip39, + trezorhal::wordlist::Wordlist, ui::{ component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx}, display::Icon, @@ -14,26 +14,32 @@ use super::super::{ }; use heapless::{String, Vec}; -pub enum Bip39EntryMsg { +pub enum WordlistEntryMsg { ResultWord(String<15>), } const MAX_WORD_LENGTH: usize = 10; -const MAX_CHOICE_LENGTH: usize = 26; +const MAX_LETTERS_LENGTH: usize = 26; /// Offer words when there will be fewer of them than this const OFFER_WORDS_THRESHOLD: usize = 10; const PROMPT: &str = "_"; +/// Type of the wordlist, deciding the list of words to be used +pub enum WordlistType { + Bip39, + Slip39, +} + /// We are offering either letters or words. -enum ChoiceFactoryBIP39 { - Letters(Vec), +enum ChoiceFactoryWordlist { + Letters(Vec), Words(Vec<&'static str, OFFER_WORDS_THRESHOLD>), } -impl ChoiceFactoryBIP39 { - fn letters(letter_choices: Vec) -> Self { +impl ChoiceFactoryWordlist { + fn letters(letter_choices: Vec) -> Self { Self::Letters(letter_choices) } @@ -42,7 +48,7 @@ impl ChoiceFactoryBIP39 { } } -impl ChoiceFactory for ChoiceFactoryBIP39 { +impl ChoiceFactory for ChoiceFactoryWordlist { type Item = ChoiceItem; fn count(&self) -> u8 { @@ -88,21 +94,23 @@ impl ChoiceFactory for ChoiceFactoryBIP39 { } } -/// Component for entering a BIP39 mnemonic. -pub struct Bip39Entry { - choice_page: ChoicePage, +/// Component for entering a mnemonic from a wordlist - BIP39 or SLIP39. +pub struct WordlistEntry { + choice_page: ChoicePage, chosen_letters: Child>>, - letter_choices: Vec, + letter_choices: Vec, textbox: TextBox, offer_words: bool, - words_list: bip39::Wordlist, + words_list: Wordlist, + wordlist_type: WordlistType, } -impl Bip39Entry { - pub fn new() -> Self { - let letter_choices: Vec = - bip39::get_available_letters("").collect(); - let choices = ChoiceFactoryBIP39::letters(letter_choices.clone()); +impl WordlistEntry { + pub fn new(wordlist_type: WordlistType) -> Self { + let words_list = Self::get_fresh_wordlist(&wordlist_type); + let letter_choices: Vec = + words_list.get_available_letters("").collect(); + let choices = ChoiceFactoryWordlist::letters(letter_choices.clone()); Self { choice_page: ChoicePage::new(choices) @@ -112,12 +120,21 @@ impl Bip39Entry { letter_choices, textbox: TextBox::empty(), offer_words: false, - words_list: bip39::Wordlist::all(), + words_list, + wordlist_type, + } + } + + /// Get appropriate wordlist with all possible words + fn get_fresh_wordlist(wordlist_type: &WordlistType) -> Wordlist { + match wordlist_type { + WordlistType::Bip39 => Wordlist::bip39(), + WordlistType::Slip39 => Wordlist::slip39(), } } /// Gets up-to-date choices for letters or words. - fn get_current_choices(&mut self) -> ChoiceFactoryBIP39 { + fn get_current_choices(&mut self) -> ChoiceFactoryWordlist { // Narrowing the word list self.words_list = self.words_list.filter_prefix(self.textbox.content()); @@ -126,11 +143,14 @@ impl Bip39Entry { if self.words_list.len() < OFFER_WORDS_THRESHOLD { self.offer_words = true; let word_choices = self.words_list.iter().collect(); - ChoiceFactoryBIP39::words(word_choices) + ChoiceFactoryWordlist::words(word_choices) } else { self.offer_words = false; - self.letter_choices = bip39::get_available_letters(self.textbox.content()).collect(); - ChoiceFactoryBIP39::letters(self.letter_choices.clone()) + self.letter_choices = self + .words_list + .get_available_letters(self.textbox.content()) + .collect(); + ChoiceFactoryWordlist::letters(self.letter_choices.clone()) } } @@ -161,7 +181,7 @@ impl Bip39Entry { } fn reset_wordlist(&mut self) { - self.words_list = bip39::Wordlist::all(); + self.words_list = Self::get_fresh_wordlist(&self.wordlist_type); } /// Get the index of DELETE item, which is always at the end. @@ -174,8 +194,8 @@ impl Bip39Entry { } } -impl Component for Bip39Entry { - type Msg = Bip39EntryMsg; +impl Component for WordlistEntry { + type Msg = WordlistEntryMsg; fn place(&mut self, bounds: Rect) -> Rect { let letters_area_height = self.chosen_letters.inner().needed_height(); @@ -201,7 +221,7 @@ impl Component for Bip39Entry { .words_list .get(page_counter as usize) .unwrap_or_default(); - return Some(Bip39EntryMsg::ResultWord(String::from(word))); + return Some(WordlistEntryMsg::ResultWord(String::from(word))); } else { let new_letter = self.letter_choices[page_counter as usize]; self.append_letter(ctx, new_letter); @@ -226,7 +246,7 @@ use super::super::{ButtonAction, ButtonPos}; use crate::ui::util; #[cfg(feature = "ui_debug")] -impl crate::trace::Trace for Bip39Entry { +impl crate::trace::Trace for WordlistEntry { fn get_btn_action(&self, pos: ButtonPos) -> String<25> { match pos { ButtonPos::Left => ButtonAction::PrevPage.string(), diff --git a/core/embed/rust/src/ui/model_tr/component/mod.rs b/core/embed/rust/src/ui/model_tr/component/mod.rs index 991974e8ff..819f755259 100644 --- a/core/embed/rust/src/ui/model_tr/component/mod.rs +++ b/core/embed/rust/src/ui/model_tr/component/mod.rs @@ -33,13 +33,13 @@ pub use flow_pages::{FlowPages, Page}; pub use frame::Frame; pub use homescreen::{Homescreen, HomescreenMsg, Lockscreen}; pub use input_methods::{ - bip39::{Bip39Entry, Bip39EntryMsg}, choice::{Choice, ChoiceFactory, ChoicePage, ChoicePageMsg}, choice_item::ChoiceItem, number_input::{NumberInput, NumberInputMsg}, passphrase::{PassphraseEntry, PassphraseEntryMsg}, pin::{PinEntry, PinEntryMsg}, simple_choice::{SimpleChoice, SimpleChoiceMsg}, + wordlist::{WordlistEntry, WordlistEntryMsg, WordlistType}, }; pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet}; pub use no_btn_dialog::{NoBtnDialog, NoBtnDialogMsg}; diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index 3c78c41afd..e2e6735827 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -37,10 +37,10 @@ use crate::{ use super::{ component::{ - Bip39Entry, Bip39EntryMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow, - FlowMsg, FlowPages, Frame, Homescreen, HomescreenMsg, Lockscreen, NoBtnDialog, - NoBtnDialogMsg, NumberInput, NumberInputMsg, Page, PassphraseEntry, PassphraseEntryMsg, - PinEntry, PinEntryMsg, Progress, ShareWords, SimpleChoice, SimpleChoiceMsg, + ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow, FlowMsg, FlowPages, Frame, + Homescreen, HomescreenMsg, Lockscreen, NoBtnDialog, NoBtnDialogMsg, NumberInput, + NumberInputMsg, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry, PinEntryMsg, Progress, + ShareWords, SimpleChoice, SimpleChoiceMsg, WordlistEntry, WordlistEntryMsg, WordlistType, }, constant, theme, }; @@ -127,10 +127,10 @@ impl ComponentMsgObj for SimpleChoice { } } -impl ComponentMsgObj for Bip39Entry { +impl ComponentMsgObj for WordlistEntry { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { match msg { - Bip39EntryMsg::ResultWord(word) => word.as_str().try_into(), + WordlistEntryMsg::ResultWord(word) => word.as_str().try_into(), } } } @@ -766,7 +766,21 @@ extern "C" fn new_request_bip39(n_args: usize, args: *const Obj, kwargs: *mut Ma let block = |_args: &[Obj], kwargs: &Map| { let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; - let obj = LayoutObj::new(Frame::new(prompt, Bip39Entry::new()).with_title_center(true))?; + let obj = LayoutObj::new( + Frame::new(prompt, WordlistEntry::new(WordlistType::Bip39)).with_title_center(true), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_request_slip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = |_args: &[Obj], kwargs: &Map| { + let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; + + let obj = LayoutObj::new( + Frame::new(prompt, WordlistEntry::new(WordlistType::Slip39)).with_title_center(true), + )?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -1024,6 +1038,13 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Get recovery word for BIP39.""" Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(), + /// def request_slip39( + /// *, + /// prompt: str, + /// ) -> str: + /// """SLIP39 word input keyboard.""" + Qstr::MP_QSTR_request_slip39 => obj_fn_kw!(0, new_request_slip39).as_obj(), + /// def request_passphrase( /// *, /// prompt: str, diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 097182db4d..f5fb7bbd8f 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -166,6 +166,14 @@ def request_bip39( """Get recovery word for BIP39.""" +# rust/src/ui/model_tr/layout.rs +def request_slip39( + *, + prompt: str, +) -> str: + """SLIP39 word input keyboard.""" + + # rust/src/ui/model_tr/layout.rs def request_passphrase( *, diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index ca073095b0..ad4420ecaa 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -759,7 +759,7 @@ def show_warning( ctx, br_type, "", - subheader or "Warning", + subheader or "WARNING", content, button_confirm=button, button_cancel=None, diff --git a/core/src/trezor/ui/layouts/tr/recovery.py b/core/src/trezor/ui/layouts/tr/recovery.py index 0757f98928..68154c7a9a 100644 --- a/core/src/trezor/ui/layouts/tr/recovery.py +++ b/core/src/trezor/ui/layouts/tr/recovery.py @@ -5,7 +5,7 @@ from trezor.enums import ButtonRequestType import trezorui2 from ..common import button_request, interact -from . import RustLayout, get_bool +from . import RustLayout, confirm_action, get_bool if TYPE_CHECKING: from trezor import wire @@ -20,7 +20,7 @@ async def request_word_count(ctx: wire.GenericContext, dry_run: bool) -> int: "word_count", ButtonRequestType.MnemonicWordCount, ) - # It can be returning a string + # It can be returning a string (for example for __debug__ in tests) return int(count) @@ -30,15 +30,11 @@ async def request_word( prompt = f"WORD {word_index + 1} OF {word_count}" if is_slip39: - raise NotImplementedError + word_choice = RustLayout(trezorui2.request_slip39(prompt=prompt)) else: - word = await interact( - ctx, - RustLayout(trezorui2.request_bip39(prompt=prompt)), - "request_word", - ButtonRequestType.MnemonicInput, - ) + word_choice = RustLayout(trezorui2.request_bip39(prompt=prompt)) + word: str = await ctx.wait(word_choice) return word @@ -54,7 +50,14 @@ async def show_remaining_shares( async def show_group_share_success( ctx: wire.GenericContext, share_index: int, group_index: int ) -> None: - raise NotImplementedError + await confirm_action( + ctx, + "share_success", + "Success", + description=f"You have entered\nShare {share_index + 1} from\nGroup {group_index + 1}", + verb="CONTINUE", + verb_cancel=None, + ) async def continue_recovery( @@ -65,19 +68,25 @@ async def continue_recovery( info_func: Callable | None, dry_run: bool, ) -> bool: - # NOTE: no need to implement `info_func`, as it is used only in - # Shamir backup, which is not implemented for TR. + # TODO: implement info_func? + # There is very limited space on the screen + # (and having middle button would mean shortening the right button text) description = text if subtext: description += f"\n\n{subtext}" + + if dry_run: + title = "SEED CHECK" + else: + title = "RECOVERY MODE" + return await get_bool( ctx, "recovery", - "START RECOVERY", + title, None, description, - verb="HOLD TO BEGIN", - hold=True, + verb=button_label.upper(), br_code=ButtonRequestType.RecoveryHomepage, )