Browse Source

qt: add HWW unlock wizardcomponent

master
Sander van Grieken 2 years ago
parent
commit
8663d89f77
  1. 51
      electrum/gui/qt/__init__.py
  2. 4
      electrum/gui/qt/wizard/server_connect.py
  3. 67
      electrum/gui/qt/wizard/wallet.py
  4. 5
      electrum/gui/qt/wizard/wizard.py
  5. 10
      electrum/plugins/jade/jade.py
  6. 8
      electrum/plugins/jade/qt.py
  7. 3
      electrum/plugins/trezor/qt.py
  8. 8
      electrum/plugins/trezor/trezor.py
  9. 7
      electrum/wizard.py

51
electrum/gui/qt/__init__.py

@ -30,7 +30,9 @@ import traceback
import threading import threading
from typing import Optional, TYPE_CHECKING, List, Sequence from typing import Optional, TYPE_CHECKING, List, Sequence
from electrum import GuiImportError from electrum import GuiImportError, WalletStorage
from .wizard.server_connect import QEServerConnectWizard
from .wizard.wallet import QENewWalletWizard
try: try:
import PyQt5 import PyQt5
@ -401,6 +403,48 @@ class ElectrumGui(BaseElectrumGui, Logger):
return window return window
def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wallet]: def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wallet]:
dialog = QENewWalletWizard(self.config, self.app, self.plugins, self.daemon, path)
result = dialog.exec()
# TODO: use dialog.open() instead to avoid new event loop spawn?
self.logger.info(f'{result}')
if result == QENewWalletWizard.Rejected:
self.logger.info('ok bye bye')
return
d = dialog.get_wizard_data()
if d['wallet_is_open']:
for window in self.windows:
if window.wallet.storage.path == d['wallet_name']:
return window.wallet
raise Exception('found by wizard but not here?!')
if not d['wallet_exists']:
self.logger.info('about to create wallet')
dialog.create_storage()
wallet_file = dialog.path
else:
wallet_file = d['wallet_name']
storage = WalletStorage(wallet_file)
if storage.is_encrypted_with_user_pw() or storage.is_encrypted_with_hw_device():
storage.decrypt(d['password'])
db = WalletDB(storage.read(), manual_upgrades=False)
# TODO
# wizard.run_upgrades(storage, db)
# TODO
# return if wallet creation is not complete
# if storage is None or db.get_action():
# return
wallet = Wallet(db, storage, config=self.config)
wallet.start_network(self.daemon.network)
self.daemon.add_wallet(wallet)
return wallet
def _start_wizard_to_select_or_create_wallet2(self, path) -> Optional[Abstract_Wallet]:
wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self)
try: try:
path, storage = wizard.select_storage(path, self.daemon.get_wallet) path, storage = wizard.select_storage(path, self.daemon.get_wallet)
@ -441,9 +485,8 @@ class ElectrumGui(BaseElectrumGui, Logger):
if self.daemon.network: if self.daemon.network:
# first-start network-setup # first-start network-setup
if not self.config.cv.NETWORK_AUTO_CONNECT.is_set(): if not self.config.cv.NETWORK_AUTO_CONNECT.is_set():
wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) dialog = QEServerConnectWizard(self.config, self.app, self.plugins, self.daemon)
wizard.init_network(self.daemon.network) dialog.exec()
wizard.terminate()
# start network # start network
self.daemon.start_network() self.daemon.start_network()

4
electrum/gui/qt/wizard/server_connect.py

