URI: 
       thardware wallets: create base class for HW Clients. add some type hints - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit f8c84fbb1e5d316a734e386ccc55190957a607ee
   DIR parent 2fec17760db5f0c99f4ec25d7914a1d592cdd7ea
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Mon, 11 Nov 2019 17:04:12 +0100
       
       hardware wallets: create base class for HW Clients. add some type hints
       
       Diffstat:
         M electrum/keystore.py                |       9 ++++-----
         M electrum/plugin.py                  |      34 +++++++++++++++++--------------
         M electrum/plugins/coldcard/coldcard… |       9 +++------
         M electrum/plugins/digitalbitbox/dig… |       5 +++--
         M electrum/plugins/hw_wallet/__init_… |       2 +-
         M electrum/plugins/hw_wallet/plugin.… |      46 +++++++++++++++++++++++++------
         M electrum/plugins/keepkey/clientbas… |       5 ++---
         M electrum/plugins/ledger/ledger.py   |       4 ++--
         M electrum/plugins/safe_t/clientbase… |       5 ++---
         M electrum/plugins/trezor/clientbase… |       6 ++----
       
       10 files changed, 75 insertions(+), 50 deletions(-)
       ---
   DIR diff --git a/electrum/keystore.py b/electrum/keystore.py
       t@@ -45,6 +45,7 @@ from .logging import Logger
        
        if TYPE_CHECKING:
            from .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
       +    from .plugins.hw_wallet import HW_PluginBase
        
        
        class KeyStore(Logger):
       t@@ -624,12 +625,10 @@ class Old_KeyStore(Deterministic_KeyStore):
                self.pw_hash_version = PW_HASH_VERSION_LATEST
        
        
       -
        class Hardware_KeyStore(KeyStore, Xpub):
       -    # Derived classes must set:
       -    #   - device
       -    #   - DEVICE_IDS
       -    #   - wallet_type
       +    hw_type: str
       +    device: str
       +    plugin: 'HW_PluginBase'
        
            type = 'hardware'
        
   DIR diff --git a/electrum/plugin.py b/electrum/plugin.py
       t@@ -28,7 +28,8 @@ import importlib.util
        import time
        import threading
        import sys
       -from typing import NamedTuple, Any, Union, TYPE_CHECKING, Optional
       +from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple,
       +                    Dict, Iterable, List)
        
        from .i18n import _
        from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException)
       t@@ -38,7 +39,7 @@ from .simple_config import SimpleConfig
        from .logging import get_logger, Logger
        
        if TYPE_CHECKING:
       -    from .plugins.hw_wallet import HW_PluginBase
       +    from .plugins.hw_wallet import HW_PluginBase, HardwareClientBase
            from .keystore import Hardware_KeyStore
        
        
       t@@ -234,7 +235,7 @@ def run_hook(name, *args):
        class BasePlugin(Logger):
        
            def __init__(self, parent, config, name):
       -        self.parent = parent  # The plugins object
       +        self.parent = parent  # type: Plugins  # The plugins object
                self.name = name
                self.config = config
                self.wallet = None
       t@@ -351,7 +352,7 @@ class DeviceMgr(ThreadJob):
                self.xpub_ids = {}
                # A list of clients.  The key is the client, the value is
                # a (path, id_) pair.
       -        self.clients = {}
       +        self.clients = {}  # type: Dict[HardwareClientBase, Tuple[Union[str, bytes], str]]
                # What we recognise.  Each entry is a (vendor_id, product_id)
                # pair.
                self.recognised_hardware = set()
       t@@ -382,7 +383,7 @@ class DeviceMgr(ThreadJob):
            def register_enumerate_func(self, func):
                self.enumerate_func.add(func)
        
       -    def create_client(self, device, handler, plugin):
       +    def create_client(self, device: 'Device', handler, plugin: 'HW_PluginBase') -> Optional['HardwareClientBase']:
                # Get from cache first
                client = self.client_lookup(device.id_)
                if client:
       t@@ -429,21 +430,22 @@ class DeviceMgr(ThreadJob):
                with self.lock:
                    self.xpub_ids[xpub] = id_
        
       -    def client_lookup(self, id_):
       +    def client_lookup(self, id_) -> Optional['HardwareClientBase']:
                with self.lock:
                    for client, (path, client_id) in self.clients.items():
                        if client_id == id_:
                            return client
                return None
        
       -    def client_by_id(self, id_):
       +    def client_by_id(self, id_) -> Optional['HardwareClientBase']:
                '''Returns a client for the device ID if one is registered.  If
                a device is wiped or in bootloader mode pairing is impossible;
                in such cases we communicate by device ID and not wallet.'''
                self.scan_devices()
                return self.client_lookup(id_)
        
       -    def client_for_keystore(self, plugin, handler, keystore: 'Hardware_KeyStore', force_pair):
       +    def client_for_keystore(self, plugin: 'HW_PluginBase', handler, keystore: 'Hardware_KeyStore',
       +                            force_pair: bool) -> Optional['HardwareClientBase']:
                self.logger.info("getting client for keystore")
                if handler is None:
                    raise Exception(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing."))
       t@@ -455,7 +457,7 @@ class DeviceMgr(ThreadJob):
                client = self.client_by_xpub(plugin, xpub, handler, devices)
                if client is None and force_pair:
                    info = self.select_device(plugin, handler, keystore, devices)
       -            client = self.force_pair_xpub(plugin, handler, info, xpub, derivation, devices)
       +            client = self.force_pair_xpub(plugin, handler, info, xpub, derivation)
                if client:
                    handler.update_status(True)
                if client:
       t@@ -463,7 +465,8 @@ class DeviceMgr(ThreadJob):
                self.logger.info("end client for keystore")
                return client
        
       -    def client_by_xpub(self, plugin, xpub, handler, devices):
       +    def client_by_xpub(self, plugin: 'HW_PluginBase', xpub, handler,
       +                       devices: Iterable['Device']) -> Optional['HardwareClientBase']:
                _id = self.xpub_id(xpub)
                client = self.client_lookup(_id)
                if client:
       t@@ -476,8 +479,8 @@ class DeviceMgr(ThreadJob):
                    if device.id_ == _id:
                        return self.create_client(device, handler, plugin)
        
       -
       -    def force_pair_xpub(self, plugin, handler, info, xpub, derivation, devices):
       +    def force_pair_xpub(self, plugin: 'HW_PluginBase', handler,
       +                        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.
                xtype = bip32.xpub_type(xpub)
       t@@ -533,7 +536,8 @@ class DeviceMgr(ThreadJob):
        
                return infos
        
       -    def select_device(self, plugin, handler, keystore, devices=None):
       +    def select_device(self, plugin: 'HW_PluginBase', handler,
       +                      keystore: 'Hardware_KeyStore', devices=None) -> 'DeviceInfo':
                '''Ask the user to select a device to use if there is more than one,
                and return the DeviceInfo for the device.'''
                while True:
       t@@ -569,7 +573,7 @@ class DeviceMgr(ThreadJob):
                    handler.win.wallet.save_keystore()
                return info
        
       -    def _scan_devices_with_hid(self):
       +    def _scan_devices_with_hid(self) -> List['Device']:
                try:
                    import hid
                except ImportError:
       t@@ -597,7 +601,7 @@ class DeviceMgr(ThreadJob):
                                              transport_ui_string='hid'))
                return devices
        
       -    def scan_devices(self):
       +    def scan_devices(self) -> List['Device']:
                self.logger.info("scanning devices...")
        
                # First see what's connected that we know about
   DIR diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py
       t@@ -18,7 +18,7 @@ from electrum.util import bfh, bh2u, versiontuple, UserFacingException
        from electrum.base_wizard import ScriptTypeNotSupported
        from electrum.logging import get_logger
        
       -from ..hw_wallet import HW_PluginBase
       +from ..hw_wallet import HW_PluginBase, HardwareClientBase
        from ..hw_wallet.plugin import LibraryFoundButUnusable, only_hook_if_libraries_available
        
        if TYPE_CHECKING:
       t@@ -60,9 +60,8 @@ except ImportError:
        
        CKCC_SIMULATED_PID = CKCC_PID ^ 0x55aa
        
       -class CKCCClient:
       -    # Challenge: I haven't found anywhere that defines a base class for this 'client',
       -    # nor an API (interface) to be met. Winging it. Gets called from lib/plugins.py mostly?
       +
       +class CKCCClient(HardwareClientBase):
        
            def __init__(self, plugin, handler, dev_path, is_simulator=False):
                self.device = plugin.device
       t@@ -463,11 +462,9 @@ class Coldcard_KeyStore(Hardware_KeyStore):
                    self.handler.show_error(exc)
        
        
       -
        class ColdcardPlugin(HW_PluginBase):
            keystore_class = Coldcard_KeyStore
            minimum_library = (0, 7, 7)
       -    client = None
        
            DEVICE_IDS = [
                (COINKITE_VID, CKCC_PID),
   DIR diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py
       t@@ -27,12 +27,13 @@ from electrum import constants
        from electrum.transaction import Transaction, PartialTransaction, PartialTxInput
        from electrum.i18n import _
        from electrum.keystore import Hardware_KeyStore
       -from ..hw_wallet import HW_PluginBase
        from electrum.util import to_string, UserCancelled, UserFacingException, bfh
        from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET
        from electrum.network import Network
        from electrum.logging import get_logger
        
       +from ..hw_wallet import HW_PluginBase, HardwareClientBase
       +
        
        _logger = get_logger(__name__)
        
       t@@ -63,7 +64,7 @@ MIN_MAJOR_VERSION = 5
        ENCRYPTION_PRIVKEY_KEY = 'encryptionprivkey'
        CHANNEL_ID_KEY = 'comserverchannelid'
        
       -class DigitalBitbox_Client():
       +class DigitalBitbox_Client(HardwareClientBase):
        
            def __init__(self, plugin, hidDevice):
                self.plugin = plugin
   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
       +from .plugin import HW_PluginBase, HardwareClientBase
        from .cmdline import CmdLineHandler
   DIR diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py
       t@@ -24,9 +24,9 @@
        # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        # SOFTWARE.
        
       -from typing import TYPE_CHECKING, Dict, List, Union, Tuple, Sequence
       +from typing import TYPE_CHECKING, Dict, List, Union, Tuple, Sequence, Optional, Type
        
       -from electrum.plugin import BasePlugin, hook
       +from electrum.plugin import BasePlugin, hook, Device, DeviceMgr
        from electrum.i18n import _
        from electrum.bitcoin import is_address, TYPE_SCRIPT, opcodes
        from electrum.util import bfh, versiontuple, UserFacingException
       t@@ -39,11 +39,7 @@ if TYPE_CHECKING:
        
        
        class HW_PluginBase(BasePlugin):
       -    # Derived classes provide:
       -    #
       -    #  class-static variables: client_class, firmware_URL, handler_class,
       -    #     libraries_available, libraries_URL, minimum_firmware,
       -    #     wallet_class, ckd_public, types, HidTransport
       +    keystore_class: Type['Hardware_KeyStore']
        
            minimum_library = (0, )
        
       t@@ -56,11 +52,11 @@ class HW_PluginBase(BasePlugin):
            def is_enabled(self):
                return True
        
       -    def device_manager(self):
       +    def device_manager(self) -> 'DeviceMgr':
                return self.parent.device_manager
        
            @hook
       -    def close_wallet(self, wallet):
       +    def close_wallet(self, wallet: 'Abstract_Wallet'):
                for keystore in wallet.get_keystores():
                    if isinstance(keystore, self.keystore_class):
                        self.device_manager().unpair_xpub(keystore.xpub)
       t@@ -141,6 +137,38 @@ 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']:
       +        raise NotImplementedError()
       +
       +    def get_xpub(self, device_id, derivation: str, xtype, wizard) -> str:
       +        raise NotImplementedError()
       +
       +
       +class HardwareClientBase:
       +
       +    def is_pairable(self) -> bool:
       +        raise NotImplementedError()
       +
       +    def close(self):
       +        raise NotImplementedError()
       +
       +    def timeout(self, cutoff) -> None:
       +        pass
       +
       +    def is_initialized(self) -> bool:
       +        """True if initialized, False if wiped."""
       +        raise NotImplementedError()
       +
       +    def label(self) -> str:
       +        """The name given by the user to the device."""
       +        raise NotImplementedError()
       +
       +    def has_usable_connection_with_device(self) -> bool:
       +        raise NotImplementedError()
       +
       +    def get_xpub(self, bip32_path: str, xtype) -> 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/keepkey/clientbase.py b/electrum/plugins/keepkey/clientbase.py
       t@@ -7,6 +7,7 @@ from electrum.util import UserCancelled
        from electrum.keystore import bip39_normalize_passphrase
        from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32
        from electrum.logging import Logger
       +from electrum.plugins.hw_wallet.plugin import HardwareClientBase
        
        
        class GuiMixin(object):
       t@@ -94,7 +95,7 @@ class GuiMixin(object):
                return self.proto.CharacterAck(**char_info)
        
        
       -class KeepKeyClientBase(GuiMixin, Logger):
       +class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
        
            def __init__(self, handler, plugin, proto):
                assert hasattr(self, 'tx_api')  # ProtocolMixin already constructed?
       t@@ -112,11 +113,9 @@ class KeepKeyClientBase(GuiMixin, Logger):
                return "%s/%s" % (self.label(), self.features.device_id)
        
            def label(self):
       -        '''The name given by the user to the device.'''
                return self.features.label
        
            def is_initialized(self):
       -        '''True if initialized, False if wiped.'''
                return self.features.initialized
        
            def is_pairable(self):
   DIR diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py
       t@@ -16,7 +16,7 @@ from electrum.util import bfh, bh2u, versiontuple, UserFacingException
        from electrum.base_wizard import ScriptTypeNotSupported
        from electrum.logging import get_logger
        
       -from ..hw_wallet import HW_PluginBase
       +from ..hw_wallet import HW_PluginBase, HardwareClientBase
        from ..hw_wallet.plugin import is_any_tx_output_on_change_branch
        
        
       t@@ -60,7 +60,7 @@ def test_pin_unlocked(func):
            return catch_exception
        
        
       -class Ledger_Client():
       +class Ledger_Client(HardwareClientBase):
            def __init__(self, hidDevice):
                self.dongleObject = btchip(hidDevice)
                self.preflightDone = False
   DIR diff --git a/electrum/plugins/safe_t/clientbase.py b/electrum/plugins/safe_t/clientbase.py
       t@@ -7,6 +7,7 @@ from electrum.util import UserCancelled
        from electrum.keystore import bip39_normalize_passphrase
        from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32
        from electrum.logging import Logger
       +from electrum.plugins.hw_wallet.plugin import HardwareClientBase
        
        
        class GuiMixin(object):
       t@@ -96,7 +97,7 @@ class GuiMixin(object):
                return self.proto.WordAck(word=word)
        
        
       -class SafeTClientBase(GuiMixin, Logger):
       +class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
        
            def __init__(self, handler, plugin, proto):
                assert hasattr(self, 'tx_api')  # ProtocolMixin already constructed?
       t@@ -114,11 +115,9 @@ class SafeTClientBase(GuiMixin, Logger):
                return "%s/%s" % (self.label(), self.features.device_id)
        
            def label(self):
       -        '''The name given by the user to the device.'''
                return self.features.label
        
            def is_initialized(self):
       -        '''True if initialized, False if wiped.'''
                return self.features.initialized
        
            def is_pairable(self):
   DIR diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py
       t@@ -7,7 +7,7 @@ from electrum.util import UserCancelled, UserFacingException
        from electrum.keystore import bip39_normalize_passphrase
        from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path
        from electrum.logging import Logger
       -from electrum.plugins.hw_wallet.plugin import OutdatedHwFirmwareException
       +from electrum.plugins.hw_wallet.plugin import OutdatedHwFirmwareException, HardwareClientBase
        
        from trezorlib.client import TrezorClient
        from trezorlib.exceptions import TrezorFailure, Cancelled, OutdatedFirmwareError
       t@@ -28,7 +28,7 @@ MESSAGES = {
        }
        
        
       -class TrezorClientBase(Logger):
       +class TrezorClientBase(HardwareClientBase, Logger):
            def __init__(self, transport, handler, plugin):
                if plugin.is_outdated_fw_ignored():
                    TrezorClient.is_outdated = lambda *args, **kwargs: False
       t@@ -86,11 +86,9 @@ class TrezorClientBase(Logger):
                return "%s/%s" % (self.label(), self.features.device_id)
        
            def label(self):
       -        '''The name given by the user to the device.'''
                return self.features.label
        
            def is_initialized(self):
       -        '''True if initialized, False if wiped.'''
                return self.features.initialized
        
            def is_pairable(self):