1
0
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:
grdddj 2024-01-29 14:38:02 +01:00 committed by Martin Milata
parent 1cddc4cdb2
commit 0579ba54fc
32 changed files with 386 additions and 61 deletions

View File

@ -0,0 +1 @@
Allow for going back to previous word in recovery process

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

View File

@ -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;

View File

@ -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,
}
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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(),

View File

@ -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!(

View File

@ -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 {

View File

@ -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.

View File

@ -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,

View File

@ -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.
///

View File

@ -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(),

View File

@ -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."""

View File

@ -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"

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {}",

View File

@ -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 {}",

View File

@ -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",

View File

@ -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 {}",

View File

@ -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 {}",

View File

@ -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"
}

View File

@ -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)

View File

@ -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))

View File

@ -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:

View File

@ -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 = [

View File

@ -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)

View File

@ -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",