From 473876c33f6119b98c7cdafffde74c470c5c52ec Mon Sep 17 00:00:00 2001 From: grdddj Date: Thu, 5 Jan 2023 10:36:16 +0100 Subject: [PATCH] WIP - Shamir wallet creation --- core/assets/model_r/right_arrow_fat.png | Bin 0 -> 165 bytes core/assets/model_r/tick_fat.png | Bin 0 -> 164 bytes core/embed/rust/librust_qstr.h | 1 - .../rust/src/ui/component/text/paragraphs.rs | 45 +++- .../model_tr/component/input_methods/mod.rs | 1 + .../component/input_methods/number_input.rs | 121 ++++++++++ .../component/input_methods/simple_choice.rs | 28 ++- .../rust/src/ui/model_tr/component/mod.rs | 1 + .../src/ui/model_tr/component/share_words.rs | 72 ++++-- core/embed/rust/src/ui/model_tr/layout.rs | 135 +++++++++-- .../src/ui/model_tr/res/arrow_right_fat.toif | Bin 0 -> 28 bytes .../rust/src/ui/model_tr/res/tick_fat.toif | Bin 0 -> 28 bytes core/embed/rust/src/ui/model_tr/theme.rs | 12 + core/embed/rust/src/ui/model_tt/layout.rs | 7 +- core/embed/rust/src/ui/model_tt/theme.rs | 7 +- core/mocks/generated/trezorui2.pyi | 18 +- core/src/trezor/strings.py | 10 +- core/src/trezor/ui/layouts/tr/__init__.py | 5 +- core/src/trezor/ui/layouts/tr/recovery.py | 2 +- core/src/trezor/ui/layouts/tr/reset.py | 225 +++++++++++++++--- 20 files changed, 591 insertions(+), 99 deletions(-) create mode 100644 core/assets/model_r/right_arrow_fat.png create mode 100644 core/assets/model_r/tick_fat.png create mode 100644 core/embed/rust/src/ui/model_tr/component/input_methods/number_input.rs create mode 100644 core/embed/rust/src/ui/model_tr/res/arrow_right_fat.toif create mode 100644 core/embed/rust/src/ui/model_tr/res/tick_fat.toif diff --git a/core/assets/model_r/right_arrow_fat.png b/core/assets/model_r/right_arrow_fat.png new file mode 100644 index 0000000000000000000000000000000000000000..8e8a37265d35fdfc5abcdf13432c7a2fa0c356bf GIT binary patch literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^EI`b`0VEiHM=;(5Qk(@Ik;M!QddeWoSh3W;3@FH6 z;_2(k{+yA8TUFh}daD3XNV3E=qQp5rH#aq}gu%HeHL)Z$MWH;iBtya7(>EZzkxv|` zNZ!-MF@)o~?(vPh3j`l`}qq)dZ-N!PC{xWt~$( F698q{DG>kw literal 0 HcmV?d00001 diff --git a/core/assets/model_r/tick_fat.png b/core/assets/model_r/tick_fat.png new file mode 100644 index 0000000000000000000000000000000000000000..03e386c419143d3ee2bf63f7bf3f1ee65b3c3c0b GIT binary patch literal 164 zcmeAS@N?(olHy`uVBq!ia0vp^96-zlA{cJxHK+qA&H|6fVg?31We{epSZZGe6l5>) z^mS!_&d9>8!qV^l6)49bS>hT|;+&tGo0?a`;9QiNSdyBeP@Y+mq2TW68xY>eCk|93 z=jq}Y!f`!0B>@C}oIh}cf%*S`X9FjeL+(P14Cfr!88bA!n1Cu7JYD@<);T3K0RVKk BDQf@# literal 0 HcmV?d00001 diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index ff3a4598bd..302cf75b12 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -67,7 +67,6 @@ static void _librust_qstrs(void) { MP_QSTR_progress_event; MP_QSTR_usb_event; - MP_QSTR_request_word_bip39; MP_QSTR_tutorial; MP_QSTR_attach_timer_fn; diff --git a/core/embed/rust/src/ui/component/text/paragraphs.rs b/core/embed/rust/src/ui/component/text/paragraphs.rs index 7fe04a11c3..d36bf0e8ae 100644 --- a/core/embed/rust/src/ui/component/text/paragraphs.rs +++ b/core/embed/rust/src/ui/component/text/paragraphs.rs @@ -509,13 +509,15 @@ pub struct Checklist { current: usize, icon_current: &'static [u8], icon_done: &'static [u8], + /// How wide will the left icon column be + check_width: i16, + /// Offset of the icon representing DONE + done_offset: Offset, + /// Offset of the icon representing CURRENT + current_offset: Offset, } impl Checklist { - const CHECK_WIDTH: i16 = 16; - const DONE_OFFSET: Offset = Offset::new(-2, 6); - const CURRENT_OFFSET: Offset = Offset::new(2, 3); - pub fn from_paragraphs( icon_current: &'static [u8], icon_done: &'static [u8], @@ -528,9 +530,27 @@ impl Checklist { current, icon_current, icon_done, + check_width: 0, + done_offset: Offset::zero(), + current_offset: Offset::zero(), } } + pub fn with_check_width(mut self, check_width: i16) -> Self { + self.check_width = check_width; + self + } + + pub fn with_done_offset(mut self, done_offset: Offset) -> Self { + self.done_offset = done_offset; + self + } + + pub fn with_current_offset(mut self, current_offset: Offset) -> Self { + self.current_offset = current_offset; + self + } + fn paint_icon(&self, layout: &TextLayout, icon: &'static [u8], offset: Offset) { let top_left = Point::new(self.area.x0, layout.bounds.y0); display::icon_top_left( @@ -550,7 +570,7 @@ where fn place(&mut self, bounds: Rect) -> Rect { self.area = bounds; - let para_area = bounds.inset(Insets::left(Self::CHECK_WIDTH)); + let para_area = bounds.inset(Insets::left(self.check_width)); self.paragraphs.place(para_area); self.area } @@ -564,10 +584,10 @@ where let current_visible = self.current.saturating_sub(self.paragraphs.offset.par); for layout in self.paragraphs.visible.iter().take(current_visible) { - self.paint_icon(layout, self.icon_done, Self::DONE_OFFSET); + self.paint_icon(layout, self.icon_done, self.done_offset); } if let Some(layout) = self.paragraphs.visible.iter().nth(current_visible) { - self.paint_icon(layout, self.icon_current, Self::CURRENT_OFFSET); + self.paint_icon(layout, self.icon_current, self.current_offset); } } @@ -577,6 +597,17 @@ where } } +impl Paginate for Checklist +where + T: ParagraphSource, +{ + fn page_count(&mut self) -> usize { + 1 + } + + fn change_page(&mut self, _to_page: usize) {} +} + #[cfg(feature = "ui_debug")] impl crate::trace::Trace for Checklist { fn trace(&self, t: &mut dyn crate::trace::Tracer) { 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 e3750ee72d..dba24e2570 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,6 +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; diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/number_input.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/number_input.rs new file mode 100644 index 0000000000..cfb6757e00 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/number_input.rs @@ -0,0 +1,121 @@ +use crate::ui::{ + component::{Component, Event, EventCtx}, + geometry::Rect, +}; + +use super::super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg}; +use heapless::String; + +pub enum NumberInputMsg { + Number(u32), +} + +struct ChoiceFactoryNumberInput { + min: u32, + max: u32, +} + +impl ChoiceFactoryNumberInput { + fn new(min: u32, max: u32) -> Self { + Self { min, max } + } +} + +impl ChoiceFactory for ChoiceFactoryNumberInput { + type Item = ChoiceItem; + + fn count(&self) -> u8 { + (self.max - self.min + 1) as u8 + } + + fn get(&self, choice_index: u8) -> ChoiceItem { + let num = self.min + choice_index as u32; + let text: String<10> = String::from(num); + let mut choice_item = ChoiceItem::new(text, ButtonLayout::default_three_icons()); + + // Disabling prev/next buttons for the first/last choice. + // (could be done to the same button if there is only one) + if choice_index == 0 { + choice_item.set_left_btn(None); + } + if choice_index == self.count() - 1 { + choice_item.set_right_btn(None); + } + + choice_item + } +} + +/// Simple wrapper around `ChoicePage` that allows for +/// inputting a list of values and receiving the chosen one. +pub struct NumberInput { + choice_page: ChoicePage, + min: u32, +} + +impl NumberInput { + pub fn new(min: u32, max: u32, init_value: u32) -> Self { + let choices = ChoiceFactoryNumberInput::new(min, max); + let initial_page = init_value - min; + Self { + min, + choice_page: ChoicePage::new(choices).with_initial_page_counter(initial_page as u8), + } + } +} + +impl Component for NumberInput { + type Msg = NumberInputMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + self.choice_page.place(bounds) + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + let msg = self.choice_page.event(ctx, event); + match msg { + Some(ChoicePageMsg::Choice(page_counter)) => { + let result_num = self.min + page_counter as u32; + Some(NumberInputMsg::Number(result_num)) + } + _ => None, + } + } + + fn paint(&mut self) { + self.choice_page.paint(); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +use super::super::{ButtonAction, ButtonPos}; + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for NumberInput { + fn get_btn_action(&self, pos: ButtonPos) -> String<25> { + match pos { + ButtonPos::Left => match self.choice_page.has_previous_choice() { + true => ButtonAction::PrevPage.string(), + false => ButtonAction::empty(), + }, + ButtonPos::Right => match self.choice_page.has_next_choice() { + true => ButtonAction::NextPage.string(), + false => ButtonAction::empty(), + }, + ButtonPos::Middle => { + let current_index = self.choice_page.page_index() as usize; + let current_num = self.min + current_index as u32; + ButtonAction::select_item(inttostr!(current_num)) + } + } + } + + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("NumberInput"); + self.report_btn_actions(t); + t.field("choice_page", &self.choice_page); + t.close(); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/simple_choice.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/simple_choice.rs index 7a0a523169..fdcb9cff13 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/simple_choice.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/simple_choice.rs @@ -11,6 +11,7 @@ use heapless::{String, Vec}; pub enum SimpleChoiceMsg { Result(String<50>), + Index(u8), } struct ChoiceFactorySimple { @@ -55,18 +56,29 @@ impl ChoiceFactory for ChoiceFactorySimple { pub struct SimpleChoice { choices: Vec, choice_page: ChoicePage>, + return_index: bool, } impl SimpleChoice { - pub fn new(str_choices: Vec, carousel: bool, show_incomplete: bool) -> Self { + pub fn new(str_choices: Vec, carousel: bool) -> Self { let choices = ChoiceFactorySimple::new(str_choices.clone(), carousel); Self { choices: str_choices, - choice_page: ChoicePage::new(choices) - .with_carousel(carousel) - .with_incomplete(show_incomplete), + choice_page: ChoicePage::new(choices).with_carousel(carousel), + return_index: false, } } + + /// Show only the currently selected item, nothing left/right. + pub fn with_only_one_item(mut self) -> Self { + self.choice_page = self.choice_page.with_only_one_item(true); + self + } + + pub fn with_return_index(mut self) -> Self { + self.return_index = true; + self + } } impl Component for SimpleChoice { @@ -80,8 +92,12 @@ impl Component for SimpleChoice { let msg = self.choice_page.event(ctx, event); match msg { Some(ChoicePageMsg::Choice(page_counter)) => { - let result = String::from(self.choices[page_counter as usize].as_ref()); - Some(SimpleChoiceMsg::Result(result)) + if self.return_index { + Some(SimpleChoiceMsg::Index(page_counter)) + } else { + let result = String::from(self.choices[page_counter as usize].as_ref()); + Some(SimpleChoiceMsg::Result(result)) + } } _ => None, } 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 029ea05c68..991974e8ff 100644 --- a/core/embed/rust/src/ui/model_tr/component/mod.rs +++ b/core/embed/rust/src/ui/model_tr/component/mod.rs @@ -36,6 +36,7 @@ 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}, diff --git a/core/embed/rust/src/ui/model_tr/component/share_words.rs b/core/embed/rust/src/ui/model_tr/component/share_words.rs index 73b7087c78..d75994bfd7 100644 --- a/core/embed/rust/src/ui/model_tr/component/share_words.rs +++ b/core/embed/rust/src/ui/model_tr/component/share_words.rs @@ -22,27 +22,33 @@ const WORD_FONT: Font = Font::NORMAL; /// Showing the given share words. pub struct ShareWords { area: Rect, + title: StrBuffer, share_words: Vec, page_index: usize, } impl ShareWords { - pub fn new(share_words: Vec) -> Self { + pub fn new(title: StrBuffer, share_words: Vec) -> Self { Self { area: Rect::zero(), + title, share_words, page_index: 0, } } fn word_index(&self) -> usize { - (self.page_index - 1) * WORDS_PER_PAGE + (self.page_index - 2) * WORDS_PER_PAGE } fn is_entry_page(&self) -> bool { self.page_index == 0 } + fn is_second_page(&self) -> bool { + self.page_index == 1 + } + fn is_final_page(&self) -> bool { self.page_index == self.total_page_count() - 1 } @@ -53,31 +59,44 @@ impl ShareWords { } else { self.share_words.len() / WORDS_PER_PAGE + 1 }; - // One page before the words, one after it - 1 + word_screens + 1 + // Two pages before the words, one after it + 2 + word_screens + 1 } /// Display the first page with user information. fn render_entry_page(&self) { - // TODO: will it be always 12, or do we need to check the length? - // It would need creating a String out of it, which is not ideal. - let free_area = text_multiline( - self.area, - "Write all 12\nwords in order on\nrecovery seed card", + display( + self.area + .top_left() + .ofs(Offset::y(Font::BOLD.line_height())), + &self.title, + Font::BOLD, + ); + + text_multiline( + self.area.split_top(15).1, + &build_string!( + 50, + "Write all ", + inttostr!(self.share_words.len() as u8), + "\nwords in order on\nrecovery seed card" + ), Font::BOLD, theme::FG, theme::BG, ); - if let Some(free_area) = free_area { - // Creating a small vertical distance - text_multiline( - free_area.split_top(3).1, - "Do NOT make\ndigital copies!", - Font::MONO, - theme::FG, - theme::BG, - ); - } + } + + /// Display the second page with user information. + fn render_second_page(&self) { + // Creating a small vertical distance to make it centered + text_multiline( + self.area.split_top(15).1, + "Do NOT make\ndigital copies!", + Font::MONO, + theme::FG, + theme::BG, + ); } /// Display the final page with user confirmation. @@ -86,7 +105,12 @@ impl ShareWords { // and to look better. text_multiline( self.area.split_top(12).1, - "I wrote down all\n12 words in order.", + &build_string!( + 50, + "I wrote down all\n", + inttostr!(self.share_words.len() as u8), + " words in order." + ), Font::MONO, theme::FG, theme::BG, @@ -100,6 +124,9 @@ impl ShareWords { for i in 0..WORDS_PER_PAGE { y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT; let index = self.word_index() + i; + if index >= self.share_words.len() { + break; + } let word = self.share_words[index]; let baseline = self.area.top_left() + Offset::new(NUMBER_X_OFFSET, y_offset); display(baseline, &inttostr!(index as u8 + 1), NUMBER_FONT); @@ -123,6 +150,8 @@ impl Component for ShareWords { fn paint(&mut self) { if self.is_entry_page() { self.render_entry_page(); + } else if self.is_second_page() { + self.render_second_page(); } else if self.is_final_page() { self.render_final_page(); } else { @@ -156,6 +185,9 @@ impl crate::trace::Trace for ShareWords { } else { for i in 0..WORDS_PER_PAGE { let index = self.word_index() + i; + if index >= self.share_words.len() { + break; + } let word = self.share_words[index]; let content = build_string!(20, inttostr!(index as u8 + 1), " ", &word, "\n"); t.string(&content); diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index d1179683dc..3c78c41afd 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -1,4 +1,4 @@ -use core::convert::TryInto; +use core::{cmp::Ordering, convert::TryInto}; use heapless::Vec; @@ -20,7 +20,8 @@ use crate::{ base::Component, paginated::{PageMsg, Paginate}, text::paragraphs::{ - Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort, Paragraphs, VecExt, + Checklist, Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort, + Paragraphs, VecExt, }, ComponentExt, Empty, Timeout, TimeoutMsg, }, @@ -38,8 +39,8 @@ use super::{ component::{ Bip39Entry, Bip39EntryMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow, FlowMsg, FlowPages, Frame, Homescreen, HomescreenMsg, Lockscreen, NoBtnDialog, - NoBtnDialogMsg, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry, PinEntryMsg, Progress, - ShareWords, SimpleChoice, SimpleChoiceMsg, + NoBtnDialogMsg, NumberInput, NumberInputMsg, Page, PassphraseEntry, PassphraseEntryMsg, + PinEntry, PinEntryMsg, Progress, ShareWords, SimpleChoice, SimpleChoiceMsg, }, constant, theme, }; @@ -95,7 +96,7 @@ where match msg { FlowMsg::Confirmed => Ok(CONFIRMED.as_obj()), FlowMsg::Cancelled => Ok(CANCELLED.as_obj()), - FlowMsg::ConfirmedIndex(page) => Ok(page.into()), + FlowMsg::ConfirmedIndex(index) => Ok(index.into()), } } } @@ -109,10 +110,19 @@ impl ComponentMsgObj for PinEntry { } } +impl ComponentMsgObj for NumberInput { + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + NumberInputMsg::Number(choice) => choice.try_into(), + } + } +} + impl ComponentMsgObj for SimpleChoice { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { match msg { SimpleChoiceMsg::Result(choice) => choice.as_str().try_into(), + SimpleChoiceMsg::Index(index) => Ok(index.into()), } } } @@ -638,16 +648,77 @@ extern "C" fn new_request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } +extern "C" fn new_request_number(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let min_count: u32 = kwargs.get(Qstr::MP_QSTR_min_count)?.try_into()?; + let max_count: u32 = kwargs.get(Qstr::MP_QSTR_max_count)?.try_into()?; + let count: u32 = kwargs.get(Qstr::MP_QSTR_count)?.try_into()?; + + let obj = LayoutObj::new(Frame::new( + title, + NumberInput::new(min_count, max_count, count), + ))?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let _title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let button: StrBuffer = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?; + let active: usize = kwargs.get(Qstr::MP_QSTR_active)?.try_into()?; + let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?; + + let mut iter_buf = IterBuf::new(); + let mut paragraphs = ParagraphVecLong::new(); + let iter = Iter::try_from_obj_with_buf(items, &mut iter_buf)?; + for (i, item) in iter.enumerate() { + let style = match i.cmp(&active) { + Ordering::Less => &theme::TEXT_MONO, + Ordering::Equal => &theme::TEXT_BOLD, + Ordering::Greater => &theme::TEXT_MONO, + }; + let text: StrBuffer = item.try_into()?; + paragraphs.add(Paragraph::new(style, text)); + } + + let confirm_btn = Some(ButtonDetails::text(button)); + + let obj = LayoutObj::new( + ButtonPage::new( + Checklist::from_paragraphs( + theme::ICON_ARROW_RIGHT_FAT.0, + theme::ICON_TICK_FAT.0, + active, + paragraphs + .into_paragraphs() + .with_spacing(theme::CHECKLIST_SPACING), + ) + .with_check_width(theme::CHECKLIST_CHECK_WIDTH) + .with_current_offset(theme::CHECKLIST_CURRENT_OFFSET), + theme::BG, + ) + .with_confirm_btn(confirm_btn), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_share_words)?; - let share_words: Vec = iter_into_vec(share_words_obj)?; + let share_words: Vec = iter_into_vec(share_words_obj)?; let confirm_btn = Some(ButtonDetails::text("HOLD TO CONFIRM".into()).with_default_duration()); let obj = LayoutObj::new( - ButtonPage::new(ShareWords::new(share_words), theme::BG).with_confirm_btn(confirm_btn), + ButtonPage::new(ShareWords::new(title, share_words), theme::BG) + .with_confirm_btn(confirm_btn), )?; Ok(obj.into()) }; @@ -660,9 +731,15 @@ extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?; let words: Vec = iter_into_vec(words_iterable)?; - // TODO: should return int, to be consistent with TT's select_word + // Returning the index of the selected word, not the word itself let obj = LayoutObj::new( - Frame::new(title, SimpleChoice::new(words, true, true)).with_title_center(true), + Frame::new( + title, + SimpleChoice::new(words, false) + .with_only_one_item() + .with_return_index(), + ) + .with_title_center(true), )?; Ok(obj.into()) }; @@ -674,17 +751,18 @@ extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mu let _dry_run: bool = kwargs.get(Qstr::MP_QSTR_dry_run)?.try_into()?; let title = "NUMBER OF WORDS".into(); - let choices: Vec = ["12".into(), "18".into(), "24".into()] + let choices: Vec = ["12", "18", "20", "24", "33"] + .map(|num| num.into()) .into_iter() .collect(); - let obj = LayoutObj::new(Frame::new(title, SimpleChoice::new(choices, false, false)))?; + let obj = LayoutObj::new(Frame::new(title, SimpleChoice::new(choices, false)))?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn new_request_word_bip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { +extern "C" fn new_request_bip39(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()?; @@ -894,8 +972,30 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Request pin on device.""" Qstr::MP_QSTR_request_pin => obj_fn_kw!(0, new_request_pin).as_obj(), + /// def request_number( + /// *, + /// title: str, + /// count: int, + /// min_count: int, + /// max_count: int, + /// ) -> object: + /// """Number input with + and - buttons, description, and info button.""" + Qstr::MP_QSTR_request_number => obj_fn_kw!(0, new_request_number).as_obj(), + + /// def show_checklist( + /// *, + /// title: str, + /// items: Iterable[str], + /// active: int, + /// button: str, + /// ) -> object: + /// """Checklist of backup steps. Active index is highlighted, previous items have check + /// mark next to them.""" + Qstr::MP_QSTR_show_checklist => obj_fn_kw!(0, new_show_checklist).as_obj(), + /// def show_share_words( /// *, + /// title: str, /// share_words: Iterable[str], /// ) -> None: /// """Shows a backup seed.""" @@ -905,23 +1005,24 @@ pub static mp_module_trezorui2: Module = obj_module! { /// *, /// title: str, /// words: Iterable[str], - /// ) -> str: # TODO: should return int, to be consistent with TT's select_word - /// """Select a word from a list.""" + /// ) -> int: + /// """Select mnemonic word from three possibilities - seed check after backup. The + /// iterable must be of exact size. Returns index in range `0..3`.""" Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(), /// def select_word_count( /// *, /// dry_run: bool, /// ) -> str: # TODO: make it return int - /// """Get word count for recovery.""" + /// """Select mnemonic word count from (12, 18, 20, 24, 33).""" Qstr::MP_QSTR_select_word_count => obj_fn_kw!(0, new_select_word_count).as_obj(), - /// def request_word_bip39( + /// def request_bip39( /// *, /// prompt: str, /// ) -> str: /// """Get recovery word for BIP39.""" - Qstr::MP_QSTR_request_word_bip39 => obj_fn_kw!(0, new_request_word_bip39).as_obj(), + Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(), /// def request_passphrase( /// *, diff --git a/core/embed/rust/src/ui/model_tr/res/arrow_right_fat.toif b/core/embed/rust/src/ui/model_tr/res/arrow_right_fat.toif new file mode 100644 index 0000000000000000000000000000000000000000..05bc52bd9ed7a993dd003b7a8027801732a04c14 GIT binary patch literal 28 kcmWIX_jG4r;9wA7U|@Kh{^R_I`Ty-XKKxIAkj}sW0DxKw(*OVf literal 0 HcmV?d00001 diff --git a/core/embed/rust/src/ui/model_tr/res/tick_fat.toif b/core/embed/rust/src/ui/model_tr/res/tick_fat.toif new file mode 100644 index 0000000000000000000000000000000000000000..2eb9735da8dde3dbcea9bf8cc47f672c55cb0f11 GIT binary patch literal 28 jcmWIX_jKoAU}F$qU|>j2NceG{;mChU1%X4!2@DJXV)qBl literal 0 HcmV?d00001 diff --git a/core/embed/rust/src/ui/model_tr/theme.rs b/core/embed/rust/src/ui/model_tr/theme.rs index 80daaa1e5e..2b7d857165 100644 --- a/core/embed/rust/src/ui/model_tr/theme.rs +++ b/core/embed/rust/src/ui/model_tr/theme.rs @@ -1,6 +1,7 @@ use crate::ui::{ component::text::TextStyle, display::{Color, Font, IconAndName}, + geometry::Offset, }; // Color palette. @@ -33,6 +34,10 @@ pub const ICON_ARROW_LEFT: IconAndName = IconAndName::new(include_res!("model_tr/res/arrow_left.toif"), "arrow_left"); // 6*10 pub const ICON_ARROW_RIGHT: IconAndName = IconAndName::new(include_res!("model_tr/res/arrow_right.toif"), "arrow_right"); // 6*10 +pub const ICON_ARROW_RIGHT_FAT: IconAndName = IconAndName::new( + include_res!("model_tr/res/arrow_right_fat.toif"), + "arrow_right_fat", +); // 4*8 pub const ICON_ARROW_UP: IconAndName = IconAndName::new(include_res!("model_tr/res/arrow_up.toif"), "arrow_up"); // 10*6 pub const ICON_ARROW_DOWN: IconAndName = @@ -54,9 +59,16 @@ pub const ICON_PREV_PAGE: IconAndName = pub const ICON_SUCCESS: IconAndName = IconAndName::new(include_res!("model_tr/res/success.toif"), "success"); pub const ICON_TICK: IconAndName = IconAndName::new(include_res!("model_tr/res/tick.toif"), "tick"); // 10*10 +pub const ICON_TICK_FAT: IconAndName = + IconAndName::new(include_res!("model_tr/res/tick_fat.toif"), "tick_fat"); // 8*6 pub const ICON_WARNING: IconAndName = IconAndName::new(include_res!("model_tr/res/warning.toif"), "warning"); // 12*12 +// checklist settings +pub const CHECKLIST_SPACING: i16 = 5; +pub const CHECKLIST_CHECK_WIDTH: i16 = 12; +pub const CHECKLIST_CURRENT_OFFSET: Offset = Offset::x(3); + // Button height is constant for both text and icon buttons. // It is a combination of content and (optional) outline/border. // It is not possible to have icons 7*7, therefore having 8*8 diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index f6d3571f4d..ed1157a099 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -1094,7 +1094,10 @@ extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut M paragraphs .into_paragraphs() .with_spacing(theme::CHECKLIST_SPACING), - ), + ) + .with_check_width(theme::CHECKLIST_CHECK_WIDTH) + .with_current_offset(theme::CHECKLIST_CURRENT_OFFSET) + .with_done_offset(theme::CHECKLIST_DONE_OFFSET), theme::button_bar(Button::with_text(button).map(|msg| { (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed) })), @@ -1590,7 +1593,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// button: str, /// ) -> object: /// """Checklist of backup steps. Active index is highlighted, previous items have check - /// mark nex to them.""" + /// mark next to them.""" Qstr::MP_QSTR_show_checklist => obj_fn_kw!(0, new_show_checklist).as_obj(), /// def confirm_recovery( diff --git a/core/embed/rust/src/ui/model_tt/theme.rs b/core/embed/rust/src/ui/model_tt/theme.rs index 78a7e6226a..7334fc3a16 100644 --- a/core/embed/rust/src/ui/model_tt/theme.rs +++ b/core/embed/rust/src/ui/model_tt/theme.rs @@ -6,7 +6,7 @@ use crate::{ FixedHeightBar, }, display::{Color, Font}, - geometry::Insets, + geometry::{Insets, Offset}, }, }; @@ -413,6 +413,11 @@ pub const BUTTON_SPACING: i16 = 6; pub const CHECKLIST_SPACING: i16 = 10; pub const RECOVERY_SPACING: i16 = 18; +// checklist settings +pub const CHECKLIST_CHECK_WIDTH: i16 = 16; +pub const CHECKLIST_DONE_OFFSET: Offset = Offset::new(-2, 6); +pub const CHECKLIST_CURRENT_OFFSET: Offset = Offset::new(2, 3); + /// Standard button height in pixels. pub const fn button_rows(count: usize) -> i16 { let count = count as i16; diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 6f6319d724..6376b0b723 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -108,6 +108,18 @@ def request_pin( """Request pin on device.""" +# rust/src/ui/model_tr/layout.rs +def show_checklist( + *, + title: str, + items: Iterable[str], + active: int, + button: str, +) -> object: + """Checklist of backup steps. Active index is highlighted, previous items have check + mark next to them.""" + + # rust/src/ui/model_tr/layout.rs def show_share_words( *, @@ -130,11 +142,11 @@ def select_word_count( *, dry_run: bool, ) -> str: # TODO: make it return int - """Get word count for recovery.""" + """Select mnemonic word count from (12, 18, 20, 24, 33).""" # rust/src/ui/model_tr/layout.rs -def request_word_bip39( +def request_bip39( *, prompt: str, ) -> str: @@ -503,7 +515,7 @@ def show_checklist( button: str, ) -> object: """Checklist of backup steps. Active index is highlighted, previous items have check - mark nex to them.""" + mark next to them.""" # rust/src/ui/model_tt/layout.rs diff --git a/core/src/trezor/strings.py b/core/src/trezor/strings.py index de8637ff23..c26414c5ba 100644 --- a/core/src/trezor/strings.py +++ b/core/src/trezor/strings.py @@ -21,12 +21,10 @@ def format_amount(amount: int, decimals: int) -> str: return s -if False: - - def format_ordinal(number: int) -> str: - return str(number) + {1: "st", 2: "nd", 3: "rd"}.get( - 4 if 10 <= number % 100 < 20 else number % 10, "th" - ) +def format_ordinal(number: int) -> str: + return str(number) + {1: "st", 2: "nd", 3: "rd"}.get( + 4 if 10 <= number % 100 < 20 else number % 10, "th" + ) def format_plural(string: str, count: int, plural: str) -> str: diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index 2a5adb9e81..ca073095b0 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -751,7 +751,6 @@ def show_warning( ctx: GenericContext, br_type: str, content: str, - header: str = "Warning", subheader: str | None = None, button: str = "Try again", br_code: ButtonRequestType = ButtonRequestType.Warning, @@ -759,8 +758,8 @@ def show_warning( return _show_modal( ctx, br_type, - header, - subheader, + "", + 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 bdb9a46834..0757f98928 100644 --- a/core/src/trezor/ui/layouts/tr/recovery.py +++ b/core/src/trezor/ui/layouts/tr/recovery.py @@ -34,7 +34,7 @@ async def request_word( else: word = await interact( ctx, - RustLayout(trezorui2.request_word_bip39(prompt=prompt)), + RustLayout(trezorui2.request_bip39(prompt=prompt)), "request_word", ButtonRequestType.MnemonicInput, ) diff --git a/core/src/trezor/ui/layouts/tr/reset.py b/core/src/trezor/ui/layouts/tr/reset.py index 2c51d5e3b7..b8073a86ac 100644 --- a/core/src/trezor/ui/layouts/tr/reset.py +++ b/core/src/trezor/ui/layouts/tr/reset.py @@ -1,115 +1,276 @@ from typing import TYPE_CHECKING from trezor.enums import ButtonRequestType +from trezor.wire import ActionCancelled import trezorui2 from ..common import interact -from . import RustLayout, confirm_action, get_bool +from . import RustLayout, confirm_action + +CONFIRMED = trezorui2.CONFIRMED # global_import_cache if TYPE_CHECKING: - from trezor import wire + from trezor.wire import GenericContext from trezor.enums import BackupType from typing import Sequence async def show_share_words( - ctx: wire.GenericContext, + ctx: GenericContext, share_words: Sequence[str], share_index: int | None = None, group_index: int | None = None, ) -> None: + from . import get_bool + + if share_index is None: + title = "RECOVERY SEED" + elif group_index is None: + title = f"SHARE #{share_index + 1}" + else: + title = f"G{group_index + 1} - SHARE {share_index + 1}" + # Showing words, asking for write down confirmation and preparing for check # until user accepts everything. while True: await interact( ctx, RustLayout( - trezorui2.show_share_words( # type: ignore [Arguments missing for parameters "title", "pages"] - share_words=share_words # type: ignore [No parameter named "share_words"] + trezorui2.show_share_words( # type: ignore [Argument missing for parameter "pages"] + title=title, + share_words=share_words, # type: ignore [No parameter named "share_words"] ) ), "backup_words", ButtonRequestType.ResetDevice, ) - ready_to_check = await get_bool( + if share_index is None: + check_title = "CHECK SEED" + elif group_index is None: + check_title = f"CHECK SHARE #{share_index + 1}" + else: + check_title = f"CHECK G{group_index + 1} - SHARE {share_index + 1}" + + if await get_bool( ctx, "backup_words", - "CHECK BACKUP", + check_title, None, "Select correct words in correct positions.", verb_cancel="SEE AGAIN", verb="BEGIN", br_code=ButtonRequestType.ResetDevice, - ) - if not ready_to_check: - continue - - # All went well, we can break the loop. - break + ): + # All went well, we can break the loop. + break async def select_word( - ctx: wire.GenericContext, + ctx: GenericContext, words: Sequence[str], share_index: int | None, checked_index: int, count: int, group_index: int | None = None, ) -> str: + from trezor.strings import format_ordinal + # TODO: it might not always be 3 words, it can happen it will be only two, # but the probability is very small - 4 words containing two items two times - # (or one in "all all" seed) assert len(words) == 3 result = await ctx.wait( RustLayout( trezorui2.select_word( # type: ignore [Argument missing for parameter "description"] - title=f"SELECT WORD {checked_index + 1}/{count}", + title=f"SELECT {format_ordinal(checked_index + 1).upper()} WORD", words=(words[0].upper(), words[1].upper(), words[2].upper()), ) ) ) - for word in words: - if word.upper() == result: - return word - raise ValueError("Invalid word") + if __debug__ and isinstance(result, str): + return result + assert isinstance(result, int) and 0 <= result <= 2 + return words[result] async def slip39_show_checklist( - ctx: wire.GenericContext, step: int, backup_type: BackupType + ctx: GenericContext, step: int, backup_type: BackupType ) -> None: - raise NotImplementedError + from trezor.enums import BackupType + + assert backup_type in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced) + + items = ( + ( + "Number of shares", + "Set threshold", + "Write down and check all shares", + ) + if backup_type == BackupType.Slip39_Basic + else ( + "Number of groups", + "Number of shares", + "Set sizes and thresholds", + ) + ) + + result = await interact( + ctx, + RustLayout( + trezorui2.show_checklist( + title="BACKUP CHECKLIST", + button="CONTINUE", + active=step, + items=items, + ) + ), + "slip39_checklist", + ButtonRequestType.ResetDevice, + ) + if result != CONFIRMED: + raise ActionCancelled + + +async def _prompt_number( + ctx: GenericContext, + title: str, + count: int, + min_count: int, + max_count: int, + br_name: str, +) -> int: + num_input = RustLayout( + trezorui2.request_number( # type: ignore [Argument missing for parameter "description"] + title=title.upper(), + count=count, + min_count=min_count, + max_count=max_count, + ) + ) + + result = await interact( + ctx, + num_input, + br_name, + ButtonRequestType.ResetDevice, + ) + + assert isinstance(result, int) + return result async def slip39_prompt_threshold( - ctx: wire.GenericContext, num_of_shares: int, group_id: int | None = None + ctx: GenericContext, num_of_shares: int, group_id: int | None = None ) -> int: - raise NotImplementedError + await confirm_action( + ctx, + "slip39_prompt_threshold", + "Set threshold", + description="= number of shares needed for recovery", + verb="BEGIN", + verb_cancel=None, + ) + + count = num_of_shares // 2 + 1 + # min value of share threshold is 2 unless the number of shares is 1 + # number of shares 1 is possible in advanced slip39 + min_count = min(2, num_of_shares) + max_count = num_of_shares + + if group_id is not None: + title = f"THRESHOLD - GROUP {group_id + 1}" + else: + title = "SET THRESHOLD" + + return await _prompt_number( + ctx, + title, + count, + min_count, + max_count, + "slip39_threshold", + ) async def slip39_prompt_number_of_shares( - ctx: wire.GenericContext, group_id: int | None = None + ctx: GenericContext, group_id: int | None = None ) -> int: - raise NotImplementedError + await confirm_action( + ctx, + "slip39_shares", + "Number of shares", + description="= total number of unique word lists used for wallet backup.", + verb="BEGIN", + verb_cancel=None, + ) + + count = 5 + min_count = 1 + max_count = 16 + + if group_id is not None: + title = f"# SHARES - GROUP {group_id + 1}" + else: + title = "NUMBER OF SHARES" + + return await _prompt_number( + ctx, + title, + count, + min_count, + max_count, + "slip39_shares", + ) -async def slip39_advanced_prompt_number_of_groups(ctx: wire.GenericContext) -> int: - raise NotImplementedError +async def slip39_advanced_prompt_number_of_groups(ctx: GenericContext) -> int: + count = 5 + min_count = 2 + max_count = 16 + + return await _prompt_number( + ctx, + "NUMBER OF GROUPS", + count, + min_count, + max_count, + "slip39_groups", + ) async def slip39_advanced_prompt_group_threshold( - ctx: wire.GenericContext, num_of_groups: int + ctx: GenericContext, num_of_groups: int ) -> int: - raise NotImplementedError + count = num_of_groups // 2 + 1 + min_count = 1 + max_count = num_of_groups + + return await _prompt_number( + ctx, + "GROUP THRESHOLD", + count, + min_count, + max_count, + "slip39_group_threshold", + ) -async def show_warning_backup(ctx: wire.GenericContext, slip39: bool) -> None: +async def show_warning_backup(ctx: GenericContext, slip39: bool) -> None: + if slip39: + description = ( + "Never make a digital copy of your shares and never upload them online." + ) + else: + description = ( + "Never make a digital copy of your seed and never upload it online." + ) + await confirm_action( ctx, "backup_warning", "Caution", - description="Never make a digital copy and never upload it online.", + description=description, verb="I understand", verb_cancel=None, br_code=ButtonRequestType.ResetDevice,