feat(core/rust/ui): recovery layouts

[no changelog]
pull/2496/head
Martin Milata 2 years ago
parent 5052594789
commit 5a9c2a1363

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

@ -27,6 +27,7 @@ static void _librust_qstrs(void) {
MP_QSTR_confirm_text; MP_QSTR_confirm_text;
MP_QSTR_confirm_total; MP_QSTR_confirm_total;
MP_QSTR_confirm_with_info; MP_QSTR_confirm_with_info;
MP_QSTR_confirm_recovery;
MP_QSTR_show_checklist; MP_QSTR_show_checklist;
MP_QSTR_show_error; MP_QSTR_show_error;
MP_QSTR_show_qr; MP_QSTR_show_qr;
@ -40,6 +41,9 @@ static void _librust_qstrs(void) {
MP_QSTR_request_bip39; MP_QSTR_request_bip39;
MP_QSTR_request_slip39; MP_QSTR_request_slip39;
MP_QSTR_select_word; 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_show_share_words;
MP_QSTR_attach_timer_fn; MP_QSTR_attach_timer_fn;
@ -85,4 +89,5 @@ static void _librust_qstrs(void) {
MP_QSTR_items; MP_QSTR_items;
MP_QSTR_active; MP_QSTR_active;
MP_QSTR_info_button; MP_QSTR_info_button;
MP_QSTR_time_ms;
} }

@ -9,7 +9,7 @@ use cstr_core::CStr;
use crate::micropython::{ffi, obj::Obj, qstr::Qstr}; use crate::micropython::{ffi, obj::Obj, qstr::Qstr};
#[allow(clippy::enum_variant_names)] // We mimic the Python exception classnames here. #[allow(clippy::enum_variant_names)] // We mimic the Python exception classnames here.
#[derive(Debug)] #[derive(Clone, Copy, Debug)]
pub enum Error { pub enum Error {
TypeError, TypeError,
OutOfRange, OutOfRange,

@ -12,6 +12,7 @@ pub mod paginated;
pub mod painter; pub mod painter;
pub mod placed; pub mod placed;
pub mod text; pub mod text;
pub mod timeout;
pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken}; pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken};
pub use border::Border; pub use border::Border;
@ -28,3 +29,4 @@ pub use text::{
formatted::FormattedText, formatted::FormattedText,
layout::{LineBreaking, PageBreaking, TextLayout}, layout::{LineBreaking, PageBreaking, TextLayout},
}; };
pub use timeout::{Timeout, TimeoutMsg};

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

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

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

@ -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( pub fn select_word(
words: [T; 3], words: [T; 3],
) -> CancelInfoConfirm< ) -> CancelInfoConfirm<

@ -117,6 +117,24 @@ where
self 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_PADDING: i32 = 2;
pub const ICON_AREA_HEIGHT: i32 = 60; pub const ICON_AREA_HEIGHT: i32 = 60;
pub const VALUE_SPACE: i32 = 5; pub const VALUE_SPACE: i32 = 5;

@ -1,8 +1,8 @@
use super::theme; use super::theme;
use crate::ui::{ use crate::ui::{
component::{Child, Component, Event, EventCtx}, component::{Child, Component, Event, EventCtx},
display, display::{self, Color, Font},
geometry::{Insets, Rect}, geometry::{Insets, Offset, Rect},
}; };
pub struct Frame<T, U> { pub struct Frame<T, U> {
@ -91,3 +91,99 @@ where
t.close(); 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();
}
}

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

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

