// Copyright 2019-2024 The Hush developers // Released under the GPLv3 #include "controller.h" #include "mainwindow.h" #include "addressbook.h" #include "settings.h" #include "version.h" #include "camount.h" #include "Model/ChatItem.h" #include "DataStore/DataStore.h" #include #include #include ChatModel *chatModel = new ChatModel(); Chat *chat = new Chat(); ContactModel *contactModel = new ContactModel(); using json = nlohmann::json; Controller::Controller(MainWindow* main) { auto cl = new ConnectionLoader(main, this); //qDebug() << __func__ << ": cl=" << cl << endl; // Execute the load connection async, so we can set up the rest of RPC properly. QTimer::singleShot(1, [=]() { cl->loadConnection(); }); // qDebug() << __func__ << "after loadConnection" << endl; this->main = main; this->ui = main->ui; auto current_server = Settings::getInstance()->getSettings().server; main->ui->current_server->setText(current_server); bool isStickyServerEnabled = Settings::getInstance()->getUseStickyServer(); main->ui->sticky_server->setText( isStickyServerEnabled ? "True" : "False" ); // Setup balances table model balancesTableModel = new BalancesTableModel(main->ui->balancesTable); main->ui->balancesTable->setModel(balancesTableModel); // Setup transactions table model transactionsTableModel = new TxTableModel(ui->transactionsTable); main->ui->transactionsTable->setModel(transactionsTableModel); // Set up timer to refresh Price priceTimer = new QTimer(main); QObject::connect(priceTimer, &QTimer::timeout, [=]() { if (Settings::getInstance()->getAllowFetchPrices()) refreshHUSHPrice(); }); priceTimer->start(Settings::priceRefreshSpeed); // Every 5 Min // Set up a timer to refresh the UI every few seconds timer = new QTimer(main); QObject::connect(timer, &QTimer::timeout, [=]() { refresh(); }); timer->start(Settings::updateSpeed); // Set up to fetch supply timer = new QTimer(main); QObject::connect(timer, &QTimer::timeout, [=]() { supplyUpdate(); }); timer->start(Settings::priceRefreshSpeed); // Create the data model model = new DataModel(); // Crate the hushdRPC zrpc = new LiteInterface(); } Controller::~Controller() { delete timer; delete txTimer; delete transactionsTableModel; delete balancesTableModel; delete model; delete zrpc; } // Called when a connection to hushd is available. void Controller::setConnection(Connection* c) { if (c == nullptr) return; this->zrpc->setConnection(c); ui->statusBar->showMessage(""); // If we're allowed to get the Hush Price, get the prices if (Settings::getInstance()->getAllowFetchPrices()) { refreshHUSHPrice(); supplyUpdate(); } // If we're allowed to check for updates, check for a new release if (Settings::getInstance()->getCheckForUpdates()) checkForUpdate(); // Force update, because this might be coming from a settings update // where we need to immediately refresh refresh(true); // Create Sietch zdust addr at startup. // Using DataStore singelton, to store the data outside of lambda, bing bada boom :D for(uint8_t i = 0; i < 8; i++) { zrpc->createNewSietchZaddr( [=] (json reply) { QString zdust = QString::fromStdString(reply.get()[0]); DataStore::getSietchDataStore()->setData("Sietch" + QString(i), zdust.toUtf8()); }); } refreshContacts( ui->listContactWidget ); ui->listChat->verticalScrollBar()->setValue( ui->listChat->verticalScrollBar()->maximum()); //fetch amounts of notes at startup fetchAndProcessUnspentNotes(); } // Build the RPC JSON Parameters for this tx void Controller::fillTxJsonParams(json& allRecepients, Tx tx, bool isChatMessage) { Q_ASSERT(allRecepients.is_array()); // Construct the JSON params json rec = json::object(); //creating the JSON dust parameters in a std::vector to iterate over there during tx std::vector dustTransactions(8); for (auto& dust : dustTransactions) { dust = json::object(); } // Create Sietch zdust addr again to not use it twice. // Using DataStore singelton, to store the data outside of lambda, bing bada boom :D for(uint8_t i = 0; i < 8; i++) { zrpc->createNewSietchZaddr( [=] (json reply) { QString zdust = QString::fromStdString(reply.get()[0]); DataStore::getSietchDataStore()->setData(QString("Sietch") + QString(i), zdust.toUtf8()); } ); } // Set sietch zdust addr to json. // Using DataStore singelton, to store the data into the dust. for(uint8_t i = 0; i < 8; i++) { dustTransactions.at(i)["address"] = DataStore::getSietchDataStore()->getData(QString("Sietch" + QString(i))).toStdString(); } DataStore::getSietchDataStore()->clear(); // clears the datastore // Only for Debugging/Testing: How many free Notes are available? int spendableNotesCount = NoteCountDataStore::getInstance()->getSpendableNotesCount(); QString addressWithMaxValue = NoteCountDataStore::getInstance()->getAddressWithMaxValue(); // Clear NoteCountDataStore DataStore::getNoteCountDataStore()->clear(); const QString possibleCharacters("0123456789abcdef"); int sizerandomString = 512; const int randomStringLength = sizerandomString; for(uint8_t i = 0; i < 8; i++) { QString randomString; QRandomGenerator *gen = QRandomGenerator::system(); for(int i=0; ibounded(0, possibleCharacters.length() - 1); QChar nextChar = possibleCharacters.at(index); randomString.append(nextChar); } dustTransactions.at(i)["memo"] = randomString.toStdString(); } CAmount balanceAvailable = getModel()->getBalVerified(); bool isNoteAutomationEnabled = Settings::getInstance()->getUseNoteAutomation(); // Create more Notes if spendableNotesCount < 30 and enough funds are available and note automation is checked in settings tab if (spendableNotesCount < 30 && balanceAvailable.toDecimalString().toDouble() > (dustTransactions.size() * 0.0001) && isNoteAutomationEnabled && isChatMessage) { // Create extra transaction for (size_t i = 0; i < dustTransactions.size(); ++i) { // Generate random memo QString randomMemo; for (int j = 0; j < randomStringLength; ++j) { int index = QRandomGenerator::system()->bounded(0, possibleCharacters.length()); randomMemo.append(possibleCharacters.at(index)); } dustTransactions.at(i)["address"] = addressWithMaxValue.toStdString(); dustTransactions.at(i)["amount"] = 10000; dustTransactions.at(i)["memo"] = randomMemo.toStdString(); } } else { // Set amount for real Sietch transaction to 0 for (auto &it : dustTransactions) { it["amount"] = 0; } } // For each addr/amt/memo, construct the JSON and also build the confirm dialog box for (const auto& toAddr : tx.toAddrs) { json rec = json::object(); rec["address"] = toAddr.addr.toStdString(); rec["amount"] = toAddr.amount.toqint64(); rec["fee"] = tx.fee.toqint64(); if (Settings::isZAddress(toAddr.addr) && !toAddr.memo.trimmed().isEmpty()) rec["memo"] = toAddr.memo.toStdString(); allRecepients.push_back(rec); } int decider = rand() % 100 + 1; int dustCount = (decider % 4 == 3) ? 5 : 6; if (tx.toAddrs.size() < 2) { dustCount++; } for (int i = 0; i < dustCount; ++i) { allRecepients.insert(allRecepients.begin(), dustTransactions.at(i)); } } void Controller::noConnection() { //qDebug()<< __func__; QIcon i = QApplication::style()->standardIcon(QStyle::SP_MessageBoxCritical); main->statusIcon->setPixmap(i.pixmap(16, 16)); main->statusIcon->setToolTip(""); main->statusLabel->setText(QObject::tr("No Connection")); main->statusLabel->setToolTip(""); main->ui->statusBar->showMessage(QObject::tr("No Connection"), 1000); // Clear balances table. QMap emptyBalances; QList emptyOutputs; QList emptyAddresses; balancesTableModel->setNewData(emptyAddresses, emptyAddresses, emptyBalances, emptyOutputs); // Clear Transactions table. QList emptyTxs; transactionsTableModel->replaceData(emptyTxs); // Clear balances ui->balSheilded->setText(""); ui->balUnconfirmed->setText(""); ui->balTransparent->setText(""); ui->balTotal->setText(""); ui->balSheilded->setToolTip(""); ui->balUnconfirmed->setToolTip(""); ui->balTransparent->setToolTip(""); ui->balTotal->setToolTip(""); } /// This will refresh all the balance data from hushd void Controller::refresh(bool force) { //qDebug()<< __func__; if (!zrpc->haveConnection()) return; getInfoThenRefresh(force); } void Controller::processInfo(const json& info) { // Testnet? QString chainName; if (!info["chain_name"].is_null()) { chainName = QString::fromStdString(info["chain_name"].get()); Settings::getInstance()->setTestnet(chainName == "test"); } QString version = QString::fromStdString(info["version"].get()); Settings::getInstance()->sethushdVersion(version); // Recurring pamynets are testnet only if (!Settings::getInstance()->isTestnet()) main->disableRecurring(); } void Controller::getInfoThenRefresh(bool force) { //qDebug()<< __func__; if (!zrpc->haveConnection()) return noConnection(); // Update current server in Info Tab auto current_server = Settings::getInstance()->getSettings().server; ui->current_server->setText(current_server); // no need to update sticky server because currently there is no // way to change that at run-time via GUI static bool prevCallSucceeded = false; zrpc->fetchInfo([=] (const json& reply) { prevCallSucceeded = true; int curBlock = reply["latest_block_height"].get(); bool doUpdate = force || (model->getLatestBlock() != curBlock); int difficulty = reply["difficulty"].get(); int num_halvings = 1; // number of halvings that have occured already int blocks_until_halving = (num_halvings*1680000 + 340000) - curBlock; int blocktime = 75; int halving_days = (blocks_until_halving * blocktime) / (60 * 60 * 24) ; int longestchain = reply["longestchain"].get(); int notarized = reply["notarized"].get(); model->setLatestBlock(curBlock); if ( Settings::getInstance()->get_currency_name() == "EUR" || Settings::getInstance()->get_currency_name() == "CHF" || Settings::getInstance()->get_currency_name() == "RUB" ) { ui->blockHeight->setText( "Block: " + QLocale(QLocale::German).toString(curBlock) ); ui->last_notarized->setText( "Block: " + QLocale(QLocale::German).toString(notarized) ); ui->longestchain->setText( "Block: " + QLocale(QLocale::German).toString(longestchain) ); ui->difficulty->setText( QLocale(QLocale::German).toString(difficulty) ); ui->halvingTime->setText( (QLocale(QLocale::German).toString(blocks_until_halving)) + " Blocks or , " + (QLocale(QLocale::German).toString(halving_days) + " days" ) ); } else { ui->blockHeight->setText( "Block: " + QLocale(QLocale::English).toString(curBlock) ); ui->last_notarized->setText( "Block: " + QLocale(QLocale::English).toString(notarized) ); ui->longestchain->setText( "Block: " + QLocale(QLocale::English).toString(longestchain) ); ui->difficulty->setText( QLocale(QLocale::English).toString(difficulty) ); ui->halvingTime->setText( (QLocale(QLocale::English).toString(blocks_until_halving)) + " Blocks or , " + (QLocale(QLocale::English).toString(halving_days) + " days" ) ); } ui->Version->setText(QString::fromStdString(reply["version"].get())); ui->Vendor->setText(QString::fromStdString(reply["vendor"].get())); main->logger->write( QString("Refresh. curblock ") % QString::number(curBlock) % ", update=" % (doUpdate ? "true" : "false") ); // Connected, so display checkmark. auto tooltip = Settings::getInstance()->getSettings().server + "\n" + QString::fromStdString(zrpc->getConnection()->getInfo().dump()); QIcon i(":/icons/res/connected.gif"); QString chainName = Settings::getInstance()->isTestnet() ? "test" : "main"; main->statusLabel->setText(chainName + "(" + QString::number(curBlock) + ")"); // use currency ComboBox as input if (Settings::getInstance()->get_currency_name() == "USD") { double price = Settings::getInstance()->getHUSHPrice(); double volume = Settings::getInstance()->getUSDVolume(); double cap = Settings::getInstance()->getUSDCAP(); main->statusLabel->setText( " HUSH/USD=$ " + (QLocale(QLocale::English).toString(price,'f', 2)) ); ui->volumeExchange->setText( " $ " + (QLocale(QLocale::English).toString(volume,'f', 2)) ); ui->marketcapTab->setText( " $ " + (QLocale(QLocale::English).toString(cap,'f', 2)) ); } else if (Settings::getInstance()->get_currency_name() == "EUR") { double price = Settings::getInstance()->getEURPrice(); double volume = Settings::getInstance()->getEURVolume(); double cap = Settings::getInstance()->getEURCAP(); main->statusLabel->setText( "HUSH/EUR "+(QLocale(QLocale::German).toString(price,'f', 2))+ " €" ); ui->volumeExchange->setText( QLocale(QLocale::German).toString(volume,'f', 2)+ " €" ); ui->marketcapTab->setText( QLocale(QLocale::German).toString(cap,'f', 2)+ " €" ); } else if (Settings::getInstance()->get_currency_name() == "BTC") { double price = Settings::getInstance()->getBTCPrice(); double volume = Settings::getInstance()->getBTCVolume(); double cap = Settings::getInstance()->getBTCCAP(); main->statusLabel->setText( " HUSH/BTC=BTC " + (QLocale(QLocale::English).toString(price, 'f',8)) ); ui->volumeExchange->setText( " BTC " + (QLocale(QLocale::English).toString(volume, 'f',8)) ); ui->marketcapTab->setText( " BTC " + (QLocale(QLocale::English).toString(cap, 'f',8)) ); } else if (Settings::getInstance()->get_currency_name() == "CNY") { double price = Settings::getInstance()->getCNYPrice(); double volume = Settings::getInstance()->getCNYVolume(); double cap = Settings::getInstance()->getCNYCAP(); main->statusLabel->setText( " HUSH/CNY=¥ /元 " + (QLocale(QLocale::Chinese).toString(price,'f', 2)) ); ui->volumeExchange->setText( " ¥ /元 " + (QLocale(QLocale::Chinese).toString(volume,'f', 2)) ); ui->marketcapTab->setText( " ¥ /元 " + (QLocale(QLocale::Chinese).toString(cap,'f', 2)) ); } else if (Settings::getInstance()->get_currency_name() == "RUB") { double price = Settings::getInstance()->getRUBPrice(); double volume = Settings::getInstance()->getRUBVolume(); double cap = Settings::getInstance()->getRUBCAP(); main->statusLabel->setText( " HUSH/RUB=₽ " + (QLocale(QLocale::German).toString(price,'f', 2)) ); ui->volumeExchange->setText( " ₽ " + (QLocale(QLocale::German).toString(volume,'f', 2)) ); ui->marketcapTab->setText( " ₽ " + (QLocale(QLocale::German).toString(cap,'f', 2)) ); } else if (Settings::getInstance()->get_currency_name() == "CAD") { double price = Settings::getInstance()->getCADPrice(); double volume = Settings::getInstance()->getCADVolume(); double cap = Settings::getInstance()->getCADCAP(); main->statusLabel->setText( " HUSH/CAD=$ " + (QLocale(QLocale::English).toString(price,'f', 2)) ); ui->volumeExchange->setText( " $ " + (QLocale(QLocale::English).toString(volume,'f', 2)) ); ui->marketcapTab->setText( " $ " + (QLocale(QLocale::English).toString(cap,'f', 2)) ); } else if (Settings::getInstance()->get_currency_name() == "SGD") { double price = Settings::getInstance()->getSGDPrice(); double volume = Settings::getInstance()->getSGDVolume(); double cap = Settings::getInstance()->getSGDCAP(); main->statusLabel->setText( " HUSH/SGD=$ " + (QLocale(QLocale::English).toString(price,'f', 2)) ); ui->volumeExchange->setText( " $ " + (QLocale(QLocale::English).toString(volume,'f', 2)) ); ui->marketcapTab->setText( " $ " + (QLocale(QLocale::English).toString(cap,'f', 2)) ); } else if (Settings::getInstance()->get_currency_name() == "CHF") { double price = Settings::getInstance()->getCHFPrice(); double volume = Settings::getInstance()->getCHFVolume(); double cap = Settings::getInstance()->getCHFCAP(); main->statusLabel->setText( " HUSH/CHF= " + (QLocale(QLocale::German).toString(price,'f', 2))+ " CHF" ); ui->volumeExchange->setText( QLocale(QLocale::German).toString(volume,'f', 2)+ " CHF" ); ui->marketcapTab->setText( QLocale(QLocale::German).toString(cap,'f', 2)+ " CHF" ); } else if (Settings::getInstance()->get_currency_name() == "INR") { double price = Settings::getInstance()->getINRPrice(); double volume = Settings::getInstance()->getINRVolume(); double cap = Settings::getInstance()->getINRCAP(); main->statusLabel->setText( " HUSH/INR=₹ " + (QLocale(QLocale::English).toString(price,'f', 2)) ); ui->volumeExchange->setText( " ₹ " + (QLocale(QLocale::English).toString(volume,'f', 2)) ); ui->marketcapTab->setText( " ₹ " + (QLocale(QLocale::English).toString(cap,'f', 2)) ); } else if (Settings::getInstance()->get_currency_name() == "GBP") { double price = Settings::getInstance()->getGBPPrice(); double volume = Settings::getInstance()->getGBPVolume(); double cap = Settings::getInstance()->getGBPCAP(); main->statusLabel->setText( " HUSH/GBP=£ " + (QLocale(QLocale::English).toString(price,'f', 2)) ); ui->volumeExchange->setText( " £ " + (QLocale(QLocale::English).toString(volume,'f', 2)) ); ui->marketcapTab->setText( " £ " + (QLocale(QLocale::English).toString(cap,'f', 2)) ); } else if (Settings::getInstance()->get_currency_name() == "AUD") { double price = Settings::getInstance()->getAUDPrice(); double volume = Settings::getInstance()->getAUDVolume(); double cap = Settings::getInstance()->getAUDCAP(); main->statusLabel->setText( " HUSH/AUD=$ " + (QLocale(QLocale::English).toString(price,'f', 2)) ); ui->volumeExchange->setText( " $ " + (QLocale(QLocale::English).toString(volume,'f', 2)) ); ui->marketcapTab->setText( " $ " + (QLocale(QLocale::English).toString(cap,'f', 2)) ); } else { main->statusLabel->setText( " HUSH/USD=$" + QString::number(Settings::getInstance()->getHUSHPrice(),'f',2 ) ); ui->volumeExchange->setText( " $ " + QString::number((double) Settings::getInstance()->getUSDVolume() ,'f',2) ); ui->marketcapTab->setText( " $ " + QString::number((double) Settings::getInstance()->getUSDCAP() ,'f',2) ); } main->statusLabel->setToolTip(tooltip); main->statusIcon->setPixmap(i.pixmap(16, 16)); main->statusIcon->setToolTip(tooltip); // See if recurring payments needs anything Recurring::getInstance()->processPending(main); // Check if the wallet is locked/encrypted zrpc->fetchWalletEncryptionStatus([=] (const json& reply) { bool isEncrypted = reply["encrypted"].get(); bool isLocked = reply["locked"].get(); model->setEncryptionStatus(isEncrypted, isLocked); }); if ( doUpdate ) { // Something changed, so refresh everything. refreshBalances(); refreshAddresses(); // This calls refreshZSentTransactions() and refreshReceivedZTrans() refreshTransactions(); } refreshBalances(); int lag = longestchain - notarized ; this->setLag(lag); }, [=](QString err) { // hushd has probably disappeared. this->noConnection(); // Prevent multiple dialog boxes, because these are called async static bool shown = false; if (!shown && prevCallSucceeded) { shown = true; // Check if the error is a compression flag error if (err.contains("compression", Qt::CaseInsensitive)) { QString statusBarMessage = QObject::tr("Compression error: ") + ":\n\n" + err; ui->statusBar->showMessage(statusBarMessage, 5000); } else { QString errorMessage = QObject::tr("There was an error connecting to the server. Please check your internet connection. The error was") + ": \n\n" + err; QMessageBox::critical( main, QObject::tr("Connection Error"), errorMessage, QMessageBox::StandardButton::Ok ); } shown = false; } prevCallSucceeded = false; }); } int Controller::getLag() { return _lag; } void Controller::setLag(int lag) { _lag = lag; } void Controller::refreshAddresses() { //qDebug()<< __func__; if (!zrpc->haveConnection()) return noConnection(); auto newzaddresses = new QList(); auto newtaddresses = new QList(); zrpc->fetchAddresses([=] (json reply) { auto zaddrs = reply["z_addresses"].get(); for (auto& it : zaddrs) { auto addr = QString::fromStdString(it.get()); newzaddresses->push_back(addr); } model->replaceZaddresses(newzaddresses); auto taddrs = reply["t_addresses"].get(); for (auto& it : taddrs) { auto addr = QString::fromStdString(it.get()); if (Settings::isTAddress(addr)) newtaddresses->push_back(addr); } model->replaceTaddresses(newtaddresses); // Refresh the sent and received txs from all these z-addresses refreshTransactions(); }); } // Function to create the data model and update the views, used below. void Controller::updateUI(bool anyUnconfirmed) { ui->unconfirmedWarning->setVisible(anyUnconfirmed); // Update balances model data, which will update the table too balancesTableModel->setNewData( model->getAllZAddresses(), model->getAllTAddresses(), model->getAllBalances(), model->getUTXOs() ); }; void Controller::supplyUpdate() { // qDebug()<< __func__ << ": updating supply"; // Get the total supply and render it with thousand decimal zrpc->fetchSupply([=] (const json& reply) { int supply = reply["supply"].get(); int zfunds = reply["zfunds"].get(); int total = reply["total"].get();; if (Settings::getInstance()->get_currency_name() == "EUR" || Settings::getInstance()->get_currency_name() == "CHF" || Settings::getInstance()->get_currency_name() == "RUB" ) { // TODO: assuming German locale is incorrect ui->supply_taddr->setText((QLocale(QLocale::German).toString(supply)+ " HUSH")); ui->supply_zaddr->setText((QLocale(QLocale::German).toString(zfunds)+ " HUSH")); ui->supply_total->setText((QLocale(QLocale::German).toString(total)+ " HUSH")); } else { // TODO: assuming English locale is incorrect as well ui->supply_taddr->setText("HUSH " + (QLocale(QLocale::English).toString(supply))); ui->supply_zaddr->setText("HUSH " +(QLocale(QLocale::English).toString(zfunds))); ui->supply_total->setText("HUSH " +(QLocale(QLocale::English).toString(total))); } //qDebug() << __func__ << ": supply=" << supply; }); } // Function to process reply of the listunspent and z_listunspent API calls, used below. void Controller::processUnspent(const json& reply, QMap* balancesMap, QList* unspentOutputs) { 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"]); CAmount amount = CAmount::fromqint64(it["value"].get()); bool spendable = it["unconfirmed_spent"].is_null() && it["spent"].is_null(); // TODO: Wait for 1 confirmations bool pending = !it["unconfirmed_spent"].is_null();; unspentOutputs->push_back( UnspentOutput{ qsAddr, txid, amount, block, spendable, pending } ); if (spendable) { (*balancesMap)[qsAddr] = (*balancesMap)[qsAddr] + CAmount::fromqint64(it["value"].get()); } } }; processFn(reply["unspent_notes"].get()); processFn(reply["utxos"].get()); processFn(reply["pending_notes"].get()); processFn(reply["pending_utxos"].get()); }; void Controller::updateUIBalances() { CAmount balT = getModel()->getBalT(); CAmount balZ = getModel()->getBalZ(); CAmount balU = getModel()->getBalU(); CAmount balVerified = getModel()->getBalVerified(); CAmount balSpendable = getModel()->getBalSpendable(); // Reduce the BalanceZ by the pending outgoing amount. We're adding // here because totalPending is already negative for outgoing txns. balZ = balZ + getModel()->getTotalPending(); CAmount balTotal = balT + balZ; CAmount balAvailable = balT + balVerified; if (balZ < 0) balZ = CAmount::fromqint64(0); // double price = (Settings::getInstance()->getBTCPrice() / 1000); // ui->PriceMemo->setText(" The price of \n one HushChat \n Message is :\n BTC " + (QLocale(QLocale::English).toString(price, 'f',8)) //+ " Messages left :" + ((balTotal.toDecimalhushString()) /0.0001) ); // Balances table ui->balSheilded->setText(balZ.toDecimalhushString()); ui->balVerified->setText(balVerified.toDecimalhushString()); ui->balUnconfirmed->setText(balU.toDecimalhushString()); ui->balTransparent->setText(balT.toDecimalhushString()); ui->balSpendable->setText(balSpendable.toDecimalhushString()); ui->balTotal->setText(balTotal.toDecimalhushString()); //TODO: refactor this madness into functions like SD uses, with currency as a variable if (Settings::getInstance()->get_currency_name() == "USD") { ui->balSheilded->setToolTip(balZ.toDecimalUSDString()); ui->balVerified->setToolTip(balVerified.toDecimalUSDString()); ui->balTransparent->setToolTip(balT.toDecimalUSDString()); ui->balTotal->setToolTip(balTotal.toDecimalUSDString()); } else if (Settings::getInstance()->get_currency_name() == "EUR") { ui->balSheilded->setToolTip(balZ.toDecimalEURString()); ui->balVerified->setToolTip(balVerified.toDecimalEURString()); ui->balTransparent->setToolTip(balT.toDecimalEURString()); ui->balTotal->setToolTip(balTotal.toDecimalEURString()); } else if (Settings::getInstance()->get_currency_name() == "BTC") { ui->balSheilded->setToolTip(balZ.toDecimalBTCString()); ui->balVerified->setToolTip(balVerified.toDecimalBTCString()); ui->balTransparent->setToolTip(balT.toDecimalBTCString()); ui->balTotal->setToolTip(balTotal.toDecimalBTCString()); } else if (Settings::getInstance()->get_currency_name() == "CNY") { ui->balSheilded->setToolTip(balZ.toDecimalCNYString()); ui->balVerified->setToolTip(balVerified.toDecimalCNYString()); ui->balTransparent->setToolTip(balT.toDecimalCNYString()); ui->balTotal->setToolTip(balTotal.toDecimalCNYString()); } else if (Settings::getInstance()->get_currency_name() == "RUB") { ui->balSheilded->setToolTip(balZ.toDecimalRUBString()); ui->balVerified->setToolTip(balVerified.toDecimalRUBString()); ui->balTransparent->setToolTip(balT.toDecimalRUBString()); ui->balTotal->setToolTip(balTotal.toDecimalRUBString()); } else if (Settings::getInstance()->get_currency_name() == "CAD") { ui->balSheilded->setToolTip(balZ.toDecimalCADString()); ui->balVerified->setToolTip(balVerified.toDecimalCADString()); ui->balTransparent->setToolTip(balT.toDecimalCADString()); ui->balTotal->setToolTip(balTotal.toDecimalCADString()); } else if (Settings::getInstance()->get_currency_name() == "SGD") { ui->balSheilded->setToolTip(balZ.toDecimalSGDString()); ui->balVerified->setToolTip(balVerified.toDecimalSGDString()); ui->balTransparent->setToolTip(balT.toDecimalSGDString()); ui->balTotal->setToolTip(balTotal.toDecimalSGDString()); } else if (Settings::getInstance()->get_currency_name() == "CHF") { ui->balSheilded->setToolTip(balZ.toDecimalCHFString()); ui->balVerified->setToolTip(balVerified.toDecimalCHFString()); ui->balTransparent->setToolTip(balT.toDecimalCHFString()); ui->balTotal->setToolTip(balTotal.toDecimalCHFString()); } else if (Settings::getInstance()->get_currency_name() == "INR") { ui->balSheilded->setToolTip(balZ.toDecimalINRString()); ui->balVerified->setToolTip(balVerified.toDecimalINRString()); ui->balTransparent->setToolTip(balT.toDecimalINRString()); ui->balTotal->setToolTip(balTotal.toDecimalINRString()); } else if (Settings::getInstance()->get_currency_name() == "GBP") { ui->balSheilded ->setToolTip(balZ.toDecimalGBPString()); ui->balVerified ->setToolTip(balVerified.toDecimalGBPString()); ui->balTransparent->setToolTip(balT.toDecimalGBPString()); ui->balTotal ->setToolTip(balTotal.toDecimalGBPString()); } else if (Settings::getInstance()->get_currency_name() == "AUD") { ui->balSheilded ->setToolTip(balZ.toDecimalAUDString()); ui->balVerified ->setToolTip(balVerified.toDecimalAUDString()); ui->balTransparent->setToolTip(balT.toDecimalAUDString()); ui->balTotal ->setToolTip(balTotal.toDecimalAUDString()); } // Send tab ui->txtAvailablehush->setText(balAvailable.toDecimalhushString()); if (Settings::getInstance()->get_currency_name() == "USD") ui->txtAvailableUSD->setText(balAvailable.toDecimalUSDString()); else if (Settings::getInstance()->get_currency_name() == "EUR") ui->txtAvailableUSD->setText(balAvailable.toDecimalEURString()); else if (Settings::getInstance()->get_currency_name() == "BTC") ui->txtAvailableUSD->setText(balAvailable.toDecimalBTCString()); else if (Settings::getInstance()->get_currency_name() == "CNY") ui->txtAvailableUSD->setText(balAvailable.toDecimalCNYString()); else if (Settings::getInstance()->get_currency_name() == "RUB") ui->txtAvailableUSD->setText(balAvailable.toDecimalRUBString()); else if (Settings::getInstance()->get_currency_name() == "CAD") ui->txtAvailableUSD->setText(balAvailable.toDecimalCADString()); else if (Settings::getInstance()->get_currency_name() == "SGD") ui->txtAvailableUSD->setText(balAvailable.toDecimalSGDString()); else if (Settings::getInstance()->get_currency_name() == "CHF") ui->txtAvailableUSD->setText(balAvailable.toDecimalCHFString()); else if (Settings::getInstance()->get_currency_name() == "INR") ui->txtAvailableUSD->setText(balAvailable.toDecimalINRString()); else if (Settings::getInstance()->get_currency_name() == "GBP") ui->txtAvailableUSD->setText(balAvailable.toDecimalGBPString()); else if (Settings::getInstance()->get_currency_name() == "AUD") ui->txtAvailableUSD->setText(balAvailable.toDecimalAUDString()); } void Controller::refreshBalances() { //qDebug()<< __func__; if (!zrpc->haveConnection()) return noConnection(); // 1. Get the Balances zrpc->fetchBalance([=] (json reply) { CAmount balT = CAmount::fromqint64(reply["tbalance"].get()); CAmount balZ = CAmount::fromqint64(reply["zbalance"].get()); CAmount balU = CAmount::fromqint64(reply["unconfirmed"].get()); CAmount balVerified = CAmount::fromqint64(reply["verified_zbalance"].get()); CAmount balSpendable = CAmount::fromqint64(reply["spendable_zbalance"].get()); model->setBalT(balT); model->setBalZ(balZ); model->setBalU(balU); model->setBalVerified(balVerified); model->setBalSpendable(balSpendable); // This is for the datamodel CAmount balAvailable = balT + balVerified; model->setAvailableBalance(balAvailable); updateUIBalances(); }); // 2. Get the UTXOs // First, create a new UTXO list. It will be replacing the existing list when everything is processed. auto newUnspentOutputs = new QList(); auto newBalances = new QMap(); // Call the Transparent and Z unspent APIs serially and then, once they're done, update the UI zrpc->fetchUnspent([=] (json reply) { processUnspent(reply, newBalances, newUnspentOutputs); // Swap out the balances and UTXOs model->replaceBalances(newBalances); model->replaceUTXOs(newUnspentOutputs); // Find if any output is not spendable or is pending bool anyUnconfirmed = std::find_if( newUnspentOutputs->constBegin(), newUnspentOutputs->constEnd(), [=](const UnspentOutput& u) -> bool { return !u.spendable || u.pending; } ) != newUnspentOutputs->constEnd(); updateUI(anyUnconfirmed); main->balancesReady(); }); } void printJsonValue(QTextStream& out, const nlohmann::json& j, int depth = 0) { if (j.is_array()) { for (auto& elem : j) { printJsonValue(out, elem, depth + 1); } } else if (j.is_object()) { for (auto it = j.begin(); it != j.end(); ++it) { std::string key = it.key(); const nlohmann::json& value = it.value(); out << QString::fromStdString(std::string(depth * 4, ' ')) << QString::fromStdString(key) << ": "; printJsonValue(out, value, depth + 1); } } else { out << QString::fromStdString(j.dump(4)) << "\n"; } } void Controller::refreshTransactions() { //qDebug()<< __func__; if (!zrpc->haveConnection()) return noConnection(); // qDebug() << __func__ << ": fetchTransactions"; zrpc->fetchTransactions([=] (json reply) { QList txdata; for (auto& it : reply.get()) { { QString address; CAmount total_amount; QList items; long confirmations; if (it.find("unconfirmed") != it.end() && it["unconfirmed"].get()) { confirmations = 0; }else{ confirmations = model->getLatestBlock() - it["block_height"].get() + 1; } auto txid = QString::fromStdString(it["txid"]); auto datetime = it["datetime"].get(); // First, check if there's outgoing metadata if (!it["outgoing_metadata"].is_null()) { for (auto o: it["outgoing_metadata"].get()) { // if (chatModel->getCidByTx(txid) == QString("0xdeadbeef")){ QString address; address = QString::fromStdString(o["address"]); // Sent items are negative CAmount amount = CAmount::fromqint64(-1* o["value"].get()); // Check for Memos if (confirmations == 0) { chatModel->addconfirmations(txid, confirmations); } if ((confirmations > 0) && (chatModel->getConfirmationByTx(txid) != QString("0xdeadbeef"))) { DataStore::getChatDataStore()->clear(); chatModel->killConfirmationCache(); chatModel->killMemoCache(); this->refresh(true); } QString memo; QString cid = ""; QString headerbytes = ""; QString publickey = ""; if (!o["memo"].is_null()) { memo = QString::fromStdString(o["memo"].get()); if (memo.startsWith("{")) { try { QJsonDocument headermemo = QJsonDocument::fromJson(memo.toUtf8()); cid = headermemo["cid"].toString(); headerbytes = headermemo["e"].toString(); chatModel->addCid(txid, cid); chatModel->addHeader(txid, headerbytes); } catch (...) { qDebug() << "Invalid JSON in memo detected! memo=" << memo; } } bool isNotarized = false;; if (confirmations > getLag()) { isNotarized = true; } if (chatModel->getCidByTx(txid) != QString("0xdeadbeef")) { cid = chatModel->getCidByTx(txid); } if (chatModel->getHeaderByTx(txid) != QString("0xdeadbeef")) { headerbytes = chatModel->getHeaderByTx(txid); } if (main->getPubkeyByAddress(address) != QString("0xdeadbeef")) { publickey = main->getPubkeyByAddress(address); } /////We need to filter out Memos smaller then the ciphertext size, or it will dump if ((memo.startsWith("{") == false) && (headerbytes.length() > 20)) { QString passphrase = DataStore::getChatDataStore()->getPassword(); int length = passphrase.length(); ////////////////Generate the secretkey for our message encryption char *hashEncryptionKeyraw = NULL; hashEncryptionKeyraw = new char[length+1]; strncpy(hashEncryptionKeyraw, passphrase.toUtf8(), length +1); const QByteArray pubkeyBobArray = QByteArray::fromHex(publickey.toLatin1()); const unsigned char *pubkeyBob = reinterpret_cast(pubkeyBobArray.constData()); #define MESSAGEAS1 ((const unsigned char *) hashEncryptionKeyraw) /////////// #define MESSAGEAS1_LEN length unsigned char sk[crypto_kx_SECRETKEYBYTES]; unsigned char pk[crypto_kx_PUBLICKEYBYTES]; if (crypto_kx_seed_keypair(pk, sk, MESSAGEAS1) !=0) { main->logger->write("Keypair outgoing error"); // qDebug() << "refreshTransactions: crypto_kx_seed_keypair error"; continue; } unsigned char server_rx[crypto_kx_SESSIONKEYBYTES], server_tx[crypto_kx_SESSIONKEYBYTES]; ////////////////Get the pubkey from Bob, so we can create the share key /////Create the shared key for sending the message if (crypto_kx_server_session_keys(server_rx, server_tx, pk, sk, pubkeyBob) != 0) { main->logger->write("Suspicious client public outgoing key, bail out "); // qDebug() << "refreshTransactions: Suspicious client public outgoing key, aborting!"; continue; } const QByteArray ba = QByteArray::fromHex(memo.toUtf8()); const unsigned char *encryptedMemo = reinterpret_cast(ba.constData()); const QByteArray ba1 = QByteArray::fromHex(headerbytes.toLatin1()); const unsigned char *header = reinterpret_cast(ba1.constData()); int encryptedMemoSize1 = ba.length(); QString memodecrypt; if ((encryptedMemoSize1 - crypto_secretstream_xchacha20poly1305_ABYTES) > 0) { //////unsigned char* as message from QString #define MESSAGE2 (const unsigned char *) encryptedMemo ///////// length of the encrypted message #define CIPHERTEXT1_LEN encryptedMemoSize1 ///////Message length is smaller then the encrypted message #define MESSAGE1_LEN encryptedMemoSize1 - crypto_secretstream_xchacha20poly1305_ABYTES //////Set the length of the decrypted message unsigned char decrypted[MESSAGE1_LEN]; unsigned char tag[crypto_secretstream_xchacha20poly1305_TAG_FINAL]; crypto_secretstream_xchacha20poly1305_state state; if (crypto_secretstream_xchacha20poly1305_init_pull(&state, header, server_tx) != 0) { /* Invalid header, no need to go any further */ // qDebug() << "refreshTransactions: crypto_secretstream_xchacha20poly1305_init_pull error! Invalid header"; continue; } if (crypto_secretstream_xchacha20poly1305_pull(&state, decrypted, NULL, tag, MESSAGE2, CIPHERTEXT1_LEN, NULL, 0) != 0) { /* Invalid/incomplete/corrupted ciphertext - abort */ // qDebug() << "refreshTransactions: crypto_secretstream_xchacha20poly1305_pull error! Invalid ciphertext"; continue; } /////Our decrypted message is now in decrypted. We need it as QString to render it /////Only the QString gives weird data, so convert first to std::string std::string decryptedMemo(reinterpret_cast(decrypted),MESSAGE1_LEN); memodecrypt = QString::fromUtf8( decryptedMemo.data(), decryptedMemo.size()); } else { memodecrypt = ""; } /////Now we can convert it to QString //////////////Give us the output of the decrypted message as debug to see if it was successfully ChatItem item = ChatItem( datetime, address, QString(""), memodecrypt, QString(""), QString(""), cid, txid, confirmations, true, isNotarized, false ); // qDebug() << "refreshTransactions: adding chatItem with memodecrypt=" << memodecrypt; DataStore::getChatDataStore()->setData(ChatIDGenerator::getInstance()->generateID(item), item); // updateUIBalances(); } } items.push_back(TransactionItemDetail{address, amount, memo}); total_amount = total_amount + amount; } { QList addresses; for (auto item : items) { // Concat all the addresses addresses.push_back(item.address); address = addresses.join(","); } } txdata.push_back(TransactionItem{ "send", datetime, address, txid,confirmations, items }); } else { { // Incoming Transaction QString memo; address = (it["address"].is_null() ? "" : QString::fromStdString(it["address"])); if (!it["memo"].is_null()) { memo = QString::fromStdString(it["memo"]); } items.push_back(TransactionItemDetail{ address, CAmount::fromqint64(it["amount"].get()), memo }); TransactionItem tx{ "Receive", datetime, address, txid,confirmations, items }; txdata.push_back(tx); QString type = ""; QString publickey = ""; QString headerbytes = ""; QString cid = ""; QString requestZaddr = ""; QString contactname = ""; bool isContact = false; if (!memo.isNull()) { if (memo.startsWith("{")) { try { QJsonDocument headermemo = QJsonDocument::fromJson(memo.toUtf8()); cid = headermemo["cid"].toString(); type = headermemo["t"].toString(); requestZaddr = headermemo["z"].toString(); headerbytes = headermemo["e"].toString(); publickey = headermemo["p"].toString(); chatModel->addCid(txid, cid); chatModel->addrequestZaddr(txid, requestZaddr); chatModel->addHeader(txid, headerbytes); // TODO: better validation of valid public key if (publickey.length() > 10){ main->addPubkey(requestZaddr, publickey); } } catch (...) { qDebug() << __func__ << ": Invalid JSON in memo! memo=" << memo.toUtf8(); } } if (chatModel->getCidByTx(txid) != QString("0xdeadbeef")) { cid = chatModel->getCidByTx(txid); } if (chatModel->getrequestZaddrByTx(txid) != QString("0xdeadbeef")) { requestZaddr = chatModel->getrequestZaddrByTx(txid); } if (chatModel->getHeaderByTx(txid) != QString("0xdeadbeef")) { headerbytes = chatModel->getHeaderByTx(txid); } if (main->getPubkeyByAddress(requestZaddr) != QString("0xdeadbeef")) { publickey = main->getPubkeyByAddress(requestZaddr); } if (contactModel->getContactbyAddress(requestZaddr) != QString("0xdeadbeef")) { isContact = true; contactname = contactModel->getContactbyAddress(requestZaddr); } bool isNotarized = false; if (confirmations > getLag()) { isNotarized = true; } int position = it["position"].get(); int ciphercheck = memo.length() - crypto_secretstream_xchacha20poly1305_ABYTES; // qDebug() << __func__ << ": position=" << position << " headerbytes=" << headerbytes // << " ciphercheck=" << ciphercheck << " for memo=" << memo; if ((memo.startsWith("{") == false) && (headerbytes > 0) && (ciphercheck > 0)) { if (chatModel->getMemoByTx(txid) == QString("0xdeadbeef")) { if (position == 1) { chatModel->addMemo(txid, headerbytes); } else { // } QString passphrase = DataStore::getChatDataStore()->getPassword(); int length = passphrase.length(); char *hashEncryptionKeyraw = NULL; hashEncryptionKeyraw = new char[length+1]; strncpy(hashEncryptionKeyraw, passphrase.toUtf8(), length +1); const QByteArray pubkeyBobArray = QByteArray::fromHex(publickey.toLatin1()); const unsigned char *pubkeyBob = reinterpret_cast(pubkeyBobArray.constData()); #define MESSAGEAS1 ((const unsigned char *) hashEncryptionKeyraw)/////////// #define MESSAGEAS1_LEN length unsigned char sk[crypto_kx_SECRETKEYBYTES]; unsigned char pk[crypto_kx_PUBLICKEYBYTES]; if (crypto_kx_seed_keypair(pk, sk, MESSAGEAS1) !=0) { main->logger->write("Suspicious outgoing key pair, bail out "); // qDebug() << "refreshTransactions: (incoming) crypto_kx_seed_keypair error!"; continue; } unsigned char client_rx[crypto_kx_SESSIONKEYBYTES], client_tx[crypto_kx_SESSIONKEYBYTES]; ////////////////Get the pubkey from Bob, so we can create the share key /////Create the shared key for sending the message if (crypto_kx_client_session_keys(client_rx, client_tx, pk, sk, pubkeyBob) != 0) { main->logger->write("Suspicious client public incoming key, bail out "); // qDebug() << "refreshTransactions: (incoming) crypto_kx_client_session_keys error!"; continue; } const QByteArray ba = QByteArray::fromHex(memo.toUtf8()); const unsigned char *encryptedMemo = reinterpret_cast(ba.constData()); const QByteArray ba1 = QByteArray::fromHex(headerbytes.toLatin1()); const unsigned char *header = reinterpret_cast(ba1.constData()); int encryptedMemoSize1 = ba.length(); //int headersize = ba1.length(); //////unsigned char* as message from QString #define MESSAGE2 (const unsigned char *) encryptedMemo ///////// length of the encrypted message #define CIPHERTEXT1_LEN encryptedMemoSize1 ///////Message length is smaller then the encrypted message #define MESSAGE1_LEN encryptedMemoSize1 - crypto_secretstream_xchacha20poly1305_ABYTES //////Set the length of the decrypted message unsigned char decrypted[MESSAGE1_LEN+1]; unsigned char tag[crypto_secretstream_xchacha20poly1305_TAG_FINAL]; crypto_secretstream_xchacha20poly1305_state state; /////Our decrypted message is now in decrypted. We need it as QString to render it /////Only the QString gives weird data, so convert first to std::string // crypto_secretstream_xchacha20poly1305_keygen(client_rx); if (crypto_secretstream_xchacha20poly1305_init_pull(&state, header, client_rx) != 0) { main->logger->write("Invalid header incoming, no need to go any further "); //qDebug() <<"refreshTransactions: (incoming) crypto_secretstream_xchacha20poly1305_init_pull error! memo=" << memo; continue; } if (crypto_secretstream_xchacha20poly1305_pull(&state, decrypted, NULL, tag, MESSAGE2, CIPHERTEXT1_LEN, NULL, 0) != 0) { main->logger->write("Invalid/incomplete/corrupted ciphertext - abort"); // qDebug() << "refreshTransactions: (incoming) crypto_secretstream_xchacha20poly1305_pull error! memo=" << memo; continue; } std::string decryptedMemo(reinterpret_cast(decrypted),MESSAGE1_LEN); /////Now we can convert it to QString QString memodecrypt; memodecrypt = QString::fromUtf8( decryptedMemo.data(), decryptedMemo.size()); ////Give us the output of the decrypted message as debug to see if it was successfully ChatItem item = ChatItem( datetime, address, contactname, memodecrypt, requestZaddr, type, cid, txid, confirmations, false, isNotarized, isContact ); auto iid = ChatIDGenerator::getInstance()->generateID(item); // qDebug() << "refreshTransactions: adding chatItem with item id=" << iid << " memodecrypt=" << memodecrypt; DataStore::getChatDataStore()->setData(iid, item); } else { // qDebug() << __func__ << ": ignoring txid="<< txid; } } else { // Add a chatitem for the initial CR ChatItem item = ChatItem( datetime, address, contactname, memo, requestZaddr, type, cid, txid, confirmations, false, isNotarized, isContact ); auto iid = ChatIDGenerator::getInstance()->generateID(item); // qDebug() << "refreshTransactions: adding chatItem for initial CR with item id="<< iid << " memo='" << memo << "'"; DataStore::getChatDataStore()->setData(iid, item); } } } } } } // Calculate the total unspent amount that's pending. This will need to be // shown in the UI so the user can keep track of pending funds CAmount totalPending; for (auto txitem : txdata) { if (txitem.confirmations == 0) { for (auto item: txitem.items) { totalPending = totalPending + item.amount; } } } getModel()->setTotalPending(totalPending); // Update UI Balance updateUIBalances(); // Update model data, which updates the table view transactionsTableModel->replaceData(txdata); // qDebug() << __func__ << ": calling renderChatBox"; chat->renderChatBox(ui, ui->listChat,ui->memoSizeChat); ui->listChat->verticalScrollBar()->setValue(ui->listChat->verticalScrollBar()->maximum()); }); } void Controller::refreshChat(QListView *listWidget, QLabel *label) { // qDebug() << __func__ << ": calling renderChatBox"; chat->renderChatBox(ui, listWidget, label); ui->listChat->verticalScrollBar()->setValue(ui->listChat->verticalScrollBar()->maximum()); } void Controller::refreshContacts(QListView *listWidget) { contactModel->renderContactList(listWidget); ui->listChat->verticalScrollBar()->setValue(ui->listChat->verticalScrollBar()->maximum()); } // If the wallet is encrpyted and locked, we need to unlock it void Controller::unlockIfEncrypted(std::function cb, std::function error) { auto encStatus = getModel()->getEncryptionStatus(); if (encStatus.first && encStatus.second) { // Wallet is encrypted and locked. Ask for the password and unlock. QString password = QInputDialog::getText( main, QObject::tr("Wallet Password"), QObject::tr("Your wallet is encrypted.\nPlease enter your wallet password"), QLineEdit::Password ); if (password.isEmpty()) { QMessageBox::critical( main, QObject::tr("Wallet Decryption Failed"), QObject::tr("Please enter a valid password"), QMessageBox::Ok ); error(); return; } zrpc->unlockWallet(password, [=](json reply) { if (isJsonResultSuccess(reply)) { cb(); // Refresh the wallet so the encryption status is now in sync. refresh(true); } else { QMessageBox::critical( main, QObject::tr("Wallet Decryption Failed"), QString::fromStdString(reply["error"].get()), QMessageBox::Ok ); error(); } }); } else { // Not locked, so just call the function cb(); } } /** * Execute a transaction with the standard UI. i.e., standard status bar message and standard error * handling */ void Controller::executeStandardUITransaction(Tx tx) { executeTransaction(tx,false, [=] (QString txid) { ui->statusBar->showMessage(Settings::txidStatusMessage + " " + txid); }, [=] (QString opid, QString errStr) { ui->statusBar->showMessage( QObject::tr(" Tx ") % opid % QObject::tr(" failed"), 15 * 1000 ); if (!opid.isEmpty()) errStr = QObject::tr("The transaction with id ") % opid % QObject::tr(" failed. The error was") + ":\n\n" + errStr; QMessageBox::critical( main, QObject::tr("Transaction Error"), errStr, QMessageBox::Ok ); } ); } // Execute a transaction! void Controller::executeTransaction(Tx tx, bool isChatMessage, const std::function submitted, const std::function error) { // Refresh the available unspent notes fetchAndProcessUnspentNotes(); unlockIfEncrypted([=] () { // First, create the json params json params = json::array(); fillTxJsonParams(params, tx, isChatMessage); std::cout << std::setw(2) << params << std::endl; zrpc->sendTransaction(QString::fromStdString(params.dump()), [=](const json& reply) { if (reply.find("txid") == reply.end()) { error("", "Couldn't understand Response: " + QString::fromStdString(reply.dump())); } else { QString txid = QString::fromStdString(reply["txid"].get()); submitted(txid); } }, [=](QString errStr) { error("", errStr); }); }, [=]() { error("", QObject::tr("Failed to unlock wallet")); }); } void Controller::checkForUpdate(bool silent) { // qDebug()<< __func__; // No checking for updates, needs testing with Gitea return; if (!zrpc->haveConnection()) return noConnection(); QUrl giteaURL("https://git.hush.is/repos/hush/SilentDragonLite/releases"); QNetworkRequest req; req.setUrl(giteaURL); QNetworkAccessManager *manager = new QNetworkAccessManager(this->main); QNetworkReply *reply = manager->get(req); QObject::connect(reply, &QNetworkReply::finished, [=] { reply->deleteLater(); manager->deleteLater(); try { if (reply->error() == QNetworkReply::NoError) { auto releases = QJsonDocument::fromJson(reply->readAll()).array(); QVersionNumber maxVersion(0, 0, 0); for (QJsonValue rel : releases) { if (!rel.toObject().contains("tag_name")) continue; QString tag = rel.toObject()["tag_name"].toString(); if (tag.startsWith("v")) tag = tag.right(tag.length() - 1); if (!tag.isEmpty()) { auto v = QVersionNumber::fromString(tag); if (v > maxVersion) maxVersion = v; } } auto currentVersion = QVersionNumber::fromString(APP_VERSION); // Get the max version that the user has hidden updates for QSettings s; auto maxHiddenVersion = QVersionNumber::fromString( s.value("update/lastversion", "0.0.0" ).toString()); qDebug() << "Version check: Current " << currentVersion << ", Available " << maxVersion; if (maxVersion > currentVersion && (!silent || maxVersion > maxHiddenVersion)) { auto ans = QMessageBox::information(main, QObject::tr("Update Available"), QObject::tr("A new release v%1 is available! You have v%2.\n\nWould you like to visit the releases page?") .arg(maxVersion.toString()) .arg(currentVersion.toString()), QMessageBox::Yes, QMessageBox::Cancel); if (ans == QMessageBox::Yes) { QDesktopServices::openUrl(QUrl("https://git.hush.is/hush/SilentDragonLite/releases")); } else { // If the user selects cancel, don't bother them again for this version s.setValue("update/lastversion", maxVersion.toString()); } } else { if (!silent) { QMessageBox::information(main, QObject::tr("No updates available"), QObject::tr("You already have the latest release v%1") .arg(currentVersion.toString())); } } } } catch (...) { // If anything at all goes wrong, just set the price to 0 and move on. qDebug() << QString("Caught something nasty"); } }); } // Get the hush->USD price from coinmarketcap using their API void Controller::refreshHUSHPrice() { // qDebug()<< __func__; if (!zrpc->haveConnection()) return; // TODO: use/render all this data QUrl cmcURL("https://api.coingecko.com/api/v3/simple/price?ids=hush&vs_currencies=btc%2Cusd%2Ceur%2Ceth%2Cgbp%2Ccny%2Cjpy%2Crub%2Ccad%2Csgd%2Cchf%2Cinr%2Caud%2Cinr&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true"); QNetworkRequest req; req.setUrl(cmcURL); QNetworkAccessManager *manager = new QNetworkAccessManager(this->main); QNetworkReply *reply = manager->get(req); QObject::connect(reply, &QNetworkReply::finished, [=] { reply->deleteLater(); manager->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()->setHUSHPrice(0); Settings::getInstance()->setEURPrice(0); Settings::getInstance()->setBTCPrice(0); Settings::getInstance()->setCNYPrice(0); Settings::getInstance()->setRUBPrice(0); Settings::getInstance()->setCADPrice(0); Settings::getInstance()->setSGDPrice(0); Settings::getInstance()->setCHFPrice(0); Settings::getInstance()->setGBPPrice(0); Settings::getInstance()->setAUDPrice(0); Settings::getInstance()->setINRPrice(0); Settings::getInstance()->setUSDVolume(0); Settings::getInstance()->setEURVolume(0); Settings::getInstance()->setBTCVolume(0); Settings::getInstance()->setCNYVolume(0); Settings::getInstance()->setRUBVolume(0); Settings::getInstance()->setCADVolume(0); Settings::getInstance()->setINRVolume(0); Settings::getInstance()->setSGDVolume(0); Settings::getInstance()->setCHFVolume(0); Settings::getInstance()->setGBPVolume(0); Settings::getInstance()->setAUDVolume(0); Settings::getInstance()->setUSDCAP(0); Settings::getInstance()->setEURCAP(0); Settings::getInstance()->setBTCCAP(0); Settings::getInstance()->setCNYCAP(0); Settings::getInstance()->setRUBCAP(0); Settings::getInstance()->setCADCAP(0); Settings::getInstance()->setINRCAP(0); Settings::getInstance()->setSGDCAP(0); Settings::getInstance()->setCHFCAP(0); Settings::getInstance()->setGBPCAP(0); Settings::getInstance()->setAUDCAP(0); return; } qDebug() << "No network errors"; auto all = reply->readAll(); auto parsed = json::parse(all, nullptr, false); if (parsed.is_discarded()) { Settings::getInstance()->setHUSHPrice(0); Settings::getInstance()->setEURPrice(0); Settings::getInstance()->setBTCPrice(0); Settings::getInstance()->setCNYPrice(0); Settings::getInstance()->setRUBPrice(0); Settings::getInstance()->setCADPrice(0); Settings::getInstance()->setSGDPrice(0); Settings::getInstance()->setCHFPrice(0); Settings::getInstance()->setGBPPrice(0); Settings::getInstance()->setAUDPrice(0); Settings::getInstance()->setINRPrice(0); Settings::getInstance()->setUSDVolume(0); Settings::getInstance()->setEURVolume(0); Settings::getInstance()->setBTCVolume(0); Settings::getInstance()->setCNYVolume(0); Settings::getInstance()->setRUBVolume(0); Settings::getInstance()->setCADVolume(0); Settings::getInstance()->setINRVolume(0); Settings::getInstance()->setSGDVolume(0); Settings::getInstance()->setCHFVolume(0); Settings::getInstance()->setGBPVolume(0); Settings::getInstance()->setAUDVolume(0); Settings::getInstance()->setUSDCAP(0); Settings::getInstance()->setEURCAP(0); Settings::getInstance()->setBTCCAP(0); Settings::getInstance()->setCNYCAP(0); Settings::getInstance()->setRUBCAP(0); Settings::getInstance()->setCADCAP(0); Settings::getInstance()->setINRCAP(0); Settings::getInstance()->setSGDCAP(0); Settings::getInstance()->setCHFCAP(0); Settings::getInstance()->setGBPCAP(0); Settings::getInstance()->setAUDCAP(0); return; } qDebug() << "Parsed JSON"; const json& item = parsed.get(); const json& hush = item["hush"].get(); if (hush["usd"] >= 0) { qDebug() << "Found hush key in price json"; qDebug() << "HUSH = $" << QString::number((double)hush["usd"]); Settings::getInstance()->setHUSHPrice( hush["usd"] ); } if (hush["eur"] >= 0) { qDebug() << "HUSH = €" << QString::number((double)hush["eur"]); Settings::getInstance()->setEURPrice(hush["eur"]); } if (hush["btc"] >= 0) { qDebug() << "HUSH = BTC" << QString::number((double)hush["btc"]); Settings::getInstance()->setBTCPrice( hush["btc"]); } if (hush["cny"] >= 0) { qDebug() << "HUSH = CNY" << QString::number((double)hush["cny"]); Settings::getInstance()->setCNYPrice( hush["cny"]); } if (hush["rub"] >= 0) { qDebug() << "HUSH = RUB" << QString::number((double)hush["rub"]); Settings::getInstance()->setRUBPrice( hush["rub"]); } if (hush["cad"] >= 0) { qDebug() << "HUSH = CAD" << QString::number((double)hush["cad"]); Settings::getInstance()->setCADPrice( hush["cad"]); } if (hush["sgd"] >= 0) { qDebug() << "HUSH = SGD" << QString::number((double)hush["sgd"]); Settings::getInstance()->setSGDPrice( hush["sgd"]); } if (hush["chf"] >= 0) { qDebug() << "HUSH = CHF" << QString::number((double)hush["chf"]); Settings::getInstance()->setCHFPrice( hush["chf"]); } if (hush["inr"] >= 0) { qDebug() << "HUSH = INR" << QString::number((double)hush["inr"]); Settings::getInstance()->setINRPrice( hush["inr"]); } if (hush["gbp"] >= 0) { qDebug() << "HUSH = GBP" << QString::number((double)hush["gbp"]); Settings::getInstance()->setGBPPrice( hush["gbp"]); } if (hush["aud"] >= 0) { qDebug() << "HUSH = AUD" << QString::number((double)hush["aud"]); Settings::getInstance()->setAUDPrice( hush["aud"]); } if (hush["btc_24h_vol"] >= 0) { qDebug() << "HUSH = usd_24h_vol" << QString::number((double)hush["usd_24h_vol"]); Settings::getInstance()->setUSDVolume( hush["usd_24h_vol"]); } if (hush["btc_24h_vol"] >= 0) { qDebug() << "HUSH = euro_24h_vol" << QString::number((double)hush["eur_24h_vol"]); Settings::getInstance()->setEURVolume( hush["eur_24h_vol"]); } if (hush["btc_24h_vol"] >= 0) { qDebug() << "HUSH = btc_24h_vol" << QString::number((double)hush["btc_24h_vol"]); Settings::getInstance()->setBTCVolume( hush["btc_24h_vol"]); } if (hush["cny_24h_vol"] >= 0) { qDebug() << "HUSH = cny_24h_vol" << QString::number((double)hush["cny_24h_vol"]); Settings::getInstance()->setCNYVolume( hush["cny_24h_vol"]); } if (hush["rub_24h_vol"] >= 0) { qDebug() << "HUSH = rub_24h_vol" << QString::number((double)hush["rub_24h_vol"]); Settings::getInstance()->setRUBVolume( hush["rub_24h_vol"]); } if (hush["cad_24h_vol"] >= 0) { qDebug() << "HUSH = cad_24h_vol" << QString::number((double)hush["cad_24h_vol"]); Settings::getInstance()->setCADVolume( hush["cad_24h_vol"]); } if (hush["sgd_24h_vol"] >= 0) { qDebug() << "HUSH = sgd_24h_vol" << QString::number((double)hush["sgd_24h_vol"]); Settings::getInstance()->setSGDVolume( hush["sgd_24h_vol"]); } if (hush["chf_24h_vol"] >= 0) { qDebug() << "HUSH = chf_24h_vol" << QString::number((double)hush["chf_24h_vol"]); Settings::getInstance()->setCHFVolume( hush["chf_24h_vol"]); } if (hush["inr_24h_vol"] >= 0) { qDebug() << "HUSH = inr_24h_vol" << QString::number((double)hush["inr_24h_vol"]); Settings::getInstance()->setINRVolume( hush["inr_24h_vol"]); } if (hush["gbp_24h_vol"] >= 0) { qDebug() << "HUSH = gbp_24h_vol" << QString::number((double)hush["gbp_24h_vol"]); Settings::getInstance()->setGBPVolume( hush["gbp_24h_vol"]); } if (hush["aud_24h_vol"] >= 0) { qDebug() << "HUSH = aud_24h_vol" << QString::number((double)hush["aud_24h_vol"]); Settings::getInstance()->setAUDVolume( hush["aud_24h_vol"]); } if (hush["usd_market_cap"] >= 0) { qDebug() << "HUSH = usd_market_cap" << QString::number((double)hush["usd_market_cap"]); Settings::getInstance()->setUSDCAP( hush["usd_market_cap"]); } if (hush["eur_market_cap"] >= 0) { qDebug() << "HUSH = eur_market_cap" << QString::number((double)hush["eur_market_cap"]); Settings::getInstance()->setEURCAP( hush["eur_market_cap"]); } if (hush["btc_market_cap"] >= 0) { qDebug() << "HUSH = btc_market_cap" << QString::number((double)hush["btc_market_cap"]); Settings::getInstance()->setBTCCAP( hush["btc_market_cap"]); } if (hush["cny_market_cap"] >= 0) { qDebug() << "HUSH = cny_market_cap" << QString::number((double)hush["cny_market_cap"]); Settings::getInstance()->setCNYCAP( hush["cny_market_cap"]); } if (hush["rub_market_cap"] >= 0) { qDebug() << "HUSH = rub_market_cap" << QString::number((double)hush["rub_market_cap"]); Settings::getInstance()->setRUBCAP( hush["rub_market_cap"]); } if (hush["cad_market_cap"] >= 0) { qDebug() << "HUSH = cad_market_cap" << QString::number((double)hush["cad_market_cap"]); Settings::getInstance()->setCADCAP( hush["cad_market_cap"]); } if (hush["sgd_market_cap"] >= 0) { qDebug() << "HUSH = sgd_market_cap" << QString::number((double)hush["sgd_market_cap"]); Settings::getInstance()->setSGDCAP( hush["sgd_market_cap"]); } if (hush["chf_market_cap"] >= 0) { qDebug() << "HUSH = chf_market_cap" << QString::number((double)hush["chf_market_cap"]); Settings::getInstance()->setCHFCAP( hush["chf_market_cap"]); } if (hush["inr_market_cap"] >= 0) { qDebug() << "HUSH = inr_market_cap" << QString::number((double)hush["inr_market_cap"]); Settings::getInstance()->setINRCAP( hush["inr_market_cap"]); } if (hush["gbp_market_cap"] >= 0) { qDebug() << "HUSH = gbp_market_cap" << QString::number((double)hush["gbp_market_cap"]); Settings::getInstance()->setGBPCAP( hush["gbp_market_cap"]); } if (hush["aud_market_cap"] >= 0) { qDebug() << "HUSH = aud_market_cap" << QString::number((double)hush["aud_market_cap"]); Settings::getInstance()->setAUDCAP( hush["aud_market_cap"]); } return; } catch (const std::exception& e) { // If anything at all goes wrong, just set the price to 0 and move on. qDebug() << QString("Caught something nasty: ") << e.what(); } // If nothing, then set the price to 0; Settings::getInstance()->setHUSHPrice(0); Settings::getInstance()->setEURPrice(0); Settings::getInstance()->setBTCPrice(0); Settings::getInstance()->setCNYPrice(0); Settings::getInstance()->setRUBPrice(0); Settings::getInstance()->setCADPrice(0); Settings::getInstance()->setSGDPrice(0); Settings::getInstance()->setCHFPrice(0); Settings::getInstance()->setGBPPrice(0); Settings::getInstance()->setAUDPrice(0); Settings::getInstance()->setINRPrice(0); Settings::getInstance()->setBTCVolume(0); Settings::getInstance()->setUSDVolume(0); Settings::getInstance()->setEURVolume(0); Settings::getInstance()->setBTCVolume(0); Settings::getInstance()->setCNYVolume(0); Settings::getInstance()->setRUBVolume(0); Settings::getInstance()->setCADVolume(0); Settings::getInstance()->setINRVolume(0); Settings::getInstance()->setSGDVolume(0); Settings::getInstance()->setCHFVolume(0); Settings::getInstance()->setGBPVolume(0); Settings::getInstance()->setAUDVolume(0); Settings::getInstance()->setUSDCAP(0); Settings::getInstance()->setEURCAP(0); Settings::getInstance()->setBTCCAP(0); Settings::getInstance()->setCNYCAP(0); Settings::getInstance()->setRUBCAP(0); Settings::getInstance()->setCADCAP(0); Settings::getInstance()->setINRCAP(0); Settings::getInstance()->setSGDCAP(0); Settings::getInstance()->setCHFCAP(0); Settings::getInstance()->setGBPCAP(0); Settings::getInstance()->setAUDCAP(0); }); } void Controller::shutdownhushd() { // Save the wallet and exit the lightclient library cleanly. if (!zrpc) { zrpc = new LiteInterface(); // qDebug() << __func__ << ": created new rpc connection zrpc=" << zrpc; } if (zrpc && zrpc->haveConnection()) { QDialog d(main); Ui_ConnectionDialog connD; connD.setupUi(&d); auto theme = Settings::getInstance()->get_theme_name(); auto size = QSize(512,512); if (theme == "Dark" || theme == "Midnight") { QMovie *movie2 = new QMovie(":/img/res/silentdragonlite-animated-startup-dark.gif");; movie2->setScaledSize(size); qDebug() << "Animation dark loaded"; connD.topIcon->setMovie(movie2); movie2->start(); connD.status->setText(QObject::tr("Please wait for SilentDragonLite to exit")); connD.statusDetail->setText(QObject::tr("It may take several minutes")); } else { QMovie *movie1 = new QMovie(":/img/res/silentdragonlite-animated-startup-dark.gif");; movie1->setScaledSize(size); qDebug() << "Animation light loaded"; connD.topIcon->setMovie(movie1); movie1->start(); connD.status->setText(QObject::tr("Please wait for SilentDragonLite to exit")); connD.statusDetail->setText(QObject::tr("It may take several minutes")); } bool finished = false; zrpc->saveWallet([&] (json) { if (!finished) d.accept(); finished = true; qDebug() << __func__ << ": saveWallet finished"; }); if (!finished) d.exec(); } else { qDebug() << __func__ << ": No zrpc object, unclean shutdown and unable to call saveWallet!"; } } /** * Get a Sapling address from the user's wallet */ QString Controller::getDefaultSaplingAddress() { for (QString addr: model->getAllZAddresses()) { if (Settings::getInstance()->isSaplingAddress(addr)) return addr; } return QString(); } QString Controller::getDefaultTAddress() { if (model->getAllTAddresses().length() > 0) return model->getAllTAddresses().at(0); else return QString(); } void Controller::fetchAndProcessUnspentNotes() { zrpc->fetchUnspent([=] (json reply) { if (reply.find("unspent_notes") == reply.end() || !reply["unspent_notes"].is_array()) { qDebug() << "Fehler: 'unspent_notes' fehlt oder ist kein Array"; return; } int spendableNotesCount = 0; std::map addressValues; std::string addressWithMaxValue; int maxValue = 0; for (const auto& note : reply["unspent_notes"]) { if (note.find("spendable") != note.end() && note.find("value") != note.end() && note["spendable"].is_boolean() && note["value"].is_number_integer()) { if (note["spendable"] && note["value"] >= 10000) { spendableNotesCount++; } std::string address = note["address"]; int value = note["value"]; addressValues[address] += value; if (addressValues[address] > maxValue) { maxValue = addressValues[address]; addressWithMaxValue = address; } } } NoteCountDataStore::getInstance()->setSpendableNotesCount(spendableNotesCount); if (!addressWithMaxValue.empty()) { NoteCountDataStore::getInstance()->setAddressWithMaxValue(QString::fromStdString(addressWithMaxValue), maxValue); } }); }