diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py
index 821de7536..250dc7b58 100644
--- a/electrum/gui/qml/__init__.py
+++ b/electrum/gui/qml/__init__.py
@@ -31,10 +31,6 @@ if TYPE_CHECKING:
from .qeapp import ElectrumQmlApplication
-class UncaughtException(Exception):
- pass
-
-
class ElectrumGui(Logger):
@profiler
diff --git a/electrum/gui/qml/components/NewWalletWizard.qml b/electrum/gui/qml/components/NewWalletWizard.qml
index cb7bd1af3..a391a2525 100644
--- a/electrum/gui/qml/components/NewWalletWizard.qml
+++ b/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()
+ }
}
}
diff --git a/electrum/gui/qml/components/OtpDialog.qml b/electrum/gui/qml/components/OtpDialog.qml
new file mode 100644
index 000000000..e971d4c24
--- /dev/null
+++ b/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
+ }
+}
diff --git a/electrum/gui/qml/components/ServerConnectWizard.qml b/electrum/gui/qml/components/ServerConnectWizard.qml
index 0f80d47c7..f126a831f 100644
--- a/electrum/gui/qml/components/ServerConnectWizard.qml
+++ b/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 {}
- }
-
}
diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml
index e7eb1455b..1ab844005 100644
--- a/electrum/gui/qml/components/WalletMainView.qml
+++ b/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()
+ }
+ }
}
diff --git a/electrum/gui/qml/components/Wallets.qml b/electrum/gui/qml/components/Wallets.qml
index 4b2954d90..583c3422d 100644
--- a/electrum/gui/qml/components/Wallets.qml
+++ b/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
diff --git a/electrum/gui/qml/components/wizard/WCAutoConnect.qml b/electrum/gui/qml/components/wizard/WCAutoConnect.qml
index 9b15d533c..51c4625a7 100644
--- a/electrum/gui/qml/components/wizard/WCAutoConnect.qml
+++ b/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')
}
diff --git a/electrum/gui/qml/components/wizard/WCBIP39Refine.qml b/electrum/gui/qml/components/wizard/WCBIP39Refine.qml
index 27cb36dd4..2c4dc8ef2 100644
--- a/electrum/gui/qml/components/wizard/WCBIP39Refine.qml
+++ b/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.')
}
diff --git a/electrum/gui/qml/components/wizard/WCCreateSeed.qml b/electrum/gui/qml/components/wizard/WCCreateSeed.qml
index 7c172842e..ee8c3ba1e 100644
--- a/electrum/gui/qml/components/wizard/WCCreateSeed.qml
+++ b/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: {
diff --git a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml
index 904432fde..f97abd7f1 100644
--- a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml
+++ b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml
@@ -11,7 +11,7 @@ WizardComponent {
valid: false
- onAccept: {
+ function apply() {
wizard_data['master_key'] = masterkey_ta.text
}
diff --git a/electrum/gui/qml/components/wizard/WCHaveSeed.qml b/electrum/gui/qml/components/wizard/WCHaveSeed.qml
index a5487cffa..2d140f98d 100644
--- a/electrum/gui/qml/components/wizard/WCHaveSeed.qml
+++ b/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.'),
'
',
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.'),
'
',
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
+ }
}
diff --git a/electrum/gui/qml/components/wizard/WCImport.qml b/electrum/gui/qml/components/wizard/WCImport.qml
new file mode 100644
index 000000000..5158d3fd5
--- /dev/null
+++ b/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
+ }
+
+}
diff --git a/electrum/gui/qml/components/wizard/WCKeystoreType.qml b/electrum/gui/qml/components/wizard/WCKeystoreType.qml
index e8837fe9d..30cb41a9d 100644
--- a/electrum/gui/qml/components/wizard/WCKeystoreType.qml
+++ b/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
}
diff --git a/electrum/gui/qml/components/wizard/WCProxyConfig.qml b/electrum/gui/qml/components/wizard/WCProxyConfig.qml
index ddc0fc60d..2943e1b3d 100644
--- a/electrum/gui/qml/components/wizard/WCProxyConfig.qml
+++ b/electrum/gui/qml/components/wizard/WCProxyConfig.qml
@@ -3,7 +3,7 @@ import "../controls"
WizardComponent {
valid: true
- onAccept: {
+ function apply() {
wizard_data['proxy'] = pc.toProxyDict()
}
diff --git a/electrum/gui/qml/components/wizard/WCServerConfig.qml b/electrum/gui/qml/components/wizard/WCServerConfig.qml
index b1a3afea5..939eb1f06 100644
--- a/electrum/gui/qml/components/wizard/WCServerConfig.qml
+++ b/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
}
diff --git a/electrum/gui/qml/components/wizard/WCWalletName.qml b/electrum/gui/qml/components/wizard/WCWalletName.qml
index 6f7cbf784..07d12f8bb 100644
--- a/electrum/gui/qml/components/wizard/WCWalletName.qml
+++ b/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
}
diff --git a/electrum/gui/qml/components/wizard/WCWalletPassword.qml b/electrum/gui/qml/components/wizard/WCWalletPassword.qml
index b6c55c69d..5e4921ac5 100644
--- a/electrum/gui/qml/components/wizard/WCWalletPassword.qml
+++ b/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 != ''
}
diff --git a/electrum/gui/qml/components/wizard/WCWalletType.qml b/electrum/gui/qml/components/wizard/WCWalletType.qml
index e7c02dd61..a8fc832a4 100644
--- a/electrum/gui/qml/components/wizard/WCWalletType.qml
+++ b/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')
}
}
diff --git a/electrum/gui/qml/components/wizard/Wizard.qml b/electrum/gui/qml/components/wizard/Wizard.qml
index 1caf02899..6a80d1fb4 100644
--- a/electrum/gui/qml/components/wizard/Wizard.qml
+++ b/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() {
diff --git a/electrum/gui/qml/components/wizard/WizardComponent.qml b/electrum/gui/qml/components/wizard/WizardComponent.qml
index 798b7ad8d..25a9b7072 100644
--- a/electrum/gui/qml/components/wizard/WizardComponent.qml
+++ b/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()
+ }
+
}
diff --git a/electrum/gui/qml/qeaddresslistmodel.py b/electrum/gui/qml/qeaddresslistmodel.py
index c82802fbc..1a8881bad 100644
--- a/electrum/gui/qml/qeaddresslistmodel.py
+++ b/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()
diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py
index 273ac8e03..c21aefee3 100644
--- a/electrum/gui/qml/qeapp.py
+++ b/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
diff --git a/electrum/gui/qml/qebitcoin.py b/electrum/gui/qml/qebitcoin.py
index b3f9cd82c..70dd6437f 100644
--- a/electrum/gui/qml/qebitcoin.py
+++ b/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)
+
diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py
index 7e5e2562c..5ae0e8232 100644
--- a/electrum/gui/qml/qedaemon.py
+++ b/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
diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py
index 1d0138a6b..329bd8a73 100644
--- a/electrum/gui/qml/qewallet.py
+++ b/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)
diff --git a/electrum/gui/qml/qewalletdb.py b/electrum/gui/qml/qewalletdb.py
index 33a736117..52ff68c01 100644
--- a/electrum/gui/qml/qewalletdb.py
+++ b/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))
diff --git a/electrum/gui/qml/qewizard.py b/electrum/gui/qml/qewizard.py
new file mode 100644
index 000000000..1f5daa8f8
--- /dev/null
+++ b/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' },
+ })
diff --git a/electrum/plugins/qml_test/__init__.py b/electrum/plugins/qml_test/__init__.py
deleted file mode 100644
index 62e390176..000000000
--- a/electrum/plugins/qml_test/__init__.py
+++ /dev/null
@@ -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']
diff --git a/electrum/plugins/qml_test/qml.py b/electrum/plugins/qml_test/qml.py
deleted file mode 100644
index 0d5233812..000000000
--- a/electrum/plugins/qml_test/qml.py
+++ /dev/null
@@ -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')
diff --git a/electrum/plugins/trustedcoin/__init__.py b/electrum/plugins/trustedcoin/__init__.py
index 81ec4f2ce..e04ed9dc8 100644
--- a/electrum/plugins/trustedcoin/__init__.py
+++ b/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']
diff --git a/electrum/plugins/trustedcoin/qml.py b/electrum/plugins/trustedcoin/qml.py
new file mode 100644
index 000000000..85b7e11b0
--- /dev/null
+++ b/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)
diff --git a/electrum/plugins/trustedcoin/qml/ChooseSeed.qml b/electrum/plugins/trustedcoin/qml/ChooseSeed.qml
new file mode 100644
index 000000000..8665dee27
--- /dev/null
+++ b/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')
+ }
+ }
+}
+
diff --git a/electrum/plugins/trustedcoin/qml/Disclaimer.qml b/electrum/plugins/trustedcoin/qml/Disclaimer.qml
new file mode 100644
index 000000000..b1a1f2884
--- /dev/null
+++ b/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')
+ }
+}
diff --git a/electrum/plugins/trustedcoin/qml/KeepDisable.qml b/electrum/plugins/trustedcoin/qml/KeepDisable.qml
new file mode 100644
index 000000000..3e10d5175
--- /dev/null
+++ b/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')
+ }
+ }
+}
diff --git a/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml b/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml
new file mode 100644
index 000000000..849111a12
--- /dev/null
+++ b/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
+ }
+ }
+}
+
diff --git a/electrum/plugins/trustedcoin/qml/Terms.qml b/electrum/plugins/trustedcoin/qml/Terms.qml
new file mode 100644
index 000000000..d0e30ffa6
--- /dev/null
+++ b/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()
+ }
+}
diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py
index f5dfdb343..34a1be886 100644
--- a/electrum/plugins/trustedcoin/trustedcoin.py
+++ b/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):
diff --git a/electrum/wizard.py b/electrum/wizard.py
new file mode 100644
index 000000000..8f8d6378c
--- /dev/null
+++ b/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