Work in progress after merge w/main; Lots of breaking changes in Settings

This commit is contained in:
kdmukai
2022-02-16 11:42:17 -06:00
parent 02ca5ac8b4
commit 225f0988c2
19 changed files with 322 additions and 374 deletions
+3 -3
View File
@@ -2,9 +2,9 @@ embit==0.4.10
numpy==1.21.1
picamera==1.13
Pillow==8.2.0
pyzbar==0.1.8
qrcode==6.1
-e git+https://github.com/kdmukai/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03#egg=pyzbar
qrcode==7.3.1
RPi.GPIO==0.7.0
six==1.16.0
spidev==3.5
-e src/vendor/urtypes
urtypes @ git+https://github.com/jreesun/urtypes.git@e0d0db277ec2339650343eaf7b220fffb9233241
+2 -1
View File
@@ -5,6 +5,7 @@ from picamera import PiCamera
from PIL import Image
from seedsigner.models import Singleton
from seedsigner.helpers import PiVideoStream
from seedsigner.models.settings import SettingsConstants
@@ -19,7 +20,7 @@ class Camera(Singleton):
from seedsigner.models import Settings
if cls._instance is None:
cls._instance = cls.__new__(cls)
cls._instance._camera_rotation = Settings.get_instance().camera_rotation
cls._instance._camera_rotation = Settings.get_instance().get_value(SettingsConstants.SETTING__CAMERA_ROTATION)
return cls._instance
+10 -36
View File
@@ -1,21 +1,7 @@
import os
import sys
import time
import re
from multiprocessing import Process, Queue
from subprocess import call
import os, sys
from embit import bip32, script, ec
from embit.networks import NETWORKS
from embit.descriptor import Descriptor
from binascii import hexlify
from threading import Thread
from embit.psbt import PSBT
from seedsigner.gui.renderer import Renderer
from .models import (EncodeQRDensity, Seed, SeedStorage, Settings,
Singleton, DecodeQR, DecodeQRStatus, EncodeQR, PSBTParser)
from .models import Seed, SeedStorage, Settings, Singleton, PSBTParser
@@ -38,7 +24,7 @@ class Controller(Singleton):
"""
from .helpers import Buttons
VERSION = "0.5.0"
VERSION = "0.5.0 prerelease 1"
# Declare class member vars with type hints to enable richer IDE support throughout
# the code.
@@ -67,30 +53,16 @@ class Controller(Singleton):
# models
# TODO: Rename "storage" to something more indicative of its temp, in-memory state
controller.storage = SeedStorage()
controller.settings = Settings.get_instance()
# Store one working psbt in memory
controller.psbt = None
controller.psbt_parser = None
# settings
# TODO: Remove these
controller.DEBUG = controller.settings.debug
controller.color = controller.settings.text_color
controller.current_bg_qr_color = controller.settings.qr_background_color
# Configure the Renderer
Renderer.configure_instance({"text_color": controller.color})
Renderer.configure_instance()
# TODO: Refactor so that we don't need the Renderer here
controller.renderer = Renderer.get_instance()
# 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 = []
@@ -134,8 +106,8 @@ class Controller(Singleton):
from .views import View, Destination, OpeningSplashView, MainMenuView, BackStackView
# Disabled to save some time during dev...
# opening_splash = OpeningSplashView()
# opening_splash.start()
opening_splash = OpeningSplashView()
opening_splash.start()
""" Class references can be stored as variables in python!
@@ -202,13 +174,14 @@ class Controller(Singleton):
finally:
# Clear the screen when exiting
self.renderer.display_blank_screen()
Renderer.get_instance().display_blank_screen()
def start_screensaver(self):
self.screensaver.start()
"""
### Menu
### Menu View handles navigation within the menu
### Sub Menu's like Seed Tools, Signing Tools, Settings are all in the Menu View
@@ -965,7 +938,7 @@ class Controller(Singleton):
def show_qr_density_tool(self):
r = self.settings_tools_view.display_qr_density_selection()
if r in (EncodeQRDensity.LOW, EncodeQRDensity.MEDIUM, EncodeQRDensity.HIGH):
if r in (EncodeQR.DENSITY__LOW, EncodeQR.DENSITY__MEDIUM, EncodeQR.DENSITY__HIGH):
self.settings.qr_density = r
return Path.SETTINGS_SUB_MENU
@@ -1084,3 +1057,4 @@ class Controller(Singleton):
return Path.MAIN_MENU
"""
+5 -5
View File
@@ -18,25 +18,25 @@ class Renderer(ConfigurableSingleton):
@classmethod
def configure_instance(cls, config=None):
def configure_instance(cls, config={}):
from seedsigner.models.settings import Settings
super().configure_instance(config)
# Instantiate the one and only Renderer instance
renderer = cls.__new__(cls)
cls._instance = renderer
renderer.color = config["text_color"]
# TODO: Use Settings values to wire up diff hardware params
settings = Settings.get_instance()
# Eventually we'll be able to plug in other display controllers
renderer.disp = ST7789()
renderer.canvas_width = renderer.disp.width
renderer.canvas_height = renderer.disp.height
renderer.canvas = Image.new('RGB', (renderer.canvas_width, renderer.canvas_height))
renderer.draw = ImageDraw.Draw(renderer.canvas)
draw = ImageDraw.Draw(renderer.canvas)
def show_image(self, image=None, alpha_overlay=None):
if image == None:
+2 -2
View File
@@ -2,7 +2,7 @@ import time
from dataclasses import dataclass
from PIL import Image, ImageDraw, ImageColor
from typing import List, Tuple
from typing import Any, List, Tuple
from seedsigner.gui.renderer import Renderer
from seedsigner.helpers.threads import BaseThread
@@ -41,7 +41,7 @@ class BaseScreen(BaseComponent):
self.paste_images: List[Tuple] = []
def display(self):
def display(self) -> Any:
try:
with self.renderer.lock:
self._render()
@@ -7,7 +7,7 @@ from seedsigner.models.settings import SettingsConstants
@dataclass
class SettingsEntryUpdateScreen(ButtonListScreen):
class SettingsEntryUpdateSelectionScreen(ButtonListScreen):
display_name: str = None
help_text: str = None
checked_buttons: List[int] = None
-1
View File
@@ -3,7 +3,6 @@ from .singleton import Singleton, ConfigurableSingleton
from .seed import *
from .qr_type import *
from .encode_qr_density import *
from .decode_qr import *
from .encode_qr import *
from .psbt_parser import *
+1
View File
@@ -698,6 +698,7 @@ class SeedQR:
return False
# TODO: Refactor this to work with the new SettingsDefinition
class SettingsQR:
def __init__(self, wordlist=None):
self.total_segments = 1
+22 -18
View File
@@ -7,17 +7,21 @@ from seedsigner.helpers.ur2.cbor_lite import CBOREncoder
from seedsigner.helpers.ur2.ur import UR
from seedsigner.helpers.bcur import (bc32encode, cbor_encode, bcur_encode)
from seedsigner.helpers.qr import QR
from seedsigner.models import Seed, QRType, EncodeQRDensity
from seedsigner.models import Seed, QRType
from urtypes.crypto import PSBT as UR_PSBT
from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP
###
### EncodeQR Class
### Purpose: used to encode psbt for displaying as qr image
###
class EncodeQR:
"""
Encode psbt for displaying as qr image
"""
DENSITY__LOW = "Low"
DENSITY__MEDIUM = "Medium"
DENSITY__HIGH = "High"
ALL_DENSITIES = [DENSITY__LOW, DENSITY__MEDIUM, DENSITY__HIGH]
WORDLIST = None
@@ -57,7 +61,7 @@ class EncodeQR:
raise Exception('Encoder Type Required')
if self.qr_density == None:
self.qr_density = EncodeQRDensity.MEDIUM
self.qr_density = EncodeQR.DENSITY__MEDIUM
self.encoder = None
if self.qr_type == QRType.PSBTSPECTER:
@@ -108,11 +112,11 @@ class UREncodePSBTQR:
qr_ur_bytes = UR("crypto-psbt", UR_PSBT(self.psbt.serialize()).to_cbor())
if qr_density == EncodeQRDensity.LOW:
if qr_density == EncodeQR.DENSITY__LOW:
self.qr_max_fragement_size = 10
elif qr_density == EncodeQRDensity.MEDIUM:
elif qr_density == EncodeQR.DENSITY__MEDIUM:
self.qr_max_fragement_size = 30
elif qr_density == EncodeQRDensity.HIGH:
elif qr_density == EncodeQR.DENSITY__HIGH:
self.qr_max_fragement_size = 120
self.ur2_encode = UREncoder(qr_ur_bytes,self.qr_max_fragement_size,0)
@@ -135,11 +139,11 @@ class SpecterEncodePSBTQR:
self.part_num_sent = 0
self.sent_complete = False
if qr_density == EncodeQRDensity.LOW:
if qr_density == EncodeQR.DENSITY__LOW:
self.qr_max_fragement_size = 40
elif qr_density == EncodeQRDensity.MEDIUM:
elif qr_density == EncodeQR.DENSITY__MEDIUM:
self.qr_max_fragement_size = 65
elif qr_density == EncodeQRDensity.HIGH:
elif qr_density == EncodeQR.DENSITY__HIGH:
self.qr_max_fragement_size = 90
self.__createParts()
@@ -263,11 +267,11 @@ class SpecterXPubQR(XPubQR):
def __init__(self, seed_phrase, passphrase, derivation, network, qr_density, wordlist):
self.qr_max_fragement_size = 65
if qr_density == EncodeQRDensity.LOW:
if qr_density == EncodeQR.DENSITY__LOW:
self.qr_max_fragement_size = 40
elif qr_density == EncodeQRDensity.MEDIUM:
elif qr_density == EncodeQR.DENSITY__MEDIUM:
self.qr_max_fragement_size = 65
elif qr_density == EncodeQRDensity.HIGH:
elif qr_density == EncodeQR.DENSITY__HIGH:
self.qr_max_fragement_size = 90
XPubQR.__init__(self, seed_phrase, passphrase, derivation, network, wordlist)
@@ -323,11 +327,11 @@ class URXPubQR(XPubQR):
XPubQR.__init__(self, seed_phrase, passphrase, derivation, network, wordlist)
if qr_density == EncodeQRDensity.LOW:
if qr_density == EncodeQR.DENSITY__LOW:
self.qr_max_fragement_size = 10
elif qr_density == EncodeQRDensity.MEDIUM:
elif qr_density == EncodeQR.DENSITY__MEDIUM:
self.qr_max_fragement_size = 30
elif qr_density == EncodeQRDensity.HIGH:
elif qr_density == EncodeQR.DENSITY__HIGH:
self.qr_max_fragement_size = 120
def derivation_to_keypath(path: str) -> list:
@@ -1,11 +0,0 @@
from enum import IntEnum
###
### QRType Class IntEum
### Purpose: used with DecodeQR and EncodeQR to communicate qr encoding type
###
class EncodeQRDensity(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
+35 -1
View File
@@ -5,6 +5,7 @@ from embit.psbt import PSBT
from io import BytesIO
from typing import List
from seedsigner.models import Seed
from seedsigner.models.seed import SeedConstants
class PSBTParser():
@@ -178,7 +179,40 @@ class PSBTParser():
return cnt
# checks that all inputs are from the same wallet
@staticmethod
def calc_derivation(network, wallet_type, script_type):
# TODO: Move this to Seed?
if network == SeedConstants.MAINNET:
network_path = "0'"
elif network == SeedConstants.TESTNET:
network_path = "1'"
else:
raise Exception("Unexpected network")
if wallet_type == SeedConstants.SINGLE_SIG:
if script_type == SeedConstants.NATIVE_SEGWIT:
return f"m/84'/{network_path}/0'"
elif script_type == SeedConstants.NESTED_SEGWIT:
return f"m/49'/{network_path}/0'"
elif script_type == SeedConstants.TAPROOT:
return f"m/86'/{network_path}/0'"
else:
raise Exception("Unexpected script type")
elif wallet_type == SeedConstants.MULTISIG:
if script_type == SeedConstants.NATIVE_SEGWIT:
return f"m/48'/{network_path}/0'/2'"
elif script_type == SeedConstants.NESTED_SEGWIT:
return f"m/48'/{network_path}/0'/1'"
elif script_type == SeedConstants.TAPROOT:
raise Exception("Taproot multisig/musig not yet supported")
else:
raise Exception("Unexpected script type")
else:
raise Exception("Unexpected wallet type") # checks that all inputs are from the same wallet
@staticmethod
def __get_policy(scope, scriptpubkey, xpubs):
"""Parse scope and get policy"""
+4 -3
View File
@@ -7,9 +7,10 @@ from typing import List
class SeedConstants:
MAINNET = "main"
TESTNET = "test"
REGTEST = "regtest"
MAINNET = ("main", "Mainnet")
TESTNET = ("test", "Testnet")
REGTEST = ("regtest", "Regtest")
ALL_NETWORKS = [MAINNET, TESTNET, REGTEST]
SINGLE_SIG = "single_sig"
MULTISIG = "multisig"
+130 -170
View File
@@ -3,12 +3,12 @@ import json
import os
from embit import bip39
from typing import List
from typing import Any, List
from .seed import SeedConstants
from .singleton import Singleton
from .qr_type import QRType
from .encode_qr_density import EncodeQRDensity
from .encode_qr import EncodeQR
@@ -17,7 +17,6 @@ class SettingsConstants:
COORDINATOR__BLUE_WALLET = "BlueWallet"
COORDINATOR__SPARROW = "Sparrow"
COORDINATOR__SPECTER_DESKTOP = "Specter Desktop"
ALL_COORDINATORS = [
COORDINATOR__BLUE_WALLET,
COORDINATOR__SPARROW,
@@ -28,7 +27,6 @@ class SettingsConstants:
OPTION__DISABLED = "Disabled"
OPTION__PROMPT = "Prompt"
OPTION__REQUIRED = "Required"
ALL_OPTIONS = [
OPTION__ENABLED,
OPTION__DISABLED,
@@ -36,18 +34,28 @@ class SettingsConstants:
OPTION__REQUIRED,
]
LANGUAGE__ENGLISH = "English"
LANGUAGE__ENGLISH = ("en", "English")
ALL_LANGUAGES = [
LANGUAGE__ENGLISH,
]
ROTATION__0 = (0, "0°")
ROTATION__90 = (90, "90°")
ROTATION__180 = (180, "180°")
ROTATION__270 = (270, "270°")
ALL_ROTATIONS = [
ROTATION__0, ROTATION__90, ROTATION__180, ROTATION__270
]
# Individual settings entry attr_names
SETTING__LANGUAGE = "language"
SETTING__PERSISTENT_SETTINGS = "persistent_settings"
SETTING__COORDINATORS = "coordinators"
SETTING__NETWORK = "network"
SETTING__QR_DENSITY = "qr_density"
SETTING__XPUB_EXPORT = "xpub_export"
SETTING__CAMERA_ROTATION = "camera_rotation"
SETTING__PRIVACY_WARNINGS = "privacy_warnings"
SETTING__DIRE_WARNINGS = "dire_warnings"
@@ -93,22 +101,57 @@ class SettingsEntry:
visibility: str = SettingsConstants.VISIBILITY__GENERAL
type: str = SettingsConstants.TYPE__ENABLED_DISABLED
help_text: str = None
possible_values: List[str] = None
possible_values_abbreviated: List[str] = None
selection_options: List[str] = None
selection_options_abbreviated: List[str] = None
default_value: str = None
def __post_init__(self):
if self.type == SettingsConstants.TYPE__ENABLED_DISABLED:
self.possible_values = [SettingsConstants.OPTION__ENABLED,
self.selection_options = [SettingsConstants.OPTION__ENABLED,
SettingsConstants.OPTION__DISABLED]
elif self.type == SettingsConstants.TYPE__ENABLED_DISABLED_PROMPT:
self.possible_values = [SettingsConstants.OPTION__ENABLED,
self.selection_options = [SettingsConstants.OPTION__ENABLED,
SettingsConstants.OPTION__DISABLED,
SettingsConstants.OPTION__PROMPT]
elif self.type == SettingsConstants.TYPE__ENABLED_DISABLED_PROMPT_REQURIED:
self.possible_values = [SettingsConstants.ALL_OPTIONS]
self.selection_options = [SettingsConstants.ALL_OPTIONS]
@property
def selection_options_display_names(self) -> List[str]:
if type(self.selection_options[0]) == tuple:
return [v[1] for v in self.selection_options]
else:
# Always return a copy so the original can't be altered
return list(self.selection_options)
def get_selection_option_value(self, i: int):
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:
if type(option) == tuple:
option_value = option[0]
display_name = option[1]
else:
option_value = option
display_name = option
if option_value == value:
return display_name
@@ -131,32 +174,62 @@ class SettingsDefinition:
attr_name=SettingsConstants.SETTING__LANGUAGE,
display_name="Language",
type=SettingsConstants.TYPE__SELECT_1,
possible_values=SettingsConstants.ALL_LANGUAGES,
selection_options=SettingsConstants.ALL_LANGUAGES,
default_value=SettingsConstants.LANGUAGE__ENGLISH),
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
attr_name=SettingsConstants.SETTING__PERSISTENT_SETTINGS,
display_name="Persistent Settings",
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,
display_name="Coordinator Software",
display_name="Coordinator software",
type=SettingsConstants.TYPE__MULTISELECT,
possible_values=SettingsConstants.ALL_COORDINATORS,
selection_options=SettingsConstants.ALL_COORDINATORS,
default_value=SettingsConstants.ALL_COORDINATORS),
# Advanced options
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__NETWORK,
display_name="Bitcoin network",
type=SettingsConstants.TYPE__SELECT_1,
visibility=SettingsConstants.VISIBILITY__ADVANCED,
selection_options=SeedConstants.ALL_NETWORKS,
default_value=SeedConstants.MAINNET[0]),
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__QR_DENSITY,
display_name="QR code density",
type=SettingsConstants.TYPE__SELECT_1,
visibility=SettingsConstants.VISIBILITY__ADVANCED,
selection_options=EncodeQR.ALL_DENSITIES,
default_value=EncodeQR.DENSITY__MEDIUM),
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__XPUB_EXPORT,
display_name="Xpub export",
visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__ENABLED),
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__QR_DENSITY,
display_name="QR code density",
type=SettingsConstants.TYPE__SELECT_1,
visibility=SettingsConstants.VISIBILITY__ADVANCED,
selection_options=SettingsConstants.ALL_ROTATIONS,
default_value=SettingsConstants.ROTATION__0),
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__PRIVACY_WARNINGS,
display_name="Show Privacy Warnings",
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,
display_name="Show Dire Warnings",
display_name="Show dire warnings",
visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__ENABLED),
@@ -187,7 +260,7 @@ class SettingsDefinition:
@classmethod
def parse_abbreviated_ini(cls, abbreviated_ini: str) -> dict:
return False
raise Exception("Not implemented, maybe not needed")
@classmethod
@@ -205,12 +278,14 @@ class SettingsDefinition:
@classmethod
def to_json(cls):
return json.dumps({})
raise Exception("Not implemented")
@classmethod
def to_html(cls):
return ""
raise Exception("Not implemented, maybe not needed")
class Settings(Singleton):
SETTINGS_FILENAME = "settings.json"
@@ -240,7 +315,7 @@ class Settings(Singleton):
# "wallet": {
# "network": SeedConstants.MAINNET,
# "coordinators": SettingsConstants.ALL_COORDINATORS,
# "qr_density": EncodeQRDensity.MEDIUM,
# "qr_density": EncodeQR.DENSITY__MEDIUM,
# "custom_derivation": "m/"
# },
# "features": {
@@ -254,7 +329,7 @@ class Settings(Singleton):
# }
# }
# # Read persistent settings, if it exists
# TODO: 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))
@@ -287,43 +362,45 @@ class Settings(Singleton):
self._data[attr_name] = value
# self.save()
# TODO: auto-intialize dict keys as getter properties?
def get_value(self, attr_name: str):
if attr_name not in self._data:
raise Exception(f"Setting for {attr_name} not found")
return self._data[attr_name]
### persistent settings handling
def get_value_display_name(self, attr_name: str) -> str:
if attr_name not in self._data:
raise Exception(f"Setting for {attr_name} not found")
settings_entry = SettingsDefinition.get_settings_entry(attr_name)
if settings_entry.type in [SettingsConstants.TYPE__FREE_ENTRY, SettingsConstants.TYPE__MULTISELECT]:
raise Exception(f"Unsupported SettingsEntry.type: {settings_entry.type}")
return settings_entry.get_selection_option_display_name_by_value(value=self._data[attr_name])
def get_multiselect_value_display_names(self, attr_name: str) -> List[str]:
if attr_name not in self._data:
raise Exception(f"Setting for {attr_name} not found")
settings_entry = SettingsDefinition.get_settings_entry(attr_name)
if settings_entry.type != SettingsConstants.TYPE__MULTISELECT:
raise Exception(f"Unsupported SettingsEntry.type: {settings_entry.type}")
display_names = []
for value in self._data[attr_name]:
display_names.append(settings_entry.get_selection_option_display_name_by_value(value))
return display_names
@property
def persistent_settings(self):
def persistent_settings(self) -> bool:
return self._data[SettingsConstants.SETTING__PERSISTENT_SETTINGS] == SettingsConstants.OPTION__ENABLED
@property
def persistent_display(self):
if self.persistent:
return "Yes"
else:
return "No"
### system
@property
def debug(self):
return False
@property
def language(self):
return "en"
@property
def wordlist(self):
# TODO: Support BIP-39 wordlists in other languages
return bip39.WORDLIST
### display
def debug(self) -> bool:
return self._data[SettingsConstants.SETTING__DEBUG] == SettingsConstants.OPTION__ENABLED
# TODO: Deeper support for GUI-wide color customization
@property
def text_color(self):
from seedsigner.gui.components import GUIConstants
@@ -333,54 +410,9 @@ class Settings(Singleton):
def qr_background_color(self):
return self._data["display"]["qr_background_color"]
@qr_background_color.setter
def qr_background_color(self, value):
self._data["display"]["qr_background_color"] = value
self.__writeConfig()
@property
def camera_rotation(self):
return 0
@camera_rotation.setter
def camera_rotation(self, value: int):
if value in [0, 90, 180, 270]:
self._data["display"]["camera_rotation"] = value
self.save()
else:
raise Exception("Unexpected display.camera_rotation settings.json value")
### wallet
@property
def network(self):
return SeedConstants.MAINNET
@network.setter
def network(self, value):
if value in [SeedConstants.MAINNET, SeedConstants.TESTNET]:
self._data["wallet"]["network"] = value
self.save()
else:
raise Exception("Unexpected wallet.network settings.json value")
@property
def coordinators(self):
return self._data[SettingsConstants.SETTING__COORDINATORS]
@property
def qr_density(self):
return self._data["wallet"]["qr_density"]
@qr_density.setter
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.save()
else:
raise Exception("Unexpected wallet.qr_density settings.json value")
def qr_psbt_type(self, coordinator):
@staticmethod
def qr_psbt_type(coordinator):
if coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
return QRType.PSBTSPECTER
else:
@@ -388,28 +420,17 @@ class Settings(Singleton):
@property
def qr_xpub_type(self):
return Settings.getXPubType(self.coordinators)
return Settings.getXPubType(self._data[SettingsConstants.SETTING__COORDINATORS])
@staticmethod
def getXPubType(software):
if software == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
def getXPubType(coordinators):
if coordinators == [SettingsConstants.COORDINATOR__SPECTER_DESKTOP]:
return QRType.SPECTERXPUBQR
elif software == "Blue Wallet":
elif coordinators == [SettingsConstants.COORDINATOR__BLUE_WALLET]:
return QRType.XPUBQR
else:
return QRType.URXPUBQR
@property
def qr_density_name(self):
if self.qr_density == EncodeQRDensity.LOW:
return "Low"
elif self.qr_density == EncodeQRDensity.MEDIUM:
return "Medium"
elif self.qr_density == EncodeQRDensity.HIGH:
return "High"
else:
return "Unknown"
@property
def custom_derivation(self):
return self._data["wallet"]["custom_derivation"]
@@ -419,64 +440,3 @@ class Settings(Singleton):
# TODO: parse and validate custom derivation path
self._data["wallet"]["custom_derivation"] = value
self.save()
@staticmethod
def calc_derivation(network, wallet_type, script_type):
# TODO: Move this to Seed?
if network == SeedConstants.MAINNET:
network_path = "0'"
elif network == SeedConstants.TESTNET:
network_path = "1'"
else:
raise Exception("Unexpected network")
if wallet_type == SeedConstants.SINGLE_SIG:
if script_type == SeedConstants.NATIVE_SEGWIT:
return f"m/84'/{network_path}/0'"
elif script_type == SeedConstants.NESTED_SEGWIT:
return f"m/49'/{network_path}/0'"
elif script_type == SeedConstants.TAPROOT:
return f"m/86'/{network_path}/0'"
else:
raise Exception("Unexpected script type")
elif wallet_type == SeedConstants.MULTISIG:
if script_type == SeedConstants.NATIVE_SEGWIT:
return f"m/48'/{network_path}/0'/2'"
elif script_type == SeedConstants.NESTED_SEGWIT:
return f"m/48'/{network_path}/0'/1'"
elif script_type == SeedConstants.TAPROOT:
raise Exception("Taproot multisig/musig not yet supported")
else:
raise Exception("Unexpected script type")
else:
raise Exception("Unexpected wallet type")
# Features
@property
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 show_xpub_details(self) -> bool:
return self._data["features"].get("show_xpub_details") == SettingsConstants.OPTION__ENABLED
@property
def passphrase(self):
return "Enabled"
@property
def show_privacy_warnings(self) -> bool:
return self._data["features"].get("show_privacy_warnings") == SettingsConstants.OPTION__ENABLED
@property
def show_dire_warnings(self) -> bool:
return self._data["features"].get("show_dire_warnings") == SettingsConstants.OPTION__ENABLED
+25 -36
View File
@@ -28,8 +28,8 @@ class PSBTSelectSeedView(View):
ENTER_WORDS = "Enter 12/24 words"
button_data = []
for seed in seeds:
button_str = seed.get_fingerprint(self.settings.network)
if not PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=seed, network=self.settings.network):
button_str = seed.get_fingerprint(self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK))
if not PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
# Doesn't look like this seed can sign the current PSBT
button_str += " (?)"
@@ -40,12 +40,11 @@ class PSBTSelectSeedView(View):
button_data.append(SCAN_SEED)
button_data.append(ENTER_WORDS)
screen = ButtonListScreen(
selected_menu_num = ButtonListScreen(
title="Select Signer",
is_button_text_centered=False,
button_data=button_data
)
selected_menu_num = screen.display()
).display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -79,7 +78,7 @@ class PSBTOverviewView(View):
self.controller.psbt_parser = PSBTParser(
self.controller.psbt,
seed=self.controller.psbt_seed,
network=self.controller.settings.network
network=self.controller.settings.get_value(SettingsConstants.SETTING__NETWORK)
)
@@ -112,8 +111,7 @@ class PSBTOverviewView(View):
class PSBTNoChangeWarningView(View):
def run(self):
screen = psbt_screens.PSBTNoChangeWarningScreen()
selected_menu_num = screen.display()
selected_menu_num = psbt_screens.PSBTNoChangeWarningScreen().display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -140,15 +138,14 @@ class PSBTAmountDetailsView(View):
# Should not be able to get here
return Destination(MainMenuView)
screen = PSBTAmountDetailsScreen(
selected_menu_num = PSBTAmountDetailsScreen(
input_amount=psbt_parser.input_amount,
num_inputs=psbt_parser.num_inputs,
spend_amount=psbt_parser.spend_amount,
num_recipients=psbt_parser.num_destinations,
fee_amount=psbt_parser.fee_amount,
change_amount=psbt_parser.change_amount,
)
selected_menu_num = screen.display()
).display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -185,15 +182,13 @@ class PSBTAddressDetailsView(View):
else:
button_data.append("Next")
screen = psbt_screens.PSBTAddressDetailsScreen(
selected_menu_num = psbt_screens.PSBTAddressDetailsScreen(
title=title,
button_data=button_data,
address=psbt_parser.destination_addresses[self.address_num],
amount=psbt_parser.destination_amounts[self.address_num],
is_change=self.is_change,
)
selected_menu_num = screen.display()
).display()
if selected_menu_num == 0:
if self.address_num < len(psbt_parser.destination_addresses) - 1:
@@ -233,10 +228,10 @@ class PSBTChangeDetailsView(View):
# Single-sig verification is easy. We expect to find a single fingerprint
# and derivation path.
print(f"seed fingerprint: {self.controller.psbt_seed.get_fingerprint(self.settings.network)}")
print(f"seed fingerprint: {self.controller.psbt_seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))}")
print(change_data)
if self.controller.psbt_seed.get_fingerprint(self.settings.network) != change_data.get("fingerprint")[0]:
if self.controller.psbt_seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) != change_data.get("fingerprint")[0]:
# TODO: Something is wrong with this psbt(?). Reroute to warning?
return Destination(MainMenuView)
@@ -254,7 +249,7 @@ class PSBTChangeDetailsView(View):
if psbt_parser.num_change_outputs > 1:
title += f" (#{self.change_address_num + 1})"
screen = psbt_screens.PSBTChangeDetailsScreen(
selected_menu_num = psbt_screens.PSBTChangeDetailsScreen(
title=title,
button_data=["Next"],
address=change_data.get("address"),
@@ -263,9 +258,7 @@ class PSBTChangeDetailsView(View):
derivation_path=derivation_path,
is_own_change_addr=is_own_change_addr,
own_addr_index=own_addr_index,
)
selected_menu_num = screen.display()
).display()
if selected_menu_num == 0:
if self.change_address_num < psbt_parser.num_change_outputs - 1:
@@ -290,10 +283,9 @@ class PSBTFinalizeView(View):
# Should not be able to get here
return Destination(MainMenuView)
screen = psbt_screens.PSBTFinalizeScreen(
selected_menu_num = psbt_screens.PSBTFinalizeScreen(
button_data=["Approve PSBT"]
)
selected_menu_num = screen.display()
).display()
if selected_menu_num == 0:
# Sign PSBT
@@ -315,8 +307,8 @@ class PSBTFinalizeView(View):
else:
self.controller.psbt = trimmed_psbt
if len(self.settings.coordinators) == 1:
return Destination(PSBTSignedQRDisplayView, view_args={"coordinator": self.settings.coordinators[0]})
if len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)) == 1:
return Destination(PSBTSignedQRDisplayView, view_args={"coordinator": self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0]})
else:
return Destination(PSBTSelectCoordinatorView)
@@ -327,11 +319,10 @@ class PSBTFinalizeView(View):
class PSBTSelectCoordinatorView(View):
def run(self):
button_data = [c for c in self.settings.coordinators]
screen = psbt_screens.PSBTSelectCoordinatorScreen(
button_data = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)
selected_menu_num = psbt_screens.PSBTSelectCoordinatorScreen(
button_data=button_data
)
selected_menu_num = screen.display()
).display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -349,11 +340,10 @@ class PSBTSignedQRDisplayView(View):
qr_encoder = EncodeQR(
psbt=self.controller.psbt,
qr_type=self.settings.qr_psbt_type(self.coordinator),
qr_density=self.settings.qr_density,
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY),
wordlist=self.settings.wordlist
)
screen = QRDisplayScreen(qr_encoder=qr_encoder)
ret = screen.display()
ret = QRDisplayScreen(qr_encoder=qr_encoder).display()
# We're done with this PSBT. Remove all related data
self.controller.psbt = None
@@ -371,14 +361,13 @@ class PSBTSigningErrorView(View):
# Should not be able to get here
return Destination(MainMenuView)
screen = WarningScreen(
selected_menu_num = WarningScreen(
title="Finalize PSBT",
warning_icon_name="warning",
warning_headline="Signing Failed",
warning_text="",
button_data=["Select Diff Seed"],
)
selected_menu_num = screen.display()
).display()
if selected_menu_num == 0:
return Destination(PSBTSelectSeedView, clear_history=True)
+7 -5
View File
@@ -111,20 +111,22 @@ class SeedOptionsView(View):
button_data = []
if self.controller.psbt:
if not PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.network):
if not PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
# This seed does not seem to be a signer for this PSBT
# TODO: How sure are we? Should disable this entirely if we're 100% sure?
SIGN_PSBT += " (?)"
button_data.append(SIGN_PSBT)
button_data.append(VIEW_WORDS)
if self.settings.xpub_export == SettingsConstants.OPTION__ENABLED:
if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED:
button_data.append(EXPORT_XPUB)
button_data.append(EXPORT_SEEDQR)
screen = seed_screens.SeedOptionsScreen(
button_data=button_data,
fingerprint=self.seed.get_fingerprint(self.settings.network),
fingerprint=self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)),
has_passphrase=self.seed.passphrase is not None
)
selected_menu_num = screen.display()
@@ -139,7 +141,7 @@ class SeedOptionsView(View):
elif button_data[selected_menu_num] == VIEW_WORDS:
return Destination(SeedWordsWarningView, view_args={"seed_num": self.seed_num})
elif button_data[selected_menu_num] == EXPORT_XPUB and self.settings.xpub_export == SettingsConstants.OPTION__ENABLED:
elif button_data[selected_menu_num] == EXPORT_XPUB and self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED:
return Destination(SeedExportXpubSigTypeView, view_args={"seed_num": self.seed_num})
elif button_data[selected_menu_num] == EXPORT_SEEDQR:
@@ -474,7 +476,7 @@ class SeedExportXpubQRDisplayView(View):
derivation=derivation_path,
network=self.settings.network,
qr_type=qr_type,
qr_density=self.settings.qr_density,
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY),
wordlist=self.seed.wordlist
)
+4 -4
View File
@@ -2,7 +2,7 @@
from . import View
from seedsigner.helpers import B, QR
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
from seedsigner.models import EncodeQRDensity
from seedsigner.models import EncodeQR
@@ -58,11 +58,11 @@ class SettingsToolsView(View):
def display_qr_density_selection(self) -> str:
r = self.renderer.display_generic_selection_menu(["... [ Return to Settings ]", "Low", "Medium", "High"], "Which QR Density?")
if r == 2:
return EncodeQRDensity.LOW
return EncodeQR.DENSITY__LOW
elif r == 3:
return EncodeQRDensity.MEDIUM
return EncodeQR.DENSITY__MEDIUM
elif r == 4:
return EncodeQRDensity.HIGH
return EncodeQR.DENSITY__HIGH
else:
return None
+54 -37
View File
@@ -41,13 +41,12 @@ class SettingsMenuView(View):
title = "Dev Options"
next = None
screen = ButtonListScreen(
selected_menu_num = ButtonListScreen(
title=title,
is_button_text_centered=False,
button_data=button_data,
selected_button=selected_button,
)
selected_menu_num = screen.display()
).display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
if self.visibility == SettingsConstants.VISIBILITY__GENERAL:
@@ -61,38 +60,53 @@ class SettingsMenuView(View):
return next
else:
return Destination(SettingsEntryUpdateView, view_args={"attr_name": settings_entries[selected_menu_num].attr_name})
# TODO: Free-entry types (are there any?) will need their own SettingsEntryUpdateFreeEntryView(?).
return Destination(SettingsEntryUpdateSelectionView, view_args={"attr_name": settings_entries[selected_menu_num].attr_name})
class SettingsEntryUpdateView(View):
class SettingsEntryUpdateSelectionView(View):
"""
Handles changes to all selection-type settings (Multiselect, SELECT_1,
Enabled/Disabled, etc).
"""
def __init__(self, attr_name: str):
super().__init__()
self.settings_entry = SettingsDefinition.get_settings_entry(attr_name)
self.selected_button = 0
def run(self):
button_data = self.settings_entry.possible_values
cur_value = self.settings.get_value(self.settings_entry.attr_name)
button_data = []
checked_buttons = []
for i, value in enumerate(button_data):
selected_button = None
for i, value in enumerate(self.settings_entry.selection_options):
print(value)
if type(value) == tuple:
value, display_name = value
else:
display_name = value
button_data.append(display_name)
if (type(cur_value) == list and value in cur_value) or value == cur_value:
checked_buttons.append(i)
screen = settings_screens.SettingsEntryUpdateScreen(
if selected_button is None:
# Highlight the selection (for multiselect highlight the first
# selected option).
selected_button = i
print(f"setting selected_button: {i} - {display_name}")
ret_value = settings_screens.SettingsEntryUpdateSelectionScreen(
display_name=self.settings_entry.display_name,
help_text=self.settings_entry.help_text,
button_data=button_data,
selected_button=self.selected_button,
selected_button=selected_button,
checked_buttons=checked_buttons,
settings_entry_type=self.settings_entry.type,
)
ret_value = screen.display()
).display()
if self.settings_entry.type == SettingsConstants.TYPE__MULTISELECT:
if ret_value == RET_CODE__BACK_BUTTON:
return Destination(
destination = None
settings_menu_view_destination = Destination(
SettingsMenuView,
view_args={
"visibility": self.settings_entry.visibility,
@@ -100,37 +114,40 @@ class SettingsEntryUpdateView(View):
}
)
if ret_value == RET_CODE__BACK_BUTTON:
return settings_menu_view_destination
value = self.settings_entry.get_selection_option_value(ret_value)
if self.settings_entry.type == SettingsConstants.TYPE__FREE_ENTRY:
updated_value = ret_value
destination = settings_menu_view_destination
elif self.settings_entry.type == SettingsConstants.TYPE__MULTISELECT:
updated_value = list(cur_value)
if ret_value not in checked_buttons:
# This is a new selection to add
cur_value.append(self.settings_entry.possible_values[ret_value])
updated_value.append(value)
else:
# This is a de-select to remove
cur_value.remove(self.settings_entry.possible_values[ret_value])
updated_value.remove(value)
else:
# All other types are single selects (e.g. Enabled/Disabled, SELECT_1)
updated_value = value
print(updated_value)
# Save the updated selections
self.settings.set_value(
attr_name=self.settings_entry.attr_name,
value=cur_value
value=updated_value
)
# Multiselects stay in place; re-initialize where in the list we left off
if destination:
return destination
# All selects stay in place; re-initialize where in the list we left off
# TODO: get Screen.scroll_y to pass in?
self.selected_button = ret_value
return self.run()
elif ret_value == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
else:
self.settings.set_value(
attr_name=self.settings_entry.attr_name,
value=self.settings_entry.possible_values[ret_value]
)
return Destination(
SettingsMenuView,
view_args={
"visibility": self.settings_entry.visibility,
"selected_attr": self.settings_entry.attr_name
}
)
-23
View File
@@ -55,7 +55,6 @@ class View:
self.canvas_height = self.renderer.canvas_height
self.buttons = self.controller.buttons
self.color = self.controller.color
def run(self, **kwargs):
@@ -64,28 +63,6 @@ class View:
else:
raise Exception("Must implement in the child class")
###
### Reusable components
###
# TODO: Obviated by seedsigner.gui.components.TopNav
def render_previous_button(self, highlight=False):
# Set up the "back" arrow in the upper left
arrow = "<"
word_font = Fonts.get_font("RobotoCondensed-Bold", 26)
top_padding = -3
bottom_padding = 3
side_padding = 3
tw, th = word_font.getsize(arrow)
self.previous_button_width = tw + 2 * side_padding
if highlight:
font_color = "black"
background_color = self.color
else:
font_color = self.color
background_color = "black"
self.renderer.draw.rectangle((0,0, self.previous_button_width, th + top_padding + bottom_padding), fill=background_color)
self.renderer.draw.text((side_padding, top_padding), arrow, fill=font_color, font=word_font)
@dataclass
+2 -2
View File
@@ -78,7 +78,7 @@ def test_specter_xpub_qr():
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.SPECTERXPUBQR, network="test", derivation="m/48h/1h/0h/2h", qr_density=EncodeQRDensity.LOW, wordlist=bip39.WORDLIST)
e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.SPECTERXPUBQR, network="test", derivation="m/48h/1h/0h/2h", qr_density=EncodeQR.DENSITY__LOW, wordlist=bip39.WORDLIST)
assert e.nextPart() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN"
assert e.nextPart() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo"
@@ -89,7 +89,7 @@ def test_ur_xpub_qr():
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.URXPUBQR, network="test", derivation="m/48h/1h/0h/2h", qr_density=EncodeQRDensity.MEDIUM, wordlist=bip39.WORDLIST)
e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.URXPUBQR, network="test", derivation="m/48h/1h/0h/2h", qr_density=EncodeQR.DENSITY__MEDIUM, wordlist=bip39.WORDLIST)
assert e.nextPart() == "UR:CRYPTO-ACCOUNT/1-4/LPADAACSJKCYYNRDRTYAHDCAOEADCYSSMECPONAOLYTAADMETAADDLOXAXHDCLAOKSRLNLKPUEGYATHPMNRTAAOLAH"
assert e.nextPart() == "UR:CRYPTO-ACCOUNT/2-4/LPAOAACSJKCYYNRDRTYAHDCASNKKGHZMLUZORPVDGUOTECSTTKTOLPCWPTNTLKZTTIZTBEAAHDCXVDTPMYQDLAWNGL"