From 046b2cfc4c4924a8d92007e282ea46c704ae9b36 Mon Sep 17 00:00:00 2001 From: tobtoht Date: Tue, 10 Nov 2020 12:38:37 +0100 Subject: [PATCH] Import transaction --- CMakeLists.txt | 2 +- monero | 2 +- src/dialog/tximportdialog.cpp | 99 +++++++++++++++++++++++++++++ src/dialog/tximportdialog.h | 36 +++++++++++ src/dialog/tximportdialog.ui | 115 ++++++++++++++++++++++++++++++++++ src/libwalletqt/Wallet.cpp | 17 +++++ src/libwalletqt/Wallet.h | 3 + src/mainwindow.cpp | 15 +++++ src/mainwindow.h | 1 + src/mainwindow.ui | 6 ++ src/utils/daemonrpc.cpp | 11 ++++ src/utils/daemonrpc.h | 4 +- 12 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 src/dialog/tximportdialog.cpp create mode 100644 src/dialog/tximportdialog.h create mode 100644 src/dialog/tximportdialog.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index 821a90b..441dc75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ if(DEBUG) set(CMAKE_VERBOSE_MAKEFILE ON) endif() -set(MONERO_HEAD "9ca5569f40a392b16946c5c3bda312eecfdcc9ab") +set(MONERO_HEAD "d029a63fb75c581fa060447b41d385c595144774") set(BUILD_GUI_DEPS ON) set(ARCH "x86-64") set(BUILD_64 ON) diff --git a/monero b/monero index 9ca5569..d029a63 160000 --- a/monero +++ b/monero @@ -1 +1 @@ -Subproject commit 9ca5569f40a392b16946c5c3bda312eecfdcc9ab +Subproject commit d029a63fb75c581fa060447b41d385c595144774 diff --git a/src/dialog/tximportdialog.cpp b/src/dialog/tximportdialog.cpp new file mode 100644 index 0000000..4b9dd17 --- /dev/null +++ b/src/dialog/tximportdialog.cpp @@ -0,0 +1,99 @@ +#include "tximportdialog.h" +#include "ui_tximportdialog.h" + +#include + +TxImportDialog::TxImportDialog(QWidget *parent, AppContext *ctx) + : QDialog(parent) + , m_ctx(ctx) + , m_loadTimer(new QTimer(this)) + , ui(new Ui::TxImportDialog) +{ + ui->setupUi(this); + ui->resp->hide(); + ui->label_loading->hide(); + + m_network = new UtilsNetworking(m_ctx->network, this); + + auto node = ctx->nodes->connection(); + m_rpc = new DaemonRpc(this, m_network, node.full); + + connect(ui->btn_load, &QPushButton::clicked, this, &TxImportDialog::loadTx); + connect(ui->btn_import, &QPushButton::clicked, this, &TxImportDialog::onImport); + + connect(m_rpc, &DaemonRpc::ApiResponse, this, &TxImportDialog::onApiResponse); + + connect(m_loadTimer, &QTimer::timeout, [this]{ + ui->label_loading->setText(ui->label_loading->text() + "."); + }); + + this->adjustSize(); +} + +void TxImportDialog::loadTx() { + QString txid = ui->line_txid->text(); + QString node = m_ctx->nodes->connection().full; + + if (!node.startsWith("http://")) + node = QString("http://%1").arg(node); + + m_rpc->setDaemonAddress(node); + m_rpc->getTransactions(QStringList() << txid, false, true); + + ui->label_loading->setText("Loading transaction"); + ui->label_loading->setHidden(false); + m_loadTimer->start(1000); +} + +void TxImportDialog::onApiResponse(const DaemonRpc::DaemonResponse &resp) { + m_loadTimer->stop(); + ui->label_loading->setHidden(true); + if (!resp.ok) { + QMessageBox::warning(this, "Import transaction", resp.status); + return; + } + + if (resp.endpoint == DaemonRpc::Endpoint::GET_TRANSACTIONS) { + ui->resp->setVisible(true); + ui->resp->setPlainText(QJsonDocument(resp.obj).toJson(QJsonDocument::Indented)); + this->adjustSize(); + + if (resp.obj.contains("missed_tx")) { + ui->btn_import->setEnabled(false); + QMessageBox::warning(this, "Load transaction", "Transaction could not be found. Make sure the txid is correct, or try connecting to a different node."); + return; + } + + QMessageBox::information(this, "Load transaction", "Transaction loaded successfully.\n\nAfter closing this message box click the Import button to import the transaction into your wallet."); + m_transaction = resp.obj; + ui->btn_import->setEnabled(true); + } +} + +void TxImportDialog::onImport() { + QJsonObject tx = m_transaction.value("txs").toArray().first().toObject(); + + QString txid = tx.value("tx_hash").toString(); + + QVector output_indices; + for (const auto &o: tx.value("output_indices").toArray()) { + output_indices.push_back(o.toInt()); + } + + quint64 height = tx.value("block_height").toInt(); + quint64 timestamp = tx.value("block_timestamp").toInt(); + + bool pool = tx.value("in_pool").toBool(); + bool double_spend_seen = tx.value("double_spend_seen").toBool(); + + if (m_ctx->currentWallet->importTransaction(tx.value("tx_hash").toString(), output_indices, height, timestamp, false, pool, double_spend_seen)) { + QMessageBox::information(this, "Import transaction", "Transaction imported successfully."); + } else { + QMessageBox::warning(this, "Import transaction", "Transaction import failed."); + } + m_ctx->refreshModels(); +} + +TxImportDialog::~TxImportDialog() { + delete ui; +} diff --git a/src/dialog/tximportdialog.h b/src/dialog/tximportdialog.h new file mode 100644 index 0000000..b8fc5c3 --- /dev/null +++ b/src/dialog/tximportdialog.h @@ -0,0 +1,36 @@ +#ifndef FEATHER_TXIMPORTDIALOG_H +#define FEATHER_TXIMPORTDIALOG_H + +#include +#include "appcontext.h" +#include "utils/daemonrpc.h" + +namespace Ui { + class TxImportDialog; +} + +class TxImportDialog : public QDialog +{ +Q_OBJECT + +public: + explicit TxImportDialog(QWidget *parent, AppContext *ctx); + ~TxImportDialog() override; + +private slots: + void loadTx(); + void onImport(); + void onApiResponse(const DaemonRpc::DaemonResponse &resp); + +private: + Ui::TxImportDialog *ui; + UtilsNetworking *m_network; + AppContext *m_ctx; + DaemonRpc *m_rpc; + QTimer *m_loadTimer; + + QJsonObject m_transaction; +}; + + +#endif //FEATHER_TXIMPORTDIALOG_H diff --git a/src/dialog/tximportdialog.ui b/src/dialog/tximportdialog.ui new file mode 100644 index 0000000..90704e3 --- /dev/null +++ b/src/dialog/tximportdialog.ui @@ -0,0 +1,115 @@ + + + TxImportDialog + + + + 0 + 0 + 700 + 442 + + + + + 700 + 0 + + + + Import Transaction + + + + + + Transaction ID + + + + + + + true + + + Debug info.. + + + + + + + + + Load + + + + + + + false + + + Import + + + + + + + Loading transaction + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + + + buttonBox + accepted() + TxImportDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + TxImportDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index 40d6dbe..3d46aad 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -476,6 +476,23 @@ bool Wallet::importOutputs(const QString& path) { return m_walletImpl->importOutputs(path.toStdString()); } +bool Wallet::importTransaction(const QString& txid, const QVector& output_indeces, quint64 height, quint64 timestamp, bool miner_tx, bool pool, bool double_spend_seen) { + std::vector o_indeces; + for (const auto &o : output_indeces) { + o_indeces.push_back(o); + } + + return m_walletImpl->importTransaction( + txid.toStdString(), + o_indeces, + height, + 17, // todo: get actual block_version + timestamp, + miner_tx, + pool, + double_spend_seen); +} + void Wallet::startRefresh() { m_refreshEnabled = true; diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index d770045..52f5039 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -206,6 +206,9 @@ public: Q_INVOKABLE bool exportOutputs(const QString& path, bool all = false); Q_INVOKABLE bool importOutputs(const QString& path); + //! import a transaction + Q_INVOKABLE bool importTransaction(const QString& txid, const QVector& output_indeces, quint64 height, quint64 timestamp, bool miner_tx, bool pool, bool double_spend_seen); + //! refreshes the wallet Q_INVOKABLE bool refresh(bool historyAndSubaddresses = false); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index d6d5149..52992a0 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -19,6 +19,7 @@ #include "dialog/torinfodialog.h" #include "dialog/viewonlydialog.h" #include "dialog/broadcasttxdialog.h" +#include "dialog/tximportdialog.h" #include "utils/utils.h" #include "utils/config.h" #include "utils/daemonrpc.h" @@ -495,6 +496,7 @@ void MainWindow::initMenu() { connect(ui->actionLoadUnsignedTxFromClipboard, &QAction::triggered, this, &MainWindow::loadUnsignedTxFromClipboard); 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); // About screen connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::menuAboutClicked); @@ -1296,6 +1298,19 @@ void MainWindow::createUnsignedTxDialog(UnsignedTransaction *tx) { dialog->deleteLater(); } +void MainWindow::importTransaction() { + + auto result = QMessageBox::warning(this, "Warning", "Using this feature may allow a remote node to associate the transaction with your IP address.\n" + "\n" + "Connect to a trusted node or run Feather over Tor if network level metadata leakage is included in your threat model.", + QMessageBox::Ok | QMessageBox::Cancel); + if (result == QMessageBox::Ok) { + auto *dialog = new TxImportDialog(this, m_ctx); + dialog->exec(); + dialog->deleteLater(); + } +} + MainWindow::~MainWindow() { delete ui; } diff --git a/src/mainwindow.h b/src/mainwindow.h index baccc6e..c555bc2 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -116,6 +116,7 @@ public slots: void onAddContact(const QString &address, const QString &name); void importContacts(); void showRestoreHeightDialog(); + void importTransaction(); // offline tx signing void exportKeyImages(); diff --git a/src/mainwindow.ui b/src/mainwindow.ui index a732207..f9e2fc8 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -395,6 +395,7 @@ + @@ -664,6 +665,11 @@ From text + + + Import transaction + + diff --git a/src/utils/daemonrpc.cpp b/src/utils/daemonrpc.cpp index c48be2e..619a52e 100644 --- a/src/utils/daemonrpc.cpp +++ b/src/utils/daemonrpc.cpp @@ -23,6 +23,17 @@ void DaemonRpc::sendRawTransaction(const QString &tx_as_hex, bool do_not_relay, connect(reply, &QNetworkReply::finished, std::bind(&DaemonRpc::onResponse, this, reply, Endpoint::SEND_RAW_TRANSACTION)); } +void DaemonRpc::getTransactions(const QStringList &txs_hashes, bool decode_as_json, bool prune) { + QJsonObject req; + req["txs_hashes"] = QJsonArray::fromStringList(txs_hashes); + req["decode_as_json"] = decode_as_json; + req["prune"] = prune; + + QString url = QString("%1/get_transactions").arg(m_daemonAddress); + QNetworkReply *reply = m_network->postJson(url, req); + connect(reply, &QNetworkReply::finished, std::bind(&DaemonRpc::onResponse, this, reply, Endpoint::GET_TRANSACTIONS)); +} + void DaemonRpc::onResponse(QNetworkReply *reply, Endpoint endpoint) { const auto ok = reply->error() == QNetworkReply::NoError; const auto err = reply->errorString(); diff --git a/src/utils/daemonrpc.h b/src/utils/daemonrpc.h index 8100ff7..27c0c94 100644 --- a/src/utils/daemonrpc.h +++ b/src/utils/daemonrpc.h @@ -13,7 +13,8 @@ class DaemonRpc : public QObject { public: enum Endpoint { - SEND_RAW_TRANSACTION = 0 + SEND_RAW_TRANSACTION = 0, + GET_TRANSACTIONS }; struct DaemonResponse { @@ -29,6 +30,7 @@ public: explicit DaemonRpc(QObject *parent, UtilsNetworking *network, QString daemonAddress); void sendRawTransaction(const QString &tx_as_hex, bool do_not_relay = false, bool do_sanity_checks = true); + void getTransactions(const QStringList &txs_hashes, bool decode_as_json = false, bool prune = false); void setDaemonAddress(const QString &daemonAddress); void setNetwork(UtilsNetworking *network);