Browse Source

wizard: fix wizard_data instance not isolated between pages,

combine is_bip39_seed and is_slip39_seed into cosigner aware needs_derivation_path
add instance id logging to wizard.log_stack()
qt: add updated signal to SeedLayout
master
Sander van Grieken 2 years ago
parent
commit
a6caa3ffe9
  1. 5
      electrum/gui/qt/seed_dialog.py
  2. 115
      electrum/gui/qt/wizard/wallet.py
  3. 8
      electrum/gui/qt/wizard/wizard.py
  4. 87
      electrum/wizard.py

5
electrum/gui/qt/seed_dialog.py

@ -25,7 +25,7 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QPixmap from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import (QVBoxLayout, QCheckBox, QHBoxLayout, QLineEdit, from PyQt5.QtWidgets import (QVBoxLayout, QCheckBox, QHBoxLayout, QLineEdit,
QLabel, QCompleter, QDialog, QStyledItemDelegate, QLabel, QCompleter, QDialog, QStyledItemDelegate,
@ -72,6 +72,8 @@ def seed_warning_msg(seed):
class SeedLayout(QVBoxLayout): class SeedLayout(QVBoxLayout):
updated = pyqtSignal()
def seed_options(self): def seed_options(self):
dialog = QDialog() dialog = QDialog()
dialog.setWindowTitle(_("Seed Options")) dialog.setWindowTitle(_("Seed Options"))
@ -128,6 +130,7 @@ class SeedLayout(QVBoxLayout):
return None return None
self.is_ext = cb_ext.isChecked() if 'ext' in self.options else False self.is_ext = cb_ext.isChecked() if 'ext' in self.options else False
self.seed_type = seed_type_values[clayout.selected_index()] if len(seed_types) >= 2 else 'electrum' self.seed_type = seed_type_values[clayout.selected_index()] if len(seed_types) >= 2 else 'electrum'
self.updated.emit()
def __init__( def __init__(
self, self,

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

@ -43,11 +43,8 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
'wallet_type': { 'gui': WCWalletType }, 'wallet_type': { 'gui': WCWalletType },
'keystore_type': { 'gui': WCKeystoreType }, 'keystore_type': { 'gui': WCKeystoreType },
'create_seed': { 'gui': WCCreateSeed }, 'create_seed': { 'gui': WCCreateSeed },
'create_ext': { 'gui': WCEnterExt },
'confirm_seed': { 'gui': WCConfirmSeed }, 'confirm_seed': { 'gui': WCConfirmSeed },
'confirm_ext': { 'gui': WCConfirmExt },
'have_seed': { 'gui': WCHaveSeed }, 'have_seed': { 'gui': WCHaveSeed },
'have_ext': { 'gui': WCEnterExt },
'script_and_derivation': { 'gui': WCScriptAndDerivation}, 'script_and_derivation': { 'gui': WCScriptAndDerivation},
'have_master_key': { 'gui': WCHaveMasterKey }, 'have_master_key': { 'gui': WCHaveMasterKey },
'multisig': { 'gui': WCMultisig }, 'multisig': { 'gui': WCMultisig },
@ -66,22 +63,37 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
}, },
'create_ext': { 'create_ext': {
'next': 'confirm_seed', 'next': 'confirm_seed',
'gui': WCEnterExt
}, },
'confirm_seed': { 'confirm_seed': {
'next': lambda d: 'confirm_ext' if d['seed_extend'] else self.on_have_or_confirm_seed(d), 'next': lambda d: 'confirm_ext' if d['seed_extend'] else self.on_have_or_confirm_seed(d),
'accept': lambda d: None if d['seed_extend'] else self.maybe_master_pubkey(d), 'accept': lambda d: None if d['seed_extend'] else self.maybe_master_pubkey(d)
}, },
'confirm_ext': { 'confirm_ext': {
'next': self.on_have_or_confirm_seed, 'next': self.on_have_or_confirm_seed,
'accept': self.maybe_master_pubkey, 'accept': self.maybe_master_pubkey,
'gui': WCConfirmExt
}, },
'have_seed': { 'have_seed': {
'next': lambda d: 'have_ext' if d['seed_extend'] else self.on_have_or_confirm_seed(d), 'next': lambda d: 'have_ext' if self.wants_ext(d) else self.on_have_or_confirm_seed(d),
'last': lambda d: self.is_single_password() and not
(self.needs_derivation_path(d) or self.is_multisig(d) or self.wants_ext(d))
}, },
'have_ext': { 'have_ext': {
'next': self.on_have_or_confirm_seed, 'next': self.on_have_or_confirm_seed,
'accept': self.maybe_master_pubkey, 'accept': self.maybe_master_pubkey,
'gui': WCEnterExt
}, },
'multisig_cosigner_seed': {
'next': lambda d: 'multisig_cosigner_have_ext' if self.wants_ext(d) else self.on_have_cosigner_seed(d),
'last': lambda d: self.is_single_password() and self.last_cosigner(d) and not
(self.needs_derivation_path(d) or self.wants_ext(d))
},
'multisig_cosigner_have_ext': {
'next': self.on_have_cosigner_seed,
'last': lambda d: self.is_single_password() and self.last_cosigner(d) and not self.needs_derivation_path(d),
'gui': WCEnterExt
}
}) })
@property @property
@ -338,6 +350,8 @@ class WCConfirmSeed(WizardComponent):
class WCEnterExt(WizardComponent): class WCEnterExt(WizardComponent):
_logger = get_logger(__name__)
def __init__(self, parent, wizard): def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('Seed Extension')) WizardComponent.__init__(self, parent, wizard, title=_('Seed Extension'))
@ -356,12 +370,41 @@ class WCEnterExt(WizardComponent):
self.layout().addStretch(1) self.layout().addStretch(1)
def on_text_edited(self, text): def on_text_edited(self, text):
# TODO also for cosigners?
self.ext_edit.warn_issue4566 = self.wizard_data['keystore_type'] == 'haveseed' and \ self.ext_edit.warn_issue4566 = self.wizard_data['keystore_type'] == 'haveseed' and \
self.wizard_data['seed_type'] == 'bip39' self.wizard_data['seed_type'] == 'bip39'
self.valid = len(text) > 0 self.validate()
def validate(self):
self.apply()
text = self.ext_edit.text()
if len(text) == 0:
self.valid = False
return
cosigner_data = self._current_cosigner(self.wizard_data)
if self.wizard_data['wallet_type'] == 'multisig':
if 'seed_variant' in cosigner_data and cosigner_data['seed_variant'] in ['bip39', 'slip39']:
# defer validation to when derivation path is known
self.valid = True
else:
if self.wizard.has_duplicate_masterkeys(self.wizard_data):
self._logger.debug('Duplicate master keys!')
# TODO: user feedback
self.valid = False
elif self.wizard.has_heterogeneous_masterkeys(self.wizard_data):
self._logger.debug('Heterogenous master keys!')
# TODO: user feedback
self.valid = False
else:
self.valid = True
else:
self.valid = True
def apply(self): def apply(self):
self.wizard_data['seed_extra_words'] = self.ext_edit.text() cosigner_data = self._current_cosigner(self.wizard_data)
cosigner_data['seed_extra_words'] = self.ext_edit.text()
class WCConfirmExt(WizardComponent): class WCConfirmExt(WizardComponent):
@ -409,6 +452,8 @@ class WCHaveSeed(WizardComponent):
parent=self, parent=self,
config=self.wizard.config, config=self.wizard.config,
) )
self.slayout.updated.connect(self.validate)
self.layout().addLayout(self.slayout) self.layout().addLayout(self.slayout)
def is_seed(self, x): def is_seed(self, x):
@ -428,8 +473,8 @@ class WCHaveSeed(WizardComponent):
self.valid = False self.valid = False
return return
if seed_type in ['bip39', 'slip39']: if seed_type in ['bip39', 'slip39'] or self.slayout.is_ext:
# defer validation to when derivation path is known # defer validation to when derivation path and/or passphrase/ext is known
self.valid = True self.valid = True
else: else:
self.apply() self.apply()
@ -445,20 +490,16 @@ class WCHaveSeed(WizardComponent):
self.valid = seed_valid self.valid = seed_valid
def apply(self): def apply(self):
wizard_data = self.wizard_data cosigner_data = self._current_cosigner(self.wizard_data)
if self.wizard_data['wallet_type'] == 'multisig' and 'multisig_current_cosigner' in self.wizard_data:
cosigner = self.wizard_data['multisig_current_cosigner'] cosigner_data['seed'] = self.slayout.get_seed()
if cosigner != 0: cosigner_data['seed_variant'] = self.slayout.seed_type
wizard_data = self.wizard_data['multisig_cosigner_data'][str(cosigner)]
wizard_data['seed'] = self.slayout.get_seed()
wizard_data['seed_variant'] = self.slayout.seed_type
if self.slayout.seed_type == 'electrum': if self.slayout.seed_type == 'electrum':
wizard_data['seed_type'] = mnemonic.seed_type(self.slayout.get_seed()) cosigner_data['seed_type'] = mnemonic.seed_type(self.slayout.get_seed())
else: else:
wizard_data['seed_type'] = self.slayout.seed_type cosigner_data['seed_type'] = self.slayout.seed_type
wizard_data['seed_extend'] = self.slayout.is_ext cosigner_data['seed_extend'] = self.slayout.is_ext
wizard_data['seed_extra_words'] = '' # empty default cosigner_data['seed_extra_words'] = '' # empty default
class WCScriptAndDerivation(WizardComponent): class WCScriptAndDerivation(WizardComponent):
@ -553,15 +594,10 @@ class WCScriptAndDerivation(WizardComponent):
def validate(self): def validate(self):
self.apply() self.apply()
wizard_data = self.wizard_data cosigner_data = self._current_cosigner(self.wizard_data)
if self.wizard_data['wallet_type'] == 'multisig' and 'multisig_current_cosigner' in self.wizard_data: derivation_valid = is_bip32_derivation(cosigner_data['derivation_path'])
cosigner = self.wizard_data['multisig_current_cosigner']
if cosigner != 0:
wizard_data = self.wizard_data['multisig_cosigner_data'][str(cosigner)]
derivation_valid = is_bip32_derivation(wizard_data['derivation_path']) if derivation_valid and self.wizard_data['wallet_type'] == 'multisig':
if self.wizard_data['wallet_type'] == 'multisig':
if self.wizard.has_duplicate_masterkeys(self.wizard_data): if self.wizard.has_duplicate_masterkeys(self.wizard_data):
self._logger.debug('Duplicate master keys!') self._logger.debug('Duplicate master keys!')
# TODO: user feedback # TODO: user feedback
@ -574,14 +610,9 @@ class WCScriptAndDerivation(WizardComponent):
self.valid = derivation_valid self.valid = derivation_valid
def apply(self): def apply(self):
wizard_data = self.wizard_data cosigner_data = self._current_cosigner(self.wizard_data)
if self.wizard_data['wallet_type'] == 'multisig' and 'multisig_current_cosigner' in self.wizard_data: cosigner_data['script_type'] = self.c_values[self.clayout.selected_index()]
cosigner = self.wizard_data['multisig_current_cosigner'] cosigner_data['derivation_path'] = str(self.derivation_path_edit.text())
if cosigner != 0:
wizard_data = self.wizard_data['multisig_cosigner_data'][str(cosigner)]
wizard_data['script_type'] = self.c_values[self.clayout.selected_index()]
wizard_data['derivation_path'] = str(self.derivation_path_edit.text())
class WCCosignerKeystore(WizardComponent): class WCCosignerKeystore(WizardComponent):
@ -686,14 +717,8 @@ class WCHaveMasterKey(WizardComponent):
def apply(self): def apply(self):
text = self.slayout.get_text() text = self.slayout.get_text()
if self.wizard_data['wallet_type'] == 'standard': cosigner_data = self._current_cosigner(self.wizard_data)
self.wizard_data['master_key'] = text cosigner_data['master_key'] = text
elif self.wizard_data['wallet_type'] == 'multisig':
cosigner = self.wizard_data['multisig_current_cosigner']
if cosigner == 0:
self.wizard_data['master_key'] = text
else:
self.wizard_data['multisig_cosigner_data'][str(cosigner)]['master_key'] = text
class WCMultisig(WizardComponent): class WCMultisig(WizardComponent):

