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)
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)
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)
# 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 && \
-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"
${LIBZIP_LIBRARIES}
)
+if(DEVICE_TREZOR_READY)
+ target_link_libraries(feather ${TREZOR_DEP_LIBS})
+endif()
+
if (WITH_SCANNER)
target_link_libraries(feather
Qt5::Multimedia
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();
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);
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) {
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?");
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("");
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);
void updateRecentlyOpened(const QString &filename);
void updateWidgetIcons();
+ QIcon hardwareDevicePairedIcon();
+ QIcon hardwareDeviceUnpairedIcon();
+
QScopedPointer<Ui::MainWindow> ui;
WindowManager *m_windowManager;
QSharedPointer<AppContext> m_ctx;
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);
}
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,
}
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"
- "<a>https://support.ledger.com/hc/en-us/articles/115005165269-Fix-connection-issues</a>";
+ "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
}
// ######################## 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);
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();
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();
});
emit deviceButtonRequest(code);
}
+void AppContext::onDeviceButtonPressed() {
+ emit deviceButtonPressed();
+}
+
void AppContext::onDeviceError(const QString &message) {
qCritical() << "Device error: " << message;
emit deviceError(message);
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
void initiateTransaction();
void endTransaction();
void deviceButtonRequest(quint64 code);
+ void deviceButtonPressed();
void deviceError(const QString &message);
private:
<file>assets/images/tor_logo_disabled.png</file>
<file>assets/images/tor_logo.png</file>
<file>assets/images/trezor.png</file>
+ <file>assets/images/trezor_white.png</file>
<file>assets/images/trezor_unpaired.png</file>
+ <file>assets/images/trezor_unpaired_white.png</file>
<file>assets/images/unconfirmed.png</file>
<file>assets/images/unlock.png</file>
<file>assets/images/unlock.svg</file>
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);
}
, 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() {
}
bool PageHardwareDevice::validatePage() {
+ m_fields->deviceType = static_cast<DeviceType>(ui->combo_deviceType->currentData().toInt());
return true;
}
</widget>
</item>
<item>
- <widget class="QComboBox" name="comboBox">
- <item>
- <property name="text">
- <string>Ledger Nano S/X</string>
- </property>
- </item>
- </widget>
+ <widget class="QComboBox" name="combo_deviceType"/>
</item>
<item>
<widget class="Line" name="line">
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++;
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;
}
CreateWalletFromDevice
};
+enum DeviceType {
+ LEDGER_NANO_S = 0,
+ LEDGER_NANO_X,
+ TREZOR_MODEL_T
+};
+
struct WizardFields {
QString walletName;
QString walletDir;
WizardMode mode;
int restoreHeight = 0;
SeedType seedType;
+ DeviceType deviceType;
};
class WalletWizard : public QWizard
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 = "");