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:
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_usb_event;
|
||||
MP_QSTR_request_word_bip39;
|
||||
MP_QSTR_tutorial;
|
||||
|
||||
MP_QSTR_attach_timer_fn;
|
||||
|
@ -509,13 +509,15 @@ pub struct Checklist<T> {
|
||||
current: usize,
|
||||
icon_current: &'static [u8],
|
||||
icon_done: &'static [u8],
|
||||
/// How wide will the left icon column be
|
||||
check_width: i16,
|
||||
/// Offset of the icon representing DONE
|
||||
done_offset: Offset,
|
||||
/// Offset of the icon representing CURRENT
|
||||
current_offset: Offset,
|
||||
}
|
||||
|
||||
impl<T> Checklist<T> {
|
||||
const CHECK_WIDTH: i16 = 16;
|
||||
const DONE_OFFSET: Offset = Offset::new(-2, 6);
|
||||
const CURRENT_OFFSET: Offset = Offset::new(2, 3);
|
||||
|
||||
pub fn from_paragraphs(
|
||||
icon_current: &'static [u8],
|
||||
icon_done: &'static [u8],
|
||||
@ -528,9 +530,27 @@ impl<T> Checklist<T> {
|
||||
current,
|
||||
icon_current,
|
||||
icon_done,
|
||||
check_width: 0,
|
||||
done_offset: Offset::zero(),
|
||||
current_offset: Offset::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_check_width(mut self, check_width: i16) -> Self {
|
||||
self.check_width = check_width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_done_offset(mut self, done_offset: Offset) -> Self {
|
||||
self.done_offset = done_offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_current_offset(mut self, current_offset: Offset) -> Self {
|
||||
self.current_offset = current_offset;
|
||||
self
|
||||
}
|
||||
|
||||
fn paint_icon(&self, layout: &TextLayout, icon: &'static [u8], offset: Offset) {
|
||||
let top_left = Point::new(self.area.x0, layout.bounds.y0);
|
||||
display::icon_top_left(
|
||||
@ -550,7 +570,7 @@ where
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
let para_area = bounds.inset(Insets::left(Self::CHECK_WIDTH));
|
||||
let para_area = bounds.inset(Insets::left(self.check_width));
|
||||
self.paragraphs.place(para_area);
|
||||
self.area
|
||||
}
|
||||
@ -564,10 +584,10 @@ where
|
||||
|
||||
let current_visible = self.current.saturating_sub(self.paragraphs.offset.par);
|
||||
for layout in self.paragraphs.visible.iter().take(current_visible) {
|
||||
self.paint_icon(layout, self.icon_done, Self::DONE_OFFSET);
|
||||
self.paint_icon(layout, self.icon_done, self.done_offset);
|
||||
}
|
||||
if let Some(layout) = self.paragraphs.visible.iter().nth(current_visible) {
|
||||
self.paint_icon(layout, self.icon_current, Self::CURRENT_OFFSET);
|
||||
self.paint_icon(layout, self.icon_current, self.current_offset);
|
||||
}
|
||||
}
|
||||
|
||||
@ -577,6 +597,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Paginate for Checklist<T>
|
||||
where
|
||||
T: ParagraphSource,
|
||||
{
|
||||
fn page_count(&mut self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn change_page(&mut self, _to_page: usize) {}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T: ParagraphSource> crate::trace::Trace for Checklist<T> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
pub mod bip39;
|
||||
pub mod choice;
|
||||
pub mod choice_item;
|
||||
pub mod number_input;
|
||||
pub mod passphrase;
|
||||
pub mod pin;
|
||||
pub mod simple_choice;
|
||||
|
@ -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 {
|
||||
Result(String<50>),
|
||||
Index(u8),
|
||||
}
|
||||
|
||||
struct ChoiceFactorySimple<const N: usize> {
|
||||
@ -55,18 +56,29 @@ impl<const N: usize> ChoiceFactory for ChoiceFactorySimple<N> {
|
||||
pub struct SimpleChoice<const N: usize> {
|
||||
choices: Vec<StrBuffer, N>,
|
||||
choice_page: ChoicePage<ChoiceFactorySimple<N>>,
|
||||
return_index: bool,
|
||||
}
|
||||
|
||||
impl<const N: usize> SimpleChoice<N> {
|
||||
pub fn new(str_choices: Vec<StrBuffer, N>, carousel: bool, show_incomplete: bool) -> Self {
|
||||
pub fn new(str_choices: Vec<StrBuffer, N>, carousel: bool) -> Self {
|
||||
let choices = ChoiceFactorySimple::new(str_choices.clone(), carousel);
|
||||
Self {
|
||||
choices: str_choices,
|
||||
choice_page: ChoicePage::new(choices)
|
||||
.with_carousel(carousel)
|
||||
.with_incomplete(show_incomplete),
|
||||
choice_page: ChoicePage::new(choices).with_carousel(carousel),
|
||||
return_index: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show only the currently selected item, nothing left/right.
|
||||
pub fn with_only_one_item(mut self) -> Self {
|
||||
self.choice_page = self.choice_page.with_only_one_item(true);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_return_index(mut self) -> Self {
|
||||
self.return_index = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Component for SimpleChoice<N> {
|
||||
@ -80,9 +92,13 @@ impl<const N: usize> Component for SimpleChoice<N> {
|
||||
let msg = self.choice_page.event(ctx, event);
|
||||
match msg {
|
||||
Some(ChoicePageMsg::Choice(page_counter)) => {
|
||||
if self.return_index {
|
||||
Some(SimpleChoiceMsg::Index(page_counter))
|
||||
} else {
|
||||
let result = String::from(self.choices[page_counter as usize].as_ref());
|
||||
Some(SimpleChoiceMsg::Result(result))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ pub use input_methods::{
|
||||
bip39::{Bip39Entry, Bip39EntryMsg},
|
||||
choice::{Choice, ChoiceFactory, ChoicePage, ChoicePageMsg},
|
||||
choice_item::ChoiceItem,
|
||||
number_input::{NumberInput, NumberInputMsg},
|
||||
passphrase::{PassphraseEntry, PassphraseEntryMsg},
|
||||
pin::{PinEntry, PinEntryMsg},
|
||||
simple_choice::{SimpleChoice, SimpleChoiceMsg},
|
||||
|
@ -22,27 +22,33 @@ const WORD_FONT: Font = Font::NORMAL;
|
||||
/// Showing the given share words.
|
||||
pub struct ShareWords<const N: usize> {
|
||||
area: Rect,
|
||||
title: StrBuffer,
|
||||
share_words: Vec<StrBuffer, N>,
|
||||
page_index: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> ShareWords<N> {
|
||||
pub fn new(share_words: Vec<StrBuffer, N>) -> Self {
|
||||
pub fn new(title: StrBuffer, share_words: Vec<StrBuffer, N>) -> Self {
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
title,
|
||||
share_words,
|
||||
page_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn word_index(&self) -> usize {
|
||||
(self.page_index - 1) * WORDS_PER_PAGE
|
||||
(self.page_index - 2) * WORDS_PER_PAGE
|
||||
}
|
||||
|
||||
fn is_entry_page(&self) -> bool {
|
||||
self.page_index == 0
|
||||
}
|
||||
|
||||
fn is_second_page(&self) -> bool {
|
||||
self.page_index == 1
|
||||
}
|
||||
|
||||
fn is_final_page(&self) -> bool {
|
||||
self.page_index == self.total_page_count() - 1
|
||||
}
|
||||
@ -53,32 +59,45 @@ impl<const N: usize> ShareWords<N> {
|
||||
} else {
|
||||
self.share_words.len() / WORDS_PER_PAGE + 1
|
||||
};
|
||||
// One page before the words, one after it
|
||||
1 + word_screens + 1
|
||||
// Two pages before the words, one after it
|
||||
2 + word_screens + 1
|
||||
}
|
||||
|
||||
/// Display the first page with user information.
|
||||
fn render_entry_page(&self) {
|
||||
// TODO: will it be always 12, or do we need to check the length?
|
||||
// It would need creating a String out of it, which is not ideal.
|
||||
let free_area = text_multiline(
|
||||
self.area,
|
||||
"Write all 12\nwords in order on\nrecovery seed card",
|
||||
display(
|
||||
self.area
|
||||
.top_left()
|
||||
.ofs(Offset::y(Font::BOLD.line_height())),
|
||||
&self.title,
|
||||
Font::BOLD,
|
||||
);
|
||||
|
||||
text_multiline(
|
||||
self.area.split_top(15).1,
|
||||
&build_string!(
|
||||
50,
|
||||
"Write all ",
|
||||
inttostr!(self.share_words.len() as u8),
|
||||
"\nwords in order on\nrecovery seed card"
|
||||
),
|
||||
Font::BOLD,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
);
|
||||
if let Some(free_area) = free_area {
|
||||
// Creating a small vertical distance
|
||||
}
|
||||
|
||||
/// Display the second page with user information.
|
||||
fn render_second_page(&self) {
|
||||
// Creating a small vertical distance to make it centered
|
||||
text_multiline(
|
||||
free_area.split_top(3).1,
|
||||
self.area.split_top(15).1,
|
||||
"Do NOT make\ndigital copies!",
|
||||
Font::MONO,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Display the final page with user confirmation.
|
||||
fn render_final_page(&self) {
|
||||
@ -86,7 +105,12 @@ impl<const N: usize> ShareWords<N> {
|
||||
// and to look better.
|
||||
text_multiline(
|
||||
self.area.split_top(12).1,
|
||||
"I wrote down all\n12 words in order.",
|
||||
&build_string!(
|
||||
50,
|
||||
"I wrote down all\n",
|
||||
inttostr!(self.share_words.len() as u8),
|
||||
" words in order."
|
||||
),
|
||||
Font::MONO,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
@ -100,6 +124,9 @@ impl<const N: usize> ShareWords<N> {
|
||||
for i in 0..WORDS_PER_PAGE {
|
||||
y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT;
|
||||
let index = self.word_index() + i;
|
||||
if index >= self.share_words.len() {
|
||||
break;
|
||||
}
|
||||
let word = self.share_words[index];
|
||||
let baseline = self.area.top_left() + Offset::new(NUMBER_X_OFFSET, y_offset);
|
||||
display(baseline, &inttostr!(index as u8 + 1), NUMBER_FONT);
|
||||
@ -123,6 +150,8 @@ impl<const N: usize> Component for ShareWords<N> {
|
||||
fn paint(&mut self) {
|
||||
if self.is_entry_page() {
|
||||
self.render_entry_page();
|
||||
} else if self.is_second_page() {
|
||||
self.render_second_page();
|
||||
} else if self.is_final_page() {
|
||||
self.render_final_page();
|
||||
} else {
|
||||
@ -156,6 +185,9 @@ impl<const N: usize> crate::trace::Trace for ShareWords<N> {
|
||||
} else {
|
||||
for i in 0..WORDS_PER_PAGE {
|
||||
let index = self.word_index() + i;
|
||||
if index >= self.share_words.len() {
|
||||
break;
|
||||
}
|
||||
let word = self.share_words[index];
|
||||
let content = build_string!(20, inttostr!(index as u8 + 1), " ", &word, "\n");
|
||||
t.string(&content);
|
||||
|
@ -1,4 +1,4 @@
|
||||
use core::convert::TryInto;
|
||||
use core::{cmp::Ordering, convert::TryInto};
|
||||
|
||||
use heapless::Vec;
|
||||
|
||||
@ -20,7 +20,8 @@ use crate::{
|
||||
base::Component,
|
||||
paginated::{PageMsg, Paginate},
|
||||
text::paragraphs::{
|
||||
Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort, Paragraphs, VecExt,
|
||||
Checklist, Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort,
|
||||
Paragraphs, VecExt,
|
||||
},
|
||||
ComponentExt, Empty, Timeout, TimeoutMsg,
|
||||
},
|
||||
@ -38,8 +39,8 @@ use super::{
|
||||
component::{
|
||||
Bip39Entry, Bip39EntryMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow,
|
||||
FlowMsg, FlowPages, Frame, Homescreen, HomescreenMsg, Lockscreen, NoBtnDialog,
|
||||
NoBtnDialogMsg, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry, PinEntryMsg, Progress,
|
||||
ShareWords, SimpleChoice, SimpleChoiceMsg,
|
||||
NoBtnDialogMsg, NumberInput, NumberInputMsg, Page, PassphraseEntry, PassphraseEntryMsg,
|
||||
PinEntry, PinEntryMsg, Progress, ShareWords, SimpleChoice, SimpleChoiceMsg,
|
||||
},
|
||||
constant, theme,
|
||||
};
|
||||
@ -95,7 +96,7 @@ where
|
||||
match msg {
|
||||
FlowMsg::Confirmed => Ok(CONFIRMED.as_obj()),
|
||||
FlowMsg::Cancelled => Ok(CANCELLED.as_obj()),
|
||||
FlowMsg::ConfirmedIndex(page) => Ok(page.into()),
|
||||
FlowMsg::ConfirmedIndex(index) => Ok(index.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -109,10 +110,19 @@ impl ComponentMsgObj for PinEntry {
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentMsgObj for NumberInput {
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
NumberInputMsg::Number(choice) => choice.try_into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> ComponentMsgObj for SimpleChoice<N> {
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
SimpleChoiceMsg::Result(choice) => choice.as_str().try_into(),
|
||||
SimpleChoiceMsg::Index(index) => Ok(index.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -638,16 +648,77 @@ extern "C" fn new_request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map)
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn new_request_number(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||
let min_count: u32 = kwargs.get(Qstr::MP_QSTR_min_count)?.try_into()?;
|
||||
let max_count: u32 = kwargs.get(Qstr::MP_QSTR_max_count)?.try_into()?;
|
||||
let count: u32 = kwargs.get(Qstr::MP_QSTR_count)?.try_into()?;
|
||||
|
||||
let obj = LayoutObj::new(Frame::new(
|
||||
title,
|
||||
NumberInput::new(min_count, max_count, count),
|
||||
))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||
let _title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||
let button: StrBuffer = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?;
|
||||
let active: usize = kwargs.get(Qstr::MP_QSTR_active)?.try_into()?;
|
||||
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
|
||||
|
||||
let mut iter_buf = IterBuf::new();
|
||||
let mut paragraphs = ParagraphVecLong::new();
|
||||
let iter = Iter::try_from_obj_with_buf(items, &mut iter_buf)?;
|
||||
for (i, item) in iter.enumerate() {
|
||||
let style = match i.cmp(&active) {
|
||||
Ordering::Less => &theme::TEXT_MONO,
|
||||
Ordering::Equal => &theme::TEXT_BOLD,
|
||||
Ordering::Greater => &theme::TEXT_MONO,
|
||||
};
|
||||
let text: StrBuffer = item.try_into()?;
|
||||
paragraphs.add(Paragraph::new(style, text));
|
||||
}
|
||||
|
||||
let confirm_btn = Some(ButtonDetails::text(button));
|
||||
|
||||
let obj = LayoutObj::new(
|
||||
ButtonPage::new(
|
||||
Checklist::from_paragraphs(
|
||||
theme::ICON_ARROW_RIGHT_FAT.0,
|
||||
theme::ICON_TICK_FAT.0,
|
||||
active,
|
||||
paragraphs
|
||||
.into_paragraphs()
|
||||
.with_spacing(theme::CHECKLIST_SPACING),
|
||||
)
|
||||
.with_check_width(theme::CHECKLIST_CHECK_WIDTH)
|
||||
.with_current_offset(theme::CHECKLIST_CURRENT_OFFSET),
|
||||
theme::BG,
|
||||
)
|
||||
.with_confirm_btn(confirm_btn),
|
||||
)?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = |_args: &[Obj], kwargs: &Map| {
|
||||
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||
let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_share_words)?;
|
||||
let share_words: Vec<StrBuffer, 24> = iter_into_vec(share_words_obj)?;
|
||||
let share_words: Vec<StrBuffer, 33> = iter_into_vec(share_words_obj)?;
|
||||
|
||||
let confirm_btn =
|
||||
Some(ButtonDetails::text("HOLD TO CONFIRM".into()).with_default_duration());
|
||||
|
||||
let obj = LayoutObj::new(
|
||||
ButtonPage::new(ShareWords::new(share_words), theme::BG).with_confirm_btn(confirm_btn),
|
||||
ButtonPage::new(ShareWords::new(title, share_words), theme::BG)
|
||||
.with_confirm_btn(confirm_btn),
|
||||
)?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
@ -660,9 +731,15 @@ extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map)
|
||||
let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?;
|
||||
let words: Vec<StrBuffer, 3> = iter_into_vec(words_iterable)?;
|
||||
|
||||
// TODO: should return int, to be consistent with TT's select_word
|
||||
// Returning the index of the selected word, not the word itself
|
||||
let obj = LayoutObj::new(
|
||||
Frame::new(title, SimpleChoice::new(words, true, true)).with_title_center(true),
|
||||
Frame::new(
|
||||
title,
|
||||
SimpleChoice::new(words, false)
|
||||
.with_only_one_item()
|
||||
.with_return_index(),
|
||||
)
|
||||
.with_title_center(true),
|
||||
)?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
@ -674,17 +751,18 @@ extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mu
|
||||
let _dry_run: bool = kwargs.get(Qstr::MP_QSTR_dry_run)?.try_into()?;
|
||||
let title = "NUMBER OF WORDS".into();
|
||||
|
||||
let choices: Vec<StrBuffer, 3> = ["12".into(), "18".into(), "24".into()]
|
||||
let choices: Vec<StrBuffer, 5> = ["12", "18", "20", "24", "33"]
|
||||
.map(|num| num.into())
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let obj = LayoutObj::new(Frame::new(title, SimpleChoice::new(choices, false, false)))?;
|
||||
let obj = LayoutObj::new(Frame::new(title, SimpleChoice::new(choices, false)))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
extern "C" fn new_request_word_bip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
extern "C" fn new_request_bip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = |_args: &[Obj], kwargs: &Map| {
|
||||
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
||||
|
||||
@ -894,8 +972,30 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// """Request pin on device."""
|
||||
Qstr::MP_QSTR_request_pin => obj_fn_kw!(0, new_request_pin).as_obj(),
|
||||
|
||||
/// def request_number(
|
||||
/// *,
|
||||
/// title: str,
|
||||
/// count: int,
|
||||
/// min_count: int,
|
||||
/// max_count: int,
|
||||
/// ) -> object:
|
||||
/// """Number input with + and - buttons, description, and info button."""
|
||||
Qstr::MP_QSTR_request_number => obj_fn_kw!(0, new_request_number).as_obj(),
|
||||
|
||||
/// def show_checklist(
|
||||
/// *,
|
||||
/// title: str,
|
||||
/// items: Iterable[str],
|
||||
/// active: int,
|
||||
/// button: str,
|
||||
/// ) -> object:
|
||||
/// """Checklist of backup steps. Active index is highlighted, previous items have check
|
||||
/// mark next to them."""
|
||||
Qstr::MP_QSTR_show_checklist => obj_fn_kw!(0, new_show_checklist).as_obj(),
|
||||
|
||||
/// def show_share_words(
|
||||
/// *,
|
||||
/// title: str,
|
||||
/// share_words: Iterable[str],
|
||||
/// ) -> None:
|
||||
/// """Shows a backup seed."""
|
||||
@ -905,23 +1005,24 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// *,
|
||||
/// title: str,
|
||||
/// words: Iterable[str],
|
||||
/// ) -> str: # TODO: should return int, to be consistent with TT's select_word
|
||||
/// """Select a word from a list."""
|
||||
/// ) -> int:
|
||||
/// """Select mnemonic word from three possibilities - seed check after backup. The
|
||||
/// iterable must be of exact size. Returns index in range `0..3`."""
|
||||
Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(),
|
||||
|
||||
/// def select_word_count(
|
||||
/// *,
|
||||
/// dry_run: bool,
|
||||
/// ) -> str: # TODO: make it return int
|
||||
/// """Get word count for recovery."""
|
||||
/// """Select mnemonic word count from (12, 18, 20, 24, 33)."""
|
||||
Qstr::MP_QSTR_select_word_count => obj_fn_kw!(0, new_select_word_count).as_obj(),
|
||||
|
||||
/// def request_word_bip39(
|
||||
/// def request_bip39(
|
||||
/// *,
|
||||
/// prompt: str,
|
||||
/// ) -> str:
|
||||
/// """Get recovery word for BIP39."""
|
||||
Qstr::MP_QSTR_request_word_bip39 => obj_fn_kw!(0, new_request_word_bip39).as_obj(),
|
||||
Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(),
|
||||
|
||||
/// def request_passphrase(
|
||||
/// *,
|
||||
|
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::{
|
||||
component::text::TextStyle,
|
||||
display::{Color, Font, IconAndName},
|
||||
geometry::Offset,
|
||||
};
|
||||
|
||||
// Color palette.
|
||||
@ -33,6 +34,10 @@ pub const ICON_ARROW_LEFT: IconAndName =
|
||||
IconAndName::new(include_res!("model_tr/res/arrow_left.toif"), "arrow_left"); // 6*10
|
||||
pub const ICON_ARROW_RIGHT: IconAndName =
|
||||
IconAndName::new(include_res!("model_tr/res/arrow_right.toif"), "arrow_right"); // 6*10
|
||||
pub const ICON_ARROW_RIGHT_FAT: IconAndName = IconAndName::new(
|
||||
include_res!("model_tr/res/arrow_right_fat.toif"),
|
||||
"arrow_right_fat",
|
||||
); // 4*8
|
||||
pub const ICON_ARROW_UP: IconAndName =
|
||||
IconAndName::new(include_res!("model_tr/res/arrow_up.toif"), "arrow_up"); // 10*6
|
||||
pub const ICON_ARROW_DOWN: IconAndName =
|
||||
@ -54,9 +59,16 @@ pub const ICON_PREV_PAGE: IconAndName =
|
||||
pub const ICON_SUCCESS: IconAndName =
|
||||
IconAndName::new(include_res!("model_tr/res/success.toif"), "success");
|
||||
pub const ICON_TICK: IconAndName = IconAndName::new(include_res!("model_tr/res/tick.toif"), "tick"); // 10*10
|
||||
pub const ICON_TICK_FAT: IconAndName =
|
||||
IconAndName::new(include_res!("model_tr/res/tick_fat.toif"), "tick_fat"); // 8*6
|
||||
pub const ICON_WARNING: IconAndName =
|
||||
IconAndName::new(include_res!("model_tr/res/warning.toif"), "warning"); // 12*12
|
||||
|
||||
// checklist settings
|
||||
pub const CHECKLIST_SPACING: i16 = 5;
|
||||
pub const CHECKLIST_CHECK_WIDTH: i16 = 12;
|
||||
pub const CHECKLIST_CURRENT_OFFSET: Offset = Offset::x(3);
|
||||
|
||||
// Button height is constant for both text and icon buttons.
|
||||
// It is a combination of content and (optional) outline/border.
|
||||
// It is not possible to have icons 7*7, therefore having 8*8
|
||||
|
@ -1094,7 +1094,10 @@ extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut M
|
||||
paragraphs
|
||||
.into_paragraphs()
|
||||
.with_spacing(theme::CHECKLIST_SPACING),
|
||||
),
|
||||
)
|
||||
.with_check_width(theme::CHECKLIST_CHECK_WIDTH)
|
||||
.with_current_offset(theme::CHECKLIST_CURRENT_OFFSET)
|
||||
.with_done_offset(theme::CHECKLIST_DONE_OFFSET),
|
||||
theme::button_bar(Button::with_text(button).map(|msg| {
|
||||
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
|
||||
})),
|
||||
@ -1590,7 +1593,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// button: str,
|
||||
/// ) -> object:
|
||||
/// """Checklist of backup steps. Active index is highlighted, previous items have check
|
||||
/// mark nex to them."""
|
||||
/// mark next to them."""
|
||||
Qstr::MP_QSTR_show_checklist => obj_fn_kw!(0, new_show_checklist).as_obj(),
|
||||
|
||||
/// def confirm_recovery(
|
||||
|
@ -6,7 +6,7 @@ use crate::{
|
||||
FixedHeightBar,
|
||||
},
|
||||
display::{Color, Font},
|
||||
geometry::Insets,
|
||||
geometry::{Insets, Offset},
|
||||
},
|
||||
};
|
||||
|
||||
@ -413,6 +413,11 @@ pub const BUTTON_SPACING: i16 = 6;
|
||||
pub const CHECKLIST_SPACING: i16 = 10;
|
||||
pub const RECOVERY_SPACING: i16 = 18;
|
||||
|
||||
// checklist settings
|
||||
pub const CHECKLIST_CHECK_WIDTH: i16 = 16;
|
||||
pub const CHECKLIST_DONE_OFFSET: Offset = Offset::new(-2, 6);
|
||||
pub const CHECKLIST_CURRENT_OFFSET: Offset = Offset::new(2, 3);
|
||||
|
||||
/// Standard button height in pixels.
|
||||
pub const fn button_rows(count: usize) -> i16 {
|
||||
let count = count as i16;
|
||||
|
@ -108,6 +108,18 @@ def request_pin(
|
||||
"""Request pin on device."""
|
||||
|
||||
|
||||
# rust/src/ui/model_tr/layout.rs
|
||||
def show_checklist(
|
||||
*,
|
||||
title: str,
|
||||
items: Iterable[str],
|
||||
active: int,
|
||||
button: str,
|
||||
) -> object:
|
||||
"""Checklist of backup steps. Active index is highlighted, previous items have check
|
||||
mark next to them."""
|
||||
|
||||
|
||||
# rust/src/ui/model_tr/layout.rs
|
||||
def show_share_words(
|
||||
*,
|
||||
@ -130,11 +142,11 @@ def select_word_count(
|
||||
*,
|
||||
dry_run: bool,
|
||||
) -> str: # TODO: make it return int
|
||||
"""Get word count for recovery."""
|
||||
"""Select mnemonic word count from (12, 18, 20, 24, 33)."""
|
||||
|
||||
|
||||
# rust/src/ui/model_tr/layout.rs
|
||||
def request_word_bip39(
|
||||
def request_bip39(
|
||||
*,
|
||||
prompt: str,
|
||||
) -> str:
|
||||
@ -503,7 +515,7 @@ def show_checklist(
|
||||
button: str,
|
||||
) -> object:
|
||||
"""Checklist of backup steps. Active index is highlighted, previous items have check
|
||||
mark nex to them."""
|
||||
mark next to them."""
|
||||
|
||||
|
||||
# rust/src/ui/model_tt/layout.rs
|
||||
|
@ -21,9 +21,7 @@ def format_amount(amount: int, decimals: int) -> str:
|
||||
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(
|
||||
4 if 10 <= number % 100 < 20 else number % 10, "th"
|
||||
)
|
||||
|
@ -751,7 +751,6 @@ def show_warning(
|
||||
ctx: GenericContext,
|
||||
br_type: str,
|
||||
content: str,
|
||||
header: str = "Warning",
|
||||
subheader: str | None = None,
|
||||
button: str = "Try again",
|
||||
br_code: ButtonRequestType = ButtonRequestType.Warning,
|
||||
@ -759,8 +758,8 @@ def show_warning(
|
||||
return _show_modal(
|
||||
ctx,
|
||||
br_type,
|
||||
header,
|
||||
subheader,
|
||||
"",
|
||||
subheader or "Warning",
|
||||
content,
|
||||
button_confirm=button,
|
||||
button_cancel=None,
|
||||
|
@ -34,7 +34,7 @@ async def request_word(
|
||||
else:
|
||||
word = await interact(
|
||||
ctx,
|
||||
RustLayout(trezorui2.request_word_bip39(prompt=prompt)),
|
||||
RustLayout(trezorui2.request_bip39(prompt=prompt)),
|
||||
"request_word",
|
||||
ButtonRequestType.MnemonicInput,
|
||||
)
|
||||
|
@ -1,115 +1,276 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from trezor.enums import ButtonRequestType
|
||||
from trezor.wire import ActionCancelled
|
||||
|
||||
import trezorui2
|
||||
|
||||
from ..common import interact
|
||||
from . import RustLayout, confirm_action, get_bool
|
||||
from . import RustLayout, confirm_action
|
||||
|
||||
CONFIRMED = trezorui2.CONFIRMED # global_import_cache
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from trezor import wire
|
||||
from trezor.wire import GenericContext
|
||||
from trezor.enums import BackupType
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
async def show_share_words(
|
||||
ctx: wire.GenericContext,
|
||||
ctx: GenericContext,
|
||||
share_words: Sequence[str],
|
||||
share_index: int | None = None,
|
||||
group_index: int | None = None,
|
||||
) -> None:
|
||||
from . import get_bool
|
||||
|
||||
if share_index is None:
|
||||
title = "RECOVERY SEED"
|
||||
elif group_index is None:
|
||||
title = f"SHARE #{share_index + 1}"
|
||||
else:
|
||||
title = f"G{group_index + 1} - SHARE {share_index + 1}"
|
||||
|
||||
# Showing words, asking for write down confirmation and preparing for check
|
||||
# until user accepts everything.
|
||||
while True:
|
||||
await interact(
|
||||
ctx,
|
||||
RustLayout(
|
||||
trezorui2.show_share_words( # type: ignore [Arguments missing for parameters "title", "pages"]
|
||||
share_words=share_words # type: ignore [No parameter named "share_words"]
|
||||
trezorui2.show_share_words( # type: ignore [Argument missing for parameter "pages"]
|
||||
title=title,
|
||||
share_words=share_words, # type: ignore [No parameter named "share_words"]
|
||||
)
|
||||
),
|
||||
"backup_words",
|
||||
ButtonRequestType.ResetDevice,
|
||||
)
|
||||
|
||||
ready_to_check = await get_bool(
|
||||
if share_index is None:
|
||||
check_title = "CHECK SEED"
|
||||
elif group_index is None:
|
||||
check_title = f"CHECK SHARE #{share_index + 1}"
|
||||
else:
|
||||
check_title = f"CHECK G{group_index + 1} - SHARE {share_index + 1}"
|
||||
|
||||
if await get_bool(
|
||||
ctx,
|
||||
"backup_words",
|
||||
"CHECK BACKUP",
|
||||
check_title,
|
||||
None,
|
||||
"Select correct words in correct positions.",
|
||||
verb_cancel="SEE AGAIN",
|
||||
verb="BEGIN",
|
||||
br_code=ButtonRequestType.ResetDevice,
|
||||
)
|
||||
if not ready_to_check:
|
||||
continue
|
||||
|
||||
):
|
||||
# All went well, we can break the loop.
|
||||
break
|
||||
|
||||
|
||||
async def select_word(
|
||||
ctx: wire.GenericContext,
|
||||
ctx: GenericContext,
|
||||
words: Sequence[str],
|
||||
share_index: int | None,
|
||||
checked_index: int,
|
||||
count: int,
|
||||
group_index: int | None = None,
|
||||
) -> str:
|
||||
from trezor.strings import format_ordinal
|
||||
|
||||
# TODO: it might not always be 3 words, it can happen it will be only two,
|
||||
# but the probability is very small - 4 words containing two items two times
|
||||
# (or one in "all all" seed)
|
||||
assert len(words) == 3
|
||||
result = await ctx.wait(
|
||||
RustLayout(
|
||||
trezorui2.select_word( # type: ignore [Argument missing for parameter "description"]
|
||||
title=f"SELECT WORD {checked_index + 1}/{count}",
|
||||
title=f"SELECT {format_ordinal(checked_index + 1).upper()} WORD",
|
||||
words=(words[0].upper(), words[1].upper(), words[2].upper()),
|
||||
)
|
||||
)
|
||||
)
|
||||
for word in words:
|
||||
if word.upper() == result:
|
||||
return word
|
||||
raise ValueError("Invalid word")
|
||||
if __debug__ and isinstance(result, str):
|
||||
return result
|
||||
assert isinstance(result, int) and 0 <= result <= 2
|
||||
return words[result]
|
||||
|
||||
|
||||
async def slip39_show_checklist(
|
||||
ctx: wire.GenericContext, step: int, backup_type: BackupType
|
||||
ctx: GenericContext, step: int, backup_type: BackupType
|
||||
) -> None:
|
||||
raise NotImplementedError
|
||||
from trezor.enums import BackupType
|
||||
|
||||
assert backup_type in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced)
|
||||
|
||||
items = (
|
||||
(
|
||||
"Number of shares",
|
||||
"Set threshold",
|
||||
"Write down and check all shares",
|
||||
)
|
||||
if backup_type == BackupType.Slip39_Basic
|
||||
else (
|
||||
"Number of groups",
|
||||
"Number of shares",
|
||||
"Set sizes and thresholds",
|
||||
)
|
||||
)
|
||||
|
||||
result = await interact(
|
||||
ctx,
|
||||
RustLayout(
|
||||
trezorui2.show_checklist(
|
||||
title="BACKUP CHECKLIST",
|
||||
button="CONTINUE",
|
||||
active=step,
|
||||
items=items,
|
||||
)
|
||||
),
|
||||
"slip39_checklist",
|
||||
ButtonRequestType.ResetDevice,
|
||||
)
|
||||
if result != CONFIRMED:
|
||||
raise ActionCancelled
|
||||
|
||||
|
||||
async def _prompt_number(
|
||||
ctx: GenericContext,
|
||||
title: str,
|
||||
count: int,
|
||||
min_count: int,
|
||||
max_count: int,
|
||||
br_name: str,
|
||||
) -> int:
|
||||
num_input = RustLayout(
|
||||
trezorui2.request_number( # type: ignore [Argument missing for parameter "description"]
|
||||
title=title.upper(),
|
||||
count=count,
|
||||
min_count=min_count,
|
||||
max_count=max_count,
|
||||
)
|
||||
)
|
||||
|
||||
result = await interact(
|
||||
ctx,
|
||||
num_input,
|
||||
br_name,
|
||||
ButtonRequestType.ResetDevice,
|
||||
)
|
||||
|
||||
assert isinstance(result, int)
|
||||
return result
|
||||
|
||||
|
||||
async def slip39_prompt_threshold(
|
||||
ctx: wire.GenericContext, num_of_shares: int, group_id: int | None = None
|
||||
ctx: GenericContext, num_of_shares: int, group_id: int | None = None
|
||||
) -> int:
|
||||
raise NotImplementedError
|
||||
await confirm_action(
|
||||
ctx,
|
||||
"slip39_prompt_threshold",
|
||||
"Set threshold",
|
||||
description="= number of shares needed for recovery",
|
||||
verb="BEGIN",
|
||||
verb_cancel=None,
|
||||
)
|
||||
|
||||
count = num_of_shares // 2 + 1
|
||||
# min value of share threshold is 2 unless the number of shares is 1
|
||||
# number of shares 1 is possible in advanced slip39
|
||||
min_count = min(2, num_of_shares)
|
||||
max_count = num_of_shares
|
||||
|
||||
if group_id is not None:
|
||||
title = f"THRESHOLD - GROUP {group_id + 1}"
|
||||
else:
|
||||
title = "SET THRESHOLD"
|
||||
|
||||
return await _prompt_number(
|
||||
ctx,
|
||||
title,
|
||||
count,
|
||||
min_count,
|
||||
max_count,
|
||||
"slip39_threshold",
|
||||
)
|
||||
|
||||
|
||||
async def slip39_prompt_number_of_shares(
|
||||
ctx: wire.GenericContext, group_id: int | None = None
|
||||
ctx: GenericContext, group_id: int | None = None
|
||||
) -> int:
|
||||
raise NotImplementedError
|
||||
await confirm_action(
|
||||
ctx,
|
||||
"slip39_shares",
|
||||
"Number of shares",
|
||||
description="= total number of unique word lists used for wallet backup.",
|
||||
verb="BEGIN",
|
||||
verb_cancel=None,
|
||||
)
|
||||
|
||||
count = 5
|
||||
min_count = 1
|
||||
max_count = 16
|
||||
|
||||
if group_id is not None:
|
||||
title = f"# SHARES - GROUP {group_id + 1}"
|
||||
else:
|
||||
title = "NUMBER OF SHARES"
|
||||
|
||||
return await _prompt_number(
|
||||
ctx,
|
||||
title,
|
||||
count,
|
||||
min_count,
|
||||
max_count,
|
||||
"slip39_shares",
|
||||
)
|
||||
|
||||
|
||||
async def slip39_advanced_prompt_number_of_groups(ctx: wire.GenericContext) -> int:
|
||||
raise NotImplementedError
|
||||
async def slip39_advanced_prompt_number_of_groups(ctx: GenericContext) -> int:
|
||||
count = 5
|
||||
min_count = 2
|
||||
max_count = 16
|
||||
|
||||
return await _prompt_number(
|
||||
ctx,
|
||||
"NUMBER OF GROUPS",
|
||||
count,
|
||||
min_count,
|
||||
max_count,
|
||||
"slip39_groups",
|
||||
)
|
||||
|
||||
|
||||
async def slip39_advanced_prompt_group_threshold(
|
||||
ctx: wire.GenericContext, num_of_groups: int
|
||||
ctx: GenericContext, num_of_groups: int
|
||||
) -> int:
|
||||
raise NotImplementedError
|
||||
count = num_of_groups // 2 + 1
|
||||
min_count = 1
|
||||
max_count = num_of_groups
|
||||
|
||||
return await _prompt_number(
|
||||
ctx,
|
||||
"GROUP THRESHOLD",
|
||||
count,
|
||||
min_count,
|
||||
max_count,
|
||||
"slip39_group_threshold",
|
||||
)
|
||||
|
||||
|
||||
async def show_warning_backup(ctx: wire.GenericContext, slip39: bool) -> None:
|
||||
async def show_warning_backup(ctx: GenericContext, slip39: bool) -> None:
|
||||
if slip39:
|
||||
description = (
|
||||
"Never make a digital copy of your shares and never upload them online."
|
||||
)
|
||||
else:
|
||||
description = (
|
||||
"Never make a digital copy of your seed and never upload it online."
|
||||
)
|
||||
|
||||
await confirm_action(
|
||||
ctx,
|
||||
"backup_warning",
|
||||
"Caution",
|
||||
description="Never make a digital copy and never upload it online.",
|
||||
description=description,
|
||||
verb="I understand",
|
||||
verb_cancel=None,
|
||||
br_code=ButtonRequestType.ResetDevice,
|
||||
|
Loading…
Reference in New Issue
Block a user