8
electrum/gui/qt/wizard/wizard.py

@ -122,7 +122,6 @@ class QEAbstractWizard(QDialog):
def update(self): def update(self):
page = self.main_widget.currentWidget() page = self.main_widget.currentWidget()
self.title.setText(page.title)
self.title.setText(f'<b>{page.title}</b>' if page.title else '') self.title.setText(f'<b>{page.title}</b>' if page.title else '')
self.back_button.setText(_('Back') if self.can_go_back() else _('Cancel')) self.back_button.setText(_('Back') if self.can_go_back() else _('Cancel'))
self.next_button.setText(_('Next') if not self.is_last(page.wizard_data) else _('Finish')) self.next_button.setText(_('Next') if not self.is_last(page.wizard_data) else _('Finish'))
@ -219,3 +218,10 @@ class WizardComponent(QWidget):
def on_updated(self, *args): def on_updated(self, *args):
self.updated.emit(self) self.updated.emit(self)
# returns (sub)dict of current cosigner (or root if first)
def _current_cosigner(self, wizard_data):
wdata = wizard_data
if wizard_data['wallet_type'] == 'multisig' and 'multisig_current_cosigner' in wizard_data:
cosigner = wizard_data['multisig_current_cosigner']
wdata = wizard_data['multisig_cosigner_data'][str(cosigner)]
return wdata

