diff --git a/src/addressbook.cpp b/src/addressbook.cpp index 82d99a5..125b109 100644 --- a/src/addressbook.cpp +++ b/src/addressbook.cpp @@ -244,17 +244,24 @@ AddressBook::AddressBook() { void AddressBook::readFromStorage() { QFile file(AddressBook::writeableFile()); - if (!file.exists()) { - return; + if (file.exists()) { + allLabels.clear(); + file.open(QIODevice::ReadOnly); + QDataStream in(&file); // read the data serialized from the file + QString version; + in >> version >> allLabels; + + file.close(); } - allLabels.clear(); - file.open(QIODevice::ReadOnly); - QDataStream in(&file); // read the data serialized from the file - QString version; - in >> version >> allLabels; - - file.close(); + // Special. + // Add the default ZecWallet donation address if it isn't already present + QList allAddresses; + std::transform(allLabels.begin(), allLabels.end(), + std::back_inserter(allAddresses), [=] (auto i) { return i.second; }); + if (!allAddresses.contains(Settings::getDonationAddr(true))) { + allLabels.append(QPair("ZecWallet donation", Settings::getDonationAddr(true))); + } } void AddressBook::writeToStorage() { diff --git a/src/addressbook.h b/src/addressbook.h index 41f9ff9..e7a45fc 100644 --- a/src/addressbook.h +++ b/src/addressbook.h @@ -10,9 +10,9 @@ class AddressBookModel : public QAbstractTableModel { public: AddressBookModel(QTableView* parent); ~AddressBookModel(); - - void addNewLabel(QString label, QString addr); - void removeItemAt(int row); + + void addNewLabel(QString label, QString addr); + void removeItemAt(int row); QPair itemAt(int row); int rowCount(const QModelIndex &parent) const; diff --git a/src/balancestablemodel.cpp b/src/balancestablemodel.cpp index 4cd484e..28261d2 100644 --- a/src/balancestablemodel.cpp +++ b/src/balancestablemodel.cpp @@ -94,7 +94,7 @@ QVariant BalancesTableModel::data(const QModelIndex &index, int role) const if(role == Qt::ToolTipRole) { switch (index.column()) { case 0: return AddressBook::addLabelToAddress(std::get<0>(modeldata->at(index.row()))); - case 1: return Settings::getUSDFormat(std::get<1>(modeldata->at(index.row()))); + case 1: return Settings::getUSDFromZecAmount(std::get<1>(modeldata->at(index.row()))); } } diff --git a/src/confirm.ui b/src/confirm.ui index e4e1a78..e7de7d7 100644 --- a/src/confirm.ui +++ b/src/confirm.ui @@ -133,6 +133,22 @@ + + + + Recurring Payment + + + + + + TextLabel + + + + + + diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 52c57ce..fb050d3 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -46,6 +46,11 @@ MainWindow::MainWindow(QWidget *parent) : rpc->checkForUpdate(false); }); + // Recurring payments + QObject::connect(ui->action_Recurring_Payments, &QAction::triggered, [=]() { + Recurring::getInstance()->showRecurringDialog(this); + }); + // Request zcash QObject::connect(ui->actionRequest_zcash, &QAction::triggered, [=]() { RequestDialog::showRequestZcash(this); @@ -603,7 +608,7 @@ void MainWindow::addressBook() { void MainWindow::donate() { // Set up a donation to me :) - removeExtraAddresses(); + clearSendForm(); ui->Address1->setText(Settings::getDonationAddr( Settings::getInstance()->isSaplingAddress(ui->inputsCombo->currentText()))); @@ -778,6 +783,8 @@ void MainWindow::balancesReady() { pendingURIPayment = ""; } + // Execute any pending Recurring payments + Recurring::getInstance()->processPending(this); } // Event filter for MacOS specific handling of payment URIs @@ -799,7 +806,7 @@ bool MainWindow::eventFilter(QObject *object, QEvent *event) { // the transaction. void MainWindow::payZcashURI(QString uri, QString myAddr) { // If the Payments UI is not ready (i.e, all balances have not loaded), defer the payment URI - if (!uiPaymentsReady) { + if (!isPaymentsReady()) { qDebug() << "Payment UI not ready, waiting for UI to pay URI"; pendingURIPayment = uri; return; @@ -825,7 +832,8 @@ void MainWindow::payZcashURI(QString uri, QString myAddr) { } // Now, set the fields on the send tab - removeExtraAddresses(); + clearSendForm(); + if (!myAddr.isEmpty()) { ui->inputsCombo->setCurrentText(myAddr); } @@ -1035,7 +1043,7 @@ void MainWindow::setupBalancesTab() { // If there's a to address, add that as well if (!to.isEmpty()) { // Remember to clear any existing address fields, because we are creating a new transaction. - this->removeExtraAddresses(); + this->clearSendForm(); ui->Address1->setText(to); } @@ -1433,6 +1441,7 @@ MainWindow::~MainWindow() delete rpc; delete labelCompleter; + delete sendTxRecurringInfo; delete amtValidator; delete feesValidator; diff --git a/src/mainwindow.h b/src/mainwindow.h index 692e746..d7079cc 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -2,7 +2,9 @@ #define MAINWINDOW_H #include "precompiled.h" + #include "logger.h" +#include "recurring.h" // Forward declare to break circular dependency. class RPC; @@ -60,6 +62,12 @@ public: void updateTAddrCombo(bool checked); void updateFromCombo(); + // Disable recurring on mainnet + void disableRecurring(); + + // Check whether the RPC is returned and payments are ready to be made + bool isPaymentsReady() { return uiPaymentsReady; } + Ui::MainWindow* ui; QLabel* statusLabel; @@ -83,11 +91,11 @@ private: void setupTurnstileDialog(); void setupSettingsModal(); void setupStatusBar(); - - void removeExtraAddresses(); + + void clearSendForm(); Tx createTxFromSendPage(); - bool confirmTx(Tx tx); + bool confirmTx(Tx tx, RecurringPaymentInfo* rpi); void turnstileDoMigration(QString fromAddr = ""); void turnstileProgress(); @@ -134,6 +142,8 @@ private: QRegExpValidator* amtValidator = nullptr; QRegExpValidator* feesValidator = nullptr; + RecurringPaymentInfo* sendTxRecurringInfo = nullptr; + QMovie* loadingMovie; }; diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 88b9d5e..f3e29bf 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -1086,6 +1086,7 @@ + @@ -1178,6 +1179,11 @@ Ctrl+M + + + &Recurring Payments + + Request zcash... diff --git a/src/newrecurring.ui b/src/newrecurring.ui index f8f8521..8d7744b 100644 --- a/src/newrecurring.ui +++ b/src/newrecurring.ui @@ -14,13 +14,27 @@ Edit Schedule - - + + + + Payment Description + + + + + - + + + <Amount> + + - + + + + Qt::Horizontal @@ -34,16 +48,6 @@ - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - @@ -51,44 +55,37 @@ - - + + - Payment Description + - - + + - TextLabel + Next Payment - - - - Qt::Horizontal + + + + Amount - - - - - - - 0 - 0 - - - - + + + + + - + - + Qt::Horizontal @@ -102,53 +99,81 @@ - - + + + + Qt::Vertical + + + + 20 + 40 + + + - - + + + + + - From + Memo - - + + + + Qt::Horizontal + + + + + - Number of payments + To - - + + - Amount + From - - + + - Qt::Vertical + Qt::Horizontal - - - 20 - 40 - + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok - + - - + + + + + + - - + + + + Number of payments + + + + + - + - + Qt::Horizontal @@ -162,47 +187,17 @@ - - - - Next Payment - - - - - - - - - To - - - - - - - - + - Memo + - - - AddressCombo - QComboBox -
addresscombo.h
-
-
txtDesc - cmbFromAddress - txtToAddr - txtAmt cmbCurrency cmbSchedule txtNumPayments diff --git a/src/recurring.cpp b/src/recurring.cpp index 64affec..73027e8 100644 --- a/src/recurring.cpp +++ b/src/recurring.cpp @@ -4,45 +4,747 @@ #include "rpc.h" #include "settings.h" #include "ui_newrecurring.h" +#include "ui_recurringdialog.h" +#include "ui_recurringpayments.h" +#include "ui_recurringmultiple.h" -void Recurring::showEditDialog(QWidget* parent, MainWindow* main, Tx tx) { +QString schedule_desc(Schedule s) { + switch (s) { + case Schedule::DAY: return "day"; + case Schedule::WEEK: return "week"; + case Schedule::MONTH: return "month"; + case Schedule::YEAR: return "5 mins"; + default: return "none"; + } +} + +RecurringPaymentInfo RecurringPaymentInfo::fromJson(QJsonObject j) { + + // We create a payment info with 0 items, and then fill them in later. + RecurringPaymentInfo r(0); + r.desc = j["desc"].toString(); + r.fromAddr = j["from"].toString(); + r.toAddr = j["to"].toString(); + r.amt = j["amt"].toString().toDouble(); + r.memo = j["memo"].toString(); + r.currency = j["currency"].toString(); + r.schedule = (Schedule)j["schedule"].toInt(); + r.startDate = QDateTime::fromSecsSinceEpoch(j["startdate"].toString().toLongLong()); + + for (auto h : j["payments"].toArray()) { + PaymentItem item; + + item.paymentNumber = h.toObject()["paymentnumber"].toInt(); + item.date = QDateTime::fromSecsSinceEpoch(h.toObject()["date"].toString().toLongLong()); + item.txid = h.toObject()["txid"].toString(); + item.status = (PaymentStatus)h.toObject()["status"].toInt(); + item.err = h.toObject()["err"].toString(); + + r.payments.append(item); + } + + return r; +} + +QString RecurringPaymentInfo::getHash() const { + auto val = getScheduleDescription() + fromAddr + toAddr; + + return QString(QCryptographicHash::hash(val.toUtf8(), + QCryptographicHash::Sha256).toHex()); +} + +QJsonObject RecurringPaymentInfo::toJson() { + QJsonArray paymentsJson; + for (auto h : payments) { + paymentsJson.append(QJsonObject{ + {"paymentnumber", h.paymentNumber}, + {"date", QString::number(h.date.toSecsSinceEpoch())}, + {"txid", h.txid}, + {"err", h.err}, + {"status", h.status} + }); + } + + auto j = QJsonObject{ + {"desc", desc}, + {"from", fromAddr}, + {"to", toAddr}, + {"amt", Settings::getDecimalString(amt)}, + {"memo", memo}, + {"currency", currency}, + {"schedule", (int)schedule}, + {"startdate", QString::number(startDate.toSecsSinceEpoch())}, + {"payments", paymentsJson} + }; + + return j; +} + +QString RecurringPaymentInfo::getAmountPretty() const { + return currency == "USD" ? Settings::getUSDFormat(amt) : Settings::getZECDisplayFormat(amt); +} + +QString RecurringPaymentInfo::getScheduleDescription() const { + return "Pay " % getAmountPretty() + % " every " % schedule_desc(schedule) % ", starting " % startDate.toString("yyyy-MMM-dd") + % ", for " % QString::number(payments.size()) % " payments"; +} + +/** + * Get the date/time when the next payment is due + */ +QDateTime RecurringPaymentInfo::getNextPayment() const { + for (auto item : payments) { + if (item.status == PaymentStatus::NOT_STARTED) + return item.date; + } + + return QDateTime::fromSecsSinceEpoch(0); +} + +/** + * Counts the number of payments that haven't been started yet + */ +int RecurringPaymentInfo::getNumPendingPayments() const { + int count = 0; + for (auto item : payments) { + if (item.status == PaymentStatus::NOT_STARTED) { + count++; + } + } + + return count; +} + +// Returns a new Recurring payment info, created from the Tx. +// The caller needs to take ownership of the returned object. +RecurringPaymentInfo* Recurring::getNewRecurringFromTx(QWidget* parent, MainWindow*, Tx tx, RecurringPaymentInfo* rpi) { Ui_newRecurringDialog ui; QDialog d(parent); ui.setupUi(&d); Settings::saveRestore(&d); - // Add all the from addresses - auto allBalances = main->getRPC()->getAllBalances(); - for (QString addr : allBalances->keys()) { - ui.cmbFromAddress->addItem(addr, allBalances->value(addr)); - } - if (!tx.fromAddr.isEmpty()) { - ui.cmbFromAddress->setCurrentText(tx.fromAddr); - ui.cmbFromAddress->setEnabled(false); + ui.lblFrom->setText(tx.fromAddr); } - - ui.cmbCurrency->addItem(Settings::getTokenName()); + ui.cmbCurrency->addItem("USD"); + ui.cmbCurrency->addItem(Settings::getTokenName()); if (tx.toAddrs.length() > 0) { - ui.txtToAddr->setText(tx.toAddrs[0].addr); - ui.txtToAddr->setEnabled(false); + ui.lblTo->setText(tx.toAddrs[0].addr); - ui.txtAmt->setText(Settings::getDecimalString(tx.toAddrs[0].amount)); - ui.txtAmt->setEnabled(false); + // Default is USD + ui.lblAmt->setText(Settings::getUSDFromZecAmount(tx.toAddrs[0].amount)); ui.txtMemo->setPlainText(tx.toAddrs[0].txtMemo); ui.txtMemo->setEnabled(false); } - ui.cmbSchedule->addItem("Every Day", QVariant(Schedule::DAY)); - ui.cmbSchedule->addItem("Every Week", QVariant(Schedule::WEEK)); - ui.cmbSchedule->addItem("Every Month", QVariant(Schedule::MONTH)); - ui.cmbSchedule->addItem("Every Year", QVariant(Schedule::YEAR)); + // Wire up ZEC/USD toggle + QObject::connect(ui.cmbCurrency, QOverload::of(&QComboBox::currentIndexChanged), [&](QString c) { + if (tx.toAddrs.length() < 1) + return; + + if (c == "USD") { + ui.lblAmt->setText(Settings::getUSDFromZecAmount(tx.toAddrs[0].amount)); + } + else { + ui.lblAmt->setText(Settings::getDecimalString(tx.toAddrs[0].amount)); + } + }); + + for (int i = Schedule::DAY; i <= Schedule::YEAR; i++) { + ui.cmbSchedule->addItem("Every " + schedule_desc((Schedule)i), QVariant(i)); + } + + QObject::connect(ui.cmbSchedule, QOverload::of(&QComboBox::currentIndexChanged), [&](int i) { + qDebug() << "schedule is " << i << " current data is " << ui.cmbSchedule->currentData().toInt(); + ui.lblNextPayment->setText(getNextPaymentDate((Schedule)ui.cmbSchedule->currentData().toInt()).toString("yyyy-MMM-dd")); + }); + ui.lblNextPayment->setText(getNextPaymentDate((Schedule)ui.cmbSchedule->currentData().toInt()).toString("yyyy-MMM-dd")); ui.txtNumPayments->setText("10"); + // If an existing RecurringPaymentInfo was passed in, set the UI values appropriately + if (rpi != nullptr) { + ui.txtDesc->setText(rpi->desc); + ui.lblTo->setText(rpi->toAddr); + ui.txtMemo->setPlainText(rpi->memo); + + ui.cmbCurrency->setCurrentText(rpi->currency); + ui.lblAmt->setText(rpi->getAmountPretty()); + ui.lblFrom->setText(rpi->fromAddr); + ui.txtNumPayments->setText(QString::number(rpi->payments.size())); + ui.cmbSchedule->setCurrentIndex(rpi->schedule - 1); // indexes start from 0 + } + ui.txtDesc->setFocus(); + if (d.exec() == QDialog::Accepted) { + // Construct a new Object and return it + auto numPayments = ui.txtNumPayments->text().toInt(); + auto r = new RecurringPaymentInfo(numPayments); + r->desc = ui.txtDesc->text(); + r->currency = ui.cmbCurrency->currentText(); + r->schedule = (Schedule)ui.cmbSchedule->currentData().toInt(); + r->startDate = QDateTime::currentDateTime(); + + updateInfoWithTx(r, tx); + return r; + } + else { + return nullptr; + } +} + +void Recurring::updateInfoWithTx(RecurringPaymentInfo* r, Tx tx) { + r->toAddr = tx.toAddrs[0].addr; + r->memo = tx.toAddrs[0].txtMemo; + r->fromAddr = tx.fromAddr; + if (r->currency.isEmpty() || r->currency == "USD") { + r->currency = "USD"; + r->amt = tx.toAddrs[0].amount * Settings::getInstance()->getZECPrice(); + } + else { + r->currency = Settings::getTokenName(); + r->amt = tx.toAddrs[0].amount; + } + + // Make sure that the number of payments is properly listed in the array + assert(r->payments.size() == r->payments.size()); + + // Update the payment dates + r->payments[0].date = r->startDate; + r->payments[0].status = PaymentStatus::NOT_STARTED; + for (int i = 1; i < r->payments.size(); i++) { + r->payments[i].date = getNextPaymentDate(r->schedule, r->payments[i-1].date); + r->payments[i].status = PaymentStatus::NOT_STARTED; + } +} + +/** + * Given a schedule and an optional previous date, calculate the next payment date/ + * If there is no previous date, it is assumed to be the current DateTime + */ +QDateTime Recurring::getNextPaymentDate(Schedule s, QDateTime start) { + QDateTime nextDate = start; + + switch (s) { + case Schedule::DAY: nextDate = nextDate.addDays(1); break; + case Schedule::WEEK: nextDate = nextDate.addDays(7); break; + case Schedule::MONTH: nextDate = nextDate.addMonths(1); break; + // TODO: For testing only, year means 5 mins + case Schedule::YEAR: nextDate = nextDate.addSecs(60 * 5); break; + //case Schedule::YEAR: nextDate = nextDate.addYears(1); break; + } + + return nextDate; +} + +QString Recurring::writeableFile() { + auto filename = QStringLiteral("recurringpayments.json"); + + auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + if (!dir.exists()) + QDir().mkpath(dir.absolutePath()); + + if (Settings::getInstance()->isTestnet()) { + return dir.filePath("testnet-" % filename); + } + else { + return dir.filePath(filename); + } +} + +void Recurring::addRecurringInfo(const RecurringPaymentInfo& rpi) { + if (payments.contains(rpi.getHash())) { + payments.remove(rpi.getHash()); + } + + payments.insert(rpi.getHash(), rpi); + + writeToStorage(); +} + +void Recurring::removeRecurringInfo(QString hash) { + if (!payments.contains(hash)) { + qDebug() << "Hash not found:" << hash << " in " << payments.keys(); + return; + } + + payments.remove(hash); + + writeToStorage(); +} + + +void Recurring::readFromStorage() { + QFile file(writeableFile()); + file.open(QIODevice::ReadOnly); + + QTextStream in(&file); + auto jsondoc = QJsonDocument::fromJson(in.readAll().toUtf8()); + + payments.clear(); + + for (auto k : jsondoc.array()) { + auto p = RecurringPaymentInfo::fromJson(k.toObject()); + payments.insert(p.getHash(), p); + } +} + + +void Recurring::writeToStorage() { + QFile file(writeableFile()); + file.open(QIODevice::ReadWrite | QIODevice::Truncate); + + QJsonArray arr; + for (auto v : payments.values()) { + arr.append(v.toJson()); + } + + QTextStream out(&file); + out << QJsonDocument(arr).toJson(); + + file.close(); +} + +/** + * Lookup the recurring payment info with the given hash, and update + * a payment made + **/ +bool Recurring::updatePaymentItem(QString hash, int paymentNumber, + QString txid, QString err, PaymentStatus status) { + if (!payments.contains(hash)) { + return false; + } + + payments[hash].payments[paymentNumber].date = QDateTime::currentDateTime(); + payments[hash].payments[paymentNumber].txid = txid; + payments[hash].payments[paymentNumber].err = err; + payments[hash].payments[paymentNumber].status = status; + + // Upda teht file on disk + writeToStorage(); + + // Then read it back to refresh the hashes + readFromStorage(); + + return true; +} + +Recurring* Recurring::getInstance() { + if (!instance) { + instance = new Recurring(); + instance->readFromStorage(); + } + + return instance; +} + +// Singleton +Recurring* Recurring::instance = nullptr; + +/** + * Main worker method that will go over all the recurring paymets and process any pending ones + */ +void Recurring::processPending(MainWindow* main) { + qDebug() << "Processing payments"; + + if (!main->isPaymentsReady()) + return; + + // For each recurring payment + for (auto rpi: payments.values()) { + // Collect all pending payments that are past due + QList pending; + + for (auto pi: rpi.payments) { + if (pi.status == PaymentStatus::NOT_STARTED && + pi.date <= QDateTime::currentDateTime()) { + pending.append(pi); + } + } + + qDebug() << "Found " << pending.size() << "Pending Payments"; + + // If there is only 1 pending payment, then we don't have to do anything special. + // Just process it + if (pending.size() == 1) { + executeRecurringPayment(main, rpi, { pending.first().paymentNumber }); + } else if (pending.size() > 1) { + // There are multiple pending payments. Ask the user what they want to do with it + // Options are: Pay latest one, Pay all or Pay none. + processMultiplePending(rpi, main); + } + } +} + +/** + * Called when a particular RecurringPaymentInfo has more than one pending payment to be processed. + * We will ask the user what he wants to do. + */ +void Recurring::processMultiplePending(RecurringPaymentInfo rpi, MainWindow* main) { + Ui_RecurringPending ui; + QDialog d(main); + ui.setupUi(&d); + Settings::saveRestore(&d); + + // Fill the UI + ui.lblDesc->setText (rpi.desc); + ui.lblTo->setText (rpi.toAddr); + ui.lblSchedule->setText(rpi.getScheduleDescription()); + + // Mark all the outstanding ones as pending, so it shows in the table correctly. + for (auto& pi: rpi.payments) { + if (pi.status == PaymentStatus::NOT_STARTED && + pi.date <= QDateTime::currentDateTime()) { + pi.status = PaymentStatus::PENDING; + } + } + + auto model = new RecurringPaymentsListViewModel(ui.tblPending, rpi); + ui.tblPending->setModel(model); + + // Select none by default + ui.rNone->setChecked(true); + + // Restore the table column layout + QSettings s; + ui.tblPending->horizontalHeader()->restoreState(s.value("recurringmultipaymentstablevgeom").toByteArray()); + + bool cancelled = (d.exec() == QDialog::Rejected); + + if (cancelled || ui.rNone->isChecked()) { + // Update the status to skip all the pending payments + for (auto& pi: rpi.payments) { + if (pi.status == PaymentStatus::PENDING) { + updatePaymentItem(rpi.getHash(), pi.paymentNumber, "", "", PaymentStatus::SKIPPED); + } + } + } else if (ui.rLast->isChecked()) { + // Update the status for all except the last to skipped + // First, collect all the payments + QList pendingPayments; + for (int i=0; i < rpi.payments.size(); i++) { + if (rpi.payments[i].status == PaymentStatus::PENDING) { + pendingPayments.append(rpi.payments[i].paymentNumber); + } + } + + // Update the status for all but the last one + for (int i=0; i < pendingPayments.size()-1; i++) { + updatePaymentItem(rpi.getHash(), pendingPayments[i], "", "", PaymentStatus::SKIPPED); + } + + // Then execute the last one. The function will update the status + executeRecurringPayment(main, rpi, {pendingPayments.last()}); + } else if (ui.rAll->isChecked()) { + // Pay all of them, in a single transaction + QList pendingPayments; + for (int i=0; i < rpi.payments.size(); i++) { + if (rpi.payments[i].status == PaymentStatus::PENDING) { + pendingPayments.append(rpi.payments[i].paymentNumber); + } + } + + // Execute all of them, and then update the status + executeRecurringPayment(main, rpi, pendingPayments); + } + + // Save the table column layout + s.setValue("recurringmultipaymentstablevgeom", ui.tblPending->horizontalHeader()->saveState()); +} + +void Recurring::executeRecurringPayment(MainWindow* main, RecurringPaymentInfo rpi, QList paymentNumbers) { + // Amount is in USD or ZEC? + auto amt = rpi.amt; + if (rpi.currency == "USD") { + // If there is no price, then fail the payment + if (Settings::getInstance()->getZECPrice() == 0) { + for (auto paymentNumber: paymentNumbers) { + updatePaymentItem(rpi.getHash(), paymentNumber, + "", QObject::tr("No ZEC price was available to convert from USD"), + PaymentStatus::ERROR); + } + return; + } + + // Translate it into ZEC + amt = rpi.amt / Settings::getInstance()->getZECPrice(); + } + + // Build a Tx + Tx tx; + tx.fromAddr = rpi.fromAddr; + tx.fee = Settings::getMinerFee(); + + // If this is a multiple payment, we'll add up all the amounts + if (paymentNumbers.size() > 1) + amt *= paymentNumbers.size(); + + tx.toAddrs.append(ToFields { rpi.toAddr, amt, rpi.memo, rpi.memo.toUtf8().toHex() }); + + // To prevent some weird race conditions, we immediately mark the payment as paid. + // If something goes wrong, we'll get the error callback below, and the status will be + // updated. If it succeeds, we'll once again update the status with the txid + for (int paymentNumber: paymentNumbers) { + updatePaymentItem(rpi.getHash(), paymentNumber, "", "", PaymentStatus::COMPLETED); + } + + // Send it off to the RPC + doSendTx(main, tx, [=] (QString txid, QString err) { + if (err.isEmpty()) { + // Success, update the rpi + for (int paymentNumber: paymentNumbers) { + updatePaymentItem(rpi.getHash(), paymentNumber, txid, "", PaymentStatus::COMPLETED); + } + } else { + // Errored out. Bummer. + for (int paymentNumber: paymentNumbers) { + updatePaymentItem(rpi.getHash(), paymentNumber, "", err, PaymentStatus::ERROR); + } + } + }); +} + +/** + * Execute a send Tx + */ +void Recurring::doSendTx(MainWindow* mainwindow, Tx tx, std::function cb) { + mainwindow->getRPC()->executeTransaction(tx, [=] (QString opid) { + mainwindow->ui->statusBar->showMessage(QObject::tr("Computing Recurring Tx: ") % opid); + }, + [=] (QString /*opid*/, QString txid) { + mainwindow->ui->statusBar->showMessage(Settings::txidStatusMessage + " " + txid); + cb(txid, ""); + }, + [=] (QString opid, QString errStr) { + mainwindow->ui->statusBar->showMessage(QObject::tr(" Tx ") % opid % QObject::tr(" failed"), 15 * 1000); + cb("", errStr); + }); + +} + +/** + * Show the list of configured recurring payments + */ +void Recurring::showRecurringDialog(MainWindow* parent) { + // Make sure only 1 is showing at a time + static bool isDialogOpen = false; + + if (isDialogOpen) + return; + + Ui_RecurringDialog rd; + QDialog d(parent); + + rd.setupUi(&d); + Settings::saveRestore(&d); + + auto model = new RecurringListViewModel(rd.tableView); + rd.tableView->setModel(model); + + // Restore the table column layout + QSettings s; + rd.tableView->horizontalHeader()->restoreState(s.value("recurringtablegeom").toByteArray()); + + // Function to show the history and pending payments for a particular recurring payment + auto showPayments = [=, &d] (const RecurringPaymentInfo& rpi) { + Ui_RecurringPayments p; + QDialog pd(&d); + + p.setupUi(&pd); + Settings::saveRestore(&pd); + + auto model = new RecurringPaymentsListViewModel(p.tableView, rpi); + p.tableView->setModel(model); + + p.tableView->setContextMenuPolicy(Qt::CustomContextMenu); + QObject::connect(p.tableView, &QTableView::customContextMenuRequested, [=, &pd] (QPoint pos) { + QModelIndex index = p.tableView->indexAt(pos); + if (index.row() < 0 || index.row() >= rpi.payments.size()) return; + + int paymentNumber = index.row(); + auto txid = rpi.payments[paymentNumber].txid; + QMenu menu(parent); + + if (!txid.isEmpty()) { + menu.addAction(QObject::tr("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)); + }); + } + + auto err = rpi.payments[paymentNumber].err; + if (!err.isEmpty()) { + menu.addAction(QObject::tr("View Error"), [=, &pd] () { + QMessageBox::information(&pd, QObject::tr("Reported Error"), "\"" + err + "\"", QMessageBox::Ok); + }); + } + + menu.exec(p.tableView->viewport()->mapToGlobal(pos)); + }); + + // Restore the table column layout + QSettings s; + p.tableView->horizontalHeader()->restoreState(s.value("recurringpaymentstablevgeom").toByteArray()); + + pd.exec(); + + // Save the table column layout + s.setValue("recurringpaymentstablevgeom", p.tableView->horizontalHeader()->saveState()); + }; + + // View Button + QObject::connect(rd.btnView, &QPushButton::clicked, [=] () { + auto selectedRows = rd.tableView->selectionModel()->selectedRows(); + if (selectedRows.size() == 1) { + auto rpi = Recurring::getInstance()->getAsList()[selectedRows[0].row()]; + showPayments(rpi); + } + }); + + // Double Click + QObject::connect(rd.tableView, &QTableView::doubleClicked, [=] (auto index) { + auto rpi = Recurring::getInstance()->getAsList()[index.row()]; + showPayments(rpi); + }); + + // Delete button + QObject::connect(rd.btnDelete, &QPushButton::clicked, [=, &d]() { + auto selectedRows = rd.tableView->selectionModel()->selectedRows(); + if (selectedRows.size() == 1) { + auto rpi = Recurring::getInstance()->getAsList()[selectedRows[0].row()]; + if (QMessageBox::warning(&d, QObject::tr("Are you sure you want to delete the recurring payment?"), + QObject::tr("Are you sure you want to delete the recurring payment?") + "\n" + + QObject::tr("All future payments will be cancelled."), + QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) { + Recurring::getInstance()->removeRecurringInfo(rpi.getHash()); + } + } + }); + + isDialogOpen = true; d.exec(); -} \ No newline at end of file + isDialogOpen = false; + + // Save the table column layout + s.setValue("recurringtablegeom", rd.tableView->horizontalHeader()->saveState()); + + delete model; +} + +/** + * Model for List of recurring payments + */ +RecurringListViewModel::RecurringListViewModel(QTableView* parent) { + this->parent = parent; + headers << tr("Amount") << tr("Schedule") << tr("Payments Left") + << tr("Next Payment") << tr("To"); +} + + +int RecurringListViewModel::rowCount(const QModelIndex&) const { + return Recurring::getInstance()->getAsList().size(); +} + +int RecurringListViewModel::columnCount(const QModelIndex&) const { + return headers.size(); +} + +QVariant RecurringListViewModel::data(const QModelIndex &index, int role) const { + auto rpi = Recurring::getInstance()->getAsList().at(index.row()); + if (role == Qt::DisplayRole) { + switch (index.column()) { + case 0: return rpi.getAmountPretty(); + case 1: return tr("Every ") + schedule_desc(rpi.schedule); + case 2: return QString::number(rpi.getNumPendingPayments()) + " of " + QString::number(rpi.payments.size()); + case 3: { + auto n = rpi.getNextPayment(); + if (n.toSecsSinceEpoch() == 0) return tr("None"); else return n; + } + case 4: return rpi.toAddr; + } + } + + return QVariant(); +} + +QVariant RecurringListViewModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::FontRole) { + QFont f; + f.setBold(true); + return f; + } + + if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { + return headers.at(section); + } + + return QVariant(); +} + +/** + * Model for history of payments for a single recurring payment + */ +RecurringPaymentsListViewModel::RecurringPaymentsListViewModel(QTableView* parent, RecurringPaymentInfo rpi) { + this->parent = parent; + this->rpi = rpi; + headers << tr("Date") << tr("Status") << tr("Txid"); +} + + +int RecurringPaymentsListViewModel::rowCount(const QModelIndex&) const { + return rpi.payments.size(); +} + +int RecurringPaymentsListViewModel::columnCount(const QModelIndex&) const { + return headers.size(); +} + +QVariant RecurringPaymentsListViewModel::data(const QModelIndex &index, int role) const { + auto item = rpi.payments[index.row()]; + + if (role == Qt::DisplayRole) { + switch (index.column()) { + case 0: return item.date; + case 1: { + switch(item.status) { + case PaymentStatus::NOT_STARTED: return tr("Not due yet"); + case PaymentStatus::PENDING: return tr("Pending"); + case PaymentStatus::SKIPPED: return tr("Skipped"); + case PaymentStatus::COMPLETED: return tr("Paid"); + case PaymentStatus::ERROR: return tr("Error"); + case PaymentStatus::UNKNOWN: return tr("Unknown"); + } + } + case 2: return item.txid; + } + } + + if (role == Qt::ToolTipRole && !item.err.isEmpty()) { + return item.err; + } + + return QVariant(); +} + +QVariant RecurringPaymentsListViewModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::FontRole) { + QFont f; + f.setBold(true); + return f; + } + + if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { + return headers.at(section); + } + + return QVariant(); +} diff --git a/src/recurring.h b/src/recurring.h index 3f27115..80442ae 100644 --- a/src/recurring.h +++ b/src/recurring.h @@ -2,8 +2,14 @@ #define RECURRING_H #include "precompiled.h" +#include "settings.h" -#include "mainwindow.h" +class MainWindow; +class Recurring; +class RecurringListViewModel; +class RecurringPaymentsListViewModel; + +struct Tx; enum Schedule { DAY = 1, @@ -12,25 +18,137 @@ enum Schedule { YEAR }; -struct RecurringPaymentInfo { - QString desc; - QString fromAddr; - QString toAddr; - double amt; - QString currency; - Schedule schedule; - int numPayments; - - long startBlock; - int completedPayments; +enum PaymentStatus { + NOT_STARTED = 0, + PENDING, + SKIPPED, + COMPLETED, + ERROR, + UNKNOWN +}; + +QString schedule_desc(Schedule s); + +class RecurringPaymentInfo { +private: + QString desc; + QString fromAddr; + QString toAddr; + double amt; + QString memo; + QString currency; + Schedule schedule; + QDateTime startDate; + + struct PaymentItem { + int paymentNumber; + QDateTime date; + QString txid; + QString err; + PaymentStatus status; + }; + + QList payments; + +friend class Recurring; +friend class RecurringListViewModel; +friend class RecurringPaymentsListViewModel; + +public: + RecurringPaymentInfo(int numPayments = 0) { + // Initialize the payments list. + for (auto i = 0; i < numPayments; i++) { + payments.append( + PaymentItem{i, QDateTime::fromSecsSinceEpoch(0), + "", "", PaymentStatus::NOT_STARTED}); + } + } + + QString getScheduleDescription() const; + QJsonObject toJson(); + + QString getAmountPretty() const; + QString getHash() const; + int getNumPendingPayments() const; + QDateTime getNextPayment() const; + + static RecurringPaymentInfo fromJson(QJsonObject j); }; class Recurring { public: - Recurring(); + static Recurring* getInstance(); + + RecurringPaymentInfo* getNewRecurringFromTx(QWidget* parent, MainWindow* main, Tx tx, RecurringPaymentInfo* rpi); + + void updateInfoWithTx(RecurringPaymentInfo* r, Tx tx); + QString writeableFile(); + + static void showRecurringDialog(MainWindow* parent); + static QDateTime getNextPaymentDate(Schedule s, QDateTime start = QDateTime::currentDateTime()); + + void addRecurringInfo(const RecurringPaymentInfo& rpi); + void removeRecurringInfo(QString hash); + + void writeToStorage(); + void readFromStorage(); + + // Worker method that goes through all pending recurring payments to see if any + // need to be processed. + void processPending(MainWindow* main); + // If multiple are pending, we need to ask the user + void processMultiplePending(RecurringPaymentInfo rpi, MainWindow* main); + // Execute a particular payment item + void executeRecurringPayment(MainWindow *, RecurringPaymentInfo rpi, QList paymentNumber); + + // Execute a Tx + void doSendTx(MainWindow* rpc, Tx tx, std::function cb); + + bool updatePaymentItem(QString hash, int paymentNumber, QString txid, QString err, PaymentStatus status); + + QList getAsList() { return payments.values(); } +private: + Recurring() = default; + QMap payments; + + static Recurring* instance; +}; + + +// Model for list of configured recurring payments +class RecurringListViewModel : public QAbstractTableModel { + +public: + RecurringListViewModel(QTableView* parent); + ~RecurringListViewModel() = default; + + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + +private: + QTableView* parent; + QStringList headers; +}; + +// Model for history of payments +class RecurringPaymentsListViewModel : public QAbstractTableModel { + +public: + RecurringPaymentsListViewModel(QTableView* parent, RecurringPaymentInfo rpi); + ~RecurringPaymentsListViewModel() = default; + + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; - static void showEditDialog(QWidget* parent, MainWindow* main, Tx tx); +private: + QTableView* parent; + QStringList headers; + RecurringPaymentInfo rpi; }; #endif // RECURRING_H \ No newline at end of file diff --git a/src/recurringdialog.ui b/src/recurringdialog.ui index c957d87..fbe26e6 100644 --- a/src/recurringdialog.ui +++ b/src/recurringdialog.ui @@ -15,26 +15,29 @@
- + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + - + - Add + View - - - Edit - - - - - + Delete diff --git a/src/recurringmultiple.ui b/src/recurringmultiple.ui new file mode 100644 index 0000000..3a2bcf8 --- /dev/null +++ b/src/recurringmultiple.ui @@ -0,0 +1,178 @@ + + + RecurringPending + + + + 0 + 0 + 883 + 801 + + + + Dialog + + + + + + No payments will be processed. You can manually pay them from the Recurring Payments Dialog box + + + true + + + + + + + Qt::Horizontal + + + + + + + Schedule + + + + + + + How should ZecWallet proceed? + + + + + + + Pay All in 1 Tx + + + + + + + Only the latest pending payment will be processed. All previous pending payments will be skipped + + + true + + + + + + + Pay Latest Only + + + + + + + Pay None + + + + + + + true + + + + + + + Qt::Horizontal + + + + + + + All pending payments collected, added up and paid in a single transaction + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Description + + + + + + + To + + + + + + + Qt::Horizontal + + + + + + + The following recurring payment has multiple payments pending + + + + + + + + + buttonBox + accepted() + RecurringPending + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + RecurringPending + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/recurringpayments.ui b/src/recurringpayments.ui new file mode 100644 index 0000000..3874370 --- /dev/null +++ b/src/recurringpayments.ui @@ -0,0 +1,80 @@ + + + RecurringPayments + + + + 0 + 0 + 577 + 704 + + + + Payments + + + + + + Qt::Vertical + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + false + + + + + + + + + buttonBox + accepted() + RecurringPayments + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + RecurringPayments + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/requestdialog.cpp b/src/requestdialog.cpp index d2fbfc9..6c8e74c 100644 --- a/src/requestdialog.cpp +++ b/src/requestdialog.cpp @@ -73,7 +73,7 @@ void RequestDialog::showPaymentConfirmation(MainWindow* main, QString paymentURI req.txtFrom->setText(payInfo.addr); req.txtMemo->setPlainText(payInfo.memo); req.txtAmount->setText(payInfo.amt); - req.txtAmountUSD->setText(Settings::getUSDFormat(req.txtAmount->text().toDouble())); + req.txtAmountUSD->setText(Settings::getUSDFromZecAmount(req.txtAmount->text().toDouble())); req.buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Pay")); @@ -112,9 +112,9 @@ void RequestDialog::showRequestZcash(MainWindow* main) { // Amount textbox req.txtAmount->setValidator(main->getAmountValidator()); QObject::connect(req.txtAmount, &QLineEdit::textChanged, [=] (auto text) { - req.txtAmountUSD->setText(Settings::getUSDFormat(text.toDouble())); + req.txtAmountUSD->setText(Settings::getUSDFromZecAmount(text.toDouble())); }); - req.txtAmountUSD->setText(Settings::getUSDFormat(req.txtAmount->text().toDouble())); + req.txtAmountUSD->setText(Settings::getUSDFromZecAmount(req.txtAmount->text().toDouble())); req.txtMemo->setAcceptButton(req.buttonBox->button(QDialogButtonBox::Ok)); req.txtMemo->setLenDisplayLabel(req.lblMemoLen); diff --git a/src/rpc.cpp b/src/rpc.cpp index 5c0d85c..f5bdfab 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -544,6 +544,10 @@ void RPC::getInfoThenRefresh(bool force) { Settings::getInstance()->setTestnet(reply["testnet"].get()); }; + // Recurring pamynets are testnet only + if (!Settings::getInstance()->isTestnet()) + main->disableRecurring(); + // Connected, so display checkmark. QIcon i(":/icons/res/connected.gif"); main->statusIcon->setPixmap(i.pixmap(16, 16)); @@ -560,6 +564,9 @@ void RPC::getInfoThenRefresh(bool force) { // See if the turnstile migration has any steps that need to be done. turnstile->executeMigrationStep(); + // See if recurring payments needs anything + Recurring::getInstance()->processPending(main); + refreshBalances(); refreshAddresses(); // This calls refreshZSentTransactions() and refreshReceivedZTrans() refreshTransactions(); @@ -643,7 +650,7 @@ void RPC::getInfoThenRefresh(bool force) { ui->lblSyncWarning->setVisible(isSyncing); ui->lblSyncWarningReceive->setVisible(isSyncing); - auto zecPrice = Settings::getUSDFormat(1); + auto zecPrice = Settings::getInstance()->getUSDFromZecAmount(1); QString tooltip; if (connections > 0) { tooltip = QObject::tr("Connected to zcashd"); @@ -764,9 +771,10 @@ void RPC::refreshBalances() { ui->balTransparent->setText(Settings::getZECDisplayFormat(balT)); ui->balTotal ->setText(Settings::getZECDisplayFormat(balTotal)); - ui->balSheilded ->setToolTip(Settings::getUSDFormat(balZ)); - ui->balTransparent->setToolTip(Settings::getUSDFormat(balT)); - ui->balTotal ->setToolTip(Settings::getUSDFormat(balTotal)); + + ui->balSheilded ->setToolTip(Settings::getZECDisplayFormat(balZ)); + ui->balTransparent->setToolTip(Settings::getZECDisplayFormat(balT)); + ui->balTotal ->setToolTip(Settings::getZECDisplayFormat(balTotal)); }); // 2. Get the UTXOs diff --git a/src/sendtab.cpp b/src/sendtab.cpp index fb005df..5f9ea5b 100644 --- a/src/sendtab.cpp +++ b/src/sendtab.cpp @@ -62,7 +62,7 @@ void MainWindow::setupSendTab() { // Disable custom fees if settings say no ui->minerFeeAmt->setReadOnly(!Settings::getInstance()->getAllowCustomFees()); QObject::connect(ui->minerFeeAmt, &QLineEdit::textChanged, [=](auto txt) { - ui->lblMinerFeeUSD->setText(Settings::getUSDFormat(txt.toDouble())); + ui->lblMinerFeeUSD->setText(Settings::getUSDFromZecAmount(txt.toDouble())); }); ui->minerFeeAmt->setText(Settings::getDecimalString(Settings::getMinerFee())); @@ -70,7 +70,7 @@ void MainWindow::setupSendTab() { QObject::connect(ui->tabWidget, &QTabWidget::currentChanged, [=] (int pos) { if (pos == 1) { QString txt = ui->minerFeeAmt->text(); - ui->lblMinerFeeUSD->setText(Settings::getUSDFormat(txt.toDouble())); + ui->lblMinerFeeUSD->setText(Settings::getUSDFromZecAmount(txt.toDouble())); } }); @@ -86,30 +86,63 @@ void MainWindow::setupSendTab() { // Recurring button QObject::connect(ui->chkRecurring, &QCheckBox::stateChanged, [=] (int checked) { if (checked) { - ui->btnRecurSchedule->setEnabled(true); + ui->btnRecurSchedule->setEnabled(true); + + // If this is the first time the button is checked, open the edit schedule dialog + if (sendTxRecurringInfo == nullptr) { + ui->btnRecurSchedule->click(); + } } else { ui->btnRecurSchedule->setEnabled(false); ui->lblRecurDesc->setText(""); } - }); // Recurring schedule button QObject::connect(ui->btnRecurSchedule, &QPushButton::clicked, this, &MainWindow::editSchedule); - // Hide the recurring section for now - ui->chkRecurring->setVisible(false); - ui->lblRecurDesc->setVisible(false); - ui->btnRecurSchedule->setVisible(false); - // Set the default state for the whole page - removeExtraAddresses(); + clearSendForm(); +} + +void MainWindow::disableRecurring() { + if (!Settings::getInstance()->isTestnet()) { + ui->chkRecurring->setEnabled(false); + ui->btnRecurSchedule->setEnabled(false); + } } void MainWindow::editSchedule() { + // Only on testnet for now + if (!Settings::getInstance()->isTestnet()) { + QMessageBox::critical(this, "Not Supported yet", + "Recurring payments are only supported on Testnet for now.", QMessageBox::Ok); + return; + } + + // Check to see that recurring payments are not selected when there are 2 or more addresses + if (ui->sendToWidgets->children().size()-1 > 2) { + QMessageBox::critical(this, tr("Cannot support multiple addresses"), + tr("Recurring payments doesn't currently support multiple addresses"), QMessageBox::Ok); + return; + } + // Open the edit schedule dialog - Recurring::showEditDialog(this, this, createTxFromSendPage()); + auto recurringInfo = Recurring::getInstance()->getNewRecurringFromTx(this, this, + createTxFromSendPage(), this->sendTxRecurringInfo); + if (recurringInfo == nullptr) { + // User pressed cancel. + // If there is no existing recurring info, uncheck the recurring box + if (sendTxRecurringInfo == nullptr) { + ui->chkRecurring->setCheckState(Qt::Unchecked); + } + } + else { + delete this->sendTxRecurringInfo; + this->sendTxRecurringInfo = recurringInfo; + ui->lblRecurDesc->setText(recurringInfo->getScheduleDescription()); + } } void MainWindow::updateLabelsAutoComplete() { @@ -192,7 +225,7 @@ void MainWindow::inputComboTextChanged(int index) { auto balFmt = Settings::getZECDisplayFormat(bal); ui->sendAddressBalance->setText(balFmt); - ui->sendAddressBalanceUSD->setText(Settings::getUSDFormat(bal)); + ui->sendAddressBalanceUSD->setText(Settings::getUSDFromZecAmount(bal)); } @@ -283,6 +316,14 @@ void MainWindow::addAddressSection() { ui->sendToLayout->insertWidget(itemNumber-1, verticalGroupBox); + // Disable recurring payments if a address section is added, since recurring payments + // aren't supported for more than 1 address + delete sendTxRecurringInfo; + sendTxRecurringInfo = nullptr; + ui->lblRecurDesc->setText(""); + ui->chkRecurring->setChecked(false); + ui->chkRecurring->setEnabled(false); + // Set focus into the address Address1->setFocus(); @@ -297,7 +338,13 @@ void MainWindow::addressChanged(int itemNumber, const QString& text) { void MainWindow::amountChanged(int item, const QString& text) { auto usd = ui->sendToWidgets->findChild(QString("AmtUSD") % QString::number(item)); - usd->setText(Settings::getUSDFormat(text.toDouble())); + usd->setText(Settings::getUSDFromZecAmount(text.toDouble())); + + // If there is a recurring payment, update the info there as well + if (sendTxRecurringInfo != nullptr) { + Recurring::getInstance()->updateInfoWithTx(sendTxRecurringInfo, createTxFromSendPage()); + ui->lblRecurDesc->setText(sendTxRecurringInfo->getScheduleDescription()); + } } void MainWindow::setMemoEnabled(int number, bool enabled) { @@ -365,7 +412,7 @@ void MainWindow::memoButtonClicked(int number, bool includeReplyTo) { } } -void MainWindow::removeExtraAddresses() { +void MainWindow::clearSendForm() { // The last one is a spacer, so ignore that int totalItems = ui->sendToWidgets->children().size() - 2; @@ -395,9 +442,15 @@ void MainWindow::removeExtraAddresses() { } // Reset the recurring button + if (Settings::getInstance()->isTestnet()) { + ui->chkRecurring->setEnabled(true); + } + ui->chkRecurring->setCheckState(Qt::Unchecked); ui->btnRecurSchedule->setEnabled(false); ui->lblRecurDesc->setText(""); + delete sendTxRecurringInfo; + sendTxRecurringInfo = nullptr; } void MainWindow::maxAmountChecked(int checked) { @@ -498,7 +551,7 @@ Tx MainWindow::createTxFromSendPage() { return tx; } -bool MainWindow::confirmTx(Tx tx) { +bool MainWindow::confirmTx(Tx tx, RecurringPaymentInfo* rpi) { auto fnSplitAddressForWrap = [=] (const QString& a) -> QString { if (! Settings::isZAddress(a)) return a; @@ -507,6 +560,10 @@ bool MainWindow::confirmTx(Tx tx) { return splitted; }; + // Update the recurring info with the latest Tx + if (rpi != nullptr) { + Recurring::getInstance()->updateInfoWithTx(rpi, tx); + } // Show a confirmation dialog QDialog d(this); @@ -561,7 +618,7 @@ bool MainWindow::confirmTx(Tx tx) { // Amount (USD) auto AmtUSD = new QLabel(confirm.sendToAddrs); AmtUSD->setObjectName(QString("AmtUSD") % QString::number(i + 1)); - AmtUSD->setText(Settings::getUSDFormat(toAddr.amount)); + AmtUSD->setText(Settings::getUSDFromZecAmount(toAddr.amount)); AmtUSD->setAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter); confirm.gridLayout->addWidget(AmtUSD, row, 2, 1, 1); @@ -612,7 +669,7 @@ bool MainWindow::confirmTx(Tx tx) { minerFeeUSD->setObjectName(QStringLiteral("minerFeeUSD")); minerFeeUSD->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); confirm.gridLayout->addWidget(minerFeeUSD, row, 2, 1, 1); - minerFeeUSD->setText(Settings::getUSDFormat(tx.fee)); + minerFeeUSD->setText(Settings::getUSDFromZecAmount(tx.fee)); if (Settings::getInstance()->getAllowCustomFees() && tx.fee != Settings::getMinerFee()) { confirm.warningLabel->setVisible(true); @@ -622,6 +679,15 @@ bool MainWindow::confirmTx(Tx tx) { } } + // Recurring payment info, show only if there is exactly one destination address + if (rpi == nullptr || tx.toAddrs.size() != 1) { + confirm.grpRecurring->setVisible(false); + } + else { + confirm.grpRecurring->setVisible(true); + confirm.lblRecurringDesc->setText(rpi->getScheduleDescription()); + } + // Syncing warning confirm.syncingWarning->setVisible(Settings::getInstance()->isSyncing()); @@ -637,13 +703,7 @@ bool MainWindow::confirmTx(Tx tx) { confirm.sendFrom->setToolTip(tooltip); // Show the dialog and submit it if the user confirms - if (d.exec() == QDialog::Accepted) { - // Then delete the additional fields from the sendTo tab - removeExtraAddresses(); - return true; - } else { - return false; - } + return d.exec() == QDialog::Accepted; } // Send button clicked @@ -661,23 +721,54 @@ void MainWindow::sendButton() { // abort the Tx return; } - + // Show a dialog to confirm the Tx - if (confirmTx(tx)) { + if (confirmTx(tx, sendTxRecurringInfo)) { + // If this is a recurring payment, save the hash so we can + // update the payment if it submits. + QString recurringPaymentHash; + + // Recurring payments are enabled only if there is exactly 1 destination address. + if (sendTxRecurringInfo && tx.toAddrs.size() == 1) { + // Add it to the list + Recurring::getInstance()->addRecurringInfo(*sendTxRecurringInfo); + recurringPaymentHash = sendTxRecurringInfo->getHash(); + } + + // Then delete the additional fields from the sendTo tab + clearSendForm(); + // And send the Tx rpc->executeTransaction(tx, + // Submitted [=] (QString opid) { ui->statusBar->showMessage(tr("Computing Tx: ") % opid); }, + // Accepted [=] (QString, QString txid) { ui->statusBar->showMessage(Settings::txidStatusMessage + " " + txid); + + // If this was a recurring payment, update the payment with the info + if (!recurringPaymentHash.isEmpty()) { + // Since this is the send button payment, this is the first payment + Recurring::getInstance()->updatePaymentItem(recurringPaymentHash, 0, + txid, "", PaymentStatus::COMPLETED); + } }, + // Errored out [=] (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; + // If this was a recurring payment, update the payment with the failure + if (!recurringPaymentHash.isEmpty()) { + // Since this is the send button payment, this is the first payment + Recurring::getInstance()->updatePaymentItem(recurringPaymentHash, 0, + "", errStr, PaymentStatus::ERROR); + } + QMessageBox::critical(this, QObject::tr("Transaction Error"), errStr, QMessageBox::Ok); } ); @@ -705,6 +796,6 @@ QString MainWindow::doSendTxValidations(Tx tx) { } void MainWindow::cancelButton() { - removeExtraAddresses(); + clearSendForm(); } diff --git a/src/settings.cpp b/src/settings.cpp index 9d7197a..c15d558 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -161,9 +161,15 @@ void Settings::saveRestore(QDialog* d) { } QString Settings::getUSDFormat(double bal) { - return "$" + QLocale(QLocale::English).toString(bal * Settings::getInstance()->getZECPrice(), 'f', 2); + return "$" + QLocale(QLocale::English).toString(bal, 'f', 2); } + +QString Settings::getUSDFromZecAmount(double bal) { + return getUSDFormat(bal * Settings::getInstance()->getZECPrice()); +} + + QString Settings::getDecimalString(double amt) { QString f = QString::number(amt, 'f', 8); @@ -182,9 +188,9 @@ QString Settings::getZECDisplayFormat(double bal) { } QString Settings::getZECUSDDisplayFormat(double bal) { - auto usdFormat = getUSDFormat(bal); + auto usdFormat = getUSDFromZecAmount(bal); if (!usdFormat.isEmpty()) - return getZECDisplayFormat(bal) % " (" % getUSDFormat(bal) % ")"; + return getZECDisplayFormat(bal) % " (" % usdFormat % ")"; else return getZECDisplayFormat(bal); } diff --git a/src/settings.h b/src/settings.h index ee3a053..827877f 100644 --- a/src/settings.h +++ b/src/settings.h @@ -84,7 +84,9 @@ public: static bool isTAddress(QString addr); static QString getDecimalString(double amt); - static QString getUSDFormat(double bal); + static QString getUSDFormat(double usdAmt); + + static QString getUSDFromZecAmount(double bal); static QString getZECDisplayFormat(double bal); static QString getZECUSDDisplayFormat(double bal); diff --git a/src/turnstile.cpp b/src/turnstile.cpp index df58b15..f05940b 100644 --- a/src/turnstile.cpp +++ b/src/turnstile.cpp @@ -246,6 +246,11 @@ void Turnstile::executeMigrationStep() { if (Settings::getInstance()->isSyncing()) return; + // Also, process payments only when the Payments UI is ready, otherwise + // we might mess things up + if (!mainwindow->isPaymentsReady()) + return; + auto plan = readMigrationPlan(); //qDebug() << QString("Executing step"); diff --git a/src/txtablemodel.cpp b/src/txtablemodel.cpp index f563921..7078023 100644 --- a/src/txtablemodel.cpp +++ b/src/txtablemodel.cpp @@ -154,7 +154,7 @@ void TxTableModel::updateAllData() { return addr; } case 2: return QDateTime::fromMSecsSinceEpoch(modeldata->at(index.row()).datetime * (qint64)1000).toLocalTime().toString(); - case 3: return Settings::getInstance()->getUSDFormat(modeldata->at(index.row()).amount); + case 3: return Settings::getInstance()->getUSDFromZecAmount(modeldata->at(index.row()).amount); } } diff --git a/zec-qt-wallet.pro b/zec-qt-wallet.pro index 3f08154..f88fe01 100644 --- a/zec-qt-wallet.pro +++ b/zec-qt-wallet.pro @@ -86,6 +86,7 @@ HEADERS += \ FORMS += \ src/mainwindow.ui \ + src/recurringpayments.ui \ src/settings.ui \ src/about.ui \ src/confirm.ui \ @@ -100,7 +101,8 @@ FORMS += \ src/createzcashconfdialog.ui \ src/recurringdialog.ui \ src/newrecurring.ui \ - src/requestdialog.ui + src/requestdialog.ui \ + src/recurringmultiple.ui TRANSLATIONS = res/zec_qt_wallet_es.ts \