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,
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.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.util import WalletFileException, get_new_wallet_name
from electrum.wallet import wallet_types
@ -51,12 +51,12 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
'confirm_ext': { 'gui': WCConfirmExt },
'have_seed': { 'gui': WCHaveSeed },
'bip39_refine': { 'gui': WCBIP39Refine },
'have_master_key': { 'gui': 'WCHaveMasterKey' },
'have_master_key': { 'gui': WCHaveMasterKey },
'multisig': { 'gui': WCMultisig },
'multisig_cosigner_keystore': { 'gui': 'WCCosignerKeystore' },
'multisig_cosigner_key': { 'gui': 'WCHaveMasterKey' },
'multisig_cosigner_seed': { 'gui': 'WCHaveSeed' },
'multisig_cosigner_bip39_refine': { 'gui': 'WCBIP39Refine' },
'multisig_cosigner_keystore': { 'gui': WCCosignerKeystore },
'multisig_cosigner_key': { 'gui': WCHaveMasterKey },
'multisig_cosigner_seed': { 'gui': WCHaveSeed },
'multisig_cosigner_bip39_refine': { 'gui': WCBIP39Refine },
'imported': { 'gui': WCImport },
'wallet_password': { 'gui': WCWalletPassword }
})
@ -105,14 +105,9 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
# data = js_data.toVariant()
# 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):
self._logger.info('Creating wallet from wizard data')
# data = js_data.toVariant()
data = self._current.wizard_data
if self.is_single_password() and single_password:
@ -401,6 +396,8 @@ class WCConfirmExt(WizardComponent):
class WCHaveSeed(WizardComponent):
_logger = get_logger(__name__)
def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('Enter Seed'))
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
class Hack:
def setEnabled(self2, b):
self.valid = b
if not b:
self.valid = b
else:
self.validate()
self.next_button = Hack()
def on_ready(self):
@ -428,24 +429,57 @@ class WCHaveSeed(WizardComponent):
else:
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):
self.wizard_data['seed'] = self.slayout.get_seed()
self.wizard_data['seed_variant'] = self.slayout.seed_type
wizard_data = 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']
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':
self.wizard_data['seed_type'] = mnemonic.seed_type(self.slayout.get_seed())
wizard_data['seed_type'] = mnemonic.seed_type(self.slayout.get_seed())
else:
self.wizard_data['seed_type'] = self.slayout.seed_type
self.wizard_data['seed_extend'] = self.slayout.is_ext
self.wizard_data['seed_extra_words'] = '' # empty default
wizard_data['seed_type'] = self.slayout.seed_type
wizard_data['seed_extend'] = self.slayout.is_ext
wizard_data['seed_extra_words'] = '' # empty default
class WCBIP39Refine(WizardComponent):
_logger = get_logger(__name__)
def __init__(self, parent, wizard):
WizardComponent.__init__(self, parent, wizard, title=_('Script type and Derivation path'))
def on_ready(self):
if self.wizard_data['wallet_type'] == 'multisig':
raise Exception('NYI')
message1 = _('Choose the type of addresses in your wallet.')
message2 = ' '.join([
@ -454,12 +488,31 @@ class WCBIP39Refine(WizardComponent):
])
hide_choices = False
default_choice_idx = 2
choices = [
('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)),
]
if self.wizard_data['wallet_type'] == 'multisig':
choices = [
# TODO: 'standard' is a backend wallet concept, wizard wants 'p2sh'
('standard', 'legacy multisig (p2sh)', normalize_bip32_derivation("m/45'/0")),
('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 ''
root_seed = bip39_to_seed(self.wizard_data['seed'], passphrase)
@ -470,7 +523,7 @@ class WCBIP39Refine(WizardComponent):
account_xpub = account_node.to_xpub()
return account_xpub
if get_account_xpub:
if self.wizard_data['wallet_type'] == 'standard':
button = QPushButton(_("Detect Existing Accounts"))
def on_account_select(account):
@ -504,13 +557,145 @@ class WCBIP39Refine(WizardComponent):
self.derivation_path_edit.textChanged.connect(self.validate)
on_choice_click(self.clayout) # set default value for derivation path
self.layout().addWidget(self.derivation_path_edit)
self.layout().addStretch(1)
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):
self.wizard_data['script_type'] = self.c_values[self.clayout.selected_index()]
self.wizard_data['derivation_path'] = str(self.derivation_path_edit.text())
wizard_data = 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']
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):
@ -578,8 +763,6 @@ class WCImport(WizardComponent):
WizardComponent.__init__(self, parent, wizard, title=_('Import Bitcoin Addresses'))
message = _(
'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()
label = WWLabel(message)
label.setMinimumWidth(400)

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

@ -148,7 +148,7 @@ class QEAbstractWizard(QDialog):
self.accept()
else:
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:
self.start()

47
electrum/wizard.py

@ -4,10 +4,11 @@ import os
from typing import List, NamedTuple, Any, Dict, Optional
from electrum.logging import get_logger
from electrum.slip39 import Slip39Error, decode_mnemonic
from electrum.storage import WalletStorage, StorageEncryptionVersion
from electrum.wallet_db import WalletDB
from electrum.bip32 import normalize_bip32_derivation, xpub_type
from electrum import keystore
from electrum import keystore, mnemonic
from electrum import bitcoin
from electrum.mnemonic import is_any_2fa_seed_type
@ -280,9 +281,9 @@ class NewWalletWizard(AbstractWizard):
return 'wallet_password'
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:
self._logger.info('maybe_master_pubkey2')
self._logger.debug('deferred, missing derivation_path')
return
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:
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):
assert data['wallet_type'] in ['standard', '2fa', 'imported', 'multisig']

Loading…
Cancel
Save