@ -1,10 +1,10 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from electrum.i18n import _ from electrum.i18n import _
from .wizard import QEAbstractWizard, WizardComponent
from electrum.wizard import ServerConnectWizard from electrum.wizard import ServerConnectWizard
from electrum.gui.qt.network_dialog import ProxyWidget, ServerWidget from electrum.gui.qt.network_dialog import ProxyWidget, ServerWidget
from electrum.gui.qt.util import ChoiceWidget from electrum.gui.qt.util import ChoiceWidget
from .wizard import QEAbstractWizard, WizardComponent
if TYPE_CHECKING: if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
@ -21,7 +21,7 @@ class QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard):
self.setWindowTitle(_('Network and server configuration')) self.setWindowTitle(_('Network and server configuration'))
# attach view names # attach gui classes
self.navmap_merge({ self.navmap_merge({
'autoconnect': { 'gui': WCAutoConnect }, 'autoconnect': { 'gui': WCAutoConnect },
'proxy_ask': { 'gui': WCProxyAsk }, 'proxy_ask': { 'gui': WCProxyAsk },

67
electrum/gui/qt/wizard/wallet.py

@ -86,8 +86,9 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
'last': lambda d: d['wallet_exists'] and not d['wallet_needs_hw_unlock'] 'last': lambda d: d['wallet_exists'] and not d['wallet_needs_hw_unlock']
}, },
'hw_unlock': { 'hw_unlock': {
'last': True, # 'last': True,
'gui': WCChooseHWDevice 'gui': WCChooseHWDevice,
'next': lambda d: self.on_hardware_device(d, new_wallet=False)
} }
}) })
@ -156,18 +157,12 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
path = os.path.join(os.path.dirname(self._daemon.config.get_wallet_path()), data['wallet_name']) path = os.path.join(os.path.dirname(self._daemon.config.get_wallet_path()), data['wallet_name'])
try:
super().create_storage(path, data) super().create_storage(path, data)
# minimally populate self after create # minimally populate self after create
self._password = data['password'] self._password = data['password']
self.path = path self.path = path
return True
except Exception as e:
self._logger.error(f"createStorage errored: {e!r}")
return False
class WCWalletName(WizardComponent): class WCWalletName(WizardComponent):
def __init__(self, parent, wizard): def __init__(self, parent, wizard):
@ -262,10 +257,10 @@ class WCWalletName(WizardComponent):
+ _("Press 'Next' to choose device to decrypt.") + _("Press 'Next' to choose device to decrypt.")
self.wallet_needs_hw_unlock = True self.wallet_needs_hw_unlock = True
else: else:
msg = _("Press 'Next' to open this wallet.") msg = _("Press 'Finish' to open this wallet.")
else: else:
msg = _("This file is already open in memory.") + "\n" \ msg = _("This file is already open in memory.") + "\n" \
+ _("Press 'Next' to create/focus window.") + _("Press 'Finish' to create/focus window.")
if msg is None: if msg is None:
msg = _('Cannot read file') msg = _('Cannot read file')
msg_label.setText(msg) msg_label.setText(msg)
@ -294,7 +289,7 @@ class WCWalletName(WizardComponent):
self.wizard_data['wallet_name'] = self.name_e.text() self.wizard_data['wallet_name'] = self.name_e.text()
self.wizard_data['wallet_exists'] = self.wallet_exists self.wizard_data['wallet_exists'] = self.wallet_exists
self.wizard_data['wallet_is_open'] = self.wallet_is_open self.wizard_data['wallet_is_open'] = self.wallet_is_open
self.wizard_data['wallet_open_password'] = self.pw_e.text() self.wizard_data['password'] = self.pw_e.text()
self.wizard_data['wallet_needs_hw_unlock'] = self.wallet_needs_hw_unlock self.wizard_data['wallet_needs_hw_unlock'] = self.wallet_needs_hw_unlock
@ -1135,3 +1130,53 @@ class WCWalletPasswordHardware(WizardComponent):
# client.handler = self.plugin.create_handler(self.wizard) # client.handler = self.plugin.create_handler(self.wizard)
self.wizard_data['password'] = client.get_password_for_storage_encryption() self.wizard_data['password'] = client.get_password_for_storage_encryption()
class WCHWUnlock(WizardComponent, Logger):
def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('unlock'))
Logger.__init__(self)
self.plugins = wizard.plugins
self.plugin = None
self._busy = True
self.password = None
self.ok_l = WWLabel(_('Hardware successfully unlocked'))
self.ok_l.setAlignment(Qt.AlignCenter)
self.layout().addWidget(self.ok_l)
def on_ready(self):
_name, _info = self.wizard_data['hardware_device']
self.plugin = self.plugins.get_plugin(_info.plugin_name)
self.title = _('Unlocking {} ({})').format(_info.model_name, _info.label)
device_id = _info.device.id_
client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)
client.handler = self.plugin.create_handler(self.wizard)
def unlock_task(client):
try:
self.password = client.get_password_for_storage_encryption()
except Exception as e:
# TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully
self.error = repr(e)
self.logger.error(repr(e))
self.unlock_done()
t = threading.Thread(target=unlock_task, args=(client,), daemon=True)
t.start()
def unlock_done(self):
self.logger.debug(f'Done unlock')
self.busy = False
self.validate()
def validate(self):
if self.password and not self.error:
self.apply()
self.valid = True
else:
self.valid = False
def apply(self):
if self.valid:
self.wizard_data['password'] = self.password

5
electrum/gui/qt/wizard/wizard.py

