diff --git a/electrum/gui/qt/wizard/wallet.py b/electrum/gui/qt/wizard/wallet.py index d91db9a64..ef23317d1 100644 --- a/electrum/gui/qt/wizard/wallet.py +++ b/electrum/gui/qt/wizard/wallet.py @@ -720,9 +720,9 @@ class WCScriptAndDerivation(WizardComponent, Logger): self.apply() cosigner_data = self.wizard.current_cosigner(self.wizard_data) - derivation_valid = is_bip32_derivation(cosigner_data['derivation_path']) + valid = is_bip32_derivation(cosigner_data['derivation_path']) - if derivation_valid: + if valid: valid, error = self.wizard.check_multisig_constraints(self.wizard_data) if not valid: # TODO: user feedback diff --git a/electrum/gui/qt/wizard/wizard.py b/electrum/gui/qt/wizard/wizard.py index 49d538018..cba6e2fa6 100644 --- a/electrum/gui/qt/wizard/wizard.py +++ b/electrum/gui/qt/wizard/wizard.py @@ -167,7 +167,7 @@ class QEAbstractWizard(QDialog, MessageBoxMixin): self.back_button.setText(_('Back') if self.can_go_back() else _('Cancel')) self.back_button.setEnabled(not page.busy) self.next_button.setText(_('Next') if not self.is_last(page.wizard_data) else _('Finish')) - self.next_button.setEnabled(page.valid) + self.next_button.setEnabled(not page.busy and page.valid) self.main_widget.setVisible(not page.busy and not bool(page.error)) self.please_wait.setVisible(page.busy) self.please_wait_l.setText(page.busy_msg if page.busy_msg else _("Please wait...")) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index e0dd51b25..a97e72beb 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -31,13 +31,8 @@ _logger = get_logger(__name__) try: from bitbox02 import bitbox02 from bitbox02 import util - from bitbox02.communication import ( - devices, - HARDENED, - u2fhid, - bitbox_api_protocol, - FirmwareVersionOutdatedException, - ) + from bitbox02.communication import (devices, HARDENED, u2fhid, bitbox_api_protocol, + FirmwareVersionOutdatedException) requirements_ok = True except ImportError as e: if not (isinstance(e, ModuleNotFoundError) and e.name == 'bitbox02'): @@ -45,6 +40,10 @@ except ImportError as e: requirements_ok = False +class BitBox02NotInitialized(UserFacingException): + pass + + class BitBox02Client(HardwareClientBase): # handler is a BitBox02_Handler, importing it would lead to a circular dependency def __init__(self, handler: HardwareHandlerBase, device: Device, config: SimpleConfig, *, plugin: HW_PluginBase): @@ -119,7 +118,7 @@ class BitBox02Client(HardwareClientBase): bitbox02_config = self.config.get("bitbox02") noise_keys = bitbox02_config.get("remote_static_noise_keys") if noise_keys is not None: - if pubkey.hex() in [noise_key for noise_key in noise_keys]: + if pubkey.hex() in noise_keys: return True return False @@ -192,7 +191,7 @@ class BitBox02Client(HardwareClientBase): def fail_if_not_initialized(self) -> None: assert self.bitbox02_device if not self.bitbox02_device.device_info()["initialized"]: - raise Exception( + raise BitBox02NotInitialized( "Please initialize the BitBox02 using the BitBox app first before using the BitBox02 in electrum" ) @@ -248,12 +247,8 @@ class BitBox02Client(HardwareClientBase): else: raise Exception("invalid xtype:{}".format(xtype)) - return self.bitbox02_device.btc_xpub( - keypath=xpub_keypath, - xpub_type=out_type, - coin=coin_network, - display=display, - ) + return self.bitbox02_device.btc_xpub(keypath=xpub_keypath, xpub_type=out_type, coin=coin_network, + display=display) @runs_in_hwd_thread def label(self) -> str: @@ -565,6 +560,7 @@ class BitBox02Client(HardwareClientBase): ) return signature + class BitBox02_KeyStore(Hardware_KeyStore): hw_type = "bitbox02" device = "BitBox02" @@ -600,7 +596,6 @@ class BitBox02_KeyStore(Hardware_KeyStore): keypath = self.get_derivation_prefix() + "/%d/%d" % sequence return client.sign_message(keypath, message.encode("utf-8"), script_type) - @runs_in_hwd_thread def sign_transaction(self, tx: PartialTransaction, password: str): if tx.is_complete(): @@ -612,7 +607,6 @@ class BitBox02_KeyStore(Hardware_KeyStore): try: self.handler.show_message("Authorize Transaction...") client.sign_transaction(self, tx, self.handler.get_wallet()) - finally: self.handler.finished() @@ -639,6 +633,7 @@ class BitBox02_KeyStore(Hardware_KeyStore): self.logger.exception("") self.handler.show_error(e) + class BitBox02Plugin(HW_PluginBase): keystore_class = BitBox02_KeyStore minimum_library = (6, 2, 0) @@ -700,9 +695,9 @@ class BitBox02Plugin(HW_PluginBase): id_ = str(d['path']) return device._replace(id_=id_) - # new wizard - def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str: + # Note: device_info.initialized for this hardware doesn't imply a seed is present, + # only that it has firmware installed if new_wallet: return 'bitbox02_start' if device_info.initialized else 'bitbox02_not_initialized' else: diff --git a/electrum/plugins/bitbox02/qt.py b/electrum/plugins/bitbox02/qt.py index febb1f17c..4e8df221d 100644 --- a/electrum/plugins/bitbox02/qt.py +++ b/electrum/plugins/bitbox02/qt.py @@ -1,29 +1,20 @@ +import threading from functools import partial from typing import TYPE_CHECKING -from PyQt5.QtWidgets import ( - QPushButton, - QLabel, - QVBoxLayout, - QLineEdit, - QHBoxLayout, -) - -from PyQt5.QtCore import Qt, QMetaObject, Q_RETURN_ARG, pyqtSlot - -from electrum.gui.qt.util import ( - WindowModalDialog, - OkButton, - ButtonsTextEdit, -) +from PyQt5.QtCore import Qt, QMetaObject, Q_RETURN_ARG, pyqtSlot, pyqtSignal +from PyQt5.QtWidgets import QLabel, QVBoxLayout, QLineEdit, QHBoxLayout from electrum.i18n import _ from electrum.plugin import hook +from electrum.util import UserCancelled, UserFacingException from .bitbox02 import BitBox02Plugin from ..hw_wallet.qt import QtHandlerBase, QtPluginBase -from ..hw_wallet.plugin import only_hook_if_libraries_available +from ..hw_wallet.plugin import only_hook_if_libraries_available, OperationCancelled + from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWUninitialized, WCHWXPub +from electrum.gui.qt.util import WindowModalDialog, OkButton, ButtonsTextEdit if TYPE_CHECKING: from electrum.gui.qt.wizard.wallet import QENewWalletWizard @@ -77,7 +68,7 @@ class Plugin(BitBox02Plugin, QtPluginBase): def extend_wizard(self, wizard: 'QENewWalletWizard'): super().extend_wizard(wizard) views = { - 'bitbox02_start': {'gui': WCScriptAndDerivation}, + 'bitbox02_start': {'gui': WCBitbox02ScriptAndDerivation}, 'bitbox02_xpub': {'gui': WCHWXPub}, 'bitbox02_not_initialized': {'gui': WCHWUninitialized}, 'bitbox02_unlock': {'gui': WCHWUnlock} @@ -92,12 +83,7 @@ class BitBox02_Handler(QtHandlerBase): super(BitBox02_Handler, self).__init__(win, "BitBox02") def name_multisig_account(self): - return QMetaObject.invokeMethod( - self, - "_name_multisig_account", - Qt.BlockingQueuedConnection, - Q_RETURN_ARG(str), - ) + return QMetaObject.invokeMethod(self, "_name_multisig_account", Qt.BlockingQueuedConnection, Q_RETURN_ARG(str)) @pyqtSlot(result=str) def _name_multisig_account(self): @@ -125,3 +111,46 @@ class BitBox02_Handler(QtHandlerBase): dialog.setLayout(vbox) dialog.exec_() return name.text().strip() + + +class WCBitbox02ScriptAndDerivation(WCScriptAndDerivation): + def __init__(self, parent, wizard): + WCScriptAndDerivation.__init__(self, parent, wizard) + self._busy = True + self.title = '' + self.client = None + + def on_ready(self): + super().on_ready() + _name, _info = self.wizard_data['hardware_device'] + plugin = self.wizard.plugins.get_plugin(_info.plugin_name) + + device_id = _info.device.id_ + self.client = self.wizard.plugins.device_manager.client_by_id(device_id, scan_now=False) + if not self.client.handler: + self.client.handler = plugin.create_handler(self.wizard) + self.client.setupRunning = True + self.check_device() + + def check_device(self): + self.error = None + self.valid = False + self.busy = True + + def check_task(): + try: + self.client.pairing_dialog() + self.title = _('Script type and Derivation path') + self.valid = True + except (UserCancelled, OperationCancelled): + self.error = _('Cancelled') + self.wizard.requestPrev.emit() + except UserFacingException as e: + self.error = str(e) + except Exception as e: + self.error = repr(e) + finally: + self.busy = False + + t = threading.Thread(target=check_task, daemon=True) + t.start()