87
electrum/wizard.py

@ -67,6 +67,9 @@ class AbstractWizard:
else: else:
raise Exception(f'accept handler for view {view} is not callable') raise Exception(f'accept handler for view {view} is not callable')
# make a clone for next view
wizard_data = copy.deepcopy(wizard_data)
is_finished = False is_finished = False
if 'next' not in nav: if 'next' not in nav:
# finished # finished
@ -109,13 +112,12 @@ class AbstractWizard:
return new_view return new_view
def resolve_prev(self): def resolve_prev(self):
prev_view = self._stack.pop() self._current = self._stack.pop()
self._logger.debug(f'resolve_prev view is {prev_view}') self._logger.debug(f'resolve_prev view is "{self._current.view}"')
self.log_stack() self.log_stack()
self._current = prev_view return self._current
return prev_view
# check if this view is the final view # check if this view is the final view
def is_last_view(self, view, wizard_data): def is_last_view(self, view, wizard_data):
@ -149,27 +151,29 @@ class AbstractWizard:
def log_stack(self): def log_stack(self):
logstr = 'wizard stack:' logstr = 'wizard stack:'
stack = copy.deepcopy(self._stack)
i = 0 i = 0
for item in stack: for item in self._stack:
self.sanitize_stack_item(item.wizard_data) ssi = self.sanitize_stack_item(item.wizard_data)
logstr += f'\n{i}: {repr(item.wizard_data)}' logstr += f'\n{i}: {hex(id(item.wizard_data))} - {repr(ssi)}'
i += 1 i += 1
current = copy.deepcopy(self._current) sci = self.sanitize_stack_item(self._current.wizard_data)
self.sanitize_stack_item(current.wizard_data) logstr += f'\nc: {hex(id(self._current.wizard_data))} - {repr(sci)}'
logstr += f'\nc: {repr(current.wizard_data)}'
self._logger.debug(logstr) self._logger.debug(logstr)
def sanitize_stack_item(self, _stack_item): 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 = {}
for item in _dict: for item in _dict:
if isinstance(_dict[item], dict): if isinstance(_dict[item], dict):
sanitize(_dict[item]) result[item] = sanitize(_dict[item])
else: else:
if item in sensitive_keys: if item in sensitive_keys:
_dict[item] = '<sensitive value removed>' result[item] = '<sensitive value removed>'
sanitize(_stack_item) else:
result[item] = _dict[item]
return result
return sanitize(_stack_item)
class NewWalletWizard(AbstractWizard): class NewWalletWizard(AbstractWizard):
@ -199,7 +203,8 @@ class NewWalletWizard(AbstractWizard):
'have_seed': { 'have_seed': {
'next': self.on_have_or_confirm_seed, 'next': self.on_have_or_confirm_seed,
'accept': self.maybe_master_pubkey, 'accept': self.maybe_master_pubkey,
'last': lambda d: self.is_single_password() and not self.is_bip39_seed(d) and not self.is_multisig(d) 'last': lambda d: self.is_single_password() and not
(self.needs_derivation_path(d) or self.is_multisig(d))
}, },
'script_and_derivation': { 'script_and_derivation': {
'next': lambda d: 'wallet_password' if not self.is_multisig(d) else 'multisig_cosigner_keystore', 'next': lambda d: 'wallet_password' if not self.is_multisig(d) else 'multisig_cosigner_keystore',
@ -218,16 +223,16 @@ class NewWalletWizard(AbstractWizard):
'next': self.on_cosigner_keystore_type 'next': self.on_cosigner_keystore_type
}, },
'multisig_cosigner_key': { 'multisig_cosigner_key': {
'next': lambda d: 'wallet_password' if self.has_all_cosigner_data(d) else 'multisig_cosigner_keystore', 'next': lambda d: 'wallet_password' if self.last_cosigner(d) else 'multisig_cosigner_keystore',
'last': lambda d: self.is_single_password() and self.has_all_cosigner_data(d) 'last': lambda d: self.is_single_password() and self.last_cosigner(d)
}, },
'multisig_cosigner_seed': { 'multisig_cosigner_seed': {
'next': self.on_have_cosigner_seed, 'next': self.on_have_cosigner_seed,
'last': lambda d: self.is_single_password() and self.has_all_cosigner_data(d) 'last': lambda d: self.is_single_password() and self.last_cosigner(d) and not self.needs_derivation_path(d)
}, },
'multisig_cosigner_script_and_derivation': { 'multisig_cosigner_script_and_derivation': {
'next': lambda d: 'wallet_password' if self.has_all_cosigner_data(d) else 'multisig_cosigner_keystore', 'next': lambda d: 'wallet_password' if self.last_cosigner(d) else 'multisig_cosigner_keystore',
'last': lambda d: self.is_single_password() and self.has_all_cosigner_data(d) 'last': lambda d: self.is_single_password() and self.last_cosigner(d)
}, },
'imported': { 'imported': {
'next': 'wallet_password', 'next': 'wallet_password',
@ -249,11 +254,21 @@ class NewWalletWizard(AbstractWizard):
def is_single_password(self): def is_single_password(self):
raise NotImplementedError() raise NotImplementedError()
def is_bip39_seed(self, wizard_data): # returns (sub)dict of current cosigner (or root if first)
return wizard_data.get('seed_variant') == 'bip39' def _current_cosigner(self, wizard_data):
wdata = wizard_data
if wizard_data['wallet_type'] == 'multisig' and 'multisig_current_cosigner' in wizard_data:
cosigner = wizard_data['multisig_current_cosigner']
wdata = wizard_data['multisig_cosigner_data'][str(cosigner)]
return wdata
def is_slip39_seed(self, wizard_data): def needs_derivation_path(self, wizard_data):
return wizard_data.get('seed_variant') == 'slip39' wdata = self._current_cosigner(wizard_data)
return 'seed_variant' in wdata and wdata['seed_variant'] in ['bip39', 'slip39']
def wants_ext(self, wizard_data):
wdata = self._current_cosigner(wizard_data)
return 'seed_variant' in wdata and wdata['seed_extend']
def is_multisig(self, wizard_data): def is_multisig(self, wizard_data):
return wizard_data['wallet_type'] == 'multisig' return wizard_data['wallet_type'] == 'multisig'
@ -276,9 +291,7 @@ class NewWalletWizard(AbstractWizard):
}.get(t) }.get(t)
def on_have_or_confirm_seed(self, wizard_data): def on_have_or_confirm_seed(self, wizard_data):
if self.is_bip39_seed(wizard_data): if self.needs_derivation_path(wizard_data):
return 'script_and_derivation'
elif self.is_slip39_seed(wizard_data):
return 'script_and_derivation' return 'script_and_derivation'
elif self.is_multisig(wizard_data): elif self.is_multisig(wizard_data):
return 'multisig_cosigner_keystore' return 'multisig_cosigner_keystore'
@ -287,7 +300,7 @@ class NewWalletWizard(AbstractWizard):
def maybe_master_pubkey(self, wizard_data): def maybe_master_pubkey(self, wizard_data):
self._logger.debug('maybe_master_pubkey') self._logger.debug('maybe_master_pubkey')
if (self.is_bip39_seed(wizard_data) or self.is_slip39_seed(wizard_data)) and 'derivation_path' not in wizard_data: if self.needs_derivation_path(wizard_data) and 'derivation_path' not in wizard_data:
self._logger.debug('deferred, missing derivation_path') self._logger.debug('deferred, missing derivation_path')
return return
@ -302,23 +315,19 @@ class NewWalletWizard(AbstractWizard):
def on_have_cosigner_seed(self, wizard_data): def on_have_cosigner_seed(self, wizard_data):
current_cosigner_data = wizard_data['multisig_cosigner_data'][str(wizard_data['multisig_current_cosigner'])] current_cosigner_data = wizard_data['multisig_cosigner_data'][str(wizard_data['multisig_current_cosigner'])]
if self.has_all_cosigner_data(wizard_data): if self.needs_derivation_path(wizard_data) and 'derivation_path' not in current_cosigner_data:
return 'wallet_password'
elif current_cosigner_data['seed_type'] == 'bip39' and 'derivation_path' not in current_cosigner_data:
return 'multisig_cosigner_script_and_derivation' return 'multisig_cosigner_script_and_derivation'
elif self.last_cosigner(wizard_data):
return 'wallet_password'
else: else:
return 'multisig_cosigner_keystore' return 'multisig_cosigner_keystore'
def has_all_cosigner_data(self, wizard_data): def last_cosigner(self, wizard_data):
# number of items in multisig_cosigner_data is less than participants? # check if we have the final number of cosigners. Doesn't check if cosigner data itself is complete
# (should be validated by wizardcomponents)
if len(wizard_data['multisig_cosigner_data']) < (wizard_data['multisig_participants'] - 1): if len(wizard_data['multisig_cosigner_data']) < (wizard_data['multisig_participants'] - 1):
return False return False
# if last cosigner uses bip39 seed, we still need derivation path
current_cosigner_data = wizard_data['multisig_cosigner_data'][str(wizard_data['multisig_current_cosigner'])]
if 'seed_type' in current_cosigner_data and current_cosigner_data['seed_type'] == 'bip39' and 'derivation_path' not in current_cosigner_data:
return False
return True return True
def has_duplicate_masterkeys(self, wizard_data) -> bool: def has_duplicate_masterkeys(self, wizard_data) -> bool:

Loading…
Cancel
Save