From 154adf00815f46c958f16e7cb5f6f8a6ab4f10a3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 18 Mar 2025 16:18:49 +0000 Subject: [PATCH] plugins: ledger: rm support for hw.1 This removes support for Ledger HW.1 and "Nano" (non-S) devices. These were manufactured/sold around 2015-2016, and are long unsupported by the upstream vendor. We previously added a deprecation warning to the GUI [0] released in 4.3.3 (2023-01-02), to warn owners of these devices. This PR now fully removes support. As a consequence, the unmaintained btchip-python dependency can now be removed, which solves [1]. [0]: https://github.com/spesmilo/electrum/commit/9b82eb6d06f8534652849f3370ab8af7d7ca9c7f [1]: https://github.com/spesmilo/electrum/issues/9370#issuecomment-2593675364 --- contrib/build-wine/deterministic.spec | 1 - .../deterministic-build/requirements-hw.txt | 2 - contrib/osx/osx.spec | 1 - contrib/requirements/requirements-hw.txt | 2 - electrum/plugins/ledger/auth2fa.py | 168 ----------------- electrum/plugins/ledger/ledger.py | 169 +++--------------- electrum/plugins/ledger/qt.py | 37 ---- 7 files changed, 21 insertions(+), 359 deletions(-) delete mode 100644 electrum/plugins/ledger/auth2fa.py diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index e7fb55270..f03b932b2 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -41,7 +41,6 @@ datas = [ datas += collect_data_files(f"{PYPKG}.plugins") datas += collect_data_files('trezorlib') # TODO is this needed? and same question for other hww libs datas += collect_data_files('safetlib') -datas += collect_data_files('btchip') datas += collect_data_files('ckcc') datas += collect_data_files('bitbox02') diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index 0dbef4fff..ffcc26784 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -4,8 +4,6 @@ base58==2.1.1 \ bitbox02==6.2.0 \ --hash=sha256:5a8290bd270468ccdf2e6ff7174d25ea2b2f191e19734a79aa573c2b982c266f \ --hash=sha256:cede06e399c98ed536fed6d8a421208daa00f97b697bd8363a941ac5f33309bf -btchip-python==0.1.32 \ - --hash=sha256:34f5e0c161c08f65dc0d070ba2ff4c315ed21c4b7e0faa32a46862d0dc1b8f55 cbor2==5.4.6 \ --hash=sha256:b893500db0fe033e570c3adc956af6eefc57e280026bd2d86fd53da9f1e594d7 certifi==2024.2.2 \ diff --git a/contrib/osx/osx.spec b/contrib/osx/osx.spec index 0d57467cb..e6501b395 100644 --- a/contrib/osx/osx.spec +++ b/contrib/osx/osx.spec @@ -44,7 +44,6 @@ datas = [ datas += collect_data_files(f"{PYPKG}.plugins") datas += collect_data_files('trezorlib') # TODO is this needed? and same question for other hww libs datas += collect_data_files('safetlib') -datas += collect_data_files('btchip') datas += collect_data_files('ckcc') datas += collect_data_files('bitbox02') diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index e552142a0..6f8b9dfb8 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -14,8 +14,6 @@ hidapi>=0.7.99.post15 libusb1>=1.6 # device plugin: ledger -# note: btchip-python only needed for "legacy" protocol and HW.1 support -btchip-python>=0.1.32 ledger-bitcoin>=0.2.0 hidapi diff --git a/electrum/plugins/ledger/auth2fa.py b/electrum/plugins/ledger/auth2fa.py deleted file mode 100644 index 9579116e7..000000000 --- a/electrum/plugins/ledger/auth2fa.py +++ /dev/null @@ -1,168 +0,0 @@ -import copy -from typing import TYPE_CHECKING - -from PyQt6.QtWidgets import (QDialog, QLineEdit, QTextEdit, QVBoxLayout, QLabel, - QWidget, QHBoxLayout, QComboBox) - -from btchip.btchip import BTChipException - -from electrum.gui.qt.util import PasswordLineEdit - -from electrum.i18n import _ -from electrum import constants, bitcoin -from electrum.logging import get_logger - -if TYPE_CHECKING: - from .ledger import Ledger_Client - - -_logger = get_logger(__name__) - - -DEBUG = False - -helpTxt = [_("Your Ledger Wallet wants to tell you a one-time PIN code.

