Merge pull request 'Import transaction' (#143) from tobtoht/feather:import_tx into master

Reviewed-on: https://git.wownero.com/feather/feather/pulls/143
This commit is contained in:
tobtoht 2020-11-10 11:48:11 +00:00
commit bd60e30c3f
12 changed files with 308 additions and 3 deletions

View file

@ -30,7 +30,7 @@ if(DEBUG)
set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_VERBOSE_MAKEFILE ON)
endif() endif()
set(MONERO_HEAD "9ca5569f40a392b16946c5c3bda312eecfdcc9ab") set(MONERO_HEAD "d029a63fb75c581fa060447b41d385c595144774")
set(BUILD_GUI_DEPS ON) set(BUILD_GUI_DEPS ON)
set(ARCH "x86-64") set(ARCH "x86-64")
set(BUILD_64 ON) set(BUILD_64 ON)

2
monero

@ -1 +1 @@
Subproject commit 9ca5569f40a392b16946c5c3bda312eecfdcc9ab Subproject commit d029a63fb75c581fa060447b41d385c595144774

View file

@ -0,0 +1,99 @@
#include "tximportdialog.h"
#include "ui_tximportdialog.h"
#include <QMessageBox>
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<quint64> 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;
}

View file

@ -0,0 +1,36 @@
#ifndef FEATHER_TXIMPORTDIALOG_H
#define FEATHER_TXIMPORTDIALOG_H
#include <QDialog>
#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

View file

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TxImportDialog</class>
<widget class="QDialog" name="TxImportDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>700</width>
<height>442</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>700</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>Import Transaction</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLineEdit" name="line_txid">
<property name="placeholderText">
<string>Transaction ID</string>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="resp">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="placeholderText">
<string>Debug info..</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="btn_load">
<property name="text">
<string>Load</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_import">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Import</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_loading">
<property name="text">
<string>Loading transaction</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>TxImportDialog</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>TxImportDialog</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>

View file

@ -476,6 +476,23 @@ bool Wallet::importOutputs(const QString& path) {
return m_walletImpl->importOutputs(path.toStdString()); return m_walletImpl->importOutputs(path.toStdString());
} }
bool Wallet::importTransaction(const QString& txid, const QVector<quint64>& output_indeces, quint64 height, quint64 timestamp, bool miner_tx, bool pool, bool double_spend_seen) {
std::vector<uint64_t> 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() void Wallet::startRefresh()
{ {
m_refreshEnabled = true; m_refreshEnabled = true;

View file

@ -206,6 +206,9 @@ public:
Q_INVOKABLE bool exportOutputs(const QString& path, bool all = false); Q_INVOKABLE bool exportOutputs(const QString& path, bool all = false);
Q_INVOKABLE bool importOutputs(const QString& path); Q_INVOKABLE bool importOutputs(const QString& path);
//! import a transaction
Q_INVOKABLE bool importTransaction(const QString& txid, const QVector<quint64>& output_indeces, quint64 height, quint64 timestamp, bool miner_tx, bool pool, bool double_spend_seen);
//! refreshes the wallet //! refreshes the wallet
Q_INVOKABLE bool refresh(bool historyAndSubaddresses = false); Q_INVOKABLE bool refresh(bool historyAndSubaddresses = false);

View file

@ -19,6 +19,7 @@
#include "dialog/torinfodialog.h" #include "dialog/torinfodialog.h"
#include "dialog/viewonlydialog.h" #include "dialog/viewonlydialog.h"
#include "dialog/broadcasttxdialog.h" #include "dialog/broadcasttxdialog.h"
#include "dialog/tximportdialog.h"
#include "utils/utils.h" #include "utils/utils.h"
#include "utils/config.h" #include "utils/config.h"
#include "utils/daemonrpc.h" #include "utils/daemonrpc.h"
@ -495,6 +496,7 @@ void MainWindow::initMenu() {
connect(ui->actionLoadUnsignedTxFromClipboard, &QAction::triggered, this, &MainWindow::loadUnsignedTxFromClipboard); connect(ui->actionLoadUnsignedTxFromClipboard, &QAction::triggered, this, &MainWindow::loadUnsignedTxFromClipboard);
connect(ui->actionLoadSignedTxFromFile, &QAction::triggered, this, &MainWindow::loadSignedTx); connect(ui->actionLoadSignedTxFromFile, &QAction::triggered, this, &MainWindow::loadSignedTx);
connect(ui->actionLoadSignedTxFromText, &QAction::triggered, this, &MainWindow::loadSignedTxFromText); connect(ui->actionLoadSignedTxFromText, &QAction::triggered, this, &MainWindow::loadSignedTxFromText);
connect(ui->actionImport_transaction, &QAction::triggered, this, &MainWindow::importTransaction);
// About screen // About screen
connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::menuAboutClicked); connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::menuAboutClicked);
@ -1296,6 +1298,19 @@ void MainWindow::createUnsignedTxDialog(UnsignedTransaction *tx) {
dialog->deleteLater(); 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() { MainWindow::~MainWindow() {
delete ui; delete ui;
} }

View file

@ -116,6 +116,7 @@ public slots:
void onAddContact(const QString &address, const QString &name); void onAddContact(const QString &address, const QString &name);
void importContacts(); void importContacts();
void showRestoreHeightDialog(); void showRestoreHeightDialog();
void importTransaction();
// offline tx signing // offline tx signing
void exportKeyImages(); void exportKeyImages();

View file

@ -395,6 +395,7 @@
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="menuLoad_transaction"/> <addaction name="menuLoad_transaction"/>
<addaction name="menuLoad_signed_transaction"/> <addaction name="menuLoad_signed_transaction"/>
<addaction name="actionImport_transaction"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionCalculator"/> <addaction name="actionCalculator"/>
<addaction name="actionCreateDesktopEntry"/> <addaction name="actionCreateDesktopEntry"/>
@ -664,6 +665,11 @@
<string>From text</string> <string>From text</string>
</property> </property>
</action> </action>
<action name="actionImport_transaction">
<property name="text">
<string>Import transaction</string>
</property>
</action>
</widget> </widget>
<layoutdefault spacing="6" margin="11"/> <layoutdefault spacing="6" margin="11"/>
<customwidgets> <customwidgets>

View file

@ -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)); 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) { void DaemonRpc::onResponse(QNetworkReply *reply, Endpoint endpoint) {
const auto ok = reply->error() == QNetworkReply::NoError; const auto ok = reply->error() == QNetworkReply::NoError;
const auto err = reply->errorString(); const auto err = reply->errorString();

View file

@ -13,7 +13,8 @@ class DaemonRpc : public QObject {
public: public:
enum Endpoint { enum Endpoint {
SEND_RAW_TRANSACTION = 0 SEND_RAW_TRANSACTION = 0,
GET_TRANSACTIONS
}; };
struct DaemonResponse { struct DaemonResponse {
@ -29,6 +30,7 @@ public:
explicit DaemonRpc(QObject *parent, UtilsNetworking *network, QString daemonAddress); 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 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 setDaemonAddress(const QString &daemonAddress);
void setNetwork(UtilsNetworking *network); void setNetwork(UtilsNetworking *network);