diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e52e00..4a730ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ option(LOCALMONERO "Include LocalMonero module" ON) option(XMRIG "Include XMRig module" ON) option(TOR_BIN "Path to Tor binary to embed inside Feather" OFF) option(CHECK_UPDATES "Enable checking for application updates" OFF) -option(USE_DEVICE_TREZOR "Trezor support compilation" OFF) +option(USE_DEVICE_TREZOR "Trezor support compilation" ON) option(DONATE_BEG "Prompt donation window every once in a while" ON) option(WITH_SCANNER "Enable webcam QR scanner" OFF) @@ -34,7 +34,7 @@ if(DEBUG) set(CMAKE_VERBOSE_MAKEFILE ON) endif() -set(MONERO_HEAD "36fb05da3394505f8033ceb8806b28909617696f") +set(MONERO_HEAD "9e7caf0efe035da08293232a73b41021911d1b5f") set(BUILD_GUI_DEPS ON) set(ARCH "x86-64") set(BUILD_64 ON) @@ -73,6 +73,8 @@ add_subdirectory(monero) set_property(TARGET wallet_merged PROPERTY FOLDER "monero") get_directory_property(ARCH_WIDTH DIRECTORY "monero" DEFINITION ARCH_WIDTH) get_directory_property(UNBOUND_LIBRARY DIRECTORY "monero" DEFINITION UNBOUND_LIBRARY) +get_directory_property(DEVICE_TREZOR_READY DIRECTORY "monero" DEFINITION DEVICE_TREZOR_READY) +get_directory_property(TREZOR_DEP_LIBS DIRECTORY "monero" DEFINITION TREZOR_DEP_LIBS) include(CMakePackageConfigHelpers) include(VersionMonero) diff --git a/Dockerfile b/Dockerfile index a162361..d77723a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -266,9 +266,11 @@ RUN git clone -b v4.1.1 --depth 1 https://github.com/fukuchi/libqrencode.git && # monero-seed: Required for Feather # Tevador's 14 word seed library +ADD contrib/monero-seed.patch . RUN git clone https://git.featherwallet.org/feather/monero-seed.git && \ cd monero-seed && \ git reset --hard 4674ef09b6faa6fe602ab5ae0b9ca8e1fd7d5e1b && \ + git apply /monero-seed.patch && \ cmake -DCMAKE_BUILD_TYPE=Release -Bbuild && \ make -Cbuild -j$THREADS && \ make -Cbuild install && \ diff --git a/Makefile b/Makefile index dc0c2a3..c17ab0e 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ CMAKEFLAGS = \ -DINSTALL_VENDORED_LIBUNBOUND=Off \ -DMANUAL_SUBMODULES=1 \ -DSTATIC=On \ - -DUSE_DEVICE_TREZOR=Off \ + -DUSE_DEVICE_TREZOR=On \ $(CMAKEFLAGS_EXTRA) release-static: CMAKEFLAGS += -DBUILD_TAG="linux-x64" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 56d9314..4c369d8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -250,6 +250,10 @@ target_link_libraries(feather ${LIBZIP_LIBRARIES} ) +if(DEVICE_TREZOR_READY) + target_link_libraries(feather ${TREZOR_DEP_LIBS}) +endif() + if (WITH_SCANNER) target_link_libraries(feather Qt5::Multimedia diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 50d13c0..732b140 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -151,7 +151,7 @@ void MainWindow::initStatusBar() { connect(m_statusBtnTor, &StatusBarButton::clicked, this, &MainWindow::menuTorClicked); this->statusBar()->addPermanentWidget(m_statusBtnTor); - m_statusBtnHwDevice = new StatusBarButton(icons()->icon("ledger.png"), "Ledger", this); + m_statusBtnHwDevice = new StatusBarButton(this->hardwareDevicePairedIcon(), this->getHardwareDevice(), this); connect(m_statusBtnHwDevice, &StatusBarButton::clicked, this, &MainWindow::menuHwDeviceClicked); this->statusBar()->addPermanentWidget(m_statusBtnHwDevice); m_statusBtnHwDevice->hide(); @@ -356,6 +356,8 @@ void MainWindow::initWalletContext() { connect(m_ctx.get(), &AppContext::createTransactionSuccess, this, &MainWindow::onCreateTransactionSuccess); connect(m_ctx.get(), &AppContext::transactionCommitted, this, &MainWindow::onTransactionCommitted); connect(m_ctx.get(), &AppContext::deviceError, this, &MainWindow::onDeviceError); + connect(m_ctx.get(), &AppContext::deviceButtonRequest, this, &MainWindow::onDeviceButtonRequest); + connect(m_ctx.get(), &AppContext::deviceButtonPressed, this, &MainWindow::onDeviceButtonPressed); connect(m_ctx.get(), &AppContext::initiateTransaction, this, &MainWindow::onInitiateTransaction); connect(m_ctx.get(), &AppContext::endTransaction, this, &MainWindow::onEndTransaction); connect(m_ctx.get(), &AppContext::customRestoreHeightSet, this, &MainWindow::onCustomRestoreHeightSet); @@ -801,6 +803,26 @@ void MainWindow::updateWidgetIcons() { m_localMoneroWidget->skinChanged(); #endif ui->conversionWidget->skinChanged(); + + m_statusBtnHwDevice->setIcon(this->hardwareDevicePairedIcon()); +} + +QIcon MainWindow::hardwareDevicePairedIcon() { + QString filename; + if (m_ctx->wallet->isLedger()) + filename = "ledger.png"; + else if (m_ctx->wallet->isTrezor()) + filename = ColorScheme::darkScheme ? "trezor_white.png" : "trezor.png"; + return icons()->icon(filename); +} + +QIcon MainWindow::hardwareDeviceUnpairedIcon() { + QString filename; + if (m_ctx->wallet->isLedger()) + filename = "ledger_unpaired.png"; + else if (m_ctx->wallet->isTrezor()) + filename = ColorScheme::darkScheme ? "trezor_unpaired_white.png" : "trezor_unpaired.png"; + return icons()->icon(filename); } void MainWindow::closeEvent(QCloseEvent *event) { @@ -1065,7 +1087,8 @@ void MainWindow::onDeviceError(const QString &error) { if (m_showDeviceError) { return; } - m_statusBtnHwDevice->setIcon(icons()->icon("ledger_unpaired.png")); + + m_statusBtnHwDevice->setIcon(this->hardwareDeviceUnpairedIcon()); while (true) { m_showDeviceError = true; auto result = QMessageBox::question(this, "Hardware device", "Lost connection to hardware device. Attempt to reconnect?"); @@ -1080,11 +1103,38 @@ void MainWindow::onDeviceError(const QString &error) { return; } } - m_statusBtnHwDevice->setIcon(icons()->icon("ledger.png")); + m_statusBtnHwDevice->setIcon(this->hardwareDevicePairedIcon()); m_ctx->wallet->startRefresh(); m_showDeviceError = false; } +void MainWindow::onDeviceButtonRequest(quint64 code) { + if (m_ctx->wallet->isTrezor()) { + switch (code) { + case 8: // Confirm refresh: Do you really want to start refresh? + { + if (m_constructingTransaction) { // This code is also used when signing a tx... + break; + } + + m_splashDialog->setMessage("Confirm refresh on device to proceed."); + m_splashDialog->setIcon(QPixmap(":/assets/images/confirmed.png")); + m_splashDialog->show(); + m_splashDialog->setEnabled(true); + break; + } + } + } +} + +void MainWindow::onDeviceButtonPressed() { + if (m_constructingTransaction) { + return; + } + + m_splashDialog->hide(); +} + void MainWindow::updateNetStats() { if (m_ctx->wallet == nullptr) { m_statusLabelNetStats->setText(""); diff --git a/src/MainWindow.h b/src/MainWindow.h index 6d33806..3d8557d 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -171,6 +171,8 @@ private slots: void showRestoreHeightDialog(); void importTransaction(); void onDeviceError(const QString &error); + void onDeviceButtonRequest(quint64 code); + void onDeviceButtonPressed(); void menuHwDeviceClicked(); void onUpdatesAvailable(const QJsonObject &updates); void toggleSearchbar(bool enabled); @@ -205,6 +207,9 @@ private: void updateRecentlyOpened(const QString &filename); void updateWidgetIcons(); + QIcon hardwareDevicePairedIcon(); + QIcon hardwareDeviceUnpairedIcon(); + QScopedPointer ui; WindowManager *m_windowManager; QSharedPointer m_ctx; diff --git a/src/WindowManager.cpp b/src/WindowManager.cpp index 82d19ba..52177bc 100644 --- a/src/WindowManager.cpp +++ b/src/WindowManager.cpp @@ -228,7 +228,7 @@ void WindowManager::tryCreateWallet(FeatherSeed seed, const QString &path, const this->onWalletOpened(wallet); } -void WindowManager::tryCreateWalletFromDevice(const QString &path, const QString &password, int restoreHeight) +void WindowManager::tryCreateWalletFromDevice(const QString &path, const QString &password, const QString &deviceName, int restoreHeight) { if (Utils::fileExists(path)) { auto err = QString("Failed to write wallet to path: \"%1\"; file already exists.").arg(path); @@ -237,7 +237,7 @@ void WindowManager::tryCreateWalletFromDevice(const QString &path, const QString } m_openingWallet = true; - m_walletManager->createWalletFromDeviceAsync(path, password, constants::networkType, "Ledger", restoreHeight); + m_walletManager->createWalletFromDeviceAsync(path, password, constants::networkType, deviceName, restoreHeight); } void WindowManager::tryCreateWalletFromKeys(const QString &path, const QString &password, const QString &address, @@ -294,16 +294,36 @@ void WindowManager::handleWalletError(const QString &message) { } void WindowManager::displayWalletErrorMessage(const QString &message) { - QString errMsg = message; + QString errMsg = QString("Error: %1").arg(message); + + // Ledger if (message.contains("No device found")) { - errMsg += "\n\nThis wallet is backed by a hardware device. Make sure the Monero app is opened on the device.\n" + errMsg += "\n\nThis wallet is backed by a Ledger hardware device. Make sure the Monero app is opened on the device.\n" "You may need to restart Feather before the device can get detected."; } if (message.contains("Unable to open device")) { errMsg += "\n\nThe device might be in use by a different application."; #if defined(Q_OS_LINUX) errMsg += "\n\nNote: On Linux you may need to follow the instructions in the link below before the device can be opened:\n" - "https://support.ledger.com/hc/en-us/articles/115005165269-Fix-connection-issues"; + "https://support.ledger.com/hc/en-us/articles/115005165269-Fix-connection-issues"; +#endif + } + + // TREZOR + if (message.contains("Unable to claim libusb device")) { + errMsg += "\n\nThis wallet is backed by a Trezor hardware device. Feather was unable to access the device. " + "Please make sure it is not opened by another program and try again."; + } + if (message.contains("Cannot get a device address")) { + errMsg += "\n\nRestart the Trezor hardware device and try again."; + } + + if (message.contains("Could not connect to the device Trezor") || message.contains("Device connect failed")) { + errMsg += "\n\nThis wallet is backed by a Trezor hardware device. Make sure the device is connected to your computer and unlocked.\n" + "You may need to restart Feather before the device can be detected."; +#if defined(Q_OS_LINUX) + errMsg += "\n\nNote: On Linux you may need to follow the instructions in the link below before the device can be opened:\n" + "https://wiki.trezor.io/Udev_rules"; #endif } @@ -330,7 +350,19 @@ void WindowManager::displayWalletErrorMessage(const QString &message) { // ######################## DEVICE ######################## void WindowManager::onDeviceButtonRequest(quint64 code) { - m_splashDialog->setMessage("Action required on device: Export the view key to open the wallet."); + QString message; + switch (code) { + case 1: // Trezor + message = "Action required on device: enter your PIN to continue."; + break; + case 8: // Trezor + message = "Action required on device: Export watch-only credentials to open the wallet."; + break; + default: + message = "Action required on device: Export the view key to open the wallet."; + } + + m_splashDialog->setMessage(message); m_splashDialog->setIcon(QPixmap(":/assets/images/key.png")); m_splashDialog->show(); m_splashDialog->setEnabled(true); diff --git a/src/WindowManager.h b/src/WindowManager.h index 4a1acb2..2a87469 100644 --- a/src/WindowManager.h +++ b/src/WindowManager.h @@ -43,7 +43,7 @@ private slots: private: void tryCreateWallet(FeatherSeed seed, const QString &path, const QString &password, const QString &seedOffset); - void tryCreateWalletFromDevice(const QString &path, const QString &password, int restoreHeight); + void tryCreateWalletFromDevice(const QString &path, const QString &password, const QString &deviceName, int restoreHeight); void tryCreateWalletFromKeys(const QString &path, const QString &password, const QString &address, const QString &viewkey, const QString &spendkey, quint64 restoreHeight); bool autoOpenWallet(); diff --git a/src/appcontext.cpp b/src/appcontext.cpp index 006f8c9..df66300 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -38,6 +38,7 @@ AppContext::AppContext(Wallet *wallet) connect(this->wallet.get(), &Wallet::transactionCreated, this, &AppContext::onTransactionCreated); connect(this->wallet.get(), &Wallet::deviceError, this, &AppContext::onDeviceError); connect(this->wallet.get(), &Wallet::deviceButtonRequest, this, &AppContext::onDeviceButtonRequest); + connect(this->wallet.get(), &Wallet::deviceButtonPressed, this, &AppContext::onDeviceButtonPressed); connect(this->wallet.get(), &Wallet::connectionStatusChanged, [this]{ this->nodes->autoConnect(); }); @@ -190,6 +191,10 @@ void AppContext::onDeviceButtonRequest(quint64 code) { emit deviceButtonRequest(code); } +void AppContext::onDeviceButtonPressed() { + emit deviceButtonPressed(); +} + void AppContext::onDeviceError(const QString &message) { qCritical() << "Device error: " << message; emit deviceError(message); diff --git a/src/appcontext.h b/src/appcontext.h index 1a7d4db..093c01c 100644 --- a/src/appcontext.h +++ b/src/appcontext.h @@ -59,6 +59,7 @@ public slots: void onAmountPrecisionChanged(int precision); void onMultiBroadcast(PendingTransaction *tx); void onDeviceButtonRequest(quint64 code); + void onDeviceButtonPressed(); void onDeviceError(const QString &message); void onTorSettingsChanged(); // should not be here @@ -92,6 +93,7 @@ signals: void initiateTransaction(); void endTransaction(); void deviceButtonRequest(quint64 code); + void deviceButtonPressed(); void deviceError(const QString &message); private: diff --git a/src/assets.qrc b/src/assets.qrc index 9956d8b..4e81320 100644 --- a/src/assets.qrc +++ b/src/assets.qrc @@ -107,7 +107,9 @@ assets/images/tor_logo_disabled.png assets/images/tor_logo.png assets/images/trezor.png + assets/images/trezor_white.png assets/images/trezor_unpaired.png + assets/images/trezor_unpaired_white.png assets/images/unconfirmed.png assets/images/unlock.png assets/images/unlock.svg diff --git a/src/assets/images/trezor_unpaired_white.png b/src/assets/images/trezor_unpaired_white.png new file mode 100644 index 0000000..751f663 Binary files /dev/null and b/src/assets/images/trezor_unpaired_white.png differ diff --git a/src/assets/images/trezor_white.png b/src/assets/images/trezor_white.png new file mode 100644 index 0000000..30a52a9 Binary files /dev/null and b/src/assets/images/trezor_white.png differ diff --git a/src/dialog/SignVerifyDialog.cpp b/src/dialog/SignVerifyDialog.cpp index 65ff1e4..96244e9 100644 --- a/src/dialog/SignVerifyDialog.cpp +++ b/src/dialog/SignVerifyDialog.cpp @@ -26,6 +26,12 @@ SignVerifyDialog::SignVerifyDialog(Wallet *wallet, QWidget *parent) ui->address->setText(m_wallet->address(0, 0)); ui->address->setCursorPosition(0); + if (m_wallet->isHwBacked()) { + // We don't have the secret spend key to sign messages + ui->btn_Sign->setEnabled(false); + ui->btn_Sign->setToolTip("Message signing is not supported on this hardware device."); + } + ui->btn_Copy->setVisible(false); } diff --git a/src/wizard/PageHardwareDevice.cpp b/src/wizard/PageHardwareDevice.cpp index 6bdb48f..83a371c 100644 --- a/src/wizard/PageHardwareDevice.cpp +++ b/src/wizard/PageHardwareDevice.cpp @@ -13,6 +13,10 @@ PageHardwareDevice::PageHardwareDevice(WizardFields *fields, QWidget *parent) , m_fields(fields) { ui->setupUi(this); + + ui->combo_deviceType->addItem("Ledger Nano S", DeviceType::LEDGER_NANO_S); + ui->combo_deviceType->addItem("Ledger Nano X", DeviceType::LEDGER_NANO_X); + ui->combo_deviceType->addItem("Trezor Model T", DeviceType::TREZOR_MODEL_T); } void PageHardwareDevice::initializePage() { @@ -28,6 +32,7 @@ int PageHardwareDevice::nextId() const { } bool PageHardwareDevice::validatePage() { + m_fields->deviceType = static_cast(ui->combo_deviceType->currentData().toInt()); return true; } diff --git a/src/wizard/PageHardwareDevice.ui b/src/wizard/PageHardwareDevice.ui index 736edea..9d2cc95 100644 --- a/src/wizard/PageHardwareDevice.ui +++ b/src/wizard/PageHardwareDevice.ui @@ -22,13 +22,7 @@ - - - - Ledger Nano S/X - - - + diff --git a/src/wizard/PageWalletFile.cpp b/src/wizard/PageWalletFile.cpp index 4171ae4..2a9373d 100644 --- a/src/wizard/PageWalletFile.cpp +++ b/src/wizard/PageWalletFile.cpp @@ -98,7 +98,14 @@ QString PageWalletFile::defaultWalletName() { do { QString walletStr = QString("wallet_%1"); if (m_fields->mode == WizardMode::CreateWalletFromDevice) { - walletStr = QString("ledger_%1"); + switch (m_fields->deviceType) { + case DeviceType::LEDGER_NANO_S: + case DeviceType::LEDGER_NANO_X: + walletStr = QString("ledger_%1"); + break; + case DeviceType::TREZOR_MODEL_T: + walletStr = QString("trezor_%1"); + } } walletName = walletStr.arg(count); count++; diff --git a/src/wizard/WalletWizard.cpp b/src/wizard/WalletWizard.cpp index 5501b6d..f169937 100644 --- a/src/wizard/WalletWizard.cpp +++ b/src/wizard/WalletWizard.cpp @@ -93,7 +93,17 @@ void WalletWizard::onCreateWallet() { restoreHeight = m_wizardFields.restoreHeight; } - emit createWalletFromDevice(walletPath, m_wizardFields.password, restoreHeight); + QString deviceName; + switch (m_wizardFields.deviceType) { + case DeviceType::LEDGER_NANO_S: + case DeviceType::LEDGER_NANO_X: + deviceName = "Ledger"; + break; + case DeviceType::TREZOR_MODEL_T: + deviceName = "Trezor"; + } + + emit createWalletFromDevice(walletPath, m_wizardFields.password, deviceName, restoreHeight); return; } diff --git a/src/wizard/WalletWizard.h b/src/wizard/WalletWizard.h index 74893cf..377ea38 100644 --- a/src/wizard/WalletWizard.h +++ b/src/wizard/WalletWizard.h @@ -21,6 +21,12 @@ enum WizardMode { CreateWalletFromDevice }; +enum DeviceType { + LEDGER_NANO_S = 0, + LEDGER_NANO_X, + TREZOR_MODEL_T +}; + struct WizardFields { QString walletName; QString walletDir; @@ -34,6 +40,7 @@ struct WizardFields { WizardMode mode; int restoreHeight = 0; SeedType seedType; + DeviceType deviceType; }; class WalletWizard : public QWizard @@ -63,7 +70,7 @@ signals: void openWallet(QString path, QString password); void defaultWalletDirChanged(QString walletDir); - void createWalletFromDevice(const QString &path, const QString &password, int restoreHeight); + void createWalletFromDevice(const QString &path, const QString &password, const QString &deviceName, int restoreHeight); void createWalletFromKeys(const QString &path, const QString &password, const QString &address, const QString &viewkey, const QString &spendkey, quint64 restoreHeight, bool deterministic = false); void createWallet(FeatherSeed seed, const QString &path, const QString &password, const QString &seedOffset = "");