From 1a2675b24669e51ac79324d2889494b95eddef62 Mon Sep 17 00:00:00 2001 From: Dusan Klinec Date: Wed, 27 Mar 2019 09:28:42 +0100 Subject: [PATCH] async device open and create from device, passphrase - passphrase entry on host added, requires early listener setting monero pull #5355 - wallet open and create from device shows splash to indicate possible long process - create from device is async to support passphrase entry --- components/PassphraseDialog.qml | 327 ++++++++++++++++++++++++++++++ components/Style.qml | 1 + main.qml | 65 ++++++ qml.qrc | 1 + src/libwalletqt/Wallet.cpp | 10 + src/libwalletqt/Wallet.h | 2 + src/libwalletqt/WalletManager.cpp | 110 +++++++++- src/libwalletqt/WalletManager.h | 21 ++ wizard/WizardController.qml | 65 +++++- wizard/WizardCreateDevice1.qml | 19 +- 10 files changed, 607 insertions(+), 14 deletions(-) create mode 100644 components/PassphraseDialog.qml diff --git a/components/PassphraseDialog.qml b/components/PassphraseDialog.qml new file mode 100644 index 00000000..c7af29f9 --- /dev/null +++ b/components/PassphraseDialog.qml @@ -0,0 +1,327 @@ +// Copyright (c) 2014-2019, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import QtQuick 2.7 +import QtQuick.Controls 2.0 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.1 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Window 2.0 + +import "../components" as MoneroComponents + +Item { + id: root + visible: false + z: parent.z + 2 + + property bool isHidden: true + property alias passphrase: passphaseInput1.text + property string walletName + property string errorText + + // same signals as Dialog has + signal accepted() + signal rejected() + signal closeCallback() + + function open(walletName, errorText) { + inactiveOverlay.visible = true + + root.walletName = walletName ? walletName : "" + root.errorText = errorText ? errorText : ""; + + leftPanel.enabled = false + middlePanel.enabled = false + titleBar.enabled = false + show(); + root.visible = true; + passphaseInput1.text = ""; + passphaseInput2.text = ""; + passphaseInput1.focus = true + } + + function close() { + inactiveOverlay.visible = false + leftPanel.enabled = true + middlePanel.enabled = true + titleBar.enabled = true + root.visible = false; + closeCallback(); + } + + function toggleIsHidden() { + passphaseInput1.echoMode = isHidden ? TextInput.Normal : TextInput.Password; + passphaseInput2.echoMode = isHidden ? TextInput.Normal : TextInput.Password; + isHidden = !isHidden; + } + + function showError(errorText) { + open(root.walletName, errorText); + } + + // TODO: implement without hardcoding sizes + width: 480 + height: 360 + + // Make window draggable + MouseArea { + anchors.fill: parent + property point lastMousePos: Qt.point(0, 0) + onPressed: { lastMousePos = Qt.point(mouseX, mouseY); } + onMouseXChanged: root.x += (mouseX - lastMousePos.x) + onMouseYChanged: root.y += (mouseY - lastMousePos.y) + } + + ColumnLayout { + z: inactiveOverlay.z + 1 + id: mainLayout + spacing: 10 + anchors { fill: parent; margins: 35 * scaleRatio } + + ColumnLayout { + id: column + + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: 400 * scaleRatio + + Label { + text: root.walletName.length > 0 ? qsTr("Please enter wallet device passphrase for: ") + root.walletName : qsTr("Please enter wallet device passphrase") + Layout.fillWidth: true + + font.pixelSize: 16 * scaleRatio + font.family: MoneroComponents.Style.fontLight.name + + color: MoneroComponents.Style.defaultFontColor + } + + Label { + text: qsTr("Warning: passphrase entry on host is a security risk as it can be captured by malware. It is advised to prefer device-based passphrase entry."); + Layout.fillWidth: true + wrapMode: Text.Wrap + + font.pixelSize: 14 * scaleRatio + font.family: MoneroComponents.Style.fontLight.name + + color: MoneroComponents.Style.warningColor + } + + Label { + text: root.errorText + visible: root.errorText + + color: MoneroComponents.Style.errorColor + font.pixelSize: 16 * scaleRatio + font.family: MoneroComponents.Style.fontLight.name + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + TextField { + id : passphaseInput1 + Layout.topMargin: 6 + Layout.fillWidth: true + horizontalAlignment: TextInput.AlignLeft + verticalAlignment: TextInput.AlignVCenter + font.family: MoneroComponents.Style.fontLight.name + font.pixelSize: 24 * scaleRatio + echoMode: TextInput.Password + bottomPadding: 10 + leftPadding: 10 + topPadding: 10 + color: MoneroComponents.Style.defaultFontColor + selectionColor: MoneroComponents.Style.dimmedFontColor + selectedTextColor: MoneroComponents.Style.defaultFontColor + KeyNavigation.tab: passphaseInput2 + + background: Rectangle { + radius: 2 + border.color: Qt.rgba(255, 255, 255, 0.35) + border.width: 1 + color: "black" + + Image { + width: 26 * scaleRatio + height: 26 * scaleRatio + opacity: 0.7 + fillMode: Image.PreserveAspectFit + source: isHidden ? "../images/eyeShow.png" : "../images/eyeHide.png" + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: 20 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + toggleIsHidden() + } + onEntered: { + parent.opacity = 0.9 + parent.width = 28 * scaleRatio + parent.height = 28 * scaleRatio + } + onExited: { + parent.opacity = 0.7 + parent.width = 26 * scaleRatio + parent.height = 26 * scaleRatio + } + } + } + } + + Keys.onEscapePressed: { + root.close() + root.rejected() + } + } + + // padding + Rectangle { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + height: 10 + opacity: 0 + color: "black" + } + + Label { + text: qsTr("Please re-enter") + Layout.fillWidth: true + + font.pixelSize: 16 * scaleRatio + font.family: MoneroComponents.Style.fontLight.name + + color: MoneroComponents.Style.defaultFontColor + } + + TextField { + id : passphaseInput2 + Layout.topMargin: 6 + Layout.fillWidth: true + horizontalAlignment: TextInput.AlignLeft + verticalAlignment: TextInput.AlignVCenter + font.family: MoneroComponents.Style.fontLight.name + font.pixelSize: 24 * scaleRatio + echoMode: TextInput.Password + KeyNavigation.tab: okButton + bottomPadding: 10 + leftPadding: 10 + topPadding: 10 + color: MoneroComponents.Style.defaultFontColor + selectionColor: MoneroComponents.Style.dimmedFontColor + selectedTextColor: MoneroComponents.Style.defaultFontColor + + background: Rectangle { + radius: 2 + border.color: Qt.rgba(255, 255, 255, 0.35) + border.width: 1 + color: "black" + + Image { + width: 26 * scaleRatio + height: 26 * scaleRatio + opacity: 0.7 + fillMode: Image.PreserveAspectFit + source: isHidden ? "../images/eyeShow.png" : "../images/eyeHide.png" + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: 20 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + toggleIsHidden() + } + onEntered: { + parent.opacity = 0.9 + parent.width = 28 * scaleRatio + parent.height = 28 * scaleRatio + } + onExited: { + parent.opacity = 0.7 + parent.width = 26 * scaleRatio + parent.height = 26 * scaleRatio + } + } + } + } + + Keys.onReturnPressed: { + if (passphaseInput1.text === passphaseInput2.text) { + root.close() + root.accepted() + } + } + Keys.onEscapePressed: { + root.close() + root.rejected() + } + } + + // padding + Rectangle { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + height: 10 + opacity: 0 + color: "black" + } + + // Ok/Cancel buttons + RowLayout { + id: buttons + spacing: 16 * scaleRatio + Layout.topMargin: 16 + Layout.alignment: Qt.AlignRight + + MoneroComponents.StandardButton { + id: cancelButton + text: qsTr("Cancel") + translationManager.emptyString + KeyNavigation.tab: passphaseInput1 + onClicked: { + root.close() + root.rejected() + } + } + MoneroComponents.StandardButton { + id: okButton + text: qsTr("Continue") + KeyNavigation.tab: cancelButton + enabled: passphaseInput1.text === passphaseInput2.text + onClicked: { + root.close() + root.accepted() + } + } + } + } + } +} diff --git a/components/Style.qml b/components/Style.qml index 52b49287..cc106239 100644 --- a/components/Style.qml +++ b/components/Style.qml @@ -17,6 +17,7 @@ QtObject { property string defaultFontColor: "white" property string dimmedFontColor: "#BBBBBB" property string lightGreyFontColor: "#DFDFDF" + property string warningColor: "#963E00" property string errorColor: "#FA6800" property string inputBoxBackground: "black" property string inputBoxBackgroundError: "#FFDDDD" diff --git a/main.qml b/main.qml index f460204b..d78f6343 100644 --- a/main.qml +++ b/main.qml @@ -85,6 +85,8 @@ ApplicationWindow { property int disconnectedEpoch: 0 property int estimatedBlockchainSize: 75 // GB property alias viewState: rootItem.state + property string prevSplashText; + property bool splashDisplayedBeforeButtonRequest; property string remoteNodeService: { // support user-defined remote node aggregators @@ -266,6 +268,8 @@ ApplicationWindow { wallet_path = moneroAccountsDir + wallet_path; // console.log("opening wallet at: ", wallet_path, "with password: ", appWindow.walletPassword); console.log("opening wallet at: ", wallet_path, ", network type: ", persistentSettings.nettype == NetworkType.MAINNET ? "mainnet" : persistentSettings.nettype == NetworkType.TESTNET ? "testnet" : "stagenet"); + + this.onWalletOpening(); walletManager.openWalletAsync(wallet_path, walletPassword, persistentSettings.nettype, persistentSettings.kdfRounds); } @@ -286,6 +290,8 @@ ApplicationWindow { currentWallet.unconfirmedMoneyReceived.disconnect(onWalletUnconfirmedMoneyReceived) currentWallet.transactionCreated.disconnect(onTransactionCreated) currentWallet.connectionStatusChanged.disconnect(onWalletConnectionStatusChanged) + currentWallet.deviceButtonRequest.disconnect(onDeviceButtonRequest); + currentWallet.deviceButtonPressed.disconnect(onDeviceButtonPressed); middlePanel.paymentClicked.disconnect(handlePayment); middlePanel.sweepUnmixableClicked.disconnect(handleSweepUnmixable); middlePanel.getProofClicked.disconnect(handleGetProof); @@ -341,6 +347,8 @@ ApplicationWindow { currentWallet.unconfirmedMoneyReceived.connect(onWalletUnconfirmedMoneyReceived) currentWallet.transactionCreated.connect(onTransactionCreated) currentWallet.connectionStatusChanged.connect(onWalletConnectionStatusChanged) + currentWallet.deviceButtonRequest.connect(onDeviceButtonRequest); + currentWallet.deviceButtonPressed.connect(onDeviceButtonPressed); middlePanel.paymentClicked.connect(handlePayment); middlePanel.sweepUnmixableClicked.connect(handleSweepUnmixable); middlePanel.getProofClicked.connect(handleGetProof); @@ -422,7 +430,26 @@ ApplicationWindow { } } + function onDeviceButtonRequest(code){ + prevSplashText = splash.messageText; + splashDisplayedBeforeButtonRequest = splash.visible; + appWindow.showProcessingSplash(qsTr("Please proceed to the device...")); + } + + function onDeviceButtonPressed(){ + if (splashDisplayedBeforeButtonRequest){ + appWindow.showProcessingSplash(prevSplashText); + } else { + hideProcessingSplash(); + } + } + + function onWalletOpening(){ + appWindow.showProcessingSplash(qsTr("Opening wallet ...")); + } + function onWalletOpened(wallet) { + hideProcessingSplash(); walletName = usefulName(wallet.path) console.log(">>> wallet opened: " + wallet) if (wallet.status !== Wallet.Status_Ok) { @@ -470,9 +497,27 @@ ApplicationWindow { } function onWalletClosed(walletAddress) { + hideProcessingSplash(); console.log(">>> wallet closed: " + walletAddress) } + function onWalletPassphraseNeeded(){ + if(rootItem.state !== "normal") return; + + hideProcessingSplash(); + + console.log(">>> wallet passphrase needed: ") + passphraseDialog.onAcceptedCallback = function() { + walletManager.onPassphraseEntered(passphraseDialog.passphrase); + this.onWalletOpening(); + } + passphraseDialog.onRejectedCallback = function() { + walletManager.onPassphraseEntered("", true); + this.onWalletOpening(); + } + passphraseDialog.open() + } + function onWalletUpdate() { console.log(">>> wallet updated") updateBalance(); @@ -1017,7 +1062,10 @@ ApplicationWindow { // walletManager.walletOpened.connect(onWalletOpened); walletManager.walletClosed.connect(onWalletClosed); + walletManager.deviceButtonRequest.connect(onDeviceButtonRequest); + walletManager.deviceButtonPressed.connect(onDeviceButtonPressed); walletManager.checkUpdatesComplete.connect(onWalletCheckUpdatesComplete); + walletManager.walletPassphraseNeeded.connect(onWalletPassphraseNeeded); if(typeof daemonManager != "undefined") { daemonManager.daemonStarted.connect(onDaemonStarted); @@ -1245,6 +1293,23 @@ ApplicationWindow { } } + PassphraseDialog { + id: passphraseDialog + visible: false + z: parent.z + 1 + anchors.fill: parent + property var onAcceptedCallback + property var onRejectedCallback + onAccepted: { + if (onAcceptedCallback) + onAcceptedCallback(); + } + onRejected: { + if (onRejectedCallback) + onRejectedCallback(); + } + } + PasswordDialog { id: passwordDialog visible: false diff --git a/qml.qrc b/qml.qrc index 61f23378..6f5088cb 100644 --- a/qml.qrc +++ b/qml.qrc @@ -117,6 +117,7 @@ pages/TxKey.qml pages/SharedRingDB.qml components/IconButton.qml + components/PassphraseDialog.qml components/PasswordDialog.qml components/NewPasswordDialog.qml components/InputDialog.qml diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index e488b635..0c320842 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -75,6 +75,16 @@ public: emit m_wallet->refreshed(); } + virtual void onDeviceButtonRequest(uint64_t code) override + { + emit m_wallet->deviceButtonRequest(code); + } + + virtual void onDeviceButtonPressed() override + { + emit m_wallet->deviceButtonPressed(); + } + private: Wallet * m_wallet; }; diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index e3357528..2866c9fd 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -322,6 +322,8 @@ signals: void newBlock(quint64 height, quint64 targetHeight); void historyModelChanged() const; void walletCreationHeightChanged(); + void deviceButtonRequest(quint64 buttonCode); + void deviceButtonPressed(); // emitted when transaction is created async void transactionCreated(PendingTransaction * transaction, QString address, QString paymentId, quint32 mixinCount); diff --git a/src/libwalletqt/WalletManager.cpp b/src/libwalletqt/WalletManager.cpp index 6ac7e179..3ac59315 100644 --- a/src/libwalletqt/WalletManager.cpp +++ b/src/libwalletqt/WalletManager.cpp @@ -13,6 +13,57 @@ #include #include +class WalletPassphraseListenerImpl : public Monero::WalletListener +{ +public: + WalletPassphraseListenerImpl(WalletManager * mgr): m_mgr(mgr), m_wallet(nullptr) {} + + virtual void moneySpent(const std::string &txId, uint64_t amount) override { (void)txId; (void)amount; }; + virtual void moneyReceived(const std::string &txId, uint64_t amount) override { (void)txId; (void)amount; }; + virtual void unconfirmedMoneyReceived(const std::string &txId, uint64_t amount) override { (void)txId; (void)amount; }; + virtual void newBlock(uint64_t height) override { (void) height; }; + virtual void updated() override {}; + virtual void refreshed() override {}; + + virtual Monero::optional onDevicePassphraseRequest(bool on_device) override + { + qDebug() << __FUNCTION__; + if (on_device) return Monero::optional(); + + m_mgr->onWalletPassphraseNeeded(m_wallet); + + if (m_mgr->m_passphrase_abort) + { + throw std::runtime_error("Passphrase entry abort"); + } + + auto tmpPass = m_mgr->m_passphrase.toStdString(); + m_mgr->m_passphrase = QString::null; + + return Monero::optional(tmpPass); + } + + virtual void onDeviceButtonRequest(uint64_t code) override + { + emit m_mgr->deviceButtonRequest(code); + } + + virtual void onDeviceButtonPressed() override + { + emit m_mgr->deviceButtonPressed(); + } + + virtual void onSetWallet(Monero::Wallet * wallet) override + { + qDebug() << __FUNCTION__; + m_wallet = wallet; + } + +private: + Monero::Wallet * m_wallet; + WalletManager * m_mgr; +}; + WalletManager * WalletManager::m_instance = nullptr; WalletManager *WalletManager::instance() @@ -41,6 +92,8 @@ Wallet *WalletManager::createWallet(const QString &path, const QString &password Wallet *WalletManager::openWallet(const QString &path, const QString &password, NetworkType::Type nettype, quint64 kdfRounds) { QMutexLocker locker(&m_mutex); + WalletPassphraseListenerImpl tmpListener(this); + if (m_currentWallet) { qDebug() << "Closing open m_currentWallet" << m_currentWallet; delete m_currentWallet; @@ -48,7 +101,9 @@ Wallet *WalletManager::openWallet(const QString &path, const QString &password, qDebug("%s: opening wallet at %s, nettype = %d ", __PRETTY_FUNCTION__, qPrintable(path), nettype); - Monero::Wallet * w = m_pimpl->openWallet(path.toStdString(), password.toStdString(), static_cast(nettype), kdfRounds); + Monero::Wallet * w = m_pimpl->openWallet(path.toStdString(), password.toStdString(), static_cast(nettype), kdfRounds, &tmpListener); + w->setListener(nullptr); + qDebug("%s: opened wallet: %s, status: %d", __PRETTY_FUNCTION__, w->address(0, 0).c_str(), w->status()); m_currentWallet = new Wallet(w); @@ -108,17 +163,48 @@ Wallet *WalletManager::createWalletFromDevice(const QString &path, const QString const QString &deviceName, quint64 restoreHeight, const QString &subaddressLookahead) { QMutexLocker locker(&m_mutex); + WalletPassphraseListenerImpl tmpListener(this); + if (m_currentWallet) { qDebug() << "Closing open m_currentWallet" << m_currentWallet; delete m_currentWallet; m_currentWallet = NULL; } Monero::Wallet * w = m_pimpl->createWalletFromDevice(path.toStdString(), password.toStdString(), static_cast(nettype), - deviceName.toStdString(), restoreHeight, subaddressLookahead.toStdString()); + deviceName.toStdString(), restoreHeight, subaddressLookahead.toStdString(), 1, &tmpListener); + w->setListener(nullptr); + m_currentWallet = new Wallet(w); + + // move wallet to the GUI thread. Otherwise it wont be emitting signals + if (m_currentWallet->thread() != qApp->thread()) { + m_currentWallet->moveToThread(qApp->thread()); + } + return m_currentWallet; } + +void WalletManager::createWalletFromDeviceAsync(const QString &path, const QString &password, NetworkType::Type nettype, + const QString &deviceName, quint64 restoreHeight, const QString &subaddressLookahead) +{ + auto lmbd = [=](){ + return this->createWalletFromDevice(path, password, nettype, deviceName, restoreHeight, subaddressLookahead); + }; + + QFuture future = QtConcurrent::run(lmbd); + + QFutureWatcher * watcher = new QFutureWatcher(); + + connect(watcher, &QFutureWatcher::finished, + this, [this, watcher]() { + QFuture future = watcher->future(); + watcher->deleteLater(); + emit walletCreated(future.result()); + }); + watcher->setFuture(future); +} + QString WalletManager::closeWallet() { QMutexLocker locker(&m_mutex); @@ -419,3 +505,23 @@ WalletManager::WalletManager(QObject *parent) : QObject(parent) { m_pimpl = Monero::WalletManagerFactory::getWalletManager(); } + +void WalletManager::onWalletPassphraseNeeded(Monero::Wallet * wallet) +{ + m_mutex_pass.lock(); + m_passphrase_abort = false; + emit this->walletPassphraseNeeded(); + + m_cond_pass.wait(&m_mutex_pass); + m_mutex_pass.unlock(); +} + +void WalletManager::onPassphraseEntered(const QString &passphrase, bool entry_abort) +{ + m_mutex_pass.lock(); + m_passphrase = passphrase; + m_passphrase_abort = entry_abort; + + m_cond_pass.wakeAll(); + m_mutex_pass.unlock(); +} diff --git a/src/libwalletqt/WalletManager.h b/src/libwalletqt/WalletManager.h index 5cefde41..44fc11bf 100644 --- a/src/libwalletqt/WalletManager.h +++ b/src/libwalletqt/WalletManager.h @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include "NetworkType.h" class Wallet; @@ -70,6 +72,13 @@ public: const QString &deviceName, quint64 restoreHeight = 0, const QString &subaddressLookahead = ""); + + Q_INVOKABLE void createWalletFromDeviceAsync(const QString &path, + const QString &password, + NetworkType::Type nettype, + const QString &deviceName, + quint64 restoreHeight = 0, + const QString &subaddressLookahead = ""); /*! * \brief closeWallet - closes current open wallet and frees memory * \return wallet address @@ -152,14 +161,22 @@ public: // clear/rename wallet cache Q_INVOKABLE bool clearWalletCache(const QString &fileName) const; + Q_INVOKABLE void onWalletPassphraseNeeded(Monero::Wallet * wallet); + Q_INVOKABLE void onPassphraseEntered(const QString &passphrase, bool entry_abort=false); + signals: void walletOpened(Wallet * wallet); + void walletCreated(Wallet * wallet); + void walletPassphraseNeeded(); + void deviceButtonRequest(quint64 buttonCode); + void deviceButtonPressed(); void walletClosed(const QString &walletAddress); void checkUpdatesComplete(const QString &result) const; public slots: private: + friend class WalletPassphraseListenerImpl; explicit WalletManager(QObject *parent = 0); static WalletManager * m_instance; @@ -167,6 +184,10 @@ private: QMutex m_mutex; QPointer m_currentWallet; + QWaitCondition m_cond_pass; + QMutex m_mutex_pass; + QString m_passphrase; + bool m_passphrase_abort; }; #endif // WALLETMANAGER_H diff --git a/wizard/WizardController.qml b/wizard/WizardController.qml index 4224e904..e380aed9 100644 --- a/wizard/WizardController.qml +++ b/wizard/WizardController.qml @@ -44,6 +44,7 @@ Rectangle { anchors.fill: parent signal useMoneroClicked() + signal walletCreatedFromDevice(bool success) function restart() { wizardStateView.state = "wizardHome" @@ -65,6 +66,7 @@ Rectangle { wizardController.walletRestoreMode = 'seed' wizardController.walletOptionsSubaddressLookahead = ''; wizardController.remoteNodes = {}; + disconnect(); } property var m_wallet; @@ -366,6 +368,28 @@ Rectangle { return success; } + function disconnect(){ + walletManager.walletCreated.disconnect(onWalletCreated); + walletManager.walletPassphraseNeeded.disconnect(onWalletPassphraseNeeded); + walletManager.deviceButtonRequest.disconnect(onDeviceButtonRequest); + walletManager.deviceButtonPressed.disconnect(onDeviceButtonPressed); + } + + function connect(){ + walletManager.walletCreated.connect(onWalletCreated); + walletManager.walletPassphraseNeeded.connect(onWalletPassphraseNeeded); + walletManager.deviceButtonRequest.connect(onDeviceButtonRequest); + walletManager.deviceButtonPressed.connect(onDeviceButtonPressed); + } + + function deviceAttentionSplash(){ + appWindow.showProcessingSplash(qsTr("Please proceed to the device...")); + } + + function creatingWalletDeviceSplash(){ + appWindow.showProcessingSplash(qsTr("Creating wallet from device...")); + } + function createWalletFromDevice() { // TODO: create wallet in temporary filename and a) move it to the path specified by user after the final // page submitted or b) delete it when program closed before reaching final page @@ -376,30 +400,61 @@ Rectangle { console.log("deleting wallet") } - var tmp_wallet_filename = oshelper.temporaryFilename(); - console.log("Creating temporary wallet", tmp_wallet_filename) + tmpWalletFilename = oshelper.temporaryFilename(); + console.log("Creating temporary wallet", tmpWalletFilename) var nettype = persistentSettings.nettype; var restoreHeight = wizardController.walletOptionsRestoreHeight; var subaddressLookahead = wizardController.walletOptionsSubaddressLookahead; var deviceName = wizardController.walletOptionsDeviceName; - var wallet = walletManager.createWalletFromDevice(tmp_wallet_filename, "", nettype, deviceName, restoreHeight, subaddressLookahead); + connect(); + walletManager.createWalletFromDeviceAsync(tmpWalletFilename, "", nettype, deviceName, restoreHeight, subaddressLookahead); + creatingWalletDeviceSplash(); + } + + function onWalletCreated(wallet) { + splash.close() var success = wallet.status === Wallet.Status_Ok; if (success) { wizardController.m_wallet = wallet; wizardController.walletOptionsIsRecoveringFromDevice = true; - wizardController.tmpWalletFilename = tmp_wallet_filename; if (!wizardController.walletOptionsDeviceIsRestore) { // User creates a hardware wallet for the first time. Use a recent block height from API. wizardController.walletOptionsRestoreHeight = wizardController.m_wallet.walletCreationHeight; } } else { console.log(wallet.errorString) + wizardController.tmpWalletFilename = ''; appWindow.showStatusMessage(qsTr(wallet.errorString), 5); walletManager.closeWallet(); } - return success; + + disconnect(); + walletCreatedFromDevice(success); + } + + function onWalletPassphraseNeeded(){ + splash.close() + + console.log(">>> wallet passphrase needed: "); + passphraseDialog.onAcceptedCallback = function() { + walletManager.onPassphraseEntered(passphraseDialog.passphrase); + creatingWalletDeviceSplash(); + } + passphraseDialog.onRejectedCallback = function() { + walletManager.onPassphraseEntered("", true); + creatingWalletDeviceSplash(); + } + passphraseDialog.open() + } + + function onDeviceButtonRequest(code){ + deviceAttentionSplash(); + } + + function onDeviceButtonPressed(){ + creatingWalletDeviceSplash(); } function openWallet(){ diff --git a/wizard/WizardCreateDevice1.qml b/wizard/WizardCreateDevice1.qml index 9ad2ca33..81130aa1 100644 --- a/wizard/WizardCreateDevice1.qml +++ b/wizard/WizardCreateDevice1.qml @@ -207,13 +207,9 @@ Rectangle { } wizardController.walletOptionsRestoreHeight = _restoreHeight; } - var written = wizardController.createWalletFromDevice(); - if(written){ - wizardController.walletOptionsIsRecoveringFromDevice = true; - wizardStateView.state = "wizardCreateWallet2"; - } else { - errorMsg.text = qsTr("Error writing wallet from hardware device. Check application logs.") + translationManager.emptyString; - } + + wizardController.walletCreatedFromDevice.connect(onCreateWalletFromDeviceCompleted); + wizardController.createWalletFromDevice(); } } } @@ -230,4 +226,13 @@ Rectangle { walletInput.reset(); } } + + function onCreateWalletFromDeviceCompleted(written){ + if(written){ + wizardStateView.state = "wizardCreateWallet2"; + } else { + errorMsg.text = qsTr("Error writing wallet from hardware device. Check application logs.") + translationManager.emptyString; + } + wizardController.walletCreatedFromDevice.disconnect(onCreateWalletFromDeviceCompleted); + } }