#include "rpc.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; reloadConnectionInfo(); // Set up a timer to refresh the UI every few seconds timer = new QTimer(main); QObject::connect(timer, &QTimer::timeout, [=]() { refresh(); }); timer->start(1 * 60 * 1000); // Query every minute // Set up the timer to watch for tx status txTimer = new QTimer(main); QObject::connect(txTimer, &QTimer::timeout, [=]() { refreshTxStatus(); }); // Start at every minute. When an operation is pending, this will change to every second txTimer->start(1 * 60 * 1000); } 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(main->getSettings()->getHost()); myurl.setPort(main->getSettings()->getPort().toInt()); request.setUrl(myurl); request.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); QString headerData = "Basic " + main->getSettings()->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", Qt::CaseInsensitive)) { explanation = QString() % "\n\nThis is most likely because we couldn't connect to zcashd. Is zcashd running and " % "accepting connections from this machine? \nIf you need to change the host/port, you can set that in the " % "File->Settings menu."; } else if (error.contains("bad request", Qt::CaseInsensitive)) { explanation = QString() % "\n\nThis is most likely an internal error. Are you using zcashd v2.0 or higher? You might " % "need to file a bug report here: "; } else if (error.contains("internal server error", 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: "; } else { explanation = QString() % "\n\nThis is most likely an internal error. Something unexpected happened. " % "You might need to file a bug report here: "; } 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, [=] (json reply) { QString statusText = QString::fromStdString("Connected (") .append(QString::number(reply["blocks"].get())) .append(")"); main->statusLabel->setText(statusText); main->statusIcon->clear(); // TODO: Add checked icon // Refresh everything. refreshBalances(); refreshTransactions(); refreshAddresses(); }); } 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); } }); } void RPC::refreshBalances() { ui->unconfirmedWarning->setVisible(false); // 1. Get the Balances getBalance([=] (json reply) { ui->balSheilded ->setText(QString::fromStdString(reply["private"]) % " ZEC"); ui->balTransparent ->setText(QString::fromStdString(reply["transparent"]) % " ZEC"); ui->balTotal ->setText(QString::fromStdString(reply["total"]) % " ZEC"); }); // 2. Get the UTXOs // First, create a new UTXO list, deleting the old one; delete utxos; utxos = new QList(); delete allBalances; allBalances = new QMap(); // Function to process reply of the listunspent and z_listunspent API calls, used below. auto processUnspent = [=] (const json& reply) { for (auto& it : reply.get()) { QString qsAddr = QString::fromStdString(it["address"]); auto confirmations = it["confirmations"].get(); if (confirmations == 0) { ui->unconfirmedWarning->setVisible(true); } utxos->push_back( UnspentOutput( qsAddr, QString::fromStdString(it["txid"]), QString::number(it["amount"].get(), 'f', 8), confirmations ) ); (*allBalances)[qsAddr] = (*allBalances)[qsAddr] + it["amount"].get(); } }; // Function to create the data model and update the views, used below. auto updateUI = [=] () { // Create the balances table model. // Delete the old Model, because the setModel() doesn't take ownership of the Model object delete balancesTableModel; balancesTableModel = new BalancesTableModel(ui->balancesTable, allBalances, utxos); ui->balancesTable->setModel(balancesTableModel); // Configure Balances Table ui->balancesTable->setColumnWidth(0, 300); // 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(), 'f', 8) % " ZEC)"; ui->inputsCombo->addItem(item); if (item.startsWith(lastFromAddr)) ui->inputsCombo->setCurrentText(item); ++i; } }; // Call the Transparent and Z unspent APIs serially and then, once they're done, update the UI getTransparentUnspent([=] (json reply) { processUnspent(reply); getZUnspent([=] (json reply) { processUnspent(reply); updateUI(); }); }); } void RPC::refreshTransactions() { // First, delete the previous headers delete transactionsTableModel; auto txdata = new QList(); 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); } transactionsTableModel = new TxTableModel(ui->transactionsTable, txdata); ui->transactionsTable->setModel(transactionsTableModel); ui->transactionsTable->setColumnWidth(1, 300); ui->transactionsTable->setColumnWidth(2, 200); ui->transactionsTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); }); } void RPC::refreshTxStatus(const QString& newOpid) { if (!newOpid.isEmpty()) { qDebug() << QString::fromStdString("Adding opid ") % newOpid; 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"]); qDebug() << QString::fromStdString("Watching opid, current status= ") % status; if (status == "success") { qDebug() << QString::fromStdString("Success, showing status message"); main->ui->statusBar->showMessage(" Tx " % id % " computed successfully and submitted"); watchingOps.remove(id); txTimer->start(1 * 60 * 1000); // 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"]); qDebug() << QString::fromStdString("Failed with message") % errorMsg; QMessageBox msg( QMessageBox::Critical, "Transaction Error", "The transaction with id " % id % " failed. The error was:\n\n" % errorMsg, QMessageBox::Ok, main ); txTimer->start(1 * 60 * 1000); watchingOps.remove(id); main->ui->statusBar->showMessage(" Tx " % id % " failed", 15 * 1000); msg.exec(); } else if (status == "executing") { // If the operation is executing, then watch every second. qDebug() << QString::fromStdString("executing, doing again in 1 sec"); txTimer->start(1 * 1000); } } } }); } /* [ { "id": "opid-ad54b34c-1d89-48af-9cb5-4825905fc62e", "status": "executing", "creation_time": 1539490482, "method": "z_sendmany", "params": { "fromaddress": "t1aWhRh9JNKWzpzjn2gmULDJzfKLC724EPS", "amounts": [ { "address": "zcVHg9euUSQF8ef7ZXztrv4LcdC1mytEUYLuoj4W5iSygLFYjm1yQCggAYnnydUaHLy2GBaxF4PX5vjaJjnj2Lq3ecQtGF4", "amount": 0.0001 } ], "minconf": 1, "fee": 0.0001 } },{ "id": "opid-7807e672-7f8c-428a-8587-3354d0ae1b88", "status": "failed", "creation_time": 1539490847, "error": { "code": -6, "message": "Insufficient protected funds, have 0.0006001, need 111.0001" }, ] */