1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-22 15:38:11 +00:00

feat(core/rust/ui): recovery layouts

[no changelog]
This commit is contained in:
Martin Milata 2022-08-30 14:10:30 +02:00
parent 5052594789
commit 5a9c2a1363
24 changed files with 760 additions and 88 deletions

BIN
core/assets/warn-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

View File

@ -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;
}

View File

@ -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,

View File

@ -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};

View File

@ -0,0 +1,60 @@
use crate::{
time::Duration,
ui::{
component::{Component, Event, EventCtx, TimerToken},
geometry::Rect,
},
};
pub struct Timeout {
time_ms: u32,
timer: Option<TimerToken>,
}
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<Self::Msg> {
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();
}
}

View File

@ -1,2 +1,3 @@
pub mod obj;
pub mod result;
pub mod util;

View File

@ -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<T, const N: usize>(iterable: Obj) -> Result<[T; N], Error>
where
T: TryFrom<Obj, Error = Error>,
{
let err = Error::ValueError(cstr!("Invalid iterable length"));
let mut vec = Vec::<T, N>::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)
}

View File

@ -434,6 +434,38 @@ impl<T> Button<T> {
)
}
pub fn abort_info_enter() -> CancelInfoConfirm<
&'static str,
impl Fn(ButtonMsg) -> Option<CancelInfoConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelInfoConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelInfoConfirmMsg>,
> {
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<

View File

@ -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;

View File

@ -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<T, U> {
@ -91,3 +91,99 @@ where
t.close();
}
}
pub struct NotificationFrame<T, U> {
area: Rect,
border: Insets,
icon: &'static [u8],
title: U,
content: Child<T>,
}
impl<T, U> NotificationFrame<T, U>
where
T: Component,
U: AsRef<str>,
{
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<T, U> Component for NotificationFrame<T, U>
where
T: Component,
U: AsRef<str>,
{
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::Msg> {
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<T, U> crate::trace::Trace for NotificationFrame<T, U>
where
T: crate::trace::Trace,
U: crate::trace::Trace + AsRef<str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("NotificationFrame");
t.field("title", &self.title);
t.field("content", &self.content);
t.close();
}
}

View File

@ -3,5 +3,6 @@ pub mod mnemonic;
pub mod passphrase;
pub mod pin;
pub mod slip39;
pub mod word_count;
mod common;

View File

@ -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<Self::Msg> {
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();
}
}

View File

@ -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};

View File

@ -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<SelectWordMsg> for Obj {
}
}
impl TryFrom<SelectWordCountMsg> for Obj {
type Error = Error;
fn try_from(value: SelectWordCountMsg) -> Result<Self, Self::Error> {
match value {
SelectWordCountMsg::Selected(i) => i.try_into(),
}
}
}
impl<T, U> ComponentMsgObj for Dialog<T, U>
where
T: ComponentMsgObj,
@ -162,6 +173,16 @@ where
}
}
impl<T, U> ComponentMsgObj for NotificationFrame<T, U>
where
T: ComponentMsgObj,
U: AsRef<str>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
self.inner().msg_try_into_obj(msg)
}
}
impl<T, U> ComponentMsgObj for SwipePage<T, U>
where
T: Component + Paginate,
@ -512,11 +533,12 @@ fn new_show_modal(
) -> Result<Obj, Error> {
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)]

Binary file not shown.

View File

@ -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)
}

View File

@ -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() {

View File

@ -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`."""

View File

@ -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:

View File

@ -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:

View File

@ -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,

View File

@ -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:

View File

@ -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

View File

@ -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",