diff --git a/requirements.txt b/requirements.txt index baf1501c..7055026a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file +urtypes @ git+https://github.com/jreesun/urtypes.git@e0d0db277ec2339650343eaf7b220fffb9233241 diff --git a/src/seedsigner/camera.py b/src/seedsigner/camera.py index 20c457e4..027858aa 100644 --- a/src/seedsigner/camera.py +++ b/src/seedsigner/camera.py @@ -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 diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 33614f77..e88cf47c 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -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 +""" \ No newline at end of file diff --git a/src/seedsigner/gui/renderer.py b/src/seedsigner/gui/renderer.py index c884375c..1f79a7ad 100644 --- a/src/seedsigner/gui/renderer.py +++ b/src/seedsigner/gui/renderer.py @@ -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: diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 3a697d6f..15d0885b 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -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() diff --git a/src/seedsigner/gui/screens/settings_screens.py b/src/seedsigner/gui/screens/settings_screens.py index d0baa32f..0bcaefca 100644 --- a/src/seedsigner/gui/screens/settings_screens.py +++ b/src/seedsigner/gui/screens/settings_screens.py @@ -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 diff --git a/src/seedsigner/models/__init__.py b/src/seedsigner/models/__init__.py index c4843c31..c21cf9cb 100644 --- a/src/seedsigner/models/__init__.py +++ b/src/seedsigner/models/__init__.py @@ -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 * diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 05e2b838..6a789a10 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -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 diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 1a1696a4..493e9c4f 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -7,18 +7,22 @@ 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 def __init__(self, **kwargs): @@ -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: diff --git a/src/seedsigner/models/encode_qr_density.py b/src/seedsigner/models/encode_qr_density.py deleted file mode 100644 index 20da4a27..00000000 --- a/src/seedsigner/models/encode_qr_density.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/seedsigner/models/psbt_parser.py b/src/seedsigner/models/psbt_parser.py index 6470f41e..2d304c97 100644 --- a/src/seedsigner/models/psbt_parser.py +++ b/src/seedsigner/models/psbt_parser.py @@ -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""" diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 89dbf05f..a4d48d91 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -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" diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 740da97b..4f08e52f 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -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, - SettingsConstants.OPTION__DISABLED] + self.selection_options = [SettingsConstants.OPTION__ENABLED, + SettingsConstants.OPTION__DISABLED] elif self.type == SettingsConstants.TYPE__ENABLED_DISABLED_PROMPT: - self.possible_values = [SettingsConstants.OPTION__ENABLED, - SettingsConstants.OPTION__DISABLED, - SettingsConstants.OPTION__PROMPT] + 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 @@ -332,55 +409,10 @@ class Settings(Singleton): @property 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 diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index 2c83e61c..191ebc81 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -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) @@ -347,13 +338,12 @@ class PSBTSignedQRDisplayView(View): def run(self): qr_encoder = EncodeQR( - psbt=self.controller.psbt, - qr_type=self.settings.qr_psbt_type(self.coordinator), - qr_density=self.settings.qr_density, + psbt=self.controller.psbt, + qr_type=self.settings.qr_psbt_type(self.coordinator), + 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) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 92c24a12..3f3c52a7 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -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 ) diff --git a/src/seedsigner/views/settings_tools_view.py b/src/seedsigner/views/settings_tools_view.py index c651ca8a..bf96fc51 100644 --- a/src/seedsigner/views/settings_tools_view.py +++ b/src/seedsigner/views/settings_tools_view.py @@ -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 diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index 09124ebd..90b9f43d 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -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,76 +60,94 @@ 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, + ).display() + + destination = None + settings_menu_view_destination = Destination( + SettingsMenuView, + view_args={ + "visibility": self.settings_entry.visibility, + "selected_attr": self.settings_entry.attr_name + } ) - ret_value = screen.display() - if self.settings_entry.type == SettingsConstants.TYPE__MULTISELECT: - if ret_value == RET_CODE__BACK_BUTTON: - return Destination( - SettingsMenuView, - view_args={ - "visibility": self.settings_entry.visibility, - "selected_attr": self.settings_entry.attr_name - } - ) + 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]) - - # Save the updated selections - self.settings.set_value( - attr_name=self.settings_entry.attr_name, - value=cur_value - ) - # Multiselects 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) + updated_value.remove(value) else: - self.settings.set_value( - attr_name=self.settings_entry.attr_name, - value=self.settings_entry.possible_values[ret_value] - ) + # All other types are single selects (e.g. Enabled/Disabled, SELECT_1) + updated_value = value + + print(updated_value) - return Destination( - SettingsMenuView, - view_args={ - "visibility": self.settings_entry.visibility, - "selected_attr": self.settings_entry.attr_name - } - ) + self.settings.set_value( + attr_name=self.settings_entry.attr_name, + value=updated_value + ) + + 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() diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index a690f081..0edf3e5f 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -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 diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index a2882306..71a51b67 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -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"