# 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)
-Subproject commit 31ced6d76a1aaa1bfb9011c86987937b7042f3ce
+Subproject commit 34aacb1b49553f17b9bb7ca1ee6dfb6524aada55
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)
${LIBZIP_INCLUDE_DIRS}
${ZLIB_INCLUDE_DIRS}
${POLYSEED_INCLUDE_DIR}
+ ${BCUR_INCLUDE_DIR}
)
if(WITH_SCANNER)
${ICU_LIBRARIES}
${LIBZIP_LIBRARIES}
${ZLIB_LIBRARIES}
+ ${BCUR_LIBRARY}
)
if(CHECK_UPDATES)
#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)
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);
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());
}
#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
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);
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);
// [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);
});
}
+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
}
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) {
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:
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();
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;
}
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()));
}
}
}
-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() {
return;
}
- TxConfAdvDialog dialog{m_wallet, "", this};
+ TxConfAdvDialog dialog{m_wallet, "", this, true};
dialog.setTransaction(tx);
dialog.exec();
}
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
}
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 {
this->statusBar()->show();
this->menuBar()->show();
ui->stackedWidget->setCurrentIndex(0);
+ this->onOfflineMode(conf()->get(Config::offlineMode).toBool());
m_checkUserActivity.start();
REVUO
};
+ enum Stack {
+ WALLET = 0,
+ LOCKED,
+ OFFLINE
+ };
+
void showOrHide();
void bringToFront();
void onShowSettingsPage(int page);
// offline tx signing
- void exportKeyImages();
- void importKeyImages();
- void exportOutputs();
- void importOutputs();
- void loadUnsignedTx();
- void loadUnsignedTxFromClipboard();
void loadSignedTx();
void loadSignedTxFromText();
void showPasswordDialog();
void showKeysDialog();
void showViewOnlyDialog();
+ void showKeyImageSyncWizard();
void showWalletCacheDebugDialog();
void showAccountSwitcherDialog();
void showAddressChecker();
-
+ void showURDialog();
+
void donateButtonClicked();
void showCalcWindow();
void payToMany();
void initWidgets();
void initMenu();
void initHome();
+ void initOffline();
void initWalletContext();
void closeEvent(QCloseEvent *event) override;
void saveGeo();
void restoreGeo();
void showDebugInfo();
- void createUnsignedTxDialog(UnsignedTransaction *tx);
void updatePasswordIcon();
void updateNetStats();
void rescanSpent();
<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><html><head/><body><p>Use a webcam to scan <span style=" font-weight:700;">animated QR codes</span></p></body></html></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><html><head/><body><p>Transfer <span style=" font-weight:700;">files</span> between computers (using a flash drive)</p></body></html></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"/>
#include "libwalletqt/WalletManager.h"
#if defined(WITH_SCANNER)
+#include "wizard/offline_tx_signing/OfflineTxSigningWizard.h"
#include "qrcode/scanner/QrCodeScanDialog.h"
#include <QMediaDevices>
#endif
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");
"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);
}
private:
void setupComboBox();
double amountDouble();
+ bool keyImageSync(bool sendAll, quint64 amount);
quint64 amount();
double conversionAmount();
<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>
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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+<?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>
#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);
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);
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();
}
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);
}
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));
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() {
}
}
-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));
}
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();
}
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
void closeDialog();
void setAmounts(quint64 amount, quint64 fee);
- void unsignedCopy();
- void unsignedQrCode();
- void unsignedSaveFile();
-
void signedCopy();
- void signedQrCode();
void signedSaveFile();
void txKeyCopy();
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
<x>0</x>
<y>0</y>
<width>800</width>
- <height>542</height>
+ <height>810</height>
</rect>
</property>
<property name="minimumSize">
<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>
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+<?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>
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+<?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>
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
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();
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));
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;
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);
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;
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;
{
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)
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 {
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;
return m_wallet2->is_deterministic();
}
+QString Wallet::walletName() const {
+ return QFileInfo(this->cachePath()).fileName();
+}
+
// #################### Balance ####################
quint64 Wallet::balance() 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();
// #################### 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);
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());
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;
bool Wallet::rescanSpent() {
QMutexLocker locker(&m_asyncMutex);
- return m_walletImpl->rescanSpent();
+ bool r = m_walletImpl->rescanSpent();
+ m_coins->refresh();
+ return r;
}
void Wallet::setNewWallet() {
return major == 0 && minor == 0;
}
+ bool isChange() const {
+ return minor == 0;
+ }
+
int major;
int minor;
};
//! return true if deterministic keys
bool isDeterministic() const;
+ QString walletName() const;
+
// ##### Balance #####
//! returns balance
quint64 balance() const;
quint64 unlockedBalance() const;
quint64 unlockedBalance(quint32 accountIndex) const;
quint64 unlockedBalanceAll() const;
+
+ quint64 viewOnlyBalance(quint32 accountIndex) const;
void updateBalance();
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);
//! 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;
bool m_useSSL;
bool donationSending = false;
bool m_newWallet = false;
+ bool m_forceKeyImageSync = false;
QTimer *m_storeTimer = nullptr;
std::set<std::string> m_selectedInputs;
, m_failed(false)
, m_coinbase(false)
, m_amount(0)
+ , m_balanceDelta(0)
, m_fee(0)
, m_blockHeight(0)
, m_subaddrAccount(0)
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
return displayAmount().toDouble();
}
-quint64 TransactionRow::atomicAmount() const
+qint64 TransactionRow::atomicAmount() const
{
return m_amount;
}
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;
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;
case Column::FiatAmount:
case Column::Amount:
{
- if (tInfo.direction() == TransactionRow::Direction_Out) {
+ if (tInfo.balanceDelta() < 0) {
result = QVariant(QColor("#BC1E1E"));
}
}
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: {
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") {
#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
#include "QrScanThread.h"
+#include <bcur/ur-decoder.hpp>
+
namespace Ui {
class QrCodeScanDialog;
}
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;
};
</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>
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+<?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>
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);
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();
}
public:
explicit QrScanThread(QObject *parent = nullptr);
void addImage(const QImage &img);
+
virtual void stop();
-
+ virtual void start();
+
signals:
void decoded(const QString &data);
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);
+ }
}
#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 {
#include <QPushButton>
#include <QFontDatabase>
#include <QTcpSocket>
+#include <QFileDialog>
#include "constants.h"
#include "networktype.h"
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();
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)) {
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);
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);
}
{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"}},
{Config::geometry, {QS("geometry"), {}}},
{Config::windowState, {QS("windowState"), {}}},
{Config::GUI_HistoryViewState, {QS("GUI_HistoryViewState"), {}}},
+ {Config::geometryOTSWizard, {QS("geometryOTSWizard"), {}}},
// Wallets
{Config::walletDirectory,{QS("walletDirectory"), ""}},
{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}},
{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"}}},
firstRun,
warnOnStagenet,
warnOnTestnet,
+ warnOnKiImport,
homeWidget,
donateBeg,
geometry,
windowState,
GUI_HistoryViewState,
+ geometryOTSWizard,
// Wallets
walletDirectory, // Directory where wallet files are stored
// Transactions
multiBroadcast,
+ offlineTxSigningMethod,
+ offlineTxSigningForceKISync,
// Misc
blockExplorer,
redditFrontend,
localMoneroFrontend,
bountiesFrontend, // unused
+ lastPath,
+
+ // UR
+ URmsPerFragment,
+ URfragmentLength,
+ URfountainCode,
+
+ // Camera
+ cameraManualExposure,
+ cameraExposureTime,
fiatSymbols,
cryptoSymbols,
socks5
};
+ enum OTSMethod {
+ UnifiedResources = 0,
+ FileTransfer
+ };
+
~Config() override;
QVariant get(ConfigKey key);
QString getFileName();
}
void QrCodeWidget::setQrCode(QrCode *qrCode) {
+ if (m_qrcode) {
+ delete m_qrcode;
+ }
+
m_qrcode = qrCode;
int k = m_qrcode->width();
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+<?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>
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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
--- /dev/null
+<?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>
setOption(QWizard::HaveHelpButton, true);
setOption(QWizard::HaveCustomButton1, true);
- // Set up a custom button layout
QList<QWizard::WizardButton> layout;
layout << QWizard::HelpButton;
layout << QWizard::CustomButton1;
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+<?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>
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+<?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>
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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