mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge pull request #394 from kdmukai/settings_qr
SettingsQR: Final changes before official release
This commit is contained in:
@@ -183,6 +183,7 @@ class SettingsUpdatedScreen(ButtonListScreen):
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.button_data = ["Home"]
|
||||
self.show_back_button = False
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
@@ -9,14 +9,12 @@ from embit import psbt, bip39
|
||||
from pyzbar import pyzbar
|
||||
from pyzbar.pyzbar import ZBarSymbol
|
||||
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 seedsigner.helpers.ur2.ur_decoder import URDecoder
|
||||
from seedsigner.models.psbt_parser import PSBTParser
|
||||
|
||||
from . import QRType, Seed
|
||||
from .settings import SettingsConstants
|
||||
from seedsigner.models import QRType, Seed
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -175,12 +173,7 @@ class DecodeQR:
|
||||
|
||||
def get_settings_data(self):
|
||||
if self.is_settings:
|
||||
return self.decoder.settings
|
||||
|
||||
|
||||
def get_settings_config_name(self):
|
||||
if self.is_settings:
|
||||
return self.decoder.config_name
|
||||
return self.decoder.data
|
||||
|
||||
|
||||
def get_address(self):
|
||||
@@ -364,7 +357,7 @@ class DecodeQR:
|
||||
return QRType.BITCOIN_ADDRESS
|
||||
|
||||
# config data
|
||||
if "type=settings" in s:
|
||||
if s.startswith("settings::"):
|
||||
return QRType.SETTINGS
|
||||
|
||||
# Seed
|
||||
@@ -578,6 +571,8 @@ class DecodeQR:
|
||||
|
||||
return descriptor
|
||||
|
||||
|
||||
|
||||
class BaseQrDecoder:
|
||||
def __init__(self):
|
||||
self.total_segments = None
|
||||
@@ -673,6 +668,8 @@ class SpecterPsbtQrDecoder(BaseAnimatedQrDecoder):
|
||||
def parse_segment(self, segment) -> str:
|
||||
return segment.split(" ")[-1].strip()
|
||||
|
||||
|
||||
|
||||
class Base64PsbtQrDecoder(BaseSingleFrameQrDecoder):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Decodes settings data from the SettingsQR Generator.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.settings = {}
|
||||
self.config_name = None
|
||||
self.data = None
|
||||
|
||||
|
||||
def add(self, segment, qr_type=QRType.SETTINGS):
|
||||
# print(f"SettingsQR:\n{segment}")
|
||||
try:
|
||||
self.settings = {}
|
||||
"""
|
||||
* Ignores unrecognized settings options.
|
||||
* Raises an Exception if a settings value is invalid.
|
||||
|
||||
# QR Settings format is space-separated key/value pairs, but should also
|
||||
# parse \n-separated keys.
|
||||
for entry in segment.split():
|
||||
key = entry.split("=")[0].strip()
|
||||
value = entry.split("=")[1].strip()
|
||||
self.settings[key] = value
|
||||
See `Settings.update()` for info on settings validation, especially for
|
||||
missing settings.
|
||||
"""
|
||||
if not segment.startswith("settings::"):
|
||||
raise Exception("Invalid SettingsQR data")
|
||||
|
||||
# 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.settings.pop("type", None)
|
||||
version = self.settings.pop("version", None)
|
||||
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
|
||||
self.complete = True
|
||||
self.collected_segments = 1
|
||||
return DecodeQRStatus.COMPLETE
|
||||
|
||||
|
||||
|
||||
@@ -1091,11 +1015,11 @@ class GenericWalletQrDecoder(BaseSingleFrameQrDecoder):
|
||||
|
||||
def get_wallet_descriptor(self):
|
||||
return self.descriptor
|
||||
|
||||
|
||||
|
||||
|
||||
class MultiSigConfigFileQRDecoder(GenericWalletQrDecoder):
|
||||
|
||||
def add(self, segment, qr_type=QRType.WALLET__CONFIGFILE):
|
||||
descriptor = DecodeQR.multisig_setup_file_to_descriptor(segment)
|
||||
return super().add(descriptor,qr_type=QRType.WALLET__CONFIGFILE)
|
||||
|
||||
|
||||
|
||||
@@ -2,13 +2,17 @@ import json
|
||||
import os
|
||||
import platform
|
||||
|
||||
from typing import Any, List
|
||||
from typing import List
|
||||
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
from .singleton import Singleton
|
||||
|
||||
|
||||
|
||||
class InvalidSettingsQRData(Exception):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Settings(Singleton):
|
||||
HOSTNAME = platform.uname()[1]
|
||||
@@ -28,11 +32,69 @@ class Settings(Singleton):
|
||||
# Read persistent settings file, if it exists
|
||||
if os.path.exists(Settings.SETTINGS_FILENAME):
|
||||
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
|
||||
|
||||
|
||||
@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):
|
||||
return json.dumps(self._data, indent=4)
|
||||
|
||||
@@ -47,32 +109,22 @@ class Settings(Singleton):
|
||||
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
|
||||
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
|
||||
the existing value for the field; most likely this is a new setting that
|
||||
the user may not have a value for when loading their persistent settings,
|
||||
in which case this would preserve the new field's default value.
|
||||
Replaces the current settings with the incoming dict.
|
||||
|
||||
If a setting is missing from `new_settings`:
|
||||
* Hidden settings that have a value remain as-is.
|
||||
* All other missing settings are set to their default value.
|
||||
"""
|
||||
for entry in SettingsDefinition.settings_entries:
|
||||
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
|
||||
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:
|
||||
# Clean the incoming data, if necessary
|
||||
@@ -80,9 +132,6 @@ class Settings(Singleton):
|
||||
if type(new_settings[entry.attr_name]) == str:
|
||||
# Break comma-separated SettingsQR input into List
|
||||
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
|
||||
# (otherwise list values will be merged instead of replaced).
|
||||
|
||||
@@ -213,7 +213,6 @@ class SettingsEntry:
|
||||
category: str
|
||||
attr_name: str
|
||||
display_name: str
|
||||
verbose_name: str = None
|
||||
abbreviated_name: str = None
|
||||
visibility: str = SettingsConstants.VISIBILITY__GENERAL
|
||||
type: str = SettingsConstants.TYPE__ENABLED_DISABLED
|
||||
@@ -229,7 +228,7 @@ class SettingsEntry:
|
||||
self.selection_options = SettingsConstants.OPTIONS__ENABLED_DISABLED_PROMPT
|
||||
|
||||
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
|
||||
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):
|
||||
""" Returns the value of the selection option at index `i` """
|
||||
value = self.selection_options[i]
|
||||
if type(value) == tuple:
|
||||
value = value[0]
|
||||
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:
|
||||
for option in self.selection_options:
|
||||
@@ -305,9 +298,8 @@ class SettingsEntry:
|
||||
return {
|
||||
"category": self.category,
|
||||
"attr_name": self.attr_name,
|
||||
"display_name": self.display_name,
|
||||
"verbose_name": self.verbose_name,
|
||||
"abbreviated_name": self.abbreviated_name,
|
||||
"display_name": self.display_name,
|
||||
"visibility": self.visibility,
|
||||
"type": self.type,
|
||||
"help_text": self.help_text,
|
||||
@@ -340,6 +332,7 @@ class SettingsDefinition:
|
||||
# TODO: Full babel multilanguage support! Until then, type == HIDDEN
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__LANGUAGE,
|
||||
abbreviated_name="lang",
|
||||
display_name="Language",
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__HIDDEN,
|
||||
@@ -349,6 +342,7 @@ class SettingsDefinition:
|
||||
# TODO: Support other bip-39 wordlist languages! Until then, type == HIDDEN
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__WORDLIST_LANGUAGE,
|
||||
abbreviated_name="wordlist_lang",
|
||||
display_name="Mnemonic language",
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__HIDDEN,
|
||||
@@ -357,12 +351,14 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__PERSISTENT_SETTINGS,
|
||||
abbreviated_name="persistent",
|
||||
display_name="Persistent settings",
|
||||
help_text="Store Settings on SD card.",
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__WALLET,
|
||||
attr_name=SettingsConstants.SETTING__COORDINATORS,
|
||||
abbreviated_name="coords",
|
||||
display_name="Coordinator software",
|
||||
type=SettingsConstants.TYPE__MULTISELECT,
|
||||
selection_options=SettingsConstants.ALL_COORDINATORS,
|
||||
@@ -375,6 +371,7 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__BTC_DENOMINATION,
|
||||
abbreviated_name="denom",
|
||||
display_name="Denomination display",
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
selection_options=SettingsConstants.ALL_BTC_DENOMINATIONS,
|
||||
@@ -406,6 +403,7 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__SIG_TYPES,
|
||||
abbreviated_name="sigs",
|
||||
display_name="Sig types",
|
||||
type=SettingsConstants.TYPE__MULTISELECT,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
@@ -414,6 +412,7 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__SCRIPT_TYPES,
|
||||
abbreviated_name="scripts",
|
||||
display_name="Script types",
|
||||
type=SettingsConstants.TYPE__MULTISELECT,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
@@ -436,6 +435,7 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__CAMERA_ROTATION,
|
||||
abbreviated_name="camera",
|
||||
display_name="Camera rotation",
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
@@ -450,18 +450,21 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__BIP85_CHILD_SEEDS,
|
||||
abbreviated_name="bip85",
|
||||
display_name="BIP-85 child seeds",
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__PRIVACY_WARNINGS,
|
||||
abbreviated_name="priv_warn",
|
||||
display_name="Show privacy warnings",
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__DIRE_WARNINGS,
|
||||
abbreviated_name="dire_warn",
|
||||
display_name="Show dire warnings",
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
@@ -474,6 +477,7 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__PARTNER_LOGOS,
|
||||
abbreviated_name="partners",
|
||||
display_name="Show partner logos",
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
@@ -489,6 +493,7 @@ class SettingsDefinition:
|
||||
# "Hidden" settings with no UI interaction
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__QR_BRIGHTNESS,
|
||||
abbreviated_name="qr_brightness",
|
||||
display_name="QR background color",
|
||||
type=SettingsConstants.TYPE__FREE_ENTRY,
|
||||
visibility=SettingsConstants.VISIBILITY__HIDDEN,
|
||||
@@ -513,8 +518,10 @@ class SettingsDefinition:
|
||||
|
||||
|
||||
@classmethod
|
||||
def parse_abbreviated_ini(cls, abbreviated_ini: str) -> dict:
|
||||
raise Exception("Not implemented, maybe not needed")
|
||||
def get_settings_entry_by_abbreviated_name(cls, abbreviated_name: str) -> SettingsEntry:
|
||||
for entry in cls.settings_entries:
|
||||
if abbreviated_name in [entry.abbreviated_name, entry.attr_name]:
|
||||
return entry
|
||||
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from embit.descriptor import Descriptor
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.gui.screens import scan_screens
|
||||
from seedsigner.models import DecodeQR, Seed
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
from .view import BackStackView, MainMenuView, NotYetImplementedView, View, Destination
|
||||
from seedsigner.views.settings_views import SettingsIngestSettingsQRView
|
||||
from seedsigner.views.view import MainMenuView, NotYetImplementedView, View, Destination
|
||||
|
||||
|
||||
|
||||
@@ -55,13 +53,8 @@ class ScanView(View):
|
||||
return Destination(PSBTSelectSeedView, skip_current_view=True)
|
||||
|
||||
elif self.decoder.is_settings:
|
||||
from seedsigner.models.settings import Settings
|
||||
settings = self.decoder.get_settings_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()})
|
||||
data = self.decoder.get_settings_data()
|
||||
return Destination(SettingsIngestSettingsQRView, view_args=dict(data=data))
|
||||
|
||||
elif self.decoder.is_wallet_descriptor:
|
||||
from seedsigner.views.seed_views import MultisigWalletDescriptorView
|
||||
@@ -113,24 +106,3 @@ class ScanView(View):
|
||||
raise Exception("QRCode not recognized or not yet supported.")
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import logging
|
||||
from seedsigner.gui.components import SeedSignerCustomIconConstants
|
||||
|
||||
from .view import View, Destination, MainMenuView
|
||||
|
||||
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
|
||||
****************************************************************************"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Must import test base before the Controller
|
||||
from base import BaseTest, FlowTest, FlowStep
|
||||
from base import FlowTestUnexpectedViewException, FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException
|
||||
from base import FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException
|
||||
|
||||
import pytest
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
@@ -251,7 +251,7 @@ class TestSeedFlows(FlowTest):
|
||||
SettingsConstants.SETTING__SIG_TYPES: SettingsConstants.MULTISIG,
|
||||
SettingsConstants.SETTING__SCRIPT_TYPES: SettingsConstants.NESTED_SEGWIT,
|
||||
SettingsConstants.SETTING__COORDINATORS: SettingsConstants.COORDINATOR__SPECTER_DESKTOP,
|
||||
}, disable_missing_entries=False)
|
||||
})
|
||||
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
|
||||
+91
-2
@@ -1,10 +1,16 @@
|
||||
import pytest
|
||||
from base import BaseTest
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings import InvalidSettingsQRData, Settings
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
|
||||
|
||||
|
||||
class TestSettings(BaseTest):
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.settings = Settings.get_instance()
|
||||
|
||||
|
||||
def test_reset_settings(self):
|
||||
""" BaseTest.reset_settings() should wipe out any previous Settings changes """
|
||||
settings = Settings.get_instance()
|
||||
@@ -14,3 +20,86 @@ class TestSettings(BaseTest):
|
||||
BaseTest.reset_settings()
|
||||
settings = Settings.get_instance()
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user