diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 9193e451..1a422d99 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -100,6 +100,8 @@ class FontAwesomeIconConstants: SQUARE_CARET_UP = "\uf151" UNLOCK = "\uf09c" X = "\u0058" + NUMBER_0 = "\u0030" + NUMBER_1 = "\u0031" class SeedSignerIconConstants: diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 64fb7ea9..554935ca 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -17,18 +17,24 @@ from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants +import seedsigner.helpers.seed_format_transformers as seed_format_transformers + logger = logging.getLogger(__name__) @dataclass class SeedMnemonicEntryScreen(BaseTopNavScreen): - initial_letters: list = None + current_word: str = None wordlist: list = None + possible_alphabet: str = "abcdefghijklmnopqrstuvwxyz" def __post_init__(self): super().__post_init__() - self.possible_alphabet = "abcdefghijklmnopqrstuvwxyz" + if not self.current_word: + self.initial_letters = [self.possible_alphabet[0]] + else: + self.initial_letters = list(self.current_word) # Set up the keyboard params self.keyboard_width = 128 @@ -404,6 +410,397 @@ def _run(self): self.renderer.show_image() +@dataclass +class SeedMnemonicDecimalEntryScreen(BaseTopNavScreen): + current_word: str = None + wordlist: list = None + possible_alphabet: str = "1234567890" + + def __post_init__(self): + super().__post_init__() + + if not self.current_word: + self.initial_letters = [self.possible_alphabet[0]] + else: + self.initial_letters = list(seed_format_transformers.convert_word_to_decimal("".join(self.current_word))) + + self.keyboard_width = 200 + text_entry_display_y = self.top_nav.height + text_entry_display_height = 30 + + self.keyboard = Keyboard( + draw=self.image_draw, + charset=self.possible_alphabet, + rows=3, + cols=4, + rect=( + GUIConstants.EDGE_PADDING, + text_entry_display_y + text_entry_display_height + 6, + GUIConstants.EDGE_PADDING + self.keyboard_width, + self.canvas_height + ), + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT] + ) + + self.decimal_seed_word_length = 4 + + self.text_entry_display = TextEntryDisplay( + canvas=self.canvas, + rect=( + GUIConstants.EDGE_PADDING, + text_entry_display_y, + GUIConstants.EDGE_PADDING + (GUIConstants.BUTTON_FONT_SIZE * self.decimal_seed_word_length), + text_entry_display_y + text_entry_display_height + ), + is_centered=False, + cur_text="".join(self.initial_letters) + ) + + self.letters = self.initial_letters + + if len(self.letters) > 1: + self.letters.append(" ") # "Lock in" the last letter as if KEY_PRESS + self.calc_possible_alphabet() + self.keyboard.update_active_keys(active_keys=self.possible_alphabet) + self.keyboard.set_selected_key(selected_letter=self.letters[-2]) + else: + self.keyboard.set_selected_key(selected_letter=self.letters[-1]) + + def calc_possible_alphabet(self): + valid_characters = [] + for character in "1234567890": + prediction_number = int("".join(self.letters).strip() + character) + if prediction_number <= 2047: + valid_characters.append(character) + self.possible_alphabet = "".join(valid_characters) + + def _render(self): + super()._render() + self.keyboard.render_keys() + self.text_entry_display.render() + self.renderer.show_image() + + def _run(self): + while True: + final_selection = None + + input = self.hw_inputs.wait_for( + HardwareButtonsConstants.ALL_KEYS, + check_release=True, + release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY2] + ) + + if self.is_input_in_top_nav: + if input == HardwareButtonsConstants.KEY_PRESS: + # User clicked the "back" arrow + return RET_CODE__BACK_BUTTON + + elif input == HardwareButtonsConstants.KEY_UP: + input = Keyboard.ENTER_BOTTOM + self.is_input_in_top_nav = False + # Re-render it without the highlight + self.top_nav.left_button.is_selected = False + self.top_nav.left_button.render() + + elif input == HardwareButtonsConstants.KEY_DOWN: + input = Keyboard.ENTER_TOP + self.is_input_in_top_nav = False + # Re-render it without the highlight + self.top_nav.left_button.is_selected = False + self.top_nav.left_button.render() + + elif input in [HardwareButtonsConstants.KEY_RIGHT, HardwareButtonsConstants.KEY_LEFT]: + # no action in this context + continue + + ret_val = self.keyboard.update_from_input(input) + + if ret_val in Keyboard.EXIT_DIRECTIONS: + self.is_input_in_top_nav = True + self.top_nav.left_button.is_selected = True + self.top_nav.left_button.render() + + elif ret_val in Keyboard.ADDITIONAL_KEYS: + if input == HardwareButtonsConstants.KEY_PRESS and ret_val == Keyboard.KEY_BACKSPACE["code"]: + self.letters = self.letters[:-2] + self.letters.append(" ") + + self.calc_possible_alphabet() + self.keyboard.update_active_keys(active_keys=self.possible_alphabet) + self.keyboard.render_keys() + + elif ret_val == Keyboard.KEY_BACKSPACE["code"]: + # We're just hovering over DEL but haven't clicked. Show blank (" ") + # in the live text entry display at the top. + self.letters = self.letters[:-1] + self.letters.append(" ") + + elif input == HardwareButtonsConstants.KEY_PRESS and ret_val in self.possible_alphabet: + # User has locked in the current letter + if self.letters[-1] != " ": + self.letters.append(" ") + else: + self.letters = self.letters[:-1] + self.letters.append(ret_val) + self.letters.append(" ") + + self.calc_possible_alphabet() + self.keyboard.update_active_keys(active_keys=self.possible_alphabet) + self.keyboard.render_keys() + + if len(self.letters) > (self.decimal_seed_word_length): + final_selection = True + + + elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN \ + or input in (Keyboard.ENTER_TOP, Keyboard.ENTER_BOTTOM): + if not len(self.letters) > self.decimal_seed_word_length: + self.letters = self.letters[:-1] + self.letters.append(ret_val) + + # Has the user made a final selection of a candidate word? + if input == HardwareButtonsConstants.KEY2: + final_selection = True + + if final_selection: + current_entered_value = "".join(self.letters[:-1]) + if current_entered_value != "": + current_entered_value_int = int(current_entered_value) + if (current_entered_value_int < 0 or current_entered_value_int > 2047): + raise ValueError("Invalid word number (must be between 0 and 2047)") + self.text_entry_display.cur_text = str(current_entered_value_int) + self.text_entry_display.render() + self.renderer.show_image() + + return seed_format_transformers.convert_decimal_to_word(current_entered_value_int) + + # Render the text entry display and cursor block + self.text_entry_display.cur_text = ''.join(self.letters) + self.text_entry_display.render() + + # Now issue one call to send the pixels to the screen + self.renderer.show_image() + +@dataclass +class SeedMnemonicBinaryEntryScreen(BaseTopNavScreen): + current_word: str = None + wordlist: list = None + possible_alphabet: str = "01" + + def __post_init__(self): + super().__post_init__() + + if not self.current_word: + self.initial_letters = [self.possible_alphabet[0]] + else: + self.initial_letters = list(seed_format_transformers.convert_word_to_11_bits("".join(self.current_word))) + + self.keyboard_width = 180 + text_entry_display_y = self.top_nav.height + text_entry_display_height = 30 + + self.keyboard = Keyboard( + draw=self.image_draw, + charset=self.possible_alphabet, + rows=2, + cols=2, + rect=( + GUIConstants.EDGE_PADDING, + text_entry_display_y + text_entry_display_height + 6, + GUIConstants.EDGE_PADDING + self.keyboard_width, + self.canvas_height + ), + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT] + ) + + self.binary_seed_word_length = 11 + + self.text_entry_display = TextEntryDisplay( + canvas=self.canvas, + rect=( + GUIConstants.EDGE_PADDING, + text_entry_display_y, + GUIConstants.EDGE_PADDING + (GUIConstants.BODY_FONT_MIN_SIZE * self.binary_seed_word_length), + text_entry_display_y + text_entry_display_height + ), + is_centered=False, + cur_text="".join(self.initial_letters) + ) + + self.highlighted_row_y = int((self.canvas_height - GUIConstants.BUTTON_HEIGHT)/2) + binary_buttons_width = GUIConstants.BUTTON_HEIGHT + GUIConstants.EDGE_PADDING + binary_buttons_height = int(0.75*GUIConstants.BUTTON_HEIGHT) + + # Button '1' for KEY1 + self.one_button = IconButton( + icon_name=FontAwesomeIconConstants.NUMBER_1, + icon_size=GUIConstants.ICON_FONT_SIZE, + is_text_centered=False, + screen_x=self.canvas_width - binary_buttons_width + GUIConstants.COMPONENT_PADDING, + screen_y=self.highlighted_row_y - 3*GUIConstants.COMPONENT_PADDING - GUIConstants.BUTTON_HEIGHT, + width=binary_buttons_width, + height=binary_buttons_height + 1, + ) + + # Button '0' for KEY3 + self.zero_button = IconButton( + icon_name=FontAwesomeIconConstants.NUMBER_0, + icon_size=GUIConstants.ICON_FONT_SIZE, + is_text_centered=False, + screen_x=self.canvas_width - binary_buttons_width + GUIConstants.COMPONENT_PADDING, + screen_y=self.highlighted_row_y + GUIConstants.BUTTON_HEIGHT + 3*GUIConstants.COMPONENT_PADDING, + width=binary_buttons_width, + height=binary_buttons_height + 1, + ) + + self.one_button.render() + self.zero_button.render() + + self.letters = self.initial_letters + + if len(self.letters) > 1: + self.letters.append(" ") # "Lock in" the last letter as if KEY_PRESS + self.calc_possible_alphabet() + self.keyboard.update_active_keys(active_keys=self.possible_alphabet) + self.keyboard.set_selected_key(selected_letter=self.letters[-2]) + else: + self.keyboard.set_selected_key(selected_letter=self.letters[-1]) + + def calc_possible_alphabet(self): + if (len(self.letters) > self.binary_seed_word_length): + self.possible_alphabet = "" + return + self.possible_alphabet = "01" + + def _render(self): + super()._render() + self.keyboard.render_keys() + self.text_entry_display.render() + self.one_button.render() + self.zero_button.render() + self.renderer.show_image() + + def _run(self): + while True: + final_selection = None + + input = self.hw_inputs.wait_for( + HardwareButtonsConstants.ALL_KEYS, + check_release=True, + release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY2] + ) + + if self.is_input_in_top_nav: + if input == HardwareButtonsConstants.KEY_PRESS: + # User clicked the "back" arrow + return RET_CODE__BACK_BUTTON + + elif input == HardwareButtonsConstants.KEY_UP: + input = Keyboard.ENTER_BOTTOM + self.is_input_in_top_nav = False + # Re-render it without the highlight + self.top_nav.left_button.is_selected = False + self.top_nav.left_button.render() + + elif input == HardwareButtonsConstants.KEY_DOWN: + input = Keyboard.ENTER_TOP + self.is_input_in_top_nav = False + # Re-render it without the highlight + self.top_nav.left_button.is_selected = False + self.top_nav.left_button.render() + + elif input in [HardwareButtonsConstants.KEY_RIGHT, HardwareButtonsConstants.KEY_LEFT]: + # no action in this context + continue + + ret_val = self.keyboard.update_from_input(input) + + if ret_val in Keyboard.EXIT_DIRECTIONS: + self.is_input_in_top_nav = True + self.top_nav.left_button.is_selected = True + self.top_nav.left_button.render() + + elif ret_val in Keyboard.ADDITIONAL_KEYS: + if input == HardwareButtonsConstants.KEY_PRESS and ret_val == Keyboard.KEY_BACKSPACE["code"]: + self.letters = self.letters[:-2] + self.letters.append(" ") + + self.calc_possible_alphabet() + self.keyboard.update_active_keys(active_keys=self.possible_alphabet) + self.keyboard.render_keys() + + elif ret_val == Keyboard.KEY_BACKSPACE["code"]: + # We're just hovering over DEL but haven't clicked. Show blank (" ") + # in the live text entry display at the top. + self.letters = self.letters[:-1] + self.letters.append(" ") + + elif input == HardwareButtonsConstants.KEY_PRESS and ret_val in self.possible_alphabet: + if self.letters[-1] != " ": + self.letters.append(" ") + else: + self.letters = self.letters[:-1] + self.letters.append(ret_val) + self.letters.append(" ") + + self.calc_possible_alphabet() + self.keyboard.update_active_keys(active_keys=self.possible_alphabet) + self.keyboard.render_keys() + + if len(self.letters) > (self.binary_seed_word_length): + final_selection = True + + + elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN \ + or input in (Keyboard.ENTER_TOP, Keyboard.ENTER_BOTTOM): + if not len(self.letters) >= self.binary_seed_word_length: + self.letters = self.letters[:-1] + self.letters.append(ret_val) + + if input == HardwareButtonsConstants.KEY1: + self.letters = self.letters[:-1] + self.letters.append("1") + self.letters.append(" ") + + self.calc_possible_alphabet() + self.keyboard.update_active_keys(active_keys=self.possible_alphabet) + self.keyboard.render_keys() + + if len(self.letters) > (self.binary_seed_word_length): + final_selection = True + + if input == HardwareButtonsConstants.KEY3: + self.letters = self.letters[:-1] + self.letters.append("0") + self.letters.append(" ") + + self.calc_possible_alphabet() + self.keyboard.update_active_keys(active_keys=self.possible_alphabet) + self.keyboard.render_keys() + + if len(self.letters) > (self.binary_seed_word_length): + final_selection = True + # Has the user made a final selection of a candidate word? + if input == HardwareButtonsConstants.KEY2 and len(self.letters) > self.binary_seed_word_length: + final_selection = True + + if final_selection: + current_entered_value = "".join(self.letters[:-1]) + if current_entered_value != "": + if len(current_entered_value) != self.binary_seed_word_length: + raise ValueError("Invalid word binary (must have 11 characters)") + self.text_entry_display.cur_text = current_entered_value + self.text_entry_display.render() + self.renderer.show_image() + + return seed_format_transformers.convert_11_bits_to_word(current_entered_value) + + # Render the text entry display and cursor block + self.text_entry_display.cur_text = ''.join(self.letters) + self.text_entry_display.render() + + # Now issue one call to send the pixels to the screen + self.renderer.show_image() @dataclass class SeedFinalizeScreen(ButtonListScreen): @@ -455,6 +852,27 @@ class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen): num_pages: int = 3 is_bottom_list: bool = True status_color: str = GUIConstants.DIRE_WARNING_COLOR + colorize_words: bool = False + + def draw_seed_word(self, draw, word, number_box_x, number_box_width, font, supersampling_factor, baseline_y, colorize_words): + if colorize_words: + for i, char in enumerate(word): + current_color = [GUIConstants.BODY_FONT_COLOR, GUIConstants.DIRE_WARNING_COLOR, GUIConstants.ACCENT_COLOR][int(i / 3) % 3] + draw.text( + ((GUIConstants.LIST_ITEM_PADDING * supersampling_factor) * i + number_box_x + number_box_width + (GUIConstants.COMPONENT_PADDING * supersampling_factor) + (i * GUIConstants.COMPONENT_PADDING * supersampling_factor), baseline_y), + font=font, + text=char, + fill=current_color, + anchor="ls", + ) + else: + draw.text( + (number_box_x + number_box_width + (GUIConstants.COMPONENT_PADDING * supersampling_factor), baseline_y), + font=font, + text=word, + fill=GUIConstants.BODY_FONT_COLOR, + anchor="ls", # Left, baSeline + ) def __post_init__(self): @@ -512,13 +930,7 @@ def __post_init__(self): ) # Now draw the word - draw.text( - (number_box_x + number_box_width + (GUIConstants.COMPONENT_PADDING * supersampling_factor), baseline_y), - font=font, - text=word, - fill=GUIConstants.BODY_FONT_COLOR, - anchor="ls", # Left, baSeline - ) + self.draw_seed_word(draw, word, number_box_x, number_box_width, font, supersampling_factor, baseline_y, self.colorize_words) number_box_y += number_box_height + (int(1.5*GUIConstants.COMPONENT_PADDING) * supersampling_factor) diff --git a/src/seedsigner/helpers/seed_format_transformers.py b/src/seedsigner/helpers/seed_format_transformers.py new file mode 100644 index 00000000..1c53d22d --- /dev/null +++ b/src/seedsigner/helpers/seed_format_transformers.py @@ -0,0 +1,26 @@ +from embit import bip39 + +def convert_word_to_11_bits(word: str) -> str: + wordlist = bip39.WORDLIST + if word not in wordlist: + raise ValueError(f"Word '{word}' is not in the dictionary") + + return f'{bip39.WORDLIST.index(word):011b}' + +def convert_word_to_decimal(word: str) -> str: + wordlist = bip39.WORDLIST + if word not in wordlist: + raise ValueError(f"Word '{word}' is not in the dictionary") + + return str(bip39.WORDLIST.index(word)) + +def convert_11_bits_to_word(bits: str) -> str: + wordlist = bip39.WORDLIST + if len(bits) != 11: + raise ValueError("Input must be 11 bits") + + return wordlist[int(bits, 2)] + +def convert_decimal_to_word(decimal: str | int) -> str: + wordlist = bip39.WORDLIST + return wordlist[int(decimal)] \ No newline at end of file diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 9483c111..e362a2f7 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -3,6 +3,8 @@ import random import time +from collections.abc import Callable + from binascii import hexlify from embit import bip39 from embit.descriptor import Descriptor @@ -12,6 +14,7 @@ from seedsigner.controller import Controller from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants from seedsigner.helpers import embit_utils +from seedsigner.helpers import seed_format_transformers from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, seed_screens) from seedsigner.gui.screens.screen import LargeIconStatusScreen, QRDisplayScreen @@ -171,6 +174,7 @@ class LoadSeedView(View): SEED_QR = (" Scan a SeedQR", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + OTHER_FORMATS = ("Other seed formats", FontAwesomeIconConstants.KEYBOARD) TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) CREATE = (" Create a seed", SeedSignerIconConstants.PLUS) @@ -179,6 +183,7 @@ def run(self): self.SEED_QR, self.TYPE_12WORD, self.TYPE_24WORD, + self.OTHER_FORMATS, ] if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED: @@ -208,6 +213,9 @@ def run(self): self.controller.storage.init_pending_mnemonic(num_words=24) return Destination(SeedMnemonicEntryView) + elif button_data[selected_menu_num] == self.OTHER_FORMATS: + return Destination(LoadOtherFormatSeedView) + elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: return Destination(SeedElectrumMnemonicStartView) @@ -215,21 +223,59 @@ def run(self): from .tools_views import ToolsMenuView return Destination(ToolsMenuView) +class LoadOtherFormatSeedView(View): + TYPE_12WORD_BINARY = ("12-word binary", FontAwesomeIconConstants.KEYBOARD) + TYPE_24WORD_BINARY = ("24-word binary", FontAwesomeIconConstants.KEYBOARD) + TYPE_12WORD_DECIMAL = ("12-word decimal", FontAwesomeIconConstants.KEYBOARD) + TYPE_24WORD_DECIMAL = ("24-word decimal", FontAwesomeIconConstants.KEYBOARD) + + def run(self): + button_data = [ + self.TYPE_12WORD_BINARY, + self.TYPE_24WORD_BINARY, + self.TYPE_12WORD_DECIMAL, + self.TYPE_24WORD_DECIMAL, + ] + + selected_menu_num = self.run_screen( + ButtonListScreen, + title="Load A Seed", + is_button_text_centered=False, + button_data=button_data + ) + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + elif button_data[selected_menu_num] == self.TYPE_12WORD_BINARY: + self.controller.storage.init_pending_mnemonic(num_words=12) + return Destination(SeedMnemonicEntryView, view_args={"entry_screen_cls": seed_screens.SeedMnemonicBinaryEntryScreen}) + + elif button_data[selected_menu_num] == self.TYPE_24WORD_BINARY: + self.controller.storage.init_pending_mnemonic(num_words=24) + return Destination(SeedMnemonicEntryView, view_args={"entry_screen_cls": seed_screens.SeedMnemonicBinaryEntryScreen}) + + elif button_data[selected_menu_num] == self.TYPE_12WORD_DECIMAL: + self.controller.storage.init_pending_mnemonic(num_words=12) + return Destination(SeedMnemonicEntryView, view_args={"entry_screen_cls": seed_screens.SeedMnemonicDecimalEntryScreen}) + elif button_data[selected_menu_num] == self.TYPE_24WORD_DECIMAL: + self.controller.storage.init_pending_mnemonic(num_words=24) + return Destination(SeedMnemonicEntryView, view_args={"entry_screen_cls": seed_screens.SeedMnemonicDecimalEntryScreen}) class SeedMnemonicEntryView(View): - def __init__(self, cur_word_index: int = 0, is_calc_final_word: bool=False): + def __init__(self, cur_word_index: int = 0, is_calc_final_word: bool=False, entry_screen_cls=seed_screens.SeedMnemonicEntryScreen): super().__init__() self.cur_word_index = cur_word_index self.cur_word = self.controller.storage.get_pending_mnemonic_word(cur_word_index) self.is_calc_final_word = is_calc_final_word - + self.entry_screen_cls = entry_screen_cls def run(self): ret = self.run_screen( - seed_screens.SeedMnemonicEntryScreen, + self.entry_screen_cls, title=f"Seed Word #{self.cur_word_index + 1}", # Human-readable 1-indexing! - initial_letters=list(self.cur_word) if self.cur_word else ["a"], + current_word=self.cur_word, wordlist=Seed.get_wordlist(wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)), ) @@ -260,7 +306,8 @@ def run(self): SeedMnemonicEntryView, view_args={ "cur_word_index": self.cur_word_index + 1, - "is_calc_final_word": self.is_calc_final_word + "is_calc_final_word": self.is_calc_final_word, + "entry_screen_cls": self.entry_screen_cls, } ) else: @@ -268,19 +315,18 @@ def run(self): try: self.controller.storage.convert_pending_mnemonic_to_pending_seed() except InvalidSeedException: - return Destination(SeedMnemonicInvalidView) + return Destination(SeedMnemonicInvalidView, view_args={"entry_screen_cls": self.entry_screen_cls}) return Destination(SeedFinalizeView) - - class SeedMnemonicInvalidView(View): EDIT = "Review & Edit" DISCARD = ("Discard", None, None, "red") - def __init__(self): + def __init__(self, entry_screen_cls=seed_screens.SeedMnemonicEntryScreen): super().__init__() self.mnemonic: List[str] = self.controller.storage.pending_mnemonic + self.entry_screen_cls = entry_screen_cls def run(self): @@ -295,7 +341,7 @@ def run(self): ) if button_data[selected_menu_num] == self.EDIT: - return Destination(SeedMnemonicEntryView, view_args={"cur_word_index": 0}) + return Destination(SeedMnemonicEntryView, view_args={"cur_word_index": 0, "entry_screen_cls": self.entry_screen_cls}) elif button_data[selected_menu_num] == self.DISCARD: self.controller.storage.discard_pending_mnemonic() @@ -611,6 +657,8 @@ def run(self): class SeedBackupView(View): VIEW_WORDS = "View Seed Words" + VIEW_BINARY = "View Seed Binary" + VIEW_DECIMAL = "View Seed Decimal" EXPORT_SEEDQR = "Export as SeedQR" def __init__(self, seed_num): @@ -620,7 +668,7 @@ def __init__(self, seed_num): def run(self): - button_data = [self.VIEW_WORDS] + button_data = [self.VIEW_WORDS, self.VIEW_BINARY, self.VIEW_DECIMAL] if self.seed.seedqr_supported: button_data.append(self.EXPORT_SEEDQR) @@ -641,6 +689,12 @@ def run(self): elif button_data[selected_menu_num] == self.EXPORT_SEEDQR: return Destination(SeedTranscribeSeedQRFormatView, view_args={"seed_num": self.seed_num}) + elif button_data[selected_menu_num] == self.VIEW_BINARY: + return Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "seed_format_transformer": seed_format_transformers.convert_word_to_11_bits}) + + elif button_data[selected_menu_num] == self.VIEW_DECIMAL: + return Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "seed_format_transformer": seed_format_transformers.convert_word_to_decimal}) + """**************************************************************************** @@ -1023,7 +1077,7 @@ def run(self): class SeedWordsView(View): - def __init__(self, seed_num: int, bip85_data: dict = None, page_index: int = 0): + def __init__(self, seed_num: int, bip85_data: dict = None, page_index: int = 0, seed_format_transformer: Callable[[str], str] = None): super().__init__() self.seed_num = seed_num if self.seed_num is None: @@ -1033,6 +1087,8 @@ def __init__(self, seed_num: int, bip85_data: dict = None, page_index: int = 0): self.bip85_data = bip85_data self.page_index = page_index + self.seed_format_transformer = seed_format_transformer + def run(self): NEXT = "Next" @@ -1049,6 +1105,9 @@ def run(self): title = "Seed Words" words = mnemonic[self.page_index*words_per_page:(self.page_index + 1)*words_per_page] + if self.seed_format_transformer: + words = [self.seed_format_transformer(word) for word in words] + button_data = [] num_pages = int(len(mnemonic)/words_per_page) if self.page_index < num_pages - 1 or self.seed_num is None: @@ -1062,6 +1121,7 @@ def run(self): page_index=self.page_index, num_pages=num_pages, button_data=button_data, + colorize_words=self.seed_format_transformer == seed_format_transformers.convert_word_to_11_bits ).display() if selected_menu_num == RET_CODE__BACK_BUTTON: @@ -1071,19 +1131,19 @@ def run(self): if self.seed_num is None and self.page_index == num_pages - 1: return Destination( SeedWordsBackupTestPromptView, - view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), + view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data, seed_format_transformer=self.seed_format_transformer), ) else: return Destination( SeedWordsView, - view_args=dict(seed_num=self.seed_num, page_index=self.page_index + 1, bip85_data=self.bip85_data) + view_args=dict(seed_num=self.seed_num, page_index=self.page_index + 1, bip85_data=self.bip85_data, seed_format_transformer=self.seed_format_transformer) ) elif button_data[selected_menu_num] == DONE: # Must clear history to avoid BACK button returning to private info return Destination( SeedWordsBackupTestPromptView, - view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), + view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data, seed_format_transformer=self.seed_format_transformer), ) @@ -1198,10 +1258,11 @@ def run(self): Seed Words Backup Test ****************************************************************************""" class SeedWordsBackupTestPromptView(View): - def __init__(self, seed_num: int, bip85_data: dict = None): + def __init__(self, seed_num: int, bip85_data: dict = None, seed_format_transformer: Callable[[str], str] = None): super().__init__() self.seed_num = seed_num self.bip85_data = bip85_data + self.seed_format_transformer = seed_format_transformer def run(self): @@ -1215,7 +1276,7 @@ def run(self): if button_data[selected_menu_num] == VERIFY: return Destination( SeedWordsBackupTestView, - view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), + view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data, seed_format_transformer=self.seed_format_transformer), ) elif button_data[selected_menu_num] == SKIP: @@ -1227,7 +1288,7 @@ def run(self): class SeedWordsBackupTestView(View): - def __init__(self, seed_num: int, bip85_data: dict = None, confirmed_list: List[bool] = None, cur_index: int = None): + def __init__(self, seed_num: int, bip85_data: dict = None, confirmed_list: List[bool] = None, cur_index: int = None, seed_format_transformer: Callable[[str], str] = None): super().__init__() self.seed_num = seed_num if self.seed_num is None: @@ -1247,6 +1308,8 @@ def __init__(self, seed_num: int, bip85_data: dict = None, confirmed_list: List[ self.cur_index = cur_index + self.seed_format_transformer = seed_format_transformer + def run(self): if self.cur_index is None: @@ -1259,6 +1322,12 @@ def run(self): fake_word2 = bip39.WORDLIST[int(random.random() * 2047)] fake_word3 = bip39.WORDLIST[int(random.random() * 2047)] + if self.seed_format_transformer: + real_word = self.seed_format_transformer(real_word) + fake_word1 = self.seed_format_transformer(fake_word1) + fake_word2 = self.seed_format_transformer(fake_word2) + fake_word3 = self.seed_format_transformer(fake_word3) + button_data = [real_word, fake_word1, fake_word2, fake_word3] random.shuffle(button_data) @@ -1282,7 +1351,7 @@ def run(self): # Continue testing the remaining words return Destination( SeedWordsBackupTestView, - view_args=dict(seed_num=self.seed_num, confirmed_list=self.confirmed_list, bip85_data=self.bip85_data), + view_args=dict(seed_num=self.seed_num, confirmed_list=self.confirmed_list, bip85_data=self.bip85_data, seed_format_transformer=self.seed_format_transformer), ) else: @@ -1295,19 +1364,21 @@ def run(self): cur_index=self.cur_index, wrong_word=button_data[selected_menu_num], confirmed_list=self.confirmed_list, + seed_format_transformer=self.seed_format_transformer ) ) class SeedWordsBackupTestMistakeView(View): - def __init__(self, seed_num: int, bip85_data: dict = None, cur_index: int = None, wrong_word: str = None, confirmed_list: List[bool] = None): + def __init__(self, seed_num: int, bip85_data: dict = None, cur_index: int = None, wrong_word: str = None, confirmed_list: List[bool] = None, seed_format_transformer: Callable[[str], str] = None): super().__init__() self.seed_num = seed_num self.bip85_data = bip85_data self.cur_index = cur_index self.wrong_word = wrong_word self.confirmed_list = confirmed_list + self.seed_format_transformer = seed_format_transformer def run(self): @@ -1326,7 +1397,7 @@ def run(self): if button_data[selected_menu_num] == REVIEW: return Destination( SeedWordsView, - view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), + view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data, seed_format_transformer=self.seed_format_transformer), ) elif button_data[selected_menu_num] == RETRY: @@ -1337,6 +1408,7 @@ def run(self): confirmed_list=self.confirmed_list, cur_index=self.cur_index, bip85_data=self.bip85_data, + seed_format_transformer=self.seed_format_transformer, ) )