From cc5b3c3c27bb96c6d8ab449bc38c233018427c5d Mon Sep 17 00:00:00 2001 From: tobtoht Date: Mon, 8 Mar 2021 21:03:20 +0100 Subject: [PATCH] History: rework --- .gitignore | 1 + src/appcontext.cpp | 23 +- src/appcontext.h | 2 +- src/dialog/TxProofDialog.cpp | 226 ++++++++ src/dialog/TxProofDialog.h | 56 ++ src/dialog/TxProofDialog.ui | 301 ++++++++++ src/dialog/transactioninfodialog.cpp | 27 +- src/dialog/transactioninfodialog.h | 6 +- src/dialog/transactioninfodialog.ui | 67 +-- src/dialog/verifyproofdialog.cpp | 131 ++++- src/dialog/verifyproofdialog.h | 8 +- src/dialog/verifyproofdialog.ui | 611 ++++++++++++--------- src/historywidget.cpp | 94 ++-- src/historywidget.h | 2 + src/historywidget.ui | 9 +- src/libwalletqt/TransactionHistory.cpp | 9 + src/libwalletqt/TransactionHistory.h | 1 + src/mainwindow.cpp | 22 +- src/mainwindow.h | 1 + src/model/HistoryView.cpp | 188 +++++++ src/model/HistoryView.h | 44 ++ src/model/ModelUtils.cpp | 2 +- src/model/TransactionHistoryModel.cpp | 47 +- src/model/TransactionHistoryModel.h | 7 +- src/model/TransactionHistoryProxyModel.cpp | 4 + src/model/TransactionHistoryProxyModel.h | 1 + src/settings.cpp | 11 + src/settings.h | 2 + src/settings.ui | 14 +- src/utils/config.cpp | 4 +- src/utils/config.h | 4 +- src/widgets/txproofwidget.cpp | 68 --- src/widgets/txproofwidget.h | 33 -- src/widgets/txproofwidget.ui | 110 ---- 34 files changed, 1504 insertions(+), 632 deletions(-) create mode 100644 src/dialog/TxProofDialog.cpp create mode 100644 src/dialog/TxProofDialog.h create mode 100644 src/dialog/TxProofDialog.ui create mode 100644 src/model/HistoryView.cpp create mode 100644 src/model/HistoryView.h delete mode 100644 src/widgets/txproofwidget.cpp delete mode 100644 src/widgets/txproofwidget.h delete mode 100644 src/widgets/txproofwidget.ui diff --git a/.gitignore b/.gitignore index 0a00d97..f436280 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ feather.cbp src/tor/* !src/tor/.gitkeep src/config-feather.h +src/assets/exec/* diff --git a/src/appcontext.cpp b/src/appcontext.cpp index 4cdbf97..cc0da0e 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -19,7 +19,6 @@ Prices *AppContext::prices = nullptr; WalletKeysFilesModel *AppContext::wallets = nullptr; TxFiatHistory *AppContext::txFiatHistory = nullptr; double AppContext::balance = 0; -QMap AppContext::txDescriptionCache; QMap AppContext::txCache; AppContext::AppContext(QCommandLineParser *cmdargs) { @@ -303,6 +302,13 @@ void AppContext::onPreferredFiatCurrencyChanged(const QString &symbol) { } } +void AppContext::onAmountPrecisionChanged(int precision) { + if (!this->currentWallet) return; + auto *model = this->currentWallet->transactionHistoryModel(); + if (!model) return; + model->amountPrecision = precision; +} + void AppContext::onWalletOpened(Wallet *wallet) { auto state = wallet->status(); if (state != Wallet::Status_Ok) { @@ -770,21 +776,28 @@ void AppContext::onTransactionCreated(PendingTransaction *tx, const QVectorcurrentWallet->history()->refresh(this->currentWallet->currentSubaddressAccount()); - this->currentWallet->coins()->refresh(this->currentWallet->currentSubaddressAccount()); + if (status) { + for (const auto &entry: txid) { + this->currentWallet->setUserNote(entry, this->tmpTxDescription); + } + this->tmpTxDescription = ""; + } // Store wallet immediately so we don't risk losing tx key if wallet crashes this->currentWallet->store(); - this->updateBalance(); + this->currentWallet->history()->refresh(this->currentWallet->currentSubaddressAccount()); + this->currentWallet->coins()->refresh(this->currentWallet->currentSubaddressAccount()); - emit transactionCommitted(status, tx, txid); + this->updateBalance(); // this tx was a donation to Feather, stop our nagging if(this->donationSending) { this->donationSending = false; config()->set(Config::donateBeg, -1); } + + emit transactionCommitted(status, tx, txid); } void AppContext::storeWallet() { diff --git a/src/appcontext.h b/src/appcontext.h index dd6369a..46d65d1 100644 --- a/src/appcontext.h +++ b/src/appcontext.h @@ -85,7 +85,6 @@ public: static Prices *prices; static WalletKeysFilesModel *wallets; static double balance; - static QMap txDescriptionCache; static QMap txCache; static TxFiatHistory *txFiatHistory; @@ -119,6 +118,7 @@ public slots: void onOpenAliasResolve(const QString &openAlias); void onSetRestoreHeight(quint64 height); void onPreferredFiatCurrencyChanged(const QString &symbol); + void onAmountPrecisionChanged(int precision); private slots: void onWSNodes(const QJsonArray &nodes); diff --git a/src/dialog/TxProofDialog.cpp b/src/dialog/TxProofDialog.cpp new file mode 100644 index 0000000..5c1d0d9 --- /dev/null +++ b/src/dialog/TxProofDialog.cpp @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "TxProofDialog.h" +#include "ui_TxProofDialog.h" + +#include + +#include "libwalletqt/Transfer.h" +#include "utils/utils.h" + +TxProofDialog::TxProofDialog(QWidget *parent, Wallet *wallet, TransactionInfo *txInfo) + : QDialog(parent) + , ui(new Ui::TxProofDialog) + , m_wallet(wallet) +{ + ui->setupUi(this); + + m_txid = txInfo->hash(); + m_txKey = m_wallet->getTxKey(m_txid); + m_direction = txInfo->direction(); + + for (auto const &t: txInfo->transfers()) { + m_OutDestinations.push_back(t->address()); + } + + for (auto const &s: txInfo->subaddrIndex()) { + m_InDestinations.push_back(wallet->address(txInfo->subaddrAccount(), s)); + } + + // Due to some logic in core we can't create OutProofs + // for churn transactions that sweep from and send to the same address + for (auto const &address : m_InDestinations) { + m_OutDestinations.removeAll(address); + } + + connect(ui->radio_SpendProof, &QRadioButton::toggled, this, &TxProofDialog::selectSpendProof); + connect(ui->radio_OutProof, &QRadioButton::toggled, this, &TxProofDialog::selectOutProof); + connect(ui->radio_InProof, &QRadioButton::toggled, this, &TxProofDialog::selectInProof); + + connect(ui->btn_getFormattedProof, &QPushButton::pressed, this, &TxProofDialog::getFormattedProof); + connect(ui->btn_getSignature, &QPushButton::pressed, this, &TxProofDialog::getSignature); + + ui->radio_SpendProof->setChecked(true); + ui->label_txid->setText(m_txid); + + ui->group_summary->hide(); // todo + + this->adjustSize(); +} + +void TxProofDialog::setTxId(const QString &txid) { + ui->label_txid->setText(txid); +} + +void TxProofDialog::selectSpendProof() { + m_mode = Mode::SpendProof; + this->resetFrames(); + + if (m_direction == TransactionInfo::Direction_In) { + this->showWarning("Your wallet did not construct this transaction. Creating a SpendProof is not possible."); + return; + } + + ui->frame_message->show(); + ui->label_summary->setText("This proof shows you created a transaction with the txid shown above."); +} + +void TxProofDialog::selectOutProof() { + m_mode = Mode::OutProof; + this->resetFrames(); + + if (m_OutDestinations.empty()) { + this->showWarning("This transaction did not spend any outputs owned by this wallet. Creating an OutProof is not possible."); + return; + } + + if (m_txKey.isEmpty()) { + this->showWarning("No transaction key stored for this transaction. Creating an OutProof is not possible."); + return; + } + + this->selectTxProof(); + ui->combo_address->addItems(m_OutDestinations); + ui->label_summary->setText("This proof shows you paid x XMR to the address selected above."); +} + +void TxProofDialog::selectInProof() { + m_mode = Mode::InProof; + this->resetFrames(); + + if (m_InDestinations.empty()) { + this->showWarning("Your wallet did not receive any outputs in this transaction."); + return; + } + + this->selectTxProof(); + ui->combo_address->addItems(m_InDestinations); + ui->label_summary->setText("This proof shows you received x XMR to the address selected above."); +} + +void TxProofDialog::selectTxProof() { + ui->frame_txKeyWarning->hide(); + ui->frame_message->show(); + ui->frame_address->show(); + ui->combo_address->clear(); +} + +void TxProofDialog::resetFrames() { + ui->frame_txKeyWarning->hide(); + ui->frame_message->hide(); + ui->frame_address->hide(); + this->toggleButtons(true); +} + +void TxProofDialog::toggleButtons(bool enabled) { + ui->btn_getFormattedProof->setEnabled(enabled); + ui->btn_getSignature->setEnabled(enabled); +} + +void TxProofDialog::showWarning(const QString &message) { + this->toggleButtons(false); + ui->frame_txKeyWarning->show(); + ui->label_txKeyWarning->setText(message); +} + +void TxProofDialog::getFormattedProof() { + QString message = ui->message->toPlainText(); + QString address = ui->combo_address->currentText(); + QString nettype = Utils::QtEnumToString(m_wallet->nettype()).toLower(); + nettype = nettype.replace(0, 1, nettype[0].toUpper()); // Capitalize first letter + + TxProof proof = this->getProof(); + + if (!proof.error.isEmpty()) { + QMessageBox::warning(this, "Get formatted proof", QString("Failed to get proof signature: %1").arg(proof.error)); + return; + } + + QStringList signatureSplit; + for (int i = 0; i < proof.proof.length(); i += 64) { + signatureSplit.append(proof.proof.mid(i, 64)); + } + QString signature = signatureSplit.join('\n'); + + QString formattedProof = [this, nettype, message, address, signature]{ + switch (m_mode) { + case Mode::SpendProof: { + return QString("-----BEGIN SPENDPROOF-----\n" + "Network: Monero %1\n" + "Txid: %2\n" + "\n" + "%3\n" + "-----BEGIN SPENDPROOF SIGNATURE-----\n" + "\n" + "%4\n" + "-----END SPENDPROOF SIGNATURE-----").arg(nettype, m_txid, message, signature); + } + case Mode::OutProof: { + return QString("-----BEGIN OUTPROOF-----\n" + "Network: Monero %1\n" + "Txid: %2\n" + "Address: %3\n" + "\n" + "%4\n" + "-----BEGIN OUTPROOF SIGNATURE-----\n" + "\n" + "%5\n" + "-----END OUTPROOF SIGNATURE-----").arg(nettype, m_txid, address, message, signature); + } + case Mode::InProof: { + return QString("-----BEGIN INPROOF-----\n" + "Network: Monero %1\n" + "Txid: %2\n" + "Address: %3\n" + "\n" + "%4\n" + "-----BEGIN INPROOF SIGNATURE-----\n" + "\n" + "%5\n" + "-----END INPROOF SIGNATURE-----").arg(nettype, m_txid, address, message, signature); + } + default: + return QString(""); + } + }(); + + Utils::copyToClipboard(formattedProof); + QMessageBox::information(this, "Get formatted proof", "Formatted proof copied to clipboard"); +} + +void TxProofDialog::getSignature() { + TxProof proof = this->getProof(); + + if (!proof.error.isEmpty()) { + QMessageBox::warning(this, "Get proof signature", QString("Failed to get proof signature: %1").arg(proof.error)); + return; + } + + Utils::copyToClipboard(proof.proof); + QMessageBox::information(this, "Get proof singature", "Proof signature copied to clipboard"); +} + +TxProof TxProofDialog::getProof() { + QString message = ui->message->toPlainText(); + QString address = ui->combo_address->currentText(); + + TxProof proof = [this, message, address]{ + switch (m_mode) { + case Mode::SpendProof: { + return m_wallet->getSpendProof(m_txid, message); + } + case Mode::OutProof: + case Mode::InProof: { // Todo: split this into separate functions + return m_wallet->getTxProof(m_txid, address, message); + } + } + }(); + + return proof; +} + +TxProofDialog::~TxProofDialog() { + delete ui; +} + diff --git a/src/dialog/TxProofDialog.h b/src/dialog/TxProofDialog.h new file mode 100644 index 0000000..1ad53ce --- /dev/null +++ b/src/dialog/TxProofDialog.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_TXPROOFDIALOG_H +#define FEATHER_TXPROOFDIALOG_H + +#include + +#include "libwalletqt/Wallet.h" +#include "libwalletqt/TransactionInfo.h" + +namespace Ui { + class TxProofDialog; +} + +class TxProofDialog : public QDialog +{ + Q_OBJECT + +public: + explicit TxProofDialog(QWidget *parent, Wallet *wallet, TransactionInfo *txid); + ~TxProofDialog() override; + void setTxId(const QString &txid); + +private slots: + void selectSpendProof(); + void selectOutProof(); + void selectInProof(); + void selectTxProof(); + +private: + enum Mode { + SpendProof = 0, + OutProof, + InProof + }; + + void getFormattedProof(); + void getSignature(); + TxProof getProof(); + void resetFrames(); + void toggleButtons(bool enabled); + void showWarning(const QString &message); + + QStringList m_OutDestinations; + QStringList m_InDestinations; + QString m_txid; + QString m_txKey; + Mode m_mode; + TransactionInfo::Direction m_direction; + + Ui::TxProofDialog *ui; + Wallet *m_wallet; +}; + +#endif //FEATHER_TXPROOFDIALOG_H diff --git a/src/dialog/TxProofDialog.ui b/src/dialog/TxProofDialog.ui new file mode 100644 index 0000000..bd599b6 --- /dev/null +++ b/src/dialog/TxProofDialog.ui @@ -0,0 +1,301 @@ + + + TxProofDialog + + + + 0 + 0 + 542 + 827 + + + + Create Tx Proof + + + + + + Select proof type: + + + + + + Prove authorship of a transaction (SpendProof) + + + + + + + Prove a payment to an address (OutProof) + + + + + + + Prove ownership of an output (InProof) + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + Txid: + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + txid + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 10 + 5 + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + No transaction key stored for this transaction. Creating an OutProof is not possible. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Message: (optional) + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + Address: + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 20 + 10 + + + + + + + + Summary: + + + + + + This proof shows I paid x XMR to ADDR. + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Get Formatted Proof + + + + + + + Get Signature + + + + + + + + + + diff --git a/src/dialog/transactioninfodialog.cpp b/src/dialog/transactioninfodialog.cpp index f9f7a00..7347c8b 100644 --- a/src/dialog/transactioninfodialog.cpp +++ b/src/dialog/transactioninfodialog.cpp @@ -20,21 +20,18 @@ TransactionInfoDialog::TransactionInfoDialog(Wallet *wallet, TransactionInfo *tx { ui->setupUi(this); - m_txProofWidget = new TxProofWidget(this, wallet, txInfo); + m_txid = txInfo->hash(); + ui->label_txid->setText(m_txid); - ui->label_txid->setText(QString(txInfo->hash())); - - if (txInfo->direction() == TransactionInfo::Direction_In) { - ui->frameTxKey->hide(); - } else { - QString txKey = m_wallet->getTxKey(txInfo->hash()); - if (txKey.isEmpty()) { - ui->btn_CopyTxKey->setEnabled(false); - ui->btn_CopyTxKey->setToolTip("Transaction key unknown"); - } - m_txKey = txKey; + QString txKey = m_wallet->getTxKey(txInfo->hash()); + if (txKey.isEmpty()) { + ui->btn_CopyTxKey->setEnabled(false); + ui->btn_CopyTxKey->setToolTip("Transaction key unknown"); } + m_txKey = txKey; + connect(ui->btn_CopyTxKey, &QPushButton::pressed, this, &TransactionInfoDialog::copyTxKey); + connect(ui->btn_createTxProof, &QPushButton::pressed, this, &TransactionInfoDialog::createTxProof); QString blockHeight = QString::number(txInfo->blockHeight()); if (blockHeight == "0") @@ -66,7 +63,7 @@ TransactionInfoDialog::TransactionInfoDialog(Wallet *wallet, TransactionInfo *tx ui->frameDestinations->hide(); } - ui->txProofWidget->addWidget(m_txProofWidget); + m_txProofDialog = new TxProofDialog(this, m_wallet, txInfo); this->adjustSize(); } @@ -75,6 +72,10 @@ void TransactionInfoDialog::copyTxKey() { Utils::copyToClipboard(m_txKey); } +void TransactionInfoDialog::createTxProof() { + m_txProofDialog->show(); +} + TransactionInfoDialog::~TransactionInfoDialog() { delete ui; } diff --git a/src/dialog/transactioninfodialog.h b/src/dialog/transactioninfodialog.h index 67ddad8..d74e231 100644 --- a/src/dialog/transactioninfodialog.h +++ b/src/dialog/transactioninfodialog.h @@ -10,7 +10,7 @@ #include "libwalletqt/Coins.h" #include "libwalletqt/TransactionInfo.h" #include "libwalletqt/Wallet.h" -#include "widgets/txproofwidget.h" +#include "dialog/TxProofDialog.h" namespace Ui { class TransactionInfoDialog; @@ -26,13 +26,15 @@ public: private: void copyTxKey(); + void createTxProof(); Ui::TransactionInfoDialog *ui; + TxProofDialog *m_txProofDialog; TransactionInfo *m_txInfo; Wallet *m_wallet; - TxProofWidget *m_txProofWidget; QString m_txKey; + QString m_txid; }; #endif //FEATHER_TRANSACTIONINFODIALOG_H diff --git a/src/dialog/transactioninfodialog.ui b/src/dialog/transactioninfodialog.ui index f7ae9fd..c761040 100644 --- a/src/dialog/transactioninfodialog.ui +++ b/src/dialog/transactioninfodialog.ui @@ -7,7 +7,7 @@ 0 0 829 - 570 + 493 @@ -187,16 +187,47 @@ Qt::Vertical - QSizePolicy::Maximum + QSizePolicy::Minimum 0 - 15 + 10 + + + + + + Copy Transaction Key + + + + + + + Create Transaction Proof + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -218,39 +249,9 @@ 0 - - - - Copy - - - - - - - Transaction Key - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - diff --git a/src/dialog/verifyproofdialog.cpp b/src/dialog/verifyproofdialog.cpp index 0bb5a6a..77493ed 100644 --- a/src/dialog/verifyproofdialog.cpp +++ b/src/dialog/verifyproofdialog.cpp @@ -5,6 +5,7 @@ #include "ui_verifyproofdialog.h" #include "libwalletqt/WalletManager.h" +#include "utils/utils.h" #include @@ -15,25 +16,34 @@ VerifyProofDialog::VerifyProofDialog(Wallet *wallet, QWidget *parent) { ui->setupUi(this); + m_success = QPixmap(":/assets/images/confirmed.png").scaled(32, 32, Qt::KeepAspectRatio, Qt::SmoothTransformation); + m_failure = QPixmap(":/assets/images/expired.png").scaled(32, 32, Qt::KeepAspectRatio, Qt::SmoothTransformation); + + ui->frame_status->hide(); + connect(ui->input_formattedProof, &QPlainTextEdit::textChanged, [this]{ + ui->frame_status->hide(); + }); + connect(ui->btn_verify, &QPushButton::clicked, this, &VerifyProofDialog::checkProof); + connect(ui->btn_verifyFormattedProof, &QPushButton::clicked, this, &VerifyProofDialog::checkFormattedProof); connect(ui->btn_clear, &QPushButton::clicked, [this]{ switch (ui->tabWidget->currentIndex()) { case 0: ui->lineEdit_spendTxID->clear(); - ui->lineEdit_spendMessage->clear(); + ui->input_SpendMessage->clear(); ui->input_SpendProof->clear(); break; case 1: ui->lineEdit_outTxID->clear(); ui->lineEdit_outAddress->clear(); - ui->lineEdit_outMessage->clear(); + ui->input_OutMessage->clear(); ui->input_OutProof->clear(); break; case 2: ui->lineEdit_inTxID->clear(); ui->lineEdit_inAddress->clear(); - ui->lineEdit_inMessage->clear(); + ui->input_inMessage->clear(); ui->input_InProof->clear(); break; } @@ -48,7 +58,7 @@ VerifyProofDialog::~VerifyProofDialog() void VerifyProofDialog::checkProof() { switch (ui->tabWidget->currentIndex()) { case 0: - this->checkSpendProof(); + this->checkSpendProof(ui->lineEdit_spendTxID->text(), ui->input_SpendMessage->toPlainText(), ui->input_SpendProof->toPlainText()); break; case 1: this->checkOutProof(); @@ -59,40 +69,105 @@ void VerifyProofDialog::checkProof() { } } -void VerifyProofDialog::checkSpendProof() { - auto r = m_wallet->checkSpendProof(ui->lineEdit_spendTxID->text(), ui->lineEdit_spendMessage->text(), ui->input_SpendProof->toPlainText()); - - if (!r.first) { - QMessageBox::information(this, "Information", m_wallet->errorString()); - return; - } - - r.second ? QMessageBox::information(this, "Information", "Proof is valid") - : QMessageBox::warning(this, "Warning", "Proof is invalid"); -} - -void VerifyProofDialog::checkOutProof() { - this->checkTxProof(ui->lineEdit_outTxID->text(), ui->lineEdit_outAddress->text(), ui->lineEdit_outMessage->text(), ui->input_OutProof->toPlainText()); -} - -void VerifyProofDialog::checkInProof() { - this->checkTxProof(ui->lineEdit_inTxID->text(), ui->lineEdit_inAddress->text(), ui->lineEdit_inMessage->text(), ui->input_InProof->toPlainText()); -} - void VerifyProofDialog::checkTxProof(const QString &txId, const QString &address, const QString &message, const QString &signature) { TxProofResult r = m_wallet->checkTxProof(txId, address, message, signature); if (!r.success) { - QMessageBox::information(this, "Information", m_wallet->errorString()); + this->proofStatus(false, m_wallet->errorString()); return; } if (!r.good) { - QMessageBox::warning(this, "Warning", "Proof is invalid"); + this->proofStatus(false, "Proof is invalid"); return; } - QString msg = QString("This address received %1 monero, with %2 confirmation(s)").arg(WalletManager::displayAmount(r.received), QString::number(r.confirmations)); - QMessageBox::information(this, "Information", QString("Proof is valid.\n\n%1").arg(msg)); + this->proofStatus(true, QString("Proof is valid.\n\nThis address received %1 XMR, with %2 confirmation(s)").arg(WalletManager::displayAmount(r.received), QString::number(r.confirmations))); +} + +void VerifyProofDialog::checkSpendProof(const QString &txId, const QString &message, const QString &signature) { + auto r = m_wallet->checkSpendProof(txId, message, signature); + + if (!r.first) { + this->proofStatus(false, m_wallet->errorString()); + return; + } + + r.second ? this->proofStatus(true, "Proof is valid") + : this->proofStatus(false, "Proof is invalid"); +} + +void VerifyProofDialog::checkOutProof() { + this->checkTxProof(ui->lineEdit_outTxID->text(), ui->lineEdit_outAddress->text(), ui->input_OutMessage->toPlainText(), ui->input_OutProof->toPlainText()); +} + +void VerifyProofDialog::checkInProof() { + this->checkTxProof(ui->lineEdit_inTxID->text(), ui->lineEdit_inAddress->text(), ui->input_inMessage->toPlainText(), ui->input_InProof->toPlainText()); +} + +void VerifyProofDialog::checkFormattedProof() { + QRegularExpression proof("-----BEGIN (?\\w+)-----\\n" + "Network: (?\\w+) (?\\w+)\\n" + "Txid: (?[0-9a-f]{64})\\n" + "(Address: (?
\\w+)\\n)?" + "\\n?" + "(?.*?)\\n" + "-----BEGIN \\1 SIGNATURE-----\\n" + "\\n?" + "(?.*?)\\n" + "-----END \\1 SIGNATURE-----", + QRegularExpression::CaseInsensitiveOption | QRegularExpression::DotMatchesEverythingOption); + + QString formattedProof = ui->input_formattedProof->toPlainText(); + QRegularExpressionMatch match = proof.match(formattedProof); + + if (!match.hasMatch()) { + this->proofStatus(false, "Unable to parse proof"); + return; + } + + QString type = match.captured("type").toLower(); + QString coin = match.captured("coin").toLower(); + QString network = match.captured("network").toLower(); + QString txid = match.captured("txid"); + QString address = match.captured("address"); + QString message = match.captured("message"); + QString signature = match.captured("signature").remove('\n'); + + QStringList validTypes = {"inproof", "spendproof", "outproof"}; + if (!validTypes.contains(type)) { + this->proofStatus(false, QString("Unknown proof type: %1").arg(type)); + return; + } + + if (coin != "monero") { + this->proofStatus(false, QString("Can't verify proof for coin: %1").arg(coin)); + return; + } + + QString walletNetwork = Utils::QtEnumToString(m_wallet->nettype()).toLower(); + if (network != walletNetwork) { + this->proofStatus(false, QString("Can't verify proof for %1 network when %2 wallet is opened").arg(network, walletNetwork)); + return; + } + + if (type == "outproof" || type == "inproof") { + this->checkTxProof(txid, address, message, signature); + } + if (type == "spendproof") { + this->checkSpendProof(txid, message, signature); + } +} + +void VerifyProofDialog::proofStatus(bool success, const QString &message) { + if (ui->tabWidget_proofFormat->currentIndex() == 0) { + ui->frame_status->show(); + ui->label_icon->setPixmap(success ? m_success : m_failure); + ui->label_status->setText(message); + } + else { + success ? QMessageBox::information(this, "Information", message) + : QMessageBox::warning(this, "Warning", message); + } } \ No newline at end of file diff --git a/src/dialog/verifyproofdialog.h b/src/dialog/verifyproofdialog.h index cf8bde7..89e44e3 100644 --- a/src/dialog/verifyproofdialog.h +++ b/src/dialog/verifyproofdialog.h @@ -5,6 +5,7 @@ #define FEATHER_VERIFYPROOFDIALOG_H #include +#include #include "libwalletqt/Wallet.h" namespace Ui { @@ -24,9 +25,14 @@ private slots: private: void checkTxProof(const QString &txId, const QString &address, const QString &message, const QString &signature); - void checkSpendProof(); + void checkSpendProof(const QString &txId, const QString &message, const QString &signature); void checkOutProof(); void checkInProof(); + void checkFormattedProof(); + void proofStatus(bool success, const QString &message); + + QPixmap m_success; + QPixmap m_failure; Ui::VerifyProofDialog *ui; Wallet *m_wallet; diff --git a/src/dialog/verifyproofdialog.ui b/src/dialog/verifyproofdialog.ui index e161ac2..76bac4f 100644 --- a/src/dialog/verifyproofdialog.ui +++ b/src/dialog/verifyproofdialog.ui @@ -6,8 +6,8 @@ 0 0 - 1123 - 413 + 770 + 587 @@ -15,298 +15,411 @@ - - - Select proof to verify: - - - - - + - 2 + 0 - + - SpendProof + Formatted - + - + - A SpendProof proves authorship of a transaction. + Paste formatted proof here: - - - + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + icon + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + Proof is valid + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + - Message: - - - - - - - - - - Transaction ID: - - - - - - - Optional message against which the signature is signed - - - - - - - SpendProof: - - - - - - - false - - - SpendProof.. + Verify - - - - Qt::Vertical - - - - 20 - 40 - - - - - + - OutProof + Manual - + - + - An OutProof shows the prover sent an output to an address. + Select proof to verify: - - - - - OutProof: - - - - - - - - - - Address: - - - - - - - - - - Address of recipient - - - - - - - Transaction ID: - - - - - - - Message: - - - - - - - Optional message against which the signature is signed - - - - - - - OutProof.. - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - InProof - - - - - - An InProof proves ownership of an output. + + + 0 + + + SpendProof + + + + + + A SpendProof proves authorship of a transaction. + + + + + + + + + Message: + + + + + + + + + + Transaction ID: + + + + + + + SpendProof: + + + + + + + false + + + SpendProof.. + + + + + + + Optional message against which the signature is signed + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + OutProof + + + + + + An OutProof shows the prover sent an output to an address. + + + + + + + + + OutProof: + + + + + + + + + + Address: + + + + + + + + + + Address of recipient + + + + + + + Transaction ID: + + + + + + + Message: + + + + + + + OutProof.. + + + + + + + Optional message against which the signature is signed + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + InProof + + + + + + An InProof proves ownership of an output. + + + + + + + + + InProof: + + + + + + + Transaction ID: + + + + + + + Output owner's address + + + + + + + InProof.. + + + + + + + Transaction that created the output + + + + + + + Optional message against which the signature is signed + + + + + + + Message: + + + + + + + Address: + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + - - - + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + - InProof: + Clear - - + + - Transaction ID: + Verify - - - - - - Address: - - - - - - - Output owner's address - - - - - - - Transaction that created the output - - - - - - - Message: - - - - - - - Optional message against which the signature is signed - - - - - - - InProof.. + + true - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Clear - - - - - - - Verify - - - true - - - - - diff --git a/src/historywidget.cpp b/src/historywidget.cpp index ab13ebe..831e575 100644 --- a/src/historywidget.cpp +++ b/src/historywidget.cpp @@ -4,6 +4,7 @@ #include "historywidget.h" #include "ui_historywidget.h" #include "dialog/transactioninfodialog.h" +#include "dialog/TxProofDialog.h" #include HistoryWidget::HistoryWidget(QWidget *parent) @@ -20,6 +21,7 @@ HistoryWidget::HistoryWidget(QWidget *parent) // copy menu m_copyMenu->setIcon(QIcon(":/assets/images/copy.png")); m_copyMenu->addAction("Transaction ID", this, [this]{copy(copyField::TxID);}); + m_copyMenu->addAction("Description", this, [this]{copy(copyField::Description);}); m_copyMenu->addAction("Date", this, [this]{copy(copyField::Date);}); m_copyMenu->addAction("Amount", this, [this]{copy(copyField::Amount);}); @@ -48,34 +50,29 @@ void HistoryWidget::showContextMenu(const QPoint &point) { } QMenu menu(this); - TransactionInfo::Direction direction; - QString txid; - bool unconfirmed; - m_txHistory->transaction(m_model->mapToSource(index).row(), [&direction, &txid, &unconfirmed](TransactionInfo &tInfo) { - direction = tInfo.direction(); - txid = tInfo.hash(); - unconfirmed = tInfo.isFailed() || tInfo.isPending(); - }); - if (AppContext::txCache.contains(txid) && unconfirmed && direction != TransactionInfo::Direction_In) { + auto *tx = ui->history->currentEntry(); + if (!tx) return; + + bool unconfirmed = tx->isFailed() || tx->isPending(); + if (AppContext::txCache.contains(tx->hash()) && unconfirmed && tx->direction() != TransactionInfo::Direction_In) { menu.addAction(QIcon(":/assets/images/info.png"), "Resend transaction", this, &HistoryWidget::onResendTransaction); } menu.addMenu(m_copyMenu); menu.addAction(QIcon(":/assets/images/info.png"), "Show details", this, &HistoryWidget::showTxDetails); menu.addAction(QIcon(":/assets/images/network.png"), "View on block explorer", this, &HistoryWidget::onViewOnBlockExplorer); + menu.addAction("Create tx proof", this, &HistoryWidget::createTxProof); menu.exec(ui->history->viewport()->mapToGlobal(point)); } void HistoryWidget::onResendTransaction() { - QModelIndex index = ui->history->currentIndex(); - QString txid; - m_txHistory->transaction(m_model->mapToSource(index).row(), [&txid](TransactionInfo &tInfo) { - txid = tInfo.hash(); - }); - - emit resendTransaction(txid); + auto *tx = ui->history->currentEntry(); + if (tx) { + QString txid = tx->hash(); + emit resendTransaction(txid); + } } void HistoryWidget::setModel(TransactionHistoryProxyModel *model, Wallet *wallet) @@ -83,39 +80,35 @@ void HistoryWidget::setModel(TransactionHistoryProxyModel *model, Wallet *wallet m_model = model; m_wallet = wallet; m_txHistory = m_wallet->history(); - ui->history->setModel(m_model); + ui->history->setHistoryModel(m_model); + m_wallet->transactionHistoryModel()->amountPrecision = config()->get(Config::amountPrecision).toInt(); - ui->history->header()->setSectionResizeMode(QHeaderView::ResizeToContents); - ui->history->header()->setSectionResizeMode(TransactionHistoryModel::Description, QHeaderView::Stretch); - ui->history->hideColumn(TransactionHistoryModel::TxID); + // Load view state + ui->history->setViewState(QByteArray::fromBase64(config()->get(Config::GUI_HistoryViewState).toByteArray())); } void HistoryWidget::resetModel() { + // Save view state + config()->set(Config::GUI_HistoryViewState, ui->history->viewState().toBase64()); + config()->sync(); + ui->history->setModel(nullptr); } void HistoryWidget::showTxDetails() { - QModelIndex index = ui->history->currentIndex(); + auto *tx = ui->history->currentEntry(); + if (!tx) return; - TransactionInfo *i = nullptr; - m_txHistory->transaction(m_model->mapToSource(index).row(), [&i](TransactionInfo &tInfo) { - i = &tInfo; - }); - - if (i != nullptr) { - auto * dialog = new TransactionInfoDialog(m_wallet, i, this); - dialog->exec(); - } + auto *dialog = new TransactionInfoDialog(m_wallet, tx, this); + dialog->show(); } void HistoryWidget::onViewOnBlockExplorer() { - QModelIndex index = ui->history->currentIndex(); + auto *tx = ui->history->currentEntry(); + if (!tx) return; - QString txid; - m_txHistory->transaction(m_model->mapToSource(index).row(), [&txid](TransactionInfo &tInfo) { - txid = tInfo.hash(); - }); + QString txid = tx->hash(); emit viewOnBlockExplorer(txid); } @@ -124,27 +117,36 @@ void HistoryWidget::setSearchText(const QString &text) { } void HistoryWidget::setSearchFilter(const QString &filter) { - if(!m_model) return; + if (!m_model) return; m_model->setSearchFilter(filter); + ui->history->setSearchMode(!filter.isEmpty()); +} + +void HistoryWidget::createTxProof() { + auto *tx = ui->history->currentEntry(); + if (!tx) return; + + auto *dialog = new TxProofDialog(this, m_wallet, tx); + dialog->exec(); + dialog->deleteLater(); } void HistoryWidget::copy(copyField field) { - QModelIndex index = ui->history->currentIndex(); + auto *tx = ui->history->currentEntry(); + if (!tx) return; - QString data; - m_txHistory->transaction(m_model->mapToSource(index).row(), [field, &data](TransactionInfo &tInfo) { + QString data = [field, tx]{ switch(field) { case copyField::TxID: - data = tInfo.hash(); - break; + return tx->hash(); case copyField::Date: - data = tInfo.timestamp().toString("yyyy-MM-dd HH:mm"); - break; + return tx->timestamp().toString("yyyy-MM-dd HH:mm"); case copyField::Amount: - data = tInfo.displayAmount(); - break; + return tx->displayAmount(); + default: + return QString(""); } - }); + }(); Utils::copyToClipboard(data); } diff --git a/src/historywidget.h b/src/historywidget.h index 35c1df1..197d053 100644 --- a/src/historywidget.h +++ b/src/historywidget.h @@ -40,10 +40,12 @@ private slots: void onViewOnBlockExplorer(); void setSearchFilter(const QString &filter); void onResendTransaction(); + void createTxProof(); private: enum copyField { TxID = 0, + Description, Date, Amount }; diff --git a/src/historywidget.ui b/src/historywidget.ui index ff8cb25..27ea48d 100644 --- a/src/historywidget.ui +++ b/src/historywidget.ui @@ -101,7 +101,7 @@ - + false @@ -115,6 +115,13 @@ + + + HistoryView + QTreeView +
model/HistoryView.h
+
+
diff --git a/src/libwalletqt/TransactionHistory.cpp b/src/libwalletqt/TransactionHistory.cpp index d804dc4..c1e272f 100644 --- a/src/libwalletqt/TransactionHistory.cpp +++ b/src/libwalletqt/TransactionHistory.cpp @@ -32,6 +32,15 @@ TransactionInfo* TransactionHistory::transaction(const QString &id) return itr != m_tinfo.end() ? *itr : nullptr; } +TransactionInfo* TransactionHistory::transaction(int index) +{ + if (index < 0 || index >= m_tinfo.size()) { + return nullptr; + } + + return m_tinfo[index]; +} + void TransactionHistory::refresh(quint32 accountIndex) { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) diff --git a/src/libwalletqt/TransactionHistory.h b/src/libwalletqt/TransactionHistory.h index 83c4097..c00a387 100644 --- a/src/libwalletqt/TransactionHistory.h +++ b/src/libwalletqt/TransactionHistory.h @@ -29,6 +29,7 @@ class TransactionHistory : public QObject public: Q_INVOKABLE bool transaction(int index, std::function callback); Q_INVOKABLE TransactionInfo * transaction(const QString &id); + TransactionInfo* transaction(int index); Q_INVOKABLE void refresh(quint32 accountIndex); Q_INVOKABLE void setTxNote(const QString &txid, const QString ¬e); Q_INVOKABLE bool writeCSV(const QString &path); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index e6094c8..0b4eee4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -255,6 +255,7 @@ MainWindow::MainWindow(AppContext *ctx, QWidget *parent) : connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, m_balanceWidget, &TickerWidget::init); connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, m_ctx, &AppContext::onPreferredFiatCurrencyChanged); connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, ui->sendWidget, QOverload<>::of(&SendWidget::onPreferredFiatCurrencyChanged)); + connect(m_windowSettings, &Settings::amountPrecisionChanged, m_ctx, &AppContext::onAmountPrecisionChanged); // Skin connect(m_windowSettings, &Settings::skinChanged, this, &MainWindow::skinChanged); @@ -584,9 +585,7 @@ void MainWindow::onWalletOpened() { m_wizard->hide(); } - this->raise(); - this->show(); - this->activateWindow(); + this->bringToFront(); this->setEnabled(true); if(!m_ctx->tor->torConnected) this->setStatusText("Wallet opened - Starting Tor (may take a while)"); @@ -766,17 +765,10 @@ void MainWindow::onCreateTransactionSuccess(PendingTransaction *tx, const QVecto } void MainWindow::onTransactionCommitted(bool status, PendingTransaction *tx, const QStringList& txid) { - if(status) { // success + if (status) { // success QString body = QString("Successfully sent %1 transaction(s).").arg(txid.count()); QMessageBox::information(this, "Transactions sent", body); ui->sendWidget->clearFields(); - - for(const auto &entry: txid) { - m_ctx->currentWallet->setUserNote(entry, m_ctx->tmpTxDescription); - AppContext::txDescriptionCache[entry] = m_ctx->tmpTxDescription; - } - - m_ctx->tmpTxDescription = ""; } else { auto err = tx->errorString(); QString body = QString("Error committing transaction: %1").arg(err); @@ -1341,6 +1333,14 @@ QString MainWindow::statusDots() { return QString(".").repeated(m_statusDots); } +void MainWindow::bringToFront() { + ensurePolished(); + setWindowState((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + show(); + raise(); + activateWindow(); +} + MainWindow::~MainWindow() { delete ui; } diff --git a/src/mainwindow.h b/src/mainwindow.h index fc6257c..0fa054a 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -172,6 +172,7 @@ private: void setStatusText(const QString &text, bool override = false, int timeout = 1000); void showBalanceDialog(); QString statusDots(); + void bringToFront(); WalletWizard *createWizard(WalletWizard::Page startPage); diff --git a/src/model/HistoryView.cpp b/src/model/HistoryView.cpp new file mode 100644 index 0000000..2eec65c --- /dev/null +++ b/src/model/HistoryView.cpp @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "HistoryView.h" + +#include "TransactionHistoryProxyModel.h" + +#include +#include + +HistoryView::HistoryView(QWidget *parent) + : QTreeView(parent) + , m_model(nullptr) + , m_headerMenu(new QMenu(this)) +{ + setUniformRowHeights(true); + setRootIsDecorated(false); + setAlternatingRowColors(true); + setDragEnabled(true); + setSortingEnabled(true); + + header()->setStretchLastSection(false); + header()->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(header(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(showHeaderMenu(QPoint))); +} + +void HistoryView::setHistoryModel(TransactionHistoryProxyModel *model) { + m_model = model; + m_model->setSortCaseSensitivity(Qt::CaseInsensitive); + m_model->setSortRole(Qt::UserRole); + m_model->setDynamicSortFilter(true); + m_model->setSortLocaleAware(true); + + QTreeView::setModel(m_model); + resetViewToDefaults(); + + m_headerMenu->clear(); + + // Actions to toggle column visibility, each carrying the corresponding + // column index as data + m_columnActions = new QActionGroup(this); + m_columnActions->setExclusive(false); + for (int visualIndex = 1; visualIndex < header()->count(); ++visualIndex) { + int logicalIndex = header()->logicalIndex(visualIndex); + QString caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString(); + if (caption.isEmpty()) { + caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::ToolTipRole).toString(); + } + + auto action = m_headerMenu->addAction(caption); + action->setCheckable(true); + action->setData(logicalIndex); + m_columnActions->addAction(action); + } + connect(m_columnActions, &QActionGroup::triggered, this, &HistoryView::toggleColumnVisibility); + + m_headerMenu->addSeparator(); + m_headerMenu->addAction(tr("Fit to window"), this, &HistoryView::fitColumnsToWindow); + m_headerMenu->addAction(tr("Fit to contents"), this, &HistoryView::fitColumnsToContents); + m_headerMenu->addSeparator(); + m_headerMenu->addAction(tr("Reset to defaults"), this, &HistoryView::resetViewToDefaults); + + fitColumnsToWindow(); +} + +TransactionHistoryModel* HistoryView::sourceModel() +{ + return dynamic_cast(m_model->sourceModel()); +} + +TransactionInfo* HistoryView::currentEntry() +{ + QModelIndexList list = selectionModel()->selectedRows(); + if (list.size() == 1) { + return this->sourceModel()->entryFromIndex(m_model->mapToSource(list.first())); + } else { + return nullptr; + } +} + +void HistoryView::setSearchMode(bool mode) { + m_inSearchMode = mode; + + if (mode) { + header()->showSection(TransactionHistoryModel::TxID); + } +} + +QByteArray HistoryView::viewState() const +{ + return header()->saveState(); +} + +bool HistoryView::setViewState(const QByteArray& state) +{ + // Reset to unsorted first (https://bugreports.qt.io/browse/QTBUG-86694) + header()->setSortIndicator(-1, Qt::AscendingOrder); + bool status = header()->restoreState(state); + m_columnsNeedRelayout = state.isEmpty(); + return status; +} + +void HistoryView::showHeaderMenu(const QPoint& position) +{ + const QList actions = m_columnActions->actions(); + for (auto& action : actions) { + Q_ASSERT(static_cast(action->data().type()) == QMetaType::Int); + if (static_cast(action->data().type()) != QMetaType::Int) { + continue; + } + int columnIndex = action->data().toInt(); + action->setChecked(!isColumnHidden(columnIndex)); + } + + m_headerMenu->popup(mapToGlobal(position)); +} + +void HistoryView::toggleColumnVisibility(QAction* action) +{ + // Verify action carries a column index as data. Since QVariant.toInt() + // below will accept anything that's interpretable as int, perform a type + // check here to make sure data actually IS int + Q_ASSERT(static_cast(action->data().type()) == QMetaType::Int); + if (static_cast(action->data().type()) != QMetaType::Int) { + return; + } + + // Toggle column visibility. Visible columns will only be hidden if at + // least one visible column remains, as the table header will disappear + // entirely when all columns are hidden + int columnIndex = action->data().toInt(); + if (action->isChecked()) { + header()->showSection(columnIndex); + if (header()->sectionSize(columnIndex) == 0) { + header()->resizeSection(columnIndex, header()->defaultSectionSize()); + } + return; + } + if ((header()->count() - header()->hiddenSectionCount()) > 1) { + header()->hideSection(columnIndex); + return; + } + action->setChecked(true); +} + +void HistoryView::fitColumnsToWindow() +{ + header()->setSectionResizeMode(QHeaderView::ResizeToContents); + header()->setSectionResizeMode(TransactionHistoryModel::Description, QHeaderView::Stretch); + header()->setStretchLastSection(false); + QCoreApplication::processEvents(); +} + +void HistoryView::fitColumnsToContents() +{ + header()->setSectionResizeMode(QHeaderView::ResizeToContents); + QCoreApplication::processEvents(); + header()->setSectionResizeMode(QHeaderView::Interactive); +} + +void HistoryView::resetViewToDefaults() +{ + if (m_inSearchMode) { + header()->showSection(TransactionHistoryModel::TxID); + } else { + header()->hideSection(TransactionHistoryModel::TxID); + } + header()->showSection(TransactionHistoryModel::Date); + header()->showSection(TransactionHistoryModel::Description); + header()->showSection(TransactionHistoryModel::Amount); + header()->showSection(TransactionHistoryModel::FiatAmount); + + // Reset column order to logical indices + for (int i = 0; i < header()->count(); ++i) { + header()->moveSection(header()->visualIndex(i), i); + } + + m_model->sort(TransactionHistoryModel::Date, Qt::DescendingOrder); + sortByColumn(TransactionHistoryModel::Date, Qt::DescendingOrder); + + // The following call only relayouts reliably if the widget has been shown + // already, so only do it if the widget is visible and let showEvent() handle + // the initial default layout. + if (isVisible()) { + fitColumnsToWindow(); + } +} diff --git a/src/model/HistoryView.h b/src/model/HistoryView.h new file mode 100644 index 0000000..75205e1 --- /dev/null +++ b/src/model/HistoryView.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_HISTORYVIEW_H +#define FEATHER_HISTORYVIEW_H + +#include +#include + +#include "TransactionHistoryModel.h" + +class HistoryView : public QTreeView +{ + Q_OBJECT + +public: + explicit HistoryView(QWidget* parent = nullptr); + void setHistoryModel(TransactionHistoryProxyModel *model); + TransactionInfo* currentEntry(); + + void setSearchMode(bool mode); + QByteArray viewState() const; + bool setViewState(const QByteArray& state); + +private slots: + void showHeaderMenu(const QPoint& position); + void toggleColumnVisibility(QAction* action); + void fitColumnsToWindow(); + void fitColumnsToContents(); + void resetViewToDefaults(); + +private: + TransactionHistoryModel* sourceModel(); + + TransactionHistoryProxyModel* m_model; + bool m_inSearchMode = false; + bool m_columnsNeedRelayout = true; + + QMenu* m_headerMenu; + QActionGroup* m_columnActions; +}; + + +#endif //FEATHER_HISTORYVIEW_H diff --git a/src/model/ModelUtils.cpp b/src/model/ModelUtils.cpp index 290effc..9a1041a 100644 --- a/src/model/ModelUtils.cpp +++ b/src/model/ModelUtils.cpp @@ -16,7 +16,7 @@ QString ModelUtils::displayAddress(const QString& address, int sections, const Q for (int i = 0; i < sections; i += 1) { list << address.mid(i*5, 5); } - list << "..."; + list << "…"; // utf-8 Horizontal Ellipsis for (int i = sections; i > 0; i -= 1) { list << address.mid(address.length() - i * 5, 5); } diff --git a/src/model/TransactionHistoryModel.cpp b/src/model/TransactionHistoryModel.cpp index 0c96526..1d9aa6e 100644 --- a/src/model/TransactionHistoryModel.cpp +++ b/src/model/TransactionHistoryModel.cpp @@ -6,6 +6,7 @@ #include "TransactionInfo.h" #include "globals.h" #include "utils/ColorScheme.h" +#include "ModelUtils.h" TransactionHistoryModel::TransactionHistoryModel(QObject *parent) : QAbstractTableModel(parent), @@ -38,6 +39,11 @@ TransactionHistory *TransactionHistoryModel::transactionHistory() const { return m_transactionHistory; } +TransactionInfo* TransactionHistoryModel::entryFromIndex(const QModelIndex &index) const { + Q_ASSERT(index.isValid() && index.row() < m_transactionHistory->count()); + return m_transactionHistory->transaction(index.row()); +} + int TransactionHistoryModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; @@ -65,8 +71,8 @@ QVariant TransactionHistoryModel::data(const QModelIndex &index, int role) const QVariant result; bool found = m_transactionHistory->transaction(index.row(), [this, &index, &result, &role](const TransactionInfo &tInfo) { - if(role == Qt::DisplayRole || role == Qt::EditRole) { - result = parseTransactionInfo(tInfo, index.column()); + if(role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole) { + result = parseTransactionInfo(tInfo, index.column(), role); } else if (role == Qt::TextAlignmentRole) { switch (index.column()) { @@ -130,31 +136,26 @@ QVariant TransactionHistoryModel::data(const QModelIndex &index, int role) const return result; } -QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionInfo &tInfo, int column) const +QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionInfo &tInfo, int column, int role) const { switch (column) { case Column::Date: - return tInfo.timestamp().toString("yyyy-MM-dd HH:mm"); - case Column::Description: { - // if this tx is still in the pool, then we wont get the - // description. We've cached it inside `AppContext::txDescriptionCache` - // for the time being. - if(tInfo.isPending()) { - auto hash = tInfo.hash(); - if (AppContext::txDescriptionCache.contains(hash)) - return AppContext::txDescriptionCache[hash]; - } + return tInfo.timestamp().toString("yyyy-MM-dd HH:mm "); + case Column::Description: return tInfo.description(); - } case Column::Amount: { - QString amount = QString::number(tInfo.balanceDelta() / globals::cdiv, 'f', 4); + if (role == Qt::UserRole) { + return tInfo.balanceDelta(); + } + QString amount = QString::number(tInfo.balanceDelta() / globals::cdiv, 'f', this->amountPrecision); amount = (tInfo.direction() == TransactionInfo::Direction_Out) ? "-" + amount : "+" + amount; return amount; } - case Column::TxID: - return tInfo.hash(); + case Column::TxID: { + return ModelUtils::displayAddress(tInfo.hash(), 1); + } case Column::FiatAmount: { double usd_price = AppContext::txFiatHistory->get(tInfo.timestamp().toString("yyyyMMdd")); @@ -164,8 +165,11 @@ QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionInfo &tI double usd_amount = usd_price * (tInfo.balanceDelta() / globals::cdiv); if(this->preferredFiatSymbol != "USD") usd_amount = AppContext::prices->convert("USD", this->preferredFiatSymbol, usd_amount); - double fiat_rounded = ceil(Utils::roundSignificant(usd_amount, 3) * 100.0) / 100.0; + if (role == Qt::UserRole) { + return usd_amount; + } + double fiat_rounded = ceil(Utils::roundSignificant(usd_amount, 3) * 100.0) / 100.0; return QString("%1").arg(Utils::amountToCurrencyString(fiat_rounded, this->preferredFiatSymbol)); } default: @@ -224,15 +228,10 @@ bool TransactionHistoryModel::setData(const QModelIndex &index, const QVariant & } Qt::ItemFlags TransactionHistoryModel::flags(const QModelIndex &index) const { - bool isPending; - m_transactionHistory->transaction(index.row(), [this, &isPending](const TransactionInfo &tInfo){ - isPending = tInfo.isPending(); - }); - if (!index.isValid()) return Qt::ItemIsEnabled; - if (index.column() == Description && !isPending) + if (index.column() == Description) return QAbstractTableModel::flags(index) | Qt::ItemIsEditable; return QAbstractTableModel::flags(index); diff --git a/src/model/TransactionHistoryModel.h b/src/model/TransactionHistoryModel.h index 3225447..b7a0427 100644 --- a/src/model/TransactionHistoryModel.h +++ b/src/model/TransactionHistoryModel.h @@ -24,9 +24,9 @@ public: enum Column { Date = 0, + TxID, Description, Amount, - TxID, FiatAmount, COUNT }; @@ -34,8 +34,11 @@ public: explicit TransactionHistoryModel(QObject * parent = nullptr); void setTransactionHistory(TransactionHistory * th); TransactionHistory * transactionHistory() const; + TransactionInfo* entryFromIndex(const QModelIndex& index) const; QString preferredFiatSymbol = "USD"; + int amountPrecision = 4; + int rowCount(const QModelIndex & parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override; @@ -48,7 +51,7 @@ signals: void transactionHistoryChanged(); private: - QVariant parseTransactionInfo(const TransactionInfo &tInfo, int column) const; + QVariant parseTransactionInfo(const TransactionInfo &tInfo, int column, int role) const; TransactionHistory * m_transactionHistory; QIcon m_unconfirmedTx; diff --git a/src/model/TransactionHistoryProxyModel.cpp b/src/model/TransactionHistoryProxyModel.cpp index 538c5aa..40b7f34 100644 --- a/src/model/TransactionHistoryProxyModel.cpp +++ b/src/model/TransactionHistoryProxyModel.cpp @@ -16,6 +16,10 @@ TransactionHistoryProxyModel::TransactionHistoryProxyModel(Wallet *wallet, QObje m_history = m_wallet->history(); } +TransactionHistory* TransactionHistoryProxyModel::history() { + return m_history; +} + bool TransactionHistoryProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { QString description, txid, subaddrlabel; diff --git a/src/model/TransactionHistoryProxyModel.h b/src/model/TransactionHistoryProxyModel.h index 019976a..a84535d 100644 --- a/src/model/TransactionHistoryProxyModel.h +++ b/src/model/TransactionHistoryProxyModel.h @@ -16,6 +16,7 @@ Q_OBJECT public: explicit TransactionHistoryProxyModel(Wallet *wallet, QObject* parent = nullptr); bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const; + TransactionHistory* history(); public slots: void setSearchFilter(const QString& searchString){ diff --git a/src/settings.cpp b/src/settings.cpp index af314e5..8029288 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -43,9 +43,15 @@ Settings::Settings(QWidget *parent) : if (m_skins.contains(settingsSkin)) ui->comboBox_skin->setCurrentIndex(m_skins.indexOf(settingsSkin)); + for (int i = 0; i <= 12; i++) { + ui->comboBox_amountPrecision->addItem(QString::number(i)); + } + ui->comboBox_amountPrecision->setCurrentIndex(config()->get(Config::amountPrecision).toInt()); + connect(ui->comboBox_skin, QOverload::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_skinChanged); connect(ui->comboBox_blockExplorer, QOverload::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_blockExplorerChanged); connect(ui->comboBox_redditFrontend, QOverload::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_redditFrontendChanged); + connect(ui->comboBox_amountPrecision, QOverload::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_amountPrecisionChanged); // setup preferred fiat currency combobox QStringList fiatCurrencies; @@ -100,6 +106,11 @@ void Settings::comboBox_redditFrontendChanged(int pos) { config()->set(Config::redditFrontend, redditFrontend); } +void Settings::comboBox_amountPrecisionChanged(int pos) { + config()->set(Config::amountPrecision, pos); + emit amountPrecisionChanged(pos); +} + void Settings::copyToClipboard() { ui->textLogs->copy(); } diff --git a/src/settings.h b/src/settings.h index 86b8c5c..cf4465c 100644 --- a/src/settings.h +++ b/src/settings.h @@ -29,6 +29,7 @@ signals: void skinChanged(QString skinName); void showHomeCCS(bool); void blockExplorerChanged(QString blockExplorer); + void amountPrecisionChanged(int precision); public slots: void updatePaths(); @@ -38,6 +39,7 @@ public slots: void comboBox_skinChanged(int pos); void comboBox_blockExplorerChanged(int pos); void comboBox_redditFrontendChanged(int pos); + void comboBox_amountPrecisionChanged(int pos); private: QStringList m_skins{"Native", "QDarkStyle", "Breeze/Dark", "Breeze/Light"}; diff --git a/src/settings.ui b/src/settings.ui index 9f0fb4b..8a24ffb 100644 --- a/src/settings.ui +++ b/src/settings.ui @@ -161,14 +161,14 @@ - + Warn before opening external link - + Hide balance @@ -201,6 +201,16 @@ + + + + Amount precision: + + + + + + diff --git a/src/utils/config.cpp b/src/utils/config.cpp index 60b7c64..61fd4a0 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -46,7 +46,9 @@ static const QHash configStrings = { {Config::firstRun,{QS("firstRun"), false}}, {Config::hideBalance, {QS("hideBalance"), false}}, {Config::redditFrontend, {QS("redditFrontend"), "old.reddit.com"}}, - {Config::showHistorySyncNotice, {QS("showHistorySyncNotice"), true}} + {Config::showHistorySyncNotice, {QS("showHistorySyncNotice"), true}}, + {Config::GUI_HistoryViewState, {QS("GUI_HistoryViewState"), {}}}, + {Config::amountPrecision, {QS("amountPrecision"), 4}} }; diff --git a/src/utils/config.h b/src/utils/config.h index 5f48b7e..9271789 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -48,7 +48,9 @@ public: firstRun, hideBalance, redditFrontend, - showHistorySyncNotice + showHistorySyncNotice, + GUI_HistoryViewState, + amountPrecision }; ~Config() override; diff --git a/src/widgets/txproofwidget.cpp b/src/widgets/txproofwidget.cpp deleted file mode 100644 index 81385af..0000000 --- a/src/widgets/txproofwidget.cpp +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -// Copyright (c) 2020-2021, The Monero Project. - -#include "txproofwidget.h" -#include "ui_txproofwidget.h" - -#include - -#include "utils/utils.h" - -TxProofWidget::TxProofWidget(QWidget *parent, Wallet *wallet, TransactionInfo *txInfo) - : QWidget(parent) - , ui(new Ui::TxProofWidget) - , m_wallet(wallet) - , m_txid(txInfo->hash()) -{ - ui->setupUi(this); - - if (txInfo->direction() == TransactionInfo::Direction_Out) { - for (auto const &d: txInfo->destinations()) { - ui->comboBox_TxProofAddresses->addItem(d); - } - } else { - ui->btn_copySpendProof->setEnabled(false); - - for (auto const &s: txInfo->subaddrIndex()) { - ui->comboBox_TxProofAddresses->addItem(wallet->address(txInfo->subaddrAccount(), s)); - } - } - - if (ui->comboBox_TxProofAddresses->count() == 0) { - ui->btn_copyTxProof->setEnabled(false); - } - - connect(ui->btn_copySpendProof, &QPushButton::clicked, this, &TxProofWidget::copySpendProof); - connect(ui->btn_copyTxProof, &QPushButton::clicked, this, &TxProofWidget::copyTxProof); - - ui->label_SpendProof->setHelpText("A SpendProof proves authorship of a transaction.\n\n" - "SpendProofs do not prove that a particular amount was sent to an address, for that use OutProofs."); - ui->label_InOutProof->setHelpText("An InProof proves ownership of an output.\n" - "An OutProof shows the prover sent an output to an address."); -} - -void TxProofWidget::copySpendProof() { - auto txproof = m_wallet->getSpendProof(m_txid, ""); - if (!txproof.error.isEmpty()) { - QMessageBox::warning(this, "Copy SpendProof", QString("Failed to copy SpendProof").arg(txproof.error)); - return; - } - - Utils::copyToClipboard(txproof.proof); - QMessageBox::information(this, "Copy SpendProof", "SpendProof copied to clipboard"); -} - -void TxProofWidget::copyTxProof() { - auto txproof = m_wallet->getTxProof(m_txid, ui->comboBox_TxProofAddresses->currentText(), ""); - if (!txproof.error.isEmpty()) { - QMessageBox::warning(this, "Copy Transaction Proof", QString("Failed to copy transaction proof: %1").arg(txproof.error)); - return; - } - - Utils::copyToClipboard(txproof.proof); - QMessageBox::information(this, "Copy Transaction Proof", "Transaction proof copied to clipboard"); -} - -TxProofWidget::~TxProofWidget() { - delete ui; -} diff --git a/src/widgets/txproofwidget.h b/src/widgets/txproofwidget.h deleted file mode 100644 index f142627..0000000 --- a/src/widgets/txproofwidget.h +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -// Copyright (c) 2020-2021, The Monero Project. - -#ifndef FEATHER_TXPROOFWIDGET_H -#define FEATHER_TXPROOFWIDGET_H - -#include - -#include "libwalletqt/Wallet.h" -#include "libwalletqt/TransactionInfo.h" - -namespace Ui { - class TxProofWidget; -} - -class TxProofWidget : public QWidget -{ -Q_OBJECT - -public: - explicit TxProofWidget(QWidget *parent, Wallet *wallet, TransactionInfo *txid); - ~TxProofWidget() override; - -private: - void copySpendProof(); - void copyTxProof(); - - Ui::TxProofWidget *ui; - QString m_txid; - Wallet *m_wallet; -}; - -#endif //FEATHER_TXPROOFWIDGET_H diff --git a/src/widgets/txproofwidget.ui b/src/widgets/txproofwidget.ui deleted file mode 100644 index ab96be4..0000000 --- a/src/widgets/txproofwidget.ui +++ /dev/null @@ -1,110 +0,0 @@ - - - TxProofWidget - - - - 0 - 0 - 647 - 79 - - - - - 0 - 0 - - - - Form - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - 0 - 0 - - - - Copy - - - - - - - SpendProof - - - - - - - - - - - - 0 - 0 - - - - Copy - - - - - - - - 0 - 0 - - - - In/OutProof - - - - - - - - 0 - 0 - - - - - - - - - - - HelpLabel - QLabel -
components.h
-
-
- - -