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):
|
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__()
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
****************************************************************************"""
|
****************************************************************************"""
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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