From 53cbea1fdaf2280e5c6149ee8c91e177fda47765 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Wed, 16 Oct 2019 13:12:32 -0700 Subject: [PATCH] Fetch balances + fix gui thread issues --- src/balancestablemodel.cpp | 6 ++-- src/balancestablemodel.h | 4 +-- src/connection.cpp | 60 ++++++++++++++++++++++++++------------ src/connection.h | 38 +++++++++++++++++++++--- src/controller.cpp | 55 +++++++++++++++++----------------- src/controller.h | 2 +- src/datamodel.cpp | 4 +-- src/datamodel.h | 8 ++--- src/liteinterface.cpp | 20 +++---------- src/liteinterface.h | 5 ++-- 10 files changed, 121 insertions(+), 81 deletions(-) diff --git a/src/balancestablemodel.cpp b/src/balancestablemodel.cpp index 12bcf09..f295ca8 100644 --- a/src/balancestablemodel.cpp +++ b/src/balancestablemodel.cpp @@ -7,7 +7,7 @@ BalancesTableModel::BalancesTableModel(QObject *parent) : QAbstractTableModel(parent) { } -void BalancesTableModel::setNewData(const QMap balances, +void BalancesTableModel::setNewData(const QMap balances, const QList outputs) { loading = false; @@ -22,7 +22,7 @@ void BalancesTableModel::setNewData(const QMap balances, // Process the address balances into a list delete modeldata; - modeldata = new QList>(); + modeldata = new QList>(); std::for_each(balances.keyBegin(), balances.keyEnd(), [=] (auto keyIt) { if (balances.value(keyIt) > 0) modeldata->push_back(std::make_tuple(keyIt, balances.value(keyIt))); @@ -72,7 +72,7 @@ QVariant BalancesTableModel::data(const QModelIndex &index, int role) const // If any of the UTXOs for this address has zero confirmations, paint it in red const auto& addr = std::get<0>(modeldata->at(index.row())); for (auto utxo : *utxos) { - if (utxo.address == addr && utxo.confirmations == 0) { + if (utxo.address == addr && !utxo.spendable) { QBrush b; b.setColor(Qt::red); return b; diff --git a/src/balancestablemodel.h b/src/balancestablemodel.h index 45e2c52..3c41fca 100644 --- a/src/balancestablemodel.h +++ b/src/balancestablemodel.h @@ -10,7 +10,7 @@ public: BalancesTableModel(QObject* parent); ~BalancesTableModel(); - void setNewData(const QMap balances, const QList outputs); + void setNewData(const QMap balances, const QList outputs); int rowCount(const QModelIndex &parent) const; int columnCount(const QModelIndex &parent) const; @@ -18,7 +18,7 @@ public: QVariant headerData(int section, Qt::Orientation orientation, int role) const; private: - QList>* modeldata = nullptr; + QList>* modeldata = nullptr; QList* utxos = nullptr; bool loading = true; diff --git a/src/connection.cpp b/src/connection.cpp index 2f5ea90..14bdbe9 100644 --- a/src/connection.cpp +++ b/src/connection.cpp @@ -33,7 +33,7 @@ void ConnectionLoader::loadConnection() { d->exec(); } -void ConnectionLoader::doAutoConnect(bool tryEzcashdStart) { +void ConnectionLoader::doAutoConnect() { qDebug() << "Doing autoconnect"; auto config = std::shared_ptr(new ConnectionConfig()); @@ -51,7 +51,7 @@ void ConnectionLoader::doAutoConnect(bool tryEzcashdStart) { // If success, set the connection main->logger->write("Connection is online."); this->doRPCSetConnection(connection); - }, [=](auto err, auto errJson) {}); + }, [=](auto err) {}); } void ConnectionLoader::doRPCSetConnection(Connection* conn) { @@ -96,6 +96,9 @@ void ConnectionLoader::showError(QString explanation) { +/*********************************************************************************** + * Connection, Executor and Callback Class + ************************************************************************************/ void Executor::run() { char* resp = litelib_execute(this->cmd.toStdString().c_str()); @@ -111,50 +114,71 @@ void Executor::run() { qDebug() << "Reply=" << reply; auto parsed = json::parse(reply.toStdString().c_str(), nullptr, false); + const bool isGuiThread = + QThread::currentThread() == QCoreApplication::instance()->thread(); + qDebug() << "executing RPC: isGUI=" << isGuiThread; + emit responseReady(parsed); } +void Callback::processRPCCallback(json resp) { + const bool isGuiThread = QThread::currentThread() == QCoreApplication::instance()->thread(); + qDebug() << "Doing RPC callback: isGUI=" << isGuiThread; + this->cb(resp); + + // Destroy self + delete this; +} + +void Callback::processError(QString resp) { + const bool isGuiThread = QThread::currentThread() == QCoreApplication::instance()->thread(); + qDebug() << "Doing RPC callback: isGUI=" << isGuiThread; + this->errCb(resp); + + // Destroy self + delete this; +} -/*********************************************************************************** - * Connection Class - ************************************************************************************/ Connection::Connection(MainWindow* m, std::shared_ptr conf) { this->config = conf; this->main = m; -} -Connection::~Connection() { + // Register the JSON type as a type that can be passed between signals and slots. + qRegisterMetaType("json"); } void Connection::doRPC(const QString cmd, const QString args, const std::function& cb, - const std::function& ne) { + const std::function& errCb) { if (shutdownInProgress) { // Ignoring RPC because shutdown in progress return; } + const bool isGuiThread = + QThread::currentThread() == QCoreApplication::instance()->thread(); + qDebug() << "Doing RPC: isGUI=" << isGuiThread; + // Create a runner. auto runner = new Executor(cmd, args); - QObject::connect(runner, &Executor::responseReady, [=] (json resp) { - cb(resp); - }); + + // Callback object. Will delete itself + auto c = new Callback(cb, errCb); + + QObject::connect(runner, &Executor::responseReady, c, &Callback::processRPCCallback); + QObject::connect(runner, &Executor::handleError, c, &Callback::processError); QThreadPool::globalInstance()->start(runner); } void Connection::doRPCWithDefaultErrorHandling(const QString cmd, const QString args, const std::function& cb) { - doRPC(cmd, args, cb, [=] (auto reply, auto parsed) { - if (!parsed.is_discarded() && !parsed["error"]["message"].is_null()) { - this->showTxError(QString::fromStdString(parsed["error"]["message"])); - } else { - this->showTxError(reply->errorString()); - } + doRPC(cmd, args, cb, [=] (QString err) { + this->showTxError(err); }); } void Connection::doRPCIgnoreError(const QString cmd, const QString args, const std::function& cb) { - doRPC(cmd, args, cb, [=] (auto, auto) { + doRPC(cmd, args, cb, [=] (auto) { // Ignored error handling }); } diff --git a/src/connection.h b/src/connection.h index 8ef02fa..ec865b7 100644 --- a/src/connection.h +++ b/src/connection.h @@ -31,7 +31,7 @@ private: Connection* makeConnection(std::shared_ptr config); - void doAutoConnect(bool tryEzcashdStart = true); + void doAutoConnect(); void showError(QString explanation); void showInformation(QString info, QString detail = ""); @@ -45,6 +45,32 @@ private: Controller* rpc; }; +/** + * An object that will call the callback function in the GUI thread, and destroy itself after the callback is finished + */ +class Callback: public QObject { + Q_OBJECT +public: + Callback(const std::function cb, const std::function errCb) { this->cb = cb; this->errCb = errCb;} + ~Callback() = default; + +public slots: + void processRPCCallback(json resp); + void processError(QString error); + +private: + std::function cb; + std::function errCb; + +}; + +/** + * A runnable that runs some lightclient Command in a non-UI thread. + * It emits the "responseReady" signal, which should be processed in a GUI thread. + * + * Since the autoDelete flag is ON, the runnable should be destroyed automatically + * by the threadpool. + */ class Executor : public QObject, public QRunnable { Q_OBJECT @@ -60,7 +86,8 @@ public: virtual void run(); signals: - void responseReady(json); + void responseReady(json); + void handleError(QString); private: QString cmd; @@ -72,17 +99,20 @@ private: * This is also a UI class, so it may show a dialog waiting for the connection. */ class Connection : public QObject { +Q_OBJECT + public: Connection(MainWindow* m, std::shared_ptr conf); - ~Connection(); + ~Connection() = default; std::shared_ptr config; MainWindow* main; void shutdown(); + void doRPC(const QString cmd, const QString args, const std::function& cb, - const std::function& ne); + const std::function& errCb); void doRPCWithDefaultErrorHandling(const QString cmd, const QString args, const std::function& cb); void doRPCIgnoreError(const QString cmd, const QString args, const std::function& cb) ; diff --git a/src/controller.cpp b/src/controller.cpp index 0e5272b..80e8efc 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -141,7 +141,7 @@ void Controller::noConnection() { main->ui->statusBar->showMessage(QObject::tr("No Connection"), 1000); // Clear balances table. - QMap emptyBalances; + QMap emptyBalances; QList emptyOutputs; balancesTableModel->setNewData(emptyBalances, emptyOutputs); @@ -295,7 +295,7 @@ void Controller::getInfoThenRefresh(bool force) { main->statusIcon->setToolTip(tooltip); }); - }, [=](QNetworkReply* reply, const json&) { + }, [=](QString err) { // zcashd has probably disappeared. this->noConnection(); @@ -304,7 +304,7 @@ void Controller::getInfoThenRefresh(bool force) { if (!shown && prevCallSucceeded) { // show error only first time shown = true; QMessageBox::critical(main, QObject::tr("Connection Error"), QObject::tr("There was an error connecting to zcashd. The error was") + ": \n\n" - + reply->errorString(), QMessageBox::StandardButton::Ok); + + err, QMessageBox::StandardButton::Ok); shown = false; } @@ -356,22 +356,25 @@ void Controller::updateUI(bool anyUnconfirmed) { }; // Function to process reply of the listunspent and z_listunspent API calls, used below. -bool Controller::processUnspent(const json& reply, QMap* balancesMap, QList* newUtxos) { +bool Controller::processUnspent(const json& reply, QMap* balancesMap, QList* newUtxos) { bool anyUnconfirmed = false; - for (auto& it : reply.get()) { - QString qsAddr = QString::fromStdString(it["address"]); - auto confirmations = it["confirmations"].get(); - if (confirmations == 0) { - anyUnconfirmed = true; - } - newUtxos->push_back( - UnspentOutput{ qsAddr, QString::fromStdString(it["txid"]), - Settings::getDecimalString(it["amount"].get()), - (int)confirmations, it["spendable"].get() }); + auto processFn = [=](const json& array) { + for (auto& it : array) { + QString qsAddr = QString::fromStdString(it["address"]); + int block = it["created_in_block"].get(); + QString txid = QString::fromStdString(it["created_in_txid"]); + QString amount = Settings::getDecimalString(it["value"].get()); + + newUtxos->push_back(UnspentOutput{ qsAddr, txid, amount, block, true }); + + (*balancesMap)[qsAddr] = (*balancesMap)[qsAddr] + it["value"].get(); + } + }; + + processFn(reply["unspent_notes"].get()); + processFn(reply["utxos"].get()); - (*balancesMap)[qsAddr] = (*balancesMap)[qsAddr] + it["amount"].get(); - } return anyUnconfirmed; }; @@ -423,23 +426,19 @@ void Controller::refreshBalances() { // 2. Get the UTXOs // First, create a new UTXO list. It will be replacing the existing list when everything is processed. auto newUtxos = new QList(); - auto newBalances = new QMap(); + auto newBalances = new QMap(); // Call the Transparent and Z unspent APIs serially and then, once they're done, update the UI - zrpc->fetchTransparentUnspent([=] (json reply) { - auto anyTUnconfirmed = processUnspent(reply, newBalances, newUtxos); - - zrpc->fetchZUnspent([=] (json reply) { - auto anyZUnconfirmed = processUnspent(reply, newBalances, newUtxos); + zrpc->fetchUnspent([=] (json reply) { + auto anyUnconfirmed = processUnspent(reply, newBalances, newUtxos); - // Swap out the balances and UTXOs - model->replaceBalances(newBalances); - model->replaceUTXOs(newUtxos); + // Swap out the balances and UTXOs + model->replaceBalances(newBalances); + model->replaceUTXOs(newUtxos); - updateUI(anyTUnconfirmed || anyZUnconfirmed); + updateUI(anyUnconfirmed); - main->balancesReady(); - }); + main->balancesReady(); }); } diff --git a/src/controller.h b/src/controller.h index 4e921ce..6de3c51 100644 --- a/src/controller.h +++ b/src/controller.h @@ -93,7 +93,7 @@ private: void refreshSentZTrans(); void refreshReceivedZTrans(QList zaddresses); - bool processUnspent (const json& reply, QMap* newBalances, QList* newUtxos); + bool processUnspent (const json& reply, QMap* newBalances, QList* newUtxos); void updateUI (bool anyUnconfirmed); void getInfoThenRefresh(bool force); diff --git a/src/datamodel.cpp b/src/datamodel.cpp index 975ad8d..5004a7d 100644 --- a/src/datamodel.cpp +++ b/src/datamodel.cpp @@ -7,7 +7,7 @@ DataModel::DataModel() { QWriteLocker locker(lock); utxos = new QList(); - balances = new QMap(); + balances = new QMap(); usedAddresses = new QMap(); zaddresses = new QList(); taddresses = new QList(); @@ -40,7 +40,7 @@ void DataModel::replaceTaddresses(QList* newT) { taddresses = newT; } -void DataModel::replaceBalances(QMap* newBalances) { +void DataModel::replaceBalances(QMap* newBalances) { QWriteLocker locker(lock); Q_ASSERT(newBalances); diff --git a/src/datamodel.h b/src/datamodel.h index bd91b74..edca668 100644 --- a/src/datamodel.h +++ b/src/datamodel.h @@ -8,7 +8,7 @@ struct UnspentOutput { QString address; QString txid; QString amount; - int confirmations; + int blockCreated; bool spendable; }; @@ -18,7 +18,7 @@ class DataModel { public: void replaceZaddresses(QList* newZ); void replaceTaddresses(QList* newZ); - void replaceBalances(QMap* newBalances); + void replaceBalances(QMap* newBalances); void replaceUTXOs(QList* utxos); void markAddressUsed(QString address); @@ -26,7 +26,7 @@ public: const QList getAllZAddresses() { QReadLocker locker(lock); return *zaddresses; } const QList getAllTAddresses() { QReadLocker locker(lock); return *taddresses; } const QList getUTXOs() { QReadLocker locker(lock); return *utxos; } - const QMap getAllBalances() { QReadLocker locker(lock); return *balances; } + const QMap getAllBalances() { QReadLocker locker(lock); return *balances; } const QMap getUsedAddresses() { QReadLocker locker(lock); return *usedAddresses; } @@ -36,7 +36,7 @@ private: QList* utxos = nullptr; - QMap* balances = nullptr; + QMap* balances = nullptr; QMap* usedAddresses = nullptr; QList* zaddresses = nullptr; QList* taddresses = nullptr; diff --git a/src/liteinterface.cpp b/src/liteinterface.cpp index dc90678..82bd0c6 100644 --- a/src/liteinterface.cpp +++ b/src/liteinterface.cpp @@ -47,28 +47,16 @@ void LiteInterface::fetchZAddresses(const std::function& cb) { // conn->doRPCWithDefaultErrorHandling(payload, cb); } -void LiteInterface::fetchTransparentUnspent(const std::function& cb) { +void LiteInterface::fetchUnspent(const std::function& cb) { if (conn == nullptr) return; - // json payload = { - // {"jsonrpc", "1.0"}, - // {"id", "someid"}, - // {"method", "listunspent"}, - // {"params", {0}} // Get UTXOs with 0 confirmations as well. - // }; - - // conn->doRPCWithDefaultErrorHandling(payload, cb); -} - -void LiteInterface::fetchZUnspent(const std::function& cb) { - if (conn == nullptr) - return; + conn->doRPCWithDefaultErrorHandling("notes", "", cb); // json payload = { // {"jsonrpc", "1.0"}, // {"id", "someid"}, - // {"method", "z_listunspent"}, + // {"method", "listunspent"}, // {"params", {0}} // Get UTXOs with 0 confirmations as well. // }; @@ -217,7 +205,7 @@ void LiteInterface::sendZTransaction(json params, const std::function& cb, - const std::function& err) { + const std::function& err) { if (conn == nullptr) return; diff --git a/src/liteinterface.h b/src/liteinterface.h index be1b1c4..1e8b243 100644 --- a/src/liteinterface.h +++ b/src/liteinterface.h @@ -28,8 +28,7 @@ public: void setConnection(Connection* c); Connection* getConnection() { return conn; } - void fetchTransparentUnspent (const std::function& cb); - void fetchZUnspent (const std::function& cb); + void fetchUnspent (const std::function& cb); void fetchTransactions (const std::function& cb); void fetchZAddresses (const std::function& cb); void fetchTAddresses (const std::function& cb); @@ -40,7 +39,7 @@ public: const std::function)> txdataFn); void fetchInfo(const std::function& cb, - const std::function& err); + const std::function& err); void fetchBlockchainInfo(const std::function& cb); void fetchNetSolOps(const std::function cb); void fetchOpStatus(const std::function& cb);