Merge pull request #394 from kdmukai/settings_qr

SettingsQR: Final changes before official release
This commit is contained in:
Nick Klockenga
2023-08-02 23:48:21 -04:00
committed by GitHub
9 changed files with 278 additions and 182 deletions
@@ -183,6 +183,7 @@ class SettingsUpdatedScreen(ButtonListScreen):
def __post_init__(self): def __post_init__(self):
# Customize defaults # Customize defaults
self.button_data = ["Home"] self.button_data = ["Home"]
self.show_back_button = False
super().__post_init__() super().__post_init__()
+31 -107
View File
@@ -9,14 +9,12 @@ from embit import psbt, bip39
from pyzbar import pyzbar from pyzbar import pyzbar
from pyzbar.pyzbar import ZBarSymbol from pyzbar.pyzbar import ZBarSymbol
from urtypes.crypto import PSBT as UR_PSBT from urtypes.crypto import PSBT as UR_PSBT
from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP from urtypes.crypto import Account, Output
from urtypes.bytes import Bytes from urtypes.bytes import Bytes
from seedsigner.helpers.ur2.ur_decoder import URDecoder from seedsigner.helpers.ur2.ur_decoder import URDecoder
from seedsigner.models.psbt_parser import PSBTParser from seedsigner.models import QRType, Seed
from seedsigner.models.settings import SettingsConstants
from . import QRType, Seed
from .settings import SettingsConstants
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -175,12 +173,7 @@ class DecodeQR:
def get_settings_data(self): def get_settings_data(self):
if self.is_settings: if self.is_settings:
return self.decoder.settings return self.decoder.data
def get_settings_config_name(self):
if self.is_settings:
return self.decoder.config_name
def get_address(self): def get_address(self):
@@ -364,7 +357,7 @@ class DecodeQR:
return QRType.BITCOIN_ADDRESS return QRType.BITCOIN_ADDRESS
# config data # config data
if "type=settings" in s: if s.startswith("settings::"):
return QRType.SETTINGS return QRType.SETTINGS
# Seed # Seed
@@ -578,6 +571,8 @@ class DecodeQR:
return descriptor return descriptor
class BaseQrDecoder: class BaseQrDecoder:
def __init__(self): def __init__(self):
self.total_segments = None self.total_segments = None
@@ -673,6 +668,8 @@ class SpecterPsbtQrDecoder(BaseAnimatedQrDecoder):
def parse_segment(self, segment) -> str: def parse_segment(self, segment) -> str:
return segment.split(" ")[-1].strip() return segment.split(" ")[-1].strip()
class Base64PsbtQrDecoder(BaseSingleFrameQrDecoder): class Base64PsbtQrDecoder(BaseSingleFrameQrDecoder):
""" """
Decodes single frame base64 encoded qr image. Decodes single frame base64 encoded qr image.
@@ -826,106 +823,33 @@ class SeedQrDecoder(BaseSingleFrameQrDecoder):
# TODO: Refactor this to work with the new SettingsDefinition
class SettingsQrDecoder(BaseSingleFrameQrDecoder): class SettingsQrDecoder(BaseSingleFrameQrDecoder):
"""
Decodes settings data from the SettingsQR Generator.
"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.settings = {} self.data = None
self.config_name = None
def add(self, segment, qr_type=QRType.SETTINGS): def add(self, segment, qr_type=QRType.SETTINGS):
# print(f"SettingsQR:\n{segment}") """
try: * Ignores unrecognized settings options.
self.settings = {} * Raises an Exception if a settings value is invalid.
# QR Settings format is space-separated key/value pairs, but should also See `Settings.update()` for info on settings validation, especially for
# parse \n-separated keys. missing settings.
for entry in segment.split(): """
key = entry.split("=")[0].strip() if not segment.startswith("settings::"):
value = entry.split("=")[1].strip() raise Exception("Invalid SettingsQR data")
self.settings[key] = value
# Leave any other parsing or validation up to the Settings class itself.
# SettingsQR are just ascii data to hand it over as-is.
self.data = segment
# Remove values only needed for import self.complete = True
self.settings.pop("type", None) self.collected_segments = 1
version = self.settings.pop("version", None) return DecodeQRStatus.COMPLETE
if not version or int(version) != 1:
raise Exception(f"Settings QR version {version} not supported")
self.config_name = self.settings.pop("name", None)
if self.config_name:
self.config_name = self.config_name.replace("_", " ")
# Have to translate the abbreviated settings into the human-readable values
# used in the normal Settings.
map_abbreviated_enable = {
"0": SettingsConstants.OPTION__DISABLED,
"1": SettingsConstants.OPTION__ENABLED,
"2": SettingsConstants.OPTION__PROMPT,
}
map_abbreviated_sig_types = {
"s": SettingsConstants.SINGLE_SIG,
"m": SettingsConstants.MULTISIG,
}
map_abbreviated_scripts = {
"na": SettingsConstants.NATIVE_SEGWIT,
"ne": SettingsConstants.NESTED_SEGWIT,
"tr": SettingsConstants.TAPROOT,
"cu": SettingsConstants.CUSTOM_DERIVATION,
}
map_abbreviated_coordinators = {
"bw": SettingsConstants.COORDINATOR__BLUE_WALLET,
"sw": SettingsConstants.COORDINATOR__SPARROW,
"sd": SettingsConstants.COORDINATOR__SPECTER_DESKTOP,
}
def convert_abbreviated_value(category, key, abbreviation_map, is_list=False, new_key_name=None):
try:
if key not in self.settings:
print(f"'{key}' not found in settings")
return
value = self.settings[key]
if not is_list:
new_value = abbreviation_map.get(value)
if not new_value:
logger.error(f"No abbreviation map value for \"{value}\" for setting {key}")
return
else:
# `value` is a comma-separated list; yields list of map matches
values = value.split(",")
new_value = []
for v in values:
mapped_value = abbreviation_map.get(v)
if not mapped_value:
logger.error(f"No abbreviation map value for \"{v}\" for setting {key}")
return
new_value.append(mapped_value)
del self.settings[key]
if new_key_name:
key = new_key_name
if category not in self.settings:
self.settings[category] = {}
self.settings[category][key] = new_value
except Exception as e:
logger.exception(e)
return
convert_abbreviated_value("wallet", "coord", map_abbreviated_coordinators, is_list=True, new_key_name="coordinators")
convert_abbreviated_value("features", "xpub", map_abbreviated_enable, new_key_name="xpub_export")
convert_abbreviated_value("features", "sigs", map_abbreviated_sig_types, is_list=True, new_key_name="sig_types")
convert_abbreviated_value("features", "scripts", map_abbreviated_scripts, is_list=True, new_key_name="script_types")
convert_abbreviated_value("features", "xp_det", map_abbreviated_enable, new_key_name="show_xpub_details")
convert_abbreviated_value("features", "passphrase", map_abbreviated_enable)
convert_abbreviated_value("features", "priv_warn", map_abbreviated_enable, new_key_name="show_privacy_warnings")
convert_abbreviated_value("features", "dire_warn", map_abbreviated_enable, new_key_name="show_dire_warnings")
self.complete = True
self.collected_segments = 1
return DecodeQRStatus.COMPLETE
except Exception as e:
logger.exception(e)
return DecodeQRStatus.INVALID
@@ -1091,11 +1015,11 @@ class GenericWalletQrDecoder(BaseSingleFrameQrDecoder):
def get_wallet_descriptor(self): def get_wallet_descriptor(self):
return self.descriptor return self.descriptor
class MultiSigConfigFileQRDecoder(GenericWalletQrDecoder): class MultiSigConfigFileQRDecoder(GenericWalletQrDecoder):
def add(self, segment, qr_type=QRType.WALLET__CONFIGFILE): def add(self, segment, qr_type=QRType.WALLET__CONFIGFILE):
descriptor = DecodeQR.multisig_setup_file_to_descriptor(segment) descriptor = DecodeQR.multisig_setup_file_to_descriptor(segment)
return super().add(descriptor,qr_type=QRType.WALLET__CONFIGFILE) return super().add(descriptor,qr_type=QRType.WALLET__CONFIGFILE)
+74 -25
View File
@@ -2,13 +2,17 @@ import json
import os import os
import platform import platform
from typing import Any, List from typing import List
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
from .singleton import Singleton from .singleton import Singleton
class InvalidSettingsQRData(Exception):
pass
class Settings(Singleton): class Settings(Singleton):
HOSTNAME = platform.uname()[1] HOSTNAME = platform.uname()[1]
@@ -28,11 +32,69 @@ class Settings(Singleton):
# Read persistent settings file, if it exists # Read persistent settings file, if it exists
if os.path.exists(Settings.SETTINGS_FILENAME): if os.path.exists(Settings.SETTINGS_FILENAME):
with open(Settings.SETTINGS_FILENAME) as settings_file: with open(Settings.SETTINGS_FILENAME) as settings_file:
settings.update(json.load(settings_file), disable_missing_entries=False) settings.update(json.load(settings_file))
return cls._instance return cls._instance
@classmethod
def parse_settingsqr(cls, data: str) -> tuple[str, dict]:
"""
Parses SettingsQR data and returns a tuple of (config_name, settings_dict).
The resulting settings config can be applied by calling `Settings.update(settings_dict)`.
"""
if not data.startswith("settings::"):
raise InvalidSettingsQRData()
version = data.split()[0].split("::")[1]
if version != "v1":
raise InvalidSettingsQRData(f"Unsupported SettingsQR version: {version}")
# Start parsing key/value settings at the nth split() index
split_index = 1
# handle optional "name" attr
config_name = None
if "name=" in data.split()[1]:
config_name = data.split("name=")[1].split()[0].replace("_", " ")
split_index += 1
updated_settings = {}
for entry in data.split()[split_index:]:
abbreviated_name, value = entry.split("=")
# Parse multi-value settings; integer-ize where needed
if "," in value:
values_updated = []
for v in value.split(","):
if v.isdigit():
v = int(v)
values_updated.append(v)
value = values_updated
elif value.isdigit():
value = int(value)
# Replace abbreviated name with full attr_name
settings_entry = SettingsDefinition.get_settings_entry_by_abbreviated_name(abbreviated_name)
if not settings_entry:
print(f"Ignoring unrecognized attribute: {abbreviated_name}")
continue
# Validate value(s) against SettingsDefinition's valid options
if type(value) is not list:
values = [value]
else:
values = value
for v in values:
if v not in [opt[0] for opt in settings_entry.selection_options]:
raise InvalidSettingsQRData(f"""{abbreviated_name} = '{v}' is not valid""")
updated_settings[settings_entry.attr_name] = value
return (config_name, updated_settings)
def __str__(self): def __str__(self):
return json.dumps(self._data, indent=4) return json.dumps(self._data, indent=4)
@@ -47,32 +109,22 @@ class Settings(Singleton):
os.fsync(settings_file.fileno()) os.fsync(settings_file.fileno())
def update(self, new_settings: dict, disable_missing_entries: bool = True): def update(self, new_settings: dict):
""" """
* disable_missing_entries: The SettingsQR Generator omits any multiselect Replaces the current settings with the incoming dict.
fields with zero selections or disabled Enabled/Disabled toggles. So if a
field is missing, interpret it as such. But if this is set to False, keep If a setting is missing from `new_settings`:
the existing value for the field; most likely this is a new setting that * Hidden settings that have a value remain as-is.
the user may not have a value for when loading their persistent settings, * All other missing settings are set to their default value.
in which case this would preserve the new field's default value.
""" """
for entry in SettingsDefinition.settings_entries: for entry in SettingsDefinition.settings_entries:
if entry.attr_name not in new_settings: if entry.attr_name not in new_settings:
if not disable_missing_entries: if entry.visibility == SettingsConstants.VISIBILITY__HIDDEN and entry.attr_name in self._data:
# Preserve existing hidden values
new_settings[entry.attr_name] = self._data[entry.attr_name]
else:
# Setting is missing; insert default # Setting is missing; insert default
new_settings[entry.attr_name] = entry.default_value new_settings[entry.attr_name] = entry.default_value
elif entry.visibility == SettingsConstants.VISIBILITY__HIDDEN:
# Missing hidden values always get their default
new_settings[entry.attr_name] = entry.default_value
elif entry.type == SettingsConstants.TYPE__MULTISELECT:
# Clear out the multiselect
new_settings[entry.attr_name] = []
elif entry.type in SettingsConstants.ALL_ENABLED_DISABLED_TYPES:
# Set DISABLED for this missing setting
new_settings[entry.attr_name] = SettingsConstants.OPTION__DISABLED
else: else:
# Clean the incoming data, if necessary # Clean the incoming data, if necessary
@@ -80,9 +132,6 @@ class Settings(Singleton):
if type(new_settings[entry.attr_name]) == str: if type(new_settings[entry.attr_name]) == str:
# Break comma-separated SettingsQR input into List # Break comma-separated SettingsQR input into List
new_settings[entry.attr_name] = new_settings[entry.attr_name].split(",") new_settings[entry.attr_name] = new_settings[entry.attr_name].split(",")
# TODO: If value is not in entry.selection_options...
# Can't just merge the _data dict; have to replace keys they have in common # Can't just merge the _data dict; have to replace keys they have in common
# (otherwise list values will be merged instead of replaced). # (otherwise list values will be merged instead of replaced).
+20 -13
View File
@@ -213,7 +213,6 @@ class SettingsEntry:
category: str category: str
attr_name: str attr_name: str
display_name: str display_name: str
verbose_name: str = None
abbreviated_name: str = None abbreviated_name: str = None
visibility: str = SettingsConstants.VISIBILITY__GENERAL visibility: str = SettingsConstants.VISIBILITY__GENERAL
type: str = SettingsConstants.TYPE__ENABLED_DISABLED type: str = SettingsConstants.TYPE__ENABLED_DISABLED
@@ -229,7 +228,7 @@ class SettingsEntry:
self.selection_options = SettingsConstants.OPTIONS__ENABLED_DISABLED_PROMPT self.selection_options = SettingsConstants.OPTIONS__ENABLED_DISABLED_PROMPT
elif self.type == SettingsConstants.TYPE__ENABLED_DISABLED_PROMPT_REQUIRED: elif self.type == SettingsConstants.TYPE__ENABLED_DISABLED_PROMPT_REQUIRED:
self.selection_options = [SettingsConstants.ALL_OPTIONS] self.selection_options = SettingsConstants.ALL_OPTIONS
# Account for List[tuple] and tuple formats as default_value # Account for List[tuple] and tuple formats as default_value
if type(self.default_value) == list and type(self.default_value[0]) == tuple: if type(self.default_value) == list and type(self.default_value[0]) == tuple:
@@ -248,18 +247,12 @@ class SettingsEntry:
def get_selection_option_value(self, i: int): def get_selection_option_value(self, i: int):
""" Returns the value of the selection option at index `i` """
value = self.selection_options[i] value = self.selection_options[i]
if type(value) == tuple: if type(value) == tuple:
value = value[0] value = value[0]
return value return value
def get_selection_option_display_name(self, i: int) -> str:
value = self.selection_options[i]
if type(value) == tuple:
value = value[1]
return value
def get_selection_option_display_name_by_value(self, value) -> str: def get_selection_option_display_name_by_value(self, value) -> str:
for option in self.selection_options: for option in self.selection_options:
@@ -305,9 +298,8 @@ class SettingsEntry:
return { return {
"category": self.category, "category": self.category,
"attr_name": self.attr_name, "attr_name": self.attr_name,
"display_name": self.display_name,
"verbose_name": self.verbose_name,
"abbreviated_name": self.abbreviated_name, "abbreviated_name": self.abbreviated_name,
"display_name": self.display_name,
"visibility": self.visibility, "visibility": self.visibility,
"type": self.type, "type": self.type,
"help_text": self.help_text, "help_text": self.help_text,
@@ -340,6 +332,7 @@ class SettingsDefinition:
# TODO: Full babel multilanguage support! Until then, type == HIDDEN # TODO: Full babel multilanguage support! Until then, type == HIDDEN
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
attr_name=SettingsConstants.SETTING__LANGUAGE, attr_name=SettingsConstants.SETTING__LANGUAGE,
abbreviated_name="lang",
display_name="Language", display_name="Language",
type=SettingsConstants.TYPE__SELECT_1, type=SettingsConstants.TYPE__SELECT_1,
visibility=SettingsConstants.VISIBILITY__HIDDEN, visibility=SettingsConstants.VISIBILITY__HIDDEN,
@@ -349,6 +342,7 @@ class SettingsDefinition:
# TODO: Support other bip-39 wordlist languages! Until then, type == HIDDEN # TODO: Support other bip-39 wordlist languages! Until then, type == HIDDEN
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
attr_name=SettingsConstants.SETTING__WORDLIST_LANGUAGE, attr_name=SettingsConstants.SETTING__WORDLIST_LANGUAGE,
abbreviated_name="wordlist_lang",
display_name="Mnemonic language", display_name="Mnemonic language",
type=SettingsConstants.TYPE__SELECT_1, type=SettingsConstants.TYPE__SELECT_1,
visibility=SettingsConstants.VISIBILITY__HIDDEN, visibility=SettingsConstants.VISIBILITY__HIDDEN,
@@ -357,12 +351,14 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
attr_name=SettingsConstants.SETTING__PERSISTENT_SETTINGS, attr_name=SettingsConstants.SETTING__PERSISTENT_SETTINGS,
abbreviated_name="persistent",
display_name="Persistent settings", display_name="Persistent settings",
help_text="Store Settings on SD card.", help_text="Store Settings on SD card.",
default_value=SettingsConstants.OPTION__DISABLED), default_value=SettingsConstants.OPTION__DISABLED),
SettingsEntry(category=SettingsConstants.CATEGORY__WALLET, SettingsEntry(category=SettingsConstants.CATEGORY__WALLET,
attr_name=SettingsConstants.SETTING__COORDINATORS, attr_name=SettingsConstants.SETTING__COORDINATORS,
abbreviated_name="coords",
display_name="Coordinator software", display_name="Coordinator software",
type=SettingsConstants.TYPE__MULTISELECT, type=SettingsConstants.TYPE__MULTISELECT,
selection_options=SettingsConstants.ALL_COORDINATORS, selection_options=SettingsConstants.ALL_COORDINATORS,
@@ -375,6 +371,7 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
attr_name=SettingsConstants.SETTING__BTC_DENOMINATION, attr_name=SettingsConstants.SETTING__BTC_DENOMINATION,
abbreviated_name="denom",
display_name="Denomination display", display_name="Denomination display",
type=SettingsConstants.TYPE__SELECT_1, type=SettingsConstants.TYPE__SELECT_1,
selection_options=SettingsConstants.ALL_BTC_DENOMINATIONS, selection_options=SettingsConstants.ALL_BTC_DENOMINATIONS,
@@ -406,6 +403,7 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__SIG_TYPES, attr_name=SettingsConstants.SETTING__SIG_TYPES,
abbreviated_name="sigs",
display_name="Sig types", display_name="Sig types",
type=SettingsConstants.TYPE__MULTISELECT, type=SettingsConstants.TYPE__MULTISELECT,
visibility=SettingsConstants.VISIBILITY__ADVANCED, visibility=SettingsConstants.VISIBILITY__ADVANCED,
@@ -414,6 +412,7 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__SCRIPT_TYPES, attr_name=SettingsConstants.SETTING__SCRIPT_TYPES,
abbreviated_name="scripts",
display_name="Script types", display_name="Script types",
type=SettingsConstants.TYPE__MULTISELECT, type=SettingsConstants.TYPE__MULTISELECT,
visibility=SettingsConstants.VISIBILITY__ADVANCED, visibility=SettingsConstants.VISIBILITY__ADVANCED,
@@ -436,6 +435,7 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__CAMERA_ROTATION, attr_name=SettingsConstants.SETTING__CAMERA_ROTATION,
abbreviated_name="camera",
display_name="Camera rotation", display_name="Camera rotation",
type=SettingsConstants.TYPE__SELECT_1, type=SettingsConstants.TYPE__SELECT_1,
visibility=SettingsConstants.VISIBILITY__ADVANCED, visibility=SettingsConstants.VISIBILITY__ADVANCED,
@@ -450,18 +450,21 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__BIP85_CHILD_SEEDS, attr_name=SettingsConstants.SETTING__BIP85_CHILD_SEEDS,
abbreviated_name="bip85",
display_name="BIP-85 child seeds", display_name="BIP-85 child seeds",
visibility=SettingsConstants.VISIBILITY__ADVANCED, visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__DISABLED), default_value=SettingsConstants.OPTION__DISABLED),
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__PRIVACY_WARNINGS, attr_name=SettingsConstants.SETTING__PRIVACY_WARNINGS,
abbreviated_name="priv_warn",
display_name="Show privacy warnings", display_name="Show privacy warnings",
visibility=SettingsConstants.VISIBILITY__ADVANCED, visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__ENABLED), default_value=SettingsConstants.OPTION__ENABLED),
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__DIRE_WARNINGS, attr_name=SettingsConstants.SETTING__DIRE_WARNINGS,
abbreviated_name="dire_warn",
display_name="Show dire warnings", display_name="Show dire warnings",
visibility=SettingsConstants.VISIBILITY__ADVANCED, visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__ENABLED), default_value=SettingsConstants.OPTION__ENABLED),
@@ -474,6 +477,7 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__PARTNER_LOGOS, attr_name=SettingsConstants.SETTING__PARTNER_LOGOS,
abbreviated_name="partners",
display_name="Show partner logos", display_name="Show partner logos",
visibility=SettingsConstants.VISIBILITY__ADVANCED, visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__ENABLED), default_value=SettingsConstants.OPTION__ENABLED),
@@ -489,6 +493,7 @@ class SettingsDefinition:
# "Hidden" settings with no UI interaction # "Hidden" settings with no UI interaction
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
attr_name=SettingsConstants.SETTING__QR_BRIGHTNESS, attr_name=SettingsConstants.SETTING__QR_BRIGHTNESS,
abbreviated_name="qr_brightness",
display_name="QR background color", display_name="QR background color",
type=SettingsConstants.TYPE__FREE_ENTRY, type=SettingsConstants.TYPE__FREE_ENTRY,
visibility=SettingsConstants.VISIBILITY__HIDDEN, visibility=SettingsConstants.VISIBILITY__HIDDEN,
@@ -513,8 +518,10 @@ class SettingsDefinition:
@classmethod @classmethod
def parse_abbreviated_ini(cls, abbreviated_ini: str) -> dict: def get_settings_entry_by_abbreviated_name(cls, abbreviated_name: str) -> SettingsEntry:
raise Exception("Not implemented, maybe not needed") for entry in cls.settings_entries:
if abbreviated_name in [entry.abbreviated_name, entry.attr_name]:
return entry
@classmethod @classmethod
+4 -32
View File
@@ -1,14 +1,12 @@
import json
import re import re
from embit.descriptor import Descriptor from embit.descriptor import Descriptor
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
from seedsigner.gui.screens import scan_screens from seedsigner.gui.screens import scan_screens
from seedsigner.models import DecodeQR, Seed from seedsigner.models import DecodeQR, Seed
from seedsigner.models.settings import SettingsConstants from seedsigner.models.settings import SettingsConstants
from seedsigner.views.settings_views import SettingsIngestSettingsQRView
from .view import BackStackView, MainMenuView, NotYetImplementedView, View, Destination from seedsigner.views.view import MainMenuView, NotYetImplementedView, View, Destination
@@ -55,13 +53,8 @@ class ScanView(View):
return Destination(PSBTSelectSeedView, skip_current_view=True) return Destination(PSBTSelectSeedView, skip_current_view=True)
elif self.decoder.is_settings: elif self.decoder.is_settings:
from seedsigner.models.settings import Settings data = self.decoder.get_settings_data()
settings = self.decoder.get_settings_data() return Destination(SettingsIngestSettingsQRView, view_args=dict(data=data))
Settings.get_instance().update(new_settings=settings)
print(json.dumps(Settings.get_instance()._data, indent=4))
return Destination(SettingsUpdatedView, {"config_name": self.decoder.get_settings_config_name()})
elif self.decoder.is_wallet_descriptor: elif self.decoder.is_wallet_descriptor:
from seedsigner.views.seed_views import MultisigWalletDescriptorView from seedsigner.views.seed_views import MultisigWalletDescriptorView
@@ -113,24 +106,3 @@ class ScanView(View):
raise Exception("QRCode not recognized or not yet supported.") raise Exception("QRCode not recognized or not yet supported.")
return Destination(MainMenuView) return Destination(MainMenuView)
class SettingsUpdatedView(View):
def __init__(self, config_name: str):
super().__init__()
self.config_name = config_name
def run(self):
from seedsigner.gui.screens.scan_screens import SettingsUpdatedScreen
screen = SettingsUpdatedScreen(config_name=self.config_name)
selected_menu_num = screen.display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
# Only one exit point
return Destination(MainMenuView)
+26 -1
View File
@@ -1,9 +1,12 @@
import logging
from seedsigner.gui.components import SeedSignerCustomIconConstants from seedsigner.gui.components import SeedSignerCustomIconConstants
from .view import View, Destination, MainMenuView from .view import View, Destination, MainMenuView
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, settings_screens) from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, settings_screens)
from seedsigner.models.settings import SettingsConstants, SettingsDefinition from seedsigner.models.settings import Settings, SettingsConstants, SettingsDefinition
logger = logging.getLogger(__name__)
@@ -185,6 +188,28 @@ class SettingsEntryUpdateSelectionView(View):
class SettingsIngestSettingsQRView(View):
def __init__(self, data: str):
super().__init__()
# May raise an Exception which will bubble up to the Controller to display to the
# user.
self.config_name, settings_update_dict = Settings.parse_settingsqr(data)
self.settings.update(settings_update_dict)
def run(self):
from seedsigner.gui.screens.scan_screens import SettingsUpdatedScreen
self.run_screen(
SettingsUpdatedScreen,
config_name=self.config_name
)
# Only one exit point
return Destination(MainMenuView)
"""**************************************************************************** """****************************************************************************
Misc Misc
****************************************************************************""" ****************************************************************************"""
+2 -2
View File
@@ -1,6 +1,6 @@
# Must import test base before the Controller # Must import test base before the Controller
from base import BaseTest, FlowTest, FlowStep from base import BaseTest, FlowTest, FlowStep
from base import FlowTestUnexpectedViewException, FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException from base import FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException
import pytest import pytest
from seedsigner.models.settings import SettingsConstants from seedsigner.models.settings import SettingsConstants
@@ -251,7 +251,7 @@ class TestSeedFlows(FlowTest):
SettingsConstants.SETTING__SIG_TYPES: SettingsConstants.MULTISIG, SettingsConstants.SETTING__SIG_TYPES: SettingsConstants.MULTISIG,
SettingsConstants.SETTING__SCRIPT_TYPES: SettingsConstants.NESTED_SEGWIT, SettingsConstants.SETTING__SCRIPT_TYPES: SettingsConstants.NESTED_SEGWIT,
SettingsConstants.SETTING__COORDINATORS: SettingsConstants.COORDINATOR__SPECTER_DESKTOP, SettingsConstants.SETTING__COORDINATORS: SettingsConstants.COORDINATOR__SPECTER_DESKTOP,
}, disable_missing_entries=False) })
self.run_sequence( self.run_sequence(
initial_destination_view_args=dict(seed_num=0), initial_destination_view_args=dict(seed_num=0),
+91 -2
View File
@@ -1,10 +1,16 @@
import pytest
from base import BaseTest from base import BaseTest
from seedsigner.models.settings import Settings from seedsigner.models.settings import InvalidSettingsQRData, Settings
from seedsigner.models.settings_definition import SettingsConstants from seedsigner.models.settings_definition import SettingsConstants
class TestSettings(BaseTest): class TestSettings(BaseTest):
@classmethod
def setup_class(cls):
cls.settings = Settings.get_instance()
def test_reset_settings(self): def test_reset_settings(self):
""" BaseTest.reset_settings() should wipe out any previous Settings changes """ """ BaseTest.reset_settings() should wipe out any previous Settings changes """
settings = Settings.get_instance() settings = Settings.get_instance()
@@ -14,3 +20,86 @@ class TestSettings(BaseTest):
BaseTest.reset_settings() BaseTest.reset_settings()
settings = Settings.get_instance() settings = Settings.get_instance()
assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
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"""
# 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])
# Now parse the settingsqr_data
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
assert(config_name == settings_name)
self.settings.update(new_settings=settings_update_dict)
# Now verify that the settings were updated correctly
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)
def test_settingsqr_version(self):
""" SettingsQR parser should accept SettingsQR v1 and reject any others """
settingsqr_data = "settings::v1 name=Foo"
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
# Accepts update with no Exceptions
self.settings.update(new_settings=settings_update_dict)
settingsqr_data = "settings::v2 name=Foo"
with pytest.raises(InvalidSettingsQRData) as e:
Settings.parse_settingsqr(settingsqr_data)
assert("Unsupported SettingsQR version" in str(e.value))
# Should also fail if version omitted
settingsqr_data = "settings name=Foo"
with pytest.raises(InvalidSettingsQRData) as e:
Settings.parse_settingsqr(settingsqr_data)
# And if "settings" is omitted entirely
settingsqr_data = "name=Foo"
with pytest.raises(InvalidSettingsQRData) as e:
Settings.parse_settingsqr(settingsqr_data)
def test_settingsqr_ignores_unrecognized_setting(self):
""" SettingsQR parser should ignore unrecognized settings """
settingsqr_data = "settings::v1 name=Foo favorite_food=bacon xpub_export=D"
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
assert("favorite_food" not in settings_update_dict)
assert("xpub_export" in settings_update_dict)
# Accepts update with no Exceptions
self.settings.update(new_settings=settings_update_dict)
def test_settingsqr_fails_unrecognized_option(self):
""" SettingsQR parser should fail if a settings has an unrecognized option """
settingsqr_data = "settings::v1 name=Foo xpub_export=Yep"
with pytest.raises(InvalidSettingsQRData) as e:
Settings.parse_settingsqr(settingsqr_data)
assert("xpub_export" in str(e.value))
def test_settingsqr_parses_line_break_separators(self):
""" SettingsQR parser should read line breaks as acceptable separators """
settingsqr_data = "settings::v1\nname=Foo\nsigs=ss,ms\nscripts=nat,nes,tr\nxpub_export=E\n"
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
assert(len(settings_update_dict.keys()) == 3)
# Accepts update with no Exceptions
self.settings.update(new_settings=settings_update_dict)
+29
View File
@@ -0,0 +1,29 @@
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
class TestSettingsQRDecoder:
def test_decode_settingsqr(self):
"""
Assume the QR reader decodes the SettingsQR content correctly and begin this test
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"""
# Now parse the settings_qr_str
decoder = DecodeQR()
status = decoder.add_data(settings_qr_str)
assert(decoder.is_settings)
assert(status == DecodeQRStatus.COMPLETE)
data = decoder.get_settings_data()
assert(data == settings_qr_str)
def test_settingsqr_version(self):
""" Should fail if the "settings" header is missing """
settings_qr_str = "name=Foo"
decoder = DecodeQR()
status = decoder.add_data(settings_qr_str)
assert(status == DecodeQRStatus.INVALID)