From e5ca820416e7f267f64e7cc66850ecfdbafd0a11 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 22 Apr 2022 08:57:55 -0500 Subject: [PATCH 1/7] Options to add entropy for the final word --- src/seedsigner/gui/screens/tools_screens.py | 264 +++++++++++++++++- src/seedsigner/helpers/mnemonic_generation.py | 22 +- src/seedsigner/views/seed_views.py | 18 +- src/seedsigner/views/tools_views.py | 147 +++++++++- 4 files changed, 422 insertions(+), 29 deletions(-) diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index 2e8b2276..c8169541 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -262,7 +262,269 @@ class ToolsDiceEntropyEntryScreen(BaseTopNavScreen): @dataclass -class ToolsCalcFinalWordShowFinalWordScreen(ButtonListScreen): +class ToolsCoinFlipEntryScreen(BaseTopNavScreen): + """ + This is a lame mega copy-paste from the dice rolls Screen. It probably isn't worth + the effort to generalize these screens. But the copy-paste is far from ideal. + """ + total_flips: int = None + + def __post_init__(self): + self.title = f"Coin Flip 1/{self.total_flips}" + super().__post_init__() + + self.coin_flips = "" + + # Set up the keyboard params + keyboard_width = self.canvas_width - 2*GUIConstants.EDGE_PADDING + text_entry_display_y = self.top_nav.height + text_entry_display_height = 30 + + keyboard_start_y = text_entry_display_y + text_entry_display_height + GUIConstants.COMPONENT_PADDING + rows = 1 + button_height = int(1.5*GUIConstants.BUTTON_FONT_SIZE + 2*GUIConstants.EDGE_PADDING) + self.keyboard_digits = Keyboard( + draw=self.renderer.draw, + charset="01", + font_size=button_height - GUIConstants.COMPONENT_PADDING, + rows=rows, + cols=4, + rect=( + GUIConstants.EDGE_PADDING, + keyboard_start_y, + GUIConstants.EDGE_PADDING + keyboard_width, + keyboard_start_y + rows * button_height + (rows - 1) * 2 + ), + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT], + render_now=False + ) + self.keyboard_digits.set_selected_key(selected_letter="0") + + self.text_entry_display = TextEntryDisplay( + canvas=self.renderer.canvas, + rect=( + GUIConstants.EDGE_PADDING, + text_entry_display_y, + self.canvas_width - GUIConstants.EDGE_PADDING, + text_entry_display_y + text_entry_display_height + ), + cursor_mode=TextEntryDisplay.CURSOR_MODE__BAR, + is_centered=False, + ) + + + def _render(self): + super()._render() + + self.keyboard_digits.render_keys() + self.text_entry_display.render() + + self.renderer.show_image() + + + def _run(self): + cursor_position = len(self.coin_flips) + + # Start the interactive update loop + while True: + input = self.hw_inputs.wait_for( + HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3], + check_release=True, + release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3] + ) + + # Check possible exit condition + if self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS: + return RET_CODE__BACK_BUTTON + + # Process normal input + if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected: + # We're navigating off the previous button + self.top_nav.is_selected = False + self.top_nav.render_buttons() + + # Override the actual input w/an ENTER signal for the Keyboard + if input == HardwareButtonsConstants.KEY_DOWN: + input = Keyboard.ENTER_TOP + else: + input = Keyboard.ENTER_BOTTOM + elif input in [HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected: + # ignore + continue + + ret_val = self.keyboard_digits.update_from_input(input) + + # Now process the result from the keyboard + if ret_val in Keyboard.EXIT_DIRECTIONS: + self.top_nav.is_selected = True + self.top_nav.render_buttons() + + elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS: + if ret_val == Keyboard.KEY_BACKSPACE["code"]: + if len(self.coin_flips) > 0: + self.coin_flips = self.coin_flips[:-1] + cursor_position -= 1 + + elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS: + # User has locked in the current letter + self.coin_flips += ret_val + cursor_position += 1 + + if cursor_position == self.total_flips: + return self.coin_flips + + # Render a new TextArea over the TopNav title bar + TextArea( + text=f"Coin Flip {cursor_position + 1}/{self.total_flips}", + font_name=GUIConstants.TOP_NAV_TITLE_FONT_NAME, + font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE, + height=self.top_nav.height, + ).render() + self.top_nav.render_buttons() + + elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN: + # Live joystick movement; haven't locked this new letter in yet. + # Leave current spot blank for now. Only update the active keyboard keys + # when a selection has been locked in (KEY_PRESS) or removed ("del"). + pass + + # Render the text entry display and cursor block + self.text_entry_display.render(self.coin_flips) + + self.renderer.show_image() + + + +@dataclass +class ToolsCalcFinalWordScreen(ButtonListScreen): + selected_final_word: str = None + selected_final_bits: str = None + checksum_bits: str = None + actual_final_word: str = None + + def __post_init__(self): + self.is_bottom_list = True + super().__post_init__() + + # First what's the total bit display width and where do the checksum bits start? + bit_font_size = GUIConstants.BUTTON_FONT_SIZE + 2 + font = Fonts.get_font(GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, bit_font_size) + (left, top, bit_display_width, bit_font_height) = font.getbbox("0" * 11, anchor="lt") + (left, top, checksum_x, bottom) = font.getbbox("0" * (11 - len(self.checksum_bits)), anchor="lt") + bit_display_x = int((self.canvas_width - bit_display_width)/2) + checksum_x += bit_display_x + + # Display the user's selected final word + if self.selected_final_word: + selection_text = self.selected_final_word + keeper_selected_bits = self.selected_final_bits[:11 - len(self.checksum_bits)] + discard_selected_bits = self.selected_final_bits[-1*len(self.checksum_bits):] + else: + # User entered coin flips or all zeros + selection_text = self.selected_final_bits + keeper_selected_bits = self.selected_final_bits + discard_selected_bits = "_" * (len(self.checksum_bits)) + + self.components.append(TextArea( + text=f"""Your Selection: \"{selection_text}\"""", + screen_y=self.top_nav.height, + )) + + # ...and its associated 11 bits + screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING + self.components.append(TextArea( + text=keeper_selected_bits, + font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, + font_size=bit_font_size, + edge_padding=0, + screen_x=bit_display_x, + screen_y=screen_y, + height=bit_font_height, + is_text_centered=False, + )) + self.components.append(TextArea( + text=discard_selected_bits, + font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, + font_color=GUIConstants.LABEL_FONT_COLOR, + font_size=bit_font_size, + edge_padding=0, + screen_x=checksum_x, + screen_y=screen_y, + height=bit_font_height, + is_text_centered=False, + )) + + # Show the checksum + self.components.append(TextArea( + text="Checksum", + edge_padding=0, + screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING, + )) + + # ...and its actual bits + checksum_spacer = "_" * (11 - len(self.checksum_bits)) + + screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING + self.components.append(TextArea( + text=checksum_spacer, + font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, + font_color=GUIConstants.LABEL_FONT_COLOR, + font_size=bit_font_size, + edge_padding=0, + screen_x=bit_display_x, + screen_y=screen_y, + height=bit_font_height, + is_text_centered=False, + )) + self.components.append(TextArea( + text=self.checksum_bits, + font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, + font_size=bit_font_size, + font_color=GUIConstants.ACCENT_COLOR, + edge_padding=0, + screen_x=checksum_x, + screen_y=screen_y, + is_text_centered=False, + )) + + # And now the actual final word + self.components.append(TextArea( + text=f"""Final Word: \"{self.actual_final_word}\"""", + screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING, + )) + + # And the bits that came from the user's selected final word... + # * 7 bits for a 12-word seed + # * 3 bits for a 24-word seed + num_checksum_bits = len(self.checksum_bits) + user_component = self.selected_final_bits[:11 - num_checksum_bits] + screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING + self.components.append(TextArea( + text=user_component, + font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, + font_size=bit_font_size, + edge_padding=0, + screen_x=bit_display_x, + screen_y=screen_y, + is_text_centered=False, + )) + + # ...and now overlay the checksum's bits + self.components.append(TextArea( + text=self.checksum_bits, + font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, + font_color=GUIConstants.ACCENT_COLOR, + font_size=bit_font_size, + edge_padding=0, + screen_x=checksum_x, + screen_y=screen_y, + is_text_centered=False, + )) + + + +@dataclass +class ToolsCalcFinalWordDoneScreen(ButtonListScreen): final_word: str = None mnemonic_word_length: int = 12 fingerprint: str = None diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index e8fb44aa..6723b512 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -8,23 +8,17 @@ from typing import List from seedsigner.models.seed import Seed -def calculate_checksum(partial_mnemonic: list, wordlist_language_code: str) -> List[str]: - """ Provide 11- or 23-word mnemonic, returns complete mnemonic w/checksum as a list """ - if len(partial_mnemonic) not in [11, 23]: - raise Exception("Pass in a 11- or 23-word mnemonic") - +def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]: + """ Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list """ + if len(mnemonic) not in [12, 24]: + raise Exception("Pass in a 12- or 24-word mnemonic") + # Work on a copy of the input list - mnemonic_copy = partial_mnemonic.copy() - - # 12-word seeds contribute 7 bits of entropy to the final word; 24-word seeds - # contribute 3 bits. But we don't have any partial entropy bits to use to help us - # create the final word. So just default to filling those missing values with zeroes - # ("abandon" is word 0000, so effectively inserts zeroes). - mnemonic_copy.append("abandon") + mnemonic_copy = mnemonic.copy() # Convert the resulting mnemonic to bytes, but we `ignore_checksum` validation - # because we have to assume it's incorrect since we just hard-coded it above; we'll - # fix that next. + # because we assume it's incorrect since we either let the user select their own + # final word OR we injected the 0000 word from the wordlist. mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=Seed.get_wordlist(wordlist_language_code)) # This function will convert the bytes back into a mnemonic, but it will also diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 9fb12d43..df5b10fb 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -142,17 +142,15 @@ class SeedMnemonicEntryView(View): self.controller.storage.update_pending_mnemonic(ret, self.cur_word_index) if self.is_calc_final_word and self.cur_word_index == self.controller.storage.pending_mnemonic_length - 2: - # Time to calculate the last word - # TODO: Option to add missing entropy for the last word: - # * 3 bits for a 24-word seed - # * 7 bits for a 12-word seed - from seedsigner.helpers import mnemonic_generation + # Time to calculate the last word. User must either select a final word to + # contribute entropy to the checksum word OR we assume 0 ("abandon"). + from seedsigner.views.tools_views import ToolsCalcFinalWordSelectFinalWordPromptView + return Destination(ToolsCalcFinalWordSelectFinalWordPromptView) + + if self.is_calc_final_word and self.cur_word_index == self.controller.storage.pending_mnemonic_length - 1: + # Time to calculate the last word. User must either select a final word to + # contribute entropy to the checksum word OR we assume 0 ("abandon"). from seedsigner.views.tools_views import ToolsCalcFinalWordShowFinalWordView - full_mnemonic = mnemonic_generation.calculate_checksum( - self.controller.storage.pending_mnemonic[:-1], # Must omit the last word's empty value - wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) - ) - self.controller.storage.update_pending_mnemonic(full_mnemonic[-1], self.cur_word_index+1) return Destination(ToolsCalcFinalWordShowFinalWordView) if self.cur_word_index < self.controller.storage.pending_mnemonic_length - 1: diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 8c6b2216..61edd8ca 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -8,7 +8,7 @@ from PIL.ImageOps import autocontrast from seedsigner.hardware.camera import Camera from seedsigner.gui.components import FontAwesomeIconConstants from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen) -from seedsigner.gui.screens.tools_screens import ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordShowFinalWordScreen +from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordDoneScreen from seedsigner.helpers import mnemonic_generation from seedsigner.models.seed import Seed from seedsigner.models.settings_definition import SettingsConstants @@ -240,15 +240,154 @@ class ToolsCalcFinalWordNumWordsView(View): elif button_data[selected_menu_num] == TWELVE: self.controller.storage.init_pending_mnemonic(12) - return Destination(SeedMnemonicEntryView, view_args={"is_calc_final_word": True}) + + # DEBUGGING + for i in range(0, 10): + self.controller.storage.update_pending_mnemonic("abandon", i) + + # return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True)) + return Destination(SeedMnemonicEntryView, view_args=dict(cur_word_index=10, is_calc_final_word=True)) elif button_data[selected_menu_num] == TWENTY_FOUR: self.controller.storage.init_pending_mnemonic(24) - return Destination(SeedMnemonicEntryView, view_args={"is_calc_final_word": True}) + + # DEBUGGING + for i in range(0, 22): + self.controller.storage.update_pending_mnemonic("abandon", i) + + # return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True)) + return Destination(SeedMnemonicEntryView, view_args=dict(cur_word_index=22, is_calc_final_word=True)) + + + +class ToolsCalcFinalWordSelectFinalWordPromptView(View): + def run(self): + mnemonic = self.controller.storage.pending_mnemonic + mnemonic_length = len(mnemonic) + COIN_FLIPS = "Finalize w/coin flips" + SELECT_WORD = f"Finalize w/{mnemonic_length}th word" + ZEROS = "Finalize w/zeros" + + button_data = [COIN_FLIPS, SELECT_WORD, ZEROS] + selected_menu_num = ButtonListScreen( + title=f"{mnemonic_length}th Word", + button_data=button_data, + is_bottom_list=True, + is_button_text_centered=True, + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + elif button_data[selected_menu_num] == COIN_FLIPS: + return Destination(ToolsCalcFinalWordCoinFlipsView) + + elif button_data[selected_menu_num] == SELECT_WORD: + return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True, cur_word_index=mnemonic_length - 1)) + + elif button_data[selected_menu_num] == ZEROS: + # User skipped the option to select a final word to provide last bits of + # entropy. We'll insert all zeros and piggy-back on the coin flip attr + if mnemonic_length == 12: + total_flips = 7 + else: + total_flips = 3 + wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) + self.controller.storage.update_pending_mnemonic(Seed.get_wordlist(wordlist_language_code)[0], mnemonic_length - 1) + return Destination(ToolsCalcFinalWordShowFinalWordView, view_args=dict(coin_flips="0" * total_flips)) + + + +class ToolsCalcFinalWordCoinFlipsView(View): + def run(self): + mnemonic = self.controller.storage.pending_mnemonic + mnemonic_length = len(mnemonic) + + if mnemonic_length == 12: + total_flips = 7 + else: + total_flips = 3 + + ret_val = ToolsCoinFlipEntryScreen( + total_flips=total_flips, + ).display() + + if ret_val == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + else: + print(ret_val) + binary_string = ret_val + "0" * (11 - total_flips) + wordlist_index = int(binary_string, 2) + wordlist = Seed.get_wordlist(self.controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) + word = wordlist[wordlist_index] + self.controller.storage.update_pending_mnemonic(word, mnemonic_length - 1) + + return Destination(ToolsCalcFinalWordShowFinalWordView, view_args=dict(coin_flips=ret_val)) class ToolsCalcFinalWordShowFinalWordView(View): + def __init__(self, coin_flips=None): + super().__init__() + self.coin_flips = coin_flips + + + def run(self): + # Construct the actual final word. The user's selected_final_word + # contributes: + # * 3 bits to a 24-word seed (plus 8-bit checksum) + # * 7 bits to a 12-word seed (plus 4-bit checksum) + from seedsigner.helpers import mnemonic_generation + + mnemonic = self.controller.storage.pending_mnemonic + mnemonic_length = len(mnemonic) + wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) + wordlist = Seed.get_wordlist(wordlist_language_code) + + final_mnemonic = mnemonic_generation.calculate_checksum( + mnemonic=self.controller.storage.pending_mnemonic, + wordlist_language_code=wordlist_language_code, + ) + self.controller.storage.update_pending_mnemonic(final_mnemonic[-1], mnemonic_length - 1) + + # Prep the user's selected word (if there was one) and the actual final word for + # the display. + if self.coin_flips: + selected_final_word = None + selected_final_bits = self.coin_flips + else: + # Convert the user's final word selection into its binary index equivalent + selected_final_word = mnemonic[-1] + selected_final_bits = format(wordlist.index(selected_final_word), '011b') + + # And grab the actual final word's checksum bits + actual_final_word = self.controller.storage.pending_mnemonic[-1] + if mnemonic_length == 12: + checksum_bits = format(wordlist.index(actual_final_word), '011b')[-4:] + else: + checksum_bits = format(wordlist.index(actual_final_word), '011b')[-8:] + + NEXT = "Next" + button_data = [NEXT] + selected_menu_num = ToolsCalcFinalWordScreen( + title="Final Word Calc", + button_data=button_data, + selected_final_word=selected_final_word, + selected_final_bits=selected_final_bits, + checksum_bits=checksum_bits, + actual_final_word=actual_final_word, + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + elif button_data[selected_menu_num] == NEXT: + return Destination(ToolsCalcFinalWordDoneView) + + + +class ToolsCalcFinalWordDoneView(View): def run(self): mnemonic = self.controller.storage.pending_mnemonic mnemonic_word_length = len(mnemonic) @@ -258,7 +397,7 @@ class ToolsCalcFinalWordShowFinalWordView(View): DISCARD = ("Discard", None, None, "red") button_data = [LOAD, DISCARD] - selected_menu_num = ToolsCalcFinalWordShowFinalWordScreen( + selected_menu_num = ToolsCalcFinalWordDoneScreen( final_word=final_word, mnemonic_word_length=mnemonic_word_length, fingerprint=self.controller.storage.get_pending_mnemonic_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)), From b4f70b536ac32ee7de115c56d9a0cfe8ca181f01 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 22 Apr 2022 10:12:15 -0500 Subject: [PATCH 2/7] Various improvements --- src/seedsigner/gui/components.py | 4 ++- src/seedsigner/gui/screens/tools_screens.py | 34 +++++++++++++++++++-- src/seedsigner/views/seed_views.py | 23 +++++++------- src/seedsigner/views/tools_views.py | 30 +++++++++--------- 4 files changed, 62 insertions(+), 29 deletions(-) diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index ebd451ea..9e6ec211 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -289,7 +289,9 @@ class TextArea(BaseComponent): # fits in its bounding rect (plus accounting for edge padding) using its given # font. # Measure from left baseline ("ls") - # getbbox() seems to ignore "\n" so won't affect height calcs + # TODO: getbbox() seems to ignore "\n" so isn't properly factored into height + # calcs and yields incorrect full_text_width. For now must specify self.height to + # render properly. Centering will be wrong. (left, top, full_text_width, bottom) = self.font.getbbox(self.text, anchor="ls") self.text_font_height = -1 * top self.bbox_height = self.text_font_height + bottom diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index c8169541..790b4c54 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -261,6 +261,24 @@ class ToolsDiceEntropyEntryScreen(BaseTopNavScreen): +@dataclass +class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen): + mnemonic_length: int = None + num_entropy_bits: int = None + + def __post_init__(self): + self.title = "Build Final Word" + self.is_bottom_list = True + self.is_button_text_centered = True + super().__post_init__() + + self.components.append(TextArea( + text=f"The {self.mnemonic_length}th word is built from {self.num_entropy_bits} more entropy bits plus auto-calculated checksum.", + screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, + )) + + + @dataclass class ToolsCoinFlipEntryScreen(BaseTopNavScreen): """ @@ -285,7 +303,7 @@ class ToolsCoinFlipEntryScreen(BaseTopNavScreen): button_height = int(1.5*GUIConstants.BUTTON_FONT_SIZE + 2*GUIConstants.EDGE_PADDING) self.keyboard_digits = Keyboard( draw=self.renderer.draw, - charset="01", + charset="10", font_size=button_height - GUIConstants.COMPONENT_PADDING, rows=rows, cols=4, @@ -298,7 +316,7 @@ class ToolsCoinFlipEntryScreen(BaseTopNavScreen): auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT], render_now=False ) - self.keyboard_digits.set_selected_key(selected_letter="0") + self.keyboard_digits.set_selected_key(selected_letter="1") self.text_entry_display = TextEntryDisplay( canvas=self.renderer.canvas, @@ -312,6 +330,15 @@ class ToolsCoinFlipEntryScreen(BaseTopNavScreen): is_centered=False, ) + self.components.append(TextArea( + text="Heads = 1", + screen_y = keyboard_start_y + self.keyboard_digits.height + 4*GUIConstants.COMPONENT_PADDING, + )) + self.components.append(TextArea( + text="Tails = 0", + screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING, + )) + def _render(self): super()._render() @@ -371,6 +398,7 @@ class ToolsCoinFlipEntryScreen(BaseTopNavScreen): cursor_position += 1 if cursor_position == self.total_flips: + # Return results as a binary string return self.coin_flips # Render a new TextArea over the TopNav title bar @@ -426,7 +454,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): discard_selected_bits = "_" * (len(self.checksum_bits)) self.components.append(TextArea( - text=f"""Your Selection: \"{selection_text}\"""", + text=f"""Your input: \"{selection_text}\"""", screen_y=self.top_nav.height, )) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index df5b10fb..9ac4c3d9 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -127,13 +127,14 @@ class SeedMnemonicEntryView(View): if ret == RET_CODE__BACK_BUTTON: if self.cur_word_index > 0: - return Destination( - SeedMnemonicEntryView, - view_args={ - "cur_word_index": self.cur_word_index - 1, - "is_calc_final_word": self.is_calc_final_word - } - ) + return Destination(BackStackView) + # return Destination( + # SeedMnemonicEntryView, + # view_args={ + # "cur_word_index": self.cur_word_index - 1, + # "is_calc_final_word": self.is_calc_final_word + # } + # ) else: self.controller.storage.discard_pending_mnemonic() return Destination(MainMenuView) @@ -142,10 +143,10 @@ class SeedMnemonicEntryView(View): self.controller.storage.update_pending_mnemonic(ret, self.cur_word_index) if self.is_calc_final_word and self.cur_word_index == self.controller.storage.pending_mnemonic_length - 2: - # Time to calculate the last word. User must either select a final word to - # contribute entropy to the checksum word OR we assume 0 ("abandon"). - from seedsigner.views.tools_views import ToolsCalcFinalWordSelectFinalWordPromptView - return Destination(ToolsCalcFinalWordSelectFinalWordPromptView) + # Time to calculate the last word. User must decide how they want to specify + # the last bits of entropy for the final word. + from seedsigner.views.tools_views import ToolsCalcFinalWordFinalizePromptView + return Destination(ToolsCalcFinalWordFinalizePromptView) if self.is_calc_final_word and self.cur_word_index == self.controller.storage.pending_mnemonic_length - 1: # Time to calculate the last word. User must either select a final word to diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 61edd8ca..dcc54f96 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -8,7 +8,7 @@ from PIL.ImageOps import autocontrast from seedsigner.hardware.camera import Camera from seedsigner.gui.components import FontAwesomeIconConstants from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen) -from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordDoneScreen +from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordDoneScreen from seedsigner.helpers import mnemonic_generation from seedsigner.models.seed import Seed from seedsigner.models.settings_definition import SettingsConstants @@ -260,20 +260,24 @@ class ToolsCalcFinalWordNumWordsView(View): -class ToolsCalcFinalWordSelectFinalWordPromptView(View): +class ToolsCalcFinalWordFinalizePromptView(View): def run(self): mnemonic = self.controller.storage.pending_mnemonic mnemonic_length = len(mnemonic) - COIN_FLIPS = "Finalize w/coin flips" - SELECT_WORD = f"Finalize w/{mnemonic_length}th word" - ZEROS = "Finalize w/zeros" + if mnemonic_length == 12: + num_entropy_bits = 7 + else: + num_entropy_bits = 3 + + COIN_FLIPS = "Coin flip entropy" + SELECT_WORD = f"Word selection entropy" + ZEROS = "Finalize with zeros" button_data = [COIN_FLIPS, SELECT_WORD, ZEROS] - selected_menu_num = ButtonListScreen( - title=f"{mnemonic_length}th Word", + selected_menu_num = ToolsCalcFinalWordFinalizePromptScreen( + mnemonic_length=mnemonic_length, + num_entropy_bits=num_entropy_bits, button_data=button_data, - is_bottom_list=True, - is_button_text_centered=True, ).display() if selected_menu_num == RET_CODE__BACK_BUTTON: @@ -283,18 +287,16 @@ class ToolsCalcFinalWordSelectFinalWordPromptView(View): return Destination(ToolsCalcFinalWordCoinFlipsView) elif button_data[selected_menu_num] == SELECT_WORD: + # Clear the final word slot, just in case we're returning via BACK button + self.controller.storage.update_pending_mnemonic(None, mnemonic_length - 1) return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True, cur_word_index=mnemonic_length - 1)) elif button_data[selected_menu_num] == ZEROS: # User skipped the option to select a final word to provide last bits of # entropy. We'll insert all zeros and piggy-back on the coin flip attr - if mnemonic_length == 12: - total_flips = 7 - else: - total_flips = 3 wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) self.controller.storage.update_pending_mnemonic(Seed.get_wordlist(wordlist_language_code)[0], mnemonic_length - 1) - return Destination(ToolsCalcFinalWordShowFinalWordView, view_args=dict(coin_flips="0" * total_flips)) + return Destination(ToolsCalcFinalWordShowFinalWordView, view_args=dict(coin_flips="0" * num_entropy_bits)) From f9e92f73d6898b34cae36eb9910ff58a64b5f4d1 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 24 Apr 2022 08:38:27 -0500 Subject: [PATCH 3/7] Removing debugging --- src/seedsigner/views/tools_views.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index dcc54f96..4af46ada 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -241,20 +241,12 @@ class ToolsCalcFinalWordNumWordsView(View): elif button_data[selected_menu_num] == TWELVE: self.controller.storage.init_pending_mnemonic(12) - # DEBUGGING - for i in range(0, 10): - self.controller.storage.update_pending_mnemonic("abandon", i) - # return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True)) return Destination(SeedMnemonicEntryView, view_args=dict(cur_word_index=10, is_calc_final_word=True)) elif button_data[selected_menu_num] == TWENTY_FOUR: self.controller.storage.init_pending_mnemonic(24) - # DEBUGGING - for i in range(0, 22): - self.controller.storage.update_pending_mnemonic("abandon", i) - # return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True)) return Destination(SeedMnemonicEntryView, view_args=dict(cur_word_index=22, is_calc_final_word=True)) From f6e20a4be1b87fd25adb70f003d6c15f0f5a8066 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 24 Apr 2022 09:06:31 -0500 Subject: [PATCH 4/7] Improved code comments --- src/seedsigner/gui/screens/tools_screens.py | 29 ++++++++++++++------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index 790b4c54..0370a6f0 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -442,15 +442,21 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): bit_display_x = int((self.canvas_width - bit_display_width)/2) checksum_x += bit_display_x - # Display the user's selected final word + # Display the user's additional entropy input if self.selected_final_word: selection_text = self.selected_final_word keeper_selected_bits = self.selected_final_bits[:11 - len(self.checksum_bits)] + + # The word's least significant bits will be rendered differently to convey + # the fact that they're being discarded. discard_selected_bits = self.selected_final_bits[-1*len(self.checksum_bits):] else: # User entered coin flips or all zeros selection_text = self.selected_final_bits keeper_selected_bits = self.selected_final_bits + + # We'll append spacer chars to preserve the vertical alignment (most + # significant n bits always rendered in same column) discard_selected_bits = "_" * (len(self.checksum_bits)) self.components.append(TextArea( @@ -458,7 +464,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): screen_y=self.top_nav.height, )) - # ...and its associated 11 bits + # ...and that entropy's associated 11 bits screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING self.components.append(TextArea( text=keeper_selected_bits, @@ -470,6 +476,9 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): height=bit_font_height, is_text_centered=False, )) + + # Render the least significant bits that will be replaced by the checksum in a + # de-emphasized font color. self.components.append(TextArea( text=discard_selected_bits, font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, @@ -482,17 +491,19 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): is_text_centered=False, )) - # Show the checksum + # Show the checksum.. self.components.append(TextArea( text="Checksum", edge_padding=0, screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING, )) - # ...and its actual bits + # ...and its actual bits. Prepend spacers to keep vertical alignment checksum_spacer = "_" * (11 - len(self.checksum_bits)) screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING + + # This time we de-emphasize the prepended spacers that are irrelevant self.components.append(TextArea( text=checksum_spacer, font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, @@ -504,6 +515,8 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): height=bit_font_height, is_text_centered=False, )) + + # And especially highlight (orange!) the actual checksum bits self.components.append(TextArea( text=self.checksum_bits, font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, @@ -515,15 +528,13 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): is_text_centered=False, )) - # And now the actual final word + # And now the *actual* final word after merging the bit data self.components.append(TextArea( text=f"""Final Word: \"{self.actual_final_word}\"""", screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING, )) - # And the bits that came from the user's selected final word... - # * 7 bits for a 12-word seed - # * 3 bits for a 24-word seed + # Once again show the bits that came from the user's entropy... num_checksum_bits = len(self.checksum_bits) user_component = self.selected_final_bits[:11 - num_checksum_bits] screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING @@ -537,7 +548,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): is_text_centered=False, )) - # ...and now overlay the checksum's bits + # ...and append the checksum's bits, still highlighted in orange self.components.append(TextArea( text=self.checksum_bits, font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, From 2f260aebf478f80e5a53e5a3af0bfc96bf72c2cf Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 24 Apr 2022 18:59:11 -0500 Subject: [PATCH 5/7] interim commit, starting to refactor to `KeyboardScreen` --- src/seedsigner/gui/screens/tools_screens.py | 142 ++------------------ src/seedsigner/views/tools_views.py | 2 +- 2 files changed, 15 insertions(+), 129 deletions(-) diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index 07c470db..fce63e19 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -169,59 +169,22 @@ class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen): @dataclass -class ToolsCoinFlipEntryScreen(BaseTopNavScreen): - """ - This is a lame mega copy-paste from the dice rolls Screen. It probably isn't worth - the effort to generalize these screens. But the copy-paste is far from ideal. - """ - total_flips: int = None - +class ToolsCoinFlipEntryScreen(KeyboardScreen): def __post_init__(self): + # Override values set by the parent class self.title = f"Coin Flip 1/{self.total_flips}" + + # Specify the keys in the keyboard + self.rows = 1 + self.cols = 4 + self.keys_charset = "10" + + # Now initialize the parent class super().__post_init__() - - self.coin_flips = "" - - # Set up the keyboard params - keyboard_width = self.canvas_width - 2*GUIConstants.EDGE_PADDING - text_entry_display_y = self.top_nav.height - text_entry_display_height = 30 - - keyboard_start_y = text_entry_display_y + text_entry_display_height + GUIConstants.COMPONENT_PADDING - rows = 1 - button_height = int(1.5*GUIConstants.BUTTON_FONT_SIZE + 2*GUIConstants.EDGE_PADDING) - self.keyboard_digits = Keyboard( - draw=self.renderer.draw, - charset="10", - font_size=button_height - GUIConstants.COMPONENT_PADDING, - rows=rows, - cols=4, - rect=( - GUIConstants.EDGE_PADDING, - keyboard_start_y, - GUIConstants.EDGE_PADDING + keyboard_width, - keyboard_start_y + rows * button_height + (rows - 1) * 2 - ), - auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT], - render_now=False - ) - self.keyboard_digits.set_selected_key(selected_letter="1") - - self.text_entry_display = TextEntryDisplay( - canvas=self.renderer.canvas, - rect=( - GUIConstants.EDGE_PADDING, - text_entry_display_y, - self.canvas_width - GUIConstants.EDGE_PADDING, - text_entry_display_y + text_entry_display_height - ), - cursor_mode=TextEntryDisplay.CURSOR_MODE__BAR, - is_centered=False, - ) - + self.components.append(TextArea( text="Heads = 1", - screen_y = keyboard_start_y + self.keyboard_digits.height + 4*GUIConstants.COMPONENT_PADDING, + screen_y = self.keyboard_digits.rect[3] + 4*GUIConstants.COMPONENT_PADDING, )) self.components.append(TextArea( text="Tails = 0", @@ -229,86 +192,9 @@ class ToolsCoinFlipEntryScreen(BaseTopNavScreen): )) - def _render(self): - super()._render() - - self.keyboard_digits.render_keys() - self.text_entry_display.render() - - self.renderer.show_image() - - - def _run(self): - cursor_position = len(self.coin_flips) - - # Start the interactive update loop - while True: - input = self.hw_inputs.wait_for( - HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3], - check_release=True, - release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3] - ) - - # Check possible exit condition - if self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS: - return RET_CODE__BACK_BUTTON - - # Process normal input - if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected: - # We're navigating off the previous button - self.top_nav.is_selected = False - self.top_nav.render_buttons() - - # Override the actual input w/an ENTER signal for the Keyboard - if input == HardwareButtonsConstants.KEY_DOWN: - input = Keyboard.ENTER_TOP - else: - input = Keyboard.ENTER_BOTTOM - elif input in [HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected: - # ignore - continue - - ret_val = self.keyboard_digits.update_from_input(input) - - # Now process the result from the keyboard - if ret_val in Keyboard.EXIT_DIRECTIONS: - self.top_nav.is_selected = True - self.top_nav.render_buttons() - - elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS: - if ret_val == Keyboard.KEY_BACKSPACE["code"]: - if len(self.coin_flips) > 0: - self.coin_flips = self.coin_flips[:-1] - cursor_position -= 1 - - elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS: - # User has locked in the current letter - self.coin_flips += ret_val - cursor_position += 1 - - if cursor_position == self.total_flips: - # Return results as a binary string - return self.coin_flips - - # Render a new TextArea over the TopNav title bar - TextArea( - text=f"Coin Flip {cursor_position + 1}/{self.total_flips}", - font_name=GUIConstants.TOP_NAV_TITLE_FONT_NAME, - font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE, - height=self.top_nav.height, - ).render() - self.top_nav.render_buttons() - - elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN: - # Live joystick movement; haven't locked this new letter in yet. - # Leave current spot blank for now. Only update the active keyboard keys - # when a selection has been locked in (KEY_PRESS) or removed ("del"). - pass - - # Render the text entry display and cursor block - self.text_entry_display.render(self.coin_flips) - - self.renderer.show_image() + def update_title(self) -> bool: + self.title = f"Coin Flip {self.cursor_position + 1}/{self.return_after_n_chars}" + return True diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index d5eb9b68..272eefa6 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -303,7 +303,7 @@ class ToolsCalcFinalWordCoinFlipsView(View): total_flips = 3 ret_val = ToolsCoinFlipEntryScreen( - total_flips=total_flips, + return_after_n_chars=total_flips, ).display() if ret_val == RET_CODE__BACK_BUTTON: From d62c8cfb0e87e11b904381cfdbcfe28967738475 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 24 Apr 2022 19:11:09 -0500 Subject: [PATCH 6/7] Finalizing refactor to `KeyboardScreen`. --- src/seedsigner/gui/screens/tools_screens.py | 5 +++-- src/seedsigner/views/tools_views.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index fce63e19..229146d5 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -172,11 +172,12 @@ class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen): class ToolsCoinFlipEntryScreen(KeyboardScreen): def __post_init__(self): # Override values set by the parent class - self.title = f"Coin Flip 1/{self.total_flips}" + self.title = f"Coin Flip 1/{self.return_after_n_chars}" # Specify the keys in the keyboard self.rows = 1 self.cols = 4 + self.key_height = GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2 + 2*GUIConstants.EDGE_PADDING self.keys_charset = "10" # Now initialize the parent class @@ -184,7 +185,7 @@ class ToolsCoinFlipEntryScreen(KeyboardScreen): self.components.append(TextArea( text="Heads = 1", - screen_y = self.keyboard_digits.rect[3] + 4*GUIConstants.COMPONENT_PADDING, + screen_y = self.keyboard.rect[3] + 4*GUIConstants.COMPONENT_PADDING, )) self.components.append(TextArea( text="Tails = 0", diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 272eefa6..bfee4738 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -242,13 +242,13 @@ class ToolsCalcFinalWordNumWordsView(View): self.controller.storage.init_pending_mnemonic(12) # return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True)) - return Destination(SeedMnemonicEntryView, view_args=dict(cur_word_index=10, is_calc_final_word=True)) + return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True)) elif button_data[selected_menu_num] == TWENTY_FOUR: self.controller.storage.init_pending_mnemonic(24) # return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True)) - return Destination(SeedMnemonicEntryView, view_args=dict(cur_word_index=22, is_calc_final_word=True)) + return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True)) From a42408a3e49e18e0864302a434cef4cf31b3e938 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 15 Jun 2022 14:51:47 -0500 Subject: [PATCH 7/7] mnemonic_generation test case bug fix; additional test case. --- src/seedsigner/helpers/mnemonic_generation.py | 10 +++++++++- tests/test_mnemonic_generation.py | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 6723b512..dab63a48 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -9,7 +9,15 @@ from seedsigner.models.seed import Seed def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]: - """ Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list """ + """ + Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list. + + If 11- or 23-words are provided, append word `0000` to end of list as temp final + word. + """ + if len(mnemonic) in [11, 23]: + mnemonic.append(Seed.get_wordlist(wordlist_language_code)[0]) + if len(mnemonic) not in [12, 24]: raise Exception("Pass in a 12- or 24-word mnemonic") diff --git a/tests/test_mnemonic_generation.py b/tests/test_mnemonic_generation.py index a5b3fd0c..a0c04ed8 100644 --- a/tests/test_mnemonic_generation.py +++ b/tests/test_mnemonic_generation.py @@ -43,6 +43,26 @@ def test_calculate_checksum(): +def test_calculate_checksum_with_default_final_word(): + """ 11-word and 23-word mnemonics use word `0000` as a temp final word to complete + the mnemonic. + """ + partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day" + mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + + partial_mnemonic += " abandon" + mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + assert mnemonic1 == mnemonic2 + + partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile" + mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + + partial_mnemonic += " abandon" + mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + assert mnemonic1 == mnemonic2 + + + def test_verify_against_coldcard_sample(): """ https://coldcard.com/docs/verifying-dice-roll-math """ dice_rolls = "123456"