diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 2d214dce7..d4d285008 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -55,7 +55,7 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis, export_meta, import_meta, bh2u, bfh, InvalidPassword, base_units, base_units_list, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, quantize_feerate, - UnknownBaseUnit, DECIMAL_POINT_DEFAULT) + UnknownBaseUnit, DECIMAL_POINT_DEFAULT, UserFacingException) from electrum.transaction import Transaction, TxOutput from electrum.address_synchronizer import AddTransactionException from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, @@ -300,12 +300,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.raise_() def on_error(self, exc_info): - if not isinstance(exc_info[1], UserCancelled): + e = exc_info[1] + if isinstance(e, UserCancelled): + pass + elif isinstance(e, UserFacingException): + self.show_error(str(e)) + else: try: traceback.print_exception(*exc_info) except OSError: - pass # see #4418; try to at least show popup: - self.show_error(str(exc_info[1])) + pass # see #4418 + self.show_error(str(e)) def on_network(self, event, *args): if event == 'wallet_updated': diff --git a/electrum/plugin.py b/electrum/plugin.py index df2b3719f..46d4fca93 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -32,7 +32,7 @@ from typing import NamedTuple, Any, Union, TYPE_CHECKING, Optional from .i18n import _ from .util import (profiler, PrintError, DaemonThread, UserCancelled, - ThreadJob, print_error) + ThreadJob, print_error, UserFacingException) from . import bip32 from . import plugins from .simple_config import SimpleConfig @@ -500,6 +500,8 @@ class DeviceMgr(ThreadJob, PrintError): continue try: client = self.create_client(device, handler, plugin) + except UserFacingException: + raise except BaseException as e: self.print_error(f'failed to create client for {plugin.name} at {device.path}: {repr(e)}') continue diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py index 444a34364..f15580053 100644 --- a/electrum/plugins/coldcard/coldcard.py +++ b/electrum/plugins/coldcard/coldcard.py @@ -13,7 +13,7 @@ from electrum.keystore import Hardware_KeyStore, xpubkey_to_pubkey, Xpub from electrum.transaction import Transaction from electrum.wallet import Standard_Wallet from electrum.crypto import hash_160 -from electrum.util import print_error, bfh, bh2u, versiontuple +from electrum.util import print_error, bfh, bh2u, versiontuple, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported from ..hw_wallet import HW_PluginBase @@ -190,8 +190,8 @@ class CKCCClient: try: __, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub) except InvalidMasterKeyVersionBytes: - raise Exception(_('Invalid xpub magic. Make sure your {} device is set to the correct chain.') - .format(self.device)) from None + raise UserFacingException(_('Invalid xpub magic. Make sure your {} device is set to the correct chain.') + .format(self.device)) from None if xtype != 'standard': xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) return xpub @@ -305,7 +305,7 @@ class Coldcard_KeyStore(Hardware_KeyStore): self.ux_busy = False if clear_client: self.client = None - raise Exception(message) + raise UserFacingException(message) def wrap_busy(func): # decorator: function takes over the UX on the device. @@ -318,7 +318,7 @@ class Coldcard_KeyStore(Hardware_KeyStore): return wrapper def decrypt_message(self, pubkey, message, password): - raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) + raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device)) @wrap_busy def sign_message(self, sequence, message, password): @@ -650,8 +650,8 @@ class ColdcardPlugin(HW_PluginBase): device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) + raise UserFacingException(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) client.handler = self.create_handler(wizard) def get_xpub(self, device_id, derivation, xtype, wizard): diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index bb9131c3d..25e69e27d 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -16,7 +16,7 @@ try: from electrum.i18n import _ from electrum.keystore import Hardware_KeyStore from ..hw_wallet import HW_PluginBase - from electrum.util import print_error, to_string, UserCancelled + from electrum.util import print_error, to_string, UserCancelled, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET import time @@ -114,7 +114,7 @@ class DigitalBitbox_Client(): def dbb_has_password(self): reply = self.hid_send_plain(b'{"ping":""}') if 'ping' not in reply: - raise Exception(_('Device communication error. Please unplug and replug your Digital Bitbox.')) + raise UserFacingException(_('Device communication error. Please unplug and replug your Digital Bitbox.')) if reply['ping'] == 'password': return True return False @@ -221,7 +221,7 @@ class DigitalBitbox_Client(): return else: if self.hid_send_encrypt(b'{"device":"info"}')['device']['lock']: - raise Exception(_("Full 2FA enabled. This is not supported yet.")) + raise UserFacingException(_("Full 2FA enabled. This is not supported yet.")) # Use existing seed self.isInitialized = True @@ -294,7 +294,7 @@ class DigitalBitbox_Client(): msg = ('{"seed":{"source": "create", "key": "%s", "filename": "%s", "entropy": "%s"}}' % (key, filename, 'Digital Bitbox Electrum Plugin')).encode('utf8') reply = self.hid_send_encrypt(msg) if 'error' in reply: - raise Exception(reply['error']['message']) + raise UserFacingException(reply['error']['message']) def dbb_erase(self): @@ -304,16 +304,16 @@ class DigitalBitbox_Client(): hid_reply = self.hid_send_encrypt(b'{"reset":"__ERASE__"}') self.handler.finished() if 'error' in hid_reply: - raise Exception(hid_reply['error']['message']) + raise UserFacingException(hid_reply['error']['message']) else: self.password = None - raise Exception('Device erased') + raise UserFacingException('Device erased') def dbb_load_backup(self, show_msg=True): backups = self.hid_send_encrypt(b'{"backup":"list"}') if 'error' in backups: - raise Exception(backups['error']['message']) + raise UserFacingException(backups['error']['message']) try: f = self.handler.win.query_choice(_("Choose a backup file:"), backups['backup']) except Exception: @@ -330,7 +330,7 @@ class DigitalBitbox_Client(): hid_reply = self.hid_send_encrypt(msg) self.handler.finished() if 'error' in hid_reply: - raise Exception(hid_reply['error']['message']) + raise UserFacingException(hid_reply['error']['message']) return True @@ -388,7 +388,7 @@ class DigitalBitbox_Client(): r = to_string(r, 'utf8') reply = json.loads(r) except Exception as e: - print_error('Exception caught ' + str(e)) + print_error('Exception caught ' + repr(e)) return reply @@ -405,7 +405,7 @@ class DigitalBitbox_Client(): if 'error' in reply: self.password = None except Exception as e: - print_error('Exception caught ' + str(e)) + print_error('Exception caught ' + repr(e)) return reply diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index a93b7a2bd..140c8447f 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -27,7 +27,7 @@ from electrum.plugin import BasePlugin, hook from electrum.i18n import _ from electrum.bitcoin import is_address, TYPE_SCRIPT -from electrum.util import bfh, versiontuple +from electrum.util import bfh, versiontuple, UserFacingException from electrum.transaction import opcodes, TxOutput, Transaction @@ -130,9 +130,9 @@ def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes: script = bfh(output.address) if not (script[0] == opcodes.OP_RETURN and script[1] == len(script) - 2 and script[1] <= 75): - raise Exception(_("Only OP_RETURN scripts, with one constant push, are supported.")) + raise UserFacingException(_("Only OP_RETURN scripts, with one constant push, are supported.")) if output.value != 0: - raise Exception(_("Amount for OP_RETURN output must be zero.")) + raise UserFacingException(_("Amount for OP_RETURN output must be zero.")) return script[2:] diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index 2498f4954..699ea9d74 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -2,7 +2,7 @@ from binascii import hexlify, unhexlify import traceback import sys -from electrum.util import bfh, bh2u, UserCancelled +from electrum.util import bfh, bh2u, UserCancelled, UserFacingException from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT from electrum.bip32 import deserialize_xpub from electrum import constants @@ -30,7 +30,7 @@ class KeepKey_KeyStore(Hardware_KeyStore): return self.plugin.get_client(self, force_pair) def decrypt_message(self, sequence, message, password): - raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) + raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device)) def sign_message(self, sequence, message, password): client = self.get_client() @@ -50,7 +50,7 @@ class KeepKey_KeyStore(Hardware_KeyStore): pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) tx_hash = txin['prevout_hash'] if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): - raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) + raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) prev_tx[tx_hash] = txin['prev_tx'] for x_pubkey in x_pubkeys: if not is_xpubkey(x_pubkey): @@ -138,7 +138,7 @@ class KeepKeyPlugin(HW_PluginBase): if handler: handler.show_error(msg) else: - raise Exception(msg) + raise UserFacingException(msg) return None return client @@ -242,8 +242,8 @@ class KeepKeyPlugin(HW_PluginBase): device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) + raise UserFacingException(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) if not device_info.initialized: diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index a2665e74f..11ff152ca 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -9,7 +9,7 @@ from electrum.i18n import _ from electrum.keystore import Hardware_KeyStore from electrum.transaction import Transaction from electrum.wallet import Standard_Wallet -from electrum.util import print_error, bfh, bh2u, versiontuple +from electrum.util import print_error, bfh, bh2u, versiontuple, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported from ..hw_wallet import HW_PluginBase @@ -46,7 +46,7 @@ def test_pin_unlocked(func): return func(self, *args, **kwargs) except BTChipException as e: if e.sw == 0x6982: - raise Exception(_('Your Ledger is locked. Please unlock it.')) + raise UserFacingException(_('Your Ledger is locked. Please unlock it.')) else: raise return catch_exception @@ -92,9 +92,9 @@ class Ledger_Client(): #self.get_client() # prompt for the PIN before displaying the dialog if necessary #self.handler.show_message("Computing master public key") if xtype in ['p2wpkh', 'p2wsh'] and not self.supports_native_segwit(): - raise Exception(MSG_NEEDS_FW_UPDATE_SEGWIT) + raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit(): - raise Exception(MSG_NEEDS_FW_UPDATE_SEGWIT) + raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) splitPath = bip32_path.split('/') if splitPath[0] == 'm': splitPath = splitPath[1:] @@ -154,7 +154,7 @@ class Ledger_Client(): if not checkFirmware(firmwareInfo): self.dongleObject.dongle.close() - raise Exception(MSG_NEEDS_FW_UPDATE_GENERIC) + raise UserFacingException(MSG_NEEDS_FW_UPDATE_GENERIC) try: self.dongleObject.getOperationMode() except BTChipException as e: @@ -172,18 +172,18 @@ class Ledger_Client(): 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 Exception('Aborted by user - please unplug the dongle and plug it again before retrying') + 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 Exception("Dongle is temporarily locked - please unplug it and replug it again") + raise UserFacingException("Dongle is temporarily locked - please unplug it and replug it again") if ((e.sw & 0xFFF0) == 0x63c0): - raise Exception("Invalid PIN - please unplug the dongle and plug it again before retrying") + 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 Exception("Invalid channel.\n" - "Please make sure that 'Browser support' is disabled on your device.") + raise UserFacingException("Invalid channel.\n" + "Please make sure that 'Browser support' is disabled on your device.") raise e def checkDevice(self): @@ -192,7 +192,7 @@ class Ledger_Client(): self.perform_hw1_preflight() except BTChipException as e: if (e.sw == 0x6d00 or e.sw == 0x6700): - raise Exception(_("Device not in Bitcoin mode")) from e + raise UserFacingException(_("Device not in Bitcoin mode")) from e raise e self.preflightDone = True @@ -238,7 +238,7 @@ class Ledger_KeyStore(Hardware_KeyStore): self.signing = False if clear_client: self.client = None - raise Exception(message) + raise UserFacingException(message) def set_and_unset_signing(func): """Function decorator to set and unset self.signing.""" @@ -258,7 +258,7 @@ class Ledger_KeyStore(Hardware_KeyStore): return address_path[2:] def decrypt_message(self, pubkey, message, password): - raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) + raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device)) @test_pin_unlocked @set_and_unset_signing @@ -357,7 +357,7 @@ class Ledger_KeyStore(Hardware_KeyStore): redeemScript = Transaction.get_preimage_script(txin) txin_prev_tx = txin.get('prev_tx') if txin_prev_tx is None and not Transaction.is_segwit_input(txin): - raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) + raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) txin_prev_tx_raw = txin_prev_tx.raw if txin_prev_tx else None inputs.append([txin_prev_tx_raw, txin['prevout_n'], @@ -586,8 +586,8 @@ class LedgerPlugin(HW_PluginBase): device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) + raise UserFacingException(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) client.handler = self.create_handler(wizard) client.get_xpub("m/44'/0'", 'standard') # TODO replace by direct derivation once Nano S > 1.1 diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py index 48f4e386a..c5a1031c4 100644 --- a/electrum/plugins/safe_t/safe_t.py +++ b/electrum/plugins/safe_t/safe_t.py @@ -2,7 +2,7 @@ from binascii import hexlify, unhexlify import traceback import sys -from electrum.util import bfh, bh2u, versiontuple, UserCancelled +from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT from electrum.bip32 import deserialize_xpub from electrum import constants @@ -31,7 +31,7 @@ class SafeTKeyStore(Hardware_KeyStore): return self.plugin.get_client(self, force_pair) def decrypt_message(self, sequence, message, password): - raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) + raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device)) def sign_message(self, sequence, message, password): client = self.get_client() @@ -51,7 +51,7 @@ class SafeTKeyStore(Hardware_KeyStore): pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) tx_hash = txin['prevout_hash'] if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): - raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) + raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) prev_tx[tx_hash] = txin['prev_tx'] for x_pubkey in x_pubkeys: if not is_xpubkey(x_pubkey): @@ -137,7 +137,7 @@ class SafeTPlugin(HW_PluginBase): if handler: handler.show_error(msg) else: - raise Exception(msg) + raise UserFacingException(msg) return None return client @@ -253,8 +253,8 @@ class SafeTPlugin(HW_PluginBase): device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) + raise UserFacingException(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) if not device_info.initialized: diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 91b5aab3d..a9ea13281 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -2,7 +2,7 @@ from binascii import hexlify, unhexlify import traceback import sys -from electrum.util import bfh, bh2u, versiontuple, UserCancelled +from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT from electrum.bip32 import deserialize_xpub from electrum import constants @@ -32,7 +32,7 @@ class TrezorKeyStore(Hardware_KeyStore): return self.plugin.get_client(self, force_pair) def decrypt_message(self, sequence, message, password): - raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) + raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device)) def sign_message(self, sequence, message, password): client = self.get_client() @@ -52,7 +52,7 @@ class TrezorKeyStore(Hardware_KeyStore): pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) tx_hash = txin['prevout_hash'] if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): - raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) + raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) prev_tx[tx_hash] = txin['prev_tx'] for x_pubkey in x_pubkeys: if not is_xpubkey(x_pubkey): @@ -139,7 +139,7 @@ class TrezorPlugin(HW_PluginBase): if handler: handler.show_error(msg) else: - raise Exception(msg) + raise UserFacingException(msg) return None return client @@ -265,8 +265,8 @@ class TrezorPlugin(HW_PluginBase): device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) + raise UserFacingException(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) if not device_info.initialized: diff --git a/electrum/util.py b/electrum/util.py index 064659f7f..fec507eb1 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -119,6 +119,10 @@ class WalletFileException(Exception): pass class BitcoinException(Exception): pass +class UserFacingException(Exception): + """Exception that contains information intended to be shown to the user.""" + + # Throw this exception to unwind the stack like when an error occurs. # However unlike other exceptions the user won't be informed. class UserCancelled(Exception):