Browse Source

qt: refactor SeedLayout/KeysLayout to SeedWidget/KeysWidget, remove the hacks left over from old to new wizard and

update validation in all cases (e.g. validate electrum seed when switching from bip39 to electrum in options dialog)
master
Sander van Grieken 1 year ago
parent
commit
97a7136b5f
No known key found for this signature in database
GPG Key ID: 9BCF8209EA402EBA
  1. 214
      electrum/gui/qt/seed_dialog.py
  2. 144
      electrum/gui/qt/wizard/wallet.py

214
electrum/gui/qt/seed_dialog.py

@ -69,66 +69,10 @@ def seed_warning_msg(seed):
]).format(len(seed.split()))
class SeedLayout(QVBoxLayout):
class SeedWidget(QWidget):
updated = pyqtSignal()
def seed_options(self):
dialog = QDialog()
dialog.setWindowTitle(_("Seed Options"))
vbox = QVBoxLayout(dialog)
seed_types = [
(value, title) for value, title in (
('electrum', _('Electrum')),
('bip39', _('BIP39 seed')),
('slip39', _('SLIP39 seed')),
)
if value in self.options or value == 'electrum'
]
if 'ext' in self.options:
cb_ext = QCheckBox(_('Extend this seed with custom words'))
cb_ext.setChecked(self.is_ext)
vbox.addWidget(cb_ext)
if len(seed_types) >= 2:
def on_selected(idx):
self.seed_type = seed_type_choice.selected_key
self.is_seed = (lambda x: bool(x)) if self.seed_type != 'electrum' else self.saved_is_seed
self.slip39_current_mnemonic_invalid = None
self.seed_status.setText('')
self.on_edit()
if self.seed_type == 'bip39':
msg = ' '.join([
'<b>' + _('Warning') + ':</b> ',
_('BIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),
_('However, we do not generate BIP39 seeds, because they do not meet our safety standard.'),
_('BIP39 seeds do not include a version number, which compromises compatibility with future software.'),
_('We do not guarantee that BIP39 imports will always be supported in Electrum.'),
])
elif self.seed_type == 'slip39':
msg = ' '.join([
'<b>' + _('Warning') + ':</b> ',
_('SLIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),
_('However, we do not generate SLIP39 seeds.'),
])
else:
msg = ''
self.update_share_buttons()
self.initialize_completer()
self.seed_warning.setText(msg)
seed_type_choice = ChoiceWidget(message=_('Seed type'), choices=seed_types, selected=self.seed_type)
seed_type_choice.itemSelected.connect(on_selected)
vbox.addWidget(seed_type_choice)
vbox.addLayout(Buttons(OkButton(dialog)))
if not dialog.exec():
return None
self.is_ext = cb_ext.isChecked() if 'ext' in self.options else False
self.seed_type = seed_type_choice.selected_key if len(seed_types) >= 2 else 'electrum'
self.updated.emit()
validChanged = pyqtSignal([bool], arguments=['valid'])
def __init__(
self,
@ -137,20 +81,38 @@ class SeedLayout(QVBoxLayout):
icon=True,
msg=None,
options=None,
is_seed=None,
is_seed=None, # only used for electrum seeds
passphrase=None,
parent=None,
for_seed_words=True,
*,
config: 'SimpleConfig',
):
QVBoxLayout.__init__(self)
self.parent = parent
QWidget.__init__(self, parent)
vbox = QVBoxLayout()
self.setLayout(vbox)
self.options = options
self.config = config
self.seed_type = 'electrum'
if options:
self.seed_types = [
(value, title) for value, title in (
('electrum', _('Electrum')),
('bip39', _('BIP39 seed')),
('slip39', _('SLIP39 seed')),
)
if value in self.options
]
assert len(self.seed_types)
self.seed_type = self.seed_types[0][0]
else:
self.seed_type = 'electrum'
self.is_seed = is_seed
if title:
self.addWidget(WWLabel(title))
vbox.addWidget(WWLabel(title))
if seed: # "read only", we already have the text
if for_seed_words:
self.seed_e = ButtonsTextEdit()
@ -162,8 +124,6 @@ class SeedLayout(QVBoxLayout):
assert for_seed_words
self.seed_e = CompletionTextEdit()
self.seed_e.setTabChangesFocus(False) # so that tab auto-completes
self.is_seed = is_seed
self.saved_is_seed = self.is_seed
self.seed_e.textChanged.connect(self.on_edit)
self.initialize_completer()
@ -176,7 +136,7 @@ class SeedLayout(QVBoxLayout):
logo.setMaximumWidth(60)
hbox.addWidget(logo)
hbox.addWidget(self.seed_e)
self.addLayout(hbox)
vbox.addLayout(hbox)
hbox = QHBoxLayout()
hbox.addStretch(1)
self.seed_type_label = QLabel('')
@ -187,7 +147,7 @@ class SeedLayout(QVBoxLayout):
if options:
opt_button = EnterButton(_('Options'), self.seed_options)
hbox.addWidget(opt_button)
self.addLayout(hbox)
vbox.addLayout(hbox)
if passphrase:
hbox = QHBoxLayout()
passphrase_e = QLineEdit()
@ -195,7 +155,7 @@ class SeedLayout(QVBoxLayout):
passphrase_e.setReadOnly(True)
hbox.addWidget(QLabel(_("Your seed extension is") + ':'))
hbox.addWidget(passphrase_e)
self.addLayout(hbox)
vbox.addLayout(hbox)
# slip39 shares
self.slip39_mnemonic_index = 0
@ -211,15 +171,75 @@ class SeedLayout(QVBoxLayout):
self.next_share_btn.clicked.connect(self.on_next_share)
hbox.addWidget(self.next_share_btn)
self.update_share_buttons()
self.addLayout(hbox)
vbox.addLayout(hbox)
self.addStretch(1)
vbox.addStretch(1)
self.seed_status = WWLabel('')
self.addWidget(self.seed_status)
vbox.addWidget(self.seed_status)
self.seed_warning = WWLabel('')
if msg:
self.seed_warning.setText(seed_warning_msg(seed))
self.addWidget(self.seed_warning)
else:
self.update_seed_warning()
vbox.addWidget(self.seed_warning)
def seed_options(self):
dialog = QDialog()
dialog.setWindowTitle(_("Seed Options"))
vbox = QVBoxLayout(dialog)
if 'ext' in self.options:
cb_ext = QCheckBox(_('Extend this seed with custom words'))
cb_ext.setChecked(self.is_ext)
vbox.addWidget(cb_ext)
def on_selected(idx):
self.seed_type = seed_type_choice.selected_key
self.slip39_current_mnemonic_invalid = None
self.seed_status.setText('')
self.update_seed_warning()
self.on_edit()
self.update_share_buttons()
self.initialize_completer()
if len(self.seed_types) > 1:
seed_type_choice = ChoiceWidget(message=_('Seed type'), choices=self.seed_types, selected=self.seed_type)
seed_type_choice.itemSelected.connect(on_selected)
vbox.addWidget(seed_type_choice)
vbox.addLayout(Buttons(OkButton(dialog)))
if not dialog.exec():
return None
if 'ext' in self.options:
self.is_ext = cb_ext.isChecked()
if len(self.seed_types) > 1:
self.seed_type = seed_type_choice.selected_key
self.update_seed_warning()
self.updated.emit()
def update_seed_warning(self):
if self.seed_type == 'bip39':
msg = ' '.join([
'<b>' + _('Warning') + ':</b> ',
_('BIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),
_('However, we do not generate BIP39 seeds, because they do not meet our safety standard.'),
_('BIP39 seeds do not include a version number, which compromises compatibility with future software.'),
_('We do not guarantee that BIP39 imports will always be supported in Electrum.'),
])
elif self.seed_type == 'slip39':
msg = ' '.join([
'<b>' + _('Warning') + ':</b> ',
_('SLIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),
_('However, we do not generate SLIP39 seeds.'),
])
else:
msg = ''
self.seed_warning.setText(msg)
def initialize_completer(self):
if self.seed_type != 'slip39':
@ -261,12 +281,12 @@ class SeedLayout(QVBoxLayout):
def on_edit(self):
s = ' '.join(self.get_seed_words())
b = self.is_seed(s)
if self.seed_type == 'bip39':
from electrum.keystore import bip39_is_checksum_valid
is_checksum, is_wordlist = bip39_is_checksum_valid(s)
label = ''
if bool(s):
valid = bool(s)
if valid:
label = ('' if is_checksum else _('BIP39 checksum failed')) if is_wordlist else _('Unknown BIP39 wordlist')
elif self.seed_type == 'slip39':
self.slip39_mnemonics[self.slip39_mnemonic_index] = s
@ -287,15 +307,13 @@ class SeedLayout(QVBoxLayout):
self.seed_status.setText(seed_status)
self.slip39_current_mnemonic_invalid = current_mnemonic_invalid
b = self.slip39_seed is not None
valid = self.slip39_seed is not None
self.update_share_buttons()
else:
valid = self.is_seed(s)
t = calc_seed_type(s)
label = _('Seed Type') + ': ' + t if t else ''
if t and not b: # electrum seed, but does not conform to dialog rules
# FIXME we should just accept any electrum seed and "redirect" the wizard automatically.
# i.e. if user selected wallet_type=="standard" but entered a 2fa seed, accept and redirect
# if user selected wallet_type=="2fa" but entered a std electrum seed, accept and redirect
if t and not valid: # electrum seed, but does not conform to dialog rules
wiztype_fullname = _('Wallet with two-factor authentication') if is_any_2fa_seed_type(t) else _("Standard wallet")
msg = ' '.join([
'<b>' + _('Warning') + ':</b> ',
@ -307,7 +325,7 @@ class SeedLayout(QVBoxLayout):
self.seed_warning.setText("")
self.seed_type_label.setText(label)
self.parent.next_button.setEnabled(b)
self.validChanged.emit(valid)
# disable suggestions if user already typed an unknown word
for word in self.get_seed_words()[:-1]:
@ -354,7 +372,10 @@ class SeedLayout(QVBoxLayout):
self.slip39_current_mnemonic_invalid = None
class KeysLayout(QVBoxLayout):
class KeysWidget(QWidget):
validChanged = pyqtSignal([bool], arguments=['valid'])
def __init__(
self,
parent=None,
@ -364,29 +385,28 @@ class KeysLayout(QVBoxLayout):
*,
config: 'SimpleConfig',
):
QVBoxLayout.__init__(self)
self.parent = parent
QWidget.__init__(self, parent)
vbox = QVBoxLayout()
self.setLayout(vbox)
self.is_valid = is_valid
self.text_e = ScanQRTextEdit(allow_multi=allow_multi, config=config)
self.text_e.textChanged.connect(self.on_edit)
if isinstance(header_layout, str):
self.addWidget(WWLabel(header_layout))
vbox.addWidget(WWLabel(header_layout))
else:
self.addLayout(header_layout)
self.addWidget(self.text_e)
vbox.addLayout(header_layout)
vbox.addWidget(self.text_e)
def get_text(self):
return self.text_e.text()
def on_edit(self):
valid = False
try:
valid = self.is_valid(self.get_text())
except Exception as e:
self.parent.next_button.setToolTip(f'{_("Error")}: {str(e)}')
else:
self.parent.next_button.setToolTip('')
self.parent.next_button.setEnabled(valid)
valid = False
self.validChanged.emit(valid)
class SeedDialog(WindowModalDialog):
@ -395,13 +415,7 @@ class SeedDialog(WindowModalDialog):
WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed')))
self.setMinimumWidth(400)
vbox = QVBoxLayout(self)
title = _("Your wallet generation seed is:")
slayout = SeedLayout(
title=title,
seed=seed,
msg=True,
passphrase=passphrase,
config=config,
)
vbox.addLayout(slayout)
title = _("Your wallet generation seed is:")
seed_widget = SeedWidget(title=title, seed=seed, msg=True, passphrase=passphrase, config=config)
vbox.addWidget(seed_widget)
vbox.addLayout(Buttons(CloseButton(self)))

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

@ -27,10 +27,9 @@ from electrum.wizard import NewWalletWizard
from electrum.gui.qt.bip39_recovery_dialog import Bip39RecoveryDialog
from electrum.gui.qt.password_dialog import PasswordLayout, PW_NEW, MSG_ENTER_PASSWORD, PasswordLayoutForHW
from electrum.gui.qt.seed_dialog import SeedLayout, MSG_PASSPHRASE_WARN_ISSUE4566, KeysLayout
from electrum.gui.qt.seed_dialog import SeedWidget, MSG_PASSPHRASE_WARN_ISSUE4566, KeysWidget
from electrum.gui.qt.util import (PasswordLineEdit, char_width_in_lineedit, WWLabel, InfoButton, font_height,
ChoiceWidget, MessageBoxMixin, WindowModalDialog, CancelButton,
Buttons, OkButton, icon_path)
ChoiceWidget, MessageBoxMixin, icon_path)
if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig
@ -437,7 +436,7 @@ class WCCreateSeed(WalletWizardComponent):
WalletWizardComponent.__init__(self, parent, wizard, title=_('Wallet Seed'))
self._busy = True
self.seed_type = 'standard' if self.wizard.config.WIZARD_DONT_CREATE_SEGWIT else 'segwit'
self.slayout = None
self.seed_widget = None
self.seed = None
def on_ready(self):
@ -446,10 +445,10 @@ class WCCreateSeed(WalletWizardComponent):
QTimer.singleShot(1, self.create_seed)
def apply(self):
if self.slayout:
if self.seed_widget:
self.wizard_data['seed'] = self.seed
self.wizard_data['seed_type'] = self.seed_type
self.wizard_data['seed_extend'] = self.slayout.is_ext
self.wizard_data['seed_extend'] = self.seed_widget.is_ext
self.wizard_data['seed_variant'] = 'electrum'
self.wizard_data['seed_extra_words'] = '' # empty default
@ -457,15 +456,15 @@ class WCCreateSeed(WalletWizardComponent):
self.busy = True
self.seed = mnemonic.Mnemonic('en').make_seed(seed_type=self.seed_type)
self.slayout = SeedLayout(
self.seed_widget = SeedWidget(
title=_('Your wallet generation seed is:'),
seed=self.seed,
options=['ext'],
options=['ext', 'electrum'],
msg=True,
parent=self,
config=self.wizard.config,
)
self.layout().addLayout(self.slayout)
self.layout().addWidget(self.seed_widget)
self.layout().addStretch(1)
self.busy = False
self.valid = True
@ -482,19 +481,16 @@ class WCConfirmSeed(WalletWizardComponent):
self.layout().addWidget(WWLabel(message))
# TODO: SeedLayout assumes too much in parent, refactor SeedLayout
# for now, fake parent.next_button.setEnabled
class Hack:
def setEnabled(self2, b):
self.valid = b
self.next_button = Hack()
self.slayout = SeedLayout(
self.seed_widget = SeedWidget(
is_seed=lambda x: x == self.wizard_data['seed'],
parent=self,
config=self.wizard.config,
)
self.layout().addLayout(self.slayout)
def seed_valid_changed(valid):
self.valid = valid
self.seed_widget.validChanged.connect(seed_valid_changed)
self.layout().addWidget(self.seed_widget)
wizard.app.clipboard().clear()
@ -583,37 +579,39 @@ class WCHaveSeed(WalletWizardComponent, Logger):
WalletWizardComponent.__init__(self, parent, wizard, title=_('Enter Seed'))
Logger.__init__(self)
self.slayout = None
self.layout().addWidget(WWLabel(_('Please enter your seed phrase in order to restore your wallet.')))
# TODO: SeedLayout assumes too much in parent, refactor SeedLayout
# for now, fake parent.next_button.setEnabled
class Hack:
def setEnabled(self2, b):
if not b:
self.valid = b
else:
self.validate()
self.next_button = Hack()
self.seed_widget = None
self.can_passphrase = True
def on_ready(self):
options = ['ext'] if self.wizard_data['wallet_type'] == '2fa' else ['ext', 'bip39', 'slip39']
self.slayout = SeedLayout(
options = ['ext', 'electrum', 'bip39', 'slip39']
if self.wizard_data['wallet_type'] == '2fa':
options = ['ext', 'electrum']
else:
if self.params and 'seed_options' in self.params:
options = self.params['seed_options']
self.seed_widget = SeedWidget(
is_seed=self.is_seed,
options=options,
parent=self,
config=self.wizard.config,
)
self.slayout.updated.connect(self.validate)
self.layout().addLayout(self.slayout)
def seed_valid_changed(valid):
if not valid:
self.valid = valid
else:
self.validate()
self.seed_widget.validChanged.connect(seed_valid_changed)
self.seed_widget.updated.connect(self.validate)
self.layout().addWidget(self.seed_widget)
self.layout().addStretch(1)
def is_seed(self, x):
# really only used for electrum seeds. bip39 and slip39 are validated in SeedWidget
t = mnemonic.calc_seed_type(x)
if self.wizard_data['wallet_type'] == 'standard':
return mnemonic.is_seed(x) and not mnemonic.is_any_2fa_seed_type(t)
@ -624,9 +622,9 @@ class WCHaveSeed(WalletWizardComponent, Logger):
return t in ['standard', 'segwit']
def validate(self):
# precond: only call when SeedLayout deems seed a valid seed
seed = self.slayout.get_seed()
seed_variant = self.slayout.seed_type
# precond: only call when SeedWidget deems seed a valid seed
seed = self.seed_widget.get_seed()
seed_variant = self.seed_widget.seed_type
wallet_type = self.wizard_data['wallet_type']
seed_valid, seed_type, validation_message, self.can_passphrase = self.wizard.validate_seed(seed, seed_variant, wallet_type)
@ -646,13 +644,13 @@ class WCHaveSeed(WalletWizardComponent, Logger):
def apply(self):
cosigner_data = self.wizard.current_cosigner(self.wizard_data)
cosigner_data['seed'] = self.slayout.get_seed()
cosigner_data['seed_variant'] = self.slayout.seed_type
if self.slayout.seed_type == 'electrum':
cosigner_data['seed_type'] = mnemonic.calc_seed_type(self.slayout.get_seed())
cosigner_data['seed'] = self.seed_widget.get_seed()
cosigner_data['seed_variant'] = self.seed_widget.seed_type
if self.seed_widget.seed_type == 'electrum':
cosigner_data['seed_type'] = mnemonic.calc_seed_type(self.seed_widget.get_seed())
else:
cosigner_data['seed_type'] = self.slayout.seed_type
cosigner_data['seed_extend'] = self.slayout.is_ext if self.can_passphrase else False
cosigner_data['seed_type'] = self.seed_widget.seed_type
cosigner_data['seed_extend'] = self.seed_widget.is_ext if self.can_passphrase else False
cosigner_data['seed_extra_words'] = '' # empty default
@ -790,13 +788,13 @@ class WCCosignerKeystore(WalletWizardComponent):
# different from old wizard: master public key for sharing is now shown on this page
self.layout().addSpacing(20)
self.layout().addWidget(WWLabel(_('Below is your master public key. Please share it with your cosigners')))
slayout = SeedLayout(
seed_widget = SeedWidget(
self.wizard_data['multisig_master_pubkey'],
icon=False,
for_seed_words=False,
config=self.wizard.config,
)
self.layout().addLayout(slayout)
self.layout().addWidget(seed_widget)
self.layout().addStretch(1)
def apply(self):
@ -811,7 +809,7 @@ class WCHaveMasterKey(WalletWizardComponent):
def __init__(self, parent, wizard):
WalletWizardComponent.__init__(self, parent, wizard, title=_('Create keystore from a master key'))
self.slayout = None
self.keys_widget = None
self.message_create = ' '.join([
_("To create a watching-only wallet, please enter your master public key (xpub/ypub/zpub)."),
@ -827,16 +825,6 @@ class WCHaveMasterKey(WalletWizardComponent):
self.label.setMinimumWidth(400)
self.header_layout.addWidget(self.label)
# TODO: KeysLayout assumes too much in parent, refactor KeysLayout
# for now, fake parent.next_button.setEnabled
class Hack:
def setEnabled(self2, b):
self.valid = b
def setToolTip(self2, b):
pass
self.next_button = Hack()
def on_ready(self):
if self.wizard_data['wallet_type'] == 'standard':
self.label.setText(self.message_create)
@ -860,12 +848,19 @@ class WCHaveMasterKey(WalletWizardComponent):
return True
else:
raise Exception(f"unexpected wallet type: {self.wizard_data['wallet_type']}")
self.slayout = KeysLayout(parent=self, header_layout=self.header_layout, is_valid=is_valid,
allow_multi=False, config=self.wizard.config)
self.layout().addLayout(self.slayout)
self.keys_widget = KeysWidget(parent=self, header_layout=self.header_layout, is_valid=is_valid,
allow_multi=False, config=self.wizard.config)
def key_valid_changed(valid):
self.valid = valid
self.keys_widget.validChanged.connect(key_valid_changed)
self.layout().addWidget(self.keys_widget)
def apply(self):
text = self.slayout.get_text()
text = self.keys_widget.get_text()
cosigner_data = self.wizard.current_cosigner(self.wizard_data)
cosigner_data['master_key'] = text
@ -942,25 +937,20 @@ class WCImport(WalletWizardComponent):
header_layout.addWidget(label)
header_layout.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignmentFlag.AlignRight)
# TODO: KeysLayout assumes too much in parent, refactor KeysLayout
# for now, fake parent.next_button.setEnabled
class Hack:
def setEnabled(self2, b):
self.valid = b
def setToolTip(self2, b):
pass
self.next_button = Hack()
def is_valid(x) -> bool:
return keystore.is_address_list(x) or keystore.is_private_key_list(x, raise_on_error=True)
self.slayout = KeysLayout(parent=self, header_layout=header_layout, is_valid=is_valid,
allow_multi=True, config=self.wizard.config)
self.layout().addLayout(self.slayout)
self.keys_widget = KeysWidget(header_layout=header_layout, is_valid=is_valid,
allow_multi=True, config=self.wizard.config)
def key_valid_changed(valid):
self.valid = valid
self.keys_widget.validChanged.connect(key_valid_changed)
self.layout().addWidget(self.keys_widget)
def apply(self):
text = self.slayout.get_text()
text = self.keys_widget.get_text()
if keystore.is_address_list(text):
self.wizard_data['address_list'] = text
elif keystore.is_private_key_list(text):

Loading…
Cancel
Save