From 7dd43fa017ea30e5af411d11bf3c43ebefd8b1b4 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 22 Aug 2023 18:14:47 +0200 Subject: [PATCH] qt: add bitbox02 to new wizard --- electrum/plugins/bitbox02/bitbox02.py | 28 +++++++ electrum/plugins/bitbox02/qt.py | 105 +++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index 320dd0e16..53b522785 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -24,6 +24,8 @@ import electrum.ecc as ecc from ..hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase +if TYPE_CHECKING: + from electrum.wizard import NewWalletWizard _logger = get_logger(__name__) @@ -720,3 +722,29 @@ class BitBox02Plugin(HW_PluginBase): # distinguish devices. id_ = str(d['path']) return device._replace(id_=id_) + + # new wizard + + def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str: + if new_wallet: + return 'bitbox_start' if device_info.initialized else 'bitbox_not_initialized' + else: + return 'bitbox_unlock' + + # insert trezor pages in new wallet wizard + def extend_wizard(self, wizard: 'NewWalletWizard'): + views = { + 'bitbox_start': { + 'next': 'bitbox_xpub', + }, + 'bitbox_xpub': { + 'next': lambda d: wizard.wallet_password_view(d) if wizard.last_cosigner(d) else 'multisig_cosigner_keystore', + 'accept': wizard.maybe_master_pubkey, + 'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d) + }, + 'bitbox_not_initialized': {}, + 'bitbox_unlock': { + 'last': True + }, + } + wizard.navmap_merge(views) diff --git a/electrum/plugins/bitbox02/qt.py b/electrum/plugins/bitbox02/qt.py index 50454b117..d9a1065e2 100644 --- a/electrum/plugins/bitbox02/qt.py +++ b/electrum/plugins/bitbox02/qt.py @@ -1,4 +1,6 @@ +import threading from functools import partial +from typing import TYPE_CHECKING from PyQt5.QtWidgets import ( QPushButton, @@ -13,7 +15,7 @@ from PyQt5.QtCore import Qt, QMetaObject, Q_RETURN_ARG, pyqtSlot from electrum.gui.qt.util import ( WindowModalDialog, OkButton, - ButtonsTextEdit, + ButtonsTextEdit, WWLabel, ) from electrum.i18n import _ @@ -22,6 +24,12 @@ from electrum.plugin import hook from .bitbox02 import BitBox02Plugin from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from ..hw_wallet.plugin import only_hook_if_libraries_available +from electrum.gui.qt.wizard.wizard import WizardComponent +from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock +from electrum.logging import Logger + +if TYPE_CHECKING: + from electrum.gui.qt.wizard.wallet import QENewWalletWizard class Plugin(BitBox02Plugin, QtPluginBase): @@ -64,6 +72,21 @@ class Plugin(BitBox02Plugin, QtPluginBase): device_name = "{} ({})".format(self.device, keystore.label) mpk_text.addButton("eye1.png", on_button_click, _("Show on {}").format(device_name)) + @hook + def init_wallet_wizard(self, wizard: 'QENewWalletWizard'): + self.extend_wizard(wizard) + + # insert trezor pages in new wallet wizard + def extend_wizard(self, wizard: 'QENewWalletWizard'): + super().extend_wizard(wizard) + views = { + 'bitbox_start': { 'gui': WCScriptAndDerivation }, + 'bitbox_xpub': { 'gui': WCBitboxXPub }, + 'bitbox_not_initialized': {'gui': WCBitboxNope}, + 'bitbox_unlock': {'gui': WCHWUnlock} + } + wizard.navmap_merge(views) + class BitBox02_Handler(QtHandlerBase): MESSAGE_DIALOG_TITLE = _("BitBox02 Status") @@ -105,3 +128,83 @@ class BitBox02_Handler(QtHandlerBase): dialog.setLayout(vbox) dialog.exec_() return name.text().strip() + + +# TODO: almost verbatim copy of trezor WCTrezorXPub, generalize! +# problem: client.get_xpub is not uniform +class WCBitboxXPub(WizardComponent, Logger): + def __init__(self, parent, wizard): + WizardComponent.__init__(self, parent, wizard, title=_('Hardware wallet information')) + Logger.__init__(self) + self.plugins = wizard.plugins + self.plugin = self.plugins.get_plugin('bitbox02') + self.busy_msg = _('Unlock your Bitbox02') + self._busy = True + + self.xpub = None + self.root_fingerprint = None + self.label = None + self.soft_device_id = None + + self.ok_l = WWLabel(_('Hardware keystore added to wallet')) + self.ok_l.setAlignment(Qt.AlignCenter) + self.layout().addWidget(self.ok_l) + + def on_ready(self): + _name, _info = self.wizard_data['hardware_device'] + device_id = _info.device.id_ + client = self.plugins.device_manager.client_by_id(device_id, scan_now=False) + if not client.handler: + client.handler = self.plugin.create_handler(self.wizard) + + cosigner = self.wizard.current_cosigner(self.wizard_data) + xtype = cosigner['script_type'] + derivation = cosigner['derivation_path'] + + def get_xpub_task(client, derivation, xtype): + try: + self.xpub = client.get_xpub(derivation, xtype) + self.root_fingerprint = client.request_root_fingerprint_from_device() + self.label = client.label() + self.soft_device_id = client.get_soft_device_id() + except Exception as e: + # TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully + self.error = repr(e) + self.logger.error(repr(e)) + self.xpub_done() + + t = threading.Thread(target=get_xpub_task, args=(client, derivation, xtype), daemon=True) + t.start() + + def xpub_done(self): + self.logger.debug(f'Done retrieve xpub: {self.xpub}') + self.busy = False + self.validate() + + def validate(self): + if self.xpub and not self.error: + self.apply() + valid, error = self.wizard.check_multisig_constraints(self.wizard_data) + if not valid: + self.error = '\n'.join([ + _('Could not add hardware keystore to wallet'), + error + ]) + self.valid = valid + else: + self.valid = False + + def apply(self): + cosigner_data = self.wizard.current_cosigner(self.wizard_data) + cosigner_data['hw_type'] = 'bitbox02' + cosigner_data['master_key'] = self.xpub + cosigner_data['root_fingerprint'] = self.root_fingerprint + cosigner_data['label'] = self.label + cosigner_data['soft_device_id'] = self.soft_device_id + + +class WCBitboxNope(WizardComponent): + def __init__(self, parent, wizard): + WizardComponent.__init__(self, parent, wizard, title=_('Bitbox02 not initialized')) + self.layout().addWidget(WWLabel(_('This Bitbox02 is not initialized. Cannot continue'))) +