Settings now json; Xpub export flow skipping screens based on Settings

This commit is contained in:
kdmukai
2021-12-01 21:40:43 -06:00
parent fabf355bb3
commit 1a037d7ae1
8 changed files with 182 additions and 220 deletions
-14
View File
@@ -1,14 +0,0 @@
[system]
debug = False
default_language = en
persistent_settings = False
[display]
text_color = ORANGE
camera_rotation = 0
[wallet]
network = main
software = Prompt
qr_density = 2
custom_derivation = m/
-12
View File
@@ -1,17 +1,5 @@
import configparser
import sys
import time
from seedsigner.controller import Controller
config = configparser.ConfigParser()
config.read("settings.ini")
# One-time setup to intialize the one and only Controller
# TODO: Refactor how deep the ConfigParser needs to be passed around.
Controller.configure_instance(config)
# Get the one and only Controller instance and start our main loop
controller = Controller.get_instance()
controller.start()
+39 -34
View File
@@ -11,11 +11,11 @@ from subprocess import call
from threading import Thread
from .models import (EncodeQRDensity, QRType, Seed, SeedStorage, Settings,
ConfigurableSingleton, DecodeQR, DecodeQRStatus, EncodeQR, PSBTParser)
Singleton, DecodeQR, DecodeQRStatus, EncodeQR, PSBTParser)
class Controller(ConfigurableSingleton):
class Controller(Singleton):
"""
The Controller is a globally available singleton that maintains SeedSigner state.
@@ -34,50 +34,49 @@ class Controller(ConfigurableSingleton):
"""
VERSION = "0.5.0"
@classmethod
def configure_instance(cls, config=None):
def get_instance(cls):
from .gui import Renderer
from .helpers import Buttons
from .views import ScreensaverView
# This is the only way to access the one and only instance
if cls._instance is None:
# Instantiate the one and only Controller instance
controller = cls.__new__(cls)
cls._instance = controller
super().configure_instance(config)
# Input Buttons
controller.buttons = Buttons.get_instance()
# Instantiate the one and only Controller instance
controller = cls.__new__(cls)
cls._instance = controller
# models
# TODO: Rename "storage" to something more indicative of its temp, in-memory state
controller.storage = SeedStorage()
controller.settings = Settings.get_instance()
# Input Buttons
controller.buttons = Buttons.get_instance()
# settings
controller.DEBUG = controller.settings.debug
controller.color = controller.settings.text_color
# models
# TODO: Rename "storage" to something more indicative of its temp, in-memory state
controller.storage = SeedStorage()
Settings.configure_instance(config)
controller.settings = Settings.get_instance()
# Configure the Renderer
Renderer.configure_instance({"text_color": controller.color})
# settings
controller.DEBUG = controller.settings.debug
controller.color = controller.settings.text_color
# TODO: Refactor so that we don't need the Renderer here
controller.renderer = Renderer.get_instance()
# Configure the Renderer
Renderer.configure_instance({"text_color": controller.color})
# Views
# controller.menu_view = MenuView()
# controller.seed_tools_view = SeedToolsView()
# controller.io_test_view = IOTestView()
# controller.signing_tools_view = SigningToolsView(controller.storage)
# controller.settings_tools_view = SettingsToolsView()
controller.screensaver = ScreensaverView(controller.buttons)
# TODO: Refactor so that we don't need the Renderer here
controller.renderer = Renderer.get_instance()
controller.back_stack = []
# Views
# controller.menu_view = MenuView()
# controller.seed_tools_view = SeedToolsView()
# controller.io_test_view = IOTestView()
# controller.signing_tools_view = SigningToolsView(controller.storage)
# controller.settings_tools_view = SettingsToolsView()
controller.screensaver = ScreensaverView(controller.buttons)
controller.back_stack = []
# Other behavior constants
controller.screensaver_activation_ms = 120 * 1000
# Other behavior constants
controller.screensaver_activation_ms = 120 * 1000
return cls._instance
@property
@@ -147,6 +146,12 @@ class Controller(ConfigurableSingleton):
next_destination = next_destination.run()
print(f"next_destination: {next_destination}")
if next_destination.skip_current_view:
# Remove the current View from history; it's forwarding us straight
# to the next View.
self.pop_back_stack()
clear_history = next_destination.clear_history
if next_destination.View_cls == BackStackView:
# "Back" arrow was clicked; load the previous view
+23 -14
View File
@@ -662,19 +662,21 @@ class SettingsQR:
def add(self, segment, qr_type=QRType.SETTINGS):
print(f"SettingsQR: {segment}")
try:
self.settings["features"] = {}
self.settings = {}
# QR Settings format is space-separated key/value pairs
for entry in segment.split(" "):
key = entry.split("=")[0].strip()
value = entry.split("=")[1].strip()
self.settings["features"][key] = value
self.settings[key] = value
# Remove values only needed for import
self.settings["features"].pop("type", None)
version = self.settings["features"].pop("version", None)
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["features"].pop("name", None)
self.config_name = self.settings.pop("name", None)
if self.config_name:
self.config_name = self.config_name.replace("_", " ")
@@ -686,9 +688,8 @@ class SettingsQR:
"2": SettingsConstants.OPTION__PROMPT,
}
map_abbreviated_sig_types = {
"s": [SeedConstants.SINGLE_SIG],
"m": [SeedConstants.MULTISIG],
"b": [SeedConstants.SINGLE_SIG, SeedConstants.MULTISIG]
"s": SeedConstants.SINGLE_SIG,
"m": SeedConstants.MULTISIG,
}
map_abbreviated_scripts = {
"na": SeedConstants.NATIVE_SEGWIT,
@@ -696,13 +697,18 @@ class SettingsQR:
"tr": SeedConstants.TAPROOT,
"cu": SeedConstants.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 category not in self.settings or key not in self.settings[category]:
logger.debug(f"{category} / {key} not found in settings")
if key not in self.settings:
logger.debug(f"'{key}' not found in settings")
return
value = self.settings[category][key]
value = self.settings[key]
if not is_list:
new_value = abbreviation_map.get(value)
@@ -710,7 +716,7 @@ class SettingsQR:
logger.error(f"No abbreviation map value for \"{value}\" for setting {key}")
return
else:
# `value` is actually a comma-separated list; yielda list of map matches
# `value` is a comma-separated list; yields list of map matches
values = value.split(",")
new_value = []
for v in values:
@@ -719,16 +725,19 @@ class SettingsQR:
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:
del self.settings[category][key]
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, new_key_name="sig_types")
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", "passphrase", map_abbreviated_enable)
convert_abbreviated_value("features", "priv_warn", map_abbreviated_enable, new_key_name="privacy_warnings")
+93 -106
View File
@@ -1,26 +1,25 @@
import configparser
import json
import os
from embit import bip39
from typing import List
from .seed import SeedConstants
from .singleton import ConfigurableSingleton
from .singleton import Singleton
from .qr_type import QRType
from .encode_qr_density import EncodeQRDensity
class SettingsConstants:
COORDINATOR__SPECTER_DESKTOP = "Specter Desktop"
COORDINATOR__BLUE_WALLET = "BlueWallet"
COORDINATOR__SPARROW = "Sparrow"
COORDINATOR__PROMPT = "Prompt"
COORDINATOR__SPECTER_DESKTOP = "Specter Desktop"
ALL_COORDINATORS = [
COORDINATOR__SPECTER_DESKTOP,
COORDINATOR__BLUE_WALLET,
COORDINATOR__SPARROW,
COORDINATOR__PROMPT,
COORDINATOR__SPECTER_DESKTOP,
]
OPTION__ENABLED = "Enabled"
@@ -35,70 +34,70 @@ class SettingsConstants:
class Settings(ConfigurableSingleton):
class Settings(Singleton):
SETTINGS_FILENAME = "settings.json"
@classmethod
def configure_instance(cls, config: configparser.ConfigParser = None):
super().configure_instance(config)
def get_instance(cls):
# This is the only way to access the one and only instance
if cls._instance is None:
# Instantiate the one and only instance
settings = cls.__new__(cls)
cls._instance = settings
# Instantiate the one and only instance
settings = cls.__new__(cls)
cls._instance = settings
# default internal data structure for settings
settings._data = {
"system": {
"debug": False,
"default_language": "en",
"persistent_settings": False,
"wordlist": bip39.WORDLIST # TODO: Just store the wordlist language
},
"display": {
"text_color": "white",
"background_color": "black",
"camera_rotation": 0,
},
"wallet": {
"network": SeedConstants.MAINNET,
"software": SettingsConstants.COORDINATOR__SPECTER_DESKTOP,
"qr_density": EncodeQRDensity.MEDIUM,
"custom_derivation": "m/"
},
"features": {
"xpub_export": SettingsConstants.OPTION__ENABLED, # ENABLED | DISABLED
"sig_types": SeedConstants.ALL_SIG_TYPES, # [single_sig, multisig]
"script_types": [t["type"] for t in SeedConstants.ALL_SCRIPT_TYPES], # [script_type1, ...]
"passphrase": SettingsConstants.OPTION__PROMPT, # ENABLED | DISABLED | PROMPT
"privacy_warnings": SettingsConstants.OPTION__ENABLED, # ENABLED | DISABLED
"dire_warnings": SettingsConstants.OPTION__ENABLED, # ENABLED | DISABLED
# default internal data structure for settings
settings._data = {
"system": {
"debug": False,
"default_language": "en",
"persistent_settings": False,
},
"display": {
"text_color": "white",
"background_color": "black",
"camera_rotation": 0,
},
"wallet": {
"network": SeedConstants.MAINNET,
"coordinators": SettingsConstants.ALL_COORDINATORS,
"qr_density": EncodeQRDensity.MEDIUM,
"custom_derivation": "m/"
},
"features": {
"xpub_export": SettingsConstants.OPTION__ENABLED, # ENABLED | DISABLED
"sig_types": SeedConstants.ALL_SIG_TYPES, # [single_sig, multisig]
"script_types": [t["type"] for t in SeedConstants.ALL_SCRIPT_TYPES], # [script_type1, ...]
"passphrase": SettingsConstants.OPTION__PROMPT, # ENABLED | DISABLED | PROMPT
"privacy_warnings": SettingsConstants.OPTION__ENABLED, # ENABLED | DISABLED
"dire_warnings": SettingsConstants.OPTION__ENABLED, # ENABLED | DISABLED
}
}
}
settings.init_complete = False
# Read persistent settings, if it exists
if os.path.exists(Settings.SETTINGS_FILENAME):
with open(Settings.SETTINGS_FILENAME) as settings_file:
settings._data.update(json.load(settings_file))
if config is not None:
# read settings.ini typically
settings.__config_to_data(config)
return cls._instance
settings.init_complete = True
def __str__(self):
return json.dumps(self._data, indent=2)
def __config_to_data(self, config: configparser.ConfigParser):
self.persistent = config.getboolean("system", "persistent_settings")
self._data["system"]["debug"] = config.getboolean("system", "debug")
self._data["system"]["default_language"] = config["system"]["default_language"]
self._data["display"]["text_color"] = config["display"]["text_color"]
self._data["display"]["camera_rotation"] = int(config["display"]["camera_rotation"])
self.network = config["wallet"]["network"]
self.software = config["wallet"]["software"]
self.qr_density = int(config["wallet"]["qr_density"])
self.custom_derivation = config["wallet"]["custom_derivation"]
def save(self):
if self._data["system"]["persistent_settings"] == True and self.init_complete == True:
with open(Settings.SETTINGS_FILENAME, 'w') as settings_file:
json.dump(self._data, settings_file)
def update(self, new_settings: dict):
self._data.update(new_settings)
# Can't just merge the _data dict; have to replace keys they have in common
# (otherwise list values will be merged instead of replaced).
for category, category_settings in new_settings.items():
for key, value in category_settings.items():
self._data[category].pop(key, None)
self._data[category][key] = value
### persistent settings handling
@@ -108,42 +107,17 @@ class Settings(ConfigurableSingleton):
return self._data["system"]["persistent_settings"]
@persistent.setter
def persistent(self, value):
def persistent(self, value: bool):
if type(value) == bool:
if value == False and value != self._data["system"]["persistent_settings"]:
# persistence is changed to false, restore defaults
self._data["system"]["persistent_settings"] = value
self.init_complete == False
self.restoreDefault()
self.init_complete == True
self._data["system"]["persistent_settings"] = value
if value:
self.save()
else:
self._data["system"]["persistent_settings"] = value
self.__writeConfig()
# persistence is changed to false, remove SETTINGS_FILE
if os.path.exists(Settings.SETTINGS_FILENAME):
os.remove(Settings.SETTINGS_FILENAME)
else:
raise Exception("Unexpected system.persistent_settings settings.ini value")
def restoreDefault(self):
config = configparser.ConfigParser()
config.read("default_settings.ini")
self.__config_to_data(config)
self.__writeSettingsIni(config)
def __writeSettingsIni(self, config):
with open('settings.ini', 'w') as configfile:
config.write(configfile)
configfile.close()
def __generateConfig(self):
config = configparser.ConfigParser()
config['system'] = self._data['system']
config['display'] = self._data['display']
config['wallet'] = self._data['wallet']
return config
def __writeConfig(self):
if self._data["system"]["persistent_settings"] == True and self.init_complete == True:
config = self.__generateConfig()
self.__writeSettingsIni(config)
raise Exception("Unexpected system.persistent_settings settings.json value")
@property
def persistent_display(self):
@@ -164,7 +138,8 @@ class Settings(ConfigurableSingleton):
@property
def wordlist(self):
return self._data["system"]["wordlist"]
# TODO: Support BIP-39 wordlists in other languages
return bip39.WORDLIST
### display
@@ -177,12 +152,12 @@ class Settings(ConfigurableSingleton):
return self._data["display"]["camera_rotation"]
@camera_rotation.setter
def camera_rotation(self, value):
def camera_rotation(self, value: int):
if value in [0, 90, 180, 270]:
self._data["display"]["camera_rotation"] = value
self.__writeConfig()
self.save()
else:
raise Exception("Unexpected display.camera_rotation settings.ini value")
raise Exception("Unexpected display.camera_rotation settings.json value")
### wallet
@@ -194,9 +169,13 @@ class Settings(ConfigurableSingleton):
def network(self, value):
if value in [SeedConstants.MAINNET, SeedConstants.TESTNET]:
self._data["wallet"]["network"] = value
self.__writeConfig()
self.save()
else:
raise Exception("Unexpected wallet.network settings.ini value")
raise Exception("Unexpected wallet.network settings.json value")
@property
def coordinators(self):
return self._data["wallet"]["coordinators"]
@property
def software(self):
@@ -206,9 +185,9 @@ class Settings(ConfigurableSingleton):
def software(self, value):
if value in SettingsConstants.ALL_COORDINATORS:
self._data["wallet"]["software"] = value
self.__writeConfig()
self.save()
else:
raise Exception("Unexpected wallet.software settings.ini value")
raise Exception("Unexpected wallet.software settings.json value")
@property
def qr_density(self):
@@ -218,9 +197,9 @@ class Settings(ConfigurableSingleton):
def qr_density(self, value):
if value in (EncodeQRDensity.LOW, EncodeQRDensity.MEDIUM, EncodeQRDensity.HIGH, int(EncodeQRDensity.LOW), int(EncodeQRDensity.MEDIUM), int(EncodeQRDensity.HIGH)):
self._data["wallet"]["qr_density"] = int(value)
self.__writeConfig()
self.save()
else:
raise Exception("Unexpected wallet.qr_density settings.ini value")
raise Exception("Unexpected wallet.qr_density settings.json value")
@property
def qr_psbt_type(self):
@@ -259,7 +238,7 @@ class Settings(ConfigurableSingleton):
def custom_derivation(self, value):
# TODO: parse and validate custom derivation path
self._data["wallet"]["custom_derivation"] = value
self.__writeConfig()
self.save()
@staticmethod
def calc_derivation(network, wallet_type, script_type):
@@ -295,17 +274,25 @@ class Settings(ConfigurableSingleton):
# Features
@property
def export_xpub(self):
self._data["features"].get("export_xpub")
def xpub_export(self):
return self._data["features"].get("xpub_export")
@property
def sig_types(self) -> List[str]:
return self._data["features"].get("sig_types")
@property
def script_types(self) -> List[str]:
return self._data["features"].get("script_types")
@property
def passphrase(self):
self._data["features"].get("passphrase")
return self._data["features"].get("passphrase")
@property
def privacy_warnings(self):
self._data["features"].get("privacy_warnings")
return self._data["features"].get("privacy_warnings")
@property
def dire_warnings(self):
self._data["features"].get("dire_warnings")
return self._data["features"].get("dire_warnings")
+26 -26
View File
@@ -56,7 +56,8 @@ class SeedOptionsView(View):
button_data = ["View Seed Words"]
if self.settings.export_xpub == SettingsConstants.OPTION__ENABLED:
print(f"{self.settings.xpub_export} == {SettingsConstants.OPTION__ENABLED}")
if self.settings.xpub_export == SettingsConstants.OPTION__ENABLED:
button_data.append("Export Xpub")
button_data.append("Export Seed as QR")
@@ -72,8 +73,8 @@ class SeedOptionsView(View):
# View seed words
return Destination(ShowSeedWordsWarningView, view_args={"seed_num": self.seed_num})
elif selected_menu_num == 1 and self.settings.export_xpub == SettingsConstants.OPTION__ENABLED:
return Destination(SeedExportXpubSigTypeView, view_args={"seed_num": self.seed_num})
elif selected_menu_num == 1 and self.settings.xpub_export == SettingsConstants.OPTION__ENABLED:
return Destination(SeedExportXpubSigTypeView, view_args={"seed_num": self.seed_num})
elif selected_menu_num == len(button_data) - 1:
# TODO: Export Seed as QR
@@ -120,6 +121,10 @@ class SeedExportXpubSigTypeView(View):
def run(self):
if len(self.settings.sig_types) == 1:
# Nothing to select; skip this screen
return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": self.settings.sig_types[0]}, skip_current_view=True)
screen = LargeButtonScreen(
title="Export Xpub",
button_data=[
@@ -148,6 +153,12 @@ class SeedExportXpubScriptTypeView(View):
def run(self):
args = {"seed_num": self.seed_num, "sig_type": self.sig_type}
if len(self.settings.sig_types) == 1:
# Nothing to select; skip this screen
args["script_type"] = self.settings.script_types[0]
return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True)
screen = ButtonListScreen(
title="Export Xpub",
is_button_text_centered=False,
@@ -156,8 +167,6 @@ class SeedExportXpubScriptTypeView(View):
)
selected_menu_num = screen.display()
args = {"seed_num": self.seed_num, "sig_type": self.sig_type}
if selected_menu_num < len(SeedConstants.ALL_SCRIPT_TYPES):
args["script_type"] = SeedConstants.ALL_SCRIPT_TYPES[selected_menu_num]["type"]
@@ -211,35 +220,26 @@ class SeedExportXpubCoordinatorView(View):
def run(self):
default_coordinator = self.settings.software
# Set up how the list should be ordered
coordinator_list = []
if default_coordinator == SettingsConstants.COORDINATOR__PROMPT:
# Use the default list, but omit "Prompt"
coordinator_list = SettingsConstants.ALL_COORDINATORS[:-1]
else:
# List the selected coordinator first, then the rest (but omit "Prompt")
coordinator_list.append(default_coordinator)
for coordinator in SettingsConstants.ALL_COORDINATORS[:-1]:
if coordinator != default_coordinator:
coordinator_list.append(coordinator)
args = {
"seed_num": self.seed_num,
"sig_type": self.sig_type,
"script_type": self.script_type,
}
if len(self.settings.coordinators) == 1:
# Nothing to select; skip this screen
args["coordinator"] = self.settings.coordinators[0]
return Destination(SeedExportXpubWarningView, view_args=args, skip_current_view=True)
screen = ButtonListScreen(
title="Export Xpub",
is_button_text_centered=False,
is_bottom_list=True,
button_data=coordinator_list,
button_data=self.settings.coordinators,
)
selected_menu_num = screen.display()
if selected_menu_num < len(coordinator_list):
args = {
"seed_num": self.seed_num,
"sig_type": self.sig_type,
"script_type": self.script_type,
"coordinator": SettingsConstants.ALL_COORDINATORS[selected_menu_num],
}
if selected_menu_num < len(self.settings.coordinators):
args["coordinator"] = self.settings.coordinators[selected_menu_num]
return Destination(SeedExportXpubWarningView, view_args=args)
elif selected_menu_num == RET_CODE__BACK_BUTTON:
+1
View File
@@ -97,6 +97,7 @@ class Destination:
"""
View_cls: View # The target View to route to
view_args: dict = None # The input args required to instantiate the target View
skip_current_view: bool = False # The current View is just forwarding; omit current View from history
clear_history: bool = False # Optionally clears the back_stack to prevent "back"
def __str__(self):
-14
View File
@@ -1,14 +0,0 @@
[system]
debug = True
default_language = en
persistent_settings = True
[display]
text_color = ORANGE
camera_rotation = 0
[wallet]
network = main
software = Prompt
qr_density = 2
custom_derivation = m/