1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-08 07:38:11 +00:00

WIP - Shamir wallet creation

This commit is contained in:
grdddj 2023-01-05 10:36:16 +01:00
parent 3c32a2097a
commit 473876c33f
20 changed files with 591 additions and 99 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

View File

@ -67,7 +67,6 @@ static void _librust_qstrs(void) {
MP_QSTR_progress_event;
MP_QSTR_usb_event;
MP_QSTR_request_word_bip39;
MP_QSTR_tutorial;
MP_QSTR_attach_timer_fn;

View File

@ -509,13 +509,15 @@ pub struct Checklist<T> {
current: usize,
icon_current: &'static [u8],
icon_done: &'static [u8],
/// How wide will the left icon column be
check_width: i16,
/// Offset of the icon representing DONE
done_offset: Offset,
/// Offset of the icon representing CURRENT
current_offset: Offset,
}
impl<T> Checklist<T> {
const CHECK_WIDTH: i16 = 16;
const DONE_OFFSET: Offset = Offset::new(-2, 6);
const CURRENT_OFFSET: Offset = Offset::new(2, 3);
pub fn from_paragraphs(
icon_current: &'static [u8],
icon_done: &'static [u8],
@ -528,9 +530,27 @@ impl<T> Checklist<T> {
current,
icon_current,
icon_done,
check_width: 0,
done_offset: Offset::zero(),
current_offset: Offset::zero(),
}
}
pub fn with_check_width(mut self, check_width: i16) -> Self {
self.check_width = check_width;
self
}
pub fn with_done_offset(mut self, done_offset: Offset) -> Self {
self.done_offset = done_offset;
self
}
pub fn with_current_offset(mut self, current_offset: Offset) -> Self {
self.current_offset = current_offset;
self
}
fn paint_icon(&self, layout: &TextLayout, icon: &'static [u8], offset: Offset) {
let top_left = Point::new(self.area.x0, layout.bounds.y0);
display::icon_top_left(
@ -550,7 +570,7 @@ where
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
let para_area = bounds.inset(Insets::left(Self::CHECK_WIDTH));
let para_area = bounds.inset(Insets::left(self.check_width));
self.paragraphs.place(para_area);
self.area
}
@ -564,10 +584,10 @@ where
let current_visible = self.current.saturating_sub(self.paragraphs.offset.par);
for layout in self.paragraphs.visible.iter().take(current_visible) {
self.paint_icon(layout, self.icon_done, Self::DONE_OFFSET);
self.paint_icon(layout, self.icon_done, self.done_offset);
}
if let Some(layout) = self.paragraphs.visible.iter().nth(current_visible) {
self.paint_icon(layout, self.icon_current, Self::CURRENT_OFFSET);
self.paint_icon(layout, self.icon_current, self.current_offset);
}
}
@ -577,6 +597,17 @@ where
}
}
impl<T> Paginate for Checklist<T>
where
T: ParagraphSource,
{
fn page_count(&mut self) -> usize {
1
}
fn change_page(&mut self, _to_page: usize) {}
}
#[cfg(feature = "ui_debug")]
impl<T: ParagraphSource> crate::trace::Trace for Checklist<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {

View File

@ -1,6 +1,7 @@
pub mod bip39;
pub mod choice;
pub mod choice_item;
pub mod number_input;
pub mod passphrase;
pub mod pin;
pub mod simple_choice;

View File

@ -0,0 +1,121 @@
use crate::ui::{
component::{Component, Event, EventCtx},
geometry::Rect,
};
use super::super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg};
use heapless::String;
pub enum NumberInputMsg {
Number(u32),
}
struct ChoiceFactoryNumberInput {
min: u32,
max: u32,
}
impl ChoiceFactoryNumberInput {
fn new(min: u32, max: u32) -> Self {
Self { min, max }
}
}
impl ChoiceFactory for ChoiceFactoryNumberInput {
type Item = ChoiceItem;
fn count(&self) -> u8 {
(self.max - self.min + 1) as u8
}
fn get(&self, choice_index: u8) -> ChoiceItem {
let num = self.min + choice_index as u32;
let text: String<10> = String::from(num);
let mut choice_item = ChoiceItem::new(text, ButtonLayout::default_three_icons());
// Disabling prev/next buttons for the first/last choice.
// (could be done to the same button if there is only one)
if choice_index == 0 {
choice_item.set_left_btn(None);
}
if choice_index == self.count() - 1 {
choice_item.set_right_btn(None);
}
choice_item
}
}
/// Simple wrapper around `ChoicePage` that allows for
/// inputting a list of values and receiving the chosen one.
pub struct NumberInput {
choice_page: ChoicePage<ChoiceFactoryNumberInput>,
min: u32,
}
impl NumberInput {
pub fn new(min: u32, max: u32, init_value: u32) -> Self {
let choices = ChoiceFactoryNumberInput::new(min, max);
let initial_page = init_value - min;
Self {
min,
choice_page: ChoicePage::new(choices).with_initial_page_counter(initial_page as u8),
}
}
}
impl Component for NumberInput {
type Msg = NumberInputMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.choice_page.place(bounds)
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let msg = self.choice_page.event(ctx, event);
match msg {
Some(ChoicePageMsg::Choice(page_counter)) => {
let result_num = self.min + page_counter as u32;
Some(NumberInputMsg::Number(result_num))
}
_ => None,
}
}
fn paint(&mut self) {
self.choice_page.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
use super::super::{ButtonAction, ButtonPos};
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for NumberInput {
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
match pos {
ButtonPos::Left => match self.choice_page.has_previous_choice() {
true => ButtonAction::PrevPage.string(),
false => ButtonAction::empty(),
},
ButtonPos::Right => match self.choice_page.has_next_choice() {
true => ButtonAction::NextPage.string(),
false => ButtonAction::empty(),
},
ButtonPos::Middle => {
let current_index = self.choice_page.page_index() as usize;
let current_num = self.min + current_index as u32;
ButtonAction::select_item(inttostr!(current_num))
}
}
}
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("NumberInput");
self.report_btn_actions(t);
t.field("choice_page", &self.choice_page);
t.close();
}
}

