]> Nutra Git (v2) - gamesguru/feather.git/commitdiff
airgapped signing with UR
authortobtoht <tob@featherwallet.org>
Sat, 2 Dec 2023 18:28:31 +0000 (19:28 +0100)
committertobtoht <tob@featherwallet.org>
Mon, 4 Dec 2023 15:06:33 +0000 (16:06 +0100)
80 files changed:
CMakeLists.txt
monero
src/CMakeLists.txt
src/CoinsWidget.cpp
src/MainWindow.cpp
src/MainWindow.h
src/MainWindow.ui
src/SendWidget.cpp
src/SendWidget.h
src/assets.qrc
src/assets/images/sign.png [new file with mode: 0644]
src/components.h
src/dialog/AddressCheckerIndexDialog.cpp [new file with mode: 0644]
src/dialog/AddressCheckerIndexDialog.h [new file with mode: 0644]
src/dialog/AddressCheckerIndexDialog.ui [new file with mode: 0644]
src/dialog/TxConfAdvDialog.cpp
src/dialog/TxConfAdvDialog.h
src/dialog/TxConfAdvDialog.ui
src/dialog/URDialog.cpp [new file with mode: 0644]
src/dialog/URDialog.h [new file with mode: 0644]
src/dialog/URDialog.ui [new file with mode: 0644]
src/dialog/URSettingsDialog.cpp [new file with mode: 0644]
src/dialog/URSettingsDialog.h [new file with mode: 0644]
src/dialog/URSettingsDialog.ui [new file with mode: 0644]
src/libwalletqt/PendingTransaction.cpp
src/libwalletqt/PendingTransaction.h
src/libwalletqt/TransactionHistory.cpp
src/libwalletqt/UnsignedTransaction.cpp
src/libwalletqt/UnsignedTransaction.h
src/libwalletqt/Wallet.cpp
src/libwalletqt/Wallet.h
src/libwalletqt/rows/TransactionRow.cpp
src/libwalletqt/rows/TransactionRow.h
src/model/TransactionHistoryModel.cpp
src/qrcode/scanner/QrCodeScanDialog.cpp
src/qrcode/scanner/QrCodeScanDialog.h
src/qrcode/scanner/QrCodeScanDialog.ui
src/qrcode/scanner/QrCodeScanWidget.cpp [new file with mode: 0644]
src/qrcode/scanner/QrCodeScanWidget.h [new file with mode: 0644]
src/qrcode/scanner/QrCodeScanWidget.ui [new file with mode: 0644]
src/qrcode/scanner/QrScanThread.cpp
src/qrcode/scanner/QrScanThread.h
src/qrcode/utils/QrCodeUtils.cpp
src/qrcode/utils/QrCodeUtils.h
src/utils/Utils.cpp
src/utils/Utils.h
src/utils/config.cpp
src/utils/config.h
src/widgets/QrCodeWidget.cpp
src/widgets/TxDetailsSimple.cpp [new file with mode: 0644]
src/widgets/TxDetailsSimple.h [new file with mode: 0644]
src/widgets/TxDetailsSimple.ui [new file with mode: 0644]
src/widgets/URWidget.cpp [new file with mode: 0644]
src/widgets/URWidget.h [new file with mode: 0644]
src/widgets/URWidget.ui [new file with mode: 0644]
src/wizard/WalletWizard.cpp
src/wizard/offline_tx_signing/OfflineTxSigningWizard.cpp [new file with mode: 0644]
src/wizard/offline_tx_signing/OfflineTxSigningWizard.h [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_Export.ui [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ExportKeyImages.cpp [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ExportKeyImages.h [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ExportOutputs.cpp [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ExportOutputs.h [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ExportSignedTx.cpp [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ExportSignedTx.h [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ExportUnsignedTx.cpp [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ExportUnsignedTx.h [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_Import.cpp [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_Import.h [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_Import.ui [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ImportKeyImages.cpp [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ImportKeyImages.h [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ImportOffline.cpp [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ImportOffline.h [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ImportSignedTx.cpp [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ImportSignedTx.h [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ImportUnsignedTx.cpp [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_ImportUnsignedTx.h [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_SignTx.cpp [new file with mode: 0644]
src/wizard/offline_tx_signing/PageOTS_SignTx.h [new file with mode: 0644]

index ea152ef4c7c1ca6b3738ac7223f31645c7711fa0..02b2c89b0cd8b9546ae9ff6826363e5084cbad3d 100644 (file)
@@ -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 31ced6d76a1aaa1bfb9011c86987937b7042f3ce..34aacb1b49553f17b9bb7ca1ee6dfb6524aada55 160000 (submodule)
--- a/monero
+++ b/monero
@@ -1 +1 @@
-Subproject commit 31ced6d76a1aaa1bfb9011c86987937b7042f3ce
+Subproject commit 34aacb1b49553f17b9bb7ca1ee6dfb6524aada55
index 0803d61f04aa16f89a1e8d33937ccb293d039fd6..a3b7c7715e97a1a04c98ef74ccd67b207d39c899 100644 (file)
@@ -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)
index 94825a140ec783cf9db7fbd0f2fd4e3d6950a477..637f235b1c81a440e6cc9f48900e70ca151c399d 100644 (file)
 #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());
 }
 
index f5719aef7811bdb89bf5d1f23ef3729b5505e7ac..ef2ce74437f730cac5bc518ad04437d0ff3b9f71 100644 (file)
@@ -7,8 +7,10 @@
 #include <QFileDialog>
 #include <QInputDialog>
 #include <QMessageBox>
+#include <QCheckBox>
 
 #include "constants.h"
+#include "dialog/AddressCheckerIndexDialog.h"
 #include "dialog/BalanceDialog.h"
 #include "dialog/DebugInfoDialog.h"
 #include "dialog/PasswordDialog.h"
 
 #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<QString, QString> &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 QVector<QStr
 
     m_wallet->addCacheTransaction(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 QVector<QStr
 
 void MainWindow::onTransactionCommitted(bool success, PendingTransaction *tx, const QStringList& txid) {
     if (!success) {
-        Utils::showError(this, "Failed to send transaction", tx->errorString());
+        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();
 
index 2de4c45ca3fa650861b90ca0dd67258ebcc4ad33..b7ba34a0a752e68bac74431a489926c95d9c9dbc 100644 (file)
@@ -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();
index f1bdc7ecee98db131936bfb284604d9d69459e45..c1f355c89fe898641e1b0848040531b75a8ee7cb 100644 (file)
@@ -24,7 +24,7 @@
     <normaloff>:/assets/images/appicons/64x64.png</normaloff>:/assets/images/appicons/64x64.png</iconset>
   </property>
   <widget class="QWidget" name="centralWidget">
-   <layout class="QGridLayout" name="gridLayout">
+   <layout class="QVBoxLayout" name="verticalLayout_14">
     <property name="leftMargin">
      <number>0</number>
     </property>
     <property name="bottomMargin">
      <number>0</number>
     </property>
-    <property name="horizontalSpacing">
-     <number>12</number>
-    </property>
-    <item row="0" column="0">
+    <item>
      <widget class="QStackedWidget" name="stackedWidget">
       <property name="currentIndex">
-       <number>1</number>
+       <number>2</number>
       </property>
       <widget class="QWidget" name="page_wallet">
        <layout class="QVBoxLayout" name="verticalLayout_11">
         </item>
        </layout>
       </widget>
+      <widget class="QWidget" name="page_offline">
+       <layout class="QVBoxLayout" name="verticalLayout_13">
+        <property name="leftMargin">
+         <number>0</number>
+        </property>
+        <property name="topMargin">
+         <number>0</number>
+        </property>
+        <property name="rightMargin">
+         <number>0</number>
+        </property>
+        <property name="bottomMargin">
+         <number>0</number>
+        </property>
+        <item>
+         <widget class="QTabWidget" name="tabWidget_2">
+          <property name="currentIndex">
+           <number>0</number>
+          </property>
+          <widget class="QWidget" name="tab_5">
+           <attribute name="icon">
+            <iconset resource="assets.qrc">
+             <normaloff>:/assets/images/network.png</normaloff>:/assets/images/network.png</iconset>
+           </attribute>
+           <attribute name="title">
+            <string>Airgapped signing</string>
+           </attribute>
+           <layout class="QVBoxLayout" name="verticalLayout_16">
+            <item>
+             <widget class="QLabel" name="label_2">
+              <property name="text">
+               <string>Feather supports two airgapped transaction signing methods:</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QGroupBox" name="groupBox">
+              <property name="title">
+               <string/>
+              </property>
+              <layout class="QVBoxLayout" name="verticalLayout_15">
+               <item>
+                <layout class="QHBoxLayout" name="horizontalLayout_5">
+                 <item>
+                  <widget class="QRadioButton" name="radio_airgapUR">
+                   <property name="sizePolicy">
+                    <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="text">
+                    <string/>
+                   </property>
+                   <property name="checked">
+                    <bool>true</bool>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="ClickableLabel" name="label_airgapUR">
+                   <property name="sizePolicy">
+                    <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="text">
+                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use a webcam to scan &lt;span style=&quot; font-weight:700;&quot;&gt;animated QR codes&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+                   </property>
+                  </widget>
+                 </item>
+                </layout>
+               </item>
+               <item>
+                <layout class="QHBoxLayout" name="horizontalLayout_6">
+                 <item>
+                  <widget class="QRadioButton" name="radio_airgapFiles">
+                   <property name="sizePolicy">
+                    <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="text">
+                    <string/>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="ClickableLabel" name="label_airgapFiles">
+                   <property name="sizePolicy">
+                    <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="text">
+                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Transfer &lt;span style=&quot; font-weight:700;&quot;&gt;files&lt;/span&gt; between computers (using a flash drive)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+                   </property>
+                  </widget>
+                 </item>
+                </layout>
+               </item>
+              </layout>
+             </widget>
+            </item>
+            <item>
+             <spacer name="verticalSpacer_3">
+              <property name="orientation">
+               <enum>Qt::Vertical</enum>
+              </property>
+              <property name="sizeType">
+               <enum>QSizePolicy::Fixed</enum>
+              </property>
+              <property name="sizeHint" stdset="0">
+               <size>
+                <width>20</width>
+                <height>10</height>
+               </size>
+              </property>
+             </spacer>
+            </item>
+            <item>
+             <widget class="QLabel" name="label_5">
+              <property name="text">
+               <string>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.</string>
+              </property>
+              <property name="wordWrap">
+               <bool>true</bool>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QLabel" name="label_6">
+              <property name="text">
+               <string>Follow through the steps in the wizard. You may need to transfer/scan multiple files/QR codes.</string>
+              </property>
+              <property name="wordWrap">
+               <bool>true</bool>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <spacer name="verticalSpacer_4">
+              <property name="orientation">
+               <enum>Qt::Vertical</enum>
+              </property>
+              <property name="sizeType">
+               <enum>QSizePolicy::Fixed</enum>
+              </property>
+              <property name="sizeHint" stdset="0">
+               <size>
+                <width>20</width>
+                <height>10</height>
+               </size>
+              </property>
+             </spacer>
+            </item>
+            <item>
+             <layout class="QHBoxLayout" name="horizontalLayout_4">
+              <item>
+               <widget class="QPushButton" name="btn_help">
+                <property name="text">
+                 <string>Help</string>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <spacer name="horizontalSpacer_2">
+                <property name="orientation">
+                 <enum>Qt::Horizontal</enum>
+                </property>
+                <property name="sizeHint" stdset="0">
+                 <size>
+                  <width>40</width>
+                  <height>20</height>
+                 </size>
+                </property>
+               </spacer>
+              </item>
+              <item>
+               <widget class="QPushButton" name="btn_checkAddress">
+                <property name="text">
+                 <string>Show address</string>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <widget class="QPushButton" name="btn_signTransaction">
+                <property name="text">
+                 <string>Sign a transaction..</string>
+                </property>
+               </widget>
+              </item>
+             </layout>
+            </item>
+            <item>
+             <spacer name="verticalSpacer_2">
+              <property name="orientation">
+               <enum>Qt::Vertical</enum>
+              </property>
+              <property name="sizeHint" stdset="0">
+               <size>
+                <width>20</width>
+                <height>0</height>
+               </size>
+              </property>
+             </spacer>
+            </item>
+            <item>
+             <widget class="QLabel" name="label">
+              <property name="enabled">
+               <bool>false</bool>
+              </property>
+              <property name="text">
+               <string>If you're unsure what this is, disable 'Offline mode' by going to Settings â†’ Network â†’ Offline.</string>
+              </property>
+              <property name="wordWrap">
+               <bool>true</bool>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </widget>
+         </widget>
+        </item>
+       </layout>
+      </widget>
      </widget>
     </item>
    </layout>
      <property name="title">
       <string>Advanced</string>
      </property>
-     <widget class="QMenu" name="menuExport">
-      <property name="title">
-       <string>Export</string>
-      </property>
-      <addaction name="actionExportOutputs"/>
-      <addaction name="actionExportKeyImages"/>
-     </widget>
-     <widget class="QMenu" name="menuImport">
-      <property name="title">
-       <string>Import</string>
-      </property>
-      <addaction name="actionImportOutputs"/>
-      <addaction name="actionImportKeyImages"/>
-     </widget>
      <addaction name="actionStore_wallet"/>
      <addaction name="actionUpdate_balance"/>
      <addaction name="actionRefresh_tabs"/>
      <addaction name="actionRescan_spent"/>
      <addaction name="actionWallet_cache_debug"/>
-     <addaction name="separator"/>
-     <addaction name="menuExport"/>
-     <addaction name="menuImport"/>
     </widget>
     <addaction name="actionInformation"/>
     <addaction name="menuAdvanced"/>
     <property name="title">
      <string>Tools</string>
     </property>
-    <widget class="QMenu" name="menuLoad_transaction">
-     <property name="title">
-      <string>Load unsigned transaction</string>
-     </property>
-     <addaction name="actionLoadUnsignedTxFromFile"/>
-     <addaction name="actionLoadUnsignedTxFromClipboard"/>
-    </widget>
     <widget class="QMenu" name="menuLoad_signed_transaction">
      <property name="title">
       <string>Broadcast transaction</string>
     <addaction name="actionSignVerify"/>
     <addaction name="actionVerifyTxProof"/>
     <addaction name="separator"/>
-    <addaction name="menuLoad_transaction"/>
+    <addaction name="actionKeyImageSync"/>
     <addaction name="menuLoad_signed_transaction"/>
     <addaction name="actionImport_transaction"/>
     <addaction name="separator"/>
+    <addaction name="actionTransmitOverUR"/>
+    <addaction name="separator"/>
     <addaction name="actionPay_to_many"/>
     <addaction name="actionAddress_checker"/>
     <addaction name="actionCalculator"/>
     <string>Check for updates</string>
    </property>
   </action>
+  <action name="actionKeyImageSync">
+   <property name="text">
+    <string>Offline transaction signing</string>
+   </property>
+  </action>
+  <action name="actionTransmitOverUR">
+   <property name="text">
+    <string>Transmit over UR</string>
+   </property>
+  </action>
  </widget>
  <layoutdefault spacing="6" margin="11"/>
  <customwidgets>
    <header>plugins/bounties/BountiesWidget.h</header>
    <container>1</container>
   </customwidget>
+  <customwidget>
+   <class>ClickableLabel</class>
+   <extends>QLabel</extends>
+   <header>components.h</header>
+  </customwidget>
  </customwidgets>
  <resources>
   <include location="assets.qrc"/>
index 6edc132af71ffc726214d7e6785df88992fa051c..03ac74cfa5c9bdf9cf8cb152d7021a341176f65c 100644 (file)
@@ -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 <QMediaDevices>
 #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);
 }
 
index 431036dbf0a489c10fdb68c07a9012835c3e8029..f379b4673d7478e5b89571eb809828ab10d21497 100644 (file)
@@ -50,6 +50,7 @@ private slots:
 private:
     void setupComboBox();
     double amountDouble();
+    bool keyImageSync(bool sendAll, quint64 amount);
 
     quint64 amount();
     double conversionAmount();
index c59556c0c785cc97ed378d9a1a2ecc973cdfd508..f6a64daa4391bf6ee0423b30fafcfeb734f77cd8 100644 (file)
     <file>assets/images/trezor_white.png</file>
     <file>assets/images/trezor_unpaired.png</file>
     <file>assets/images/trezor_unpaired_white.png</file>
+    <file>assets/images/sign.png</file>
     <file>assets/images/unconfirmed.png</file>
     <file>assets/images/unlock.svg</file>
     <file>assets/images/unpaid.png</file>
diff --git a/src/assets/images/sign.png b/src/assets/images/sign.png
new file mode 100644 (file)
index 0000000..46eee91
Binary files /dev/null and b/src/assets/images/sign.png differ
index e00a7e82eeea95ffba7ba943a560173f2cc9defc..2085b5745c0e986d510a9dfc32fff5cedc0734f6 100644 (file)
@@ -93,4 +93,28 @@ private:
     QLabel *m_infoLabel;
 };
 
+class U32Validator : public QValidator {
+public:
+    U32Validator(QObject *parent = nullptr) : QValidator(parent) {}
+
+    QValidator::State validate(QString &input, int &pos) const override {
+        if (input.isEmpty()) {
+            return QValidator::Intermediate;
+        }
+
+        bool ok;
+        qint64 value = input.toLongLong(&ok);
+
+        if (!ok) {
+            return QValidator::Invalid;
+        }
+
+        if (value < 0 || value > 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 (file)
index 0000000..4c186fb
--- /dev/null
@@ -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 (file)
index 0000000..1b103e4
--- /dev/null
@@ -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::AddressCheckerIndexDialog> ui;
+    Wallet *m_wallet;
+};
+
+#endif //ADDRESSCHECKERINDEXDIALOG_H
diff --git a/src/dialog/AddressCheckerIndexDialog.ui b/src/dialog/AddressCheckerIndexDialog.ui
new file mode 100644 (file)
index 0000000..6621003
--- /dev/null
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AddressCheckerIndexDialog</class>
+ <widget class="QWidget" name="AddressCheckerIndexDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>712</width>
+    <height>127</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Check address</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Index:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLineEdit" name="line_index">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="btn_showAddress">
+       <property name="text">
+        <string>Show address</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string/>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout">
+      <item>
+       <widget class="QLabel" name="address">
+        <property name="text">
+         <string>...</string>
+        </property>
+        <property name="textInteractionFlags">
+         <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
index 31540ef3027652a4a2c5c558d71eb4485f49137a..941a871847860d579c2bafa43e3e33dca1f6181e 100644 (file)
@@ -6,6 +6,7 @@
 
 #include <QFileDialog>
 #include <QMessageBox>
+#include <QTreeWidgetItem>
 
 #include "constants.h"
 #include "dialog/QrCodeDialog.h"
 #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())));
-
-    auto outputs = ci->outputs();
-
-    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();
-    }
-    ui->label_outputs->setText(QString("Outputs (%1)").arg(QString::number(outputs.size())));
-
-    ui->label_ringSize->setText(QString("Ring size: %1").arg(QString::number(ci->minMixinCount() + 1)));
-}
-
-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;
+    ui->treeInputs->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+    ui->treeInputs->resizeColumnToContents(1);
+    ui->treeInputs->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+
+    ui->label_inputs->setText(QString("Inputs (%1)").arg(QString::number(ci->inputs().size())));
+
+    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->treeOutputs->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+    ui->treeOutputs->resizeColumnToContents(1);
+    ui->treeOutputs->header()->setSectionResizeMode(0, QHeaderView::Stretch);
 
-    bool success = m_utx->sign(fn);
+    ui->label_outputs->setText(QString("Outputs (%1)").arg(QString::number(ci->outputs().size())));
 
-    if (success) {
-        Utils::showInfo(this, "Transaction saved successfully");
-    } else {
-        Utils::showError(this, "Failed to save transaction to file");
-    }
+    this->adjustSize();
 }
 
-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");
-    }
+void TxConfAdvDialog::signTransaction() {
+    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();
 }
 
index 443a455d3623b7b42bed3608f604c010e5d8c68e..75d558a73426155de87c1356b9b584af41d90ea6 100644 (file)
@@ -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
index 80fbc07b8d0885d004a22d53a8be50becf570d08..964f1c872ce27b66973c88d47a4a562ee3149ef2 100644 (file)
@@ -7,7 +7,7 @@
     <x>0</x>
     <y>0</y>
     <width>800</width>
-    <height>542</height>
+    <height>810</height>
    </rect>
   </property>
   <property name="minimumSize">
@@ -23,7 +23,7 @@
    <item>
     <layout class="QFormLayout" name="formLayout_2">
      <item row="0" column="0">
-      <widget class="QLabel" name="label">
+      <widget class="QLabel" name="label_txid">
        <property name="text">
         <string>Transaction ID:</string>
        </property>
        </property>
       </widget>
      </item>
-    </layout>
-   </item>
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout_2">
-     <item>
-      <layout class="QVBoxLayout" name="verticalLayout_3">
-       <item>
-        <layout class="QHBoxLayout" name="horizontalLayout_3">
-         <item>
-          <widget class="QLabel" name="label_description">
-           <property name="text">
-            <string>Description:</string>
-           </property>
-           <property name="textInteractionFlags">
-            <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
-           </property>
-          </widget>
-         </item>
-         <item>
-          <widget class="QLineEdit" name="line_description"/>
-         </item>
-        </layout>
-       </item>
-       <item>
-        <widget class="QLabel" name="label_size">
-         <property name="text">
-          <string>Size: </string>
-         </property>
-         <property name="textInteractionFlags">
-          <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QLabel" name="label_ringSize">
-         <property name="text">
-          <string>Ringsize:</string>
-         </property>
-         <property name="textInteractionFlags">
-          <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
-         </property>
-        </widget>
-       </item>
-      </layout>
+     <item row="1" column="0">
+      <widget class="QLabel" name="label_amount">
+       <property name="text">
+        <string>Amount: </string>
+       </property>
+      </widget>
      </item>
-     <item>
-      <widget class="Line" name="line">
+     <item row="1" column="1">
+      <widget class="QLabel" name="amount">
+       <property name="text">
+        <string>TextLabel</string>
+       </property>
+       <property name="textInteractionFlags">
+        <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="0">
+      <widget class="QLabel" name="label_fee">
+       <property name="text">
+        <string>Fee: </string>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="1">
+      <widget class="QLabel" name="fee">
+       <property name="text">
+        <string>TextLabel</string>
+       </property>
+       <property name="textInteractionFlags">
+        <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+       </property>
+      </widget>
+     </item>
+     <item row="3" column="1">
+      <widget class="Line" name="line_2">
        <property name="orientation">
-        <enum>Qt::Vertical</enum>
+        <enum>Qt::Horizontal</enum>
        </property>
       </widget>
      </item>
-     <item>
-      <layout class="QFormLayout" name="formLayout">
-       <item row="0" column="0">
-        <widget class="QLabel" name="label_amount">
-         <property name="text">
-          <string>Amount: </string>
-         </property>
-        </widget>
-       </item>
-       <item row="0" column="1">
-        <widget class="QLabel" name="amount">
-         <property name="text">
-          <string>TextLabel</string>
-         </property>
-         <property name="textInteractionFlags">
-          <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
-         </property>
-        </widget>
-       </item>
-       <item row="1" column="0">
-        <widget class="QLabel" name="label_fee">
-         <property name="text">
-          <string>Fee: </string>
-         </property>
-        </widget>
-       </item>
-       <item row="1" column="1">
-        <widget class="QLabel" name="fee">
-         <property name="text">
-          <string>TextLabel</string>
-         </property>
-         <property name="textInteractionFlags">
-          <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
-         </property>
-        </widget>
-       </item>
-       <item row="2" column="1">
-        <widget class="Line" name="line_2">
-         <property name="orientation">
-          <enum>Qt::Horizontal</enum>
-         </property>
-        </widget>
-       </item>
-       <item row="3" column="0">
-        <widget class="QLabel" name="label_4">
-         <property name="text">
-          <string>Total:</string>
-         </property>
-        </widget>
-       </item>
-       <item row="3" column="1">
-        <widget class="QLabel" name="total">
-         <property name="text">
-          <string>TextLabel</string>
-         </property>
-         <property name="textInteractionFlags">
-          <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
-         </property>
-        </widget>
-       </item>
-      </layout>
+     <item row="4" column="0">
+      <widget class="QLabel" name="label_4">
+       <property name="text">
+        <string>Total:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="4" column="1">
+      <widget class="QLabel" name="total">
+       <property name="text">
+        <string>TextLabel</string>
+       </property>
+       <property name="textInteractionFlags">
+        <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+       </property>
+      </widget>
      </item>
     </layout>
    </item>
     </widget>
    </item>
    <item>
-    <widget class="QTextEdit" name="inputs">
-     <property name="sizePolicy">
-      <sizepolicy hsizetype="Expanding" vsizetype="Maximum">
-       <horstretch>0</horstretch>
-       <verstretch>0</verstretch>
-      </sizepolicy>
+    <widget class="QTreeWidget" name="treeInputs">
+     <property name="selectionMode">
+      <enum>QAbstractItemView::NoSelection</enum>
      </property>
-     <property name="maximumSize">
-      <size>
-       <width>16777215</width>
-       <height>100</height>
-      </size>
-     </property>
-     <property name="readOnly">
-      <bool>true</bool>
+     <property name="rootIsDecorated">
+      <bool>false</bool>
      </property>
+     <attribute name="headerStretchLastSection">
+      <bool>false</bool>
+     </attribute>
+     <column>
+      <property name="text">
+       <string>Pubkey</string>
+      </property>
+     </column>
+     <column>
+      <property name="text">
+       <string>Amount</string>
+      </property>
+     </column>
     </widget>
    </item>
    <item>
-    <widget class="QLabel" name="label_outputs">
-     <property name="text">
-      <string>Outputs</string>
-     </property>
-    </widget>
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <item>
+      <widget class="QLabel" name="label_outputs">
+       <property name="text">
+        <string>Outputs</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer_2">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
    </item>
    <item>
-    <widget class="QTextEdit" name="outputs">
-     <property name="sizePolicy">
-      <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
-       <horstretch>0</horstretch>
-       <verstretch>0</verstretch>
-      </sizepolicy>
+    <widget class="QTreeWidget" name="treeOutputs">
+     <property name="selectionMode">
+      <enum>QAbstractItemView::NoSelection</enum>
      </property>
-     <property name="readOnly">
-      <bool>true</bool>
+     <property name="rootIsDecorated">
+      <bool>false</bool>
      </property>
+     <attribute name="headerStretchLastSection">
+      <bool>false</bool>
+     </attribute>
+     <column>
+      <property name="text">
+       <string>Address</string>
+      </property>
+     </column>
+     <column>
+      <property name="text">
+       <string>Amount</string>
+      </property>
+     </column>
     </widget>
    </item>
    <item>
    </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout">
-     <item>
-      <widget class="QToolButton" name="btn_exportUnsigned">
-       <property name="text">
-        <string>Export unsigned</string>
-       </property>
-       <property name="popupMode">
-        <enum>QToolButton::InstantPopup</enum>
-       </property>
-      </widget>
-     </item>
      <item>
       <widget class="QToolButton" name="btn_exportSigned">
        <property name="text">
       </spacer>
      </item>
      <item>
-      <widget class="QPushButton" name="btn_sign">
+      <widget class="QPushButton" name="btn_close">
        <property name="text">
-        <string>Sign</string>
+        <string>Cancel</string>
        </property>
       </widget>
      </item>
      <item>
-      <widget class="QPushButton" name="btn_close">
+      <widget class="QPushButton" name="btn_sign">
        <property name="text">
-        <string>Cancel</string>
+        <string>Sign</string>
+       </property>
+       <property name="icon">
+        <iconset resource="../assets.qrc">
+         <normaloff>:/assets/images/sign.png</normaloff>:/assets/images/sign.png</iconset>
        </property>
       </widget>
      </item>
    </item>
   </layout>
  </widget>
- <resources/>
+ <resources>
+  <include location="../assets.qrc"/>
+ </resources>
  <connections/>
 </ui>
diff --git a/src/dialog/URDialog.cpp b/src/dialog/URDialog.cpp
new file mode 100644 (file)
index 0000000..37444c6
--- /dev/null
@@ -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 <QFileDialog>
+
+#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 (file)
index 0000000..d7d871b
--- /dev/null
@@ -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 <QDialog>
+
+#include "components.h"
+
+namespace Ui {
+    class URDialog;
+}
+
+class URDialog : public WindowModalDialog
+{
+    Q_OBJECT
+
+public:
+    explicit URDialog(QWidget *parent = nullptr);
+    ~URDialog() override;
+
+private:
+    QScopedPointer<Ui::URDialog> ui;
+};
+
+
+#endif //FEATHER_URDIALOG_H
diff --git a/src/dialog/URDialog.ui b/src/dialog/URDialog.ui
new file mode 100644 (file)
index 0000000..aca80b7
--- /dev/null
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>URDialog</class>
+ <widget class="QDialog" name="URDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>788</width>
+    <height>703</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Transmit UR</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="tabSend">
+      <attribute name="title">
+       <string>Send</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_3">
+       <item>
+        <widget class="URWidget" name="widgetUR" native="true">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout">
+         <item>
+          <widget class="QPushButton" name="btn_loadFile">
+           <property name="text">
+            <string>Load file</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="btn_loadClipboard">
+           <property name="text">
+            <string>Load from clipboard</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <spacer name="horizontalSpacer">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tabReceive">
+      <attribute name="title">
+       <string>Receive</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <widget class="QrCodeScanWidget" name="widgetScanner" native="true"/>
+       </item>
+       <item>
+        <widget class="QRadioButton" name="radio_clipboard">
+         <property name="text">
+          <string>Copy to clipboard</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QRadioButton" name="radio_file">
+         <property name="text">
+          <string>Save to file</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>QrCodeScanWidget</class>
+   <extends>QWidget</extends>
+   <header>qrcode/scanner/QrCodeScanWidget.h</header>
+   <container>1</container>
+  </customwidget>
+  <customwidget>
+   <class>URWidget</class>
+   <extends>QWidget</extends>
+   <header>widgets/URWidget.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>URDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>URDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/src/dialog/URSettingsDialog.cpp b/src/dialog/URSettingsDialog.cpp
new file mode 100644 (file)
index 0000000..07eb4bc
--- /dev/null
@@ -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 <QFileDialog>
+
+#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 (file)
index 0000000..a12db7f
--- /dev/null
@@ -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 <QDialog>
+
+#include "components.h"
+
+namespace Ui {
+    class URSettingsDialog;
+}
+
+class URSettingsDialog : public WindowModalDialog
+{
+    Q_OBJECT
+
+public:
+    explicit URSettingsDialog(QWidget *parent = nullptr);
+    ~URSettingsDialog() override;
+
+private:
+    QScopedPointer<Ui::URSettingsDialog> ui;
+};
+
+
+#endif //FEATHER_URSETTINGSDIALOG_H
diff --git a/src/dialog/URSettingsDialog.ui b/src/dialog/URSettingsDialog.ui
new file mode 100644 (file)
index 0000000..f0c1fed
--- /dev/null
@@ -0,0 +1,166 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>URSettingsDialog</class>
+ <widget class="QDialog" name="URSettingsDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>595</width>
+    <height>174</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>UR Options</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QFormLayout" name="formLayout">
+     <item row="0" column="0">
+      <widget class="QLabel" name="label_2">
+       <property name="text">
+        <string>Fragment length</string>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <layout class="QHBoxLayout" name="horizontalLayout_2">
+       <item>
+        <widget class="QSpinBox" name="spin_fragmentLength">
+         <property name="minimum">
+          <number>10</number>
+         </property>
+         <property name="maximum">
+          <number>1000</number>
+         </property>
+         <property name="singleStep">
+          <number>10</number>
+         </property>
+         <property name="value">
+          <number>100</number>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="label">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="text">
+          <string>bytes</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </item>
+     <item row="1" column="0">
+      <widget class="QLabel" name="label_3">
+       <property name="text">
+        <string>Speed</string>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="1">
+      <layout class="QHBoxLayout" name="horizontalLayout">
+       <item>
+        <widget class="QSpinBox" name="spin_speed">
+         <property name="minimum">
+          <number>10</number>
+         </property>
+         <property name="maximum">
+          <number>1000</number>
+         </property>
+         <property name="singleStep">
+          <number>10</number>
+         </property>
+         <property name="value">
+          <number>100</number>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="label_4">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="text">
+          <string>ms / fragment</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QCheckBox" name="check_fountainCode">
+     <property name="text">
+      <string>Fountain code</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_3">
+     <item>
+      <widget class="QPushButton" name="btn_reset">
+       <property name="text">
+        <string>Reset</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QDialogButtonBox" name="buttonBox">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="standardButtons">
+        <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>URSettingsDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>URSettingsDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
index 4fa7fad9126d8062e43d3d776002c92a876c7f44..c48fa98a3eb1872f28fc1cb9081a1d29ceab31a7 100644 (file)
@@ -69,8 +69,8 @@ QList<QVariant> 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
index 42b70eae36ef1fbb58efc5e9a0760c7ffa48ac71..17535e26a1103198d0c8b2c752529d557604b920 100644 (file)
@@ -40,7 +40,7 @@ public:
     QStringList txid() const;
     quint64 txCount() const;
     QList<QVariant> subaddrIndices() const;
-    QByteArray unsignedTxToBin() const;
+    std::string unsignedTxToBin() const;
     QString unsignedTxToBase64() const;
     QString signedTxToHex(int index) const;
     void refresh();
index d3bf977fcabe692d7a9c950bbcb1aecba5436f1d..35029925e0f2ed78357d23788b98773cf7b19749 100644 (file)
@@ -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;
index 14effc6306c13594d385027511a3f8a111062c48..7b94cead821d5ec90f58eac0cef5130731f096e3 100644 (file)
@@ -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)
index b79076f79af74525583a4681bfe3d21a793ddf8d..4f2cc106fd4edd4e9290dba44322d481a21c5722 100644 (file)
 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;
index 06f3457c50f809145cdd0f03ccc13bfbf3adb49a..774fdedf8e48b6961dfb46117fc213ef9aa9b16c 100644 (file)
@@ -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<std::string> 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<std::string> 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() {
index 747af3b36205317f301c2422a9c7a61db59076ea..f906981e5b5c16ce0e51e940fd9a17a788416fc6 100644 (file)
@@ -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<std::string> m_selectedInputs;
index 673fe92f44d243a887050fd600991fa9a52285bd..f50c4bc9ceb423a874ea41a41e359ac809ef813f 100644 (file)
@@ -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;
 }
index 8c59a0b4abed64b9de939acb07001dd6506cc761..6b6cc8e0e6368187f32bfbc3d16bb2422e372d43 100644 (file)
@@ -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<Transfer*> m_transfers;
     mutable QList<Ring*> 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;
index cd7c0024a5c5717fc24c083b1ba8c8fc06a36d22..dd7eb74a1f94761758f96329c09a588f9402f644 100644 (file)
@@ -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") {
index 6bc4eb1d0e387b2fdccc25903a1556f397da5139..e2bbd891a466add75dffc2fe8df02a1480711fc5 100644 (file)
 
 #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<QCameraDevice> cameras = QMediaDevices::videoInputs();
-    for (const auto &camera : cameras) {
-        ui->combo_camera->addItem(camera.description());
-    }
-    
-    connect(ui->combo_camera, QOverload<int>::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);
-}
-
-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<QCameraDevice> 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();
+    ui->widget_scanner->startCapture(scan_ur);
 }
 
-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
index 43ac9b08776c10aff5626e6ff3fc1a9667186401..0527c1e74f587c4930df4aa33f020d2e5f485966 100644 (file)
@@ -13,6 +13,8 @@
 
 #include "QrScanThread.h"
 
+#include <bcur/ur-decoder.hpp>
+
 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::QrCodeScanDialog> ui;
-
-    QrScanThread *m_thread;
-    QScopedPointer<QCamera> m_camera;
-    QMediaCaptureSession m_captureSession;
-    QVideoSink m_sink;
 };
 
 
index 9cda99941c41a8fa35b8f6f6c9269d680dcc3b32..139a87c5af4cd1b097a9fae5f9bf4b0a0235a010 100644 (file)
   </property>
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
-    <widget class="QVideoWidget" name="viewfinder" native="true">
-     <property name="sizePolicy">
-      <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
-       <horstretch>0</horstretch>
-       <verstretch>0</verstretch>
-      </sizepolicy>
-     </property>
-    </widget>
-   </item>
-   <item>
-    <widget class="QFrame" name="frame_unavailable">
-     <property name="frameShape">
-      <enum>QFrame::StyledPanel</enum>
-     </property>
-     <property name="frameShadow">
-      <enum>QFrame::Raised</enum>
-     </property>
-     <layout class="QHBoxLayout" name="horizontalLayout_3">
-      <item>
-       <widget class="QLabel" name="icon_warning">
-        <property name="sizePolicy">
-         <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
-          <horstretch>0</horstretch>
-          <verstretch>0</verstretch>
-         </sizepolicy>
-        </property>
-        <property name="text">
-         <string>icon</string>
-        </property>
-       </widget>
-      </item>
-      <item>
-       <spacer name="horizontalSpacer">
-        <property name="orientation">
-         <enum>Qt::Horizontal</enum>
-        </property>
-        <property name="sizeType">
-         <enum>QSizePolicy::Preferred</enum>
-        </property>
-        <property name="sizeHint" stdset="0">
-         <size>
-          <width>55</width>
-          <height>0</height>
-         </size>
-        </property>
-       </spacer>
-      </item>
-      <item>
-       <widget class="QLabel" name="label_2">
-        <property name="text">
-         <string>Lost connection to camera. Please restart scan dialog.</string>
-        </property>
-        <property name="wordWrap">
-         <bool>true</bool>
-        </property>
-       </widget>
-      </item>
-     </layout>
-    </widget>
-   </item>
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout_2">
-     <item>
-      <widget class="QLabel" name="label_3">
-       <property name="sizePolicy">
-        <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
-         <horstretch>0</horstretch>
-         <verstretch>0</verstretch>
-        </sizepolicy>
-       </property>
-       <property name="text">
-        <string>Camera:</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QComboBox" name="combo_camera"/>
-     </item>
-    </layout>
+    <widget class="QrCodeScanWidget" name="widget_scanner" native="true"/>
    </item>
   </layout>
  </widget>
  <customwidgets>
   <customwidget>
-   <class>QVideoWidget</class>
+   <class>QrCodeScanWidget</class>
    <extends>QWidget</extends>
-   <header>qvideowidget.h</header>
+   <header>qrcode/scanner/QrCodeScanWidget.h</header>
    <container>1</container>
   </customwidget>
  </customwidgets>
diff --git a/src/qrcode/scanner/QrCodeScanWidget.cpp b/src/qrcode/scanner/QrCodeScanWidget.cpp
new file mode 100644 (file)
index 0000000..0c9b6e2
--- /dev/null
@@ -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 <QMediaDevices>
+#include <QComboBox>
+
+#include "utils/config.h"
+#include "utils/Icons.h"
+#include <bcur/bc-ur.hpp>
+
+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<int>::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<QCameraDevice> 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<QCameraDevice> 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 (file)
index 0000000..8c8b86f
--- /dev/null
@@ -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 <QWidget>
+#include <QCamera>
+#include <QScopedPointer>
+#include <QMediaCaptureSession>
+#include <QTimer>
+#include <QVideoSink>
+
+#include "QrScanThread.h"
+
+#include <bcur/ur-decoder.hpp>
+
+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::QrCodeScanWidget> ui;
+
+    bool m_scan_ur = false;
+    QrScanThread *m_thread;
+    QScopedPointer<QCamera> 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 (file)
index 0000000..7f2edb4
--- /dev/null
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>QrCodeScanWidget</class>
+ <widget class="QWidget" name="QrCodeScanWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>526</width>
+    <height>406</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <item>
+      <widget class="QLabel" name="label_3">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="text">
+        <string>Camera:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QComboBox" name="combo_camera">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="btn_refresh">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="text">
+        <string>Refresh</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="InfoFrame" name="frame_error">
+     <property name="frameShape">
+      <enum>QFrame::StyledPanel</enum>
+     </property>
+     <property name="frameShadow">
+      <enum>QFrame::Raised</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QVideoWidget" name="viewfinder" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QProgressBar" name="progressBar_UR">
+     <property name="maximum">
+      <number>1</number>
+     </property>
+     <property name="value">
+      <number>0</number>
+     </property>
+     <property name="format">
+      <string>%v / %m</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QCheckBox" name="check_manualExposure">
+       <property name="text">
+        <string>Manual exposure</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QSlider" name="slider_exposure">
+       <property name="minimum">
+        <number>1</number>
+       </property>
+       <property name="maximum">
+        <number>100</number>
+       </property>
+       <property name="pageStep">
+        <number>1</number>
+       </property>
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+  <zorder>progressBar_UR</zorder>
+  <zorder></zorder>
+  <zorder>viewfinder</zorder>
+  <zorder>frame_error</zorder>
+  <zorder>horizontalLayoutWidget</zorder>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>InfoFrame</class>
+   <extends>QFrame</extends>
+   <header>components.h</header>
+   <container>1</container>
+  </customwidget>
+  <customwidget>
+   <class>QVideoWidget</class>
+   <extends>QWidget</extends>
+   <header>qvideowidget.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
index 98f1f7953f048595f9f0c9a35775f8edb6e88210..c529ea2b4681acf45653f8664b71430f1e64fba0 100644 (file)
@@ -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();
 }
index 771f35234c64ba201c2a36270023b45b2c93af2c..1ca42d06ee4d53daf10139d3cb4a5c7be4ee9528 100644 (file)
@@ -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);
 
index c531c7f92d5070cdcce47e6c0c4a156adfa325d3..19ec02ce760b7ba91235aadd8db7fd0f86a10459 100644 (file)
@@ -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);
+    }
 }
 
 
index 2898fe4b4277c176fa70e4c99738091293ffd4d4..a5cfa78b22878479388080be545fba4841b28faa 100644 (file)
@@ -5,20 +5,23 @@
 #define FEATHER_QRCODEUTILS_H
 
 #include <QImage>
+#include <QString>
 
 #include <ZXing/ReadBarcode.h>
 
-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 {
index f2c9c7ccd5c5867c333b0a17b2ddb796b97f0528..86cfe8cb9b8b30d1043de0207c503de226f56a34 100644 (file)
@@ -8,6 +8,7 @@
 #include <QPushButton>
 #include <QFontDatabase>
 #include <QTcpSocket>
+#include <QFileDialog>
 
 #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<QWidget*>(object)) {
index b43b450cde7f586a940568b78e2085dc64813391..2367bd48a02850b7477c39d5e31fdd20c96ccbdb 100644 (file)
@@ -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);
 }
 
index fcc92b72d4211e94878ef060707da775019ef579..3f463152c99dfb0cb867d90000432c1bc2402185 100644 (file)
@@ -20,6 +20,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> 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<Config::ConfigKey, ConfigDirective> 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<Config::ConfigKey, ConfigDirective> 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<Config::ConfigKey, ConfigDirective> 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"}}},
index 4ce33a5d8404b729196bc5c8347182a5d3249ab9..546a5d4044151366e8c5e70b05f5541b7751e250 100644 (file)
@@ -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();
index 44e0a81e761d4476a8b4125a4e2cd25c5f877918..90efe3415eed666e6b1ec5074786cb820ccc5650 100644 (file)
@@ -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 (file)
index 0000000..09113a3
--- /dev/null
@@ -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<QString> 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<QString> 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 (file)
index 0000000..b6f1cea
--- /dev/null
@@ -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 <QWidget>
+
+#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::TxDetailsSimple> ui;
+};
+
+#endif //FEATHER_TXDETAILSSIMPLE_H
diff --git a/src/widgets/TxDetailsSimple.ui b/src/widgets/TxDetailsSimple.ui
new file mode 100644 (file)
index 0000000..275fb40
--- /dev/null
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TxDetailsSimple</class>
+ <widget class="QWidget" name="TxDetailsSimple">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>386</width>
+    <height>152</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QLabel" name="label_note">
+     <property name="text">
+      <string>note</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QFormLayout" name="formLayout">
+     <property name="horizontalSpacing">
+      <number>15</number>
+     </property>
+     <property name="verticalSpacing">
+      <number>7</number>
+     </property>
+     <item row="0" column="0">
+      <widget class="QLabel" name="label_3">
+       <property name="text">
+        <string>Address:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <widget class="QLabel" name="label_address">
+       <property name="text">
+        <string>address</string>
+       </property>
+       <property name="textInteractionFlags">
+        <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="0">
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Amount:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="1">
+      <widget class="QLabel" name="label_amount">
+       <property name="text">
+        <string>amount</string>
+       </property>
+       <property name="textInteractionFlags">
+        <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="0">
+      <widget class="QLabel" name="label_2">
+       <property name="text">
+        <string>Fee:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="1">
+      <widget class="QLabel" name="label_fee">
+       <property name="text">
+        <string>fee</string>
+       </property>
+       <property name="textInteractionFlags">
+        <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+       </property>
+      </widget>
+     </item>
+     <item row="3" column="1">
+      <widget class="Line" name="line">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+      </widget>
+     </item>
+     <item row="4" column="0">
+      <widget class="QLabel" name="label_6">
+       <property name="text">
+        <string>Total:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="4" column="1">
+      <widget class="QLabel" name="label_total">
+       <property name="text">
+        <string>total</string>
+       </property>
+       <property name="textInteractionFlags">
+        <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/widgets/URWidget.cpp b/src/widgets/URWidget.cpp
new file mode 100644 (file)
index 0000000..da1adb0
--- /dev/null
@@ -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 <QDesktopServices>
+#include <QMenu>
+
+#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 (file)
index 0000000..acf9df4
--- /dev/null
@@ -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 <QWidget>
+#include <QTimer>
+
+#include "qrcode/QrCode.h"
+#include "widgets/QrCodeWidget.h"
+#include <bcur/bc-ur.hpp>
+
+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::URWidget> ui;
+    QTimer m_timer;
+    ur::UREncoder *m_urencoder = nullptr;
+    QrCode *m_code = nullptr;
+    QList<std::string> 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 (file)
index 0000000..7d224a7
--- /dev/null
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>URWidget</class>
+ <widget class="QWidget" name="URWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QrCodeWidget" name="qrWidget" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="contextMenuPolicy">
+      <enum>Qt::DefaultContextMenu</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QLabel" name="label_seq">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="text">
+        <string>...</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="btn_options">
+       <property name="text">
+        <string/>
+       </property>
+       <property name="icon">
+        <iconset resource="../assets.qrc">
+         <normaloff>:/assets/images/preferences.svg</normaloff>:/assets/images/preferences.svg</iconset>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>QrCodeWidget</class>
+   <extends>QWidget</extends>
+   <header>widgets/QrCodeWidget.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources>
+  <include location="../assets.qrc"/>
+ </resources>
+ <connections/>
+</ui>
index acb50a3473bca49166a098d9e5175b98d69edbf8..6aae57ed2950aa43cba5efbe9dd7019a70256ec7 100644 (file)
@@ -68,7 +68,6 @@ WalletWizard::WalletWizard(QWidget *parent)
     setOption(QWizard::HaveHelpButton, true);
     setOption(QWizard::HaveCustomButton1, true);
 
-    // Set up a custom button layout
     QList<QWizard::WizardButton> 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 (file)
index 0000000..ca3052e
--- /dev/null
@@ -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 <QApplication>
+#include <QScreen>
+#include <QPushButton>
+
+#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<QWizard::WizardButton> 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 (file)
index 0000000..b3f72b0
--- /dev/null
@@ -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 <QWizard>
+#include "Wallet.h"
+
+#include <QFileDialog>
+#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 (file)
index 0000000..1a33ad5
--- /dev/null
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PageOTS_Export</class>
+ <widget class="QWizardPage" name="PageOTS_Export">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>758</width>
+    <height>734</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>WizardPage</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QFormLayout" name="formLayout">
+     <item row="0" column="0">
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Method:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <widget class="QComboBox" name="combo_method">
+       <item>
+        <property name="text">
+         <string>Animated QR Codes</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Files</string>
+        </property>
+       </item>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QLabel" name="label_step">
+     <property name="text">
+      <string>details</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QStackedWidget" name="stackedWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="page_UR">
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <widget class="URWidget" name="widget_UR" native="true">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="label_instructions">
+         <property name="text">
+          <string>Scan this animated QR code with your view-only wallet.</string>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="page_files">
+      <layout class="QVBoxLayout" name="verticalLayout_3">
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_2">
+         <item>
+          <widget class="QPushButton" name="btn_export">
+           <property name="text">
+            <string>Export to file</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <spacer name="horizontalSpacer_2">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <layout class="QVBoxLayout" name="layout_extra"/>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>URWidget</class>
+   <extends>QWidget</extends>
+   <header>widgets/URWidget.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/wizard/offline_tx_signing/PageOTS_ExportKeyImages.cpp b/src/wizard/offline_tx_signing/PageOTS_ExportKeyImages.cpp
new file mode 100644 (file)
index 0000000..721deb8
--- /dev/null
@@ -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 <QCheckBox>
+
+#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 (file)
index 0000000..1fd33b3
--- /dev/null
@@ -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 <QWizardPage>
+#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 (file)
index 0000000..f8b8fa0
--- /dev/null
@@ -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 <QFileDialog>
+#include <QCheckBox>
+
+#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 (file)
index 0000000..bca2ad3
--- /dev/null
@@ -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 <QWizardPage>
+#include <QCheckBox>
+#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 (file)
index 0000000..f801ae3
--- /dev/null
@@ -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 <QFileDialog>
+
+#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 (file)
index 0000000..d7e0d2e
--- /dev/null
@@ -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 <QWizardPage>
+#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 (file)
index 0000000..b063557
--- /dev/null
@@ -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 (file)
index 0000000..0f8eeb8
--- /dev/null
@@ -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 <QWizardPage>
+#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 (file)
index 0000000..314815a
--- /dev/null
@@ -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 <QFileDialog>
+
+#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 (file)
index 0000000..97d76f7
--- /dev/null
@@ -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 <QWizardPage>
+#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 (file)
index 0000000..561a772
--- /dev/null
@@ -0,0 +1,184 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PageOTS_Import</class>
+ <widget class="QWizardPage" name="PageOTS_Import">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>741</width>
+    <height>499</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>WizardPage</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QLabel" name="label_step">
+     <property name="text">
+      <string>details</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="text">
+        <string>Method:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QComboBox" name="combo_method">
+       <item>
+        <property name="text">
+         <string>Animated QR codes</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>File transfer</string>
+        </property>
+       </item>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>0</width>
+         <height>0</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QStackedWidget" name="stackedWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="page">
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <property name="leftMargin">
+        <number>0</number>
+       </property>
+       <property name="topMargin">
+        <number>0</number>
+       </property>
+       <property name="rightMargin">
+        <number>0</number>
+       </property>
+       <property name="bottomMargin">
+        <number>0</number>
+       </property>
+       <item>
+        <widget class="QGroupBox" name="groupBox">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="title">
+          <string/>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_4">
+          <item>
+           <widget class="QLabel" name="label_instructions">
+            <property name="sizePolicy">
+             <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+              <horstretch>0</horstretch>
+              <verstretch>0</verstretch>
+             </sizepolicy>
+            </property>
+            <property name="text">
+             <string>Scan the animated QR code shown on the view-only wallet.</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="layout_scanner"/>
+          </item>
+         </layout>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="page_2">
+      <layout class="QVBoxLayout" name="verticalLayout_3">
+       <property name="leftMargin">
+        <number>0</number>
+       </property>
+       <property name="topMargin">
+        <number>0</number>
+       </property>
+       <property name="rightMargin">
+        <number>0</number>
+       </property>
+       <property name="bottomMargin">
+        <number>0</number>
+       </property>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_2">
+         <item>
+          <widget class="QPushButton" name="btn_import">
+           <property name="text">
+            <string>Import from file</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <spacer name="horizontalSpacer_2">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <widget class="InfoFrame" name="frame_status">
+     <property name="frameShape">
+      <enum>QFrame::StyledPanel</enum>
+     </property>
+     <property name="frameShadow">
+      <enum>QFrame::Raised</enum>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>InfoFrame</class>
+   <extends>QFrame</extends>
+   <header>components.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/wizard/offline_tx_signing/PageOTS_ImportKeyImages.cpp b/src/wizard/offline_tx_signing/PageOTS_ImportKeyImages.cpp
new file mode 100644 (file)
index 0000000..44e1643
--- /dev/null
@@ -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 <QCheckBox>
+#include <QFileDialog>
+
+#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 (file)
index 0000000..70d04df
--- /dev/null
@@ -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 <QWizardPage>
+#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 (file)
index 0000000..50d3923
--- /dev/null
@@ -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 <QFileDialog>
+
+#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 (file)
index 0000000..e676e56
--- /dev/null
@@ -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 <QWizardPage>
+#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 (file)
index 0000000..953a3d2
--- /dev/null
@@ -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 <QFileDialog>
+
+#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 (file)
index 0000000..5615f79
--- /dev/null
@@ -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 <QWizardPage>
+#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 (file)
index 0000000..32e9f94
--- /dev/null
@@ -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 <QFileDialog>
+
+#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 (file)
index 0000000..4045d43
--- /dev/null
@@ -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 <QWizardPage>
+#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 (file)
index 0000000..400d31e
--- /dev/null
@@ -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 (file)
index 0000000..1a403f4
--- /dev/null
@@ -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 <QWizardPage>
+#include <QCheckBox>
+#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