Browse Source

hw wallets: introduce HardwareHandlerBase

previously, client.handler was sometimes
- an InstallWizard
- a QtHandlerBase where win was an ElectrumWindow
- a QtHandlerBase where win was an InstallWizard
- a CmdLineHandler

That's just too much dynamic untyped undocumented polymorphism...
Now it will never be an InstallWizard (replaced with QtHandlerBase where win is an InstallWizard),
and now in all cases client.handler is an instance of HardwareHandlerBase, yay.

related: #6063
master
SomberNight 6 years ago
parent
commit
6760c3f252
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 1
      electrum/base_wizard.py
  2. 4
      electrum/gui/qt/installwizard.py
  3. 4
      electrum/keystore.py
  4. 22
      electrum/plugin.py
  5. 4
      electrum/plugins/coldcard/cmdline.py
  6. 2
      electrum/plugins/hw_wallet/__init__.py
  7. 4
      electrum/plugins/hw_wallet/cmdline.py
  8. 50
      electrum/plugins/hw_wallet/plugin.py
  9. 9
      electrum/plugins/hw_wallet/qt.py
  10. 3
      electrum/plugins/keepkey/keepkey.py
  11. 6
      electrum/plugins/keepkey/qt.py
  12. 6
      electrum/plugins/safe_t/qt.py
  13. 3
      electrum/plugins/safe_t/safe_t.py
  14. 6
      electrum/plugins/trezor/qt.py
  15. 3
      electrum/plugins/trezor/trezor.py

1
electrum/base_wizard.py

@ -535,6 +535,7 @@ class BaseWizard(Logger):
if self.wallet_type == 'standard' and isinstance(self.keystores[0], Hardware_KeyStore): if self.wallet_type == 'standard' and isinstance(self.keystores[0], Hardware_KeyStore):
# offer encrypting with a pw derived from the hw device # offer encrypting with a pw derived from the hw device
k = self.keystores[0] # type: Hardware_KeyStore k = self.keystores[0] # type: Hardware_KeyStore
assert isinstance(self.plugin, HW_PluginBase)
try: try:
k.handler = self.plugin.create_handler(self) k.handler = self.plugin.create_handler(self)
password = k.get_password_for_storage_encryption() password = k.get_password_for_storage_encryption()

4
electrum/gui/qt/installwizard.py

@ -358,10 +358,6 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
return db return db
def finished(self):
"""Called in hardware client wrapper, in order to close popups."""
return
def on_error(self, exc_info): def on_error(self, exc_info):
if not isinstance(exc_info[1], UserCancelled): if not isinstance(exc_info[1], UserCancelled):
self.logger.error("on_error", exc_info=exc_info) self.logger.error("on_error", exc_info=exc_info)

4
electrum/keystore.py

@ -48,7 +48,7 @@ from .logging import Logger
if TYPE_CHECKING: if TYPE_CHECKING:
from .gui.qt.util import TaskThread from .gui.qt.util import TaskThread
from .plugins.hw_wallet import HW_PluginBase, HardwareClientBase from .plugins.hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase
class KeyStore(Logger, ABC): class KeyStore(Logger, ABC):
@ -723,7 +723,7 @@ class Hardware_KeyStore(Xpub, KeyStore):
# device reconnects # device reconnects
self.xpub = d.get('xpub') self.xpub = d.get('xpub')
self.label = d.get('label') self.label = d.get('label')
self.handler = None self.handler = None # type: Optional[HardwareHandlerBase]
run_hook('init_keystore', self) run_hook('init_keystore', self)
def set_label(self, label): def set_label(self, label):

22
electrum/plugin.py

