URI: 
       thww: distinguish devices based on "soft device id" (not just labels) - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 9d0bb295e6f55a2bff9f5b6770fa744c16af6e8a
   DIR parent 7dabbdd08234ae036177d056a513a2189e9f14e1
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Wed,  8 Apr 2020 14:43:01 +0200
       
       hww: distinguish devices based on "soft device id" (not just labels)
       
       fixes #5759
       
       Diffstat:
         M electrum/base_wizard.py             |       4 +++-
         M electrum/keystore.py                |       5 +++++
         M electrum/plugin.py                  |      14 +++++++++++---
         M electrum/plugins/hw_wallet/plugin.… |      12 +++++++++++-
         M electrum/plugins/keepkey/clientbas… |       3 +++
         M electrum/plugins/ledger/ledger.py   |      12 +++++++++++-
         M electrum/plugins/safe_t/clientbase… |       3 +++
         M electrum/plugins/trezor/clientbase… |       3 +++
       
       8 files changed, 50 insertions(+), 6 deletions(-)
       ---
   DIR diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
       t@@ -438,6 +438,7 @@ class BaseWizard(Logger):
                    if not client: raise Exception("failed to find client for device id")
                    root_fingerprint = client.request_root_fingerprint_from_device()
                    label = client.label()  # use this as device_info.label might be outdated!
       +            soft_device_id = client.get_soft_device_id()  # use this as device_info.device_id might be outdated!
                except ScriptTypeNotSupported:
                    raise  # this is handled in derivation_dialog
                except BaseException as e:
       t@@ -451,6 +452,7 @@ class BaseWizard(Logger):
                    'root_fingerprint': root_fingerprint,
                    'xpub': xpub,
                    'label': label,
       +            'soft_device_id': soft_device_id,
                }
                k = hardware_keystore(d)
                self.on_keystore(k)
       t@@ -612,7 +614,7 @@ class BaseWizard(Logger):
                if os.path.exists(path):
                    raise Exception('file already exists at path')
                if not self.pw_args:
       -            return
       +            return  # FIXME
                pw_args = self.pw_args
                self.pw_args = None  # clean-up so that it can get GC-ed
                storage = WalletStorage(path)
   DIR diff --git a/electrum/keystore.py b/electrum/keystore.py
       t@@ -724,6 +724,7 @@ class Hardware_KeyStore(Xpub, KeyStore):
                # device reconnects
                self.xpub = d.get('xpub')
                self.label = d.get('label')
       +        self.soft_device_id = d.get('soft_device_id')  # type: Optional[str]
                self.handler = None  # type: Optional[HardwareHandlerBase]
                run_hook('init_keystore', self)
        
       t@@ -747,6 +748,7 @@ class Hardware_KeyStore(Xpub, KeyStore):
                    'derivation': self.get_derivation_prefix(),
                    'root_fingerprint': self.get_root_fingerprint(),
                    'label':self.label,
       +            'soft_device_id': self.soft_device_id,
                }
        
            def unpaired(self):
       t@@ -788,6 +790,9 @@ class Hardware_KeyStore(Xpub, KeyStore):
                if self.label != client.label():
                    self.label = client.label()
                    self.is_requesting_to_be_rewritten_to_wallet_file = True
       +        if self.soft_device_id != client.get_soft_device_id():
       +            self.soft_device_id = client.get_soft_device_id()
       +            self.is_requesting_to_be_rewritten_to_wallet_file = True
        
        
        KeyStoreWithMPK = Union[KeyStore, MasterPublicKeyMixin]  # intersection really...
   DIR diff --git a/electrum/plugin.py b/electrum/plugin.py
       t@@ -306,6 +306,7 @@ class DeviceInfo(NamedTuple):
            initialized: Optional[bool] = None
            exception: Optional[Exception] = None
            plugin_name: Optional[str] = None  # manufacturer, e.g. "trezor"
       +    soft_device_id: Optional[str] = None  # if available, used to distinguish same-type hw devices
        
        
        class HardwarePluginToScan(NamedTuple):
       t@@ -548,7 +549,8 @@ class DeviceMgr(ThreadJob):
                    infos.append(DeviceInfo(device=device,
                                            label=client.label(),
                                            initialized=client.is_initialized(),
       -                                    plugin_name=plugin.name))
       +                                    plugin_name=plugin.name,
       +                                    soft_device_id=client.get_soft_device_id()))
        
                return infos
        
       t@@ -575,6 +577,11 @@ class DeviceMgr(ThreadJob):
                    devices = None
                if len(infos) == 1:
                    return infos[0]
       +        # select device by id
       +        if keystore.soft_device_id:
       +            for info in infos:
       +                if info.soft_device_id == keystore.soft_device_id:
       +                    return info
                # select device by label automatically;
                # but only if not a placeholder label and only if there is no collision
                device_labels = [info.label for info in infos]
       t@@ -583,7 +590,7 @@ class DeviceMgr(ThreadJob):
                    for info in infos:
                        if info.label == keystore.label:
                            return info
       -        # ask user to select device
       +        # ask user to select device manually
                msg = _("Please select which {} device to use:").format(plugin.device)
                descriptions = ["{label} ({init}, {transport})"
                                .format(label=info.label or _("An unnamed {}").format(info.plugin_name),
       t@@ -594,8 +601,9 @@ class DeviceMgr(ThreadJob):
                if c is None:
                    raise UserCancelled()
                info = infos[c]
       -        # save new label
       +        # save new label / soft_device_id
                keystore.set_label(info.label)
       +        keystore.soft_device_id = info.soft_device_id
                wallet = handler.get_wallet()
                if wallet is not None:
                    wallet.save_keystore()
   DIR diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py
       t@@ -167,7 +167,7 @@ class HW_PluginBase(BasePlugin):
        class HardwareClientBase:
        
            plugin: 'HW_PluginBase'
       -    handler: Optional['HardwareHandlerBase']
       +    handler = None  # type: Optional['HardwareHandlerBase']
        
            def is_pairable(self) -> bool:
                raise NotImplementedError()
       t@@ -191,6 +191,16 @@ class HardwareClientBase:
                """
                raise NotImplementedError()
        
       +    def get_soft_device_id(self) -> Optional[str]:
       +        """An id-like string that is used to distinguish devices programmatically.
       +        This is a long term id for the device, that does not change between reconnects.
       +        This method should not prompt the user, i.e. no user interaction, as it is used
       +        during USB device enumeration (called for each unpaired device).
       +        Stored in the wallet file.
       +        """
       +        # This functionality is optional. If not implemented just return None:
       +        return None
       +
            def has_usable_connection_with_device(self) -> bool:
                raise NotImplementedError()
        
   DIR diff --git a/electrum/plugins/keepkey/clientbase.py b/electrum/plugins/keepkey/clientbase.py
       t@@ -119,6 +119,9 @@ class KeepKeyClientBase(HardwareClientBase, GuiMixin, Logger):
            def label(self):
                return self.features.label
        
       +    def get_soft_device_id(self):
       +        return self.features.device_id
       +
            def is_initialized(self):
                return self.features.initialized
        
   DIR diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py
       t@@ -66,6 +66,7 @@ class Ledger_Client(HardwareClientBase):
                self.dongleObject = btchip(hidDevice)
                self.preflightDone = False
                self._is_hw1 = is_hw1
       +        self._soft_device_id = None
        
            def is_pairable(self):
                return True
       t@@ -82,6 +83,14 @@ class Ledger_Client(HardwareClientBase):
            def label(self):
                return ""
        
       +    def get_soft_device_id(self):
       +        if self._soft_device_id is None:
       +            # modern ledger can provide xpub without user interaction
       +            # (hw1 would prompt for PIN)
       +            if not self.is_hw1():
       +                self._soft_device_id = self.request_root_fingerprint_from_device()
       +        return self._soft_device_id
       +
            def is_hw1(self) -> bool:
                return self._is_hw1
        
       t@@ -176,7 +185,8 @@ class Ledger_Client(HardwareClientBase):
                            # Acquire the new client on the next run
                        else:
                            raise e
       -            if self.has_detached_pin_support(self.dongleObject) and not self.is_pin_validated(self.dongleObject) and (self.handler is not None):
       +            if self.has_detached_pin_support(self.dongleObject) and not self.is_pin_validated(self.dongleObject):
       +                assert self.handler, "no handler for client"
                        remaining_attempts = self.dongleObject.getVerifyPinRemainingAttempts()
                        if remaining_attempts != 1:
                            msg = "Enter your Ledger PIN - remaining attempts : " + str(remaining_attempts)
   DIR diff --git a/electrum/plugins/safe_t/clientbase.py b/electrum/plugins/safe_t/clientbase.py
       t@@ -121,6 +121,9 @@ class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
            def label(self):
                return self.features.label
        
       +    def get_soft_device_id(self):
       +        return self.features.device_id
       +
            def is_initialized(self):
                return self.features.initialized
        
   DIR diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py
       t@@ -98,6 +98,9 @@ class TrezorClientBase(HardwareClientBase, Logger):
            def label(self):
                return self.features.label
        
       +    def get_soft_device_id(self):
       +        return self.features.device_id
       +
            def is_initialized(self):
                return self.features.initialized