diff --git a/electrum/gui/qt/seed_dialog.py b/electrum/gui/qt/seed_dialog.py index 620910db9..2cd23476e 100644 --- a/electrum/gui/qt/seed_dialog.py +++ b/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([ - '' + _('Warning') + ': ', - _('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([ - '' + _('Warning') + ': ', - _('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([ + '' + _('Warning') + ': ', + _('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([ + '' + _('Warning') + ': ', + _('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([ '' + _('Warning') + ': ', @@ -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))) diff --git a/electrum/gui/qt/wizard/wallet.py b/electrum/gui/qt/wizard/wallet.py index 4a2596e62..4437f5c24 100644 --- a/electrum/gui/qt/wizard/wallet.py +++ b/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):