diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 77230839a..d6a2b20bf 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -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: @@ -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) @@ -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) diff --git a/electrum/keystore.py b/electrum/keystore.py index d82d3f260..4f1fb9090 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -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) @@ -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): @@ -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... diff --git a/electrum/plugin.py b/electrum/plugin.py index dbebf3a9f..bf775187a 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -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): @@ -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 @@ -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] @@ -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), @@ -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() diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index d5280802c..80f5b07f3 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -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() @@ -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() diff --git a/electrum/plugins/keepkey/clientbase.py b/electrum/plugins/keepkey/clientbase.py index 6b71ca320..92cae8207 100644 --- a/electrum/plugins/keepkey/clientbase.py +++ b/electrum/plugins/keepkey/clientbase.py @@ -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 diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 8e0040f5a..1866d735f 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -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 @@ -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 @@ -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) diff --git a/electrum/plugins/safe_t/clientbase.py b/electrum/plugins/safe_t/clientbase.py index 8ff666285..18bbdbab4 100644 --- a/electrum/plugins/safe_t/clientbase.py +++ b/electrum/plugins/safe_t/clientbase.py @@ -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 diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py index 101f927b6..19814b4c6 100644 --- a/electrum/plugins/trezor/clientbase.py +++ b/electrum/plugins/trezor/clientbase.py @@ -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