diff --git a/l10n/messages.pot b/l10n/messages.pot index 080ca8a1..b87cb99b 100644 --- a/l10n/messages.pot +++ b/l10n/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: seedsigner 0.8.6\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-11-21 14:39-0600\n" +"POT-Creation-Date: 2025-12-14 05:46-0600\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -597,6 +597,10 @@ msgstr "" msgid "Next {}" msgstr "" +#: src/seedsigner/hardware/camera.py +msgid "Camera error. Check camera connections." +msgstr "" + #: src/seedsigner/models/settings_definition.py msgid "Enabled" msgstr "" @@ -613,6 +617,21 @@ msgstr "" msgid "Required" msgstr "" +#. QR code format option; "default" = this is the format most wallets use +#: src/seedsigner/models/settings_definition.py +msgid "Animated (default)" +msgstr "" + +#. QR code format option (static = single frame, not animated) +#: src/seedsigner/models/settings_definition.py +msgid "Static" +msgstr "" + +#. QR code format option: old format that Specter Desktop used to use +#: src/seedsigner/models/settings_definition.py +msgid "Specter legacy" +msgstr "" + #: src/seedsigner/models/settings_definition.py msgid "BTC" msgstr "" @@ -718,10 +737,6 @@ msgstr "" msgid "Persistent settings" msgstr "" -#: src/seedsigner/models/settings_definition.py -msgid "Coordinator software" -msgstr "" - #: src/seedsigner/models/settings_definition.py msgid "Denomination display" msgstr "" @@ -746,6 +761,10 @@ msgstr "" msgid "Script types" msgstr "" +#: src/seedsigner/models/settings_definition.py +msgid "Xpub QR format" +msgstr "" + #: src/seedsigner/models/settings_definition.py msgid "Show xpub details" msgstr "" @@ -853,7 +872,7 @@ msgid "" "addresses." msgstr "" -#: src/seedsigner/views/psbt_views.py +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/view.py msgid "Continue" msgstr "" @@ -1003,9 +1022,8 @@ msgstr "" msgid "QRCode is invalid or is a data format not yet supported." msgstr "" -#: src/seedsigner/views/scan_views.py src/seedsigner/views/seed_views.py -#: src/seedsigner/views/view.py -msgid "Done" +#: src/seedsigner/views/scan_views.py src/seedsigner/views/view.py +msgid "Back to Main Menu" msgstr "" #. This is on the opening splash screen, displayed above the HRF logo @@ -1074,6 +1092,10 @@ msgstr "" msgid "Checksum failure; not a valid seed phrase." msgstr "" +#: src/seedsigner/views/seed_views.py +msgid "Done" +msgstr "" + #: src/seedsigner/views/seed_views.py msgid "Edit passphrase" msgstr "" @@ -1159,6 +1181,10 @@ msgstr "" msgid "Export Xpub" msgstr "" +#: src/seedsigner/views/seed_views.py +msgid "Xpub QR Format" +msgstr "" + #: src/seedsigner/views/seed_views.py msgid "Xpub can be used to view all future transactions." msgstr "" @@ -1321,10 +1347,6 @@ msgstr "" msgid "Cancel" msgstr "" -#: src/seedsigner/views/seed_views.py -msgid "Can't validate a single sig addr without specifying a seed" -msgstr "" - #: src/seedsigner/views/seed_views.py msgid "Return to transaction" msgstr "" @@ -1518,6 +1540,18 @@ msgstr "" msgid "System Error" msgstr "" +#: src/seedsigner/views/view.py +msgid "Hardware Error" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Cannot access camera" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Disconnect power and check for a loose camera connection." +msgstr "" + #: src/seedsigner/views/view.py msgid "Update setting" msgstr "" @@ -1532,3 +1566,13 @@ msgstr "" msgid "Option Disabled" msgstr "" +#: src/seedsigner/views/view.py +msgid "Action Required" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "" +"You must remove the\n" +"MicroSD card to continue." +msgstr "" + diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 3702f81b..9c82c17d 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -52,7 +52,7 @@ class GUIConstants: "default": "OpenSans-Regular", SettingsConstants.LOCALE__ARABIC: "NotoSansAR-Regular", SettingsConstants.LOCALE__CHINESE_SIMPLIFIED: "NotoSansSC-Regular", - # SettingsConstants.LOCALE__CHINESE_TRADITIONAL: "NotoSansTC-Regular", + SettingsConstants.LOCALE__HINDI: "NotoSansDevanagari-Regular", SettingsConstants.LOCALE__JAPANESE: "NotoSansJP-Regular", SettingsConstants.LOCALE__KOREAN: "NotoSansKR-Regular", SettingsConstants.LOCALE__PERSIAN: "NotoSansAR-Regular", # Uses the Arabic font @@ -63,9 +63,10 @@ class GUIConstants: TOP_NAV_TITLE_FONT_NAME["default"] = "OpenSans-SemiBold" TOP_NAV_TITLE_FONT_SIZE = { "default": 20, + SettingsConstants.LOCALE__CHINESE_SIMPLIFIED: 23, # Some chars won't render below 23px + SettingsConstants.LOCALE__HINDI: 22, SettingsConstants.LOCALE__JAPANESE: 22, # Titles won't render below 22px SettingsConstants.LOCALE__KOREAN: 23, # Titles won't render below 23px - SettingsConstants.LOCALE__CHINESE_SIMPLIFIED: 23, # Some chars won't render below 23px } TOP_NAV_HEIGHT = 48 TOP_NAV_BUTTON_SIZE = 32 @@ -73,9 +74,10 @@ class GUIConstants: BODY_FONT_NAME = BASE_LOCALE_FONTS.copy() BODY_FONT_SIZE = { "default": 17, + SettingsConstants.LOCALE__CHINESE_SIMPLIFIED: 18, + SettingsConstants.LOCALE__HINDI: 18, SettingsConstants.LOCALE__JAPANESE: 18, SettingsConstants.LOCALE__KOREAN: 18, - SettingsConstants.LOCALE__CHINESE_SIMPLIFIED: 18, } BODY_FONT_MAX_SIZE = TOP_NAV_TITLE_FONT_SIZE["default"] BODY_FONT_MIN_SIZE = 15 @@ -95,6 +97,7 @@ class GUIConstants: "default": 18, "ar": 18, "fa": 18, + SettingsConstants.LOCALE__HINDI: 20, SettingsConstants.LOCALE__JAPANESE: 20, SettingsConstants.LOCALE__KOREAN: 20, SettingsConstants.LOCALE__CHINESE_SIMPLIFIED: 20, @@ -389,7 +392,13 @@ class TextArea(BaseComponent): # Guaranteed to be a single line of text, possibly wider than self.width self.text_lines = [{"text": self.text, "text_width": full_text_width}] self.text_width = full_text_width - if self.text_width > self.visible_width: + + # Technically, the math says that we should enable scrolling for as little as + # 1px beyond the available width, but it's not worth having the text twitch + # back and forth by such a small amount. + min_scrollable_diff = 1 + + if self.text_width > self.visible_width + min_scrollable_diff: # We'll have to left justify the text and scroll it (if scrolling is enabled, # otherwise it'll just run off the right edge). self.is_text_centered = False @@ -566,7 +575,7 @@ class TextArea(BaseComponent): self.horizontal_scroll_position = 0 self.scroll_increment_sign = 1 # flip to negative to scroll text to the right - self.renderer = Renderer.get_instance() + self.renderer = Renderer.get_instance() def stop_scrolling(self): @@ -587,45 +596,57 @@ class TextArea(BaseComponent): readability. 45px/sec is better but still perceptually a bit stuttery. """ max_scroll = self.rendered_text_img.width - self.visible_width + last_render_time = None + + # The scrolling pauses at the start and end of the text line. These vars track + # when we started holding and how long we should hold for. + hold_started_at = None + cur_hold_duration = None while self.keep_running: if not self.scrolling_active: time.sleep(0.1) continue - with self.renderer.lock: - if not self.scrolling_active: - # We were stopped while waiting for the lock + if cur_hold_duration is not None: + # We're currently holding; see if we've held long enough + hold_time_elapsed = time.time() - hold_started_at + if hold_time_elapsed < cur_hold_duration: + # Still have to hold longer; skip scrolling logic + time.sleep(0.1) + continue + else: + # We've held long enough; reset the vars and resume scrolling + hold_started_at = None + cur_hold_duration = None + + else: + # We're not holding, but if we've reached either end, we need to start + # holding. + if self.horizontal_scroll_position == 0: + # Pause on initial (left-justified) position... + hold_started_at = time.time() + cur_hold_duration = self.begin_hold_secs + + # Next scroll direction will be left + self.scroll_increment_sign = 1 + + # Don't count those pause seconds + last_render_time = None continue - img = self.rendered_text_img.crop((self.horizontal_scroll_position, 0, self.horizontal_scroll_position + self.visible_width, self.rendered_text_img.height)) - self.renderer.canvas.paste(img, (self.screen_x, self.screen_y - self.scroll_y)) - self.renderer.show_image() + elif self.horizontal_scroll_position == max_scroll: + # ...and slight pause at end of scroll + hold_started_at = time.time() + cur_hold_duration = self.end_hold_secs - if self.horizontal_scroll_position == 0: - # Pause on initial (left-justified) position... - time.sleep(self.begin_hold_secs) + # Don't count those pause seconds + last_render_time = None - # Don't count those pause seconds - last_render_time = None + # Scroll will be to the right + self.scroll_increment_sign = -1 + continue - # Scroll the text left - self.scroll_increment_sign = 1 - - elif self.horizontal_scroll_position == max_scroll: - # ...and slight pause at end of scroll - time.sleep(self.end_hold_secs) - - # Don't count those pause seconds - last_render_time = None - - # Scroll the text right - self.scroll_increment_sign = -1 - - else: - # No need to CPU limit when running in its own thread? - time.sleep(0.02) - next_render_time = time.time() if not last_render_time: @@ -633,18 +654,39 @@ class TextArea(BaseComponent): # "get off zero" for the real increment calc logic to kick in. scroll_position_increment = 1 * self.scroll_increment_sign else: + # Calculate how far to scroll based on time elapsed since last render scroll_position_increment = int(self.horizontal_scroll_speed * (next_render_time - last_render_time) * self.scroll_increment_sign) + # Only render an update if we're going to move at least 1px if abs(scroll_position_increment) > 0: - self.horizontal_scroll_position += scroll_position_increment - self.horizontal_scroll_position = max(0, min(self.horizontal_scroll_position, max_scroll)) + # max: Don't over-scroll when returning to the left edge (0) + # min: Don't over-scroll when revealing the right edge (max_scroll) + self.horizontal_scroll_position = max( + 0, + min(self.horizontal_scroll_position + scroll_position_increment, max_scroll) + ) + + # Render the scroll update + with self.renderer.lock: + if not self.scrolling_active: + # We were stopped while waiting for the lock + continue + + # The pre-rendered text img slides within a cropping window + img = self.rendered_text_img.crop((self.horizontal_scroll_position, 0, self.horizontal_scroll_position + self.visible_width, self.rendered_text_img.height)) + self.renderer.canvas.paste(img, (self.screen_x, self.screen_y - self.scroll_y)) + self.renderer.show_image() last_render_time = next_render_time + else: - # Wait to accumulate more time before scrolling + # Wait to accumulate more time so we can scroll at least 1px pass + # Free up the processor for a bit each loop + time.sleep(0.02) + def render(self): """ Even if we need to animate for scrolling, all instances should explicitly render diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index c54fa576..44f89436 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -62,7 +62,6 @@ class ScanScreen(BaseScreen): self.camera.start_video_stream_mode(resolution=self.resolution, framerate=self.framerate, format="rgb") self.frames_decode_status = ThreadsafeCounter() - self.frames_decoded_counter = ThreadsafeCounter() self.threads.append(ScanScreen.LivePreviewThread( decoder=self.decoder, @@ -70,12 +69,11 @@ class ScanScreen(BaseScreen): instructions_text=self.instructions_text, render_rect=self.render_rect, frame_decode_status=self.frames_decode_status, - frames_decoded_counter=self.frames_decoded_counter, )) class LivePreviewThread(BaseThread): - def __init__(self, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: tuple[int,int,int,int], frame_decode_status: ThreadsafeCounter, frames_decoded_counter: ThreadsafeCounter): + def __init__(self, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: tuple[int,int,int,int], frame_decode_status: ThreadsafeCounter): from seedsigner.hardware.camera import Camera self.camera = Camera.get_instance() @@ -87,8 +85,6 @@ class ScanScreen(BaseScreen): else: self.render_rect = (0, 0, self.renderer.canvas_width, self.renderer.canvas_height) self.frame_decode_status = frame_decode_status - self.frames_decoded_counter = frames_decoded_counter - self.last_frame_decoded_count = self.frames_decoded_counter.cur_count self.render_width = self.render_rect[2] - self.render_rect[0] self.render_height = self.render_rect[3] - self.render_rect[1] self.decoder_fps = "0.0" @@ -97,39 +93,23 @@ class ScanScreen(BaseScreen): def run(self): - from timeit import default_timer as timer - instructions_font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_button_font_size()) # pre-calculate how big the animated QR percent display can be - left, top, right, bottom = instructions_font.getbbox("100%") + (left, top, right, bottom) = instructions_font.getbbox("100%") progress_text_width = right - left - start_time = time.time() num_frames = 0 - debug = False - show_framerate = False # enable for debugging / testing while self.keep_running: frame = self.camera.read_video_stream(as_image=True) if frame is not None: num_frames += 1 - cur_time = time.time() - cur_fps = num_frames / (cur_time - start_time) scan_text = None progress_percentage = self.decoder.get_percent_complete() if progress_percentage == 0: # We've just started scanning, no results yet - if show_framerate: - scan_text = f"{cur_fps:0.2f} | {self.decoder_fps}" - else: - scan_text = self.instructions_text - - elif debug: - # Special debugging output for animated QRs - scan_text = f"{self.decoder.get_percent_complete()}% | {self.decoder.get_percent_complete(weight_mixed_frames=True)}% (new)" - if show_framerate: - scan_text += f" {cur_fps:0.2f} | {self.decoder_fps}" + scan_text = self.instructions_text with self.renderer.lock: # Use nearest neighbor resizing for max speed @@ -216,7 +196,6 @@ class ScanScreen(BaseScreen): # Render the dot to indicate successful QR frame read indicator_size = 10 - self.last_frame_decoded_count = self.frames_decoded_counter.cur_count status_color_map = { ScanScreen.FRAME__ADDED_PART: GUIConstants.SUCCESS_COLOR, ScanScreen.FRAME__REPEATED_PART: GUIConstants.INACTIVE_COLOR, @@ -267,7 +246,6 @@ class ScanScreen(BaseScreen): self.camera.stop_video_stream_mode() break - self.frames_decoded_counter.increment() # Notify the live preview thread how our most recent decode went if status == DecodeQRStatus.FALSE: # Did not find anything to decode in the current frame diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 20e3379d..a8ba8044 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -389,7 +389,6 @@ class ButtonListScreen(BaseTopNavScreen): if self.has_scroll_arrows: self.arrow_half_width = 10 - self.cur_scroll_y = self.scroll_y_initial_offset if self.scroll_y_initial_offset is not None else 0 self.up_arrow_img = Image.new("RGBA", size=(2 * self.arrow_half_width, 8), color="black") self.up_arrow_img_y = self.top_nav.height - 12 arrow_draw = ImageDraw.Draw(self.up_arrow_img) @@ -1087,21 +1086,6 @@ class ResetScreen(BaseTopNavScreen): -@dataclass -class PowerOffScreen(BaseTopNavScreen): - def __post_init__(self): - self.title = _("Powering Off") - self.show_back_button = False - super().__post_init__() - - self.components.append(TextArea( - text=_("Please wait about 30 seconds before disconnecting power."), - screen_y=self.top_nav.height, - height=self.canvas_height - self.top_nav.height, - )) - - - @dataclass class PowerOffNotRequiredScreen(BaseTopNavScreen): def __post_init__(self): diff --git a/src/seedsigner/hardware/camera.py b/src/seedsigner/hardware/camera.py index 865e1cd1..44919fa4 100644 --- a/src/seedsigner/hardware/camera.py +++ b/src/seedsigner/hardware/camera.py @@ -9,9 +9,7 @@ from seedsigner.models.singleton import Singleton class CameraConnectionError(Exception): - def __init__(self, *args, **kwargs): - message = _("Camera error. Check camera connections.") - super().__init__(message) + pass diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index b3e90680..444c3fed 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -110,14 +110,3 @@ def get_partial_final_word(coin_flips: str, wordlist_language_code: str = Settin wordlist_index = int(binary_string, 2) return Seed.get_wordlist(wordlist_language_code)[wordlist_index] - - - -# Note: This currently isn't being used since we're now chaining hashed bytes for the -# image-based entropy and aren't just ingesting a single image. -def generate_mnemonic_from_image(image, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: - import hashlib - hash = hashlib.sha256(image.tobytes()) - - # Return as a list - return bip39.mnemonic_from_bytes(hash.digest(), wordlist=Seed.get_wordlist(wordlist_language_code)).split() diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 37aeb788..86be8c66 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -238,7 +238,11 @@ class BaseSimpleAnimatedQREncoder(BaseQrEncoder): @dataclass -class SpecterXPubQrEncoder(BaseSimpleAnimatedQREncoder, BaseXpubQrEncoder): +class SpecterLegacyXPubQrEncoder(BaseSimpleAnimatedQREncoder, BaseXpubQrEncoder): + """ + Legacy "pXofY" format. Included here for compatibility with much older versions of + Specter Desktop. Can probably eventually be removed. + """ @property def qr_max_fragment_size(self): density_mapping = { diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index 325f7af4..e9310d1d 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -38,17 +38,18 @@ class SettingsConstants: ] # User-facing selection options - COORDINATOR__BLUE_WALLET = "bw" - COORDINATOR__NUNCHUK = "nun" - COORDINATOR__SPARROW = "spa" - COORDINATOR__SPECTER_DESKTOP = "spd" - COORDINATOR__KEEPER = "kpr" - ALL_COORDINATORS = [ - (COORDINATOR__BLUE_WALLET, "BlueWallet"), - (COORDINATOR__NUNCHUK, "Nunchuk"), - (COORDINATOR__SPARROW, "Sparrow"), - (COORDINATOR__SPECTER_DESKTOP, "Specter Desktop"), - (COORDINATOR__KEEPER, "Keeper"), + XPUB_QR_FORMAT__UR_CRYPTO_ACCOUNT = "urca" + XPUB_QR_FORMAT__STATIC = "sta" + XPUB_QR_FORMAT__SPECTER_LEGACY = "spl" + ALL_XPUB_QR_FORMATS = [ + # TRANSLATOR_NOTE: QR code format option; "default" = this is the format most wallets use + (XPUB_QR_FORMAT__UR_CRYPTO_ACCOUNT, _mft("Animated (default)")), + + # TRANSLATOR_NOTE: QR code format option (static = single frame, not animated) + (XPUB_QR_FORMAT__STATIC, _mft("Static")), + + # TRANSLATOR_NOTE: QR code format option: old format that Specter Desktop used to use + (XPUB_QR_FORMAT__SPECTER_LEGACY, _mft("Specter legacy")), ] # Over-specifying current and possible future locales to reduce/eliminate main repo @@ -129,6 +130,7 @@ class SettingsConstants: # --------- Beta languages ------------------------------------------------------ LOCALE__CHINESE_SIMPLIFIED: "(beta) 简体中文 (Chinese Simplified)", + LOCALE__HINDI: "(beta) हिन्दी (Hindi)", LOCALE__JAPANESE: "(beta) 日本語 (Japanese)", LOCALE__KOREAN: "(beta) 한국어 (Korean)", LOCALE__THAI: "(beta) ไทย (Thai)", @@ -149,7 +151,6 @@ class SettingsConstants: # LOCALE__GUJARATI: "ગુજરાતી (Gujarati)", LOCALE__HAUSA: "Hausa", # LOCALE__HEBREW: "עברית (Hebrew)", - # LOCALE__HINDI: "हिन्दी (Hindi)", LOCALE__CROATIAN: "Hrvatski", LOCALE__INDONESIAN: "Indonesia", LOCALE__JAVANESE: "Jawa (Javanese)", @@ -326,7 +327,7 @@ class SettingsConstants: SETTING__LOCALE = "locale" SETTING__WORDLIST_LANGUAGE = "wordlist_language" SETTING__PERSISTENT_SETTINGS = "persistent_settings" - SETTING__COORDINATORS = "coordinators" + SETTING__XPUB_QR_FORMAT = "xpub_qr" SETTING__BTC_DENOMINATION = "denomination" SETTING__DISPLAY_CONFIGURATION = "display_config" @@ -568,19 +569,6 @@ class SettingsDefinition: help_text=SettingsConstants.PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT, default_value=SettingsConstants.OPTION__DISABLED), - SettingsEntry(category=SettingsConstants.CATEGORY__WALLET, - attr_name=SettingsConstants.SETTING__COORDINATORS, - abbreviated_name="coords", - display_name=_mft("Coordinator software"), - type=SettingsConstants.TYPE__MULTISELECT, - selection_options=SettingsConstants.ALL_COORDINATORS, - default_value=[ - SettingsConstants.COORDINATOR__BLUE_WALLET, - SettingsConstants.COORDINATOR__NUNCHUK, - SettingsConstants.COORDINATOR__SPARROW, - SettingsConstants.COORDINATOR__SPECTER_DESKTOP, - ]), - SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, attr_name=SettingsConstants.SETTING__BTC_DENOMINATION, abbreviated_name="denom", @@ -625,6 +613,17 @@ class SettingsDefinition: selection_options=SettingsConstants.ALL_SCRIPT_TYPES, default_value=[SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT, SettingsConstants.TAPROOT]), + SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, + attr_name=SettingsConstants.SETTING__XPUB_QR_FORMAT, + display_name=_mft("Xpub QR format"), + visibility=SettingsConstants.VISIBILITY__ADVANCED, + type=SettingsConstants.TYPE__MULTISELECT, + selection_options=SettingsConstants.ALL_XPUB_QR_FORMATS, + default_value=[ + SettingsConstants.XPUB_QR_FORMAT__UR_CRYPTO_ACCOUNT, + SettingsConstants.XPUB_QR_FORMAT__STATIC, + ]), + SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__XPUB_DETAILS, display_name=_mft("Show xpub details"), diff --git a/src/seedsigner/resources/fonts/NotoSansDevanagari-Regular.ttf b/src/seedsigner/resources/fonts/NotoSansDevanagari-Regular.ttf new file mode 100644 index 00000000..ab3f4334 Binary files /dev/null and b/src/seedsigner/resources/fonts/NotoSansDevanagari-Regular.ttf differ diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 4ff2e77e..15dad4a8 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -11,7 +11,7 @@ from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconCo from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, seed_screens) from seedsigner.gui.screens.screen import ButtonOption, ButtonOptionWithoutTranslation -from seedsigner.models.encode_qr import CompactSeedQrEncoder, GenericStaticQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrXpubQrEncoder +from seedsigner.models.encode_qr import CompactSeedQrEncoder, GenericStaticQrEncoder, SeedQrEncoder, SpecterLegacyXPubQrEncoder, StaticXpubQrEncoder, UrXpubQrEncoder from seedsigner.models.qr_type import QRType from seedsigner.models.seed import Seed from seedsigner.models.settings import Settings, SettingsConstants @@ -719,7 +719,7 @@ class SeedExportXpubScriptTypeView(View): del args["sig_type"] return Destination(ToolsAddressExplorerAddressTypeView, view_args=args, skip_current_view=True) else: - return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True) + return Destination(SeedExportXpubQRFormatView, view_args=args, skip_current_view=True) title = _("Export Xpub") if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: @@ -754,7 +754,7 @@ class SeedExportXpubScriptTypeView(View): del args["sig_type"] return Destination(ToolsAddressExplorerAddressTypeView, view_args=args) else: - return Destination(SeedExportXpubCoordinatorView, view_args=args) + return Destination(SeedExportXpubQRFormatView, view_args=args) @@ -785,7 +785,7 @@ class SeedExportXpubCustomDerivationView(View): return Destination(ToolsAddressExplorerAddressTypeView, view_args=dict(seed_num=self.seed_num, script_type=self.script_type, custom_derivation=custom_derivation)) return Destination( - SeedExportXpubCoordinatorView, + SeedExportXpubQRFormatView, view_args={ "seed_num": self.seed_num, "sig_type": self.sig_type, @@ -796,7 +796,7 @@ class SeedExportXpubCustomDerivationView(View): -class SeedExportXpubCoordinatorView(View): +class SeedExportXpubQRFormatView(View): def __init__(self, seed_num: int, sig_type: str, script_type: str, custom_derivation: str = None): super().__init__() self.seed_num = seed_num @@ -812,41 +812,39 @@ class SeedExportXpubCoordinatorView(View): "script_type": self.script_type, "custom_derivation": self.custom_derivation, } - if len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)) == 1: + if len(self.settings.get_value(SettingsConstants.SETTING__XPUB_QR_FORMAT)) == 1: # Nothing to select; skip this screen - args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0] + args["xpub_qr_format"] = self.settings.get_value(SettingsConstants.SETTING__XPUB_QR_FORMAT)[0] return Destination(SeedExportXpubWarningView, view_args=args, skip_current_view=True) button_data = [] - for display_name, setting_option in zip(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS), self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)): + for display_name, setting_option in zip(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__XPUB_QR_FORMAT), self.settings.get_value(SettingsConstants.SETTING__XPUB_QR_FORMAT)): button_data.append(ButtonOption(display_name, return_data=setting_option)) selected_menu_num = self.run_screen( ButtonListScreen, - title=_("Export Xpub"), + title=_("Xpub QR Format"), is_button_text_centered=False, button_data=button_data, + is_bottom_list=True, ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - # coordinators_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__COORDINATORS) - # selected_display_name = button_data[selected_menu_num] - # args["coordinator"] = coordinators_settings_entry.get_selection_option_value_by_display_name(selected_display_name) - args["coordinator"] = button_data[selected_menu_num].return_data + args["xpub_qr_format"] = button_data[selected_menu_num].return_data return Destination(SeedExportXpubWarningView, view_args=args) class SeedExportXpubWarningView(View): - def __init__(self, seed_num: int, sig_type: str, script_type: str, coordinator: str, custom_derivation: str): + def __init__(self, seed_num: int, sig_type: str, script_type: str, xpub_qr_format: str, custom_derivation: str): super().__init__() self.seed_num = seed_num self.sig_type = sig_type self.script_type = script_type - self.coordinator = coordinator + self.xpub_qr_format = xpub_qr_format self.custom_derivation = custom_derivation @@ -857,7 +855,7 @@ class SeedExportXpubWarningView(View): "seed_num": self.seed_num, "sig_type": self.sig_type, "script_type": self.script_type, - "coordinator": self.coordinator, + "xpub_qr_format": self.xpub_qr_format, "custom_derivation": self.custom_derivation, }, skip_current_view=True, # Prevent going BACK to WarningViews @@ -887,11 +885,11 @@ class SeedExportXpubDetailsView(View): Collects the user input from all the previous screens leading up to this and finally calculates the xpub and displays the summary view to the user. """ - def __init__(self, seed_num: int, sig_type: str, script_type: str, coordinator: str, custom_derivation: str): + def __init__(self, seed_num: int, sig_type: str, script_type: str, xpub_qr_format: str, custom_derivation: str): super().__init__() self.sig_type = sig_type self.script_type = script_type - self.coordinator = coordinator + self.xpub_qr_format = xpub_qr_format self.custom_derivation = custom_derivation self.seed_num = seed_num @@ -954,7 +952,7 @@ class SeedExportXpubDetailsView(View): return Destination( SeedExportXpubQRDisplayView, dict(seed_num=self.seed_num, - coordinator=self.coordinator, + xpub_qr_format=self.xpub_qr_format, derivation_path=derivation_path, sig_type=self.sig_type ) @@ -966,7 +964,7 @@ class SeedExportXpubDetailsView(View): class SeedExportXpubQRDisplayView(View): - def __init__(self, seed_num: int, coordinator: str, derivation_path: str, sig_type: str = SettingsConstants.SINGLE_SIG): + def __init__(self, seed_num: int, xpub_qr_format: str, derivation_path: str, sig_type: str = SettingsConstants.SINGLE_SIG): super().__init__() self.seed = self.controller.get_seed(seed_num) @@ -978,14 +976,14 @@ class SeedExportXpubQRDisplayView(View): sig_type=sig_type ) - if coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP: - self.qr_encoder = SpecterXPubQrEncoder(**encoder_args) - - elif coordinator in [SettingsConstants.COORDINATOR__BLUE_WALLET, - SettingsConstants.COORDINATOR__KEEPER]: + if xpub_qr_format == SettingsConstants.XPUB_QR_FORMAT__STATIC: self.qr_encoder = StaticXpubQrEncoder(**encoder_args) + elif xpub_qr_format == SettingsConstants.XPUB_QR_FORMAT__SPECTER_LEGACY: + self.qr_encoder = SpecterLegacyXPubQrEncoder(**encoder_args) + else: + # Default: UR crypto-address self.qr_encoder = UrXpubQrEncoder(**encoder_args) diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index 9c8c8a15..673086ff 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -237,8 +237,8 @@ def generate_screenshots(locale): ) add_settings_entries(SettingsConstants.VISIBILITY__HARDWARE) - settingsqr_data_persistent = f"settings::v1 name=English_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}" - settingsqr_data_not_persistent = f"settings::v1 name=Mode_Ephemeral persistent=D coords=spa,spd denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}" + settingsqr_data_persistent = f"settings::v1 name=English_noob_mode persistent=E xpub_qr=urca,sta denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}" + settingsqr_data_not_persistent = f"settings::v1 name=Mode_Ephemeral persistent=D xpub_qr=urca,sta denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}" # Set up screenshot-specific callbacks to inject data before the View is run and # reset data after the View is run. @@ -334,10 +334,10 @@ def generate_screenshots(locale): ScreenshotConfig(seed_views.SeedExportXpubSigTypeView, dict(seed_num=0)), ScreenshotConfig(seed_views.SeedExportXpubScriptTypeView, dict(seed_num=0, sig_type="msig")), ScreenshotConfig(seed_views.SeedExportXpubCustomDerivationView, dict(seed_num=0, sig_type="ss", script_type="")), - ScreenshotConfig(seed_views.SeedExportXpubCoordinatorView, dict(seed_num=0, sig_type="ss", script_type="nat")), - ScreenshotConfig(seed_views.SeedExportXpubWarningView, dict(seed_num=0, sig_type="msig", script_type="nes", coordinator="spd", custom_derivation="")), - ScreenshotConfig(seed_views.SeedExportXpubDetailsView, dict(seed_num=0, sig_type="ss", script_type="nat", coordinator="bw", custom_derivation="")), - ScreenshotConfig(SeedExportXpubQR_ScreenBrightnessView, dict(seed_num=0, coordinator="bw", derivation_path="m/84'/0'/0'")), + ScreenshotConfig(seed_views.SeedExportXpubQRFormatView, dict(seed_num=0, sig_type="ss", script_type="nat")), + ScreenshotConfig(seed_views.SeedExportXpubWarningView, dict(seed_num=0, sig_type="msig", script_type="nes", xpub_qr_format="urca", custom_derivation="")), + ScreenshotConfig(seed_views.SeedExportXpubDetailsView, dict(seed_num=0, sig_type="ss", script_type="nat", xpub_qr_format="urca", custom_derivation="")), + ScreenshotConfig(SeedExportXpubQR_ScreenBrightnessView, dict(seed_num=0, xpub_qr_format="urca", derivation_path="m/84'/0'/0'")), ScreenshotConfig(seed_views.SeedWordsWarningView, dict(seed_num=0)), ScreenshotConfig(seed_views.SeedWordsView, dict(seed_num=0)), diff --git a/tests/test_controller.py b/tests/test_controller.py index 319a30ec..f414dc89 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -4,7 +4,6 @@ import pytest from base import BaseTest from seedsigner.controller import Controller -from seedsigner.models.settings_definition import SettingsConstants class TestController(BaseTest): @@ -80,37 +79,3 @@ class TestController(BaseTest): # ...get a new copy of the instance and confirm change controller = Controller.get_instance() assert controller.unverified_address == "123abc" - - - def test_missing_settings_get_defaults(self): - """ Should gracefully handle all missing fields from `settings.json` """ - - controller = Controller.get_instance() - - # Settings defaults - assert controller.settings.get_value(SettingsConstants.SETTING__LOCALE) == SettingsConstants.LOCALE__ENGLISH - assert controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH - assert controller.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED - assert controller.settings.get_value(SettingsConstants.SETTING__COORDINATORS) == [i for i,j in SettingsConstants.ALL_COORDINATORS if i!="kpr"] - assert controller.settings.get_value(SettingsConstants.SETTING__BTC_DENOMINATION) == SettingsConstants.BTC_DENOMINATION__THRESHOLD - - # Advanced Settings defaults - assert controller.settings.get_value(SettingsConstants.SETTING__NETWORK) == SettingsConstants.MAINNET - assert controller.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__MEDIUM - assert controller.settings.get_value(SettingsConstants.SETTING__SIG_TYPES) == [i for i,j in SettingsConstants.ALL_SIG_TYPES] - assert controller.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) == [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT, SettingsConstants.TAPROOT] - assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED - assert controller.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__ENABLED - assert controller.settings.get_value(SettingsConstants.SETTING__CAMERA_ROTATION) == SettingsConstants.CAMERA_ROTATION__180 - assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED - assert controller.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__DISABLED - assert controller.settings.get_value(SettingsConstants.SETTING__MESSAGE_SIGNING) == SettingsConstants.OPTION__DISABLED - assert controller.settings.get_value(SettingsConstants.SETTING__PRIVACY_WARNINGS) == SettingsConstants.OPTION__ENABLED - assert controller.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED - assert controller.settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS_TIPS) == SettingsConstants.OPTION__ENABLED - assert controller.settings.get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED - assert controller.settings.get_value(SettingsConstants.SETTING__MICROSD_TOAST_TIMER) == SettingsConstants.MICROSD_TOAST_TIMER_FIVE_SECONDS - - # Hidden Settings defaults - assert controller.settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS) == 62 - diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index 2d864c12..3b33c0c3 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -1,4 +1,4 @@ -from seedsigner.models.encode_qr import CompactSeedQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrPsbtQrEncoder, UrXpubQrEncoder +from seedsigner.models.encode_qr import CompactSeedQrEncoder, SeedQrEncoder, SpecterLegacyXPubQrEncoder, StaticXpubQrEncoder, UrPsbtQrEncoder, UrXpubQrEncoder from embit import psbt from binascii import a2b_base64 @@ -54,7 +54,7 @@ def test_xpub_qr(): def test_specter_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = SpecterXPubQrEncoder(seed=Seed(mnemonic.split(" "), passphrase="pass"), network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) + e = SpecterLegacyXPubQrEncoder(seed=Seed(mnemonic.split(" "), passphrase="pass"), network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) assert e.next_part() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN" assert e.next_part() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo" diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index 2b2339f1..23b50671 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -184,7 +184,7 @@ class TestSeedFlows(FlowTest): """ Selecting "Export XPUB" from the SeedOptionsView should enter the Export XPUB flow and end at the MainMenuView """ - def flowtest_standard_xpub(sig_tuple, script_tuple, coord_tuple): + def flowtest_standard_xpub(sig_tuple, script_tuple, xpub_qr_tuple): if sig_tuple[0] == SettingsConstants.SINGLE_SIG: sig_selection = seed_views.SeedExportXpubSigTypeView.SINGLE_SIG else: @@ -195,7 +195,7 @@ class TestSeedFlows(FlowTest): FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB), FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=sig_selection), FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=ButtonOption(script_tuple[1], return_data=script_tuple[0])), - FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=ButtonOption(coord_tuple[1], return_data=coord_tuple[0])), + FlowStep(seed_views.SeedExportXpubQRFormatView, button_data_selection=ButtonOption(xpub_qr_tuple[1], return_data=xpub_qr_tuple[0])), FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0), FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0), FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0), @@ -211,17 +211,17 @@ class TestSeedFlows(FlowTest): # these are lists of (constant_value, display_name) tuples sig_types: list[tuple[str, str]] = SettingsConstants.ALL_SIG_TYPES script_types: list[tuple[str, str]] = SettingsConstants.ALL_SCRIPT_TYPES - coordinators: list[tuple[str, str]] = SettingsConstants.ALL_COORDINATORS + xpub_qr_formats: list[tuple[str, str]] = SettingsConstants.ALL_XPUB_QR_FORMATS # enable non-defaults so they're available in views self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in sig_types]) self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in script_types]) - self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in coordinators]) + self.settings.set_value(SettingsConstants.SETTING__XPUB_QR_FORMAT, [x for x,y in xpub_qr_formats]) - # exhaustively test flows thru standard sig_types, script_types, and coordinators + # exhaustively test flows thru standard sig_types, script_types, and xpub_qr_formats for sig_tuple in sig_types: for script_tuple in script_types: - for coord_tuple in coordinators: + for xpub_qr_tuple in xpub_qr_formats: # skip custom derivation if script_tuple[0] == SettingsConstants.CUSTOM_DERIVATION: continue @@ -229,13 +229,13 @@ class TestSeedFlows(FlowTest): elif sig_tuple[0] == SettingsConstants.MULTISIG and script_tuple[0] == SettingsConstants.TAPROOT: continue else: - print('\n\ntest_standard_xpubs(%s, %s, %s)' % (sig_tuple, script_tuple, coord_tuple)) - flowtest_standard_xpub(sig_tuple, script_tuple, coord_tuple) + print('\n\ntest_standard_xpubs(%s, %s, %s)' % (sig_tuple, script_tuple, xpub_qr_tuple)) + flowtest_standard_xpub(sig_tuple, script_tuple, xpub_qr_tuple) def test_export_xpub_disabled_not_available_flow(self): """ - If sig_type/script_type/coordinator disabled, then these options are not available + If sig_type/script_type/xpub_qr_format disabled, then these options are not available """ # Load a finalized Seed into the Controller mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split() @@ -245,17 +245,17 @@ class TestSeedFlows(FlowTest): # these are lists of (constant_value, display_name) tuples sig_types: list[tuple[str, str]] = SettingsConstants.ALL_SIG_TYPES script_types: list[tuple[str, str]] = SettingsConstants.ALL_SCRIPT_TYPES - coordinators: list[tuple[str, str]] = SettingsConstants.ALL_COORDINATORS + xpub_qr_formats: list[tuple[str, str]] = SettingsConstants.ALL_XPUB_QR_FORMATS # these are the disabled types that we will be testing disabled_sig = SettingsConstants.MULTISIG disabled_script = SettingsConstants.TAPROOT - disabled_coord = SettingsConstants.COORDINATOR__NUNCHUK + disabled_xpub_qr_format = SettingsConstants.XPUB_QR_FORMAT__SPECTER_LEGACY # enable all but our target disabled type self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in sig_types if x!=disabled_sig]) self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in script_types if x!=disabled_script]) - self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in coordinators if x!=disabled_coord]) + self.settings.set_value(SettingsConstants.SETTING__XPUB_QR_FORMAT, [x for x,y in xpub_qr_formats if x!=disabled_xpub_qr_format]) # If multisig isn't an option, then the sig type selection is skipped altogether self.run_sequence( @@ -286,7 +286,7 @@ class TestSeedFlows(FlowTest): FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB), FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True), FlowStep(seed_views.SeedExportXpubScriptTypeView, screen_return_value=0), - FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=disabled_coord), + FlowStep(seed_views.SeedExportXpubQRFormatView, button_data_selection=disabled_xpub_qr_format), ] ) @@ -307,8 +307,8 @@ class TestSeedFlows(FlowTest): SettingsConstants.CUSTOM_DERIVATION ]) - # Ensure that all coordinators are enabled - self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x, y in SettingsConstants.ALL_COORDINATORS]) + # Ensure that all xpub_qr_formats are enabled + self.settings.set_value(SettingsConstants.SETTING__XPUB_QR_FORMAT, [x for x, y in SettingsConstants.ALL_XPUB_QR_FORMATS]) # Set up button_data selections sig_type = seed_views.SeedExportXpubSigTypeView.SINGLE_SIG @@ -316,9 +316,9 @@ class TestSeedFlows(FlowTest): custom_derivation = SettingsConstants.CUSTOM_DERIVATION script_type = ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES)[2], return_data=custom_derivation) - specter = SettingsConstants.COORDINATOR__SPECTER_DESKTOP - assert SettingsConstants.ALL_COORDINATORS[3][0] == specter - coordinator = ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[3], return_data=specter) + specter_legacy = SettingsConstants.XPUB_QR_FORMAT__SPECTER_LEGACY + assert SettingsConstants.ALL_XPUB_QR_FORMATS[2][0] == specter_legacy + xpub_qr_format = ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__XPUB_QR_FORMAT)[2], return_data=specter_legacy) self.run_sequence( initial_destination_view_args=dict(seed_num=0), @@ -327,7 +327,7 @@ class TestSeedFlows(FlowTest): FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=sig_type), FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=script_type), FlowStep(seed_views.SeedExportXpubCustomDerivationView, screen_return_value="m/0'/0'"), - FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=coordinator), + FlowStep(seed_views.SeedExportXpubQRFormatView, button_data_selection=xpub_qr_format), FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0), FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0), FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0), @@ -338,18 +338,18 @@ class TestSeedFlows(FlowTest): def test_export_xpub_skip_non_option_flow(self): """ - Export XPUB flows w/o user choices when no other options for sig_types, script_types, and/or coordinators + Export XPUB flows w/o user choices when no other options for sig_types, script_types, and/or xpub_qr_formats """ # Load a finalized Seed into the Controller mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split() self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic)) self.controller.storage.finalize_pending_seed() - # exclusively set only one choice for each of sig_types, script_types and coordinators + # exclusively set only one choice for each of sig_types, script_types and xpub_qr_formats self.settings.update({ SettingsConstants.SETTING__SIG_TYPES: SettingsConstants.MULTISIG, SettingsConstants.SETTING__SCRIPT_TYPES: SettingsConstants.NESTED_SEGWIT, - SettingsConstants.SETTING__COORDINATORS: SettingsConstants.COORDINATOR__SPECTER_DESKTOP, + SettingsConstants.SETTING__XPUB_QR_FORMAT: SettingsConstants.XPUB_QR_FORMAT__UR_CRYPTO_ACCOUNT, }) self.run_sequence( @@ -358,7 +358,7 @@ class TestSeedFlows(FlowTest): FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB), FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True), FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True), - FlowStep(seed_views.SeedExportXpubCoordinatorView, is_redirect=True), + FlowStep(seed_views.SeedExportXpubQRFormatView, is_redirect=True), FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0), FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0), FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0), @@ -379,7 +379,7 @@ class TestSeedFlows(FlowTest): # Make sure all options are enabled self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in SettingsConstants.ALL_SIG_TYPES]) self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in SettingsConstants.ALL_SCRIPT_TYPES]) - self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in SettingsConstants.ALL_COORDINATORS]) + self.settings.set_value(SettingsConstants.SETTING__XPUB_QR_FORMAT, [x for x,y in SettingsConstants.ALL_XPUB_QR_FORMATS]) self.run_sequence( initial_destination_view_args=dict(seed_num=0), @@ -389,7 +389,7 @@ class TestSeedFlows(FlowTest): # Skips past the script type options via redirect FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True), - FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[0], return_data=SettingsConstants.ALL_COORDINATORS[0][0])), + FlowStep(seed_views.SeedExportXpubQRFormatView, button_data_selection=ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__XPUB_QR_FORMAT)[0], return_data=SettingsConstants.ALL_XPUB_QR_FORMATS[0][0])), FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0), FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0), FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0), diff --git a/tests/test_flows_settings.py b/tests/test_flows_settings.py index 9a849682..75fc530b 100644 --- a/tests/test_flows_settings.py +++ b/tests/test_flows_settings.py @@ -39,10 +39,11 @@ class TestSettingsFlows(FlowTest): def test_multiselect(self): """ Multiselect Settings options should stay in-place; requires BACK to exit. """ # Which option are we testing? - settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__COORDINATORS) + settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__XPUB_QR_FORMAT) self.run_sequence([ FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS), + FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_views.SettingsMenuView.ADVANCED), FlowStep(settings_views.SettingsMenuView, button_data_selection=ButtonOption(settings_entry.display_name)), FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # select/deselect first option FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option @@ -78,11 +79,11 @@ class TestSettingsFlows(FlowTest): MainMenuView. """ def load_persistent_settingsqr_into_decoder(view: scan_views.ScanView): - settingsqr_data_persistent: str = "settings::v1 name=Total_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" + settingsqr_data_persistent: str = "settings::v1 name=Total_noob_mode persistent=E xpub_qr=urca,sta denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" view.decoder.add_data(settingsqr_data_persistent) def load_not_persistent_settingsqr_into_decoder(view: scan_views.ScanView): - settingsqr_data_not_persistent: str = "settings::v1 name=Ephemeral_noob_mode persistent=D coords=spa,spd denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" + settingsqr_data_not_persistent: str = "settings::v1 name=Ephemeral_noob_mode persistent=D xpub_qr=urca,sta denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" view.decoder.add_data(settingsqr_data_not_persistent) def _run_test(initial_setting_state: str, load_settingsqr_into_decoder: Callable, expected_setting_state: str): diff --git a/tests/test_settings.py b/tests/test_settings.py index aed45b3e..8cc391f9 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,7 +1,7 @@ import pytest from base import BaseTest from seedsigner.models.settings import InvalidSettingsQRData, Settings -from seedsigner.models.settings_definition import SettingsConstants +from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition @@ -14,6 +14,10 @@ class TestSettings(BaseTest): def test_reset_settings(self): """ BaseTest.reset_settings() should wipe out any previous Settings changes """ + settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS) + assert settings_entry.default_value == SettingsConstants.OPTION__DISABLED + + # Change the setting from its default settings = Settings.get_instance() settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, SettingsConstants.OPTION__ENABLED) assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED @@ -23,18 +27,27 @@ class TestSettings(BaseTest): assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED + def test_settings_defaults(self): + """ Settings should initialize to their default values """ + BaseTest.reset_settings() + settings = Settings.get_instance() + for settings_entry in SettingsDefinition.get_settings_entries(): + assert settings.get_value(settings_entry.attr_name) == settings_entry.default_value + + def test_parse_settingsqr_data(self): """ SettingsQR parser should successfully parse a valid settingsqr input string and return the resulting config_name and formatted settings_update_dict. """ settings_name = "Test SettingsQR" - settingsqr_data = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D coords=spa,spd denom=thr network=M qr_density=M sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" + settingsqr_data = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D denom=thr network=M qr_density=M sigs=ss,ms scripts=nat,nes,tr xpub_qr=urca,sta xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" + # First explicitly set settings that differ from the settingsqr_data self.settings.set_value(SettingsConstants.SETTING__COMPACT_SEEDQR, SettingsConstants.OPTION__DISABLED) self.settings.set_value(SettingsConstants.SETTING__DIRE_WARNINGS, SettingsConstants.OPTION__DISABLED) - self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [SettingsConstants.COORDINATOR__BLUE_WALLET, SettingsConstants.COORDINATOR__SPARROW]) + self.settings.set_value(SettingsConstants.SETTING__XPUB_QR_FORMAT, [SettingsConstants.XPUB_QR_FORMAT__STATIC, SettingsConstants.XPUB_QR_FORMAT__SPECTER_LEGACY]) # Now parse the settingsqr_data config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) @@ -45,10 +58,10 @@ class TestSettings(BaseTest): assert self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED assert self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED - coordinators = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS) - assert SettingsConstants.COORDINATOR__BLUE_WALLET not in coordinators - assert SettingsConstants.COORDINATOR__SPARROW in coordinators - assert SettingsConstants.COORDINATOR__SPECTER_DESKTOP in coordinators + xpub_qr_formats = self.settings.get_value(SettingsConstants.SETTING__XPUB_QR_FORMAT) + assert SettingsConstants.XPUB_QR_FORMAT__UR_CRYPTO_ACCOUNT in xpub_qr_formats + assert SettingsConstants.XPUB_QR_FORMAT__STATIC in xpub_qr_formats + assert SettingsConstants.XPUB_QR_FORMAT__SPECTER_LEGACY not in xpub_qr_formats def test_settingsqr_version(self): diff --git a/tests/test_settingsqr_decoder.py b/tests/test_settingsqr_decoder.py index 9ec08232..3f7285c1 100644 --- a/tests/test_settingsqr_decoder.py +++ b/tests/test_settingsqr_decoder.py @@ -9,7 +9,7 @@ class TestSettingsQRDecoder: with parsing the result. """ settings_name = "Test SettingsQR" - settings_qr_str = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D coords=spa,spd denom=thr network=M qr_density=M sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" + settings_qr_str = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D xpub_qr=urca,sta denom=thr network=M qr_density=M sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" # Now parse the settings_qr_str decoder = DecodeQR()