From 51b0fcc19d28ab26d12966d6e2710a2a8850e7ff Mon Sep 17 00:00:00 2001 From: tobtoht Date: Sat, 2 Dec 2023 19:28:31 +0100 Subject: [PATCH] airgapped signing with UR --- CMakeLists.txt | 5 + monero | 2 +- src/CMakeLists.txt | 6 +- src/CoinsWidget.cpp | 27 +- src/MainWindow.cpp | 240 +++++++++------ src/MainWindow.h | 18 +- src/MainWindow.ui | 281 ++++++++++++++++-- src/SendWidget.cpp | 23 +- src/SendWidget.h | 1 + src/assets.qrc | 1 + src/assets/images/sign.png | Bin 0 -> 3679 bytes src/components.h | 24 ++ src/dialog/AddressCheckerIndexDialog.cpp | 33 ++ src/dialog/AddressCheckerIndexDialog.h | 29 ++ src/dialog/AddressCheckerIndexDialog.ui | 81 +++++ src/dialog/TxConfAdvDialog.cpp | 149 ++++------ src/dialog/TxConfAdvDialog.h | 9 +- src/dialog/TxConfAdvDialog.ui | 279 ++++++++--------- src/dialog/URDialog.cpp | 94 ++++++ src/dialog/URDialog.h | 28 ++ src/dialog/URDialog.ui | 157 ++++++++++ src/dialog/URSettingsDialog.cpp | 41 +++ src/dialog/URSettingsDialog.h | 28 ++ src/dialog/URSettingsDialog.ui | 166 +++++++++++ src/libwalletqt/PendingTransaction.cpp | 4 +- src/libwalletqt/PendingTransaction.h | 2 +- src/libwalletqt/TransactionHistory.cpp | 15 +- src/libwalletqt/UnsignedTransaction.cpp | 7 +- src/libwalletqt/UnsignedTransaction.h | 19 +- src/libwalletqt/Wallet.cpp | 80 ++++- src/libwalletqt/Wallet.h | 24 +- src/libwalletqt/rows/TransactionRow.cpp | 13 +- src/libwalletqt/rows/TransactionRow.h | 7 +- src/model/TransactionHistoryModel.cpp | 6 +- src/qrcode/scanner/QrCodeScanDialog.cpp | 81 +---- src/qrcode/scanner/QrCodeScanDialog.h | 18 +- src/qrcode/scanner/QrCodeScanDialog.ui | 84 +----- src/qrcode/scanner/QrCodeScanWidget.cpp | 252 ++++++++++++++++ src/qrcode/scanner/QrCodeScanWidget.h | 63 ++++ src/qrcode/scanner/QrCodeScanWidget.ui | 165 ++++++++++ src/qrcode/scanner/QrScanThread.cpp | 15 +- src/qrcode/scanner/QrScanThread.h | 4 +- src/qrcode/utils/QrCodeUtils.cpp | 26 +- src/qrcode/utils/QrCodeUtils.h | 19 +- src/utils/Utils.cpp | 37 +++ src/utils/Utils.h | 5 + src/utils/config.cpp | 12 + src/utils/config.h | 19 ++ src/widgets/QrCodeWidget.cpp | 4 + src/widgets/TxDetailsSimple.cpp | 77 +++++ src/widgets/TxDetailsSimple.h | 32 ++ src/widgets/TxDetailsSimple.ui | 116 ++++++++ src/widgets/URWidget.cpp | 80 +++++ src/widgets/URWidget.h | 44 +++ src/widgets/URWidget.ui | 85 ++++++ src/wizard/WalletWizard.cpp | 1 - .../OfflineTxSigningWizard.cpp | 107 +++++++ .../OfflineTxSigningWizard.h | 59 ++++ .../offline_tx_signing/PageOTS_Export.ui | 123 ++++++++ .../PageOTS_ExportKeyImages.cpp | 69 +++++ .../PageOTS_ExportKeyImages.h | 35 +++ .../PageOTS_ExportOutputs.cpp | 70 +++++ .../PageOTS_ExportOutputs.h | 36 +++ .../PageOTS_ExportSignedTx.cpp | 66 ++++ .../PageOTS_ExportSignedTx.h | 33 ++ .../PageOTS_ExportUnsignedTx.cpp | 54 ++++ .../PageOTS_ExportUnsignedTx.h | 33 ++ .../offline_tx_signing/PageOTS_Import.cpp | 102 +++++++ .../offline_tx_signing/PageOTS_Import.h | 47 +++ .../offline_tx_signing/PageOTS_Import.ui | 184 ++++++++++++ .../PageOTS_ImportKeyImages.cpp | 60 ++++ .../PageOTS_ImportKeyImages.h | 33 ++ .../PageOTS_ImportOffline.cpp | 95 ++++++ .../PageOTS_ImportOffline.h | 40 +++ .../PageOTS_ImportSignedTx.cpp | 42 +++ .../PageOTS_ImportSignedTx.h | 33 ++ .../PageOTS_ImportUnsignedTx.cpp | 37 +++ .../PageOTS_ImportUnsignedTx.h | 28 ++ .../offline_tx_signing/PageOTS_SignTx.cpp | 20 ++ .../offline_tx_signing/PageOTS_SignTx.h | 23 ++ 80 files changed, 3943 insertions(+), 624 deletions(-) create mode 100644 src/assets/images/sign.png create mode 100644 src/dialog/AddressCheckerIndexDialog.cpp create mode 100644 src/dialog/AddressCheckerIndexDialog.h create mode 100644 src/dialog/AddressCheckerIndexDialog.ui create mode 100644 src/dialog/URDialog.cpp create mode 100644 src/dialog/URDialog.h create mode 100644 src/dialog/URDialog.ui create mode 100644 src/dialog/URSettingsDialog.cpp create mode 100644 src/dialog/URSettingsDialog.h create mode 100644 src/dialog/URSettingsDialog.ui create mode 100644 src/qrcode/scanner/QrCodeScanWidget.cpp create mode 100644 src/qrcode/scanner/QrCodeScanWidget.h create mode 100644 src/qrcode/scanner/QrCodeScanWidget.ui create mode 100644 src/widgets/TxDetailsSimple.cpp create mode 100644 src/widgets/TxDetailsSimple.h create mode 100644 src/widgets/TxDetailsSimple.ui create mode 100644 src/widgets/URWidget.cpp create mode 100644 src/widgets/URWidget.h create mode 100644 src/widgets/URWidget.ui create mode 100644 src/wizard/offline_tx_signing/OfflineTxSigningWizard.cpp create mode 100644 src/wizard/offline_tx_signing/OfflineTxSigningWizard.h create mode 100644 src/wizard/offline_tx_signing/PageOTS_Export.ui create mode 100644 src/wizard/offline_tx_signing/PageOTS_ExportKeyImages.cpp create mode 100644 src/wizard/offline_tx_signing/PageOTS_ExportKeyImages.h create mode 100644 src/wizard/offline_tx_signing/PageOTS_ExportOutputs.cpp create mode 100644 src/wizard/offline_tx_signing/PageOTS_ExportOutputs.h create mode 100644 src/wizard/offline_tx_signing/PageOTS_ExportSignedTx.cpp create mode 100644 src/wizard/offline_tx_signing/PageOTS_ExportSignedTx.h create mode 100644 src/wizard/offline_tx_signing/PageOTS_ExportUnsignedTx.cpp create mode 100644 src/wizard/offline_tx_signing/PageOTS_ExportUnsignedTx.h create mode 100644 src/wizard/offline_tx_signing/PageOTS_Import.cpp create mode 100644 src/wizard/offline_tx_signing/PageOTS_Import.h create mode 100644 src/wizard/offline_tx_signing/PageOTS_Import.ui create mode 100644 src/wizard/offline_tx_signing/PageOTS_ImportKeyImages.cpp create mode 100644 src/wizard/offline_tx_signing/PageOTS_ImportKeyImages.h create mode 100644 src/wizard/offline_tx_signing/PageOTS_ImportOffline.cpp create mode 100644 src/wizard/offline_tx_signing/PageOTS_ImportOffline.h create mode 100644 src/wizard/offline_tx_signing/PageOTS_ImportSignedTx.cpp create mode 100644 src/wizard/offline_tx_signing/PageOTS_ImportSignedTx.h create mode 100644 src/wizard/offline_tx_signing/PageOTS_ImportUnsignedTx.cpp create mode 100644 src/wizard/offline_tx_signing/PageOTS_ImportUnsignedTx.h create mode 100644 src/wizard/offline_tx_signing/PageOTS_SignTx.cpp create mode 100644 src/wizard/offline_tx_signing/PageOTS_SignTx.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ea152ef..02b2c89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,6 +86,11 @@ message(STATUS "libsodium: libraries at ${SODIUM_LIBRARY}") # QrEncode find_package(QREncode REQUIRED) +# bc-ur +find_path(BCUR_INCLUDE_DIR "bcur/bc-ur.hpp") +find_library(BCUR_LIBRARY bcur) +message(STATUS "bcur: libraries at ${BCUR_INCLUDE_DIR}") + # Polyseed find_package(Polyseed REQUIRED) if(Polyseed_SUBMODULE) diff --git a/monero b/monero index 31ced6d..34aacb1 160000 --- a/monero +++ b/monero @@ -1 +1 @@ -Subproject commit 31ced6d76a1aaa1bfb9011c86987937b7042f3ce +Subproject commit 34aacb1b49553f17b9bb7ca1ee6dfb6524aada55 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0803d61..a3b7c77 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -82,7 +82,9 @@ endif() if (WITH_SCANNER) file(GLOB QRCODE_UTILS_FILES "qrcode/utils/*.h" - "qrcode/utils/*.cpp") + "qrcode/utils/*.cpp" + "wizard/offline_tx_signing/*.h" + "wizard/offline_tx_signing/*.cpp") endif() if (WITH_SCANNER) @@ -152,6 +154,7 @@ target_include_directories(feather PUBLIC ${LIBZIP_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS} ${POLYSEED_INCLUDE_DIR} + ${BCUR_INCLUDE_DIR} ) if(WITH_SCANNER) @@ -257,6 +260,7 @@ target_link_libraries(feather ${ICU_LIBRARIES} ${LIBZIP_LIBRARIES} ${ZLIB_LIBRARIES} + ${BCUR_LIBRARY} ) if(CHECK_UPDATES) diff --git a/src/CoinsWidget.cpp b/src/CoinsWidget.cpp index 94825a1..637f235 100644 --- a/src/CoinsWidget.cpp +++ b/src/CoinsWidget.cpp @@ -11,6 +11,10 @@ #include "utils/Icons.h" #include "utils/Utils.h" +#ifdef WITH_SCANNER +#include "wizard/offline_tx_signing/OfflineTxSigningWizard.h" +#endif + CoinsWidget::CoinsWidget(Wallet *wallet, QWidget *parent) : QWidget(parent) , ui(new Ui::CoinsWidget) @@ -186,7 +190,14 @@ void CoinsWidget::spendSelected() { QStringList keyimages; for (QModelIndex index: list) { - keyimages << m_model->entryFromIndex(m_proxyModel->mapToSource(index))->keyImage(); + QString keyImage = m_model->entryFromIndex(m_proxyModel->mapToSource(index))->keyImage(); + + if (keyImage == "0100000000000000000000000000000000000000000000000000000000000000") { + Utils::showError(this, "Unable to select output to spend", "Selected output has unknown key image"); + return; + } + + keyimages << keyImage; } m_wallet->setSelectedInputs(keyimages); @@ -238,6 +249,20 @@ void CoinsWidget::onSweepOutputs() { int ret = dialog.exec(); if (!ret) return; + if (m_wallet->keyImageSyncNeeded(totalAmount, false)) { +#if defined(WITH_SCANNER) + OfflineTxSigningWizard wizard(this, m_wallet); + auto r = wizard.exec(); + + if (r == QDialog::Rejected) { + return; + } +#else + Utils::showError(this, "Can't open offline transaction signing wizard", "Feather was built without webcam QR scanner support"); + return; +#endif + } + m_wallet->sweepOutputs(keyImages, dialog.address(), dialog.churn(), dialog.outputs()); } diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index f5719ae..ef2ce74 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -7,8 +7,10 @@ #include #include #include +#include #include "constants.h" +#include "dialog/AddressCheckerIndexDialog.h" #include "dialog/BalanceDialog.h" #include "dialog/DebugInfoDialog.h" #include "dialog/PasswordDialog.h" @@ -34,6 +36,11 @@ #include "wallet/wallet_errors.h" +#ifdef WITH_SCANNER +#include "wizard/offline_tx_signing/OfflineTxSigningWizard.h" +#include "dialog/URDialog.h" +#endif + #ifdef CHECK_UPDATES #include "utils/updater/UpdateDialog.h" #endif @@ -68,8 +75,11 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa this->initWidgets(); this->initMenu(); this->initHome(); + this->initOffline(); this->initWalletContext(); + this->onOfflineMode(conf()->get(Config::offlineMode).toBool()); + // Websocket notifier connect(websocketNotifier(), &WebsocketNotifier::CCSReceived, ui->ccsWidget->model(), &CCSModel::updateEntries); connect(websocketNotifier(), &WebsocketNotifier::BountyReceived, ui->bountiesWidget->model(), &BountiesModel::updateBounties); @@ -279,14 +289,6 @@ void MainWindow::initMenu() { connect(ui->actionRescan_spent, &QAction::triggered, this, &MainWindow::rescanSpent); connect(ui->actionWallet_cache_debug, &QAction::triggered, this, &MainWindow::showWalletCacheDebugDialog); - // [Wallet] -> [Advanced] -> [Export] - connect(ui->actionExportOutputs, &QAction::triggered, this, &MainWindow::exportOutputs); - connect(ui->actionExportKeyImages, &QAction::triggered, this, &MainWindow::exportKeyImages); - - // [Wallet] -> [Advanced] -> [Import] - connect(ui->actionImportOutputs, &QAction::triggered, this, &MainWindow::importOutputs); - connect(ui->actionImportKeyImages, &QAction::triggered, this, &MainWindow::importKeyImages); - // [Wallet] -> [History] connect(ui->actionExport_CSV, &QAction::triggered, this, &MainWindow::onExportHistoryCSV); @@ -344,16 +346,20 @@ void MainWindow::initMenu() { // [Tools] connect(ui->actionSignVerify, &QAction::triggered, this, &MainWindow::menuSignVerifyClicked); connect(ui->actionVerifyTxProof, &QAction::triggered, this, &MainWindow::menuVerifyTxProof); - connect(ui->actionLoadUnsignedTxFromFile, &QAction::triggered, this, &MainWindow::loadUnsignedTx); - connect(ui->actionLoadUnsignedTxFromClipboard, &QAction::triggered, this, &MainWindow::loadUnsignedTxFromClipboard); + connect(ui->actionKeyImageSync, &QAction::triggered, this, &MainWindow::showKeyImageSyncWizard); connect(ui->actionLoadSignedTxFromFile, &QAction::triggered, this, &MainWindow::loadSignedTx); connect(ui->actionLoadSignedTxFromText, &QAction::triggered, this, &MainWindow::loadSignedTxFromText); connect(ui->actionImport_transaction, &QAction::triggered, this, &MainWindow::importTransaction); + connect(ui->actionTransmitOverUR, &QAction::triggered, this, &MainWindow::showURDialog); connect(ui->actionPay_to_many, &QAction::triggered, this, &MainWindow::payToMany); connect(ui->actionAddress_checker, &QAction::triggered, this, &MainWindow::showAddressChecker); connect(ui->actionCalculator, &QAction::triggered, this, &MainWindow::showCalcWindow); connect(ui->actionCreateDesktopEntry, &QAction::triggered, this, &MainWindow::onCreateDesktopEntry); + if (m_wallet->viewOnly()) { + ui->actionKeyImageSync->setText("Key image sync"); + } + // TODO: Allow creating desktop entry on Windows and Mac #if defined(Q_OS_WIN) || defined(Q_OS_MACOS) ui->actionCreateDesktopEntry->setDisabled(true); @@ -413,6 +419,48 @@ void MainWindow::initHome() { }); } +void MainWindow::initOffline() { + // TODO: check if we have any cameras available + + connect(ui->btn_help, &QPushButton::clicked, [this] { + windowManager()->showDocs(this, "offline_tx_signing"); + }); + connect(ui->btn_checkAddress, &QPushButton::clicked, [this]{ + AddressCheckerIndexDialog dialog{m_wallet, this}; + dialog.exec(); + }); + connect(ui->btn_signTransaction, &QPushButton::clicked, [this] { + this->showKeyImageSyncWizard(); + }); + + switch (conf()->get(Config::offlineTxSigningMethod).toInt()) { + case OfflineTxSigningWizard::Method::FILES: + ui->radio_airgapFiles->setChecked(true); + break; + default: + ui->radio_airgapUR->setChecked(true); + } + + // We can't use rich text for radio buttons + connect(ui->label_airgapUR, &ClickableLabel::clicked, [this] { + ui->radio_airgapUR->setChecked(true); + }); + connect(ui->label_airgapFiles, &ClickableLabel::clicked, [this] { + ui->radio_airgapFiles->setChecked(true); + }); + + connect(ui->radio_airgapFiles, &QCheckBox::toggled, [this] (bool checked){ + if (checked) { + conf()->set(Config::offlineTxSigningMethod, OfflineTxSigningWizard::Method::FILES); + } + }); + connect(ui->radio_airgapUR, &QCheckBox::toggled, [this](bool checked) { + if (checked) { + conf()->set(Config::offlineTxSigningMethod, OfflineTxSigningWizard::Method::UR); + } + }); +} + void MainWindow::initWalletContext() { connect(m_wallet, &Wallet::balanceUpdated, this, &MainWindow::onBalanceUpdated); connect(m_wallet, &Wallet::synchronized, this, &MainWindow::onSynchronized); //TODO @@ -619,11 +667,22 @@ void MainWindow::onProxySettingsChanged() { } void MainWindow::onOfflineMode(bool offline) { - if (!m_wallet) { + this->onConnectionStatusChanged(Wallet::ConnectionStatus_Disconnected); + m_wallet->setOffline(offline); + + if (m_wallet->viewOnly()) { return; } - m_wallet->setOffline(offline); - this->onConnectionStatusChanged(Wallet::ConnectionStatus_Disconnected); + + if (ui->stackedWidget->currentIndex() != Stack::LOCKED) { + ui->stackedWidget->setCurrentIndex(offline ? Stack::OFFLINE: Stack::WALLET); + } + + ui->actionPay_to_many->setVisible(!offline); + ui->menuView->setDisabled(offline); + + m_statusLabelBalance->setVisible(!offline); + m_statusBtnProxySettings->setVisible(!offline); } void MainWindow::onMultiBroadcast(const QMap &txHexMap) { @@ -665,7 +724,7 @@ void MainWindow::onConnectionStatusChanged(int status) QIcon icon; if (conf()->get(Config::offlineMode).toBool()) { icon = icons()->icon("status_offline.svg"); - this->setStatusText("Offline"); + this->setStatusText("Offline mode"); } else { switch(status){ case Wallet::ConnectionStatus_Disconnected: @@ -853,8 +912,31 @@ void MainWindow::onTransactionCreated(PendingTransaction *tx, const QVectoraddCacheTransaction(tx->txid()[0], tx->signedTxToHex(0)); + // Offline transaction signing + if (m_wallet->viewOnly()) { +#ifdef WITH_SCANNER + OfflineTxSigningWizard wizard(this, m_wallet, tx); + wizard.exec(); + + if (!wizard.readyToCommit()) { + return; + } else { + tx = wizard.signedTx(); + } + + if (tx->txCount() == 0) { + Utils::showError(this, "Failed to load transaction", "No transactions were found", {"You have found a bug. Please contact the developers."}, "report_an_issue"); + m_wallet->disposeTransaction(tx); + return; + } +#else + Utils::showError(this, "Can't open offline transaction signing wizard", "Feather was built without webcam QR scanner support"); + return; +#endif + } + // Show advanced dialog on multi-destination transactions - if (address.size() > 1 || m_wallet->viewOnly()) { + if (address.size() > 1) { TxConfAdvDialog dialog_adv{m_wallet, m_wallet->tmpTxDescription, this}; dialog_adv.setTransaction(tx, !m_wallet->viewOnly()); dialog_adv.exec(); @@ -884,7 +966,11 @@ void MainWindow::onTransactionCreated(PendingTransaction *tx, const QVectorerrorString()); + QString error = tx->errorString(); + if (m_wallet->viewOnly() && error.contains("double spend")) { + m_wallet->setForceKeyImageSync(true); + } + Utils::showError(this, "Failed to send transaction", error); return; } @@ -964,6 +1050,29 @@ void MainWindow::showViewOnlyDialog() { dialog.exec(); } +void MainWindow::showKeyImageSyncWizard() { +#ifdef WITH_SCANNER + OfflineTxSigningWizard wizard{this, m_wallet}; + wizard.exec(); + + if (wizard.readyToSign()) { + TxConfAdvDialog dialog{m_wallet, "", this, true}; + dialog.setUnsignedTransaction(wizard.unsignedTransaction()); + auto r = dialog.exec(); + + if (r != QDialog::Accepted) { + return; + } + + wizard.setStartId(OfflineTxSigningWizard::Page_ExportSignedTx); + wizard.restart(); + wizard.exec(); + } +#else + Utils::showError(this, "Can't open offline transaction signing wizard", "Feather was built without webcam QR scanner support"); +#endif +} + void MainWindow::menuHwDeviceClicked() { Utils::showInfo(this, "Hardware device", QString("This wallet is backed by a %1 hardware device.").arg(this->getHardwareDevice())); } @@ -1204,81 +1313,9 @@ void MainWindow::showAddressChecker() { } } -void MainWindow::exportKeyImages() { - QString fn = QFileDialog::getSaveFileName(this, "Save key images to file", QString("%1/%2_%3").arg(QDir::homePath(), this->walletName(), QString::number(QDateTime::currentSecsSinceEpoch())), "Key Images (*_keyImages)"); - if (fn.isEmpty()) return; - if (!fn.endsWith("_keyImages")) fn += "_keyImages"; - bool r = m_wallet->exportKeyImages(fn, true); - if (!r) { - Utils::showError(this, "Failed to export key images", m_wallet->errorString()); - } else { - Utils::showInfo(this, "Successfully exported key images"); - } -} - -void MainWindow::importKeyImages() { - QString fn = QFileDialog::getOpenFileName(this, "Import key image file", QDir::homePath(), "Key Images (*_keyImages);;All Files (*)"); - if (fn.isEmpty()) return; - bool r = m_wallet->importKeyImages(fn); - if (!r) { - Utils::showError(this, "Failed to import key images", m_wallet->errorString()); - } else { - Utils::showInfo(this, "Successfully imported key images"); - m_wallet->refreshModels(); - } -} - -void MainWindow::exportOutputs() { - QString fn = QFileDialog::getSaveFileName(this, "Save outputs to file", QString("%1/%2_%3").arg(QDir::homePath(), this->walletName(), QString::number(QDateTime::currentSecsSinceEpoch())), "Outputs (*_outputs)"); - if (fn.isEmpty()) return; - if (!fn.endsWith("_outputs")) fn += "_outputs"; - bool r = m_wallet->exportOutputs(fn, true); - if (!r) { - Utils::showError(this, "Failed to export outputs", m_wallet->errorString()); - } else { - Utils::showInfo(this, "Successfully exported outputs."); - } -} - -void MainWindow::importOutputs() { - QString fn = QFileDialog::getOpenFileName(this, "Import outputs file", QDir::homePath(), "Outputs (*_outputs);;All Files (*)"); - if (fn.isEmpty()) return; - bool r = m_wallet->importOutputs(fn); - if (!r) { - Utils::showError(this, "Failed to import outputs", m_wallet->errorString()); - } else { - Utils::showInfo(this, "Successfully imported outputs"); - m_wallet->refreshModels(); - } -} - -void MainWindow::loadUnsignedTx() { - QString fn = QFileDialog::getOpenFileName(this, "Select transaction to load", QDir::homePath(), "Transaction (*unsigned_monero_tx);;All Files (*)"); - if (fn.isEmpty()) return; - UnsignedTransaction *tx = m_wallet->loadTxFile(fn); - auto err = m_wallet->errorString(); - if (!err.isEmpty()) { - Utils::showError(this, "Failed to load transaction", err); - return; - } - - this->createUnsignedTxDialog(tx); -} - -void MainWindow::loadUnsignedTxFromClipboard() { - QString unsigned_tx = Utils::copyFromClipboard(); - if (unsigned_tx.isEmpty()) { - Utils::showError(this, "Unable to load unsigned transaction", "Clipboard is empty"); - return; - } - UnsignedTransaction *tx = m_wallet->loadTxFromBase64Str(unsigned_tx); - auto err = m_wallet->errorString(); - if (!err.isEmpty()) { - Utils::showError(this, "Unable to load unsigned transaction", err); - return; - } - - this->createUnsignedTxDialog(tx); +void MainWindow::showURDialog() { + URDialog dialog{this}; + dialog.exec(); } void MainWindow::loadSignedTx() { @@ -1291,7 +1328,7 @@ void MainWindow::loadSignedTx() { return; } - TxConfAdvDialog dialog{m_wallet, "", this}; + TxConfAdvDialog dialog{m_wallet, "", this, true}; dialog.setTransaction(tx); dialog.exec(); } @@ -1301,12 +1338,6 @@ void MainWindow::loadSignedTxFromText() { dialog.exec(); } -void MainWindow::createUnsignedTxDialog(UnsignedTransaction *tx) { - TxConfAdvDialog dialog{m_wallet, "", this}; - dialog.setUnsignedTransaction(tx); - dialog.exec(); -} - void MainWindow::importTransaction() { if (conf()->get(Config::torPrivacyLevel).toInt() == Config::allTorExceptNode) { // TODO: don't show if connected to local node @@ -1425,6 +1456,18 @@ void MainWindow::updateNetStats() { } void MainWindow::rescanSpent() { + QMessageBox warning{this}; + warning.setWindowTitle("Warning"); + warning.setText("Rescanning spent outputs reveals which outputs you own to the node. " + "Make sure you are connected to a trusted node.\n\n" + "Do you want to proceed?"); + warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + + auto r = warning.exec(); + if (r == QMessageBox::No) { + return; + } + if (!m_wallet->rescanSpent()) { Utils::showError(this, "Failed to rescan spent outputs", m_wallet->errorString()); } else { @@ -1773,6 +1816,7 @@ void MainWindow::unlockWallet(const QString &password) { this->statusBar()->show(); this->menuBar()->show(); ui->stackedWidget->setCurrentIndex(0); + this->onOfflineMode(conf()->get(Config::offlineMode).toBool()); m_checkUserActivity.start(); diff --git a/src/MainWindow.h b/src/MainWindow.h index 2de4c45..b7ba34a 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -102,6 +102,12 @@ public: REVUO }; + enum Stack { + WALLET = 0, + LOCKED, + OFFLINE + }; + void showOrHide(); void bringToFront(); @@ -137,12 +143,6 @@ private slots: void onShowSettingsPage(int page); // offline tx signing - void exportKeyImages(); - void importKeyImages(); - void exportOutputs(); - void importOutputs(); - void loadUnsignedTx(); - void loadUnsignedTxFromClipboard(); void loadSignedTx(); void loadSignedTxFromText(); @@ -166,10 +166,12 @@ private slots: void showPasswordDialog(); void showKeysDialog(); void showViewOnlyDialog(); + void showKeyImageSyncWizard(); void showWalletCacheDebugDialog(); void showAccountSwitcherDialog(); void showAddressChecker(); - + void showURDialog(); + void donateButtonClicked(); void showCalcWindow(); void payToMany(); @@ -202,6 +204,7 @@ private: void initWidgets(); void initMenu(); void initHome(); + void initOffline(); void initWalletContext(); void closeEvent(QCloseEvent *event) override; @@ -209,7 +212,6 @@ private: void saveGeo(); void restoreGeo(); void showDebugInfo(); - void createUnsignedTxDialog(UnsignedTransaction *tx); void updatePasswordIcon(); void updateNetStats(); void rescanSpent(); diff --git a/src/MainWindow.ui b/src/MainWindow.ui index f1bdc7e..c1f355c 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -24,7 +24,7 @@ :/assets/images/appicons/64x64.png:/assets/images/appicons/64x64.png - + 0 @@ -37,13 +37,10 @@ 0 - - 12 - - + - 1 + 2 @@ -468,6 +465,235 @@ + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + :/assets/images/network.png:/assets/images/network.png + + + Airgapped signing + + + + + + Feather supports two airgapped transaction signing methods: + + + + + + + + + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + + 0 + 0 + + + + <html><head/><body><p>Use a webcam to scan <span style=" font-weight:700;">animated QR codes</span></p></body></html> + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + <html><head/><body><p>Transfer <span style=" font-weight:700;">files</span> between computers (using a flash drive)</p></body></html> + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + To initiate an airgapped transaction, try to send a transaction using your online/view-only wallet. Then click 'Sign a transaction..' below to begin the signing process. + + + true + + + + + + + Follow through the steps in the wizard. You may need to transfer/scan multiple files/QR codes. + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + + Help + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Show address + + + + + + + Sign a transaction.. + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + false + + + If you're unsure what this is, disable 'Offline mode' by going to Settings → Network → Offline. + + + true + + + + + + + + + @@ -521,28 +747,11 @@ Advanced - - - Export - - - - - - - Import - - - - - - - @@ -560,13 +769,6 @@ Tools - - - Load unsigned transaction - - - - Broadcast transaction @@ -577,10 +779,12 @@ - + + + @@ -948,6 +1152,16 @@ Check for updates + + + Offline transaction signing + + + + + Transmit over UR + + @@ -981,6 +1195,11 @@
plugins/bounties/BountiesWidget.h
1 + + ClickableLabel + QLabel +
components.h
+
diff --git a/src/SendWidget.cpp b/src/SendWidget.cpp index 6edc132..03ac74c 100644 --- a/src/SendWidget.cpp +++ b/src/SendWidget.cpp @@ -14,6 +14,7 @@ #include "libwalletqt/WalletManager.h" #if defined(WITH_SCANNER) +#include "wizard/offline_tx_signing/OfflineTxSigningWizard.h" #include "qrcode/scanner/QrCodeScanDialog.h" #include #endif @@ -132,9 +133,9 @@ void SendWidget::scanClicked() { return; } - auto dialog = new QrCodeScanDialog(this); + auto dialog = new QrCodeScanDialog(this, false); dialog->exec(); - ui->lineAddress->setText(dialog->decodedString); + ui->lineAddress->setText(dialog->decodedString()); dialog->deleteLater(); #else Utils::showError(this, "Can't open QR scanner", "Feather was built without webcam QR scanner support"); @@ -222,6 +223,24 @@ void SendWidget::sendClicked() { "Spendable balance: %1").arg(WalletManager::displayAmount(unlocked_balance))); return; } + + // TODO: allow using file-only airgapped signing without scanner + + if (m_wallet->keyImageSyncNeeded(amount, sendAll)) { + #if defined(WITH_SCANNER) + OfflineTxSigningWizard wizard(this, m_wallet); + auto r = wizard.exec(); + m_wallet->setForceKeyImageSync(false); + + if (r == QDialog::Rejected) { + return; + } + #else + Utils::showError(this, "Can't open offline transaction signing wizard", "Feather was built without webcam QR scanner support"); + return; + #endif + } + m_wallet->createTransaction(recipient, amount, description, sendAll); } diff --git a/src/SendWidget.h b/src/SendWidget.h index 431036d..f379b46 100644 --- a/src/SendWidget.h +++ b/src/SendWidget.h @@ -50,6 +50,7 @@ private slots: private: void setupComboBox(); double amountDouble(); + bool keyImageSync(bool sendAll, quint64 amount); quint64 amount(); double conversionAmount(); diff --git a/src/assets.qrc b/src/assets.qrc index c59556c..f6a64da 100644 --- a/src/assets.qrc +++ b/src/assets.qrc @@ -103,6 +103,7 @@ assets/images/trezor_white.png assets/images/trezor_unpaired.png assets/images/trezor_unpaired_white.png + assets/images/sign.png assets/images/unconfirmed.png assets/images/unlock.svg assets/images/unpaid.png diff --git a/src/assets/images/sign.png b/src/assets/images/sign.png new file mode 100644 index 0000000000000000000000000000000000000000..46eee911c66ac8add0a54590b9435051d9328f4e GIT binary patch literal 3679 zcmaJ^=Q|sW7fp>wNX#Ip5k>7$DJ7+Ljn=MRrKPsmTNO=gjlH*8RW)lXQM^{|P3%3Z z)?4e>_aFE@&%O72xaT<^?mb^lgtq2WC^<7Z004lhsVeFIMXUcB1pGT)R+97q07@P; zC3$_H%>7KTzMc(3&)83ovovw{pS4+<%T8*l$L8@V)Es-9Ace%|@;cf|Ci12_<(CRN zyX2gad z9yKd88{BM8Ybo>Jj189eceh+RYor#FB!>Cv2^DO(V;D8;P2-TLkGV6upmpQqYvOdS zx+<>0z*3|SL1Q+FSdmi1*{Y?;ZbY>1&Ua5U5=}0{ z^e8Ud1NX?$Qpc})i99%i-w$28jD_&j*e^1)I0+iHvQ`=(Vk*t?>sYOU*5`jwBmF$n zm=RH{s!`mjl3S9)qmb8VJ=oOPnY8?tok~}bb=s4iJUDzB^+7Fb9~h zkL}pT&$Hoh+r`9pbP#Y)@xT`(fSc1k@@+g9$>48ICn4pofrq5=K+?&yL z*OG(Or1#p8EbzKz;R6CIV@yIMSDkEhx6wkqPdYC=wI|*o=HUkg#?38%Z|O!+d!a8Y zhboNGjHLHmdjjS$i=PskEMwcwDMifOUiW1dPE&9V8YCde&IU@L7HN{|Fv!4CS!*D> zHu(Y~ejLuqiVjPr81#)to~OYTmtko&EI42{1?DZca`>E3yNXf@5q*CrhQr|i)(+&B0|H+G zLm5uX6&z~ElCjQVl!$bLW*V3&2AUv?q?h$S*RSFDs{zeWIhFHXIU4H+Z=Z*8mOX)mFbRI59Ds_M^3+;dc^ zGibp~G9(HL_m&c(o~8#3^|h{3FR|-VVXmi@fAU+s)FH>LX!#IgpqAS+&0UDHGC~2i zI2iw&O57mQlz_E~0n}(oUf5VGM;%@+H!4!wMU-V2@}7C6%<-=H2t6x7k}Wi2@MJbr zvly}SQ$A`2W=K+xTl-i)OZ9J4^9>bYI5CjwiO=SdaPwhIw}{eh!@JW2naetX&OiN4 z*p(o8T}LV4!S*tl;m3n-b4GgR{Uiy!kXQ3eLT_@`482##U)qMv=X65M#EmZcvR zLirX(fxA0kA3K+7&xwS)uLJx*{-ZxYj%0}R1gh<)+(KNa9*KRUg+$wlnqK_)7L0dJ#_Bo-_ zcRfCbq;p5)Mh9F|q*-0j#wP1WyA9)1b84zgOK`BMCNyk8vRFtYN zo#N;oF~oTy->ezxx_`lTBzoD|klzaxWe%_t;=13{u_QmDeZ3C6(>GPG(Xn8KHPq9a zgJ37RNPXM?@jO+88xC&|&5Avg*PfSw@Y?t#F90v5(yo_ShFM2#tRGXZnzVjRmV3KF z9Uck6457=h!NmC-GJJQ0odrH*Y@RDBm<55h*?{bsQo}sjeN%$OpE8y$uUo1qKgL>T z=@?~VHB=#1d;b{j17rE(KpJScv{b7vd}^oV%pUx#mzgqik`;Isfpxbgb;%0=o6VYo z#99k5XphCzHvG>vq1?R%eTaQbFS&JbRMa`zI!lct?@eV0H8EU*i_|$vHR8u75p-9v0CV%3A#uLUTHhimPibeYHuQ)3&Pf~8M# zZo~Pn?$!KN=5<`U=>$ZBl|1=_5pSC=C%MYb59HPcJ4w&f=ED%n(=)tOuFEP$jp_S> z6Y08Rm&DQWQN3`Kh(&p|rc0444f+vhc?#}F4ddFy00b0U-M$m$pw#hC>N#_&%=MDC zt7d{|dA8KM+F_S>vhq$69(k_fyN8U9`+QSOSZ*s;9l`#a$lMw77M-BQ121>gtr_El zj{7Aj!6!Aku;|{;MZp~w*evB5lMlhNufD-Th<0hicF7s*n5dE649v97FKHL?2^z*^ zV`E%{&K)c#M@OLoPxm%)28@CLsvLlkUyC*@dYfet&`O}|W3ZmXSe~wId$P1#OkYTl zhTLpjS%x3bzrh^S8xZ=XBUK|!LnOQHWR!}OBIVG>SklX5<&MhB6q@Pb3N}~eZ5;~t7-H(suwX+r1YcxpC^^@XdZExmGw7r z;GYX@Kpsu4$w#j{A>IFiQ%)tqUV6QE>6wE987P*7Gv9?Is%i*7uDjb3KxDa*dScKi zm$LRY)H8U*T+U?ID-;39o1ulkhf2FXDG^ziEpzFJ8f3l%-|0Yzx?T+DkgNt6-}#|3 z=|tOQ1VDeXh(olN4IJn2+VApwb96B1oeG6*{TH<zYfd zm@{(CIwmxKMlTPEmQEDOg8Cb*x5tfQT;A?-mdw|}`6Jye*IVpx*_rdAjrK-#iS&%p zciV^N6&oBOY?5jp8TjX$ylC-w&Rm-SWO-w?!FwGSD?T4jD*J^8an4zGjg;|p2}-O~ zD0b@j+ZsMr4%`Ib{djc{KUh16KF{NzOyzKTEF`wb$5`!OvMaR{bSzVrb|2ERwoaph=! zhHUWgDeh!hu$0rTAlHoj2txiz=J&8T`Q?yE+hM8xewh(+m_s-`!j`%jaPU5^MKDQV za;GKvR4&DAPW+B+)qjXQ)(2F#MYPZsZT>Pn-0CiKW8g5L`7rykRBU|h#p7(+4)EPz zm%{Y1gp z{){3(_uHReuztMp7$#zFB<98XGr2_jHkaWKEzffVeM!kr%}+dWREAdAQg!`1st|2aCc8tQ`abY=4FF*)LHKU7mBsK|&u~(~ zC3@X7etD-%a>sj2F(KY^Kfc8vWq<8i4scFAxid(Z-P`A$_ahEx>2tlWLRr&-5Hlh& zWz({x@=N)Ul{PxR2(-oO({Y$dctkX&MY4JsAH45HEb#XOf%%t!eqRvP<)fxN6omTm znoVNUB?aMKK&h#RA1NgJ;y4hK{#B>s-@8}haFQ;95+LLEtFXY&8~DAdd%0h`h{!WG zZ-?93fwft-=ue*@^nOEWf$J|};kuGxI$LrM4?0`28a+Aq4dq5J z#t)P16~DxLE=D)|?jF)14Y^>|#)2``wZ#$}ip$YexwqY@ z;tE~?nw_eFjc%FB*61{{xuJT4LgYDWe{hwr`}u9mY3K_R?{Htf z*9$~A)aaG9=UK0=Wf>?O%~MJ5O9;bmVqz+hBylKTShN-k(GB6$Q~NZz-xJ@NX9Nj6 zon{LQT1DwoA#9&h{S}r=B~JVk7Omj@s`nT@a~58-rtY8!j&uObBx*TdRDqm_j%RQ( z{7}wLzZ;_~Gjsqs1s(1poS0$3(~d(O`knnVu^r~DP2r7T$CJAIXLYbo2ynU(7w-X3 z^ThMEx*rtTY@PK+80iYYB;AL~9LFs~KWn&X)wn7LmvG7y6D#z4=I~Ek?R*O;Y-p6) zP-t}ut1IA^s2qzN2iU6!tqdr?FzXNwG<$ z|NX3n(4ZJeSKw-&OLNx==UpOJ)~K5*e4;B-3fok(J}#7q-yIzrhiprmU${p UINT32_MAX) { + return QValidator::Invalid; + } + + return QValidator::Acceptable; + } +}; + #endif //FEATHER_COMPONENTS_H diff --git a/src/dialog/AddressCheckerIndexDialog.cpp b/src/dialog/AddressCheckerIndexDialog.cpp new file mode 100644 index 0000000..4c186fb --- /dev/null +++ b/src/dialog/AddressCheckerIndexDialog.cpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "AddressCheckerIndexDialog.h" +#include "ui_AddressCheckerIndexDialog.h" + +#include "utils/Utils.h" +#include "components.h" + +AddressCheckerIndexDialog::AddressCheckerIndexDialog(Wallet *wallet, QWidget *parent) + : WindowModalDialog(parent) + , ui(new Ui::AddressCheckerIndexDialog) + , m_wallet(wallet) +{ + ui->setupUi(this); + + connect(ui->btn_showAddress, &QPushButton::clicked, [this] { + this->showAddress(ui->line_index->text().toUInt()); + }); + + auto indexValidator = new U32Validator(this); + ui->line_index->setValidator(indexValidator); + ui->line_index->setText("0"); + + this->showAddress(0); + this->adjustSize(); +} + +void AddressCheckerIndexDialog::showAddress(uint32_t index) { + ui->address->setText(m_wallet->address(m_wallet->currentSubaddressAccount(), index)); +} + +AddressCheckerIndexDialog::~AddressCheckerIndexDialog() = default; \ No newline at end of file diff --git a/src/dialog/AddressCheckerIndexDialog.h b/src/dialog/AddressCheckerIndexDialog.h new file mode 100644 index 0000000..1b103e4 --- /dev/null +++ b/src/dialog/AddressCheckerIndexDialog.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef ADDRESSCHECKERINDEXDIALOG_H +#define ADDRESSCHECKERINDEXDIALOG_H + +#include "components.h" +#include "Wallet.h" + +namespace Ui { + class AddressCheckerIndexDialog; +} + +class AddressCheckerIndexDialog : public WindowModalDialog +{ + Q_OBJECT + + public: + explicit AddressCheckerIndexDialog(Wallet *wallet, QWidget *parent = nullptr); + ~AddressCheckerIndexDialog() override; + +private: + void showAddress(uint32_t index); + + QScopedPointer ui; + Wallet *m_wallet; +}; + +#endif //ADDRESSCHECKERINDEXDIALOG_H diff --git a/src/dialog/AddressCheckerIndexDialog.ui b/src/dialog/AddressCheckerIndexDialog.ui new file mode 100644 index 0000000..6621003 --- /dev/null +++ b/src/dialog/AddressCheckerIndexDialog.ui @@ -0,0 +1,81 @@ + + + AddressCheckerIndexDialog + + + + 0 + 0 + 712 + 127 + + + + Check address + + + + + + + + Index: + + + + + + + + 0 + 0 + + + + + + + + Show address + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + ... + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + diff --git a/src/dialog/TxConfAdvDialog.cpp b/src/dialog/TxConfAdvDialog.cpp index 31540ef..941a871 100644 --- a/src/dialog/TxConfAdvDialog.cpp +++ b/src/dialog/TxConfAdvDialog.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "constants.h" #include "dialog/QrCodeDialog.h" @@ -14,24 +15,20 @@ #include "libwalletqt/WalletManager.h" #include "qrcode/QrCode.h" #include "utils/AppData.h" +#include "utils/ColorScheme.h" #include "utils/config.h" #include "utils/Utils.h" -TxConfAdvDialog::TxConfAdvDialog(Wallet *wallet, const QString &description, QWidget *parent) +TxConfAdvDialog::TxConfAdvDialog(Wallet *wallet, const QString &description, QWidget *parent, bool offline) : WindowModalDialog(parent) , ui(new Ui::TxConfAdvDialog) , m_wallet(wallet) - , m_exportUnsignedMenu(new QMenu(this)) , m_exportSignedMenu(new QMenu(this)) , m_exportTxKeyMenu(new QMenu(this)) + , m_offline(offline) { ui->setupUi(this); - m_exportUnsignedMenu->addAction("Copy to clipboard", this, &TxConfAdvDialog::unsignedCopy); - m_exportUnsignedMenu->addAction("Show as QR code", this, &TxConfAdvDialog::unsignedQrCode); - m_exportUnsignedMenu->addAction("Save to file", this, &TxConfAdvDialog::unsignedSaveFile); - ui->btn_exportUnsigned->setMenu(m_exportUnsignedMenu); - m_exportSignedMenu->addAction("Copy to clipboard", this, &TxConfAdvDialog::signedCopy); m_exportSignedMenu->addAction("Save to file", this, &TxConfAdvDialog::signedSaveFile); ui->btn_exportSigned->setMenu(m_exportSignedMenu); @@ -39,8 +36,6 @@ TxConfAdvDialog::TxConfAdvDialog(Wallet *wallet, const QString &description, QWi m_exportTxKeyMenu->addAction("Copy to clipboard", this, &TxConfAdvDialog::txKeyCopy); ui->btn_exportTxKey->setMenu(m_exportTxKeyMenu); - ui->line_description->setText(description); - connect(ui->btn_sign, &QPushButton::clicked, this, &TxConfAdvDialog::signTransaction); connect(ui->btn_send, &QPushButton::clicked, this, &TxConfAdvDialog::broadcastTransaction); connect(ui->btn_close, &QPushButton::clicked, this, &TxConfAdvDialog::closeDialog); @@ -49,9 +44,11 @@ TxConfAdvDialog::TxConfAdvDialog(Wallet *wallet, const QString &description, QWi ui->fee->setFont(Utils::getMonospaceFont()); ui->total->setFont(Utils::getMonospaceFont()); - ui->inputs->setFont(Utils::getMonospaceFont()); - ui->outputs->setFont(Utils::getMonospaceFont()); - + if (m_offline) { + ui->txid->hide(); + ui->label_txid->hide(); + } + this->adjustSize(); } @@ -77,17 +74,6 @@ void TxConfAdvDialog::setTransaction(PendingTransaction *tx, bool isSigned) { this->setAmounts(tx->amount(), tx->fee()); - auto size_str = [this, isSigned]{ - if (isSigned) { - auto size = m_tx->signedTxToHex(0).size() / 2; - return QString("Size: %1 bytes (%2 bytes unsigned)").arg(QString::number(size), QString::number(m_tx->unsignedTxToBin().size())); - } else { - - return QString("Size: %1 bytes (unsigned)").arg(QString::number(m_tx->unsignedTxToBin().size())); - } - }(); - ui->label_size->setText(size_str); - this->setupConstructionData(ptx); } @@ -95,14 +81,12 @@ void TxConfAdvDialog::setUnsignedTransaction(UnsignedTransaction *utx) { m_utx = utx; m_utx->refresh(); - ui->btn_exportUnsigned->hide(); ui->btn_exportSigned->hide(); ui->btn_exportTxKey->hide(); ui->btn_sign->show(); ui->btn_send->hide(); ui->txid->setText("n/a"); - ui->label_size->setText("Size: n/a"); this->setAmounts(utx->amount(0), utx->fee(0)); @@ -131,66 +115,63 @@ void TxConfAdvDialog::setAmounts(quint64 amount, quint64 fee) { int maxLengthFiat = Utils::maxLength(amounts_fiat); std::for_each(amounts_fiat.begin(), amounts_fiat.end(), [maxLengthFiat](QString& amount){amount = amount.rightJustified(maxLengthFiat, ' ');}); - ui->amount->setText(QString("%1 (%2 %3)").arg(amounts[0], amounts_fiat[0], preferredCur)); - ui->fee->setText(QString("%1 (%2 %3)").arg(amounts[1], amounts_fiat[1], preferredCur)); - ui->total->setText(QString("%1 (%2 %3)").arg(amounts[2], amounts_fiat[2], preferredCur)); + if (m_offline) { + ui->amount->setText(amount_str); + ui->fee->setText(fee_str); + ui->total->setText(total); + } else { + ui->amount->setText(QString("%1 (%2 %3)").arg(amounts[0], amounts_fiat[0], preferredCur)); + ui->fee->setText(QString("%1 (%2 %3)").arg(amounts[1], amounts_fiat[1], preferredCur)); + ui->total->setText(QString("%1 (%2 %3)").arg(amounts[2], amounts_fiat[2], preferredCur)); + } } void TxConfAdvDialog::setupConstructionData(ConstructionInfo *ci) { - QString inputs_str; - auto inputs = ci->inputs(); - for (const auto& i: inputs) { - inputs_str += QString("%1 %2\n").arg(i->pubKey(), WalletManager::displayAmount(i->amount())); + for (const auto &in: ci->inputs()) { + auto *item = new QTreeWidgetItem(ui->treeInputs); + item->setText(0, in->pubKey()); + item->setFont(0, Utils::getMonospaceFont()); + item->setText(1, WalletManager::displayAmount(in->amount())); } - ui->inputs->setText(inputs_str); - ui->label_inputs->setText(QString("Inputs (%1)").arg(QString::number(inputs.size()))); + ui->treeInputs->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + ui->treeInputs->resizeColumnToContents(1); + ui->treeInputs->header()->setSectionResizeMode(0, QHeaderView::Stretch); - auto outputs = ci->outputs(); + ui->label_inputs->setText(QString("Inputs (%1)").arg(QString::number(ci->inputs().size()))); - QTextCursor cursor = ui->outputs->textCursor(); - for (const auto& o: outputs) { - auto address = o->address(); - auto amount = WalletManager::displayAmount(o->amount()); - auto index = m_wallet->subaddressIndex(address); - cursor.insertText(address, Utils::addressTextFormat(index, o->amount())); - cursor.insertText(QString(" %1").arg(amount), QTextCharFormat()); - cursor.insertBlock(); + for (const auto &out: ci->outputs()) { + auto *item = new QTreeWidgetItem(ui->treeOutputs); + item->setText(0, out->address()); + item->setText(1, WalletManager::displayAmount(out->amount())); + item->setFont(0, Utils::getMonospaceFont()); + auto index = m_wallet->subaddressIndex(out->address()); + QBrush brush; + if (index.isChange()) { + brush = QBrush(ColorScheme::YELLOW.asColor(true)); + item->setToolTip(0, "Wallet change/primary address"); + // item->setHidden(true); + } + else if (index.isValid()) { + brush = QBrush(ColorScheme::GREEN.asColor(true)); + item->setToolTip(0, "Wallet receive address"); + } + else if (out->amount() == 0) { + brush = QBrush(ColorScheme::GRAY.asColor(true)); + item->setToolTip(0, "Dummy output (Min. 2 outs consensus rule)"); + } + item->setBackground(0, brush); } - ui->label_outputs->setText(QString("Outputs (%1)").arg(QString::number(outputs.size()))); + ui->treeOutputs->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + ui->treeOutputs->resizeColumnToContents(1); + ui->treeOutputs->header()->setSectionResizeMode(0, QHeaderView::Stretch); - ui->label_ringSize->setText(QString("Ring size: %1").arg(QString::number(ci->minMixinCount() + 1))); + ui->label_outputs->setText(QString("Outputs (%1)").arg(QString::number(ci->outputs().size()))); + + this->adjustSize(); } void TxConfAdvDialog::signTransaction() { - QString defaultName = QString("%1_signed_monero_tx").arg(QString::number(QDateTime::currentSecsSinceEpoch())); - QString fn = QFileDialog::getSaveFileName(this, "Save signed transaction to file", QDir::home().filePath(defaultName), "Transaction (*signed_monero_tx)"); - if (fn.isEmpty()) { - return; - } - - bool success = m_utx->sign(fn); - - if (success) { - Utils::showInfo(this, "Transaction saved successfully"); - } else { - Utils::showError(this, "Failed to save transaction to file"); - } -} - -void TxConfAdvDialog::unsignedSaveFile() { - QString defaultName = QString("%1_unsigned_monero_tx").arg(QString::number(QDateTime::currentSecsSinceEpoch())); - QString fn = QFileDialog::getSaveFileName(this, "Save transaction to file", QDir::home().filePath(defaultName), "Transaction (*unsigned_monero_tx)"); - if (fn.isEmpty()) { - return; - } - - bool success = m_tx->saveToFile(fn); - - if (success) { - Utils::showInfo(this, "Transaction saved successfully"); - } else { - Utils::showError(this, "Failed to save transaction to file"); - } + this->accept(); } void TxConfAdvDialog::signedSaveFile() { @@ -209,21 +190,6 @@ void TxConfAdvDialog::signedSaveFile() { } } -void TxConfAdvDialog::unsignedQrCode() { - if (m_tx->unsignedTxToBin().size() > 2953) { - Utils::showError(this, "Unable to show QR code", "Transaction size exceeds the maximum size for QR codes (2953 bytes)"); - return; - } - - QrCode qr(m_tx->unsignedTxToBin(), QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::LOW); - QrCodeDialog dialog{this, &qr, "Unsigned Transaction"}; - dialog.exec(); -} - -void TxConfAdvDialog::unsignedCopy() { - Utils::copyToClipboard(m_tx->unsignedTxToBase64()); -} - void TxConfAdvDialog::signedCopy() { Utils::copyToClipboard(m_tx->signedTxToHex(0)); } @@ -237,12 +203,9 @@ void TxConfAdvDialog::txKeyCopy() { Utils::copyToClipboard(m_tx->transaction(0)->txKey()); } -void TxConfAdvDialog::signedQrCode() { -} - void TxConfAdvDialog::broadcastTransaction() { if (m_tx == nullptr) return; - m_wallet->commitTransaction(m_tx, ui->line_description->text()); + m_wallet->commitTransaction(m_tx); QDialog::accept(); } diff --git a/src/dialog/TxConfAdvDialog.h b/src/dialog/TxConfAdvDialog.h index 443a455..75d558a 100644 --- a/src/dialog/TxConfAdvDialog.h +++ b/src/dialog/TxConfAdvDialog.h @@ -22,7 +22,7 @@ class TxConfAdvDialog : public WindowModalDialog Q_OBJECT public: - explicit TxConfAdvDialog(Wallet *wallet, const QString &description, QWidget *parent = nullptr); + explicit TxConfAdvDialog(Wallet *wallet, const QString &description, QWidget *parent = nullptr, bool offline = false); ~TxConfAdvDialog() override; void setTransaction(PendingTransaction *tx, bool isSigned = true); // #TODO: have libwallet return a UnsignedTransaction, this is just dumb @@ -35,12 +35,7 @@ private: void closeDialog(); void setAmounts(quint64 amount, quint64 fee); - void unsignedCopy(); - void unsignedQrCode(); - void unsignedSaveFile(); - void signedCopy(); - void signedQrCode(); void signedSaveFile(); void txKeyCopy(); @@ -49,10 +44,10 @@ private: Wallet *m_wallet; PendingTransaction *m_tx = nullptr; UnsignedTransaction *m_utx = nullptr; - QMenu *m_exportUnsignedMenu; QMenu *m_exportSignedMenu; QMenu *m_exportTxKeyMenu; QString m_txid; + bool m_offline; }; #endif //FEATHER_TXCONFADVDIALOG_H diff --git a/src/dialog/TxConfAdvDialog.ui b/src/dialog/TxConfAdvDialog.ui index 80fbc07..964f1c8 100644 --- a/src/dialog/TxConfAdvDialog.ui +++ b/src/dialog/TxConfAdvDialog.ui @@ -7,7 +7,7 @@ 0 0 800 - 542 + 810 @@ -23,7 +23,7 @@ - + Transaction ID: @@ -39,119 +39,63 @@ - - - - - - - - - - - - Description: - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - Size: - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Ringsize: - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - Qt::Vertical + + + + Amount: - - - - - - Amount: - - - - - - - TextLabel - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Fee: - - - - - - - TextLabel - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Qt::Horizontal - - - - - - - Total: - - - - - - - TextLabel - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Fee: + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal + + + + + + + Total: + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + @@ -179,42 +123,73 @@ - - - - 0 - 0 - + + + QAbstractItemView::NoSelection - - - 16777215 - 100 - - - - true + + false + + false + + + + Pubkey + + + + + Amount + + - - - Outputs - - + + + + + Outputs + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + - - - - 0 - 0 - + + + QAbstractItemView::NoSelection - - true + + false + + false + + + + Address + + + + + Amount + + @@ -232,16 +207,6 @@ - - - - Export unsigned - - - QToolButton::InstantPopup - - - @@ -276,16 +241,20 @@ - + - Sign + Cancel - + - Cancel + Sign + + + + :/assets/images/sign.png:/assets/images/sign.png @@ -300,6 +269,8 @@ - + + + diff --git a/src/dialog/URDialog.cpp b/src/dialog/URDialog.cpp new file mode 100644 index 0000000..37444c6 --- /dev/null +++ b/src/dialog/URDialog.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "URDialog.h" +#include "ui_URDialog.h" + +#include + +#include "utils/Utils.h" + +URDialog::URDialog(QWidget *parent) + : WindowModalDialog(parent) + , ui(new Ui::URDialog) +{ + ui->setupUi(this); + + connect(ui->btn_loadFile, &QPushButton::clicked, [this]{ + QString fn = QFileDialog::getOpenFileName(this, "Load file", QDir::homePath(), "All Files (*)"); + if (fn.isEmpty()) { + return; + } + + QFile file(fn); + if (!file.open(QIODevice::ReadOnly)) { + return; + } + + QByteArray qdata = file.readAll(); + std::string data = qdata.toStdString(); + file.close(); + + ui->widgetUR->setData("any", data); + }); + + connect(ui->btn_loadClipboard, &QPushButton::clicked, [this]{ + QString qdata = Utils::copyFromClipboard(); + if (qdata.length() < 10) { + Utils::showError(this, "Not enough data on clipboard to encode as UR"); + return; + } + + std::string data = qdata.toStdString(); + + ui->widgetUR->setData("ana", data); + }); + + connect(ui->tabWidget, &QTabWidget::currentChanged, [this](int index){ + if (index == 1) { + ui->widgetScanner->startCapture(true); + } + }); + + connect(ui->widgetScanner, &QrCodeScanWidget::finished, [this](bool success){ + if (!success) { + Utils::showError(this, "Unable to scan UR"); + ui->widgetScanner->reset(); + return; + } + + if (ui->radio_clipboard->isChecked()) { + Utils::copyToClipboard(QString::fromStdString(ui->widgetScanner->getURData())); + Utils::showInfo(this, "Data copied to clipboard"); + } + else if (ui->radio_file->isChecked()) { + QString fn = QFileDialog::getSaveFileName(this, "Save to file", QDir::homePath(), "ur_data"); + if (fn.isEmpty()) { + ui->widgetScanner->reset(); + return; + } + + QFile file{fn}; + if (!file.open(QIODevice::WriteOnly)) { + Utils::showError(this, "Failed to save file", QString("Could not open file %1 for writing").arg(fn)); + ui->widgetScanner->reset(); + return; + } + + std::string data = ui->widgetScanner->getURData(); + file.write(data.data(), data.size()); + file.close(); + + Utils::showInfo(this, "Successfully saved data to file"); + } + + ui->widgetScanner->reset(); + }); + + ui->radio_file->setChecked(true); + ui->tabWidget->setCurrentIndex(0); + + this->resize(600, 700); +} + +URDialog::~URDialog() = default; \ No newline at end of file diff --git a/src/dialog/URDialog.h b/src/dialog/URDialog.h new file mode 100644 index 0000000..d7d871b --- /dev/null +++ b/src/dialog/URDialog.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_URDIALOG_H +#define FEATHER_URDIALOG_H + +#include + +#include "components.h" + +namespace Ui { + class URDialog; +} + +class URDialog : public WindowModalDialog +{ + Q_OBJECT + +public: + explicit URDialog(QWidget *parent = nullptr); + ~URDialog() override; + +private: + QScopedPointer ui; +}; + + +#endif //FEATHER_URDIALOG_H diff --git a/src/dialog/URDialog.ui b/src/dialog/URDialog.ui new file mode 100644 index 0000000..aca80b7 --- /dev/null +++ b/src/dialog/URDialog.ui @@ -0,0 +1,157 @@ + + + URDialog + + + + 0 + 0 + 788 + 703 + + + + Transmit UR + + + + + + 0 + + + + Send + + + + + + + 0 + 0 + + + + + + + + + + Load file + + + + + + + Load from clipboard + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Receive + + + + + + + + + Copy to clipboard + + + + + + + Save to file + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + QrCodeScanWidget + QWidget +
qrcode/scanner/QrCodeScanWidget.h
+ 1 +
+ + URWidget + QWidget +
widgets/URWidget.h
+ 1 +
+
+ + + + buttonBox + accepted() + URDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + URDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/dialog/URSettingsDialog.cpp b/src/dialog/URSettingsDialog.cpp new file mode 100644 index 0000000..07eb4bc --- /dev/null +++ b/src/dialog/URSettingsDialog.cpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "URSettingsDialog.h" +#include "ui_URSettingsDialog.h" + +#include + +#include "utils/config.h" +#include "utils/Utils.h" + +URSettingsDialog::URSettingsDialog(QWidget *parent) + : WindowModalDialog(parent) + , ui(new Ui::URSettingsDialog) +{ + ui->setupUi(this); + + ui->spin_fragmentLength->setValue(conf()->get(Config::URfragmentLength).toInt()); + ui->spin_speed->setValue(conf()->get(Config::URmsPerFragment).toInt()); + ui->check_fountainCode->setChecked(conf()->get(Config::URfountainCode).toBool()); + + connect(ui->spin_fragmentLength, &QSpinBox::valueChanged, [](int value){ + conf()->set(Config::URfragmentLength, value); + }); + connect(ui->spin_speed, &QSpinBox::valueChanged, [](int value){ + conf()->set(Config::URmsPerFragment, value); + }); + connect(ui->check_fountainCode, &QCheckBox::toggled, [](bool toggled){ + conf()->set(Config::URfountainCode, toggled); + }); + + connect(ui->btn_reset, &QPushButton::clicked, [this]{ + ui->spin_speed->setValue(100); + ui->spin_fragmentLength->setValue(100); + ui->check_fountainCode->setChecked(false); + }); + + this->adjustSize(); +} + +URSettingsDialog::~URSettingsDialog() = default; \ No newline at end of file diff --git a/src/dialog/URSettingsDialog.h b/src/dialog/URSettingsDialog.h new file mode 100644 index 0000000..a12db7f --- /dev/null +++ b/src/dialog/URSettingsDialog.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_URSETTINGSDIALOG_H +#define FEATHER_URSETTINGSDIALOG_H + +#include + +#include "components.h" + +namespace Ui { + class URSettingsDialog; +} + +class URSettingsDialog : public WindowModalDialog +{ + Q_OBJECT + +public: + explicit URSettingsDialog(QWidget *parent = nullptr); + ~URSettingsDialog() override; + +private: + QScopedPointer ui; +}; + + +#endif //FEATHER_URSETTINGSDIALOG_H diff --git a/src/dialog/URSettingsDialog.ui b/src/dialog/URSettingsDialog.ui new file mode 100644 index 0000000..f0c1fed --- /dev/null +++ b/src/dialog/URSettingsDialog.ui @@ -0,0 +1,166 @@ + + + URSettingsDialog + + + + 0 + 0 + 595 + 174 + + + + UR Options + + + + + + + + Fragment length + + + + + + + + + 10 + + + 1000 + + + 10 + + + 100 + + + + + + + + 0 + 0 + + + + bytes + + + + + + + + + Speed + + + + + + + + + 10 + + + 1000 + + + 10 + + + 100 + + + + + + + + 0 + 0 + + + + ms / fragment + + + + + + + + + + + Fountain code + + + + + + + + + Reset + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + URSettingsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + URSettingsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/libwalletqt/PendingTransaction.cpp b/src/libwalletqt/PendingTransaction.cpp index 4fa7fad..c48fa98 100644 --- a/src/libwalletqt/PendingTransaction.cpp +++ b/src/libwalletqt/PendingTransaction.cpp @@ -69,8 +69,8 @@ QList PendingTransaction::subaddrIndices() const return result; } -QByteArray PendingTransaction::unsignedTxToBin() const { - return QByteArray::fromStdString(m_pimpl->unsignedTxToBin()); +std::string PendingTransaction::unsignedTxToBin() const { + return m_pimpl->unsignedTxToBin(); } QString PendingTransaction::unsignedTxToBase64() const diff --git a/src/libwalletqt/PendingTransaction.h b/src/libwalletqt/PendingTransaction.h index 42b70ea..17535e2 100644 --- a/src/libwalletqt/PendingTransaction.h +++ b/src/libwalletqt/PendingTransaction.h @@ -40,7 +40,7 @@ public: QStringList txid() const; quint64 txCount() const; QList subaddrIndices() const; - QByteArray unsignedTxToBin() const; + std::string unsignedTxToBin() const; QString unsignedTxToBase64() const; QString signedTxToHex(int index) const; void refresh(); diff --git a/src/libwalletqt/TransactionHistory.cpp b/src/libwalletqt/TransactionHistory.cpp index d3bf977..3502992 100644 --- a/src/libwalletqt/TransactionHistory.cpp +++ b/src/libwalletqt/TransactionHistory.cpp @@ -91,6 +91,7 @@ void TransactionHistory::refresh() t->m_paymentId = QString::fromStdString(payment_id); t->m_coinbase = pd.m_coinbase; t->m_amount = pd.m_amount; + t->m_balanceDelta = pd.m_amount; t->m_fee = pd.m_fee; t->m_direction = TransactionRow::Direction_In; t->m_hash = QString::fromStdString(epee::string_tools::pod_to_hex(pd.m_tx_hash)); @@ -129,16 +130,17 @@ void TransactionHistory::refresh() uint64_t change = pd.m_change == (uint64_t)-1 ? 0 : pd.m_change; // change may not be known uint64_t fee = pd.m_amount_in - pd.m_amount_out; - std::string payment_id = epee::string_tools::pod_to_hex(i->second.m_payment_id); if (payment_id.substr(16).find_first_not_of('0') == std::string::npos) payment_id = payment_id.substr(0,16); - auto* t = new TransactionRow(); t->m_paymentId = QString::fromStdString(payment_id); - t->m_amount = pd.m_amount_in - change - fee; + + t->m_amount = pd.m_amount_out - change; + t->m_balanceDelta = change - pd.m_amount_in; t->m_fee = fee; + t->m_direction = TransactionRow::Direction_Out; t->m_hash = QString::fromStdString(epee::string_tools::pod_to_hex(hash)); t->m_blockHeight = pd.m_block_height; @@ -180,6 +182,7 @@ void TransactionHistory::refresh() const crypto::hash &hash = i->first; uint64_t amount = pd.m_amount_in; uint64_t fee = amount - pd.m_amount_out; + uint64_t change = pd.m_change == (uint64_t)-1 ? 0 : pd.m_change; std::string payment_id = epee::string_tools::pod_to_hex(i->second.m_payment_id); if (payment_id.substr(16).find_first_not_of('0') == std::string::npos) payment_id = payment_id.substr(0,16); @@ -187,8 +190,11 @@ void TransactionHistory::refresh() auto *t = new TransactionRow(); t->m_paymentId = QString::fromStdString(payment_id); - t->m_amount = amount - pd.m_change - fee; + + t->m_amount = pd.m_amount_out - change; + t->m_balanceDelta = change - pd.m_amount_in; t->m_fee = fee; + t->m_direction = TransactionRow::Direction_Out; t->m_failed = is_failed; t->m_pending = true; @@ -233,6 +239,7 @@ void TransactionHistory::refresh() auto *t = new TransactionRow(); t->m_paymentId = QString::fromStdString(payment_id); t->m_amount = pd.m_amount; + t->m_balanceDelta = pd.m_amount; t->m_direction = TransactionRow::Direction_In; t->m_hash = QString::fromStdString(epee::string_tools::pod_to_hex(pd.m_tx_hash)); t->m_blockHeight = pd.m_block_height; diff --git a/src/libwalletqt/UnsignedTransaction.cpp b/src/libwalletqt/UnsignedTransaction.cpp index 14effc6..7b94cea 100644 --- a/src/libwalletqt/UnsignedTransaction.cpp +++ b/src/libwalletqt/UnsignedTransaction.cpp @@ -73,8 +73,11 @@ bool UnsignedTransaction::sign(const QString &fileName) const { if(!m_pimpl->sign(fileName.toStdString())) return false; - // export key images - return m_walletImpl->exportKeyImages(fileName.toStdString() + "_keyImages"); + return true; +} + +bool UnsignedTransaction::signToStr(std::string &data) const { + return m_pimpl->signToStr(data); } void UnsignedTransaction::setFilename(const QString &fileName) diff --git a/src/libwalletqt/UnsignedTransaction.h b/src/libwalletqt/UnsignedTransaction.h index b79076f..4f2cc10 100644 --- a/src/libwalletqt/UnsignedTransaction.h +++ b/src/libwalletqt/UnsignedTransaction.h @@ -12,13 +12,6 @@ class UnsignedTransaction : public QObject { Q_OBJECT - Q_PROPERTY(Status status READ status) - Q_PROPERTY(QString errorString READ errorString) - Q_PROPERTY(quint64 txCount READ txCount) - Q_PROPERTY(QString confirmationMessage READ confirmationMessage) - Q_PROPERTY(QStringList recipientAddress READ recipientAddress) - Q_PROPERTY(QStringList paymentId READ paymentId) - Q_PROPERTY(quint64 minMixinCount READ minMixinCount) public: enum Status { @@ -30,16 +23,18 @@ public: Status status() const; QString errorString() const; - Q_INVOKABLE quint64 amount(size_t index) const; - Q_INVOKABLE quint64 fee(size_t index) const; - Q_INVOKABLE quint64 mixin(size_t index) const; + quint64 amount(size_t index) const; + quint64 fee(size_t index) const; + quint64 mixin(size_t index) const; QStringList recipientAddress() const; QStringList paymentId() const; quint64 txCount() const; QString confirmationMessage() const; quint64 minMixinCount() const; - Q_INVOKABLE bool sign(const QString &fileName) const; - Q_INVOKABLE void setFilename(const QString &fileName); + bool sign(const QString &fileName) const; + bool signToStr(std::string &data) const; + + void setFilename(const QString &fileName); void refresh(); ConstructionInfo * constructionInfo(int index) const; diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index 06f3457..774fded 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -126,6 +126,10 @@ bool Wallet::isDeterministic() const { return m_wallet2->is_deterministic(); } +QString Wallet::walletName() const { + return QFileInfo(this->cachePath()).fileName(); +} + // #################### Balance #################### quint64 Wallet::balance() const { @@ -158,6 +162,14 @@ quint64 Wallet::unlockedBalanceAll() const { return result; } +quint64 Wallet::viewOnlyBalance(quint32 accountIndex) const { + std::vector kis; + for (const auto & ki : m_selectedInputs) { + kis.push_back(ki); + } + return m_walletImpl->viewOnlyBalance(accountIndex, kis); +} + void Wallet::updateBalance() { quint64 balance = this->balance(); quint64 spendable = this->unlockedBalance(); @@ -507,22 +519,71 @@ void Wallet::onWalletPassphraseNeeded(bool on_device) { // #################### Import / Export #################### +void Wallet::setForceKeyImageSync(bool enabled) { + m_forceKeyImageSync = enabled; +} + +bool Wallet::hasUnknownKeyImages() const { + return m_walletImpl->hasUnknownKeyImages(); +} + +bool Wallet::keyImageSyncNeeded(quint64 amount, bool sendAll) const { + if (m_forceKeyImageSync) { + return true; + } + + if (!this->viewOnly()) { + return false; + } + + if (sendAll) { + return this->hasUnknownKeyImages(); + } + + // 0.001 XMR to account for tx fee + return ((amount + WalletManager::amountFromDouble(0.001)) > this->viewOnlyBalance(this->currentSubaddressAccount())); +} + bool Wallet::exportKeyImages(const QString& path, bool all) { return m_walletImpl->exportKeyImages(path.toStdString(), all); } +bool Wallet::exportKeyImagesToStr(std::string &keyImages, bool all) { + return m_walletImpl->exportKeyImagesToStr(keyImages, all); +} + +bool Wallet::exportKeyImagesForOutputsFromStr(const std::string &outputs, std::string &keyImages) { + return m_walletImpl->exportKeyImagesForOutputsFromStr(outputs, keyImages); +} + bool Wallet::importKeyImages(const QString& path) { - return m_walletImpl->importKeyImages(path.toStdString()); + bool r = m_walletImpl->importKeyImages(path.toStdString()); + this->coins()->refresh(); + return r; +} + +bool Wallet::importKeyImagesFromStr(const std::string &keyImages) { + bool r = m_walletImpl->importKeyImagesFromStr(keyImages); + this->coins()->refresh(); + return r; } bool Wallet::exportOutputs(const QString& path, bool all) { return m_walletImpl->exportOutputs(path.toStdString(), all); } +bool Wallet::exportOutputsToStr(std::string& outputs, bool all) { + return m_walletImpl->exportOutputsToStr(outputs, all); +} + bool Wallet::importOutputs(const QString& path) { return m_walletImpl->importOutputs(path.toStdString()); } +bool Wallet::importOutputsFromStr(const std::string &outputs) { + return m_walletImpl->importOutputsFromStr(outputs); +} + bool Wallet::importTransaction(const QString& txid) { std::vector txids = {txid.toStdString()}; return m_walletImpl->scanTransactions(txids); @@ -845,6 +906,12 @@ UnsignedTransaction * Wallet::loadTxFile(const QString &fileName) return result; } +UnsignedTransaction * Wallet::loadUnsignedTransactionFromStr(const std::string &data) { + Monero::UnsignedTransaction *ptImpl = m_walletImpl->loadUnsignedTxFromStr(data); + UnsignedTransaction *result = new UnsignedTransaction(ptImpl, m_walletImpl, this); + return result; +} + UnsignedTransaction * Wallet::loadTxFromBase64Str(const QString &unsigned_tx) { Monero::UnsignedTransaction *ptImpl = m_walletImpl->loadUnsignedTxFromBase64Str(unsigned_tx.toStdString()); @@ -860,6 +927,13 @@ PendingTransaction * Wallet::loadSignedTxFile(const QString &fileName) return result; } +PendingTransaction * Wallet::loadSignedTxFromStr(const std::string &data) +{ + Monero::PendingTransaction *ptImpl = m_walletImpl->loadSignedTxFromStr(data); + PendingTransaction *result = new PendingTransaction(ptImpl, this); + return result; +} + bool Wallet::submitTxFile(const QString &fileName) const { qDebug() << "Trying to submit " << fileName; @@ -1149,7 +1223,9 @@ bool Wallet::createViewOnly(const QString &path, const QString &password) const bool Wallet::rescanSpent() { QMutexLocker locker(&m_asyncMutex); - return m_walletImpl->rescanSpent(); + bool r = m_walletImpl->rescanSpent(); + m_coins->refresh(); + return r; } void Wallet::setNewWallet() { diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index 747af3b..f906981 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -53,6 +53,10 @@ struct SubaddressIndex { return major == 0 && minor == 0; } + bool isChange() const { + return minor == 0; + } + int major; int minor; }; @@ -133,6 +137,8 @@ public: //! return true if deterministic keys bool isDeterministic() const; + QString walletName() const; + // ##### Balance ##### //! returns balance quint64 balance() const; @@ -143,6 +149,8 @@ public: quint64 unlockedBalance() const; quint64 unlockedBalance(quint32 accountIndex) const; quint64 unlockedBalanceAll() const; + + quint64 viewOnlyBalance(quint32 accountIndex) const; void updateBalance(); @@ -235,13 +243,24 @@ public: void onWalletPassphraseNeeded(bool on_device) override; // ##### Import / Export ##### + void setForceKeyImageSync(bool enabled); + bool hasUnknownKeyImages() const; + bool keyImageSyncNeeded(quint64 amount, bool sendAll) const; + //! export/import key images bool exportKeyImages(const QString& path, bool all = false); + bool exportKeyImagesToStr(std::string &keyImages, bool all = false); + bool exportKeyImagesForOutputsFromStr(const std::string &outputs, std::string &keyImages); + bool importKeyImages(const QString& path); + bool importKeyImagesFromStr(const std::string &keyImages); //! export/import outputs bool exportOutputs(const QString& path, bool all = false); + bool exportOutputsToStr(std::string& outputs, bool all); + bool importOutputs(const QString& path); + bool importOutputsFromStr(const std::string &outputs); //! import a transaction bool importTransaction(const QString& txid); @@ -315,12 +334,14 @@ public: //! Sign a transfer from file UnsignedTransaction * loadTxFile(const QString &fileName); - + UnsignedTransaction * loadUnsignedTransactionFromStr(const std::string &data); + //! Load an unsigned transaction from a base64 encoded string UnsignedTransaction * loadTxFromBase64Str(const QString &unsigned_tx); //! Load a signed transaction from file PendingTransaction * loadSignedTxFile(const QString &fileName); + PendingTransaction * loadSignedTxFromStr(const std::string &data); //! Submit a transfer from file bool submitTxFile(const QString &fileName) const; @@ -490,6 +511,7 @@ private: bool m_useSSL; bool donationSending = false; bool m_newWallet = false; + bool m_forceKeyImageSync = false; QTimer *m_storeTimer = nullptr; std::set m_selectedInputs; diff --git a/src/libwalletqt/rows/TransactionRow.cpp b/src/libwalletqt/rows/TransactionRow.cpp index 673fe92..f50c4bc 100644 --- a/src/libwalletqt/rows/TransactionRow.cpp +++ b/src/libwalletqt/rows/TransactionRow.cpp @@ -12,6 +12,7 @@ TransactionRow::TransactionRow() , m_failed(false) , m_coinbase(false) , m_amount(0) + , m_balanceDelta(0) , m_fee(0) , m_blockHeight(0) , m_subaddrAccount(0) @@ -41,15 +42,9 @@ bool TransactionRow::isCoinbase() const return m_coinbase; } -quint64 TransactionRow::balanceDelta() const +qint64 TransactionRow::balanceDelta() const { - if (m_direction == Direction_In) { - return m_amount; - } - else if (m_direction == Direction_Out) { - return m_amount + m_fee; - } - return m_amount; + return m_balanceDelta; } double TransactionRow::amount() const @@ -58,7 +53,7 @@ double TransactionRow::amount() const return displayAmount().toDouble(); } -quint64 TransactionRow::atomicAmount() const +qint64 TransactionRow::atomicAmount() const { return m_amount; } diff --git a/src/libwalletqt/rows/TransactionRow.h b/src/libwalletqt/rows/TransactionRow.h index 8c59a0b..6b6cc8e 100644 --- a/src/libwalletqt/rows/TransactionRow.h +++ b/src/libwalletqt/rows/TransactionRow.h @@ -28,9 +28,9 @@ public: bool isPending() const; bool isFailed() const; bool isCoinbase() const; - quint64 balanceDelta() const; + qint64 balanceDelta() const; double amount() const; - quint64 atomicAmount() const; + qint64 atomicAmount() const; QString displayAmount() const; QString fee() const; quint64 atomicFee() const; @@ -58,7 +58,8 @@ private: friend class TransactionHistory; mutable QList m_transfers; mutable QList m_rings; - quint64 m_amount; + qint64 m_amount; // Amount that was sent (to destinations) or received, excludes tx fee + qint64 m_balanceDelta; // How much the total balance was mutated as a result of this tx (includes tx fee) quint64 m_blockHeight; QString m_description; quint64 m_confirmations; diff --git a/src/model/TransactionHistoryModel.cpp b/src/model/TransactionHistoryModel.cpp index cd7c002..dd7eb74 100644 --- a/src/model/TransactionHistoryModel.cpp +++ b/src/model/TransactionHistoryModel.cpp @@ -117,7 +117,7 @@ QVariant TransactionHistoryModel::data(const QModelIndex &index, int role) const case Column::FiatAmount: case Column::Amount: { - if (tInfo.direction() == TransactionRow::Direction_Out) { + if (tInfo.balanceDelta() < 0) { result = QVariant(QColor("#BC1E1E")); } } @@ -159,7 +159,7 @@ QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionRow &tIn return tInfo.balanceDelta(); } QString amount = QString::number(tInfo.balanceDelta() / constants::cdiv, 'f', conf()->get(Config::amountPrecision).toInt()); - amount = (tInfo.direction() == TransactionRow::Direction_Out) ? "-" + amount : "+" + amount; + amount = (tInfo.balanceDelta() < 0) ? amount : "+" + amount; return amount; } case Column::TxID: { @@ -172,7 +172,7 @@ QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionRow &tIn return QString("?"); } - double usd_amount = usd_price * (tInfo.balanceDelta() / constants::cdiv); + double usd_amount = usd_price * (abs(tInfo.balanceDelta()) / constants::cdiv); QString preferredFiatCurrency = conf()->get(Config::preferredFiatCurrency).toString(); if (preferredFiatCurrency != "USD") { diff --git a/src/qrcode/scanner/QrCodeScanDialog.cpp b/src/qrcode/scanner/QrCodeScanDialog.cpp index 6bc4eb1..e2bbd89 100644 --- a/src/qrcode/scanner/QrCodeScanDialog.cpp +++ b/src/qrcode/scanner/QrCodeScanDialog.cpp @@ -13,92 +13,21 @@ #include "Utils.h" -QrCodeScanDialog::QrCodeScanDialog(QWidget *parent) +QrCodeScanDialog::QrCodeScanDialog(QWidget *parent, bool scan_ur) : QDialog(parent) , ui(new Ui::QrCodeScanDialog) - , m_sink(new QVideoSink(this)) { ui->setupUi(this); this->setWindowTitle("Scan QR code"); - - QPixmap pixmap = QPixmap(":/assets/images/warning.png"); - ui->icon_warning->setPixmap(pixmap.scaledToWidth(32, Qt::SmoothTransformation)); - - const QList cameras = QMediaDevices::videoInputs(); - for (const auto &camera : cameras) { - ui->combo_camera->addItem(camera.description()); - } - connect(ui->combo_camera, QOverload::of(&QComboBox::currentIndexChanged), this, &QrCodeScanDialog::onCameraSwitched); - - connect(ui->viewfinder->videoSink(), &QVideoSink::videoFrameChanged, this, &QrCodeScanDialog::handleFrameCaptured); - - this->onCameraSwitched(0); - - m_thread = new QrScanThread(this); - m_thread->start(); - - connect(m_thread, &QrScanThread::decoded, this, &QrCodeScanDialog::onDecoded); + ui->widget_scanner->startCapture(scan_ur); } -void QrCodeScanDialog::handleFrameCaptured(const QVideoFrame &frame) { - QImage img = this->videoFrameToImage(frame); - m_thread->addImage(img); -} - -QImage QrCodeScanDialog::videoFrameToImage(const QVideoFrame &videoFrame) -{ - auto handleType = videoFrame.handleType(); - - if (handleType == QVideoFrame::NoHandle) { - - QImage image = videoFrame.toImage(); - - if (image.isNull()) { - return {}; - } - - if (image.format() != QImage::Format_ARGB32) { - image = image.convertToFormat(QImage::Format_ARGB32); - } - - return image.copy(); - } - - return {}; -} - - -void QrCodeScanDialog::onCameraSwitched(int index) { - const QList cameras = QMediaDevices::videoInputs(); - - if (index >= cameras.size()) { - return; - } - - m_camera.reset(new QCamera(cameras.at(index))); - m_captureSession.setCamera(m_camera.data()); - m_captureSession.setVideoOutput(ui->viewfinder); - - connect(m_camera.data(), &QCamera::activeChanged, [this](bool active){ - ui->frame_unavailable->setVisible(!active); - }); - - m_camera->start(); -} - -void QrCodeScanDialog::onDecoded(const QString &data) { - decodedString = data; - this->accept(); +QString QrCodeScanDialog::decodedString() { + return ui->widget_scanner->decodedString; } QrCodeScanDialog::~QrCodeScanDialog() { - m_thread->stop(); - m_thread->quit(); - if (!m_thread->wait(5000)) - { - m_thread->terminate(); - m_thread->wait(); - } + } \ No newline at end of file diff --git a/src/qrcode/scanner/QrCodeScanDialog.h b/src/qrcode/scanner/QrCodeScanDialog.h index 43ac9b0..0527c1e 100644 --- a/src/qrcode/scanner/QrCodeScanDialog.h +++ b/src/qrcode/scanner/QrCodeScanDialog.h @@ -13,6 +13,8 @@ #include "QrScanThread.h" +#include + namespace Ui { class QrCodeScanDialog; } @@ -22,25 +24,13 @@ class QrCodeScanDialog : public QDialog Q_OBJECT public: - explicit QrCodeScanDialog(QWidget *parent); + explicit QrCodeScanDialog(QWidget *parent, bool scan_ur = false); ~QrCodeScanDialog() override; - QString decodedString = ""; - -private slots: - void onCameraSwitched(int index); - void onDecoded(const QString &data); + QString decodedString(); private: - QImage videoFrameToImage(const QVideoFrame &videoFrame); - void handleFrameCaptured(const QVideoFrame &videoFrame); - QScopedPointer ui; - - QrScanThread *m_thread; - QScopedPointer m_camera; - QMediaCaptureSession m_captureSession; - QVideoSink m_sink; }; diff --git a/src/qrcode/scanner/QrCodeScanDialog.ui b/src/qrcode/scanner/QrCodeScanDialog.ui index 9cda999..139a87c 100644 --- a/src/qrcode/scanner/QrCodeScanDialog.ui +++ b/src/qrcode/scanner/QrCodeScanDialog.ui @@ -15,93 +15,15 @@ - - - - 0 - 0 - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - 0 - 0 - - - - icon - - - - - - - Qt::Horizontal - - - QSizePolicy::Preferred - - - - 55 - 0 - - - - - - - - Lost connection to camera. Please restart scan dialog. - - - true - - - - - - - - - - - - - 0 - 0 - - - - Camera: - - - - - - - + - QVideoWidget + QrCodeScanWidget QWidget -
qvideowidget.h
+
qrcode/scanner/QrCodeScanWidget.h
1
diff --git a/src/qrcode/scanner/QrCodeScanWidget.cpp b/src/qrcode/scanner/QrCodeScanWidget.cpp new file mode 100644 index 0000000..0c9b6e2 --- /dev/null +++ b/src/qrcode/scanner/QrCodeScanWidget.cpp @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "QrCodeScanWidget.h" +#include "ui_QrCodeScanWidget.h" + +#include +#include + +#include "utils/config.h" +#include "utils/Icons.h" +#include + +QrCodeScanWidget::QrCodeScanWidget(QWidget *parent) + : QWidget(parent) + , ui(new Ui::QrCodeScanWidget) + , m_sink(new QVideoSink(this)) + , m_thread(new QrScanThread(this)) +{ + ui->setupUi(this); + + this->setWindowTitle("Scan QR code"); + + ui->frame_error->hide(); + ui->frame_error->setInfo(icons()->icon("warning.png"), "Lost connection to camera"); + + this->refreshCameraList(); + + connect(ui->combo_camera, QOverload::of(&QComboBox::currentIndexChanged), this, &QrCodeScanWidget::onCameraSwitched); + connect(ui->viewfinder->videoSink(), &QVideoSink::videoFrameChanged, this, &QrCodeScanWidget::handleFrameCaptured); + connect(ui->btn_refresh, &QPushButton::clicked, [this]{ + this->refreshCameraList(); + this->onCameraSwitched(0); + }); + connect(m_thread, &QrScanThread::decoded, this, &QrCodeScanWidget::onDecoded); + + connect(ui->check_manualExposure, &QCheckBox::toggled, [this](bool enabled) { + if (!m_camera) { + return; + } + + ui->slider_exposure->setVisible(enabled); + if (enabled) { + m_camera->setExposureMode(QCamera::ExposureManual); + } else { + // Qt-bug: this does not work for cameras that only support V4L2_EXPOSURE_APERTURE_PRIORITY + // Check with v4l2-ctl -L + m_camera->setExposureMode(QCamera::ExposureAuto); + } + conf()->set(Config::cameraManualExposure, enabled); + }); + + connect(ui->slider_exposure, &QSlider::valueChanged, [this](int value) { + if (!m_camera) { + return; + } + + float exposure = 0.00033 * value; + m_camera->setExposureMode(QCamera::ExposureManual); + m_camera->setManualExposureTime(exposure); + conf()->set(Config::cameraExposureTime, value); + }); + + ui->check_manualExposure->setVisible(false); + ui->slider_exposure->setVisible(false); +} + +void QrCodeScanWidget::startCapture(bool scan_ur) { + m_scan_ur = scan_ur; + ui->progressBar_UR->setVisible(m_scan_ur); + ui->progressBar_UR->setFormat("Progress: %v%"); + + if (ui->combo_camera->count() < 1) { + ui->frame_error->setText("No cameras found. Attach a camera and press 'Refresh'."); + ui->frame_error->show(); + return; + } + + this->onCameraSwitched(0); + + if (!m_thread->isRunning()) { + m_thread->start(); + } +} + +void QrCodeScanWidget::reset() { + this->decodedString = ""; + m_done = false; + ui->progressBar_UR->setValue(0); + m_decoder = ur::URDecoder(); + m_thread->start(); + m_handleFrames = true; +} + +void QrCodeScanWidget::stop() { + m_camera->stop(); + m_thread->stop(); +} + +void QrCodeScanWidget::pause() { + m_handleFrames = false; +} + +void QrCodeScanWidget::refreshCameraList() { + ui->combo_camera->clear(); + const QList cameras = QMediaDevices::videoInputs(); + for (const auto &camera : cameras) { + ui->combo_camera->addItem(camera.description()); + } +} + +void QrCodeScanWidget::handleFrameCaptured(const QVideoFrame &frame) { + if (!m_handleFrames) { + return; + } + + if (!m_thread->isRunning()) { + return; + } + + QImage img = this->videoFrameToImage(frame); + if (img.format() == QImage::Format_ARGB32) { + m_thread->addImage(img); + } +} + +QImage QrCodeScanWidget::videoFrameToImage(const QVideoFrame &videoFrame) +{ + QImage image = videoFrame.toImage(); + + if (image.isNull()) { + return {}; + } + + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); + } + + return image.copy(); +} + + +void QrCodeScanWidget::onCameraSwitched(int index) { + const QList cameras = QMediaDevices::videoInputs(); + + if (index < 0) { + return; + } + + if (index >= cameras.size()) { + return; + } + + if (m_camera) { + m_camera->stop(); + } + + ui->frame_error->setVisible(false); + + m_camera.reset(new QCamera(cameras.at(index), this)); + m_captureSession.setCamera(m_camera.data()); + m_captureSession.setVideoOutput(ui->viewfinder); + + bool manualExposureSupported = m_camera->isExposureModeSupported(QCamera::ExposureManual); + ui->check_manualExposure->setVisible(manualExposureSupported); + + qDebug() << "Supported camera features: " << m_camera->supportedFeatures(); + qDebug() << "Current focus mode: " << m_camera->focusMode(); + if (m_camera->isExposureModeSupported(QCamera::ExposureBarcode)) { + qDebug() << "Barcode exposure mode is supported"; + } + + connect(m_camera.data(), &QCamera::activeChanged, [this](bool active){ + ui->frame_error->setText("Lost connection to camera"); + ui->frame_error->setVisible(!active); + }); + + connect(m_camera.data(), &QCamera::errorOccurred, [this](QCamera::Error error, const QString &errorString) { + if (error == QCamera::Error::CameraError) { + ui->frame_error->setText(QString("Error: %1").arg(errorString)); + ui->frame_error->setVisible(true); + } + }); + + m_camera->start(); + + bool useManualExposure = conf()->get(Config::cameraManualExposure).toBool() && manualExposureSupported; + ui->check_manualExposure->setChecked(useManualExposure); + if (useManualExposure) { + ui->slider_exposure->setValue(conf()->get(Config::cameraExposureTime).toInt()); + } +} + +void QrCodeScanWidget::onDecoded(const QString &data) { + if (m_done) { + return; + } + + if (m_scan_ur) { + bool success = m_decoder.receive_part(data.toStdString()); + if (!success) { + return; + } + + ui->progressBar_UR->setValue(m_decoder.estimated_percent_complete() * 100); + ui->progressBar_UR->setMaximum(100); + + if (m_decoder.is_complete()) { + m_done = true; + m_thread->stop(); + emit finished(m_decoder.is_success()); + } + + return; + } + + decodedString = data; + m_done = true; + m_thread->stop(); + emit finished(true); +} + +std::string QrCodeScanWidget::getURData() { + if (!m_decoder.is_success()) { + return ""; + } + + ur::ByteVector cbor = m_decoder.result_ur().cbor(); + std::string data; + auto i = cbor.begin(); + auto end = cbor.end(); + ur::CborLite::decodeBytes(i, end, data); + return data; +} + +QString QrCodeScanWidget::getURError() { + if (!m_decoder.is_failure()) { + return {}; + } + return QString::fromStdString(m_decoder.result_error().what()); +} + +QrCodeScanWidget::~QrCodeScanWidget() +{ + m_thread->stop(); + m_thread->quit(); + if (!m_thread->wait(5000)) + { + m_thread->terminate(); + m_thread->wait(); + } +} \ No newline at end of file diff --git a/src/qrcode/scanner/QrCodeScanWidget.h b/src/qrcode/scanner/QrCodeScanWidget.h new file mode 100644 index 0000000..8c8b86f --- /dev/null +++ b/src/qrcode/scanner/QrCodeScanWidget.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_QRCODESCANWIDGET_H +#define FEATHER_QRCODESCANWIDGET_H + +#include +#include +#include +#include +#include +#include + +#include "QrScanThread.h" + +#include + +namespace Ui { + class QrCodeScanWidget; +} + +class QrCodeScanWidget : public QWidget +{ + Q_OBJECT + +public: + explicit QrCodeScanWidget(QWidget *parent); + ~QrCodeScanWidget() override; + + QString decodedString = ""; + std::string getURData(); + QString getURError(); + + void startCapture(bool scan_ur = false); + void reset(); + void stop(); + void pause(); + +signals: + void finished(bool success); + +private slots: + void onCameraSwitched(int index); + void onDecoded(const QString &data); + +private: + void refreshCameraList(); + QImage videoFrameToImage(const QVideoFrame &videoFrame); + void handleFrameCaptured(const QVideoFrame &videoFrame); + + QScopedPointer ui; + + bool m_scan_ur = false; + QrScanThread *m_thread; + QScopedPointer m_camera; + QMediaCaptureSession m_captureSession; + QVideoSink m_sink; + ur::URDecoder m_decoder; + bool m_done = false; + bool m_handleFrames = true; +}; + +#endif //FEATHER_QRCODESCANWIDGET_H diff --git a/src/qrcode/scanner/QrCodeScanWidget.ui b/src/qrcode/scanner/QrCodeScanWidget.ui new file mode 100644 index 0000000..7f2edb4 --- /dev/null +++ b/src/qrcode/scanner/QrCodeScanWidget.ui @@ -0,0 +1,165 @@ + + + QrCodeScanWidget + + + + 0 + 0 + 526 + 406 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 0 + 0 + + + + Camera: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + Refresh + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + 0 + 0 + + + + + + + + 1 + + + 0 + + + %v / %m + + + + + + + + + Manual exposure + + + + + + + 1 + + + 100 + + + 1 + + + Qt::Horizontal + + + + + + + progressBar_UR + + viewfinder + frame_error + horizontalLayoutWidget + + + + InfoFrame + QFrame +
components.h
+ 1 +
+ + QVideoWidget + QWidget +
qvideowidget.h
+ 1 +
+
+ + +
diff --git a/src/qrcode/scanner/QrScanThread.cpp b/src/qrcode/scanner/QrScanThread.cpp index 98f1f79..c529ea2 100644 --- a/src/qrcode/scanner/QrScanThread.cpp +++ b/src/qrcode/scanner/QrScanThread.cpp @@ -15,9 +15,9 @@ QrScanThread::QrScanThread(QObject *parent) void QrScanThread::processQImage(const QImage &qimg) { const auto hints = ZXing::DecodeHints() - .setFormats(ZXing::BarcodeFormat::QRCode | ZXing::BarcodeFormat::DataMatrix) + .setFormats(ZXing::BarcodeFormat::QRCode) .setTryHarder(true) - .setBinarizer(ZXing::Binarizer::FixedThreshold); + .setMaxNumberOfSymbols(1); const auto result = QrCodeUtils::ReadBarcode(qimg, hints); @@ -32,9 +32,20 @@ void QrScanThread::stop() m_waitCondition.wakeOne(); } +void QrScanThread::start() +{ + m_queue.clear(); + m_running = true; + m_waitCondition.wakeOne(); + QThread::start(); +} + void QrScanThread::addImage(const QImage &img) { QMutexLocker locker(&m_mutex); + if (m_queue.length() > 100) { + return; + } m_queue.append(img); m_waitCondition.wakeOne(); } diff --git a/src/qrcode/scanner/QrScanThread.h b/src/qrcode/scanner/QrScanThread.h index 771f352..1ca42d0 100644 --- a/src/qrcode/scanner/QrScanThread.h +++ b/src/qrcode/scanner/QrScanThread.h @@ -21,8 +21,10 @@ class QrScanThread : public QThread public: explicit QrScanThread(QObject *parent = nullptr); void addImage(const QImage &img); + virtual void stop(); - + virtual void start(); + signals: void decoded(const QString &data); diff --git a/src/qrcode/utils/QrCodeUtils.cpp b/src/qrcode/utils/QrCodeUtils.cpp index c531c7f..19ec02c 100644 --- a/src/qrcode/utils/QrCodeUtils.cpp +++ b/src/qrcode/utils/QrCodeUtils.cpp @@ -16,22 +16,36 @@ Result QrCodeUtils::ReadBarcode(const QImage& img, const ZXing::DecodeHints& hin return ZXing::ImageFormat::XRGB; #endif - case QImage::Format_RGB888: return ZXing::ImageFormat::RGB; + case QImage::Format_RGB888: + return ZXing::ImageFormat::RGB; case QImage::Format_RGBX8888: - case QImage::Format_RGBA8888: return ZXing::ImageFormat::RGBX; + case QImage::Format_RGBA8888: + return ZXing::ImageFormat::RGBX; - case QImage::Format_Grayscale8: return ZXing::ImageFormat::Lum; + case QImage::Format_Grayscale8: + return ZXing::ImageFormat::Lum; - default: return ZXing::ImageFormat::None; + default: + return ZXing::ImageFormat::None; } }; auto exec = [&](const QImage& img){ - return Result(ZXing::ReadBarcode({ img.bits(), img.width(), img.height(), ImgFmtFromQImg(img) }, hints)); + auto res = ZXing::ReadBarcode({ img.bits(), img.width(), img.height(), ImgFmtFromQImg(img) }, hints); + return Result(res.text(), res.isValid()); }; - return ImgFmtFromQImg(img) == ZXing::ImageFormat::None ? exec(img.convertToFormat(QImage::Format_RGBX8888)) : exec(img); + try { + if (ImgFmtFromQImg(img) == ZXing::ImageFormat::None) { + return exec(img.convertToFormat(QImage::Format_RGBX8888)); + } else { + return exec(img); + } + } + catch (...) { + return Result("", false); + } } diff --git a/src/qrcode/utils/QrCodeUtils.h b/src/qrcode/utils/QrCodeUtils.h index 2898fe4..a5cfa78 100644 --- a/src/qrcode/utils/QrCodeUtils.h +++ b/src/qrcode/utils/QrCodeUtils.h @@ -5,20 +5,23 @@ #define FEATHER_QRCODEUTILS_H #include +#include #include -class Result : private ZXing::Result +class Result { public: - explicit Result(ZXing::Result&& r) : - m_result(std::move(r)){ } - - inline QString text() const { return QString::fromStdString(m_result.text()); } - bool isValid() const { return m_result.isValid(); } - + explicit Result(const std::string &text, bool isValid) + : m_text(QString::fromStdString(text)) + , m_valid(isValid){} + + [[nodiscard]] QString text() const { return m_text; } + [[nodiscard]] bool isValid() const { return m_valid; } + private: - ZXing::Result m_result; + QString m_text = ""; + bool m_valid = false; }; class QrCodeUtils { diff --git a/src/utils/Utils.cpp b/src/utils/Utils.cpp index f2c9c7c..86cfe8c 100644 --- a/src/utils/Utils.cpp +++ b/src/utils/Utils.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "constants.h" #include "networktype.h" @@ -111,6 +112,28 @@ QStringList fileFind(const QRegularExpression &pattern, const QString &baseDir, return rtn; } +QString getSaveFileName(QWidget* parent, const QString &caption, const QString &filename, const QString &filter) { + QDir lastPath{conf()->get(Config::lastPath).toString()}; + QString fn = QFileDialog::getSaveFileName(parent, caption, lastPath.filePath(filename), filter); + if (fn.isEmpty()) { + return {}; + } + QFileInfo fileInfo(fn); + conf()->set(Config::lastPath, fileInfo.absolutePath()); + return fn; +} + +QString getOpenFileName(QWidget* parent, const QString& caption, const QString& filter) { + QString lastPath = conf()->get(Config::lastPath).toString(); + QString fn = QFileDialog::getOpenFileName(parent, caption, lastPath, filter); + if (fn.isEmpty()) { + return {}; + } + QFileInfo fileInfo(fn); + conf()->set(Config::lastPath, fileInfo.absolutePath()); + return fn; +} + bool dirExists(const QString &path) { QDir pathDir(path); return pathDir.exists(); @@ -645,6 +668,20 @@ void showMsg(const Message &m) { showMsg(m.parent, QMessageBox::Warning, "Error", m.title, m.description, m.helpItems, m.doc, m.highlight); } +void openDir(QWidget *parent, const QString &message, const QString &dir) { + QMessageBox openDir{parent}; + openDir.setWindowTitle("Info"); + openDir.setText(message); + QPushButton *copy = openDir.addButton("Open directory", QMessageBox::HelpRole); + openDir.addButton(QMessageBox::Ok); + openDir.setDefaultButton(QMessageBox::Ok); + openDir.exec(); + + if (openDir.clickedButton() == copy) { + QDesktopServices::openUrl(QUrl::fromLocalFile(dir)); + } +} + QWindow *windowForQObject(QObject* object) { while (object) { if (auto widget = qobject_cast(object)) { diff --git a/src/utils/Utils.h b/src/utils/Utils.h index b43b450..2367bd4 100644 --- a/src/utils/Utils.h +++ b/src/utils/Utils.h @@ -45,6 +45,9 @@ namespace Utils bool pixmapWrite(const QString &path, const QPixmap &pixmap); QStringList fileFind(const QRegularExpression &pattern, const QString &baseDir, int level, int depth, int maxPerDir); + QString getSaveFileName(QWidget *parent, const QString &caption, const QString &filename, const QString &filter); + QString getOpenFileName(QWidget *parent, const QString &caption, const QString &filter); + QString portablePath(); bool isPortableMode(); bool portableFileExists(const QString &dir); @@ -107,6 +110,8 @@ namespace Utils void showMsg(QWidget *parent, QMessageBox::Icon icon, const QString &windowTitle, const QString &title, const QString &description, const QStringList &helpItems = {}, const QString &doc = "", const QString &highlight = "", const QString &link = ""); void showMsg(const Message &message); + void openDir(QWidget *parent, const QString &message, const QString& dir); + QWindow* windowForQObject(QObject* object); } diff --git a/src/utils/config.cpp b/src/utils/config.cpp index fcc92b7..3f46315 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -20,6 +20,7 @@ static const QHash configStrings = { {Config::firstRun, {QS("firstRun"), true}}, {Config::warnOnStagenet,{QS("warnOnStagenet"), true}}, {Config::warnOnTestnet,{QS("warnOnTestnet"), true}}, + {Config::warnOnKiImport,{QS("warnOnKiImport"), true}}, {Config::logLevel,{QS("logLevel"), 0}}, {Config::homeWidget,{QS("homeWidget"), "ccs"}}, @@ -29,6 +30,7 @@ static const QHash configStrings = { {Config::geometry, {QS("geometry"), {}}}, {Config::windowState, {QS("windowState"), {}}}, {Config::GUI_HistoryViewState, {QS("GUI_HistoryViewState"), {}}}, + {Config::geometryOTSWizard, {QS("geometryOTSWizard"), {}}}, // Wallets {Config::walletDirectory,{QS("walletDirectory"), ""}}, @@ -82,6 +84,8 @@ static const QHash configStrings = { {Config::offlineMode, {QS("offlineMode"), false}}, {Config::multiBroadcast, {QS("multiBroadcast"), true}}, + {Config::offlineTxSigningMethod, {QS("offlineTxSigningMethod"), Config::OTSMethod::UnifiedResources}}, + {Config::offlineTxSigningForceKISync, {QS("offlineTxSigningForceKISync"), false}}, {Config::warnOnExternalLink,{QS("warnOnExternalLink"), true}}, {Config::hideBalance, {QS("hideBalance"), false}}, {Config::hideNotifications, {QS("hideNotifications"), false}}, @@ -94,6 +98,14 @@ static const QHash configStrings = { {Config::redditFrontend, {QS("redditFrontend"), "old.reddit.com"}}, {Config::localMoneroFrontend, {QS("localMoneroFrontend"), "https://localmonero.co"}}, {Config::bountiesFrontend, {QS("bountiesFrontend"), "https://bounties.monero.social"}}, + {Config::lastPath, {QS("lastPath"), QDir::homePath()}}, + + {Config::URmsPerFragment, {QS("URmsPerFragment"), 80}}, + {Config::URfragmentLength, {QS("URfragmentLength"), 150}}, + {Config::URfountainCode, {QS("URfountainCode"), false}}, + + {Config::cameraManualExposure, {QS("cameraManualExposure"), false}}, + {Config::cameraExposureTime, {QS("cameraExposureTime"), 10}}, {Config::fiatSymbols, {QS("fiatSymbols"), QStringList{"USD", "EUR", "GBP", "CAD", "AUD", "RUB"}}}, {Config::cryptoSymbols, {QS("cryptoSymbols"), QStringList{"BTC", "ETH", "LTC", "XMR", "ZEC"}}}, diff --git a/src/utils/config.h b/src/utils/config.h index 4ce33a5..546a5d4 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -24,6 +24,7 @@ public: firstRun, warnOnStagenet, warnOnTestnet, + warnOnKiImport, homeWidget, donateBeg, @@ -32,6 +33,7 @@ public: geometry, windowState, GUI_HistoryViewState, + geometryOTSWizard, // Wallets walletDirectory, // Directory where wallet files are stored @@ -118,12 +120,24 @@ public: // Transactions multiBroadcast, + offlineTxSigningMethod, + offlineTxSigningForceKISync, // Misc blockExplorer, redditFrontend, localMoneroFrontend, bountiesFrontend, // unused + lastPath, + + // UR + URmsPerFragment, + URfragmentLength, + URfountainCode, + + // Camera + cameraManualExposure, + cameraExposureTime, fiatSymbols, cryptoSymbols, @@ -153,6 +167,11 @@ public: socks5 }; + enum OTSMethod { + UnifiedResources = 0, + FileTransfer + }; + ~Config() override; QVariant get(ConfigKey key); QString getFileName(); diff --git a/src/widgets/QrCodeWidget.cpp b/src/widgets/QrCodeWidget.cpp index 44e0a81..90efe34 100644 --- a/src/widgets/QrCodeWidget.cpp +++ b/src/widgets/QrCodeWidget.cpp @@ -12,6 +12,10 @@ QrCodeWidget::QrCodeWidget(QWidget *parent) : QWidget(parent) } void QrCodeWidget::setQrCode(QrCode *qrCode) { + if (m_qrcode) { + delete m_qrcode; + } + m_qrcode = qrCode; int k = m_qrcode->width(); diff --git a/src/widgets/TxDetailsSimple.cpp b/src/widgets/TxDetailsSimple.cpp new file mode 100644 index 0000000..09113a3 --- /dev/null +++ b/src/widgets/TxDetailsSimple.cpp @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "TxDetailsSimple.h" +#include "ui_TxDetailsSimple.h" + +#include "constants.h" +#include "libwalletqt/WalletManager.h" +#include "utils/AppData.h" +#include "utils/ColorScheme.h" +#include "utils/config.h" +#include "utils/Utils.h" + +TxDetailsSimple::TxDetailsSimple(QWidget *parent) + : QWidget(parent) + , ui(new Ui::TxDetailsSimple) +{ + ui->setupUi(this); + + ui->label_amount->setFont(Utils::getMonospaceFont()); + ui->label_fee->setFont(Utils::getMonospaceFont()); + ui->label_total->setFont(Utils::getMonospaceFont()); +} + +void TxDetailsSimple::setDetails(Wallet *wallet, PendingTransaction *tx, const QString &address) { + ui->label_note->hide(); + + QString preferredCur = conf()->get(Config::preferredFiatCurrency).toString(); + + auto convert = [preferredCur](double amount){ + return QString::number(appData()->prices.convert("XMR", preferredCur, amount), 'f', 2); + }; + + QString amount = WalletManager::displayAmount(tx->amount()); + QString fee = WalletManager::displayAmount(tx->fee()); + QString total = WalletManager::displayAmount(tx->amount() + tx->fee()); + QVector amounts = {amount, fee, total}; + int maxLength = Utils::maxLength(amounts); + std::for_each(amounts.begin(), amounts.end(), [maxLength](QString& amount){amount = amount.rightJustified(maxLength, ' ');}); + + QString amount_fiat = convert(tx->amount() / constants::cdiv); + QString fee_fiat = convert(tx->fee() / constants::cdiv); + QString total_fiat = convert((tx->amount() + tx->fee()) / constants::cdiv); + QVector amounts_fiat = {amount_fiat, fee_fiat, total_fiat}; + int maxLengthFiat = Utils::maxLength(amounts_fiat); + std::for_each(amounts_fiat.begin(), amounts_fiat.end(), [maxLengthFiat](QString& amount){amount = amount.rightJustified(maxLengthFiat, ' ');}); + + ui->label_amount->setText(QString("%1 (%2 %3)").arg(amounts[0], amounts_fiat[0], preferredCur)); + ui->label_fee->setText(QString("%1 (%2 %3)").arg(amounts[1], amounts_fiat[1], preferredCur)); + ui->label_total->setText(QString("%1 (%2 %3)").arg(amounts[2], amounts_fiat[2], preferredCur)); + + auto subaddressIndex = wallet->subaddressIndex(address); + QString addressExtra; + + ui->label_address->setText(Utils::displayAddress(address, 2)); + ui->label_address->setFont(Utils::getMonospaceFont()); + ui->label_address->setToolTip(address); + + if (subaddressIndex.isValid()) { + ui->label_note->setText("Note: this is a churn transaction."); + ui->label_note->show(); + ui->label_address->setStyleSheet(ColorScheme::GREEN.asStylesheet(true)); + ui->label_address->setToolTip("Wallet receive address"); + } + + if (subaddressIndex.isPrimary()) { + ui->label_address->setStyleSheet(ColorScheme::YELLOW.asStylesheet(true)); + ui->label_address->setToolTip("Wallet change/primary address"); + } + + if (tx->fee() > WalletManager::amountFromDouble(0.01)) { + ui->label_fee->setStyleSheet(ColorScheme::RED.asStylesheet(true)); + ui->label_fee->setToolTip("Unrealistic fee. You may be connected to a malicious node."); + } +} + +TxDetailsSimple::~TxDetailsSimple() = default; \ No newline at end of file diff --git a/src/widgets/TxDetailsSimple.h b/src/widgets/TxDetailsSimple.h new file mode 100644 index 0000000..b6f1cea --- /dev/null +++ b/src/widgets/TxDetailsSimple.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_TXDETAILSSIMPLE_H +#define FEATHER_TXDETAILSSIMPLE_H + +#include + +#include "components.h" +#include "libwalletqt/PendingTransaction.h" +#include "libwalletqt/WalletManager.h" +#include "libwalletqt/Wallet.h" + +namespace Ui { + class TxDetailsSimple; +} + +class TxDetailsSimple : public QWidget +{ + Q_OBJECT + +public: + explicit TxDetailsSimple(QWidget *parent); + ~TxDetailsSimple() override; + + void setDetails(Wallet *wallet, PendingTransaction *tx, const QString &address); + +private: + QScopedPointer ui; +}; + +#endif //FEATHER_TXDETAILSSIMPLE_H diff --git a/src/widgets/TxDetailsSimple.ui b/src/widgets/TxDetailsSimple.ui new file mode 100644 index 0000000..275fb40 --- /dev/null +++ b/src/widgets/TxDetailsSimple.ui @@ -0,0 +1,116 @@ + + + TxDetailsSimple + + + + 0 + 0 + 386 + 152 + + + + Form + + + + + + note + + + true + + + + + + + 15 + + + 7 + + + + + Address: + + + + + + + address + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Amount: + + + + + + + amount + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Fee: + + + + + + + fee + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal + + + + + + + Total: + + + + + + + total + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + diff --git a/src/widgets/URWidget.cpp b/src/widgets/URWidget.cpp new file mode 100644 index 0000000..da1adb0 --- /dev/null +++ b/src/widgets/URWidget.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "URWidget.h" +#include "ui_URWidget.h" + +#include +#include + +#include "dialog/URSettingsDialog.h" +#include "utils/config.h" + +URWidget::URWidget(QWidget *parent) + : QWidget(parent) + , ui(new Ui::URWidget) +{ + ui->setupUi(this); + + connect(&m_timer, &QTimer::timeout, this, &URWidget::nextQR); + connect(ui->btn_options, &QPushButton::clicked, this, &URWidget::setOptions); +} + +void URWidget::setData(const QString &type, const std::string &data) { + m_type = type; + m_data = data; + + m_timer.stop(); + allParts.clear(); + + if (m_data.empty()) { + return; + } + + std::string type_std = m_type.toStdString(); + + ur::ByteVector a = ur::string_to_bytes(m_data); + ur::ByteVector cbor; + ur::CborLite::encodeBytes(cbor, a); + ur::UR h = ur::UR(type_std, cbor); + + int bytesPerFragment = conf()->get(Config::URfragmentLength).toInt(); + + delete m_urencoder; + m_urencoder = new ur::UREncoder(h, bytesPerFragment); + + for (int i=0; i < m_urencoder->seq_len(); i++) { + allParts.append(m_urencoder->next_part()); + } + + m_timer.setInterval(conf()->get(Config::URmsPerFragment).toInt()); + m_timer.start(); +} + +void URWidget::nextQR() { + currentIndex = currentIndex % m_urencoder->seq_len(); + + std::string data; + if (conf()->get(Config::URfountainCode).toBool()) { + data = m_urencoder->next_part(); + } else { + data = allParts[currentIndex]; + } + + ui->label_seq->setText(QString("%1/%2").arg(QString::number(currentIndex % m_urencoder->seq_len() + 1), QString::number(m_urencoder->seq_len()))); + + m_code = new QrCode{QString::fromStdString(data), QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::MEDIUM}; + ui->qrWidget->setQrCode(m_code); + + currentIndex += 1; +} + +void URWidget::setOptions() { + URSettingsDialog dialog{this}; + dialog.exec(); + this->setData(m_type, m_data); +} + +URWidget::~URWidget() { + delete m_urencoder; +} diff --git a/src/widgets/URWidget.h b/src/widgets/URWidget.h new file mode 100644 index 0000000..acf9df4 --- /dev/null +++ b/src/widgets/URWidget.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_URWIDGET_H +#define FEATHER_URWIDGET_H + +#include +#include + +#include "qrcode/QrCode.h" +#include "widgets/QrCodeWidget.h" +#include + +namespace Ui { + class URWidget; +} + +class URWidget : public QWidget +{ + Q_OBJECT + +public: + explicit URWidget(QWidget *parent = nullptr); + ~URWidget(); + + void setData(const QString &type, const std::string &data); + +private slots: + void nextQR(); + void setOptions(); + +private: + QScopedPointer ui; + QTimer m_timer; + ur::UREncoder *m_urencoder = nullptr; + QrCode *m_code = nullptr; + QList allParts; + qsizetype currentIndex = 0; + + std::string m_data; + QString m_type; +}; + +#endif //FEATHER_URWIDGET_H diff --git a/src/widgets/URWidget.ui b/src/widgets/URWidget.ui new file mode 100644 index 0000000..7d224a7 --- /dev/null +++ b/src/widgets/URWidget.ui @@ -0,0 +1,85 @@ + + + URWidget + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + + 0 + 0 + + + + Qt::DefaultContextMenu + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + ... + + + + + + + + + + + :/assets/images/preferences.svg:/assets/images/preferences.svg + + + + + + + + + + QrCodeWidget + QWidget +
widgets/QrCodeWidget.h
+ 1 +
+
+ + + + +
diff --git a/src/wizard/WalletWizard.cpp b/src/wizard/WalletWizard.cpp index acb50a3..6aae57e 100644 --- a/src/wizard/WalletWizard.cpp +++ b/src/wizard/WalletWizard.cpp @@ -68,7 +68,6 @@ WalletWizard::WalletWizard(QWidget *parent) setOption(QWizard::HaveHelpButton, true); setOption(QWizard::HaveCustomButton1, true); - // Set up a custom button layout QList layout; layout << QWizard::HelpButton; layout << QWizard::CustomButton1; diff --git a/src/wizard/offline_tx_signing/OfflineTxSigningWizard.cpp b/src/wizard/offline_tx_signing/OfflineTxSigningWizard.cpp new file mode 100644 index 0000000..ca3052e --- /dev/null +++ b/src/wizard/offline_tx_signing/OfflineTxSigningWizard.cpp @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "OfflineTxSigningWizard.h" + +#include "PageOTS_ExportOutputs.h" +#include "PageOTS_ImportKeyImages.h" +#include "PageOTS_ExportUnsignedTx.h" +#include "PageOTS_ExportSignedTx.h" + +#include "PageOTS_ImportOffline.h" +#include "PageOTS_ExportKeyImages.h" +#include "PageOTS_ImportUnsignedTx.h" +#include "PageOTS_SignTx.h" +#include "PageOTS_ImportSignedTx.h" + +#include +#include +#include + +#include "utils/config.h" + +OfflineTxSigningWizard::OfflineTxSigningWizard(QWidget *parent, Wallet *wallet, PendingTransaction *tx) + : QWizard(parent) + , m_wallet(wallet) +{ + m_wizardFields.scanWidget = new QrCodeScanWidget(nullptr); + + // View-only + setPage(Page_ExportOutputs, new PageOTS_ExportOutputs(this, m_wallet)); + setPage(Page_ImportKeyImages, new PageOTS_ImportKeyImages(this, m_wallet, &m_wizardFields)); + setPage(Page_ExportUnsignedTx, new PageOTS_ExportUnsignedTx(this, m_wallet, tx)); + setPage(Page_ImportSignedTx, new PageOTS_ImportSignedTx(this, m_wallet, &m_wizardFields)); + + // Offline + setPage(Page_ImportOffline, new PageOTS_ImportOffline(this, m_wallet, &m_wizardFields)); + setPage(Page_ExportKeyImages, new PageOTS_ExportKeyImages(this, m_wallet, &m_wizardFields)); + setPage(Page_ImportUnsignedTx, new PageOTS_ImportUnsignedTx(this, m_wallet, &m_wizardFields)); + setPage(Page_SignTx, new PageOTS_SignTx(this)); + setPage(Page_ExportSignedTx, new PageOTS_ExportSignedTx(this, m_wallet, &m_wizardFields)); + + if (tx) { + setStartId(Page_ExportUnsignedTx); + } else { + setStartId(m_wallet->viewOnly() ? Page_ExportOutputs : Page_ImportOffline); + } + + this->setWindowTitle("Offline transaction signing"); + + QList layout; + layout << QWizard::CancelButton; + layout << QWizard::HelpButton; + layout << QWizard::Stretch; + layout << QWizard::BackButton; + layout << QWizard::NextButton; + layout << QWizard::FinishButton; + layout << QWizard::CommitButton; + this->setButtonLayout(layout); + + setOption(QWizard::HaveHelpButton); + // setOption(QWizard::HaveCustomButton1, true); + setOption(QWizard::NoBackButtonOnStartPage); + setWizardStyle(WizardStyle::ModernStyle); + + bool geo = this->restoreGeometry(QByteArray::fromBase64(conf()->get(Config::geometryOTSWizard).toByteArray())); + if (!geo) { + QScreen *currentScreen = QApplication::screenAt(this->geometry().center()); + if (!currentScreen) { + currentScreen = QApplication::primaryScreen(); + } + int availableHeight = currentScreen->availableGeometry().height() - 100; + + this->resize(availableHeight, availableHeight); + } + + // Anti-glare + // QFile f(":qdarkstyle/style.qss"); + // if (!f.exists()) { + // printf("Unable to set stylesheet, file not found\n"); + // f.close(); + // } else { + // f.open(QFile::ReadOnly | QFile::Text); + // QTextStream ts(&f); + // QString qdarkstyle = ts.readAll(); + // this->setStyleSheet(qdarkstyle); + // } +} + +bool OfflineTxSigningWizard::readyToCommit() { + return m_wizardFields.readyToCommit; +} + +bool OfflineTxSigningWizard::readyToSign() { + return m_wizardFields.readyToSign; +} + +UnsignedTransaction* OfflineTxSigningWizard::unsignedTransaction() { + return m_wizardFields.utx; +} + +PendingTransaction* OfflineTxSigningWizard::signedTx() { + return m_wizardFields.tx; +} + +OfflineTxSigningWizard::~OfflineTxSigningWizard() { + conf()->set(Config::geometryOTSWizard, QString(saveGeometry().toBase64())); +} \ No newline at end of file diff --git a/src/wizard/offline_tx_signing/OfflineTxSigningWizard.h b/src/wizard/offline_tx_signing/OfflineTxSigningWizard.h new file mode 100644 index 0000000..b3f72b0 --- /dev/null +++ b/src/wizard/offline_tx_signing/OfflineTxSigningWizard.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_OFFLINETXSIGNINGWIZARD_H +#define FEATHER_OFFLINETXSIGNINGWIZARD_H + +#include +#include "Wallet.h" + +#include +#include "qrcode/scanner/QrCodeScanWidget.h" + +struct TxWizardFields { + UnsignedTransaction *utx = nullptr; + PendingTransaction *tx = nullptr; + std::string signedTx; + QrCodeScanWidget *scanWidget = nullptr; + bool readyToCommit = false; + bool readyToSign = false; + std::string keyImages; +}; + +class OfflineTxSigningWizard : public QWizard +{ + Q_OBJECT + +public: + enum Page { + Page_ExportOutputs = 0, + Page_ExportKeyImages, + Page_ImportKeyImages, + Page_ExportUnsignedTx, + Page_ImportUnsignedTx, + Page_SignTx, + Page_ExportSignedTx, + Page_ImportSignedTx, + Page_ImportOffline + }; + + enum Method { + UR = 0, + FILES, + }; + + explicit OfflineTxSigningWizard(QWidget *parent, Wallet *wallet, PendingTransaction *tx = nullptr); + ~OfflineTxSigningWizard() override; + + bool readyToCommit(); + bool readyToSign(); + UnsignedTransaction* unsignedTransaction(); + PendingTransaction* signedTx(); + +private: + Wallet *m_wallet; + TxWizardFields m_wizardFields; +}; + + +#endif //FEATHER_OFFLINETXSIGNINGWIZARD_H diff --git a/src/wizard/offline_tx_signing/PageOTS_Export.ui b/src/wizard/offline_tx_signing/PageOTS_Export.ui new file mode 100644 index 0000000..1a33ad5 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_Export.ui @@ -0,0 +1,123 @@ + + + PageOTS_Export + + + + 0 + 0 + 758 + 734 + + + + WizardPage + + + + + + + + Method: + + + + + + + + Animated QR Codes + + + + + Files + + + + + + + + + + details + + + + + + + 0 + + + + + + + + 0 + 0 + + + + + + + + Scan this animated QR code with your view-only wallet. + + + true + + + + + + + + + + + + + Export to file + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + URWidget + QWidget +
widgets/URWidget.h
+ 1 +
+
+ + +
diff --git a/src/wizard/offline_tx_signing/PageOTS_ExportKeyImages.cpp b/src/wizard/offline_tx_signing/PageOTS_ExportKeyImages.cpp new file mode 100644 index 0000000..721deb8 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ExportKeyImages.cpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "PageOTS_ExportKeyImages.h" +#include "ui_PageOTS_Export.h" +#include "OfflineTxSigningWizard.h" + +#include + +#include "utils/config.h" +#include "utils/Utils.h" + +PageOTS_ExportKeyImages::PageOTS_ExportKeyImages(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields) + : QWizardPage(parent) + , ui(new Ui::PageOTS_Export) + , m_wallet(wallet) + , m_wizardFields(wizardFields) +{ + ui->setupUi(this); + this->setTitle("2. Export key images"); + + ui->label_step->hide(); + ui->label_instructions->setText("Scan this animated QR code with the view-only wallet."); + + connect(ui->btn_export, &QPushButton::clicked, this, &PageOTS_ExportKeyImages::exportKeyImages); + connect(ui->combo_method, &QComboBox::currentIndexChanged, [this](int index){ + conf()->set(Config::offlineTxSigningMethod, index); + ui->stackedWidget->setCurrentIndex(index); + }); +} + +void PageOTS_ExportKeyImages::exportKeyImages() { + QString defaultName = QString("%1_%2").arg(m_wallet->walletName(), QString::number(QDateTime::currentSecsSinceEpoch())); + QString fn = Utils::getSaveFileName(this, "Save key images to file", defaultName, "Key Images (*_keyImages)"); + if (fn.isEmpty()) { + return; + } + if (!fn.endsWith("_keyImages")) { + fn += "_keyImages"; + } + + QFile file{fn}; + if (!file.open(QIODevice::WriteOnly)) { + Utils::showError(this, "Failed to export key images", QString("Could not open file %1 for writing").arg(fn)); + return; + } + + file.write(m_wizardFields->keyImages.data(), m_wizardFields->keyImages.size()); + file.close(); + + QFileInfo fileInfo(fn); + Utils::openDir(this, "Successfully exported key images", fileInfo.absolutePath()); +} + +void PageOTS_ExportKeyImages::setupUR(bool all) { + // TODO: check if empty + std::string ki_export; + m_wallet->exportKeyImagesToStr(ki_export, all); + ui->widget_UR->setData("xmr-keyimage", m_wizardFields->keyImages); +} + +void PageOTS_ExportKeyImages::initializePage() { + ui->combo_method->setCurrentIndex(conf()->get(Config::offlineTxSigningMethod).toInt()); + this->setupUR(false); +} + +int PageOTS_ExportKeyImages::nextId() const { + return OfflineTxSigningWizard::Page_ImportUnsignedTx; +} diff --git a/src/wizard/offline_tx_signing/PageOTS_ExportKeyImages.h b/src/wizard/offline_tx_signing/PageOTS_ExportKeyImages.h new file mode 100644 index 0000000..1fd33b3 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ExportKeyImages.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_PAGEOTS_EXPORTKEYIMAGES_H +#define FEATHER_PAGEOTS_EXPORTKEYIMAGES_H + +#include +#include "Wallet.h" +#include "OfflineTxSigningWizard.h" + +namespace Ui { + class PageOTS_Export; +} + +class PageOTS_ExportKeyImages : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageOTS_ExportKeyImages(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields); + void initializePage() override; + [[nodiscard]] int nextId() const override; + +private slots: + void exportKeyImages(); + +private: + void setupUR(bool all); + + Ui::PageOTS_Export *ui; + Wallet *m_wallet; + TxWizardFields *m_wizardFields; +}; + +#endif //FEATHER_PAGEOTS_EXPORTKEYIMAGES_H diff --git a/src/wizard/offline_tx_signing/PageOTS_ExportOutputs.cpp b/src/wizard/offline_tx_signing/PageOTS_ExportOutputs.cpp new file mode 100644 index 0000000..f8b8fa0 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ExportOutputs.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "PageOTS_ExportOutputs.h" +#include "ui_PageOTS_Export.h" +#include "OfflineTxSigningWizard.h" + +#include +#include + +#include "utils/Utils.h" +#include "utils/config.h" + +PageOTS_ExportOutputs::PageOTS_ExportOutputs(QWidget *parent, Wallet *wallet) + : QWizardPage(parent) + , ui(new Ui::PageOTS_Export) + , m_wallet(wallet) + , m_check_exportAll(new QCheckBox(this)) +{ + ui->setupUi(this); + this->setTitle("1. Export outputs"); + + ui->label_step->hide(); + ui->label_instructions->setText("Scan this animated QR code with your offline wallet (Tools → Offline Transaction Signing)."); + + m_check_exportAll->setText("Export all outputs"); + ui->layout_extra->addWidget(m_check_exportAll); + connect(m_check_exportAll, &QCheckBox::toggled, this, &PageOTS_ExportOutputs::setupUR); + + connect(ui->btn_export, &QPushButton::clicked, this, &PageOTS_ExportOutputs::exportOutputs); + connect(ui->combo_method, &QComboBox::currentIndexChanged, [this](int index){ + conf()->set(Config::offlineTxSigningMethod, index); + ui->stackedWidget->setCurrentIndex(index); + }); +} + +void PageOTS_ExportOutputs::exportOutputs() { + QString defaultName = QString("%1_%2").arg(m_wallet->walletName(), QString::number(QDateTime::currentSecsSinceEpoch())); + QString fn = Utils::getSaveFileName(this, "Save outputs to file", defaultName, "Outputs (*_outputs)"); + if (fn.isEmpty()) { + return; + } + if (!fn.endsWith("_outputs")) { + fn += "_outputs"; + } + + bool r = m_wallet->exportOutputs(fn, m_check_exportAll->isChecked()); + if (!r) { + Utils::showError(this, "Failed to export outputs", m_wallet->errorString()); + return; + } + + QFileInfo fileInfo(fn); + Utils::openDir(this, "Successfully exported outputs", fileInfo.absolutePath()); +} + +void PageOTS_ExportOutputs::setupUR(bool all) { + std::string output_export; + m_wallet->exportOutputsToStr(output_export, all); + ui->widget_UR->setData("xmr-output", output_export); +} + +void PageOTS_ExportOutputs::initializePage() { + ui->combo_method->setCurrentIndex(conf()->get(Config::offlineTxSigningMethod).toInt()); + this->setupUR(false); +} + +int PageOTS_ExportOutputs::nextId() const { + return OfflineTxSigningWizard::Page_ImportKeyImages; +} diff --git a/src/wizard/offline_tx_signing/PageOTS_ExportOutputs.h b/src/wizard/offline_tx_signing/PageOTS_ExportOutputs.h new file mode 100644 index 0000000..bca2ad3 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ExportOutputs.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_PAGEOTS_EXPORTOUTPUTS_H +#define FEATHER_PAGEOTS_EXPORTOUTPUTS_H + +#include +#include +#include "Wallet.h" + +namespace Ui { + class PageOTS_Export; +} + +class PageOTS_ExportOutputs : public QWizardPage +{ + Q_OBJECT + +public: + explicit PageOTS_ExportOutputs(QWidget *parent, Wallet *wallet); + void initializePage() override; + [[nodiscard]] int nextId() const override; + +private slots: + void exportOutputs(); + +private: + void setupUR(bool all); + + Ui::PageOTS_Export *ui; + QCheckBox *m_check_exportAll; + Wallet *m_wallet; +}; + + +#endif //FEATHER_PAGEOTS_EXPORTOUTPUTS_H diff --git a/src/wizard/offline_tx_signing/PageOTS_ExportSignedTx.cpp b/src/wizard/offline_tx_signing/PageOTS_ExportSignedTx.cpp new file mode 100644 index 0000000..f801ae3 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ExportSignedTx.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "PageOTS_ExportSignedTx.h" +#include "ui_PageOTS_Export.h" + +#include + +#include "OfflineTxSigningWizard.h" +#include "dialog/TxConfDialog.h" +#include "dialog/TxConfAdvDialog.h" +#include "utils/config.h" +#include "utils/Utils.h" + +PageOTS_ExportSignedTx::PageOTS_ExportSignedTx(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields) + : QWizardPage(parent) + , ui(new Ui::PageOTS_Export) + , m_wallet(wallet) + , m_wizardFields(wizardFields) +{ + ui->setupUi(this); + + this->setTitle("4. Export signed transaction"); + + ui->label_step->hide(); + ui->label_instructions->setText("Scan this animated QR code with your view-only wallet."); + + connect(ui->btn_export, &QPushButton::clicked, this, &PageOTS_ExportSignedTx::exportSignedTx); + connect(ui->combo_method, &QComboBox::currentIndexChanged, [this](int index){ + conf()->set(Config::offlineTxSigningMethod, index); + ui->stackedWidget->setCurrentIndex(index); + }); +} + +void PageOTS_ExportSignedTx::exportSignedTx() { + QString defaultName = QString("%1_signed_monero_tx").arg(QString::number(QDateTime::currentSecsSinceEpoch())); + QString fn = Utils::getSaveFileName(this, "Save signed transaction to file", defaultName, "Transaction (*signed_monero_tx)"); + if (fn.isEmpty()) { + return; + } + + bool r = m_wizardFields->utx->sign(fn); + + if (!r) { + Utils::showError(this, "Failed to save transaction to file"); + return; + } + + QFileInfo fileInfo(fn); + Utils::openDir(this, "Transaction saved successfully", fileInfo.absolutePath()); +} + +void PageOTS_ExportSignedTx::initializePage() { + if (!m_wizardFields->utx) { + Utils::showError(this, "Unknown error"); + this->close(); + } + + ui->combo_method->setCurrentIndex(conf()->get(Config::offlineTxSigningMethod).toInt()); + m_wizardFields->utx->signToStr(m_wizardFields->signedTx); + ui->widget_UR->setData("xmr-txsigned", m_wizardFields->signedTx); +} + +int PageOTS_ExportSignedTx::nextId() const { + return -1; +} diff --git a/src/wizard/offline_tx_signing/PageOTS_ExportSignedTx.h b/src/wizard/offline_tx_signing/PageOTS_ExportSignedTx.h new file mode 100644 index 0000000..d7e0d2e --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ExportSignedTx.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_PAGEOTS_EXPORTSIGNEDTX_H +#define FEATHER_PAGEOTS_EXPORTSIGNEDTX_H + +#include +#include "Wallet.h" +#include "OfflineTxSigningWizard.h" + +namespace Ui { + class PageOTS_Export; +} + +class PageOTS_ExportSignedTx : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageOTS_ExportSignedTx(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields); + void initializePage() override; + [[nodiscard]] int nextId() const override; + +private slots: + void exportSignedTx(); + +private: + Ui::PageOTS_Export *ui; + Wallet *m_wallet; + TxWizardFields *m_wizardFields; +}; + +#endif //FEATHER_PAGEOTS_EXPORTSIGNEDTX_H diff --git a/src/wizard/offline_tx_signing/PageOTS_ExportUnsignedTx.cpp b/src/wizard/offline_tx_signing/PageOTS_ExportUnsignedTx.cpp new file mode 100644 index 0000000..b063557 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ExportUnsignedTx.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "PageOTS_ExportUnsignedTx.h" +#include "ui_PageOTS_Export.h" +#include "OfflineTxSigningWizard.h" + +#include "utils/Utils.h" +#include "utils/config.h" + +PageOTS_ExportUnsignedTx::PageOTS_ExportUnsignedTx(QWidget *parent, Wallet *wallet, PendingTransaction *tx) + : QWizardPage(parent) + , ui(new Ui::PageOTS_Export) + , m_wallet(wallet) + , m_tx(tx) +{ + ui->setupUi(this); + this->setTitle("3. Export unsigned transaction"); + + ui->label_step->hide(); + ui->label_instructions->setText("Scan this animated QR code with the offline wallet."); + + connect(ui->btn_export, &QPushButton::clicked, this, &PageOTS_ExportUnsignedTx::exportUnsignedTx); + connect(ui->combo_method, &QComboBox::currentIndexChanged, [this](int index){ + conf()->set(Config::offlineTxSigningMethod, index); + ui->stackedWidget->setCurrentIndex(index); + }); +} + +void PageOTS_ExportUnsignedTx::initializePage() { + ui->combo_method->setCurrentIndex(conf()->get(Config::offlineTxSigningMethod).toInt()); + ui->widget_UR->setData("xmr-txunsigned", m_tx->unsignedTxToBin()); +} + +void PageOTS_ExportUnsignedTx::exportUnsignedTx() { + QString defaultName = QString("%1_unsigned_monero_tx").arg(QString::number(QDateTime::currentSecsSinceEpoch())); + QString fn = Utils::getSaveFileName(this, "Save transaction to file", defaultName, "Transaction (*unsigned_monero_tx)"); + if (fn.isEmpty()) { + return; + } + + bool r = m_tx->saveToFile(fn); + if (!r) { + Utils::showError(this, "Failed to export unsigned transaction", m_wallet->errorString()); + return; + } + + QFileInfo fileInfo(fn); + Utils::openDir(this, "Successfully exported unsigned transaction", fileInfo.absolutePath()); +} + +int PageOTS_ExportUnsignedTx::nextId() const { + return OfflineTxSigningWizard::Page_ImportSignedTx; +} diff --git a/src/wizard/offline_tx_signing/PageOTS_ExportUnsignedTx.h b/src/wizard/offline_tx_signing/PageOTS_ExportUnsignedTx.h new file mode 100644 index 0000000..0f8eeb8 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ExportUnsignedTx.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_PAGEOTS_EXPORTUNSIGNEDTX_H +#define FEATHER_PAGEOTS_EXPORTUNSIGNEDTX_H + +#include +#include "Wallet.h" +#include "PendingTransaction.h" + +namespace Ui { + class PageOTS_Export; +} + +class PageOTS_ExportUnsignedTx : public QWizardPage +{ + Q_OBJECT + +public: + explicit PageOTS_ExportUnsignedTx(QWidget *parent, Wallet *wallet, PendingTransaction *tx = nullptr); + void initializePage() override; + int nextId() const override; + +private slots: + void exportUnsignedTx(); + +private: + Ui::PageOTS_Export *ui; + Wallet *m_wallet; + PendingTransaction *m_tx; +}; + +#endif //FEATHER_PAGEOTS_EXPORTUNSIGNEDTX_H diff --git a/src/wizard/offline_tx_signing/PageOTS_Import.cpp b/src/wizard/offline_tx_signing/PageOTS_Import.cpp new file mode 100644 index 0000000..314815a --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_Import.cpp @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "PageOTS_Import.h" +#include "ui_PageOTS_Import.h" +#include "OfflineTxSigningWizard.h" + +#include + +#include "utils/config.h" +#include "utils/Icons.h" +#include "utils/Utils.h" + +PageOTS_Import::PageOTS_Import(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields, int step, const QString &type, const QString &fileType, const QString &successButtonText) + : QWizardPage(parent) + , m_wallet(wallet) + , m_wizardFields(wizardFields) + , m_scanWidget(wizardFields->scanWidget) + , m_type(type) + , m_fileType(fileType) + , m_successButtonText(successButtonText) + , ui(new Ui::PageOTS_Import) +{ + ui->setupUi(this); + + this->setTitle(QString("%1. Import %2").arg(QString::number(step), m_type)); + this->setCommitPage(true); + this->setButtonText(QWizard::CommitButton, "Next"); + this->setButtonText(QWizard::FinishButton, "Next"); + + ui->label_step->hide(); + ui->frame_status->hide(); + + connect(ui->btn_import, &QPushButton::clicked, this, &PageOTS_Import::importFromFile); + connect(ui->combo_method, &QComboBox::currentIndexChanged, [this](int index){ + conf()->set(Config::offlineTxSigningMethod, index); + ui->stackedWidget->setCurrentIndex(index); + }); +} + +void PageOTS_Import::onScanFinished(bool success) { + if (!success) { + m_scanWidget->pause(); + Utils::showError(this, "Failed to scan QR code", m_scanWidget->getURError()); + m_scanWidget->reset(); + return; + } + + std::string data = m_scanWidget->getURData(); + importFromStr(data); +} + +void PageOTS_Import::onSuccess() { + m_success = true; + emit completeChanged(); + + if (this->wizard()->button(QWizard::FinishButton)->isVisible()) { + this->wizard()->button(QWizard::FinishButton)->click(); + } else { + this->wizard()->button(QWizard::CommitButton)->click(); + } + + ui->frame_status->show(); + ui->frame_status->setInfo(icons()->icon("confirmed.svg"), QString("%1 imported successfully").arg(m_type)); + this->setButtonText(QWizard::FinishButton, m_successButtonText); +} + +void PageOTS_Import::importFromFile() { + QString fn = Utils::getOpenFileName(this, QString("Import %1 file").arg(m_type), QString("%1;;All Files (*)").arg(m_fileType)); + if (fn.isEmpty()) { + return; + } + + QFile file(fn); + if (!file.open(QIODevice::ReadOnly)) { + return; + } + + QByteArray qdata = file.readAll(); + std::string data = qdata.toStdString(); + file.close(); + + importFromStr(data); +} + +void PageOTS_Import::initializePage() { + ui->combo_method->setCurrentIndex(conf()->get(Config::offlineTxSigningMethod).toInt()); + m_scanWidget->reset(); + connect(m_scanWidget, &QrCodeScanWidget::finished, this, &PageOTS_Import::onScanFinished); + ui->layout_scanner->addWidget(m_scanWidget); + m_scanWidget->startCapture(true); +} + +bool PageOTS_Import::isComplete() const { + return m_success; +} + +bool PageOTS_Import::validatePage() { + m_scanWidget->disconnect(); + m_scanWidget->pause(); + return true; +} \ No newline at end of file diff --git a/src/wizard/offline_tx_signing/PageOTS_Import.h b/src/wizard/offline_tx_signing/PageOTS_Import.h new file mode 100644 index 0000000..97d76f7 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_Import.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_PAGEOTS_IMPORT_H +#define FEATHER_PAGEOTS_IMPORT_H + +#include +#include "Wallet.h" +#include "qrcode/scanner/QrCodeScanWidget.h" +#include "OfflineTxSigningWizard.h" + +namespace Ui { + class PageOTS_Import; +} + +class PageOTS_Import : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageOTS_Import(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields, int step, const QString &type, const QString &fileType, const QString &successButtonText = "Next"); + void initializePage() override; + bool validatePage() override; + bool isComplete() const override; + bool openFile(std::string &data); + +private slots: + void onScanFinished(bool success); + +private: + virtual void importFromStr(const std::string &data) = 0; + virtual void importFromFile(); + +protected: + void onSuccess(); + + Ui::PageOTS_Import *ui; + TxWizardFields *m_wizardFields; + QrCodeScanWidget *m_scanWidget; + bool m_success = false; + Wallet *m_wallet; + QString m_type; + QString m_successButtonText; + QString m_fileType; +}; + +#endif //FEATHER_PAGEOTS_IMPORT_H diff --git a/src/wizard/offline_tx_signing/PageOTS_Import.ui b/src/wizard/offline_tx_signing/PageOTS_Import.ui new file mode 100644 index 0000000..561a772 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_Import.ui @@ -0,0 +1,184 @@ + + + PageOTS_Import + + + + 0 + 0 + 741 + 499 + + + + WizardPage + + + + + + details + + + + + + + + + + 0 + 0 + + + + Method: + + + + + + + + Animated QR codes + + + + + File transfer + + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + + + + 0 + 0 + + + + Scan the animated QR code shown on the view-only wallet. + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Import from file + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/offline_tx_signing/PageOTS_ImportKeyImages.cpp b/src/wizard/offline_tx_signing/PageOTS_ImportKeyImages.cpp new file mode 100644 index 0000000..44e1643 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ImportKeyImages.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "PageOTS_ImportKeyImages.h" +#include "ui_PageOTS_Import.h" +#include "OfflineTxSigningWizard.h" + +#include +#include + +#include "utils/config.h" +#include "utils/Icons.h" +#include "utils/Utils.h" + +PageOTS_ImportKeyImages::PageOTS_ImportKeyImages(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields) + : PageOTS_Import(parent, wallet, wizardFields, 2, "key images", "Key Images (*_keyImages)", "Create transaction") +{ +} + +void PageOTS_ImportKeyImages::importFromStr(const std::string &data) { + if (!proceed()) { + m_scanWidget->reset(); + return; + } + + bool r = m_wallet->importKeyImagesFromStr(data); + if (!r) { + m_scanWidget->pause(); + Utils::showError(this, "Failed to import key images", m_wallet->errorString()); + m_scanWidget->reset(); + return; + } + + PageOTS_Import::onSuccess(); +} + +bool PageOTS_ImportKeyImages::proceed() { + if (!conf()->get(Config::warnOnKiImport).toBool()) { + return true; + } + + QMessageBox warning{this}; + warning.setWindowTitle("Warning"); + warning.setText("Key image import reveals which outputs you own to the node. " + "Make sure you are connected to a trusted node.\n\n" + "Do you want to proceed?"); + warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + + switch(warning.exec()) { + case QMessageBox::No: + return false; + default: + conf()->set(Config::warnOnKiImport, false); + return true; + } +} + +int PageOTS_ImportKeyImages::nextId() const { + return -1; +} diff --git a/src/wizard/offline_tx_signing/PageOTS_ImportKeyImages.h b/src/wizard/offline_tx_signing/PageOTS_ImportKeyImages.h new file mode 100644 index 0000000..70d04df --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ImportKeyImages.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_PAGEOTS_IMPORTKEYIMAGES_H +#define FEATHER_PAGEOTS_IMPORTKEYIMAGES_H + +#include +#include "Wallet.h" +#include "qrcode/scanner/QrCodeScanWidget.h" +#include "OfflineTxSigningWizard.h" +#include "PageOTS_Import.h" + +namespace Ui { + class PageOTS_Import; +} + +class PageOTS_ImportKeyImages : public PageOTS_Import +{ + Q_OBJECT + +public: + explicit PageOTS_ImportKeyImages(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields); + int nextId() const override; + +private slots: + void importFromStr(const std::string &data) override; + +private: + void onSuccess(); + bool proceed(); +}; + +#endif //FEATHER_PAGEOTS_IMPORTKEYIMAGES_H diff --git a/src/wizard/offline_tx_signing/PageOTS_ImportOffline.cpp b/src/wizard/offline_tx_signing/PageOTS_ImportOffline.cpp new file mode 100644 index 0000000..50d3923 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ImportOffline.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "PageOTS_ImportOffline.h" +#include "ui_PageOTS_Import.h" +#include "OfflineTxSigningWizard.h" + +#include + +#include "dialog/TxConfAdvDialog.h" +#include "utils/config.h" +#include "utils/Icons.h" +#include "utils/Utils.h" + +PageOTS_ImportOffline::PageOTS_ImportOffline(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields) + : PageOTS_Import(parent, wallet, wizardFields, 1, "outputs or unsigned transactions", "All Files (*)", "Next") +{ +} + +void PageOTS_ImportOffline::importFromStr(const std::string &data) { + Utils::Message message{this, Utils::ERROR}; + + if (this->isOutputs(data)) { + std::string keyImages; + bool r = m_wallet->exportKeyImagesForOutputsFromStr(data, keyImages); + if (!r) { + m_scanWidget->pause(); + message.title = "Failed to import outputs"; + QString error = m_wallet->errorString(); + message.description = error; + if (error.contains("Failed to decrypt")) { + message.helpItems = {"You may have opened the wrong view-only wallet."}; + } + Utils::showMsg(message); + m_scanWidget->reset(); + return; + } + + m_wizardFields->keyImages = keyImages; + ui->frame_status->show(); + ui->frame_status->setInfo(icons()->icon("confirmed.svg"), "Outputs imported successfully"); + } + else if (this->isUnsignedTransaction(data)) { + UnsignedTransaction *utx = m_wallet->loadUnsignedTransactionFromStr(data); + + if (utx->status() != UnsignedTransaction::Status_Ok) { + m_scanWidget->pause(); + message.title = "Failed to import unsigned transaction"; + QString error = m_wallet->errorString(); + message.description = error; + if (error.contains("Failed to decrypt")) { + message.helpItems = {"You may have opened the wrong view-only wallet."}; + } + Utils::showMsg(message); + m_scanWidget->reset(); + return; + } + + ui->frame_status->show(); + ui->frame_status->setInfo(icons()->icon("confirmed.svg"), "Unsigned transaction imported successfully"); + m_wizardFields->utx = utx; + m_wizardFields->readyToSign = true; + } + else { + Utils::showError(this, "Failed to import outputs or unsigned transaction", "Unrecognized data"); + return; + } + + PageOTS_Import::onSuccess(); +} + +bool PageOTS_ImportOffline::isOutputs(const std::string &data) { + std::string outputMagic = "Monero output export"; + const size_t magiclen = outputMagic.length(); + if (data.size() < magiclen || memcmp(data.data(), outputMagic.data(), magiclen) != 0) { + return false; + } + m_importType = ImportType::OUTPUTS; + return true; +} + +bool PageOTS_ImportOffline::isUnsignedTransaction(const std::string &data) { + std::string utxMagic = "Monero unsigned tx set"; + const size_t magiclen = utxMagic.length(); + if (data.size() < magiclen || memcmp(data.data(), utxMagic.data(), magiclen) != 0) { + return false; + } + m_importType = ImportType::UNSIGNED_TX; + return true; +} + + +int PageOTS_ImportOffline::nextId() const { + return m_importType == ImportType::OUTPUTS ? OfflineTxSigningWizard::Page_ExportKeyImages : OfflineTxSigningWizard::Page_SignTx; +} diff --git a/src/wizard/offline_tx_signing/PageOTS_ImportOffline.h b/src/wizard/offline_tx_signing/PageOTS_ImportOffline.h new file mode 100644 index 0000000..e676e56 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ImportOffline.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_PAGEOTS_IMPORTOFFLINE_H +#define FEATHER_PAGEOTS_IMPORTOFFLINE_H + +#include +#include "Wallet.h" +#include "qrcode/scanner/QrCodeScanWidget.h" +#include "OfflineTxSigningWizard.h" +#include "PageOTS_Import.h" + +namespace Ui { + class PageOTS_Import; +} + +class PageOTS_ImportOffline : public PageOTS_Import +{ +Q_OBJECT + +enum ImportType { + OUTPUTS = 0, + UNSIGNED_TX +}; + +public: + explicit PageOTS_ImportOffline(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields); + int nextId() const override; + +private slots: + void importFromStr(const std::string &data) override; + +private: + bool isOutputs(const std::string &data); + bool isUnsignedTransaction(const std::string &data); + + ImportType m_importType = UNSIGNED_TX; +}; + +#endif //FEATHER_PAGEOTS_IMPORT_H diff --git a/src/wizard/offline_tx_signing/PageOTS_ImportSignedTx.cpp b/src/wizard/offline_tx_signing/PageOTS_ImportSignedTx.cpp new file mode 100644 index 0000000..953a3d2 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ImportSignedTx.cpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "PageOTS_ImportSignedTx.h" +#include "ui_PageOTS_Import.h" +#include "OfflineTxSigningWizard.h" + +#include + +#include "dialog/TxConfDialog.h" +#include "dialog/TxConfAdvDialog.h" +#include "utils/config.h" +#include "utils/Icons.h" +#include "utils/Utils.h" + +PageOTS_ImportSignedTx::PageOTS_ImportSignedTx(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields) + : PageOTS_Import(parent, wallet, wizardFields, 4, "signed transaction", "Transaction (*signed_monero_tx)", "Send..") +{ +} + +void PageOTS_ImportSignedTx::importFromStr(const std::string &data) { + PendingTransaction *tx = m_wallet->loadSignedTxFromStr(data); + if (tx->status() != PendingTransaction::Status_Ok) { + m_scanWidget->pause(); + Utils::showError(this, "Failed to import signed transaction", m_wallet->errorString()); + m_scanWidget->reset(); + return; + } + + m_wizardFields->tx = tx; + PageOTS_Import::onSuccess(); +} + +int PageOTS_ImportSignedTx::nextId() const { + return -1; +} + +bool PageOTS_ImportSignedTx::validatePage() { + m_scanWidget->disconnect(); + m_wizardFields->readyToCommit = true; + return true; +} diff --git a/src/wizard/offline_tx_signing/PageOTS_ImportSignedTx.h b/src/wizard/offline_tx_signing/PageOTS_ImportSignedTx.h new file mode 100644 index 0000000..5615f79 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ImportSignedTx.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_PAGEOTS_IMPORTSIGNEDTX_H +#define FEATHER_PAGEOTS_IMPORTSIGNEDTX_H + +#include +#include "Wallet.h" +#include "qrcode/scanner/QrCodeScanWidget.h" +#include "OfflineTxSigningWizard.h" +#include "PageOTS_Import.h" + +namespace Ui { + class PageOTS_Import; +} + +class PageOTS_ImportSignedTx : public PageOTS_Import +{ +Q_OBJECT + +public: + explicit PageOTS_ImportSignedTx(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields); +// void initializePage() override; + int nextId() const override; + +private slots: + void importFromStr(const std::string &data) override; + +private: + bool validatePage() override; +}; + +#endif //FEATHER_PAGEOTS_IMPORTSIGNEDTX_H diff --git a/src/wizard/offline_tx_signing/PageOTS_ImportUnsignedTx.cpp b/src/wizard/offline_tx_signing/PageOTS_ImportUnsignedTx.cpp new file mode 100644 index 0000000..32e9f94 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ImportUnsignedTx.cpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "PageOTS_ImportUnsignedTx.h" +#include "ui_PageOTS_Import.h" +#include "OfflineTxSigningWizard.h" + +#include + +#include "dialog/TxConfAdvDialog.h" +#include "utils/config.h" +#include "utils/Icons.h" +#include "utils/Utils.h" + +PageOTS_ImportUnsignedTx::PageOTS_ImportUnsignedTx(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields) + : PageOTS_Import(parent, wallet, wizardFields, 3, "unsigned transaction", "Transaction (*unsigned_monero_tx)", "Review transaction") +{ +} + +void PageOTS_ImportUnsignedTx::importFromStr(const std::string &data) { + UnsignedTransaction *utx = m_wallet->loadUnsignedTransactionFromStr(data); + + if (utx->status() != UnsignedTransaction::Status_Ok) { + m_scanWidget->pause(); + Utils::showError(this, "Failed to import unsigned transaction", m_wallet->errorString()); + m_scanWidget->reset(); + return; + } + + m_wizardFields->utx = utx; + m_wizardFields->readyToSign = true; + PageOTS_Import::onSuccess(); +} + +int PageOTS_ImportUnsignedTx::nextId() const { + return -1; +} diff --git a/src/wizard/offline_tx_signing/PageOTS_ImportUnsignedTx.h b/src/wizard/offline_tx_signing/PageOTS_ImportUnsignedTx.h new file mode 100644 index 0000000..4045d43 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_ImportUnsignedTx.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_PAGEOTS_IMPORTUNSIGNEDTX_H +#define FEATHER_PAGEOTS_IMPORTUNSIGNEDTX_H + +#include +#include "Wallet.h" +#include "OfflineTxSigningWizard.h" +#include "PageOTS_Import.h" + +namespace Ui { + class PageOTS_Import; +} + +class PageOTS_ImportUnsignedTx : public PageOTS_Import +{ +Q_OBJECT + +public: + explicit PageOTS_ImportUnsignedTx(QWidget *parent, Wallet *wallet, TxWizardFields *wizardFields); + [[nodiscard]] int nextId() const override; + +private slots: + void importFromStr(const std::string &data) override; +}; + +#endif //FEATHER_PAGEOTS_IMPORTUNSIGNEDTX_H diff --git a/src/wizard/offline_tx_signing/PageOTS_SignTx.cpp b/src/wizard/offline_tx_signing/PageOTS_SignTx.cpp new file mode 100644 index 0000000..400d31e --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_SignTx.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "PageOTS_SignTx.h" + +PageOTS_SignTx::PageOTS_SignTx(QWidget *parent) + : QWizardPage(parent) +{ + // Serves no purpose other than to close the wizard. +} + +int PageOTS_SignTx::nextId() const { + return -1; +} + +void PageOTS_SignTx::initializePage() { + QTimer::singleShot(1, [this]{ + this->wizard()->button(QWizard::FinishButton)->click(); + }); +} \ No newline at end of file diff --git a/src/wizard/offline_tx_signing/PageOTS_SignTx.h b/src/wizard/offline_tx_signing/PageOTS_SignTx.h new file mode 100644 index 0000000..1a403f4 --- /dev/null +++ b/src/wizard/offline_tx_signing/PageOTS_SignTx.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_PAGEOTS_SIGNTX_H +#define FEATHER_PAGEOTS_SIGNTX_H + +#include +#include +#include "Wallet.h" +#include "OfflineTxSigningWizard.h" + +class PageOTS_SignTx : public QWizardPage +{ + Q_OBJECT + +public: + explicit PageOTS_SignTx(QWidget *parent); + void initializePage() override; + [[nodiscard]] int nextId() const override; +}; + + +#endif //FEATHER_PAGEOTS_SIGNTX_H