From 2bc4b22384abf10062ea32fc07e57a413c775940 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 30 May 2022 22:04:57 -0500 Subject: [PATCH 1/9] Basic framework functional for single sig from seed --- src/seedsigner/controller.py | 4 + src/seedsigner/gui/screens/screen.py | 82 +++++-- src/seedsigner/gui/screens/seed_screens.py | 3 +- src/seedsigner/gui/screens/tools_screens.py | 51 +++- src/seedsigner/hardware/buttons.py | 12 +- src/seedsigner/helpers/embit_utils.py | 95 ++++++++ src/seedsigner/models/encode_qr.py | 23 ++ src/seedsigner/models/psbt_parser.py | 36 --- src/seedsigner/models/seed.py | 10 +- src/seedsigner/views/scan_views.py | 8 +- src/seedsigner/views/seed_views.py | 134 +++++----- src/seedsigner/views/tools_views.py | 257 +++++++++++++++++++- 12 files changed, 565 insertions(+), 150 deletions(-) create mode 100644 src/seedsigner/helpers/embit_utils.py diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index a03a160b..ff7ae4a3 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -69,6 +69,8 @@ class Controller(Singleton): image_entropy_preview_frames: List[Image] = None image_entropy_final_image: Image = None + + address_explorer_data: dict = None # TODO: end refactor section # Destination placeholder for when we need to jump out to a side flow but intend to @@ -77,6 +79,7 @@ class Controller(Singleton): FLOW__PSBT = "psbt" FLOW__VERIFY_MULTISIG_ADDR = "multisig_addr" FLOW__VERIFY_SINGLESIG_ADDR = "singlesig_addr" + FLOW__ADDRESS_EXPLORER = "address_explorer" resume_main_flow: str = None back_stack: BackStack = None @@ -224,6 +227,7 @@ class Controller(Singleton): self.resume_main_flow = None self.multisig_wallet_descriptor = None self.unverified_address = None + self.address_explorer_data = None print(f"back_stack: {self.back_stack}") diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index ba64f741..6ea6fc17 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -5,7 +5,7 @@ from PIL import Image, ImageDraw, ImageColor from typing import Any, List, Tuple from seedsigner.gui.renderer import Renderer -from seedsigner.models.threads import BaseThread +from seedsigner.models.threads import BaseThread, ThreadsafeCounter from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.settings import Settings, SettingsConstants @@ -660,41 +660,71 @@ class LargeButtonScreen(BaseTopNavScreen): class QRDisplayScreen(BaseScreen): qr_encoder: EncodeQR = None + class QRDisplayThread(BaseThread): + def __init__(self, qr_encoder: EncodeQR, qr_brightness: ThreadsafeCounter, renderer: Renderer): + super().__init__() + self.qr_encoder = qr_encoder + self.qr_brightness = qr_brightness + self.renderer = renderer + + + def run(self): + # Loop whether the QR is a single frame or animated; each loop might adjust + # brightness setting. + while self.keep_running: + # convert the self.qr_brightness integer (31-255) into hex triplets + hex_color = (hex(self.qr_brightness.cur_count).split('x')[1]) * 3 + image = self.qr_encoder.next_part_image(240,240, border=2, background_color=hex_color) + with self.renderer.lock: + self.renderer.show_image(image) + + # Target n held frames per second before rendering next QR image + time.sleep(5/30.0) + + + def __post_init__(self): + from seedsigner.models.settings import Settings + super().__post_init__() + + # Shared coordination var so the display thread can detect success + settings = Settings.get_instance() + self.qr_brightness = ThreadsafeCounter(initial_value=settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS)) + + self.threads.append(QRDisplayScreen.QRDisplayThread( + qr_encoder=self.qr_encoder, + qr_brightness=self.qr_brightness, + renderer=self.renderer, + )) + + def _run(self): from seedsigner.models.settings import Settings - settings = Settings.get_instance() - cur_brightness = settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS) - # Loop whether the QR is a single frame or animated; each loop might adjust - # brightness setting. while True: - ret = self._run_callback() - if ret is not None: - return ret - - # convert the cur_brightness integer (31-255) into hex triplets - hex_color = (hex(cur_brightness).split('x')[1]) * 3 - image = self.qr_encoder.next_part_image(240,240, border=2, background_color=hex_color) - self.renderer.show_image(image) - - # Target n held frames per second before rendering next QR image - time.sleep(5/30.0) - - if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_DOWN): + user_input = self.hw_inputs.wait_for( + [ + HardwareButtonsConstants.KEY_UP, + HardwareButtonsConstants.KEY_DOWN, + HardwareButtonsConstants.KEY_LEFT, + HardwareButtonsConstants.KEY_RIGHT, + ] + HardwareButtonsConstants.KEYS__ANYCLICK, + check_release=True, + release_keys=HardwareButtonsConstants.KEYS__ANYCLICK + ) + if user_input == HardwareButtonsConstants.KEY_DOWN: # Reduce QR code background brightness - cur_brightness = max(31, cur_brightness - 31) + self.qr_brightness.set_value(max(31, self.qr_brightness.cur_count - 31)) - elif self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_UP): + elif user_input == HardwareButtonsConstants.KEY_UP: # Incrase QR code background brightness - cur_brightness = min(cur_brightness + 31, 255) + self.qr_brightness.set_value(min(self.qr_brightness.cur_count + 31, 255)) - elif self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_RIGHT): + else: + # Any other input exits the screen + self.threads[-1].stop() break - - settings.set_value(SettingsConstants.SETTING__QR_BRIGHTNESS, cur_brightness) - - # TODO: handle left as BACK + Settings.get_instance().set_value(SettingsConstants.SETTING__QR_BRIGHTNESS, self.qr_brightness.cur_count) diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index b86cbf17..71f3f838 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -436,7 +436,6 @@ class SeedFinalizeScreen(ButtonListScreen): @dataclass class SeedOptionsScreen(ButtonListScreen): # Customize defaults - is_bottom_list: bool = True fingerprint: str = None has_passphrase: bool = False @@ -445,6 +444,8 @@ class SeedOptionsScreen(ButtonListScreen): self.top_nav_icon_color = "blue" self.title = self.fingerprint self.is_button_text_centered = False + self.is_bottom_list = True + super().__post_init__() diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index 2e8b2276..fa13fc41 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -1,11 +1,14 @@ from dataclasses import dataclass +from typing import Any from PIL.Image import Image from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.hardware.camera import Camera -from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, GUIConstants, IconTextLine, SeedSignerCustomIconConstants, TextArea +from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, FormattedAddress, GUIConstants, IconTextLine, SeedSignerCustomIconConstants, TextArea -from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen +from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, QRDisplayScreen from seedsigner.hardware.buttons import HardwareButtonsConstants +from seedsigner.helpers.qr import QR +from seedsigner.models.settings_definition import SettingsConstants @@ -289,3 +292,47 @@ class ToolsCalcFinalWordShowFinalWordScreen(ButtonListScreen): is_text_centered=True, screen_y=self.components[-1].screen_y + self.components[-1].height + 3*GUIConstants.COMPONENT_PADDING, )) + + + +@dataclass +class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen): + fingerprint: str = None + wallet_descriptor: Any = None + script_type: str = None + custom_derivation_path: str = None + + def __post_init__(self): + self.title = "Address Explorer" + self.is_bottom_list = True + super().__post_init__() + + if self.fingerprint: + self.components.append(IconTextLine( + icon_name=SeedSignerCustomIconConstants.FINGERPRINT, + icon_color="blue", + label_text="Fingerprint", + value_text=self.fingerprint, + screen_x=GUIConstants.EDGE_PADDING, + screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, + )) + + if self.script_type: + self.components.append(IconTextLine( + icon_name=SeedSignerCustomIconConstants.PATH, + label_text="Derivation", + value_text="Native Segwit" if self.script_type == SettingsConstants.NATIVE_SEGWIT else "Nested Segwit", + screen_x=GUIConstants.EDGE_PADDING, + screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING, + )) + else: + self.components.append(IconTextLine( + icon_name=SeedSignerCustomIconConstants.PATH, + label_text="Derivation", + value_text=self.custom_derivation_path, + screen_x=GUIConstants.EDGE_PADDING, + screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING, + )) + else: + raise Exception("Implement wallet descriptor support!") + diff --git a/src/seedsigner/hardware/buttons.py b/src/seedsigner/hardware/buttons.py index d1ae20e4..c64d4c1e 100644 --- a/src/seedsigner/hardware/buttons.py +++ b/src/seedsigner/hardware/buttons.py @@ -1,3 +1,4 @@ +from typing import List import RPi.GPIO as GPIO import time @@ -145,10 +146,13 @@ class HardwareButtons(Singleton): HardwareButtonsConstants.release_lock = True return True - def check_for_low(self, key) -> bool: - if self.GPIO.input(key) == self.GPIO.LOW: - self.update_last_input_time() - return True + def check_for_low(self, key: int = None, keys: List[int] = None) -> bool: + if key: + keys = [key] + for key in keys: + if self.GPIO.input(key) == self.GPIO.LOW: + self.update_last_input_time() + return True else: return False diff --git a/src/seedsigner/helpers/embit_utils.py b/src/seedsigner/helpers/embit_utils.py new file mode 100644 index 00000000..9fecbf61 --- /dev/null +++ b/src/seedsigner/helpers/embit_utils.py @@ -0,0 +1,95 @@ +import embit +from embit import bip32 +from embit.bip32 import HDKey +from embit.descriptor import Descriptor +from embit.networks import NETWORKS +from embit.networks import NETWORKS + +from seedsigner.models.settings_definition import SettingsConstants + + +""" + Collection of generic embit-powered util methods. +""" +# TODO: PR these directly into `embit`? Or replace with new/existing methods already in `embit`? + + +def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG, script_type: str = SettingsConstants.NATIVE_SEGWIT) -> str: + if network == SettingsConstants.MAINNET: + network_path = "0'" + elif network == SettingsConstants.TESTNET: + network_path = "1'" + elif network == SettingsConstants.REGTEST: + network_path = "1'" + else: + raise Exception("Unexpected network") + + if wallet_type == SettingsConstants.SINGLE_SIG: + if script_type == SettingsConstants.NATIVE_SEGWIT: + return f"m/84'/{network_path}/0'" + elif script_type == SettingsConstants.NESTED_SEGWIT: + return f"m/49'/{network_path}/0'" + elif script_type == SettingsConstants.TAPROOT: + return f"m/86'/{network_path}/0'" + else: + raise Exception("Unexpected script type") + + elif wallet_type == SettingsConstants.MULTISIG: + if script_type == SettingsConstants.NATIVE_SEGWIT: + return f"m/48'/{network_path}/0'/2'" + elif script_type == SettingsConstants.NESTED_SEGWIT: + return f"m/48'/{network_path}/0'/1'" + elif script_type == SettingsConstants.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 + + + +def get_xpub(seed_bytes, derivation_path: str, embit_network: str = "main") -> HDKey: + root = bip32.HDKey.from_seed(seed_bytes, version=NETWORKS[embit_network]["xprv"]) + xprv = root.derive(derivation_path) + xpub = xprv.to_public() + return xpub + + + +def get_single_sig_address(xpub: HDKey, script_type: str = SettingsConstants.NATIVE_SEGWIT, index: int = 0, is_change: bool = False, embit_network: str = "main") -> str: + if is_change: + pubkey = xpub.derive([1,index]).key + else: + pubkey = xpub.derive([0,index]).key + + if script_type == SettingsConstants.NATIVE_SEGWIT: + return embit.script.p2wpkh(pubkey).address(network=NETWORKS[embit_network]) + + elif script_type == SettingsConstants.NESTED_SEGWIT: + return embit.script.p2sh(embit.script.p2wpkh(pubkey)).address(network=NETWORKS[embit_network]) + + elif script_type == SettingsConstants.LEGACY_P2PKH: + return embit.script.p2pkh(pubkey).address(network=NETWORKS[embit_network]) + + elif script_type == SettingsConstants.TAPROOT: + # TODO: Not yet implemented! + raise Exception("Taproot verification not yet implemented!") + + + +def get_multisig_address(self, descriptor: Descriptor, script_type: str = SettingsConstants.NATIVE_SEGWIT, index: int = 0, is_change: bool = False, embit_network: str = "main"): + if is_change: + branch_index = 1 + else: + branch_index = 0 + + if script_type in [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT]: + return descriptor.derive(index, branch_index=branch_index).script_pubkey().address(network=NETWORKS[embit_network]) + + elif self.script_type == SettingsConstants.LEGACY_P2PKH: + # TODO: Not yet implemented! + raise Exception("Legacy P2PKH verification not yet implemented!") + + elif self.script_type == SettingsConstants.TAPROOT: + # TODO: Not yet implemented! + raise Exception("Taproot verification not yet implemented!") diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index d342597a..37e41848 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -37,6 +37,7 @@ class EncodeQR: qr_type: str = None qr_density: str = SettingsConstants.DENSITY__MEDIUM wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH + bitcoin_address: str = None def __post_init__(self): self.qr = QR() @@ -95,6 +96,9 @@ class EncodeQR: elif self.qr_type == QRType.SEED__COMPACTSEEDQR: self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed_phrase, wordlist_language_code=self.wordlist_language_code) + + elif self.qr_type == QRType.BITCOIN_ADDRESS: + self.encoder = BitcoinAddressEncoder(address=self.bitcoin_address) else: raise Exception('QR Type not supported') @@ -318,6 +322,25 @@ class CompactSeedQrEncoder(SeedQrEncoder): +class BitcoinAddressEncoder(BaseQrEncoder): + def __init__(self, address: str): + super().__init__() + self.address = address + + + def seq_len(self): + return 1 + + + def next_part(self): + return self.address + + + @property + def is_complete(self): + return True + + class XpubQrEncoder(BaseQrEncoder): def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code): self.seed_phrase = seed_phrase diff --git a/src/seedsigner/models/psbt_parser.py b/src/seedsigner/models/psbt_parser.py index af29a882..03444fd8 100644 --- a/src/seedsigner/models/psbt_parser.py +++ b/src/seedsigner/models/psbt_parser.py @@ -189,42 +189,6 @@ class PSBTParser(): return cnt - @staticmethod - def calc_derivation(network, wallet_type, script_type): - # TODO: Move this to Seed? - if network == SettingsConstants.MAINNET: - network_path = "0'" - elif network == SettingsConstants.TESTNET: - network_path = "1'" - elif network == SettingsConstants.REGTEST: - # TODO: Is this right? - network_path = "1'" - else: - raise Exception("Unexpected network") - - if wallet_type == SettingsConstants.SINGLE_SIG: - if script_type == SettingsConstants.NATIVE_SEGWIT: - return f"m/84'/{network_path}/0'" - elif script_type == SettingsConstants.NESTED_SEGWIT: - return f"m/49'/{network_path}/0'" - elif script_type == SettingsConstants.TAPROOT: - return f"m/86'/{network_path}/0'" - else: - raise Exception("Unexpected script type") - - elif wallet_type == SettingsConstants.MULTISIG: - if script_type == SettingsConstants.NATIVE_SEGWIT: - return f"m/48'/{network_path}/0'/2'" - elif script_type == SettingsConstants.NESTED_SEGWIT: - return f"m/48'/{network_path}/0'/1'" - elif script_type == SettingsConstants.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 43a2f4d8..276037a7 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -6,6 +6,7 @@ from embit.networks import NETWORKS from typing import List from seedsigner.models.settings import SettingsConstants +from seedsigner.helpers import embit_utils @@ -106,12 +107,11 @@ class Seed: root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"]) return hexlify(root.child(0).fingerprint).decode('utf-8') + def get_xpub(self, wallet_path: str = '/', network: str = SettingsConstants.MAINNET): - root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"]) - xprv = root.derive(wallet_path) - xpub = xprv.to_public() - return xpub - + return embit_utils.get_xpub(seed_bytes=self.seed_bytes, derivation_path=wallet_path, embit_network=SettingsConstants.map_network_to_embit(network)) + + ### override operators def __eq__(self, other): if isinstance(other, Seed): diff --git a/src/seedsigner/views/scan_views.py b/src/seedsigner/views/scan_views.py index 1376cf1d..d263d261 100644 --- a/src/seedsigner/views/scan_views.py +++ b/src/seedsigner/views/scan_views.py @@ -15,13 +15,13 @@ class ScanView(View): def run(self): from seedsigner.gui.screens.scan_screens import ScanScreen - # Run the live preview and QR code capture process - # TODO: Does this belong in its own BaseThread? wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code) - screen = ScanScreen(decoder=self.decoder) - screen.display() + # Start the live preview and background QR reading + ScanScreen(decoder=self.decoder).display() + + # Handle the results if self.decoder.is_complete: if self.decoder.is_seed: seed_mnemonic = self.decoder.get_seed_phrase() diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 9fb12d43..29778e4d 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -10,6 +10,7 @@ from typing import List from seedsigner.controller import Controller from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants +from seedsigner.helpers import embit_utils from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, seed_screens) from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen @@ -18,7 +19,7 @@ from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.psbt_parser import PSBTParser from seedsigner.models.qr_type import QRType from seedsigner.models.seed import InvalidSeedException, Seed -from seedsigner.models.settings import SettingsConstants +from seedsigner.models.settings import Settings, SettingsConstants from seedsigner.models.settings_definition import SettingsDefinition from seedsigner.models.threads import BaseThread, ThreadsafeCounter from seedsigner.views.psbt_views import PSBTChangeDetailsView @@ -349,11 +350,18 @@ class SeedOptionsView(View): REVIEW_PSBT = "Review PSBT" VERIFY_ADDRESS = "Verify Addr" EXPORT_XPUB = "Export Xpub" + EXPLORER = "Address Explorer" BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT) DISCARD = ("Discard Seed", None, None, "red") button_data = [] + if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: + # Jump straight back into the address explorer script type selection flow + # But do ont cancel the `resume_main_flow` as we'll still need that after + # derivation path is specified. + return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG), skip_current_view=True) + if self.controller.unverified_address: if self.controller.resume_main_flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR: # Jump straight back into the single sig addr verification flow @@ -363,7 +371,7 @@ class SeedOptionsView(View): addr = self.controller.unverified_address["address"][:7] VERIFY_ADDRESS += f" {addr}" button_data.append(VERIFY_ADDRESS) - + if self.controller.psbt: if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)): if self.controller.resume_main_flow and self.controller.resume_main_flow == Controller.FLOW__PSBT: @@ -382,13 +390,14 @@ class SeedOptionsView(View): if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED: button_data.append(EXPORT_XPUB) + button_data.append(EXPLORER) button_data.append(BACKUP) button_data.append(DISCARD) selected_menu_num = seed_screens.SeedOptionsScreen( button_data=button_data, fingerprint=self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)), - has_passphrase=self.seed.passphrase is not None + has_passphrase=self.seed.passphrase is not None, ).display() if selected_menu_num == RET_CODE__BACK_BUTTON: @@ -404,16 +413,20 @@ class SeedOptionsView(View): return Destination(ScanView) elif button_data[selected_menu_num] == VERIFY_ADDRESS: - return Destination(SeedAddressVerificationView, view_args={"seed_num": self.seed_num}) + return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num)) elif button_data[selected_menu_num] == EXPORT_XPUB: - return Destination(SeedExportXpubSigTypeView, view_args={"seed_num": self.seed_num}) + return Destination(SeedExportXpubSigTypeView, view_args=dict(seed_num=self.seed_num)) + + elif button_data[selected_menu_num] == EXPLORER: + self.controller.resume_main_flow = Controller.FLOW__ADDRESS_EXPLORER + return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG)) elif button_data[selected_menu_num] == BACKUP: - return Destination(SeedBackupView, view_args={"seed_num": self.seed_num}) + return Destination(SeedBackupView, view_args=dict(seed_num=self.seed_num)) elif button_data[selected_menu_num] == DISCARD: - return Destination(SeedDiscardView, view_args={"seed_num": self.seed_num}) + return Destination(SeedDiscardView, view_args=dict(seed_num=self.seed_num)) @@ -488,23 +501,37 @@ class SeedExportXpubScriptTypeView(View): def run(self): + from .tools_views import ToolsAddressExplorerAddressTypeView args = {"seed_num": self.seed_num, "sig_type": self.sig_type} if len(self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)) == 1: # Nothing to select; skip this screen args["script_type"] = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)[0] - return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True) + + if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: + del args["sig_type"] + return Destination(ToolsAddressExplorerAddressTypeView, view_args=args, skip_current_view=True) + else: + return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True) button_data = [] for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES): button_data.append(script_type) + + title = "Export Xpub" + if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: + title = "Address Explorer" + selected_menu_num = ButtonListScreen( - title="Export Xpub", + title=title, is_button_text_centered=False, button_data=button_data, + is_bottom_list=True, ).display() - if selected_menu_num < len(button_data): + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + else: script_types_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES) selected_display_name = button_data[selected_menu_num] args["script_type"] = script_types_settings_entry.get_selection_option_value_by_display_name(selected_display_name) @@ -512,10 +539,11 @@ class SeedExportXpubScriptTypeView(View): if args["script_type"] == SettingsConstants.CUSTOM_DERIVATION: return Destination(SeedExportXpubCustomDerivationView, view_args=args) - return Destination(SeedExportXpubCoordinatorView, view_args=args) - - elif selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) + if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: + del args["sig_type"] + return Destination(ToolsAddressExplorerAddressTypeView, view_args=args) + else: + return Destination(SeedExportXpubCoordinatorView, view_args=args) @@ -535,9 +563,13 @@ class SeedExportXpubCustomDerivationView(View): if ret == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - # ret should be the custom derivation path; store it in Settings + # ret will be the custom derivation path custom_derivation = ret + if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: + from .tools_views import ToolsAddressExplorerAddressTypeView + return Destination(ToolsAddressExplorerAddressTypeView, view_args=dict(seed_num=self.seed_num, script_type=self.script_type, custom_derivation=self.custom_derivation_path)) + return Destination( SeedExportXpubCoordinatorView, view_args={ @@ -651,7 +683,7 @@ class SeedExportXpubDetailsView(View): if self.script_type == SettingsConstants.CUSTOM_DERIVATION: derivation_path = self.custom_derivation else: - derivation_path = PSBTParser.calc_derivation( + derivation_path = embit_utils.get_standard_derivation_path( network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), wallet_type=self.sig_type, script_type=self.script_type @@ -1254,7 +1286,7 @@ class AddressVerificationStartView(View): # TODO: detect single sig vs multisig or have to prompt? destination = Destination(NotYetImplementedView) - derivation_path = PSBTParser.calc_derivation( + derivation_path = embit_utils.get_standard_derivation_path( network=self.controller.unverified_address["network"], wallet_type=sig_type, script_type=self.controller.unverified_address["script_type"] @@ -1298,7 +1330,7 @@ class AddressVerificationSigTypeView(View): destination = Destination(LoadMultisigWalletDescriptorView) self.controller.unverified_address["sig_type"] = sig_type - derivation_path = PSBTParser.calc_derivation( + derivation_path = embit_utils.get_standard_derivation_path( network=self.controller.unverified_address["network"], wallet_type=sig_type, script_type=self.controller.unverified_address["script_type"] @@ -1412,7 +1444,7 @@ class SeedAddressVerificationView(View): seed=self.seed, descriptor=self.controller.multisig_wallet_descriptor, script_type=self.script_type, - network=embit_network, + embit_network=embit_network, derivation_path=self.derivation_path, threadsafe_counter=self.threadsafe_counter, verified_index=self.verified_index, @@ -1483,7 +1515,7 @@ class SeedAddressVerificationView(View): class BruteForceAddressVerificationThread(BaseThread): - def __init__(self, address: str, seed: Seed, descriptor: Descriptor, script_type: str, network: str, derivation_path: str, threadsafe_counter: ThreadsafeCounter, verified_index: ThreadsafeCounter, verified_index_is_change: ThreadsafeCounter): + def __init__(self, address: str, seed: Seed, descriptor: Descriptor, script_type: str, embit_network: str, derivation_path: str, threadsafe_counter: ThreadsafeCounter, verified_index: ThreadsafeCounter, verified_index_is_change: ThreadsafeCounter): """ Either seed or descriptor will be None """ @@ -1492,17 +1524,15 @@ class SeedAddressVerificationView(View): self.seed = seed self.descriptor = descriptor self.script_type = script_type - self.network = network + self.embit_network = embit_network self.derivation_path = derivation_path self.threadsafe_counter = threadsafe_counter self.verified_index = verified_index self.verified_index_is_change = verified_index_is_change if self.seed: - root = embit.bip32.HDKey.from_seed(self.seed.seed_bytes, version=network["xprv"]) - xprv = root.derive(self.derivation_path) - self.xpub = xprv.to_public() - + self.xpub = self.seed.get_xpub(wallet_path=self.derivation_path, network=Settings.get_instance().get_value(SettingsConstants.SETTING__NETWORK)) + def run(self): while self.keep_running: @@ -1512,9 +1542,12 @@ class SeedAddressVerificationView(View): i = self.threadsafe_counter.cur_count if self.descriptor: - (receive_address, change_address) = self.derive_multisig(i) + receive_address = embit_utils.get_multisig_address(self.descriptor, self.script_type, index=i, is_change=False, embit_network=self.embit_network) + change_address = embit_utils.get_multisig_address(self.descriptor, self.script_type, index=i, is_change=True, embit_network=self.embit_network) + else: - (receive_address, change_address) = self.derive_single_sig(i) + receive_address = embit_utils.get_single_sig_address(self.xpub, self.script_type, index=i, is_change=False, embit_network=self.embit_network) + change_address = embit_utils.get_single_sig_address(self.xpub, self.script_type, index=i, is_change=True, embit_network=self.embit_network) if self.address == receive_address: self.verified_index.set_value(i) @@ -1530,47 +1563,8 @@ class SeedAddressVerificationView(View): # Increment our index counter self.threadsafe_counter.increment() - - - def derive_single_sig(self, index): - r_pubkey = self.xpub.derive([0,index]).key - c_pubkey = self.xpub.derive([1,index]).key - - receive_address = "" - change_address = "" - - if self.script_type == SettingsConstants.NATIVE_SEGWIT: - receive_address = embit.script.p2wpkh(r_pubkey).address(network=self.network) - change_address = embit.script.p2wpkh(c_pubkey).address(network=self.network) - elif self.script_type == SettingsConstants.NESTED_SEGWIT: - receive_address = embit.script.p2sh(embit.script.p2wpkh(r_pubkey)).address(network=self.network) - change_address = embit.script.p2sh(embit.script.p2wpkh(c_pubkey)).address(network=self.network) - elif self.script_type == SettingsConstants.LEGACY_P2PKH: - receive_address = embit.script.p2pkh(r_pubkey).address(network=self.network) - change_address = embit.script.p2pkh(c_pubkey).address(network=self.network) - elif self.script_type == SettingsConstants.TAPROOT: - # TODO: Not yet implemented! - raise Exception("Taproot verification not yet implemented!") - - return (receive_address, change_address) - def derive_multisig(self, index): - if self.script_type in [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT]: - receive_address = self.descriptor.derive(index, branch_index=0).script_pubkey().address(network=self.network) - change_address = self.descriptor.derive(index, branch_index=1).script_pubkey().address(network=self.network) - - elif self.script_type == SettingsConstants.LEGACY_P2PKH: - # TODO: Not yet implemented! - raise Exception("Taproot verification not yet implemented!") - - elif self.script_type == SettingsConstants.TAPROOT: - # TODO: Not yet implemented! - raise Exception("Taproot verification not yet implemented!") - - return (receive_address, change_address) - - class AddressVerificationSuccessView(View): def __init__(self, seed_num: int): @@ -1634,6 +1628,7 @@ class MultisigWalletDescriptorView(View): RETURN = "Return to PSBT" VERIFY = "Verify Addr" + EXPLORER = "Address Explorer" OK = "OK" button_data = [OK] @@ -1643,6 +1638,8 @@ class MultisigWalletDescriptorView(View): elif self.controller.resume_main_flow == Controller.FLOW__VERIFY_MULTISIG_ADDR and self.controller.unverified_address: VERIFY += f""" {self.controller.unverified_address["address"][:7]}""" button_data = [VERIFY] + elif self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: + button_data = [EXPLORER] selected_menu_num = seed_screens.MultisigWalletDescriptorScreen( policy=policy, @@ -1664,4 +1661,9 @@ class MultisigWalletDescriptorView(View): # TODO: Route properly when multisig brute-force addr verification is done return Destination(SeedAddressVerificationView) + elif button_data[selected_menu_num] == EXPLORER: + self.controller.resume_main_flow = None + # TODO: Finish implementation + return Destination(NotYetImplementedView) + return Destination(MainMenuView) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 8c6b2216..0d2a9a28 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -4,15 +4,19 @@ import time from PIL import Image from PIL.ImageOps import autocontrast +from seedsigner.controller import Controller +from seedsigner.gui.screens.screen import LoadingScreenThread, QRDisplayScreen from seedsigner.hardware.camera import Camera -from seedsigner.gui.components import FontAwesomeIconConstants +from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerCustomIconConstants from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen) -from seedsigner.gui.screens.tools_screens import ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordShowFinalWordScreen -from seedsigner.helpers import mnemonic_generation +from seedsigner.gui.screens.tools_screens import ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordShowFinalWordScreen, ToolsAddressExplorerAddressTypeScreen +from seedsigner.helpers import embit_utils, mnemonic_generation +from seedsigner.models.encode_qr import EncodeQR +from seedsigner.models.qr_type import QRType from seedsigner.models.seed import Seed from seedsigner.models.settings_definition import SettingsConstants -from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedWordsWarningView +from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedWordsWarningView, SeedExportXpubScriptTypeView from .view import View, Destination, BackStackView @@ -23,7 +27,8 @@ class ToolsMenuView(View): IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA) DICE = ("New seed", FontAwesomeIconConstants.DICE) KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD) - button_data = [IMAGE, DICE, KEYBOARD] + EXPLORER = "Address Explorer" + button_data = [IMAGE, DICE, KEYBOARD, EXPLORER] screen = ButtonListScreen( title="Tools", is_button_text_centered=False, @@ -43,6 +48,10 @@ class ToolsMenuView(View): elif button_data[selected_menu_num] == KEYBOARD: return Destination(ToolsCalcFinalWordNumWordsView) + elif button_data[selected_menu_num] == EXPLORER: + return Destination(ToolsAddressExplorerSelectSourceView) + + """**************************************************************************** @@ -200,7 +209,7 @@ class ToolsDiceEntropyEntryView(View): def run(self): ret = ToolsDiceEntropyEntryScreen( - total_rolls=self.total_rolls, + return_after_n_chars=self.total_rolls, ).display() if ret == RET_CODE__BACK_BUTTON: @@ -276,3 +285,239 @@ class ToolsCalcFinalWordShowFinalWordView(View): elif button_data[selected_menu_num] == DISCARD: return Destination(SeedDiscardView) + + +"""**************************************************************************** + Address Explorer Views +****************************************************************************""" +class ToolsAddressExplorerSelectSourceView(View): + def run(self): + SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE) + SCAN_DESCRIPTOR = ("Scan wallet descriptor", FontAwesomeIconConstants.QRCODE) + TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + button_data = [] + + seeds = self.controller.storage.seeds + for seed in seeds: + button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) + + if seed.passphrase is not None: + # TODO: Include lock icon on right side of button + pass + button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue")) + + button_data.append(SCAN_SEED) + button_data.append(SCAN_DESCRIPTOR) + button_data.append(TYPE_12WORD) + button_data.append(TYPE_24WORD) + + selected_menu_num = ButtonListScreen( + title="Address Explorer", + button_data=button_data, + is_button_text_centered=False, + is_bottom_list=True, + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + # Most of the options require us to go through a side flow(s) before we can + # continue to the address explorer. Set the Controller-level flow so that it + # knows to re-route us once the side flow is complete. + self.controller.resume_main_flow = Controller.FLOW__ADDRESS_EXPLORER + + if len(seeds) > 0 and selected_menu_num < len(seeds): + # User selected one of the n seeds + return Destination( + SeedExportXpubScriptTypeView, + view_args=dict( + seed_num=selected_menu_num, + sig_type=SettingsConstants.SINGLE_SIG, + ) + ) + + elif button_data[selected_menu_num] in [SCAN_SEED, SCAN_DESCRIPTOR]: + from seedsigner.views.scan_views import ScanView + return Destination(ScanView) + + elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]: + from seedsigner.views.seed_views import SeedMnemonicEntryView + if button_data[selected_menu_num] == TYPE_12WORD: + self.controller.storage.init_pending_mnemonic(num_words=12) + else: + self.controller.storage.init_pending_mnemonic(num_words=24) + return Destination(SeedMnemonicEntryView) + + + +class ToolsAddressExplorerAddressTypeView(View): + def __init__(self, seed_num: int = None, script_type: str = None, custom_derivation: str = None): + """ + If the explorer source is a seed, `seed_num` and `script_type` must be + specified. `custom_derivation` can be specified as needed. + + If the source is a multisig or single sig wallet descriptor, `seed_num`, + `script_type`, and `custom_derivation` should be `None`. + """ + super().__init__() + self.seed_num = seed_num + self.script_type = script_type + self.custom_derivation = custom_derivation + + network = self.settings.get_value(SettingsConstants.SETTING__NETWORK) + + # Store everything in the Controller's `address_explorer_data` so we don't have + # to keep passing vals around from View to View and recalculating. + data = dict( + seed_num=seed_num, + network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), + embit_network=SettingsConstants.map_network_to_embit(network), + script_type=script_type, + ) + if self.seed_num is not None: + self.seed = self.controller.storage.seeds[seed_num] + data["seed_num"] = self.seed + + if self.script_type == SettingsConstants.CUSTOM_DERIVATION: + derivation_path = self.custom_derivation + else: + derivation_path = embit_utils.get_standard_derivation_path( + network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), + wallet_type=SettingsConstants.SINGLE_SIG, + script_type=self.script_type, + ) + + data["derivation_path"] = derivation_path + data["xpub"] = self.seed.get_xpub(derivation_path, network=network) + + else: + # TODO: + data["wallet_descriptor"] = None + raise Exception("Not yet implemented") + + self.controller.address_explorer_data = data + + + def run(self): + # embit_utils.get_single_sig_address(xpub=xpub, script_type=self.script_type, index=0, is_change=True, embit_network=embit_network) + data = self.controller.address_explorer_data + + RECEIVE = "Receive Addresses" + CHANGE = "Change Addresses" + button_data = [RECEIVE, CHANGE] + + selected_menu_num = ToolsAddressExplorerAddressTypeScreen( + button_data=button_data, + fingerprint=self.seed.get_fingerprint() if self.seed else None, + wallet_descriptor=data["wallet_descriptor"] if "wallet_descriptor" in data else None, + script_type=data["script_type"] if "script_type" in data else None, + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + elif button_data[selected_menu_num] in [RECEIVE, CHANGE]: + return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=button_data[selected_menu_num] == CHANGE)) + + + +class ToolsAddressExplorerAddressListView(View): + def __init__(self, is_change: bool = False, start_index: int = 0, selected_button_index: int = 0): + super().__init__() + self.is_change = is_change + self.start_index = start_index + self.selected_button_index = selected_button_index + self.loading_screen = LoadingScreenThread(text="Calculating addrs...") + + + def run(self): + self.loading_screen.start() + + try: + addresses = [] + button_data = [] + data = self.controller.address_explorer_data + addrs_per_screen = 10 + + addr_storage_key = "receive_addrs" + if self.is_change: + addr_storage_key = "change_addrs" + + if addr_storage_key in data and len(data[addr_storage_key]) >= self.start_index + addrs_per_screen: + # We already calculated this range addresses; just retrieve them + addresses = data[addr_storage_key][self.start_index:self.start_index + addrs_per_screen] + + else: + if addr_storage_key not in data: + data[addr_storage_key] = [] + + if "xpub" in data: + # Single sig explore from seed + if "script_type" in data: + # Standard derivation path + for i in range(self.start_index, self.start_index + addrs_per_screen): + address = embit_utils.get_single_sig_address(xpub=data["xpub"], script_type=data["script_type"], index=i, is_change=self.is_change, embit_network=data["embit_network"]) + addresses.append(address) + data[addr_storage_key].append(address) + else: + # Custom derivation path + raise Exception("Not yet implemented") + + for i, address in enumerate(addresses): + cur_index = i + self.start_index + if cur_index < 10: + end_digits = -6 + elif cur_index < 100: + end_digits = -5 + else: + end_digits = -4 + button_data.append(f"{cur_index}:{address[:8]}...{address[end_digits:]}") + + button_data.append(("Next {}".format(addrs_per_screen), None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)) + + screen = ButtonListScreen( + title="{} Addrs".format("Receive" if not self.is_change else "Change"), + button_data=button_data, + button_font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, + button_font_size=GUIConstants.BUTTON_FONT_SIZE + 4, + is_button_text_centered=False, + is_bottom_list=True, + selected_button=self.selected_button_index, + ) + finally: + # Everything is set. Stop the loading screen + self.loading_screen.stop() + + selected_menu_num = screen.display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + if selected_menu_num == len(addresses): + # User clicked NEXT + return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index + addrs_per_screen)) + + index = selected_menu_num + self.start_index + return Destination(ToolsAddressExplorerAddressView, view_args=dict(index=index, address=addresses[selected_menu_num], is_change=self.is_change, start_index=self.start_index), skip_current_view=True) + + + +class ToolsAddressExplorerAddressView(View): + def __init__(self, index: int, address: str, is_change: bool, start_index: int): + super().__init__() + self.index = index + self.address = address + self.is_change = is_change + self.start_index = start_index + + data = self.controller.address_explorer_data + + def run(self): + qr_encoder = EncodeQR(qr_type=QRType.BITCOIN_ADDRESS, bitcoin_address=self.address) + QRDisplayScreen( + qr_encoder=qr_encoder, + ).display() + + # Exiting/Cancelling the QR display screen always returns to the list + return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index, selected_button_index=self.index - self.start_index), skip_current_view=True) From c8a433acbdd0fe24bbbd7c654cb59ecf1fe6080e Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 31 May 2022 07:58:09 -0500 Subject: [PATCH 2/9] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6ae5b379..8422bd5c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ src/seedsigner.egg-info/ .nova .vscode src/seedsigner/models/settings_definition.json +*.mo \ No newline at end of file From c0691e4c303c145c916c287b82d26dff457d2a88 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 1 Jun 2022 20:13:03 -0500 Subject: [PATCH 3/9] Bugfix and more in-depth explainer comments --- src/seedsigner/gui/screens/seed_screens.py | 7 +++++-- src/seedsigner/views/seed_views.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 71f3f838..8878cf98 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -1498,6 +1498,7 @@ class SeedAddressVerificationScreen(ButtonListScreen): print(f"verified_index: {self.verified_index.cur_count}") if self.verified_index.cur_count is not None: print("Screen callback returning success!") + self.threads[-1].stop() return 1 @@ -1511,10 +1512,12 @@ class SeedAddressVerificationScreen(ButtonListScreen): def run(self): - font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BODY_FONT_SIZE) while self.keep_running: if self.verified_index.cur_count is not None: - # Have to trigger a hw_input event to break the Screen out of the wait_for loop + # This thread will detect the success state while its parent Screen + # holds in its `wait_for`. Have to trigger a hw_input event to break + # the Screen._run out of the `wait_for` state. The Screen will then + # call its `_run_callback` and detect the success state and exit. HardwareButtons.get_instance().trigger_override(force_release=True) return diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 29778e4d..6c6a8c28 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1427,7 +1427,7 @@ class SeedAddressVerificationView(View): return Destination(NotYetImplementedView) # TODO: This should be in `Seed` or `PSBT` utility class - embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.network)] + embit_network = SettingsConstants.map_network_to_embit(self.network) # The ThreadsafeCounter will be shared by the brute-force thread to keep track of # its current addr index number and the Screen to display its progress and From 1c6b56636908deb182993b229f7ff5572e85df0f Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 1 Jun 2022 20:30:13 -0500 Subject: [PATCH 4/9] Cleaner, guaranteed thread exits --- src/seedsigner/gui/screens/screen.py | 2 ++ src/seedsigner/gui/screens/seed_screens.py | 2 ++ src/seedsigner/views/seed_views.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 6ea6fc17..5340dcdb 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -722,6 +722,8 @@ class QRDisplayScreen(BaseScreen): else: # Any other input exits the screen self.threads[-1].stop() + while self.threads[-1].is_alive(): + time.sleep(0.01) break Settings.get_instance().set_value(SettingsConstants.SETTING__QR_BRIGHTNESS, self.qr_brightness.cur_count) diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 8878cf98..7893616e 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -1499,6 +1499,8 @@ class SeedAddressVerificationScreen(ButtonListScreen): if self.verified_index.cur_count is not None: print("Screen callback returning success!") self.threads[-1].stop() + while self.threads[-1].is_alive(): + time.sleep(0.01) return 1 diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 6c6a8c28..98f280ce 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1509,6 +1509,8 @@ class SeedAddressVerificationView(View): # Halt the thread if the user gave up (will already be stopped if it verified the # target addr). self.addr_verification_thread.stop() + while self.addr_verification_thread.is_alive(): + time.sleep(0.01) return Destination(MainMenuView) From cbd53fd3d91765af7975f33106f8f036d30ee806 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 12 Jun 2022 15:42:35 -0500 Subject: [PATCH 5/9] Multisig wallet descriptor support --- src/seedsigner/gui/screens/tools_screens.py | 12 +++++-- src/seedsigner/helpers/embit_utils.py | 9 ++--- src/seedsigner/views/seed_views.py | 13 ++++--- src/seedsigner/views/tools_views.py | 38 +++++++++++++++------ 4 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index fa13fc41..669e3d12 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -298,7 +298,7 @@ class ToolsCalcFinalWordShowFinalWordScreen(ButtonListScreen): @dataclass class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen): fingerprint: str = None - wallet_descriptor: Any = None + wallet_descriptor_display_name: Any = None script_type: str = None custom_derivation_path: str = None @@ -333,6 +333,12 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen): screen_x=GUIConstants.EDGE_PADDING, screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING, )) + else: - raise Exception("Implement wallet descriptor support!") - + self.components.append(IconTextLine( + label_text="Wallet descriptor", + value_text=self.wallet_descriptor_display_name, + is_text_centered=False, + screen_x=GUIConstants.EDGE_PADDING, + screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, + )) diff --git a/src/seedsigner/helpers/embit_utils.py b/src/seedsigner/helpers/embit_utils.py index 9fecbf61..5c6a5a6b 100644 --- a/src/seedsigner/helpers/embit_utils.py +++ b/src/seedsigner/helpers/embit_utils.py @@ -77,19 +77,20 @@ def get_single_sig_address(xpub: HDKey, script_type: str = SettingsConstants.NAT -def get_multisig_address(self, descriptor: Descriptor, script_type: str = SettingsConstants.NATIVE_SEGWIT, index: int = 0, is_change: bool = False, embit_network: str = "main"): +def get_multisig_address(descriptor: Descriptor, index: int = 0, is_change: bool = False, embit_network: str = "main"): if is_change: branch_index = 1 else: branch_index = 0 - if script_type in [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT]: + if descriptor.is_segwit: + # Could be native segwit or nested segwit (descriptor.is_wrapped) return descriptor.derive(index, branch_index=branch_index).script_pubkey().address(network=NETWORKS[embit_network]) - elif self.script_type == SettingsConstants.LEGACY_P2PKH: + elif descriptor.is_legacy: # TODO: Not yet implemented! raise Exception("Legacy P2PKH verification not yet implemented!") - elif self.script_type == SettingsConstants.TAPROOT: + elif descriptor.is_taproot: # TODO: Not yet implemented! raise Exception("Taproot verification not yet implemented!") diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 98f280ce..51e23909 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1544,12 +1544,12 @@ class SeedAddressVerificationView(View): i = self.threadsafe_counter.cur_count if self.descriptor: - receive_address = embit_utils.get_multisig_address(self.descriptor, self.script_type, index=i, is_change=False, embit_network=self.embit_network) - change_address = embit_utils.get_multisig_address(self.descriptor, self.script_type, index=i, is_change=True, embit_network=self.embit_network) + receive_address = embit_utils.get_multisig_address(descriptor=self.descriptor, index=i, is_change=False, embit_network=self.embit_network) + change_address = embit_utils.get_multisig_address(descriptor=self.descriptor, index=i, is_change=True, embit_network=self.embit_network) else: - receive_address = embit_utils.get_single_sig_address(self.xpub, self.script_type, index=i, is_change=False, embit_network=self.embit_network) - change_address = embit_utils.get_single_sig_address(self.xpub, self.script_type, index=i, is_change=True, embit_network=self.embit_network) + receive_address = embit_utils.get_single_sig_address(xpub=self.xpub, script_type=self.script_type, index=i, is_change=False, embit_network=self.embit_network) + change_address = embit_utils.get_single_sig_address(xpub=self.xpub, script_type=self.script_type, index=i, is_change=True, embit_network=self.embit_network) if self.address == receive_address: self.verified_index.set_value(i) @@ -1660,12 +1660,11 @@ class MultisigWalletDescriptorView(View): elif button_data[selected_menu_num] == VERIFY: self.controller.resume_main_flow = None - # TODO: Route properly when multisig brute-force addr verification is done return Destination(SeedAddressVerificationView) elif button_data[selected_menu_num] == EXPLORER: + from seedsigner.views.tools_views import ToolsAddressExplorerAddressTypeView self.controller.resume_main_flow = None - # TODO: Finish implementation - return Destination(NotYetImplementedView) + return Destination(ToolsAddressExplorerAddressTypeView) return Destination(MainMenuView) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 0d2a9a28..a8c5fad3 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -2,6 +2,7 @@ import hashlib import os import time +from embit.descriptor import Descriptor from PIL import Image from PIL.ImageOps import autocontrast from seedsigner.controller import Controller @@ -392,9 +393,7 @@ class ToolsAddressExplorerAddressTypeView(View): data["xpub"] = self.seed.get_xpub(derivation_path, network=network) else: - # TODO: - data["wallet_descriptor"] = None - raise Exception("Not yet implemented") + data["wallet_descriptor"] = self.controller.multisig_wallet_descriptor self.controller.address_explorer_data = data @@ -407,11 +406,17 @@ class ToolsAddressExplorerAddressTypeView(View): CHANGE = "Change Addresses" button_data = [RECEIVE, CHANGE] + wallet_descriptor_display_name = None + if "wallet_descriptor" in data: + wallet_descriptor_display_name = data["wallet_descriptor"].brief_policy.replace(" (sorted)", "") + + script_type = data["script_type"] if "script_type" in data else None + selected_menu_num = ToolsAddressExplorerAddressTypeScreen( button_data=button_data, - fingerprint=self.seed.get_fingerprint() if self.seed else None, - wallet_descriptor=data["wallet_descriptor"] if "wallet_descriptor" in data else None, - script_type=data["script_type"] if "script_type" in data else None, + fingerprint=self.seed.get_fingerprint() if self.seed_num else None, + wallet_descriptor_display_name=wallet_descriptor_display_name, + script_type=script_type, ).display() if selected_menu_num == RET_CODE__BACK_BUTTON: @@ -428,12 +433,10 @@ class ToolsAddressExplorerAddressListView(View): self.is_change = is_change self.start_index = start_index self.selected_button_index = selected_button_index - self.loading_screen = LoadingScreenThread(text="Calculating addrs...") def run(self): - self.loading_screen.start() - + self.loading_screen = None try: addresses = [] button_data = [] @@ -449,6 +452,9 @@ class ToolsAddressExplorerAddressListView(View): addresses = data[addr_storage_key][self.start_index:self.start_index + addrs_per_screen] else: + self.loading_screen = LoadingScreenThread(text="Calculating addrs...") + self.loading_screen.start() + if addr_storage_key not in data: data[addr_storage_key] = [] @@ -463,6 +469,17 @@ class ToolsAddressExplorerAddressListView(View): else: # Custom derivation path raise Exception("Not yet implemented") + + elif "wallet_descriptor" in data: + descriptor: Descriptor = data["wallet_descriptor"] + if descriptor.is_basic_multisig: + for i in range(self.start_index, self.start_index + addrs_per_screen): + address = embit_utils.get_multisig_address(descriptor=descriptor, index=i, is_change=self.is_change, embit_network=data["embit_network"]) + addresses.append(address) + data[addr_storage_key].append(address) + + else: + raise Exception("Single sig descriptors not yet supported") for i, address in enumerate(addresses): cur_index = i + self.start_index @@ -487,7 +504,8 @@ class ToolsAddressExplorerAddressListView(View): ) finally: # Everything is set. Stop the loading screen - self.loading_screen.stop() + if self.loading_screen: + self.loading_screen.stop() selected_menu_num = screen.display() From 36635a1b7bf82b37018643fbab527fea1c3e4512 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 15 Jun 2022 15:08:24 -0500 Subject: [PATCH 6/9] Update seed_views.py --- src/seedsigner/views/seed_views.py | 48 ++++++++++++++---------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 0744f40d..df968338 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -676,10 +676,6 @@ class SeedExportXpubDetailsView(View): def run(self): - # The calc_derivation takes a few moments. Run the loading screen while we wait. - self.loading_screen = LoadingScreenThread(text="Generating xpub...") - self.loading_screen.start() - if self.script_type == SettingsConstants.CUSTOM_DERIVATION: derivation_path = self.custom_derivation else: @@ -689,7 +685,16 @@ class SeedExportXpubDetailsView(View): script_type=self.script_type ) - if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED: + if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__DISABLED: + # We're just skipping right past this screen + selected_menu_num = 0 + + else: + # The derivation calc takes a few moments. Run the loading screen while we wait. + self.loading_screen = LoadingScreenThread(text="Generating xpub...") + self.loading_screen.start() + + try: embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))] version = embit.bip32.detect_version( derivation_path, @@ -707,33 +712,26 @@ class SeedExportXpubDetailsView(View): xpub = xprv.to_public() xpub_base58 = xpub.to_string(version=version) - screen = seed_screens.SeedExportXpubDetailsScreen( - fingerprint=fingerprint, - has_passphrase=self.seed.passphrase is not None, - derivation_path=derivation_path, - xpub=xpub_base58, - ) - + finally: self.loading_screen.stop() - selected_menu_num = screen.display() - else: - selected_menu_num = 0 - - finally: - self.loading_screen.stop() + selected_menu_num = seed_screens.SeedExportXpubDetailsScreen( + fingerprint=fingerprint, + has_passphrase=self.seed.passphrase is not None, + derivation_path=derivation_path, + xpub=xpub_base58, + ).display() if selected_menu_num == 0: return Destination( SeedExportXpubQRDisplayView, - { - "seed_num": self.seed_num, - "sig_type": self.sig_type, - "script_type": self.script_type, - "coordinator": self.coordinator, - "derivation_path": derivation_path, - } + dict(seed_num=self.seed_num, + sig_type=self.sig_type, + script_type=self.script_type, + coordinator=self.coordinator, + derivation_path=derivation_path, + ) ) elif selected_menu_num == RET_CODE__BACK_BUTTON: From b8eca14c0eb10bfa85f21d35f4ced39d710af677 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 25 Jun 2022 11:54:53 -0700 Subject: [PATCH 7/9] Update controller.py --- src/seedsigner/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 44b06e69..48b2977b 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -197,9 +197,9 @@ class Controller(Singleton): def run(self, some_arg, other_arg): print(other_arg) - class OtherView(): + class OtherView(View): def run(self): - return (MyView, {"some_arg": 1, "other_arg": "hello"}) + return (MyView, dict(some_arg=1, other_arg="hello")) When `OtherView` is instantiated and run, we capture its return values: From 0f758b6552b6e13e740b3728709bb38023dc646a Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 27 Jun 2022 11:58:14 -0700 Subject: [PATCH 8/9] Adds Taproot address support * Bugfix after latest merges from `dev`. * More build-out and bugfixes for custom derivation but still not supported. --- src/seedsigner/gui/screens/tools_screens.py | 6 +++--- src/seedsigner/helpers/embit_utils.py | 3 +-- src/seedsigner/views/seed_views.py | 2 +- src/seedsigner/views/tools_views.py | 12 ++++++------ 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index bbea0726..65ffcc3d 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -8,7 +8,7 @@ from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, Formatted from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen from seedsigner.hardware.buttons import HardwareButtonsConstants from seedsigner.helpers.qr import QR -from seedsigner.models.settings_definition import SettingsConstants +from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition @@ -394,11 +394,11 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen): screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, )) - if self.script_type: + if self.script_type != SettingsConstants.CUSTOM_DERIVATION: self.components.append(IconTextLine( icon_name=SeedSignerCustomIconConstants.PATH, label_text="Derivation", - value_text="Native Segwit" if self.script_type == SettingsConstants.NATIVE_SEGWIT else "Nested Segwit", + value_text=SettingsDefinition.get_settings_entry(attr_name=SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(value=self.script_type), screen_x=GUIConstants.EDGE_PADDING, screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING, )) diff --git a/src/seedsigner/helpers/embit_utils.py b/src/seedsigner/helpers/embit_utils.py index 5c6a5a6b..bdd95b7f 100644 --- a/src/seedsigner/helpers/embit_utils.py +++ b/src/seedsigner/helpers/embit_utils.py @@ -72,8 +72,7 @@ def get_single_sig_address(xpub: HDKey, script_type: str = SettingsConstants.NAT return embit.script.p2pkh(pubkey).address(network=NETWORKS[embit_network]) elif script_type == SettingsConstants.TAPROOT: - # TODO: Not yet implemented! - raise Exception("Taproot verification not yet implemented!") + return embit.script.p2tr(pubkey).address(network=NETWORKS[embit_network]) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 14828aa1..736b95a7 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -567,7 +567,7 @@ class SeedExportXpubCustomDerivationView(View): if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: from .tools_views import ToolsAddressExplorerAddressTypeView - return Destination(ToolsAddressExplorerAddressTypeView, view_args=dict(seed_num=self.seed_num, script_type=self.script_type, custom_derivation=self.custom_derivation_path)) + return Destination(ToolsAddressExplorerAddressTypeView, view_args=dict(seed_num=self.seed_num, script_type=self.script_type, custom_derivation=custom_derivation)) return Destination( SeedExportXpubCoordinatorView, diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index a7c8fe78..da8926e1 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -11,7 +11,7 @@ from seedsigner.gui.screens.screen import LoadingScreenThread, QRDisplayScreen from seedsigner.hardware.camera import Camera from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerCustomIconConstants from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen) -from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordShowFinalWordScreen, ToolsAddressExplorerAddressTypeScreen +from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen from seedsigner.helpers import embit_utils, mnemonic_generation from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.qr_type import QRType @@ -532,7 +532,6 @@ class ToolsAddressExplorerAddressTypeView(View): def run(self): - # embit_utils.get_single_sig_address(xpub=xpub, script_type=self.script_type, index=0, is_change=True, embit_network=embit_network) data = self.controller.address_explorer_data RECEIVE = "Receive Addresses" @@ -547,9 +546,10 @@ class ToolsAddressExplorerAddressTypeView(View): selected_menu_num = ToolsAddressExplorerAddressTypeScreen( button_data=button_data, - fingerprint=self.seed.get_fingerprint() if self.seed_num else None, + fingerprint=self.seed.get_fingerprint() if self.seed_num is not None else None, wallet_descriptor_display_name=wallet_descriptor_display_name, script_type=script_type, + custom_derivation_path=self.custom_derivation, ).display() if selected_menu_num == RET_CODE__BACK_BUTTON: @@ -593,15 +593,15 @@ class ToolsAddressExplorerAddressListView(View): if "xpub" in data: # Single sig explore from seed - if "script_type" in data: + if "script_type" in data and data["script_type"] != SettingsConstants.CUSTOM_DERIVATION: # Standard derivation path for i in range(self.start_index, self.start_index + addrs_per_screen): address = embit_utils.get_single_sig_address(xpub=data["xpub"], script_type=data["script_type"], index=i, is_change=self.is_change, embit_network=data["embit_network"]) addresses.append(address) data[addr_storage_key].append(address) else: - # Custom derivation path - raise Exception("Not yet implemented") + # TODO: Custom derivation path + raise Exception("Custom Derivation address explorer not yet implemented") elif "wallet_descriptor" in data: descriptor: Descriptor = data["wallet_descriptor"] From 67be53ad942d447e729d8dd3a3ce0abc72ae3278 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 6 Aug 2022 20:50:54 -0500 Subject: [PATCH 9/9] Update seed_views.py --- src/seedsigner/views/seed_views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 00759882..e37a9d04 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -723,8 +723,6 @@ class SeedExportXpubDetailsView(View): return Destination( SeedExportXpubQRDisplayView, dict(seed_num=self.seed_num, - sig_type=self.sig_type, - script_type=self.script_type, coordinator=self.coordinator, derivation_path=derivation_path, )