mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-28 00:58:09 +00:00
feat(core): allow for going back to previous word in recovery process
This commit is contained in:
parent
1cddc4cdb2
commit
0579ba54fc
1
core/.changelog.d/3458.added
Normal file
1
core/.changelog.d/3458.added
Normal file
@ -0,0 +1 @@
|
||||
Allow for going back to previous word in recovery process
|
BIN
core/assets/model_r/arrow_left_big.png
Normal file
BIN
core/assets/model_r/arrow_left_big.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 460 B |
@ -143,6 +143,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_buttons__try_again;
|
||||
MP_QSTR_buttons__turn_off;
|
||||
MP_QSTR_buttons__turn_on;
|
||||
MP_QSTR_can_go_back;
|
||||
MP_QSTR_cancel_arrow;
|
||||
MP_QSTR_cancel_cross;
|
||||
MP_QSTR_cardano__addr_base;
|
||||
@ -431,6 +432,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_inputs__cancel;
|
||||
MP_QSTR_inputs__delete;
|
||||
MP_QSTR_inputs__enter;
|
||||
MP_QSTR_inputs__previous;
|
||||
MP_QSTR_inputs__return;
|
||||
MP_QSTR_inputs__show;
|
||||
MP_QSTR_inputs__space;
|
||||
@ -595,6 +597,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_plurals__transaction_of_x_operations;
|
||||
MP_QSTR_plurals__x_groups_needed;
|
||||
MP_QSTR_plurals__x_shares_needed;
|
||||
MP_QSTR_prefill_word;
|
||||
MP_QSTR_progress__authenticity_check;
|
||||
MP_QSTR_progress__done;
|
||||
MP_QSTR_progress__loading_transaction;
|
||||
|
@ -843,6 +843,7 @@ pub enum TranslatedString {
|
||||
words__writable = 830,
|
||||
words__yes = 831,
|
||||
reboot_to_bootloader__just_a_moment = 832,
|
||||
inputs__previous = 833,
|
||||
}
|
||||
|
||||
impl TranslatedString {
|
||||
@ -1681,6 +1682,7 @@ impl TranslatedString {
|
||||
Self::words__writable => "Writable",
|
||||
Self::words__yes => "Yes",
|
||||
Self::reboot_to_bootloader__just_a_moment => "Just a moment...",
|
||||
Self::inputs__previous => "PREVIOUS",
|
||||
}
|
||||
}
|
||||
|
||||
@ -2520,6 +2522,7 @@ impl TranslatedString {
|
||||
Qstr::MP_QSTR_words__writable => Some(Self::words__writable),
|
||||
Qstr::MP_QSTR_words__yes => Some(Self::words__yes),
|
||||
Qstr::MP_QSTR_reboot_to_bootloader__just_a_moment => Some(Self::reboot_to_bootloader__just_a_moment),
|
||||
Qstr::MP_QSTR_inputs__previous => Some(Self::inputs__previous),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,8 @@ pub struct Maybe<T> {
|
||||
}
|
||||
|
||||
impl<T> Maybe<T> {
|
||||
pub fn new(pad: Pad, inner: T, visible: bool) -> Self {
|
||||
pub fn new(bg_color: Color, inner: T, visible: bool) -> Self {
|
||||
let pad = Pad::with_background(bg_color);
|
||||
Self {
|
||||
inner,
|
||||
visible,
|
||||
@ -19,12 +20,12 @@ impl<T> Maybe<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn visible(clear: Color, inner: T) -> Self {
|
||||
Self::new(Pad::with_background(clear), inner, true)
|
||||
pub fn visible(bg_color: Color, inner: T) -> Self {
|
||||
Self::new(bg_color, inner, true)
|
||||
}
|
||||
|
||||
pub fn hidden(clear: Color, inner: T) -> Self {
|
||||
Self::new(Pad::with_background(clear), inner, false)
|
||||
pub fn hidden(bg_color: Color, inner: T) -> Self {
|
||||
Self::new(bg_color, inner, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ enum WordlistAction {
|
||||
Letter(char),
|
||||
Word(&'static str),
|
||||
Delete,
|
||||
Previous,
|
||||
}
|
||||
|
||||
const MAX_WORD_LENGTH: usize = 10;
|
||||
@ -47,6 +48,9 @@ struct ChoiceFactoryWordlist {
|
||||
offer_words: bool,
|
||||
/// We want to randomize the order in which we show the words
|
||||
word_random_order: Vec<usize, OFFER_WORDS_THRESHOLD>,
|
||||
/// Whether the input is empty - and we should show PREVIOUS instead of
|
||||
/// DELETE
|
||||
empty_input: bool,
|
||||
}
|
||||
|
||||
impl ChoiceFactoryWordlist {
|
||||
@ -72,6 +76,7 @@ impl ChoiceFactoryWordlist {
|
||||
wordlist,
|
||||
offer_words,
|
||||
word_random_order,
|
||||
empty_input: prefix.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -93,17 +98,31 @@ impl ChoiceFactory for ChoiceFactoryWordlist {
|
||||
// Putting DELETE as the first option in both cases
|
||||
// (is a requirement for WORDS, doing it for LETTERS as well to unite it)
|
||||
if choice_index == DELETE_INDEX {
|
||||
return (
|
||||
TR::inputs__delete.map_translated(|t| {
|
||||
ChoiceItem::new(
|
||||
t,
|
||||
ButtonLayout::arrow_armed_arrow(TR::buttons__confirm.into()),
|
||||
)
|
||||
.with_icon(theme::ICON_DELETE)
|
||||
.with_middle_action_without_release()
|
||||
}),
|
||||
WordlistAction::Delete,
|
||||
);
|
||||
if self.empty_input {
|
||||
return (
|
||||
TR::inputs__previous.map_translated(|t| {
|
||||
ChoiceItem::new(
|
||||
t,
|
||||
ButtonLayout::arrow_armed_arrow(TR::buttons__select.into()),
|
||||
)
|
||||
.with_icon(theme::ICON_DELETE)
|
||||
.with_middle_action_without_release()
|
||||
}),
|
||||
WordlistAction::Previous,
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
TR::inputs__delete.map_translated(|t| {
|
||||
ChoiceItem::new(
|
||||
t,
|
||||
ButtonLayout::arrow_armed_arrow(TR::buttons__select.into()),
|
||||
)
|
||||
.with_icon(theme::ICON_DELETE)
|
||||
.with_middle_action_without_release()
|
||||
}),
|
||||
WordlistAction::Delete,
|
||||
);
|
||||
}
|
||||
}
|
||||
if self.offer_words {
|
||||
// Taking a random (but always the same) word on this position
|
||||
@ -140,10 +159,12 @@ pub struct WordlistEntry {
|
||||
textbox: TextBox<MAX_WORD_LENGTH>,
|
||||
offer_words: bool,
|
||||
wordlist_type: WordlistType,
|
||||
/// Whether going back is allowed (is not on the very first word).
|
||||
can_go_back: bool,
|
||||
}
|
||||
|
||||
impl WordlistEntry {
|
||||
pub fn new(wordlist_type: WordlistType) -> Self {
|
||||
pub fn new(wordlist_type: WordlistType, can_go_back: bool) -> Self {
|
||||
let choices = ChoiceFactoryWordlist::new(wordlist_type, "");
|
||||
let choices_count = <ChoiceFactoryWordlist as ChoiceFactory>::count(&choices);
|
||||
Self {
|
||||
@ -156,6 +177,27 @@ impl WordlistEntry {
|
||||
textbox: TextBox::empty(),
|
||||
offer_words: false,
|
||||
wordlist_type,
|
||||
can_go_back,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prefilled_word(word: &str, wordlist_type: WordlistType, can_go_back: bool) -> Self {
|
||||
// Word may be empty string, fallback to normal input
|
||||
if word.is_empty() {
|
||||
return Self::new(wordlist_type, can_go_back);
|
||||
}
|
||||
|
||||
let choices = ChoiceFactoryWordlist::new(wordlist_type, word);
|
||||
Self {
|
||||
// Showing the chosen word at index 1
|
||||
choice_page: ChoicePage::new(choices)
|
||||
.with_incomplete(true)
|
||||
.with_initial_page_counter(1),
|
||||
chosen_letters: Child::new(ChangingTextLine::center_mono(String::from(word))),
|
||||
textbox: TextBox::new(String::from(word)),
|
||||
offer_words: false,
|
||||
wordlist_type,
|
||||
can_go_back,
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,6 +280,11 @@ impl Component for WordlistEntry {
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some((action, long_press)) = self.choice_page.event(ctx, event) {
|
||||
match action {
|
||||
WordlistAction::Previous => {
|
||||
if self.can_go_back {
|
||||
return Some("");
|
||||
}
|
||||
}
|
||||
WordlistAction::Delete => {
|
||||
// Deleting all when long-pressed
|
||||
if long_press {
|
||||
|
@ -1347,9 +1347,19 @@ extern "C" fn new_request_passphrase(n_args: usize, args: *const Obj, kwargs: *m
|
||||
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()?;
|
||||
let prefill_word: StrBuffer = kwargs.get(Qstr::MP_QSTR_prefill_word)?.try_into()?;
|
||||
let can_go_back: bool = kwargs.get(Qstr::MP_QSTR_can_go_back)?.try_into()?;
|
||||
|
||||
let obj = LayoutObj::new(
|
||||
Frame::new(prompt, WordlistEntry::new(WordlistType::Bip39)).with_title_centered(),
|
||||
Frame::new(
|
||||
prompt,
|
||||
WordlistEntry::prefilled_word(
|
||||
prefill_word.as_ref(),
|
||||
WordlistType::Bip39,
|
||||
can_go_back,
|
||||
),
|
||||
)
|
||||
.with_title_centered(),
|
||||
)?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
@ -1359,9 +1369,19 @@ extern "C" fn new_request_bip39(n_args: usize, args: *const Obj, kwargs: *mut Ma
|
||||
extern "C" fn new_request_slip39(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()?;
|
||||
let prefill_word: StrBuffer = kwargs.get(Qstr::MP_QSTR_prefill_word)?.try_into()?;
|
||||
let can_go_back: bool = kwargs.get(Qstr::MP_QSTR_can_go_back)?.try_into()?;
|
||||
|
||||
let obj = LayoutObj::new(
|
||||
Frame::new(prompt, WordlistEntry::new(WordlistType::Slip39)).with_title_centered(),
|
||||
Frame::new(
|
||||
prompt,
|
||||
WordlistEntry::prefilled_word(
|
||||
prefill_word.as_ref(),
|
||||
WordlistType::Slip39,
|
||||
can_go_back,
|
||||
),
|
||||
)
|
||||
.with_title_centered(),
|
||||
)?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
@ -1972,6 +1992,8 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// def request_bip39(
|
||||
/// *,
|
||||
/// prompt: str,
|
||||
/// prefill_word: str,
|
||||
/// can_go_back: bool,
|
||||
/// ) -> str:
|
||||
/// """Get recovery word for BIP39."""
|
||||
Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(),
|
||||
@ -1979,6 +2001,8 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// def request_slip39(
|
||||
/// *,
|
||||
/// prompt: str,
|
||||
/// prefill_word: str,
|
||||
/// can_go_back: bool,
|
||||
/// ) -> str:
|
||||
/// """SLIP39 word input keyboard."""
|
||||
Qstr::MP_QSTR_request_slip39 => obj_fn_kw!(0, new_request_slip39).as_obj(),
|
||||
|
BIN
core/embed/rust/src/ui/model_tr/res/arrow_left_big.toif
Normal file
BIN
core/embed/rust/src/ui/model_tr/res/arrow_left_big.toif
Normal file
Binary file not shown.
@ -63,6 +63,7 @@ pub fn textstyle_number(num: i32) -> &'static TextStyle {
|
||||
include_icon!(ICON_ARM_LEFT, "model_tr/res/arm_left.toif"); // 10*6
|
||||
include_icon!(ICON_ARM_RIGHT, "model_tr/res/arm_right.toif"); // 10*6
|
||||
include_icon!(ICON_ARROW_LEFT, "model_tr/res/arrow_left.toif"); // 4*7
|
||||
include_icon!(ICON_ARROW_LEFT_BIG, "model_tr/res/arrow_left_big.toif"); // 8*7
|
||||
include_icon!(ICON_ARROW_RIGHT, "model_tr/res/arrow_right.toif"); // 4*7
|
||||
include_icon!(ICON_ARROW_RIGHT_FAT, "model_tr/res/arrow_right_fat.toif"); // 4*8
|
||||
include_icon!(
|
||||
|
@ -16,6 +16,7 @@ use crate::{
|
||||
},
|
||||
},
|
||||
};
|
||||
use heapless::String;
|
||||
|
||||
const MAX_LENGTH: usize = 8;
|
||||
|
||||
@ -171,6 +172,23 @@ impl Bip39Input {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prefilled_word(word: &str) -> Self {
|
||||
// Word may be empty string, fallback to normal input
|
||||
if word.is_empty() {
|
||||
return Self::new();
|
||||
}
|
||||
|
||||
// Styling the input to reflect already filled word
|
||||
Self {
|
||||
button: Button::with_icon(theme::ICON_LIST_CHECK).styled(theme::button_pin_confirm()),
|
||||
textbox: TextBox::new(String::from(word)),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
options_num: bip39::options_num(word),
|
||||
suggested_word: bip39::complete_word(word),
|
||||
button_suggestion: Button::empty().styled(theme::button_suggestion_confirm()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute a bitmask of all letters contained in given key text. Lowest bit
|
||||
/// is 'a', second lowest 'b', etc.
|
||||
fn key_mask(key: usize) -> u32 {
|
||||
|
@ -2,7 +2,7 @@ use crate::ui::{
|
||||
component::{maybe::paint_overlapping, Child, Component, Event, EventCtx, Label, Maybe},
|
||||
geometry::{Alignment2D, Grid, Offset, Rect},
|
||||
model_tt::{
|
||||
component::{Button, ButtonMsg},
|
||||
component::{Button, ButtonMsg, Swipe, SwipeDirection},
|
||||
theme,
|
||||
},
|
||||
};
|
||||
@ -11,6 +11,7 @@ pub const MNEMONIC_KEY_COUNT: usize = 9;
|
||||
|
||||
pub enum MnemonicKeyboardMsg {
|
||||
Confirmed,
|
||||
Previous,
|
||||
}
|
||||
|
||||
pub struct MnemonicKeyboard<T, U> {
|
||||
@ -22,6 +23,10 @@ pub struct MnemonicKeyboard<T, U> {
|
||||
input: Child<Maybe<T>>,
|
||||
/// Key buttons.
|
||||
keys: [Child<Button<&'static str>>; MNEMONIC_KEY_COUNT],
|
||||
/// Swipe controller - allowing for going to the previous word.
|
||||
swipe: Swipe,
|
||||
/// Whether going back is allowed (is not on the very first word).
|
||||
can_go_back: bool,
|
||||
}
|
||||
|
||||
impl<T, U> MnemonicKeyboard<T, U>
|
||||
@ -29,13 +34,17 @@ where
|
||||
T: MnemonicInput,
|
||||
U: AsRef<str>,
|
||||
{
|
||||
pub fn new(input: T, prompt: U) -> Self {
|
||||
pub fn new(input: T, prompt: U, can_go_back: bool) -> Self {
|
||||
// Input might be already pre-filled
|
||||
let prompt_visible = input.is_empty();
|
||||
|
||||
Self {
|
||||
prompt: Child::new(Maybe::visible(
|
||||
prompt: Child::new(Maybe::new(
|
||||
theme::BG,
|
||||
Label::centered(prompt, theme::label_keyboard_prompt()),
|
||||
prompt_visible,
|
||||
)),
|
||||
back: Child::new(Maybe::hidden(
|
||||
back: Child::new(Maybe::new(
|
||||
theme::BG,
|
||||
Button::with_icon_blend(
|
||||
theme::IMAGE_BG_BACK_BTN_TALL,
|
||||
@ -44,11 +53,14 @@ where
|
||||
)
|
||||
.styled(theme::button_reset())
|
||||
.with_long_press(theme::ERASE_HOLD_DURATION),
|
||||
!prompt_visible,
|
||||
)),
|
||||
input: Child::new(Maybe::hidden(theme::BG, input)),
|
||||
input: Child::new(Maybe::new(theme::BG, input, !prompt_visible)),
|
||||
keys: T::keys()
|
||||
.map(|t| Button::with_text(t).styled(theme::button_pin()))
|
||||
.map(Child::new),
|
||||
swipe: Swipe::new().right(),
|
||||
can_go_back,
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,6 +118,7 @@ where
|
||||
let prompt_size = self.prompt.inner().inner().max_size();
|
||||
let prompt_area = Rect::snap(prompt_center, prompt_size, Alignment2D::CENTER);
|
||||
|
||||
self.swipe.place(bounds);
|
||||
self.prompt.place(prompt_area);
|
||||
self.back.place(back_area);
|
||||
self.input.place(input_area);
|
||||
@ -116,6 +129,13 @@ where
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// Swipe will cause going back to the previous word when allowed.
|
||||
if self.can_go_back {
|
||||
if let Some(SwipeDirection::Right) = self.swipe.event(ctx, event) {
|
||||
return Some(MnemonicKeyboardMsg::Previous);
|
||||
}
|
||||
}
|
||||
|
||||
match self.input.event(ctx, event) {
|
||||
Some(MnemonicInputMsg::Confirmed) => {
|
||||
// Confirmed, bubble up.
|
||||
|
@ -79,8 +79,7 @@ where
|
||||
let erase_btn = Maybe::hidden(theme::BG, erase_btn).into_child();
|
||||
|
||||
let cancel_btn = Button::with_icon(theme::ICON_CANCEL).styled(theme::button_cancel());
|
||||
let cancel_btn =
|
||||
Maybe::new(Pad::with_background(theme::BG), cancel_btn, allow_cancel).into_child();
|
||||
let cancel_btn = Maybe::new(theme::BG, cancel_btn, allow_cancel).into_child();
|
||||
|
||||
Self {
|
||||
allow_cancel,
|
||||
|
@ -203,6 +203,61 @@ impl Slip39Input {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prefilled_word(word: &str) -> Self {
|
||||
// Word may be empty string, fallback to normal input
|
||||
if word.is_empty() {
|
||||
return Self::new();
|
||||
}
|
||||
|
||||
let (buff, input_mask, final_word) = Self::setup_from_prefilled_word(word);
|
||||
|
||||
Self {
|
||||
// Button has the same style the whole time
|
||||
button: Button::empty().styled(theme::button_pin_confirm()),
|
||||
textbox: TextBox::new(buff),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
final_word,
|
||||
input_mask,
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_from_prefilled_word(
|
||||
word: &str,
|
||||
) -> (String<MAX_LENGTH>, Slip39Mask, Option<&'static str>) {
|
||||
let mut buff: String<MAX_LENGTH> = String::new();
|
||||
|
||||
// Gradually appending encoded key digits to the buffer and checking if
|
||||
// have not already formed a final word.
|
||||
for ch in word.chars() {
|
||||
let mut index = 0;
|
||||
for (i, key) in Self::keys().iter().enumerate() {
|
||||
if key.contains(ch) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
buff.push(Self::key_digit(index))
|
||||
.assert_if_debugging_ui("Text buffer is too small");
|
||||
|
||||
let sequence: Option<u16> = buff.parse().ok();
|
||||
let input_mask = sequence
|
||||
.and_then(slip39::word_completion_mask)
|
||||
.map(Slip39Mask)
|
||||
.unwrap_or_else(Slip39Mask::full);
|
||||
let final_word = if input_mask.is_final() {
|
||||
sequence.and_then(slip39::button_sequence_to_word)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// As soon as we have a final word, we can stop.
|
||||
if final_word.is_some() {
|
||||
return (buff, input_mask, final_word);
|
||||
}
|
||||
}
|
||||
(buff, Slip39Mask::full(), None)
|
||||
}
|
||||
|
||||
/// Convert a key index into the key digit. This is what we push into the
|
||||
/// input buffer.
|
||||
///
|
||||
|
@ -177,6 +177,7 @@ where
|
||||
panic!("invalid mnemonic")
|
||||
}
|
||||
}
|
||||
MnemonicKeyboardMsg::Previous => "".try_into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1270,7 +1271,13 @@ extern "C" fn new_request_passphrase(n_args: usize, args: *const Obj, kwargs: *m
|
||||
extern "C" fn new_request_bip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
||||
let obj = LayoutObj::new(MnemonicKeyboard::new(Bip39Input::new(), prompt))?;
|
||||
let prefill_word: StrBuffer = kwargs.get(Qstr::MP_QSTR_prefill_word)?.try_into()?;
|
||||
let can_go_back: bool = kwargs.get(Qstr::MP_QSTR_can_go_back)?.try_into()?;
|
||||
let obj = LayoutObj::new(MnemonicKeyboard::new(
|
||||
Bip39Input::prefilled_word(prefill_word.as_ref()),
|
||||
prompt,
|
||||
can_go_back,
|
||||
))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
@ -1279,7 +1286,13 @@ extern "C" fn new_request_bip39(n_args: usize, args: *const Obj, kwargs: *mut Ma
|
||||
extern "C" fn new_request_slip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
||||
let obj = LayoutObj::new(MnemonicKeyboard::new(Slip39Input::new(), prompt))?;
|
||||
let prefill_word: StrBuffer = kwargs.get(Qstr::MP_QSTR_prefill_word)?.try_into()?;
|
||||
let can_go_back: bool = kwargs.get(Qstr::MP_QSTR_can_go_back)?.try_into()?;
|
||||
let obj = LayoutObj::new(MnemonicKeyboard::new(
|
||||
Slip39Input::prefilled_word(prefill_word.as_ref()),
|
||||
prompt,
|
||||
can_go_back,
|
||||
))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
@ -1936,6 +1949,8 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// def request_bip39(
|
||||
/// *,
|
||||
/// prompt: str,
|
||||
/// prefill_word: str,
|
||||
/// can_go_back: bool,
|
||||
/// ) -> str:
|
||||
/// """BIP39 word input keyboard."""
|
||||
Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(),
|
||||
@ -1943,6 +1958,8 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// def request_slip39(
|
||||
/// *,
|
||||
/// prompt: str,
|
||||
/// prefill_word: str,
|
||||
/// can_go_back: bool,
|
||||
/// ) -> str:
|
||||
/// """SLIP39 word input keyboard."""
|
||||
Qstr::MP_QSTR_request_slip39 => obj_fn_kw!(0, new_request_slip39).as_obj(),
|
||||
|
@ -308,6 +308,8 @@ def request_passphrase(
|
||||
def request_bip39(
|
||||
*,
|
||||
prompt: str,
|
||||
prefill_word: str,
|
||||
can_go_back: bool,
|
||||
) -> str:
|
||||
"""Get recovery word for BIP39."""
|
||||
|
||||
@ -316,6 +318,8 @@ def request_bip39(
|
||||
def request_slip39(
|
||||
*,
|
||||
prompt: str,
|
||||
prefill_word: str,
|
||||
can_go_back: bool,
|
||||
) -> str:
|
||||
"""SLIP39 word input keyboard."""
|
||||
|
||||
@ -756,6 +760,8 @@ def request_passphrase(
|
||||
def request_bip39(
|
||||
*,
|
||||
prompt: str,
|
||||
prefill_word: str,
|
||||
can_go_back: bool,
|
||||
) -> str:
|
||||
"""BIP39 word input keyboard."""
|
||||
|
||||
@ -764,6 +770,8 @@ def request_bip39(
|
||||
def request_slip39(
|
||||
*,
|
||||
prompt: str,
|
||||
prefill_word: str,
|
||||
can_go_back: bool,
|
||||
) -> str:
|
||||
"""SLIP39 word input keyboard."""
|
||||
|
||||
|
@ -337,6 +337,7 @@ class TR:
|
||||
inputs__cancel: str = "CANCEL"
|
||||
inputs__delete: str = "DELETE"
|
||||
inputs__enter: str = "ENTER"
|
||||
inputs__previous: str = "PREVIOUS"
|
||||
inputs__return: str = "RETURN"
|
||||
inputs__show: str = "SHOW"
|
||||
inputs__space: str = "SPACE"
|
||||
|
@ -50,15 +50,35 @@ async def request_mnemonic(
|
||||
|
||||
await button_request("mnemonic", code=ButtonRequestType.MnemonicInput)
|
||||
|
||||
words: list[str] = []
|
||||
for i in range(word_count):
|
||||
# Allowing to go back to previous words, therefore cannot use just loop over range(word_count)
|
||||
words: list[str] = [""] * word_count
|
||||
i = 0
|
||||
while True:
|
||||
# All the words have been entered
|
||||
if i >= word_count:
|
||||
break
|
||||
|
||||
# Prefilling the previously inputted word in case of going back
|
||||
word = await request_word(
|
||||
i, word_count, is_slip39=backup_types.is_slip39_word_count(word_count)
|
||||
i,
|
||||
word_count,
|
||||
is_slip39=backup_types.is_slip39_word_count(word_count),
|
||||
prefill_word=words[i],
|
||||
)
|
||||
words.append(word)
|
||||
|
||||
# User has decided to go back
|
||||
if not word:
|
||||
if i > 0:
|
||||
i -= 1
|
||||
continue
|
||||
|
||||
words[i] = word
|
||||
|
||||
i += 1
|
||||
|
||||
try:
|
||||
word_validity.check(backup_type, words)
|
||||
non_empty_words = [word for word in words if word]
|
||||
word_validity.check(backup_type, non_empty_words)
|
||||
except word_validity.AlreadyAdded:
|
||||
# show_share_already_added
|
||||
await show_recovery_warning(
|
||||
|
@ -18,15 +18,27 @@ async def request_word_count(dry_run: bool) -> int:
|
||||
return int(count)
|
||||
|
||||
|
||||
async def request_word(word_index: int, word_count: int, is_slip39: bool) -> str:
|
||||
async def request_word(
|
||||
word_index: int, word_count: int, is_slip39: bool, prefill_word: str = ""
|
||||
) -> str:
|
||||
from trezor.wire.context import wait
|
||||
|
||||
prompt = TR.recovery__word_x_of_y_template.format(word_index + 1, word_count)
|
||||
|
||||
can_go_back = word_index > 0
|
||||
|
||||
if is_slip39:
|
||||
word_choice = RustLayout(trezorui2.request_slip39(prompt=prompt))
|
||||
word_choice = RustLayout(
|
||||
trezorui2.request_slip39(
|
||||
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
|
||||
)
|
||||
)
|
||||
else:
|
||||
word_choice = RustLayout(trezorui2.request_bip39(prompt=prompt))
|
||||
word_choice = RustLayout(
|
||||
trezorui2.request_bip39(
|
||||
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
|
||||
)
|
||||
)
|
||||
|
||||
word: str = await wait(word_choice)
|
||||
return word
|
||||
|
@ -32,12 +32,23 @@ async def request_word_count(dry_run: bool) -> int:
|
||||
return int(count)
|
||||
|
||||
|
||||
async def request_word(word_index: int, word_count: int, is_slip39: bool) -> str:
|
||||
async def request_word(
|
||||
word_index: int, word_count: int, is_slip39: bool, prefill_word: str = ""
|
||||
) -> str:
|
||||
prompt = TR.recovery__type_word_x_of_y_template.format(word_index + 1, word_count)
|
||||
can_go_back = word_index > 0
|
||||
if is_slip39:
|
||||
keyboard = RustLayout(trezorui2.request_slip39(prompt=prompt))
|
||||
keyboard = RustLayout(
|
||||
trezorui2.request_slip39(
|
||||
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
|
||||
)
|
||||
)
|
||||
else:
|
||||
keyboard = RustLayout(trezorui2.request_bip39(prompt=prompt))
|
||||
keyboard = RustLayout(
|
||||
trezorui2.request_bip39(
|
||||
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
|
||||
)
|
||||
)
|
||||
|
||||
word: str = await ctx_wait(keyboard)
|
||||
return word
|
||||
|
@ -19,7 +19,7 @@ def translate_dict(
|
||||
translated_text = translator.translate(
|
||||
value, src=from_lang, dest=to_lang
|
||||
).text
|
||||
new_dict[key] = translated_text + " (TODO)"
|
||||
new_dict[key] = translated_text
|
||||
except Exception as e:
|
||||
print(f"Error translating {value}: {e}")
|
||||
new_dict[key] = MISSING_VALUE
|
||||
|
@ -355,6 +355,7 @@
|
||||
"inputs__cancel": "ZRUŠIT",
|
||||
"inputs__delete": "SMAZAT",
|
||||
"inputs__enter": "ZADAT",
|
||||
"inputs__previous": "PŘEDCHOZÍ",
|
||||
"inputs__return": "VRÁTIT",
|
||||
"inputs__show": "UKÁZAT",
|
||||
"inputs__space": "MEZERNÍK",
|
||||
@ -512,6 +513,7 @@
|
||||
"progress__signing_transaction": "Podpis transakce...",
|
||||
"progress__syncing": "Synchronizace...",
|
||||
"progress__x_seconds_left_template": "Zbývá {} sekund",
|
||||
"reboot_to_bootloader__just_a_moment": "Jenom chvilku...",
|
||||
"reboot_to_bootloader__restart": "Chcete restartovat Trezor v režimu bootloader?",
|
||||
"reboot_to_bootloader__title": "PŘEJÍT DO REŽIMU BOOTLOADER",
|
||||
"reboot_to_bootloader__version_by_template": "Verze firmwaru {}\nod {}",
|
||||
|
@ -355,6 +355,7 @@
|
||||
"inputs__cancel": "ABBRECHEN",
|
||||
"inputs__delete": "LÖSCHEN",
|
||||
"inputs__enter": "EINGEBEN",
|
||||
"inputs__previous": "VORHERIGE",
|
||||
"inputs__return": "ZURÜCK",
|
||||
"inputs__show": "ANZEIGEN",
|
||||
"inputs__space": "LEER",
|
||||
@ -512,6 +513,7 @@
|
||||
"progress__signing_transaction": "Transaktion wird signiert...",
|
||||
"progress__syncing": "Wird synchronisiert...",
|
||||
"progress__x_seconds_left_template": "{} Sekunden verbleibend",
|
||||
"reboot_to_bootloader__just_a_moment": "Einen Augenblick...",
|
||||
"reboot_to_bootloader__restart": "Möchtest du Trezor im Bootloader-Modus neu starten?",
|
||||
"reboot_to_bootloader__title": "ZUM BOOTLOADER",
|
||||
"reboot_to_bootloader__version_by_template": "Firmware Version {}\nvon {}",
|
||||
|
@ -343,6 +343,7 @@
|
||||
"inputs__delete": "DELETE",
|
||||
"inputs__enter": "ENTER",
|
||||
"inputs__return": "RETURN",
|
||||
"inputs__previous": "PREVIOUS",
|
||||
"inputs__show": "SHOW",
|
||||
"inputs__space": "SPACE",
|
||||
"joint__title": "JOINT TRANSACTION",
|
||||
|
@ -355,6 +355,7 @@
|
||||
"inputs__cancel": "CANCELAR",
|
||||
"inputs__delete": "ELIMINAR",
|
||||
"inputs__enter": "INTRODUCIR",
|
||||
"inputs__previous": "ANTERIOR",
|
||||
"inputs__return": "VOLVER",
|
||||
"inputs__show": "MOSTRAR",
|
||||
"inputs__space": "ESPACIO",
|
||||
@ -512,6 +513,7 @@
|
||||
"progress__signing_transaction": "Firmando transacción...",
|
||||
"progress__syncing": "Sincronizando...",
|
||||
"progress__x_seconds_left_template": "Quedan {} segundos",
|
||||
"reboot_to_bootloader__just_a_moment": "Sólo un momento...",
|
||||
"reboot_to_bootloader__restart": "¿Deseas reiniciar el Trezor en modo bootloader?",
|
||||
"reboot_to_bootloader__title": "IR A BOOTLOADER",
|
||||
"reboot_to_bootloader__version_by_template": "Versión de firmware {}\npor {}",
|
||||
|
@ -355,6 +355,7 @@
|
||||
"inputs__cancel": "ANNULER",
|
||||
"inputs__delete": "SUPPRIMER",
|
||||
"inputs__enter": "SAISIR",
|
||||
"inputs__previous": "PREVIOUS",
|
||||
"inputs__return": "RETOUR",
|
||||
"inputs__show": "AFFICHER",
|
||||
"inputs__space": "ESPACE",
|
||||
@ -512,6 +513,7 @@
|
||||
"progress__signing_transaction": "Signature de la transaction en cours",
|
||||
"progress__syncing": "Synchronisation en cours",
|
||||
"progress__x_seconds_left_template": "{} secondes restantes",
|
||||
"reboot_to_bootloader__just_a_moment": "Juste un moment...",
|
||||
"reboot_to_bootloader__restart": "Voulez-vous redémarrer Trezor en mode bootloader?",
|
||||
"reboot_to_bootloader__title": "ACCÉDER BOOTLOADER",
|
||||
"reboot_to_bootloader__version_by_template": "Version du firmware {}\npar {}",
|
||||
|
@ -831,5 +831,6 @@
|
||||
"829": "words__warning",
|
||||
"830": "words__writable",
|
||||
"831": "words__yes",
|
||||
"832": "reboot_to_bootloader__just_a_moment"
|
||||
"832": "reboot_to_bootloader__just_a_moment",
|
||||
"833": "inputs__previous"
|
||||
}
|
||||
|
@ -24,6 +24,8 @@ OK = (RIGHT, BOTTOM)
|
||||
CANCEL = (LEFT, BOTTOM)
|
||||
INFO = (MID, BOTTOM)
|
||||
|
||||
RECOVERY_DELETE = (LEFT, TOP)
|
||||
|
||||
CORNER_BUTTON = (215, 25)
|
||||
|
||||
CONFIRM_WORD = (MID, TOP)
|
||||
|
@ -4,6 +4,7 @@ from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .. import buttons
|
||||
from .. import translations as TR
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from trezorlib.debuglink import DebugLink, LayoutContent
|
||||
@ -147,3 +148,7 @@ def _move_one_closer(
|
||||
return debug.press_left(wait=True)
|
||||
else:
|
||||
return debug.press_right(wait=True)
|
||||
|
||||
|
||||
def get_possible_btn_texts(path: str) -> str:
|
||||
return "|".join(TR.translate(path))
|
||||
|
@ -2,12 +2,17 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from .. import buttons
|
||||
from .. import translations as TR
|
||||
from .common import go_next
|
||||
from .common import get_possible_btn_texts, go_next
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from trezorlib.debuglink import DebugLink, LayoutContent
|
||||
|
||||
|
||||
DELETE_BTN_TEXTS = get_possible_btn_texts("inputs__delete") + get_possible_btn_texts(
|
||||
"inputs__previous"
|
||||
)
|
||||
|
||||
|
||||
def enter_word(
|
||||
debug: "DebugLink", word: str, is_slip39: bool = False
|
||||
) -> "LayoutContent":
|
||||
@ -117,6 +122,60 @@ def enter_shares(debug: "DebugLink", shares: list[str]) -> None:
|
||||
|
||||
|
||||
def enter_seed(debug: "DebugLink", seed_words: list[str]) -> None:
|
||||
prepare_enter_seed(debug)
|
||||
|
||||
for word in seed_words:
|
||||
enter_word(debug, word, is_slip39=False)
|
||||
|
||||
TR.assert_in(debug.read_layout().text_content(), "recovery__wallet_recovered")
|
||||
|
||||
|
||||
def enter_seed_previous_correct(
|
||||
debug: "DebugLink", seed_words: list[str], bad_indexes: dict[int, str]
|
||||
) -> None:
|
||||
prepare_enter_seed(debug)
|
||||
|
||||
i = 0
|
||||
go_back = False
|
||||
bad_word = ""
|
||||
while True:
|
||||
assert i >= 0
|
||||
|
||||
if i >= len(seed_words):
|
||||
break
|
||||
|
||||
if go_back:
|
||||
go_back = False
|
||||
if debug.model == "T":
|
||||
debug.swipe_right(wait=True)
|
||||
for _ in range(len(bad_word)):
|
||||
debug.click(buttons.RECOVERY_DELETE, wait=True)
|
||||
elif debug.model == "Safe 3":
|
||||
layout = debug.read_layout()
|
||||
|
||||
while layout.get_middle_choice() not in DELETE_BTN_TEXTS:
|
||||
layout = debug.press_right(wait=True)
|
||||
layout = debug.press_middle(wait=True)
|
||||
|
||||
for _ in range(len(bad_word)):
|
||||
while layout.get_middle_choice() not in DELETE_BTN_TEXTS:
|
||||
layout = debug.press_left(wait=True)
|
||||
layout = debug.press_middle(wait=True)
|
||||
continue
|
||||
|
||||
if i in bad_indexes:
|
||||
word = bad_indexes.pop(i)
|
||||
bad_word = word
|
||||
go_back = True
|
||||
else:
|
||||
word = seed_words[i]
|
||||
i += 1
|
||||
layout = enter_word(debug, word, is_slip39=False)
|
||||
|
||||
TR.assert_in(debug.read_layout().text_content(), "recovery__wallet_recovered")
|
||||
|
||||
|
||||
def prepare_enter_seed(debug: "DebugLink") -> None:
|
||||
TR.assert_in(debug.read_layout().text_content(), "recovery__enter_backup")
|
||||
if debug.model == "T":
|
||||
debug.click(buttons.OK, wait=True)
|
||||
@ -124,14 +183,8 @@ def enter_seed(debug: "DebugLink", seed_words: list[str]) -> None:
|
||||
debug.press_right(wait=True)
|
||||
TR.assert_equals(debug.read_layout().title(), "recovery__title_recover")
|
||||
debug.press_right()
|
||||
|
||||
debug.press_right(wait=True)
|
||||
|
||||
assert "MnemonicKeyboard" in debug.read_layout().all_components()
|
||||
for word in seed_words:
|
||||
enter_word(debug, word, is_slip39=False)
|
||||
|
||||
TR.assert_in(debug.read_layout().text_content(), "recovery__wallet_recovered")
|
||||
layout = debug.press_right(wait=True)
|
||||
assert "MnemonicKeyboard" in layout.all_components()
|
||||
|
||||
|
||||
def finalize(debug: "DebugLink") -> None:
|
||||
|
@ -24,7 +24,12 @@ from trezorlib import device, exceptions
|
||||
|
||||
from .. import buttons
|
||||
from .. import translations as TR
|
||||
from .common import go_back, go_next, navigate_to_action_and_press
|
||||
from .common import (
|
||||
get_possible_btn_texts,
|
||||
go_back,
|
||||
go_next,
|
||||
navigate_to_action_and_press,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from trezorlib.debuglink import DebugLink
|
||||
@ -42,14 +47,9 @@ PIN24 = "875163065288639289952973"
|
||||
PIN50 = "31415926535897932384626433832795028841971693993751"
|
||||
PIN60 = PIN50 + "9" * 10
|
||||
|
||||
|
||||
def _get_possible_btns(path: str) -> str:
|
||||
return "|".join(TR.translate(path))
|
||||
|
||||
|
||||
DELETE = _get_possible_btns("inputs__delete")
|
||||
SHOW = _get_possible_btns("inputs__show")
|
||||
ENTER = _get_possible_btns("inputs__enter")
|
||||
DELETE = get_possible_btn_texts("inputs__delete")
|
||||
SHOW = get_possible_btn_texts("inputs__show")
|
||||
ENTER = get_possible_btn_texts("inputs__enter")
|
||||
|
||||
|
||||
TR_PIN_ACTIONS = [
|
||||
|
@ -68,3 +68,15 @@ def test_recovery_bip39(device_handler: "BackgroundDeviceHandler"):
|
||||
recovery.select_number_of_words(debug, num_of_words=12)
|
||||
recovery.enter_seed(debug, MNEMONIC12.split())
|
||||
recovery.finalize(debug)
|
||||
|
||||
|
||||
@pytest.mark.setup_client(uninitialized=True)
|
||||
def test_recovery_bip39_previous_word(device_handler: "BackgroundDeviceHandler"):
|
||||
with prepare_recovery_and_evaluate(device_handler) as debug:
|
||||
recovery.confirm_recovery(debug)
|
||||
|
||||
recovery.select_number_of_words(debug, num_of_words=12)
|
||||
seed_words: list[str] = MNEMONIC12.split()
|
||||
bad_indexes = {1: seed_words[-1], 7: seed_words[0]}
|
||||
recovery.enter_seed_previous_correct(debug, seed_words, bad_indexes)
|
||||
recovery.finalize(debug)
|
||||
|
@ -848,8 +848,9 @@
|
||||
"TR_en_test_pin.py::test_pin_short": "a87619c455ec81763cbb133279f9e7fbb3b9a5a02c58e1da63640d51a4e107c3",
|
||||
"TR_en_test_pin.py::test_wipe_code_same_as_pin": "329962b3b949dc1429d361e4ae6c5224b4a75c981ae833fd3bf1618df44fbd2a",
|
||||
"TR_en_test_pin.py::test_wipe_code_setup": "4afe5ceb8f953892eea8b6b7c6997a1b5afa40b81372265a84a567b61773d89b",
|
||||
"TR_en_test_recovery.py::test_recovery_bip39": "43a56fb1dea38503cfddf55c4a6e8d5a5188be7354bd5c2b31fde197f2af3443",
|
||||
"TR_en_test_recovery.py::test_recovery_slip39_basic": "40df8fd6302577ff4f1d1d6d214d1ffd22791912ee4225ae86460a3fa3f3ac0f",
|
||||
"TR_en_test_recovery.py::test_recovery_bip39": "f70c19e62380af74f43fbfa5fb75c03db47417348477298c51c88e37f1240444",
|
||||
"TR_en_test_recovery.py::test_recovery_bip39_previous_word": "c3b2bd8858c6ab627905cb49f122281b08e2ce6965f49baabdcd419c68affe09",
|
||||
"TR_en_test_recovery.py::test_recovery_slip39_basic": "a5afe54583805edf3b9f5f07778237b6a2f1b5c6d20db998cde78f5a3714c0de",
|
||||
"TR_en_test_reset_bip39.py::test_reset_bip39": "5ae502b3041376c4dac8de8b315242e20c29e76cbd93acc81790b61a65bb2401",
|
||||
"TR_en_test_reset_slip39_advanced.py::test_reset_slip39_advanced[16of16]": "53763c7177a26069cc164a84d93c6b6b53bfe3f58a770913b4b62b537fd9301d",
|
||||
"TR_en_test_reset_slip39_advanced.py::test_reset_slip39_advanced[2of2]": "ae94835bc49ebce6fdaa05671734701f6d2911cdee54e99e083edddbb070ffa7",
|
||||
@ -7637,6 +7638,7 @@
|
||||
"TT_en_test_pin.py::test_wipe_code_same_as_pin": "f13b6559d4d73c2853218fee85dc951c41c5b4e3867ee0e989443940233f2f96",
|
||||
"TT_en_test_pin.py::test_wipe_code_setup": "3d6a04cc7c8d3a061758a9559277a548bf4492ef59afd1d040693372d197383c",
|
||||
"TT_en_test_recovery.py::test_recovery_bip39": "65a138f634806c6483c55c6ce5365b8a7a4073a3c0c340b1826042262faa8545",
|
||||
"TT_en_test_recovery.py::test_recovery_bip39_previous_word": "a009899ccd3305cb6737c8fa645cc9eedf4e46d6669a621a07d8cd9447d80f2f",
|
||||
"TT_en_test_recovery.py::test_recovery_slip39_basic": "9b0f5a7b8d2ab0fed1e5389076bc035e24dce377d275824220f1aa61e9bb4810",
|
||||
"TT_en_test_reset_bip39.py::test_reset_bip39": "19fd9f6233d72224696c547528a0079934a86cb41539b6f7149aab57b0aaec42",
|
||||
"TT_en_test_reset_slip39_advanced.py::test_reset_slip39_advanced[16of16]": "1d604f1766ce861e616745dcb5c89122165eb26ecc7f40039e50b8fe8c61a861",
|
||||
|
Loading…
Reference in New Issue
Block a user