From 27549a42a7b505e41d2d93a6dab3840da69369d2 Mon Sep 17 00:00:00 2001 From: Easy <86452817+easyuxd@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:12:06 -0700 Subject: [PATCH 01/39] Updated colors and icons New color: Dire Warning New icons: info, sign --- src/seedsigner/gui/components.py | 41 ++++++++++-------- .../resources/fonts/seedsigner-icons.otf | Bin 29216 -> 29956 bytes 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 9193e451b..b707b17e0 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -23,17 +23,18 @@ class GUIConstants: COMPONENT_PADDING = 8 LIST_ITEM_PADDING = 4 - BACKGROUND_COLOR = "black" + BACKGROUND_COLOR = "#000000" + INACTIVE_COLOR = "#414141" + ACCENT_COLOR = "#FF9F0A" # Active Color WARNING_COLOR = "#FFD60A" - DIRE_WARNING_COLOR = "#FF453A" + DIRE_WARNING_COLOR = "#FF5700" + ERROR_COLOR = "#FF1B0A" SUCCESS_COLOR = "#30D158" - INFO_COLOR = "#0084FF" - ACCENT_COLOR = "#FF9F0A" + INFO_COLOR = "#409CFF" BITCOIN_ORANGE = "#FF9416" TESTNET_COLOR = "#00F100" REGTEST_COLOR = "#00CAF1" GREEN_INDICATOR_COLOR = "#00FF00" - INACTIVE_COLOR = "#414141" ICON_FONT_NAME__FONT_AWESOME = "Font_Awesome_6_Free-Solid-900" ICON_FONT_NAME__SEEDSIGNER = "seedsigner-icons" @@ -126,24 +127,26 @@ class SeedSignerIconConstants: RESTART = "\ue911" # Messaging icons - ERROR = "\ue912" - SUCCESS = "\ue913" - WARNING = "\ue914" + INFO = "\ue912" + ERROR = "\ue913" + SUCCESS = "\ue914" + WARNING = "\ue915" # Informational icons - ADDRESS = "\ue915" - CHANGE = "\ue916" - DERIVATION = "\ue917" - FEE = "\ue918" - FINGERPRINT = "\ue919" - PASSPHRASE = "\ue91a" + ADDRESS = "\ue916" + CHANGE = "\ue917" + DERIVATION = "\ue918" + FEE = "\ue919" + FINGERPRINT = "\ue91a" + PASSPHRASE = "\ue91b" # Misc icons - BITCOIN = "\ue91b" - BITCOIN_ALT = "\ue91c" - BRIGHTNESS = "\ue91d" - MICROSD = "\ue91e" - QRCODE = "\ue91f" + BITCOIN = "\ue91c" + BITCOIN_ALT = "\ue91d" + BRIGHTNESS = "\ue91e" + MICROSD = "\ue91f" + QRCODE = "\ue920" + SIGN = "\ue921" MIN_VALUE = SCAN MAX_VALUE = QRCODE diff --git a/src/seedsigner/resources/fonts/seedsigner-icons.otf b/src/seedsigner/resources/fonts/seedsigner-icons.otf index ef4292b28effe8af1de63336109fa6c4892e95c2..0b3b1bad6f3e9f0f068cc68574ec0f07e1ae6b36 100644 GIT binary patch delta 1083 zcmYjQOK1~O6uoaUFEggqRQsWesEL@?2BWstPyJQVrfMry+K3;j1{-XwX{$-GZcI^e zETxS4go+C()P*E2gaHw8& zK9fXCTU!ehzyR1_#nwQ;{r&pKMF4a=K#xBVXxlZ~{!RyoUj_hI+%}sJbOC;apU>O(P*gG{%iatfb9};qn+VUM^)9tE6hE_Yjr0Q$~oI(jDO&@xHB3L z^%u*^8*F$7iM`QKe=k(*#*maF*tN^URqf^?sNeZ zxuk`2T~O#cacdq_vLxN9{3bw(h?`B(pN_unNJCvsHOjJY_AZ$up@JP!RxG?&y{G1F zZQaH%+fFzB*wNMcD0pe#;`TEgBjGEZJC3h1Qi`&?c)C8*Sn75(m3bWIxVfn`9Z}ep zx&tJ_oG1M3Y28v;O@ES++Q*8Ja7X>d?6vzP!&rTg7~ysG)#d5h^=7S-j+EttyDplyaU;rZmp~ z1cRvVvz+8yAps1!oJr8f&5__7v*gm1Y_j!AvS{GPZZJ}`Fu~Jy_zP*Mt&;!% delta 610 zcmYk4Ur19?9LIm>-re1~jc)1|4({R_Ev3M@F)VVx(Ei4MY z!$M3;u*;Eh==YcVIt`&Stf!HU@SQ|pPHGL{5?LPJxqWeWmWeoDmV2+Fu@{vD_DR* zBOrZ|=sMd;WcrZ}>zyVGp25~^8L(k%sTdPyfNWY`Ae@RrG{zibe(OkCM{{Q>Z$Js&W5}XGGt@M`)Y~qL7`~%<}uvP#7 From cf0e7a63e9ab0d684ae72d5e0fa26e0cbaf6c0a3 Mon Sep 17 00:00:00 2001 From: Easy <86452817+easyuxd@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:22:22 -0700 Subject: [PATCH 02/39] Change icon for Sign PSBT --- src/seedsigner/gui/screens/psbt_screens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/gui/screens/psbt_screens.py b/src/seedsigner/gui/screens/psbt_screens.py index 1a6b65066..5265730ce 100644 --- a/src/seedsigner/gui/screens/psbt_screens.py +++ b/src/seedsigner/gui/screens/psbt_screens.py @@ -743,7 +743,7 @@ def __post_init__(self): super().__post_init__() icon = Icon( - icon_name=FontAwesomeIconConstants.PAPER_PLANE, + icon_name=SeedSignerIconConstants.SIGN, icon_color=GUIConstants.INFO_COLOR, icon_size=GUIConstants.ICON_LARGE_BUTTON_SIZE, screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING From 5cf5a01dea49eb4fb087297397a0d3b84df38853 Mon Sep 17 00:00:00 2001 From: copy2018 <144347307+copy2018@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:48:52 +0000 Subject: [PATCH 03/39] Update README.md to fix version naming for verification step Included the latest version in the verification instructions. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f28e6a8d4..9f1c34227 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ The result should confirm that 1 key was *either* imported or updated. *Ignore* Next, you will run the *verify* command on the signature (.sig) file. (*Verify* must be run from inside the same folder that you downloaded the files into earlier. The `*`'s in this command will auto-fill the version from your current folder, so it should be copied and pasted as-is.) ``` -gpg --verify seedsigner.0.7.*.sha256.txt.sig +gpg --verify seedsigner.0.8.*.sha256.txt.sig ``` When the verify command completes successfully, it should display output like this: From b5e3d4f537d881d750dd87576ee4611167374acb Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 30 Dec 2024 13:46:52 -0600 Subject: [PATCH 04/39] add SeedAddressVerificationSuccessScreen --- l10n/messages.pot | 58 ++++++++++---------- src/seedsigner/gui/screens/screen.py | 17 +++--- src/seedsigner/gui/screens/seed_screens.py | 62 ++++++++++++++++++---- src/seedsigner/views/seed_views.py | 34 ++---------- 4 files changed, 94 insertions(+), 77 deletions(-) diff --git a/l10n/messages.pot b/l10n/messages.pot index cdbd2e776..e68a847a9 100644 --- a/l10n/messages.pot +++ b/l10n/messages.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: seedsigner 0.8.5\n" +"Project-Id-Version: seedsigner 0.8.5-rc1\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-11-03 14:15-0600\n" +"POT-Creation-Date: 2024-12-30 13:43-0600\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -121,9 +121,8 @@ msgid "OP_RETURN" msgstr "" #. Label for a change output in the PSBT Overview flow diagram -#. Used in a sentence describing the address type (change or receive) #: src/seedsigner/gui/screens/psbt_screens.py -#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/psbt_views.py msgid "change" msgstr "" @@ -204,11 +203,15 @@ msgstr "" msgid "Darker" msgstr "" -#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +#: src/seedsigner/gui/screens/screen.py +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Success!" msgstr "" -#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +#: src/seedsigner/gui/screens/screen.py +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "OK" msgstr "" @@ -391,6 +394,25 @@ msgstr "" msgid "Checking address {}" msgstr "" +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Address Verified" +msgstr "" + +#. Describes the address type (change or receive) +#: src/seedsigner/gui/screens/seed_screens.py +msgid "change address" +msgstr "" + +#. Describes the address type (change or receive) +#: src/seedsigner/gui/screens/seed_screens.py +msgid "receive address" +msgstr "" + +#. Describes the address index (e.g. "index 7") +#: src/seedsigner/gui/screens/seed_screens.py +msgid "index {}" +msgstr "" + #: src/seedsigner/gui/screens/seed_screens.py msgid "Multisig Verification" msgstr "" @@ -1262,30 +1284,6 @@ msgstr "" msgid "Can't validate a single sig addr without specifying a seed" msgstr "" -#: src/seedsigner/views/seed_views.py -msgid "multisig" -msgstr "" - -#. Inserts the seed fingerprint -#: src/seedsigner/views/seed_views.py -msgid "seed {}" -msgstr "" - -#. Used in a sentence describing the address type (change or receive) -#: src/seedsigner/views/seed_views.py -msgid "receive" -msgstr "" - -#. Address verification success message (e.g. "bc1qabc = seed 12345678's -#. receive address #0.") -#: src/seedsigner/views/seed_views.py -msgid "{} = {}'s {} address #{}." -msgstr "" - -#: src/seedsigner/views/seed_views.py -msgid "Address Verified" -msgstr "" - #: src/seedsigner/views/seed_views.py msgid "Scan Descriptor" msgstr "" diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 821424d19..38a6cbd77 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -332,7 +332,7 @@ def __post_init__(self): active_button_label = button_option.active_button_label else: - raise Exception("Refactor needed!") + raise Exception("Refactor to ButtonOption approach needed!") button_kwargs = dict( text=_(button_label), # Wrap here for just-in-time translations @@ -923,13 +923,14 @@ def __post_init__(self): self.components.append(self.warning_headline_textarea) next_y = next_y + self.warning_headline_textarea.height - self.components.append(TextArea( - height=self.buttons[0].screen_y - next_y, - text=_(self.text), - width=self.canvas_width, - edge_padding=self.text_edge_padding, # Don't render all the way up to the far left/right edges - screen_y=next_y, - )) + if self.text: + self.components.append(TextArea( + height=self.buttons[0].screen_y - next_y, + text=_(self.text), + width=self.canvas_width, + edge_padding=self.text_edge_padding, # Don't render all the way up to the far left/right edges + screen_y=next_y, + )) diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 6856f5783..4f768c8ad 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -1,26 +1,26 @@ -import math import logging +import math import time from dataclasses import dataclass from gettext import gettext as _ +from PIL import Image, ImageDraw, ImageFilter from typing import List -from PIL import Image, ImageDraw, ImageFilter -from seedsigner.gui.renderer import Renderer +from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants from seedsigner.helpers.qr import QR -from seedsigner.models.threads import BaseThread, ThreadsafeCounter - -from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, ButtonOption, KeyboardScreen, WarningEdgesMixin -from ..components import (Button, FontAwesomeIconConstants, Fonts, FormattedAddress, IconButton, +from seedsigner.gui.components import (Button, FontAwesomeIconConstants, Fonts, FormattedAddress, IconButton, IconTextLine, SeedSignerIconConstants, TextArea, GUIConstants, reflow_text_into_pages) - from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay -from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants +from seedsigner.gui.renderer import Renderer +from seedsigner.models.threads import BaseThread, ThreadsafeCounter + +from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, ButtonOption, KeyboardScreen, LargeIconStatusScreen, WarningEdgesMixin logger = logging.getLogger(__name__) + @dataclass class SeedMnemonicEntryScreen(BaseTopNavScreen): initial_letters: list = None @@ -1057,7 +1057,6 @@ def _run(self): self.hw_button2.render() self.renderer.show_image() - @@ -1512,6 +1511,49 @@ def run(self): +@dataclass +class SeedAddressVerificationSuccessScreen(LargeIconStatusScreen): + address: str = None + verified_index: int = None + verified_index_is_change: bool = None + + + def __post_init__(self): + # Customize defaults + self.title = _("Success!") + self.status_headline = _("Address Verified") + self.button_data = [ButtonOption("OK")] + self.is_bottom_list = True + self.show_back_button = False, + super().__post_init__() + + if self.verified_index_is_change: + # TRANSLATOR_NOTE: Describes the address type (change or receive) + address_type = _("change address") + else: + # TRANSLATOR_NOTE: Describes the address type (change or receive) + address_type = _("receive address") + + self.components.append(FormattedAddress( + screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING, + address=self.address, + max_lines=1, # Use abbreviated format w/ellipsis + )) + + self.components.append(TextArea( + text=address_type, + screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING, + )) + + # TRANSLATOR_NOTE: Describes the address index (e.g. "index 7") + index_str = _("index {}").format(self.verified_index) + self.components.append(TextArea( + text=index_str, + screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING, + )) + + + @dataclass class LoadMultisigWalletDescriptorScreen(ButtonListScreen): def __post_init__(self): diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index d478a4d08..11f1496fc 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1977,36 +1977,12 @@ def __init__(self, seed_num: int): def run(self): - from seedsigner.gui.screens.screen import LargeIconStatusScreen - address = self.controller.unverified_address["address"] - sig_type = self.controller.unverified_address["sig_type"] - verified_index = self.controller.unverified_address["verified_index"] - verified_index_is_change = self.controller.unverified_address["verified_index_is_change"] - - if sig_type == SettingsConstants.MULTISIG: - source = _("multisig") - else: - # TRANSLATOR_NOTE: Inserts the seed fingerprint - source = _("seed {}").format(self.seed.get_fingerprint()) - - # TRANSLATOR_NOTE: Used in a sentence describing the address type (change or receive) - change_text = _("change") - - # TRANSLATOR_NOTE: Used in a sentence describing the address type (change or receive) - receive_text = _("receive") - - # TRANSLATOR_NOTE: Address verification success message (e.g. "bc1qabc = seed 12345678's receive address #0.") - text = _("{} = {}'s {} address #{}.").format( - address[:7], - source, - change_text if verified_index_is_change else receive_text, - verified_index + self.run_screen( + seed_screens.SeedAddressVerificationSuccessScreen, + address = self.controller.unverified_address["address"], + verified_index = self.controller.unverified_address["verified_index"], + verified_index_is_change = self.controller.unverified_address["verified_index_is_change"], ) - LargeIconStatusScreen( - status_headline=_("Address Verified"), - text=text, - show_back_button=False, - ).display() return Destination(MainMenuView) From 5ad4818f3bd42c75e19ceb344d1ddd9317dd790f Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 30 Dec 2024 15:03:27 -0600 Subject: [PATCH 05/39] bugfix on `show_back_button` --- src/seedsigner/gui/screens/seed_screens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 4f768c8ad..5fc70017e 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -1524,7 +1524,7 @@ def __post_init__(self): self.status_headline = _("Address Verified") self.button_data = [ButtonOption("OK")] self.is_bottom_list = True - self.show_back_button = False, + self.show_back_button = False super().__post_init__() if self.verified_index_is_change: From 8c902aa6fb182e8655558b0d84e36bdc90201b62 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 1 Jan 2025 16:20:13 -0600 Subject: [PATCH 06/39] Refactor / simplify `HardwareButtons` --- src/seedsigner/gui/screens/screen.py | 30 ++--- src/seedsigner/gui/screens/seed_screens.py | 35 +++--- .../gui/screens/settings_screens.py | 2 +- src/seedsigner/hardware/buttons.py | 117 ++++++++---------- src/seedsigner/views/seed_views.py | 105 ++++++++-------- 5 files changed, 134 insertions(+), 155 deletions(-) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 821424d19..fa2865c48 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -71,6 +71,13 @@ def display(self) -> Any: for t in self.get_threads(): t.stop() + for t in self.get_threads(): + # Wait for each thread to stop; equivalent to `join()` but gracefully + # handles threads that were never run (necessary for screenshot generator + # compatibility, perhaps other edge cases). + while t.is_alive(): + time.sleep(0.01) + def clear_screen(self): # Clear the whole canvas @@ -226,11 +233,7 @@ def _run(self): time.sleep(0.1) continue - user_input = self.hw_inputs.wait_for( - HardwareButtonsConstants.ALL_KEYS, - check_release=True, - release_keys=HardwareButtonsConstants.KEYS__ANYCLICK - ) + user_input = self.hw_inputs.wait_for(HardwareButtonsConstants.ALL_KEYS) with self.renderer.lock: if not self.top_nav.is_selected and user_input in [ @@ -447,6 +450,7 @@ def _run(self): while True: ret = self._run_callback() if ret is not None: + print("Exiting ButtonListScreen due to _run_callback") return ret user_input = self.hw_inputs.wait_for( @@ -455,9 +459,7 @@ def _run(self): HardwareButtonsConstants.KEY_DOWN, HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT, - ] + HardwareButtonsConstants.KEYS__ANYCLICK, - check_release=True, - release_keys=HardwareButtonsConstants.KEYS__ANYCLICK + ] + HardwareButtonsConstants.KEYS__ANYCLICK ) with self.renderer.lock: @@ -641,9 +643,7 @@ def swap_selected_button(new_selected_button: int): HardwareButtonsConstants.KEY_DOWN, HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT - ] + HardwareButtonsConstants.KEYS__ANYCLICK, - check_release=True, - release_keys=HardwareButtonsConstants.KEYS__ANYCLICK + ] + HardwareButtonsConstants.KEYS__ANYCLICK ) with self.renderer.lock: @@ -858,9 +858,7 @@ def _run(self): HardwareButtonsConstants.KEY_DOWN, HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT, - ] + HardwareButtonsConstants.KEYS__ANYCLICK, - check_release=True, - release_keys=HardwareButtonsConstants.KEYS__ANYCLICK + ] + HardwareButtonsConstants.KEYS__ANYCLICK ) if user_input == HardwareButtonsConstants.KEY_DOWN: # Reduce QR code background brightness @@ -1191,9 +1189,7 @@ def _run(self): # 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] + HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3] ) with self.renderer.lock: diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 6856f5783..ae5e137a5 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -243,11 +243,7 @@ def _render(self): def _run(self): while True: - input = self.hw_inputs.wait_for( - HardwareButtonsConstants.ALL_KEYS, - check_release=True, - release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY2] - ) + input = self.hw_inputs.wait_for(HardwareButtonsConstants.ALL_KEYS) with self.renderer.lock: if self.is_input_in_top_nav: @@ -885,11 +881,7 @@ def _run(self): # Start the interactive update loop while True: - input = self.hw_inputs.wait_for( - HardwareButtonsConstants.ALL_KEYS, - check_release=True, - release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY1, HardwareButtonsConstants.KEY2, HardwareButtonsConstants.KEY3] - ) + input = self.hw_inputs.wait_for(HardwareButtonsConstants.ALL_KEYS) keyboard_swap = False @@ -1467,13 +1459,13 @@ def __post_init__(self): def _run_callback(self): - # Exit the screen on success via a non-None value - logger.info(f"verified_index: {self.verified_index.cur_count}") + # Exit the screen on success via a non-None value. + # see: ButtonListScreen._run() if self.verified_index.cur_count is not None: - logger.info("Screen callback returning success!") - self.threads[-1].stop() - while self.threads[-1].is_alive(): - time.sleep(0.01) + # Note that the ProgressThread will have already exited on its own. + + # Return a success value (anything other than None) to end the + # ButtonListScreen._run() loop. return 1 @@ -1490,10 +1482,13 @@ def run(self): while self.keep_running: if self.verified_index.cur_count is not None: # This thread will detect the success state while its parent Screen - # holds in its `wait_for`. Have to trigger a hw_input event to break - # the Screen._run out of the `wait_for` state. The Screen will then - # call its `_run_callback` and detect the success state and exit. - HardwareButtons.get_instance().trigger_override(force_release=True) + # blocks in its `wait_for`. Have to trigger a hw_input override event + # to break the Screen._run out of the `wait_for` state. The Screen + # will then call its `_run_callback` and detect the success state and + # exit. + HardwareButtons.get_instance().trigger_override() + + # Exit the loop and thereby end this thread return textarea = TextArea( diff --git a/src/seedsigner/gui/screens/settings_screens.py b/src/seedsigner/gui/screens/settings_screens.py index 082ad4971..794e5b2e0 100644 --- a/src/seedsigner/gui/screens/settings_screens.py +++ b/src/seedsigner/gui/screens/settings_screens.py @@ -183,7 +183,7 @@ def _run(self): screen_y=int((self.canvas_height - msg_height)/ 2), ) while True: - input = self.hw_inputs.wait_for(keys=HardwareButtonsConstants.ALL_KEYS, check_release=False) + input = self.hw_inputs.wait_for(keys=HardwareButtonsConstants.ALL_KEYS) if input == HardwareButtonsConstants.KEY1: # Note that there are three distinct screen updates that happen at diff --git a/src/seedsigner/hardware/buttons.py b/src/seedsigner/hardware/buttons.py index f37dfa80a..4261dd31f 100644 --- a/src/seedsigner/hardware/buttons.py +++ b/src/seedsigner/hardware/buttons.py @@ -33,6 +33,7 @@ class HardwareButtons(Singleton): KEY2_PIN = 12 KEY3_PIN = 8 + @classmethod def get_instance(cls): # This is the only way to access the one and only instance @@ -53,7 +54,7 @@ def get_instance(cls): cls._instance.GPIO = GPIO cls._instance.override_ind = False - cls._instance.add_events([HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN, HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT, HardwareButtonsConstants.KEY1, HardwareButtonsConstants.KEY2, HardwareButtonsConstants.KEY3]) + # cls._instance.add_events([HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN, HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT, HardwareButtonsConstants.KEY1, HardwareButtonsConstants.KEY2, HardwareButtonsConstants.KEY3]) # Track state over time so we can apply input delays/ignores as needed cls._instance.cur_input = None # Track which direction or button was last pressed @@ -65,7 +66,6 @@ def get_instance(cls): return cls._instance - @classmethod def get_instance_no_hardware(cls): # This is the only way to access the one and only instance @@ -73,17 +73,23 @@ def get_instance_no_hardware(cls): cls._instance = cls.__new__(cls) + def wait_for(self, keys=[]) -> int: + """ + Block execution until one of the target keys is pressed. - def wait_for(self, keys=[], check_release=True, release_keys=[]) -> int: + Optionally override the wait by calling `trigger_override()`. + """ # TODO: Refactor to keep control in the Controller and not here from seedsigner.controller import Controller controller = Controller.get_instance() - - if not release_keys: - release_keys = keys self.override_ind = False while True: + if self.override_ind: + # Break out of the wait_for without waiting for user input + self.override_ind = False + return HardwareButtonsConstants.OVERRIDE + cur_time = int(time.time() * 1000) if cur_time - self.last_input_time > controller.screensaver_activation_ms and not controller.is_screensaver_running: # Start the screensaver. Will block execution until input detected. @@ -99,48 +105,42 @@ def wait_for(self, keys=[], check_release=True, release_keys=[]) -> int: # Resume from a fresh loop continue + # Check each candidate key to see if it was pressed for key in keys: - if not check_release or ((check_release and key in release_keys and HardwareButtonsConstants.release_lock) or check_release and key not in release_keys): - # when check release is False or the release lock is released (True) - if self.GPIO.input(key) == GPIO.LOW or self.override_ind: - HardwareButtonsConstants.release_lock = False - if self.override_ind: - self.override_ind = False - return HardwareButtonsConstants.OVERRIDE - - if self.cur_input != key: - self.cur_input = key - self.cur_input_started = int(time.time() * 1000) # in milliseconds - self.last_input_time = self.cur_input_started + if self.GPIO.input(key) == GPIO.LOW: + if self.cur_input != key: + self.cur_input = key + self.cur_input_started = int(time.time() * 1000) # in milliseconds + self.last_input_time = self.cur_input_started + return key + + else: + # Still pressing the same input + if cur_time - self.last_input_time > self.next_repeat_threshold: + # Too much time has elapsed to consider this the same + # continuous input. Treat as a new separate press. + self.cur_input_started = cur_time + self.last_input_time = cur_time + return key + + elif cur_time - self.cur_input_started > self.first_repeat_threshold: + # We're good to relay this immediately as continuous + # input. + self.last_input_time = cur_time return key else: - # Still pressing the same input - if cur_time - self.last_input_time > self.next_repeat_threshold: - # Too much time has elapsed to consider this the same - # continuous input. Treat as a new separate press. - self.cur_input_started = cur_time - self.last_input_time = cur_time - return key - - elif cur_time - self.cur_input_started > self.first_repeat_threshold: - # We're good to relay this immediately as continuous - # input. - self.last_input_time = cur_time - return key - - else: - # We're not yet at the first repeat threshold; triggering - # a key now would be too soon and yields a bad user - # experience when only a single click was intended but - # a second input is processed because of race condition - # against human response time to release the button. - # So there has to be a delay before we allow the first - # continuous repeat to register. So we'll ignore this - # round's input and **won't update any of our - # timekeeping vars**. But once we cross the threshold, - # we let the repeats fly. - pass + # We're not yet at the first repeat threshold; triggering + # a key now would be too soon and yields a bad user + # experience when only a single click was intended but + # a second input is processed because of race condition + # against human response time to release the button. + # So there has to be a delay before we allow the first + # continuous repeat to register. So we'll ignore this + # round's input and **won't update any of our + # timekeeping vars**. But once we cross the threshold, + # we let the repeats fly. + pass time.sleep(0.01) # wait 10 ms to give CPU chance to do other things @@ -149,29 +149,13 @@ def update_last_input_time(self): self.last_input_time = int(time.time() * 1000) - def add_events(self, keys=[]): - for key in keys: - GPIO.add_event_detect(key, self.GPIO.RISING, callback=HardwareButtons.rising_callback) - - - def rising_callback(channel): - HardwareButtonsConstants.release_lock = True - - - def trigger_override(self, force_release = False) -> bool: - if force_release: - HardwareButtonsConstants.release_lock = True - - if not self.override_ind: - self.override_ind = True - return True - return False + def trigger_override(self) -> bool: + """ Set the override flag to break out of the current `wait_for` loop """ + self.override_ind = True - def force_release(self) -> bool: - HardwareButtonsConstants.release_lock = True - return True def check_for_low(self, key: int = None, keys: List[int] = None) -> bool: + """ Returns True if one of the target keys/key is pressed """ if key: keys = [key] for key in keys: @@ -181,12 +165,15 @@ def check_for_low(self, key: int = None, keys: List[int] = None) -> bool: else: return False + def has_any_input(self) -> bool: + """ Returns True if any of the keys are pressed """ for key in HardwareButtonsConstants.ALL_KEYS: if self.GPIO.input(key) == GPIO.LOW: return True return False + # class used as short hand for static button/channel lookup values # TODO: Implement `release_lock` functionality as a global somewhere. Mixes up design # patterns to have a static constants class plus a settable global value. @@ -227,5 +214,3 @@ class HardwareButtonsConstants: KEYS__LEFT_RIGHT_UP_DOWN = [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN] KEYS__ANYCLICK = [KEY_PRESS, KEY1, KEY2, KEY3] - - release_lock = True # released when True, locked when False diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index d478a4d08..35587630f 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1848,66 +1848,69 @@ def __init__(self, seed_num: int = None): def run(self): # Start brute-force calculations from the zero-th index - self.addr_verification_thread.start() + try: + self.addr_verification_thread.start() + + button_data = [self.SKIP_10, self.CANCEL] + + script_type_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES) + script_type_display = script_type_settings_entry.get_selection_option_display_name_by_value(self.script_type) + + sig_type_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SIG_TYPES) + sig_type_display = sig_type_settings_entry.get_selection_option_display_name_by_value(self.sig_type) + + network_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__NETWORK) + network_display = network_settings_entry.get_selection_option_display_name_by_value(self.network) + mainnet = network_settings_entry.get_selection_option_display_name_by_value(SettingsConstants.MAINNET) + + # Display the Screen to show the brute-forcing progress. + # Using a loop here to handle the SKIP_10 button presses to increment the counter + # and resume displaying the screen. User won't even notice that the Screen is + # being re-constructed. + while True: + selected_menu_num = self.run_screen( + seed_screens.SeedAddressVerificationScreen, + address=self.address, + derivation_path=self.derivation_path, + script_type=script_type_display, + sig_type=sig_type_display, + network=network_display, + is_mainnet=network_display == mainnet, + threadsafe_counter=self.threadsafe_counter, + verified_index=self.verified_index, + button_data=button_data, + ) - button_data = [self.SKIP_10, self.CANCEL] + if self.verified_index.cur_count is not None: + break - script_type_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES) - script_type_display = script_type_settings_entry.get_selection_option_display_name_by_value(self.script_type) + if selected_menu_num == RET_CODE__BACK_BUTTON: + break - sig_type_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SIG_TYPES) - sig_type_display = sig_type_settings_entry.get_selection_option_display_name_by_value(self.sig_type) + if selected_menu_num is None: + # Only happens in the test suite; the screen isn't actually executed so + # it returns before the brute force thread has completed. + time.sleep(0.1) + continue - network_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__NETWORK) - network_display = network_settings_entry.get_selection_option_display_name_by_value(self.network) - mainnet = network_settings_entry.get_selection_option_display_name_by_value(SettingsConstants.MAINNET) + if button_data[selected_menu_num] == self.SKIP_10: + self.threadsafe_counter.increment(10) - # Display the Screen to show the brute-forcing progress. - # Using a loop here to handle the SKIP_10 button presses to increment the counter - # and resume displaying the screen. User won't even notice that the Screen is - # being re-constructed. - while True: - selected_menu_num = self.run_screen( - seed_screens.SeedAddressVerificationScreen, - address=self.address, - derivation_path=self.derivation_path, - script_type=script_type_display, - sig_type=sig_type_display, - network=network_display, - is_mainnet=network_display == mainnet, - threadsafe_counter=self.threadsafe_counter, - verified_index=self.verified_index, - button_data=button_data, - ) + elif button_data[selected_menu_num] == self.CANCEL: + break if self.verified_index.cur_count is not None: - break - - if selected_menu_num == RET_CODE__BACK_BUTTON: - break - - if selected_menu_num is None: - # Only happens in the test suite; the screen isn't actually executed so - # it returns before the brute force thread has completed. - time.sleep(0.1) - continue + # Successfully verified the addr; update the data + self.controller.unverified_address["verified_index"] = self.verified_index.cur_count + self.controller.unverified_address["verified_index_is_change"] = self.verified_index_is_change.cur_count == 1 + return Destination(SeedAddressVerificationSuccessView, view_args=dict(seed_num=self.seed_num)) - if button_data[selected_menu_num] == self.SKIP_10: - self.threadsafe_counter.increment(10) - - elif button_data[selected_menu_num] == self.CANCEL: - break - - if self.verified_index.cur_count is not None: - # Successfully verified the addr; update the data - self.controller.unverified_address["verified_index"] = self.verified_index.cur_count - self.controller.unverified_address["verified_index_is_change"] = self.verified_index_is_change.cur_count == 1 - return Destination(SeedAddressVerificationSuccessView, view_args=dict(seed_num=self.seed_num)) - - else: + finally: # Halt the thread if the user gave up (will already be stopped if it verified the # target addr). self.addr_verification_thread.stop() + + # Block until the thread has stopped while self.addr_verification_thread.is_alive(): time.sleep(0.01) @@ -1933,7 +1936,7 @@ def __init__(self, address: str, seed: Seed, descriptor: Descriptor, script_type if self.seed: self.xpub = self.seed.get_xpub(wallet_path=self.derivation_path, network=Settings.get_instance().get_value(SettingsConstants.SETTING__NETWORK)) - + def run(self): from seedsigner.helpers import embit_utils @@ -1965,7 +1968,7 @@ def run(self): # Increment our index counter self.threadsafe_counter.increment() - + class SeedAddressVerificationSuccessView(View): From 9efb70067db62ddd23c0e17ce744f5cf424f89ce Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 1 Jan 2025 16:40:07 -0600 Subject: [PATCH 07/39] cleanup --- src/seedsigner/hardware/buttons.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/seedsigner/hardware/buttons.py b/src/seedsigner/hardware/buttons.py index 4261dd31f..db037e59b 100644 --- a/src/seedsigner/hardware/buttons.py +++ b/src/seedsigner/hardware/buttons.py @@ -54,8 +54,6 @@ def get_instance(cls): cls._instance.GPIO = GPIO cls._instance.override_ind = False - # cls._instance.add_events([HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN, HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT, HardwareButtonsConstants.KEY1, HardwareButtonsConstants.KEY2, HardwareButtonsConstants.KEY3]) - # Track state over time so we can apply input delays/ignores as needed cls._instance.cur_input = None # Track which direction or button was last pressed cls._instance.cur_input_started = None # Track when that input began From 7c09dc1e2ab0fac811345b21ca601026a9372d09 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 2 Jan 2025 15:04:08 -0600 Subject: [PATCH 08/39] Removing outdated comment --- src/seedsigner/hardware/buttons.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/seedsigner/hardware/buttons.py b/src/seedsigner/hardware/buttons.py index db037e59b..2f1a6725f 100644 --- a/src/seedsigner/hardware/buttons.py +++ b/src/seedsigner/hardware/buttons.py @@ -173,8 +173,6 @@ def has_any_input(self) -> bool: # class used as short hand for static button/channel lookup values -# TODO: Implement `release_lock` functionality as a global somewhere. Mixes up design -# patterns to have a static constants class plus a settable global value. class HardwareButtonsConstants: if GPIO.RPI_INFO['P1_REVISION'] == 3: #This indicates that we have revision 3 GPIO KEY_UP = 31 From 526784448155d94a51fa3981a3583332afa8e1b2 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 2 Jan 2025 16:40:04 -0600 Subject: [PATCH 09/39] bugfix on edge case Compact SeedQRs --- src/seedsigner/models/decode_qr.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 4ac166423..d21439213 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -417,6 +417,9 @@ def detect_segment_type(s, wordlist_language_code=None): # 32 bytes for 24-word CompactSeedQR; 16 bytes for 12-word CompactSeedQR if len(s) == 32 or len(s) == 16: try: + if not isinstance(s, bytes): + # TODO: remove this check & conversion once above cast to str is removed + s = s.encode() bitstream = "" for b in s: bitstream += bin(b).lstrip('0b').zfill(8) From 75ba1e07ecd9387af40cdf2d55878c1683031900 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 2 Jan 2025 17:31:34 -0600 Subject: [PATCH 10/39] Further bugfix; convert first, then check length --- src/seedsigner/models/decode_qr.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index d21439213..e98a2c561 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -414,12 +414,17 @@ def detect_segment_type(s, wordlist_language_code=None): pass # Is it byte data? + if not isinstance(s, bytes): + try: + # TODO: remove this check & conversion once above cast to str is removed + s = s.encode() + except UnicodeError: + # Couldn't convert back to bytes; shouldn't happen + raise Exception("Conversion to bytes failed") + # 32 bytes for 24-word CompactSeedQR; 16 bytes for 12-word CompactSeedQR if len(s) == 32 or len(s) == 16: try: - if not isinstance(s, bytes): - # TODO: remove this check & conversion once above cast to str is removed - s = s.encode() bitstream = "" for b in s: bitstream += bin(b).lstrip('0b').zfill(8) From cf83ebe033824a360700243103681d853cf9338e Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 2 Jan 2025 17:32:24 -0600 Subject: [PATCH 11/39] add test case --- tests/test_seedqr.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index d792683c1..7a93ac013 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -4,7 +4,6 @@ from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder from seedsigner.models.qr_type import QRType -from seedsigner.models.seed import Seed @@ -104,3 +103,28 @@ def test_compact_seedqr_handles_null_bytes(): # 12-word seed, multiple null bytes in a row entropy = os.urandom(10) + b'\x00\x00' + os.urandom(4) run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.SEED__COMPACTSEEDQR) + + +def test_compact_seedqr_bytes_interpretable_as_str(): + """ + Should successfully decode a Compact SeedQR whose bytes can be interpreted as a valid + string. Most Compact SeedQR byte data will raise a UnicodeDecodeError when attempting to + interpret it as a string, but edge cases are possible. + + see: Issue #656 + """ + # Randomly generated to pass the str.decode() step; 12- and 24-word entropy. + entropy_bytes_tests = [ + b'\x00' * 16, # abandon * 11 + about + b'\x12\x15\\1j`3\x0bkL}f\x00ZYK', + b'tv\x1bZjmqN@t\x13\x1aK\\v)', + b'|9\x05\x1aHF9j\xda\xb6v\x05\x08#\x12=', + b"iHK`4\x1a5\xd3\xaf\xd3\xb47htJ.}<\xea\xbf\x88Xh\x01.?R2^\xc2\xb1'", + b'|Z\x11\x1dt\xdd\x97~t&f &G$H|^[\xd3\x9d\x19Z{\ng^', + ] + + for entropy_bytes in entropy_bytes_tests: + entropy_bytes.decode() # should not raise an exception + mnemonic_length = 12 if len(entropy_bytes) == 16 else 24 + run_encode_decode_test(entropy_bytes, mnemonic_length=mnemonic_length, qr_type=QRType.SEED__COMPACTSEEDQR) From 799019311800404dca6389c7fbd5e1c97d79d73d Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 3 Jan 2025 08:11:28 -0600 Subject: [PATCH 12/39] bugfix for method arg change --- tools/seed_phrase_to_qr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/seed_phrase_to_qr.py b/tools/seed_phrase_to_qr.py index 57041e9c4..0e3f78051 100644 --- a/tools/seed_phrase_to_qr.py +++ b/tools/seed_phrase_to_qr.py @@ -31,9 +31,9 @@ seed_phrase = input("\nEnter 12- or 24-word test seed phrase: ").strip().split(" ") if format == COMPACT: - encoder = CompactSeedQrEncoder(seed_phrase=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + encoder = CompactSeedQrEncoder(mnemonic=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) else: - encoder = SeedQrEncoder(seed_phrase=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + encoder = SeedQrEncoder(mnemonic=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=3) qr.add_data(encoder.next_part()) From e972fc83f8811d51de8d3fbe41c272bba1787279 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 13 Jan 2025 10:36:47 -0600 Subject: [PATCH 13/39] swapping out print() for logging.info() --- src/seedsigner/gui/screens/screen.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index fa2865c48..1e7e28155 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -1,3 +1,4 @@ +import logging import time from dataclasses import dataclass, field @@ -15,6 +16,8 @@ from seedsigner.models.settings import SettingsConstants from seedsigner.models.threads import BaseThread, ThreadsafeCounter +logger = logging.getLogger(__name__) + # Must be huge numbers to avoid conflicting with the selected_button returned by the # screens with buttons. @@ -450,7 +453,7 @@ def _run(self): while True: ret = self._run_callback() if ret is not None: - print("Exiting ButtonListScreen due to _run_callback") + logging.info("Exiting ButtonListScreen due to _run_callback") return ret user_input = self.hw_inputs.wait_for( From 0179a62d46eb25e8c36fd06b767e90dcf9e1be34 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 14 Jan 2025 10:40:43 -0600 Subject: [PATCH 14/39] Adds missing text wrapping for "Electrum seeds" --- l10n/messages.pot | 10 +++++++--- src/seedsigner/models/settings_definition.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/l10n/messages.pot b/l10n/messages.pot index e68a847a9..af0e990e8 100644 --- a/l10n/messages.pot +++ b/l10n/messages.pot @@ -1,14 +1,14 @@ # Translations template for seedsigner. -# Copyright (C) 2024 ORGANIZATION +# Copyright (C) 2025 ORGANIZATION # This file is distributed under the same license as the seedsigner project. -# FIRST AUTHOR , 2024. +# FIRST AUTHOR , 2025. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: seedsigner 0.8.5-rc1\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-12-30 13:43-0600\n" +"POT-Creation-Date: 2025-01-14 10:35-0600\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -766,6 +766,10 @@ msgstr "" msgid "BIP-85 child seeds" msgstr "" +#: src/seedsigner/models/settings_definition.py +msgid "Electrum seeds" +msgstr "" + #: src/seedsigner/models/settings_definition.py msgid "Native Segwit only" msgstr "" diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index 7a64ae020..086525195 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -504,7 +504,7 @@ class SettingsDefinition: SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS, abbreviated_name="electrum", - display_name="Electrum seeds", + display_name=_mft("Electrum seeds"), help_text=_mft("Native Segwit only"), visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__DISABLED), From ab18ec11d1a58101d2cd2d992797f07cd2a4a8fb Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 14 Jan 2025 11:54:53 -0600 Subject: [PATCH 15/39] Sync submodules to latest translations and screenshots --- seedsigner-screenshots | 2 +- src/seedsigner/resources/seedsigner-translations | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/seedsigner-screenshots b/seedsigner-screenshots index 23cf46e14..9966c9d80 160000 --- a/seedsigner-screenshots +++ b/seedsigner-screenshots @@ -1 +1 @@ -Subproject commit 23cf46e1482b49ed429b9ff31d29457af3ea07be +Subproject commit 9966c9d8055c33e008f85f634279f5a5b4af9fad diff --git a/src/seedsigner/resources/seedsigner-translations b/src/seedsigner/resources/seedsigner-translations index 597e03daf..0f6602af3 160000 --- a/src/seedsigner/resources/seedsigner-translations +++ b/src/seedsigner/resources/seedsigner-translations @@ -1 +1 @@ -Subproject commit 597e03dafbc234ea4a7d71fe03cd828472c3eb2c +Subproject commit 0f6602af397bdde4ef09644a55df2c201e3ddaf5 From b4ee0e7f5a9d7d33dbc9bcda0c81a5045ae1f95a Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 14 Jan 2025 11:59:58 -0600 Subject: [PATCH 16/39] Update submodule branch reference --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 6bf87ec18..d4ffa6ec8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "src/seedsigner/resources/seedsigner-translations"] path = src/seedsigner/resources/seedsigner-translations url = https://github.com/SeedSigner/seedsigner-translations.git - branch = 0.8.5-rc1 + branch = dev [submodule "seedsigner-screenshots"] path = seedsigner-screenshots url = https://github.com/SeedSigner/seedsigner-screenshots.git From 46d9a53774f1535a56e6a8529639675b5c1caa68 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 14 Jan 2025 12:33:11 -0600 Subject: [PATCH 17/39] Update translations repo pointer to latest commit --- src/seedsigner/resources/seedsigner-translations | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/resources/seedsigner-translations b/src/seedsigner/resources/seedsigner-translations index 0f6602af3..112fc9e9d 160000 --- a/src/seedsigner/resources/seedsigner-translations +++ b/src/seedsigner/resources/seedsigner-translations @@ -1 +1 @@ -Subproject commit 0f6602af397bdde4ef09644a55df2c201e3ddaf5 +Subproject commit 112fc9e9d2e64c7e2ea640fdb5ccdb5ea0ec4236 From b4b484be920628b65b423a2114d0534b60659494 Mon Sep 17 00:00:00 2001 From: Easy <86452817+easyuxd@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:16:01 -0800 Subject: [PATCH 18/39] Added input/keyboard icons (delete, space) --- src/seedsigner/gui/components.py | 4 ++++ .../resources/fonts/seedsigner-icons.otf | Bin 29956 -> 31228 bytes 2 files changed, 4 insertions(+) diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index b707b17e0..02a090fc3 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -148,6 +148,10 @@ class SeedSignerIconConstants: QRCODE = "\ue920" SIGN = "\ue921" + # Input icons + DELETE = "\ue922" + SPACE = "\ue923" + MIN_VALUE = SCAN MAX_VALUE = QRCODE diff --git a/src/seedsigner/resources/fonts/seedsigner-icons.otf b/src/seedsigner/resources/fonts/seedsigner-icons.otf index 0b3b1bad6f3e9f0f068cc68574ec0f07e1ae6b36..3848d9121bb1b5254dbfcda9e4d1167b600a4463 100644 GIT binary patch delta 1700 zcmZuxU1%It6h3!nXJ_)eCKxF=mJN+HyVfW%M1*2uYd0^^70nbxL4!$}v~IdFDRynh zEZSmW6btiUT#YDL(3$#_mlk{of~YU*%f2XxpdtxbDK>R?vum&4y)(Pr765=b0dkyCtQAo?2TwH=seCh$1 z7%|w;%*+qxdw+j1dhj~6g~cWPcInF3jeV4Kjg(_J>jY`?L!Gk0H;DG@@~nZuziC)> z2qB_RCEVfku@Sa!eTrRR(IKLv)2}Yhot%#i4h)eq5E^nGQKs(s2+9PAL1B6Wd#mIN zAr{-*=Ui5L*|_tQGN$aO`_^OOG1hXZ{5X{lh02GWj2gS!(5O;V4fbc$AiBc{4R;yT zZY(_0O>xIn$6LazjdilUY{03iwXRe1b4MN>9Q3z`p~KmYq$A8@m#E*F(^BDAzMKDc z{jxT6+$uj-S?eheM=si&?lMizcM2w_Zr9^{6Q;>)5x0LNT$}S>T~Dt)uh}O!|Ht(* zoEJ^=RNbO1w_xkF;Yikgos&{9&v4RQ&$pca4IFCC5vW|x;?!Z9dNo~IRV5g%1b0;= zwYwmrPS^7-r?Bg-auPzQ4OOLd={;4t?*m4tZ7rA|NHX7kDCyU1z4S{p(ZhKeP-i*6 zAs8y6oqsK1eOG#}_trVichO%W9xp=DbwLJ6E1Xo3aaE|1c17m-88WTO0AM9ZlgY9e z5CD<|dNH0z0DZ>_0>CoO+e;)X8ojAP&J2SoAclgxoyTZ866jn7kQjAxoslJl{wJc>`3S=YvYgSf7QSC>AlBrZQBl2FMa$ssk(jaG(Mc zk(B4&_2*3?1bC%Si11zWC?|%a_#L+1Y3NkGv+_-kp^|g3_bJ7wlJjKtbn@3e5NTYVt%L3mMa1_Px)^wW_o=#Z2Pq+A`5fx`3$qqmw)CZ)2w;Tu)jb@=- z2o&v3Ao*s~4G${o{Z^IQR!Pws1O-zNG=L?3-RX;G+l~<=Hv~Wb3yEw4)Xs1 D007Va delta 586 zcmYjNJ7`l;82-*V_t9D*ZC#37?M&`n%;4bbP0pS(j^s$#x717a-x>WmKPVM<8TMNq_?W~wKV2*! zcABl|VX_v}es+BQ(GB7Do)Koi1!eU5$#@MTe%w9cjykH$~ zpR7%w7MHb29hmO!U&*by>4=Sp19UpFx68p!=jy|54Cp83j0w>xTy0o&MHRZs_9bF^ z#ZFW=?Xk<%2iNaNJ#h2D!hb)}dMdA{H9mqkB@QBm3zS`Mxp0Ncu7Wsy4Ock*=OvtP MqP1-< Date: Wed, 15 Jan 2025 14:49:03 -0600 Subject: [PATCH 19/39] Start using named variables in l10n English source strings --- l10n/messages.pot | 9 +++++---- src/seedsigner/gui/screens/tools_screens.py | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/l10n/messages.pot b/l10n/messages.pot index af0e990e8..84ffd5bcc 100644 --- a/l10n/messages.pot +++ b/l10n/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: seedsigner 0.8.5-rc1\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-01-14 10:35-0600\n" +"POT-Creation-Date: 2025-01-15 10:45-0600\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -539,11 +539,12 @@ msgstr "" msgid "Build Final Word" msgstr "" -#. Number of BIP-39 seed words, and the entropy -- in bits, contained within. +#. Final word calc. `mnemonic_length` = 12 or 24. `num_bits` = 7 or 3 (bits of +#. entropy in final word). #: src/seedsigner/gui/screens/tools_screens.py msgid "" -"The {}th word is built from {} more entropy bits plus auto-calculated " -"checksum." +"The {mnemonic_length}th word is built from {num_bits} more entropy bits " +"plus auto-calculated checksum." msgstr "" #. current coin-flip number vs total flips (e.g. flip 3 of 4) diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index 5b40f4425..c99c2b7ab 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -183,8 +183,8 @@ def __post_init__(self): super().__post_init__() self.components.append(TextArea( - # TRANSLATOR_NOTE: Number of BIP-39 seed words, and the entropy -- in bits, contained within. - text=_("The {}th word is built from {} more entropy bits plus auto-calculated checksum.").format(self.mnemonic_length, self.num_entropy_bits), + # TRANSLATOR_NOTE: Final word calc. `mnemonic_length` = 12 or 24. `num_bits` = 7 or 3 (bits of entropy in final word). + text=_("The {mnemonic_length}th word is built from {num_bits} more entropy bits plus auto-calculated checksum.").format(mnemonic_length=self.mnemonic_length, num_bits=self.num_entropy_bits), screen_y=self.top_nav.height + int(GUIConstants.COMPONENT_PADDING/2), )) @@ -454,7 +454,7 @@ def __post_init__(self): self.components.append(IconTextLine( # TRANSLATOR_NOTE: a label for a BIP-380-ish Output Descriptor label_text=_("Wallet descriptor"), - value_text=self.wallet_descriptor_display_name, + value_text=self.wallet_descriptor_display_name, # TODO: English text from embit (e.g. "1 / 2 multisig"); make l10 friendly is_text_centered=True, screen_x=GUIConstants.EDGE_PADDING, screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, From 568534d6309c46dd7017b1eee672b8cd7b2951e9 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 15 Jan 2025 14:49:18 -0600 Subject: [PATCH 20/39] Sync submodule pointers --- seedsigner-screenshots | 2 +- src/seedsigner/resources/seedsigner-translations | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/seedsigner-screenshots b/seedsigner-screenshots index 9966c9d80..f31390676 160000 --- a/seedsigner-screenshots +++ b/seedsigner-screenshots @@ -1 +1 @@ -Subproject commit 9966c9d8055c33e008f85f634279f5a5b4af9fad +Subproject commit f313906766347bbcc99a167d434d055407c34dd7 diff --git a/src/seedsigner/resources/seedsigner-translations b/src/seedsigner/resources/seedsigner-translations index 112fc9e9d..281a3dab8 160000 --- a/src/seedsigner/resources/seedsigner-translations +++ b/src/seedsigner/resources/seedsigner-translations @@ -1 +1 @@ -Subproject commit 112fc9e9d2e64c7e2ea640fdb5ccdb5ea0ec4236 +Subproject commit 281a3dab8304ab17eef57fb6b1a7170ae53c204b From f9534e79e3fe19a7e48ccef2e2182585241005ce Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 20 Jan 2025 11:39:39 -0600 Subject: [PATCH 21/39] fixes uppercase bug; adds tests --- src/seedsigner/models/decode_qr.py | 110 ++++++++++-------- tests/test_decodepsbtqr.py | 173 ++++++++++++++++++----------- 2 files changed, 174 insertions(+), 109 deletions(-) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index e98a2c561..c37fee3f3 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -507,8 +507,7 @@ def base43_decode(s): def is_bitcoin_address(s): if re.search(r'^bitcoin\:.*', s, re.IGNORECASE): return True - elif re.search(r'^((bc1|tb1|bcr|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', s): - # TODO: Handle regtest bcrt? + elif re.search(r'^((bc1|tb1|bcr|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', s, re.IGNORECASE): return True else: return False @@ -940,61 +939,78 @@ def __init__(self): def add(self, segment, qr_type=QRType.BITCOIN_ADDRESS): - r = re.search(r'((bc1q|tb1q|bcrt1q|bc1p|tb1p|bcrt1p|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,64})', segment) - if r != None: - self.address = r.group(1) - - if re.search(r'^((bc1q|tb1q|bcrt1q|bc1p|tb1p|bcrt1p|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,64})$', self.address) != None: - self.complete = True - self.collected_segments = 1 - - # get address type - r = re.search(r'^((bc1q|tb1q|bcrt1q|bc1p|tb1p|bcrt1p|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,64})$', self.address) - if r != None: - r = r.group(2) - - if r == "1": - # Legacy P2PKH. mainnet - self.address_type = (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET) + """ + Input may be prefixed with "bitcoin:" but will be ignored. - elif r == "m" or r == "n": - self.address_type = (SettingsConstants.LEGACY_P2PKH, SettingsConstants.TESTNET) + RegEx searches for a recognizable bitcoin address. + * The `^` ensures that the specified address prefixes can only match at + the beginning of the address. - elif r == "3": - # Nested segwit single sig (p2sh-p2wpkh), nested segwit multisig (p2sh-p2wsh), or legacy multisig (p2sh); mainnet - # TODO: Would be more correct to use a P2SH constant - self.address_type = (SettingsConstants.NESTED_SEGWIT, SettingsConstants.MAINNET) + Result will yield the following match groups: + * group 1: complete address + * group 2: address prefix + """ + address_match = re.search(r'^((bc1q|tb1q|bcrt1q|bc1p|tb1p|bcrt1p|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,64})', segment.split(":")[-1], re.IGNORECASE) + if address_match != None: + self.address = address_match.group(1) + self.complete = True + self.collected_segments = 1 + + # Have to handle wallets that uppercase bech32 addresses. + # Note that it's safe to lowercase the prefix for ALL addr formats. + addr_prefix = address_match.group(2).lower() + + if addr_prefix == "1": + # Legacy P2PKH. mainnet + self.address_type = (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET) - elif r == "2": - # Nested segwit single sig (p2sh-p2wpkh), nested segwit multisig (p2sh-p2wsh), or legacy multisig (p2sh); testnet / regtest - self.address_type = (SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET) + elif addr_prefix == "m" or addr_prefix == "n": + self.address_type = (SettingsConstants.LEGACY_P2PKH, SettingsConstants.TESTNET) - elif r == "bc1q": - # Native Segwit (single sig or multisig), mainnet - self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET) + elif addr_prefix == "3": + # Nested segwit single sig (p2sh-p2wpkh), nested segwit multisig (p2sh-p2wsh), or legacy multisig (p2sh); mainnet + # TODO: Would be more correct to use a P2SH constant + self.address_type = (SettingsConstants.NESTED_SEGWIT, SettingsConstants.MAINNET) - elif r == "tb1q": - # Native Segwit (single sig or multisig), testnet - self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TESTNET) + elif addr_prefix == "2": + # Nested segwit single sig (p2sh-p2wpkh), nested segwit multisig (p2sh-p2wsh), or legacy multisig (p2sh); testnet / regtest + self.address_type = (SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET) - elif r == "bcrt1q": - # Native Segwit (single sig or multisig), regtest - self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.REGTEST) + elif addr_prefix == "bc1q": + # Native Segwit (single sig or multisig), mainnet + self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET) - elif r == "bc1p": - # Native Segwit (single sig or multisig), mainnet - self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.MAINNET) + elif addr_prefix == "tb1q": + # Native Segwit (single sig or multisig), testnet + self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TESTNET) - elif r == "tb1p": - # Native Segwit (single sig or multisig), testnet - self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.TESTNET) + elif addr_prefix == "bcrt1q": + # Native Segwit (single sig or multisig), regtest + self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.REGTEST) - elif r == "bcrt1p": - # Native Segwit (single sig or multisig), regtest - self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.REGTEST) - - return DecodeQRStatus.COMPLETE + elif addr_prefix == "bc1p": + # Native Segwit (single sig or multisig), mainnet + self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.MAINNET) + + elif addr_prefix == "tb1p": + # Native Segwit (single sig or multisig), testnet + self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.TESTNET) + + elif addr_prefix == "bcrt1p": + # Native Segwit (single sig or multisig), regtest + self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.REGTEST) + + else: + logger.debug(f"Unknown address prefix: {addr_prefix}") + return DecodeQRStatus.INVALID + + # If the addr type is case-insensitive, ensure we return it lowercase + if self.address_type[0] in [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TAPROOT]: + self.address = self.address.lower() + + return DecodeQRStatus.COMPLETE + logger.debug(f"Invalid address: {segment}") return DecodeQRStatus.INVALID diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py index 9b7d6981d..a3bbd2f55 100644 --- a/tests/test_decodepsbtqr.py +++ b/tests/test_decodepsbtqr.py @@ -281,69 +281,118 @@ def test_short_4_letter_mnemonic_qr(): assert d.get_seed_phrase() == ["height", "demise", "useless", "trap", "grow", "lion", "found", "off", "key", "clown", "transfer", "enroll"] -def test_bitcoin_address(): - bad1 = "loremipsum" - bad2 = "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae" - bad3 = "121802020768124106400009195602431595117715840445" - - legacy_address1 = "1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY" - legacy_address2 = "16ftSEQ4ctQFDtVZiUBusQUjRrGhM3JYwe" - - main_bech32_address = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" - test_bech32_address = "tb1qkurj377gtlmu0j5flcykcsh2xagexh9h3jk06a" - - main_nested_segwit_address = "3Nu78Cqcf6hsD4sUBAN9nP13tYiHU9QPFX" - test_nested_segwit_address = "2N6JbrvPMMwbBhu2KxqXyyHUQz3XKspvyfm" - - main_bech32_address2 = "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=12000" - main_bech32_address3 = "BITCOIN:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?junk" - - d = DecodeQR() - d.add_data(bad1) - - assert d.qr_type == QRType.INVALID - - d = DecodeQR() - d.add_data(legacy_address1) - - assert d.get_address() == legacy_address1 - assert d.get_address_type() == (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET) - - d = DecodeQR() - d.add_data(legacy_address2) - - assert d.get_address() == legacy_address2 - assert d.get_address_type() == (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET) - - d = DecodeQR() - d.add_data(main_bech32_address) - - assert d.get_address() == main_bech32_address - assert d.get_address_type() == (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET) - - d = DecodeQR() - d.add_data(test_bech32_address) - - assert d.get_address() == test_bech32_address - assert d.get_address_type() == (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TESTNET) - - d = DecodeQR() - d.add_data(main_nested_segwit_address) - - assert d.get_address() == main_nested_segwit_address - assert d.get_address_type() == (SettingsConstants.NESTED_SEGWIT, SettingsConstants.MAINNET) - - d = DecodeQR() - d.add_data(test_nested_segwit_address) - - assert d.get_address() == test_nested_segwit_address - assert d.get_address_type() == (SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET) +# Test data for bitcoin address decoding +legacy_address1 = "1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY" +legacy_address2 = "16ftSEQ4ctQFDtVZiUBusQUjRrGhM3JYwe" + +main_nested_segwit_address = "3Nu78Cqcf6hsD4sUBAN9nP13tYiHU9QPFX" +test_nested_segwit_address = "2N6JbrvPMMwbBhu2KxqXyyHUQz3XKspvyfm" + +main_native_segwit_address = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" +test_native_segwit_address = "tb1qkurj377gtlmu0j5flcykcsh2xagexh9h3jk06a" + +main_taproot_address = "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr" +test_taproot_address = "tb1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqlqt9zj" +regtest_taproot_address = "bcrt1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqjeprhg" + + + +def test_bitcoin_address(): + """ + Decoder should parse various types of valid bitcoin addresses with or without the + "bitcoin:" prefix and optional query params. + """ + def decode(address, expected_script_type, expected_network=SettingsConstants.MAINNET): + for data in [address, "bitcoin:" + address, "bitcoin:" + address + "?amount=12000"]: + d = DecodeQR() + d.add_data(data) + assert d.get_address() == address + assert d.get_address_type() == (expected_script_type, expected_network) + + decode(legacy_address1, SettingsConstants.LEGACY_P2PKH) + decode(legacy_address2, SettingsConstants.LEGACY_P2PKH) + decode(main_nested_segwit_address, SettingsConstants.NESTED_SEGWIT) + decode(test_nested_segwit_address, SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET) + decode(main_native_segwit_address, SettingsConstants.NATIVE_SEGWIT) + decode(test_native_segwit_address, SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TESTNET) + decode(main_taproot_address, SettingsConstants.TAPROOT) + decode(test_taproot_address, SettingsConstants.TAPROOT, SettingsConstants.TESTNET) + decode(regtest_taproot_address, SettingsConstants.TAPROOT, SettingsConstants.REGTEST) + + + +def test_invalid_bitcoin_address(): + """ + Decoder should fail to parse invalid address data. + * Test incorrect "bitcoin:" prefix. + * Test invalid addresses. + * Test valid addresses w/additional prefixes (to ensure regexp is not finding + mid-string matches) which make the data invalid. + """ + bad_inputs = [ + # wrong separator char + "bitcoin=bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", + + # Unrecognized addr prefix + "bitcoin:bcfakehrp1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", + + # valid addr w/garbage addr prefix + "abcbc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", + + # valid "bitcoin:" prefix w/garbage addr prefix + "bitcoin:abcbc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", + + # typo in "bitcoin:" prefix + "bitcon:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", + ] - d = DecodeQR() - d.add_data(main_bech32_address2) - - assert d.get_address() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" - assert d.get_address_type() == (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET) + for bad_input in bad_inputs: + d = DecodeQR() + status = d.add_data(bad_input) + assert status == DecodeQRStatus.INVALID + + + +def test_bitcoin_address_ignores_case_where_allowed(): + """ + Decoder should ignore case in QR data prefix and in the address itself (for the + address types where case is ignored). + """ + def decode(address, expected_script_type, is_case_sensitive, expected_network=SettingsConstants.MAINNET): + addr_variations = [address] + if not is_case_sensitive: + # Test as-is and all uppercase + addr_variations.append(address.upper()) + + for addr_variation in addr_variations: + # First add prefix capitalizations + variations_1 = ["bitcoin:" + addr_variation, "BITCOIN:" + addr_variation] + + # Now add query params + variations_2 = [v + "?amount=12000" for v in variations_1] + variations_3 = [v + "?AMOUNT=12000" for v in variations_1] + for data in [addr_variation] + variations_1 + variations_2 + variations_3: + d = DecodeQR() + d.add_data(data) + assert d.get_address_type() == (expected_script_type, expected_network) + + if not is_case_sensitive: + assert d.get_address() == addr_variation.lower() + else: + assert d.get_address() == addr_variation + + # Case sensitive address types + decode(legacy_address1, SettingsConstants.LEGACY_P2PKH, is_case_sensitive=True) + decode(legacy_address2, SettingsConstants.LEGACY_P2PKH, is_case_sensitive=True) + decode(main_nested_segwit_address, SettingsConstants.NESTED_SEGWIT, is_case_sensitive=True) + decode(test_nested_segwit_address, SettingsConstants.NESTED_SEGWIT, is_case_sensitive=True, expected_network=SettingsConstants.TESTNET) + + # Case insensitive address types + decode(main_native_segwit_address, SettingsConstants.NATIVE_SEGWIT, is_case_sensitive=False) + decode(test_native_segwit_address, SettingsConstants.NATIVE_SEGWIT, is_case_sensitive=False, expected_network=SettingsConstants.TESTNET) + decode(main_taproot_address, SettingsConstants.TAPROOT, is_case_sensitive=False) + decode(test_taproot_address, SettingsConstants.TAPROOT, is_case_sensitive=False, expected_network=SettingsConstants.TESTNET) + decode(regtest_taproot_address, SettingsConstants.TAPROOT, is_case_sensitive=False, expected_network=SettingsConstants.REGTEST) From 2bd32333e4f463f1e03af1810fbed51003164ca4 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 20 Jan 2025 11:52:40 -0600 Subject: [PATCH 22/39] minor improvement after reviewing coverage --- src/seedsigner/models/decode_qr.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index c37fee3f3..34a8ea932 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -964,7 +964,7 @@ def add(self, segment, qr_type=QRType.BITCOIN_ADDRESS): # Legacy P2PKH. mainnet self.address_type = (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET) - elif addr_prefix == "m" or addr_prefix == "n": + elif addr_prefix in ["m", "n"]: self.address_type = (SettingsConstants.LEGACY_P2PKH, SettingsConstants.TESTNET) elif addr_prefix == "3": @@ -999,10 +999,7 @@ def add(self, segment, qr_type=QRType.BITCOIN_ADDRESS): elif addr_prefix == "bcrt1p": # Native Segwit (single sig or multisig), regtest self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.REGTEST) - - else: - logger.debug(f"Unknown address prefix: {addr_prefix}") - return DecodeQRStatus.INVALID + # Note: there is no final "else" here because the regex won't return any other matches. # If the addr type is case-insensitive, ensure we return it lowercase if self.address_type[0] in [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TAPROOT]: From 684d1d077d9170d9377c5c995d69cc276b10bee1 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 20 Jan 2025 16:59:25 -0600 Subject: [PATCH 23/39] Add testnet legacy addr to tests --- tests/test_decodepsbtqr.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py index a3bbd2f55..6e5230bb4 100644 --- a/tests/test_decodepsbtqr.py +++ b/tests/test_decodepsbtqr.py @@ -284,6 +284,7 @@ def test_short_4_letter_mnemonic_qr(): # Test data for bitcoin address decoding legacy_address1 = "1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY" legacy_address2 = "16ftSEQ4ctQFDtVZiUBusQUjRrGhM3JYwe" +test_legacy_address = "mkpZhYtJu2r87Js3pDiWJDmPte2NRZ8bJV" main_nested_segwit_address = "3Nu78Cqcf6hsD4sUBAN9nP13tYiHU9QPFX" test_nested_segwit_address = "2N6JbrvPMMwbBhu2KxqXyyHUQz3XKspvyfm" @@ -309,8 +310,9 @@ def decode(address, expected_script_type, expected_network=SettingsConstants.MAI assert d.get_address() == address assert d.get_address_type() == (expected_script_type, expected_network) - decode(legacy_address1, SettingsConstants.LEGACY_P2PKH) + decode(legacy_address1, SettingsConstants.LEGACY_P2PKH) decode(legacy_address2, SettingsConstants.LEGACY_P2PKH) + decode(test_legacy_address, SettingsConstants.LEGACY_P2PKH, SettingsConstants.TESTNET) decode(main_nested_segwit_address, SettingsConstants.NESTED_SEGWIT) decode(test_nested_segwit_address, SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET) decode(main_native_segwit_address, SettingsConstants.NATIVE_SEGWIT) @@ -384,6 +386,7 @@ def decode(address, expected_script_type, is_case_sensitive, expected_network=Se # Case sensitive address types decode(legacy_address1, SettingsConstants.LEGACY_P2PKH, is_case_sensitive=True) decode(legacy_address2, SettingsConstants.LEGACY_P2PKH, is_case_sensitive=True) + decode(test_legacy_address, SettingsConstants.LEGACY_P2PKH, is_case_sensitive=True, expected_network=SettingsConstants.TESTNET) decode(main_nested_segwit_address, SettingsConstants.NESTED_SEGWIT, is_case_sensitive=True) decode(test_nested_segwit_address, SettingsConstants.NESTED_SEGWIT, is_case_sensitive=True, expected_network=SettingsConstants.TESTNET) From a90e7a6cc8861b74612d521d008031b22ba7cad9 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 20 Jan 2025 17:12:07 -0600 Subject: [PATCH 24/39] Use same test key for all test addrs; add native segwit regtest --- tests/test_decodepsbtqr.py | 62 +++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py index 6e5230bb4..7672d62b9 100644 --- a/tests/test_decodepsbtqr.py +++ b/tests/test_decodepsbtqr.py @@ -281,20 +281,20 @@ def test_short_4_letter_mnemonic_qr(): assert d.get_seed_phrase() == ["height", "demise", "useless", "trap", "grow", "lion", "found", "off", "key", "clown", "transfer", "enroll"] -# Test data for bitcoin address decoding -legacy_address1 = "1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY" -legacy_address2 = "16ftSEQ4ctQFDtVZiUBusQUjRrGhM3JYwe" -test_legacy_address = "mkpZhYtJu2r87Js3pDiWJDmPte2NRZ8bJV" +# Test data for bitcoin address decoding. All generated from test key: ["abandon"] * 11 + ["about"] +legacy_address_mainnet = "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA" +legacy_address_testnet = "mkpZhYtJu2r87Js3pDiWJDmPte2NRZ8bJV" -main_nested_segwit_address = "3Nu78Cqcf6hsD4sUBAN9nP13tYiHU9QPFX" -test_nested_segwit_address = "2N6JbrvPMMwbBhu2KxqXyyHUQz3XKspvyfm" +nested_segwit_address_mainnet = "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf" +nested_segwit_address_testnet = "2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2" -main_native_segwit_address = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" -test_native_segwit_address = "tb1qkurj377gtlmu0j5flcykcsh2xagexh9h3jk06a" +native_segwit_address_mainnet = "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu" +native_segwit_address_testnet = "tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl" +native_segwit_address_regtest = "bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk" -main_taproot_address = "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr" -test_taproot_address = "tb1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqlqt9zj" -regtest_taproot_address = "bcrt1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqjeprhg" +taproot_address_mainnet = "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr" +taproot_address_testnet = "tb1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqlqt9zj" +taproot_address_regtest = "bcrt1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqjeprhg" @@ -310,16 +310,16 @@ def decode(address, expected_script_type, expected_network=SettingsConstants.MAI assert d.get_address() == address assert d.get_address_type() == (expected_script_type, expected_network) - decode(legacy_address1, SettingsConstants.LEGACY_P2PKH) - decode(legacy_address2, SettingsConstants.LEGACY_P2PKH) - decode(test_legacy_address, SettingsConstants.LEGACY_P2PKH, SettingsConstants.TESTNET) - decode(main_nested_segwit_address, SettingsConstants.NESTED_SEGWIT) - decode(test_nested_segwit_address, SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET) - decode(main_native_segwit_address, SettingsConstants.NATIVE_SEGWIT) - decode(test_native_segwit_address, SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TESTNET) - decode(main_taproot_address, SettingsConstants.TAPROOT) - decode(test_taproot_address, SettingsConstants.TAPROOT, SettingsConstants.TESTNET) - decode(regtest_taproot_address, SettingsConstants.TAPROOT, SettingsConstants.REGTEST) + decode(legacy_address_mainnet, SettingsConstants.LEGACY_P2PKH) + decode(legacy_address_testnet, SettingsConstants.LEGACY_P2PKH, SettingsConstants.TESTNET) + decode(nested_segwit_address_mainnet, SettingsConstants.NESTED_SEGWIT) + decode(nested_segwit_address_testnet, SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET) + decode(native_segwit_address_mainnet, SettingsConstants.NATIVE_SEGWIT) + decode(native_segwit_address_testnet, SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TESTNET) + decode(native_segwit_address_regtest, SettingsConstants.NATIVE_SEGWIT, SettingsConstants.REGTEST) + decode(taproot_address_mainnet, SettingsConstants.TAPROOT) + decode(taproot_address_testnet, SettingsConstants.TAPROOT, SettingsConstants.TESTNET) + decode(taproot_address_regtest, SettingsConstants.TAPROOT, SettingsConstants.REGTEST) @@ -384,18 +384,18 @@ def decode(address, expected_script_type, is_case_sensitive, expected_network=Se assert d.get_address() == addr_variation # Case sensitive address types - decode(legacy_address1, SettingsConstants.LEGACY_P2PKH, is_case_sensitive=True) - decode(legacy_address2, SettingsConstants.LEGACY_P2PKH, is_case_sensitive=True) - decode(test_legacy_address, SettingsConstants.LEGACY_P2PKH, is_case_sensitive=True, expected_network=SettingsConstants.TESTNET) - decode(main_nested_segwit_address, SettingsConstants.NESTED_SEGWIT, is_case_sensitive=True) - decode(test_nested_segwit_address, SettingsConstants.NESTED_SEGWIT, is_case_sensitive=True, expected_network=SettingsConstants.TESTNET) + decode(legacy_address_mainnet, SettingsConstants.LEGACY_P2PKH, is_case_sensitive=True) + decode(legacy_address_testnet, SettingsConstants.LEGACY_P2PKH, is_case_sensitive=True, expected_network=SettingsConstants.TESTNET) + decode(nested_segwit_address_mainnet, SettingsConstants.NESTED_SEGWIT, is_case_sensitive=True) + decode(nested_segwit_address_testnet, SettingsConstants.NESTED_SEGWIT, is_case_sensitive=True, expected_network=SettingsConstants.TESTNET) # Case insensitive address types - decode(main_native_segwit_address, SettingsConstants.NATIVE_SEGWIT, is_case_sensitive=False) - decode(test_native_segwit_address, SettingsConstants.NATIVE_SEGWIT, is_case_sensitive=False, expected_network=SettingsConstants.TESTNET) - decode(main_taproot_address, SettingsConstants.TAPROOT, is_case_sensitive=False) - decode(test_taproot_address, SettingsConstants.TAPROOT, is_case_sensitive=False, expected_network=SettingsConstants.TESTNET) - decode(regtest_taproot_address, SettingsConstants.TAPROOT, is_case_sensitive=False, expected_network=SettingsConstants.REGTEST) + decode(native_segwit_address_mainnet, SettingsConstants.NATIVE_SEGWIT, is_case_sensitive=False) + decode(native_segwit_address_testnet, SettingsConstants.NATIVE_SEGWIT, is_case_sensitive=False, expected_network=SettingsConstants.TESTNET) + decode(native_segwit_address_regtest, SettingsConstants.NATIVE_SEGWIT, is_case_sensitive=False, expected_network=SettingsConstants.REGTEST) + decode(taproot_address_mainnet, SettingsConstants.TAPROOT, is_case_sensitive=False) + decode(taproot_address_testnet, SettingsConstants.TAPROOT, is_case_sensitive=False, expected_network=SettingsConstants.TESTNET) + decode(taproot_address_regtest, SettingsConstants.TAPROOT, is_case_sensitive=False, expected_network=SettingsConstants.REGTEST) From b541112ab4ebb72fd0a389032bb3ff66616e8d87 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 21 Jan 2025 08:45:52 -0600 Subject: [PATCH 25/39] Enable basic p2tr addr verification; add test --- src/seedsigner/views/seed_views.py | 8 ++------ tests/test_flows_tools.py | 31 +++++++++++++++++++----------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 79313f04c..7a9c5121b 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1722,8 +1722,8 @@ def run(self): destination = Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__VERIFY_SINGLESIG_ADDR), skip_current_view=True) elif self.controller.unverified_address["script_type"] == SettingsConstants.TAPROOT: - # TODO: add Taproot support - return Destination(NotYetImplementedView) + sig_type = SettingsConstants.SINGLE_SIG + destination = Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__VERIFY_SINGLESIG_ADDR), skip_current_view=True) derivation_path = embit_utils.get_standard_derivation_path( network=self.controller.unverified_address["network"], @@ -1816,10 +1816,6 @@ def __init__(self, seed_num: int = None): self.sig_type = self.controller.unverified_address["sig_type"] self.network = self.controller.unverified_address["network"] - if self.script_type == SettingsConstants.TAPROOT: - # TODO: Taproot addr verification - return Destination(NotYetImplementedView) - # TODO: This should be in `Seed` or `PSBT` utility class embit_network = SettingsConstants.map_network_to_embit(self.network) diff --git a/tests/test_flows_tools.py b/tests/test_flows_tools.py index 968b72fc9..a7bfa2c47 100644 --- a/tests/test_flows_tools.py +++ b/tests/test_flows_tools.py @@ -257,16 +257,25 @@ def test__verify_address__singlesig__flow(self): settings = controller.settings settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.REGTEST) - def load_address_into_decoder(view: scan_views.ScanView): + addrs = [ # Native segwit regtest receive addr @ index 6 - view.decoder.add_data("bcrt1q4e9q5taxnsvc6m0uxv6h75mkzvnkxeqk6l90u2") + "bcrt1q4e9q5taxnsvc6m0uxv6h75mkzvnkxeqk6l90u2", - self.run_sequence([ - FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), - FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.VERIFY_ADDRESS), - FlowStep(scan_views.ScanAddressView, before_run=load_address_into_decoder), # simulate read address QR - FlowStep(seed_views.AddressVerificationStartView, is_redirect=True), - FlowStep(seed_views.SeedSelectSeedView, screen_return_value=0), - FlowStep(seed_views.SeedAddressVerificationView), - FlowStep(seed_views.SeedAddressVerificationSuccessView), - ]) + # Taproot regtest change addr @ index 48 + "bcrt1pj5v8ean2hc5lh2djsgfx4j9uc0n67942ngv6q9r49qv88ex5mrwsn3u4f7", + ] + + for test_addr in addrs: + def load_address_into_decoder(view: scan_views.ScanView): + # Native segwit regtest receive addr @ index 6 + view.decoder.add_data(test_addr) + + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), + FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.VERIFY_ADDRESS), + FlowStep(scan_views.ScanAddressView, before_run=load_address_into_decoder), # simulate read address QR + FlowStep(seed_views.AddressVerificationStartView, is_redirect=True), + FlowStep(seed_views.SeedSelectSeedView, screen_return_value=0), + FlowStep(seed_views.SeedAddressVerificationView), + FlowStep(seed_views.SeedAddressVerificationSuccessView), + ]) From defaed08992d83cc5d460234a9dbf8909294e9b0 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 21 Jan 2025 12:27:40 -0600 Subject: [PATCH 26/39] Remove incorrect and unnecessary comments --- src/seedsigner/models/decode_qr.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 34a8ea932..1135a140f 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -989,15 +989,12 @@ def add(self, segment, qr_type=QRType.BITCOIN_ADDRESS): self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.REGTEST) elif addr_prefix == "bc1p": - # Native Segwit (single sig or multisig), mainnet self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.MAINNET) elif addr_prefix == "tb1p": - # Native Segwit (single sig or multisig), testnet self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.TESTNET) elif addr_prefix == "bcrt1p": - # Native Segwit (single sig or multisig), regtest self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.REGTEST) # Note: there is no final "else" here because the regex won't return any other matches. From 9942acd96a3a379d482e6fbc0e8975411ca47829 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 18 Jan 2025 23:07:44 -0600 Subject: [PATCH 27/39] swap icons in for SPACE and DEL --- src/seedsigner/gui/keyboard.py | 53 ++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/seedsigner/gui/keyboard.py b/src/seedsigner/gui/keyboard.py index b92b062b1..0ccc91e94 100644 --- a/src/seedsigner/gui/keyboard.py +++ b/src/seedsigner/gui/keyboard.py @@ -3,7 +3,7 @@ from typing import Tuple from gettext import gettext as _ -from seedsigner.gui.components import Fonts, GUIConstants +from seedsigner.gui.components import Fonts, GUIConstants, SeedSignerIconConstants from seedsigner.hardware.buttons import HardwareButtonsConstants @@ -30,46 +30,43 @@ class Keyboard: ENTER_RIGHT = "enter_right" REGULAR_KEY_FONT = "regular" - COMPACT_KEY_FONT = "compact" + ICON_KEY_FONT = GUIConstants.ICON_FONT_NAME__SEEDSIGNER - # TRANSLATOR_NOTE: The abbreviated label for the special key on a standard keyboard. - del_label = _("del") KEY_BACKSPACE = { "code": "DEL", - "letter": del_label, - "font": COMPACT_KEY_FONT, + "letter": SeedSignerIconConstants.DELETE, + "font": ICON_KEY_FONT, "size": 2, } - # TRANSLATOR_NOTE: The abbreviated label for the special key on a standard keyboard. - space_label = _("space") + KEY_SPACE = { "code": "SPACE", - "letter": space_label, - "font": COMPACT_KEY_FONT, + "letter": SeedSignerIconConstants.SPACE, + "font": ICON_KEY_FONT, "size": 1, } KEY_SPACE_2 = { "code": "SPACE", - "letter": space_label, - "font": COMPACT_KEY_FONT, + "letter": SeedSignerIconConstants.SPACE, + "font": ICON_KEY_FONT, "size": 2, } KEY_SPACE_3 = { "code": "SPACE", - "letter": space_label, - "font": COMPACT_KEY_FONT, + "letter": SeedSignerIconConstants.SPACE, + "font": ICON_KEY_FONT, "size": 3, } KEY_SPACE_4 = { "code": "SPACE", - "letter": space_label, - "font": COMPACT_KEY_FONT, + "letter": SeedSignerIconConstants.SPACE, + "font": ICON_KEY_FONT, "size": 4, } KEY_SPACE_5 = { "code": "SPACE", - "letter": space_label, - "font": COMPACT_KEY_FONT, + "letter": SeedSignerIconConstants.SPACE, + "font": ICON_KEY_FONT, "size": 5, } KEY_CURSOR_LEFT = { @@ -123,9 +120,11 @@ def __post_init__(self): def render_key(self): font = self.keyboard.font + text_height = self.keyboard.text_height if self.is_additional_key: - if Keyboard.ADDITIONAL_KEYS[self.code]["font"] == Keyboard.COMPACT_KEY_FONT: - font = self.keyboard.additonal_key_compact_font + if Keyboard.ADDITIONAL_KEYS[self.code]["font"] == Keyboard.ICON_KEY_FONT: + font = self.keyboard.icon_key_font + text_height = self.keyboard.icon_key_height outline_color = "#333" if not self.is_active: @@ -159,15 +158,12 @@ def render_key(self): radius=4 ) - # Fixed-width fonts will all have same height, ignoring below baseline (e.g. "Q" or "q") - (left, top, right, bottom) = font.getbbox("X", anchor="ls") - text_height = -1 * top self.keyboard.draw.text( ( self.screen_x + int(self.keyboard.key_width * self.size / 2), self.screen_y + self.keyboard.key_height - int((self.keyboard.key_height - text_height)/2) ), - _(self.letter), + self.letter, fill=font_color, font=font, anchor="ms" @@ -217,8 +213,15 @@ def __init__(self, # Set up the rendering and state params self.active_keys = list(self.charset) + self.icon_key_font = Fonts.get_font(GUIConstants.ICON_FONT_NAME__SEEDSIGNER, 26) + + # Fixed-width fonts will all have same height, ignoring below baseline (e.g. "Q" or "q") + (left, top, right, bottom) = self.font.getbbox("X", anchor="ls") + self.text_height = -1 * top + + (left, top, right, bottom) = self.icon_key_font.getbbox(SeedSignerIconConstants.DELETE + SeedSignerIconConstants.SPACE, anchor="ls") + self.icon_key_height = -1 * top - self.additonal_key_compact_font = Fonts.get_font("RobotoCondensed-Bold", 18) self.x_start = rect[0] self.y_start = rect[1] self.x_gap = 2 From 9697d5ec3a0e7cbb7c853ba9c8ca0a619ea80cf1 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 19 Jan 2025 10:02:40 -0600 Subject: [PATCH 28/39] UI tweaks, minor fixes --- src/seedsigner/gui/components.py | 20 +++++-------------- src/seedsigner/gui/keyboard.py | 8 ++++---- src/seedsigner/gui/screens/screen.py | 2 +- .../gui/screens/settings_screens.py | 13 ++++++------ src/seedsigner/gui/screens/tools_screens.py | 2 +- src/seedsigner/views/psbt_views.py | 7 +++---- src/seedsigner/views/scan_views.py | 6 ++++++ src/seedsigner/views/seed_views.py | 5 ++++- src/seedsigner/views/view.py | 14 ++++++++++--- tests/screenshot_generator/generator.py | 18 +++++++++++------ 10 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 6414b71a7..6c0f68ae3 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -217,9 +217,9 @@ class SeedSignerIconConstants: # Messaging icons INFO = "\ue912" - ERROR = "\ue913" - SUCCESS = "\ue914" - WARNING = "\ue915" + SUCCESS = "\ue913" + WARNING = "\ue914" + ERROR = "\ue915" # Informational icons ADDRESS = "\ue916" @@ -241,8 +241,9 @@ class SeedSignerIconConstants: DELETE = "\ue922" SPACE = "\ue923" + # Must be updated whenever new icons are added. See usage in `Icon` class below. MIN_VALUE = SCAN - MAX_VALUE = QRCODE + MAX_VALUE = SPACE @@ -280,17 +281,6 @@ def calc_text_centering(font: ImageFont, -def load_icon(icon_name: str, load_selected_variant: bool = False): - icon_url = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "icons", icon_name) - icon = Image.open(icon_url + ".png").convert("RGB") - if not load_selected_variant: - return icon - else: - icon_selected = Image.open(icon_url + "_selected.png").convert("RGB") - return (icon, icon_selected) - - - def load_image(image_name: str) -> Image.Image: image_url = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "img", image_name) image = Image.open(image_url).convert("RGB") diff --git a/src/seedsigner/gui/keyboard.py b/src/seedsigner/gui/keyboard.py index 0ccc91e94..bc679ec5a 100644 --- a/src/seedsigner/gui/keyboard.py +++ b/src/seedsigner/gui/keyboard.py @@ -71,14 +71,14 @@ class Keyboard: } KEY_CURSOR_LEFT = { "code": "CURSOR_LEFT", - "letter": "<", - "font": REGULAR_KEY_FONT, + "letter": SeedSignerIconConstants.CHEVRON_LEFT, + "font": ICON_KEY_FONT, "size": 1, } KEY_CURSOR_RIGHT = { "code": "CURSOR_RIGHT", - "letter": ">", - "font": REGULAR_KEY_FONT, + "letter": SeedSignerIconConstants.CHEVRON_RIGHT, + "font": ICON_KEY_FONT, "size": 1, } KEY_PREVIOUS_PAGE = { diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 637b00a9c..aa50f88ec 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -309,7 +309,7 @@ def __post_init__(self): if len(self.button_data) == 1: button_list_height = button_height else: - button_list_height = (len(self.button_data) * button_height) + (GUIConstants.COMPONENT_PADDING * (len(self.button_data) - 1)) + button_list_height = (len(self.button_data) * button_height) + (GUIConstants.LIST_ITEM_PADDING * (len(self.button_data) - 1)) if self.is_bottom_list: button_list_y = self.canvas_height - (button_list_height + GUIConstants.EDGE_PADDING) diff --git a/src/seedsigner/gui/screens/settings_screens.py b/src/seedsigner/gui/screens/settings_screens.py index 794e5b2e0..c41eadfc1 100644 --- a/src/seedsigner/gui/screens/settings_screens.py +++ b/src/seedsigner/gui/screens/settings_screens.py @@ -6,7 +6,7 @@ from typing import List from seedsigner.helpers.l10n import mark_for_translation as _mft -from seedsigner.gui.components import Button, CheckboxButton, CheckedSelectionButton, FontAwesomeIconConstants, Fonts, GUIConstants, Icon, IconButton, IconTextLine, TextArea +from seedsigner.gui.components import Button, CheckboxButton, CheckedSelectionButton, FontAwesomeIconConstants, Fonts, GUIConstants, Icon, IconButton, IconTextLine, SeedSignerIconConstants, TextArea from seedsigner.gui.screens.scan_screens import ScanScreen from seedsigner.gui.screens.screen import BaseScreen, BaseTopNavScreen, ButtonListScreen, ButtonOption from seedsigner.hardware.buttons import HardwareButtonsConstants @@ -82,7 +82,7 @@ def __post_init__(self): self.components.append(self.joystick_click_button) self.joystick_up_button = IconButton( - icon_name=FontAwesomeIconConstants.ANGLE_UP, + icon_name=SeedSignerIconConstants.CHEVRON_UP, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, width=input_button_width, height=input_button_height, @@ -93,7 +93,7 @@ def __post_init__(self): self.components.append(self.joystick_up_button) self.joystick_down_button = IconButton( - icon_name=FontAwesomeIconConstants.ANGLE_DOWN, + icon_name=SeedSignerIconConstants.CHEVRON_DOWN, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, width=input_button_width, height=input_button_height, @@ -104,9 +104,8 @@ def __post_init__(self): self.components.append(self.joystick_down_button) self.joystick_left_button = IconButton( - text=FontAwesomeIconConstants.ANGLE_LEFT, - font_name=GUIConstants.ICON_FONT_NAME__FONT_AWESOME, - font_size=GUIConstants.ICON_INLINE_FONT_SIZE, + icon_name=SeedSignerIconConstants.CHEVRON_LEFT, + icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, width=input_button_width, height=input_button_height, screen_x=dpad_center_x - input_button_width - GUIConstants.COMPONENT_PADDING, @@ -116,7 +115,7 @@ def __post_init__(self): self.components.append(self.joystick_left_button) self.joystick_right_button = IconButton( - icon_name=FontAwesomeIconConstants.ANGLE_RIGHT, + icon_name=SeedSignerIconConstants.CHEVRON_RIGHT, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, width=input_button_width, height=input_button_height, diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index c99c2b7ab..0b291c01f 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -140,7 +140,7 @@ def __post_init__(self): self.rows = 3 self.cols = 3 self.keyboard_font_name = GUIConstants.ICON_FONT_NAME__FONT_AWESOME - self.keyboard_font_size = None # Force auto-scaling to Key height + self.keyboard_font_size = 36 self.keys_charset = "".join([ FontAwesomeIconConstants.DICE_ONE, FontAwesomeIconConstants.DICE_TWO, diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index 636751d12..b4caf9380 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -460,17 +460,16 @@ def __init__(self, is_change: bool = True, is_multisig: bool = False): def run(self): if self.is_multisig: - title = _("Caution") # TRANSLATOR_NOTE: Variable is either "change" or "self-transfer". - text = _("PSBT's {} address could not be verified with your multisig wallet descriptor.").format(_("change") if self.is_change else _("self-transfer")) + text = _("PSBT's {} address could not be verified from wallet descriptor.").format(_("change") if self.is_change else _("self-transfer")) else: - title = _("Suspicious PSBT") # TRANSLATOR_NOTE: Variable is either "change" or "self-transfer". text = _("PSBT's {} address could not be generated from your seed.").format(_("change") if self.is_change else _("self-transfer")) DireWarningScreen( - title=title, + title=_("Suspicious PSBT"), status_headline=_("Address Verification Failed"), + status_icon_name=SeedSignerIconConstants.ERROR, text=text, button_data=[ButtonOption("Discard PSBT")], show_back_button=False, diff --git a/src/seedsigner/views/scan_views.py b/src/seedsigner/views/scan_views.py index aa83c6bd5..dc7d828b0 100644 --- a/src/seedsigner/views/scan_views.py +++ b/src/seedsigner/views/scan_views.py @@ -2,6 +2,7 @@ import re from gettext import gettext as _ +from seedsigner.gui.components import SeedSignerIconConstants from seedsigner.helpers.l10n import mark_for_translation as _mft from seedsigner.models.settings import SettingsConstants from seedsigner.views.view import BackStackView, ErrorView, MainMenuView, NotYetImplementedView, View, Destination @@ -163,8 +164,13 @@ def run(self): # For now, don't even try to re-do the attempted operation, just reset and # start everything over. self.controller.resume_main_flow = None + + # TODO: Refactor this warning screen into its own Screen class; the + # screenshot generator is currently manually re-creating it, but it would be + # better if a dedicated Screen could just be instantiated instead. return Destination(ErrorView, view_args=dict( title=_("Error"), + status_icon_name=SeedSignerIconConstants.WARNING, status_headline=_("Unknown QR Type"), text=_("QRCode is invalid or is a data format not yet supported."), button_text=_("Done"), diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 7a9c5121b..ae3695879 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -279,8 +279,9 @@ def __init__(self): def run(self): button_data = [self.EDIT, self.DISCARD] selected_menu_num = self.run_screen( - WarningScreen, + DireWarningScreen, title=_("Invalid Mnemonic!"), + status_icon_name=SeedSignerIconConstants.ERROR, status_headline=None, text=_("Checksum failure; not a valid seed phrase."), show_back_button=False, @@ -1206,6 +1207,7 @@ def run(self): DireWarningScreen( title=_("BIP-85 Index Error"), show_back_button=False, + status_icon_name=SeedSignerIconConstants.ERROR, status_headline=_("Invalid Child Index"), text=_("BIP-85 Child Index must be between 0 and 2^31-1."), button_data=[ButtonOption("Try Again")] @@ -1366,6 +1368,7 @@ def run(self): selected_menu_num = DireWarningScreen( title=_("Verification Error"), show_back_button=False, + status_icon_name=SeedSignerIconConstants.ERROR, status_headline=status_headline, button_data=button_data, text=text, diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index e7673eef7..62811458c 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -300,6 +300,7 @@ def run(self): class ErrorView(View): title: str = _mft("Error") show_back_button: bool = True + status_icon_name: str = SeedSignerIconConstants.ERROR status_headline: str = None text: str = None button_text: str = None @@ -309,6 +310,7 @@ def run(self): self.run_screen( WarningScreen, title=self.title, + status_icon_name=self.status_icon_name, status_headline=self.status_headline, text=self.text, button_data=[ButtonOption(self.button_text)], @@ -324,9 +326,14 @@ class NetworkMismatchErrorView(ErrorView): def __post_init__(self): from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView - self.title: str = _("Network Mismatch") - self.show_back_button: bool = False - self.button_text: str = _("Change Setting") + + # TRANSLATOR_NOTE: The network setting (mainnet/testnet/regtest) doesn't match the provided derivation path + self.title = _("Network Mismatch") + self.status_icon_name = SeedSignerIconConstants.WARNING + self.show_back_button = False + + # TRANSLATOR_NOTE: Button option to alter a setting + self.button_text = _("Change Setting") self.next_destination = Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=SettingsConstants.SETTING__NETWORK), clear_history=True) super().__post_init__() @@ -347,6 +354,7 @@ def run(self): self.run_screen( DireWarningScreen, title=_("System Error"), + status_icon_name=SeedSignerIconConstants.ERROR, status_headline=self.error[0], text=self.error[1] + "\n" + self.error[2], allow_text_overflow=True, # Fit what we can, let the rest go off the edges diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index 114f5c68c..c82ae5ad0 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -24,6 +24,7 @@ patch('PIL.ImageFont.core.HAVE_RAQM', False).start() from seedsigner.controller import Controller +from seedsigner.gui.components import SeedSignerIconConstants from seedsigner.gui.renderer import Renderer from seedsigner.gui.screens.seed_screens import SeedAddPassphraseScreen from seedsigner.gui.toast import BaseToastOverlayManagerThread, RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread @@ -359,12 +360,17 @@ def PSBTOpReturnView_raw_hex_data_cb_before(): ScreenshotConfig(UnhandledExceptionView, dict(error=["IndexError", "line 1, in some_buggy_code.py", "list index out of range"])), ScreenshotConfig(NetworkMismatchErrorView, dict(derivation_path="m/84'/1'/0'")), ScreenshotConfig(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)), - ScreenshotConfig(ErrorView, dict( - title="Error", - status_headline="Unknown QR Type", - text="QRCode is invalid or is a data format not yet supported.", - button_text="Back", - )), + ScreenshotConfig( + ErrorView, + dict( + title="Error", + status_icon_name=SeedSignerIconConstants.WARNING, + status_headline="Unknown QR Type", + text="QRCode is invalid or is a data format not yet supported.", + button_text="Back", + ), + screenshot_name="ScanView__UnknownQRType" + ), ] } From 5e16c984b2132a22fdf1a9c4027887ea92e66a7b Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 19 Jan 2025 10:22:05 -0600 Subject: [PATCH 29/39] Associated changes to translation source strings --- l10n/messages.pot | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/l10n/messages.pot b/l10n/messages.pot index 84ffd5bcc..07abe61b4 100644 --- a/l10n/messages.pot +++ b/l10n/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: seedsigner 0.8.5-rc1\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-01-15 10:45-0600\n" +"POT-Creation-Date: 2025-01-19 10:20-0600\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -36,16 +36,6 @@ msgstr "" msgid "sats" msgstr "" -#. The abbreviated label for the special key on a standard keyboard. -#: src/seedsigner/gui/keyboard.py -msgid "del" -msgstr "" - -#. The abbreviated label for the special key on a standard keyboard. -#: src/seedsigner/gui/keyboard.py -msgid "space" -msgstr "" - #: src/seedsigner/gui/toast.py msgid "" "You can remove\n" @@ -215,7 +205,7 @@ msgstr "" msgid "OK" msgstr "" -#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py +#: src/seedsigner/gui/screens/screen.py msgid "Caution" msgstr "" @@ -901,18 +891,16 @@ msgstr "" #. Variable is either "change" or "self-transfer". #: src/seedsigner/views/psbt_views.py -msgid "" -"PSBT's {} address could not be verified with your multisig wallet " -"descriptor." +msgid "PSBT's {} address could not be verified from wallet descriptor." msgstr "" +#. Variable is either "change" or "self-transfer". #: src/seedsigner/views/psbt_views.py -msgid "Suspicious PSBT" +msgid "PSBT's {} address could not be generated from your seed." msgstr "" -#. Variable is either "change" or "self-transfer". #: src/seedsigner/views/psbt_views.py -msgid "PSBT's {} address could not be generated from your seed." +msgid "Suspicious PSBT" msgstr "" #: src/seedsigner/views/psbt_views.py @@ -1459,10 +1447,13 @@ msgstr "" msgid "Back to Main Menu" msgstr "" +#. The network setting (mainnet/testnet/regtest) doesn't match the provided +#. derivation path #: src/seedsigner/views/view.py msgid "Network Mismatch" msgstr "" +#. Button option to alter a setting #: src/seedsigner/views/view.py msgid "Change Setting" msgstr "" From 7edcd335952fb4453c3f9029381510bd938a2616 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 23 Jan 2025 09:42:35 -0600 Subject: [PATCH 30/39] Improve DireWarning vs Error usage and color consistency --- src/seedsigner/gui/screens/screen.py | 19 ++++++++++++++++++- src/seedsigner/views/psbt_views.py | 1 - src/seedsigner/views/view.py | 10 +++++----- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index aa50f88ec..47f6645ee 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -1013,9 +1013,12 @@ def __post_init__(self): @dataclass class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen): + """ + Exclamation point icon + yellow WARNING color + """ title: str = _mft("Caution") status_icon_name: str = SeedSignerIconConstants.WARNING - status_color: str = "yellow" + status_color: str = GUIConstants.WARNING_COLOR status_headline: str = _mft("Privacy Leak!") # The colored text under the alert icon button_data: list = field(default_factory=lambda: [ButtonOption("I Understand")]) @@ -1023,11 +1026,25 @@ class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen): @dataclass class DireWarningScreen(WarningScreen): + """ + Exclamation point icon + orange DIRE_WARNING color + """ status_headline: str = _mft("Classified Info!") # The colored text under the alert icon status_color: str = GUIConstants.DIRE_WARNING_COLOR +@dataclass +class ErrorScreen(WarningScreen): + """ + X icon + red ERROR color + """ + title: str = _mft("Error") + status_icon_name: str = SeedSignerIconConstants.ERROR + status_color: str = GUIConstants.ERROR_COLOR + + + @dataclass class ResetScreen(BaseTopNavScreen): def __post_init__(self): diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index b4caf9380..b39c93677 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -469,7 +469,6 @@ def run(self): DireWarningScreen( title=_("Suspicious PSBT"), status_headline=_("Address Verification Failed"), - status_icon_name=SeedSignerIconConstants.ERROR, text=text, button_data=[ButtonOption("Discard PSBT")], show_back_button=False, diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 62811458c..66c3a2293 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -5,7 +5,7 @@ from seedsigner.helpers.l10n import mark_for_translation as _mft from seedsigner.gui.components import SeedSignerIconConstants from seedsigner.gui.screens import RET_CODE__POWER_BUTTON, RET_CODE__BACK_BUTTON -from seedsigner.gui.screens.screen import BaseScreen, ButtonOption, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen +from seedsigner.gui.screens.screen import BaseScreen, ButtonOption, LargeButtonScreen, WarningScreen, ErrorScreen from seedsigner.models.settings import Settings, SettingsConstants from seedsigner.models.settings_definition import SettingsDefinition from seedsigner.models.threads import BaseThread @@ -245,6 +245,7 @@ def run(self): class RestartView(View): def run(self): + from seedsigner.gui.screens.screen import ResetScreen thread = RestartView.DoResetThread() thread.start() self.run_screen(ResetScreen) @@ -270,6 +271,7 @@ def run(self): class PowerOffView(View): def run(self): + from seedsigner.gui.screens.screen import PowerOffNotRequiredScreen self.run_screen(PowerOffNotRequiredScreen) return Destination(BackStackView) @@ -308,7 +310,7 @@ class ErrorView(View): def run(self): self.run_screen( - WarningScreen, + ErrorScreen, title=self.title, status_icon_name=self.status_icon_name, status_headline=self.status_headline, @@ -349,12 +351,10 @@ def __post_init__(self): class UnhandledExceptionView(View): error: list[str] - def run(self): self.run_screen( - DireWarningScreen, + ErrorScreen, title=_("System Error"), - status_icon_name=SeedSignerIconConstants.ERROR, status_headline=self.error[0], text=self.error[1] + "\n" + self.error[2], allow_text_overflow=True, # Fit what we can, let the rest go off the edges From c0fe6766b22497d9adda3efafbe25bfeb855a6c8 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 23 Jan 2025 09:43:41 -0600 Subject: [PATCH 31/39] add `ScanInvalidQRTypeView` --- src/seedsigner/views/scan_views.py | 35 ++++++++++++++++--------- tests/screenshot_generator/generator.py | 19 +++----------- tests/test_flows_seed.py | 2 +- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/seedsigner/views/scan_views.py b/src/seedsigner/views/scan_views.py index dc7d828b0..54097c3e5 100644 --- a/src/seedsigner/views/scan_views.py +++ b/src/seedsigner/views/scan_views.py @@ -2,10 +2,10 @@ import re from gettext import gettext as _ -from seedsigner.gui.components import SeedSignerIconConstants from seedsigner.helpers.l10n import mark_for_translation as _mft from seedsigner.models.settings import SettingsConstants from seedsigner.views.view import BackStackView, ErrorView, MainMenuView, NotYetImplementedView, View, Destination +from seedsigner.gui.screens.screen import ButtonOption logger = logging.getLogger(__name__) @@ -164,18 +164,7 @@ def run(self): # For now, don't even try to re-do the attempted operation, just reset and # start everything over. self.controller.resume_main_flow = None - - # TODO: Refactor this warning screen into its own Screen class; the - # screenshot generator is currently manually re-creating it, but it would be - # better if a dedicated Screen could just be instantiated instead. - return Destination(ErrorView, view_args=dict( - title=_("Error"), - status_icon_name=SeedSignerIconConstants.WARNING, - status_headline=_("Unknown QR Type"), - text=_("QRCode is invalid or is a data format not yet supported."), - button_text=_("Done"), - next_destination=Destination(MainMenuView, clear_history=True), - )) + return Destination(ScanInvalidQRTypeView) return Destination(MainMenuView) @@ -218,3 +207,23 @@ class ScanAddressView(ScanView): @property def is_valid_qr_type(self): return self.decoder.is_address + + + +class ScanInvalidQRTypeView(View): + def run(self): + from seedsigner.gui.screens import WarningScreen + + # TODO: This screen says "Error" but is intentionally using the WarningScreen in + # order to avoid the perception that something is broken on our end. This should + # either change to use the red ErrorScreen or the "Error" title should be + # changed to something softer. + self.run_screen( + WarningScreen, + title=_("Error"), + status_headline=_("Unknown QR Type"), + text=_("QRCode is invalid or is a data format not yet supported."), + button_data=[ButtonOption("Done")], + ) + + return Destination(MainMenuView, clear_history=True) diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index c82ae5ad0..ca697eb9d 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -24,10 +24,9 @@ patch('PIL.ImageFont.core.HAVE_RAQM', False).start() from seedsigner.controller import Controller -from seedsigner.gui.components import SeedSignerIconConstants from seedsigner.gui.renderer import Renderer from seedsigner.gui.screens.seed_screens import SeedAddPassphraseScreen -from seedsigner.gui.toast import BaseToastOverlayManagerThread, RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread +from seedsigner.gui.toast import RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread from seedsigner.hardware.microsd import MicroSD from seedsigner.helpers import embit_utils from seedsigner.models.decode_qr import DecodeQR @@ -37,9 +36,9 @@ from seedsigner.models.settings import Settings from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition from seedsigner.views import (MainMenuView, PowerOptionsView, RestartView, NotYetImplementedView, UnhandledExceptionView, - psbt_views, seed_views, settings_views, tools_views) + psbt_views, seed_views, settings_views, tools_views, scan_views) from seedsigner.views.screensaver import OpeningSplashView -from seedsigner.views.view import ErrorView, NetworkMismatchErrorView, OptionDisabledView, PowerOffView, View +from seedsigner.views.view import NetworkMismatchErrorView, OptionDisabledView, PowerOffView from .utils import ScreenshotComplete, ScreenshotConfig, ScreenshotRenderer @@ -360,17 +359,7 @@ def PSBTOpReturnView_raw_hex_data_cb_before(): ScreenshotConfig(UnhandledExceptionView, dict(error=["IndexError", "line 1, in some_buggy_code.py", "list index out of range"])), ScreenshotConfig(NetworkMismatchErrorView, dict(derivation_path="m/84'/1'/0'")), ScreenshotConfig(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)), - ScreenshotConfig( - ErrorView, - dict( - title="Error", - status_icon_name=SeedSignerIconConstants.WARNING, - status_headline="Unknown QR Type", - text="QRCode is invalid or is a data format not yet supported.", - button_text="Back", - ), - screenshot_name="ScanView__UnknownQRType" - ), + ScreenshotConfig(scan_views.ScanInvalidQRTypeView) ] } diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index 490e9f15b..2e63e702b 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -646,7 +646,7 @@ def load_invalid_signmessage_qr(view: scan_views.ScanView): FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.SIGN_MESSAGE), FlowStep(scan_views.ScanView, before_run=load_invalid_signmessage_qr), # simulate read message QR; ret val is ignored - FlowStep(ErrorView), + FlowStep(scan_views.ScanInvalidQRTypeView), FlowStep(MainMenuView), ]) From abc96e37fe6b878002c4159643497d3e3a0a0850 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 23 Jan 2025 10:06:05 -0600 Subject: [PATCH 32/39] version up all deprecated CI actions --- .github/workflows/build.yml | 12 ++++++------ .github/workflows/tests.yml | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a0649dc73..3c2de8c8c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: target: [ "pi0", "pi2", "pi02w", "pi4" ] steps: - name: checkout seedsigner-os - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: "seedsigner/seedsigner-os" # use the os-ref input parameter in case of workflow_dispatch or default to main in case of cron triggers @@ -42,7 +42,7 @@ jobs: fetch-depth: 0 - name: checkout source - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # ref defaults to repo default-branch=dev (cron) or SHA of event (workflow_dispatch) path: "seedsigner-os/opt/rootfs-overlay/opt" @@ -78,7 +78,7 @@ jobs: ls -la src - name: restore build cache - uses: actions/cache@v3 + uses: actions/cache@v4 # Caching reduces the build time to ~50% (currently: ~30 mins instead of ~1 hour, # while consuming ~850 MB storage space). with: @@ -113,7 +113,7 @@ jobs: ls -la seedsigner-os/images - name: upload images - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: seedsigner_os_images path: "seedsigner-os/images/*.img" @@ -127,7 +127,7 @@ jobs: needs: build steps: - name: download images - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: seedsigner_os_images path: images @@ -148,7 +148,7 @@ jobs: sha256sum *.img > seedsigner_os.${{ env.source_hash }}.sha256 - name: upload checksums - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: seedsigner_os_images path: "images/*.sha256" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9fba7af51..20e530ce9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,12 +26,12 @@ jobs: python-version: ["3.10", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # Needs to also pull the seedsigner-translations repo submodules: recursive - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -63,7 +63,7 @@ jobs: - name: Coverage report run: coverage report - name: Archive CI Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ci-artifacts path: artifacts/** From 05d0644529968b5d65b63f2cda2ae1f73f6a0eba Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 23 Jan 2025 10:20:30 -0600 Subject: [PATCH 33/39] bugfix: set distinct artifact upload names --- .github/workflows/build.yml | 4 ++-- .github/workflows/tests.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3c2de8c8c..4cea156b3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -115,7 +115,7 @@ jobs: - name: upload images uses: actions/upload-artifact@v4 with: - name: seedsigner_os_images + name: seedsigner_os_images-${{ matrix.target }} path: "seedsigner-os/images/*.img" if-no-files-found: error # maximum 90 days retention @@ -150,7 +150,7 @@ jobs: - name: upload checksums uses: actions/upload-artifact@v4 with: - name: seedsigner_os_images + name: seedsigner_os_hashes-${{ matrix.target }} path: "images/*.sha256" if-no-files-found: error # maximum 90 days retention diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 20e530ce9..9be339c08 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -65,7 +65,7 @@ jobs: - name: Archive CI Artifacts uses: actions/upload-artifact@v4 with: - name: ci-artifacts + name: ci-artifacts-${{ matrix.python-version }} path: artifacts/** retention-days: 10 # Upload also when tests fail. The workflow result (red/green) will From 11965d4b7fa91ecef143fd8bbda5ebee78d1c3b1 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 23 Jan 2025 10:32:53 -0600 Subject: [PATCH 34/39] bugfix: wildcard syntax, download artifact name --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4cea156b3..7af6a7e3e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -101,7 +101,7 @@ jobs: - name: rename image run: | cd seedsigner-os/images - mv seedsigner_os*.img seedsigner_os.${{ env.img_version }}.${{ matrix.target }}.img + mv *.img seedsigner_os.${{ env.img_version }}.${{ matrix.target }}.img - name: print sha256sum run: | @@ -129,7 +129,7 @@ jobs: - name: download images uses: actions/download-artifact@v4 with: - name: seedsigner_os_images + name: seedsigner_os_images-${{ matrix.target }} path: images - name: list images From 09007c03ec359b49b89919939ab03e05bacd573a Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 23 Jan 2025 10:48:09 -0600 Subject: [PATCH 35/39] bugfix: revert cache to v3 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7af6a7e3e..26ecf7423 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -78,7 +78,7 @@ jobs: ls -la src - name: restore build cache - uses: actions/cache@v4 + uses: actions/cache@v3 # Caching reduces the build time to ~50% (currently: ~30 mins instead of ~1 hour, # while consuming ~850 MB storage space). with: From b526d86a63d625e39b45fa5a9b08c583518a280f Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 23 Jan 2025 12:20:01 -0600 Subject: [PATCH 36/39] stick w/cache@v4, but try w/out `--no-clean` --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26ecf7423..e17d03ede 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -78,7 +78,7 @@ jobs: ls -la src - name: restore build cache - uses: actions/cache@v3 + uses: actions/cache@v4 # Caching reduces the build time to ~50% (currently: ~30 mins instead of ~1 hour, # while consuming ~850 MB storage space). with: @@ -92,7 +92,7 @@ jobs: - name: build run: | cd seedsigner-os/opt - ./build.sh --${{ matrix.target }} --skip-repo --no-clean + ./build.sh --${{ matrix.target }} --skip-repo - name: list image (before rename) run: | From 3966675197ca811bbec2dcb340d117b3838c0eb8 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 23 Jan 2025 12:30:28 -0600 Subject: [PATCH 37/39] Try disabling cache --- .github/workflows/build.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e17d03ede..43ce651c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,17 +77,17 @@ jobs: ls -la . ls -la src - - name: restore build cache - uses: actions/cache@v4 - # Caching reduces the build time to ~50% (currently: ~30 mins instead of ~1 hour, - # while consuming ~850 MB storage space). - with: - path: | - ~/.buildroot-ccache/ - seedsigner-os/buildroot_dl - key: build-cache-${{ matrix.target }}-${{ env.builder_hash }} - restore-keys: | - build-cache-${{ matrix.target }}- + # - name: restore build cache + # uses: actions/cache@v4 + # # Caching reduces the build time to ~50% (currently: ~30 mins instead of ~1 hour, + # # while consuming ~850 MB storage space). + # with: + # path: | + # ~/.buildroot-ccache/ + # seedsigner-os/buildroot_dl + # key: build-cache-${{ matrix.target }}-${{ env.builder_hash }} + # restore-keys: | + # build-cache-${{ matrix.target }}- - name: build run: | From 3e4deb4bda31010d7963ac6ada6f3b5f67f53184 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 23 Jan 2025 12:45:19 -0600 Subject: [PATCH 38/39] Remove build.yml from PR --- .github/workflows/build.yml | 42 ++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 43ce651c8..a0649dc73 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: target: [ "pi0", "pi2", "pi02w", "pi4" ] steps: - name: checkout seedsigner-os - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: repository: "seedsigner/seedsigner-os" # use the os-ref input parameter in case of workflow_dispatch or default to main in case of cron triggers @@ -42,7 +42,7 @@ jobs: fetch-depth: 0 - name: checkout source - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: # ref defaults to repo default-branch=dev (cron) or SHA of event (workflow_dispatch) path: "seedsigner-os/opt/rootfs-overlay/opt" @@ -77,22 +77,22 @@ jobs: ls -la . ls -la src - # - name: restore build cache - # uses: actions/cache@v4 - # # Caching reduces the build time to ~50% (currently: ~30 mins instead of ~1 hour, - # # while consuming ~850 MB storage space). - # with: - # path: | - # ~/.buildroot-ccache/ - # seedsigner-os/buildroot_dl - # key: build-cache-${{ matrix.target }}-${{ env.builder_hash }} - # restore-keys: | - # build-cache-${{ matrix.target }}- + - name: restore build cache + uses: actions/cache@v3 + # Caching reduces the build time to ~50% (currently: ~30 mins instead of ~1 hour, + # while consuming ~850 MB storage space). + with: + path: | + ~/.buildroot-ccache/ + seedsigner-os/buildroot_dl + key: build-cache-${{ matrix.target }}-${{ env.builder_hash }} + restore-keys: | + build-cache-${{ matrix.target }}- - name: build run: | cd seedsigner-os/opt - ./build.sh --${{ matrix.target }} --skip-repo + ./build.sh --${{ matrix.target }} --skip-repo --no-clean - name: list image (before rename) run: | @@ -101,7 +101,7 @@ jobs: - name: rename image run: | cd seedsigner-os/images - mv *.img seedsigner_os.${{ env.img_version }}.${{ matrix.target }}.img + mv seedsigner_os*.img seedsigner_os.${{ env.img_version }}.${{ matrix.target }}.img - name: print sha256sum run: | @@ -113,9 +113,9 @@ jobs: ls -la seedsigner-os/images - name: upload images - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: - name: seedsigner_os_images-${{ matrix.target }} + name: seedsigner_os_images path: "seedsigner-os/images/*.img" if-no-files-found: error # maximum 90 days retention @@ -127,9 +127,9 @@ jobs: needs: build steps: - name: download images - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v3 with: - name: seedsigner_os_images-${{ matrix.target }} + name: seedsigner_os_images path: images - name: list images @@ -148,9 +148,9 @@ jobs: sha256sum *.img > seedsigner_os.${{ env.source_hash }}.sha256 - name: upload checksums - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: - name: seedsigner_os_hashes-${{ matrix.target }} + name: seedsigner_os_images path: "images/*.sha256" if-no-files-found: error # maximum 90 days retention From 18c13daca8ca4dab85bec9828ea6c37977c33e0f Mon Sep 17 00:00:00 2001 From: Daniel Bast <2790401+dbast@users.noreply.github.com> Date: Thu, 23 Jan 2025 21:50:26 +0100 Subject: [PATCH 39/39] Fix build: Building in Docker Container + update action versions --- .github/workflows/build.yml | 40 ++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a0649dc73..87a407fe3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: target: [ "pi0", "pi2", "pi02w", "pi4" ] steps: - name: checkout seedsigner-os - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: "seedsigner/seedsigner-os" # use the os-ref input parameter in case of workflow_dispatch or default to main in case of cron triggers @@ -42,7 +42,7 @@ jobs: fetch-depth: 0 - name: checkout source - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # ref defaults to repo default-branch=dev (cron) or SHA of event (workflow_dispatch) path: "seedsigner-os/opt/rootfs-overlay/opt" @@ -78,7 +78,7 @@ jobs: ls -la src - name: restore build cache - uses: actions/cache@v3 + uses: actions/cache@v4 # Caching reduces the build time to ~50% (currently: ~30 mins instead of ~1 hour, # while consuming ~850 MB storage space). with: @@ -89,10 +89,25 @@ jobs: restore-keys: | build-cache-${{ matrix.target }}- + - name: Create build container + run: | + cd seedsigner-os + docker build -t seedsigner-os-build . + - name: build run: | - cd seedsigner-os/opt - ./build.sh --${{ matrix.target }} --skip-repo --no-clean + mkdir -p \ + ~/.buildroot-ccache \ + seedsigner-os/buildroot_dl + docker run \ + --rm \ + -v "$(pwd)/seedsigner-os/opt:/opt" \ + -v "$(pwd)/seedsigner-os/images:/images" \ + -v "$(pwd)/seedsigner-os/buildroot_dl:/buildroot_dl" \ + -v "${HOME}/.buildroot-ccache:/root/.buildroot-ccache" \ + seedsigner-os-build \ + --${{ matrix.target }} --skip-repo --no-clean + sudo chown -R $USER:$USER seedsigner-os/images seedsigner-os/buildroot_dl ~/.buildroot-ccache/ - name: list image (before rename) run: | @@ -113,9 +128,9 @@ jobs: ls -la seedsigner-os/images - name: upload images - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: seedsigner_os_images + name: seedsigner_os_images-${{ matrix.target }} path: "seedsigner-os/images/*.img" if-no-files-found: error # maximum 90 days retention @@ -127,14 +142,13 @@ jobs: needs: build steps: - name: download images - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: seedsigner_os_images path: images - name: list images run: | - ls -la images + ls -lRa images - name: get seedsigner latest commit hash id: get-seedsigner-hash @@ -145,12 +159,14 @@ jobs: - name: write sha256sum run: | cd images + # each downloaded image is in its own subfolder + find . -name "*.img" -exec mv {} . \; sha256sum *.img > seedsigner_os.${{ env.source_hash }}.sha256 - name: upload checksums - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: seedsigner_os_images + name: seedsigner_os_images_sha256 path: "images/*.sha256" if-no-files-found: error # maximum 90 days retention