diff --git a/images/createWalletFromDevice.png b/images/createWalletFromDevice.png new file mode 100755 index 00000000..9aa1e2f4 Binary files /dev/null and b/images/createWalletFromDevice.png differ diff --git a/main.qml b/main.qml index 07035d53..3f5769ab 100644 --- a/main.qml +++ b/main.qml @@ -222,6 +222,9 @@ ApplicationWindow { if (typeof wizard.m_wallet !== 'undefined') { console.log("using wizard wallet") //Set restoreHeight + if (persistentSettings.restore_height == 0 && persistentSettings.is_recovering_from_device && walletManager.localDaemonSynced()) { + persistentSettings.restore_height = walletManager.blockchainHeight() - 1; + } if(persistentSettings.restore_height > 0){ // We store restore height in own variable for performance reasons. restoreHeight = persistentSettings.restore_height @@ -331,7 +334,9 @@ ApplicationWindow { currentDaemonAddress = localDaemonAddress console.log("initializing with daemon address: ", currentDaemonAddress) - currentWallet.initAsync(currentDaemonAddress, 0, persistentSettings.is_recovering, persistentSettings.restore_height); + currentWallet.initAsync(currentDaemonAddress, 0, persistentSettings.is_recovering, persistentSettings.is_recovering_from_device, persistentSettings.restore_height); + // save wallet keys in case wallet settings have been changed in the init + currentWallet.setPassword(walletPassword); } function walletPath() { @@ -1014,6 +1019,7 @@ ApplicationWindow { property string payment_id property int restore_height : 0 property bool is_recovering : false + property bool is_recovering_from_device : false property bool customDecorations : true property string daemonFlags property int logLevel: 0 diff --git a/qml.qrc b/qml.qrc index 844cbb7f..870204e8 100644 --- a/qml.qrc +++ b/qml.qrc @@ -85,9 +85,11 @@ images/prevPage.png wizard/WizardOptions.qml images/createWallet.png + images/createWalletFromDevice.png images/openAccount.png images/recoverWallet.png wizard/WizardCreateWallet.qml + wizard/WizardCreateWalletFromDevice.qml images/greyTriangle.png images/copyToClipboard.png wizard/WizardPassword.qml diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index 0047f8dd..2c6e92c9 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -172,12 +172,18 @@ bool Wallet::store(const QString &path) return m_walletImpl->store(path.toStdString()); } -bool Wallet::init(const QString &daemonAddress, quint64 upperTransactionLimit, bool isRecovering, quint64 restoreHeight) +bool Wallet::init(const QString &daemonAddress, quint64 upperTransactionLimit, bool isRecovering, bool isRecoveringFromDevice, quint64 restoreHeight) { qDebug() << "init non async"; if (isRecovering){ qDebug() << "RESTORING"; m_walletImpl->setRecoveringFromSeed(true); + } + if (isRecoveringFromDevice){ + qDebug() << "RESTORING FROM DEVICE"; + m_walletImpl->setRecoveringFromDevice(true); + } + if (isRecovering || isRecoveringFromDevice) { m_walletImpl->setRefreshFromBlockHeight(restoreHeight); } m_walletImpl->init(daemonAddress.toStdString(), upperTransactionLimit, m_daemonUsername.toStdString(), m_daemonPassword.toStdString()); @@ -191,7 +197,7 @@ void Wallet::setDaemonLogin(const QString &daemonUsername, const QString &daemon m_daemonPassword = daemonPassword; } -void Wallet::initAsync(const QString &daemonAddress, quint64 upperTransactionLimit, bool isRecovering, quint64 restoreHeight) +void Wallet::initAsync(const QString &daemonAddress, quint64 upperTransactionLimit, bool isRecovering, bool isRecoveringFromDevice, quint64 restoreHeight) { qDebug() << "initAsync: " + daemonAddress; // Change status to disconnected if connected @@ -201,7 +207,7 @@ void Wallet::initAsync(const QString &daemonAddress, quint64 upperTransactionLim } QFuture future = QtConcurrent::run(this, &Wallet::init, - daemonAddress, upperTransactionLimit, isRecovering, restoreHeight); + daemonAddress, upperTransactionLimit, isRecovering, isRecoveringFromDevice, restoreHeight); QFutureWatcher * watcher = new QFutureWatcher(); connect(watcher, &QFutureWatcher::finished, diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index df413363..d0f053ac 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -111,10 +111,10 @@ public: Q_INVOKABLE bool store(const QString &path = ""); //! initializes wallet - Q_INVOKABLE bool init(const QString &daemonAddress, quint64 upperTransactionLimit = 0, bool isRecovering = false, quint64 restoreHeight = 0); + Q_INVOKABLE bool init(const QString &daemonAddress, quint64 upperTransactionLimit = 0, bool isRecovering = false, bool isRecoveringFromDevice = false, quint64 restoreHeight = 0); //! initializes wallet asynchronously - Q_INVOKABLE void initAsync(const QString &daemonAddress, quint64 upperTransactionLimit = 0, bool isRecovering = false, quint64 restoreHeight = 0); + Q_INVOKABLE void initAsync(const QString &daemonAddress, quint64 upperTransactionLimit = 0, bool isRecovering = false, bool isRecoveringFromDevice = false, quint64 restoreHeight = 0); // Set daemon rpc user/pass Q_INVOKABLE void setDaemonLogin(const QString &daemonUsername = "", const QString &daemonPassword = ""); diff --git a/src/libwalletqt/WalletManager.cpp b/src/libwalletqt/WalletManager.cpp index f6eb771a..355527ad 100644 --- a/src/libwalletqt/WalletManager.cpp +++ b/src/libwalletqt/WalletManager.cpp @@ -104,6 +104,20 @@ Wallet *WalletManager::createWalletFromKeys(const QString &path, const QString & return m_currentWallet; } +Wallet *WalletManager::createWalletFromDevice(const QString &path, const QString &password, NetworkType::Type nettype, + const QString &deviceName, quint64 restoreHeight, const QString &subaddressLookahead) +{ + QMutexLocker locker(&m_mutex); + 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()); + m_currentWallet = new Wallet(w); + return m_currentWallet; +} QString WalletManager::closeWallet() { diff --git a/src/libwalletqt/WalletManager.h b/src/libwalletqt/WalletManager.h index dd44c634..75626928 100644 --- a/src/libwalletqt/WalletManager.h +++ b/src/libwalletqt/WalletManager.h @@ -62,6 +62,12 @@ public: const QString &spendkey = "", quint64 restoreHeight = 0); + Q_INVOKABLE Wallet * createWalletFromDevice(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 diff --git a/wizard/WizardCreateWalletFromDevice.qml b/wizard/WizardCreateWalletFromDevice.qml new file mode 100644 index 00000000..ae63e2a0 --- /dev/null +++ b/wizard/WizardCreateWalletFromDevice.qml @@ -0,0 +1,123 @@ +// Copyright (c) 2014-2018, 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.2 +import moneroComponents.WalletManager 1.0 +import moneroComponents.Wallet 1.0 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.2 +import 'utils.js' as Utils + +ColumnLayout { + opacity: 0 + visible: false + + Behavior on opacity { + NumberAnimation { duration: 100; easing.type: Easing.InQuad } + } + + + onOpacityChanged: visible = opacity !== 0 + + function onWizardRestarted() { + // reset account name field + uiItem.accountNameText = defaultAccountName + } + + //! function called each time we display this page + + function onPageOpened(settingsOblect) { + uiItem.checkNextButton() + uiItem.deviceNameDropdown.update() + } + + function onPageClosed(settingsObject) { + settingsObject['account_name'] = uiItem.accountNameText + settingsObject['wallet_path'] = uiItem.walletPath + var restoreHeight = parseInt(uiItem.restoreHeight); + settingsObject['restore_height'] = isNaN(restoreHeight)? 0 : restoreHeight; + settingsObject['subaddress_lookahead'] = uiItem.subaddressLookahead; + settingsObject['deviceName'] = uiItem.deviceName; + var walletFullPath = wizard.createWalletPath(uiItem.walletPath,uiItem.accountNameText); + if (!wizard.walletPathValid(walletFullPath)) { + return false; + } + return createWalletFromDevice(settingsObject) + } + + //! function called each time we hide this page + // + + + function createWalletFromDevice(settingsObject) { + // 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 + + // Always delete the wallet object before creating new - we could be stepping back from recovering wallet + if (typeof m_wallet !== 'undefined') { + walletManager.closeWallet() + console.log("deleting wallet") + } + + var tmp_wallet_filename = oshelper.temporaryFilename(); + console.log("Creating temporary wallet", tmp_wallet_filename) + var nettype = appWindow.persistentSettings.nettype; + var restoreHeight = settingsObject.restore_height; + var subaddressLookahead = settingsObject.subaddress_lookahead; + var deviceName = settingsObject.deviceName; + + var wallet = walletManager.createWalletFromDevice(tmp_wallet_filename, "", nettype, deviceName, restoreHeight, subaddressLookahead); + + var success = wallet.status === Wallet.Status_Ok; + if (success) { + m_wallet = wallet; + settingsObject['is_recovering_from_device'] = true; + settingsObject['tmp_wallet_filename'] = tmp_wallet_filename + } else { + console.log(wallet.errorString) + walletErrorDialog.text = wallet.errorString; + walletErrorDialog.open(); + walletManager.closeWallet(); + } + return success; + } + + WizardManageWalletUI { + id: uiItem + titleText: qsTr("Create a new wallet from hardware device") + translationManager.emptyString + wordsTextItem.clipboardButtonVisible: false + wordsTextItem.tipTextVisible: false + restoreHeightVisible:true + recoverMode: false + recoverFromDevice: true + } + + Component.onCompleted: { + parent.wizardRestarted.connect(onWizardRestarted) + } +} diff --git a/wizard/WizardMain.qml b/wizard/WizardMain.qml index ec685fe4..ae94ed38 100644 --- a/wizard/WizardMain.qml +++ b/wizard/WizardMain.qml @@ -54,6 +54,7 @@ ColumnLayout { "create_wallet" : [welcomePage, optionsPage, createWalletPage, passwordPage, daemonSettingsPage, finishPage ], "recovery_wallet" : [welcomePage, optionsPage, recoveryWalletPage, passwordPage, daemonSettingsPage, finishPage ], "create_view_only_wallet" : [ createViewOnlyWalletPage, passwordPage ], + "create_wallet_from_device" : [welcomePage, optionsPage, createWalletFromDevicePage, passwordPage, daemonSettingsPage, finishPage ], } property string currentPath: "create_wallet" @@ -162,6 +163,16 @@ ColumnLayout { rootItem.state = "wizard"; } + function openCreateWalletFromDevicePage() { + wizardRestarted(); + print ("show create wallet from device page"); + currentPath = "create_wallet_from_device" + pages = paths[currentPath] + wizard.nextButton.visible = true + // goto next page + switchPage(true); + } + function createWalletPath(folder_path,account_name){ // Remove trailing slash - (default on windows and mac) @@ -233,6 +244,7 @@ ColumnLayout { appWindow.persistentSettings.auto_donations_amount = false //settings.auto_donations_amount appWindow.persistentSettings.restore_height = (isNaN(settings.restore_height))? 0 : settings.restore_height appWindow.persistentSettings.is_recovering = (settings.is_recovering === undefined)? false : settings.is_recovering + appWindow.persistentSettings.is_recovering_from_device = (settings.is_recovering_from_device === undefined)? false : settings.is_recovering_from_device } // reading settings from persistent storage @@ -263,6 +275,7 @@ ColumnLayout { onCreateWalletClicked: wizard.openCreateWalletPage() onRecoveryWalletClicked: wizard.openRecoveryWalletPage() onOpenWalletClicked: wizard.openOpenWalletPage(); + onCreateWalletFromDeviceClicked: wizard.openCreateWalletFromDevicePage() } WizardCreateWallet { @@ -283,6 +296,12 @@ ColumnLayout { Layout.topMargin: wizardTopMargin } + WizardCreateWalletFromDevice { + id: createWalletFromDevicePage + Layout.bottomMargin: wizardBottomMargin + Layout.topMargin: wizardTopMargin + } + WizardPassword { id: passwordPage Layout.bottomMargin: wizardBottomMargin diff --git a/wizard/WizardManageWalletUI.qml b/wizard/WizardManageWalletUI.qml index a0c268cf..f1b853e1 100644 --- a/wizard/WizardManageWalletUI.qml +++ b/wizard/WizardManageWalletUI.qml @@ -44,6 +44,7 @@ ColumnLayout { property alias wordsTextItem : memoTextItem property alias restoreHeight : restoreHeightItem.text property alias restoreHeightVisible: restoreHeightItem.visible + property alias subaddressLookahead : subaddressLookaheadItem.text property alias walletName : accountName.text property alias progressDotsModel : progressDots.model property alias recoverFromKeysAddress: addressLine.text; @@ -53,6 +54,10 @@ ColumnLayout { property bool recoverMode: false // Recover form seed or keys property bool recoverFromSeedMode: true + // Recover form hardware device + property bool recoverFromDevice: false + property var deviceName: deviceNameModel.get(deviceNameDropdown.currentIndex).column2 + property alias deviceNameDropdown: deviceNameDropdown property int rowSpacing: 10 function checkFields(){ @@ -215,7 +220,7 @@ ColumnLayout { // Recover from seed RowLayout { id: recoverFromSeed - visible: !recoverMode || ( recoverMode && recoverFromSeedMode) + visible: !recoverFromDevice && (!recoverMode || ( recoverMode && recoverFromSeedMode)) WizardMemoTextInput { id : memoTextItem Layout.fillWidth: true @@ -303,9 +308,56 @@ ColumnLayout { fontBold: false } } + + // Subaddress lookahead + RowLayout { + visible: recoverFromDevice + LineEdit { + id: subaddressLookaheadItem + Layout.fillWidth: true + Layout.maximumWidth: 600 * scaleRatio + Layout.minimumWidth: 200 * scaleRatio + placeholderFontBold: true + placeholderFontFamily: "Arial" + placeholderColor: Style.legacy_placeholderFontColor + placeholderText: qsTr("Subaddress lookahead (optional): :") + translationManager.emptyString + placeholderOpacity: 1.0 + borderColor: Qt.rgba(0, 0, 0, 0.15) + backgroundColor: "white" + fontColor: "black" + fontBold: false + } + } + + // Device name + ColumnLayout { + visible: recoverFromDevice + Label { + Layout.topMargin: 20 * scaleRatio + fontFamily: "Arial" + fontColor: "#555555" + fontSize: 14 * scaleRatio + text: qsTr("Device name") + translationManager.emptyString + } + ListModel { + id: deviceNameModel + ListElement { column1: qsTr("Ledger") ; column2: "Ledger"; } +// ListElement { column1: qsTr("Trezor") ; column2: "Trezor"; } + } + StandardDropdown { + id: deviceNameDropdown + dataModel: deviceNameModel + Layout.fillWidth: true + Layout.topMargin: 6 + colorHeaderBackground: "black" + releasedColor: "#363636" + pressedColor: "#202020" + } + } // Wallet store location ColumnLayout { + z: deviceNameDropdown.z - 1 Label { Layout.fillWidth: true Layout.topMargin: 20 * scaleRatio diff --git a/wizard/WizardOptions.qml b/wizard/WizardOptions.qml index 65a32582..3406c036 100644 --- a/wizard/WizardOptions.qml +++ b/wizard/WizardOptions.qml @@ -37,9 +37,10 @@ ColumnLayout { signal createWalletClicked() signal recoveryWalletClicked() signal openWalletClicked() + signal createWalletFromDeviceClicked() opacity: 0 visible: false - property int buttonSize: (isMobile) ? 80 * scaleRatio : 190 * scaleRatio + property int buttonSize: (isMobile) ? 80 * scaleRatio : 140 * scaleRatio property int buttonImageSize: (isMobile) ? buttonSize - 10 * scaleRatio : buttonSize - 30 * scaleRatio function onPageClosed() { @@ -227,6 +228,51 @@ ColumnLayout { wrapMode: Text.WordWrap } } + + GridLayout { + Layout.fillHeight: true + Layout.fillWidth: true + flow: !isMobile ? GridLayout.TopToBottom : GridLayout.LeftToRight + rowSpacing: 20 * scaleRatio + columnSpacing: 10 * scaleRatio + + Rectangle { + Layout.preferredHeight: page.buttonSize + Layout.preferredWidth: page.buttonSize + radius: page.buttonSize + color: createWalletFromDeviceArea.containsMouse ? "#DBDBDB" : "#FFFFFF" + + + Image { + width: page.buttonImageSize + height: page.buttonImageSize + fillMode: Image.PreserveAspectFit + horizontalAlignment: Image.AlignRight + verticalAlignment: Image.AlignTop + anchors.centerIn: parent + source: "qrc:///images/createWalletFromDevice.png" + } + + MouseArea { + id: createWalletFromDeviceArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + page.createWalletFromDeviceClicked() + } + } + } + + Text { + Layout.preferredWidth: page.buttonSize + font.family: "Arial" + font.pixelSize: 16 * scaleRatio + color: "#4A4949" + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + text: qsTr("Create a new wallet from hardware device") + translationManager.emptyString + } + } } ColumnLayout {