commit b78cbcffd11364ed4651af1886d507b36bc1b013
parent aaff48720fac40fa4edfdf42927eff2ebeb35722
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:
3 files changed, 71 insertions(+), 15 deletions(-)
diff --git a/electrum/plugin.py b/electrum/plugin.py
@@ -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.
@@ -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)
@@ -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)
@@ -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
diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py
@@ -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,
@@ -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
@@ -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']
@@ -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:
diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py
@@ -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
@@ -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):
@@ -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):
@@ -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:
@@ -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