feat(core/bolt): allow back in multishare setup
What changed, and why it matters
This commit is a user-interface feature change for Trezor hardware wallets. It adds an on-screen 'back' arrow during multi-share backup setup so users can return to a previous step, and moves the 'More info' help into a menu button. There is no indication of a security bug or vulnerability fix in the code or commit message.
No security action required; treat as a normal feature/UI commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch modifies the Bolt UI layout used on Trezor Model T/Safe devices. It introduces a BackConfirm button bar, replaces the info icon in the number-input dialog with an up-arrow back button, adds a header menu button for ‘More info’, and wires the new BACK/INFO flow messages through the MicroPython layout code. Tests are updated to exercise back navigation in SLIP39 basic and advanced backup flows. No cryptographic, authorization, or memory-safety changes are present.
Changed components
core/embed/rust/src/ui/layout_bolt/component/button.rscore/embed/rust/src/ui/layout_bolt/component/frame.rscore/embed/rust/src/ui/layout_bolt/component/number_input.rscore/embed/rust/src/ui/layout_bolt/component_msg_obj.rscore/embed/rust/src/ui/layout_bolt/ui_firmware.rscore/src/trezor/ui/layouts/bolt/reset.pytests/device_tests/test_msg_backup_device.pytests/input_flows.pyInspect captured patch +348 / −51
### core/embed/rust/src/ui/layout_bolt/component/button.rs
@@ -396,6 +396,19 @@ impl Button {
))
}
+ /// Create a button bar with an up-arrow back button on the left and a
+ /// confirm button on the right. The back button is only shown when
+ /// `show_back` is set, otherwise the confirm button takes the full width.
+ pub fn back_confirm(right: Button, show_back: bool) -> FixedHeightBar<BackConfirm> {
+ theme::button_bar(BackConfirm {
+ left: Button::with_icon(theme::ICON_UP)
+ .map(|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| BackConfirmMsg::Back)),
+ right: right
+ .map(|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| BackConfirmMsg::Confirmed)),
+ show_back,
+ })
+ }
+
pub fn cancel_confirm_text(
left: Option<TString<'static>>,
right: Option<TString<'static>>,
@@ -486,6 +499,63 @@ pub enum CancelConfirmMsg {
Confirmed,
}
+#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))]
+pub enum BackConfirmMsg {
+ Back,
+ Confirmed,
+}
+
+/// Button bar with an optional up-arrow back button on the left and a
+/// confirm button on the right. The confirm button takes the full width when
+/// the back button is not shown.
+pub struct BackConfirm {
+ left: MsgMap<Button, BackConfirmHandlerFn>,
+ right: MsgMap<Button, BackConfirmHandlerFn>,
+ show_back: bool,
+}
+
+type BackConfirmHandlerFn = fn(ButtonMsg) -> Option<BackConfirmMsg>;
+
+impl Component for BackConfirm {
+ type Msg = BackConfirmMsg;
+
+ fn place(&mut self, bounds: Rect) -> Rect {
+ let (left_area, right_area) = if self.show_back {
+ let (left, rest) = bounds.split_left(theme::BUTTON_WIDTH);
+ (left, rest.inset(Insets::left(theme::BUTTON_SPACING)))
+ } else {
+ (Rect::zero(), bounds)
+ };
+ self.left.place(left_area);
+ self.right.place(right_area);
+ bounds
+ }
+
+ fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
+ self.left
+ .event(ctx, event)
+ .or_else(|| self.right.event(ctx, event))
+ }
+
+ fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
+ if self.show_back {
+ self.left.render(target);
+ }
+ self.right.render(target);
+ }
+}
+
+#[cfg(feature = "ui_debug")]
+impl crate::trace::Trace for BackConfirm {
+ fn trace(&self, t: &mut dyn crate::trace::Tracer) {
+ t.component("BackConfirm");
+ if self.show_back {
+ t.child("left_button", &self.left);
+ }
+ t.child("right_button", &self.right);
+ }
+}
+
type CancelInfoConfirm<F0, F1, F2> =
FixedHeightBar<Split<MsgMap<Button, F0>, Split<MsgMap<Button, F1>, MsgMap<Button, F2>>>>;
### core/embed/rust/src/ui/layout_bolt/component/frame.rs
@@ -92,6 +92,28 @@ where
self.with_button(theme::ICON_CORNER_INFO, CancelInfoConfirmMsg::Info)
}
+ pub fn with_menu_button(self) -> Self {
+ self.with_menu_button_msg(CancelInfoConfirmMsg::Info)
+ }
+
+ fn with_menu_button_msg(mut self, msg: CancelInfoConfirmMsg) -> Self {
+ // The touch area is intentionally not expanded downwards so that the
+ // button does not overlap interactive content right below it (e.g.
+ // the number input's increment button).
+ let touch_area = Insets {
+ left: self.border.left * 4,
+ bottom: 0,
+ ..self.border
+ };
+ self.button = Some(Child::new(
+ Button::with_icon(theme::ICON_CORNER_MENU)
+ .with_expanded_touch_area(touch_area)
+ .styled(theme::button_moreinfo()),
+ ));
+ self.button_msg = msg;
+ self
+ }
+
pub fn inner(&self) -> &T {
self.content.inner()
}
### core/embed/rust/src/ui/layout_bolt/component/mod.rs
@@ -40,8 +40,8 @@ mod welcome_screen;
#[cfg(all(feature = "micropython", feature = "translations"))]
pub use address_details::AddressDetails;
pub use button::{
- Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelConfirmMsg,
- CancelInfoConfirmMsg, IconText, SelectWordMsg,
+ BackConfirmMsg, Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet,
+ CancelConfirmMsg, CancelInfoConfirmMsg, IconText, SelectWordMsg,
};
#[cfg(feature = "translations")]
pub use coinjoin_progress::CoinJoinProgress;
### core/embed/rust/src/ui/layout_bolt/component/number_input.rs
@@ -12,7 +12,7 @@ use crate::ui::shape::{self, Renderer};
#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))]
pub enum NumberInputDialogMsg {
Selected,
- InfoRequested,
+ Back,
}
pub struct NumberInputDialog<F>
@@ -24,7 +24,7 @@ where
input: Child<NumberInput>,
paragraphs: Child<Paragraphs<Paragraph<'static>>>,
paragraphs_pad: Pad,
- info_button: Child<Button>,
+ back_button: Child<Button>,
confirm_button: Child<Button>,
}
@@ -40,7 +40,7 @@ where
input: NumberInput::new(min, max, init_value).into_child(),
paragraphs: Paragraphs::new(Paragraph::new(&theme::TEXT_NORMAL, text)).into_child(),
paragraphs_pad: Pad::with_background(theme::BG),
- info_button: Button::with_icon(theme::ICON_CORNER_INFO).into_child(),
+ back_button: Button::with_icon(theme::ICON_UP).into_child(),
confirm_button: Button::with_text(TR::buttons__continue.into())
.styled(theme::button_confirm())
.into_child(),
@@ -85,7 +85,7 @@ where
self.input.place(input_area);
self.paragraphs.place(content_area);
self.paragraphs_pad.place(content_area);
- self.info_button.place(grid.row_col(0, 0));
+ self.back_button.place(grid.row_col(0, 0));
self.confirm_button.place(grid.cells(GridCellSpan {
from: (0, 1),
to: (0, 2),
@@ -98,8 +98,8 @@ where
self.update_text(ctx, i);
}
self.paragraphs.event(ctx, event);
- if let Some(ButtonMsg::Clicked) = self.info_button.event(ctx, event) {
- return Some(Self::Msg::InfoRequested);
+ if let Some(ButtonMsg::Clicked) = self.back_button.event(ctx, event) {
+ return Some(Self::Msg::Back);
}
if let Some(ButtonMsg::Clicked) = self.confirm_button.event(ctx, event) {
return Some(Self::Msg::Selected);
@@ -111,7 +111,7 @@ where
self.input.render(target);
self.paragraphs_pad.render(target);
self.paragraphs.render(target);
- self.info_button.render(target);
+ self.back_button.render(target);
self.confirm_button.render(target);
}
}
@@ -125,7 +125,7 @@ where
t.component("NumberInputDialog");
t.child("input", &self.input);
t.child("paragraphs", &self.paragraphs);
- t.child("info_button", &self.info_button);
+ t.child("back_button", &self.back_button);
t.child("confirm_button", &self.confirm_button);
}
}
### core/embed/rust/src/ui/layout_bolt/component_msg_obj.rs
@@ -1,12 +1,12 @@
use core::convert::TryInto;
use super::component::{
- AddressDetails, ButtonPage, CancelConfirmMsg, CancelInfoConfirmMsg, CoinJoinProgress, Dialog,
- DialogMsg, FidoConfirm, FidoMsg, Frame, FrameMsg, Homescreen, HomescreenMsg, IconDialog,
- Lockscreen, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputDialog,
- NumberInputDialogMsg, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg,
- Progress, SelectMenu, SelectMenuMsg, SelectWordCountMsg, SelectWordMsg, SetBrightnessDialog,
- SimplePage,
+ AddressDetails, BackConfirmMsg, ButtonPage, CancelConfirmMsg, CancelInfoConfirmMsg,
+ CoinJoinProgress, Dialog, DialogMsg, FidoConfirm, FidoMsg, Frame, FrameMsg, Homescreen,
+ HomescreenMsg, IconDialog, Lockscreen, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg,
+ NumberInputDialog, NumberInputDialogMsg, PassphraseKeyboard, PassphraseKeyboardMsg,
+ PinKeyboard, PinKeyboardMsg, Progress, SelectMenu, SelectMenuMsg, SelectWordCountMsg,
+ SelectWordMsg, SetBrightnessDialog, SimplePage,
};
use crate::micropython::{Error, Obj};
use crate::strutil::TString;
@@ -15,9 +15,9 @@ use crate::ui::component::placed::GridPlaced;
use crate::ui::component::text::paragraphs::{ParagraphSource, Paragraphs};
#[cfg(not(feature = "clippy"))]
use crate::ui::component::Timeout;
-use crate::ui::component::{Component, FormattedText, Never, Paginate};
+use crate::ui::component::{Component, FlowMsg, FormattedText, MsgMap, Never, Paginate};
use crate::ui::layout::obj::ComponentMsgObj;
-use crate::ui::layout::result::{CANCELLED, CONFIRMED, INFO};
+use crate::ui::layout::result::{BACK, CANCELLED, CONFIRMED, INFO};
impl TryFrom<CancelConfirmMsg> for Obj {
type Error = Error;
@@ -30,6 +30,17 @@ impl TryFrom<CancelConfirmMsg> for Obj {
}
}
+impl TryFrom<BackConfirmMsg> for Obj {
+ type Error = Error;
+
+ fn try_from(value: BackConfirmMsg) -> Result<Self, Self::Error> {
+ match value {
+ BackConfirmMsg::Back => Ok(BACK.as_obj()),
+ BackConfirmMsg::Confirmed => Ok(CONFIRMED.as_obj()),
+ }
+ }
+}
+
impl TryFrom<CancelInfoConfirmMsg> for Obj {
type Error = Error;
@@ -190,15 +201,24 @@ where
}
}
-impl<F> ComponentMsgObj for NumberInputDialog<F>
+/// Layout returned by `request_number`: the number input screen with the
+/// menu button in the header and a back button (up arrow) in the button bar.
+pub type RequestNumberScreen<F> =
+ MsgMap<Frame<NumberInputDialog<F>>, fn(FrameMsg<NumberInputDialogMsg>) -> Option<FlowMsg>>;
+
+impl<F> ComponentMsgObj for RequestNumberScreen<F>
where
F: Fn(u32) -> TString<'static>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
- let value = self.value();
+ // Return not only the result, but also the currently displayed number,
+ // so that Python can e.g. show the corresponding "more info" text.
+ let value = self.inner().inner().value();
match msg {
- NumberInputDialogMsg::Selected => Ok((CONFIRMED.as_obj(), value).try_into()?),
- NumberInputDialogMsg::InfoRequested => Ok((INFO.as_obj(), value).try_into()?),
+ FlowMsg::Confirmed => Ok((CONFIRMED.as_obj(), value).try_into()?),
+ FlowMsg::Back => Ok((BACK.as_obj(), value).try_into()?),
+ FlowMsg::Info => Ok((INFO.as_obj(), value).try_into()?),
+ msg => msg.try_into(),
}
}
}
### core/embed/rust/src/ui/layout_bolt/theme/mod.rs
@@ -56,6 +56,7 @@ include_icon!(ICON_CLICK, "layout_bolt/res/finger24.toif");
include_icon!(ICON_CORNER_CANCEL, "layout_bolt/res/x32.toif");
include_icon!(ICON_CORNER_INFO, "layout_bolt/res/info32.toif");
+include_icon!(ICON_CORNER_MENU, "layout_bolt/res/menu32.toif");
// Checklist symbols.
include_icon!(ICON_LIST_CURRENT, "layout_bolt/res/arrow-right16.toif");
### core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
@@ -4,10 +4,10 @@ use core::cmp::Ordering;
use super::component::FidoConfirm;
use super::component::{
check_homescreen_format, AddressDetails, Bip39Input, Button, ButtonMsg, ButtonPage,
- ButtonStyleSheet, CancelConfirmMsg, CoinJoinProgress, Dialog, Frame, Homescreen, IconDialog,
- Lockscreen, MnemonicKeyboard, NumberInputDialog, PassphraseKeyboard, PinKeyboard, Progress,
- SelectMenu, SelectWordCount, SelectWordCountLayout, SetBrightnessDialog, ShareWords,
- SimplePage, Slip39Input,
+ ButtonStyleSheet, CancelConfirmMsg, CancelInfoConfirmMsg, CoinJoinProgress, Dialog, Frame,
+ FrameMsg, Homescreen, IconDialog, Lockscreen, MnemonicKeyboard, NumberInputDialog,
+ NumberInputDialogMsg, PassphraseKeyboard, PinKeyboard, Progress, SelectMenu, SelectWordCount,
+ SelectWordCountLayout, SetBrightnessDialog, ShareWords, SimplePage, Slip39Input,
};
use super::{fonts, theme, UIBolt};
use crate::io::BinaryData;
@@ -26,7 +26,7 @@ use crate::ui::component::text::paragraphs::{
};
use crate::ui::component::text::TextStyle;
use crate::ui::component::{
- Border, ComponentExt, Empty, FormattedText, Jpeg, Label, Never, Timeout,
+ Border, ComponentExt, Empty, FlowMsg, FormattedText, Jpeg, Label, Never, Timeout,
};
use crate::ui::layout::obj::{LayoutMaybeTrace, LayoutObj, RootComponent};
use crate::ui::layout::util::{ConfirmValueParams, PropsList, RecoveryType};
@@ -667,11 +667,28 @@ impl FirmwareUI for UIBolt {
more_info_callback: Option<impl Fn(u32) -> TString<'static> + 'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
debug_assert!(more_info_callback.is_some());
- let layout = RootComponent::new(Frame::left_aligned(
- theme::label_title(),
- title,
- NumberInputDialog::new(min_count, max_count, count, more_info_callback.unwrap())?,
- ));
+ // The "more info" content is driven from Python: the menu button in
+ // the header emits `FlowMsg::Info` and the layout returns the
+ // currently displayed number along with the result (see
+ // `ComponentMsgObj for RequestNumberScreen`).
+ // The up-arrow button on the left goes back to the previous screen;
+ // going back is the only way out of the prompt -- interrupting the
+ // flow is not possible.
+ let map_fn: fn(FrameMsg<NumberInputDialogMsg>) -> Option<FlowMsg> = |msg| match msg {
+ FrameMsg::Content(NumberInputDialogMsg::Selected) => Some(FlowMsg::Confirmed),
+ FrameMsg::Content(NumberInputDialogMsg::Back) => Some(FlowMsg::Back),
+ FrameMsg::Button(CancelInfoConfirmMsg::Info) => Some(FlowMsg::Info),
+ _ => None,
+ };
+ let layout = RootComponent::new(
+ Frame::left_aligned(
+ theme::label_title(),
+ title,
+ NumberInputDialog::new(min_count, max_count, count, more_info_callback.unwrap())?,
+ )
+ .with_menu_button()
+ .map(map_fn),
+ );
Ok(layout)
}
@@ -819,7 +836,7 @@ impl FirmwareUI for UIBolt {
button: TString<'static>,
active: usize,
items: [TString<'static>; MAX_CHECKLIST_ITEMS],
- _back_button: bool,
+ back_button: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
let mut paragraphs = ParagraphVecLong::new();
for (i, item) in items.into_iter().enumerate() {
@@ -846,9 +863,7 @@ impl FirmwareUI for UIBolt {
.with_check_width(theme::CHECKLIST_CHECK_WIDTH)
.with_current_offset(theme::CHECKLIST_CURRENT_OFFSET)
.with_done_offset(theme::CHECKLIST_DONE_OFFSET),
- theme::button_bar(Button::with_text(button).map(|msg| {
- (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
- })),
+ Button::back_confirm(Button::with_text(button), back_button),
),
));
Ok(layout)
### core/src/trezor/ui/layouts/bolt/reset.py
@@ -3,6 +3,7 @@
import trezorui_api
from trezor import TR
from trezor.enums import ButtonRequestType
+from trezor.wire import ActionCancelled
from ..common import interact, raise_if_not_confirmed
@@ -98,6 +99,7 @@ async def slip39_show_checklist(
button=TR.buttons__continue,
active=step,
items=items,
+ back_button=back_button,
) as layout:
return await interact(layout, "slip39_checklist", ButtonRequestType.ResetDevice)
@@ -110,7 +112,9 @@ async def _prompt_number(
min_count: int,
max_count: int,
br_name: str,
-) -> int:
+) -> int | trezorui_api.UiResult:
+ from trezor.ui.layouts.menu import Menu, leaf_from_layout, show_menu
+
with trezorui_api.request_number(
title=title,
count=count,
@@ -126,23 +130,36 @@ async def _prompt_number(
ButtonRequestType.ResetDevice,
raise_on_cancel=None,
)
- if __debug__:
- if not isinstance(result, tuple):
- # DebugLink currently can't send number of shares and it doesn't
- # change the counter either so just use the initial value.
- result = result, count
+
+ if result is trezorui_api.CANCELLED:
+ # not reachable from the UI, only via debuglink
+ raise ActionCancelled
+
+ if __debug__ and not isinstance(result, tuple):
+ # sent by debuglink. debuglink does not change the number of
+ # shares anyway so use the initial one
+ result = (result, count)
status, value = result
- if status == CONFIRMED:
+ if status is CONFIRMED:
assert isinstance(value, int)
return value
- with trezorui_api.show_simple(
- title=None,
- text=info(value),
- button=TR.buttons__ok_i_understand,
- ) as layout:
- await interact(layout, None, raise_on_cancel=None)
+ if status is trezorui_api.BACK:
+ return trezorui_api.BACK
+
+ if status is trezorui_api.INFO:
+ # shows the menu with the "more info" screen
+ leaf = leaf_from_layout(
+ TR.buttons__more_info,
+ lambda: trezorui_api.show_info_with_cancel(
+ title=title,
+ items=[("", info(value), False)],
+ ),
+ )
+ await show_menu(Menu([leaf]))
+ else:
+ raise RuntimeError
def slip39_prompt_threshold(
### tests/device_tests/test_msg_backup_device.py
@@ -190,7 +190,7 @@ def test_backup_slip39_advanced(
assert expected_ms == actual_ms
-@pytest.mark.models("safe3,delizia,eckhart") # going back is supported on these layouts
+@pytest.mark.models("t2,safe") # going back is supported on all core layouts
@pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6)
def test_backup_slip39_basic_back_navigation(
session: Session, backup_method: messages.BackupMethod
@@ -216,7 +216,7 @@ def test_backup_slip39_basic_back_navigation(
assert expected_ms == actual_ms
-@pytest.mark.models("safe3,delizia,eckhart") # going back is supported on these layouts
+@pytest.mark.models("t2,safe") # going back is supported on all core layouts
@pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_ADVANCED_20)
def test_backup_slip39_advanced_back_navigation(
session: Session, backup_method: messages.BackupMethod
### tests/input_flows.py
@@ -2730,6 +2730,69 @@ def __init__(
self.mnemonics: list[str] = []
self.method = method
+ def input_flow_bolt(self) -> BRGeneratorType:
+ assert self.method in (
+ messages.BackupMethod.Display,
+ messages.BackupMethod.N1W1,
+ )
+ if self.method is messages.BackupMethod.Display:
+ assert (yield).name == "backup_intro"
+ self.debug.press_yes()
+
+ # checklist: set number of shares (no going back from the first step)
+ assert (yield).name == "slip39_checklist"
+ self.debug.press_yes()
+
+ # number of shares prompt: change the default 5 to 3
+ assert (yield).name == "slip39_shares"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 5
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # checklist: set threshold -- go back via the up arrow
+ assert (yield).name == "slip39_checklist"
+ self.debug.click(self.debug.screen_buttons.cancel())
+
+ # number of shares prompt: the previously entered value is preselected
+ assert (yield).name == "slip39_shares"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 3
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # checklist: set threshold
+ assert (yield).name == "slip39_checklist"
+ self.debug.press_yes()
+
+ # threshold prompt: increase the default 2 to 3
+ assert (yield).name == "slip39_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 2
+ self.debug.click(self.debug.screen_buttons.number_input_plus())
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # checklist: write down the shares -- go back via the up arrow
+ assert (yield).name == "slip39_checklist"
+ self.debug.click(self.debug.screen_buttons.cancel())
+
+ # threshold prompt: the previously entered value is preselected
+ assert (yield).name == "slip39_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 3
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # checklist: write down the shares
+ assert (yield).name == "slip39_checklist"
+ self.debug.press_yes()
+
+ if self.method is messages.BackupMethod.Display:
+ assert (yield).name == "backup_warning"
+ self.debug.press_yes()
+
+ # Mnemonic phrases
+ self.mnemonics = yield from load_N_shares(self.debug, 3, self.method)
+
def input_flow_caesar(self) -> BRGeneratorType:
assert self.method is messages.BackupMethod.Display
@@ -2962,6 +3025,95 @@ def __init__(
self.mnemonics: list[list[str]] = []
self.method = method
+ def input_flow_bolt(self) -> BRGeneratorType:
+ assert self.method in (
+ messages.BackupMethod.Display,
+ messages.BackupMethod.N1W1,
+ )
+ if self.method is messages.BackupMethod.Display:
+ assert (yield).name == "backup_intro"
+ self.debug.press_yes()
+
+ # checklist: set number of groups (no going back from the first step)
+ assert (yield).name == "slip39_checklist"
+ self.debug.press_yes()
+
+ # number of groups prompt: change the default 5 to 2
+ assert (yield).name == "slip39_groups"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 5
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # checklist: set group threshold
+ assert (yield).name == "slip39_checklist"
+ self.debug.press_yes()
+
+ # group threshold prompt: keep the default 2
+ assert (yield).name == "slip39_group_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 2
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # checklist: set sizes and thresholds of the groups
+ assert (yield).name == "slip39_checklist"
+ self.debug.press_yes()
+
+ # group 1 shares prompt: change the default 5 to 3
+ assert (yield).name == "slip39_shares"
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # group 1 threshold prompt: keep the default 2
+ assert (yield).name == "slip39_threshold"
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # group 2 shares prompt: keep the default 5 -- go back via the up arrow
+ assert (yield).name == "slip39_shares"
+ self.debug.click(self.debug.screen_buttons.cancel())
+
+ # group 1 threshold prompt: the previously entered value is preselected
+ assert (yield).name == "slip39_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 2
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # group 2 shares prompt: keep the default 5
+ assert (yield).name == "slip39_shares"
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # group 2 threshold prompt: change the default 3 to 4 -- go back
+ assert (yield).name == "slip39_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 3
+ self.debug.click(self.debug.screen_buttons.number_input_plus())
+ self.debug.click(self.debug.screen_buttons.cancel())
+
+ # group 2 shares prompt: the previously entered value is preselected
+ assert (yield).name == "slip39_shares"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 5
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # group 2 threshold prompt: the default 3 is shown again -- keep it
+ assert (yield).name == "slip39_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 3
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ if self.method is messages.BackupMethod.Display:
+ assert (yield).name == "backup_warning"
+ self.debug.press_yes()
+
+ # Mnemonic phrases - show & confirm shares for all groups
+ # 2 groups: 2-of-3 and 3-of-5
+ self.mnemonics = yield from load_N_groups(
+ self.debug, [(2, 3), (3, 5)], self.method
+ )
+
def input_flow_caesar(self) -> BRGeneratorType:
assert self.method is messages.BackupMethod.Display
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.