#include "rpc.h" #include "utils.h" #include "transactionitem.h" #include "settings.h" using json = nlohmann::json; RPC::RPC(QNetworkAccessManager* client, MainWindow* main) { this->restclient = client; this->main = main; this->ui = main->ui; // Setup balances table model balancesTableModel = new BalancesTableModel(main->ui->balancesTable); main->ui->balancesTable->setModel(balancesTableModel); main->ui->balancesTable->setColumnWidth(0, 300); // Setup transactions table model transactionsTableModel = new TxTableModel(ui->transactionsTable); main->ui->transactionsTable->setModel(transactionsTableModel); main->ui->transactionsTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); main->ui->transactionsTable->setColumnWidth(1, 350); main->ui->transactionsTable->setColumnWidth(2, 200); main->ui->transactionsTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); reloadConnectionInfo(); // Set up a timer to refresh the UI every few seconds timer = new QTimer(main); QObject::connect(timer, &QTimer::timeout, [=]() { refresh(); }); timer->start(Utils::updateSpeed); // Set up the timer to watch for tx status txTimer = new QTimer(main); QObject::connect(txTimer, &QTimer::timeout, [=]() { refreshTxStatus(); }); // Start at every 10s. When an operation is pending, this will change to every second txTimer->start(Utils::updateSpeed); // Set up timer to refresh Price priceTimer = new QTimer(main); QObject::connect(priceTimer, &QTimer::timeout, [=]() { refreshZECPrice(); }); priceTimer->start(60 * 60 * 1000); // Every hour } RPC::~RPC() { delete timer; delete txTimer; delete transactionsTableModel; delete balancesTableModel; delete utxos; delete allBalances; delete zaddresses; delete restclient; } void RPC::reloadConnectionInfo() { // Reset for any errors caused. firstTime = true; QUrl myurl; myurl.setScheme("http"); //https also applicable myurl.setHost(Settings::getInstance()->getHost()); myurl.setPort(Settings::getInstance()->getPort().toInt()); request.setUrl(myurl); request.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); QString headerData = "Basic " + Settings::getInstance()->getUsernamePassword().toLocal8Bit().toBase64(); request.setRawHeader("Authorization", headerData.toLocal8Bit()); } void RPC::doRPC(const json& payload, const std::function& cb) { QNetworkReply *reply = restclient->post(request, QByteArray::fromStdString(payload.dump())); QObject::connect(reply, &QNetworkReply::finished, [=] { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { auto parsed = json::parse(reply->readAll(), nullptr, false); if (!parsed.is_discarded() && !parsed["error"]["message"].is_null()) { handleConnectionError(QString::fromStdString(parsed["error"]["message"])); } else { handleConnectionError(reply->errorString()); } return; } auto parsed = json::parse(reply->readAll(), nullptr, false); if (parsed.is_discarded()) { handleConnectionError("Unknown error"); } cb(parsed["result"]); }); } void RPC::getZAddresses(const std::function& cb) { json payload = { {"jsonrpc", "1.0"}, {"id", "someid"}, {"method", "z_listaddresses"}, }; doRPC(payload, cb); } void RPC::getTransparentUnspent(const std::function& cb) { json payload = { {"jsonrpc", "1.0"}, {"id", "someid"}, {"method", "listunspent"}, {"params", {0}} // Get UTXOs with 0 confirmations as well. }; doRPC(payload, cb); } void RPC::getZUnspent(const std::function& cb) { json payload = { {"jsonrpc", "1.0"}, {"id", "someid"}, {"method", "z_listunspent"}, {"params", {0}} // Get UTXOs with 0 confirmations as well. }; doRPC(payload, cb); } void RPC::newZaddr(const std::function& cb) { json payload = { {"jsonrpc", "1.0"}, {"id", "someid"}, {"method", "z_getnewaddress"}, }; doRPC(payload, cb); } void RPC::newTaddr(const std::function& cb) { json payload = { {"jsonrpc", "1.0"}, {"id", "someid"}, {"method", "getnewaddress"}, }; doRPC(payload, cb); } void RPC::getBalance(const std::function& cb) { json payload = { {"jsonrpc", "1.0"}, {"id", "someid"}, {"method", "z_gettotalbalance"}, {"params", {0}} // Get Unconfirmed balance as well. }; doRPC(payload, cb); } void RPC::getTransactions(const std::function& cb) { json payload = { {"jsonrpc", "1.0"}, {"id", "someid"}, {"method", "listtransactions"} }; doRPC(payload, cb); } void RPC::doSendRPC(const json& payload, const std::function& cb) { QNetworkReply *reply = restclient->post(request, QByteArray::fromStdString(payload.dump())); QObject::connect(reply, &QNetworkReply::finished, [=] { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { auto parsed = json::parse(reply->readAll(), nullptr, false); if (!parsed.is_discarded() && !parsed["error"]["message"].is_null()) { handleTxError(QString::fromStdString(parsed["error"]["message"])); } else { handleTxError(reply->errorString()); } return; } auto parsed = json::parse(reply->readAll(), nullptr, false); if (parsed.is_discarded()) { handleTxError("Unknown error"); } cb(parsed["result"]); }); } void RPC::sendZTransaction(json params, const std::function& cb) { json payload = { {"jsonrpc", "1.0"}, {"id", "someid"}, {"method", "z_sendmany"}, {"params", params} }; doSendRPC(payload, cb); } void RPC::handleConnectionError(const QString& error) { if (error.isNull()) return; QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxCritical); main->statusIcon->setPixmap(icon.pixmap(16, 16)); main->statusLabel->setText("No Connection"); if (firstTime) { this->firstTime = false; QMessageBox msg(main); msg.setIcon(QMessageBox::Icon::Critical); msg.setWindowTitle("Connection Error"); QString explanation; if (error.contains("authentication", Qt::CaseInsensitive)) { explanation = QString() % "\n\nThis is most likely because of misconfigured rpcuser/rpcpassword. " % "zcashd needs the following options set in ~/.zcash/zcash.conf\n\n" % "rpcuser=\n" % "rpcpassword=\n" % "\nIf you're connecting to a remote note, you can change the username/password in the " % "File->Settings menu."; } else if (error.contains("connection refused", Qt::CaseInsensitive)) { auto confLocation = Settings::getInstance()->getZcashdConfLocation(); if (confLocation.isEmpty()) { explanation = QString() % "\n\nA zcash.conf was not found on this machine. If you are connecting to a remote/non-standard node " % "please set the host/port and user/password in the File->Settings menu."; } else { explanation = QString() % "\n\nA zcash.conf was found at\n" % confLocation % "\nbut we can't connect to zcashd. Is rpcuser= and rpcpassword= set in the zcash.conf file?"; } } else if (error.contains("internal server error", Qt::CaseInsensitive) || error.contains("rewinding", Qt::CaseInsensitive) || error.contains("loading", Qt::CaseInsensitive)) { explanation = QString() % "\n\nIf you just started zcashd, then it's still loading and you might have to wait a while. If zcashd is ready, then you've run into " % "something unexpected, and might need to file a bug report here: https://github.com/adityapk00/zcash-qt-wallet/issues"; } else { explanation = QString() % "\n\nThis is most likely an internal error. Something unexpected happened. " % "You might need to file a bug report here: https://github.com/adityapk00/zcash-qt-wallet/issues"; } msg.setText("There was a network connection error. The error was: \n\n" + error + explanation); msg.exec(); return; } } void RPC::handleTxError(const QString& error) { if (error.isNull()) return; QMessageBox msg(main); msg.setIcon(QMessageBox::Icon::Critical); msg.setWindowTitle("Transaction Error"); msg.setText("There was an error sending the transaction. The error was: \n\n" + error); msg.exec(); } /// This will refresh all the balance data from zcashd void RPC::refresh() { // First, test the connection to see if we can actually get info. getInfoThenRefresh(); } void RPC::getInfoThenRefresh() { json payload = { {"jsonrpc", "1.0"}, {"id", "someid"}, {"method", "getinfo"} }; doRPC(payload, [=] (const json& reply) { // Testnet? if (reply.find("testnet") != reply.end()) { Settings::getInstance()->setTestnet(reply["testnet"].get()); }; // Connected, so display checkmark. QIcon i(":/icons/res/connected.png"); main->statusIcon->setPixmap(i.pixmap(16, 16)); // Refresh everything. refreshBalances(); refreshTransactions(); refreshAddresses(); // Call to see if the blockchain is syncing. json payload = { {"jsonrpc", "1.0"}, {"id", "someid"}, {"method", "getblockchaininfo"} }; doRPC(payload, [=](const json& reply) { auto progress = reply["verificationprogress"].get(); QString statusText = QString() % (progress < 0.99 ? "Syncing" : "Connected") % " (" % (Settings::getInstance()->isTestnet() ? "testnet:" : "") % QString::number(reply["blocks"].get()) % (progress < 0.99 ? ("/" % QString::number(progress*100, 'f', 0) % "%") : QString()) % ")"; main->statusLabel->setText(statusText); auto zecPrice = Settings::getInstance()->getUSDFormat(1); if (!zecPrice.isEmpty()) { main->statusLabel->setToolTip("1 ZEC = " + zecPrice); main->statusIcon->setToolTip("1 ZEC = " + zecPrice); } }); }); } void RPC::refreshAddresses() { delete zaddresses; zaddresses = new QList(); getZAddresses([=] (json reply) { for (auto& it : reply.get()) { auto addr = QString::fromStdString(it.get()); zaddresses->push_back(addr); } }); } // Function to create the data model and update the views, used below. void RPC::updateUI(bool anyUnconfirmed) { ui->unconfirmedWarning->setVisible(anyUnconfirmed); // Update balances model data, which will update the table too balancesTableModel->setNewData(allBalances, utxos); // Add all the addresses into the inputs combo box auto lastFromAddr = ui->inputsCombo->currentText().split("(")[0].trimmed(); ui->inputsCombo->clear(); auto i = allBalances->constBegin(); while (i != allBalances->constEnd()) { QString item = i.key() % "(" % QString::number(i.value(), 'g', 8) % " " % Utils::getTokenName() % ")"; ui->inputsCombo->addItem(item); if (item.startsWith(lastFromAddr)) ui->inputsCombo->setCurrentText(item); ++i; } }; // Function to process reply of the listunspent and z_listunspent API calls, used below. bool RPC::processUnspent(const json& reply) { bool anyUnconfirmed = false; for (auto& it : reply.get()) { QString qsAddr = QString::fromStdString(it["address"]); auto confirmations = it["confirmations"].get(); if (confirmations == 0) { anyUnconfirmed = true; } utxos->push_back( UnspentOutput( qsAddr, QString::fromStdString(it["txid"]), QString::number(it["amount"].get(), 'g', 8), confirmations ) ); (*allBalances)[qsAddr] = (*allBalances)[qsAddr] + it["amount"].get(); } return anyUnconfirmed; }; void RPC::refreshBalances() { // 1. Get the Balances getBalance([=] (json reply) { ui->balSheilded ->setText(QString::fromStdString(reply["private"]) % " " % Utils::getTokenName()); ui->balTransparent ->setText(QString::fromStdString(reply["transparent"]) % " " % Utils::getTokenName()); ui->balTotal ->setText(QString::fromStdString(reply["total"]) % " " % Utils::getTokenName()); }); // 2. Get the UTXOs // First, create a new UTXO list, deleting the old one; delete utxos; utxos = new QList(); delete allBalances; allBalances = new QMap(); // Call the Transparent and Z unspent APIs serially and then, once they're done, update the UI getTransparentUnspent([=] (json reply) { auto anyTUnconfirmed = processUnspent(reply); getZUnspent([=] (json reply) { auto anyZUnconfirmed = processUnspent(reply); updateUI(anyTUnconfirmed || anyZUnconfirmed); }); }); } void RPC::refreshTransactions() { auto txdata = new QList(); /* auto getZReceivedTransactions = ([=] (const json& reply) { for (auto& it : reply.get()) { TransactionItem tx( QString("receive"), QDateTime::fromSecsSinceEpoch(it["time"].get()).toLocalTime().toString(), (it["address"].is_null() ? "" : QString::fromStdString(it["address"])), QString::fromStdString(it["txid"]), it["amount"].get(), it["confirmations"].get() ); txdata->push_front(tx); } }); */ getTransactions([=] (json reply) { for (auto& it : reply.get()) { TransactionItem tx( QString::fromStdString(it["category"]), QDateTime::fromSecsSinceEpoch(it["time"].get()).toLocalTime().toString(), (it["address"].is_null() ? "" : QString::fromStdString(it["address"])), QString::fromStdString(it["txid"]), it["amount"].get(), it["confirmations"].get() ); txdata->push_front(tx); } // Update model data, which updates the table view transactionsTableModel->setNewData(txdata); }); } void RPC::refreshTxStatus(const QString& newOpid) { if (!newOpid.isEmpty()) { watchingOps.insert(newOpid); } // Make an RPC to load pending operation statues json payload = { {"jsonrpc", "1.0"}, {"id", "someid"}, {"method", "z_getoperationstatus"}, }; doRPC(payload, [=] (const json& reply) { // There's an array for each item in the status for (auto& it : reply.get()) { // If we were watching this Tx and it's status became "success", then we'll show a status bar alert QString id = QString::fromStdString(it["id"]); if (watchingOps.contains(id)) { // And if it ended up successful QString status = QString::fromStdString(it["status"]); if (status == "success") { auto txid = QString::fromStdString(it["result"]["txid"]); qDebug() << "Tx completed: " << 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(); } else if (status == "failed") { // If it failed, then we'll actually show a warning. auto errorMsg = QString::fromStdString(it["error"]["message"]); QMessageBox msg( QMessageBox::Critical, "Transaction Error", "The transaction with id " % id % " failed. The error was:\n\n" % errorMsg, QMessageBox::Ok, main ); watchingOps.remove(id); txTimer->start(10 * 1000); 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(1 * 1000); } } } // If there is some op that we are watching, then show the loading bar, otherwise hide it if (watchingOps.empty()) { main->loadingLabel->setVisible(false); } else { main->loadingLabel->setVisible(true); main->loadingLabel->setToolTip(QString::number(watchingOps.size()) + " tx computing. This can take several minutes."); } }); } void RPC::refreshZECPrice() { QUrl cmcURL("https://api.coinmarketcap.com/v1/ticker/"); QNetworkRequest req; req.setUrl(cmcURL); QNetworkReply *reply = restclient->get(req); QObject::connect(reply, &QNetworkReply::finished, [=] { reply->deleteLater(); try { if (reply->error() != QNetworkReply::NoError) { auto parsed = json::parse(reply->readAll(), nullptr, false); if (!parsed.is_discarded() && !parsed["error"]["message"].is_null()) { qDebug() << QString::fromStdString(parsed["error"]["message"]); } else { qDebug() << reply->errorString(); } Settings::getInstance()->setZECPrice(0); return; } auto all = reply->readAll(); auto parsed = json::parse(all, nullptr, false); if (parsed.is_discarded()) { Settings::getInstance()->setZECPrice(0); return; } for (const json& item : parsed.get()) { if (item["symbol"].get().compare("ZEC") == 0) { QString price = QString::fromStdString(item["price_usd"].get()); qDebug() << "ZEC Price=" << price; Settings::getInstance()->setZECPrice(price.toDouble()); return; } } } catch (...) { // If anything at all goes wrong, just set the price to 0 and move on. qDebug() << QString("Caught something nasty"); } // If nothing, then set the price to 0; Settings::getInstance()->setZECPrice(0); }); }