diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 9193e451..2ec78c61 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -89,6 +89,7 @@ class FontAwesomeIconConstants: DICE_FOUR = "\uf524" DICE_FIVE = "\uf523" DICE_SIX = "\uf526" + COINS = "\uf51e" KEYBOARD = "\uf11c" LOCK = "\uf023" MAP = "\uf279" diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index 13fa33be..c4868f61 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -164,6 +164,41 @@ def update_title(self) -> bool: +@dataclass +class ToolsCoinEntropyEntryScreen(KeyboardScreen): + def __post_init__(self): + # Override values set by the parent class + self.title = f"Coin Flip 1/{self.return_after_n_chars}" + + # Specify the keys in the keyboard + self.rows = 2 + self.cols = 2 + self.key_height = GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2 + 2*GUIConstants.EDGE_PADDING + self.keys_charset = "".join([ + "H", + "T", + ]) + + # Map Key display chars to actual output values + self.keys_to_values = { + "H": "1", + "T": "0", + } + + # Now initialize the parent class + super().__post_init__() + + self.components.append(TextArea( + text="(H)eads: 1 / (T)ails: 0", + screen_y = self.keyboard.rect[3] + 3*GUIConstants.COMPONENT_PADDING, + )) + + def update_title(self) -> bool: + self.title = f"Coin Flip {self.cursor_position + 1}/{self.return_after_n_chars}" + return True + + + @dataclass class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen): mnemonic_length: int = None diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index b3e90680..b5f99d45 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -16,6 +16,8 @@ DICE__NUM_ROLLS__12WORD = 50 DICE__NUM_ROLLS__24WORD = 99 +COIN__NUM_FLIPS__12WORD = 128 +COIN__NUM_FLIPS__24WORD = 256 diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index eabb1171..115f2913 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -12,7 +12,7 @@ from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerIconConstants from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen) from seedsigner.gui.screens.tools_screens import (ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen, - ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, + ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsCoinEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen) from seedsigner.helpers import embit_utils, mnemonic_generation from seedsigner.models.encode_qr import GenericStaticQrEncoder @@ -28,12 +28,13 @@ class ToolsMenuView(View): IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA) DICE = ("New seed", FontAwesomeIconConstants.DICE) + COIN = (" New seed", FontAwesomeIconConstants.COINS) KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD) ADDRESS_EXPLORER = "Address Explorer" VERIFY_ADDRESS = "Verify address" def run(self): - button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.ADDRESS_EXPLORER, self.VERIFY_ADDRESS] + button_data = [self.IMAGE, self.DICE, self.COIN, self.KEYBOARD, self.ADDRESS_EXPLORER, self.VERIFY_ADDRESS] selected_menu_num = self.run_screen( ButtonListScreen, @@ -51,6 +52,9 @@ def run(self): elif button_data[selected_menu_num] == self.DICE: return Destination(ToolsDiceEntropyMnemonicLengthView) + elif button_data[selected_menu_num] == self.COIN: + return Destination(ToolsCoinEntropyMnemonicLengthView) + elif button_data[selected_menu_num] == self.KEYBOARD: return Destination(ToolsCalcFinalWordNumWordsView) @@ -236,6 +240,58 @@ def run(self): +"""**************************************************************************** + Coin flips Views +****************************************************************************""" +class ToolsCoinEntropyMnemonicLengthView(View): + def run(self): + TWELVE = f"12 words ({mnemonic_generation.COIN__NUM_FLIPS__12WORD} flips)" + TWENTY_FOUR = f"24 words ({mnemonic_generation.COIN__NUM_FLIPS__24WORD} flips)" + + button_data = [TWELVE, TWENTY_FOUR] + selected_menu_num = ButtonListScreen( + title="Mnemonic Length", + is_bottom_list=True, + is_button_text_centered=True, + button_data=button_data, + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + elif button_data[selected_menu_num] == TWELVE: + return Destination(ToolsCoinEntropyEntryView, view_args=dict(total_flips=mnemonic_generation.COIN__NUM_FLIPS__12WORD)) + + elif button_data[selected_menu_num] == TWENTY_FOUR: + return Destination(ToolsCoinEntropyEntryView, view_args=dict(total_flips=mnemonic_generation.COIN__NUM_FLIPS__24WORD)) + + + +class ToolsCoinEntropyEntryView(View): + def __init__(self, total_flips: int): + super().__init__() + self.total_flips = total_flips + + + def run(self): + ret = ToolsCoinEntropyEntryScreen( + return_after_n_chars=self.total_flips, + ).display() + + if ret == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + coin_seed_phrase = mnemonic_generation.generate_mnemonic_from_coin_flips(ret) + + # Add the mnemonic as an in-memory Seed + seed = Seed(coin_seed_phrase, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) + self.controller.storage.set_pending_seed(seed) + + # Cannot return BACK to this View + return Destination(SeedWordsWarningView, view_args={"seed_num": None}, clear_history=True) + + + """**************************************************************************** Calc final word Views ****************************************************************************""" diff --git a/tests/test_mnemonic_generation.py b/tests/test_mnemonic_generation.py index a1f46e85..563ca86f 100644 --- a/tests/test_mnemonic_generation.py +++ b/tests/test_mnemonic_generation.py @@ -30,6 +30,29 @@ def test_dice_rolls(): +def test_coin_flips(): + """ Given random coin flips, the resulting mnemonic should be valid. """ + coin_flips = "" + for i in range(0, mnemonic_generation.COIN__NUM_FLIPS__24WORD): + # Do not need truly rigorous random for this test + coin_flips += str(random.randint(0, 1)) + + mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips) + + assert len(mnemonic) == 24 + assert bip39.mnemonic_is_valid(" ".join(mnemonic)) + + coin_flips = "" + for i in range(0, mnemonic_generation.COIN__NUM_FLIPS__12WORD): + # Do not need truly rigorous random for this test + coin_flips += str(random.randint(0, 1)) + + mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips) + assert len(mnemonic) == 12 + assert bip39.mnemonic_is_valid(" ".join(mnemonic)) + + + def test_calculate_checksum_input_type(): """ Given an 11-word or 23-word mnemonic, the calculated checksum should yield a @@ -195,3 +218,59 @@ def test_50_dice_rolls(): actual = " ".join(mnemonic) assert bip39.mnemonic_is_valid(actual) assert actual == expected + + + +def test_256_coin_flips(): + """ 256 coin flips input should yield the same 24-word mnemonic as iancoleman.io/bip39 """ + # Check "Show entropy details", paste in coin flip sequence, click "Binary", select "Mnemonic Length" as "24 Words" + coin_flips = "1010101010101110110001000000001100100011000000000011000000001001110000000000000010000000000110000001110010110010000100110011001010101010101011101100010000000011001000110000000000110000000010011100000000000000100000000001100000011100101100100001001100110010" + expected = "grocery secret mountain turkey moment elbow size castle eagle water nominee general usual awesome attack daring cruise marriage debate tortoise document capital avocado engine" + + mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips) + actual = " ".join(mnemonic) + assert bip39.mnemonic_is_valid(actual) + assert actual == expected + + coin_flips = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + expected = "gun library main saddle doctor meat pizza bone brave output matter chef merry flag abuse puppy first rotate era tent news arrest pepper finger" + + mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips) + actual = " ".join(mnemonic) + assert bip39.mnemonic_is_valid(actual) + assert actual == expected + + coin_flips = "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + expected = "rural oval civil ignore moon glide any pony perfect gain stable flag fortune require roast stereo mad guitar page flat reduce give borrow leisure" + + mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips) + actual = " ".join(mnemonic) + assert bip39.mnemonic_is_valid(actual) + assert actual == expected + + + +def test_128_coin_flips(): + """ 128 coin flips input should yield the same 12-word mnemonic as iancoleman.io/bip39 """ + # Check "Show entropy details", paste in coin flip sequence, click "Binary", select "Mnemonic Length" as "12 Words" + coin_flips = "10101010101011101100010000000011001000110000000000110000000010011100000000000000100000000001100000011100101100100001001100110010" + expected = "prevent style echo subway next museum palace lobster toward office shoe unfair" + mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips) + actual = " ".join(mnemonic) + assert bip39.mnemonic_is_valid(actual) + assert actual == expected + + coin_flips = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + expected = "earth naive tongue material rebel cotton credit quarter market peanut memory other" + mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips) + actual = " ".join(mnemonic) + assert bip39.mnemonic_is_valid(actual) + assert actual == expected + + coin_flips = "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + expected = "exit pulp believe feature horror vehicle home more patrol hair drink resist" + mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips) + actual = " ".join(mnemonic) + assert bip39.mnemonic_is_valid(actual) + assert actual == expected +