URI: 
       thw wallets: introduce HardwareHandlerBase - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 6760c3f252797d137ebceee5a71a7a86025f12ad
   DIR parent 3b7299bfde92262c0cf089c0692a3ceaaba69b95
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Tue, 31 Mar 2020 14:40:25 +0200
       
       hw wallets: introduce HardwareHandlerBase
       
       previously, client.handler was sometimes
       - an InstallWizard
       - a QtHandlerBase where win was an ElectrumWindow
       - a QtHandlerBase where win was an InstallWizard
       - a CmdLineHandler
       
       That's just too much dynamic untyped undocumented polymorphism...
       Now it will never be an InstallWizard (replaced with QtHandlerBase where win is an InstallWizard),
       and now in all cases client.handler is an instance of HardwareHandlerBase, yay.
       
       related: #6063
       
       Diffstat:
         M electrum/base_wizard.py             |       1 +
         M electrum/gui/qt/installwizard.py    |       4 ----
         M electrum/keystore.py                |       4 ++--
         M electrum/plugin.py                  |      22 +++++++++++++---------
         M electrum/plugins/coldcard/cmdline.… |       4 +++-
         M electrum/plugins/hw_wallet/__init_… |       2 +-
         M electrum/plugins/hw_wallet/cmdline… |       4 +++-
         M electrum/plugins/hw_wallet/plugin.… |      50 +++++++++++++++++++++++++++++--
         M electrum/plugins/hw_wallet/qt.py    |       9 +++++----
         M electrum/plugins/keepkey/keepkey.py |       3 +--
         M electrum/plugins/keepkey/qt.py      |       6 +++---
         M electrum/plugins/safe_t/qt.py       |       6 +++---
         M electrum/plugins/safe_t/safe_t.py   |       3 +--
         M electrum/plugins/trezor/qt.py       |       6 +++---
         M electrum/plugins/trezor/trezor.py   |       3 +--
       
       15 files changed, 87 insertions(+), 40 deletions(-)
       ---
   DIR diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
       t@@ -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()
   DIR diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py
       t@@ -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)
   DIR diff --git a/electrum/keystore.py b/electrum/keystore.py
       t@@ -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):
       t@@ -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):
   DIR diff --git a/electrum/plugin.py b/electrum/plugin.py
       t@@ -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
        
        
       t@@ -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:
       t@@ -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:
       t@@ -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)
       t@@ -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.
       t@@ -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.'''
       t@@ -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.'''
       t@@ -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']:
   DIR diff --git a/electrum/plugins/coldcard/cmdline.py b/electrum/plugins/coldcard/cmdline.py
       t@@ -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
   DIR diff --git a/electrum/plugins/hw_wallet/__init__.py b/electrum/plugins/hw_wallet/__init__.py
       t@@ -1,2 +1,2 @@
       -from .plugin import HW_PluginBase, HardwareClientBase
       +from .plugin import HW_PluginBase, HardwareClientBase, HardwareHandlerBase
        from .cmdline import CmdLineHandler
   DIR diff --git a/electrum/plugins/hw_wallet/cmdline.py b/electrum/plugins/hw_wallet/cmdline.py
       t@@ -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
   DIR diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py
       t@@ -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):
       t@@ -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.
       t@@ -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()
        
       t@@ -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()])
        
   DIR diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py
       t@@ -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
       t@@ -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.'''
        
       t@@ -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)
       t@@ -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()
   DIR diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py
       t@@ -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)
       t@@ -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
   DIR diff --git a/electrum/plugins/keepkey/qt.py b/electrum/plugins/keepkey/qt.py
       t@@ -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):
       t@@ -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
   DIR diff --git a/electrum/plugins/safe_t/qt.py b/electrum/plugins/safe_t/qt.py
       t@@ -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):
       t@@ -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
   DIR diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py
       t@@ -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)
       t@@ -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
   DIR diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py
       t@@ -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):
       t@@ -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
   DIR diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py
       t@@ -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)
       t@@ -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