URI: 
       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