Browse Source

qt: add have_master_key gui, implement cosigners in have_seed and bip39_refine guis

fix adding data from accept handler
master
Sander van Grieken 2 years ago
parent
commit
d395b97e83
  1. 249
      electrum/gui/qt/wizard/wallet.py
  2. 2
      electrum/gui/qt/wizard/wizard.py
  3. 47
      electrum/wizard.py

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

@ -5,10 +5,10 @@ from PyQt5.QtGui import QPen, QPainter, QPalette
from PyQt5.QtWidgets import (QApplication, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QWidget, from PyQt5.QtWidgets import (QApplication, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QWidget,
QFileDialog, QSlider, QGridLayout) QFileDialog, QSlider, QGridLayout)
from electrum.bip32 import is_bip32_derivation, BIP32Node from electrum.bip32 import is_bip32_derivation, BIP32Node, normalize_bip32_derivation, xpub_type
from electrum.daemon import Daemon from electrum.daemon import Daemon
from electrum.i18n import _ from electrum.i18n import _
from electrum.keystore import bip44_derivation, bip39_to_seed from electrum.keystore import bip44_derivation, bip39_to_seed, purpose48_derivation
from electrum.storage import StorageReadWriteError from electrum.storage import StorageReadWriteError
from electrum.util import WalletFileException, get_new_wallet_name from electrum.util import WalletFileException, get_new_wallet_name
from electrum.wallet import wallet_types from electrum.wallet import wallet_types
@ -51,12 +51,12 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
'confirm_ext': { 'gui': WCConfirmExt }, 'confirm_ext': { 'gui': WCConfirmExt },
'have_seed': { 'gui': WCHaveSeed }, 'have_seed': { 'gui': WCHaveSeed },
'bip39_refine': { 'gui': WCBIP39Refine }, 'bip39_refine': { 'gui': WCBIP39Refine },
'have_master_key': { 'gui': 'WCHaveMasterKey' }, 'have_master_key': { 'gui': WCHaveMasterKey },
'multisig': { 'gui': WCMultisig }, 'multisig': { 'gui': WCMultisig },
'multisig_cosigner_keystore': { 'gui': 'WCCosignerKeystore' }, 'multisig_cosigner_keystore': { 'gui': WCCosignerKeystore },
'multisig_cosigner_key': { 'gui': 'WCHaveMasterKey' }, 'multisig_cosigner_key': { 'gui': WCHaveMasterKey },
'multisig_cosigner_seed': { 'gui': 'WCHaveSeed' }, 'multisig_cosigner_seed': { 'gui': WCHaveSeed },
'multisig_cosigner_bip39_refine': { 'gui': 'WCBIP39Refine' }, 'multisig_cosigner_bip39_refine': { 'gui': WCBIP39Refine },
'imported': { 'gui': WCImport }, 'imported': { 'gui': WCImport },
'wallet_password': { 'gui': WCWalletPassword } 'wallet_password': { 'gui': WCWalletPassword }
}) })
@ -105,14 +105,9 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
# data = js_data.toVariant() # data = js_data.toVariant()
# return self.has_heterogeneous_masterkeys(data) # return self.has_heterogeneous_masterkeys(data)
# #
# @pyqtSlot(str, str, result=bool)
# def isMatchingSeed(self, seed, seed_again):
# return mnemonic.is_matching_seed(seed=seed, seed_again=seed_again)
#
def create_storage(self, single_password: str = None): def create_storage(self, single_password: str = None):
self._logger.info('Creating wallet from wizard data') self._logger.info('Creating wallet from wizard data')
# data = js_data.toVariant()
data = self._current.wizard_data data = self._current.wizard_data
if self.is_single_password() and single_password: if self.is_single_password() and single_password:
@ -401,6 +396,8 @@ class WCConfirmExt(WizardComponent):
class WCHaveSeed(WizardComponent): class WCHaveSeed(WizardComponent):
_logger = get_logger(__name__)
def __init__(self, parent, wizard): def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('Enter Seed')) WizardComponent.__init__(self, parent, wizard, title=_('Enter Seed'))
self.layout().addWidget(WWLabel(_('Please enter your seed phrase in order to restore your wallet.'))) self.layout().addWidget(WWLabel(_('Please enter your seed phrase in order to restore your wallet.')))
@ -409,7 +406,11 @@ class WCHaveSeed(WizardComponent):
# for now, fake parent.next_button.setEnabled # for now, fake parent.next_button.setEnabled
class Hack: class Hack:
def setEnabled(self2, b): def setEnabled(self2, b):
self.valid = b if not b:
self.valid = b
else:
self.validate()
self.next_button = Hack() self.next_button = Hack()
def on_ready(self): def on_ready(self):
@ -428,24 +429,57 @@ class WCHaveSeed(WizardComponent):
else: else:
return mnemonic.seed_type(x) in ['standard', 'segwit'] return mnemonic.seed_type(x) 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
wallet_type = self.wizard_data['wallet_type']
seed_valid, seed_type, validation_message = self.wizard.validate_seed(seed, seed_variant, wallet_type)
if not seed_valid:
self.valid = False
return
if seed_type in ['bip39', 'slip39']:
# defer validation to when derivation path is known
self.valid = True
else:
self.apply()
if self.wizard.has_duplicate_masterkeys(self.wizard_data):
self._logger.debug('Duplicate master keys!')
# TODO: user feedback
seed_valid = False
elif self.wizard.has_heterogeneous_masterkeys(self.wizard_data):
self._logger.debug('Heterogenous master keys!')
# TODO: user feedback
seed_valid = False
self.valid = seed_valid
def apply(self): def apply(self):
self.wizard_data['seed'] = self.slayout.get_seed() wizard_data = self.wizard_data
self.wizard_data['seed_variant'] = self.slayout.seed_type if self.wizard_data['wallet_type'] == 'multisig' and 'multisig_current_cosigner' in self.wizard_data:
cosigner = self.wizard_data['multisig_current_cosigner']
if cosigner != 0:
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':
self.wizard_data['seed_type'] = mnemonic.seed_type(self.slayout.get_seed()) wizard_data['seed_type'] = mnemonic.seed_type(self.slayout.get_seed())
else: else:
self.wizard_data['seed_type'] = self.slayout.seed_type wizard_data['seed_type'] = self.slayout.seed_type
self.wizard_data['seed_extend'] = self.slayout.is_ext wizard_data['seed_extend'] = self.slayout.is_ext
self.wizard_data['seed_extra_words'] = '' # empty default wizard_data['seed_extra_words'] = '' # empty default
class WCBIP39Refine(WizardComponent): class WCBIP39Refine(WizardComponent):
_logger = get_logger(__name__)
def __init__(self, parent, wizard): def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('Script type and Derivation path')) WizardComponent.__init__(self, parent, wizard, title=_('Script type and Derivation path'))
def on_ready(self): def on_ready(self):
if self.wizard_data['wallet_type'] == 'multisig':
raise Exception('NYI')
message1 = _('Choose the type of addresses in your wallet.') message1 = _('Choose the type of addresses in your wallet.')
message2 = ' '.join([ message2 = ' '.join([
@ -454,12 +488,31 @@ class WCBIP39Refine(WizardComponent):
]) ])
hide_choices = False hide_choices = False
default_choice_idx = 2 if self.wizard_data['wallet_type'] == 'multisig':
choices = [ choices = [
('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)), # TODO: 'standard' is a backend wallet concept, wizard wants 'p2sh'
('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)), ('standard', 'legacy multisig (p2sh)', normalize_bip32_derivation("m/45'/0")),
('p2wpkh', 'native segwit (p2wpkh)', bip44_derivation(0, bip43_purpose=84)), ('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')),
] ('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')),
]
if 'multisig_current_cosigner' in self.wizard_data:
# get script type of first cosigner
ks = self.wizard.keystore_from_data(self.wizard_data['wallet_type'], self.wizard_data)
script_type = xpub_type(ks.get_master_public_key())
script_types = [*zip(*choices)][0]
chosen_idx = script_types.index(script_type)
default_choice_idx = chosen_idx
hide_choices = True
else:
default_choice_idx = 2
else:
default_choice_idx = 2
choices = [
# TODO: 'standard' is a backend wallet concept, wizard wants 'p2pkh'
('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)),
('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)),
('p2wpkh', 'native segwit (p2wpkh)', bip44_derivation(0, bip43_purpose=84)),
]
passphrase = self.wizard_data['seed_extra_words'] if self.wizard_data['seed_extend'] else '' passphrase = self.wizard_data['seed_extra_words'] if self.wizard_data['seed_extend'] else ''
root_seed = bip39_to_seed(self.wizard_data['seed'], passphrase) root_seed = bip39_to_seed(self.wizard_data['seed'], passphrase)
@ -470,7 +523,7 @@ class WCBIP39Refine(WizardComponent):
account_xpub = account_node.to_xpub() account_xpub = account_node.to_xpub()
return account_xpub return account_xpub
if get_account_xpub: if self.wizard_data['wallet_type'] == 'standard':
button = QPushButton(_("Detect Existing Accounts")) button = QPushButton(_("Detect Existing Accounts"))
def on_account_select(account): def on_account_select(account):
@ -504,13 +557,145 @@ class WCBIP39Refine(WizardComponent):
self.derivation_path_edit.textChanged.connect(self.validate) self.derivation_path_edit.textChanged.connect(self.validate)
on_choice_click(self.clayout) # set default value for derivation path on_choice_click(self.clayout) # set default value for derivation path
self.layout().addWidget(self.derivation_path_edit) self.layout().addWidget(self.derivation_path_edit)
self.layout().addStretch(1)
def validate(self): def validate(self):
self.valid = is_bip32_derivation(self.derivation_path_edit.text()) self.apply()
derivation_valid = is_bip32_derivation(self.wizard_data['derivation_path'])
if self.wizard_data['wallet_type'] == 'multisig':
if self.wizard.has_duplicate_masterkeys(self.wizard_data):
self._logger.debug('Duplicate master keys!')
# TODO: user feedback
derivation_valid = False
elif self.wizard.has_heterogeneous_masterkeys(self.wizard_data):
self._logger.debug('Heterogenous master keys!')
# TODO: user feedback
derivation_valid = False
self.valid = derivation_valid
def apply(self): def apply(self):
self.wizard_data['script_type'] = self.c_values[self.clayout.selected_index()] wizard_data = self.wizard_data
self.wizard_data['derivation_path'] = str(self.derivation_path_edit.text()) if self.wizard_data['wallet_type'] == 'multisig' and 'multisig_current_cosigner' in self.wizard_data:
cosigner = self.wizard_data['multisig_current_cosigner']
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):
def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard)
message = _('Add a cosigner to your multi-sig wallet')
choices = [
('key', _('Enter cosigner key')),
('seed', _('Enter cosigner seed')),
('hw_device', _('Cosign with hardware device'))
]
self.c_values = [x[0] for x in choices]
c_titles = [x[1] for x in choices]
self.clayout = ChoicesLayout(message, c_titles)
self.layout().addLayout(self.clayout.layout())
self.cosigner = 0
self.participants = 0
self._valid = True
def on_ready(self):
self.participants = self.wizard_data['multisig_participants']
# cosigner index is determined here and put on the wizard_data dict in apply()
# as this page is the start for each additional cosigner
self.cosigner = 2 + len(self.wizard_data['multisig_cosigner_data'])
self.wizard_data['multisig_current_cosigner'] = self.cosigner
self.title = _("Add Cosigner {}").format(self.wizard_data['multisig_current_cosigner'])
# 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(
self.wizard_data['multisig_master_pubkey'],
icon=False,
for_seed_words=False,
config=self.wizard.config,
)
self.layout().addLayout(slayout)
self.layout().addStretch(1)
def apply(self):
self.wizard_data['cosigner_keystore_type'] = self.c_values[self.clayout.selected_index()]
self.wizard_data['multisig_current_cosigner'] = self.cosigner
self.wizard_data['multisig_cosigner_data'][str(self.cosigner)] = {}
class WCHaveMasterKey(WizardComponent):
def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('Create keystore from a master key'))
self.message_create = ' '.join([
_("To create a watching-only wallet, please enter your master public key (xpub/ypub/zpub)."),
_("To create a spending wallet, please enter a master private key (xprv/yprv/zprv).")
])
self.message_cosign = ' '.join([
_('Please enter the master public key (xpub) of your cosigner.'),
_('Enter their master private key (xprv) if you want to be able to sign for them.')
])
self.header_layout = QHBoxLayout()
self.label = WWLabel()
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.wallet_type == 'standard':
# v = keystore.is_master_key
# self.add_xpub_dialog(title=title, message=message, run_next=self.on_restore_from_key, is_valid=v)
# else:
# i = len(self.keystores) + 1
# self.add_cosigner_dialog(index=i, run_next=self.on_restore_from_key, is_valid=keystore.is_bip32_key)
if self.wizard_data['wallet_type'] == 'standard':
self.label.setText(self.message_create)
v = lambda x: bool(keystore.from_master_key(x))
self.slayout = KeysLayout(parent=self, header_layout=self.header_layout, is_valid=v,
allow_multi=False, config=self.wizard.config)
self.layout().addLayout(self.slayout)
elif self.wizard_data['wallet_type'] == 'multisig':
if 'multisig_current_cosigner' in self.wizard_data:
self.title = _("Add Cosigner {}").format(self.wizard_data['multisig_current_cosigner'])
self.label.setText(self.message_cosign)
else:
self.wizard_data['multisig_current_cosigner'] = 0
self.label.setText(self.message_create)
v = lambda x: keystore.is_bip32_key(x)
self.slayout = KeysLayout(parent=self, header_layout=self.header_layout, is_valid=v,
allow_multi=False, config=self.wizard.config)
self.layout().addLayout(self.slayout)
def apply(self):
text = self.slayout.get_text()
if self.wizard_data['wallet_type'] == 'standard':
self.wizard_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):
@ -578,8 +763,6 @@ class WCImport(WizardComponent):
WizardComponent.__init__(self, parent, wizard, title=_('Import Bitcoin Addresses')) WizardComponent.__init__(self, parent, wizard, title=_('Import Bitcoin Addresses'))
message = _( message = _(
'Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.') 'Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.')
# self.add_xpub_dialog(title=title, message=message, run_next=self.on_import,
# is_valid=v, allow_multi=True, show_wif_help=True)
header_layout = QHBoxLayout() header_layout = QHBoxLayout()
label = WWLabel(message) label = WWLabel(message)
label.setMinimumWidth(400) label.setMinimumWidth(400)

