diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index cd2118f24..ce34e6a0e 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -30,7 +30,9 @@ import traceback import threading 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: import PyQt5 @@ -401,6 +403,48 @@ class ElectrumGui(BaseElectrumGui, Logger): return window 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) try: path, storage = wizard.select_storage(path, self.daemon.get_wallet) @@ -441,9 +485,8 @@ class ElectrumGui(BaseElectrumGui, Logger): if self.daemon.network: # first-start network-setup if not self.config.cv.NETWORK_AUTO_CONNECT.is_set(): - wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) - wizard.init_network(self.daemon.network) - wizard.terminate() + dialog = QEServerConnectWizard(self.config, self.app, self.plugins, self.daemon) + dialog.exec() # start network self.daemon.start_network() diff --git a/electrum/gui/qt/wizard/server_connect.py b/electrum/gui/qt/wizard/server_connect.py index 79d47cedb..01d943f58 100644 --- a/electrum/gui/qt/wizard/server_connect.py +++ b/electrum/gui/qt/wizard/server_connect.py @@ -1,10 +1,10 @@ from typing import TYPE_CHECKING from electrum.i18n import _ -from .wizard import QEAbstractWizard, WizardComponent from electrum.wizard import ServerConnectWizard from electrum.gui.qt.network_dialog import ProxyWidget, ServerWidget from electrum.gui.qt.util import ChoiceWidget +from .wizard import QEAbstractWizard, WizardComponent if TYPE_CHECKING: from electrum.simple_config import SimpleConfig @@ -21,7 +21,7 @@ class QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard): self.setWindowTitle(_('Network and server configuration')) - # attach view names + # attach gui classes self.navmap_merge({ 'autoconnect': { 'gui': WCAutoConnect }, 'proxy_ask': { 'gui': WCProxyAsk }, diff --git a/electrum/gui/qt/wizard/wallet.py b/electrum/gui/qt/wizard/wallet.py index cbf4eb1c9..a09134532 100644 --- a/electrum/gui/qt/wizard/wallet.py +++ b/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'] }, 'hw_unlock': { - 'last': True, - 'gui': WCChooseHWDevice + # 'last': True, + 'gui': WCChooseHWDevice, + 'next': lambda d: self.on_hardware_device(d, new_wallet=False) } }) @@ -156,17 +157,11 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard): 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 - self._password = data['password'] - self.path = path - - return True - except Exception as e: - self._logger.error(f"createStorage errored: {e!r}") - return False + # minimally populate self after create + self._password = data['password'] + self.path = path class WCWalletName(WizardComponent): @@ -262,10 +257,10 @@ class WCWalletName(WizardComponent): + _("Press 'Next' to choose device to decrypt.") self.wallet_needs_hw_unlock = True else: - msg = _("Press 'Next' to open this wallet.") + msg = _("Press 'Finish' to open this wallet.") else: 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: msg = _('Cannot read file') msg_label.setText(msg) @@ -294,7 +289,7 @@ class WCWalletName(WizardComponent): self.wizard_data['wallet_name'] = self.name_e.text() self.wizard_data['wallet_exists'] = self.wallet_exists 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 @@ -1135,3 +1130,53 @@ class WCWalletPasswordHardware(WizardComponent): # client.handler = self.plugin.create_handler(self.wizard) 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 diff --git a/electrum/gui/qt/wizard/wizard.py b/electrum/gui/qt/wizard/wizard.py index bafb9e690..7a5bbf703 100644 --- a/electrum/gui/qt/wizard/wizard.py +++ b/electrum/gui/qt/wizard/wizard.py @@ -13,15 +13,12 @@ from electrum.gui.qt.util import Buttons, icon_path, MessageBoxMixin if TYPE_CHECKING: from electrum.simple_config import SimpleConfig - from electrum.plugin import Plugins - from electrum.daemon import Daemon from electrum.gui.qt import QElectrumApplication class QEAbstractWizard(QDialog, MessageBoxMixin): _logger = get_logger(__name__) - # def __init__(self, config: 'SimpleConfig', app: QApplication, plugins: 'Plugins', *, gui_object: 'ElectrumGui'): def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication'): QDialog.__init__(self, None) self.app = app @@ -51,6 +48,7 @@ class QEAbstractWizard(QDialog, MessageBoxMixin): please_wait_layout.addWidget(self.please_wait_l) please_wait_layout.addStretch(1) self.please_wait = QWidget() + self.please_wait.setVisible(False) self.please_wait.setLayout(please_wait_layout) error_layout = QVBoxLayout() @@ -63,6 +61,7 @@ class QEAbstractWizard(QDialog, MessageBoxMixin): error_layout.addWidget(self.error_msg) error_layout.addStretch(1) self.error = QWidget() + self.error.setVisible(False) self.error.setLayout(error_layout) outer_vbox = QVBoxLayout(self) diff --git a/electrum/plugins/jade/jade.py b/electrum/plugins/jade/jade.py index 0aee7a0bd..14d4ae9fc 100644 --- a/electrum/plugins/jade/jade.py +++ b/electrum/plugins/jade/jade.py @@ -482,8 +482,11 @@ class JadePlugin(HW_PluginBase): # new wizard - def wizard_entry_for_device(self, device_info: 'DeviceInfo') -> str: - return 'jade_start' if device_info.initialized else 'jade_not_initialized' + 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' + else: + return 'jade_unlock' # insert trezor pages in new wallet wizard def extend_wizard(self, wizard: 'NewWalletWizard'): @@ -496,6 +499,9 @@ class JadePlugin(HW_PluginBase): 'accept': wizard.maybe_master_pubkey, '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) diff --git a/electrum/plugins/jade/qt.py b/electrum/plugins/jade/qt.py index 5c8557a87..7eb2da6e8 100644 --- a/electrum/plugins/jade/qt.py +++ b/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 import plugin 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 .jade import JadePlugin @@ -50,6 +50,7 @@ class Plugin(JadePlugin, QtPluginBase): 'jade_start': { 'gui': WCScriptAndDerivation }, 'jade_xpub': { 'gui': WCJadeXPub }, 'jade_not_initialized': {'gui': WCJadeNope}, + 'jade_unlock': {'gui': WCHWUnlock} } wizard.navmap_merge(views) @@ -64,7 +65,9 @@ class Jade_Handler(QtHandlerBase): 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): WizardComponent.__init__(self, parent, wizard, title=_('Hardware wallet information')) Logger.__init__(self) @@ -139,3 +142,4 @@ class WCJadeNope(WizardComponent): def __init__(self, parent, wizard): WizardComponent.__init__(self, parent, wizard, title=_('Jade not initialized')) self.layout().addWidget(WWLabel(_('This Jade is not initialized. Cannot continue'))) + diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py index ee0b27371..5fea71493 100644 --- a/electrum/plugins/trezor/qt.py +++ b/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, 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 .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings, @@ -478,6 +478,7 @@ class Plugin(TrezorPlugin, QtPlugin): 'trezor_not_initialized': { 'gui': WCTrezorInitMethod }, 'trezor_choose_new_recover': { 'gui': WCTrezorInitParams }, 'trezor_do_init': { 'gui': WCTrezorInit }, + 'trezor_unlock': {'gui': WCHWUnlock}, } wizard.navmap_merge(views) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 0e9fb6ccf..fd86ea4b0 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -529,8 +529,11 @@ class TrezorPlugin(HW_PluginBase): # new wizard - def wizard_entry_for_device(self, device_info: 'DeviceInfo') -> str: - return 'trezor_not_initialized' if not device_info.initialized else 'trezor_start' + 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' + else: # unlock existing wallet + return 'trezor_unlock' # insert trezor pages in new wallet wizard def extend_wizard(self, wizard: 'NewWalletWizard'): @@ -552,5 +555,8 @@ class TrezorPlugin(HW_PluginBase): 'trezor_do_init': { 'next': 'trezor_start', }, + 'trezor_unlock': { + 'last': True + }, } wizard.navmap_merge(views) diff --git a/electrum/wizard.py b/electrum/wizard.py index 8491f5e90..75ca06fb8 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -174,6 +174,7 @@ class AbstractWizard: def sanitize_stack_item(self, _stack_item) -> dict: sensitive_keys = ['seed', 'seed_extra_words', 'master_key', 'private_key_list', 'password'] + def sanitize(_dict): result = {} for item in _dict: @@ -322,11 +323,11 @@ class NewWalletWizard(AbstractWizard): def wallet_password_view(self, wizard_data: dict) -> str: 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'] - 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) - 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: if self.needs_derivation_path(wizard_data):