diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 71848c5..92116dd 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -82,8 +82,9 @@ MainWindow::MainWindow(QWidget *parent) : // Export transactions QObject::connect(ui->actionExport_transactions, &QAction::triggered, this, &MainWindow::exportTransactions); + // Z-board seems to have been abandoned // z-Board.net - QObject::connect(ui->actionz_board_net, &QAction::triggered, this, &MainWindow::postToZBoard); + // QObject::connect(ui->actionz_board_net, &QAction::triggered, this, &MainWindow::postToZBoard); // Validate Address QObject::connect(ui->actionValidate_Address, &QAction::triggered, this, &MainWindow::validateAddress); @@ -177,6 +178,11 @@ void MainWindow::restoreSavedStates() { ui->balancesTable->horizontalHeader()->restoreState(s.value("baltablegeometry").toByteArray()); ui->transactionsTable->horizontalHeader()->restoreState(s.value("tratablegeometry").toByteArray()); + + // Explicitly set the tx table resize headers, since some previous values may have made them + // non-expandable. + ui->transactionsTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Interactive); + ui->transactionsTable->horizontalHeader()->setSectionResizeMode(4, QHeaderView::Interactive); } void MainWindow::doClose() { @@ -637,8 +643,7 @@ void MainWindow::donate() { // Set up a donation to me :) clearSendForm(); - ui->Address1->setText(Settings::getDonationAddr( - Settings::getInstance()->isSaplingAddress(ui->inputsCombo->currentText()))); + ui->Address1->setText(Settings::getDonationAddr()); ui->Address1->setCursorPosition(0); ui->Amount1->setText("0.01"); ui->MemoTxt1->setText(tr("Thanks for supporting ZecWallet!")); @@ -710,12 +715,12 @@ void MainWindow::postToZBoard() { QMap topics; // Insert the main topic automatically - topics.insert("#Main_Area", Settings::getInstance()->isTestnet() ? Settings::getDonationAddr(true) : Settings::getZboardAddr()); + topics.insert("#Main_Area", Settings::getInstance()->isTestnet() ? Settings::getDonationAddr() : 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, Settings::getInstance()->isTestnet() ? Settings::getDonationAddr(true) : topicsMap[t]); + topics.insert(t, Settings::getInstance()->isTestnet() ? Settings::getDonationAddr() : topicsMap[t]); zb.topicsList->addItem(t); } }); @@ -791,20 +796,7 @@ void MainWindow::postToZBoard() { tx.fee = Settings::getMinerFee(); // And send the Tx - rpc->executeTransaction(tx, [=] (QString opid) { - ui->statusBar->showMessage(tr("Computing Tx: ") % opid); - }, - [=] (QString /*opid*/, 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(this, QObject::tr("Transaction Error"), errStr, QMessageBox::Ok); - }); + rpc->executeStandardUITransaction(tx); } } @@ -825,7 +817,7 @@ void MainWindow::doImport(QList* keys) { keys->pop_front(); bool rescan = keys->isEmpty(); - if (key.startsWith("S") || + if (key.startsWith("SK") || key.startsWith("secret")) { // Z key rpc->importZPrivKey(key, rescan, [=] (auto) { this->doImport(keys); }); } else { @@ -949,6 +941,16 @@ void MainWindow::importPrivKey() { return key.trimmed().split(" ")[0]; }); + // Special case. + // Sometimes, when importing from a paperwallet or such, the key is split by newlines, and might have + // been pasted like that. So check to see if the whole thing is one big private key + if (Settings::getInstance()->isValidSaplingPrivateKey(keys->join(""))) { + auto multiline = keys; + keys = new QList(); + keys->append(multiline->join("")); + delete multiline; + } + // Start the import. The function takes ownership of keys QTimer::singleShot(1, [=]() {doImport(keys);}); @@ -1304,6 +1306,9 @@ std::function MainWindow::addZAddrsToComboList(bool sapling) { return [=] (bool checked) { if (checked && this->rpc->getAllZAddresses() != nullptr) { auto addrs = this->rpc->getAllZAddresses(); + + // Save the current address, so we can update it later + auto zaddr = ui->listReceiveAddresses->currentText(); ui->listReceiveAddresses->clear(); std::for_each(addrs->begin(), addrs->end(), [=] (auto addr) { @@ -1315,6 +1320,10 @@ std::function MainWindow::addZAddrsToComboList(bool sapling) { } } }); + + if (!zaddr.isEmpty() && Settings::isZAddress(zaddr)) { + ui->listReceiveAddresses->setCurrentText(zaddr); + } // If z-addrs are empty, then create a new one. if (addrs->isEmpty()) { @@ -1508,6 +1517,10 @@ void MainWindow::setupReceiveTab() { void MainWindow::updateTAddrCombo(bool checked) { if (checked) { auto utxos = this->rpc->getUTXOs(); + + // Save the current address so we can restore it later + auto currentTaddr = ui->listReceiveAddresses->currentText(); + ui->listReceiveAddresses->clear(); // Maintain a set of addresses so we don't duplicate any, because we'll be adding @@ -1550,7 +1563,17 @@ void MainWindow::updateTAddrCombo(bool checked) { } } - // 4. Add a last, disabled item if there are remaining items + // 4. Add the previously selected t-address + if (!currentTaddr.isEmpty() && Settings::isTAddress(currentTaddr)) { + // Make sure the current taddr is in the list + if (!addrs.contains(currentTaddr)) { + auto bal = rpc->getAllBalances()->value(currentTaddr); + ui->listReceiveAddresses->addItem(currentTaddr, bal); + } + ui->listReceiveAddresses->setCurrentText(currentTaddr); + } + + // 5. Add a last, disabled item if there are remaining items if (allTaddrs->size() > addrs.size()) { auto num = QString::number(allTaddrs->size() - addrs.size()); ui->listReceiveAddresses->addItem("-- " + num + " more --", 0); diff --git a/src/mainwindow.ui b/src/mainwindow.ui index b148265..44287c1 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -1149,14 +1149,6 @@ &Export all private keys - - - &z-board.net - - - Ctrl+A, Ctrl+Z - - Address &book diff --git a/src/rpc.cpp b/src/rpc.cpp index c38be5b..cdf992c 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -27,8 +27,7 @@ RPC::RPC(MainWindow* main) { // Setup transactions table model transactionsTableModel = new TxTableModel(ui->transactionsTable); main->ui->transactionsTable->setModel(transactionsTableModel); - main->ui->transactionsTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); - + // Set up timer to refresh Price priceTimer = new QTimer(main); QObject::connect(priceTimer, &QTimer::timeout, [=]() { @@ -220,7 +219,7 @@ void RPC::importTPrivKey(QString addr, bool rescan, const std::function& cb) { - QString method = address.startsWith("z") ? "z_validateaddress" : "validateaddress"; + QString method = Settings::isZAddress(address) ? "z_validateaddress" : "validateaddress"; json payload = { {"jsonrpc", "1.0"}, @@ -963,6 +962,29 @@ void RPC::addNewTxToWatch(const QString& newOpid, WatchedTx wtx) { watchTxStatus(); } +/** + * Execute a transaction with the standard UI. i.e., standard status bar message and standard error + * handling + */ +void RPC::executeStandardUITransaction(Tx tx) { + executeTransaction(tx, + [=] (QString opid) { + ui->statusBar->showMessage(QObject::tr("Computing Tx: ") % opid); + }, + [=] (QString, 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 RPC::executeTransaction(Tx tx, diff --git a/src/rpc.h b/src/rpc.h index 9064b17..a6474fe 100644 --- a/src/rpc.h +++ b/src/rpc.h @@ -58,6 +58,8 @@ public: void refreshZECPrice(); void getZboardTopics(std::function)> cb); + void executeStandardUITransaction(Tx tx); + void executeTransaction(Tx tx, const std::function submitted, const std::function computed, diff --git a/src/sendtab.cpp b/src/sendtab.cpp index eedfce0..7487d60 100644 --- a/src/sendtab.cpp +++ b/src/sendtab.cpp @@ -512,7 +512,12 @@ Tx MainWindow::createTxFromSendPage() { // If address is sprout, then we can't send change to sapling, because of turnstile. sendChangeToSapling = sendChangeToSapling && !Settings::getInstance()->isSproutAddress(addr); - double amt = ui->sendToWidgets->findChild(QString("Amount") % QString::number(i+1))->text().trimmed().toDouble(); + QString amtStr = ui->sendToWidgets->findChild(QString("Amount") % QString::number(i+1))->text().trimmed(); + if (amtStr.isEmpty()) { + amtStr = "-1";; // The user didn't specify an amount + } + + double amt = amtStr.toDouble(); totalAmt += amt; QString memo = ui->sendToWidgets->findChild(QString("MemoTxt") % QString::number(i+1))->text().trimmed(); @@ -521,8 +526,7 @@ Tx MainWindow::createTxFromSendPage() { if (Settings::getInstance()->getAllowCustomFees()) { tx.fee = ui->minerFeeAmt->text().toDouble(); - } - else { + } else { tx.fee = Settings::getMinerFee(); } @@ -731,6 +735,8 @@ bool MainWindow::confirmTx(Tx tx, RecurringPaymentInfo* rpi) { // Send button clicked void MainWindow::sendButton() { + // Create a Tx from the values on the send tab. Note that this Tx object + // might not be valid yet. Tx tx = createTxFromSendPage(); QString error = doSendTxValidations(tx); @@ -810,7 +816,7 @@ QString MainWindow::doSendTxValidations(Tx tx) { // This technically shouldn't be possible, but issue #62 seems to have discovered a bug // somewhere, so just add a check to make sure. if (toAddr.amount < 0) { - return QString(tr("Amount '%1' is invalid!").arg(toAddr.amount)); + return QString(tr("Amount for address '%1' is invalid!").arg(toAddr.addr)); } } diff --git a/src/settings.cpp b/src/settings.cpp index 66bdef7..4682700 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -178,6 +178,7 @@ void Settings::saveRestore(QDialog* d) { void Settings::saveRestoreTableHeader(QTableView* table, QDialog* d, QString tablename) { table->horizontalHeader()->restoreState(QSettings().value(tablename).toByteArray()); + table->horizontalHeader()->setStretchLastSection(true); QObject::connect(d, &QDialog::finished, [=](auto) { QSettings().setValue(tablename, table->horizontalHeader()->saveState()); @@ -250,17 +251,12 @@ QString Settings::getTokenName() { } } -QString Settings::getDonationAddr(bool sapling) { +QString Settings::getDonationAddr() { if (Settings::getInstance()->isTestnet()) - if (sapling) return "ztestsapling1wn6889vznyu42wzmkakl2effhllhpe4azhu696edg2x6me4kfsnmqwpglaxzs7tmqsq7kudemp5"; - else - return "ztn6fYKBii4Fp4vbGhkPgrtLU4XjXp4ZBMZgShtopmDGbn1L2JLTYbBp2b7SSkNr9F3rQeNZ9idmoR7s4JCVUZ7iiM5byhF"; else - if (sapling) return "zs1gv64eu0v2wx7raxqxlmj354y9ycznwaau9kduljzczxztvs4qcl00kn2sjxtejvrxnkucw5xx9u"; - else - return "zcEgrceTwvoiFdEvPWcsJHAMrpLsprMF6aRJiQa3fan5ZphyXLPuHghnEPrEPRoEVzUy65GnMVyCTRdkT6BYBepnXh6NBYs"; + } bool Settings::addToZcashConf(QString confLocation, QString line) { @@ -320,13 +316,23 @@ double Settings::getZboardAmount() { QString Settings::getZboardAddr() { if (Settings::getInstance()->isTestnet()) { - return getDonationAddr(true); + return getDonationAddr(); } else { return "zs10m00rvkhfm4f7n23e4sxsx275r7ptnggx39ygl0vy46j9mdll5c97gl6dxgpk0njuptg2mn9w5s"; } } +bool Settings::isValidSaplingPrivateKey(QString pk) { + if (isTestnet()) { + QRegExp zspkey("^secret-extended-key-test[0-9a-z]{278}$", Qt::CaseInsensitive); + return zspkey.exactMatch(pk); + } else { + QRegExp zspkey("^secret-extended-key-main[0-9a-z]{278}$", Qt::CaseInsensitive); + return zspkey.exactMatch(pk); + } +} + bool Settings::isValidAddress(QString addr) { QRegExp zcexp("^z[a-z0-9]{94}$", Qt::CaseInsensitive); QRegExp zsexp("^z[a-z0-9]{77}$", Qt::CaseInsensitive); diff --git a/src/settings.h b/src/settings.h index 612fe4b..a0fbff5 100644 --- a/src/settings.h +++ b/src/settings.h @@ -37,6 +37,8 @@ public: bool isSaplingAddress(QString addr); bool isSproutAddress(QString addr); + bool isValidSaplingPrivateKey(QString pk); + bool isSyncing(); void setSyncing(bool syncing); @@ -101,7 +103,7 @@ public: static QString getZECUSDDisplayFormat(double bal); static QString getTokenName(); - static QString getDonationAddr(bool sapling); + static QString getDonationAddr(); static double getMinerFee(); static double getZboardAmount(); diff --git a/src/txtablemodel.cpp b/src/txtablemodel.cpp index b083b3c..e2d3651 100644 --- a/src/txtablemodel.cpp +++ b/src/txtablemodel.cpp @@ -104,9 +104,9 @@ void TxTableModel::updateAllData() { QVariant TxTableModel::data(const QModelIndex &index, int role) const { - // Align column 4,5 (confirmations, amount) right + // Align numeric columns (confirmations, amount) right if (role == Qt::TextAlignmentRole && - (index.column() == 3 || index.column() == 4)) + (index.column() == Column::Confirmations || index.column() == Column::Amount)) return QVariant(Qt::AlignRight | Qt::AlignVCenter); auto dat = modeldata->at(index.row()); @@ -125,23 +125,23 @@ void TxTableModel::updateAllData() { if (role == Qt::DisplayRole) { switch (index.column()) { - case 0: return dat.type; - case 1: { + case Column::Type: return dat.type; + case Column::Address: { auto addr = dat.address; if (addr.trimmed().isEmpty()) return "(Shielded)"; else return addr; } - case 2: return QDateTime::fromMSecsSinceEpoch(dat.datetime * (qint64)1000).toLocalTime().toString(); - case 3: return QString::number(dat.confirmations); - case 4: return Settings::getZECDisplayFormat(dat.amount); + case Column::Time: return QDateTime::fromMSecsSinceEpoch(dat.datetime * (qint64)1000).toLocalTime().toString(); + case Column::Confirmations: return QString::number(dat.confirmations); + case Column::Amount: return Settings::getZECDisplayFormat(dat.amount); } } if (role == Qt::ToolTipRole) { switch (index.column()) { - case 0: { + case Column::Type: { if (dat.memo.startsWith("zcash:")) { return Settings::paymentURIPretty(Settings::parseURI(dat.memo)); } else { @@ -149,16 +149,16 @@ void TxTableModel::updateAllData() { (dat.memo.isEmpty() ? "" : " tx memo: \"" + dat.memo + "\""); } } - case 1: { + case Column::Address: { auto addr = modeldata->at(index.row()).address; if (addr.trimmed().isEmpty()) return "(Shielded)"; else return addr; } - case 2: return QDateTime::fromMSecsSinceEpoch(modeldata->at(index.row()).datetime * (qint64)1000).toLocalTime().toString(); - case 3: return QString("%1 Network Confirmations").arg(QString::number(dat.confirmations)); - case 4: return Settings::getInstance()->getUSDFromZecAmount(modeldata->at(index.row()).amount); + case Column::Time: return QDateTime::fromMSecsSinceEpoch(modeldata->at(index.row()).datetime * (qint64)1000).toLocalTime().toString(); + case Column::Confirmations: return QString("%1 Network Confirmations").arg(QString::number(dat.confirmations)); + case Column::Amount: return Settings::getInstance()->getUSDFromZecAmount(modeldata->at(index.row()).amount); } } @@ -187,7 +187,7 @@ void TxTableModel::updateAllData() { QVariant TxTableModel::headerData(int section, Qt::Orientation orientation, int role) const { - if (role == Qt::TextAlignmentRole && (section == 3 || section == 4)) + if (role == Qt::TextAlignmentRole && (section == Column::Confirmations || section == Column::Amount)) return QVariant(Qt::AlignRight | Qt::AlignVCenter); if (role == Qt::FontRole) { diff --git a/src/txtablemodel.h b/src/txtablemodel.h index 9c09a2c..f295bc5 100644 --- a/src/txtablemodel.h +++ b/src/txtablemodel.h @@ -11,6 +11,15 @@ public: TxTableModel(QObject* parent); ~TxTableModel(); + enum Column + { + Type = 0, + Address = 1, + Time = 2, + Confirmations = 3, + Amount = 4 + }; + void addTData (const QList& data); void addZSentData(const QList& data); void addZRecvData(const QList& data);