From a801b00dc2898340cde054a739d1ac9897c31b1c Mon Sep 17 00:00:00 2001 From: obrusvit Date: Wed, 27 Mar 2024 11:09:03 +0100 Subject: [PATCH] feat(core): T3T1 ShareWords component --- .../src/ui/model_mercury/component/footer.rs | 9 +- .../src/ui/model_mercury/component/frame.rs | 3 +- .../src/ui/model_mercury/component/mod.rs | 5 + .../rust/src/ui/model_mercury/constant.rs | 5 + .../embed/rust/src/ui/model_mercury/layout.rs | 34 +++---- .../rust/src/ui/shape/corner_highlight.rs | 4 +- core/mocks/generated/trezorui2.pyi | 2 +- core/src/trezor/ui/layouts/mercury/reset.py | 38 ++------ tests/common.py | 96 +++++++++++-------- 9 files changed, 98 insertions(+), 98 deletions(-) diff --git a/core/embed/rust/src/ui/model_mercury/component/footer.rs b/core/embed/rust/src/ui/model_mercury/component/footer.rs index d99491c4d..18d557a59 100644 --- a/core/embed/rust/src/ui/model_mercury/component/footer.rs +++ b/core/embed/rust/src/ui/model_mercury/component/footer.rs @@ -2,7 +2,6 @@ use crate::{ strutil::TString, ui::{ component::{text::TextStyle, Component, Event, EventCtx, Never}, - constant::WIDTH, geometry::{Alignment, Offset, Rect}, model_mercury::theme, shape::{Renderer, Text}, @@ -10,9 +9,9 @@ use crate::{ }; /// Component showing a task instruction (e.g. "Swipe up") and optionally task -/// description (e.g. "Confirm transaction") to a user. The component -/// is typically placed at the bottom of the screen. The height of the provided -/// area must be 18px (only instruction) or 37px (both description and +/// description (e.g. "Confirm transaction") to a user. A host of this component +/// is responsible of providing the exact area considering also the spacing. The +/// height must be 18px (only instruction) or 37px (both description and /// instruction). The content and style of both description and instruction is /// configurable separatedly. pub struct Footer<'a> { @@ -26,7 +25,7 @@ pub struct Footer<'a> { impl<'a> Footer<'a> { /// height of the component with only instruction [px] pub const HEIGHT_SIMPLE: i16 = 18; - /// height for component with both description and instruction [px] + /// height of the component with both description and instruction [px] pub const HEIGHT_DEFAULT: i16 = 37; pub fn new>>(instruction: T) -> Self { diff --git a/core/embed/rust/src/ui/model_mercury/component/frame.rs b/core/embed/rust/src/ui/model_mercury/component/frame.rs index 7448fd0fd..98b63dad7 100644 --- a/core/embed/rust/src/ui/model_mercury/component/frame.rs +++ b/core/embed/rust/src/ui/model_mercury/component/frame.rs @@ -127,8 +127,9 @@ where fn place(&mut self, bounds: Rect) -> Rect { let (mut header_area, content_area) = bounds.split_top(TITLE_HEIGHT); - let content_area = content_area.inset(Insets::top(TITLE_SPACE)); + let content_area = content_area.inset(Insets::top(SPACING)); + header_area = header_area.inset(Insets::sides(SPACING)); if let Some(b) = &mut self.button { let (rest, button_area) = header_area.split_right(TITLE_HEIGHT); header_area = rest; diff --git a/core/embed/rust/src/ui/model_mercury/component/mod.rs b/core/embed/rust/src/ui/model_mercury/component/mod.rs index c9a95b3ba..a6da95bc1 100644 --- a/core/embed/rust/src/ui/model_mercury/component/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/mod.rs @@ -12,6 +12,10 @@ mod error; mod frame; mod loader; mod result; +mod scroll; +mod share_words; +mod simple_page; +mod swipe; mod welcome_screen; pub use button::{ @@ -33,6 +37,7 @@ pub use keyboard::{ pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet}; pub use result::{ResultFooter, ResultScreen, ResultStyle}; pub use scroll::ScrollBar; +pub use share_words::ShareWords; pub use simple_page::SimplePage; pub use swipe::{Swipe, SwipeDirection}; pub use vertical_menu::{VerticalMenu, VerticalMenuChoiceMsg}; diff --git a/core/embed/rust/src/ui/model_mercury/constant.rs b/core/embed/rust/src/ui/model_mercury/constant.rs index ba2c4a345..9f0d9e3d5 100644 --- a/core/embed/rust/src/ui/model_mercury/constant.rs +++ b/core/embed/rust/src/ui/model_mercury/constant.rs @@ -20,3 +20,8 @@ pub const fn screen() -> Rect { Rect::from_top_left_and_size(Point::zero(), SIZE) } pub const SCREEN: Rect = screen(); + +/// Spacing between components (e.g. header and main content) and offsets from +/// the side of the screen. Generally applied everywhere except the top side of +/// the header. [px] +pub const SPACING: i16 = 2; diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 63075a631..3f34816dc 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -1,4 +1,5 @@ use core::{cmp::Ordering, convert::TryInto}; +use heapless::Vec; use crate::{ error::Error, @@ -52,8 +53,8 @@ use super::{ FidoMsg, Frame, FrameMsg, Homescreen, HomescreenMsg, IconDialog, Lockscreen, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputDialog, NumberInputDialogMsg, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress, - SelectWordCount, SelectWordCountMsg, SelectWordMsg, SimplePage, Slip39Input, VerticalMenu, - VerticalMenuChoiceMsg, + SelectWordCount, SelectWordCountMsg, SelectWordMsg, ShareWords, SimplePage, Slip39Input, + VerticalMenu, VerticalMenuChoiceMsg, }, theme, }; @@ -206,6 +207,15 @@ where } } +impl ComponentMsgObj for ShareWords<'_> { + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + PageMsg::Confirmed => Ok(CONFIRMED.as_obj()), + _ => Err(Error::TypeError), + } + } +} + impl ComponentMsgObj for VerticalMenu where T: AsRef, @@ -1350,20 +1360,12 @@ extern "C" fn new_show_tx_context_menu(n_args: usize, args: *const Obj, kwargs: extern "C" fn new_show_share_words(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 pages: Obj = kwargs.get(Qstr::MP_QSTR_pages)?; + let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_pages)?; + let share_words_vec: Vec = util::iter_into_vec(share_words_obj)?; - let mut paragraphs = ParagraphVecLong::new(); - for page in IterBuf::new().try_iterate(pages)? { - let text: StrBuffer = page.try_into()?; - paragraphs.add(Paragraph::new(&theme::TEXT_MONO, text).break_after()); - } - - let obj = LayoutObj::new(Frame::left_aligned( - title, - ButtonPage::<_, StrBuffer>::new(paragraphs.into_paragraphs(), theme::BG) - .with_hold()? - .without_cancel(), - ))?; + let share_words = ShareWords::new(share_words_vec); + let frame_with_share_words = Frame::left_aligned(title, share_words).with_info_button(); + let obj = LayoutObj::new(frame_with_share_words)?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -2081,7 +2083,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// title: str, /// pages: Iterable[str], /// ) -> LayoutObj[UiResult]: - /// """Show mnemonic for backup. Expects the words pre-divided into individual pages.""" + /// """Show mnemonic for backup.""" Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, new_show_share_words).as_obj(), /// def request_number( diff --git a/core/embed/rust/src/ui/shape/corner_highlight.rs b/core/embed/rust/src/ui/shape/corner_highlight.rs index 619a5169b..f25f4ff87 100644 --- a/core/embed/rust/src/ui/shape/corner_highlight.rs +++ b/core/embed/rust/src/ui/shape/corner_highlight.rs @@ -96,7 +96,7 @@ impl Shape<'_> for CornerHighlight { ) } - fn draw(&mut self, canvas: &mut dyn Canvas, cache: &DrawingCache<'_>) { + fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache<'_>) { let align: Alignment2D = self.corner.into(); // base circle @@ -150,7 +150,7 @@ impl Shape<'_> for CornerHighlight { }); } - fn cleanup(&mut self, cache: &super::DrawingCache<'_>) {} + fn cleanup(&mut self, _cache: &super::DrawingCache<'_>) {} } impl<'s> ShapeClone<'s> for CornerHighlight { diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 696ff828e..66cbb534d 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -401,7 +401,7 @@ def show_share_words( title: str, pages: Iterable[str], ) -> LayoutObj[UiResult]: - """Show mnemonic for backup. Expects the words pre-divided into individual pages.""" + """Show mnemonic for backup.""" # rust/src/ui/model_mercury/layout.rs diff --git a/core/src/trezor/ui/layouts/mercury/reset.py b/core/src/trezor/ui/layouts/mercury/reset.py index 0dd51dc96..c0d9dd46e 100644 --- a/core/src/trezor/ui/layouts/mercury/reset.py +++ b/core/src/trezor/ui/layouts/mercury/reset.py @@ -18,30 +18,6 @@ if TYPE_CHECKING: CONFIRMED = trezorui2.CONFIRMED # global_import_cache -def _split_share_into_pages(share_words: Sequence[str], per_page: int = 4) -> list[str]: - pages: list[str] = [] - current = "" - fill = 2 - - for i, word in enumerate(share_words): - if i % per_page == 0: - if i != 0: - pages.append(current) - current = "" - - # Align numbers to the right. - lastnum = i + per_page + 1 - fill = 1 if lastnum < 10 else 2 - else: - current += "\n" - current += f"{i + 1:>{fill}}. {word}" - - if current: - pages.append(current) - - return pages - - async def show_share_words( share_words: Sequence[str], share_index: int | None = None, @@ -56,13 +32,11 @@ async def show_share_words( group_index + 1, share_index + 1 ) - pages = _split_share_into_pages(share_words) - result = await interact( RustLayout( trezorui2.show_share_words( title=title, - pages=pages, + pages=share_words, ), ), "backup_words", @@ -80,11 +54,11 @@ async def select_word( group_index: int | None = None, ) -> str: if share_index is None: - title: str = TR.reset__check_seed_title + description: str = TR.reset__check_seed_title elif group_index is None: - title = TR.reset__check_share_title_template.format(share_index + 1) + description: str = TR.reset__check_share_title_template.format(share_index + 1) else: - title = TR.reset__check_group_share_title_template.format( + description: str = TR.reset__check_group_share_title_template.format( group_index + 1, share_index + 1 ) @@ -98,10 +72,10 @@ async def select_word( result = await ctx_wait( RustLayout( trezorui2.select_word( - title=title, - description=TR.reset__select_word_x_of_y_template.format( + title=TR.reset__select_word_x_of_y_template.format( checked_index + 1, count ), + description=description, words=(words[0], words[1], words[2]), ) ) diff --git a/tests/common.py b/tests/common.py index 606100895..db479e3e8 100644 --- a/tests/common.py +++ b/tests/common.py @@ -174,24 +174,9 @@ def click_through( def read_and_confirm_mnemonic( debug: "DebugLink", choose_wrong: bool = False ) -> Generator[None, "ButtonRequest", Optional[str]]: - # TODO: these are very similar, reuse some code - if debug.model is models.T2T1: - mnemonic = yield from read_and_confirm_mnemonic_tt(debug, choose_wrong) - elif debug.model is models.T2B1: - mnemonic = yield from read_and_confirm_mnemonic_tr(debug, choose_wrong) - elif debug.model is models.T3T1: - mnemonic = yield from read_and_confirm_mnemonic_tt(debug, choose_wrong) - else: - raise ValueError(f"Unknown model: {debug.model}") - - return mnemonic - - -def read_and_confirm_mnemonic_tt( - debug: "DebugLink", choose_wrong: bool = False -) -> Generator[None, "ButtonRequest", Optional[str]]: - """Read a given number of mnemonic words from Trezor T screen and correctly - answer confirmation questions. Return the full mnemonic. + """Read a given number of mnemonic words from the screen and answer + confirmation questions. + Return the full mnemonic or None if `choose_wrong` is True. For use in an input flow function. Example: @@ -201,6 +186,23 @@ def read_and_confirm_mnemonic_tt( mnemonic = yield from read_and_confirm_mnemonic(client.debug) """ + if debug.model is models.T2T1: + mnemonic = yield from read_mnemonic_from_screen_tt(debug) + elif debug.model is models.T2B1: + mnemonic = yield from read_mnemonic_from_screen_tr(debug) + elif debug.model is models.T3T1: + mnemonic = yield from read_mnemonic_from_screen_mercury(debug) + else: + raise ValueError(f"Unknown model: {debug.model}") + + if not check_share(debug, mnemonic, choose_wrong): + return None + return " ".join(mnemonic) + + +def read_mnemonic_from_screen_tt( + debug: "DebugLink", +) -> Generator[None, "ButtonRequest", list[str]]: mnemonic: list[str] = [] br = yield assert br.pages is not None @@ -215,27 +217,12 @@ def read_and_confirm_mnemonic_tt( debug.swipe_up() debug.press_yes() - - # check share - for _ in range(3): - # Word position is the first number in the text - word_pos_match = re.search(r"\d+", debug.wait_layout().text_content()) - assert word_pos_match is not None - word_pos = int(word_pos_match.group(0)) - - index = word_pos - 1 - if choose_wrong: - debug.input(mnemonic[(index + 1) % len(mnemonic)]) - return None - else: - debug.input(mnemonic[index]) - - return " ".join(mnemonic) + return mnemonic -def read_and_confirm_mnemonic_tr( - debug: "DebugLink", choose_wrong: bool = False -) -> Generator[None, "ButtonRequest", Optional[str]]: +def read_mnemonic_from_screen_tr( + debug: "DebugLink", +) -> Generator[None, "ButtonRequest", list[str]]: mnemonic: list[str] = [] yield # write down all 12 words in order debug.press_yes() @@ -250,20 +237,47 @@ def read_and_confirm_mnemonic_tr( yield # Select correct words... debug.press_right() + return mnemonic + - # check share +def read_mnemonic_from_screen_mercury( + debug: "DebugLink", +) -> Generator[None, "ButtonRequest", list[str]]: + mnemonic: list[str] = [] + br = yield + assert br.pages is not None + + debug.wait_layout() + + for i in range(br.pages): + words = debug.wait_layout().seed_words() + mnemonic.extend(words) + debug.swipe_up() + + return mnemonic + + +def check_share( + debug: "DebugLink", mnemonic: list[str], choose_wrong: bool = False +) -> bool: + """ + Given the mnemonic word list, proceed with the backup check: + three rounds of `Select word X of Y` choices. + """ for _ in range(3): - word_pos_match = re.search(r"\d+", debug.wait_layout().title()) + # Word position is the first number in the text + word_pos_match = re.search(r"\d+", debug.wait_layout().text_content()) assert word_pos_match is not None word_pos = int(word_pos_match.group(0)) + index = word_pos - 1 if choose_wrong: debug.input(mnemonic[(index + 1) % len(mnemonic)]) - return None + return False else: debug.input(mnemonic[index]) - return " ".join(mnemonic) + return True def click_info_button_tt(debug: "DebugLink"):