diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 8cd7d4a11..f1ff514e5 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -535,6 +535,7 @@ class BaseWizard(Logger): if self.wallet_type == 'standard' and isinstance(self.keystores[0], Hardware_KeyStore): # offer encrypting with a pw derived from the hw device k = self.keystores[0] # type: Hardware_KeyStore + assert isinstance(self.plugin, HW_PluginBase) try: k.handler = self.plugin.create_handler(self) password = k.get_password_for_storage_encryption() diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py index ed470d571..88056d8ae 100644 --- a/electrum/gui/qt/installwizard.py +++ b/electrum/gui/qt/installwizard.py @@ -358,10 +358,6 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): return db - def finished(self): - """Called in hardware client wrapper, in order to close popups.""" - return - def on_error(self, exc_info): if not isinstance(exc_info[1], UserCancelled): self.logger.error("on_error", exc_info=exc_info) diff --git a/electrum/keystore.py b/electrum/keystore.py index 7620edb2a..c0ec81c44 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -48,7 +48,7 @@ from .logging import Logger if TYPE_CHECKING: from .gui.qt.util import TaskThread - from .plugins.hw_wallet import HW_PluginBase, HardwareClientBase + from .plugins.hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase class KeyStore(Logger, ABC): @@ -723,7 +723,7 @@ class Hardware_KeyStore(Xpub, KeyStore): # device reconnects self.xpub = d.get('xpub') self.label = d.get('label') - self.handler = None + self.handler = None # type: Optional[HardwareHandlerBase] run_hook('init_keystore', self) def set_label(self, label): diff --git a/electrum/plugin.py b/electrum/plugin.py index 0bcc5d20e..c84cd9aea 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -39,7 +39,7 @@ from .simple_config import SimpleConfig from .logging import get_logger, Logger if TYPE_CHECKING: - from .plugins.hw_wallet import HW_PluginBase, HardwareClientBase + from .plugins.hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase from .keystore import Hardware_KeyStore @@ -386,7 +386,8 @@ class DeviceMgr(ThreadJob): def register_enumerate_func(self, func): self.enumerate_func.add(func) - def create_client(self, device: 'Device', handler, plugin: 'HW_PluginBase') -> Optional['HardwareClientBase']: + def create_client(self, device: 'Device', handler: Optional['HardwareHandlerBase'], + plugin: 'HW_PluginBase') -> Optional['HardwareClientBase']: # Get from cache first client = self.client_lookup(device.id_) if client: @@ -447,7 +448,8 @@ class DeviceMgr(ThreadJob): self.scan_devices() return self.client_lookup(id_) - def client_for_keystore(self, plugin: 'HW_PluginBase', handler, keystore: 'Hardware_KeyStore', + def client_for_keystore(self, plugin: 'HW_PluginBase', handler: Optional['HardwareHandlerBase'], + keystore: 'Hardware_KeyStore', force_pair: bool) -> Optional['HardwareClientBase']: self.logger.info("getting client for keystore") if handler is None: @@ -468,7 +470,7 @@ class DeviceMgr(ThreadJob): self.logger.info("end client for keystore") return client - def client_by_xpub(self, plugin: 'HW_PluginBase', xpub, handler, + def client_by_xpub(self, plugin: 'HW_PluginBase', xpub, handler: 'HardwareHandlerBase', devices: Iterable['Device']) -> Optional['HardwareClientBase']: _id = self.xpub_id(xpub) client = self.client_lookup(_id) @@ -482,7 +484,7 @@ class DeviceMgr(ThreadJob): if device.id_ == _id: return self.create_client(device, handler, plugin) - def force_pair_xpub(self, plugin: 'HW_PluginBase', handler, + def force_pair_xpub(self, plugin: 'HW_PluginBase', handler: 'HardwareHandlerBase', info: 'DeviceInfo', xpub, derivation) -> Optional['HardwareClientBase']: # The wallet has not been previously paired, so let the user # choose an unpaired device and compare its first address. @@ -510,7 +512,8 @@ class DeviceMgr(ThreadJob): 'its seed (and passphrase, if any). Otherwise all bitcoins you ' 'receive will be unspendable.').format(plugin.device)) - def unpaired_device_infos(self, handler, plugin: 'HW_PluginBase', devices: List['Device'] = None, + def unpaired_device_infos(self, handler: Optional['HardwareHandlerBase'], plugin: 'HW_PluginBase', + devices: List['Device'] = None, include_failing_clients=False) -> List['DeviceInfo']: '''Returns a list of DeviceInfo objects: one for each connected, unpaired device accepted by the plugin.''' @@ -539,7 +542,7 @@ class DeviceMgr(ThreadJob): return infos - def select_device(self, plugin: 'HW_PluginBase', handler, + def select_device(self, plugin: 'HW_PluginBase', handler: 'HardwareHandlerBase', keystore: 'Hardware_KeyStore', devices: List['Device'] = None) -> 'DeviceInfo': '''Ask the user to select a device to use if there is more than one, and return the DeviceInfo for the device.''' @@ -581,8 +584,9 @@ class DeviceMgr(ThreadJob): info = infos[c] # save new label keystore.set_label(info.label) - if handler.win.wallet is not None: - handler.win.wallet.save_keystore() + wallet = handler.get_wallet() + if wallet is not None: + wallet.save_keystore() return info def _scan_devices_with_hid(self) -> List['Device']: diff --git a/electrum/plugins/coldcard/cmdline.py b/electrum/plugins/coldcard/cmdline.py index 6e6e69a21..7df86f1f2 100644 --- a/electrum/plugins/coldcard/cmdline.py +++ b/electrum/plugins/coldcard/cmdline.py @@ -2,13 +2,15 @@ from electrum.plugin import hook from electrum.util import print_msg, raw_input, print_stderr from electrum.logging import get_logger +from ..hw_wallet.cmdline import CmdLineHandler + from .coldcard import ColdcardPlugin _logger = get_logger(__name__) -class ColdcardCmdLineHandler: +class ColdcardCmdLineHandler(CmdLineHandler): def get_passphrase(self, msg, confirm): raise NotImplementedError diff --git a/electrum/plugins/hw_wallet/__init__.py b/electrum/plugins/hw_wallet/__init__.py index 6e3ce1a00..8fd806783 100644 --- a/electrum/plugins/hw_wallet/__init__.py +++ b/electrum/plugins/hw_wallet/__init__.py @@ -1,2 +1,2 @@ -from .plugin import HW_PluginBase, HardwareClientBase +from .plugin import HW_PluginBase, HardwareClientBase, HardwareHandlerBase from .cmdline import CmdLineHandler diff --git a/electrum/plugins/hw_wallet/cmdline.py b/electrum/plugins/hw_wallet/cmdline.py index b91e094b8..5210267f1 100644 --- a/electrum/plugins/hw_wallet/cmdline.py +++ b/electrum/plugins/hw_wallet/cmdline.py @@ -1,11 +1,13 @@ from electrum.util import print_stderr, raw_input from electrum.logging import get_logger +from .plugin import HardwareHandlerBase + _logger = get_logger(__name__) -class CmdLineHandler: +class CmdLineHandler(HardwareHandlerBase): def get_passphrase(self, msg, confirm): import getpass diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index 97201f40e..61129601d 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -37,6 +37,7 @@ from electrum.keystore import Xpub, Hardware_KeyStore if TYPE_CHECKING: from electrum.wallet import Abstract_Wallet + from electrum.base_wizard import BaseWizard class HW_PluginBase(BasePlugin): @@ -63,7 +64,7 @@ class HW_PluginBase(BasePlugin): if isinstance(keystore, self.keystore_class): self.device_manager().unpair_xpub(keystore.xpub) - def setup_device(self, device_info, wizard, purpose): + def setup_device(self, device_info, wizard: 'BaseWizard', purpose): """Called when creating a new wallet or when using the device to decrypt an existing wallet. Select the device to use. If the device is uninitialized, go through the initialization process. @@ -139,15 +140,23 @@ class HW_PluginBase(BasePlugin): def is_outdated_fw_ignored(self) -> bool: return self._ignore_outdated_fw - def create_client(self, device: 'Device', handler) -> Optional['HardwareClientBase']: + def create_client(self, device: 'Device', + handler: Optional['HardwareHandlerBase']) -> Optional['HardwareClientBase']: raise NotImplementedError() - def get_xpub(self, device_id, derivation: str, xtype, wizard) -> str: + def get_xpub(self, device_id, derivation: str, xtype, wizard: 'BaseWizard') -> str: + raise NotImplementedError() + + def create_handler(self, window) -> 'HardwareHandlerBase': + # note: in Qt GUI, 'window' is either an ElectrumWindow or an InstallWizard raise NotImplementedError() class HardwareClientBase: + plugin: 'HW_PluginBase' + handler: Optional['HardwareHandlerBase'] + def is_pairable(self) -> bool: raise NotImplementedError() @@ -191,6 +200,41 @@ class HardwareClientBase: return password +class HardwareHandlerBase: + """An interface between the GUI and the device handling logic for handling I/O.""" + win = None + device: str + + def get_wallet(self) -> Optional['Abstract_Wallet']: + if self.win is not None: + if hasattr(self.win, 'wallet'): + return self.win.wallet + + def update_status(self, paired: bool) -> None: + pass + + def query_choice(self, msg: str, labels: Sequence[str]) -> Optional[int]: + raise NotImplementedError() + + def yes_no_question(self, msg: str) -> bool: + raise NotImplementedError() + + def show_message(self, msg: str, on_cancel=None) -> None: + raise NotImplementedError() + + def show_error(self, msg: str, blocking: bool = False) -> None: + raise NotImplementedError() + + def finished(self) -> None: + pass + + def get_word(self, msg: str) -> str: + raise NotImplementedError() + + def get_passphrase(self, msg: str, confirm: bool) -> Optional[str]: + raise NotImplementedError() + + def is_any_tx_output_on_change_branch(tx: PartialTransaction) -> bool: return any([txout.is_change for txout in tx.outputs()]) diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py index 4dbd2ac15..309e05783 100644 --- a/electrum/plugins/hw_wallet/qt.py +++ b/electrum/plugins/hw_wallet/qt.py @@ -35,13 +35,14 @@ from electrum.gui.qt.password_dialog import PasswordLayout, PW_PASSPHRASE from electrum.gui.qt.util import (read_QIcon, WWLabel, OkButton, WindowModalDialog, Buttons, CancelButton, TaskThread, char_width_in_lineedit) from electrum.gui.qt.main_window import StatusBarButton, ElectrumWindow +from electrum.gui.qt.installwizard import InstallWizard from electrum.i18n import _ from electrum.logging import Logger from electrum.util import parse_URI, InvalidBitcoinURI, UserCancelled from electrum.plugin import hook, DeviceUnpairableError -from .plugin import OutdatedHwFirmwareException, HW_PluginBase +from .plugin import OutdatedHwFirmwareException, HW_PluginBase, HardwareHandlerBase if TYPE_CHECKING: from electrum.wallet import Abstract_Wallet @@ -50,7 +51,7 @@ if TYPE_CHECKING: # The trickiest thing about this handler was getting windows properly # parented on macOS. -class QtHandlerBase(QObject, Logger): +class QtHandlerBase(HardwareHandlerBase, QObject, Logger): '''An interface between the GUI (here, QT) and the device handling logic for handling I/O.''' @@ -63,7 +64,7 @@ class QtHandlerBase(QObject, Logger): yes_no_signal = pyqtSignal(object) status_signal = pyqtSignal(object) - def __init__(self, win, device): + def __init__(self, win: Union[ElectrumWindow, InstallWizard], device: str): QObject.__init__(self) Logger.__init__(self) self.clear_signal.connect(self.clear_dialog) @@ -267,5 +268,5 @@ class QtPluginBase(object): dev_name = f"{plugin.device} ({keystore.label})" receive_address_e.addButton("eye1.png", show_address, _("Show on {}").format(dev_name)) - def create_handler(self, window: ElectrumWindow) -> 'QtHandlerBase': + def create_handler(self, window: Union[ElectrumWindow, InstallWizard]) -> 'QtHandlerBase': raise NotImplementedError() diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index 3a1c31c4b..e035307f9 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -282,7 +282,6 @@ class KeepKeyPlugin(HW_PluginBase): if client is None: raise UserFacingException(_('Failed to create a client for this device.') + '\n' + _('Make sure it is in the correct state.')) - # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) if not device_info.initialized: self.initialize_device(device_id, wizard, client.handler) @@ -294,7 +293,7 @@ class KeepKeyPlugin(HW_PluginBase): raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) devmgr = self.device_manager() client = devmgr.client_by_id(device_id) - client.handler = wizard + client.handler = self.create_handler(wizard) xpub = client.get_xpub(derivation, xtype) client.used() return xpub diff --git a/electrum/plugins/keepkey/qt.py b/electrum/plugins/keepkey/qt.py index eb6f0cb63..fce633df9 100644 --- a/electrum/plugins/keepkey/qt.py +++ b/electrum/plugins/keepkey/qt.py @@ -195,9 +195,6 @@ class QtPlugin(QtPluginBase): # icon_file # pin_matrix_widget_class - def create_handler(self, window): - return QtHandler(window, self.pin_matrix_widget_class(), self.device) - @only_hook_if_libraries_available @hook def receive_menu(self, menu, addrs, wallet): @@ -302,6 +299,9 @@ class Plugin(KeepKeyPlugin, QtPlugin): icon_paired = "keepkey.png" icon_unpaired = "keepkey_unpaired.png" + def create_handler(self, window): + return QtHandler(window, self.pin_matrix_widget_class(), self.device) + @classmethod def pin_matrix_widget_class(self): from keepkeylib.qt.pinmatrix import PinMatrixWidget diff --git a/electrum/plugins/safe_t/qt.py b/electrum/plugins/safe_t/qt.py index 224eec802..aa44495bb 100644 --- a/electrum/plugins/safe_t/qt.py +++ b/electrum/plugins/safe_t/qt.py @@ -71,9 +71,6 @@ class QtPlugin(QtPluginBase): # icon_file # pin_matrix_widget_class - def create_handler(self, window): - return QtHandler(window, self.pin_matrix_widget_class(), self.device) - @only_hook_if_libraries_available @hook def receive_menu(self, menu, addrs, wallet): @@ -176,6 +173,9 @@ class Plugin(SafeTPlugin, QtPlugin): icon_unpaired = "safe-t_unpaired.png" icon_paired = "safe-t.png" + def create_handler(self, window): + return QtHandler(window, self.pin_matrix_widget_class(), self.device) + @classmethod def pin_matrix_widget_class(self): from safetlib.qt.pinmatrix import PinMatrixWidget diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py index 8c10bf293..22064094c 100644 --- a/electrum/plugins/safe_t/safe_t.py +++ b/electrum/plugins/safe_t/safe_t.py @@ -256,7 +256,6 @@ class SafeTPlugin(HW_PluginBase): if client is None: raise UserFacingException(_('Failed to create a client for this device.') + '\n' + _('Make sure it is in the correct state.')) - # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) if not device_info.initialized: self.initialize_device(device_id, wizard, client.handler) @@ -268,7 +267,7 @@ class SafeTPlugin(HW_PluginBase): raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) devmgr = self.device_manager() client = devmgr.client_by_id(device_id) - client.handler = wizard + client.handler = self.create_handler(wizard) xpub = client.get_xpub(derivation, xtype) client.used() return xpub diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py index 3a8b2cba3..37a45ce6b 100644 --- a/electrum/plugins/trezor/qt.py +++ b/electrum/plugins/trezor/qt.py @@ -169,9 +169,6 @@ class QtPlugin(QtPluginBase): # icon_file # pin_matrix_widget_class - def create_handler(self, window): - return QtHandler(window, self.pin_matrix_widget_class(), self.device) - @only_hook_if_libraries_available @hook def receive_menu(self, menu, addrs, wallet): @@ -377,6 +374,9 @@ class Plugin(TrezorPlugin, QtPlugin): icon_unpaired = "trezor_unpaired.png" icon_paired = "trezor.png" + def create_handler(self, window): + return QtHandler(window, self.pin_matrix_widget_class(), self.device) + @classmethod def pin_matrix_widget_class(self): from trezorlib.qt.pinmatrix import PinMatrixWidget diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 71341d91c..d1d94b010 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -282,7 +282,6 @@ class TrezorPlugin(HW_PluginBase): .format(self.device, client.label(), self.firmware_URL)) raise OutdatedHwFirmwareException(msg) - # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) if not device_info.initialized: self.initialize_device(device_id, wizard, client.handler) @@ -295,7 +294,7 @@ class TrezorPlugin(HW_PluginBase): raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) devmgr = self.device_manager() client = devmgr.client_by_id(device_id) - client.handler = wizard + client.handler = self.create_handler(wizard) xpub = client.get_xpub(derivation, xtype) client.used() return xpub