tledger: fix enumerating ledger devices with new bitcoin app (1.5.1) - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit b78cbcffd11364ed4651af1886d507b36bc1b013 DIR parent aaff48720fac40fa4edfdf42927eff2ebeb35722 HTML Author: SomberNight <somber.night@protonmail.com> Date: Wed, 18 Nov 2020 15:14:55 +0100 ledger: fix enumerating ledger devices with new bitcoin app (1.5.1) see https://github.com/bitcoin-core/HWI/issues/402 Diffstat: M electrum/plugin.py | 17 ++++++++++++++--- M electrum/plugins/hw_wallet/plugin.… | 12 ++++++++++-- M electrum/plugins/ledger/ledger.py | 57 +++++++++++++++++++++++++------ 3 files changed, 71 insertions(+), 15 deletions(-) --- DIR diff --git a/electrum/plugin.py b/electrum/plugin.py t@@ -409,6 +409,7 @@ class DeviceMgr(ThreadJob): self.clients = {} # type: Dict[HardwareClientBase, Tuple[Union[str, bytes], str]] # What we recognise. (vendor_id, product_id) -> Plugin self._recognised_hardware = {} # type: Dict[Tuple[int, int], HW_PluginBase] + self._recognised_vendor = {} # type: Dict[int, HW_PluginBase] # vendor_id -> Plugin # Custom enumerate functions for devices we don't know about. self._enumerate_func = set() # Needs self.lock. t@@ -433,6 +434,10 @@ class DeviceMgr(ThreadJob): for pair in device_pairs: self._recognised_hardware[pair] = plugin + def register_vendor_ids(self, vendor_ids: Iterable[int], *, plugin: 'HW_PluginBase'): + for vendor_id in vendor_ids: + self._recognised_vendor[vendor_id] = plugin + def register_enumerate_func(self, func): with self.lock: self._enumerate_func.add(func) t@@ -589,7 +594,7 @@ class DeviceMgr(ThreadJob): devices = [dev for dev in devices if not self.xpub_by_id(dev.id_)] infos = [] for device in devices: - if device.product_key not in plugin.DEVICE_IDS: + if not plugin.can_recognize_device(device): continue try: client = self.create_client(device, handler, plugin) t@@ -680,11 +685,17 @@ class DeviceMgr(ThreadJob): devices = [] for d in hid.enumerate(0, 0): - product_key = (d['vendor_id'], d['product_id']) + vendor_id = d['vendor_id'] + product_key = (vendor_id, d['product_id']) + plugin = None if product_key in self._recognised_hardware: plugin = self._recognised_hardware[product_key] + elif vendor_id in self._recognised_vendor: + plugin = self._recognised_vendor[vendor_id] + if plugin: device = plugin.create_device_from_hid_enumeration(d, product_key=product_key) - devices.append(device) + if device: + devices.append(device) return devices @runs_in_hwd_thread DIR diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py t@@ -24,7 +24,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from typing import TYPE_CHECKING, Dict, List, Union, Tuple, Sequence, Optional, Type +from typing import TYPE_CHECKING, Dict, List, Union, Tuple, Sequence, Optional, Type, Iterable, Any from functools import partial from electrum.plugin import (BasePlugin, hook, Device, DeviceMgr, DeviceInfo, t@@ -51,6 +51,8 @@ class HW_PluginBase(BasePlugin): minimum_library = (0, ) maximum_library = (float('inf'), ) + DEVICE_IDS: Iterable[Any] + def __init__(self, parent, config, name): BasePlugin.__init__(self, parent, config, name) self.device = self.keystore_class.device t@@ -63,7 +65,7 @@ class HW_PluginBase(BasePlugin): def device_manager(self) -> 'DeviceMgr': return self.parent.device_manager - def create_device_from_hid_enumeration(self, d: dict, *, product_key) -> 'Device': + def create_device_from_hid_enumeration(self, d: dict, *, product_key) -> Optional['Device']: # Older versions of hid don't provide interface_number interface_number = d.get('interface_number', -1) usage_page = d['usage_page'] t@@ -192,6 +194,12 @@ class HW_PluginBase(BasePlugin): # note: in Qt GUI, 'window' is either an ElectrumWindow or an InstallWizard raise NotImplementedError() + def can_recognize_device(self, device: Device) -> bool: + """Whether the plugin thinks it can handle the given device. + Used for filtering all connected hardware devices to only those by this vendor. + """ + return device.product_key in self.DEVICE_IDS + class HardwareClientBase: DIR diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py t@@ -16,7 +16,7 @@ from electrum.wallet import Standard_Wallet from electrum.util import bfh, bh2u, versiontuple, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported from electrum.logging import get_logger -from electrum.plugin import runs_in_hwd_thread +from electrum.plugin import runs_in_hwd_thread, Device from ..hw_wallet import HW_PluginBase, HardwareClientBase from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, validate_op_return_output, LibraryFoundButUnusable t@@ -95,15 +95,7 @@ class Ledger_Client(HardwareClientBase): return self._product_key[0] == 0x2581 def device_model_name(self): - if self.is_hw1(): - return "Ledger HW.1" - if self._product_key == (0x2c97, 0x0000): - return "Ledger Blue" - if self._product_key == (0x2c97, 0x0001): - return "Ledger Nano S" - if self._product_key == (0x2c97, 0x0004): - return "Ledger Nano X" - return None + return LedgerPlugin.device_name_from_product_key(self._product_key) @runs_in_hwd_thread def has_usable_connection_with_device(self): t@@ -594,6 +586,11 @@ class LedgerPlugin(HW_PluginBase): (0x2c97, 0x0009), # RFU (0x2c97, 0x000a) # RFU ] + VENDOR_IDS = (0x2c97, ) + LEDGER_MODEL_IDS = { + 0x10: "Ledger Nano S", + 0x40: "Ledger Nano X", + } SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') def __init__(self, parent, config, name): t@@ -602,7 +599,10 @@ class LedgerPlugin(HW_PluginBase): self.libraries_available = self.check_libraries_available() if not self.libraries_available: return + # to support legacy devices and legacy firmwares self.device_manager().register_devices(self.DEVICE_IDS, plugin=self) + # to support modern firmware + self.device_manager().register_vendor_ids(self.VENDOR_IDS, plugin=self) def get_library_version(self): try: t@@ -617,6 +617,43 @@ class LedgerPlugin(HW_PluginBase): else: raise LibraryFoundButUnusable(library_version=version) + @classmethod + def _recognize_device(cls, product_key) -> Tuple[bool, Optional[str]]: + """Returns (can_recognize, model_name) tuple.""" + # legacy product_keys + if product_key in cls.DEVICE_IDS: + if product_key[0] == 0x2581: + return True, "Ledger HW.1" + if product_key == (0x2c97, 0x0000): + return True, "Ledger Blue" + if product_key == (0x2c97, 0x0001): + return True, "Ledger Nano S" + if product_key == (0x2c97, 0x0004): + return True, "Ledger Nano X" + return True, None + # modern product_keys + if product_key[0] == 0x2c97: + product_id = product_key[1] + model_id = product_id >> 8 + if model_id in cls.LEDGER_MODEL_IDS: + model_name = cls.LEDGER_MODEL_IDS[model_id] + return True, model_name + # give up + return False, None + + def can_recognize_device(self, device: Device) -> bool: + return self._recognize_device(device.product_key)[0] + + @classmethod + def device_name_from_product_key(cls, product_key) -> Optional[str]: + return cls._recognize_device(product_key)[1] + + def create_device_from_hid_enumeration(self, d, *, product_key): + device = super().create_device_from_hid_enumeration(d, product_key=product_key) + if not self.can_recognize_device(device): + return None + return device + @runs_in_hwd_thread def get_btchip_device(self, device): ledger = False