@ -13,8 +13,8 @@ pub use button::{
Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelConfirmMsg, Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelConfirmMsg,
CancelInfoConfirmMsg, SelectWordMsg, CancelInfoConfirmMsg, SelectWordMsg,
}; };
pub use dialog::{Dialog, DialogLayout, DialogMsg, IconDialog}; pub use dialog::{Dialog, DialogMsg, IconDialog};
pub use frame::Frame; pub use frame::{Frame, NotificationFrame};
pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg}; pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use keyboard::{ pub use keyboard::{
bip39::Bip39Input, bip39::Bip39Input,
@ -22,6 +22,7 @@ pub use keyboard::{
passphrase::{PassphraseKeyboard, PassphraseKeyboardMsg}, passphrase::{PassphraseKeyboard, PassphraseKeyboardMsg},
pin::{PinKeyboard, PinKeyboardMsg}, pin::{PinKeyboard, PinKeyboardMsg},
slip39::Slip39Input, slip39::Slip39Input,
word_count::{SelectWordCount, SelectWordCountMsg},
}; };
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet}; pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use number_input::{NumberInputDialog, NumberInputDialogMsg}; pub use number_input::{NumberInputDialog, NumberInputDialogMsg};

@ -1,5 +1,4 @@
use core::{cmp::Ordering, convert::TryInto, ops::Deref}; use core::{cmp::Ordering, convert::TryInto, ops::Deref};
use cstr_core::cstr;
use crate::{ use crate::{
error::Error, error::Error,
@ -18,12 +17,13 @@ use crate::{
paginated::{PageMsg, Paginate}, paginated::{PageMsg, Paginate},
painter, painter,
text::paragraphs::{Checklist, Paragraphs}, text::paragraphs::{Checklist, Paragraphs},
Border, Component, Border, Component, Timeout, TimeoutMsg,
}, },
geometry, geometry,
layout::{ layout::{
obj::{ComponentMsgObj, LayoutObj}, obj::{ComponentMsgObj, LayoutObj},
result::{CANCELLED, CONFIRMED, INFO}, result::{CANCELLED, CONFIRMED, INFO},
util::iter_into_array,
}, },
}, },
}; };
@ -32,9 +32,10 @@ use super::{
component::{ component::{
Bip39Input, Button, ButtonMsg, ButtonStyleSheet, CancelConfirmMsg, CancelInfoConfirmMsg, Bip39Input, Button, ButtonMsg, ButtonStyleSheet, CancelConfirmMsg, CancelInfoConfirmMsg,
Dialog, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, IconDialog, MnemonicInput, Dialog, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, IconDialog, MnemonicInput,
MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputDialog, NumberInputDialogMsg, MnemonicKeyboard, MnemonicKeyboardMsg, NotificationFrame, NumberInputDialog,
PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, SelectWordMsg, NumberInputDialogMsg, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard,
Slip39Input, SwipeHoldPage, SwipePage, PinKeyboardMsg, SelectWordCount, SelectWordCountMsg, SelectWordMsg, Slip39Input,
SwipeHoldPage, SwipePage,
}, },
theme, 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> impl<T, U> ComponentMsgObj for Dialog<T, U>
where where
T: ComponentMsgObj, 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> impl<T, U> ComponentMsgObj for SwipePage<T, U>
where where
T: Component + Paginate, T: Component + Paginate,
@ -512,11 +533,12 @@ fn new_show_modal(
) -> Result<Obj, Error> { ) -> Result<Obj, Error> {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_description, StrBuffer::empty())?; 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 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 { let obj = match (allow_cancel, time_ms) {
LayoutObj::new( (true, 0) => LayoutObj::new(
IconDialog::new( IconDialog::new(
icon, icon,
title, title,
@ -528,19 +550,29 @@ fn new_show_modal(
) )
.with_description(description), .with_description(description),
)? )?
.into() .into(),
} else { (false, 0) => LayoutObj::new(
LayoutObj::new(
IconDialog::new( IconDialog::new(
icon, icon,
title, 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) (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), .with_description(description),
)? )?
.into() .into(),
}; };
Ok(obj) 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 title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?; let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?; let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?;
let words: [StrBuffer; 3] = iter_into_array(words_iterable)?;
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 paragraphs = Paragraphs::new().add(theme::TEXT_NORMAL, description); let paragraphs = Paragraphs::new().add(theme::TEXT_NORMAL, description);
let buttons = Button::select_word(words); 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) } 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] #[no_mangle]
pub static mp_module_trezorui2: Module = obj_module! { pub static mp_module_trezorui2: Module = obj_module! {
Qstr::MP_QSTR___name__ => Qstr::MP_QSTR_trezorui2.to_obj(), 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( /// def show_error(
/// *, /// *,
/// title: str, /// title: str,
/// button: str, /// button: str = "CONTINUE",
/// description: str = "", /// description: str = "",
/// allow_cancel: bool = False, /// allow_cancel: bool = False,
/// time_ms: int = 0,
/// ) -> object: /// ) -> object:
/// """Error modal.""" /// """Error modal."""
Qstr::MP_QSTR_show_error => obj_fn_kw!(0, new_show_error).as_obj(), 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( /// def show_warning(
/// *, /// *,
/// title: str, /// title: str,
/// button: str, /// button: str = "CONTINUE",
/// description: str = "", /// description: str = "",
/// allow_cancel: bool = False, /// allow_cancel: bool = False,
/// time_ms: int = 0,
/// ) -> object: /// ) -> object:
/// """Warning modal.""" /// """Warning modal."""
Qstr::MP_QSTR_show_warning => obj_fn_kw!(0, new_show_warning).as_obj(), 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( /// def show_success(
/// *, /// *,
/// title: str, /// title: str,
/// button: str, /// button: str = "CONTINUE",
/// description: str = "", /// description: str = "",
/// allow_cancel: bool = False, /// allow_cancel: bool = False,
/// time_ms: int = 0,
/// ) -> object: /// ) -> object:
/// """Success modal.""" /// """Success modal."""
Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(), 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( /// def show_info(
/// *, /// *,
/// title: str, /// title: str,
/// button: str, /// button: str = "CONTINUE",
/// description: str = "", /// description: str = "",
/// allow_cancel: bool = False, /// allow_cancel: bool = False,
/// time_ms: int = 0,
/// ) -> object: /// ) -> object:
/// """Info modal.""" /// """Info modal."""
Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(), 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 /// """Checklist of backup steps. Active index is highlighted, previous items have check
/// mark nex to them.""" /// mark nex to them."""
Qstr::MP_QSTR_show_checklist => obj_fn_kw!(0, new_show_checklist).as_obj(), 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)] #[cfg(test)]

@ -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_BACK: &[u8] = include_res!("model_tt/res/back.toif");
pub const ICON_CLICK: &[u8] = include_res!("model_tt/res/click.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_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_CURRENT: &[u8] = include_res!("model_tt/res/current.toif");
pub const ICON_LIST_CHECK: &[u8] = include_res!("model_tt/res/check.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 { pub fn button_default() -> ButtonStyleSheet {
ButtonStyleSheet { ButtonStyleSheet {
normal: &ButtonStyle { normal: &ButtonStyle {
@ -411,6 +428,8 @@ pub const KEYBOARD_SPACING: i32 = 8;
pub const BUTTON_HEIGHT: i32 = 38; pub const BUTTON_HEIGHT: i32 = 38;
pub const BUTTON_SPACING: i32 = 6; pub const BUTTON_SPACING: i32 = 6;
pub const CHECKLIST_SPACING: i32 = 10; pub const CHECKLIST_SPACING: i32 = 10;
pub const RECOVERY_SPACING: i32 = 18;
/// Standard button height in pixels. /// Standard button height in pixels.
pub const fn button_rows(count: usize) -> i32 { pub const fn button_rows(count: usize) -> i32 {
let count = count as i32; let count = count as i32;
@ -439,3 +458,7 @@ pub const fn borders() -> Insets {
pub const fn borders_scroll() -> Insets { pub const fn borders_scroll() -> Insets {
Insets::new(13, 5, 14, 10) Insets::new(13, 5, 14, 10)
} }
pub const fn borders_notification() -> Insets {
Insets::new(6, 10, 14, 10)
}

@ -34,7 +34,6 @@ pub fn u32_to_str(num: u32, buffer: &mut [u8]) -> Option<&str> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::str;
#[test] #[test]
fn u32_to_str_valid() { fn u32_to_str_valid() {

@ -159,9 +159,10 @@ def confirm_modify_fee(
def show_error( def show_error(
*, *,
title: str, title: str,
button: str, button: str = "CONTINUE",
description: str = "", description: str = "",
allow_cancel: bool = False, allow_cancel: bool = False,
time_ms: int = 0,
) -> object: ) -> object:
"""Error modal.""" """Error modal."""
@ -170,9 +171,10 @@ def show_error(
def show_warning( def show_warning(
*, *,
title: str, title: str,
button: str, button: str = "CONTINUE",
description: str = "", description: str = "",
allow_cancel: bool = False, allow_cancel: bool = False,
time_ms: int = 0,
) -> object: ) -> object:
"""Warning modal.""" """Warning modal."""
@ -181,9 +183,10 @@ def show_warning(
def show_success( def show_success(
*, *,
title: str, title: str,
button: str, button: str = "CONTINUE",
description: str = "", description: str = "",
allow_cancel: bool = False, allow_cancel: bool = False,
time_ms: int = 0,
) -> object: ) -> object:
"""Success modal.""" """Success modal."""
@ -192,9 +195,10 @@ def show_success(
def show_info( def show_info(
*, *,
title: str, title: str,
button: str, button: str = "CONTINUE",
description: str = "", description: str = "",
allow_cancel: bool = False, allow_cancel: bool = False,
time_ms: int = 0,
) -> object: ) -> object:
"""Info modal.""" """Info modal."""
@ -308,3 +312,39 @@ def show_checklist(
) -> object: ) -> object:
"""Checklist of backup steps. Active index is highlighted, previous items have check """Checklist of backup steps. Active index is highlighted, previous items have check
mark nex to them.""" 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`."""

@ -148,11 +148,13 @@ async def homescreen_dialog(
info_func: Callable | None = None, info_func: Callable | None = None,
) -> None: ) -> None:
while True: 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 # go forward in the recovery process
break break
# user has chosen to abort, confirm the choice # user has chosen to abort, confirm the choice
dry_run = storage.recovery.is_dry_run()
try: try:
await confirm_abort(ctx, dry_run) await confirm_abort(ctx, dry_run)
except wire.ActionCancelled: except wire.ActionCancelled:

@ -1,13 +1,12 @@
import storage.recovery
from trezor import ui from trezor import ui
class RecoveryHomescreen(ui.Component): 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__() super().__init__()
self.text = text self.text = text
self.subtext = subtext self.subtext = subtext
self.dry_run = storage.recovery.is_dry_run() self.dry_run = dry_run
def on_render(self) -> None: def on_render(self) -> None:
if not self.repaint: if not self.repaint:

@ -112,8 +112,9 @@ async def continue_recovery(
text: str, text: str,
subtext: str | None, subtext: str | None,
info_func: Callable | None, info_func: Callable | None,
dry_run: bool,
) -> bool: ) -> bool:
homepage = RecoveryHomescreen(text, subtext) homepage = RecoveryHomescreen(dry_run, text, subtext)
if info_func is not None: if info_func is not None:
content = InfoConfirm( content = InfoConfirm(
homepage, homepage,

@ -277,7 +277,6 @@ async def confirm_path_warning(
trezorui2.show_warning( trezorui2.show_warning(
title="Unknown path", title="Unknown path",
description=path, description=path,
button="CONTINUE",
) )
), ),
"path_warning", "path_warning",
@ -758,13 +757,11 @@ async def confirm_metadata(
layout = trezorui2.show_warning( layout = trezorui2.show_warning(
title="Unusually high fee", title="Unusually high fee",
description=param or "", description=param or "",
button="CONTINUE",
) )
elif br_type == "change_count_over_threshold": elif br_type == "change_count_over_threshold":
layout = trezorui2.show_warning( layout = trezorui2.show_warning(
title="A lot of change-outputs", title="A lot of change-outputs",
description=f"{param} outputs" if param is not None else "", description=f"{param} outputs" if param is not None else "",
button="CONTINUE",
) )
else: else:
if param is not None: if param is not None:
@ -929,7 +926,16 @@ async def show_popup(
description_param: str = "", description_param: str = "",
timeout_ms: int = 3000, timeout_ms: int = 3000,
) -> None: ) -> 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: def draw_simple_text(title: str, description: str = "") -> None:

@ -1,9 +1,12 @@
from typing import TYPE_CHECKING 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 import trezorui2
from ..common import button_request, interact
from . import _RustLayout from . import _RustLayout
if TYPE_CHECKING: if TYPE_CHECKING:
@ -12,8 +15,26 @@ if TYPE_CHECKING:
pass 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: 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( async def request_word(
@ -42,13 +63,54 @@ async def show_remaining_shares(
shares_remaining: list[int], shares_remaining: list[int],
group_threshold: int, group_threshold: int,
) -> None: ) -> 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( async def show_group_share_success(
ctx: wire.GenericContext, share_index: int, group_index: int ctx: wire.GenericContext, share_index: int, group_index: int
) -> None: ) -> 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( async def continue_recovery(
@ -57,5 +119,41 @@ async def continue_recovery(
text: str, text: str,
subtext: str | None, subtext: str | None,
info_func: Callable | None, info_func: Callable | None,
dry_run: bool,
) -> 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

@ -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[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[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_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_dry_run": "98f8d8961d364bd839ece099ef67576cdcd27529267619cadf963dec6cc7144d",
"TTui2_reset_recovery-test_recovery_bip39_dryrun.py::test_invalid_seed_core": "fb6d1ba5b6f8b0c28d7c114a1cad2170029800a50cea1dce1b222b56c53d4169", "TTui2_reset_recovery-test_recovery_bip39_dryrun.py::test_invalid_seed_core": "98f8d8961d364bd839ece099ef67576cdcd27529267619cadf963dec6cc7144d",
"TTui2_reset_recovery-test_recovery_bip39_dryrun.py::test_seed_mismatch": "fb6d1ba5b6f8b0c28d7c114a1cad2170029800a50cea1dce1b222b56c53d4169", "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_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_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_nopin_nopassphrase": "3919d9404e9f9a4880bd084edbfa02fbb04641008e04b83458633691e69bf239",
"TTui2_reset_recovery-test_recovery_bip39_t2.py::test_tt_pin_passphrase": "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_abort": "7717c45923e9f73efd1201a728e659db2cf3631c7d244418b77fc04301875a10",
"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_extra_share_entered": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "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": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_group_threshold_reached": "101e7e7eee51ed188985046f5e01fbc4600ba263db9015217b916b5a3a6ce65c",
"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_noabort": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_noabort": "3bc2a8d4c8371e8fb799e22d21a9a497f13939208b38d2a85aead1b875952aa7",
"TTui2_reset_recovery-test_recovery_slip39_advanced.py::test_same_share": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "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": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "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": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "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": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "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": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "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": "7a5048ee96f76bb2e2a6d64fd89dfc22eb6fe792eaa769058249d0f552ee59d3", "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": "7a5048ee96f76bb2e2a6d64fd89dfc22eb6fe792eaa769058249d0f552ee59d3", "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": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "TTui2_reset_recovery-test_recovery_slip39_basic.py::test_1of1": "04fd1206841ecf3debd8c29a86a90f611714291e0dbd762e20d3caf4fb851268",
"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_abort": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "TTui2_reset_recovery-test_recovery_slip39_basic.py::test_abort": "7717c45923e9f73efd1201a728e659db2cf3631c7d244418b77fc04301875a10",
"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_ask_word_number": "e53306364b3a4cc2d23da5adeafa6f02fd946dcf042c6c77efd1ce221a319ea8", "TTui2_reset_recovery-test_recovery_slip39_basic.py::test_ask_word_number": "0ed3d06281d5b16c9258f10c936f07d1664d0643f7f957edd79566fd605926e7",
"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_noabort": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "TTui2_reset_recovery-test_recovery_slip39_basic.py::test_noabort": "55fa3f81be735f0dffa9cc3469885e7cae3c4721d8030d1b9da32a2b3e15784f",
"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_recover_with_pin_passphrase": "ff0120b13a8ec8ecfe3a70d3dce62a9eaafa116632284d85983e7d1f040d6d4a", "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": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "TTui2_reset_recovery-test_recovery_slip39_basic.py::test_same_share": "c94d6cfd3a3617de12b65be94eec59b0dfdeb4d5c91c04c04bd9db60632761d0",
"TTui2_reset_recovery-test_recovery_slip39_basic.py::test_secret[shares0-491b795b80fc21ccdf466c0fbc98c8fc]": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "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": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "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]": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "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]": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "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]": "4b94af756dc9288eca587760a32e66abcac622da498d6a9b5bfb5f965f295d2f", "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": "7a5048ee96f76bb2e2a6d64fd89dfc22eb6fe792eaa769058249d0f552ee59d3", "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": "7a5048ee96f76bb2e2a6d64fd89dfc22eb6fe792eaa769058249d0f552ee59d3", "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.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_Advanced-bac-f67baa1c": "bd1f14226b2b3b778dc146ab8e4d0b0535657649330ca5dd3efa4c00a70ecb24",
"TTui2_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Basic-backup-6348e7fe": "9f0568a1782a4d392cf64c2f4478b6fadc6ffcaea8a3c706c04ba798d2fa5195", "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_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_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_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": "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_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_device_pin": "6818c19bba26ef8acecd87cadbe5bf678449a519e49cc365311708d91abe92be",
"TTui2_reset_recovery-test_reset_bip39_t2.py::test_reset_failed_check": "fad48c5d40c5df33cbf4abbb7695f6c22b490ab3f118cfdf3b64b2bc2936920f", "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_bip39.py::test_reset_recovery": "447d34b14feaffdca56fb48e2490c4a47a875edc7d7ad1952feb7d9df9934705",
"TTui2_reset_recovery-test_reset_recovery_slip39_advanced.py::test_reset_recovery": "6dc1c0d4106d8a789a7eae973a3b768cb1a5383fdf42866e51516e67fd4a36ca", "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": "78cd86e1e473e5cdd541840a657723f663d9294fc4fdc74af97274ea1b3939b8", "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_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": "e68ba714482d7d6239a4e4d43d890a3de340230c5618fc820b1c24027c87b72d",
"TTui2_reset_recovery-test_reset_slip39_basic.py::test_reset_device_slip39_basic_256": "146b4d7880fd9bd325da46d675ec4ee4f88e27916eba3198d911c3c4c6a5e29f", "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_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_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_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_mismatch": "a8e165eb64558ee3f38adb334a123d20ae40088515d559e06e3dcc6ab960f865",
"TTui2_test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "25eac0cb6ea45c0cb9cfcad3b4ac3ec33af9212a7b812370c8132ef9f14c7700", "TTui2_test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "2b681988285d472e128edcb972cff8784ed34c9cba2f2a02e70a243d6561f86a",
"TTui2_test_msg_changepin_t2.py::test_change_failed": "e207e2c62f6930e9e112d7a1a31b9a66c14580df8aac82ea40e2f243d987e878", "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_invalid_current": "5e04bc7ab716549d8aa70087cac37c8e1beafaad9929713a631e11845102d4e9",
"TTui2_test_msg_changepin_t2.py::test_change_pin": "2b891a989548802893f1b6a486e9751704a460ce4f59b65b39315318e11171f2", "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_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_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_1": "eeb5afb34b4bbf42b8c635fdd34bae5c1e3693facb16e6d64e629746612a2c3f",
"TTui2_test_msg_loaddevice.py::test_load_device_2": "a95020926a62b4078cb0034f6e7a772e49fc42121c9197b534437e26c306a994", "TTui2_test_msg_loaddevice.py::test_load_device_2": "a95020926a62b4078cb0034f6e7a772e49fc42121c9197b534437e26c306a994",

Loading…
Cancel
Save