mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-07-18 20:48:18 +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__try_again;
|
||||||
MP_QSTR_buttons__turn_off;
|
MP_QSTR_buttons__turn_off;
|
||||||
MP_QSTR_buttons__turn_on;
|
MP_QSTR_buttons__turn_on;
|
||||||
|
MP_QSTR_can_go_back;
|
||||||
MP_QSTR_cancel_arrow;
|
MP_QSTR_cancel_arrow;
|
||||||
MP_QSTR_cancel_cross;
|
MP_QSTR_cancel_cross;
|
||||||
MP_QSTR_cardano__addr_base;
|
MP_QSTR_cardano__addr_base;
|
||||||
@ -431,6 +432,7 @@ static void _librust_qstrs(void) {
|
|||||||
MP_QSTR_inputs__cancel;
|
MP_QSTR_inputs__cancel;
|
||||||
MP_QSTR_inputs__delete;
|
MP_QSTR_inputs__delete;
|
||||||
MP_QSTR_inputs__enter;
|
MP_QSTR_inputs__enter;
|
||||||
|
MP_QSTR_inputs__previous;
|
||||||
MP_QSTR_inputs__return;
|
MP_QSTR_inputs__return;
|
||||||
MP_QSTR_inputs__show;
|
MP_QSTR_inputs__show;
|
||||||
MP_QSTR_inputs__space;
|
MP_QSTR_inputs__space;
|
||||||
@ -595,6 +597,7 @@ static void _librust_qstrs(void) {
|
|||||||
MP_QSTR_plurals__transaction_of_x_operations;
|
MP_QSTR_plurals__transaction_of_x_operations;
|
||||||
MP_QSTR_plurals__x_groups_needed;
|
MP_QSTR_plurals__x_groups_needed;
|
||||||
MP_QSTR_plurals__x_shares_needed;
|
MP_QSTR_plurals__x_shares_needed;
|
||||||
|
MP_QSTR_prefill_word;
|
||||||
MP_QSTR_progress__authenticity_check;
|
MP_QSTR_progress__authenticity_check;
|
||||||
MP_QSTR_progress__done;
|
MP_QSTR_progress__done;
|
||||||
MP_QSTR_progress__loading_transaction;
|
MP_QSTR_progress__loading_transaction;
|
||||||
|
@ -843,6 +843,7 @@ pub enum TranslatedString {
|
|||||||
words__writable = 830,
|
words__writable = 830,
|
||||||
words__yes = 831,
|
words__yes = 831,
|
||||||
reboot_to_bootloader__just_a_moment = 832,
|
reboot_to_bootloader__just_a_moment = 832,
|
||||||
|
inputs__previous = 833,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TranslatedString {
|
impl TranslatedString {
|
||||||
@ -1681,6 +1682,7 @@ impl TranslatedString {
|
|||||||
Self::words__writable => "Writable",
|
Self::words__writable => "Writable",
|
||||||
Self::words__yes => "Yes",
|
Self::words__yes => "Yes",
|
||||||
Self::reboot_to_bootloader__just_a_moment => "Just a moment...",
|
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__writable => Some(Self::words__writable),
|
||||||
Qstr::MP_QSTR_words__yes => Some(Self::words__yes),
|
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_reboot_to_bootloader__just_a_moment => Some(Self::reboot_to_bootloader__just_a_moment),
|
||||||
|
Qstr::MP_QSTR_inputs__previous => Some(Self::inputs__previous),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,8 @@ pub struct Maybe<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T> 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 {
|
Self {
|
||||||
inner,
|
inner,
|
||||||
visible,
|
visible,
|
||||||
@ -19,12 +20,12 @@ impl<T> Maybe<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn visible(clear: Color, inner: T) -> Self {
|
pub fn visible(bg_color: Color, inner: T) -> Self {
|
||||||
Self::new(Pad::with_background(clear), inner, true)
|
Self::new(bg_color, inner, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hidden(clear: Color, inner: T) -> Self {
|
pub fn hidden(bg_color: Color, inner: T) -> Self {
|
||||||
Self::new(Pad::with_background(clear), inner, false)
|
Self::new(bg_color, inner, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ enum WordlistAction {
|
|||||||
Letter(char),
|
Letter(char),
|
||||||
Word(&'static str),
|
Word(&'static str),
|
||||||
Delete,
|
Delete,
|
||||||
|
Previous,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_WORD_LENGTH: usize = 10;
|
const MAX_WORD_LENGTH: usize = 10;
|
||||||
@ -47,6 +48,9 @@ struct ChoiceFactoryWordlist {
|
|||||||
offer_words: bool,
|
offer_words: bool,
|
||||||
/// We want to randomize the order in which we show the words
|
/// We want to randomize the order in which we show the words
|
||||||
word_random_order: Vec<usize, OFFER_WORDS_THRESHOLD>,
|
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 {
|
impl ChoiceFactoryWordlist {
|
||||||
@ -72,6 +76,7 @@ impl ChoiceFactoryWordlist {
|
|||||||
wordlist,
|
wordlist,
|
||||||
offer_words,
|
offer_words,
|
||||||
word_random_order,
|
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
|
// Putting DELETE as the first option in both cases
|
||||||
// (is a requirement for WORDS, doing it for LETTERS as well to unite it)
|
// (is a requirement for WORDS, doing it for LETTERS as well to unite it)
|
||||||
if choice_index == DELETE_INDEX {
|
if choice_index == DELETE_INDEX {
|
||||||
return (
|
if self.empty_input {
|
||||||
TR::inputs__delete.map_translated(|t| {
|
return (
|
||||||
ChoiceItem::new(
|
TR::inputs__previous.map_translated(|t| {
|
||||||
t,
|
ChoiceItem::new(
|
||||||
ButtonLayout::arrow_armed_arrow(TR::buttons__confirm.into()),
|
t,
|
||||||
)
|
ButtonLayout::arrow_armed_arrow(TR::buttons__select.into()),
|
||||||
.with_icon(theme::ICON_DELETE)
|
)
|
||||||
.with_middle_action_without_release()
|
.with_icon(theme::ICON_DELETE)
|
||||||
}),
|
.with_middle_action_without_release()
|
||||||
WordlistAction::Delete,
|
}),
|
||||||
);
|
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 {
|
if self.offer_words {
|
||||||
// Taking a random (but always the same) word on this position
|
// Taking a random (but always the same) word on this position
|
||||||
@ -140,10 +159,12 @@ pub struct WordlistEntry {
|
|||||||
textbox: TextBox<MAX_WORD_LENGTH>,
|
textbox: TextBox<MAX_WORD_LENGTH>,
|
||||||
offer_words: bool,
|
offer_words: bool,
|
||||||
wordlist_type: WordlistType,
|
wordlist_type: WordlistType,
|
||||||
|
/// Whether going back is allowed (is not on the very first word).
|
||||||
|
can_go_back: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WordlistEntry {
|
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 = ChoiceFactoryWordlist::new(wordlist_type, "");
|
||||||
let choices_count = <ChoiceFactoryWordlist as ChoiceFactory>::count(&choices);
|
let choices_count = <ChoiceFactoryWordlist as ChoiceFactory>::count(&choices);
|
||||||
Self {
|
Self {
|
||||||
@ -156,6 +177,27 @@ impl WordlistEntry {
|
|||||||
textbox: TextBox::empty(),
|
textbox: TextBox::empty(),
|
||||||
offer_words: false,
|
offer_words: false,
|
||||||
wordlist_type,
|
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> {
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
if let Some((action, long_press)) = self.choice_page.event(ctx, event) {
|
if let Some((action, long_press)) = self.choice_page.event(ctx, event) {
|
||||||
match action {
|
match action {
|
||||||
|
WordlistAction::Previous => {
|
||||||
|
if self.can_go_back {
|
||||||
|
return Some("");
|
||||||
|
}
|
||||||
|
}
|
||||||
WordlistAction::Delete => {
|
WordlistAction::Delete => {
|
||||||
// Deleting all when long-pressed
|
// Deleting all when long-pressed
|
||||||
if long_press {
|
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 {
|
extern "C" fn new_request_bip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||||
let block = |_args: &[Obj], kwargs: &Map| {
|
let block = |_args: &[Obj], kwargs: &Map| {
|
||||||
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
||||||
|
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(
|
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())
|
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 {
|
extern "C" fn new_request_slip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||||
let block = |_args: &[Obj], kwargs: &Map| {
|
let block = |_args: &[Obj], kwargs: &Map| {
|
||||||
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
||||||
|
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(
|
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())
|
Ok(obj.into())
|
||||||
};
|
};
|
||||||
@ -1972,6 +1992,8 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
|||||||
/// def request_bip39(
|
/// def request_bip39(
|
||||||
/// *,
|
/// *,
|
||||||
/// prompt: str,
|
/// prompt: str,
|
||||||
|
/// prefill_word: str,
|
||||||
|
/// can_go_back: bool,
|
||||||
/// ) -> str:
|
/// ) -> str:
|
||||||
/// """Get recovery word for BIP39."""
|
/// """Get recovery word for BIP39."""
|
||||||
Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(),
|
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(
|
/// def request_slip39(
|
||||||
/// *,
|
/// *,
|
||||||
/// prompt: str,
|
/// prompt: str,
|
||||||
|
/// prefill_word: str,
|
||||||
|
/// can_go_back: bool,
|
||||||
/// ) -> str:
|
/// ) -> str:
|
||||||
/// """SLIP39 word input keyboard."""
|
/// """SLIP39 word input keyboard."""
|
||||||
Qstr::MP_QSTR_request_slip39 => obj_fn_kw!(0, new_request_slip39).as_obj(),
|
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_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_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, "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, "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!(ICON_ARROW_RIGHT_FAT, "model_tr/res/arrow_right_fat.toif"); // 4*8
|
||||||
include_icon!(
|
include_icon!(
|
||||||
|
@ -16,6 +16,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use heapless::String;
|
||||||
|
|
||||||
const MAX_LENGTH: usize = 8;
|
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
|
/// Compute a bitmask of all letters contained in given key text. Lowest bit
|
||||||
/// is 'a', second lowest 'b', etc.
|
/// is 'a', second lowest 'b', etc.
|
||||||
fn key_mask(key: usize) -> u32 {
|
fn key_mask(key: usize) -> u32 {
|
||||||
|
@ -2,7 +2,7 @@ use crate::ui::{
|
|||||||
component::{maybe::paint_overlapping, Child, Component, Event, EventCtx, Label, Maybe},
|
component::{maybe::paint_overlapping, Child, Component, Event, EventCtx, Label, Maybe},
|
||||||
geometry::{Alignment2D, Grid, Offset, Rect},
|
geometry::{Alignment2D, Grid, Offset, Rect},
|
||||||
model_tt::{
|
model_tt::{
|
||||||
component::{Button, ButtonMsg},
|
component::{Button, ButtonMsg, Swipe, SwipeDirection},
|
||||||
theme,
|
theme,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -11,6 +11,7 @@ pub const MNEMONIC_KEY_COUNT: usize = 9;
|
|||||||
|
|
||||||
pub enum MnemonicKeyboardMsg {
|
pub enum MnemonicKeyboardMsg {
|
||||||
Confirmed,
|
Confirmed,
|
||||||
|
Previous,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MnemonicKeyboard<T, U> {
|
pub struct MnemonicKeyboard<T, U> {
|
||||||
@ -22,6 +23,10 @@ pub struct MnemonicKeyboard<T, U> {
|
|||||||
input: Child<Maybe<T>>,
|
input: Child<Maybe<T>>,
|
||||||
/// Key buttons.
|
/// Key buttons.
|
||||||
keys: [Child<Button<&'static str>>; MNEMONIC_KEY_COUNT],
|
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>
|
impl<T, U> MnemonicKeyboard<T, U>
|
||||||
@ -29,13 +34,17 @@ where
|
|||||||
T: MnemonicInput,
|
T: MnemonicInput,
|
||||||
U: AsRef<str>,
|
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 {
|
Self {
|
||||||
prompt: Child::new(Maybe::visible(
|
prompt: Child::new(Maybe::new(
|
||||||
theme::BG,
|
theme::BG,
|
||||||
Label::centered(prompt, theme::label_keyboard_prompt()),
|
Label::centered(prompt, theme::label_keyboard_prompt()),
|
||||||
|
prompt_visible,
|
||||||
)),
|
)),
|
||||||
back: Child::new(Maybe::hidden(
|
back: Child::new(Maybe::new(
|
||||||
theme::BG,
|
theme::BG,
|
||||||
Button::with_icon_blend(
|
Button::with_icon_blend(
|
||||||
theme::IMAGE_BG_BACK_BTN_TALL,
|
theme::IMAGE_BG_BACK_BTN_TALL,
|
||||||
@ -44,11 +53,14 @@ where
|
|||||||
)
|
)
|
||||||
.styled(theme::button_reset())
|
.styled(theme::button_reset())
|
||||||
.with_long_press(theme::ERASE_HOLD_DURATION),
|
.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()
|
keys: T::keys()
|
||||||
.map(|t| Button::with_text(t).styled(theme::button_pin()))
|
.map(|t| Button::with_text(t).styled(theme::button_pin()))
|
||||||
.map(Child::new),
|
.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_size = self.prompt.inner().inner().max_size();
|
||||||
let prompt_area = Rect::snap(prompt_center, prompt_size, Alignment2D::CENTER);
|
let prompt_area = Rect::snap(prompt_center, prompt_size, Alignment2D::CENTER);
|
||||||
|
|
||||||
|
self.swipe.place(bounds);
|
||||||
self.prompt.place(prompt_area);
|
self.prompt.place(prompt_area);
|
||||||
self.back.place(back_area);
|
self.back.place(back_area);
|
||||||
self.input.place(input_area);
|
self.input.place(input_area);
|
||||||
@ -116,6 +129,13 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
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) {
|
match self.input.event(ctx, event) {
|
||||||
Some(MnemonicInputMsg::Confirmed) => {
|
Some(MnemonicInputMsg::Confirmed) => {
|
||||||
// Confirmed, bubble up.
|
// Confirmed, bubble up.
|
||||||
|
@ -79,8 +79,7 @@ where
|
|||||||
let erase_btn = Maybe::hidden(theme::BG, erase_btn).into_child();
|
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 = Button::with_icon(theme::ICON_CANCEL).styled(theme::button_cancel());
|
||||||
let cancel_btn =
|
let cancel_btn = Maybe::new(theme::BG, cancel_btn, allow_cancel).into_child();
|
||||||
Maybe::new(Pad::with_background(theme::BG), cancel_btn, allow_cancel).into_child();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
allow_cancel,
|
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
|
/// Convert a key index into the key digit. This is what we push into the
|
||||||
/// input buffer.
|
/// input buffer.
|
||||||
///
|
///
|
||||||
|
@ -177,6 +177,7 @@ where
|
|||||||
panic!("invalid mnemonic")
|
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 {
|
extern "C" fn new_request_bip39(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 prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
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())
|
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) }
|
||||||
@ -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 {
|
extern "C" fn new_request_slip39(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 prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
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())
|
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) }
|
||||||
@ -1936,6 +1949,8 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
|||||||
/// def request_bip39(
|
/// def request_bip39(
|
||||||
/// *,
|
/// *,
|
||||||
/// prompt: str,
|
/// prompt: str,
|
||||||
|
/// prefill_word: str,
|
||||||
|
/// can_go_back: bool,
|
||||||
/// ) -> str:
|
/// ) -> str:
|
||||||
/// """BIP39 word input keyboard."""
|
/// """BIP39 word input keyboard."""
|
||||||
Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(),
|
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(
|
/// def request_slip39(
|
||||||
/// *,
|
/// *,
|
||||||
/// prompt: str,
|
/// prompt: str,
|
||||||
|
/// prefill_word: str,
|
||||||
|
/// can_go_back: bool,
|
||||||
/// ) -> str:
|
/// ) -> str:
|
||||||
/// """SLIP39 word input keyboard."""
|
/// """SLIP39 word input keyboard."""
|
||||||
Qstr::MP_QSTR_request_slip39 => obj_fn_kw!(0, new_request_slip39).as_obj(),
|
Qstr::MP_QSTR_request_slip39 => obj_fn_kw!(0, new_request_slip39).as_obj(),
|
||||||
|
@ -308,6 +308,8 @@ def request_passphrase(
|
|||||||
def request_bip39(
|
def request_bip39(
|
||||||
*,
|
*,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
|
prefill_word: str,
|
||||||
|
can_go_back: bool,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get recovery word for BIP39."""
|
"""Get recovery word for BIP39."""
|
||||||
|
|
||||||
@ -316,6 +318,8 @@ def request_bip39(
|
|||||||
def request_slip39(
|
def request_slip39(
|
||||||
*,
|
*,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
|
prefill_word: str,
|
||||||
|
can_go_back: bool,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""SLIP39 word input keyboard."""
|
"""SLIP39 word input keyboard."""
|
||||||
|
|
||||||
@ -756,6 +760,8 @@ def request_passphrase(
|
|||||||
def request_bip39(
|
def request_bip39(
|
||||||
*,
|
*,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
|
prefill_word: str,
|
||||||
|
can_go_back: bool,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""BIP39 word input keyboard."""
|
"""BIP39 word input keyboard."""
|
||||||
|
|
||||||
@ -764,6 +770,8 @@ def request_bip39(
|
|||||||
def request_slip39(
|
def request_slip39(
|
||||||
*,
|
*,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
|
prefill_word: str,
|
||||||
|
can_go_back: bool,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""SLIP39 word input keyboard."""
|
"""SLIP39 word input keyboard."""
|
||||||
|
|
||||||
|
@ -337,6 +337,7 @@ class TR:
|
|||||||
inputs__cancel: str = "CANCEL"
|
inputs__cancel: str = "CANCEL"
|
||||||
inputs__delete: str = "DELETE"
|
inputs__delete: str = "DELETE"
|
||||||
inputs__enter: str = "ENTER"
|
inputs__enter: str = "ENTER"
|
||||||
|
inputs__previous: str = "PREVIOUS"
|
||||||
inputs__return: str = "RETURN"
|
inputs__return: str = "RETURN"
|
||||||
inputs__show: str = "SHOW"
|
inputs__show: str = "SHOW"
|
||||||
inputs__space: str = "SPACE"
|
inputs__space: str = "SPACE"
|
||||||
|
@ -50,15 +50,35 @@ async def request_mnemonic(
|
|||||||
|
|
||||||
await button_request("mnemonic", code=ButtonRequestType.MnemonicInput)
|
await button_request("mnemonic", code=ButtonRequestType.MnemonicInput)
|
||||||
|
|
||||||
words: list[str] = []
|
# Allowing to go back to previous words, therefore cannot use just loop over range(word_count)
|
||||||
for i in 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(
|
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:
|
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:
|
except word_validity.AlreadyAdded:
|
||||||
# show_share_already_added
|
# show_share_already_added
|
||||||
await show_recovery_warning(
|
await show_recovery_warning(
|
||||||
|
@ -18,15 +18,27 @@ async def request_word_count(dry_run: bool) -> int:
|
|||||||
return int(count)
|
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
|
from trezor.wire.context import wait
|
||||||
|
|
||||||
prompt = TR.recovery__word_x_of_y_template.format(word_index + 1, word_count)
|
prompt = TR.recovery__word_x_of_y_template.format(word_index + 1, word_count)
|
||||||
|
|
||||||
|
can_go_back = word_index > 0
|
||||||
|
|
||||||
if is_slip39:
|
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:
|
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)
|
word: str = await wait(word_choice)
|
||||||
return word
|
return word
|
||||||
|
@ -32,12 +32,23 @@ async def request_word_count(dry_run: bool) -> int:
|
|||||||
return int(count)
|
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)
|
prompt = TR.recovery__type_word_x_of_y_template.format(word_index + 1, word_count)
|
||||||
|
can_go_back = word_index > 0
|
||||||
if is_slip39:
|
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:
|
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)
|
word: str = await ctx_wait(keyboard)
|
||||||
return word
|
return word
|
||||||
|
@ -19,7 +19,7 @@ def translate_dict(
|
|||||||
translated_text = translator.translate(
|
translated_text = translator.translate(
|
||||||
value, src=from_lang, dest=to_lang
|
value, src=from_lang, dest=to_lang
|
||||||
).text
|
).text
|
||||||
new_dict[key] = translated_text + " (TODO)"
|
new_dict[key] = translated_text
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error translating {value}: {e}")
|
print(f"Error translating {value}: {e}")
|
||||||
new_dict[key] = MISSING_VALUE
|
new_dict[key] = MISSING_VALUE
|
||||||
|
@ -355,6 +355,7 @@
|
|||||||
"inputs__cancel": "ZRUŠIT",
|
"inputs__cancel": "ZRUŠIT",
|
||||||
"inputs__delete": "SMAZAT",
|
"inputs__delete": "SMAZAT",
|
||||||
"inputs__enter": "ZADAT",
|
"inputs__enter": "ZADAT",
|
||||||
|
"inputs__previous": "PŘEDCHOZÍ",
|
||||||
"inputs__return": "VRÁTIT",
|
"inputs__return": "VRÁTIT",
|
||||||
"inputs__show": "UKÁZAT",
|
"inputs__show": "UKÁZAT",
|
||||||
"inputs__space": "MEZERNÍK",
|
"inputs__space": "MEZERNÍK",
|
||||||
@ -512,6 +513,7 @@
|
|||||||
"progress__signing_transaction": "Podpis transakce...",
|
"progress__signing_transaction": "Podpis transakce...",
|
||||||
"progress__syncing": "Synchronizace...",
|
"progress__syncing": "Synchronizace...",
|
||||||
"progress__x_seconds_left_template": "Zbývá {} sekund",
|
"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__restart": "Chcete restartovat Trezor v režimu bootloader?",
|
||||||
"reboot_to_bootloader__title": "PŘEJÍT DO REŽIMU BOOTLOADER",
|
"reboot_to_bootloader__title": "PŘEJÍT DO REŽIMU BOOTLOADER",
|
||||||
"reboot_to_bootloader__version_by_template": "Verze firmwaru {}\nod {}",
|
"reboot_to_bootloader__version_by_template": "Verze firmwaru {}\nod {}",
|
||||||
|
@ -355,6 +355,7 @@
|
|||||||
"inputs__cancel": "ABBRECHEN",
|
"inputs__cancel": "ABBRECHEN",
|
||||||
"inputs__delete": "LÖSCHEN",
|
"inputs__delete": "LÖSCHEN",
|
||||||
"inputs__enter": "EINGEBEN",
|
"inputs__enter": "EINGEBEN",
|
||||||
|
"inputs__previous": "VORHERIGE",
|
||||||
"inputs__return": "ZURÜCK",
|
"inputs__return": "ZURÜCK",
|
||||||
"inputs__show": "ANZEIGEN",
|
"inputs__show": "ANZEIGEN",
|
||||||
"inputs__space": "LEER",
|
"inputs__space": "LEER",
|
||||||
@ -512,6 +513,7 @@
|
|||||||
"progress__signing_transaction": "Transaktion wird signiert...",
|
"progress__signing_transaction": "Transaktion wird signiert...",
|
||||||
"progress__syncing": "Wird synchronisiert...",
|
"progress__syncing": "Wird synchronisiert...",
|
||||||
"progress__x_seconds_left_template": "{} Sekunden verbleibend",
|
"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__restart": "Möchtest du Trezor im Bootloader-Modus neu starten?",
|
||||||
"reboot_to_bootloader__title": "ZUM BOOTLOADER",
|
"reboot_to_bootloader__title": "ZUM BOOTLOADER",
|
||||||
"reboot_to_bootloader__version_by_template": "Firmware Version {}\nvon {}",
|
"reboot_to_bootloader__version_by_template": "Firmware Version {}\nvon {}",
|
||||||
|
@ -343,6 +343,7 @@
|
|||||||
"inputs__delete": "DELETE",
|
"inputs__delete": "DELETE",
|
||||||
"inputs__enter": "ENTER",
|
"inputs__enter": "ENTER",
|
||||||
"inputs__return": "RETURN",
|
"inputs__return": "RETURN",
|
||||||
|
"inputs__previous": "PREVIOUS",
|
||||||
"inputs__show": "SHOW",
|
"inputs__show": "SHOW",
|
||||||
"inputs__space": "SPACE",
|
"inputs__space": "SPACE",
|
||||||
"joint__title": "JOINT TRANSACTION",
|
"joint__title": "JOINT TRANSACTION",
|
||||||
|
@ -355,6 +355,7 @@
|
|||||||
"inputs__cancel": "CANCELAR",
|
"inputs__cancel": "CANCELAR",
|
||||||
"inputs__delete": "ELIMINAR",
|
"inputs__delete": "ELIMINAR",
|
||||||
"inputs__enter": "INTRODUCIR",
|
"inputs__enter": "INTRODUCIR",
|
||||||
|
"inputs__previous": "ANTERIOR",
|
||||||
"inputs__return": "VOLVER",
|
"inputs__return": "VOLVER",
|
||||||
"inputs__show": "MOSTRAR",
|
"inputs__show": "MOSTRAR",
|
||||||
"inputs__space": "ESPACIO",
|
"inputs__space": "ESPACIO",
|
||||||
@ -512,6 +513,7 @@
|
|||||||
"progress__signing_transaction": "Firmando transacción...",
|
"progress__signing_transaction": "Firmando transacción...",
|
||||||
"progress__syncing": "Sincronizando...",
|
"progress__syncing": "Sincronizando...",
|
||||||
"progress__x_seconds_left_template": "Quedan {} segundos",
|
"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__restart": "¿Deseas reiniciar el Trezor en modo bootloader?",
|
||||||
"reboot_to_bootloader__title": "IR A BOOTLOADER",
|
"reboot_to_bootloader__title": "IR A BOOTLOADER",
|
||||||
"reboot_to_bootloader__version_by_template": "Versión de firmware {}\npor {}",
|
"reboot_to_bootloader__version_by_template": "Versión de firmware {}\npor {}",
|
||||||
|
@ -355,6 +355,7 @@
|
|||||||
"inputs__cancel": "ANNULER",
|
"inputs__cancel": "ANNULER",
|
||||||
"inputs__delete": "SUPPRIMER",
|
"inputs__delete": "SUPPRIMER",
|
||||||
"inputs__enter": "SAISIR",
|
"inputs__enter": "SAISIR",
|
||||||
|
"inputs__previous": "PREVIOUS",
|
||||||
"inputs__return": "RETOUR",
|
"inputs__return": "RETOUR",
|
||||||
"inputs__show": "AFFICHER",
|
"inputs__show": "AFFICHER",
|
||||||
"inputs__space": "ESPACE",
|
"inputs__space": "ESPACE",
|
||||||
@ -512,6 +513,7 @@
|
|||||||
"progress__signing_transaction": "Signature de la transaction en cours",
|
"progress__signing_transaction": "Signature de la transaction en cours",
|
||||||
"progress__syncing": "Synchronisation en cours",
|
"progress__syncing": "Synchronisation en cours",
|
||||||
"progress__x_seconds_left_template": "{} secondes restantes",
|
"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__restart": "Voulez-vous redémarrer Trezor en mode bootloader?",
|
||||||
"reboot_to_bootloader__title": "ACCÉDER BOOTLOADER",
|
"reboot_to_bootloader__title": "ACCÉDER BOOTLOADER",
|
||||||
"reboot_to_bootloader__version_by_template": "Version du firmware {}\npar {}",
|
"reboot_to_bootloader__version_by_template": "Version du firmware {}\npar {}",
|
||||||
|
@ -831,5 +831,6 @@
|
|||||||
"829": "words__warning",
|
"829": "words__warning",
|
||||||
"830": "words__writable",
|
"830": "words__writable",
|
||||||
"831": "words__yes",
|
"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)
|
CANCEL = (LEFT, BOTTOM)
|
||||||
INFO = (MID, BOTTOM)
|
INFO = (MID, BOTTOM)
|
||||||
|
|
||||||
|
RECOVERY_DELETE = (LEFT, TOP)
|
||||||
|
|
||||||
CORNER_BUTTON = (215, 25)
|
CORNER_BUTTON = (215, 25)
|
||||||
|
|
||||||
CONFIRM_WORD = (MID, TOP)
|
CONFIRM_WORD = (MID, TOP)
|
||||||
|
@ -4,6 +4,7 @@ from enum import Enum
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from .. import buttons
|
from .. import buttons
|
||||||
|
from .. import translations as TR
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from trezorlib.debuglink import DebugLink, LayoutContent
|
from trezorlib.debuglink import DebugLink, LayoutContent
|
||||||
@ -147,3 +148,7 @@ def _move_one_closer(
|
|||||||
return debug.press_left(wait=True)
|
return debug.press_left(wait=True)
|
||||||
else:
|
else:
|
||||||
return debug.press_right(wait=True)
|
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 buttons
|
||||||
from .. import translations as TR
|
from .. import translations as TR
|
||||||
from .common import go_next
|
from .common import get_possible_btn_texts, go_next
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from trezorlib.debuglink import DebugLink, LayoutContent
|
from trezorlib.debuglink import DebugLink, LayoutContent
|
||||||
|
|
||||||
|
|
||||||
|
DELETE_BTN_TEXTS = get_possible_btn_texts("inputs__delete") + get_possible_btn_texts(
|
||||||
|
"inputs__previous"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def enter_word(
|
def enter_word(
|
||||||
debug: "DebugLink", word: str, is_slip39: bool = False
|
debug: "DebugLink", word: str, is_slip39: bool = False
|
||||||
) -> "LayoutContent":
|
) -> "LayoutContent":
|
||||||
@ -117,6 +122,60 @@ def enter_shares(debug: "DebugLink", shares: list[str]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def enter_seed(debug: "DebugLink", seed_words: 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")
|
TR.assert_in(debug.read_layout().text_content(), "recovery__enter_backup")
|
||||||
if debug.model == "T":
|
if debug.model == "T":
|
||||||
debug.click(buttons.OK, wait=True)
|
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)
|
debug.press_right(wait=True)
|
||||||
TR.assert_equals(debug.read_layout().title(), "recovery__title_recover")
|
TR.assert_equals(debug.read_layout().title(), "recovery__title_recover")
|
||||||
debug.press_right()
|
debug.press_right()
|
||||||
|
layout = debug.press_right(wait=True)
|
||||||
debug.press_right(wait=True)
|
assert "MnemonicKeyboard" in layout.all_components()
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
def finalize(debug: "DebugLink") -> None:
|
def finalize(debug: "DebugLink") -> None:
|
||||||
|
@ -24,7 +24,12 @@ from trezorlib import device, exceptions
|
|||||||
|
|
||||||
from .. import buttons
|
from .. import buttons
|
||||||
from .. import translations as TR
|
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:
|
if TYPE_CHECKING:
|
||||||
from trezorlib.debuglink import DebugLink
|
from trezorlib.debuglink import DebugLink
|
||||||
@ -42,14 +47,9 @@ PIN24 = "875163065288639289952973"
|
|||||||
PIN50 = "31415926535897932384626433832795028841971693993751"
|
PIN50 = "31415926535897932384626433832795028841971693993751"
|
||||||
PIN60 = PIN50 + "9" * 10
|
PIN60 = PIN50 + "9" * 10
|
||||||
|
|
||||||
|
DELETE = get_possible_btn_texts("inputs__delete")
|
||||||
def _get_possible_btns(path: str) -> str:
|
SHOW = get_possible_btn_texts("inputs__show")
|
||||||
return "|".join(TR.translate(path))
|
ENTER = get_possible_btn_texts("inputs__enter")
|
||||||
|
|
||||||
|
|
||||||
DELETE = _get_possible_btns("inputs__delete")
|
|
||||||
SHOW = _get_possible_btns("inputs__show")
|
|
||||||
ENTER = _get_possible_btns("inputs__enter")
|
|
||||||
|
|
||||||
|
|
||||||
TR_PIN_ACTIONS = [
|
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.select_number_of_words(debug, num_of_words=12)
|
||||||
recovery.enter_seed(debug, MNEMONIC12.split())
|
recovery.enter_seed(debug, MNEMONIC12.split())
|
||||||
recovery.finalize(debug)
|
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_pin_short": "a87619c455ec81763cbb133279f9e7fbb3b9a5a02c58e1da63640d51a4e107c3",
|
||||||
"TR_en_test_pin.py::test_wipe_code_same_as_pin": "329962b3b949dc1429d361e4ae6c5224b4a75c981ae833fd3bf1618df44fbd2a",
|
"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_pin.py::test_wipe_code_setup": "4afe5ceb8f953892eea8b6b7c6997a1b5afa40b81372265a84a567b61773d89b",
|
||||||
"TR_en_test_recovery.py::test_recovery_bip39": "43a56fb1dea38503cfddf55c4a6e8d5a5188be7354bd5c2b31fde197f2af3443",
|
"TR_en_test_recovery.py::test_recovery_bip39": "f70c19e62380af74f43fbfa5fb75c03db47417348477298c51c88e37f1240444",
|
||||||
"TR_en_test_recovery.py::test_recovery_slip39_basic": "40df8fd6302577ff4f1d1d6d214d1ffd22791912ee4225ae86460a3fa3f3ac0f",
|
"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_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[16of16]": "53763c7177a26069cc164a84d93c6b6b53bfe3f58a770913b4b62b537fd9301d",
|
||||||
"TR_en_test_reset_slip39_advanced.py::test_reset_slip39_advanced[2of2]": "ae94835bc49ebce6fdaa05671734701f6d2911cdee54e99e083edddbb070ffa7",
|
"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_same_as_pin": "f13b6559d4d73c2853218fee85dc951c41c5b4e3867ee0e989443940233f2f96",
|
||||||
"TT_en_test_pin.py::test_wipe_code_setup": "3d6a04cc7c8d3a061758a9559277a548bf4492ef59afd1d040693372d197383c",
|
"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": "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_recovery.py::test_recovery_slip39_basic": "9b0f5a7b8d2ab0fed1e5389076bc035e24dce377d275824220f1aa61e9bb4810",
|
||||||
"TT_en_test_reset_bip39.py::test_reset_bip39": "19fd9f6233d72224696c547528a0079934a86cb41539b6f7149aab57b0aaec42",
|
"TT_en_test_reset_bip39.py::test_reset_bip39": "19fd9f6233d72224696c547528a0079934a86cb41539b6f7149aab57b0aaec42",
|
||||||
"TT_en_test_reset_slip39_advanced.py::test_reset_slip39_advanced[16of16]": "1d604f1766ce861e616745dcb5c89122165eb26ecc7f40039e50b8fe8c61a861",
|
"TT_en_test_reset_slip39_advanced.py::test_reset_slip39_advanced[16of16]": "1d604f1766ce861e616745dcb5c89122165eb26ecc7f40039e50b8fe8c61a861",
|
||||||
|
Loading…
Reference in New Issue
Block a user