From 5246f3d510b33f05c89c8828431ba99d89bc1082 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 7 Nov 2022 18:08:00 +0100 Subject: [PATCH] qml: refactor is_last checks to mostly lambdas, add multisig flow for 1st cosigner keystore, add initial flow and view placeholders for additional cosigners. --- .../qml/components/wizard/WCCosignerKey.qml | 19 ++++++ .../components/wizard/WCCosignerKeystore.qml | 39 +++++++++++ .../qml/components/wizard/WCCosignerSeed.qml | 19 ++++++ .../gui/qml/components/wizard/WCMultisig.qml | 2 + .../components/wizard/WCShowMasterPubkey.qml | 65 +++++++++++++++++++ electrum/gui/qml/qebitcoin.py | 13 ++++ electrum/gui/qml/qewizard.py | 6 +- electrum/plugins/trustedcoin/qml.py | 4 +- electrum/wizard.py | 50 ++++++++++---- 9 files changed, 201 insertions(+), 16 deletions(-) create mode 100644 electrum/gui/qml/components/wizard/WCCosignerKey.qml create mode 100644 electrum/gui/qml/components/wizard/WCCosignerKeystore.qml create mode 100644 electrum/gui/qml/components/wizard/WCCosignerSeed.qml create mode 100644 electrum/gui/qml/components/wizard/WCShowMasterPubkey.qml diff --git a/electrum/gui/qml/components/wizard/WCCosignerKey.qml b/electrum/gui/qml/components/wizard/WCCosignerKey.qml new file mode 100644 index 000000000..b20b29dce --- /dev/null +++ b/electrum/gui/qml/components/wizard/WCCosignerKey.qml @@ -0,0 +1,19 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.1 + +import org.electrum 1.0 + +import "../controls" + +WizardComponent { + id: root + + valid: false + + ColumnLayout { + Label { + text: qsTr('TODO: Cosigner key entry') + } + } +} diff --git a/electrum/gui/qml/components/wizard/WCCosignerKeystore.qml b/electrum/gui/qml/components/wizard/WCCosignerKeystore.qml new file mode 100644 index 000000000..0ca30c297 --- /dev/null +++ b/electrum/gui/qml/components/wizard/WCCosignerKeystore.qml @@ -0,0 +1,39 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.1 + +import org.electrum 1.0 + +import "../controls" + +WizardComponent { + id: root + + valid: keystoregroup.checkedButton !== null + + function apply() { + wizard_data['cosigner_keystore_type'] = keystoregroup.checkedButton.keystoretype + } + + ButtonGroup { + id: keystoregroup + } + + ColumnLayout { + Label { + text: qsTr('Add a cosigner to your multi-sig wallet') + } + RadioButton { + ButtonGroup.group: keystoregroup + property string keystoretype: 'key' + checked: true + text: qsTr('Cosigner key') + } + RadioButton { + ButtonGroup.group: keystoregroup + property string keystoretype: 'seed' + text: qsTr('Cosigner seed') + } + } +} + diff --git a/electrum/gui/qml/components/wizard/WCCosignerSeed.qml b/electrum/gui/qml/components/wizard/WCCosignerSeed.qml new file mode 100644 index 000000000..b4c2ac240 --- /dev/null +++ b/electrum/gui/qml/components/wizard/WCCosignerSeed.qml @@ -0,0 +1,19 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.1 + +import org.electrum 1.0 + +import "../controls" + +WizardComponent { + id: root + + valid: false + + ColumnLayout { + Label { + text: qsTr('TODO: Cosigner seed entry') + } + } +} diff --git a/electrum/gui/qml/components/wizard/WCMultisig.qml b/electrum/gui/qml/components/wizard/WCMultisig.qml index 204ab7f62..6d08c7848 100644 --- a/electrum/gui/qml/components/wizard/WCMultisig.qml +++ b/electrum/gui/qml/components/wizard/WCMultisig.qml @@ -24,8 +24,10 @@ WizardComponent { } function apply() { + wizard_data['multisig'] = true wizard_data['multisig_participants'] = participants wizard_data['multisig_signatures'] = signatures + wizard_data['multisig_cosigner_data'] = {} } ColumnLayout { diff --git a/electrum/gui/qml/components/wizard/WCShowMasterPubkey.qml b/electrum/gui/qml/components/wizard/WCShowMasterPubkey.qml new file mode 100644 index 000000000..2deed2e24 --- /dev/null +++ b/electrum/gui/qml/components/wizard/WCShowMasterPubkey.qml @@ -0,0 +1,65 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.1 + +import org.electrum 1.0 + +import "../controls" + +WizardComponent { + valid: true + + property string masterPubkey: '' + + ColumnLayout { + width: parent.width + + Label { + text: qsTr('Here is your master public key. Please share it with your cosigners') + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + TextHighlightPane { + Layout.fillWidth: true + padding: 0 + leftPadding: constants.paddingSmall + + RowLayout { + width: parent.width + Label { + Layout.fillWidth: true + text: masterPubkey + font.pixelSize: constants.fontSizeMedium + font.family: FixedFont + wrapMode: Text.Wrap + } + ToolButton { + icon.source: '../../../icons/share.png' + icon.color: 'transparent' + onClicked: { + var dialog = app.genericShareDialog.createObject(app, + { title: qsTr('Master public key'), text: masterPubkey } + ) + dialog.open() + } + } + } + } + } + + Bitcoin { + id: bitcoin + } + + onReadyChanged: { + if (!ready) + return + + if (wizard_data['seed_variant'] == 'electrum') { + masterPubkey = bitcoin.getMultisigMasterPubkey(wizard_data['seed_variant'], wizard_data['seed'], wizard_data['seed_extra_words']) + } else { + masterPubkey = bitcoin.getMultisigMasterPubkey(wizard_data['seed_variant'], wizard_data['seed'], wizard_data['seed_extra_words'], wizard_data['derivation_path']) + } + } +} diff --git a/electrum/gui/qml/qebitcoin.py b/electrum/gui/qml/qebitcoin.py index 1d2cf6c5b..51f496aa2 100644 --- a/electrum/gui/qml/qebitcoin.py +++ b/electrum/gui/qml/qebitcoin.py @@ -169,3 +169,16 @@ class QEBitcoin(QObject): def isPrivateKeyList(self, csv: str): return keystore.is_private_key_list(csv) + @pyqtSlot(str, str, str, result=str) + @pyqtSlot(str, str, str, str, result=str) + def getMultisigMasterPubkey(self, seed_variant, seed, seed_extra_words, derivation_path = None): + if seed_variant == 'electrum': + k = keystore.from_seed(seed, seed_extra_words, True) + elif seed_variant == 'bip39': + root_seed = keystore.bip39_to_seed(seed, seed_extra_words) + derivation = normalize_bip32_derivation(derivation_path) + k = keystore.from_bip43_rootseed(root_seed, derivation, xtype='p2wsh') + else: + raise Exception(f'Unsupported seed variant {seed_variant}') + + return k.get_master_public_key() diff --git a/electrum/gui/qml/qewizard.py b/electrum/gui/qml/qewizard.py index 90392f670..a1017ecd9 100644 --- a/electrum/gui/qml/qewizard.py +++ b/electrum/gui/qml/qewizard.py @@ -60,6 +60,10 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard): 'bip39_refine': { 'gui': 'WCBIP39Refine' }, 'have_master_key': { 'gui': 'WCHaveMasterKey' }, 'multisig': { 'gui': 'WCMultisig' }, + 'multisig_show_masterpubkey': { 'gui': 'WCShowMasterPubkey' }, + 'multisig_cosigner_keystore': { 'gui': 'WCCosignerKeystore' }, + 'multisig_cosigner_key': { 'gui': 'WCCosignerKey' }, + 'multisig_cosigner_seed': { 'gui': 'WCCosignerSeed' }, 'imported': { 'gui': 'WCImport' }, 'wallet_password': { 'gui': 'WCWalletPassword' } }) @@ -74,7 +78,7 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard): self._path = path self.pathChanged.emit() - def last_if_single_password(self, *args): + def is_single_password(self): return self._daemon.singlePasswordEnabled @pyqtSlot('QJSValue', bool, str) diff --git a/electrum/plugins/trustedcoin/qml.py b/electrum/plugins/trustedcoin/qml.py index da6c65324..ea51db35c 100644 --- a/electrum/plugins/trustedcoin/qml.py +++ b/electrum/plugins/trustedcoin/qml.py @@ -329,7 +329,7 @@ class Plugin(TrustedCoinPlugin): 'next': lambda d: 'trustedcoin_tos_email' if d['trustedcoin_keepordisable'] != 'disable' else 'wallet_password', 'accept': self.recovery_disable, - 'last': lambda v,d: wizard.last_if_single_password() and d['trustedcoin_keepordisable'] == 'disable' + 'last': lambda v,d: wizard.is_single_password() and d['trustedcoin_keepordisable'] == 'disable' }, 'trustedcoin_tos_email': { 'gui': '../../../../plugins/trustedcoin/qml/Terms', @@ -339,7 +339,7 @@ class Plugin(TrustedCoinPlugin): 'gui': '../../../../plugins/trustedcoin/qml/ShowConfirmOTP', 'accept': self.on_accept_otp_secret, 'next': 'wallet_password', - 'last': wizard.last_if_single_password + 'last': lambda v,d: wizard.is_single_password() } } wizard.navmap_merge(views) diff --git a/electrum/wizard.py b/electrum/wizard.py index e72327400..73960efa9 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -148,27 +148,39 @@ class NewWalletWizard(AbstractWizard): 'next': 'confirm_seed' }, 'confirm_seed': { - 'next': 'wallet_password', - 'last': self.last_if_single_password + 'next': lambda d: 'wallet_password' if not self.is_multisig(d) else 'multisig_show_masterpubkey', + 'last': lambda v,d: self.is_single_password() }, 'have_seed': { 'next': self.on_have_seed, - 'last': self.last_if_single_password_and_not_bip39 + 'last': lambda v,d: self.is_single_password() and not self.is_bip39_seed(d) and not self.is_multisig(d) }, 'bip39_refine': { - 'next': 'wallet_password', - 'last': self.last_if_single_password + 'next': lambda d: 'wallet_password' if not self.is_multisig(d) else 'multisig_show_masterpubkey', + 'last': lambda v,d: self.is_single_password() }, 'have_master_key': { - 'next': 'wallet_password', - 'last': self.last_if_single_password + 'next': lambda d: 'wallet_password' if not self.is_multisig(d) else 'multisig_show_masterpubkey', + 'last': lambda v,d: self.is_single_password() }, 'multisig': { - 'next': 'first_cosigner' + 'next': 'keystore_type' + }, + 'multisig_show_masterpubkey': { + 'next': 'multisig_cosigner_keystore' + }, + 'multisig_cosigner_keystore': { + 'next': self.on_cosigner_keystore_type + }, + 'multisig_cosigner_key': { + 'next': 'multisig_cosigner_keystore' # TODO + }, + 'multisig_cosigner_seed': { + 'next': 'multisig_cosigner_keystore' # TODO }, 'imported': { 'next': 'wallet_password', - 'last': self.last_if_single_password + 'last': lambda v,d: self.is_single_password() }, 'wallet_password': { 'last': True @@ -181,11 +193,14 @@ class NewWalletWizard(AbstractWizard): self._current = WizardViewState('wallet_name', initial_data, {}) return self._current - def last_if_single_password(self, view, wizard_data): + def is_single_password(self): raise NotImplementedError() - def last_if_single_password_and_not_bip39(self, view, wizard_data): - return self.last_if_single_password(view, wizard_data) and not wizard_data['seed_variant'] == 'bip39' + def is_bip39_seed(self, wizard_data): + return wizard_data['seed_variant'] == 'bip39' + + def is_multisig(self, wizard_data): + return 'multisig' in wizard_data and wizard_data['multisig'] == True def on_wallet_type(self, wizard_data): t = wizard_data['wallet_type'] @@ -205,11 +220,20 @@ class NewWalletWizard(AbstractWizard): }.get(t) def on_have_seed(self, wizard_data): - if (wizard_data['seed_type'] == 'bip39'): + if self.is_bip39_seed(wizard_data): return 'bip39_refine' + elif self.is_multisig(wizard_data): + return 'multisig_show_masterpubkey' else: return 'wallet_password' + def on_cosigner_keystore_type(self, wizard_data): + t = wizard_data['cosigner_keystore_type'] + return { + 'key': 'multisig_cosigner_key', + 'seed': 'multisig_cosigner_seed' + }.get(t) + def finished(self, wizard_data): self._logger.debug('finished') # override