Merge pull request #844 from kdmukai/xpub_export_replace_coordinators

[Refactor] Xpub export: Replace the "coordinator" setting with "xpub_qr_type"
This commit is contained in:
Nick Klockenga
2026-01-19 21:32:14 -05:00
committed by GitHub
11 changed files with 166 additions and 144 deletions
+57 -13
View File
@@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""
+5 -1
View File
@@ -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 = {
+24 -25
View File
@@ -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
@@ -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"
@@ -569,19 +570,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",
@@ -632,6 +620,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"),
+23 -25
View File
@@ -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
@@ -720,7 +720,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:
@@ -755,7 +755,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)
@@ -786,7 +786,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,
@@ -797,7 +797,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
@@ -813,41 +813,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
@@ -858,7 +856,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
@@ -888,11 +886,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
@@ -955,7 +953,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
)
@@ -967,7 +965,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)
@@ -979,14 +977,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)
+6 -6
View File
@@ -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 xpub_export=E 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 xpub_export=E 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 xpub_export=E 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 xpub_export=E 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)),
-36
View File
@@ -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,38 +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__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED
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
+2 -2
View File
@@ -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"
+25 -25
View File
@@ -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),
+4 -3
View File
@@ -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 xpub_export=E 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 xpub_export=E 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 xpub_export=E 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 xpub_export=E 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):
+19 -7
View File
@@ -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,26 @@ 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 xpub_export=E 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 xpub_export=E 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 +57,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):
+1 -1
View File
@@ -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 xpub_export=E 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 xpub_export=E 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()