@ -13,15 +13,12 @@ from electrum.gui.qt.util import Buttons, icon_path, MessageBoxMixin
if TYPE_CHECKING: if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
from electrum.plugin import Plugins
from electrum.daemon import Daemon
from electrum.gui.qt import QElectrumApplication from electrum.gui.qt import QElectrumApplication
class QEAbstractWizard(QDialog, MessageBoxMixin): class QEAbstractWizard(QDialog, MessageBoxMixin):
_logger = get_logger(__name__) _logger = get_logger(__name__)
# def __init__(self, config: 'SimpleConfig', app: QApplication, plugins: 'Plugins', *, gui_object: 'ElectrumGui'):
def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication'): def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication'):
QDialog.__init__(self, None) QDialog.__init__(self, None)
self.app = app self.app = app
@ -51,6 +48,7 @@ class QEAbstractWizard(QDialog, MessageBoxMixin):
please_wait_layout.addWidget(self.please_wait_l) please_wait_layout.addWidget(self.please_wait_l)
please_wait_layout.addStretch(1) please_wait_layout.addStretch(1)
self.please_wait = QWidget() self.please_wait = QWidget()
self.please_wait.setVisible(False)
self.please_wait.setLayout(please_wait_layout) self.please_wait.setLayout(please_wait_layout)
error_layout = QVBoxLayout() error_layout = QVBoxLayout()
@ -63,6 +61,7 @@ class QEAbstractWizard(QDialog, MessageBoxMixin):
error_layout.addWidget(self.error_msg) error_layout.addWidget(self.error_msg)
error_layout.addStretch(1) error_layout.addStretch(1)
self.error = QWidget() self.error = QWidget()
self.error.setVisible(False)
self.error.setLayout(error_layout) self.error.setLayout(error_layout)
outer_vbox = QVBoxLayout(self) outer_vbox = QVBoxLayout(self)

10
electrum/plugins/jade/jade.py

@ -482,8 +482,11 @@ class JadePlugin(HW_PluginBase):
# new wizard # new wizard
def wizard_entry_for_device(self, device_info: 'DeviceInfo') -> str: def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str:
if new_wallet:
return 'jade_start' if device_info.initialized else 'jade_not_initialized' return 'jade_start' if device_info.initialized else 'jade_not_initialized'
else:
return 'jade_unlock'
# insert trezor pages in new wallet wizard # insert trezor pages in new wallet wizard
def extend_wizard(self, wizard: 'NewWalletWizard'): def extend_wizard(self, wizard: 'NewWalletWizard'):
@ -496,6 +499,9 @@ class JadePlugin(HW_PluginBase):
'accept': wizard.maybe_master_pubkey, 'accept': wizard.maybe_master_pubkey,
'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d) 'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d)
}, },
'jade_not_initialized': {} 'jade_not_initialized': {},
'jade_unlock': {
'last': True
},
} }
wizard.navmap_merge(views) wizard.navmap_merge(views)

8
electrum/plugins/jade/qt.py

@ -12,7 +12,7 @@ from electrum.logging import Logger
from electrum.plugins.hw_wallet.qt import QtHandlerBase, QtPluginBase from electrum.plugins.hw_wallet.qt import QtHandlerBase, QtPluginBase
from electrum.plugins.hw_wallet import plugin from electrum.plugins.hw_wallet import plugin
from electrum.gui.qt.util import WWLabel from electrum.gui.qt.util import WWLabel
from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock
from electrum.gui.qt.wizard.wizard import WizardComponent from electrum.gui.qt.wizard.wizard import WizardComponent
from .jade import JadePlugin from .jade import JadePlugin
@ -50,6 +50,7 @@ class Plugin(JadePlugin, QtPluginBase):
'jade_start': { 'gui': WCScriptAndDerivation }, 'jade_start': { 'gui': WCScriptAndDerivation },
'jade_xpub': { 'gui': WCJadeXPub }, 'jade_xpub': { 'gui': WCJadeXPub },
'jade_not_initialized': {'gui': WCJadeNope}, 'jade_not_initialized': {'gui': WCJadeNope},
'jade_unlock': {'gui': WCHWUnlock}
} }
wizard.navmap_merge(views) wizard.navmap_merge(views)
@ -64,7 +65,9 @@ class Jade_Handler(QtHandlerBase):
super(Jade_Handler, self).__init__(win, 'Jade') super(Jade_Handler, self).__init__(win, 'Jade')
class WCJadeXPub(WizardComponent, Logger): # TODO: almost verbatim copy of trezor WCTrezorXPub, generalize! # TODO: almost verbatim copy of trezor WCTrezorXPub, generalize!
# problem: client.get_xpub is not uniform
class WCJadeXPub(WizardComponent, Logger):
def __init__(self, parent, wizard): def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('Hardware wallet information')) WizardComponent.__init__(self, parent, wizard, title=_('Hardware wallet information'))
Logger.__init__(self) Logger.__init__(self)
@ -139,3 +142,4 @@ class WCJadeNope(WizardComponent):
def __init__(self, parent, wizard): def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('Jade not initialized')) WizardComponent.__init__(self, parent, wizard, title=_('Jade not initialized'))
self.layout().addWidget(WWLabel(_('This Jade is not initialized. Cannot continue'))) self.layout().addWidget(WWLabel(_('This Jade is not initialized. Cannot continue')))

