diff --git a/electrum/plugin.py b/electrum/plugin.py index 20375f205..3ac957f59 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -43,7 +43,7 @@ from .logging import get_logger, Logger if TYPE_CHECKING: from .plugins.hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase - from .keystore import Hardware_KeyStore + from .keystore import Hardware_KeyStore, KeyStore from .wallet import Abstract_Wallet @@ -776,3 +776,55 @@ class DeviceMgr(ThreadJob): except ImportError: ret["hidapi.version"] = None return ret + + def trigger_pairings( + self, + keystores: Sequence['KeyStore'], + *, + allow_user_interaction: bool = True, + devices: Sequence['Device'] = None, + ) -> None: + """Given a list of keystores, try to pair each with a connected hardware device. + + E.g. for a multisig-wallet, it is more user-friendly to use this method than to + try to pair each keystore individually. Consider the following scenario: + - three hw keystores in a 2-of-3 multisig wallet, devices d2 (for ks2) and d3 (for ks3) are connected + - assume none of the devices are paired yet + 1. if we tried to individually pair keystores, we might try with ks1 first + - but ks1 cannot be paired automatically, as neither d2 nor d3 matches the stored fingerprint + - the user might then be prompted if they want to manually pair ks1 with either d2 or d3, + which is confusing and error-prone. It's especially problematic if the hw device does + not support labels (such as Ledger), as then the user cannot easily distinguish + same-type devices. (see #4199) + 2. instead, if using this method, we would auto-pair ks2-d2 and ks3-d3 first, + and then tell the user ks1 could not be paired (and there are no devices left to try) + """ + from .keystore import Hardware_KeyStore + keystores = [ks for ks in keystores if isinstance(ks, Hardware_KeyStore)] + if not keystores: + return + if devices is None: + devices = self.scan_devices() + # first pair with all devices that can be auto-selected + for ks in keystores: + try: + ks.plugin.get_client( + keystore=ks, + force_pair=True, + allow_user_interaction=False, + devices=devices, + ) + except UserCancelled: + pass + if allow_user_interaction: + # now do manual selections + for ks in keystores: + try: + ks.plugin.get_client( + keystore=ks, + force_pair=True, + allow_user_interaction=True, + devices=devices, + ) + except UserCancelled: + pass diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py index 56b8b62a2..88b51d670 100644 --- a/electrum/plugins/hw_wallet/qt.py +++ b/electrum/plugins/hw_wallet/qt.py @@ -227,28 +227,8 @@ class QtPluginBase(object): 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) # Trigger pairings - 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 - + devmgr = self.device_manager() + trigger_pairings = partial(devmgr.trigger_pairings, relevant_keystores, allow_user_interaction=True) some_keystore = relevant_keystores[0] some_keystore.thread.add(trigger_pairings)