" \ - "For best security you should unplug your device, open a text editor on another computer, " \ - "put your cursor into it, and plug your device into that computer. " \ - "It will output a summary of the transaction being signed and a one-time PIN.

" \ - "Verify the transaction summary and type the PIN code here.

" \ - "Before pressing enter, plug the device back into this computer.
"), - _("Verify the address below.
Type the character from your security card corresponding to the BOLD character."), - ] - -class LedgerAuthDialog(QDialog): - def __init__(self, handler, data, *, client: 'Ledger_Client'): - '''Ask user for 2nd factor authentication. Support text and security card methods. - Use last method from settings, but support downgrade. - ''' - QDialog.__init__(self, handler.top_level_window()) - self.handler = handler - self.txdata = data - self.idxs = self.txdata['keycardData'] if self.txdata['confirmationType'] > 1 else '' - self.setMinimumWidth(650) - self.setWindowTitle(_("Ledger Wallet Authentication")) - self.cfg = copy.deepcopy(self.handler.win.wallet.get_keystore().cfg) - self.dongle = client.dongleObject.dongle - self.pin = '' - - self.devmode = self.getDevice2FAMode() - if self.devmode == 0x11 or self.txdata['confirmationType'] == 1: - self.cfg['mode'] = 0 - - vbox = QVBoxLayout() - self.setLayout(vbox) - - def on_change_mode(idx): - self.cfg['mode'] = 0 if self.devmode == 0x11 else idx if idx > 0 else 1 - if self.cfg['mode'] > 0: - self.handler.win.wallet.get_keystore().cfg = self.cfg - self.handler.win.wallet.save_keystore() - self.update_dlg() - def return_pin(): - self.pin = self.pintxt.text() if self.txdata['confirmationType'] == 1 else self.cardtxt.text() - if self.cfg['mode'] == 1: - self.pin = ''.join(chr(int(str(i),16)) for i in self.pin) - self.accept() - - self.modebox = QWidget() - modelayout = QHBoxLayout() - self.modebox.setLayout(modelayout) - modelayout.addWidget(QLabel(_("Method:"))) - self.modes = QComboBox() - modelayout.addWidget(self.modes, 2) - modelayout.addStretch(1) - self.modebox.setMaximumHeight(50) - vbox.addWidget(self.modebox) - - self.populate_modes() - self.modes.currentIndexChanged.connect(on_change_mode) - - self.helpmsg = QTextEdit() - self.helpmsg.setStyleSheet("QTextEdit { color:black; background-color: lightgray; }") - self.helpmsg.setReadOnly(True) - vbox.addWidget(self.helpmsg) - - self.pinbox = QWidget() - pinlayout = QHBoxLayout() - self.pinbox.setLayout(pinlayout) - self.pintxt = PasswordLineEdit() - self.pintxt.setMaxLength(4) - self.pintxt.returnPressed.connect(return_pin) - pinlayout.addWidget(QLabel(_("Enter PIN:"))) - pinlayout.addWidget(self.pintxt) - pinlayout.addWidget(QLabel(_("NOT DEVICE PIN - see above"))) - pinlayout.addStretch(1) - self.pinbox.setVisible(self.cfg['mode'] == 0) - vbox.addWidget(self.pinbox) - - self.cardbox = QWidget() - card = QVBoxLayout() - self.cardbox.setLayout(card) - self.addrtext = QTextEdit() - self.addrtext.setStyleSheet(''' - QTextEdit { - color:blue; background-color:lightgray; padding:15px 10px; border:none; - font-size:20pt; font-family: "Courier New", monospace; } - ''') - self.addrtext.setReadOnly(True) - self.addrtext.setMaximumHeight(130) - card.addWidget(self.addrtext) - - def pin_changed(s): - if len(s) < len(self.idxs): - i = self.idxs[len(s)] - addr = self.txdata['address'] - if not constants.net.TESTNET: - text = addr[:i] + '' + addr[i:i+1] + '' + addr[i+1:] - else: - # pin needs to be created from mainnet address - addr_mainnet = bitcoin.script_to_address(bitcoin.address_to_script(addr), net=constants.BitcoinMainnet) - addr_mainnet = addr_mainnet[:i] + '' + addr_mainnet[i:i+1] + '' + addr_mainnet[i+1:] - text = str(addr) + '\n' + str(addr_mainnet) - self.addrtext.setHtml(str(text)) - else: - self.addrtext.setHtml(_("Press Enter")) - - pin_changed('') - cardpin = QHBoxLayout() - cardpin.addWidget(QLabel(_("Enter PIN:"))) - self.cardtxt = PasswordLineEdit() - self.cardtxt.setMaxLength(len(self.idxs)) - self.cardtxt.textChanged.connect(pin_changed) - self.cardtxt.returnPressed.connect(return_pin) - cardpin.addWidget(self.cardtxt) - cardpin.addWidget(QLabel(_("NOT DEVICE PIN - see above"))) - cardpin.addStretch(1) - card.addLayout(cardpin) - self.cardbox.setVisible(self.cfg['mode'] == 1) - vbox.addWidget(self.cardbox) - - self.update_dlg() - - def populate_modes(self): - self.modes.blockSignals(True) - self.modes.clear() - self.modes.addItem(_("Summary Text PIN (requires dongle replugging)") if self.txdata['confirmationType'] == 1 else _("Summary Text PIN is Disabled")) - if self.txdata['confirmationType'] > 1: - self.modes.addItem(_("Security Card Challenge")) - self.modes.blockSignals(False) - - def update_dlg(self): - self.modes.setCurrentIndex(self.cfg['mode']) - self.modebox.setVisible(True) - self.helpmsg.setText(helpTxt[self.cfg['mode']]) - self.helpmsg.setMinimumHeight(180 if self.txdata['confirmationType'] == 1 else 100) - self.helpmsg.setVisible(True) - self.pinbox.setVisible(self.cfg['mode'] == 0) - self.cardbox.setVisible(self.cfg['mode'] == 1) - self.pintxt.setFocus() if self.cfg['mode'] == 0 else self.cardtxt.setFocus() - self.setMaximumHeight(400) - - def getDevice2FAMode(self): - apdu = [0xe0, 0x24, 0x01, 0x00, 0x00, 0x01] # get 2fa mode - try: - mode = self.dongle.exchange(bytearray(apdu)) - return mode - except BTChipException as e: - _logger.debug('Device getMode Failed') - return 0x11 diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 53524ee53..74ab74596 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -39,13 +39,12 @@ try: from ledgercomm.interfaces.hid_device import HID # legacy imports - # note: we could replace "btchip" with "ledger_bitcoin.btchip" but the latter does not support HW.1 import hid - from btchip.btchipComm import HIDDongleHIDAPI - from btchip.btchip import btchip - from btchip.btchipUtils import compress_public_key - from btchip.bitcoinTransaction import bitcoinTransaction - from btchip.btchipException import BTChipException + from ledger_bitcoin.btchip.btchipComm import HIDDongleHIDAPI + from ledger_bitcoin.btchip.btchip import btchip + from ledger_bitcoin.btchip.btchipUtils import compress_public_key + from ledger_bitcoin.btchip.bitcoinTransaction import bitcoinTransaction + from ledger_bitcoin.btchip.btchipException import BTChipException LEDGER_BITCOIN = True except ImportError as e: @@ -310,7 +309,7 @@ class Ledger_Client(HardwareClientBase, ABC): def construct_new(*args, device: Device, **kwargs) -> 'Ledger_Client': """The 'real' constructor, that automatically decides which subclass to use.""" if LedgerPlugin.is_hw1(device.product_key): - return Ledger_Client_Legacy_HW1(*args, **kwargs, device=device) + raise Exception("ledger hw.1 devices are no longer supported") # for nano S or newer hw, decide which client impl to use based on software/firmware version: hid_device = HID() hid_device.path = device.path @@ -328,8 +327,10 @@ class Ledger_Client(HardwareClientBase, ABC): _logger.info(f"ledger_bitcoin.createClient() got exc: {e}. falling back to old plugin.") cl = None if isinstance(cl, ledger_bitcoin.client.NewClient): + _logger.debug(f"Ledger_Client.construct_new(). creating NewClient for {device=}.") return Ledger_Client_New(hid_device, *args, **kwargs) else: + _logger.debug(f"Ledger_Client.construct_new(). creating LegacyClient for {device=}.") return Ledger_Client_Legacy(hid_device, *args, **kwargs) def __init__(self, *, plugin: HW_PluginBase): @@ -552,7 +553,6 @@ class Ledger_Client_Legacy(Ledger_Client): chipInputs = [] redeemScripts = [] changePath = "" - output = None p2shTransaction = False segwitTransaction = False pin = "" @@ -606,30 +606,16 @@ class Ledger_Client_Legacy(Ledger_Client): if not is_txin_legacy_multisig(txin): self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen - txOutput = bytearray() - txOutput += var_int(len(tx.outputs())) - for o in tx.outputs(): - txOutput += int.to_bytes(o.value, length=8, byteorder="little", signed=False) - script = o.scriptpubkey - txOutput += var_int(len(script)) - txOutput += script - txOutput = bytes(txOutput) - if not self.supports_multi_output(): if len(tx.outputs()) > 2: self.give_error("Transaction with more than 2 outputs not supported") for txout in tx.outputs(): - if self.is_hw1() and txout.address and not is_b58_address(txout.address): - self.give_error(_("This {} device can only send to base58 addresses.").format(keystore.device)) if not txout.address: - if self.is_hw1(): - self.give_error(_("Only address outputs are supported by {}").format(keystore.device)) # note: max_size based on https://github.com/LedgerHQ/ledger-app-btc/commit/3a78dee9c0484821df58975803e40d58fbfc2c38#diff-c61ccd96a6d8b54d48f54a3bc4dfa7e2R26 validate_op_return_output(txout, max_size=190) # Output "change" detection - # - only one output and one change is authorized (for hw.1 and nano) - # - at most one output can bypass confirmation (~change) (for all) + # - at most one output can bypass confirmation (~change) if not p2shTransaction: has_change = False any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) @@ -643,10 +629,6 @@ class Ledger_Client_Legacy(Ledger_Client): assert changePath changePath = convert_bip32_intpath_to_strpath(changePath)[2:] has_change = True - else: - output = txout.address - else: - output = txout.address try: # Get trusted inputs from the original transactions @@ -681,22 +663,11 @@ class Ledger_Client_Legacy(Ledger_Client): firstTransaction = True inputIndex = 0 rawTx = tx.serialize_to_network(include_sigs=False) - if self.is_hw1(): - self.dongleObject.enableAlternate2fa(False) if segwitTransaction: self.dongleObject.startUntrustedTransaction(True, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version) # we don't set meaningful outputAddress, amount and fees # as we only care about the alternateEncoding==True branch outputData = self.dongleObject.finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) - outputData['outputData'] = txOutput - if outputData['confirmationNeeded']: - outputData['address'] = output - self.handler.finished() - # do the authenticate dialog and get pin: - pin = self.handler.get_auth(outputData, client=self) - if not pin: - raise UserWarning() - self.handler.show_message(_("Confirmed. Signing Transaction...")) while inputIndex < len(inputs): self.handler.show_message(_("Signing transaction...") + f" (phase2, {inputIndex}/{len(inputs)})") singleInput = [chipInputs[inputIndex]] @@ -716,24 +687,14 @@ class Ledger_Client_Legacy(Ledger_Client): # we don't set meaningful outputAddress, amount and fees # as we only care about the alternateEncoding==True branch outputData = self.dongleObject.finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) - outputData['outputData'] = txOutput - if outputData['confirmationNeeded']: - outputData['address'] = output - self.handler.finished() - # do the authenticate dialog and get pin: - pin = self.handler.get_auth(outputData, client=self) - if not pin: - raise UserWarning() - self.handler.show_message(_("Confirmed. Signing Transaction...")) - else: - # Sign input with the provided PIN - inputSignature = self.dongleObject.untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) - inputSignature[0] = 0x30 # force for 1.4.9+ - my_pubkey = inputs[inputIndex][4] - tx.add_signature_to_txin(txin_idx=inputIndex, - signing_pubkey=my_pubkey, - sig=inputSignature) - inputIndex = inputIndex + 1 + # Sign input with the provided PIN + inputSignature = self.dongleObject.untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) + inputSignature[0] = 0x30 # force for 1.4.9+ + my_pubkey = inputs[inputIndex][4] + tx.add_signature_to_txin(txin_idx=inputIndex, + signing_pubkey=my_pubkey, + sig=inputSignature) + inputIndex = inputIndex + 1 firstTransaction = False except UserWarning: self.handler.show_error(_('Cancelled by user')) @@ -770,12 +731,6 @@ class Ledger_Client_Legacy(Ledger_Client): try: info = self.dongleObject.signMessagePrepare(address_path, message) pin = "" - if info['confirmationNeeded']: - # do the authenticate dialog and get pin: - pin = self.handler.get_auth(info, client=self) - if not pin: - raise UserWarning(_('Cancelled by user')) - pin = str(pin).encode() signature = self.dongleObject.signMessageSign(pin) except BTChipException as e: if e.sw == 0x6a80: @@ -813,88 +768,6 @@ class Ledger_Client_Legacy(Ledger_Client): return bytes([27 + 4 + (signature[0] & 0x01)]) + r_padded + s_padded -class Ledger_Client_Legacy_HW1(Ledger_Client_Legacy): - """Even "legacy-er" client for deprecated HW.1 support.""" - - MIN_SUPPORTED_HW1_FW_VERSION = "1.0.2" - - def __init__(self, product_key: Tuple[int, int], - plugin: HW_PluginBase, device: 'Device'): - # note: Ledger_Client_Legacy.__init__ is *not* called - Ledger_Client.__init__(self, plugin=plugin) - self._product_key = product_key - assert self.is_hw1() - - ledger = device.product_key[1] in (0x3b7c, 0x4b7c) - dev = hid.device() - dev.open_path(device.path) - dev.set_nonblocking(True) - hid_device = HIDDongleHIDAPI(dev, ledger, debug=False) - self.dongleObject = btchip(hid_device) - - self._preflightDone = False - self.signing = False - self._soft_device_id = None - - @runs_in_hwd_thread - def checkDevice(self): - super().checkDevice() - self._perform_hw1_preflight() - - def _perform_hw1_preflight(self): - assert self.is_hw1() - if self._preflightDone: - return - try: - firmwareInfo = self.dongleObject.getFirmwareVersion() - firmware = firmwareInfo['version'] - if versiontuple(firmware) < versiontuple(self.MIN_SUPPORTED_HW1_FW_VERSION): - self.close() - raise UserFacingException( - _("Unsupported device firmware (too old).") + f"\nInstalled: {firmware}. Needed: >={self.MIN_SUPPORTED_HW1_FW_VERSION}") - try: - self.dongleObject.getOperationMode() - except BTChipException as e: - if (e.sw == 0x6985): - self.close() - self.handler.get_setup() - # Acquire the new client on the next run - else: - raise e - if self.has_detached_pin_support(self.dongleObject) and not self.is_pin_validated(self.dongleObject): - assert self.handler, "no handler for client" - remaining_attempts = self.dongleObject.getVerifyPinRemainingAttempts() - if remaining_attempts != 1: - msg = "Enter your Ledger PIN - remaining attempts : " + str(remaining_attempts) - else: - msg = "Enter your Ledger PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped." - confirmed, p, pin = self.password_dialog(msg) - if not confirmed: - raise UserFacingException(_('Aborted by user - please unplug the dongle and plug it again before retrying')) - pin = pin.encode() - self.dongleObject.verifyPin(pin) - except BTChipException as e: - if (e.sw == 0x6faa): - raise UserFacingException(_('Dongle is temporarily locked - please unplug it and replug it again')) - if ((e.sw & 0xFFF0) == 0x63c0): - raise UserFacingException(_('Invalid PIN - please unplug the dongle and plug it again before retrying')) - if e.sw == 0x6f00 and e.message == 'Invalid channel': - # based on docs 0x6f00 might be a more general error, hence we also compare message to be sure - raise UserFacingException(_("Invalid channel.\nPlease make sure that 'Browser support' is disabled on your device.")) - if e.sw == 0x6d00 or e.sw == 0x6700: - raise UserFacingException(_("Device not in Bitcoin mode")) from e - raise e - else: - deprecation_warning = ( - "This Ledger device (HW.1) is being deprecated.\n\nIt is no longer supported by Ledger.\n" - "Future versions of Electrum will no longer be compatible with it.\n\n" - "You should move your coins and migrate to a modern hardware device.") - _logger.warning(deprecation_warning.replace("\n", " ")) - if self.handler: - self.handler.show_message(deprecation_warning) - self._preflightDone = True - - class Ledger_Client_New(Ledger_Client): """Client based on the ledger_bitcoin library, targeting versions 2.1.* and above.""" @@ -1333,10 +1206,10 @@ class LedgerPlugin(HW_PluginBase): keystore_class = Ledger_KeyStore minimum_library = (0, 2, 0) maximum_library = (0, 4, 0) - DEVICE_IDS = [(0x2581, 0x1807), # HW.1 legacy btchip - (0x2581, 0x2b7c), # HW.1 transitional production - (0x2581, 0x3b7c), # HW.1 ledger production - (0x2581, 0x4b7c), # HW.1 ledger test + DEVICE_IDS = [(0x2581, 0x1807), # HW.1 legacy btchip # not supported anymore (but we log an exception) + (0x2581, 0x2b7c), # HW.1 transitional production # not supported anymore + (0x2581, 0x3b7c), # HW.1 ledger production # not supported anymore + (0x2581, 0x4b7c), # HW.1 ledger test # not supported anymore (0x2c97, 0x0000), # Blue (0x2c97, 0x0001), # Nano-S (0x2c97, 0x0004), # Nano-X diff --git a/electrum/plugins/ledger/qt.py b/electrum/plugins/ledger/qt.py index 6e2d1f19e..e36d38a13 100644 --- a/electrum/plugins/ledger/qt.py +++ b/electrum/plugins/ledger/qt.py @@ -52,46 +52,9 @@ class Plugin(LedgerPlugin, QtPluginBase): class Ledger_Handler(QtHandlerBase): - setup_signal = pyqtSignal() - auth_signal = pyqtSignal(object, object) MESSAGE_DIALOG_TITLE = _("Ledger Status") def __init__(self, win): super(Ledger_Handler, self).__init__(win, 'Ledger') - self.setup_signal.connect(self.setup_dialog) - self.auth_signal.connect(self.auth_dialog) - def word_dialog(self, msg): - response = QInputDialog.getText(self.top_level_window(), "Ledger Wallet Authentication", msg, QLineEdit.EchoMode.Password) - if not response[1]: - self.word = None - else: - self.word = str(response[0]) - self.done.set() - - def auth_dialog(self, data, client: 'Ledger_Client'): - try: - from .auth2fa import LedgerAuthDialog - except ImportError as e: - self.message_dialog(repr(e)) - return - dialog = LedgerAuthDialog(self, data, client=client) - dialog.exec() - self.word = dialog.pin - self.done.set() - - def get_auth(self, data, *, client: 'Ledger_Client'): - self.done.clear() - self.auth_signal.emit(data, client) - self.done.wait() - return self.word - - def get_setup(self): - self.done.clear() - self.setup_signal.emit() - self.done.wait() - return - - def setup_dialog(self): - self.show_error(_('Initialization of Ledger HW devices is currently disabled.'))