3
electrum/plugins/trezor/qt.py

@ -17,7 +17,7 @@ from electrum.plugins.hw_wallet.plugin import only_hook_if_libraries_available
from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton, from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,
OkButton, CloseButton, PasswordLineEdit, getOpenFileName, ChoiceWidget) OkButton, CloseButton, PasswordLineEdit, getOpenFileName, ChoiceWidget)
from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock
from electrum.gui.qt.wizard.wizard import WizardComponent from electrum.gui.qt.wizard.wizard import WizardComponent
from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings, from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings,
@ -478,6 +478,7 @@ class Plugin(TrezorPlugin, QtPlugin):
'trezor_not_initialized': { 'gui': WCTrezorInitMethod }, 'trezor_not_initialized': { 'gui': WCTrezorInitMethod },
'trezor_choose_new_recover': { 'gui': WCTrezorInitParams }, 'trezor_choose_new_recover': { 'gui': WCTrezorInitParams },
'trezor_do_init': { 'gui': WCTrezorInit }, 'trezor_do_init': { 'gui': WCTrezorInit },
'trezor_unlock': {'gui': WCHWUnlock},
} }
wizard.navmap_merge(views) wizard.navmap_merge(views)

8
electrum/plugins/trezor/trezor.py

@ -529,8 +529,11 @@ class TrezorPlugin(HW_PluginBase):
# new wizard # new wizard
def wizard_entry_for_device(self, device_info: 'DeviceInfo') -> str: def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str:
if new_wallet: # new wallet
return 'trezor_not_initialized' if not device_info.initialized else 'trezor_start' return 'trezor_not_initialized' if not device_info.initialized else 'trezor_start'
else: # unlock existing wallet
return 'trezor_unlock'
# insert trezor pages in new wallet wizard # insert trezor pages in new wallet wizard
def extend_wizard(self, wizard: 'NewWalletWizard'): def extend_wizard(self, wizard: 'NewWalletWizard'):
@ -552,5 +555,8 @@ class TrezorPlugin(HW_PluginBase):
'trezor_do_init': { 'trezor_do_init': {
'next': 'trezor_start', 'next': 'trezor_start',
}, },
'trezor_unlock': {
'last': True
},
} }
wizard.navmap_merge(views) wizard.navmap_merge(views)

7
electrum/wizard.py

@ -174,6 +174,7 @@ class AbstractWizard:
def sanitize_stack_item(self, _stack_item) -> dict: def sanitize_stack_item(self, _stack_item) -> dict:
sensitive_keys = ['seed', 'seed_extra_words', 'master_key', 'private_key_list', 'password'] sensitive_keys = ['seed', 'seed_extra_words', 'master_key', 'private_key_list', 'password']
def sanitize(_dict): def sanitize(_dict):
result = {} result = {}
for item in _dict: for item in _dict:
@ -322,11 +323,11 @@ class NewWalletWizard(AbstractWizard):
def wallet_password_view(self, wizard_data: dict) -> str: def wallet_password_view(self, wizard_data: dict) -> str:
return 'wallet_password_hardware' if self.is_hardware(wizard_data) else 'wallet_password' return 'wallet_password_hardware' if self.is_hardware(wizard_data) else 'wallet_password'
def on_hardware_device(self, wizard_data: dict) -> str: def on_hardware_device(self, wizard_data: dict, new_wallet=True) -> str:
_type, _info = wizard_data['hardware_device'] _type, _info = wizard_data['hardware_device']
run_hook('init_wallet_wizard', self) run_hook('init_wallet_wizard', self) # TODO: currently only used for hww, hook name might be confusing
plugin = self.plugins.get_plugin(_type) plugin = self.plugins.get_plugin(_type)
return plugin.wizard_entry_for_device(_info) return plugin.wizard_entry_for_device(_info, new_wallet=new_wallet)
def on_have_or_confirm_seed(self, wizard_data: dict) -> str: def on_have_or_confirm_seed(self, wizard_data: dict) -> str:
if self.needs_derivation_path(wizard_data): if self.needs_derivation_path(wizard_data):

Loading…
Cancel
Save