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_progress_event;
MP_QSTR_usb_event; MP_QSTR_usb_event;
MP_QSTR_request_word_bip39;
MP_QSTR_tutorial; MP_QSTR_tutorial;
MP_QSTR_attach_timer_fn; MP_QSTR_attach_timer_fn;

View File

@ -509,13 +509,15 @@ pub struct Checklist<T> {
current: usize, current: usize,
icon_current: &'static [u8], icon_current: &'static [u8],
icon_done: &'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> { 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( pub fn from_paragraphs(
icon_current: &'static [u8], icon_current: &'static [u8],
icon_done: &'static [u8], icon_done: &'static [u8],
@ -528,9 +530,27 @@ impl<T> Checklist<T> {
current, current,
icon_current, icon_current,
icon_done, 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) { fn paint_icon(&self, layout: &TextLayout, icon: &'static [u8], offset: Offset) {
let top_left = Point::new(self.area.x0, layout.bounds.y0); let top_left = Point::new(self.area.x0, layout.bounds.y0);
display::icon_top_left( display::icon_top_left(
@ -550,7 +570,7 @@ where
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds; 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.paragraphs.place(para_area);
self.area self.area
} }
@ -564,10 +584,10 @@ where
let current_visible = self.current.saturating_sub(self.paragraphs.offset.par); let current_visible = self.current.saturating_sub(self.paragraphs.offset.par);
for layout in self.paragraphs.visible.iter().take(current_visible) { 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) { 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")] #[cfg(feature = "ui_debug")]
impl<T: ParagraphSource> crate::trace::Trace for Checklist<T> { impl<T: ParagraphSource> crate::trace::Trace for Checklist<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) { fn trace(&self, t: &mut dyn crate::trace::Tracer) {

View File

@ -1,6 +1,7 @@
pub mod bip39; pub mod bip39;
pub mod choice; pub mod choice;
pub mod choice_item; pub mod choice_item;
pub mod number_input;
pub mod passphrase; pub mod passphrase;
pub mod pin; pub mod pin;
pub mod simple_choice; 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 { pub enum SimpleChoiceMsg {
Result(String<50>), Result(String<50>),
Index(u8),
} }
struct ChoiceFactorySimple<const N: usize> { struct ChoiceFactorySimple<const N: usize> {
@ -55,18 +56,29 @@ impl<const N: usize> ChoiceFactory for ChoiceFactorySimple<N> {
pub struct SimpleChoice<const N: usize> { pub struct SimpleChoice<const N: usize> {
choices: Vec<StrBuffer, N>, choices: Vec<StrBuffer, N>,
choice_page: ChoicePage<ChoiceFactorySimple<N>>, choice_page: ChoicePage<ChoiceFactorySimple<N>>,
return_index: bool,
} }
impl<const N: usize> SimpleChoice<N> { 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); let choices = ChoiceFactorySimple::new(str_choices.clone(), carousel);
Self { Self {
choices: str_choices, choices: str_choices,
choice_page: ChoicePage::new(choices) choice_page: ChoicePage::new(choices).with_carousel(carousel),
.with_carousel(carousel) return_index: false,
.with_incomplete(show_incomplete),
} }
} }
/// 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> { impl<const N: usize> Component for SimpleChoice<N> {
@ -80,8 +92,12 @@ impl<const N: usize> Component for SimpleChoice<N> {
let msg = self.choice_page.event(ctx, event); let msg = self.choice_page.event(ctx, event);
match msg { match msg {
Some(ChoicePageMsg::Choice(page_counter)) => { Some(ChoicePageMsg::Choice(page_counter)) => {
let result = String::from(self.choices[page_counter as usize].as_ref()); if self.return_index {
Some(SimpleChoiceMsg::Result(result)) Some(SimpleChoiceMsg::Index(page_counter))
} else {
let result = String::from(self.choices[page_counter as usize].as_ref());
Some(SimpleChoiceMsg::Result(result))
}
} }
_ => None, _ => None,
} }

View File

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

View File

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

View File

@ -1,4 +1,4 @@
use core::convert::TryInto; use core::{cmp::Ordering, convert::TryInto};
use heapless::Vec; use heapless::Vec;
@ -20,7 +20,8 @@ use crate::{
base::Component, base::Component,
paginated::{PageMsg, Paginate}, paginated::{PageMsg, Paginate},
text::paragraphs::{ text::paragraphs::{
Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort, Paragraphs, VecExt, Checklist, Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort,
Paragraphs, VecExt,
}, },
ComponentExt, Empty, Timeout, TimeoutMsg, ComponentExt, Empty, Timeout, TimeoutMsg,
}, },
@ -38,8 +39,8 @@ use super::{
component::{ component::{
Bip39Entry, Bip39EntryMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow, Bip39Entry, Bip39EntryMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow,
FlowMsg, FlowPages, Frame, Homescreen, HomescreenMsg, Lockscreen, NoBtnDialog, FlowMsg, FlowPages, Frame, Homescreen, HomescreenMsg, Lockscreen, NoBtnDialog,
NoBtnDialogMsg, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry, PinEntryMsg, Progress, NoBtnDialogMsg, NumberInput, NumberInputMsg, Page, PassphraseEntry, PassphraseEntryMsg,
ShareWords, SimpleChoice, SimpleChoiceMsg, PinEntry, PinEntryMsg, Progress, ShareWords, SimpleChoice, SimpleChoiceMsg,
}, },
constant, theme, constant, theme,
}; };
@ -95,7 +96,7 @@ where
match msg { match msg {
FlowMsg::Confirmed => Ok(CONFIRMED.as_obj()), FlowMsg::Confirmed => Ok(CONFIRMED.as_obj()),
FlowMsg::Cancelled => Ok(CANCELLED.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> { impl<const N: usize> ComponentMsgObj for SimpleChoice<N> {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg { match msg {
SimpleChoiceMsg::Result(choice) => choice.as_str().try_into(), 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) } 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 { extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| { 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_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 = let confirm_btn =
Some(ButtonDetails::text("HOLD TO CONFIRM".into()).with_default_duration()); Some(ButtonDetails::text("HOLD TO CONFIRM".into()).with_default_duration());
let obj = LayoutObj::new( 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()) 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_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?;
let words: Vec<StrBuffer, 3> = iter_into_vec(words_iterable)?; 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( 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()) 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 _dry_run: bool = kwargs.get(Qstr::MP_QSTR_dry_run)?.try_into()?;
let title = "NUMBER OF WORDS".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() .into_iter()
.collect(); .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()) Ok(obj.into())
}; };
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_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 block = |_args: &[Obj], kwargs: &Map| {
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; 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.""" /// """Request pin on device."""
Qstr::MP_QSTR_request_pin => obj_fn_kw!(0, new_request_pin).as_obj(), 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( /// def show_share_words(
/// *, /// *,
/// title: str,
/// share_words: Iterable[str], /// share_words: Iterable[str],
/// ) -> None: /// ) -> None:
/// """Shows a backup seed.""" /// """Shows a backup seed."""
@ -905,23 +1005,24 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// *, /// *,
/// title: str, /// title: str,
/// words: Iterable[str], /// words: Iterable[str],
/// ) -> str: # TODO: should return int, to be consistent with TT's select_word /// ) -> int:
/// """Select a word from a list.""" /// """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(), Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(),
/// def select_word_count( /// def select_word_count(
/// *, /// *,
/// dry_run: bool, /// dry_run: bool,
/// ) -> str: # TODO: make it return int /// ) -> 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(), 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, /// prompt: str,
/// ) -> str: /// ) -> str:
/// """Get recovery word for BIP39.""" /// """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( /// def request_passphrase(
/// *, /// *,

Binary file not shown.

View File

@ -1,6 +1,7 @@
use crate::ui::{ use crate::ui::{
component::text::TextStyle, component::text::TextStyle,
display::{Color, Font, IconAndName}, display::{Color, Font, IconAndName},
geometry::Offset,
}; };
// Color palette. // 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 IconAndName::new(include_res!("model_tr/res/arrow_left.toif"), "arrow_left"); // 6*10
pub const ICON_ARROW_RIGHT: IconAndName = pub const ICON_ARROW_RIGHT: IconAndName =
IconAndName::new(include_res!("model_tr/res/arrow_right.toif"), "arrow_right"); // 6*10 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 = pub const ICON_ARROW_UP: IconAndName =
IconAndName::new(include_res!("model_tr/res/arrow_up.toif"), "arrow_up"); // 10*6 IconAndName::new(include_res!("model_tr/res/arrow_up.toif"), "arrow_up"); // 10*6
pub const ICON_ARROW_DOWN: IconAndName = pub const ICON_ARROW_DOWN: IconAndName =
@ -54,9 +59,16 @@ pub const ICON_PREV_PAGE: IconAndName =
pub const ICON_SUCCESS: IconAndName = pub const ICON_SUCCESS: IconAndName =
IconAndName::new(include_res!("model_tr/res/success.toif"), "success"); 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: 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 = pub const ICON_WARNING: IconAndName =
IconAndName::new(include_res!("model_tr/res/warning.toif"), "warning"); // 12*12 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. // Button height is constant for both text and icon buttons.
// It is a combination of content and (optional) outline/border. // It is a combination of content and (optional) outline/border.
// It is not possible to have icons 7*7, therefore having 8*8 // 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 paragraphs
.into_paragraphs() .into_paragraphs()
.with_spacing(theme::CHECKLIST_SPACING), .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| { theme::button_bar(Button::with_text(button).map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed) (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
})), })),
@ -1590,7 +1593,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// button: str, /// button: str,
/// ) -> 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 next 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( /// def confirm_recovery(

View File

@ -6,7 +6,7 @@ use crate::{
FixedHeightBar, FixedHeightBar,
}, },
display::{Color, Font}, 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 CHECKLIST_SPACING: i16 = 10;
pub const RECOVERY_SPACING: i16 = 18; 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. /// Standard button height in pixels.
pub const fn button_rows(count: usize) -> i16 { pub const fn button_rows(count: usize) -> i16 {
let count = count as i16; let count = count as i16;

View File

@ -108,6 +108,18 @@ def request_pin(
"""Request pin on device.""" """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 # rust/src/ui/model_tr/layout.rs
def show_share_words( def show_share_words(
*, *,
@ -130,11 +142,11 @@ def select_word_count(
*, *,
dry_run: bool, dry_run: bool,
) -> str: # TODO: make it return int ) -> 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 # rust/src/ui/model_tr/layout.rs
def request_word_bip39( def request_bip39(
*, *,
prompt: str, prompt: str,
) -> str: ) -> str:
@ -503,7 +515,7 @@ def show_checklist(
button: str, button: str,
) -> 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 next to them."""
# rust/src/ui/model_tt/layout.rs # rust/src/ui/model_tt/layout.rs

View File

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

View File

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

View File

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

View File

@ -1,115 +1,276 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled
import trezorui2 import trezorui2
from ..common import interact 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: if TYPE_CHECKING:
from trezor import wire from trezor.wire import GenericContext
from trezor.enums import BackupType from trezor.enums import BackupType
from typing import Sequence from typing import Sequence
async def show_share_words( async def show_share_words(
ctx: wire.GenericContext, ctx: GenericContext,
share_words: Sequence[str], share_words: Sequence[str],
share_index: int | None = None, share_index: int | None = None,
group_index: int | None = None, group_index: int | None = 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 # Showing words, asking for write down confirmation and preparing for check
# until user accepts everything. # until user accepts everything.
while True: while True:
await interact( await interact(
ctx, ctx,
RustLayout( RustLayout(
trezorui2.show_share_words( # type: ignore [Arguments missing for parameters "title", "pages"] trezorui2.show_share_words( # type: ignore [Argument missing for parameter "pages"]
share_words=share_words # type: ignore [No parameter named "share_words"] title=title,
share_words=share_words, # type: ignore [No parameter named "share_words"]
) )
), ),
"backup_words", "backup_words",
ButtonRequestType.ResetDevice, 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, ctx,
"backup_words", "backup_words",
"CHECK BACKUP", check_title,
None, None,
"Select correct words in correct positions.", "Select correct words in correct positions.",
verb_cancel="SEE AGAIN", verb_cancel="SEE AGAIN",
verb="BEGIN", verb="BEGIN",
br_code=ButtonRequestType.ResetDevice, br_code=ButtonRequestType.ResetDevice,
) ):
if not ready_to_check: # All went well, we can break the loop.
continue break
# All went well, we can break the loop.
break
async def select_word( async def select_word(
ctx: wire.GenericContext, ctx: GenericContext,
words: Sequence[str], words: Sequence[str],
share_index: int | None, share_index: int | None,
checked_index: int, checked_index: int,
count: int, count: int,
group_index: int | None = None, group_index: int | None = None,
) -> str: ) -> str:
from trezor.strings import format_ordinal
# TODO: it might not always be 3 words, it can happen it will be only two, # 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 # but the probability is very small - 4 words containing two items two times
# (or one in "all all" seed)
assert len(words) == 3 assert len(words) == 3
result = await ctx.wait( result = await ctx.wait(
RustLayout( RustLayout(
trezorui2.select_word( # type: ignore [Argument missing for parameter "description"] 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()), words=(words[0].upper(), words[1].upper(), words[2].upper()),
) )
) )
) )
for word in words: if __debug__ and isinstance(result, str):
if word.upper() == result: return result
return word assert isinstance(result, int) and 0 <= result <= 2
raise ValueError("Invalid word") return words[result]
async def slip39_show_checklist( async def slip39_show_checklist(
ctx: wire.GenericContext, step: int, backup_type: BackupType ctx: GenericContext, step: int, backup_type: BackupType
) -> None: ) -> 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( 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: ) -> 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( async def slip39_prompt_number_of_shares(
ctx: wire.GenericContext, group_id: int | None = None ctx: GenericContext, group_id: int | None = None
) -> int: ) -> 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: async def slip39_advanced_prompt_number_of_groups(ctx: GenericContext) -> int:
raise NotImplementedError 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( async def slip39_advanced_prompt_group_threshold(
ctx: wire.GenericContext, num_of_groups: int ctx: GenericContext, num_of_groups: int
) -> 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( await confirm_action(
ctx, ctx,
"backup_warning", "backup_warning",
"Caution", "Caution",
description="Never make a digital copy and never upload it online.", description=description,
verb="I understand", verb="I understand",
verb_cancel=None, verb_cancel=None,
br_code=ButtonRequestType.ResetDevice, br_code=ButtonRequestType.ResetDevice,