From 66e9f502b0c38d592bc795dff1b487c0d5427db1 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 23 Aug 2023 11:33:19 +0200 Subject: [PATCH] qt: generalize wizard HWW xpub --- electrum/gui/qt/wizard/wallet.py | 100 +++++++++++++++++++++++++++---- electrum/plugins/bitbox02/qt.py | 93 ++-------------------------- electrum/plugins/jade/qt.py | 93 ++-------------------------- electrum/plugins/trezor/qt.py | 81 ++++--------------------- 4 files changed, 108 insertions(+), 259 deletions(-) diff --git a/electrum/gui/qt/wizard/wallet.py b/electrum/gui/qt/wizard/wallet.py index a09134532..37f91e31b 100644 --- a/electrum/gui/qt/wizard/wallet.py +++ b/electrum/gui/qt/wizard/wallet.py @@ -86,7 +86,6 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard): 'last': lambda d: d['wallet_exists'] and not d['wallet_needs_hw_unlock'] }, 'hw_unlock': { - # 'last': True, 'gui': WCChooseHWDevice, 'next': lambda d: self.on_hardware_device(d, new_wallet=False) } @@ -830,7 +829,7 @@ class WCMultisig(WizardComponent): class WCImport(WizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Import Bitcoin Addresses')) + WizardComponent.__init__(self, parent, wizard, title=_('Import Bitcoin Addresses or Private Keys')) message = _( 'Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.') header_layout = QHBoxLayout() @@ -1111,7 +1110,7 @@ class WCChooseHWDevice(WizardComponent, Logger): class WCWalletPasswordHardware(WizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Password HW')) + WizardComponent.__init__(self, parent, wizard, title=_('Encrypt using hardware')) self.plugins = wizard.plugins self.playout = PasswordLayoutForHW(MSG_HW_STORAGE_ENCRYPTION) @@ -1133,7 +1132,7 @@ class WCWalletPasswordHardware(WizardComponent): class WCHWUnlock(WizardComponent, Logger): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('unlock')) + WizardComponent.__init__(self, parent, wizard, title=_('Unlocking hardware')) Logger.__init__(self) self.plugins = wizard.plugins self.plugin = None @@ -1157,19 +1156,14 @@ class WCHWUnlock(WizardComponent, Logger): try: self.password = client.get_password_for_storage_encryption() except Exception as e: - # TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully - self.error = repr(e) + self.error = repr(e) # TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully self.logger.error(repr(e)) - self.unlock_done() + self.busy = False + self.validate() t = threading.Thread(target=unlock_task, args=(client,), daemon=True) t.start() - def unlock_done(self): - self.logger.debug(f'Done unlock') - self.busy = False - self.validate() - def validate(self): if self.password and not self.error: self.apply() @@ -1180,3 +1174,85 @@ class WCHWUnlock(WizardComponent, Logger): def apply(self): if self.valid: self.wizard_data['password'] = self.password + + +class WCHWXPub(WizardComponent, Logger): + def __init__(self, parent, wizard): + WizardComponent.__init__(self, parent, wizard, title=_('Retrieving extended public key from hardware')) + Logger.__init__(self) + self.plugins = wizard.plugins + self.plugin = None + 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'] + self.plugin = self.plugins.get_plugin(_info.plugin_name) + self.title = _('Retrieving extended public key from {} ({})').format(_info.model_name, _info.label) + + 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 = self.get_xpub_from_client(client, 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: + self.error = repr(e) # TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully + self.logger.error(repr(e)) + self.logger.debug(f'Done retrieve xpub: {self.xpub}') + self.busy = False + self.validate() + + t = threading.Thread(target=get_xpub_task, args=(client, derivation, xtype), daemon=True) + t.start() + + def get_xpub_from_client(self, client, derivation, xtype): # override for HWW specific client if needed + return client.get_xpub(derivation, xtype) + + 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): + _name, _info = self.wizard_data['hardware_device'] + cosigner_data = self.wizard.current_cosigner(self.wizard_data) + cosigner_data['hw_type'] = _info.plugin_name + 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 WCHWUninitialized(WizardComponent): + def __init__(self, parent, wizard): + WizardComponent.__init__(self, parent, wizard, title=_('Hardware not initialized')) + + def on_ready(self): + _name, _info = self.wizard_data['hardware_device'] + self.layout().addWidget(WWLabel(_('This {} is not initialized. Cannot continue').format(_info.model_name))) diff --git a/electrum/plugins/bitbox02/qt.py b/electrum/plugins/bitbox02/qt.py index d9a1065e2..b128516cc 100644 --- a/electrum/plugins/bitbox02/qt.py +++ b/electrum/plugins/bitbox02/qt.py @@ -1,4 +1,3 @@ -import threading from functools import partial from typing import TYPE_CHECKING @@ -15,7 +14,7 @@ from PyQt5.QtCore import Qt, QMetaObject, Q_RETURN_ARG, pyqtSlot from electrum.gui.qt.util import ( WindowModalDialog, OkButton, - ButtonsTextEdit, WWLabel, + ButtonsTextEdit, ) from electrum.i18n import _ @@ -24,9 +23,7 @@ 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 +from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWUninitialized, WCHWXPub if TYPE_CHECKING: from electrum.gui.qt.wizard.wallet import QENewWalletWizard @@ -80,9 +77,9 @@ class Plugin(BitBox02Plugin, QtPluginBase): 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_start': {'gui': WCScriptAndDerivation}, + 'bitbox_xpub': {'gui': WCHWXPub}, + 'bitbox_not_initialized': {'gui': WCHWUninitialized}, 'bitbox_unlock': {'gui': WCHWUnlock} } wizard.navmap_merge(views) @@ -128,83 +125,3 @@ 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'))) - diff --git a/electrum/plugins/jade/qt.py b/electrum/plugins/jade/qt.py index 7eb2da6e8..0fdbe63d7 100644 --- a/electrum/plugins/jade/qt.py +++ b/electrum/plugins/jade/qt.py @@ -1,19 +1,15 @@ -import threading from functools import partial from typing import TYPE_CHECKING -from PyQt5.QtCore import pyqtSignal, Qt +from PyQt5.QtCore import pyqtSignal from electrum.i18n import _ from electrum.plugin import hook from electrum.wallet import Standard_Wallet -from electrum.logging import Logger from electrum.plugins.hw_wallet.qt import QtHandlerBase, QtPluginBase from electrum.plugins.hw_wallet import plugin -from electrum.gui.qt.util import WWLabel -from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock -from electrum.gui.qt.wizard.wizard import WizardComponent +from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWXPub, WCHWUninitialized from .jade import JadePlugin @@ -47,9 +43,9 @@ class Plugin(JadePlugin, QtPluginBase): def extend_wizard(self, wizard: 'QENewWalletWizard'): super().extend_wizard(wizard) views = { - 'jade_start': { 'gui': WCScriptAndDerivation }, - 'jade_xpub': { 'gui': WCJadeXPub }, - 'jade_not_initialized': {'gui': WCJadeNope}, + 'jade_start': {'gui': WCScriptAndDerivation}, + 'jade_xpub': {'gui': WCHWXPub}, + 'jade_not_initialized': {'gui': WCHWUninitialized}, 'jade_unlock': {'gui': WCHWUnlock} } wizard.navmap_merge(views) @@ -64,82 +60,3 @@ class Jade_Handler(QtHandlerBase): def __init__(self, win): super(Jade_Handler, self).__init__(win, 'Jade') - -# TODO: almost verbatim copy of trezor WCTrezorXPub, generalize! -# problem: client.get_xpub is not uniform -class WCJadeXPub(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('jade') - self.busy_msg = _('Unlock your Jade') - 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'] = 'jade' - 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 WCJadeNope(WizardComponent): - def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Jade not initialized')) - self.layout().addWidget(WWLabel(_('This Jade is not initialized. Cannot continue'))) - diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py index 5fea71493..501eb66d7 100644 --- a/electrum/plugins/trezor/qt.py +++ b/electrum/plugins/trezor/qt.py @@ -17,7 +17,7 @@ from electrum.plugins.hw_wallet.plugin import only_hook_if_libraries_available from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton, OkButton, CloseButton, PasswordLineEdit, getOpenFileName, ChoiceWidget) -from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock +from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWXPub from electrum.gui.qt.wizard.wizard import WizardComponent from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings, @@ -473,11 +473,11 @@ class Plugin(TrezorPlugin, QtPlugin): def extend_wizard(self, wizard: 'QENewWalletWizard'): super().extend_wizard(wizard) views = { - 'trezor_start': { 'gui': WCScriptAndDerivation }, - 'trezor_xpub': { 'gui': WCTrezorXPub }, - 'trezor_not_initialized': { 'gui': WCTrezorInitMethod }, - 'trezor_choose_new_recover': { 'gui': WCTrezorInitParams }, - 'trezor_do_init': { 'gui': WCTrezorInit }, + 'trezor_start': {'gui': WCScriptAndDerivation}, + 'trezor_xpub': {'gui': WCTrezorXPub}, + 'trezor_not_initialized': {'gui': WCTrezorInitMethod}, + 'trezor_choose_new_recover': {'gui': WCTrezorInitParams}, + 'trezor_do_init': {'gui': WCTrezorInit}, 'trezor_unlock': {'gui': WCHWUnlock}, } wizard.navmap_merge(views) @@ -801,73 +801,12 @@ class SettingsDialog(WindowModalDialog): invoke_client(None) -class WCTrezorXPub(WizardComponent, Logger): +class WCTrezorXPub(WCHWXPub): 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('trezor') - self._busy = True - - self.xpub = None - self.root_fingerprint = None - self.label = None - self.soft_device_id = None + WCHWXPub.__init__(self, parent, wizard) - 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) - 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, True) - 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'] = 'trezor' - 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 + def get_xpub_from_client(self, client, derivation, xtype): + return client.get_xpub(derivation, xtype, True) class WCTrezorInitMethod(WizardComponent, Logger):