mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-30 17:21:21 +00:00
feat(core): T3T1 ShareWords component
This commit is contained in:
parent
8a5afbe585
commit
8978f36096
@ -2,7 +2,6 @@ use crate::{
|
|||||||
strutil::TString,
|
strutil::TString,
|
||||||
ui::{
|
ui::{
|
||||||
component::{text::TextStyle, Component, Event, EventCtx, Never},
|
component::{text::TextStyle, Component, Event, EventCtx, Never},
|
||||||
constant::WIDTH,
|
|
||||||
geometry::{Alignment, Offset, Rect},
|
geometry::{Alignment, Offset, Rect},
|
||||||
model_mercury::theme,
|
model_mercury::theme,
|
||||||
shape::{Renderer, Text},
|
shape::{Renderer, Text},
|
||||||
@ -10,9 +9,9 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Component showing a task instruction (e.g. "Swipe up") and optionally task
|
/// Component showing a task instruction (e.g. "Swipe up") and optionally task
|
||||||
/// description (e.g. "Confirm transaction") to a user. The component
|
/// description (e.g. "Confirm transaction") to a user. A host of this component
|
||||||
/// is typically placed at the bottom of the screen. The height of the provided
|
/// is responsible of providing the exact area considering also the spacing. The
|
||||||
/// area must be 18px (only instruction) or 37px (both description and
|
/// height must be 18px (only instruction) or 37px (both description and
|
||||||
/// instruction). The content and style of both description and instruction is
|
/// instruction). The content and style of both description and instruction is
|
||||||
/// configurable separatedly.
|
/// configurable separatedly.
|
||||||
pub struct Footer<'a> {
|
pub struct Footer<'a> {
|
||||||
@ -26,7 +25,7 @@ pub struct Footer<'a> {
|
|||||||
impl<'a> Footer<'a> {
|
impl<'a> Footer<'a> {
|
||||||
/// height of the component with only instruction [px]
|
/// height of the component with only instruction [px]
|
||||||
pub const HEIGHT_SIMPLE: i16 = 18;
|
pub const HEIGHT_SIMPLE: i16 = 18;
|
||||||
/// height for component with both description and instruction [px]
|
/// height of the component with both description and instruction [px]
|
||||||
pub const HEIGHT_DEFAULT: i16 = 37;
|
pub const HEIGHT_DEFAULT: i16 = 37;
|
||||||
|
|
||||||
pub fn new<T: Into<TString<'a>>>(instruction: T) -> Self {
|
pub fn new<T: Into<TString<'a>>>(instruction: T) -> Self {
|
||||||
|
@ -127,8 +127,9 @@ where
|
|||||||
|
|
||||||
fn place(&mut self, bounds: Rect) -> Rect {
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
let (mut header_area, content_area) = bounds.split_top(TITLE_HEIGHT);
|
let (mut header_area, content_area) = bounds.split_top(TITLE_HEIGHT);
|
||||||
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
|
let content_area = content_area.inset(Insets::top(SPACING));
|
||||||
|
|
||||||
|
header_area = header_area.inset(Insets::sides(SPACING));
|
||||||
if let Some(b) = &mut self.button {
|
if let Some(b) = &mut self.button {
|
||||||
let (rest, button_area) = header_area.split_right(TITLE_HEIGHT);
|
let (rest, button_area) = header_area.split_right(TITLE_HEIGHT);
|
||||||
header_area = rest;
|
header_area = rest;
|
||||||
|
@ -12,6 +12,10 @@ mod error;
|
|||||||
mod frame;
|
mod frame;
|
||||||
mod loader;
|
mod loader;
|
||||||
mod result;
|
mod result;
|
||||||
|
mod scroll;
|
||||||
|
mod share_words;
|
||||||
|
mod simple_page;
|
||||||
|
mod swipe;
|
||||||
mod welcome_screen;
|
mod welcome_screen;
|
||||||
|
|
||||||
pub use button::{
|
pub use button::{
|
||||||
@ -33,6 +37,7 @@ pub use keyboard::{
|
|||||||
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
|
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
|
||||||
pub use result::{ResultFooter, ResultScreen, ResultStyle};
|
pub use result::{ResultFooter, ResultScreen, ResultStyle};
|
||||||
pub use scroll::ScrollBar;
|
pub use scroll::ScrollBar;
|
||||||
|
pub use share_words::ShareWords;
|
||||||
pub use simple_page::SimplePage;
|
pub use simple_page::SimplePage;
|
||||||
pub use swipe::{Swipe, SwipeDirection};
|
pub use swipe::{Swipe, SwipeDirection};
|
||||||
pub use vertical_menu::{VerticalMenu, VerticalMenuChoiceMsg};
|
pub use vertical_menu::{VerticalMenu, VerticalMenuChoiceMsg};
|
||||||
|
@ -20,3 +20,8 @@ pub const fn screen() -> Rect {
|
|||||||
Rect::from_top_left_and_size(Point::zero(), SIZE)
|
Rect::from_top_left_and_size(Point::zero(), SIZE)
|
||||||
}
|
}
|
||||||
pub const SCREEN: Rect = screen();
|
pub const SCREEN: Rect = screen();
|
||||||
|
|
||||||
|
/// Spacing between components (e.g. header and main content) and offsets from
|
||||||
|
/// the side of the screen. Generally applied everywhere except the top side of
|
||||||
|
/// the header. [px]
|
||||||
|
pub const SPACING: i16 = 2;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use core::{cmp::Ordering, convert::TryInto};
|
use core::{cmp::Ordering, convert::TryInto};
|
||||||
|
use heapless::Vec;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
@ -52,8 +53,8 @@ use super::{
|
|||||||
FidoMsg, Frame, FrameMsg, Homescreen, HomescreenMsg, IconDialog, Lockscreen, MnemonicInput,
|
FidoMsg, Frame, FrameMsg, Homescreen, HomescreenMsg, IconDialog, Lockscreen, MnemonicInput,
|
||||||
MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputDialog, NumberInputDialogMsg,
|
MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputDialog, NumberInputDialogMsg,
|
||||||
PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress,
|
PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress,
|
||||||
SelectWordCount, SelectWordCountMsg, SelectWordMsg, SimplePage, Slip39Input, VerticalMenu,
|
SelectWordCount, SelectWordCountMsg, SelectWordMsg, ShareWords, SimplePage, Slip39Input,
|
||||||
VerticalMenuChoiceMsg,
|
VerticalMenu, VerticalMenuChoiceMsg,
|
||||||
},
|
},
|
||||||
theme,
|
theme,
|
||||||
};
|
};
|
||||||
@ -206,6 +207,15 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ComponentMsgObj for ShareWords<'_> {
|
||||||
|
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||||
|
match msg {
|
||||||
|
PageMsg::Confirmed => Ok(CONFIRMED.as_obj()),
|
||||||
|
_ => Err(Error::TypeError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> ComponentMsgObj for VerticalMenu<T>
|
impl<T> ComponentMsgObj for VerticalMenu<T>
|
||||||
where
|
where
|
||||||
T: AsRef<str>,
|
T: AsRef<str>,
|
||||||
@ -1350,20 +1360,12 @@ extern "C" fn new_show_tx_context_menu(n_args: usize, args: *const Obj, kwargs:
|
|||||||
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 = move |_args: &[Obj], kwargs: &Map| {
|
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||||
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||||
let pages: Obj = kwargs.get(Qstr::MP_QSTR_pages)?;
|
let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_pages)?;
|
||||||
|
let share_words_vec: Vec<TString, 33> = util::iter_into_vec(share_words_obj)?;
|
||||||
|
|
||||||
let mut paragraphs = ParagraphVecLong::new();
|
let share_words = ShareWords::new(share_words_vec);
|
||||||
for page in IterBuf::new().try_iterate(pages)? {
|
let frame_with_share_words = Frame::left_aligned(title, share_words).with_info_button();
|
||||||
let text: StrBuffer = page.try_into()?;
|
let obj = LayoutObj::new(frame_with_share_words)?;
|
||||||
paragraphs.add(Paragraph::new(&theme::TEXT_MONO, text).break_after());
|
|
||||||
}
|
|
||||||
|
|
||||||
let obj = LayoutObj::new(Frame::left_aligned(
|
|
||||||
title,
|
|
||||||
ButtonPage::<_, StrBuffer>::new(paragraphs.into_paragraphs(), theme::BG)
|
|
||||||
.with_hold()?
|
|
||||||
.without_cancel(),
|
|
||||||
))?;
|
|
||||||
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) }
|
||||||
@ -2081,7 +2083,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
|||||||
/// title: str,
|
/// title: str,
|
||||||
/// pages: Iterable[str],
|
/// pages: Iterable[str],
|
||||||
/// ) -> LayoutObj[UiResult]:
|
/// ) -> LayoutObj[UiResult]:
|
||||||
/// """Show mnemonic for backup. Expects the words pre-divided into individual pages."""
|
/// """Show mnemonic for backup."""
|
||||||
Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, new_show_share_words).as_obj(),
|
Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, new_show_share_words).as_obj(),
|
||||||
|
|
||||||
/// def request_number(
|
/// def request_number(
|
||||||
|
@ -96,7 +96,7 @@ impl Shape<'_> for CornerHighlight {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&mut self, canvas: &mut dyn Canvas, cache: &DrawingCache<'_>) {
|
fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache<'_>) {
|
||||||
let align: Alignment2D = self.corner.into();
|
let align: Alignment2D = self.corner.into();
|
||||||
|
|
||||||
// base circle
|
// base circle
|
||||||
@ -150,7 +150,7 @@ impl Shape<'_> for CornerHighlight {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cleanup(&mut self, cache: &super::DrawingCache<'_>) {}
|
fn cleanup(&mut self, _cache: &super::DrawingCache<'_>) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'s> ShapeClone<'s> for CornerHighlight {
|
impl<'s> ShapeClone<'s> for CornerHighlight {
|
||||||
|
@ -401,7 +401,7 @@ def show_share_words(
|
|||||||
title: str,
|
title: str,
|
||||||
pages: Iterable[str],
|
pages: Iterable[str],
|
||||||
) -> LayoutObj[UiResult]:
|
) -> LayoutObj[UiResult]:
|
||||||
"""Show mnemonic for backup. Expects the words pre-divided into individual pages."""
|
"""Show mnemonic for backup."""
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_mercury/layout.rs
|
# rust/src/ui/model_mercury/layout.rs
|
||||||
|
@ -18,30 +18,6 @@ if TYPE_CHECKING:
|
|||||||
CONFIRMED = trezorui2.CONFIRMED # global_import_cache
|
CONFIRMED = trezorui2.CONFIRMED # global_import_cache
|
||||||
|
|
||||||
|
|
||||||
def _split_share_into_pages(share_words: Sequence[str], per_page: int = 4) -> list[str]:
|
|
||||||
pages: list[str] = []
|
|
||||||
current = ""
|
|
||||||
fill = 2
|
|
||||||
|
|
||||||
for i, word in enumerate(share_words):
|
|
||||||
if i % per_page == 0:
|
|
||||||
if i != 0:
|
|
||||||
pages.append(current)
|
|
||||||
current = ""
|
|
||||||
|
|
||||||
# Align numbers to the right.
|
|
||||||
lastnum = i + per_page + 1
|
|
||||||
fill = 1 if lastnum < 10 else 2
|
|
||||||
else:
|
|
||||||
current += "\n"
|
|
||||||
current += f"{i + 1:>{fill}}. {word}"
|
|
||||||
|
|
||||||
if current:
|
|
||||||
pages.append(current)
|
|
||||||
|
|
||||||
return pages
|
|
||||||
|
|
||||||
|
|
||||||
async def show_share_words(
|
async def show_share_words(
|
||||||
share_words: Sequence[str],
|
share_words: Sequence[str],
|
||||||
share_index: int | None = None,
|
share_index: int | None = None,
|
||||||
@ -56,13 +32,11 @@ async def show_share_words(
|
|||||||
group_index + 1, share_index + 1
|
group_index + 1, share_index + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
pages = _split_share_into_pages(share_words)
|
|
||||||
|
|
||||||
result = await interact(
|
result = await interact(
|
||||||
RustLayout(
|
RustLayout(
|
||||||
trezorui2.show_share_words(
|
trezorui2.show_share_words(
|
||||||
title=title,
|
title=title,
|
||||||
pages=pages,
|
pages=share_words,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
"backup_words",
|
"backup_words",
|
||||||
@ -80,11 +54,11 @@ async def select_word(
|
|||||||
group_index: int | None = None,
|
group_index: int | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
if share_index is None:
|
if share_index is None:
|
||||||
title: str = TR.reset__check_seed_title
|
description: str = TR.reset__check_seed_title
|
||||||
elif group_index is None:
|
elif group_index is None:
|
||||||
title = TR.reset__check_share_title_template.format(share_index + 1)
|
description: str = TR.reset__check_share_title_template.format(share_index + 1)
|
||||||
else:
|
else:
|
||||||
title = TR.reset__check_group_share_title_template.format(
|
description: str = TR.reset__check_group_share_title_template.format(
|
||||||
group_index + 1, share_index + 1
|
group_index + 1, share_index + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -98,10 +72,10 @@ async def select_word(
|
|||||||
result = await ctx_wait(
|
result = await ctx_wait(
|
||||||
RustLayout(
|
RustLayout(
|
||||||
trezorui2.select_word(
|
trezorui2.select_word(
|
||||||
title=title,
|
title=TR.reset__select_word_x_of_y_template.format(
|
||||||
description=TR.reset__select_word_x_of_y_template.format(
|
|
||||||
checked_index + 1, count
|
checked_index + 1, count
|
||||||
),
|
),
|
||||||
|
description=description,
|
||||||
words=(words[0], words[1], words[2]),
|
words=(words[0], words[1], words[2]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -181,24 +181,9 @@ def click_through(
|
|||||||
def read_and_confirm_mnemonic(
|
def read_and_confirm_mnemonic(
|
||||||
debug: "DebugLink", choose_wrong: bool = False
|
debug: "DebugLink", choose_wrong: bool = False
|
||||||
) -> Generator[None, "ButtonRequest", Optional[str]]:
|
) -> Generator[None, "ButtonRequest", Optional[str]]:
|
||||||
# TODO: these are very similar, reuse some code
|
"""Read a given number of mnemonic words from the screen and answer
|
||||||
if debug.model is models.T2T1:
|
confirmation questions.
|
||||||
mnemonic = yield from read_and_confirm_mnemonic_tt(debug, choose_wrong)
|
Return the full mnemonic or None if `choose_wrong` is True.
|
||||||
elif debug.model is models.T2B1:
|
|
||||||
mnemonic = yield from read_and_confirm_mnemonic_tr(debug, choose_wrong)
|
|
||||||
elif debug.model is models.T3T1:
|
|
||||||
mnemonic = yield from read_and_confirm_mnemonic_tt(debug, choose_wrong)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown model: {debug.model}")
|
|
||||||
|
|
||||||
return mnemonic
|
|
||||||
|
|
||||||
|
|
||||||
def read_and_confirm_mnemonic_tt(
|
|
||||||
debug: "DebugLink", choose_wrong: bool = False
|
|
||||||
) -> Generator[None, "ButtonRequest", Optional[str]]:
|
|
||||||
"""Read a given number of mnemonic words from Trezor T screen and correctly
|
|
||||||
answer confirmation questions. Return the full mnemonic.
|
|
||||||
|
|
||||||
For use in an input flow function.
|
For use in an input flow function.
|
||||||
Example:
|
Example:
|
||||||
@ -208,6 +193,23 @@ def read_and_confirm_mnemonic_tt(
|
|||||||
|
|
||||||
mnemonic = yield from read_and_confirm_mnemonic(client.debug)
|
mnemonic = yield from read_and_confirm_mnemonic(client.debug)
|
||||||
"""
|
"""
|
||||||
|
if debug.model is models.T2T1:
|
||||||
|
mnemonic = yield from read_mnemonic_from_screen_tt(debug)
|
||||||
|
elif debug.model is models.T2B1:
|
||||||
|
mnemonic = yield from read_mnemonic_from_screen_tr(debug)
|
||||||
|
elif debug.model is models.T3T1:
|
||||||
|
mnemonic = yield from read_mnemonic_from_screen_mercury(debug)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown model: {debug.model}")
|
||||||
|
|
||||||
|
if not check_share(debug, mnemonic, choose_wrong):
|
||||||
|
return None
|
||||||
|
return " ".join(mnemonic)
|
||||||
|
|
||||||
|
|
||||||
|
def read_mnemonic_from_screen_tt(
|
||||||
|
debug: "DebugLink",
|
||||||
|
) -> Generator[None, "ButtonRequest", list[str]]:
|
||||||
mnemonic: list[str] = []
|
mnemonic: list[str] = []
|
||||||
br = yield
|
br = yield
|
||||||
assert br.pages is not None
|
assert br.pages is not None
|
||||||
@ -222,27 +224,12 @@ def read_and_confirm_mnemonic_tt(
|
|||||||
debug.swipe_up()
|
debug.swipe_up()
|
||||||
|
|
||||||
debug.press_yes()
|
debug.press_yes()
|
||||||
|
return mnemonic
|
||||||
# check share
|
|
||||||
for _ in range(3):
|
|
||||||
# Word position is the first number in the text
|
|
||||||
word_pos_match = re.search(r"\d+", debug.wait_layout().text_content())
|
|
||||||
assert word_pos_match is not None
|
|
||||||
word_pos = int(word_pos_match.group(0))
|
|
||||||
|
|
||||||
index = word_pos - 1
|
|
||||||
if choose_wrong:
|
|
||||||
debug.input(mnemonic[(index + 1) % len(mnemonic)])
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
debug.input(mnemonic[index])
|
|
||||||
|
|
||||||
return " ".join(mnemonic)
|
|
||||||
|
|
||||||
|
|
||||||
def read_and_confirm_mnemonic_tr(
|
def read_mnemonic_from_screen_tr(
|
||||||
debug: "DebugLink", choose_wrong: bool = False
|
debug: "DebugLink",
|
||||||
) -> Generator[None, "ButtonRequest", Optional[str]]:
|
) -> Generator[None, "ButtonRequest", list[str]]:
|
||||||
mnemonic: list[str] = []
|
mnemonic: list[str] = []
|
||||||
yield # write down all 12 words in order
|
yield # write down all 12 words in order
|
||||||
debug.press_yes()
|
debug.press_yes()
|
||||||
@ -257,20 +244,47 @@ def read_and_confirm_mnemonic_tr(
|
|||||||
|
|
||||||
yield # Select correct words...
|
yield # Select correct words...
|
||||||
debug.press_right()
|
debug.press_right()
|
||||||
|
return mnemonic
|
||||||
|
|
||||||
# check share
|
|
||||||
|
def read_mnemonic_from_screen_mercury(
|
||||||
|
debug: "DebugLink",
|
||||||
|
) -> Generator[None, "ButtonRequest", list[str]]:
|
||||||
|
mnemonic: list[str] = []
|
||||||
|
br = yield
|
||||||
|
assert br.pages is not None
|
||||||
|
|
||||||
|
debug.wait_layout()
|
||||||
|
|
||||||
|
for i in range(br.pages):
|
||||||
|
words = debug.wait_layout().seed_words()
|
||||||
|
mnemonic.extend(words)
|
||||||
|
debug.swipe_up()
|
||||||
|
|
||||||
|
return mnemonic
|
||||||
|
|
||||||
|
|
||||||
|
def check_share(
|
||||||
|
debug: "DebugLink", mnemonic: list[str], choose_wrong: bool = False
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Given the mnemonic word list, proceed with the backup check:
|
||||||
|
three rounds of `Select word X of Y` choices.
|
||||||
|
"""
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
word_pos_match = re.search(r"\d+", debug.wait_layout().title())
|
# Word position is the first number in the text
|
||||||
|
word_pos_match = re.search(r"\d+", debug.wait_layout().text_content())
|
||||||
assert word_pos_match is not None
|
assert word_pos_match is not None
|
||||||
word_pos = int(word_pos_match.group(0))
|
word_pos = int(word_pos_match.group(0))
|
||||||
|
|
||||||
index = word_pos - 1
|
index = word_pos - 1
|
||||||
if choose_wrong:
|
if choose_wrong:
|
||||||
debug.input(mnemonic[(index + 1) % len(mnemonic)])
|
debug.input(mnemonic[(index + 1) % len(mnemonic)])
|
||||||
return None
|
return False
|
||||||
else:
|
else:
|
||||||
debug.input(mnemonic[index])
|
debug.input(mnemonic[index])
|
||||||
|
|
||||||
return " ".join(mnemonic)
|
return True
|
||||||
|
|
||||||
|
|
||||||
def click_info_button_tt(debug: "DebugLink"):
|
def click_info_button_tt(debug: "DebugLink"):
|
||||||
|
Loading…
Reference in New Issue
Block a user