Browse Source

hww: smarter auto-selection of which device to pair with

scenario1:
- 2of2 multisig wallet with trezor1 and trezor2 keystores
- only trezor2 connected
- previously we would pair first keystore with connected device and then display error.
  now we will pair the device with the correct keystore on the first try

scenario2:
- standard wallet with trezor1 keystore
- trezor2 connected (different device)
- previously we would pair trezor2 with the keystore and then display error.
  now we will prompt the user to select which device to pair with (out of one)

related: #5789
master
SomberNight 6 years ago
parent
commit
4ef313a1ac
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 46
      electrum/plugin.py
  2. 11
      electrum/plugins/coldcard/coldcard.py
  3. 9
      electrum/plugins/digitalbitbox/digitalbitbox.py
  4. 11
      electrum/plugins/hw_wallet/plugin.py
  5. 35
      electrum/plugins/hw_wallet/qt.py
  6. 9
      electrum/plugins/keepkey/keepkey.py
  7. 9
      electrum/plugins/ledger/ledger.py
  8. 9
      electrum/plugins/safe_t/safe_t.py
  9. 9
      electrum/plugins/trezor/trezor.py

46
electrum/plugin.py

@ -29,7 +29,7 @@ import time
import threading import threading
import sys import sys
from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple, from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple,
Dict, Iterable, List) Dict, Iterable, List, Sequence)
from .i18n import _ from .i18n import _
from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException) from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException)
@ -289,6 +289,7 @@ class BasePlugin(Logger):
class DeviceUnpairableError(UserFacingException): pass class DeviceUnpairableError(UserFacingException): pass
class HardwarePluginLibraryUnavailable(Exception): pass class HardwarePluginLibraryUnavailable(Exception): pass
class CannotAutoSelectDevice(Exception): pass
class Device(NamedTuple): class Device(NamedTuple):
@ -460,18 +461,26 @@ class DeviceMgr(ThreadJob):
@with_scan_lock @with_scan_lock
def client_for_keystore(self, plugin: 'HW_PluginBase', handler: Optional['HardwareHandlerBase'], def client_for_keystore(self, plugin: 'HW_PluginBase', handler: Optional['HardwareHandlerBase'],
keystore: 'Hardware_KeyStore', keystore: 'Hardware_KeyStore',
force_pair: bool) -> Optional['HardwareClientBase']: force_pair: bool, *,
devices: Sequence['Device'] = None,
allow_user_interaction: bool = True) -> Optional['HardwareClientBase']:
self.logger.info("getting client for keystore") self.logger.info("getting client for keystore")
if handler is None: if handler is None:
raise Exception(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing.")) raise Exception(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing."))
handler.update_status(False) handler.update_status(False)
if devices is None:
devices = self.scan_devices() devices = self.scan_devices()
xpub = keystore.xpub xpub = keystore.xpub
derivation = keystore.get_derivation_prefix() derivation = keystore.get_derivation_prefix()
assert derivation is not None assert derivation is not None
client = self.client_by_xpub(plugin, xpub, handler, devices) client = self.client_by_xpub(plugin, xpub, handler, devices)
if client is None and force_pair: if client is None and force_pair:
info = self.select_device(plugin, handler, keystore, devices) try:
info = self.select_device(plugin, handler, keystore, devices,
allow_user_interaction=allow_user_interaction)
except CannotAutoSelectDevice:
pass
else:
client = self.force_pair_xpub(plugin, handler, info, xpub, derivation) client = self.force_pair_xpub(plugin, handler, info, xpub, derivation)
if client: if client:
handler.update_status(True) handler.update_status(True)
@ -481,7 +490,7 @@ class DeviceMgr(ThreadJob):
return client return client
def client_by_xpub(self, plugin: 'HW_PluginBase', xpub, handler: 'HardwareHandlerBase', def client_by_xpub(self, plugin: 'HW_PluginBase', xpub, handler: 'HardwareHandlerBase',
devices: Iterable['Device']) -> Optional['HardwareClientBase']: devices: Sequence['Device']) -> Optional['HardwareClientBase']:
_id = self.xpub_id(xpub) _id = self.xpub_id(xpub)
client = self.client_lookup(_id) client = self.client_lookup(_id)
if client: if client:
@ -523,7 +532,7 @@ class DeviceMgr(ThreadJob):
'receive will be unspendable.').format(plugin.device)) 'receive will be unspendable.').format(plugin.device))
def unpaired_device_infos(self, handler: Optional['HardwareHandlerBase'], plugin: 'HW_PluginBase', def unpaired_device_infos(self, handler: Optional['HardwareHandlerBase'], plugin: 'HW_PluginBase',
devices: List['Device'] = None, devices: Sequence['Device'] = None,
include_failing_clients=False) -> List['DeviceInfo']: include_failing_clients=False) -> List['DeviceInfo']:
'''Returns a list of DeviceInfo objects: one for each connected, '''Returns a list of DeviceInfo objects: one for each connected,
unpaired device accepted by the plugin.''' unpaired device accepted by the plugin.'''
@ -555,15 +564,17 @@ class DeviceMgr(ThreadJob):
return infos return infos
def select_device(self, plugin: 'HW_PluginBase', handler: 'HardwareHandlerBase', def select_device(self, plugin: 'HW_PluginBase', handler: 'HardwareHandlerBase',
keystore: 'Hardware_KeyStore', devices: List['Device'] = None) -> 'DeviceInfo': keystore: 'Hardware_KeyStore', devices: Sequence['Device'] = None,
'''Ask the user to select a device to use if there is more than one, *, allow_user_interaction: bool = True) -> 'DeviceInfo':
and return the DeviceInfo for the device.''' """Select the device to use for keystore."""
# ideally this should not be called from the GUI thread... # ideally this should not be called from the GUI thread...
# assert handler.get_gui_thread() != threading.current_thread(), 'must not be called from GUI thread' # assert handler.get_gui_thread() != threading.current_thread(), 'must not be called from GUI thread'
while True: while True:
infos = self.unpaired_device_infos(handler, plugin, devices) infos = self.unpaired_device_infos(handler, plugin, devices)
if infos: if infos:
break break
if not allow_user_interaction:
raise CannotAutoSelectDevice()
msg = _('Please insert your {}').format(plugin.device) msg = _('Please insert your {}').format(plugin.device)
if keystore.label: if keystore.label:
msg += ' ({})'.format(keystore.label) msg += ' ({})'.format(keystore.label)
@ -575,14 +586,14 @@ class DeviceMgr(ThreadJob):
if not handler.yes_no_question(msg): if not handler.yes_no_question(msg):
raise UserCancelled() raise UserCancelled()
devices = None devices = None
if len(infos) == 1:
return infos[0] # select device automatically. (but only if we have reasonable expectation it is the correct one)
# select device by id # method 1: select device by id
if keystore.soft_device_id: if keystore.soft_device_id:
for info in infos: for info in infos:
if info.soft_device_id == keystore.soft_device_id: if info.soft_device_id == keystore.soft_device_id:
return info return info
# select device by label automatically; # method 2: select device by label
# but only if not a placeholder label and only if there is no collision # but only if not a placeholder label and only if there is no collision
device_labels = [info.label for info in infos] device_labels = [info.label for info in infos]
if (keystore.label not in PLACEHOLDER_HW_CLIENT_LABELS if (keystore.label not in PLACEHOLDER_HW_CLIENT_LABELS
@ -590,6 +601,15 @@ class DeviceMgr(ThreadJob):
for info in infos: for info in infos:
if info.label == keystore.label: if info.label == keystore.label:
return info return info
# method 3: if there is only one device connected, and we don't have useful label/soft_device_id
# saved for keystore anyway, select it
if (len(infos) == 1
and keystore.label in PLACEHOLDER_HW_CLIENT_LABELS
and keystore.soft_device_id is None):
return infos[0]
if not allow_user_interaction:
raise CannotAutoSelectDevice()
# ask user to select device manually # ask user to select device manually
msg = _("Please select which {} device to use:").format(plugin.device) msg = _("Please select which {} device to use:").format(plugin.device)
descriptions = ["{label} ({init}, {transport})" descriptions = ["{label} ({init}, {transport})"
@ -638,7 +658,7 @@ class DeviceMgr(ThreadJob):
return devices return devices
@with_scan_lock @with_scan_lock
def scan_devices(self) -> List['Device']: def scan_devices(self) -> Sequence['Device']:
self.logger.info("scanning devices...") self.logger.info("scanning devices...")
# First see what's connected that we know about # First see what's connected that we know about

11
electrum/plugins/coldcard/coldcard.py

@ -4,7 +4,7 @@
# #
import os, time, io import os, time, io
import traceback import traceback
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
import struct import struct
from electrum import bip32 from electrum import bip32
@ -536,11 +536,12 @@ class ColdcardPlugin(HW_PluginBase):
xpub = client.get_xpub(derivation, xtype) xpub = client.get_xpub(derivation, xtype)
return xpub return xpub
def get_client(self, keystore, force_pair=True) -> 'CKCCClient': def get_client(self, keystore, force_pair=True, *,
devices=None, allow_user_interaction=True) -> Optional['CKCCClient']:
# Acquire a connection to the hardware device (via USB) # Acquire a connection to the hardware device (via USB)
devmgr = self.device_manager() client = super().get_client(keystore, force_pair,
handler = keystore.handler devices=devices,
client = devmgr.client_for_keystore(self, handler, keystore, force_pair) allow_user_interaction=allow_user_interaction)
if client is not None: if client is not None:
client.ping_check() client.ping_check()

9
electrum/plugins/digitalbitbox/digitalbitbox.py

@ -741,10 +741,11 @@ class DigitalBitboxPlugin(HW_PluginBase):
return xpub return xpub
def get_client(self, keystore, force_pair=True): def get_client(self, keystore, force_pair=True, *,
devmgr = self.device_manager() devices=None, allow_user_interaction=True):
handler = keystore.handler client = super().get_client(keystore, force_pair,
client = devmgr.client_for_keystore(self, handler, keystore, force_pair) devices=devices,
allow_user_interaction=allow_user_interaction)
if client is not None: if client is not None:
client.check_device_dialog() client.check_device_dialog()
return client return client

11
electrum/plugins/hw_wallet/plugin.py

@ -83,8 +83,15 @@ class HW_PluginBase(BasePlugin):
""" """
raise NotImplementedError() raise NotImplementedError()
def get_client(self, keystore: 'Hardware_KeyStore', force_pair: bool = True) -> Optional['HardwareClientBase']: def get_client(self, keystore: 'Hardware_KeyStore', force_pair: bool = True, *,
raise NotImplementedError() devices: Sequence['Device'] = None,
allow_user_interaction: bool = True) -> Optional['HardwareClientBase']:
devmgr = self.device_manager()
handler = keystore.handler
client = devmgr.client_for_keystore(self, handler, keystore, force_pair,
devices=devices,
allow_user_interaction=allow_user_interaction)
return client
def show_address(self, wallet: 'Abstract_Wallet', address, keystore: 'Hardware_KeyStore' = None): def show_address(self, wallet: 'Abstract_Wallet', address, keystore: 'Hardware_KeyStore' = None):
pass # implemented in child classes pass # implemented in child classes

35
electrum/plugins/hw_wallet/qt.py

@ -206,9 +206,11 @@ class QtPluginBase(object):
@hook @hook
def load_wallet(self: Union['QtPluginBase', HW_PluginBase], wallet: 'Abstract_Wallet', window: ElectrumWindow): def load_wallet(self: Union['QtPluginBase', HW_PluginBase], wallet: 'Abstract_Wallet', window: ElectrumWindow):
for keystore in wallet.get_keystores(): relevant_keystores = [keystore for keystore in wallet.get_keystores()
if not isinstance(keystore, self.keystore_class): if isinstance(keystore, self.keystore_class)]
continue if not relevant_keystores:
return
for keystore in relevant_keystores:
if not self.libraries_available: if not self.libraries_available:
message = keystore.plugin.get_library_not_available_message() message = keystore.plugin.get_library_not_available_message()
window.show_error(message) window.show_error(message)
@ -224,8 +226,31 @@ class QtPluginBase(object):
keystore.handler = handler keystore.handler = handler
keystore.thread = TaskThread(window, on_error=partial(self.on_task_thread_error, window, keystore)) keystore.thread = TaskThread(window, on_error=partial(self.on_task_thread_error, window, keystore))
self.add_show_address_on_hw_device_button_for_receive_addr(wallet, keystore, window) self.add_show_address_on_hw_device_button_for_receive_addr(wallet, keystore, window)
# Trigger a pairing # Trigger pairings
keystore.thread.add(partial(self.get_client, keystore)) def trigger_pairings():
devmgr = self.device_manager()
devices = devmgr.scan_devices()
# first pair with all devices that can be auto-selected
for keystore in relevant_keystores:
try:
self.get_client(keystore=keystore,
force_pair=True,
allow_user_interaction=False,
devices=devices)
except UserCancelled:
pass
# now do manual selections
for keystore in relevant_keystores:
try:
self.get_client(keystore=keystore,
force_pair=True,
allow_user_interaction=True,
devices=devices)
except UserCancelled:
pass
some_keystore = relevant_keystores[0]
some_keystore.thread.add(trigger_pairings)
def _on_status_bar_button_click(self, *, window: ElectrumWindow, keystore: 'Hardware_KeyStore'): def _on_status_bar_button_click(self, *, window: ElectrumWindow, keystore: 'Hardware_KeyStore'):
try: try:

9
electrum/plugins/keepkey/keepkey.py

@ -179,10 +179,11 @@ class KeepKeyPlugin(HW_PluginBase):
return client return client
def get_client(self, keystore, force_pair=True) -> Optional['KeepKeyClient']: def get_client(self, keystore, force_pair=True, *,
devmgr = self.device_manager() devices=None, allow_user_interaction=True) -> Optional['KeepKeyClient']:
handler = keystore.handler client = super().get_client(keystore, force_pair,
client = devmgr.client_for_keystore(self, handler, keystore, force_pair) devices=devices,
allow_user_interaction=allow_user_interaction)
# returns the client for a given keystore. can use xpub # returns the client for a given keystore. can use xpub
if client: if client:
client.used() client.used()

9
electrum/plugins/ledger/ledger.py

@ -612,11 +612,12 @@ class LedgerPlugin(HW_PluginBase):
xpub = client.get_xpub(derivation, xtype) xpub = client.get_xpub(derivation, xtype)
return xpub return xpub
def get_client(self, keystore, force_pair=True): def get_client(self, keystore, force_pair=True, *,
devices=None, allow_user_interaction=True):
# All client interaction should not be in the main GUI thread # All client interaction should not be in the main GUI thread
devmgr = self.device_manager() client = super().get_client(keystore, force_pair,
handler = keystore.handler devices=devices,
client = devmgr.client_for_keystore(self, handler, keystore, force_pair) allow_user_interaction=allow_user_interaction)
# returns the client for a given keystore. can use xpub # returns the client for a given keystore. can use xpub
#if client: #if client:
# client.used() # client.used()

9
electrum/plugins/safe_t/safe_t.py

@ -141,10 +141,11 @@ class SafeTPlugin(HW_PluginBase):
return client return client
def get_client(self, keystore, force_pair=True) -> Optional['SafeTClient']: def get_client(self, keystore, force_pair=True, *,
devmgr = self.device_manager() devices=None, allow_user_interaction=True) -> Optional['SafeTClient']:
handler = keystore.handler client = super().get_client(keystore, force_pair,
client = devmgr.client_for_keystore(self, handler, keystore, force_pair) devices=devices,
allow_user_interaction=allow_user_interaction)
# returns the client for a given keystore. can use xpub # returns the client for a given keystore. can use xpub
if client: if client:
client.used() client.used()

9
electrum/plugins/trezor/trezor.py

@ -177,10 +177,11 @@ class TrezorPlugin(HW_PluginBase):
# note that this call can still raise! # note that this call can still raise!
return TrezorClientBase(transport, handler, self) return TrezorClientBase(transport, handler, self)
def get_client(self, keystore, force_pair=True) -> Optional['TrezorClientBase']: def get_client(self, keystore, force_pair=True, *,
devmgr = self.device_manager() devices=None, allow_user_interaction=True) -> Optional['TrezorClientBase']:
handler = keystore.handler client = super().get_client(keystore, force_pair,
client = devmgr.client_for_keystore(self, handler, keystore, force_pair) devices=devices,
allow_user_interaction=allow_user_interaction)
# returns the client for a given keystore. can use xpub # returns the client for a given keystore. can use xpub
if client: if client:
client.used() client.used()

Loading…
Cancel
Save