2
electrum/gui/qt/wizard/wizard.py

@ -148,7 +148,7 @@ class QEAbstractWizard(QDialog):
self.accept() self.accept()
else: else:
next = self.submit(wd) next = self.submit(wd)
self.load_next_component(next['view'], wd) self.load_next_component(next['view'], next['wizard_data'])
def start_wizard(self) -> str: def start_wizard(self) -> str:
self.start() self.start()

47
electrum/wizard.py

@ -4,10 +4,11 @@ import os
from typing import List, NamedTuple, Any, Dict, Optional from typing import List, NamedTuple, Any, Dict, Optional
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.slip39 import Slip39Error, decode_mnemonic
from electrum.storage import WalletStorage, StorageEncryptionVersion from electrum.storage import WalletStorage, StorageEncryptionVersion
from electrum.wallet_db import WalletDB from electrum.wallet_db import WalletDB
from electrum.bip32 import normalize_bip32_derivation, xpub_type from electrum.bip32 import normalize_bip32_derivation, xpub_type
from electrum import keystore from electrum import keystore, mnemonic
from electrum import bitcoin from electrum import bitcoin
from electrum.mnemonic import is_any_2fa_seed_type from electrum.mnemonic import is_any_2fa_seed_type
@ -280,9 +281,9 @@ class NewWalletWizard(AbstractWizard):
return 'wallet_password' return 'wallet_password'
def maybe_master_pubkey(self, wizard_data): def maybe_master_pubkey(self, wizard_data):
self._logger.info('maybe_master_pubkey') self._logger.debug('maybe_master_pubkey')
if self.is_bip39_seed(wizard_data) and 'derivation_path' not in wizard_data: if self.is_bip39_seed(wizard_data) and 'derivation_path' not in wizard_data:
self._logger.info('maybe_master_pubkey2') self._logger.debug('deferred, missing derivation_path')
return return
wizard_data['multisig_master_pubkey'] = self.keystore_from_data(wizard_data['wallet_type'], wizard_data).get_master_public_key() wizard_data['multisig_master_pubkey'] = self.keystore_from_data(wizard_data['wallet_type'], wizard_data).get_master_public_key()
@ -367,6 +368,46 @@ class NewWalletWizard(AbstractWizard):
else: else:
raise Exception('no seed or master_key in data') raise Exception('no seed or master_key in data')
def validate_seed(self, seed, seed_variant, wallet_type):
seed_type = ''
seed_valid = False
validation_message = ''
if seed_variant == 'electrum':
seed_type = mnemonic.seed_type(seed)
if seed_type != '':
seed_valid = True
elif seed_variant == 'bip39':
is_checksum, is_wordlist = keystore.bip39_is_checksum_valid(seed)
status = ('checksum: ' + ('ok' if is_checksum else 'failed')) if is_wordlist else 'unknown wordlist'
validation_message = 'BIP39 (%s)' % status
if is_checksum:
seed_type = 'bip39'
seed_valid = True
elif seed_variant == 'slip39': # TODO: incomplete impl, this code only validates a single share.
try:
share = decode_mnemonic(seed)
seed_type = 'slip39'
validation_message = 'SLIP39: share #%d in %dof%d scheme' % (share.group_index, share.group_threshold, share.group_count)
except Slip39Error as e:
validation_message = 'SLIP39: %s' % str(e)
seed_valid = False # for now
else:
raise Exception(f'unknown seed variant {seed_variant}')
# check if seed matches wallet type
if wallet_type == '2fa' and not is_any_2fa_seed_type(seed_type):
seed_valid = False
elif wallet_type == 'standard' and seed_type not in ['old', 'standard', 'segwit', 'bip39']:
seed_valid = False
elif wallet_type == 'multisig' and seed_type not in ['standard', 'segwit', 'bip39']:
seed_valid = False
self._logger.debug(f'seed verified: {seed_valid}, type={seed_type}, validation_message={validation_message}')
return seed_valid, seed_type, validation_message
def create_storage(self, path, data): def create_storage(self, path, data):
assert data['wallet_type'] in ['standard', '2fa', 'imported', 'multisig'] assert data['wallet_type'] in ['standard', '2fa', 'imported', 'multisig']

Loading…
Cancel
Save