Browse Source

Merge pull request #7986 from accumulator/wizard

New approach to implement new wallet wizard
patch-4
ThomasV 2 years ago
committed by GitHub
parent
commit
d922db0bdf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      electrum/gui/qml/__init__.py
  2. 138
      electrum/gui/qml/components/NewWalletWizard.qml
  3. 92
      electrum/gui/qml/components/OtpDialog.qml
  4. 28
      electrum/gui/qml/components/ServerConnectWizard.qml
  5. 23
      electrum/gui/qml/components/WalletMainView.qml
  6. 14
      electrum/gui/qml/components/Wallets.qml
  7. 7
      electrum/gui/qml/components/wizard/WCAutoConnect.qml
  8. 7
      electrum/gui/qml/components/wizard/WCBIP39Refine.qml
  9. 10
      electrum/gui/qml/components/wizard/WCCreateSeed.qml
  10. 2
      electrum/gui/qml/components/wizard/WCHaveMasterKey.qml
  11. 43
      electrum/gui/qml/components/wizard/WCHaveSeed.qml
  12. 102
      electrum/gui/qml/components/wizard/WCImport.qml
  13. 2
      electrum/gui/qml/components/wizard/WCKeystoreType.qml
  14. 2
      electrum/gui/qml/components/wizard/WCProxyConfig.qml
  15. 2
      electrum/gui/qml/components/wizard/WCServerConfig.qml
  16. 2
      electrum/gui/qml/components/wizard/WCWalletName.qml
  17. 2
      electrum/gui/qml/components/wizard/WCWalletPassword.qml
  18. 11
      electrum/gui/qml/components/wizard/WCWalletType.qml
  19. 27
      electrum/gui/qml/components/wizard/Wizard.qml
  20. 16
      electrum/gui/qml/components/wizard/WizardComponent.qml
  21. 4
      electrum/gui/qml/qeaddresslistmodel.py
  22. 5
      electrum/gui/qml/qeapp.py
  23. 36
      electrum/gui/qml/qebitcoin.py
  24. 34
      electrum/gui/qml/qedaemon.py
  25. 47
      electrum/gui/qml/qewallet.py
  26. 70
      electrum/gui/qml/qewalletdb.py
  27. 115
      electrum/gui/qml/qewizard.py
  28. 5
      electrum/plugins/qml_test/__init__.py
  29. 15
      electrum/plugins/qml_test/qml.py
  30. 2
      electrum/plugins/trustedcoin/__init__.py
  31. 395
      electrum/plugins/trustedcoin/qml.py
  32. 38
      electrum/plugins/trustedcoin/qml/ChooseSeed.qml
  33. 27
      electrum/plugins/trustedcoin/qml/Disclaimer.qml
  34. 35
      electrum/plugins/trustedcoin/qml/KeepDisable.qml
  35. 137
      electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml
  36. 67
      electrum/plugins/trustedcoin/qml/Terms.qml
  37. 7
      electrum/plugins/trustedcoin/trustedcoin.py
  38. 319
      electrum/wizard.py

4
electrum/gui/qml/__init__.py

@ -31,10 +31,6 @@ if TYPE_CHECKING:
from .qeapp import ElectrumQmlApplication
class UncaughtException(Exception):
pass
class ElectrumGui(Logger):
@profiler

138
electrum/gui/qml/components/NewWalletWizard.qml

