diff --git a/.gitignore b/.gitignore index 47a850c2..392faf91 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ src/seedsigner.egg-info/ .vscode src/seedsigner/models/settings_definition.json .idea +*.mo diff --git a/README.md b/README.md index 5ff41944..16e1160e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ * [Project Summary](#project-summary) * [Shopping List](#shopping-list) * [Software Installation](#software-installation) - * [Verifying Your Software](#verifying-your-software) + * [Verifying the Software](#verifying-the-software) * [Enclosure Designs](#enclosure-designs) * [SeedQR Printable Templates](#seedqr-printable-templates) * [Manual Installation Instructions](#manual-installation-instructions) diff --git a/docs/seed_qr/README.md b/docs/seed_qr/README.md index 3eba91de..f22905a3 100644 --- a/docs/seed_qr/README.md +++ b/docs/seed_qr/README.md @@ -480,3 +480,72 @@ b'\n\xcb\xba\x00\x8d\x9b\xa0\x05\xf5\x99k@\xa3G\\\xd9' +--- + +## Test Vectors 7-9: Additional Compact SeedQR problem characters +Explicitly check Compact SeedQRs whose byte stream contains `\n`, `\r`, or `\r\n`: + +`\n`: +```bash +# 12-word seed: +dignity utility vacant shiver thought canoe feel multiply item youth actor coyote + +# Standard SeedQR digit stream: +049619221923158517990268067811630950204300210397 + +# CompactSeedQR bitstream: +00111110000111100000101111000001111000110001111000001110010000110001010100110100100010110111011011011111111011000000101010011000 + +# CompactSeedQR bytestream: +b'>\x1e\x0b\xc1\xe3\x1e\x0eC\x154\x8bv\xdf\xec\n\x98' +``` + + + + + +

CompactSeedQR
+ + +`\r`: +```bash +# 12-word seed: +corn voice scrap arrow original diamond trial property benefit choose junk lock + +# Standard SeedQR digit stream: +038719631547010112530489185713790169032209701051 + +# CompactSeedQR bitstream: +00110000011111101010111100000101100001100101100111001010011110100111101000001101011000110001010100100101000010011110010101000001 + +# CompactSeedQR bytestream: +b'0~\xaf\x05\x86Y\xcazz\rc\x15%\t\xe5A' +``` + + + + + +

CompactSeedQR
+ + +`\r\n`: +```bash +# 12-word seed: +vocal tray giggle tool duck letter category pattern train magnet excite swamp + +# Standard SeedQR digit stream: +196218530783182905421028028912901848107106301753 + +# CompactSeedQR bitstream: +11110101010111001111010110000111111100100101010000111101000000010000100100001101000010101110011100010000101111010011101101101101 + +# CompactSeedQR bytestream: +b'\xf5\\\xf5\x87\xf2T=\x01\t\r\n\xe7\x10\xbd;m' +``` + + + + + +

CompactSeedQR
diff --git a/docs/seed_qr/img/vector7_compact_12word.png b/docs/seed_qr/img/vector7_compact_12word.png new file mode 100644 index 00000000..afdd9d3a Binary files /dev/null and b/docs/seed_qr/img/vector7_compact_12word.png differ diff --git a/docs/seed_qr/img/vector8_compact_12word.png b/docs/seed_qr/img/vector8_compact_12word.png new file mode 100644 index 00000000..539b0ac2 Binary files /dev/null and b/docs/seed_qr/img/vector8_compact_12word.png differ diff --git a/docs/seed_qr/img/vector9_compact_12word.png b/docs/seed_qr/img/vector9_compact_12word.png new file mode 100644 index 00000000..213453f1 Binary files /dev/null and b/docs/seed_qr/img/vector9_compact_12word.png differ diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 44b06e69..5de5d493 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 @@ -197,9 +200,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: @@ -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/psbt_screens.py b/src/seedsigner/gui/screens/psbt_screens.py index 053d47e5..0154fdea 100644 --- a/src/seedsigner/gui/screens/psbt_screens.py +++ b/src/seedsigner/gui/screens/psbt_screens.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from lzma import is_check_supported from PIL import Image, ImageDraw, ImageFilter from typing import List +import time from seedsigner.gui.renderer import Renderer from seedsigner.models.threads import BaseThread @@ -439,7 +440,7 @@ class PSBTOverviewScreen(ButtonListScreen): self.renderer.show_image() # No need to CPU limit when running in its own thread? - # time.sleep(0.02) + time.sleep(0.02) @@ -696,19 +697,3 @@ class PSBTFinalizeScreen(ButtonListScreen): text="Click to authorize this transaction", screen_y=icon.screen_y + icon.height + GUIConstants.COMPONENT_PADDING )) - - - -@dataclass -class PSBTSelectCoordinatorScreen(ButtonListScreen): - def __post_init__(self): - # Customize defaults - self.title = "Signed PSBT" - self.is_bottom_list = True - super().__post_init__() - - self.components.append(TextArea( - text="Export as a QR code for:", - is_text_centered=True, - screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, - )) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index debaae45..552407bd 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -6,7 +6,7 @@ from typing import Any, List, Tuple from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay 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 @@ -661,41 +661,73 @@ 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() + while self.threads[-1].is_alive(): + time.sleep(0.01) 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 79db667b..84b05885 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__() @@ -1361,6 +1362,9 @@ 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() + while self.threads[-1].is_alive(): + time.sleep(0.01) return 1 @@ -1374,10 +1378,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/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index 229146d5..65ffcc3d 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, KeyboardScreen from seedsigner.hardware.buttons import HardwareButtonsConstants +from seedsigner.helpers.qr import QR +from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition @@ -366,3 +369,53 @@ class ToolsCalcFinalWordDoneScreen(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_display_name: 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 != SettingsConstants.CUSTOM_DERIVATION: + self.components.append(IconTextLine( + icon_name=SeedSignerCustomIconConstants.PATH, + label_text="Derivation", + 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, + )) + 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: + 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/hardware/buttons.py b/src/seedsigner/hardware/buttons.py index d187f8c8..ca2011aa 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 @@ -157,10 +158,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..bdd95b7f --- /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: + return embit.script.p2tr(pubkey).address(network=NETWORKS[embit_network]) + + + +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 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 descriptor.is_legacy: + # TODO: Not yet implemented! + raise Exception("Legacy P2PKH verification not yet implemented!") + + elif descriptor.is_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/psbt_views.py b/src/seedsigner/views/psbt_views.py index 3ad69f02..613a9052 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -484,44 +484,18 @@ class PSBTFinalizeView(View): else: self.controller.psbt = trimmed_psbt - - 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) + return Destination(PSBTSignedQRDisplayView) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) -class PSBTSelectCoordinatorView(View): - def run(self): - button_data = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS) - selected_menu_num = psbt_screens.PSBTSelectCoordinatorScreen( - button_data=button_data - ).display() - - if selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - - return Destination(PSBTSignedQRDisplayView, view_args={"coordinator": button_data[selected_menu_num]}) - - - class PSBTSignedQRDisplayView(View): - def __init__(self, coordinator: str): - super().__init__() - self.coordinator = coordinator - def run(self): - qr_psbt_type = QRType.PSBT__UR2 - if self.coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP: - qr_psbt_type = QRType.PSBT__SPECTER - qr_encoder = EncodeQR( psbt=self.controller.psbt, - qr_type=qr_psbt_type, + qr_type=QRType.PSBT__UR2, # All coordinators (as of 2022-08) use this format qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY), wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE), ) 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 5acafd9e..f03a5d9d 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 @@ -348,11 +349,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 @@ -362,7 +370,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: @@ -381,13 +389,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: @@ -403,16 +412,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)) @@ -487,23 +500,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) @@ -511,10 +538,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) @@ -534,9 +562,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=custom_derivation)) + return Destination( SeedExportXpubCoordinatorView, view_args={ @@ -646,29 +678,39 @@ 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 ) - if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED: - embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))] - version = embit.bip32.detect_version( - derivation_path, - default="xpub", - network=embit_network - ) + if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__DISABLED: + # We're just skipping right past this screen + selected_menu_num = 0 - root = embit.bip32.HDKey.from_seed( - self.seed.seed_bytes, - version=embit_network["xprv"] - ) + 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() - fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8') - xprv = root.derive(derivation_path) - xpub = xprv.to_public() - xpub_base58 = xpub.to_string(version=version) + try: + embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))] + version = embit.bip32.detect_version( + derivation_path, + default="xpub", + network=embit_network + ) + root = embit.bip32.HDKey.from_seed( + self.seed.seed_bytes, + version=embit_network["xprv"] + ) + fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8') + xprv = root.derive(derivation_path) + xpub = xprv.to_public() + xpub_base58 = xpub.to_string(version=version) + + finally: + self.loading_screen.stop() selected_menu_num = seed_screens.SeedExportXpubDetailsScreen( fingerprint=fingerprint, @@ -677,18 +719,13 @@ class SeedExportXpubDetailsView(View): xpub=xpub_base58, ).display() - else: - selected_menu_num = 0 - - if selected_menu_num == 0: return Destination( SeedExportXpubQRDisplayView, - { - "seed_num": self.seed_num, - "coordinator": self.coordinator, - "derivation_path": derivation_path, - } + dict(seed_num=self.seed_num, + coordinator=self.coordinator, + derivation_path=derivation_path, + ) ) elif selected_menu_num == RET_CODE__BACK_BUTTON: @@ -1245,13 +1282,14 @@ class AddressVerificationStartView(View): destination = Destination(SeedSingleSigAddressVerificationSelectSeedView) elif self.controller.unverified_address["script_type"] == SettingsConstants.TAPROOT: - destination = Destination(NotYetImplementedView) + # TODO: add Taproot support + return Destination(NotYetImplementedView) elif self.controller.unverified_address["script_type"] == SettingsConstants.LEGACY_P2PKH: # TODO: detect single sig vs multisig or have to prompt? - destination = Destination(NotYetImplementedView) + return 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"] @@ -1295,7 +1333,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"] @@ -1399,7 +1437,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 @@ -1416,7 +1454,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, @@ -1481,13 +1519,15 @@ 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) 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 """ @@ -1496,17 +1536,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: @@ -1516,9 +1554,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(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, change_address) = self.derive_single_sig(i) + 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) @@ -1534,47 +1575,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): @@ -1638,6 +1640,7 @@ class MultisigWalletDescriptorView(View): RETURN = "Return to PSBT" VERIFY = "Verify Addr" + EXPLORER = "Address Explorer" OK = "OK" button_data = [OK] @@ -1647,6 +1650,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, @@ -1665,7 +1670,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 + return Destination(ToolsAddressExplorerAddressTypeView) + return Destination(MainMenuView) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index bfee4738..da8926e1 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -2,17 +2,22 @@ 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 +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 ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordDoneScreen -from seedsigner.helpers import mnemonic_generation +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 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 +28,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 +49,10 @@ class ToolsMenuView(View): elif button_data[selected_menu_num] == KEYBOARD: return Destination(ToolsCalcFinalWordNumWordsView) + elif button_data[selected_menu_num] == EXPLORER: + return Destination(ToolsAddressExplorerSelectSourceView) + + """**************************************************************************** @@ -409,3 +419,256 @@ class ToolsCalcFinalWordDoneView(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: + data["wallet_descriptor"] = self.controller.multisig_wallet_descriptor + + self.controller.address_explorer_data = data + + + def run(self): + data = self.controller.address_explorer_data + + RECEIVE = "Receive Addresses" + 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_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: + 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 + + + def run(self): + self.loading_screen = None + 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: + self.loading_screen = LoadingScreenThread(text="Calculating addrs...") + self.loading_screen.start() + + 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 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: + # TODO: Custom derivation path + raise Exception("Custom Derivation address explorer 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 + 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 + if self.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)