diff --git a/electrum/gui/qml/components/wizard/WCBIP39Refine.qml b/electrum/gui/qml/components/wizard/WCBIP39Refine.qml index 16b8971ed..5ee048063 100644 --- a/electrum/gui/qml/components/wizard/WCBIP39Refine.qml +++ b/electrum/gui/qml/components/wizard/WCBIP39Refine.qml @@ -9,11 +9,18 @@ import "../controls" WizardComponent { valid: false - property bool isMultisig + property bool isMultisig: false + property int cosigner: 0 + property int participants: 0 function apply() { - wizard_data['script_type'] = scripttypegroup.checkedButton.scripttype - wizard_data['derivation_path'] = derivationpathtext.text + if (cosigner) { + wizard_data['multisig_cosigner_data'][cosigner.toString()]['script_type'] = scripttypegroup.checkedButton.scripttype + wizard_data['multisig_cosigner_data'][cosigner.toString()]['derivation_path'] = derivationpathtext.text + } else { + wizard_data['script_type'] = scripttypegroup.checkedButton.scripttype + wizard_data['derivation_path'] = derivationpathtext.text + } } function getScriptTypePurposeDict() { @@ -37,7 +44,7 @@ WizardComponent { var p = isMultisig ? getMultisigScriptTypePurposeDict() : getScriptTypePurposeDict() if (!scripttypegroup.checkedButton.scripttype in p) return - if (!bitcoin.verify_derivation_path(derivationpathtext.text)) + if (!bitcoin.verifyDerivationPath(derivationpathtext.text)) return valid = true } @@ -76,13 +83,20 @@ WizardComponent { id: mainLayout width: parent.width - Label { text: qsTr('Script type and Derivation path') } + Label { + text: qsTr('Script type and Derivation path') + } Button { text: qsTr('Detect Existing Accounts') enabled: false visible: !isMultisig } - Label { text: qsTr('Choose the type of addresses in your wallet.') } + + Label { + text: qsTr('Choose the type of addresses in your wallet.') + } + + // standard RadioButton { ButtonGroup.group: scripttypegroup property string scripttype: 'p2pkh' @@ -102,6 +116,8 @@ WizardComponent { text: qsTr('native segwit (p2wpkh)') visible: !isMultisig } + + // multisig RadioButton { ButtonGroup.group: scripttypegroup property string scripttype: 'p2sh' @@ -121,11 +137,13 @@ WizardComponent { text: qsTr('native segwit multisig (p2wsh)') visible: isMultisig } + InfoTextArea { Layout.preferredWidth: parent.width text: qsTr('You can override the suggested derivation path.') + ' ' + qsTr('If you are not sure what this is, leave this field unchanged.') } + TextField { id: derivationpathtext Layout.fillWidth: true @@ -140,7 +158,12 @@ WizardComponent { } Component.onCompleted: { - isMultisig = 'multisig' in wizard_data && wizard_data['multisig'] == true + isMultisig = wizard_data['wallet_type'] == 'multisig' + if (isMultisig) { + participants = wizard_data['multisig_participants'] + if ('multisig_current_cosigner' in wizard_data) + cosigner = wizard_data['multisig_current_cosigner'] + } } } diff --git a/electrum/gui/qml/components/wizard/WCCosignerKey.qml b/electrum/gui/qml/components/wizard/WCCosignerKey.qml deleted file mode 100644 index b20b29dce..000000000 --- a/electrum/gui/qml/components/wizard/WCCosignerKey.qml +++ /dev/null @@ -1,19 +0,0 @@ -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 index 0c6163943..625f810ab 100644 --- a/electrum/gui/qml/components/wizard/WCCosignerKeystore.qml +++ b/electrum/gui/qml/components/wizard/WCCosignerKeystore.qml @@ -17,6 +17,7 @@ WizardComponent { function apply() { wizard_data['cosigner_keystore_type'] = keystoregroup.checkedButton.keystoretype wizard_data['multisig_current_cosigner'] = cosigner + wizard_data['multisig_cosigner_data'][cosigner.toString()] = {} } ButtonGroup { diff --git a/electrum/gui/qml/components/wizard/WCCosignerSeed.qml b/electrum/gui/qml/components/wizard/WCCosignerSeed.qml deleted file mode 100644 index a3deb24b4..000000000 --- a/electrum/gui/qml/components/wizard/WCCosignerSeed.qml +++ /dev/null @@ -1,30 +0,0 @@ -import QtQuick 2.6 -import QtQuick.Layouts 1.0 -import QtQuick.Controls 2.1 - -import org.electrum 1.0 - -import "../controls" - -WCHaveSeed { - id: root - - headingtext: qsTr('Cosigner #%1 of %2').arg(cosigner).arg(participants) - - property int cosigner: 0 - property int participants: 0 - - function apply() { - console.log('apply fn called') - wizard_data['cosigner_seed'] = seed - wizard_data['cosigner_seed_variant'] = seed_variant - wizard_data['cosigner_seed_type'] = seed_type - wizard_data['cosigner_seed_extend'] = seed_extend - wizard_data['cosigner_seed_extra_words'] = seed_extra_words - } - - Component.onCompleted: { - participants = wizard_data['multisig_participants'] - cosigner = wizard_data['multisig_current_cosigner'] - } -} diff --git a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml index f97abd7f1..62947e56b 100644 --- a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml +++ b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml @@ -11,18 +11,32 @@ WizardComponent { valid: false + property int cosigner: 0 + property int participants: 0 + function apply() { - wizard_data['master_key'] = masterkey_ta.text + if (cosigner) { + wizard_data['multisig_cosigner_data'][cosigner.toString()]['master_key'] = masterkey_ta.text + } else { + wizard_data['master_key'] = masterkey_ta.text + } } function verifyMasterKey(key) { - return valid = bitcoin.verify_master_key(key) + return valid = bitcoin.verifyMasterKey(key.trim(), wizard_data['wallet_type']) } ColumnLayout { width: parent.width - Label { text: qsTr('Create keystore from a master key') } + Label { + text: qsTr('Cosigner #%1 of %2').arg(cosigner).arg(participants) + visible: cosigner + } + + Label { + text: qsTr('Create keystore from a master key') + } RowLayout { TextArea { @@ -59,6 +73,18 @@ WizardComponent { } } } + + TextArea { + id: validationtext + text: bitcoin.validationMessage + visible: bitcoin.validationMessage + Layout.fillWidth: true + readOnly: true + wrapMode: TextInput.WordWrap + background: Rectangle { + color: 'transparent' + } + } } Component { @@ -83,4 +109,12 @@ WizardComponent { Bitcoin { id: bitcoin } + + Component.onCompleted: { + if (wizard_data['wallet_type'] == 'multisig') { + if ('multisig_current_cosigner' in wizard_data) + cosigner = wizard_data['multisig_current_cosigner'] + participants = wizard_data['multisig_participants'] + } + } } diff --git a/electrum/gui/qml/components/wizard/WCHaveSeed.qml b/electrum/gui/qml/components/wizard/WCHaveSeed.qml index 91acbf8aa..6710d3c7a 100644 --- a/electrum/gui/qml/components/wizard/WCHaveSeed.qml +++ b/electrum/gui/qml/components/wizard/WCHaveSeed.qml @@ -9,26 +9,27 @@ import "../controls" WizardComponent { id: root + valid: false property bool is2fa: false - - property string headingtext - - // expose for WCCosignerSeed 'subclass' - property alias seed: seedtext.text - property alias seed_variant: seed_variant_cb.currentValue - property alias seed_type: bitcoin.seed_type - property alias seed_extend: extendcb.checked - property string seed_extra_words: extendcb.checked ? customwordstext.text : '' + property int cosigner: 0 + property int participants: 0 function apply() { - console.log('apply fn called (WCHaveSeed)') - wizard_data['seed'] = seedtext.text - wizard_data['seed_variant'] = seed_variant_cb.currentValue - wizard_data['seed_type'] = bitcoin.seed_type - wizard_data['seed_extend'] = extendcb.checked - wizard_data['seed_extra_words'] = extendcb.checked ? customwordstext.text : '' + if (cosigner) { + wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed'] = seedtext.text + wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_variant'] = seed_variant_cb.currentValue + wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_type'] = bitcoin.seed_type + wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_extend'] = extendcb.checked + wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_extra_words'] = extendcb.checked ? customwordstext.text : '' + } else { + wizard_data['seed'] = seedtext.text + wizard_data['seed_variant'] = seed_variant_cb.currentValue + wizard_data['seed_type'] = bitcoin.seed_type + wizard_data['seed_extend'] = extendcb.checked + wizard_data['seed_extra_words'] = extendcb.checked ? customwordstext.text : '' + } } function setSeedTypeHelpText() { @@ -69,8 +70,8 @@ WizardComponent { Label { Layout.columnSpan: 2 - visible: headingtext - text: headingtext + text: qsTr('Cosigner #%1 of %2').arg(cosigner).arg(participants) + visible: cosigner } Label { @@ -129,7 +130,8 @@ WizardComponent { } TextArea { id: validationtext - visible: text != '' + text: bitcoin.validationMessage + visible: bitcoin.validationMessage Layout.fillWidth: true readOnly: true wrapMode: TextInput.WordWrap @@ -157,7 +159,6 @@ WizardComponent { id: bitcoin onSeedTypeChanged: contentText.text = bitcoin.seed_type onSeedValidChanged: root.valid = bitcoin.seed_valid - onValidationMessageChanged: validationtext.text = bitcoin.validation_message } Timer { @@ -168,8 +169,13 @@ WizardComponent { } Component.onCompleted: { - if (wizard_data['wallet_type'] == '2fa') - root.is2fa = true + if (wizard_data['wallet_type'] == '2fa') { + is2fa = true + } else if (wizard_data['wallet_type'] == 'multisig') { + participants = wizard_data['multisig_participants'] + if ('multisig_current_cosigner' in wizard_data) + cosigner = wizard_data['multisig_current_cosigner'] + } setSeedTypeHelpText() } diff --git a/electrum/gui/qml/components/wizard/WCMultisig.qml b/electrum/gui/qml/components/wizard/WCMultisig.qml index c6aa1a212..bc4fa7426 100644 --- a/electrum/gui/qml/components/wizard/WCMultisig.qml +++ b/electrum/gui/qml/components/wizard/WCMultisig.qml @@ -24,7 +24,6 @@ WizardComponent { } function apply() { - wizard_data['multisig'] = true wizard_data['multisig_participants'] = participants wizard_data['multisig_signatures'] = signatures wizard_data['multisig_cosigner_data'] = {} diff --git a/electrum/gui/qml/components/wizard/WizardComponent.qml b/electrum/gui/qml/components/wizard/WizardComponent.qml index 1a0d6e9b7..9e29251c5 100644 --- a/electrum/gui/qml/components/wizard/WizardComponent.qml +++ b/electrum/gui/qml/components/wizard/WizardComponent.qml @@ -14,14 +14,21 @@ Item { apply() } + // override this in descendants to put data from the view in wizard_data function apply() { } + function checkIsLast() { apply() last = wizard.wiz.isLast(wizard_data) } Component.onCompleted: { - checkIsLast() + // NOTE: Use Qt.callLater to execute checkIsLast(), and by extension apply(), + // otherwise Component.onCompleted handler in descendants is processed + // _after_ apply() is called, which may lead to setting the wrong + // wizard_data keys if apply() depends on variables set in descendant + // Component.onCompleted handler. + Qt.callLater(checkIsLast) } } diff --git a/electrum/gui/qml/qebitcoin.py b/electrum/gui/qml/qebitcoin.py index 0beed351e..d327b1081 100644 --- a/electrum/gui/qml/qebitcoin.py +++ b/electrum/gui/qml/qebitcoin.py @@ -31,7 +31,7 @@ class QEBitcoin(QObject): seedType = '' validationMessageChanged = pyqtSignal() - validationMessage = '' + _validationMessage = '' @pyqtProperty('QString', notify=generatedSeedChanged) def generated_seed(self): @@ -46,13 +46,13 @@ class QEBitcoin(QObject): return self.seedType @pyqtProperty('QString', notify=validationMessageChanged) - def validation_message(self): - return self.validationMessage + def validationMessage(self): + return self._validationMessage - @validation_message.setter - def validation_message(self, msg): - if self.validationMessage != msg: - self.validationMessage = msg + @validationMessage.setter + def validationMessage(self, msg): + if self._validationMessage != msg: + self._validationMessage = msg self.validationMessageChanged.emit() @pyqtSlot() @@ -115,28 +115,34 @@ class QEBitcoin(QObject): self._logger.debug('seed verified: ' + str(seed_valid)) - @pyqtSlot(str, result=bool) @pyqtSlot(str, str, result=bool) - def verify_master_key(self, key, wallet_type='standard'): + def verifyMasterKey(self, key, wallet_type='standard'): self.validationMessage = '' if not keystore.is_master_key(key): self.validationMessage = _('Not a master key') return False + k = keystore.from_master_key(key) + has_xpub = isinstance(k, keystore.Xpub) + assert has_xpub + t1 = xpub_type(k.xpub) + if wallet_type == 'standard': - # validation message? - k = keystore.from_master_key(key) - has_xpub = isinstance(k, keystore.Xpub) - assert has_xpub - t1 = xpub_type(k.xpub) if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: self.validationMessage = '%s: %s' % (_('Wrong key type'), t1) return False return True - return False + elif wallet_type == 'multisig': + if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']: + self.validationMessage = '%s: %s' % (_('Wrong key type'), t1) + return False + # TODO: check against other cosigner xpubs + return True + + raise Exception(f'Unsupported wallet type: {wallet_type}') @pyqtSlot(str, result=bool) - def verify_derivation_path(self, path): + def verifyDerivationPath(self, path): return is_bip32_derivation(path) @pyqtSlot(str, result='QVariantMap') diff --git a/electrum/gui/qml/qewizard.py b/electrum/gui/qml/qewizard.py index 8c7514bf8..bd0f26661 100644 --- a/electrum/gui/qml/qewizard.py +++ b/electrum/gui/qml/qewizard.py @@ -62,13 +62,9 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard): 'multisig': { 'gui': 'WCMultisig' }, 'multisig_show_masterpubkey': { 'gui': 'WCShowMasterPubkey' }, 'multisig_cosigner_keystore': { 'gui': 'WCCosignerKeystore' }, - 'multisig_cosigner_key': { 'gui': 'WCCosignerKey' }, - 'multisig_cosigner_seed': { 'gui': 'WCCosignerSeed', - 'accept': self.accept_cosigner_seed - }, - 'multisig_cosigner_bip39_refine': { 'gui': 'WCBIP39Refine', - 'accept': self.accept_cosigner_bip39refine - }, + 'multisig_cosigner_key': { 'gui': 'WCHaveMasterKey' }, + 'multisig_cosigner_seed': { 'gui': 'WCHaveSeed' }, + 'multisig_cosigner_bip39_refine': { 'gui': 'WCBIP39Refine' }, 'imported': { 'gui': 'WCImport' }, 'wallet_password': { 'gui': 'WCWalletPassword' } }) @@ -86,21 +82,6 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard): def is_single_password(self): return self._daemon.singlePasswordEnabled - def accept_cosigner_seed(self, wizard_data): - self._logger.debug('accept_cosigner_seed') - cosigner = wizard_data['multisig_current_cosigner'] if 'multisig_current_cosigner' in wizard_data else 2 - wizard_data['multisig_cosigner_data'][str(cosigner)] = { - 'seed': wizard_data['cosigner_seed'], - 'seed_variant': wizard_data['cosigner_seed_variant'], - 'seed_type': wizard_data['cosigner_seed_type'], - 'seed_extend': wizard_data['cosigner_seed_extend'], - 'seed_extra_words': wizard_data['cosigner_seed_extra_words'] - } - - def accept_cosigner_bip39refine(self, wizard_data): - pass # TODO - - @pyqtSlot('QJSValue', bool, str) def createStorage(self, js_data, single_password_enabled, single_password): self._logger.info('Creating wallet from wizard data') diff --git a/electrum/wizard.py b/electrum/wizard.py index afdb8651a..a58a209d4 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -169,15 +169,19 @@ class NewWalletWizard(AbstractWizard): 'multisig_show_masterpubkey': { 'next': 'multisig_cosigner_keystore' }, - 'multisig_cosigner_keystore': { + 'multisig_cosigner_keystore': { # this view should set 'multisig_current_cosigner' 'next': self.on_cosigner_keystore_type }, 'multisig_cosigner_key': { - 'next': lambda d: 'multisig_cosigner_keystore' if self.has_all_cosigner_data(d) else 'wallet_password', + 'next': lambda d: 'wallet_password' if self.has_all_cosigner_data(d) else 'multisig_cosigner_keystore', 'last': lambda v,d: self.is_single_password() and self.has_all_cosigner_data(d) }, 'multisig_cosigner_seed': { - 'next': lambda d: 'multisig_cosigner_keystore' if self.has_all_cosigner_data(d) else 'wallet_password', + 'next': self.on_have_cosigner_seed, + 'last': lambda v,d: self.is_single_password() and self.has_all_cosigner_data(d) + }, + 'multisig_cosigner_bip39_refine': { + 'next': lambda d: 'wallet_password' if self.has_all_cosigner_data(d) else 'multisig_cosigner_keystore', 'last': lambda v,d: self.is_single_password() and self.has_all_cosigner_data(d) }, 'imported': { @@ -202,7 +206,7 @@ class NewWalletWizard(AbstractWizard): return wizard_data['seed_variant'] == 'bip39' def is_multisig(self, wizard_data): - return 'multisig' in wizard_data and wizard_data['multisig'] is True + return wizard_data['wallet_type'] == 'multisig' def on_wallet_type(self, wizard_data): t = wizard_data['wallet_type'] @@ -236,8 +240,26 @@ class NewWalletWizard(AbstractWizard): 'seed': 'multisig_cosigner_seed' }.get(t) + def on_have_cosigner_seed(self, wizard_data): + current_cosigner_data = wizard_data['multisig_cosigner_data'][str(wizard_data['multisig_current_cosigner'])] + if self.has_all_cosigner_data(wizard_data): + return 'wallet_password' + elif current_cosigner_data['seed_type'] == 'bip39' and 'derivation_path' not in current_cosigner_data: + return 'multisig_cosigner_bip39_refine' + else: + return 'multisig_cosigner_keystore' + def has_all_cosigner_data(self, wizard_data): - return len(wizard_data['multisig_cosigner_data']) < (wizard_data['multisig_participants'] - 1) + # number of items in multisig_cosigner_data is less than participants? + if len(wizard_data['multisig_cosigner_data']) < (wizard_data['multisig_participants'] - 1): + 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 current_cosigner_data['seed_type'] == 'bip39' and 'derivation_path' not in current_cosigner_data: + return False + + return True def finished(self, wizard_data): self._logger.debug('finished')