@ -39,7 +39,7 @@ from .simple_config import SimpleConfig
from .logging import get_logger, Logger from .logging import get_logger, Logger
if TYPE_CHECKING: if TYPE_CHECKING:
from .plugins.hw_wallet import HW_PluginBase, HardwareClientBase from .plugins.hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase
from .keystore import Hardware_KeyStore from .keystore import Hardware_KeyStore
@ -386,7 +386,8 @@ class DeviceMgr(ThreadJob):
def register_enumerate_func(self, func): def register_enumerate_func(self, func):
self.enumerate_func.add(func) self.enumerate_func.add(func)
def create_client(self, device: 'Device', handler, plugin: 'HW_PluginBase') -> Optional['HardwareClientBase']: def create_client(self, device: 'Device', handler: Optional['HardwareHandlerBase'],
plugin: 'HW_PluginBase') -> Optional['HardwareClientBase']:
# Get from cache first # Get from cache first
client = self.client_lookup(device.id_) client = self.client_lookup(device.id_)
if client: if client:
@ -447,7 +448,8 @@ class DeviceMgr(ThreadJob):
self.scan_devices() self.scan_devices()
return self.client_lookup(id_) return self.client_lookup(id_)
def client_for_keystore(self, plugin: 'HW_PluginBase', handler, keystore: 'Hardware_KeyStore', def client_for_keystore(self, plugin: 'HW_PluginBase', handler: Optional['HardwareHandlerBase'],
keystore: 'Hardware_KeyStore',
force_pair: bool) -> Optional['HardwareClientBase']: force_pair: bool) -> Optional['HardwareClientBase']:
self.logger.info("getting client for keystore") self.logger.info("getting client for keystore")
if handler is None: if handler is None:
@ -468,7 +470,7 @@ class DeviceMgr(ThreadJob):
self.logger.info("end client for keystore") self.logger.info("end client for keystore")
return client return client
def client_by_xpub(self, plugin: 'HW_PluginBase', xpub, handler, def client_by_xpub(self, plugin: 'HW_PluginBase', xpub, handler: 'HardwareHandlerBase',
devices: Iterable['Device']) -> Optional['HardwareClientBase']: devices: Iterable['Device']) -> Optional['HardwareClientBase']:
_id = self.xpub_id(xpub) _id = self.xpub_id(xpub)
client = self.client_lookup(_id) client = self.client_lookup(_id)
@ -482,7 +484,7 @@ class DeviceMgr(ThreadJob):
if device.id_ == _id: if device.id_ == _id:
return self.create_client(device, handler, plugin) return self.create_client(device, handler, plugin)
def force_pair_xpub(self, plugin: 'HW_PluginBase', handler, def force_pair_xpub(self, plugin: 'HW_PluginBase', handler: 'HardwareHandlerBase',
info: 'DeviceInfo', xpub, derivation) -> Optional['HardwareClientBase']: info: 'DeviceInfo', xpub, derivation) -> Optional['HardwareClientBase']:
# The wallet has not been previously paired, so let the user # The wallet has not been previously paired, so let the user
# choose an unpaired device and compare its first address. # choose an unpaired device and compare its first address.
@ -510,7 +512,8 @@ class DeviceMgr(ThreadJob):
'its seed (and passphrase, if any). Otherwise all bitcoins you ' 'its seed (and passphrase, if any). Otherwise all bitcoins you '
'receive will be unspendable.').format(plugin.device)) 'receive will be unspendable.').format(plugin.device))
def unpaired_device_infos(self, handler, plugin: 'HW_PluginBase', devices: List['Device'] = None, def unpaired_device_infos(self, handler: Optional['HardwareHandlerBase'], plugin: 'HW_PluginBase',
devices: List['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.'''
@ -539,7 +542,7 @@ class DeviceMgr(ThreadJob):
return infos return infos
def select_device(self, plugin: 'HW_PluginBase', handler, def select_device(self, plugin: 'HW_PluginBase', handler: 'HardwareHandlerBase',
keystore: 'Hardware_KeyStore', devices: List['Device'] = None) -> 'DeviceInfo': keystore: 'Hardware_KeyStore', devices: List['Device'] = None) -> 'DeviceInfo':
'''Ask the user to select a device to use if there is more than one, '''Ask the user to select a device to use if there is more than one,
and return the DeviceInfo for the device.''' and return the DeviceInfo for the device.'''
@ -581,8 +584,9 @@ class DeviceMgr(ThreadJob):
info = infos[c] info = infos[c]
# save new label # save new label
keystore.set_label(info.label) keystore.set_label(info.label)
if handler.win.wallet is not None: wallet = handler.get_wallet()
handler.win.wallet.save_keystore() if wallet is not None:
wallet.save_keystore()
return info return info
def _scan_devices_with_hid(self) -> List['Device']: def _scan_devices_with_hid(self) -> List['Device']:

4
electrum/plugins/coldcard/cmdline.py

@ -2,13 +2,15 @@ from electrum.plugin import hook
from electrum.util import print_msg, raw_input, print_stderr from electrum.util import print_msg, raw_input, print_stderr
from electrum.logging import get_logger from electrum.logging import get_logger
from ..hw_wallet.cmdline import CmdLineHandler
from .coldcard import ColdcardPlugin from .coldcard import ColdcardPlugin
_logger = get_logger(__name__) _logger = get_logger(__name__)
class ColdcardCmdLineHandler: class ColdcardCmdLineHandler(CmdLineHandler):
def get_passphrase(self, msg, confirm): def get_passphrase(self, msg, confirm):
raise NotImplementedError raise NotImplementedError

2
electrum/plugins/hw_wallet/__init__.py

@ -1,2 +1,2 @@
from .plugin import HW_PluginBase, HardwareClientBase from .plugin import HW_PluginBase, HardwareClientBase, HardwareHandlerBase
from .cmdline import CmdLineHandler from .cmdline import CmdLineHandler

4
electrum/plugins/hw_wallet/cmdline.py

@ -1,11 +1,13 @@
from electrum.util import print_stderr, raw_input from electrum.util import print_stderr, raw_input
from electrum.logging import get_logger from electrum.logging import get_logger
from .plugin import HardwareHandlerBase
_logger = get_logger(__name__) _logger = get_logger(__name__)
class CmdLineHandler: class CmdLineHandler(HardwareHandlerBase):
def get_passphrase(self, msg, confirm): def get_passphrase(self, msg, confirm):
import getpass import getpass

50
electrum/plugins/hw_wallet/plugin.py

@ -37,6 +37,7 @@ from electrum.keystore import Xpub, Hardware_KeyStore
if TYPE_CHECKING: if TYPE_CHECKING:
from electrum.wallet import Abstract_Wallet from electrum.wallet import Abstract_Wallet
from electrum.base_wizard import BaseWizard
class HW_PluginBase(BasePlugin): class HW_PluginBase(BasePlugin):
@ -63,7 +64,7 @@ class HW_PluginBase(BasePlugin):
if isinstance(keystore, self.keystore_class): if isinstance(keystore, self.keystore_class):
self.device_manager().unpair_xpub(keystore.xpub) self.device_manager().unpair_xpub(keystore.xpub)
def setup_device(self, device_info, wizard, purpose): def setup_device(self, device_info, wizard: 'BaseWizard', purpose):
"""Called when creating a new wallet or when using the device to decrypt """Called when creating a new wallet or when using the device to decrypt
an existing wallet. Select the device to use. If the device is an existing wallet. Select the device to use. If the device is
uninitialized, go through the initialization process. uninitialized, go through the initialization process.
@ -139,15 +140,23 @@ class HW_PluginBase(BasePlugin):
def is_outdated_fw_ignored(self) -> bool: def is_outdated_fw_ignored(self) -> bool:
return self._ignore_outdated_fw return self._ignore_outdated_fw
def create_client(self, device: 'Device', handler) -> Optional['HardwareClientBase']: def create_client(self, device: 'Device',
handler: Optional['HardwareHandlerBase']) -> Optional['HardwareClientBase']:
raise NotImplementedError() raise NotImplementedError()
def get_xpub(self, device_id, derivation: str, xtype, wizard) -> str: def get_xpub(self, device_id, derivation: str, xtype, wizard: 'BaseWizard') -> str:
raise NotImplementedError()
def create_handler(self, window) -> 'HardwareHandlerBase':
# note: in Qt GUI, 'window' is either an ElectrumWindow or an InstallWizard
raise NotImplementedError() raise NotImplementedError()
class HardwareClientBase: class HardwareClientBase:
plugin: 'HW_PluginBase'
handler: Optional['HardwareHandlerBase']
def is_pairable(self) -> bool: def is_pairable(self) -> bool:
raise NotImplementedError() raise NotImplementedError()
@ -191,6 +200,41 @@ class HardwareClientBase:
return password return password
class HardwareHandlerBase:
"""An interface between the GUI and the device handling logic for handling I/O."""
win = None
device: str
def get_wallet(self) -> Optional['Abstract_Wallet']:
if self.win is not None:
if hasattr(self.win, 'wallet'):
return self.win.wallet
def update_status(self, paired: bool) -> None:
pass
def query_choice(self, msg: str, labels: Sequence[str]) -> Optional[int]:
raise NotImplementedError()
def yes_no_question(self, msg: str) -> bool:
raise NotImplementedError()
def show_message(self, msg: str, on_cancel=None) -> None:
raise NotImplementedError()
def show_error(self, msg: str, blocking: bool = False) -> None:
raise NotImplementedError()
def finished(self) -> None:
pass
def get_word(self, msg: str) -> str:
raise NotImplementedError()
def get_passphrase(self, msg: str, confirm: bool) -> Optional[str]:
raise NotImplementedError()
def is_any_tx_output_on_change_branch(tx: PartialTransaction) -> bool: def is_any_tx_output_on_change_branch(tx: PartialTransaction) -> bool:
return any([txout.is_change for txout in tx.outputs()]) return any([txout.is_change for txout in tx.outputs()])

9
electrum/plugins/hw_wallet/qt.py

@ -35,13 +35,14 @@ from electrum.gui.qt.password_dialog import PasswordLayout, PW_PASSPHRASE
from electrum.gui.qt.util import (read_QIcon, WWLabel, OkButton, WindowModalDialog, from electrum.gui.qt.util import (read_QIcon, WWLabel, OkButton, WindowModalDialog,
Buttons, CancelButton, TaskThread, char_width_in_lineedit) Buttons, CancelButton, TaskThread, char_width_in_lineedit)
from electrum.gui.qt.main_window import StatusBarButton, ElectrumWindow from electrum.gui.qt.main_window import StatusBarButton, ElectrumWindow
from electrum.gui.qt.installwizard import InstallWizard
from electrum.i18n import _ from electrum.i18n import _
from electrum.logging import Logger from electrum.logging import Logger
from electrum.util import parse_URI, InvalidBitcoinURI, UserCancelled from electrum.util import parse_URI, InvalidBitcoinURI, UserCancelled
from electrum.plugin import hook, DeviceUnpairableError from electrum.plugin import hook, DeviceUnpairableError
from .plugin import OutdatedHwFirmwareException, HW_PluginBase from .plugin import OutdatedHwFirmwareException, HW_PluginBase, HardwareHandlerBase
if TYPE_CHECKING: if TYPE_CHECKING:
from electrum.wallet import Abstract_Wallet from electrum.wallet import Abstract_Wallet
@ -50,7 +51,7 @@ if TYPE_CHECKING:
# The trickiest thing about this handler was getting windows properly # The trickiest thing about this handler was getting windows properly
# parented on macOS. # parented on macOS.
class QtHandlerBase(QObject, Logger): class QtHandlerBase(HardwareHandlerBase, QObject, Logger):
'''An interface between the GUI (here, QT) and the device handling '''An interface between the GUI (here, QT) and the device handling
logic for handling I/O.''' logic for handling I/O.'''
@ -63,7 +64,7 @@ class QtHandlerBase(QObject, Logger):
yes_no_signal = pyqtSignal(object) yes_no_signal = pyqtSignal(object)
status_signal = pyqtSignal(object) status_signal = pyqtSignal(object)
def __init__(self, win, device): def __init__(self, win: Union[ElectrumWindow, InstallWizard], device: str):
QObject.__init__(self) QObject.__init__(self)
Logger.__init__(self) Logger.__init__(self)
self.clear_signal.connect(self.clear_dialog) self.clear_signal.connect(self.clear_dialog)
@ -267,5 +268,5 @@ class QtPluginBase(object):
dev_name = f"{plugin.device} ({keystore.label})" dev_name = f"{plugin.device} ({keystore.label})"
receive_address_e.addButton("eye1.png", show_address, _("Show on {}").format(dev_name)) receive_address_e.addButton("eye1.png", show_address, _("Show on {}").format(dev_name))
def create_handler(self, window: ElectrumWindow) -> 'QtHandlerBase': def create_handler(self, window: Union[ElectrumWindow, InstallWizard]) -> 'QtHandlerBase':
raise NotImplementedError() raise NotImplementedError()

3
electrum/plugins/keepkey/keepkey.py

@ -282,7 +282,6 @@ class KeepKeyPlugin(HW_PluginBase):
if client is None: if client is None:
raise UserFacingException(_('Failed to create a client for this device.') + '\n' + raise UserFacingException(_('Failed to create a client for this device.') + '\n' +
_('Make sure it is in the correct state.')) _('Make sure it is in the correct state.'))
# fixme: we should use: client.handler = wizard
client.handler = self.create_handler(wizard) client.handler = self.create_handler(wizard)
if not device_info.initialized: if not device_info.initialized:
self.initialize_device(device_id, wizard, client.handler) self.initialize_device(device_id, wizard, client.handler)
@ -294,7 +293,7 @@ class KeepKeyPlugin(HW_PluginBase):
raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device))
devmgr = self.device_manager() devmgr = self.device_manager()
client = devmgr.client_by_id(device_id) client = devmgr.client_by_id(device_id)
client.handler = wizard client.handler = self.create_handler(wizard)
xpub = client.get_xpub(derivation, xtype) xpub = client.get_xpub(derivation, xtype)
client.used() client.used()
return xpub return xpub

6
electrum/plugins/keepkey/qt.py

@ -195,9 +195,6 @@ class QtPlugin(QtPluginBase):
# icon_file # icon_file
# pin_matrix_widget_class # pin_matrix_widget_class
def create_handler(self, window):
return QtHandler(window, self.pin_matrix_widget_class(), self.device)
@only_hook_if_libraries_available @only_hook_if_libraries_available
@hook @hook
def receive_menu(self, menu, addrs, wallet): def receive_menu(self, menu, addrs, wallet):
@ -302,6 +299,9 @@ class Plugin(KeepKeyPlugin, QtPlugin):
icon_paired = "keepkey.png" icon_paired = "keepkey.png"
icon_unpaired = "keepkey_unpaired.png" icon_unpaired = "keepkey_unpaired.png"
def create_handler(self, window):
return QtHandler(window, self.pin_matrix_widget_class(), self.device)
@classmethod @classmethod
def pin_matrix_widget_class(self): def pin_matrix_widget_class(self):
from keepkeylib.qt.pinmatrix import PinMatrixWidget from keepkeylib.qt.pinmatrix import PinMatrixWidget

6
electrum/plugins/safe_t/qt.py

@ -71,9 +71,6 @@ class QtPlugin(QtPluginBase):
# icon_file # icon_file
# pin_matrix_widget_class # pin_matrix_widget_class
def create_handler(self, window):
return QtHandler(window, self.pin_matrix_widget_class(), self.device)
@only_hook_if_libraries_available @only_hook_if_libraries_available
@hook @hook
def receive_menu(self, menu, addrs, wallet): def receive_menu(self, menu, addrs, wallet):
@ -176,6 +173,9 @@ class Plugin(SafeTPlugin, QtPlugin):
icon_unpaired = "safe-t_unpaired.png" icon_unpaired = "safe-t_unpaired.png"
icon_paired = "safe-t.png" icon_paired = "safe-t.png"
def create_handler(self, window):
return QtHandler(window, self.pin_matrix_widget_class(), self.device)
@classmethod @classmethod
def pin_matrix_widget_class(self): def pin_matrix_widget_class(self):
from safetlib.qt.pinmatrix import PinMatrixWidget from safetlib.qt.pinmatrix import PinMatrixWidget

3
electrum/plugins/safe_t/safe_t.py

@ -256,7 +256,6 @@ class SafeTPlugin(HW_PluginBase):
if client is None: if client is None:
raise UserFacingException(_('Failed to create a client for this device.') + '\n' + raise UserFacingException(_('Failed to create a client for this device.') + '\n' +
_('Make sure it is in the correct state.')) _('Make sure it is in the correct state.'))
# fixme: we should use: client.handler = wizard
client.handler = self.create_handler(wizard) client.handler = self.create_handler(wizard)
if not device_info.initialized: if not device_info.initialized:
self.initialize_device(device_id, wizard, client.handler) self.initialize_device(device_id, wizard, client.handler)
@ -268,7 +267,7 @@ class SafeTPlugin(HW_PluginBase):
raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device))
devmgr = self.device_manager() devmgr = self.device_manager()
client = devmgr.client_by_id(device_id) client = devmgr.client_by_id(device_id)
client.handler = wizard client.handler = self.create_handler(wizard)
xpub = client.get_xpub(derivation, xtype) xpub = client.get_xpub(derivation, xtype)
client.used() client.used()
return xpub return xpub