@ -13,142 +13,26 @@ Wizard {
signal walletCreated
property alias path: walletdb.path
// State transition functions. These functions are called when the 'Next'
// button is pressed. Depending on the data create the next page
// in the conversation.
function walletnameDone(d) {
console.log('wallet name done')
var page = _loadNextComponent(components.wallettype, wizard_data)
page.next.connect(function() {wallettypeDone()})
}
function wallettypeDone(d) {
console.log('wallet type done')
var page = _loadNextComponent(components.keystore, wizard_data)
page.next.connect(function() {keystoretypeDone()})
}
function keystoretypeDone(d) {
console.log('keystore type done')
var page
switch(wizard_data['keystore_type']) {
case 'createseed':
page = _loadNextComponent(components.createseed, wizard_data)
page.next.connect(function() {createseedDone()})
break
case 'haveseed':
page = _loadNextComponent(components.haveseed, wizard_data)
page.next.connect(function() {haveseedDone()})
if (wizard_data['seed_type'] != 'bip39' && Daemon.singlePasswordEnabled)
page.last = true
break
case 'masterkey':
page = _loadNextComponent(components.havemasterkey, wizard_data)
page.next.connect(function() {havemasterkeyDone()})
if (Daemon.singlePasswordEnabled)
page.last = true
break
}
}
function createseedDone(d) {
console.log('create seed done')
var page = _loadNextComponent(components.confirmseed, wizard_data)
if (Daemon.singlePasswordEnabled)
page.last = true
else
page.next.connect(function() {confirmseedDone()})
}
function confirmseedDone(d) {
console.log('confirm seed done')
var page = _loadNextComponent(components.walletpassword, wizard_data)
page.last = true
}
function haveseedDone(d) {
console.log('have seed done')
if (wizard_data['seed_type'] == 'bip39') {
var page = _loadNextComponent(components.bip39refine, wizard_data)
if (Daemon.singlePasswordEnabled)
page.last = true
else
page.next.connect(function() {bip39refineDone()})
} else {
var page = _loadNextComponent(components.walletpassword, wizard_data)
page.last = true
}
}
function bip39refineDone(d) {
console.log('bip39 refine done')
var page = _loadNextComponent(components.walletpassword, wizard_data)
page.last = true
}
function havemasterkeyDone(d) {
console.log('have master key done')
var page = _loadNextComponent(components.walletpassword, wizard_data)
page.last = true
}
Item {
id: components
property Component walletname: Component {
WCWalletName {}
}
property Component wallettype: Component {
WCWalletType {}
}
property Component keystore: Component {
WCKeystoreType {}
}
property Component createseed: Component {
WCCreateSeed {}
}
property Component haveseed: Component {
WCHaveSeed {}
}
property Component confirmseed: Component {
WCConfirmSeed {}
}
property Component bip39refine: Component {
WCBIP39Refine {}
}
property Component havemasterkey: Component {
WCHaveMasterKey {}
}
property Component walletpassword: Component {
WCWalletPassword {}
}
}
property string path
wiz: Daemon.newWalletWizard
Component.onCompleted: {
_setWizardData({})
var start = _loadNextComponent(components.walletname)
start.next.connect(function() {walletnameDone()})
var view = wiz.start_wizard()
_loadNextComponent(view)
}
onAccepted: {
console.log('Finished new wallet wizard')
walletdb.create_storage(wizard_data, Daemon.singlePasswordEnabled, Daemon.singlePassword)
wiz.createStorage(wizard_data, Daemon.singlePasswordEnabled, Daemon.singlePassword)
}
WalletDB {
id: walletdb
onCreateSuccess: walletwizard.walletCreated()
Connections {
target: wiz
function onCreateSuccess() {
walletwizard.path = wiz.path
walletwizard.walletCreated()
}
}
}

92
electrum/gui/qml/components/OtpDialog.qml

@ -0,0 +1,92 @@
import QtQuick 2.6
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.14
import QtQuick.Controls.Material 2.0
import org.electrum 1.0
import "controls"
ElDialog {
id: dialog
title: qsTr('Trustedcoin')
iconSource: '../../../icons/trustedcoin-status.png'
property string otpauth
property bool _waiting: false
property string _otpError
standardButtons: Dialog.Cancel
modal: true
parent: Overlay.overlay
Overlay.modal: Rectangle {
color: "#aa000000"
}
focus: true
ColumnLayout {
width: parent.width
Label {
text: qsTr('Enter Authenticator code')
font.pixelSize: constants.fontSizeLarge
Layout.alignment: Qt.AlignHCenter
}
TextField {
id: otpEdit
Layout.preferredWidth: fontMetrics.advanceWidth(passwordCharacter) * 6
Layout.alignment: Qt.AlignHCenter
font.pixelSize: constants.fontSizeXXLarge
maximumLength: 6
inputMethodHints: Qt.ImhSensitiveData | Qt.ImhDigitsOnly
echoMode: TextInput.Password
focus: true
onTextChanged: {
if (activeFocus)
_otpError = ''
}
}
Label {
opacity: _otpError ? 1 : 0
text: _otpError
color: constants.colorError
Layout.alignment: Qt.AlignHCenter
}
Button {
Layout.columnSpan: 2
Layout.alignment: Qt.AlignHCenter
text: qsTr('Submit')
enabled: !_waiting
onClicked: {
_waiting = true
Daemon.currentWallet.submitOtp(otpEdit.text)
}
}
}
Connections {
target: Daemon.currentWallet
function onOtpSuccess() {
_waiting = false
otpauth = otpEdit.text
dialog.accept()
}
function onOtpFailed(code, message) {
_waiting = false
_otpError = message
otpEdit.text = ''
}
}
FontMetrics {
id: fontMetrics
font: otpEdit.font
}
}

28
electrum/gui/qml/components/ServerConnectWizard.qml

@ -11,6 +11,8 @@ Wizard {
enter: null // disable transition
wiz: Daemon.serverConnectWizard
onAccepted: {
var proxy = wizard_data['proxy']
if (proxy && proxy['enabled'] == true) {
@ -25,29 +27,7 @@ Wizard {
}
Component.onCompleted: {
var start = _loadNextComponent(autoconnect)
start.next.connect(function() {autoconnectDone()})
}
function autoconnectDone() {
var page = _loadNextComponent(proxyconfig, wizard_data)
page.next.connect(function() {proxyconfigDone()})
}
function proxyconfigDone() {
var page = _loadNextComponent(serverconfig, wizard_data)
}
property Component autoconnect: Component {
WCAutoConnect {}
}
property Component proxyconfig: Component {
WCProxyConfig {}
var view = wiz.start_wizard()
_loadNextComponent(view)
}
property Component serverconfig: Component {
WCServerConfig {}
}
}

23
electrum/gui/qml/components/WalletMainView.qml

@ -256,6 +256,19 @@ Item {
}
}
Connections {
target: Daemon.currentWallet
function onOtpRequested() {
console.log('OTP requested')
var dialog = otpDialog.createObject(mainView)
dialog.accepted.connect(function() {
console.log('accepted ' + dialog.otpauth)
Daemon.currentWallet.finish_otp(dialog.otpauth)
})
dialog.open()
}
}
Component {
id: sendDialog
SendDialog {
@ -304,5 +317,15 @@ Item {
onClosed: destroy()
}
}
Component {
id: otpDialog
OtpDialog {
width: parent.width * 2/3
anchors.centerIn: parent
onClosed: destroy()
}
}
}

14
electrum/gui/qml/components/Wallets.qml

@ -129,7 +129,10 @@ Pane {
Label { text: 'derivation prefix (BIP32)'; visible: Daemon.currentWallet.isDeterministic; color: Material.accentColor; Layout.columnSpan: 2 }
Label { text: Daemon.currentWallet.derivationPrefix; visible: Daemon.currentWallet.isDeterministic; Layout.columnSpan: 2 }
Label { text: 'txinType'; color: Material.accentColor }
Label { text: 'wallet type'; color: Material.accentColor }
Label { text: Daemon.currentWallet.walletType }
Label { text: 'txin Type'; color: Material.accentColor }
Label { text: Daemon.currentWallet.txinType }
Label { text: 'is deterministic'; color: Material.accentColor }
@ -148,11 +151,16 @@ Pane {
Label { text: Daemon.currentWallet.isLightning }
Label { text: 'has Seed'; color: Material.accentColor }
Label { text: Daemon.currentWallet.hasSeed; Layout.columnSpan: 3 }
Label { text: Daemon.currentWallet.hasSeed }
Label { Layout.columnSpan:4; text: qsTr('Master Public Key'); color: Material.accentColor }
Label {
visible: Daemon.currentWallet.masterPubkey
Layout.columnSpan:4; text: qsTr('Master Public Key'); color: Material.accentColor
}
TextHighlightPane {
visible: Daemon.currentWallet.masterPubkey
Layout.columnSpan: 4
Layout.fillWidth: true
padding: 0

7
electrum/gui/qml/components/wizard/WCAutoConnect.qml

@ -1,14 +1,12 @@
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.1
import ".."
import "../controls"
WizardComponent {
valid: true
last: serverconnectgroup.checkedButton.connecttype === 'auto'
onAccept: {
function apply() {
wizard_data['autoconnect'] = serverconnectgroup.checkedButton.connecttype === 'auto'
}
@ -22,17 +20,18 @@ WizardComponent {
ButtonGroup {
id: serverconnectgroup
onCheckedButtonChanged: checkIsLast()
}
RadioButton {
ButtonGroup.group: serverconnectgroup
property string connecttype: 'auto'
text: qsTr('Auto connect')
checked: true
}
RadioButton {
ButtonGroup.group: serverconnectgroup
property string connecttype: 'manual'
checked: true
text: qsTr('Select servers manually')
}

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

@ -10,10 +10,11 @@ import "../controls"
WizardComponent {
valid: false
onAccept: {
function apply() {
wizard_data['script_type'] = scripttypegroup.checkedButton.scripttype
wizard_data['derivation_path'] = derivationpathtext.text
}
function getScriptTypePurposeDict() {
return {
'p2pkh': 44,
@ -51,10 +52,9 @@ WizardComponent {
clip:true
interactive: height < contentHeight
GridLayout {
ColumnLayout {
id: mainLayout
width: parent.width
columns: 1
Label { text: qsTr('Script type and Derivation path') }
Button {
@ -79,6 +79,7 @@ WizardComponent {
text: qsTr('native segwit (p2wpkh)')
}
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.')
}

10
electrum/gui/qml/components/wizard/WCCreateSeed.qml

@ -10,9 +10,8 @@ import "../controls"
WizardComponent {
valid: seedtext.text != ''
onAccept: {
function apply() {
wizard_data['seed'] = seedtext.text
wizard_data['seed_type'] = 'segwit'
wizard_data['seed_extend'] = extendcb.checked
wizard_data['seed_extra_words'] = extendcb.checked ? customwordstext.text : ''
}
@ -73,11 +72,16 @@ WizardComponent {
}
Component.onCompleted : {
setWarningText(12)
bitcoin.generate_seed()
}
}
}
onReadyChanged: {
if (!ready)
return
bitcoin.generate_seed(wizard_data['seed_type'])
}
Bitcoin {
id: bitcoin
onGeneratedSeedChanged: {

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

@ -11,7 +11,7 @@ WizardComponent {
valid: false
onAccept: {
function apply() {
wizard_data['master_key'] = masterkey_ta.text
}

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

@ -12,38 +12,39 @@ WizardComponent {
id: root
valid: false
onAccept: {
property bool is2fa: false
function apply() {
wizard_data['seed'] = seedtext.text
wizard_data['seed_variant'] = seed_variant.currentValue
wizard_data['seed_type'] = bitcoin.seed_type
wizard_data['seed_extend'] = extendcb.checked
wizard_data['seed_extra_words'] = extendcb.checked ? customwordstext.text : ''
wizard_data['seed_bip39'] = seed_type.getTypeCode() == 'BIP39'
wizard_data['seed_slip39'] = seed_type.getTypeCode() == 'SLIP39'
}
function setSeedTypeHelpText() {
var t = {
'Electrum': [
'electrum': [
qsTr('Electrum seeds are the default seed type.'),
qsTr('If you are restoring from a seed previously created by Electrum, choose this option')
].join(' '),
'BIP39': [
'bip39': [
qsTr('BIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),
'<br/><br/>',
qsTr('However, we do not generate BIP39 seeds, because they do not meet our safety standard.'),
qsTr('BIP39 seeds do not include a version number, which compromises compatibility with future software.')
].join(' '),
'SLIP39': [
'slip39': [
qsTr('SLIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'),
'<br/><br/>',
qsTr('However, we do not generate SLIP39 seeds.')
].join(' ')
}
infotext.text = t[seed_type.currentText]
infotext.text = t[seed_variant.currentValue]
}
function checkValid() {
bitcoin.verify_seed(seedtext.text, seed_type.getTypeCode() == 'BIP39', seed_type.getTypeCode() == 'SLIP39')
bitcoin.verify_seed(seedtext.text, seed_variant.currentValue, wizard_data['wallet_type'])
}
Flickable {
@ -58,19 +59,25 @@ WizardComponent {
columns: 2
Label {
visible: !is2fa
text: qsTr('Seed Type')
Layout.fillWidth: true
}
ComboBox {
id: seed_type
model: ['Electrum', 'BIP39'/*, 'SLIP39'*/]
id: seed_variant
visible: !is2fa
textRole: 'text'
valueRole: 'value'
model: [
{ text: qsTr('Electrum'), value: 'electrum' },
{ text: qsTr('BIP39'), value: 'bip39' }
]
onActivated: {
setSeedTypeHelpText()
checkIsLast()
checkValid()
}
function getTypeCode() {
return currentText
}
}
InfoTextArea {
id: infotext
@ -91,7 +98,7 @@ WizardComponent {
Rectangle {
anchors.fill: contentText
color: 'green'
color: root.valid ? 'green' : 'red'
border.color: Material.accentColor
radius: 2
}
@ -148,4 +155,12 @@ WizardComponent {
Component.onCompleted: {
setSeedTypeHelpText()
}
onReadyChanged: {
if (!ready)
return
if (wizard_data['wallet_type'] == '2fa')
root.is2fa = true
}
}

102
electrum/gui/qml/components/wizard/WCImport.qml

@ -0,0 +1,102 @@
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
function apply() {
if (bitcoin.isAddressList(import_ta.text)) {
wizard_data['address_list'] = import_ta.text
} else if (bitcoin.isPrivateKeyList(import_ta.text)) {
wizard_data['private_key_list'] = import_ta.text
}
}
function verify(text) {
return bitcoin.isAddressList(text) || bitcoin.isPrivateKeyList(text)
}
ColumnLayout {
width: parent.width
Label { text: qsTr('Import Bitcoin Addresses') }
InfoTextArea {
text: qsTr('Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.')
}
RowLayout {
TextArea {
id: import_ta
Layout.fillWidth: true
Layout.minimumHeight: 80
focus: true
wrapMode: TextEdit.WrapAnywhere
onTextChanged: valid = verify(text)
}
ColumnLayout {
Layout.alignment: Qt.AlignTop
ToolButton {
icon.source: '../../../icons/paste.png'
icon.height: constants.iconSizeMedium
icon.width: constants.iconSizeMedium
onClicked: {
if (verify(AppController.clipboardToText())) {
if (import_ta.text != '')
import_ta.text = import_ta.text + '\n'
import_ta.text = import_ta.text + AppController.clipboardToText()
}
}
}
ToolButton {
icon.source: '../../../icons/qrcode.png'
icon.height: constants.iconSizeMedium
icon.width: constants.iconSizeMedium
scale: 1.2
onClicked: {
var scan = qrscan.createObject(root)
scan.onFound.connect(function() {
if (verify(scan.scanData)) {
if (import_ta.text != '')
import_ta.text = import_ta.text + ',\n'
import_ta.text = import_ta.text + scan.scanData
}
scan.destroy()
})
}
}
}
}
}
Component {
id: qrscan
QRScan {
width: root.width
height: root.height
ToolButton {
icon.source: '../../../icons/closebutton.png'
icon.height: constants.iconSizeMedium
icon.width: constants.iconSizeMedium
anchors.right: parent.right
anchors.top: parent.top
onClicked: {
parent.destroy()
}
}
}
}
Bitcoin {
id: bitcoin
}
}

2
electrum/gui/qml/components/wizard/WCKeystoreType.qml

@ -4,7 +4,7 @@ import QtQuick.Controls 2.1
WizardComponent {
valid: keystoregroup.checkedButton !== null
onAccept: {
function apply() {
wizard_data['keystore_type'] = keystoregroup.checkedButton.keystoretype
}

2
electrum/gui/qml/components/wizard/WCProxyConfig.qml

@ -3,7 +3,7 @@ import "../controls"
WizardComponent {
valid: true
onAccept: {
function apply() {
wizard_data['proxy'] = pc.toProxyDict()
}

2
electrum/gui/qml/components/wizard/WCServerConfig.qml

@ -4,7 +4,7 @@ WizardComponent {
valid: true
last: true
onAccept: {
function apply() {
wizard_data['oneserver'] = !sc.auto_server
wizard_data['server'] = sc.address
}

2
electrum/gui/qml/components/wizard/WCWalletName.qml

@ -7,7 +7,7 @@ import org.electrum 1.0
WizardComponent {
valid: wallet_name.text.length > 0
onAccept: {
function apply() {
wizard_data['wallet_name'] = wallet_name.text
}

2
electrum/gui/qml/components/wizard/WCWalletPassword.qml

@ -7,7 +7,7 @@ import "../controls"
WizardComponent {
valid: password1.text === password2.text && password1.text.length > 4
onAccept: {
function apply() {
wizard_data['password'] = password1.text
wizard_data['encrypt'] = password1.text != ''
}

11
electrum/gui/qml/components/wizard/WCWalletType.qml

@ -4,8 +4,13 @@ import QtQuick.Controls 2.1
WizardComponent {
valid: wallettypegroup.checkedButton !== null
onAccept: {
function apply() {
wizard_data['wallet_type'] = wallettypegroup.checkedButton.wallettype
if (wizard_data['wallet_type'] == 'standard')
wizard_data['seed_type'] = 'segwit'
else if (wizard_data['wallet_type'] == '2fa')
wizard_data['seed_type'] = '2fa_segwit'
// TODO: multisig
}
ButtonGroup {
@ -22,7 +27,6 @@ WizardComponent {
text: qsTr('Standard Wallet')
}
RadioButton {
enabled: false
ButtonGroup.group: wallettypegroup
property string wallettype: '2fa'
text: qsTr('Wallet with two-factor authentication')
@ -34,9 +38,8 @@ WizardComponent {
text: qsTr('Multi-signature wallet')
}
RadioButton {
enabled: false
ButtonGroup.group: wallettypegroup
property string wallettype: 'import'
property string wallettype: 'imported'
text: qsTr('Import Bitcoin addresses or private keys')
}
}

27
electrum/gui/qml/components/wizard/Wizard.qml

@ -11,7 +11,8 @@ Dialog {
height: parent.height
property var wizard_data
property alias pages : pages
property alias pages: pages
property QtObject wiz
function _setWizardData(wdata) {
wizard_data = {}
@ -24,12 +25,19 @@ Dialog {
// Here we do some manual binding of page.valid -> pages.pagevalid and
// page.last -> pages.lastpage to propagate the state without the binding
// going stale.
function _loadNextComponent(comp, wdata={}) {
function _loadNextComponent(view, wdata={}) {
// remove any existing pages after current page
while (pages.contentChildren[pages.currentIndex+1]) {
pages.takeItem(pages.currentIndex+1).destroy()
}
var url = Qt.resolvedUrl(wiz.viewToComponent(view))
console.log(url)
var comp = Qt.createComponent(url)
if (comp.status == Component.Error) {
console.log(comp.errorString())
return null
}
var page = comp.createObject(pages)
page.validChanged.connect(function() {
pages.pagevalid = page.valid
@ -37,6 +45,19 @@ Dialog {
page.lastChanged.connect(function() {
pages.lastpage = page.last
} )
page.next.connect(function() {
var newview = wiz.submit(page.wizard_data)
if (newview.view) {
console.log('next view: ' + newview.view)
var newpage = _loadNextComponent(newview.view, newview.wizard_data)
} else {
console.log('END')
}
})
page.prev.connect(function() {
var wdata = wiz.prev()
console.log('prev view data: ' + JSON.stringify(wdata))
})
Object.assign(page.wizard_data, wdata) // deep copy
page.ready = true // signal page it can access wizard_data
pages.pagevalid = page.valid
@ -58,10 +79,12 @@ Dialog {
clip:true
function prev() {
currentItem.prev()
currentIndex = currentIndex - 1
_setWizardData(pages.contentChildren[currentIndex].wizard_data)
pages.pagevalid = pages.contentChildren[currentIndex].valid
pages.lastpage = pages.contentChildren[currentIndex].last
}
function next() {

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

@ -2,9 +2,25 @@ import QtQuick 2.0
Item {
signal next
signal prev
signal accept
property var wizard_data : ({})
property bool valid
property bool last: false
property bool ready: false
onAccept: {
apply()
}
function apply() { }
function checkIsLast() {
apply()
last = wizard.wiz.isLast(wizard_data)
}
Component.onCompleted: {
checkIsLast()
}
}

4
electrum/gui/qml/qeaddresslistmodel.py

@ -65,7 +65,7 @@ class QEAddressListModel(QAbstractListModel):
def init_model(self):
r_addresses = self.wallet.get_receiving_addresses()
c_addresses = self.wallet.get_change_addresses()
n_addresses = len(r_addresses) + len(c_addresses)
n_addresses = len(r_addresses) + len(c_addresses) if self.wallet.use_change else 0
def insert_row(atype, alist, address, iaddr):
item = self.addr_to_model(address)
@ -80,7 +80,7 @@ class QEAddressListModel(QAbstractListModel):
insert_row('receive', self.receive_addresses, address, i)
i = i + 1
i = 0
for address in c_addresses:
for address in c_addresses if self.wallet.use_change else []:
insert_row('change', self.change_addresses, address, i)
i = i + 1
self.endInsertRows()

5
electrum/gui/qml/qeapp.py

@ -29,6 +29,7 @@ from .qechannelopener import QEChannelOpener
from .qelnpaymentdetails import QELnPaymentDetails
from .qechanneldetails import QEChannelDetails
from .qeswaphelper import QESwapHelper
from .qewizard import QENewWalletWizard, QEServerConnectWizard
notification = None
@ -217,6 +218,8 @@ class ElectrumQmlApplication(QGuiApplication):
qmlRegisterType(QERequestDetails, 'org.electrum', 1, 0, 'RequestDetails')
qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property')
qmlRegisterUncreatableType(QENewWalletWizard, 'org.electrum', 1, 0, 'NewWalletWizard', 'NewWalletWizard can only be used as property')
qmlRegisterUncreatableType(QEServerConnectWizard, 'org.electrum', 1, 0, 'ServerConnectWizard', 'ServerConnectWizard can only be used as property')
self.engine = QQmlApplicationEngine(parent=self)
@ -254,6 +257,8 @@ class ElectrumQmlApplication(QGuiApplication):
'protocol_version': version.PROTOCOL_VERSION
})
self.plugins.load_plugin('trustedcoin')
qInstallMessageHandler(self.message_handler)
# get notified whether root QML document loads or not

36
electrum/gui/qml/qebitcoin.py

@ -10,6 +10,7 @@ from electrum.logging import get_logger
from electrum.slip39 import decode_mnemonic, Slip39Error
from electrum.util import parse_URI, create_bip21_uri, InvalidBitcoinURI, get_asyncio_loop
from electrum.transaction import tx_from_any
from electrum.mnemonic import is_any_2fa_seed_type
from .qetypes import QEAmount
@ -67,22 +68,18 @@ class QEBitcoin(QObject):
asyncio.run_coroutine_threadsafe(co_gen_seed(seed_type, language), get_asyncio_loop())
@pyqtSlot(str)
@pyqtSlot(str,bool,bool)
@pyqtSlot(str,bool,bool,str,str,str)
def verify_seed(self, seed, bip39=False, slip39=False, wallet_type='standard', language='en'):
self._logger.debug('bip39 ' + str(bip39))
self._logger.debug('slip39 ' + str(slip39))
@pyqtSlot(str,str)
@pyqtSlot(str,str,str)
def verify_seed(self, seed, seed_variant, wallet_type='standard'):
seed_type = ''
seed_valid = False
self.validationMessage = ''
if not (bip39 or slip39):
if seed_variant == 'electrum':
seed_type = mnemonic.seed_type(seed)
if seed_type != '':
seed_valid = True
elif bip39:
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'
self.validationMessage = 'BIP39 (%s)' % status
@ -90,8 +87,7 @@ class QEBitcoin(QObject):
if is_checksum:
seed_type = 'bip39'
seed_valid = True
elif slip39: # TODO: incomplete impl, this code only validates a single share.
elif seed_variant == 'slip39': # TODO: incomplete impl, this code only validates a single share.
try:
share = decode_mnemonic(seed)
seed_type = 'slip39'
@ -99,10 +95,13 @@ class QEBitcoin(QObject):
except Slip39Error as e:
self.validationMessage = 'SLIP39: %s' % str(e)
seed_valid = False # for now
else:
raise Exception(f'unknown seed variant {seed_variant}')
# cosigning seed
if wallet_type != 'standard' and seed_type not in ['standard', 'segwit']:
seed_type = ''
# 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
self.seedType = seed_type
@ -161,3 +160,12 @@ class QEBitcoin(QObject):
return True
except:
return False
@pyqtSlot(str, result=bool)
def isAddressList(self, csv: str):
return keystore.is_address_list(csv)
@pyqtSlot(str, result=bool)
def isPrivateKeyList(self, csv: str):
return keystore.is_private_key_list(csv)

34
electrum/gui/qml/qedaemon.py

@ -14,6 +14,7 @@ from .auth import AuthMixin, auth_protect
from .qefx import QEFX
from .qewallet import QEWallet
from .qewalletdb import QEWalletDB
from .qewizard import QENewWalletWizard, QEServerConnectWizard
# wallet list model. supports both wallet basenames (wallet file basenames)
# and whole Wallet instances (loaded wallets)
@ -121,16 +122,21 @@ class QEDaemon(AuthMixin, QObject):
_loaded_wallets = QEWalletListModel()
_available_wallets = None
_current_wallet = None
_new_wallet_wizard = None
_server_connect_wizard = None
_path = None
_use_single_password = False
_password = None
walletLoaded = pyqtSignal()
walletRequiresPassword = pyqtSignal()
activeWalletsChanged = pyqtSignal()
availableWalletsChanged = pyqtSignal()
walletOpenError = pyqtSignal([str], arguments=["error"])
fxChanged = pyqtSignal()
newWalletWizardChanged = pyqtSignal()
serverConnectWizardChanged = pyqtSignal()
walletLoaded = pyqtSignal()
walletRequiresPassword = pyqtSignal()
walletOpenError = pyqtSignal([str], arguments=["error"])
walletDeleteError = pyqtSignal([str,str], arguments=['code', 'message'])
@pyqtSlot()
@ -157,7 +163,9 @@ class QEDaemon(AuthMixin, QObject):
if not password:
password = self._password
if self._path not in self.daemon._wallets:
wallet_already_open = self._path in self.daemon._wallets
if not wallet_already_open:
# pre-checks, let walletdb trigger any necessary user interactions
self._walletdb.path = self._path
self._walletdb.password = password
@ -168,9 +176,10 @@ class QEDaemon(AuthMixin, QObject):
try:
wallet = self.daemon.load_wallet(self._path, password)
if wallet != None:
self._loaded_wallets.add_wallet(wallet_path=self._path, wallet=wallet)
self._current_wallet = QEWallet.getInstanceFor(wallet)
self._current_wallet.password = password
if not wallet_already_open:
self._loaded_wallets.add_wallet(wallet_path=self._path, wallet=wallet)
self._current_wallet.password = password
self.walletLoaded.emit()
if self.daemon.config.get('single_password'):
@ -283,3 +292,16 @@ class QEDaemon(AuthMixin, QObject):
self.daemon.update_password_for_directory(old_password=self._password, new_password=password)
self._password = password
@pyqtProperty(QENewWalletWizard, notify=newWalletWizardChanged)
def newWalletWizard(self):
if not self._new_wallet_wizard:
self._new_wallet_wizard = QENewWalletWizard(self)
return self._new_wallet_wizard
@pyqtProperty(QEServerConnectWizard, notify=serverConnectWizardChanged)
def serverConnectWizard(self):
if not self._server_connect_wizard:
self._server_connect_wizard = QEServerConnectWizard(self)
return self._server_connect_wizard

47
electrum/gui/qml/qewallet.py

@ -3,6 +3,7 @@ import queue
import threading
import time
from typing import Optional, TYPE_CHECKING
from functools import partial
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer
@ -13,6 +14,7 @@ from electrum.logging import get_logger
from electrum.network import TxBroadcastError, BestEffortRequestFailed
from electrum.transaction import PartialTxOutput
from electrum.util import (parse_max_spend, InvalidPassword, event_listener)
from electrum.plugin import run_hook
from .auth import AuthMixin, auth_protect
from .qeaddresslistmodel import QEAddressListModel
@ -63,6 +65,9 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
#broadcastSucceeded = pyqtSignal([str], arguments=['txid'])
broadcastFailed = pyqtSignal([str,str,str], arguments=['txid','code','reason'])
labelsUpdated = pyqtSignal()
otpRequested = pyqtSignal()
otpSuccess = pyqtSignal()
otpFailed = pyqtSignal([str,str], arguments=['code','message'])
_network_signal = pyqtSignal(str, object)
@ -298,12 +303,18 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
def canHaveLightning(self):
return self.wallet.can_have_lightning()
@pyqtProperty(str, notify=dataChanged)
def walletType(self):
return self.wallet.wallet_type
@pyqtProperty(bool, notify=dataChanged)
def hasSeed(self):
return self.wallet.has_seed()
@pyqtProperty(str, notify=dataChanged)
def txinType(self):
if self.wallet.wallet_type == 'imported':
return self.wallet.txin_type
return self.wallet.get_txin_type(self.wallet.dummy_address())
@pyqtProperty(bool, notify=dataChanged)
@ -327,6 +338,9 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
keystores = self.wallet.get_keystores()
if len(keystores) > 1:
self._logger.debug('multiple keystores not supported yet')
if len(keystores) == 0:
self._logger.debug('no keystore')
return ''
return keystores[0].get_derivation_prefix()
@pyqtProperty(str, notify=dataChanged)
@ -413,6 +427,17 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
@auth_protect
def sign(self, tx, *, broadcast: bool = False):
sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx, partial(self.on_sign_complete, broadcast),
self.on_sign_failed)
if sign_hook:
self.do_sign(tx, False)
self._logger.debug('plugin needs to sign tx too')
sign_hook(tx)
return
self.do_sign(tx, broadcast)
def do_sign(self, tx, broadcast):
tx = self.wallet.sign_transaction(tx, self.password)
if tx is None:
@ -431,6 +456,23 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
if broadcast:
self.broadcast(tx)
# this assumes a 2fa wallet, but there are no other tc_sign_wrapper hooks, so that's ok
def on_sign_complete(self, broadcast, tx):
self.otpSuccess.emit()
if broadcast:
self.broadcast(tx)
def on_sign_failed(self, error):
self.otpFailed.emit('error', error)
def request_otp(self, on_submit):
self._otp_on_submit = on_submit
self.otpRequested.emit()
@pyqtSlot(str)
def submitOtp(self, otp):
self._otp_on_submit(otp)
def broadcast(self, tx):
assert tx.is_complete()
@ -544,7 +586,10 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
addr = None
if self.wallet.config.get('bolt11_fallback', True):
addr = self.wallet.get_unused_address()
# if addr is None, we ran out of addresses. for lightning enabled wallets, ignore for now
# if addr is None, we ran out of addresses
if addr is None:
# TODO: remove oldest unpaid request having a fallback address and try again
pass
key = self.wallet.create_request(None, None, default_expiry, addr)
else:
key, addr = self.create_bitcoin_request(None, None, default_expiry, ignore_gap)

70
electrum/gui/qml/qewalletdb.py

@ -29,10 +29,8 @@ class QEWalletDB(QObject):
requiresSplitChanged = pyqtSignal()
splitFinished = pyqtSignal()
readyChanged = pyqtSignal()
createError = pyqtSignal([str], arguments=["error"])
createSuccess = pyqtSignal()
invalidPassword = pyqtSignal()
def reset(self):
self._path = None
self._needsPassword = False
@ -172,69 +170,3 @@ class QEWalletDB(QObject):
self._ready = True
self.readyChanged.emit()
@pyqtSlot('QJSValue',bool,str)
def create_storage(self, js_data, single_password_enabled, single_password):
self._logger.info('Creating wallet from wizard data')
data = js_data.toVariant()
self._logger.debug(str(data))
assert data['wallet_type'] == 'standard' # only standard wallets for now
if single_password_enabled and single_password:
data['encrypt'] = True
data['password'] = single_password
try:
path = os.path.join(os.path.dirname(self.daemon.config.get_wallet_path()), data['wallet_name'])
if os.path.exists(path):
raise Exception('file already exists at path')
storage = WalletStorage(path)
if data['keystore_type'] in ['createseed', 'haveseed']:
if data['seed_type'] in ['old', 'standard', 'segwit']: #2fa, 2fa-segwit
self._logger.debug('creating keystore from electrum seed')
k = keystore.from_seed(data['seed'], data['seed_extra_words'], data['wallet_type'] == 'multisig')
elif data['seed_type'] == 'bip39':
self._logger.debug('creating keystore from bip39 seed')
root_seed = keystore.bip39_to_seed(data['seed'], data['seed_extra_words'])
derivation = normalize_bip32_derivation(data['derivation_path'])
script = data['script_type'] if data['script_type'] != 'p2pkh' else 'standard'
k = keystore.from_bip43_rootseed(root_seed, derivation, xtype=script)
else:
raise Exception('unsupported/unknown seed_type %s' % data['seed_type'])
elif data['keystore_type'] == 'masterkey':
k = keystore.from_master_key(data['master_key'])
has_xpub = isinstance(k, keystore.Xpub)
assert has_xpub
t1 = xpub_type(k.xpub)
if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']:
raise Exception('wrong key type %s' % t1)
else:
raise Exception('unsupported/unknown keystore_type %s' % data['keystore_type'])
if data['encrypt']:
if k.may_have_password():
k.update_password(None, data['password'])
storage.set_password(data['password'], enc_version=StorageEncryptionVersion.USER_PASSWORD)
db = WalletDB('', manual_upgrades=False)
db.set_keystore_encryption(bool(data['password']) and data['encrypt'])
db.put('wallet_type', data['wallet_type'])
if 'seed_type' in data:
db.put('seed_type', data['seed_type'])
db.put('keystore', k.dump())
if k.can_have_deterministic_lightning_xprv():
db.put('lightning_xprv', k.get_lightning_xprv(data['password'] if data['encrypt'] else None))
db.load_plugins()
db.write(storage)
# minimally populate self after create
self._password = data['password']
self.path = path
self.createSuccess.emit()
except Exception as e:
self._logger.error(repr(e))
self.createError.emit(str(e))

115
electrum/gui/qml/qewizard.py

@ -0,0 +1,115 @@
import os
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from PyQt5.QtQml import QQmlApplicationEngine
from electrum.logging import get_logger
from electrum.wizard import NewWalletWizard, ServerConnectWizard
class QEAbstractWizard(QObject):
_logger = get_logger(__name__)
def __init__(self, parent = None):
QObject.__init__(self, parent)
@pyqtSlot(result=str)
def start_wizard(self):
self.start()
return self._current.view
@pyqtSlot(str, result=str)
def viewToComponent(self, view):
return self.navmap[view]['gui'] + '.qml'
@pyqtSlot('QJSValue', result='QVariant')
def submit(self, wizard_data):
wdata = wizard_data.toVariant()
self._logger.debug(str(wdata))
view = self.resolve_next(self._current.view, wdata)
return { 'view': view.view, 'wizard_data': view.wizard_data }
@pyqtSlot(result='QVariant')
def prev(self):
viewstate = self.resolve_prev()
return viewstate.wizard_data
@pyqtSlot('QJSValue', result=bool)
def isLast(self, wizard_data):
wdata = wizard_data.toVariant()
return self.is_last_view(self._current.view, wdata)
class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
createError = pyqtSignal([str], arguments=["error"])
createSuccess = pyqtSignal()
def __init__(self, daemon, parent = None):
NewWalletWizard.__init__(self, daemon)
QEAbstractWizard.__init__(self, parent)
self._daemon = daemon
# attach view names
self.navmap_merge({
'wallet_name': { 'gui': 'WCWalletName' },
'wallet_type': { 'gui': 'WCWalletType' },
'keystore_type': { 'gui': 'WCKeystoreType' },
'create_seed': { 'gui': 'WCCreateSeed' },
'confirm_seed': { 'gui': 'WCConfirmSeed' },
'have_seed': { 'gui': 'WCHaveSeed' },
'bip39_refine': { 'gui': 'WCBIP39Refine' },
'have_master_key': { 'gui': 'WCHaveMasterKey' },
'imported': { 'gui': 'WCImport' },
'wallet_password': { 'gui': 'WCWalletPassword' }
})
pathChanged = pyqtSignal()
@pyqtProperty(str, notify=pathChanged)
def path(self):
return self._path
@path.setter
def path(self, path):
self._path = path
self.pathChanged.emit()
def last_if_single_password(self, *args):
return self._daemon.singlePasswordEnabled
@pyqtSlot('QJSValue', bool, str)
def createStorage(self, js_data, single_password_enabled, single_password):
self._logger.info('Creating wallet from wizard data')
data = js_data.toVariant()
self._logger.debug(str(data))
if single_password_enabled and single_password:
data['encrypt'] = True
data['password'] = single_password
path = os.path.join(os.path.dirname(self._daemon.daemon.config.get_wallet_path()), data['wallet_name'])
try:
self.create_storage(path, data)
# minimally populate self after create
self._password = data['password']
self.path = path
self.createSuccess.emit()
except Exception as e:
self._logger.error(repr(e))
self.createError.emit(str(e))
class QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard):
def __init__(self, daemon, parent = None):
ServerConnectWizard.__init__(self, daemon)
QEAbstractWizard.__init__(self, parent)
self._daemon = daemon
# attach view names
self.navmap_merge({
'autoconnect': { 'gui': 'WCAutoConnect' },
'proxy_config': { 'gui': 'WCProxyConfig' },
'server_config': { 'gui': 'WCServerConfig' },
})

5
electrum/plugins/qml_test/__init__.py

@ -1,5 +0,0 @@
from electrum.i18n import _
fullname = 'QML Plugin Test'
description = '%s\n%s' % (_("Plugin to test QML integration from plugins."), _("Note: Used for development"))
available_for = ['qml']

15
electrum/plugins/qml_test/qml.py

@ -1,15 +0,0 @@
from typing import TYPE_CHECKING
from PyQt5.QtQml import QQmlApplicationEngine
from electrum.plugin import hook, BasePlugin
from electrum.logging import get_logger
if TYPE_CHECKING:
from electrum.gui.qml import ElectrumGui
class Plugin(BasePlugin):
def __init__(self, parent, config, name):
BasePlugin.__init__(self, parent, config, name)
@hook
def init_qml(self, gui: 'ElectrumGui'):
self.logger.debug('init_qml hook called')

2
electrum/plugins/trustedcoin/__init__.py

@ -8,4 +8,4 @@ description = ''.join([
])
requires_wallet_type = ['2fa']
registers_wallet_type = '2fa'
available_for = ['qt', 'cmdline', 'kivy']
available_for = ['qt', 'cmdline', 'kivy', 'qml']

395
electrum/plugins/trustedcoin/qml.py

@ -0,0 +1,395 @@
import threading
import socket
import base64
from typing import TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
from electrum.i18n import _
from electrum.plugin import hook
from electrum.bip32 import xpub_type, BIP32Node
from electrum.util import UserFacingException
from electrum import keystore
from electrum.gui.qml.qewallet import QEWallet
from electrum.gui.qml.plugins import PluginQObject
from .trustedcoin import (TrustedCoinPlugin, server, ErrorConnectingServer,
MOBILE_DISCLAIMER, get_user_id, get_signing_xpub,
TrustedCoinException, make_xpub)
if TYPE_CHECKING:
from electrum.gui.qml import ElectrumGui
from electrum.wallet import Abstract_Wallet
class Plugin(TrustedCoinPlugin):
class QSignalObject(PluginQObject):
canSignWithoutServerChanged = pyqtSignal()
_canSignWithoutServer = False
termsAndConditionsChanged = pyqtSignal()
_termsAndConditions = ''
termsAndConditionsErrorChanged = pyqtSignal()
_termsAndConditionsError = ''
otpError = pyqtSignal([str], arguments=['message'])
otpSuccess = pyqtSignal()
disclaimerChanged = pyqtSignal()
keystoreChanged = pyqtSignal()
otpSecretChanged = pyqtSignal()
_otpSecret = ''
shortIdChanged = pyqtSignal()
_shortId = ''
_remoteKeyState = ''
remoteKeyStateChanged = pyqtSignal()
remoteKeyError = pyqtSignal([str], arguments=['message'])
requestOtp = pyqtSignal()
def __init__(self, plugin, parent):
super().__init__(plugin, parent)
@pyqtProperty(str, notify=disclaimerChanged)
def disclaimer(self):
return '\n\n'.join(MOBILE_DISCLAIMER)
@pyqtProperty(bool, notify=canSignWithoutServerChanged)
def canSignWithoutServer(self):
return self._canSignWithoutServer
@pyqtProperty('QVariantMap', notify=keystoreChanged)
def keystore(self):
return self._keystore
@pyqtProperty(str, notify=otpSecretChanged)
def otpSecret(self):
return self._otpSecret
@pyqtProperty(str, notify=shortIdChanged)
def shortId(self):
return self._shortId
@pyqtSlot(str)
def otpSubmit(self, otp):
self._plugin.on_otp(otp)
@pyqtProperty(str, notify=termsAndConditionsChanged)
def termsAndConditions(self):
return self._termsAndConditions
@pyqtProperty(str, notify=termsAndConditionsErrorChanged)
def termsAndConditionsError(self):
return self._termsAndConditionsError
@pyqtProperty(str, notify=remoteKeyStateChanged)
def remoteKeyState(self):
return self._remoteKeyState
@remoteKeyState.setter
def remoteKeyState(self, new_state):
if self._remoteKeyState != new_state:
self._remoteKeyState = new_state
self.remoteKeyStateChanged.emit()
@pyqtSlot()
def fetchTermsAndConditions(self):
def fetch_task():
try:
self.plugin.logger.debug('TOS')
tos = server.get_terms_of_service()
except ErrorConnectingServer as e:
self._termsAndConditionsError = _('Error connecting to server')
self.termsAndConditionsErrorChanged.emit()
except Exception as e:
self._termsAndConditionsError = '%s: %s' % (_('Error'), repr(e))
self.termsAndConditionsErrorChanged.emit()
else:
self._termsAndConditions = tos
self.termsAndConditionsChanged.emit()
finally:
self._busy = False
self.busyChanged.emit()
self._busy = True
self.busyChanged.emit()
t = threading.Thread(target=fetch_task)
t.daemon = True
t.start()
@pyqtSlot(str)
def createKeystore(self, email):
self.remoteKeyState = ''
xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.plugin.create_keys()
def create_remote_key_task():
try:
self.plugin.logger.debug('create remote key')
r = server.create(xpub1, xpub2, email)
otp_secret = r['otp_secret']
_xpub3 = r['xpubkey_cosigner']
_id = r['id']
except (socket.error, ErrorConnectingServer) as e:
self.remoteKeyState = 'error'
self.remoteKeyError.emit(f'Network error: {str(e)}')
except TrustedCoinException as e:
if e.status_code == 409:
self.remoteKeyState = 'wallet_known'
self._shortId = short_id
self.shortIdChanged.emit()
else:
self.remoteKeyState = 'error'
self.logger.warning(str(e))
self.remoteKeyError.emit(f'Service error: {str(e)}')
except (KeyError,TypeError) as e: # catch any assumptions
self.remoteKeyState = 'error'
self.remoteKeyError.emit(f'Error: {str(e)}')
self.logger.error(str(e))
else:
if short_id != _id:
self.remoteKeyState = 'error'
self.logger.error("unexpected trustedcoin short_id: expected {}, received {}".format(short_id, _id))
self.remoteKeyError.emit('Unexpected short_id')
return
if xpub3 != _xpub3:
self.remoteKeyState = 'error'
self.logger.error("unexpected trustedcoin xpub3: expected {}, received {}".format(xpub3, _xpub3))
self.remoteKeyError.emit('Unexpected trustedcoin xpub3')
return
self._otpSecret = otp_secret
self.otpSecretChanged.emit()
self._shortId = short_id
self.shortIdChanged.emit()
finally:
self._busy = False
self.busyChanged.emit()
self._busy = True
self.busyChanged.emit()
t = threading.Thread(target=create_remote_key_task)
t.daemon = True
t.start()
@pyqtSlot()
def resetOtpSecret(self):
self.remoteKeyState = ''
xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.plugin.create_keys()
def reset_otp_task():
try:
self.plugin.logger.debug('reset_otp')
r = server.get_challenge(short_id)
challenge = r.get('challenge')
message = 'TRUSTEDCOIN CHALLENGE: ' + challenge
def f(xprv):
rootnode = BIP32Node.from_xkey(xprv)
key = rootnode.subkey_at_private_derivation((0, 0)).eckey
sig = key.sign_message(message, True)
return base64.b64encode(sig).decode()
signatures = [f(x) for x in [xprv1, xprv2]]
r = server.reset_auth(short_id, challenge, signatures)
otp_secret = r.get('otp_secret')
except (socket.error, ErrorConnectingServer) as e:
self.remoteKeyState = 'error'
self.remoteKeyError.emit(f'Network error: {str(e)}')
except Exception as e:
self.remoteKeyState = 'error'
self.remoteKeyError.emit(f'Error: {str(e)}')
else:
self._otpSecret = otp_secret
self.otpSecretChanged.emit()
finally:
self._busy = False
self.busyChanged.emit()
self._busy = True
self.busyChanged.emit()
t = threading.Thread(target=reset_otp_task, daemon=True)
t.start()
@pyqtSlot(str, int)
def checkOtp(self, short_id, otp):
def check_otp_task():
try:
self.plugin.logger.debug(f'check OTP, shortId={short_id}, otp={otp}')
server.auth(short_id, otp)
except TrustedCoinException as e:
if e.status_code == 400: # invalid OTP
self.plugin.logger.debug('Invalid one-time password.')
self.otpError.emit(_('Invalid one-time password.'))
else:
self.plugin.logger.error(str(e))
self.otpError.emit(f'Service error: {str(e)}')
except Exception as e:
self.plugin.logger.error(str(e))
self.otpError.emit(f'Error: {str(e)}')
else:
self.plugin.logger.debug('OTP verify success')
self.otpSuccess.emit()
finally:
self._busy = False
self.busyChanged.emit()
self._busy = True
self.busyChanged.emit()
t = threading.Thread(target=check_otp_task, daemon=True)
t.start()
def __init__(self, *args):
super().__init__(*args)
@hook
def load_wallet(self, wallet: 'Abstract_Wallet'):
if not isinstance(wallet, self.wallet_class):
return
self.logger.debug(f'plugin enabled for wallet "{str(wallet)}"')
#wallet.handler_2fa = HandlerTwoFactor(self, window)
if wallet.can_sign_without_server():
self.so._canSignWithoutServer = True
self.so.canSignWithoutServerChanged.emit()
msg = ' '.join([
_('This wallet was restored from seed, and it contains two master private keys.'),
_('Therefore, two-factor authentication is disabled.')
])
self.logger.info(msg)
#action = lambda: window.show_message(msg)
#else:
#action = partial(self.settings_dialog, window)
#button = StatusBarButton(read_QIcon("trustedcoin-status.png"),
#_("TrustedCoin"), action)
#window.statusBar().addPermanentWidget(button)
self.start_request_thread(wallet)
@hook
def init_qml(self, gui: 'ElectrumGui'):
self.logger.debug(f'init_qml hook called, gui={str(type(gui))}')
self._app = gui.app
# important: QSignalObject needs to be parented, as keeping a ref
# in the plugin is not enough to avoid gc
self.so = Plugin.QSignalObject(self, self._app)
# extend wizard
self.extend_wizard()
def extend_wizard(self):
wizard = self._app.daemon.newWalletWizard
self.logger.debug(repr(wizard))
views = {
'trustedcoin_start': {
'gui': '../../../../plugins/trustedcoin/qml/Disclaimer',
'next': 'trustedcoin_choose_seed'
},
'trustedcoin_choose_seed': {
'gui': '../../../../plugins/trustedcoin/qml/ChooseSeed',
'next': lambda d: 'trustedcoin_create_seed' if d['keystore_type'] == 'createseed'
else 'trustedcoin_have_seed'
},
'trustedcoin_create_seed': {
'gui': 'WCCreateSeed',
'next': 'trustedcoin_confirm_seed'
},
'trustedcoin_confirm_seed': {
'gui': 'WCConfirmSeed',
'next': 'trustedcoin_tos_email'
},
'trustedcoin_have_seed': {
'gui': 'WCHaveSeed',
'next': 'trustedcoin_keep_disable'
},
'trustedcoin_keep_disable': {
'gui': '../../../../plugins/trustedcoin/qml/KeepDisable',
'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'
},
'trustedcoin_tos_email': {
'gui': '../../../../plugins/trustedcoin/qml/Terms',
'next': 'trustedcoin_show_confirm_otp'
},
'trustedcoin_show_confirm_otp': {
'gui': '../../../../plugins/trustedcoin/qml/ShowConfirmOTP',
'accept': self.on_accept_otp_secret,
'next': 'wallet_password',
'last': wizard.last_if_single_password
}
}
wizard.navmap_merge(views)
# combined create_keystore and create_remote_key pre
def create_keys(self):
wizard = self._app.daemon.newWalletWizard
wizard_data = wizard._current.wizard_data
xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(wizard_data['seed'], wizard_data['seed_extra_words'])
# NOTE: at this point, old style wizard creates a wallet file (w. password if set) and
# stores the keystores and wizard state, in order to separate offline seed creation
# and online retrieval of the OTP secret. For mobile, we don't do this, but
# for desktop the wizard should support this usecase.
data = {'x1/': {'xpub': xpub1}, 'x2/': {'xpub': xpub2}}
# Generate third key deterministically.
long_user_id, short_id = get_user_id(data)
xtype = xpub_type(xpub1)
xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id)
return (xprv1,xpub1,xprv2,xpub2,xpub3,short_id)
def on_accept_otp_secret(self, wizard_data):
self.logger.debug('OTP secret accepted, creating keystores')
xprv1,xpub1,xprv2,xpub2,xpub3,short_id = self.create_keys()
k1 = keystore.from_xprv(xprv1)
k2 = keystore.from_xpub(xpub2)
k3 = keystore.from_xpub(xpub3)
wizard_data['x1/'] = k1.dump()
wizard_data['x2/'] = k2.dump()
wizard_data['x3/'] = k3.dump()
def recovery_disable(self, wizard_data):
if wizard_data['trustedcoin_keepordisable'] != 'disable':
return
self.logger.debug('2fa disabled, creating keystores')
xprv1,xpub1,xprv2,xpub2,xpub3,short_id = self.create_keys()
k1 = keystore.from_xprv(xprv1)
k2 = keystore.from_xprv(xprv2)
k3 = keystore.from_xpub(xpub3)
wizard_data['x1/'] = k1.dump()
wizard_data['x2/'] = k2.dump()
wizard_data['x3/'] = k3.dump()
# regular wallet prompt functions
def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):
self.logger.debug('prompt_user_for_otp')
self.on_success = on_success
self.on_failure = on_failure if on_failure else lambda x: self.logger.error(x)
self.wallet = wallet
self.tx = tx
qewallet = QEWallet.getInstanceFor(wallet)
qewallet.request_otp(self.on_otp)
def on_otp(self, otp):
self.logger.debug(f'on_otp {otp} for tx {repr(self.tx)}')
try:
self.wallet.on_otp(self.tx, otp)
except UserFacingException as e:
self.on_failure(_('Invalid one-time password.'))
except TrustedCoinException as e:
if e.status_code == 400: # invalid OTP
self.on_failure(_('Invalid one-time password.'))
else:
self.on_failure(_('Error') + ':\n' + str(e))
except Exception as e:
self.on_failure(_('Error') + ':\n' + str(e))
else:
self.on_success(self.tx)

38
electrum/plugins/trustedcoin/qml/ChooseSeed.qml

@ -0,0 +1,38 @@
import QtQuick 2.6
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.1
import "../../../gui/qml/components/wizard"
WizardComponent {
valid: keystoregroup.checkedButton !== null
onAccept: {
wizard_data['keystore_type'] = keystoregroup.checkedButton.keystoretype
}
ButtonGroup {
id: keystoregroup
}
ColumnLayout {
width: parent.width
Label {
text: qsTr('Do you want to create a new seed, or restore a wallet using an existing seed?')
Layout.preferredWidth: parent.width
wrapMode: Text.Wrap
}
RadioButton {
ButtonGroup.group: keystoregroup
property string keystoretype: 'createseed'
checked: true
text: qsTr('Create a new seed')
}
RadioButton {
ButtonGroup.group: keystoregroup
property string keystoretype: 'haveseed'
text: qsTr('I already have a seed')
}
}
}

27
electrum/plugins/trustedcoin/qml/Disclaimer.qml

@ -0,0 +1,27 @@
import QtQuick 2.6
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.1
import org.electrum 1.0
import "../../../gui/qml/components/wizard"
WizardComponent {
valid: true
property QtObject plugin
ColumnLayout {
width: parent.width
Label {
Layout.preferredWidth: parent.width
text: plugin ? plugin.disclaimer : ''
wrapMode: Text.Wrap
}
}
Component.onCompleted: {
plugin = AppController.plugin('trustedcoin')
}
}

35
electrum/plugins/trustedcoin/qml/KeepDisable.qml

@ -0,0 +1,35 @@
import QtQuick 2.6
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.1
import "../../../gui/qml/components/wizard"
WizardComponent {
valid: keepordisablegroup.checkedButton
function apply() {
wizard_data['trustedcoin_keepordisable'] = keepordisablegroup.checkedButton.keepordisable
}
ButtonGroup {
id: keepordisablegroup
onCheckedButtonChanged: checkIsLast()
}
ColumnLayout {
Label {
text: qsTr('Restore 2FA wallet')
}
RadioButton {
ButtonGroup.group: keepordisablegroup
property string keepordisable: 'keep'
checked: true
text: qsTr('Keep')
}
RadioButton {
ButtonGroup.group: keepordisablegroup
property string keepordisable: 'disable'
text: qsTr('Disable')
}
}
}

137
electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml

@ -0,0 +1,137 @@
import QtQuick 2.6
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.1
import "../../../gui/qml/components/wizard"
import "../../../gui/qml/components/controls"
WizardComponent {
valid: otpVerified
property QtObject plugin
property bool otpVerified: false
function apply() {
wizard_data['trustedcoin_new_otp_secret'] = requestNewSecret.checked
}
ColumnLayout {
width: parent.width
Label {
text: qsTr('Authenticator secret')
}
InfoTextArea {
id: errorBox
iconStyle: InfoTextArea.IconStyle.Error
visible: !otpVerified && plugin.remoteKeyState == 'error'
}
InfoTextArea {
iconStyle: InfoTextArea.IconStyle.Warn
visible: plugin.remoteKeyState == 'wallet_known'
text: qsTr('This wallet is already registered with TrustedCoin. ')
+ qsTr('To finalize wallet creation, please enter your Google Authenticator Code. ')
}
QRImage {
Layout.alignment: Qt.AlignHCenter
visible: plugin.remoteKeyState == ''
qrdata: encodeURI('otpauth://totp/Electrum 2FA ' + wizard_data['wallet_name']
+ '?secret=' + plugin.otpSecret + '&digits=6')
render: plugin.otpSecret
}
TextHighlightPane {
Layout.alignment: Qt.AlignHCenter
visible: plugin.otpSecret
Label {
text: plugin.otpSecret
font.family: FixedFont
font.bold: true
}
}
Label {
visible: !otpVerified && plugin.otpSecret
Layout.preferredWidth: parent.width
wrapMode: Text.Wrap
text: qsTr('Enter or scan into authenticator app. Then authenticate below')
}
Label {
visible: !otpVerified && plugin.remoteKeyState == 'wallet_known'
Layout.preferredWidth: parent.width
wrapMode: Text.Wrap
text: qsTr('If you still have your OTP secret, then authenticate below')
}
TextField {
id: otp_auth
visible: !otpVerified && (plugin.otpSecret || plugin.remoteKeyState == 'wallet_known')
Layout.alignment: Qt.AlignHCenter
focus: true
inputMethodHints: Qt.ImhSensitiveData | Qt.ImhDigitsOnly
font.family: FixedFont
font.pixelSize: constants.fontSizeLarge
onTextChanged: {
if (text.length >= 6) {
plugin.checkOtp(plugin.shortId, otp_auth.text)
text = ''
}
}
}
Label {
visible: !otpVerified && plugin.remoteKeyState == 'wallet_known'
Layout.preferredWidth: parent.width
wrapMode: Text.Wrap
text: qsTr('Otherwise, you can request your OTP secret from the server, by pressing the button below')
}
Button {
Layout.alignment: Qt.AlignHCenter
visible: plugin.remoteKeyState == 'wallet_known' && !otpVerified
text: qsTr('Request OTP secret')
onClicked: plugin.resetOtpSecret()
}
Image {
Layout.alignment: Qt.AlignHCenter
source: '../../../gui/icons/confirmed.png'
visible: otpVerified
Layout.preferredWidth: constants.iconSizeXLarge
Layout.preferredHeight: constants.iconSizeXLarge
}
}
BusyIndicator {
anchors.centerIn: parent
visible: plugin ? plugin.busy : false
running: visible
}
Component.onCompleted: {
plugin = AppController.plugin('trustedcoin')
plugin.createKeystore(wizard_data['2fa_email'])
otp_auth.forceActiveFocus()
}
Connections {
target: plugin
function onOtpError(message) {
console.log('OTP verify error')
errorBox.text = message
}
function onOtpSuccess() {
console.log('OTP verify success')
otpVerified = true
}
function onRemoteKeyError(message) {
errorBox.text = message
}
}
}

67
electrum/plugins/trustedcoin/qml/Terms.qml

@ -0,0 +1,67 @@
import QtQuick 2.6
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.1
import org.electrum 1.0
import "../../../gui/qml/components/wizard"
import "../../../gui/qml/components/controls"
WizardComponent {
valid: !plugin ? false
: email.text.length > 0 // TODO: validate email address
&& plugin.termsAndConditions
property QtObject plugin
onAccept: {
wizard_data['2fa_email'] = email.text
}
ColumnLayout {
anchors.fill: parent
Label { text: qsTr('Terms and conditions') }
TextHighlightPane {
Layout.fillWidth: true
Layout.fillHeight: true
rightPadding: 0
Flickable {
anchors.fill: parent
contentHeight: termsText.height
clip: true
boundsBehavior: Flickable.StopAtBounds
Label {
id: termsText
width: parent.width
rightPadding: constants.paddingSmall
wrapMode: Text.Wrap
text: plugin ? plugin.termsAndConditions : ''
}
ScrollIndicator.vertical: ScrollIndicator { }
}
BusyIndicator {
anchors.centerIn: parent
visible: plugin ? plugin.busy : false
running: visible
}
}
Label { text: qsTr('Email') }
TextField {
id: email
Layout.fillWidth: true
placeholderText: qsTr('Enter your email address')
}
}
Component.onCompleted: {
plugin = AppController.plugin('trustedcoin')
plugin.fetchTermsAndConditions()
}
}

7
electrum/plugins/trustedcoin/trustedcoin.py

@ -69,7 +69,7 @@ def get_billing_xpub():
return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU"
DISCLAIMER = [
DESKTOP_DISCLAIMER = [
_("Two-factor authentication is a service provided by TrustedCoin. "
"It uses a multi-signature wallet, where you own 2 of 3 keys. "
"The third key is stored on a remote server that signs transactions on "
@ -86,8 +86,9 @@ DISCLAIMER = [
"To be safe from malware, you may want to do this on an offline "
"computer, and move your wallet later to an online computer."),
]
DISCLAIMER = DESKTOP_DISCLAIMER
KIVY_DISCLAIMER = [
MOBILE_DISCLAIMER = [
_("Two-factor authentication is a service provided by TrustedCoin. "
"To use it, you must have a separate device with Google Authenticator."),
_("This service uses a multi-signature wallet, where you own 2 of 3 keys. "
@ -98,6 +99,8 @@ KIVY_DISCLAIMER = [
"your funds at any time and at no cost, without the remote server, by "
"using the 'restore wallet' option with your wallet seed."),
]
KIVY_DISCLAIMER = MOBILE_DISCLAIMER
RESTORE_MSG = _("Enter the seed for your 2-factor wallet:")
class TrustedCoinException(Exception):

319
electrum/wizard.py

@ -0,0 +1,319 @@
import copy
import os
from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any, Dict, Optional, Union
from electrum.logging import get_logger
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 bitcoin
class WizardViewState(NamedTuple):
view: str
wizard_data: Dict[str, Any]
params: Dict[str, Any]
class AbstractWizard:
# serve as a base for all UIs, so no qt
# encapsulate wizard state
# encapsulate navigation decisions, UI agnostic
# encapsulate stack, go backwards
# allow extend/override flow in subclasses e.g.
# - override: replace 'next' value to own fn
# - extend: add new keys to navmap, wire up flow by override
_logger = get_logger(__name__)
navmap = {}
_current = WizardViewState(None, {}, {})
_stack = [] # type: List[WizardViewState]
def navmap_merge(self, additional_navmap):
# NOTE: only merges one level deep. Deeper dict levels will overwrite
for k,v in additional_navmap.items():
if k in self.navmap:
self.navmap[k].update(v)
else:
self.navmap[k] = v
# from current view and wizard_data, resolve the new view
# returns WizardViewState tuple (view name, wizard_data, view params)
# view name is the string id of the view in the nav map
# wizard data is the (stacked) wizard data dict containing user input and choices
# view params are transient, meant for extra configuration of a view (e.g. info
# msg in a generic choice dialog)
# exception: stay on this view
def resolve_next(self, view, wizard_data):
assert view
self._logger.debug(f'view={view}')
assert view in self.navmap
nav = self.navmap[view]
if 'accept' in nav:
# allow python scope to append to wizard_data before
# adding to stack or finishing
if callable(nav['accept']):
nav['accept'](wizard_data)
else:
self._logger.error(f'accept handler for view {view} not callable')
if not 'next' in nav:
# finished
self.finished(wizard_data)
return (None, wizard_data, {})
nexteval = nav['next']
# simple string based next view
if isinstance(nexteval, str):
new_view = WizardViewState(nexteval, wizard_data, {})
else:
# handler fn based next view
nv = nexteval(wizard_data)
self._logger.debug(repr(nv))
# append wizard_data and params if not returned
if isinstance(nv, str):
new_view = WizardViewState(nv, wizard_data, {})
elif len(nv) == 1:
new_view = WizardViewState(nv[0], wizard_data, {})
elif len(nv) == 2:
new_view = WizardViewState(nv[0], nv[1], {})
else:
new_view = nv
self._stack.append(copy.deepcopy(self._current))
self._current = new_view
self._logger.debug(f'resolve_next view is {self._current.view}')
self._logger.debug('stack:' + repr(self._stack))
return new_view
def resolve_prev(self):
prev_view = self._stack.pop()
self._logger.debug(f'resolve_prev view is {prev_view}')
self._logger.debug('stack:' + repr(self._stack))
self._current = prev_view
return prev_view
# check if this view is the final view
def is_last_view(self, view, wizard_data):
assert view
assert view in self.navmap
nav = self.navmap[view]
if not 'last' in nav:
return False
lastnav = nav['last']
# bool literal
if isinstance(lastnav, bool):
return lastnav
elif callable(lastnav):
# handler fn based
l = lastnav(view, wizard_data)
self._logger.debug(f'view "{view}" last: {l}')
return l
else:
raise Exception('last handler for view {view} is not callable nor a bool literal')
def finished(self, wizard_data):
self._logger.debug('finished.')
def reset(self):
self.stack = []
self._current = WizardViewState(None, {}, {})
class NewWalletWizard(AbstractWizard):
_logger = get_logger(__name__)
def __init__(self, daemon):
self.navmap = {
'wallet_name': {
'next': 'wallet_type'
},
'wallet_type': {
'next': self.on_wallet_type
},
'keystore_type': {
'next': self.on_keystore_type
},
'create_seed': {
'next': 'confirm_seed'
},
'confirm_seed': {
'next': 'wallet_password',
'last': self.last_if_single_password
},
'have_seed': {
'next': self.on_have_seed,
'last': self.last_if_single_password_and_not_bip39
},
'bip39_refine': {
'next': 'wallet_password',
'last': self.last_if_single_password
},
'have_master_key': {
'next': 'wallet_password',
'last': self.last_if_single_password
},
'imported': {
'next': 'wallet_password',
'last': self.last_if_single_password
},
'wallet_password': {
'last': True
}
}
self._daemon = daemon
def start(self, initial_data = {}):
self.reset()
self._current = WizardViewState('wallet_name', initial_data, {})
return self._current
def last_if_single_password(self, view, wizard_data):
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 on_wallet_type(self, wizard_data):
t = wizard_data['wallet_type']
return {
'standard': 'keystore_type',
'2fa': 'trustedcoin_start',
'imported': 'imported'
}.get(t)
def on_keystore_type(self, wizard_data):
t = wizard_data['keystore_type']
return {
'createseed': 'create_seed',
'haveseed': 'have_seed',
'masterkey': 'have_master_key'
}.get(t)
def on_have_seed(self, wizard_data):
if (wizard_data['seed_type'] == 'bip39'):
return 'bip39_refine'
else:
return 'wallet_password'
def finished(self, wizard_data):
self._logger.debug('finished')
# override
def create_storage(self, path, data):
# only standard and 2fa wallets for now
assert data['wallet_type'] in ['standard', '2fa', 'imported']
if os.path.exists(path):
raise Exception('file already exists at path')
storage = WalletStorage(path)
k = None
if not 'keystore_type' in data:
assert data['wallet_type'] == 'imported'
addresses = {}
if 'private_key_list' in data:
k = keystore.Imported_KeyStore({})
keys = keystore.get_private_keys(data['private_key_list'])
for pk in keys:
assert bitcoin.is_private_key(pk)
txin_type, pubkey = k.import_privkey(pk, None)
addr = bitcoin.pubkey_to_address(txin_type, pubkey)
addresses[addr] = {'type': txin_type, 'pubkey': pubkey}
elif 'address_list' in data:
for addr in data['address_list'].split():
addresses[addr] = {}
elif data['keystore_type'] in ['createseed', 'haveseed']:
if data['seed_type'] in ['old', 'standard', 'segwit']:
self._logger.debug('creating keystore from electrum seed')
k = keystore.from_seed(data['seed'], data['seed_extra_words'], data['wallet_type'] == 'multisig')
elif data['seed_type'] == 'bip39':
self._logger.debug('creating keystore from bip39 seed')
root_seed = keystore.bip39_to_seed(data['seed'], data['seed_extra_words'])
derivation = normalize_bip32_derivation(data['derivation_path'])
script = data['script_type'] if data['script_type'] != 'p2pkh' else 'standard'
k = keystore.from_bip43_rootseed(root_seed, derivation, xtype=script)
elif data['seed_type'] == '2fa_segwit': # TODO: legacy 2fa '2fa'
self._logger.debug('creating keystore from 2fa seed')
k = keystore.from_xprv(data['x1/']['xprv'])
else:
raise Exception('unsupported/unknown seed_type %s' % data['seed_type'])
elif data['keystore_type'] == 'masterkey':
k = keystore.from_master_key(data['master_key'])
has_xpub = isinstance(k, keystore.Xpub)
assert has_xpub
t1 = xpub_type(k.xpub)
if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']:
raise Exception('wrong key type %s' % t1)
else:
raise Exception('unsupported/unknown keystore_type %s' % data['keystore_type'])
if data['encrypt']:
if k and k.may_have_password():
k.update_password(None, data['password'])
storage.set_password(data['password'], enc_version=StorageEncryptionVersion.USER_PASSWORD)
db = WalletDB('', manual_upgrades=False)
db.set_keystore_encryption(bool(data['password']) and data['encrypt'])
db.put('wallet_type', data['wallet_type'])
if 'seed_type' in data:
db.put('seed_type', data['seed_type'])
if data['wallet_type'] == 'standard':
db.put('keystore', k.dump())
elif data['wallet_type'] == '2fa':
db.put('x1/', k.dump())
if data['trustedcoin_keepordisable'] == 'disable':
k2 = keystore.from_xprv(data['x2/']['xprv'])
if data['encrypt'] and k2.may_have_password():
k2.update_password(None, data['password'])
db.put('x2/', k2.dump())
else:
db.put('x2/', data['x2/'])
db.put('x3/', data['x3/'])
db.put('use_trustedcoin', True)
elif data['wallet_type'] == 'imported':
if k:
db.put('keystore', k.dump())
db.put('addresses', addresses)
if k and k.can_have_deterministic_lightning_xprv():
db.put('lightning_xprv', k.get_lightning_xprv(data['password'] if data['encrypt'] else None))
db.load_plugins()
db.write(storage)
class ServerConnectWizard(AbstractWizard):
_logger = get_logger(__name__)
def __init__(self, daemon):
self.navmap = {
'autoconnect': {
'next': 'proxy_config',
'last': lambda v,d: d['autoconnect']
},
'proxy_config': {
'next': 'server_config'
},
'server_config': {
'last': True
}
}
self._daemon = daemon
def start(self, initial_data = {}):
self.reset()
self._current = WizardViewState('autoconnect', initial_data, {})
return self._current
Loading…
Cancel
Save