electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit bd32b88f625a698bcc466faa17a3adb58ed229a4
parent dace2e5495165a1a6ff186442966dde1934b69c0
Author: SomberNight <somber.night@protonmail.com>
Date:   Thu,  8 Nov 2018 19:46:15 +0100

introduce UserFacingException

we should not raise generic Exception when wanting to communicate with
the user. it makes distinguishing programming errors and messages hard,
as the caller will necessarily need to catch all Exceptions then

Diffstat:
Melectrum/gui/qt/main_window.py | 13+++++++++----
Melectrum/plugin.py | 4+++-
Melectrum/plugins/coldcard/coldcard.py | 14+++++++-------
Melectrum/plugins/digitalbitbox/digitalbitbox.py | 20++++++++++----------
Melectrum/plugins/hw_wallet/plugin.py | 6+++---
Melectrum/plugins/keepkey/keepkey.py | 12++++++------
Melectrum/plugins/ledger/ledger.py | 32++++++++++++++++----------------
Melectrum/plugins/safe_t/safe_t.py | 12++++++------
Melectrum/plugins/trezor/trezor.py | 12++++++------
Melectrum/util.py | 4++++
10 files changed, 70 insertions(+), 59 deletions(-)

diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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):