6
electrum/plugins/trezor/qt.py

@ -169,9 +169,6 @@ class QtPlugin(QtPluginBase):
# icon_file # icon_file
# pin_matrix_widget_class # pin_matrix_widget_class
def create_handler(self, window):
return QtHandler(window, self.pin_matrix_widget_class(), self.device)
@only_hook_if_libraries_available @only_hook_if_libraries_available
@hook @hook
def receive_menu(self, menu, addrs, wallet): def receive_menu(self, menu, addrs, wallet):
@ -377,6 +374,9 @@ class Plugin(TrezorPlugin, QtPlugin):
icon_unpaired = "trezor_unpaired.png" icon_unpaired = "trezor_unpaired.png"
icon_paired = "trezor.png" icon_paired = "trezor.png"
def create_handler(self, window):
return QtHandler(window, self.pin_matrix_widget_class(), self.device)
@classmethod @classmethod
def pin_matrix_widget_class(self): def pin_matrix_widget_class(self):
from trezorlib.qt.pinmatrix import PinMatrixWidget from trezorlib.qt.pinmatrix import PinMatrixWidget

3
electrum/plugins/trezor/trezor.py

@ -282,7 +282,6 @@ class TrezorPlugin(HW_PluginBase):
.format(self.device, client.label(), self.firmware_URL)) .format(self.device, client.label(), self.firmware_URL))
raise OutdatedHwFirmwareException(msg) raise OutdatedHwFirmwareException(msg)
# fixme: we should use: client.handler = wizard
client.handler = self.create_handler(wizard) client.handler = self.create_handler(wizard)
if not device_info.initialized: if not device_info.initialized:
self.initialize_device(device_id, wizard, client.handler) self.initialize_device(device_id, wizard, client.handler)
@ -295,7 +294,7 @@ class TrezorPlugin(HW_PluginBase):
raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device))
devmgr = self.device_manager() devmgr = self.device_manager()
client = devmgr.client_by_id(device_id) client = devmgr.client_by_id(device_id)
client.handler = wizard client.handler = self.create_handler(wizard)
xpub = client.get_xpub(derivation, xtype) xpub = client.get_xpub(derivation, xtype)
client.used() client.used()
return xpub return xpub

Loading…
Cancel
Save