Qt: let user enable/disable keystores with seed or hw wallet
This commit is contained in:
@@ -495,12 +495,18 @@ class ElectrumGui(BaseElectrumGui, Logger):
|
||||
if window.should_stop_wallet_on_close:
|
||||
self.daemon.stop_wallet(window.wallet.storage.path)
|
||||
|
||||
def reload_window(self, window):
|
||||
# bump counter so that we do not close the app
|
||||
self._num_wizards_in_progress += 1
|
||||
wallet = window.wallet
|
||||
window.should_stop_wallet_on_close = False
|
||||
window.close()
|
||||
self._create_window_for_wallet(wallet)
|
||||
self._num_wizards_in_progress -= 1
|
||||
|
||||
def reload_windows(self):
|
||||
for window in list(self.windows):
|
||||
wallet = window.wallet
|
||||
window.should_stop_wallet_on_close = False
|
||||
window.close()
|
||||
self._create_window_for_wallet(wallet)
|
||||
self.reload_window(window)
|
||||
|
||||
def ask_terms_of_use(self):
|
||||
"""Ask the user to accept the terms of use.
|
||||
|
||||
@@ -143,7 +143,7 @@ def protected(func):
|
||||
parent = self.top_level_window()
|
||||
password = None
|
||||
msg = kwargs.get('message')
|
||||
while self.wallet.has_keystore_encryption():
|
||||
while self._protected_requires_password():
|
||||
password = self.wallet.get_unlocked_password() or self.password_dialog(parent=parent, msg=msg)
|
||||
if password is None:
|
||||
# User cancelled password input
|
||||
@@ -176,6 +176,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
self.gui_thread = gui_object.gui_thread
|
||||
assert wallet, "no wallet"
|
||||
self.wallet = wallet
|
||||
self._protected_requires_password = self.wallet.has_keystore_encryption
|
||||
if wallet.has_lightning() and not self.config.cv.GUI_QT_SHOW_TAB_CHANNELS.is_set():
|
||||
self.config.GUI_QT_SHOW_TAB_CHANNELS = True # override default, but still allow disabling tab manually
|
||||
|
||||
|
||||
@@ -4,20 +4,23 @@
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
from functools import partial
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import (QLabel, QVBoxLayout, QGridLayout,
|
||||
QHBoxLayout, QPushButton, QWidget, QStackedWidget)
|
||||
from PyQt6.QtWidgets import (
|
||||
QLabel, QVBoxLayout, QGridLayout,
|
||||
QHBoxLayout, QPushButton, QWidget, QTabWidget)
|
||||
|
||||
from electrum.plugin import run_hook
|
||||
from electrum.i18n import _
|
||||
from electrum.wallet import Multisig_Wallet
|
||||
from electrum.util import ChoiceItem
|
||||
|
||||
from .main_window import protected
|
||||
from electrum.gui.qt.wizard.wallet import QEKeystoreWizard
|
||||
from .qrtextedit import ShowQRTextEdit
|
||||
from .util import (
|
||||
read_QIcon, WindowModalDialog, Buttons,
|
||||
WWLabel, CloseButton, HelpButton, font_height, ShowQRLineEdit, ChoiceWidget,
|
||||
WWLabel, CloseButton, HelpButton, font_height, ShowQRLineEdit
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -29,7 +32,10 @@ class WalletInfoDialog(WindowModalDialog):
|
||||
def __init__(self, parent: QWidget, *, window: 'ElectrumWindow'):
|
||||
WindowModalDialog.__init__(self, parent, _("Wallet Information"))
|
||||
self.setMinimumSize(800, 100)
|
||||
wallet = window.wallet
|
||||
self.window = window
|
||||
self.wallet = wallet = window.wallet
|
||||
# required for @protected decorator
|
||||
self._protected_requires_password = lambda: self.wallet.has_keystore_encryption() or self.wallet.storage.is_encrypted_with_user_pw()
|
||||
config = window.config
|
||||
vbox = QVBoxLayout()
|
||||
wallet_type = wallet.db.get('wallet_type', '')
|
||||
@@ -107,32 +113,18 @@ class WalletInfoDialog(WindowModalDialog):
|
||||
if wallet.is_deterministic():
|
||||
keystores = wallet.get_keystores()
|
||||
|
||||
ks_stack = QStackedWidget()
|
||||
self.keystore_tabs = QTabWidget()
|
||||
|
||||
def select_ks(index):
|
||||
ks_stack.setCurrentIndex(index)
|
||||
|
||||
# only show the combobox in case multiple accounts are available
|
||||
if len(keystores) > 1:
|
||||
def label(idx, ks):
|
||||
if isinstance(wallet, Multisig_Wallet) and hasattr(ks, 'label'):
|
||||
return _("cosigner") + f' {idx+1}: {ks.get_type_text()} {ks.label}'
|
||||
else:
|
||||
return _("keystore") + f' {idx+1}'
|
||||
|
||||
labels = [ChoiceItem(key=idx, label=label(idx, ks))
|
||||
for idx, ks in enumerate(wallet.get_keystores())]
|
||||
|
||||
keystore_choice = ChoiceWidget(message=_("Select keystore"), choices=labels)
|
||||
keystore_choice.itemSelected.connect(lambda x: select_ks(x))
|
||||
vbox.addWidget(keystore_choice)
|
||||
|
||||
for ks in keystores:
|
||||
for idx, ks in enumerate(keystores):
|
||||
ks_w = QWidget()
|
||||
ks_vbox = QVBoxLayout()
|
||||
ks_vbox.setContentsMargins(0, 0, 0, 0)
|
||||
ks_w.setLayout(ks_vbox)
|
||||
|
||||
status_label = _('This keystore is watching-only (disabled)') if ks.is_watching_only() else _('This keystore is active (enabled)')
|
||||
ks_vbox.addWidget(QLabel(status_label))
|
||||
label = f'{ks.label}' if hasattr(ks, 'label') and ks.label else ''
|
||||
ks_vbox.addWidget(QLabel(_('Type') + ': ' + f'{ks.get_type_text()}' + ' ' + label))
|
||||
|
||||
mpk_text = ShowQRTextEdit(ks.get_master_public_key(), config=config)
|
||||
mpk_text.setMaximumHeight(max(150, 10 * font_height()))
|
||||
mpk_text.addCopyButton()
|
||||
@@ -157,18 +149,64 @@ class WalletInfoDialog(WindowModalDialog):
|
||||
bip32fp_hbox.addWidget(bip32fp_text)
|
||||
bip32fp_hbox.addStretch()
|
||||
ks_vbox.addLayout(bip32fp_hbox)
|
||||
|
||||
ks_stack.addWidget(ks_w)
|
||||
|
||||
select_ks(0)
|
||||
vbox.addWidget(ks_stack)
|
||||
ks_buttons = []
|
||||
if not ks.is_watching_only():
|
||||
rm_keystore_button = QPushButton('Disable keystore')
|
||||
rm_keystore_button.clicked.connect(partial(self.disable_keystore, ks))
|
||||
ks_buttons.insert(0, rm_keystore_button)
|
||||
else:
|
||||
add_keystore_button = QPushButton('Enable Keystore')
|
||||
add_keystore_button.clicked.connect(self.enable_keystore)
|
||||
ks_buttons.insert(0, add_keystore_button)
|
||||
ks_vbox.addLayout(Buttons(*ks_buttons))
|
||||
tab_label = _("Cosigner") + f' {idx+1}' if len(keystores) > 1 else _("Keystore")
|
||||
index = self.keystore_tabs.addTab(ks_w, tab_label)
|
||||
if not ks.is_watching_only():
|
||||
self.keystore_tabs.setTabIcon(index, read_QIcon('confirmed.svg'))
|
||||
vbox.addWidget(self.keystore_tabs)
|
||||
|
||||
vbox.addStretch(1)
|
||||
|
||||
buttons = [CloseButton(self)]
|
||||
btn_export_info = run_hook('wallet_info_buttons', window, self)
|
||||
if btn_export_info is None:
|
||||
btn_export_info = []
|
||||
buttons = btn_export_info + buttons
|
||||
|
||||
btn_close = CloseButton(self)
|
||||
btns = Buttons(*btn_export_info, btn_close)
|
||||
btns = Buttons(*buttons)
|
||||
vbox.addLayout(btns)
|
||||
self.setLayout(vbox)
|
||||
|
||||
def disable_keystore(self, keystore):
|
||||
if self.wallet.has_channels():
|
||||
self.window.show_message(_('Cannot disable keystore: You have active lightning channels'))
|
||||
return
|
||||
|
||||
msg = _('Disable keystore? This will make the keytore watching-only.')
|
||||
if self.wallet.storage.is_encrypted_with_hw_device():
|
||||
msg += '\n\n' + _('Note that this will disable wallet file encryption, because it uses your hardware wallet device.')
|
||||
if not self.window.question(msg):
|
||||
return
|
||||
self.accept()
|
||||
self.wallet.disable_keystore(keystore)
|
||||
self.window.gui_object.reload_windows()
|
||||
|
||||
def enable_keystore(self, b: bool):
|
||||
dialog = QEKeystoreWizard(self.window.config, self.window.wallet.wallet_type, self.window.gui_object.app, self.window.gui_object.plugins)
|
||||
result = dialog.run()
|
||||
if not result:
|
||||
return
|
||||
keystore, is_hardware = result
|
||||
for k in self.wallet.get_keystores():
|
||||
if k.xpub == keystore.xpub:
|
||||
break
|
||||
else:
|
||||
self.window.show_error(_('Keystore not found in this wallet'))
|
||||
return
|
||||
self._enable_keystore(keystore, is_hardware)
|
||||
|
||||
@protected
|
||||
def _enable_keystore(self, keystore, is_hardware, password):
|
||||
self.accept()
|
||||
self.wallet.enable_keystore(keystore, is_hardware, password)
|
||||
self.window.gui_object.reload_windows()
|
||||
|
||||
@@ -17,13 +17,13 @@ from electrum.keystore import bip44_derivation, bip39_to_seed, purpose48_derivat
|
||||
from electrum.plugin import run_hook, HardwarePluginLibraryUnavailable
|
||||
from electrum.storage import StorageReadWriteError
|
||||
from electrum.util import WalletFileException, get_new_wallet_name, UserFacingException, InvalidPassword
|
||||
from electrum.util import is_subpath, ChoiceItem
|
||||
from electrum.util import is_subpath, ChoiceItem, multisig_type
|
||||
from electrum.wallet import wallet_types
|
||||
from .wizard import QEAbstractWizard, WizardComponent
|
||||
from electrum.logging import get_logger, Logger
|
||||
from electrum import WalletStorage, mnemonic, keystore
|
||||
from electrum.wallet_db import WalletDB
|
||||
from electrum.wizard import NewWalletWizard
|
||||
from electrum.wizard import NewWalletWizard, KeystoreWizard
|
||||
|
||||
from electrum.gui.qt.bip39_recovery_dialog import Bip39RecoveryDialog
|
||||
from electrum.gui.qt.password_dialog import PasswordLayout, PW_NEW, MSG_ENTER_PASSWORD, PasswordLayoutForHW
|
||||
@@ -49,6 +49,33 @@ MSG_HW_STORAGE_ENCRYPTION = _("Set wallet file encryption.") + '\n'\
|
||||
|
||||
|
||||
|
||||
class QEKeystoreWizard(KeystoreWizard, QEAbstractWizard, MessageBoxMixin):
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
def __init__(self, config: 'SimpleConfig', wallet_type: str, app: 'QElectrumApplication', plugins: 'Plugins', *, start_viewstate=None):
|
||||
KeystoreWizard.__init__(self, plugins)
|
||||
QEAbstractWizard.__init__(self, config, app, start_viewstate=start_viewstate)
|
||||
self._wallet_type = wallet_type
|
||||
self.window_title = _('Extend wallet keystore')
|
||||
# attach gui classes to views
|
||||
self.navmap_merge({
|
||||
'keystore_type': {'gui': WCExtendKeystore},
|
||||
'enterseed': {'gui': WCHaveSeed},
|
||||
'choose_hardware_device': {'gui': WCChooseHWDevice},
|
||||
'script_and_derivation': {'gui': WCScriptAndDerivation},
|
||||
'wallet_password': {'gui': WCWalletPassword},
|
||||
'wallet_password_hardware': {'gui': WCWalletPasswordHardware},
|
||||
})
|
||||
|
||||
def is_single_password(self):
|
||||
return True
|
||||
|
||||
def run(self):
|
||||
if self.exec() == QDialog.DialogCode.Rejected:
|
||||
return
|
||||
return self._result
|
||||
|
||||
|
||||
class QENewWalletWizard(NewWalletWizard, QEAbstractWizard, MessageBoxMixin):
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
@@ -411,6 +438,7 @@ class WCWalletType(WalletWizardComponent):
|
||||
|
||||
|
||||
class WCKeystoreType(WalletWizardComponent):
|
||||
|
||||
def __init__(self, parent, wizard):
|
||||
WalletWizardComponent.__init__(self, parent, wizard, title=_('Keystore'))
|
||||
message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
|
||||
@@ -420,7 +448,6 @@ class WCKeystoreType(WalletWizardComponent):
|
||||
ChoiceItem(key='masterkey', label=_('Use a master key')),
|
||||
ChoiceItem(key='hardware', label=_('Use a hardware device')),
|
||||
]
|
||||
|
||||
self.choice_w = ChoiceWidget(message=message, choices=choices)
|
||||
self.layout().addWidget(self.choice_w)
|
||||
self.layout().addStretch(1)
|
||||
@@ -430,6 +457,32 @@ class WCKeystoreType(WalletWizardComponent):
|
||||
self.wizard_data['keystore_type'] = self.choice_w.selected_key
|
||||
|
||||
|
||||
|
||||
class WCExtendKeystore(WalletWizardComponent):
|
||||
|
||||
def __init__(self, parent, wizard):
|
||||
WalletWizardComponent.__init__(self, parent, wizard, title=_('Keystore'))
|
||||
message = _('What type of signing method do you want to add?')
|
||||
choices = [
|
||||
ChoiceItem(key='haveseed', label=_('Enter seed')),
|
||||
ChoiceItem(key='hardware', label=_('Use a hardware device')),
|
||||
]
|
||||
self.choice_w = ChoiceWidget(message=message, choices=choices)
|
||||
self.layout().addWidget(self.choice_w)
|
||||
self.layout().addStretch(1)
|
||||
self._valid = True
|
||||
self.wizard_data['wallet_type'] = self._wallet_type = wizard._wallet_type
|
||||
|
||||
def apply(self):
|
||||
self.wizard_data['wallet_type'] = self._wallet_type
|
||||
self.wizard_data['keystore_type'] = self.choice_w.selected_key
|
||||
if multisig_type(self._wallet_type):
|
||||
self.wizard_data['wallet_type'] = self._wallet_type = 'multisig'
|
||||
self.wizard_data['multisig_participants'] = 2
|
||||
self.wizard_data['multisig_signatures'] = 2
|
||||
self.wizard_data['multisig_cosigner_data'] = {}
|
||||
|
||||
|
||||
class WCCreateSeed(WalletWizardComponent):
|
||||
def __init__(self, parent, wizard):
|
||||
WalletWizardComponent.__init__(self, parent, wizard, title=_('Wallet Seed'))
|
||||
|
||||
@@ -3035,7 +3035,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
||||
return False
|
||||
|
||||
def has_password(self) -> bool:
|
||||
return self.has_keystore_encryption() or self.has_storage_encryption()
|
||||
return self.has_keystore_encryption() or self.has_storage_encryption() #and self.storage.is_encrypted_with_user_pw())
|
||||
|
||||
def can_have_keystore_encryption(self):
|
||||
return self.keystore and self.keystore.may_have_password()
|
||||
@@ -3951,6 +3951,22 @@ class Deterministic_Wallet(Abstract_Wallet):
|
||||
def get_txin_type(self, address=None):
|
||||
return self.txin_type
|
||||
|
||||
def enable_keystore(self, keystore, is_hardware_keystore: bool, password):
|
||||
if not is_hardware_keystore and self.storage.is_encrypted_with_user_pw():
|
||||
keystore.update_password(None, password)
|
||||
self._update_keystore(keystore)
|
||||
|
||||
def disable_keystore(self, keystore):
|
||||
from .keystore import BIP32_KeyStore
|
||||
assert not self.has_channels()
|
||||
if keystore.thread:
|
||||
keystore.thread.stop()
|
||||
if self.storage.is_encrypted_with_hw_device():
|
||||
password = keystore.get_password_for_storage_encryption()
|
||||
self.update_password(password, None, encrypt_storage=False)
|
||||
new = BIP32_KeyStore({'xpub':keystore.xpub})
|
||||
self._update_keystore(new)
|
||||
|
||||
|
||||
class Standard_Wallet(Simple_Wallet, Deterministic_Wallet):
|
||||
""" Deterministic Wallet with a single pubkey per address """
|
||||
@@ -3989,6 +4005,12 @@ class Standard_Wallet(Simple_Wallet, Deterministic_Wallet):
|
||||
tx.add_info_from_wallet(self)
|
||||
self.keystore.add_slip_19_ownership_proofs_to_tx(tx=tx, password=None)
|
||||
|
||||
def _update_keystore(self, keystore):
|
||||
assert self.keystore.xpub == keystore.xpub
|
||||
self.keystore = keystore
|
||||
self.save_keystore()
|
||||
|
||||
|
||||
|
||||
class Multisig_Wallet(Deterministic_Wallet):
|
||||
# generic m of n
|
||||
@@ -4039,6 +4061,16 @@ class Multisig_Wallet(Deterministic_Wallet):
|
||||
def get_keystores(self):
|
||||
return [self.keystores[i] for i in sorted(self.keystores.keys())]
|
||||
|
||||
def _update_keystore(self, keystore):
|
||||
for name, k in self.keystores.items():
|
||||
if k.xpub == keystore.xpub:
|
||||
break
|
||||
else:
|
||||
raise Exception('keystore not found')
|
||||
self.keystores[name] = keystore
|
||||
self.keystore = keystore
|
||||
self.save_keystore()
|
||||
|
||||
def can_have_keystore_encryption(self):
|
||||
return any([k.may_have_password() for k in self.get_keystores()])
|
||||
|
||||
|
||||
@@ -422,7 +422,8 @@ class NewWalletWizard(AbstractWizard):
|
||||
if data['keystore_type'] in ['createseed', 'haveseed'] and 'seed' in data:
|
||||
seed_extension = data['seed_extra_words'] if data['seed_extend'] else ''
|
||||
if data['seed_variant'] == 'electrum':
|
||||
return keystore.from_seed(data['seed'], passphrase=seed_extension, for_multisig=True)
|
||||
for_multisig = wallet_type in ['multisig']
|
||||
return keystore.from_seed(data['seed'], passphrase=seed_extension, for_multisig=for_multisig)
|
||||
elif data['seed_variant'] == 'bip39':
|
||||
root_seed = keystore.bip39_to_seed(data['seed'], passphrase=seed_extension)
|
||||
derivation = normalize_bip32_derivation(data['derivation_path'])
|
||||
@@ -800,3 +801,55 @@ class TermsOfUseWizard(AbstractWizard):
|
||||
self._current = WizardViewState(start_view, initial_data, params)
|
||||
return self._current
|
||||
|
||||
|
||||
class KeystoreWizard(NewWalletWizard):
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
def __init__(self, plugins):
|
||||
AbstractWizard.__init__(self)
|
||||
self.plugins = plugins
|
||||
self.navmap = {
|
||||
'keystore_type': {
|
||||
'next': self.on_keystore_type
|
||||
},
|
||||
'enterseed': {
|
||||
'accept': self.update_keystore,
|
||||
'last': True
|
||||
},
|
||||
'choose_hardware_device': {
|
||||
'next': self.on_hardware_device,
|
||||
},
|
||||
}
|
||||
|
||||
def maybe_master_pubkey(self, wizard_data):
|
||||
self.update_keystore(wizard_data)
|
||||
|
||||
def update_keystore(self, wizard_data):
|
||||
wallet_type = wizard_data['wallet_type']
|
||||
keystore = self.keystore_from_data(wallet_type, wizard_data)
|
||||
self._result = keystore, (wizard_data['keystore_type'] == 'hardware')
|
||||
|
||||
def on_keystore_type(self, wizard_data: dict) -> str:
|
||||
t = wizard_data['keystore_type']
|
||||
return {
|
||||
'haveseed': 'enterseed',
|
||||
'hardware': 'choose_hardware_device'
|
||||
}.get(t)
|
||||
|
||||
def is_multisig(self, wizard_data: dict) -> bool:
|
||||
return wizard_data['wallet_type'] == 'multisig'
|
||||
|
||||
def last_cosigner(self, wizard_data: dict) -> bool:
|
||||
# one at a time
|
||||
return True
|
||||
|
||||
def start(self, initial_data: dict = None) -> WizardViewState:
|
||||
if initial_data is None:
|
||||
initial_data = {}
|
||||
self.reset()
|
||||
start_view = 'keystore_type'
|
||||
params = self.navmap[start_view].get('params', {})
|
||||
self._current = WizardViewState(start_view, initial_data, params)
|
||||
return self._current
|
||||
|
||||
|
||||
Reference in New Issue
Block a user