Browse Source

qml: consolidate multisig support into existing pages WCHaveSeed, WCHaveMasterKey, WCBIP39Refine.

Have these put cosigner data directly in the correct wizard_data leafs instead of relying on
wizard accept handlers.
patch-4
Sander van Grieken 2 years ago
parent
commit
4c784ffe1f
  1. 37
      electrum/gui/qml/components/wizard/WCBIP39Refine.qml
  2. 19
      electrum/gui/qml/components/wizard/WCCosignerKey.qml
  3. 1
      electrum/gui/qml/components/wizard/WCCosignerKeystore.qml
  4. 30
      electrum/gui/qml/components/wizard/WCCosignerSeed.qml
  5. 40
      electrum/gui/qml/components/wizard/WCHaveMasterKey.qml
  6. 48
      electrum/gui/qml/components/wizard/WCHaveSeed.qml
  7. 1
      electrum/gui/qml/components/wizard/WCMultisig.qml
  8. 9
      electrum/gui/qml/components/wizard/WizardComponent.qml
  9. 38
      electrum/gui/qml/qebitcoin.py
  10. 25
      electrum/gui/qml/qewizard.py
  11. 32
      electrum/wizard.py

37
electrum/gui/qml/components/wizard/WCBIP39Refine.qml

@ -9,11 +9,18 @@ import "../controls"
WizardComponent { WizardComponent {
valid: false valid: false
property bool isMultisig property bool isMultisig: false
property int cosigner: 0
property int participants: 0
function apply() { function apply() {
wizard_data['script_type'] = scripttypegroup.checkedButton.scripttype if (cosigner) {
wizard_data['derivation_path'] = derivationpathtext.text 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() { function getScriptTypePurposeDict() {
@ -37,7 +44,7 @@ WizardComponent {
var p = isMultisig ? getMultisigScriptTypePurposeDict() : getScriptTypePurposeDict() var p = isMultisig ? getMultisigScriptTypePurposeDict() : getScriptTypePurposeDict()
if (!scripttypegroup.checkedButton.scripttype in p) if (!scripttypegroup.checkedButton.scripttype in p)
return return
if (!bitcoin.verify_derivation_path(derivationpathtext.text)) if (!bitcoin.verifyDerivationPath(derivationpathtext.text))
return return
valid = true valid = true
} }
@ -76,13 +83,20 @@ WizardComponent {
id: mainLayout id: mainLayout
width: parent.width width: parent.width
Label { text: qsTr('Script type and Derivation path') } Label {
text: qsTr('Script type and Derivation path')
}
Button { Button {
text: qsTr('Detect Existing Accounts') text: qsTr('Detect Existing Accounts')
enabled: false enabled: false
visible: !isMultisig 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 { RadioButton {
ButtonGroup.group: scripttypegroup ButtonGroup.group: scripttypegroup
property string scripttype: 'p2pkh' property string scripttype: 'p2pkh'
@ -102,6 +116,8 @@ WizardComponent {
text: qsTr('native segwit (p2wpkh)') text: qsTr('native segwit (p2wpkh)')
visible: !isMultisig visible: !isMultisig
} }
// multisig
RadioButton { RadioButton {
ButtonGroup.group: scripttypegroup ButtonGroup.group: scripttypegroup
property string scripttype: 'p2sh' property string scripttype: 'p2sh'
@ -121,11 +137,13 @@ WizardComponent {
text: qsTr('native segwit multisig (p2wsh)') text: qsTr('native segwit multisig (p2wsh)')
visible: isMultisig visible: isMultisig
} }
InfoTextArea { InfoTextArea {
Layout.preferredWidth: parent.width Layout.preferredWidth: parent.width
text: qsTr('You can override the suggested derivation path.') + ' ' + text: qsTr('You can override the suggested derivation path.') + ' ' +
qsTr('If you are not sure what this is, leave this field unchanged.') qsTr('If you are not sure what this is, leave this field unchanged.')
} }
TextField { TextField {
id: derivationpathtext id: derivationpathtext
Layout.fillWidth: true Layout.fillWidth: true
@ -140,7 +158,12 @@ WizardComponent {
} }
Component.onCompleted: { 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']
}
} }
} }

19
electrum/gui/qml/components/wizard/WCCosignerKey.qml

@ -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')
}
}
}

1
electrum/gui/qml/components/wizard/WCCosignerKeystore.qml

@ -17,6 +17,7 @@ WizardComponent {
function apply() { function apply() {
wizard_data['cosigner_keystore_type'] = keystoregroup.checkedButton.keystoretype wizard_data['cosigner_keystore_type'] = keystoregroup.checkedButton.keystoretype
wizard_data['multisig_current_cosigner'] = cosigner wizard_data['multisig_current_cosigner'] = cosigner
wizard_data['multisig_cosigner_data'][cosigner.toString()] = {}
} }
ButtonGroup { ButtonGroup {

30
electrum/gui/qml/components/wizard/WCCosignerSeed.qml

@ -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']
}
}

40
electrum/gui/qml/components/wizard/WCHaveMasterKey.qml

@ -11,18 +11,32 @@ WizardComponent {
valid: false valid: false
property int cosigner: 0
property int participants: 0
function apply() { 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) { function verifyMasterKey(key) {
return valid = bitcoin.verify_master_key(key) return valid = bitcoin.verifyMasterKey(key.trim(), wizard_data['wallet_type'])
} }
ColumnLayout { ColumnLayout {
width: parent.width 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 { RowLayout {
TextArea { 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 { Component {
@ -83,4 +109,12 @@ WizardComponent {
Bitcoin { Bitcoin {
id: 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']
}
}
} }

48
electrum/gui/qml/components/wizard/WCHaveSeed.qml

@ -9,26 +9,27 @@ import "../controls"
WizardComponent { WizardComponent {
id: root id: root
valid: false valid: false
property bool is2fa: false property bool is2fa: false
property int cosigner: 0
property string headingtext property int participants: 0
// 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 : ''
function apply() { function apply() {
console.log('apply fn called (WCHaveSeed)') if (cosigner) {
wizard_data['seed'] = seedtext.text wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed'] = seedtext.text
wizard_data['seed_variant'] = seed_variant_cb.currentValue wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_variant'] = seed_variant_cb.currentValue
wizard_data['seed_type'] = bitcoin.seed_type wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_type'] = bitcoin.seed_type
wizard_data['seed_extend'] = extendcb.checked wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_extend'] = extendcb.checked
wizard_data['seed_extra_words'] = extendcb.checked ? customwordstext.text : '' 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() { function setSeedTypeHelpText() {
@ -69,8 +70,8 @@ WizardComponent {
Label { Label {
Layout.columnSpan: 2 Layout.columnSpan: 2
visible: headingtext text: qsTr('Cosigner #%1 of %2').arg(cosigner).arg(participants)
text: headingtext visible: cosigner
} }
Label { Label {
@ -129,7 +130,8 @@ WizardComponent {
} }
TextArea { TextArea {
id: validationtext id: validationtext
visible: text != '' text: bitcoin.validationMessage
visible: bitcoin.validationMessage
Layout.fillWidth: true Layout.fillWidth: true
readOnly: true readOnly: true
wrapMode: TextInput.WordWrap wrapMode: TextInput.WordWrap
@ -157,7 +159,6 @@ WizardComponent {
id: bitcoin id: bitcoin
onSeedTypeChanged: contentText.text = bitcoin.seed_type onSeedTypeChanged: contentText.text = bitcoin.seed_type
onSeedValidChanged: root.valid = bitcoin.seed_valid onSeedValidChanged: root.valid = bitcoin.seed_valid
onValidationMessageChanged: validationtext.text = bitcoin.validation_message
} }
Timer { Timer {
@ -168,8 +169,13 @@ WizardComponent {
} }
Component.onCompleted: { Component.onCompleted: {
if (wizard_data['wallet_type'] == '2fa') if (wizard_data['wallet_type'] == '2fa') {
root.is2fa = true 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() setSeedTypeHelpText()
} }

1
electrum/gui/qml/components/wizard/WCMultisig.qml

@ -24,7 +24,6 @@ WizardComponent {
} }
function apply() { function apply() {
wizard_data['multisig'] = true
wizard_data['multisig_participants'] = participants wizard_data['multisig_participants'] = participants
wizard_data['multisig_signatures'] = signatures wizard_data['multisig_signatures'] = signatures
wizard_data['multisig_cosigner_data'] = {} wizard_data['multisig_cosigner_data'] = {}

9
electrum/gui/qml/components/wizard/WizardComponent.qml

@ -14,14 +14,21 @@ Item {
apply() apply()
} }
// override this in descendants to put data from the view in wizard_data
function apply() { } function apply() { }
function checkIsLast() { function checkIsLast() {
apply() apply()
last = wizard.wiz.isLast(wizard_data) last = wizard.wiz.isLast(wizard_data)
} }
Component.onCompleted: { 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)
} }
} }

38
electrum/gui/qml/qebitcoin.py

@ -31,7 +31,7 @@ class QEBitcoin(QObject):
seedType = '' seedType = ''
validationMessageChanged = pyqtSignal() validationMessageChanged = pyqtSignal()
validationMessage = '' _validationMessage = ''
@pyqtProperty('QString', notify=generatedSeedChanged) @pyqtProperty('QString', notify=generatedSeedChanged)
def generated_seed(self): def generated_seed(self):
@ -46,13 +46,13 @@ class QEBitcoin(QObject):
return self.seedType return self.seedType
@pyqtProperty('QString', notify=validationMessageChanged) @pyqtProperty('QString', notify=validationMessageChanged)
def validation_message(self): def validationMessage(self):
return self.validationMessage return self._validationMessage
@validation_message.setter @validationMessage.setter
def validation_message(self, msg): def validationMessage(self, msg):
if self.validationMessage != msg: if self._validationMessage != msg:
self.validationMessage = msg self._validationMessage = msg
self.validationMessageChanged.emit() self.validationMessageChanged.emit()
@pyqtSlot() @pyqtSlot()
@ -115,28 +115,34 @@ class QEBitcoin(QObject):
self._logger.debug('seed verified: ' + str(seed_valid)) self._logger.debug('seed verified: ' + str(seed_valid))
@pyqtSlot(str, result=bool)
@pyqtSlot(str, 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 = '' self.validationMessage = ''
if not keystore.is_master_key(key): if not keystore.is_master_key(key):
self.validationMessage = _('Not a master key') self.validationMessage = _('Not a master key')
return False 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': 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']: if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']:
self.validationMessage = '%s: %s' % (_('Wrong key type'), t1) self.validationMessage = '%s: %s' % (_('Wrong key type'), t1)
return False return False
return True 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) @pyqtSlot(str, result=bool)
def verify_derivation_path(self, path): def verifyDerivationPath(self, path):
return is_bip32_derivation(path) return is_bip32_derivation(path)
@pyqtSlot(str, result='QVariantMap') @pyqtSlot(str, result='QVariantMap')

25
electrum/gui/qml/qewizard.py

@ -62,13 +62,9 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
'multisig': { 'gui': 'WCMultisig' }, 'multisig': { 'gui': 'WCMultisig' },
'multisig_show_masterpubkey': { 'gui': 'WCShowMasterPubkey' }, 'multisig_show_masterpubkey': { 'gui': 'WCShowMasterPubkey' },
'multisig_cosigner_keystore': { 'gui': 'WCCosignerKeystore' }, 'multisig_cosigner_keystore': { 'gui': 'WCCosignerKeystore' },
'multisig_cosigner_key': { 'gui': 'WCCosignerKey' }, 'multisig_cosigner_key': { 'gui': 'WCHaveMasterKey' },
'multisig_cosigner_seed': { 'gui': 'WCCosignerSeed', 'multisig_cosigner_seed': { 'gui': 'WCHaveSeed' },
'accept': self.accept_cosigner_seed 'multisig_cosigner_bip39_refine': { 'gui': 'WCBIP39Refine' },
},
'multisig_cosigner_bip39_refine': { 'gui': 'WCBIP39Refine',
'accept': self.accept_cosigner_bip39refine
},
'imported': { 'gui': 'WCImport' }, 'imported': { 'gui': 'WCImport' },
'wallet_password': { 'gui': 'WCWalletPassword' } 'wallet_password': { 'gui': 'WCWalletPassword' }
}) })
@ -86,21 +82,6 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
def is_single_password(self): def is_single_password(self):
return self._daemon.singlePasswordEnabled 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) @pyqtSlot('QJSValue', bool, str)
def createStorage(self, js_data, single_password_enabled, single_password): def createStorage(self, js_data, single_password_enabled, single_password):
self._logger.info('Creating wallet from wizard data') self._logger.info('Creating wallet from wizard data')

32
electrum/wizard.py

@ -169,15 +169,19 @@ class NewWalletWizard(AbstractWizard):
'multisig_show_masterpubkey': { 'multisig_show_masterpubkey': {
'next': 'multisig_cosigner_keystore' 'next': 'multisig_cosigner_keystore'
}, },
'multisig_cosigner_keystore': { 'multisig_cosigner_keystore': { # this view should set 'multisig_current_cosigner'
'next': self.on_cosigner_keystore_type 'next': self.on_cosigner_keystore_type
}, },
'multisig_cosigner_key': { '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) 'last': lambda v,d: self.is_single_password() and self.has_all_cosigner_data(d)
}, },
'multisig_cosigner_seed': { '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) 'last': lambda v,d: self.is_single_password() and self.has_all_cosigner_data(d)
}, },
'imported': { 'imported': {
@ -202,7 +206,7 @@ class NewWalletWizard(AbstractWizard):
return wizard_data['seed_variant'] == 'bip39' return wizard_data['seed_variant'] == 'bip39'
def is_multisig(self, wizard_data): 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): def on_wallet_type(self, wizard_data):
t = wizard_data['wallet_type'] t = wizard_data['wallet_type']
@ -236,8 +240,26 @@ class NewWalletWizard(AbstractWizard):
'seed': 'multisig_cosigner_seed' 'seed': 'multisig_cosigner_seed'
}.get(t) }.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): 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): def finished(self, wizard_data):
self._logger.debug('finished') self._logger.debug('finished')

Loading…
Cancel
Save