diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 342221e6c..9663f9d69 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -6,7 +6,7 @@ import QtQml 2.6 Item { id: rootItem - property string title: Daemon.currentWallet.name + property string title: Daemon.currentWallet ? Daemon.currentWallet.name : '' property QtObject menu: Menu { id: menu diff --git a/electrum/gui/qml/components/Wallets.qml b/electrum/gui/qml/components/Wallets.qml index f38fe29ae..ec19f195f 100644 --- a/electrum/gui/qml/components/Wallets.qml +++ b/electrum/gui/qml/components/Wallets.qml @@ -41,7 +41,8 @@ Pane { } function changePassword() { - // TODO: show set password dialog + // trigger dialog via wallet (auth then signal) + Daemon.currentWallet.start_change_password() } property QtObject menu: Menu { @@ -58,7 +59,7 @@ Pane { id: changePasswordComp MenuItem { icon.color: 'transparent' - enabled: false + enabled: Daemon.currentWallet // != null action: Action { text: qsTr('Change Password'); onTriggered: rootItem.changePassword() @@ -308,6 +309,23 @@ Pane { } } + Connections { + target: Daemon.currentWallet + function onRequestNewPassword() { // new wallet password + var dialog = app.passwordDialog.createObject(app, + { + 'confirmPassword': true, + 'title': qsTr('Enter new password'), + 'infotext': qsTr('If you forget your password, you\'ll need to\ + restore from seed. Please make sure you have your seed stored safely') + } ) + dialog.accepted.connect(function() { + Daemon.currentWallet.set_password(dialog.password) + }) + dialog.open() + } + } + Component { id: share GenericShareDialog { diff --git a/electrum/gui/qml/components/controls/PasswordDialog.qml b/electrum/gui/qml/components/controls/PasswordDialog.qml new file mode 100644 index 000000000..563f9c9a4 --- /dev/null +++ b/electrum/gui/qml/components/controls/PasswordDialog.qml @@ -0,0 +1,115 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.3 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +Dialog { + id: passworddialog + + title: qsTr("Enter Password") + + property bool confirmPassword: false + property string password + property string infotext + + parent: Overlay.overlay + modal: true + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + Overlay.modal: Rectangle { + color: "#aa000000" + } + + header: GridLayout { + columns: 2 + rowSpacing: 0 + + Image { + source: "../../../icons/lock.png" + Layout.preferredWidth: constants.iconSizeXLarge + Layout.preferredHeight: constants.iconSizeXLarge + Layout.leftMargin: constants.paddingMedium + Layout.topMargin: constants.paddingMedium + Layout.bottomMargin: constants.paddingMedium + } + + Label { + text: title + elide: Label.ElideRight + Layout.fillWidth: true + topPadding: constants.paddingXLarge + bottomPadding: constants.paddingXLarge + font.bold: true + font.pixelSize: constants.fontSizeMedium + } + + Rectangle { + Layout.columnSpan: 2 + Layout.fillWidth: true + Layout.leftMargin: constants.paddingXXSmall + Layout.rightMargin: constants.paddingXXSmall + height: 1 + color: Qt.rgba(0,0,0,0.5) + } + } + + ColumnLayout { + width: parent.width + + InfoTextArea { + visible: infotext + text: infotext + Layout.preferredWidth: password_layout.width + } + + GridLayout { + id: password_layout + columns: 2 + Layout.fillWidth: true + Layout.margins: constants.paddingXXLarge + + Label { + text: qsTr('Password') + } + + TextField { + id: pw_1 + echoMode: TextInput.Password + } + + Label { + text: qsTr('Password (again)') + visible: confirmPassword + } + + TextField { + id: pw_2 + echoMode: TextInput.Password + visible: confirmPassword + } + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: constants.paddingXXLarge + + Button { + text: qsTr("Ok") + enabled: confirmPassword ? pw_1.text == pw_2.text : true + onClicked: { + password = pw_1.text + passworddialog.accept() + } + } + Button { + text: qsTr("Cancel") + onClicked: { + passworddialog.reject() + } + } + } + } + +} diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 606f2aa1d..9320857d4 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -171,6 +171,14 @@ ApplicationWindow } } + property alias passwordDialog: _passwordDialog + Component { + id: _passwordDialog + PasswordDialog { + onClosed: destroy() + } + } + NotificationPopup { id: notificationPopup } @@ -225,14 +233,23 @@ ApplicationWindow Connections { target: Daemon.currentWallet function onAuthRequired() { - var dialog = app.messageDialog.createObject(app, {'text': 'Auth placeholder', 'yesno': true}) - dialog.yesClicked.connect(function() { + if (Daemon.currentWallet.verify_password('')) { + // wallet has no password Daemon.currentWallet.authProceed() - }) - dialog.noClicked.connect(function() { - Daemon.currentWallet.authCancel() - }) - dialog.open() + } else { + var dialog = app.passwordDialog.createObject(app, {'title': qsTr('Enter current password')}) + dialog.accepted.connect(function() { + if (Daemon.currentWallet.verify_password(dialog.password)) { + Daemon.currentWallet.authProceed() + } else { + Daemon.currentWallet.authCancel() + } + }) + dialog.rejected.connect(function() { + Daemon.currentWallet.authCancel() + }) + dialog.open() + } } } diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 6858982d5..75893e9cb 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -52,6 +52,8 @@ class QEAppController(QObject): def on_wallet_loaded(self): qewallet = self._qedaemon.currentWallet + if not qewallet: + return # attach to the wallet user notification events # connect only once try: diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 28d602477..c531a5df4 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -152,7 +152,12 @@ class QEDaemon(AuthMixin, QObject): path = wallet.wallet.storage.path self._logger.debug('Ok to delete wallet with path %s' % path) # TODO checks, e.g. existing LN channels, unpaid requests, etc + self._logger.debug('Not deleting yet, just unloading for now') + # TODO actually delete + # TODO walletLoaded signal is confusing self.daemon.stop_wallet(path) + self._current_wallet = None + self.walletLoaded.emit() @pyqtProperty('QString') def path(self): diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 8333ac501..b72dd2012 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -7,9 +7,10 @@ import threading from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QTimer from electrum.i18n import _ -from electrum.util import register_callback, Satoshis, format_time, parse_max_spend +from electrum.util import register_callback, Satoshis, format_time, parse_max_spend, InvalidPassword from electrum.logging import get_logger from electrum.wallet import Wallet, Abstract_Wallet +from electrum.storage import StorageEncryptionVersion from electrum import bitcoin from electrum.transaction import PartialTxOutput from electrum.invoices import (Invoice, InvoiceError, @@ -331,7 +332,7 @@ class QEWallet(AuthMixin, QObject): self.wallet.init_lightning(password=None) # TODO pass password if needed self.isLightningChanged.emit() - @pyqtSlot('QString', int, int, bool) + @pyqtSlot(str, int, int, bool) def send_onchain(self, address, amount, fee=None, rbf=False): self._logger.info('send_onchain: %s %d' % (address,amount)) coins = self.wallet.get_spendable_coins(None) @@ -437,9 +438,9 @@ class QEWallet(AuthMixin, QObject): return req_key, addr - @pyqtSlot(QEAmount, 'QString', int) - @pyqtSlot(QEAmount, 'QString', int, bool) - @pyqtSlot(QEAmount, 'QString', int, bool, bool) + @pyqtSlot(QEAmount, str, int) + @pyqtSlot(QEAmount, str, int, bool) + @pyqtSlot(QEAmount, str, int, bool, bool) def create_request(self, amount: QEAmount, message: str, expiration: int, is_lightning: bool = False, ignore_gap: bool = False): try: if is_lightning: @@ -463,29 +464,52 @@ class QEWallet(AuthMixin, QObject): self._requestModel.add_invoice(self.wallet.get_request(key)) self.requestCreateSuccess.emit() - @pyqtSlot('QString') + @pyqtSlot(str) def delete_request(self, key: str): self._logger.debug('delete req %s' % key) self.wallet.delete_request(key) self._requestModel.delete_invoice(key) - @pyqtSlot('QString', result='QVariant') + @pyqtSlot(str, result='QVariant') def get_request(self, key: str): return self._requestModel.get_model_invoice(key) - @pyqtSlot('QString') + @pyqtSlot(str) def delete_invoice(self, key: str): self._logger.debug('delete inv %s' % key) self.wallet.delete_invoice(key) self._invoiceModel.delete_invoice(key) - @pyqtSlot('QString', result='QVariant') + @pyqtSlot(str, result='QVariant') def get_invoice(self, key: str): return self._invoiceModel.get_model_invoice(key) - @pyqtSlot(str) + @pyqtSlot(str, result=bool) + def verify_password(self, password): + try: + self.wallet.storage.check_password(password) + return True + except InvalidPassword as e: + return False + + requestNewPassword = pyqtSignal() + @pyqtSlot() @auth_protect + def start_change_password(self): + self.requestNewPassword.emit() + + @pyqtSlot(str) def set_password(self, password): storage = self.wallet.storage + + # HW wallet not supported yet + if storage.is_encrypted_with_hw_device(): + return + self._logger.debug('Ok to set password for wallet with path %s' % storage.path) - # TODO + if password: + enc_version = StorageEncryptionVersion.USER_PASSWORD + else: + enc_version = StorageEncryptionVersion.PLAINTEXT + storage.set_password(password, enc_version=enc_version) + self.wallet.save_db()