diff --git a/core/assets/warn-icon.png b/core/assets/warn-icon.png new file mode 100644 index 000000000..2ae3841dd Binary files /dev/null and b/core/assets/warn-icon.png differ diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 0435e810c..ccff303b8 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -27,6 +27,7 @@ static void _librust_qstrs(void) { MP_QSTR_confirm_text; MP_QSTR_confirm_total; MP_QSTR_confirm_with_info; + MP_QSTR_confirm_recovery; MP_QSTR_show_checklist; MP_QSTR_show_error; MP_QSTR_show_qr; @@ -40,6 +41,9 @@ static void _librust_qstrs(void) { MP_QSTR_request_bip39; MP_QSTR_request_slip39; MP_QSTR_select_word; + MP_QSTR_select_word_count; + MP_QSTR_show_group_share_success; + MP_QSTR_show_remaining_shares; MP_QSTR_show_share_words; MP_QSTR_attach_timer_fn; @@ -85,4 +89,5 @@ static void _librust_qstrs(void) { MP_QSTR_items; MP_QSTR_active; MP_QSTR_info_button; + MP_QSTR_time_ms; } diff --git a/core/embed/rust/src/error.rs b/core/embed/rust/src/error.rs index 13e278603..aa930832a 100644 --- a/core/embed/rust/src/error.rs +++ b/core/embed/rust/src/error.rs @@ -9,7 +9,7 @@ use cstr_core::CStr; use crate::micropython::{ffi, obj::Obj, qstr::Qstr}; #[allow(clippy::enum_variant_names)] // We mimic the Python exception classnames here. -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub enum Error { TypeError, OutOfRange, diff --git a/core/embed/rust/src/ui/component/mod.rs b/core/embed/rust/src/ui/component/mod.rs index 915fb0b59..d33b70648 100644 --- a/core/embed/rust/src/ui/component/mod.rs +++ b/core/embed/rust/src/ui/component/mod.rs @@ -12,6 +12,7 @@ pub mod paginated; pub mod painter; pub mod placed; pub mod text; +pub mod timeout; pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken}; pub use border::Border; @@ -28,3 +29,4 @@ pub use text::{ formatted::FormattedText, layout::{LineBreaking, PageBreaking, TextLayout}, }; +pub use timeout::{Timeout, TimeoutMsg}; diff --git a/core/embed/rust/src/ui/component/timeout.rs b/core/embed/rust/src/ui/component/timeout.rs new file mode 100644 index 000000000..dbd58a42d --- /dev/null +++ b/core/embed/rust/src/ui/component/timeout.rs @@ -0,0 +1,60 @@ +use crate::{ + time::Duration, + ui::{ + component::{Component, Event, EventCtx, TimerToken}, + geometry::Rect, + }, +}; + +pub struct Timeout { + time_ms: u32, + timer: Option, +} + +pub enum TimeoutMsg { + TimedOut, +} + +impl Timeout { + pub fn new(time_ms: u32) -> Self { + Self { + time_ms, + timer: None, + } + } +} + +impl Component for Timeout { + type Msg = TimeoutMsg; + + fn place(&mut self, _bounds: Rect) -> Rect { + Rect::zero() + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + match event { + // Set up timer. + Event::Attach => { + self.timer = Some(ctx.request_timer(Duration::from_millis(self.time_ms))); + None + } + // Fire. + Event::Timer(token) if Some(token) == self.timer => { + self.timer = None; + Some(TimeoutMsg::TimedOut) + } + _ => None, + } + } + + fn paint(&mut self) {} +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Timeout { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("Timeout"); + t.int(self.time_ms as i64); + t.close(); + } +} diff --git a/core/embed/rust/src/ui/layout/mod.rs b/core/embed/rust/src/ui/layout/mod.rs index 7aa7dfad3..0a67444f6 100644 --- a/core/embed/rust/src/ui/layout/mod.rs +++ b/core/embed/rust/src/ui/layout/mod.rs @@ -1,2 +1,3 @@ pub mod obj; pub mod result; +pub mod util; diff --git a/core/embed/rust/src/ui/layout/util.rs b/core/embed/rust/src/ui/layout/util.rs new file mode 100644 index 000000000..8b0806bb5 --- /dev/null +++ b/core/embed/rust/src/ui/layout/util.rs @@ -0,0 +1,23 @@ +use crate::{ + error::Error, + micropython::{ + iter::{Iter, IterBuf}, + obj::Obj, + }, +}; +use cstr_core::cstr; +use heapless::Vec; + +pub fn iter_into_array(iterable: Obj) -> Result<[T; N], Error> +where + T: TryFrom, +{ + let err = Error::ValueError(cstr!("Invalid iterable length")); + let mut vec = Vec::::new(); + let mut iter_buf = IterBuf::new(); + for item in Iter::try_from_obj_with_buf(iterable, &mut iter_buf)? { + vec.push(item.try_into()?).map_err(|_| err)?; + } + // Returns error if array.len() != N + vec.into_array().map_err(|_| err) +} diff --git a/core/embed/rust/src/ui/model_tt/component/button.rs b/core/embed/rust/src/ui/model_tt/component/button.rs index 876d219bb..c7d3e917a 100644 --- a/core/embed/rust/src/ui/model_tt/component/button.rs +++ b/core/embed/rust/src/ui/model_tt/component/button.rs @@ -434,6 +434,38 @@ impl Button { ) } + pub fn abort_info_enter() -> CancelInfoConfirm< + &'static str, + impl Fn(ButtonMsg) -> Option, + impl Fn(ButtonMsg) -> Option, + impl Fn(ButtonMsg) -> Option, + > { + let left = Button::with_text("ABORT").styled(theme::button_cancel()); + let middle = Button::with_text("INFO"); + let right = Button::with_text("ENTER").styled(theme::button_confirm()); + theme::button_bar(( + GridPlaced::new(left) + .with_grid(1, 3) + .with_spacing(theme::BUTTON_SPACING) + .with_row_col(0, 0) + .map(|msg| { + (matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Cancelled) + }), + GridPlaced::new(middle) + .with_grid(1, 3) + .with_spacing(theme::BUTTON_SPACING) + .with_row_col(0, 1) + .map(|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Info)), + GridPlaced::new(right) + .with_grid(1, 3) + .with_spacing(theme::BUTTON_SPACING) + .with_row_col(0, 2) + .map(|msg| { + (matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Confirmed) + }), + )) + } + pub fn select_word( words: [T; 3], ) -> CancelInfoConfirm< diff --git a/core/embed/rust/src/ui/model_tt/component/dialog.rs b/core/embed/rust/src/ui/model_tt/component/dialog.rs index 89df49576..44d9c10b3 100644 --- a/core/embed/rust/src/ui/model_tt/component/dialog.rs +++ b/core/embed/rust/src/ui/model_tt/component/dialog.rs @@ -117,6 +117,24 @@ where self } + pub fn new_shares(lines: [T; 4], controls: U) -> Self { + let [l0, l1, l2, l3] = lines; + Self { + image: Child::new(Image::new(theme::IMAGE_SUCCESS)), + paragraphs: Paragraphs::new() + .with_placement(LinearPlacement::vertical().align_at_center()) + .add_color(theme::TEXT_NORMAL, theme::OFF_WHITE, l0) + .centered() + .add(theme::TEXT_MEDIUM, l1) + .centered() + .add_color(theme::TEXT_NORMAL, theme::OFF_WHITE, l2) + .centered() + .add(theme::TEXT_MEDIUM, l3) + .centered(), + controls: Child::new(controls), + } + } + pub const ICON_AREA_PADDING: i32 = 2; pub const ICON_AREA_HEIGHT: i32 = 60; pub const VALUE_SPACE: i32 = 5; diff --git a/core/embed/rust/src/ui/model_tt/component/frame.rs b/core/embed/rust/src/ui/model_tt/component/frame.rs index 0e9c08792..b7b2a8ed6 100644 --- a/core/embed/rust/src/ui/model_tt/component/frame.rs +++ b/core/embed/rust/src/ui/model_tt/component/frame.rs @@ -1,8 +1,8 @@ use super::theme; use crate::ui::{ component::{Child, Component, Event, EventCtx}, - display, - geometry::{Insets, Rect}, + display::{self, Color, Font}, + geometry::{Insets, Offset, Rect}, }; pub struct Frame { @@ -91,3 +91,99 @@ where t.close(); } } + +pub struct NotificationFrame { + area: Rect, + border: Insets, + icon: &'static [u8], + title: U, + content: Child, +} + +impl NotificationFrame +where + T: Component, + U: AsRef, +{ + const HEIGHT: i32 = 42; + const COLOR: Color = theme::YELLOW; + const FONT: Font = theme::FONT_BOLD; + const TEXT_OFFSET: Offset = Offset::new(1, -2); + const ICON_SPACE: i32 = 8; + + pub fn new(icon: &'static [u8], title: U, content: T) -> Self { + Self { + icon, + title, + area: Rect::zero(), + border: theme::borders_notification(), + content: Child::new(content), + } + } + + pub fn inner(&self) -> &T { + self.content.inner() + } +} + +impl Component for NotificationFrame +where + T: Component, + U: AsRef, +{ + type Msg = T::Msg; + + fn place(&mut self, bounds: Rect) -> Rect { + let (title_area, content_area) = bounds.split_top(Self::HEIGHT); + let content_area = content_area.inset(self.border); + + self.area = title_area; + self.content.place(content_area); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.content.event(ctx, event) + } + + fn paint(&mut self) { + let toif_info = unwrap!(display::toif_info(self.icon), "Invalid TOIF data"); + let icon_width = toif_info.0.y; + let text_width = Self::FONT.text_width(self.title.as_ref()); + let text_height = Self::FONT.text_height(); + let text_center = + self.area.center() + Offset::new((icon_width + Self::ICON_SPACE) / 2, text_height / 2); + let icon_center = self.area.center() - Offset::x((text_width + Self::ICON_SPACE) / 2); + + display::rect_fill_rounded(self.area, Self::COLOR, theme::BG, 2); + display::text_center( + text_center + Self::TEXT_OFFSET, + self.title.as_ref(), + Self::FONT, + theme::BG, + Self::COLOR, + ); + display::icon(icon_center, self.icon, theme::BG, Self::COLOR); + + self.content.paint(); + } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(self.area); + self.content.bounds(sink); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for NotificationFrame +where + T: crate::trace::Trace, + U: crate::trace::Trace + AsRef, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("NotificationFrame"); + t.field("title", &self.title); + t.field("content", &self.content); + t.close(); + } +} diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/mod.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/mod.rs index 959db583a..1b4b6c442 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/mod.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/mod.rs @@ -3,5 +3,6 @@ pub mod mnemonic; pub mod passphrase; pub mod pin; pub mod slip39; +pub mod word_count; mod common; diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/word_count.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/word_count.rs new file mode 100644 index 000000000..5d580c7ac --- /dev/null +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/word_count.rs @@ -0,0 +1,73 @@ +use crate::ui::{ + component::{Component, Event, EventCtx}, + geometry::{Grid, GridCellSpan, Rect}, + model_tt::{ + component::button::{Button, ButtonMsg}, + theme, + }, +}; + +const NUMBERS: [u32; 5] = [12, 18, 20, 24, 33]; +const LABELS: [&str; 5] = ["12", "18", "20", "24", "33"]; +const CELLS: [(usize, usize); 5] = [(0, 0), (0, 2), (0, 4), (1, 1), (1, 3)]; + +pub struct SelectWordCount { + button: [Button<&'static str>; NUMBERS.len()], +} + +pub enum SelectWordCountMsg { + Selected(u32), +} + +impl SelectWordCount { + pub fn new() -> Self { + SelectWordCount { + button: LABELS.map(Button::with_text), + } + } +} + +impl Component for SelectWordCount { + type Msg = SelectWordCountMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + let (_, bounds) = bounds.split_bottom(theme::button_rows(2)); + let grid = Grid::new(bounds, 2, 6).with_spacing(theme::BUTTON_SPACING); + for (btn, (x, y)) in self.button.iter_mut().zip(CELLS) { + btn.place(grid.cells(GridCellSpan { + from: (x, y), + to: (x, y + 1), + })); + } + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + for (i, btn) in self.button.iter_mut().enumerate() { + if let Some(ButtonMsg::Clicked) = btn.event(ctx, event) { + return Some(SelectWordCountMsg::Selected(NUMBERS[i])); + } + } + None + } + + fn paint(&mut self) { + for btn in self.button.iter_mut() { + btn.paint() + } + } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + for btn in self.button.iter() { + btn.bounds(sink) + } + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for SelectWordCount { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("SelectWordCount"); + t.close(); + } +} diff --git a/core/embed/rust/src/ui/model_tt/component/mod.rs b/core/embed/rust/src/ui/model_tt/component/mod.rs index 8ce54b82f..e3e789138 100644 --- a/core/embed/rust/src/ui/model_tt/component/mod.rs +++ b/core/embed/rust/src/ui/model_tt/component/mod.rs @@ -13,8 +13,8 @@ pub use button::{ Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelConfirmMsg, CancelInfoConfirmMsg, SelectWordMsg, }; -pub use dialog::{Dialog, DialogLayout, DialogMsg, IconDialog}; -pub use frame::Frame; +pub use dialog::{Dialog, DialogMsg, IconDialog}; +pub use frame::{Frame, NotificationFrame}; pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg}; pub use keyboard::{ bip39::Bip39Input, @@ -22,6 +22,7 @@ pub use keyboard::{ passphrase::{PassphraseKeyboard, PassphraseKeyboardMsg}, pin::{PinKeyboard, PinKeyboardMsg}, slip39::Slip39Input, + word_count::{SelectWordCount, SelectWordCountMsg}, }; pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet}; pub use number_input::{NumberInputDialog, NumberInputDialogMsg}; diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index cd0ca271a..9109267e8 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -1,5 +1,4 @@ use core::{cmp::Ordering, convert::TryInto, ops::Deref}; -use cstr_core::cstr; use crate::{ error::Error, @@ -18,12 +17,13 @@ use crate::{ paginated::{PageMsg, Paginate}, painter, text::paragraphs::{Checklist, Paragraphs}, - Border, Component, + Border, Component, Timeout, TimeoutMsg, }, geometry, layout::{ obj::{ComponentMsgObj, LayoutObj}, result::{CANCELLED, CONFIRMED, INFO}, + util::iter_into_array, }, }, }; @@ -32,9 +32,10 @@ use super::{ component::{ Bip39Input, Button, ButtonMsg, ButtonStyleSheet, CancelConfirmMsg, CancelInfoConfirmMsg, Dialog, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, IconDialog, MnemonicInput, - MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputDialog, NumberInputDialogMsg, - PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, SelectWordMsg, - Slip39Input, SwipeHoldPage, SwipePage, + MnemonicKeyboard, MnemonicKeyboardMsg, NotificationFrame, NumberInputDialog, + NumberInputDialogMsg, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, + PinKeyboardMsg, SelectWordCount, SelectWordCountMsg, SelectWordMsg, Slip39Input, + SwipeHoldPage, SwipePage, }, theme, }; @@ -72,6 +73,16 @@ impl TryFrom for Obj { } } +impl TryFrom for Obj { + type Error = Error; + + fn try_from(value: SelectWordCountMsg) -> Result { + match value { + SelectWordCountMsg::Selected(i) => i.try_into(), + } + } +} + impl ComponentMsgObj for Dialog where T: ComponentMsgObj, @@ -162,6 +173,16 @@ where } } +impl ComponentMsgObj for NotificationFrame +where + T: ComponentMsgObj, + U: AsRef, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + self.inner().msg_try_into_obj(msg) + } +} + impl ComponentMsgObj for SwipePage where T: Component + Paginate, @@ -512,11 +533,12 @@ fn new_show_modal( ) -> Result { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let description: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_description, StrBuffer::empty())?; - let button: StrBuffer = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?; + let button: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_button, "CONTINUE".into())?; let allow_cancel: bool = kwargs.get_or(Qstr::MP_QSTR_allow_cancel, true)?; + let time_ms: u32 = kwargs.get_or(Qstr::MP_QSTR_time_ms, 0)?; - let obj = if allow_cancel { - LayoutObj::new( + let obj = match (allow_cancel, time_ms) { + (true, 0) => LayoutObj::new( IconDialog::new( icon, title, @@ -528,19 +550,29 @@ fn new_show_modal( ) .with_description(description), )? - .into() - } else { - LayoutObj::new( + .into(), + (false, 0) => LayoutObj::new( IconDialog::new( icon, title, - Button::with_text(button).styled(button_style).map(|msg| { + theme::button_bar(Button::with_text(button).styled(button_style).map(|msg| { (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed) + })), + ) + .with_description(description), + )? + .into(), + (_, time_ms) => LayoutObj::new( + IconDialog::new( + icon, + title, + Timeout::new(time_ms).map(|msg| { + (matches!(msg, TimeoutMsg::TimedOut)).then(|| CancelConfirmMsg::Confirmed) }), ) .with_description(description), )? - .into() + .into(), }; Ok(obj) @@ -709,17 +741,7 @@ extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?; let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?; - - let mut words = [StrBuffer::empty(), StrBuffer::empty(), StrBuffer::empty()]; - let mut iter_buf = IterBuf::new(); - let mut iter = Iter::try_from_obj_with_buf(words_iterable, &mut iter_buf)?; - let words_err = || Error::ValueError(cstr!("Invalid words count")); - for item in &mut words { - *item = iter.next().ok_or_else(words_err)?.try_into()?; - } - if iter.next().is_some() { - return Err(words_err()); - } + let words: [StrBuffer; 3] = iter_into_array(words_iterable)?; let paragraphs = Paragraphs::new().add(theme::TEXT_NORMAL, description); let buttons = Button::select_word(words); @@ -826,6 +848,140 @@ extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut M unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } +extern "C" fn new_confirm_recovery(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).unwrap().try_into().unwrap(); + let description: StrBuffer = kwargs + .get(Qstr::MP_QSTR_description) + .unwrap() + .try_into() + .unwrap(); + let button: StrBuffer = kwargs + .get(Qstr::MP_QSTR_button) + .unwrap() + .try_into() + .unwrap(); + let dry_run: bool = kwargs + .get(Qstr::MP_QSTR_dry_run) + .unwrap() + .try_into() + .unwrap(); + let info_button: bool = kwargs.get_or(Qstr::MP_QSTR_info_button, false).unwrap(); + + let paragraphs = Paragraphs::new() + .with_spacing(theme::RECOVERY_SPACING) + .add(theme::TEXT_BOLD, title) + .centered() + .add_color(theme::TEXT_NORMAL, theme::OFF_WHITE, description) + .centered(); + + let notification = if dry_run { + "SEED CHECK" + } else { + "RECOVERY MODE" + }; + + let obj = if info_button { + LayoutObj::new( + NotificationFrame::new( + theme::ICON_WARN, + notification, + Dialog::new(paragraphs, Button::<&'static str>::abort_info_enter()), + ) + .into_child(), + )? + } else { + LayoutObj::new( + NotificationFrame::new( + theme::ICON_WARN, + notification, + Dialog::new(paragraphs, Button::cancel_confirm_text(None, button)), + ) + .into_child(), + )? + }; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let dry_run: bool = kwargs + .get(Qstr::MP_QSTR_dry_run) + .unwrap() + .try_into() + .unwrap(); + let title = if dry_run { + "SEED CHECK" + } else { + "RECOVERY MODE" + }; + + let paragraphs = Paragraphs::new() + .add(theme::TEXT_BOLD, "Number of words?") + .centered(); + + let obj = LayoutObj::new( + Frame::new(title, Dialog::new(paragraphs, SelectWordCount::new())) + .with_border(theme::borders()) + .into_child(), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_group_share_success( + n_args: usize, + args: *const Obj, + kwargs: *mut Map, +) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let lines_iterable: Obj = kwargs.get(Qstr::MP_QSTR_lines)?; + let lines: [StrBuffer; 4] = iter_into_array(lines_iterable)?; + + let obj = LayoutObj::new(IconDialog::new_shares( + lines, + theme::button_bar(Button::with_text("CONTINUE").map(|msg| { + (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed) + })), + ))?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_remaining_shares(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let pages_iterable: Obj = kwargs.get(Qstr::MP_QSTR_pages)?; + + let mut paragraphs = Paragraphs::new(); + let mut iter_buf = IterBuf::new(); + let iter = Iter::try_from_obj_with_buf(pages_iterable, &mut iter_buf)?; + for page in iter { + let [title, description]: [StrBuffer; 2] = iter_into_array(page)?; + paragraphs = paragraphs + .add(theme::TEXT_BOLD, title) + .add(theme::TEXT_NORMAL, description) + .add_break(); + } + + let obj = LayoutObj::new(Frame::new( + "REMAINING SHARES", + SwipePage::new( + paragraphs, + theme::button_bar(Button::with_text("CONTINUE").map(|msg| { + (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed) + })), + theme::BG, + ), + ))?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + #[no_mangle] pub static mp_module_trezorui2: Module = obj_module! { Qstr::MP_QSTR___name__ => Qstr::MP_QSTR_trezorui2.to_obj(), @@ -932,9 +1088,10 @@ pub static mp_module_trezorui2: Module = obj_module! { /// def show_error( /// *, /// title: str, - /// button: str, + /// button: str = "CONTINUE", /// description: str = "", /// allow_cancel: bool = False, + /// time_ms: int = 0, /// ) -> object: /// """Error modal.""" Qstr::MP_QSTR_show_error => obj_fn_kw!(0, new_show_error).as_obj(), @@ -942,9 +1099,10 @@ pub static mp_module_trezorui2: Module = obj_module! { /// def show_warning( /// *, /// title: str, - /// button: str, + /// button: str = "CONTINUE", /// description: str = "", /// allow_cancel: bool = False, + /// time_ms: int = 0, /// ) -> object: /// """Warning modal.""" Qstr::MP_QSTR_show_warning => obj_fn_kw!(0, new_show_warning).as_obj(), @@ -952,9 +1110,10 @@ pub static mp_module_trezorui2: Module = obj_module! { /// def show_success( /// *, /// title: str, - /// button: str, + /// button: str = "CONTINUE", /// description: str = "", /// allow_cancel: bool = False, + /// time_ms: int = 0, /// ) -> object: /// """Success modal.""" Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(), @@ -962,9 +1121,10 @@ pub static mp_module_trezorui2: Module = obj_module! { /// def show_info( /// *, /// title: str, - /// button: str, + /// button: str = "CONTINUE", /// description: str = "", /// allow_cancel: bool = False, + /// time_ms: int = 0, /// ) -> object: /// """Info modal.""" Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(), @@ -1068,6 +1228,38 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Checklist of backup steps. Active index is highlighted, previous items have check /// mark nex to them.""" Qstr::MP_QSTR_show_checklist => obj_fn_kw!(0, new_show_checklist).as_obj(), + + /// def confirm_recovery( + /// *, + /// title: str, + /// description: str, + /// button: str, + /// dry_run: bool, + /// info_button: bool, + /// ) -> object: + /// """Device recovery homescreen.""" + Qstr::MP_QSTR_confirm_recovery => obj_fn_kw!(0, new_confirm_recovery).as_obj(), + + /// def select_word_count( + /// *, + /// dry_run: bool, + /// ) -> int | trezorui2.CANCELLED: + /// """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 show_group_share_success( + /// *, + /// lines: Iterable[str] + /// ) -> int: + /// """Shown after successfully finishing a group.""" + Qstr::MP_QSTR_show_group_share_success => obj_fn_kw!(0, new_show_group_share_success).as_obj(), + + /// def show_remaining_shares( + /// *, + /// pages: Iterable[tuple[str, str]], + /// ) -> int: + /// """Shows SLIP39 state after info button is pressed on `confirm_recovery`.""" + Qstr::MP_QSTR_show_remaining_shares => obj_fn_kw!(0, new_show_remaining_shares).as_obj(), }; #[cfg(test)] diff --git a/core/embed/rust/src/ui/model_tt/res/warn-icon.toif b/core/embed/rust/src/ui/model_tt/res/warn-icon.toif new file mode 100644 index 000000000..90cd8122c Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/warn-icon.toif differ diff --git a/core/embed/rust/src/ui/model_tt/theme.rs b/core/embed/rust/src/ui/model_tt/theme.rs index 4b31b03a9..58f5333cc 100644 --- a/core/embed/rust/src/ui/model_tt/theme.rs +++ b/core/embed/rust/src/ui/model_tt/theme.rs @@ -57,6 +57,7 @@ pub const ICON_SPACE: &[u8] = include_res!("model_tt/res/space.toif"); pub const ICON_BACK: &[u8] = include_res!("model_tt/res/back.toif"); pub const ICON_CLICK: &[u8] = include_res!("model_tt/res/click.toif"); pub const ICON_NEXT: &[u8] = include_res!("model_tt/res/next.toif"); +pub const ICON_WARN: &[u8] = include_res!("model_tt/res/warn-icon.toif"); pub const ICON_LIST_CURRENT: &[u8] = include_res!("model_tt/res/current.toif"); pub const ICON_LIST_CHECK: &[u8] = include_res!("model_tt/res/check.toif"); @@ -151,6 +152,22 @@ pub fn label_warning_value() -> LabelStyle { } } +pub fn label_recovery_title() -> LabelStyle { + LabelStyle { + font: FONT_BOLD, + text_color: FG, + background_color: BG, + } +} + +pub fn label_recovery_description() -> LabelStyle { + LabelStyle { + font: FONT_NORMAL, + text_color: OFF_WHITE, + background_color: BG, + } +} + pub fn button_default() -> ButtonStyleSheet { ButtonStyleSheet { normal: &ButtonStyle { @@ -411,6 +428,8 @@ pub const KEYBOARD_SPACING: i32 = 8; pub const BUTTON_HEIGHT: i32 = 38; pub const BUTTON_SPACING: i32 = 6; pub const CHECKLIST_SPACING: i32 = 10; +pub const RECOVERY_SPACING: i32 = 18; + /// Standard button height in pixels. pub const fn button_rows(count: usize) -> i32 { let count = count as i32; @@ -439,3 +458,7 @@ pub const fn borders() -> Insets { pub const fn borders_scroll() -> Insets { Insets::new(13, 5, 14, 10) } + +pub const fn borders_notification() -> Insets { + Insets::new(6, 10, 14, 10) +} diff --git a/core/embed/rust/src/ui/util.rs b/core/embed/rust/src/ui/util.rs index caf028716..1300aae16 100644 --- a/core/embed/rust/src/ui/util.rs +++ b/core/embed/rust/src/ui/util.rs @@ -34,7 +34,6 @@ pub fn u32_to_str(num: u32, buffer: &mut [u8]) -> Option<&str> { #[cfg(test)] mod tests { use super::*; - use std::str; #[test] fn u32_to_str_valid() { diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 1e4e0bb16..a76f85d69 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -159,9 +159,10 @@ def confirm_modify_fee( def show_error( *, title: str, - button: str, + button: str = "CONTINUE", description: str = "", allow_cancel: bool = False, + time_ms: int = 0, ) -> object: """Error modal.""" @@ -170,9 +171,10 @@ def show_error( def show_warning( *, title: str, - button: str, + button: str = "CONTINUE", description: str = "", allow_cancel: bool = False, + time_ms: int = 0, ) -> object: """Warning modal.""" @@ -181,9 +183,10 @@ def show_warning( def show_success( *, title: str, - button: str, + button: str = "CONTINUE", description: str = "", allow_cancel: bool = False, + time_ms: int = 0, ) -> object: """Success modal.""" @@ -192,9 +195,10 @@ def show_success( def show_info( *, title: str, - button: str, + button: str = "CONTINUE", description: str = "", allow_cancel: bool = False, + time_ms: int = 0, ) -> object: """Info modal.""" @@ -308,3 +312,39 @@ def show_checklist( ) -> object: """Checklist of backup steps. Active index is highlighted, previous items have check mark nex to them.""" + + +# rust/src/ui/model_tt/layout.rs +def confirm_recovery( + *, + title: str, + description: str, + button: str, + dry_run: bool, + info_button: bool, +) -> object: + """Device recovery homescreen.""" + + +# rust/src/ui/model_tt/layout.rs +def select_word_count( + *, + dry_run: bool, +) -> int | trezorui2.CANCELLED: + """Select mnemonic word count from (12, 18, 20, 24, 33).""" + + +# rust/src/ui/model_tt/layout.rs +def show_group_share_success( + *, + lines: Iterable[str] +) -> int: + """Shown after successfully finishing a group.""" + + +# rust/src/ui/model_tt/layout.rs +def show_remaining_shares( + *, + pages: Iterable[tuple[str, str]], +) -> int: + """Shows SLIP39 state after info button is pressed on `confirm_recovery`.""" diff --git a/core/src/apps/management/recovery_device/layout.py b/core/src/apps/management/recovery_device/layout.py index cc7da5c8c..6b3bbc7c8 100644 --- a/core/src/apps/management/recovery_device/layout.py +++ b/core/src/apps/management/recovery_device/layout.py @@ -148,11 +148,13 @@ async def homescreen_dialog( info_func: Callable | None = None, ) -> None: while True: - if await continue_recovery(ctx, button_label, text, subtext, info_func): + dry_run = storage.recovery.is_dry_run() + if await continue_recovery( + ctx, button_label, text, subtext, info_func, dry_run + ): # go forward in the recovery process break # user has chosen to abort, confirm the choice - dry_run = storage.recovery.is_dry_run() try: await confirm_abort(ctx, dry_run) except wire.ActionCancelled: diff --git a/core/src/trezor/ui/components/tt/recovery.py b/core/src/trezor/ui/components/tt/recovery.py index dac24bb8a..27365717b 100644 --- a/core/src/trezor/ui/components/tt/recovery.py +++ b/core/src/trezor/ui/components/tt/recovery.py @@ -1,13 +1,12 @@ -import storage.recovery from trezor import ui class RecoveryHomescreen(ui.Component): - def __init__(self, text: str, subtext: str | None = None): + def __init__(self, dry_run: bool, text: str, subtext: str | None = None): super().__init__() self.text = text self.subtext = subtext - self.dry_run = storage.recovery.is_dry_run() + self.dry_run = dry_run def on_render(self) -> None: if not self.repaint: diff --git a/core/src/trezor/ui/layouts/tt/recovery.py b/core/src/trezor/ui/layouts/tt/recovery.py index 1655889fe..31cd03a2b 100644 --- a/core/src/trezor/ui/layouts/tt/recovery.py +++ b/core/src/trezor/ui/layouts/tt/recovery.py @@ -112,8 +112,9 @@ async def continue_recovery( text: str, subtext: str | None, info_func: Callable | None, + dry_run: bool, ) -> bool: - homepage = RecoveryHomescreen(text, subtext) + homepage = RecoveryHomescreen(dry_run, text, subtext) if info_func is not None: content = InfoConfirm( homepage, diff --git a/core/src/trezor/ui/layouts/tt_v2/__init__.py b/core/src/trezor/ui/layouts/tt_v2/__init__.py index 192944dbd..25531bdf4 100644 --- a/core/src/trezor/ui/layouts/tt_v2/__init__.py +++ b/core/src/trezor/ui/layouts/tt_v2/__init__.py @@ -277,7 +277,6 @@ async def confirm_path_warning( trezorui2.show_warning( title="Unknown path", description=path, - button="CONTINUE", ) ), "path_warning", @@ -758,13 +757,11 @@ async def confirm_metadata( layout = trezorui2.show_warning( title="Unusually high fee", description=param or "", - button="CONTINUE", ) elif br_type == "change_count_over_threshold": layout = trezorui2.show_warning( title="A lot of change-outputs", description=f"{param} outputs" if param is not None else "", - button="CONTINUE", ) else: if param is not None: @@ -929,7 +926,16 @@ async def show_popup( description_param: str = "", timeout_ms: int = 3000, ) -> None: - raise NotImplementedError + if subtitle: + title += f"\n{subtitle}".format(subtitle) + await _RustLayout( + trezorui2.show_error( + title=title, + description=description.format(description_param), + button="", + time_ms=timeout_ms, + ) + ) def draw_simple_text(title: str, description: str = "") -> None: diff --git a/core/src/trezor/ui/layouts/tt_v2/recovery.py b/core/src/trezor/ui/layouts/tt_v2/recovery.py index c599fed48..2ec5b3b04 100644 --- a/core/src/trezor/ui/layouts/tt_v2/recovery.py +++ b/core/src/trezor/ui/layouts/tt_v2/recovery.py @@ -1,9 +1,12 @@ from typing import TYPE_CHECKING -from trezor import wire +from trezor import strings, wire +from trezor.crypto.slip39 import MAX_SHARE_COUNT +from trezor.enums import ButtonRequestType import trezorui2 +from ..common import button_request, interact from . import _RustLayout if TYPE_CHECKING: @@ -12,8 +15,26 @@ if TYPE_CHECKING: pass +async def _is_confirmed_info( + ctx: wire.GenericContext, + dialog: _RustLayout, + info_func: Callable, +) -> bool: + while True: + result = await ctx.wait(dialog) + + if result is trezorui2.INFO: + await info_func(ctx) + else: + return result is trezorui2.CONFIRMED + + async def request_word_count(ctx: wire.GenericContext, dry_run: bool) -> int: - raise NotImplementedError + selector = _RustLayout(trezorui2.select_word_count(dry_run=dry_run)) + count = await interact( + ctx, selector, "word_count", ButtonRequestType.MnemonicWordCount + ) + return int(count) async def request_word( @@ -42,13 +63,54 @@ async def show_remaining_shares( shares_remaining: list[int], group_threshold: int, ) -> None: - raise NotImplementedError + pages: list[tuple[str, str]] = [] + for remaining, group in groups: + if 0 < remaining < MAX_SHARE_COUNT: + title = strings.format_plural( + "{count} more {plural} starting", remaining, "share" + ) + words = "\n".join(group) + pages.append((title, words)) + elif ( + remaining == MAX_SHARE_COUNT and shares_remaining.count(0) < group_threshold + ): + groups_remaining = group_threshold - shares_remaining.count(0) + title = strings.format_plural( + "{count} more {plural} starting", groups_remaining, "group" + ) + words = "\n".join(group) + pages.append((title, words)) + + result = await interact( + ctx, + _RustLayout(trezorui2.show_remaining_shares(pages=pages)), + "show_shares", + ButtonRequestType.Other, + ) + if result is not trezorui2.CONFIRMED: + raise wire.ActionCancelled async def show_group_share_success( ctx: wire.GenericContext, share_index: int, group_index: int ) -> None: - raise NotImplementedError + result = await interact( + ctx, + _RustLayout( + trezorui2.show_group_share_success( + lines=[ + "You have entered", + f"Share {share_index + 1}", + "from", + f"Group {group_index + 1}", + ], + ) + ), + "share_success", + ButtonRequestType.Other, + ) + if result is not trezorui2.CONFIRMED: + raise wire.ActionCancelled async def continue_recovery( @@ -57,5 +119,41 @@ async def continue_recovery( text: str, subtext: str | None, info_func: Callable | None, + dry_run: bool, ) -> bool: - return False + title = text + if subtext: + title += "\n" + title += subtext + + description = "It is safe to eject Trezor\nand continue later" + + if info_func is not None: + homepage = _RustLayout( + trezorui2.confirm_recovery( + title=title, + description=description, + button=button_label.upper(), + info_button=True, + dry_run=dry_run, + ) + ) + await button_request(ctx, "recovery", ButtonRequestType.RecoveryHomepage) + return await _is_confirmed_info(ctx, homepage, info_func) + else: + homepage = _RustLayout( + trezorui2.confirm_recovery( + title=text, + description=description, + button=button_label.upper(), + info_button=False, + dry_run=dry_run, + ) + ) + result = await interact( + ctx, + homepage, + "recovery", + ButtonRequestType.RecoveryHomepage, + ) + return result is trezorui2.CONFIRMED diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index d10e9e688..def0554ad 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -2485,37 +2485,37 @@ "TTui2_reset_recovery-test_recovery_bip39_dryrun.py::test_bad_parameters[passphrase_protection-True]": "f03b50df7f4a161078fa903c44f37272961b70358d4014d30a12888e1fd2caf1", "TTui2_reset_recovery-test_recovery_bip39_dryrun.py::test_bad_parameters[pin_protection-True]": "f03b50df7f4a161078fa903c44f37272961b70358d4014d30a12888e1fd2caf1", "TTui2_reset_recovery-test_recovery_bip39_dryrun.py::test_bad_parameters[u2f_counter-1]": "f03b50df7f4a161078fa903c44f37272961b70358d4014d30a12888e1fd2caf1", -"TTui2_reset_recovery-test_recovery_bip39_dryrun.py::test_dry_run": "fb6d1ba5b6f8b0c28d7c114a1cad2170029800a50cea1dce1b222b56c53d4169", -"TTui2_reset_recovery-test_recovery_bip39_dryrun.py::test_invalid_seed_core": "fb6d1ba5b6f8b0c28d7c114a1cad2170029800a50cea1dce1b222b56c53d4169", -"TTui2_reset_recovery-test_recovery_bip39_dryrun.py::test_seed_mismatch": "fb6d1ba5b6f8b0c28d7c114a1cad2170029800a50cea1dce1b222b56c53d4169", +"TTui2_reset_recovery-test_recovery_bip39_dryrun.py::test_dry_run": "98f8d8961d364bd839ece099ef67576cdcd27529267619cadf963dec6cc7144d", +"TTui2_reset_recovery-test_recovery_bip39_dryrun.py::test_invalid_seed_core": "98f8d8961d364bd839ece099ef67576cdcd27529267619cadf963dec6cc7144d", +"TTui2_reset_recovery-test_recovery_bip39_dryrun.py::test_seed_mismatch": "98f8d8961d364bd839ece099ef67576cdcd27529267619cadf963dec6cc7144d", "TTui2_reset_recovery-test_recovery_bip39_dryrun.py::test_uninitialized": "8711e2fa6f7b301add7641e08ffb4bacf29bcd41530b1dd435fdbddb49b4bdf8", "TTui2_reset_recovery-test_recovery_bip39_t2.py::test_already_initialized": "f03b50df7f4a161078fa903c44f37272961b70358d4014d30a12888e1fd2caf1", "TTui2_reset_recovery-test_recovery_bip39_t2.py::test_tt_nopin_nopassphrase": "3919d9404e9f9a4880bd084edbfa02fbb04641008e04b83458633691e69bf239", "TTui2_reset_recovery-test_recovery_bip39_t2.py::test_tt_pin_passphrase": "3919d9404e9f9a4880bd084edbfa02fbb04641008e04b83458633691e69bf239", -"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_abort": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_extra_share_entered": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_group_threshold_reached": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_noabort": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_same_share": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f1-afc2dad5": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0-eb47093e": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares0-c2d2-850ffa77": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares1-c41d-ca9ddec8": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_dryrun": "7a5048ee96f76bb2e2a6d64fd89dfc22eb6fe792eaa769058249d0f552ee59d3", -"TTui2_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_invalid_seed_dryrun": "7a5048ee96f76bb2e2a6d64fd89dfc22eb6fe792eaa769058249d0f552ee59d3", -"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_1of1": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_abort": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_ask_word_number": "e53306364b3a4cc2d23da5adeafa6f02fd946dcf042c6c77efd1ce221a319ea8", -"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_noabort": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_recover_with_pin_passphrase": "ff0120b13a8ec8ecfe3a70d3dce62a9eaafa116632284d85983e7d1f040d6d4a", -"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_same_share": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_secret[shares0-491b795b80fc21ccdf466c0fbc98c8fc]": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_secret[shares1-b770e0da1363247652de97a39-a50896b7": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[0]": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[1]": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[2]": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", -"TTui2_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_dryrun": "7a5048ee96f76bb2e2a6d64fd89dfc22eb6fe792eaa769058249d0f552ee59d3", -"TTui2_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_invalid_seed_dryrun": "7a5048ee96f76bb2e2a6d64fd89dfc22eb6fe792eaa769058249d0f552ee59d3", +"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_abort": "7717c45923e9f73efd1201a728e659db2cf3631c7d244418b77fc04301875a10", +"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_extra_share_entered": "b9575651cef23a6265294937ef7726475b1b3eaba418528b3116dee96b2dfe9e", +"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_group_threshold_reached": "101e7e7eee51ed188985046f5e01fbc4600ba263db9015217b916b5a3a6ce65c", +"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_noabort": "3bc2a8d4c8371e8fb799e22d21a9a497f13939208b38d2a85aead1b875952aa7", +"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_same_share": "0ed614e0e794f9fa767686c79c704c78707d8e2bd3393206cd5bdc2fd7201759", +"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f1-afc2dad5": "43fc36511b94da979441e3683888b1aa5587d5d58d719b021539df17e2848fb6", +"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0-eb47093e": "10e782cd14802e4d5003619888a169f6c387c9a3b74e3a058482878b83e1dd3a", +"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares0-c2d2-850ffa77": "a41b5343855c5a4aae5a5e7b937623e0302fda752561a36fb34b3c5f95bd655a", +"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares1-c41d-ca9ddec8": "566c82e28c238e2aeaa6e3a4b98bdfe6efa9aaf54a17098669ad5e2ab8682262", +"TTui2_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_dryrun": "bb859924ef406c39a8ee48e959e781fb051c8cc9453b429e01eecd2a6ccea997", +"TTui2_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_invalid_seed_dryrun": "157eb87f8a2d7f89fdbfea064a111a3582c8e150f1dd6553fbc7ef393ab145d8", +"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_1of1": "04fd1206841ecf3debd8c29a86a90f611714291e0dbd762e20d3caf4fb851268", +"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_abort": "7717c45923e9f73efd1201a728e659db2cf3631c7d244418b77fc04301875a10", +"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_ask_word_number": "0ed3d06281d5b16c9258f10c936f07d1664d0643f7f957edd79566fd605926e7", +"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_noabort": "55fa3f81be735f0dffa9cc3469885e7cae3c4721d8030d1b9da32a2b3e15784f", +"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_recover_with_pin_passphrase": "3d24011a6388d2e0d31138a7029cb5672da572a0aabc9322473ff9ca2f42bde0", +"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_same_share": "c94d6cfd3a3617de12b65be94eec59b0dfdeb4d5c91c04c04bd9db60632761d0", +"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_secret[shares0-491b795b80fc21ccdf466c0fbc98c8fc]": "4b4670e1287dbb625c7a286d8c6a5c235d9ddb1d2a2b23824bfca16037eae082", +"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_secret[shares1-b770e0da1363247652de97a39-a50896b7": "3eb8b93b8a88287bbe846281569138b508be404f713c59672149dd8ff6fdb502", +"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[0]": "6f689a518dcc01536ad99107261c856e16344f1f414c42da0eb18fc441c6fe3e", +"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[1]": "1b01e0fe891dc6a3d390fa2c06e19d7255a5167a4d8bf5e53dda1ea2e68487e3", +"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[2]": "26e22c5bc293462e886522c972928c3c2dac5ccee9d8355484736e6df9d33a56", +"TTui2_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_dryrun": "9548fcdd9e0790576135e805ca87e53f17b7c414e722659d71ece125e768c914", +"TTui2_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_invalid_seed_dryrun": "97b30d608c58c7831ceef9fd9127d9699766b1ae98843886d8f9b5220e536562", "TTui2_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Bip39-backup_flow_bip39]": "e858239e4efffb8c185c098c8c7a0b9ca19d4b3c4836ee43b6db926abf7918bc", "TTui2_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Advanced-bac-f67baa1c": "bd1f14226b2b3b778dc146ab8e4d0b0535657649330ca5dd3efa4c00a70ecb24", "TTui2_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Basic-backup-6348e7fe": "9f0568a1782a4d392cf64c2f4478b6fadc6ffcaea8a3c706c04ba798d2fa5195", @@ -2523,14 +2523,14 @@ "TTui2_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced-backup-dcbda5cf": "00ffcd324fa349282cd08524722e92b0b8469739259f66b8de30182dba6f6607", "TTui2_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic-backup_fl-1577de4d": "77797bb5a33f8ffafb778b85a1a5dd2f59ec651c03e71c681656118ea15ed0cc", "TTui2_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "f03b50df7f4a161078fa903c44f37272961b70358d4014d30a12888e1fd2caf1", -"TTui2_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "e34eb8420d9e74571c36e39352ba2308d90a021b2f5ef2e78afb167764ea931d", +"TTui2_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "5b9750276d378866c33339e9bb644fe7be522b765d75d5f41dc955058a25760e", "TTui2_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "c9791eaa949c37a6996ec08677b1aff00dd294097c62cc51b905e115fd32cdb3", "TTui2_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "a21078e36aa4b49377ef3fc1ad6084f725eb3941608a03e1bc899dca828d07ad", "TTui2_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "6818c19bba26ef8acecd87cadbe5bf678449a519e49cc365311708d91abe92be", "TTui2_reset_recovery-test_reset_bip39_t2.py::test_reset_failed_check": "fad48c5d40c5df33cbf4abbb7695f6c22b490ab3f118cfdf3b64b2bc2936920f", -"TTui2_reset_recovery-test_reset_recovery_bip39.py::test_reset_recovery": "1c463175327e8a0286464e8c7165de3a1434b3c48e7f7b6ed47edfaa992bb039", -"TTui2_reset_recovery-test_reset_recovery_slip39_advanced.py::test_reset_recovery": "6dc1c0d4106d8a789a7eae973a3b768cb1a5383fdf42866e51516e67fd4a36ca", -"TTui2_reset_recovery-test_reset_recovery_slip39_basic.py::test_reset_recovery": "78cd86e1e473e5cdd541840a657723f663d9294fc4fdc74af97274ea1b3939b8", +"TTui2_reset_recovery-test_reset_recovery_bip39.py::test_reset_recovery": "447d34b14feaffdca56fb48e2490c4a47a875edc7d7ad1952feb7d9df9934705", +"TTui2_reset_recovery-test_reset_recovery_slip39_advanced.py::test_reset_recovery": "014bafb47680c0cf601e01fa82df249dbdb4c78a170d871683d878e11d428a3e", +"TTui2_reset_recovery-test_reset_recovery_slip39_basic.py::test_reset_recovery": "40ccc74d7bc2ea9d5a76e62c4867316a84625c373c0bae5998b518f8060604d7", "TTui2_reset_recovery-test_reset_slip39_advanced.py::test_reset_device_slip39_advanced": "16fdec338958b038ecb96614cb8b47d5dcdd61bf1422e9dff8bfd8022fef1536", "TTui2_reset_recovery-test_reset_slip39_basic.py::test_reset_device_slip39_basic": "e68ba714482d7d6239a4e4d43d890a3de340230c5618fc820b1c24027c87b72d", "TTui2_reset_recovery-test_reset_slip39_basic.py::test_reset_device_slip39_basic_256": "146b4d7880fd9bd325da46d675ec4ee4f88e27916eba3198d911c3c4c6a5e29f", @@ -2626,13 +2626,13 @@ "TTui2_test_msg_backup_device.py::test_no_backup_show_entropy_fails": "8711e2fa6f7b301add7641e08ffb4bacf29bcd41530b1dd435fdbddb49b4bdf8", "TTui2_test_msg_change_wipe_code_t2.py::test_set_pin_to_wipe_code": "a6976555523e774fc1eb0ff1c192cdca6f6298cebc962a8d4b87d197a945af87", "TTui2_test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "e7a3858d2db160253ff3dbde450e5632fcc385ff529d586de28c49f0bf4ed059", -"TTui2_test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "8fd746c535ec5add348b76002a7936cc85c3206edbb59f225ad075912329452d", -"TTui2_test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "25eac0cb6ea45c0cb9cfcad3b4ac3ec33af9212a7b812370c8132ef9f14c7700", -"TTui2_test_msg_changepin_t2.py::test_change_failed": "e207e2c62f6930e9e112d7a1a31b9a66c14580df8aac82ea40e2f243d987e878", +"TTui2_test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "a8e165eb64558ee3f38adb334a123d20ae40088515d559e06e3dcc6ab960f865", +"TTui2_test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "2b681988285d472e128edcb972cff8784ed34c9cba2f2a02e70a243d6561f86a", +"TTui2_test_msg_changepin_t2.py::test_change_failed": "5409de461cc6264246e07a5393c7fba972453b1af329f1ea27121512cffda419", "TTui2_test_msg_changepin_t2.py::test_change_invalid_current": "5e04bc7ab716549d8aa70087cac37c8e1beafaad9929713a631e11845102d4e9", "TTui2_test_msg_changepin_t2.py::test_change_pin": "2b891a989548802893f1b6a486e9751704a460ce4f59b65b39315318e11171f2", "TTui2_test_msg_changepin_t2.py::test_remove_pin": "0483000d2760100596744b4270119860925f767028dfc6453141d4279fadb468", -"TTui2_test_msg_changepin_t2.py::test_set_failed": "391b309cadaefcaab9086f7e003faec88b7e38c13f2738b5ad1aa4bfd5d89566", +"TTui2_test_msg_changepin_t2.py::test_set_failed": "8870468b6656512de925b8c9a84bc3755dc39ae2b86503ff823f068efa38e5a8", "TTui2_test_msg_changepin_t2.py::test_set_pin": "9fa58d0b6e5dcaa581f7bbccc4e6a84c4de732200c2bc8465b83a79beceb55d5", "TTui2_test_msg_loaddevice.py::test_load_device_1": "eeb5afb34b4bbf42b8c635fdd34bae5c1e3693facb16e6d64e629746612a2c3f", "TTui2_test_msg_loaddevice.py::test_load_device_2": "a95020926a62b4078cb0034f6e7a772e49fc42121c9197b534437e26c306a994",