mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-07-08 15:48:08 +00:00
WIP - Shamir wallet creation
This commit is contained in:
parent
3c32a2097a
commit
473876c33f
BIN
core/assets/model_r/right_arrow_fat.png
Normal file
BIN
core/assets/model_r/right_arrow_fat.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 165 B |
BIN
core/assets/model_r/tick_fat.png
Normal file
BIN
core/assets/model_r/tick_fat.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 164 B |
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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,9 +92,13 @@ 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)) => {
|
||||||
|
if self.return_index {
|
||||||
|
Some(SimpleChoiceMsg::Index(page_counter))
|
||||||
|
} else {
|
||||||
let result = String::from(self.choices[page_counter as usize].as_ref());
|
let result = String::from(self.choices[page_counter as usize].as_ref());
|
||||||
Some(SimpleChoiceMsg::Result(result))
|
Some(SimpleChoiceMsg::Result(result))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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},
|
||||||
|
@ -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,32 +59,45 @@ 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
|
|
||||||
|
/// Display the second page with user information.
|
||||||
|
fn render_second_page(&self) {
|
||||||
|
// Creating a small vertical distance to make it centered
|
||||||
text_multiline(
|
text_multiline(
|
||||||
free_area.split_top(3).1,
|
self.area.split_top(15).1,
|
||||||
"Do NOT make\ndigital copies!",
|
"Do NOT make\ndigital copies!",
|
||||||
Font::MONO,
|
Font::MONO,
|
||||||
theme::FG,
|
theme::FG,
|
||||||
theme::BG,
|
theme::BG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Display the final page with user confirmation.
|
/// Display the final page with user confirmation.
|
||||||
fn render_final_page(&self) {
|
fn render_final_page(&self) {
|
||||||
@ -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);
|
||||||
|
@ -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(
|
||||||
/// *,
|
/// *,
|
||||||
|
BIN
core/embed/rust/src/ui/model_tr/res/arrow_right_fat.toif
Normal file
BIN
core/embed/rust/src/ui/model_tr/res/arrow_right_fat.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_tr/res/tick_fat.toif
Normal file
BIN
core/embed/rust/src/ui/model_tr/res/tick_fat.toif
Normal file
Binary file not shown.
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -21,8 +21,6 @@ def format_amount(amount: int, decimals: int) -> str:
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
if False:
|
|
||||||
|
|
||||||
def format_ordinal(number: int) -> str:
|
def format_ordinal(number: int) -> str:
|
||||||
return str(number) + {1: "st", 2: "nd", 3: "rd"}.get(
|
return str(number) + {1: "st", 2: "nd", 3: "rd"}.get(
|
||||||
4 if 10 <= number % 100 < 20 else number % 10, "th"
|
4 if 10 <= number % 100 < 20 else number % 10, "th"
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# All went well, we can break the loop.
|
# All went well, we can break the loop.
|
||||||
break
|
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,
|
||||||
|
Loading…
Reference in New Issue
Block a user