View File

@ -11,6 +11,7 @@ use heapless::{String, Vec};
pub enum SimpleChoiceMsg {
Result(String<50>),
Index(u8),
}
struct ChoiceFactorySimple<const N: usize> {
@ -55,18 +56,29 @@ impl<const N: usize> ChoiceFactory for ChoiceFactorySimple<N> {
pub struct SimpleChoice<const N: usize> {
choices: Vec<StrBuffer, N>,
choice_page: ChoicePage<ChoiceFactorySimple<N>>,
return_index: bool,
}
impl<const N: usize> SimpleChoice<N> {
pub fn new(str_choices: Vec<StrBuffer, N>, carousel: bool, show_incomplete: bool) -> Self {
pub fn new(str_choices: Vec<StrBuffer, N>, carousel: bool) -> Self {
let choices = ChoiceFactorySimple::new(str_choices.clone(), carousel);
Self {
choices: str_choices,
choice_page: ChoicePage::new(choices)
.with_carousel(carousel)
.with_incomplete(show_incomplete),
choice_page: ChoicePage::new(choices).with_carousel(carousel),
return_index: false,
}
}
/// Show only the currently selected item, nothing left/right.
pub fn with_only_one_item(mut self) -> Self {
self.choice_page = self.choice_page.with_only_one_item(true);
self
}
pub fn with_return_index(mut self) -> Self {
self.return_index = true;
self
}
}
impl<const N: usize> Component for SimpleChoice<N> {
@ -80,9 +92,13 @@ impl<const N: usize> Component for SimpleChoice<N> {
let msg = self.choice_page.event(ctx, event);
match msg {
Some(ChoicePageMsg::Choice(page_counter)) => {
if self.return_index {
Some(SimpleChoiceMsg::Index(page_counter))
} else {
let result = String::from(self.choices[page_counter as usize].as_ref());
Some(SimpleChoiceMsg::Result(result))
}
}
_ => None,
}
}

View File

@ -36,6 +36,7 @@ pub use input_methods::{
bip39::{Bip39Entry, Bip39EntryMsg},
choice::{Choice, ChoiceFactory, ChoicePage, ChoicePageMsg},
choice_item::ChoiceItem,
number_input::{NumberInput, NumberInputMsg},
passphrase::{PassphraseEntry, PassphraseEntryMsg},
pin::{PinEntry, PinEntryMsg},
simple_choice::{SimpleChoice, SimpleChoiceMsg},

View File

@ -22,27 +22,33 @@ const WORD_FONT: Font = Font::NORMAL;
/// Showing the given share words.
pub struct ShareWords<const N: usize> {
area: Rect,
title: StrBuffer,
share_words: Vec<StrBuffer, N>,
page_index: usize,
}
impl<const N: usize> ShareWords<N> {
pub fn new(share_words: Vec<StrBuffer, N>) -> Self {
pub fn new(title: StrBuffer, share_words: Vec<StrBuffer, N>) -> Self {
Self {
area: Rect::zero(),
title,
share_words,
page_index: 0,
}
}
fn word_index(&self) -> usize {
(self.page_index - 1) * WORDS_PER_PAGE
(self.page_index - 2) * WORDS_PER_PAGE
}
fn is_entry_page(&self) -> bool {
self.page_index == 0
}
fn is_second_page(&self) -> bool {
self.page_index == 1
}
fn is_final_page(&self) -> bool {
self.page_index == self.total_page_count() - 1
}
@ -53,32 +59,45 @@ impl<const N: usize> ShareWords<N> {
} else {
self.share_words.len() / WORDS_PER_PAGE + 1
};
// One page before the words, one after it
1 + word_screens + 1
// Two pages before the words, one after it
2 + word_screens + 1
}
/// Display the first page with user information.
fn render_entry_page(&self) {
// TODO: will it be always 12, or do we need to check the length?
// It would need creating a String out of it, which is not ideal.
let free_area = text_multiline(
self.area,
"Write all 12\nwords in order on\nrecovery seed card",
display(
self.area
.top_left()
.ofs(Offset::y(Font::BOLD.line_height())),
&self.title,
Font::BOLD,
);
text_multiline(
self.area.split_top(15).1,
&build_string!(
50,
"Write all ",
inttostr!(self.share_words.len() as u8),
"\nwords in order on\nrecovery seed card"
),
Font::BOLD,
theme::FG,
theme::BG,
);
if let Some(free_area) = free_area {
// Creating a small vertical distance
}
/// Display the second page with user information.
fn render_second_page(&self) {
// Creating a small vertical distance to make it centered
text_multiline(
free_area.split_top(3).1,
self.area.split_top(15).1,
"Do NOT make\ndigital copies!",
Font::MONO,
theme::FG,
theme::BG,
);
}
}
/// Display the final page with user confirmation.
fn render_final_page(&self) {
@ -86,7 +105,12 @@ impl<const N: usize> ShareWords<N> {
// and to look better.
text_multiline(
self.area.split_top(12).1,
"I wrote down all\n12 words in order.",
&build_string!(
50,
"I wrote down all\n",
inttostr!(self.share_words.len() as u8),
" words in order."
),
Font::MONO,
theme::FG,
theme::BG,
@ -100,6 +124,9 @@ impl<const N: usize> ShareWords<N> {
for i in 0..WORDS_PER_PAGE {
y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT;
let index = self.word_index() + i;
if index >= self.share_words.len() {
break;
}
let word = self.share_words[index];
let baseline = self.area.top_left() + Offset::new(NUMBER_X_OFFSET, y_offset);
display(baseline, &inttostr!(index as u8 + 1), NUMBER_FONT);
@ -123,6 +150,8 @@ impl<const N: usize> Component for ShareWords<N> {
fn paint(&mut self) {
if self.is_entry_page() {
self.render_entry_page();
} else if self.is_second_page() {
self.render_second_page();
} else if self.is_final_page() {
self.render_final_page();
} else {
@ -156,6 +185,9 @@ impl<const N: usize> crate::trace::Trace for ShareWords<N> {
} else {
for i in 0..WORDS_PER_PAGE {
let index = self.word_index() + i;
if index >= self.share_words.len() {
break;
}
let word = self.share_words[index];
let content = build_string!(20, inttostr!(index as u8 + 1), " ", &word, "\n");
t.string(&content);

View File

@ -1,4 +1,4 @@
use core::convert::TryInto;
use core::{cmp::Ordering, convert::TryInto};
use heapless::Vec;
@ -20,7 +20,8 @@ use crate::{
base::Component,
paginated::{PageMsg, Paginate},
text::paragraphs::{
Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort, Paragraphs, VecExt,
Checklist, Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort,
Paragraphs, VecExt,
},
ComponentExt, Empty, Timeout, TimeoutMsg,
},
@ -38,8 +39,8 @@ use super::{
component::{
Bip39Entry, Bip39EntryMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow,
FlowMsg, FlowPages, Frame, Homescreen, HomescreenMsg, Lockscreen, NoBtnDialog,
NoBtnDialogMsg, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry, PinEntryMsg, Progress,
ShareWords, SimpleChoice, SimpleChoiceMsg,
NoBtnDialogMsg, NumberInput, NumberInputMsg, Page, PassphraseEntry, PassphraseEntryMsg,
PinEntry, PinEntryMsg, Progress, ShareWords, SimpleChoice, SimpleChoiceMsg,
},
constant, theme,
};
@ -95,7 +96,7 @@ where
match msg {
FlowMsg::Confirmed => Ok(CONFIRMED.as_obj()),
FlowMsg::Cancelled => Ok(CANCELLED.as_obj()),
FlowMsg::ConfirmedIndex(page) => Ok(page.into()),
FlowMsg::ConfirmedIndex(index) => Ok(index.into()),
}
}
}
@ -109,10 +110,19 @@ impl ComponentMsgObj for PinEntry {
}
}
impl ComponentMsgObj for NumberInput {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
NumberInputMsg::Number(choice) => choice.try_into(),
}
}
}
impl<const N: usize> ComponentMsgObj for SimpleChoice<N> {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
SimpleChoiceMsg::Result(choice) => choice.as_str().try_into(),
SimpleChoiceMsg::Index(index) => Ok(index.into()),
}
}
}
@ -638,16 +648,77 @@ extern "C" fn new_request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map)
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_request_number(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let min_count: u32 = kwargs.get(Qstr::MP_QSTR_min_count)?.try_into()?;
let max_count: u32 = kwargs.get(Qstr::MP_QSTR_max_count)?.try_into()?;
let count: u32 = kwargs.get(Qstr::MP_QSTR_count)?.try_into()?;
let obj = LayoutObj::new(Frame::new(
title,
NumberInput::new(min_count, max_count, count),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let _title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let button: StrBuffer = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?;
let active: usize = kwargs.get(Qstr::MP_QSTR_active)?.try_into()?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let mut iter_buf = IterBuf::new();
let mut paragraphs = ParagraphVecLong::new();
let iter = Iter::try_from_obj_with_buf(items, &mut iter_buf)?;
for (i, item) in iter.enumerate() {
let style = match i.cmp(&active) {
Ordering::Less => &theme::TEXT_MONO,
Ordering::Equal => &theme::TEXT_BOLD,
Ordering::Greater => &theme::TEXT_MONO,
};
let text: StrBuffer = item.try_into()?;
paragraphs.add(Paragraph::new(style, text));
}
let confirm_btn = Some(ButtonDetails::text(button));
let obj = LayoutObj::new(
ButtonPage::new(
Checklist::from_paragraphs(
theme::ICON_ARROW_RIGHT_FAT.0,
theme::ICON_TICK_FAT.0,
active,
paragraphs
.into_paragraphs()
.with_spacing(theme::CHECKLIST_SPACING),
)
.with_check_width(theme::CHECKLIST_CHECK_WIDTH)
.with_current_offset(theme::CHECKLIST_CURRENT_OFFSET),
theme::BG,
)
.with_confirm_btn(confirm_btn),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_share_words)?;
let share_words: Vec<StrBuffer, 24> = iter_into_vec(share_words_obj)?;
let share_words: Vec<StrBuffer, 33> = iter_into_vec(share_words_obj)?;
let confirm_btn =
Some(ButtonDetails::text("HOLD TO CONFIRM".into()).with_default_duration());
let obj = LayoutObj::new(
ButtonPage::new(ShareWords::new(share_words), theme::BG).with_confirm_btn(confirm_btn),
ButtonPage::new(ShareWords::new(title, share_words), theme::BG)
.with_confirm_btn(confirm_btn),
)?;
Ok(obj.into())
};
@ -660,9 +731,15 @@ extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map)
let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?;
let words: Vec<StrBuffer, 3> = iter_into_vec(words_iterable)?;
// TODO: should return int, to be consistent with TT's select_word
// Returning the index of the selected word, not the word itself
let obj = LayoutObj::new(
Frame::new(title, SimpleChoice::new(words, true, true)).with_title_center(true),
Frame::new(
title,
SimpleChoice::new(words, false)
.with_only_one_item()
.with_return_index(),
)
.with_title_center(true),
)?;
Ok(obj.into())
};
@ -674,17 +751,18 @@ extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mu
let _dry_run: bool = kwargs.get(Qstr::MP_QSTR_dry_run)?.try_into()?;
let title = "NUMBER OF WORDS".into();
let choices: Vec<StrBuffer, 3> = ["12".into(), "18".into(), "24".into()]
let choices: Vec<StrBuffer, 5> = ["12", "18", "20", "24", "33"]
.map(|num| num.into())
.into_iter()
.collect();
let obj = LayoutObj::new(Frame::new(title, SimpleChoice::new(choices, false, false)))?;
let obj = LayoutObj::new(Frame::new(title, SimpleChoice::new(choices, false)))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_request_word_bip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
extern "C" fn new_request_bip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
@ -894,8 +972,30 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Request pin on device."""
Qstr::MP_QSTR_request_pin => obj_fn_kw!(0, new_request_pin).as_obj(),
/// def request_number(
/// *,
/// title: str,
/// count: int,
/// min_count: int,
/// max_count: int,
/// ) -> object:
/// """Number input with + and - buttons, description, and info button."""
Qstr::MP_QSTR_request_number => obj_fn_kw!(0, new_request_number).as_obj(),
/// def show_checklist(
/// *,
/// title: str,
/// items: Iterable[str],
/// active: int,
/// button: str,
/// ) -> object:
/// """Checklist of backup steps. Active index is highlighted, previous items have check
/// mark next to them."""
Qstr::MP_QSTR_show_checklist => obj_fn_kw!(0, new_show_checklist).as_obj(),
/// def show_share_words(
/// *,
/// title: str,
/// share_words: Iterable[str],
/// ) -> None:
/// """Shows a backup seed."""
@ -905,23 +1005,24 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// *,
/// title: str,
/// words: Iterable[str],
/// ) -> str: # TODO: should return int, to be consistent with TT's select_word
/// """Select a word from a list."""
/// ) -> int:
/// """Select mnemonic word from three possibilities - seed check after backup. The
/// iterable must be of exact size. Returns index in range `0..3`."""
Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(),
/// def select_word_count(
/// *,
/// dry_run: bool,
/// ) -> str: # TODO: make it return int
/// """Get word count for recovery."""
/// """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 request_word_bip39(
/// def request_bip39(
/// *,
/// prompt: str,
/// ) -> str:
/// """Get recovery word for BIP39."""
Qstr::MP_QSTR_request_word_bip39 => obj_fn_kw!(0, new_request_word_bip39).as_obj(),
Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(),
/// def request_passphrase(
/// *,

Binary file not shown.

View File

@ -1,6 +1,7 @@
use crate::ui::{
component::text::TextStyle,
display::{Color, Font, IconAndName},
geometry::Offset,
};
// Color palette.
@ -33,6 +34,10 @@ pub const ICON_ARROW_LEFT: IconAndName =
IconAndName::new(include_res!("model_tr/res/arrow_left.toif"), "arrow_left"); // 6*10
pub const ICON_ARROW_RIGHT: IconAndName =
IconAndName::new(include_res!("model_tr/res/arrow_right.toif"), "arrow_right"); // 6*10
pub const ICON_ARROW_RIGHT_FAT: IconAndName = IconAndName::new(
include_res!("model_tr/res/arrow_right_fat.toif"),
"arrow_right_fat",
); // 4*8
pub const ICON_ARROW_UP: IconAndName =
IconAndName::new(include_res!("model_tr/res/arrow_up.toif"), "arrow_up"); // 10*6
pub const ICON_ARROW_DOWN: IconAndName =
@ -54,9 +59,16 @@ pub const ICON_PREV_PAGE: IconAndName =
pub const ICON_SUCCESS: IconAndName =
IconAndName::new(include_res!("model_tr/res/success.toif"), "success");
pub const ICON_TICK: IconAndName = IconAndName::new(include_res!("model_tr/res/tick.toif"), "tick"); // 10*10
pub const ICON_TICK_FAT: IconAndName =
IconAndName::new(include_res!("model_tr/res/tick_fat.toif"), "tick_fat"); // 8*6
pub const ICON_WARNING: IconAndName =
IconAndName::new(include_res!("model_tr/res/warning.toif"), "warning"); // 12*12
// checklist settings
pub const CHECKLIST_SPACING: i16 = 5;
pub const CHECKLIST_CHECK_WIDTH: i16 = 12;
pub const CHECKLIST_CURRENT_OFFSET: Offset = Offset::x(3);
// Button height is constant for both text and icon buttons.
// It is a combination of content and (optional) outline/border.
// It is not possible to have icons 7*7, therefore having 8*8

View File

@ -1094,7 +1094,10 @@ extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut M
paragraphs
.into_paragraphs()
.with_spacing(theme::CHECKLIST_SPACING),
),
)
.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)
})),
@ -1590,7 +1593,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// button: str,
/// ) -> object:
/// """Checklist of backup steps. Active index is highlighted, previous items have check
/// mark nex to them."""
/// mark next to them."""
Qstr::MP_QSTR_show_checklist => obj_fn_kw!(0, new_show_checklist).as_obj(),
/// def confirm_recovery(

View File

@ -6,7 +6,7 @@ use crate::{
FixedHeightBar,
},
display::{Color, Font},
geometry::Insets,
geometry::{Insets, Offset},
},
};
@ -413,6 +413,11 @@ pub const BUTTON_SPACING: i16 = 6;
pub const CHECKLIST_SPACING: i16 = 10;
pub const RECOVERY_SPACING: i16 = 18;
// checklist settings
pub const CHECKLIST_CHECK_WIDTH: i16 = 16;
pub const CHECKLIST_DONE_OFFSET: Offset = Offset::new(-2, 6);
pub const CHECKLIST_CURRENT_OFFSET: Offset = Offset::new(2, 3);
/// Standard button height in pixels.
pub const fn button_rows(count: usize) -> i16 {
let count = count as i16;

View File

@ -108,6 +108,18 @@ def request_pin(
"""Request pin on device."""
# rust/src/ui/model_tr/layout.rs
def show_checklist(
*,
title: str,
items: Iterable[str],
active: int,
button: str,
) -> object:
"""Checklist of backup steps. Active index is highlighted, previous items have check
mark next to them."""
# rust/src/ui/model_tr/layout.rs
def show_share_words(
*,
@ -130,11 +142,11 @@ def select_word_count(
*,
dry_run: bool,
) -> str: # TODO: make it return int
"""Get word count for recovery."""
"""Select mnemonic word count from (12, 18, 20, 24, 33)."""
# rust/src/ui/model_tr/layout.rs
def request_word_bip39(
def request_bip39(
*,
prompt: str,
) -> str:
@ -503,7 +515,7 @@ def show_checklist(
button: str,
) -> object:
"""Checklist of backup steps. Active index is highlighted, previous items have check
mark nex to them."""
mark next to them."""
# rust/src/ui/model_tt/layout.rs

View File

@ -21,8 +21,6 @@ def format_amount(amount: int, decimals: int) -> str:
return s
if False:
def format_ordinal(number: int) -> str:
return str(number) + {1: "st", 2: "nd", 3: "rd"}.get(
4 if 10 <= number % 100 < 20 else number % 10, "th"

View File

@ -751,7 +751,6 @@ def show_warning(
ctx: GenericContext,
br_type: str,
content: str,
header: str = "Warning",
subheader: str | None = None,
button: str = "Try again",
br_code: ButtonRequestType = ButtonRequestType.Warning,
@ -759,8 +758,8 @@ def show_warning(
return _show_modal(
ctx,
br_type,
header,
subheader,
"",
subheader or "Warning",
content,
button_confirm=button,
button_cancel=None,

View File

@ -34,7 +34,7 @@ async def request_word(
else:
word = await interact(
ctx,
RustLayout(trezorui2.request_word_bip39(prompt=prompt)),
RustLayout(trezorui2.request_bip39(prompt=prompt)),
"request_word",
ButtonRequestType.MnemonicInput,
)

View File

@ -1,115 +1,276 @@
from typing import TYPE_CHECKING
from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled
import trezorui2
from ..common import interact
from . import RustLayout, confirm_action, get_bool
from . import RustLayout, confirm_action
CONFIRMED = trezorui2.CONFIRMED # global_import_cache
if TYPE_CHECKING:
from trezor import wire
from trezor.wire import GenericContext
from trezor.enums import BackupType
from typing import Sequence
async def show_share_words(
ctx: wire.GenericContext,
ctx: GenericContext,
share_words: Sequence[str],
share_index: int | None = None,
group_index: int | None = None,
) -> None:
from . import get_bool
if share_index is None:
title = "RECOVERY SEED"
elif group_index is None:
title = f"SHARE #{share_index + 1}"
else:
title = f"G{group_index + 1} - SHARE {share_index + 1}"
# Showing words, asking for write down confirmation and preparing for check
# until user accepts everything.
while True:
await interact(
ctx,
RustLayout(
trezorui2.show_share_words( # type: ignore [Arguments missing for parameters "title", "pages"]
share_words=share_words # type: ignore [No parameter named "share_words"]
trezorui2.show_share_words( # type: ignore [Argument missing for parameter "pages"]
title=title,
share_words=share_words, # type: ignore [No parameter named "share_words"]
)
),
"backup_words",
ButtonRequestType.ResetDevice,
)
ready_to_check = await get_bool(
if share_index is None:
check_title = "CHECK SEED"
elif group_index is None:
check_title = f"CHECK SHARE #{share_index + 1}"
else:
check_title = f"CHECK G{group_index + 1} - SHARE {share_index + 1}"
if await get_bool(
ctx,
"backup_words",
"CHECK BACKUP",
check_title,
None,
"Select correct words in correct positions.",
verb_cancel="SEE AGAIN",
verb="BEGIN",
br_code=ButtonRequestType.ResetDevice,
)
if not ready_to_check:
continue
):
# All went well, we can break the loop.
break
async def select_word(
ctx: wire.GenericContext,
ctx: GenericContext,
words: Sequence[str],
share_index: int | None,
checked_index: int,
count: int,
group_index: int | None = None,
) -> str:
from trezor.strings import format_ordinal
# TODO: it might not always be 3 words, it can happen it will be only two,
# but the probability is very small - 4 words containing two items two times
# (or one in "all all" seed)
assert len(words) == 3
result = await ctx.wait(
RustLayout(
trezorui2.select_word( # type: ignore [Argument missing for parameter "description"]
title=f"SELECT WORD {checked_index + 1}/{count}",
title=f"SELECT {format_ordinal(checked_index + 1).upper()} WORD",
words=(words[0].upper(), words[1].upper(), words[2].upper()),
)
)
)
for word in words:
if word.upper() == result:
return word
raise ValueError("Invalid word")
if __debug__ and isinstance(result, str):
return result
assert isinstance(result, int) and 0 <= result <= 2
return words[result]
async def slip39_show_checklist(
ctx: wire.GenericContext, step: int, backup_type: BackupType
ctx: GenericContext, step: int, backup_type: BackupType
) -> None:
raise NotImplementedError
from trezor.enums import BackupType
assert backup_type in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced)
items = (
(
"Number of shares",
"Set threshold",
"Write down and check all shares",
)
if backup_type == BackupType.Slip39_Basic
else (
"Number of groups",
"Number of shares",
"Set sizes and thresholds",
)
)
result = await interact(
ctx,
RustLayout(
trezorui2.show_checklist(
title="BACKUP CHECKLIST",
button="CONTINUE",
active=step,
items=items,
)
),
"slip39_checklist",
ButtonRequestType.ResetDevice,
)
if result != CONFIRMED:
raise ActionCancelled
async def _prompt_number(
ctx: GenericContext,
title: str,
count: int,
min_count: int,
max_count: int,
br_name: str,
) -> int:
num_input = RustLayout(
trezorui2.request_number( # type: ignore [Argument missing for parameter "description"]
title=title.upper(),
count=count,
min_count=min_count,
max_count=max_count,
)
)
result = await interact(
ctx,
num_input,
br_name,
ButtonRequestType.ResetDevice,
)
assert isinstance(result, int)
return result
async def slip39_prompt_threshold(
ctx: wire.GenericContext, num_of_shares: int, group_id: int | None = None
ctx: GenericContext, num_of_shares: int, group_id: int | None = None
) -> int:
raise NotImplementedError
await confirm_action(
ctx,
"slip39_prompt_threshold",
"Set threshold",
description="= number of shares needed for recovery",
verb="BEGIN",
verb_cancel=None,
)
count = num_of_shares // 2 + 1
# min value of share threshold is 2 unless the number of shares is 1
# number of shares 1 is possible in advanced slip39
min_count = min(2, num_of_shares)
max_count = num_of_shares
if group_id is not None:
title = f"THRESHOLD - GROUP {group_id + 1}"
else:
title = "SET THRESHOLD"
return await _prompt_number(
ctx,
title,
count,
min_count,
max_count,
"slip39_threshold",
)
async def slip39_prompt_number_of_shares(
ctx: wire.GenericContext, group_id: int | None = None
ctx: GenericContext, group_id: int | None = None
) -> int:
raise NotImplementedError
await confirm_action(
ctx,
"slip39_shares",
"Number of shares",
description="= total number of unique word lists used for wallet backup.",
verb="BEGIN",
verb_cancel=None,
)
count = 5
min_count = 1
max_count = 16
if group_id is not None:
title = f"# SHARES - GROUP {group_id + 1}"
else:
title = "NUMBER OF SHARES"
return await _prompt_number(
ctx,
title,
count,
min_count,
max_count,
"slip39_shares",
)
async def slip39_advanced_prompt_number_of_groups(ctx: wire.GenericContext) -> int:
raise NotImplementedError
async def slip39_advanced_prompt_number_of_groups(ctx: GenericContext) -> int:
count = 5
min_count = 2
max_count = 16
return await _prompt_number(
ctx,
"NUMBER OF GROUPS",
count,
min_count,
max_count,
"slip39_groups",
)
async def slip39_advanced_prompt_group_threshold(
ctx: wire.GenericContext, num_of_groups: int
ctx: GenericContext, num_of_groups: int
) -> int:
raise NotImplementedError
count = num_of_groups // 2 + 1
min_count = 1
max_count = num_of_groups
return await _prompt_number(
ctx,
"GROUP THRESHOLD",
count,
min_count,
max_count,
"slip39_group_threshold",
)
async def show_warning_backup(ctx: wire.GenericContext, slip39: bool) -> None:
async def show_warning_backup(ctx: GenericContext, slip39: bool) -> None:
if slip39:
description = (
"Never make a digital copy of your shares and never upload them online."
)
else:
description = (
"Never make a digital copy of your seed and never upload it online."
)
await confirm_action(
ctx,
"backup_warning",
"Caution",
description="Never make a digital copy and never upload it online.",
description=description,
verb="I understand",
verb_cancel=None,
br_code=ButtonRequestType.ResetDevice,