From 284a8641d9db9568b4850ffab201c061fa648ac3 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Tue, 23 Oct 2018 15:47:54 -0700 Subject: [PATCH] Add sent and received shielded Txs in the transactions tab. --- src/mainwindow.h | 1 + src/precompiled.h | 2 + src/rpc.cpp | 86 ++++++++++++++++++++++++++++++++----------- src/rpc.h | 16 +++++++- src/sendtab.cpp | 7 ++-- src/senttxstore.cpp | 86 +++++++++++++++++++++++++++++++++++++++++++ src/senttxstore.h | 18 +++++++++ src/transactionitem.h | 15 -------- src/txtablemodel.cpp | 62 ++++++++++++++++++++----------- src/txtablemodel.h | 18 ++++++--- src/utils.h | 5 ++- zcash-qt-wallet.pro | 3 +- 12 files changed, 247 insertions(+), 72 deletions(-) create mode 100644 src/senttxstore.cpp create mode 100644 src/senttxstore.h delete mode 100644 src/transactionitem.h diff --git a/src/mainwindow.h b/src/mainwindow.h index a0f29c2..9ef337c 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -21,6 +21,7 @@ struct ToFields { struct Tx { QString fromAddr; QList toAddrs; + double fee; }; namespace Ui { diff --git a/src/precompiled.h b/src/precompiled.h index b63568a..c3e837b 100644 --- a/src/precompiled.h +++ b/src/precompiled.h @@ -35,6 +35,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/src/rpc.cpp b/src/rpc.cpp index 9c135fc..1f94ba2 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -1,7 +1,7 @@ #include "rpc.h" #include "utils.h" -#include "transactionitem.h" #include "settings.h" +#include "senttxstore.h" using json = nlohmann::json; @@ -35,7 +35,7 @@ RPC::RPC(QNetworkAccessManager* client, MainWindow* main) { // Set up the timer to watch for tx status txTimer = new QTimer(main); QObject::connect(txTimer, &QTimer::timeout, [=]() { - refreshTxStatus(); + watchTxStatus(); }); // Start at every 10s. When an operation is pending, this will change to every second txTimer->start(Utils::updateSpeed); @@ -45,7 +45,7 @@ RPC::RPC(QNetworkAccessManager* client, MainWindow* main) { QObject::connect(priceTimer, &QTimer::timeout, [=]() { refreshZECPrice(); }); - priceTimer->start(60 * 60 * 1000); // Every hour + priceTimer->start(Utils::priceRefreshSpeed); // Every hour } RPC::~RPC() { @@ -389,7 +389,7 @@ void RPC::getReceivedZTrans(QList zaddrs) { } } - transactionsTableModel->addNewData(txdata); + transactionsTableModel->addZRecvData(txdata); // Cleanup both responses; delete zaddrTxids; @@ -417,7 +417,7 @@ void RPC::getInfoThenRefresh() { doRPC(payload, [=] (const json& reply) { // Testnet? - if (reply.find("testnet") != reply.end()) { + if (!reply["testnet"].is_null()) { Settings::getInstance()->setTestnet(reply["testnet"].get()); }; @@ -425,13 +425,11 @@ void RPC::getInfoThenRefresh() { QIcon i(":/icons/res/connected.png"); main->statusIcon->setPixmap(i.pixmap(16, 16)); - // Expect 2 data additions, then automatically refresh the table - transactionsTableModel->prepNewData(2); - // Refresh everything. refreshBalances(); refreshAddresses(); refreshTransactions(); + refreshZSentTransactions(); // Call to see if the blockchain is syncing. json payload = { @@ -559,14 +557,14 @@ void RPC::refreshTransactions() { for (auto& it : reply.get()) { double fee = 0; - if (it.find("fee") != it.end()) { + if (!it["fee"].is_null()) { fee = it["fee"].get(); } TransactionItem tx{ QString::fromStdString(it["category"]), it["time"].get(), - (it["address"].is_null() ? "(shielded)" : QString::fromStdString(it["address"])), + (it["address"].is_null() ? "" : QString::fromStdString(it["address"])), QString::fromStdString(it["txid"]), it["amount"].get() + fee, it["confirmations"].get() @@ -576,15 +574,55 @@ void RPC::refreshTransactions() { } // Update model data, which updates the table view - transactionsTableModel->addNewData(txdata); + transactionsTableModel->addTData(txdata); }); } -void RPC::refreshTxStatus(const QString& newOpid) { - if (!newOpid.isEmpty()) { - watchingOps.insert(newOpid); +// Read sent Z transactions from the file. +void RPC::refreshZSentTransactions() { + auto sentZTxs = SentTxStore::readSentTxFile(); + QList txids; + + for (auto sentTx: sentZTxs) { + txids.push_back(sentTx.txid); } + // Look up all the txids to get the confirmation count for them. + getBatchRPC(txids, + [=] (QString txid) { + json payload = { + {"jsonrpc", "1.0"}, + {"id", "senttxid"}, + {"method", "gettransaction"}, + {"params", {txid.toStdString()}} + }; + + return payload; + }, + [=] (QMap* txidList) { + auto newSentZTxs = sentZTxs; + // Update the original sent list with the confirmation count + // TODO: This whole thing is kinda inefficient. We should probably just update the file + // with the confirmed block number, so we don't have to keep calling gettransaction for the + // sent items. + for (TransactionItem& sentTx: newSentZTxs) { + auto error = txidList->value(sentTx.txid)["confirmations"].is_null(); + if (!error) + sentTx.confirmations = txidList->value(sentTx.txid)["confirmations"].get(); + } + + transactionsTableModel->addZSentData(newSentZTxs); + } + ); +} + +void RPC::addNewTxToWatch(Tx tx, const QString& newOpid) { + watchingOps.insert(newOpid, tx); + + watchTxStatus(); +} + +void RPC::watchTxStatus() { // Make an RPC to load pending operation statues json payload = { {"jsonrpc", "1.0"}, @@ -602,12 +640,13 @@ void RPC::refreshTxStatus(const QString& newOpid) { QString status = QString::fromStdString(it["status"]); if (status == "success") { auto txid = QString::fromStdString(it["result"]["txid"]); - qDebug() << "Tx completed: " << txid; + + SentTxStore::addToSentTx(watchingOps.value(id), txid); + main->ui->statusBar->showMessage(Utils::txidStatusMessage + " " + txid); main->loadingLabel->setVisible(false); watchingOps.remove(id); - txTimer->start(Utils::updateSpeed); // Refresh balances to show unconfirmed balances refresh(); @@ -622,17 +661,19 @@ void RPC::refreshTxStatus(const QString& newOpid) { main ); - watchingOps.remove(id); - txTimer->start(Utils::updateSpeed); + watchingOps.remove(id); main->ui->statusBar->showMessage(" Tx " % id % " failed", 15 * 1000); main->loadingLabel->setVisible(false); msg.exec(); - } else if (status == "executing") { - // If the operation is executing, then watch every second. - txTimer->start(Utils::quickUpdateSpeed); - } + } + } + + if (watchingOps.isEmpty()) { + txTimer->start(Utils::updateSpeed); + } else { + txTimer->start(Utils::quickUpdateSpeed); } } @@ -646,6 +687,7 @@ void RPC::refreshTxStatus(const QString& newOpid) { }); } +// Get the ZEC->USD price from coinmarketcap using their API void RPC::refreshZECPrice() { QUrl cmcURL("https://api.coinmarketcap.com/v1/ticker/"); diff --git a/src/rpc.h b/src/rpc.h index 22b5b0c..1acf0d6 100644 --- a/src/rpc.h +++ b/src/rpc.h @@ -11,6 +11,16 @@ using json = nlohmann::json; + +struct TransactionItem { + QString type; + unsigned long datetime; + QString address; + QString txid; + double amount; + unsigned long confirmations; +}; + class RPC { public: @@ -18,11 +28,12 @@ public: ~RPC(); void refresh(); // Refresh all transactions - void refreshTxStatus(const QString& newOpid = QString()); // Refresh the status of all pending txs. void refreshAddresses(); // Refresh wallet Z-addrs void refreshZECPrice(); void sendZTransaction (json params, const std::function& cb); + void watchTxStatus(); + void addNewTxToWatch(Tx tx, const QString& newOpid); BalancesTableModel* getBalancesModel() { return balancesTableModel; } const QList* getAllZAddresses() { return zaddresses; } @@ -40,6 +51,7 @@ private: void refreshBalances(); void refreshTransactions(); + void refreshZSentTransactions(); bool processUnspent (const json& reply); void updateUI (bool anyUnconfirmed); @@ -69,7 +81,7 @@ private: QMap* allBalances = nullptr; QList* zaddresses = nullptr; - QSet watchingOps; + QMap watchingOps; TxTableModel* transactionsTableModel = nullptr; BalancesTableModel* balancesTableModel = nullptr; diff --git a/src/sendtab.cpp b/src/sendtab.cpp index cbf9f1d..5a3c623 100644 --- a/src/sendtab.cpp +++ b/src/sendtab.cpp @@ -319,6 +319,7 @@ Tx MainWindow::createTxFromSendPage() { tx.toAddrs.push_back( ToFields{addr, amt, memo, memo.toUtf8().toHex()} ); } + tx.fee = Utils::getMinerFee(); return tx; } @@ -416,7 +417,7 @@ bool MainWindow::confirmTx(Tx tx, ToFields devFee) { minerFee->setObjectName(QStringLiteral("minerFee")); minerFee->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); confirm.gridLayout->addWidget(minerFee, i, 1, 1, 1); - minerFee->setText(Settings::getInstance()->getZECDisplayFormat(Utils::getMinerFee())); + minerFee->setText(Settings::getInstance()->getZECDisplayFormat(tx.fee)); auto minerFeeUSD = new QLabel(confirm.sendToAddrs); QSizePolicy sizePolicy1(QSizePolicy::Minimum, QSizePolicy::Preferred); @@ -424,7 +425,7 @@ bool MainWindow::confirmTx(Tx tx, ToFields devFee) { minerFeeUSD->setObjectName(QStringLiteral("minerFeeUSD")); minerFeeUSD->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); confirm.gridLayout->addWidget(minerFeeUSD, i, 2, 1, 1); - minerFeeUSD->setText(Settings::getInstance()->getUSDFormat(Utils::getMinerFee())); + minerFeeUSD->setText(Settings::getInstance()->getUSDFormat(tx.fee)); if (!devFee.addr.isEmpty()) { auto labelDevFee = new QLabel(confirm.sendToAddrs); @@ -517,7 +518,7 @@ void MainWindow::sendButton() { ui->statusBar->showMessage("Computing Tx: " % opid); // And then start monitoring the transaction - rpc->refreshTxStatus(opid); + rpc->addNewTxToWatch(tx, opid); }); } } diff --git a/src/senttxstore.cpp b/src/senttxstore.cpp new file mode 100644 index 0000000..711b33b --- /dev/null +++ b/src/senttxstore.cpp @@ -0,0 +1,86 @@ +#include "senttxstore.h" +#include "settings.h" + +/// Get the location of the app data file to be written. +QString SentTxStore::writeableFile() { + auto filename = QStringLiteral("senttxstore.dat"); + + auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + if (!dir.exists()) + QDir().mkpath(dir.absolutePath()); + + if (Settings::getInstance()->isTestnet()) { + return dir.filePath("testnet-" % filename); + } else { + return dir.filePath(filename); + } +} + +QList SentTxStore::readSentTxFile() { + QFile data(writeableFile()); + if (!data.exists()) { + return QList(); + } + + QJsonDocument jsonDoc; + + data.open(QFile::ReadOnly); + jsonDoc = QJsonDocument().fromJson(data.readAll()); + data.close(); + + QList items; + + for (auto i : jsonDoc.array()) { + auto sentTx = i.toObject(); + TransactionItem t{"sent", (unsigned long)sentTx["datetime"].toInt(), + sentTx["address"].toString(), + sentTx["txid"].toString(), sentTx["amount"].toDouble(), 0}; + items.push_back(t); + } + + return items; +} + +void SentTxStore::addToSentTx(Tx tx, QString txid) { + QFile data(writeableFile()); + QJsonDocument jsonDoc; + + // If data doesn't exist, then create a blank one + if (!data.exists()) { + QJsonArray a; + jsonDoc.setArray(a); + + QFile newFile(writeableFile()); + newFile.open(QFile::WriteOnly); + newFile.write(jsonDoc.toJson()); + newFile.close(); + } else { + data.open(QFile::ReadOnly); + jsonDoc = QJsonDocument().fromJson(data.readAll()); + data.close(); + } + + // Calculate total amount in this tx + double totalAmount = 0; + for (auto i : tx.toAddrs) { + totalAmount += i.amount; + } + + auto list = jsonDoc.array(); + QJsonObject txItem; + txItem["type"] = "sent"; + txItem["datetime"] = QDateTime().currentSecsSinceEpoch(); + txItem["address"] = QString(); // The sent address is blank, to be consistent with t-Addr sent behaviour + txItem["txid"] = txid; + txItem["amount"] = -totalAmount; + txItem["fee"] = -tx.fee; + list.append(txItem); + + jsonDoc.setArray(list); + + QFile writer(writeableFile()); + if (writer.open(QFile::WriteOnly | QFile::Truncate)) { + writer.write(jsonDoc.toJson()); + } + writer.close(); +} \ No newline at end of file diff --git a/src/senttxstore.h b/src/senttxstore.h new file mode 100644 index 0000000..f41f014 --- /dev/null +++ b/src/senttxstore.h @@ -0,0 +1,18 @@ +#ifndef SENTTXSTORE_H +#define SENTTXSTORE_H + +#include "precompiled.h" +#include "mainwindow.h" +#include "rpc.h" + +class SentTxStore { +public: + static QList readSentTxFile(); + static void addToSentTx(Tx tx, QString txid); + +private: + static QString writeableFile(); + +}; + +#endif // SENTTXSTORE_H diff --git a/src/transactionitem.h b/src/transactionitem.h deleted file mode 100644 index 48480c2..0000000 --- a/src/transactionitem.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef TRANSACTIONITEM_H -#define TRANSACTIONITEM_H - -#include "precompiled.h" - -struct TransactionItem { - QString type; - unsigned long datetime; - QString address; - QString txid; - double amount; - unsigned long confirmations; -}; - -#endif // TRANSACTIONITEM_H diff --git a/src/txtablemodel.cpp b/src/txtablemodel.cpp index 964c785..3472dbd 100644 --- a/src/txtablemodel.cpp +++ b/src/txtablemodel.cpp @@ -1,6 +1,7 @@ #include "txtablemodel.h" #include "settings.h" #include "utils.h" +#include "rpc.h" TxTableModel::TxTableModel(QObject *parent) : QAbstractTableModel(parent) { @@ -11,33 +12,46 @@ TxTableModel::~TxTableModel() { delete modeldata; } -void TxTableModel::prepNewData(int expect) { - newmodeldata = new QList(); - expectedData = expect; +void TxTableModel::addZSentData(const QList& data) { + delete zsTrans; + zsTrans = new QList(); + std::copy(data.begin(), data.end(), std::back_inserter(*zsTrans)); + + updateAllData(); } -void TxTableModel::addNewData(const QList& data) { - // Make sure we're expecting some data. - Q_ASSERT(expectedData > 0); +void TxTableModel::addZRecvData(const QList& data) { + delete zrTrans; + zrTrans = new QList(); + std::copy(data.begin(), data.end(), std::back_inserter(*zrTrans)); - // Add all - std::copy(data.begin(), data.end(), std::back_inserter(*newmodeldata)); - expectedData--; + updateAllData(); +} - if (expectedData == 0) { - delete modeldata; - modeldata = newmodeldata; - newmodeldata = nullptr; +void TxTableModel::addTData(const QList& data) { + delete tTrans; + tTrans = new QList(); + std::copy(data.begin(), data.end(), std::back_inserter(*tTrans)); - // Sort by reverse time - std::sort(modeldata->begin(), modeldata->end(), [=] (auto a, auto b) { - return a.datetime > b.datetime; // reverse sort - }); + updateAllData(); +} - dataChanged(index(0, 0), index(modeldata->size()-1, columnCount(index(0,0))-1)); - layoutChanged(); - } +void TxTableModel::updateAllData() { + delete modeldata; + modeldata = new QList(); + + if (tTrans != nullptr) std::copy( tTrans->begin(), tTrans->end(), std::back_inserter(*modeldata)); + if (zsTrans != nullptr) std::copy(zsTrans->begin(), zsTrans->end(), std::back_inserter(*modeldata)); + if (zrTrans != nullptr) std::copy(zrTrans->begin(), zrTrans->end(), std::back_inserter(*modeldata)); + + // Sort by reverse time + std::sort(modeldata->begin(), modeldata->end(), [=] (auto a, auto b) { + return a.datetime > b.datetime; // reverse sort + }); + + dataChanged(index(0, 0), index(modeldata->size()-1, columnCount(index(0,0))-1)); + layoutChanged(); } int TxTableModel::rowCount(const QModelIndex&) const @@ -73,7 +87,13 @@ void TxTableModel::addNewData(const QList& data) { if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { switch (index.column()) { case 0: return modeldata->at(index.row()).type; - case 1: return modeldata->at(index.row()).address; + case 1: { + auto addr = modeldata->at(index.row()).address; + if (addr.trimmed().isEmpty()) + return "(Shielded)"; + else + return addr; + } case 2: return QDateTime::fromSecsSinceEpoch(modeldata->at(index.row()).datetime).toLocalTime().toString(); case 3: { if (role == Qt::DisplayRole) diff --git a/src/txtablemodel.h b/src/txtablemodel.h index 03bfd78..b03e7a2 100644 --- a/src/txtablemodel.h +++ b/src/txtablemodel.h @@ -1,17 +1,19 @@ #ifndef STRINGSTABLEMODEL_H #define STRINGSTABLEMODEL_H -#include "transactionitem.h" #include "precompiled.h" +struct TransactionItem; + class TxTableModel: public QAbstractTableModel { public: TxTableModel(QObject* parent); ~TxTableModel(); - void prepNewData (int expectedData); - void addNewData (const QList& data); + void addTData (const QList& data); + void addZSentData(const QList& data); + void addZRecvData(const QList& data); QString getTxId(int row); @@ -21,9 +23,13 @@ public: QVariant headerData(int section, Qt::Orientation orientation, int role) const; private: - QList* modeldata = nullptr; - QList* newmodeldata = nullptr; - int expectedData; + void updateAllData(); + + QList* tTrans = nullptr; + QList* zrTrans = nullptr; // Z received + QList* zsTrans = nullptr; // Z sent + + QList* modeldata = nullptr; QList headers; }; diff --git a/src/utils.h b/src/utils.h index e11a1fe..c13b3e0 100644 --- a/src/utils.h +++ b/src/utils.h @@ -18,8 +18,9 @@ public: static double getDevFee(); static double getTotalFee(); - static const int updateSpeed = 20 * 1000; // 20 sec - static const int quickUpdateSpeed = 5 * 1000; // 5 sec + static const int updateSpeed = 20 * 1000; // 20 sec + static const int quickUpdateSpeed = 5 * 1000; // 5 sec + static const int priceRefreshSpeed = 60 * 60 * 1000; // 1 hr private: Utils() = delete; }; diff --git a/zcash-qt-wallet.pro b/zcash-qt-wallet.pro index 522771b..2a9bf3b 100644 --- a/zcash-qt-wallet.pro +++ b/zcash-qt-wallet.pro @@ -48,6 +48,7 @@ SOURCES += \ src/3rdparty/qrcode/QrSegment.cpp \ src/settings.cpp \ src/sendtab.cpp \ + src/senttxstore.cpp \ src/txtablemodel.cpp \ src/utils.cpp @@ -63,7 +64,7 @@ HEADERS += \ src/3rdparty/json/json.hpp \ src/settings.h \ src/txtablemodel.h \ - src/transactionitem.h \ + src/senttxstore.h \ src/utils.h FORMS += \