From e43b005047d06f393fcedfbe4669720265cbbb53 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 28 Jul 2023 15:42:13 +0200 Subject: [PATCH] qt: implement initial bip39 refine and account detect, restore from seed pages --- electrum/gui/qt/wizard/wallet.py | 124 ++++++++++++++++++++++++++++++- electrum/gui/qt/wizard/wizard.py | 7 +- 2 files changed, 127 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/wizard/wallet.py b/electrum/gui/qt/wizard/wallet.py index 68e72df76..64aa66014 100644 --- a/electrum/gui/qt/wizard/wallet.py +++ b/electrum/gui/qt/wizard/wallet.py @@ -5,8 +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.daemon import Daemon from electrum.i18n import _ +from electrum.keystore import bip44_derivation, bip39_to_seed from electrum.storage import StorageReadWriteError from electrum.util import WalletFileException, get_new_wallet_name from electrum.wallet import wallet_types @@ -14,6 +16,7 @@ from .wizard import QEAbstractWizard, WizardComponent from electrum.logging import get_logger from electrum import WalletStorage, mnemonic, keystore from electrum.wizard import NewWalletWizard +from ..bip39_recovery_dialog import Bip39RecoveryDialog from ..password_dialog import PasswordLayout, PW_NEW, MSG_ENTER_PASSWORD from ..seed_dialog import SeedLayout, MSG_PASSPHRASE_WARN_ISSUE4566, KeysLayout from ..util import ChoicesLayout, PasswordLineEdit, char_width_in_lineedit, WWLabel, InfoButton, font_height @@ -46,8 +49,8 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard): 'create_ext': { 'gui': WCCreateExt }, 'confirm_seed': { 'gui': WCConfirmSeed }, 'confirm_ext': { 'gui': WCConfirmExt }, - 'have_seed': { 'gui': 'WCHaveSeed' }, - 'bip39_refine': { 'gui': 'WCBIP39Refine' }, + 'have_seed': { 'gui': WCHaveSeed }, + 'bip39_refine': { 'gui': WCBIP39Refine }, 'have_master_key': { 'gui': 'WCHaveMasterKey' }, 'multisig': { 'gui': WCMultisig }, 'multisig_cosigner_keystore': { 'gui': 'WCCosignerKeystore' }, @@ -293,7 +296,9 @@ class WCCreateSeed(WizardComponent): self.seed_type = 'standard' if self.wizard.config.WIZARD_DONT_CREATE_SEGWIT else 'segwit' self.slayout = None self.seed = None - QTimer.singleShot(100, self.create_seed) + + def on_ready(self): + QTimer.singleShot(1, self.create_seed) def apply(self): if self.slayout: @@ -395,6 +400,119 @@ class WCConfirmExt(WizardComponent): pass +class WCHaveSeed(WizardComponent): + 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.'))) + + # 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() + + def on_ready(self): + options = ['ext'] if self.wizard_data['wallet_type'] == '2fa' else ['ext', 'bip39', 'slip39'] + self.slayout = SeedLayout( + is_seed=self.is_seed, + options=options, + parent=self, + config=self.wizard.config, + ) + self.layout().addLayout(self.slayout) + + def is_seed(self, x): + if self.wizard_data['wallet_type'] == 'standard': + return mnemonic.is_seed(x) + else: + return mnemonic.seed_type(x) in ['standard', 'segwit'] + + def apply(self): + self.wizard_data['seed'] = self.slayout.get_seed() + self.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()) + 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 + + +class WCBIP39Refine(WizardComponent): + 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([ + _('You can override the suggested derivation path.'), + _('If you are not sure what this is, leave this field unchanged.') + ]) + 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)), + ] + + passphrase = self.wizard_data['seed_extra_words'] if self.wizard_data['seed_extend'] else '' + root_seed = bip39_to_seed(self.wizard_data['seed'], passphrase) + + def get_account_xpub(account_path): + root_node = BIP32Node.from_rootseed(root_seed, xtype="standard") + account_node = root_node.subkey_at_private_derivation(account_path) + account_xpub = account_node.to_xpub() + return account_xpub + + if get_account_xpub: + button = QPushButton(_("Detect Existing Accounts")) + + def on_account_select(account): + script_type = account["script_type"] + if script_type == "p2pkh": + script_type = "standard" + button_index = self.c_values.index(script_type) + button = self.clayout.group.buttons()[button_index] + button.setChecked(True) + self.derivation_path_edit.setText(account["derivation_path"]) + + button.clicked.connect(lambda: Bip39RecoveryDialog(self, get_account_xpub, on_account_select)) + self.layout().addWidget(button, alignment=Qt.AlignLeft) + self.layout().addWidget(QLabel(_("Or"))) + + self.c_values = [x[0] for x in choices] + c_titles = [x[1] for x in choices] + c_default_text = [x[2] for x in choices] + + def on_choice_click(clayout): + idx = clayout.selected_index() + self.derivation_path_edit.setText(c_default_text[idx]) + self.clayout = ChoicesLayout(message1, c_titles, on_choice_click, + checked_index=default_choice_idx) + if not hide_choices: + self.layout().addLayout(self.clayout.layout()) + + self.layout().addWidget(WWLabel(message2)) + + self.derivation_path_edit = QLineEdit() + 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) + + def validate(self): + self.valid = is_bip32_derivation(self.derivation_path_edit.text()) + + 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()) + + class WCMultisig(WizardComponent): def __init__(self, parent, wizard): WizardComponent.__init__(self, parent, wizard, title=_('Multi-Signature Wallet')) diff --git a/electrum/gui/qt/wizard/wizard.py b/electrum/gui/qt/wizard/wizard.py index c8f3663c8..77daa0e50 100644 --- a/electrum/gui/qt/wizard/wizard.py +++ b/electrum/gui/qt/wizard/wizard.py @@ -96,12 +96,12 @@ class QEAbstractWizard(QDialog): self._logger.error(f'not a class: {comp!r}') raise e page.wizard_data = wdata - page.config = self.config page.updated.connect(self.on_page_updated) self._logger.debug(f'{page!r}') # add to stack and update wizard self.main_widget.setCurrentIndex(self.main_widget.addWidget(page)) + page.on_ready() page.apply() self.update() @@ -208,6 +208,11 @@ class WizardComponent(QWidget): @abstractmethod def apply(self): + # called to apply UI component values to wizard_data + pass + + def on_ready(self): + # called when wizard_data is available pass @pyqtSlot()