#include "mainwindow.h" #include "addressbook.h" #include "ui_mainwindow.h" #include "ui_addressbook.h" #include "ui_zboard.h" #include "ui_privkey.h" #include "ui_about.h" #include "ui_settings.h" #include "ui_turnstile.h" #include "ui_turnstileprogress.h" #include "rpc.h" #include "balancestablemodel.h" #include "settings.h" #include "turnstile.h" #include "senttxstore.h" #include "connection.h" #include "precompiled.h" using json = nlohmann::json; MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); // Status Bar setupStatusBar(); // Settings editor setupSettingsModal(); // Set up exit action QObject::connect(ui->actionExit, &QAction::triggered, this, &MainWindow::close); // Set up donate action QObject::connect(ui->actionDonate, &QAction::triggered, this, &MainWindow::donate); // Set up check for updates action QObject::connect(ui->actionCheck_for_Updates, &QAction::triggered, [=] () { QDesktopServices::openUrl(QUrl("https://github.com/adityapk00/zec-qt-wallet/releases")); }); // Import Private Key QObject::connect(ui->actionImport_Private_Key, &QAction::triggered, this, &MainWindow::importPrivKey); // Export All Private Keys QObject::connect(ui->actionExport_All_Private_Keys, &QAction::triggered, this, &MainWindow::exportAllKeys); // z-Board.net QObject::connect(ui->actionz_board_net, &QAction::triggered, this, &MainWindow::postToZBoard); // Address Book QObject::connect(ui->action_Address_Book, &QAction::triggered, this, &MainWindow::addressBook); // Set up about action QObject::connect(ui->actionAbout, &QAction::triggered, [=] () { QDialog aboutDialog(this); Ui_about about; about.setupUi(&aboutDialog); Settings::saveRestore(&aboutDialog); QString version = QString("Version ") % QString(APP_VERSION) % " (" % QString(__DATE__) % ")"; about.versionLabel->setText(version); aboutDialog.exec(); }); // Initialize to the balances tab ui->tabWidget->setCurrentIndex(0); setupSendTab(); setupTransactionsTab(); setupRecieveTab(); setupBalancesTab(); setupTurnstileDialog(); setupZcashdTab(); rpc = new RPC(this); restoreSavedStates(); } void MainWindow::restoreSavedStates() { QSettings s; restoreGeometry(s.value("geometry").toByteArray()); ui->balancesTable->horizontalHeader()->restoreState(s.value("baltablegeometry").toByteArray()); ui->transactionsTable->horizontalHeader()->restoreState(s.value("tratablegeometry").toByteArray()); } void MainWindow::closeEvent(QCloseEvent* event) { QSettings s; s.setValue("geometry", saveGeometry()); s.setValue("baltablegeometry", ui->balancesTable->horizontalHeader()->saveState()); s.setValue("tratablegeometry", ui->transactionsTable->horizontalHeader()->saveState()); // Let the RPC know to shutdown any running service. rpc->shutdownZcashd(); // Bubble up QMainWindow::closeEvent(event); } void MainWindow::turnstileProgress() { Ui_TurnstileProgress progress; QDialog d(this); progress.setupUi(&d); Settings::saveRestore(&d); QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxWarning); progress.msgIcon->setPixmap(icon.pixmap(64, 64)); bool migrationFinished = false; auto fnUpdateProgressUI = [=, &migrationFinished] () mutable { // Get the plan progress if (rpc->getTurnstile()->isMigrationPresent()) { auto curProgress = rpc->getTurnstile()->getPlanProgress(); progress.progressTxt->setText(QString::number(curProgress.step) % QString(" / ") % QString::number(curProgress.totalSteps)); progress.progressBar->setValue(100 * curProgress.step / curProgress.totalSteps); auto nextTxBlock = curProgress.nextBlock - Settings::getInstance()->getBlockNumber(); progress.fromAddr->setText(curProgress.from); progress.toAddr->setText(curProgress.to); if (curProgress.step == curProgress.totalSteps) { migrationFinished = true; auto txt = QString("Turnstile migration finished"); if (curProgress.hasErrors) { txt = txt + ". There were some errors.\n\nYour funds are all in your wallet, so you should be able to finish moving them manually."; } progress.nextTx->setText(txt); } else { progress.nextTx->setText(QString("Next transaction in ") % QString::number(nextTxBlock < 0 ? 0 : nextTxBlock) % " blocks via " % curProgress.via % "\n" % (nextTxBlock <= 0 ? "(waiting for confirmations)" : "")); } } else { progress.progressTxt->setText(""); progress.progressBar->setValue(0); progress.nextTx->setText("No turnstile migration is in progress"); } }; QTimer progressTimer(this); QObject::connect(&progressTimer, &QTimer::timeout, fnUpdateProgressUI); progressTimer.start(Settings::updateSpeed); fnUpdateProgressUI(); auto curProgress = rpc->getTurnstile()->getPlanProgress(); // Abort button if (curProgress.step != curProgress.totalSteps) progress.buttonBox->button(QDialogButtonBox::Discard)->setText("Abort"); else progress.buttonBox->button(QDialogButtonBox::Discard)->setVisible(false); QObject::connect(progress.buttonBox->button(QDialogButtonBox::Discard), &QPushButton::clicked, [&] () { if (curProgress.step != curProgress.totalSteps) { auto abort = QMessageBox::warning(this, "Are you sure you want to Abort?", "Are you sure you want to abort the migration?\nAll further transactions will be cancelled.\nAll your funds are still in your wallet.", QMessageBox::Yes, QMessageBox::No); if (abort == QMessageBox::Yes) { rpc->getTurnstile()->removeFile(); d.close(); ui->statusBar->showMessage("Automatic Sapling turnstile migration aborted."); } } }); d.exec(); if (migrationFinished || curProgress.step == curProgress.totalSteps) { // Finished, so delete the file rpc->getTurnstile()->removeFile(); } } void MainWindow::turnstileDoMigration(QString fromAddr) { // Return if there is no connection if (rpc->getAllZAddresses() == nullptr) return; Ui_Turnstile turnstile; QDialog d(this); turnstile.setupUi(&d); Settings::saveRestore(&d); QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxInformation); turnstile.msgIcon->setPixmap(icon.pixmap(64, 64)); auto fnGetAllSproutBalance = [=] () { double bal = 0; for (auto addr : *rpc->getAllZAddresses()) { if (Settings::getInstance()->isSproutAddress(addr)) { bal += rpc->getAllBalances()->value(addr); } } return bal; }; //turnstile.migrateZaddList->addItem("All Sprout z-Addrs"); turnstile.fromBalance->setText(Settings::getInstance()->getZECUSDDisplayFormat(fnGetAllSproutBalance())); for (auto addr : *rpc->getAllZAddresses()) { if (Settings::getInstance()->isSaplingAddress(addr)) { turnstile.migrateTo->addItem(addr); } else { turnstile.migrateZaddList->addItem(addr); } } auto fnUpdateSproutBalance = [=] (QString addr) { double bal = 0; if (addr.startsWith("All")) { bal = fnGetAllSproutBalance(); } else { bal = rpc->getAllBalances()->value(addr); } auto balTxt = Settings::getInstance()->getZECUSDDisplayFormat(bal); if (bal < Turnstile::minMigrationAmount) { turnstile.fromBalance->setStyleSheet("color: red;"); turnstile.fromBalance->setText(balTxt % " [You need at least " % Settings::getInstance()->getZECDisplayFormat(Turnstile::minMigrationAmount) % " for automatic migration]"); turnstile.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); } else { turnstile.fromBalance->setStyleSheet(""); turnstile.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); turnstile.fromBalance->setText(balTxt); } }; if (!fromAddr.isEmpty()) turnstile.migrateZaddList->setCurrentText(fromAddr); fnUpdateSproutBalance(turnstile.migrateZaddList->currentText()); // Combo box selection event QObject::connect(turnstile.migrateZaddList, &QComboBox::currentTextChanged, fnUpdateSproutBalance); // Privacy level combobox // Num tx over num blocks QList> privOptions; privOptions.push_back(std::make_tuple(3, 576)); privOptions.push_back(std::make_tuple(5, 1152)); privOptions.push_back(std::make_tuple(10, 2304)); QObject::connect(turnstile.privLevel, QOverload::of(&QComboBox::currentIndexChanged), [=] (auto idx) { // Update the fees turnstile.minerFee->setText( Settings::getInstance()->getZECUSDDisplayFormat(std::get<0>(privOptions[idx]) * Settings::getMinerFee())); }); for (auto i : privOptions) { turnstile.privLevel->addItem(QString::number(std::get<0>(i)) % " tx over " % QString::number(std::get<1>(i)) % " blocks (" % QString::number((int)(std::get<1>(i) / 24 / 24)) % " days)" // 24 blks/hr * 24 hrs per day ); } turnstile.buttonBox->button(QDialogButtonBox::Ok)->setText("Start"); if (d.exec() == QDialog::Accepted) { auto privLevel = privOptions[turnstile.privLevel->currentIndex()]; rpc->getTurnstile()->planMigration( turnstile.migrateZaddList->currentText(), turnstile.migrateTo->currentText(), std::get<0>(privLevel), std::get<1>(privLevel)); QMessageBox::information(this, "Backup your wallet.dat", "The migration will now start. You can check progress in the File -> Sapling Turnstile menu.\n\nYOU MUST BACKUP YOUR wallet.dat NOW!\n\nNew Addresses have been added to your wallet which will be used for the migration.", QMessageBox::Ok); } } void MainWindow::setupTurnstileDialog() { // Turnstile migration QObject::connect(ui->actionTurnstile_Migration, &QAction::triggered, [=] () { // If there is current migration that is present, show the progress button if (rpc->getTurnstile()->isMigrationPresent()) turnstileProgress(); else turnstileDoMigration(); }); } void MainWindow::setupStatusBar() { // Status Bar loadingLabel = new QLabel(); loadingMovie = new QMovie(":/icons/res/loading.gif"); loadingMovie->setScaledSize(QSize(32, 16)); loadingMovie->start(); loadingLabel->setAttribute(Qt::WA_NoSystemBackground); loadingLabel->setMovie(loadingMovie); ui->statusBar->addPermanentWidget(loadingLabel); loadingLabel->setVisible(false); // Custom status bar menu ui->statusBar->setContextMenuPolicy(Qt::CustomContextMenu); QObject::connect(ui->statusBar, &QStatusBar::customContextMenuRequested, [=](QPoint pos) { auto msg = ui->statusBar->currentMessage(); QMenu menu(this); if (!msg.isEmpty() && msg.startsWith(Settings::txidStatusMessage)) { auto txid = msg.split(":")[1].trimmed(); menu.addAction("Copy txid", [=]() { QGuiApplication::clipboard()->setText(txid); }); menu.addAction("View tx on block explorer", [=]() { QString url; if (Settings::getInstance()->isTestnet()) { url = "https://explorer.testnet.z.cash/tx/" + txid; } else { url = "https://explorer.zcha.in/transactions/" + txid; } QDesktopServices::openUrl(QUrl(url)); }); } menu.addAction("Refresh", [=]() { rpc->refresh(true); }); QPoint gpos(mapToGlobal(pos).x(), mapToGlobal(pos).y() + this->height() - ui->statusBar->height()); menu.exec(gpos); }); statusLabel = new QLabel(); ui->statusBar->addPermanentWidget(statusLabel); statusIcon = new QLabel(); ui->statusBar->addPermanentWidget(statusIcon); } void MainWindow::setupSettingsModal() { // Set up File -> Settings action QObject::connect(ui->actionSettings, &QAction::triggered, [=]() { QDialog settingsDialog(this); Ui_Settings settings; settings.setupUi(&settingsDialog); Settings::saveRestore(&settingsDialog); // Setup save sent check box QObject::connect(settings.chkSaveTxs, &QCheckBox::stateChanged, [=](auto checked) { Settings::getInstance()->setSaveZtxs(checked); }); // Setup clear button QObject::connect(settings.btnClearSaved, &QCheckBox::clicked, [=]() { if (QMessageBox::warning(this, "Clear saved history?", "Shielded z-Address transactions are stored locally in your wallet, outside zcashd. You may delete this saved information safely any time for your privacy.\nDo you want to delete the saved shielded transactions now?", QMessageBox::Yes, QMessageBox::Cancel)) { SentTxStore::deleteHistory(); // Reload after the clear button so existing txs disappear rpc->refresh(true); } }); // Save sent transactions settings.chkSaveTxs->setChecked(Settings::getInstance()->getSaveZtxs()); // Connection Settings QIntValidator validator(0, 65535); settings.port->setValidator(&validator); // If values are coming from zcash.conf, then disable all the fields auto zcashConfLocation = Settings::getInstance()->getZcashdConfLocation(); if (!zcashConfLocation.isEmpty()) { settings.confMsg->setText("Settings are being read from \n" + zcashConfLocation); settings.hostname->setEnabled(false); settings.port->setEnabled(false); settings.rpcuser->setEnabled(false); settings.rpcpassword->setEnabled(false); } else { // Load current values into the dialog auto conf = Settings::getInstance()->getSettings(); settings.hostname->setText(conf.host); settings.port->setText(conf.port); settings.rpcuser->setText(conf.rpcuser); settings.rpcpassword->setText(conf.rpcpassword); settings.confMsg->setText("No local zcash.conf found. Please configure connection manually."); settings.hostname->setEnabled(true); settings.port->setEnabled(true); settings.rpcuser->setEnabled(true); settings.rpcpassword->setEnabled(true); } // Connection tab by default settings.tabWidget->setCurrentIndex(0); if (settingsDialog.exec() == QDialog::Accepted) { if (zcashConfLocation.isEmpty()) { // Save settings Settings::getInstance()->saveSettings( settings.hostname->text(), settings.port->text(), settings.rpcuser->text(), settings.rpcpassword->text()); auto cl = new ConnectionLoader(this, rpc); cl->loadConnection(); } } }); } void MainWindow::addressBook() { // Check to see if there is a target. QRegExp re("Address[0-9]+", Qt::CaseInsensitive); for (auto target: ui->sendToWidgets->findChildren(re)) { if (target->hasFocus()) { AddressBook::open(this, target); return; } }; // If there was no target, then just run with no target. AddressBook::open(this); } void MainWindow::donate() { // Set up a donation to me :) ui->Address1->setText(Settings::getDonationAddr( Settings::getInstance()->isSaplingAddress(ui->inputsCombo->currentText()))); ui->Address1->setCursorPosition(0); ui->Amount1->setText("0.01"); ui->MemoTxt1->setText("Thanks for supporting zec-qt-wallet!"); ui->statusBar->showMessage("Donate 0.01 " % Settings::getTokenName() % " to support zec-qt-wallet"); // And switch to the send tab. ui->tabWidget->setCurrentIndex(1); } void MainWindow::postToZBoard() { QDialog d(this); Ui_zboard zb; zb.setupUi(&d); Settings::saveRestore(&d); if (rpc->getConnection() == nullptr) return; // Fill the from field with sapling addresses. for (auto i = rpc->getAllBalances()->keyBegin(); i != rpc->getAllBalances()->keyEnd(); i++) { if (Settings::getInstance()->isSaplingAddress(*i) && rpc->getAllBalances()->value(*i) > 0) { zb.fromAddr->addItem(*i); } } QMap topics; // Insert the main topic automatically topics.insert("#Main_Area", Settings::getZboardAddr()); zb.topicsList->addItem(topics.firstKey()); // Then call the API to get topics, and if it returns successfully, then add the rest of the topics rpc->getZboardTopics([&](QMap topicsMap) { for (auto t : topicsMap.keys()) { topics.insert(t, topicsMap[t]); zb.topicsList->addItem(t); } }); // Testnet warning if (Settings::getInstance()->isTestnet()) { zb.testnetWarning->setText("You are on testnet, your post won't actually appear on z-board.net"); } else { zb.testnetWarning->setText(""); } QRegExpValidator v(QRegExp("^[a-zA-Z0-9_]{3,20}$"), zb.postAs); zb.postAs->setValidator(&v); zb.feeAmount->setText(Settings::getInstance()->getZECUSDDisplayFormat(Settings::getZboardAmount() + Settings::getMinerFee())); auto fnBuildNameMemo = [=]() -> QString { auto memo = zb.memoTxt->toPlainText().trimmed(); if (!zb.postAs->text().trimmed().isEmpty()) memo = zb.postAs->text().trimmed() + ":: " + memo; return memo; }; auto fnUpdateMemoSize = [=]() { QString txt = fnBuildNameMemo(); zb.memoSize->setText(QString::number(txt.toUtf8().size()) + "/512"); if (txt.toUtf8().size() <= 512) { // Everything is fine zb.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); zb.memoSize->setStyleSheet(""); } else { // Overweight zb.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); zb.memoSize->setStyleSheet("color: red;"); } // Disallow blank memos if (zb.memoTxt->toPlainText().trimmed().isEmpty()) { zb.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); } else { zb.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); } }; // Memo text changed QObject::connect(zb.memoTxt, &QPlainTextEdit::textChanged, fnUpdateMemoSize); QObject::connect(zb.postAs, &QLineEdit::textChanged, fnUpdateMemoSize); zb.memoTxt->setFocus(); fnUpdateMemoSize(); if (d.exec() == QDialog::Accepted) { // Create a transaction. Tx tx; // Send from your first sapling address that has a balance. tx.fromAddr = zb.fromAddr->currentText(); if (tx.fromAddr.isEmpty()) { QMessageBox::critical(this, "Error Posting Message", "You need a sapling address with available balance to post", QMessageBox::Ok); return; } auto memo = zb.memoTxt->toPlainText().trimmed(); if (!zb.postAs->text().trimmed().isEmpty()) memo = zb.postAs->text().trimmed() + ":: " + memo; auto toAddr = topics[zb.topicsList->currentText()]; tx.toAddrs.push_back(ToFields{ toAddr, Settings::getZboardAmount(), memo, memo.toUtf8().toHex() }); tx.fee = Settings::getMinerFee(); json params = json::array(); rpc->fillTxJsonParams(params, tx); std::cout << std::setw(2) << params << std::endl; // And send the Tx rpc->sendZTransaction(params, [=](const json& reply) { QString opid = QString::fromStdString(reply.get()); ui->statusBar->showMessage("Computing Tx: " % opid); // And then start monitoring the transaction rpc->addNewTxToWatch(tx, opid); }); } } void MainWindow::doImport(QList* keys) { if (keys->isEmpty()) { delete keys; QMessageBox::information(this, "Imported", "The keys were imported. It may take several minutes to rescan the blockchain. Until then, functionality may be limited", QMessageBox::Ok); ui->statusBar->showMessage("Private key import rescan finished"); return; } // Pop the first key QString key = keys->first(); keys->pop_front(); bool rescan = keys->isEmpty(); if (key.startsWith("S") || key.startsWith("secret")) { // Z key rpc->importZPrivKey(key, rescan, [=] (auto) { this->doImport(keys); }); } else { rpc->importTPrivKey(key, rescan, [=] (auto) { this->doImport(keys); }); } } void MainWindow::importPrivKey() { QDialog d(this); Ui_PrivKey pui; pui.setupUi(&d); Settings::saveRestore(&d); pui.buttonBox->button(QDialogButtonBox::Save)->setVisible(false); pui.helpLbl->setText(QString() % "Please paste your private keys (z-Addr or t-Addr) here, one per line.\n" % "The keys will be imported into your connected zcashd node"); if (d.exec() == QDialog::Accepted && !pui.privKeyTxt->toPlainText().trimmed().isEmpty()) { auto rawkeys = pui.privKeyTxt->toPlainText().trimmed().split("\n"); QList keysTmp; // Filter out all the empty keys. std::copy_if(rawkeys.begin(), rawkeys.end(), std::back_inserter(keysTmp), [=] (auto key) { return !key.startsWith("#") && !key.trimmed().isEmpty(); }); auto keys = new QList(); std::transform(keysTmp.begin(), keysTmp.end(), std::back_inserter(*keys), [=](auto key) { return key.trimmed().split(" ")[0]; }); // Start the import. The function takes ownership of keys doImport(keys); } } void MainWindow::exportAllKeys() { QDialog d(this); Ui_PrivKey pui; pui.setupUi(&d); // Make the window big by default auto ps = this->geometry(); QMargins margin = QMargins() + 50; d.setGeometry(ps.marginsRemoved(margin)); Settings::saveRestore(&d); pui.privKeyTxt->setPlainText("Loading..."); pui.privKeyTxt->setReadOnly(true); pui.privKeyTxt->setLineWrapMode(QPlainTextEdit::LineWrapMode::NoWrap); pui.helpLbl->setText("These are all the private keys for all the addresses in your wallet"); // Disable the save button until it finishes loading pui.buttonBox->button(QDialogButtonBox::Save)->setEnabled(false); pui.buttonBox->button(QDialogButtonBox::Ok)->setVisible(false); // Wire up save button QObject::connect(pui.buttonBox->button(QDialogButtonBox::Save), &QPushButton::clicked, [=] () { QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"), "zcash-all-privatekeys.txt"); QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { QMessageBox::information(this, tr("Unable to open file"), file.errorString()); return; } QTextStream out(&file); out << pui.privKeyTxt->toPlainText(); }); // Call the API auto isDialogAlive = std::make_shared(true); rpc->getAllPrivKeys([=] (auto privKeys) { // Check to see if we are still showing. if (! *isDialogAlive.get()) return; QString allKeysTxt; for (auto keypair : privKeys) { allKeysTxt = allKeysTxt % keypair.second % " # addr=" % keypair.first % "\n"; } pui.privKeyTxt->setPlainText(allKeysTxt); pui.buttonBox->button(QDialogButtonBox::Save)->setEnabled(true); }); d.exec(); *isDialogAlive.get() = false; } void MainWindow::setupBalancesTab() { ui->unconfirmedWarning->setVisible(false); // Setup context menu on balances tab ui->balancesTable->setContextMenuPolicy(Qt::CustomContextMenu); QObject::connect(ui->balancesTable, &QTableView::customContextMenuRequested, [=] (QPoint pos) { QModelIndex index = ui->balancesTable->indexAt(pos); if (index.row() < 0) return; index = index.sibling(index.row(), 0); auto addr = ui->balancesTable->model()->data(index).toString(); QMenu menu(this); menu.addAction("Copy address", [=] () { QClipboard *clipboard = QGuiApplication::clipboard(); clipboard->setText(addr); ui->statusBar->showMessage("Copied to clipboard", 3 * 1000); }); menu.addAction("Get private key", [=] () { auto fnCB = [=] (const json& reply) { auto privKey = QString::fromStdString(reply.get()); QDialog d(this); Ui_PrivKey pui; pui.setupUi(&d); pui.helpLbl->setText("Private Key:"); pui.privKeyTxt->setPlainText(privKey); pui.privKeyTxt->setReadOnly(true); pui.privKeyTxt->selectAll(); pui.buttonBox->button(QDialogButtonBox::Save)->setVisible(false); pui.buttonBox->button(QDialogButtonBox::Ok)->setVisible(false); d.exec(); }; if (Settings::getInstance()->isZAddress(addr)) rpc->getZPrivKey(addr, fnCB); else rpc->getTPrivKey(addr, fnCB); }); menu.addAction("Send from " % addr.left(40) % (addr.size() > 40 ? "..." : ""), [=]() { // Find the inputs combo for (int i = 0; i < ui->inputsCombo->count(); i++) { if (ui->inputsCombo->itemText(i).startsWith(addr)) { ui->inputsCombo->setCurrentIndex(i); break; } } // And switch to the send tab. ui->tabWidget->setCurrentIndex(1); }); if (addr.startsWith("t")) { menu.addAction("View on block explorer", [=] () { QString url; if (Settings::getInstance()->isTestnet()) { url = "https://explorer.testnet.z.cash/address/" + addr; } else { url = "https://explorer.zcha.in/accounts/" + addr; } QDesktopServices::openUrl(QUrl(url)); }); } if (Settings::getInstance()->isSproutAddress(addr)) { menu.addAction("Migrate to Sapling", [=] () { this->turnstileDoMigration(addr); }); } menu.exec(ui->balancesTable->viewport()->mapToGlobal(pos)); }); } void MainWindow::setupZcashdTab() { ui->zcashdlogo->setBasePixmap(QPixmap(":/img/res/zcashdlogo.gif")); } void MainWindow::setupTransactionsTab() { // Double click opens up memo if one exists QObject::connect(ui->transactionsTable, &QTableView::doubleClicked, [=] (auto index) { auto txModel = dynamic_cast(ui->transactionsTable->model()); QString memo = txModel->getMemo(index.row()); if (!memo.isEmpty()) { QMessageBox::information(this, "Memo", memo, QMessageBox::Ok); } }); // Set up context menu on transactions tab ui->transactionsTable->setContextMenuPolicy(Qt::CustomContextMenu); // Table right click QObject::connect(ui->transactionsTable, &QTableView::customContextMenuRequested, [=] (QPoint pos) { QModelIndex index = ui->transactionsTable->indexAt(pos); if (index.row() < 0) return; QMenu menu(this); auto txModel = dynamic_cast(ui->transactionsTable->model()); QString txid = txModel->getTxId(index.row()); QString memo = txModel->getMemo(index.row()); menu.addAction("Copy txid", [=] () { QGuiApplication::clipboard()->setText(txid); ui->statusBar->showMessage("Copied to clipboard", 3 * 1000); }); menu.addAction("View on block explorer", [=] () { QString url; if (Settings::getInstance()->isTestnet()) { url = "https://explorer.testnet.z.cash/tx/" + txid; } else { url = "https://explorer.zcha.in/transactions/" + txid; } QDesktopServices::openUrl(QUrl(url)); }); if (!memo.isEmpty()) { menu.addAction("View Memo", [=] () { QMessageBox::information(this, "Memo", memo, QMessageBox::Ok); }); } menu.exec(ui->transactionsTable->viewport()->mapToGlobal(pos)); }); } void MainWindow::addNewZaddr(bool sapling) { rpc->newZaddr(sapling, [=] (json reply) { QString addr = QString::fromStdString(reply.get()); // Make sure the RPC class reloads the z-addrs for future use rpc->refreshAddresses(); // Just double make sure the z-address is still checked if (( sapling && ui->rdioZSAddr->isChecked()) || (!sapling && ui->rdioZAddr->isChecked())) { ui->listRecieveAddresses->insertItem(0, addr); ui->listRecieveAddresses->setCurrentIndex(0); ui->statusBar->showMessage(QString::fromStdString("Created new zAddr") % (sapling ? "(Sapling)" : "(Sprout)"), 10 * 1000); } }); } // Adds sapling or sprout z-addresses to the combo box. Technically, returns a // lambda, which can be connected to the appropriate signal std::function MainWindow::addZAddrsToComboList(bool sapling) { return [=] (bool checked) { if (checked && this->rpc->getAllZAddresses() != nullptr) { auto addrs = this->rpc->getAllZAddresses(); ui->listRecieveAddresses->clear(); std::for_each(addrs->begin(), addrs->end(), [=] (auto addr) { if ( (sapling && Settings::getInstance()->isSaplingAddress(addr)) || (!sapling && !Settings::getInstance()->isSaplingAddress(addr))) ui->listRecieveAddresses->addItem(addr); }); // If z-addrs are empty, then create a new one. if (addrs->isEmpty()) { addNewZaddr(sapling); } } }; } void MainWindow::setupRecieveTab() { auto addNewTAddr = [=] () { rpc->newTaddr([=] (json reply) { QString addr = QString::fromStdString(reply.get()); // Just double make sure the t-address is still checked if (ui->rdioTAddr->isChecked()) { ui->listRecieveAddresses->insertItem(0, addr); ui->listRecieveAddresses->setCurrentIndex(0); ui->statusBar->showMessage("Created new t-Addr", 10 * 1000); } }); }; // Connect t-addr radio button QObject::connect(ui->rdioTAddr, &QRadioButton::toggled, [=] (bool checked) { // Whenever the t-address is selected, we generate a new address, because we don't // want to reuse t-addrs if (checked && this->rpc->getUTXOs() != nullptr) { auto utxos = this->rpc->getUTXOs(); ui->listRecieveAddresses->clear(); std::for_each(utxos->begin(), utxos->end(), [=] (auto& utxo) { auto addr = utxo.address; if (addr.startsWith("t") && ui->listRecieveAddresses->findText(addr) < 0) { ui->listRecieveAddresses->addItem(addr); } }); addNewTAddr(); } }); // zAddr toggle button, one for sprout and one for sapling QObject::connect(ui->rdioZAddr, &QRadioButton::toggled, addZAddrsToComboList(false)); QObject::connect(ui->rdioZSAddr, &QRadioButton::toggled, addZAddrsToComboList(true)); // Explicitly get new address button. QObject::connect(ui->btnRecieveNewAddr, &QPushButton::clicked, [=] () { if (ui->rdioZAddr->isChecked()) { addNewZaddr(false); } else if (ui->rdioZSAddr->isChecked()) { addNewZaddr(true); } else if (ui->rdioTAddr->isChecked()) { addNewTAddr(); } }); // Focus enter for the Receive Tab QObject::connect(ui->tabWidget, &QTabWidget::currentChanged, [=] (int tab) { if (tab == 2) { // Switched to receive tab, so update everything. // Hide Sapling radio button if Sapling is not active if (Settings::getInstance()->isSaplingActive()) { ui->rdioZSAddr->setVisible(true); ui->rdioZSAddr->setChecked(true); ui->rdioZAddr->setText("z-Addr(Sprout)"); } else { ui->rdioZSAddr->setVisible(false); ui->rdioZAddr->setChecked(true); ui->rdioZAddr->setText("z-Addr"); // Don't use the "Sprout" label if there's no Sapling } // And then select the first one ui->listRecieveAddresses->setCurrentIndex(0); } }); // Select item in address list QObject::connect(ui->listRecieveAddresses, QOverload::of(&QComboBox::currentIndexChanged), [=] (const QString& addr) { if (addr.isEmpty()) { // Draw empty stuff ui->txtRecieve->clear(); ui->qrcodeDisplay->clear(); return; } ui->txtRecieve->setPlainText(addr); ui->qrcodeDisplay->setAddress(addr); }); } MainWindow::~MainWindow() { delete ui; delete rpc; delete labelCompleter; delete loadingMovie; }