Recurring (#131)
* Save created recurring info * Add to confirm dialog * Update RPI at confirm * Make singleton * Add history to recurring payments * USD/ZEC switch * Fix check state * recurring item serialization * Add recurring payments to file * Refactor * Address view model * Wire up dialog * Update windows installer logos * Store all payments in the store * Save table geometry * Add recurring payments view * Add deletion * Add recurring payment execution * Add donation address to address book * Add multiple payment handling * Disable recurring for multiple payments * Handle pay last * Handle pay all * Reomve frequency * Enable recurring payments only for testnet * For testing, allow payments in 5 min intervals * Fix request money amounts
This commit is contained in:
@@ -244,17 +244,24 @@ AddressBook::AddressBook() {
|
|||||||
void AddressBook::readFromStorage() {
|
void AddressBook::readFromStorage() {
|
||||||
QFile file(AddressBook::writeableFile());
|
QFile file(AddressBook::writeableFile());
|
||||||
|
|
||||||
if (!file.exists()) {
|
if (file.exists()) {
|
||||||
return;
|
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();
|
// Special.
|
||||||
file.open(QIODevice::ReadOnly);
|
// Add the default ZecWallet donation address if it isn't already present
|
||||||
QDataStream in(&file); // read the data serialized from the file
|
QList<QString> allAddresses;
|
||||||
QString version;
|
std::transform(allLabels.begin(), allLabels.end(),
|
||||||
in >> version >> allLabels;
|
std::back_inserter(allAddresses), [=] (auto i) { return i.second; });
|
||||||
|
if (!allAddresses.contains(Settings::getDonationAddr(true))) {
|
||||||
file.close();
|
allLabels.append(QPair<QString, QString>("ZecWallet donation", Settings::getDonationAddr(true)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AddressBook::writeToStorage() {
|
void AddressBook::writeToStorage() {
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ class AddressBookModel : public QAbstractTableModel {
|
|||||||
public:
|
public:
|
||||||
AddressBookModel(QTableView* parent);
|
AddressBookModel(QTableView* parent);
|
||||||
~AddressBookModel();
|
~AddressBookModel();
|
||||||
|
|
||||||
void addNewLabel(QString label, QString addr);
|
void addNewLabel(QString label, QString addr);
|
||||||
void removeItemAt(int row);
|
void removeItemAt(int row);
|
||||||
QPair<QString, QString> itemAt(int row);
|
QPair<QString, QString> itemAt(int row);
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent) const;
|
int rowCount(const QModelIndex &parent) const;
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ QVariant BalancesTableModel::data(const QModelIndex &index, int role) const
|
|||||||
if(role == Qt::ToolTipRole) {
|
if(role == Qt::ToolTipRole) {
|
||||||
switch (index.column()) {
|
switch (index.column()) {
|
||||||
case 0: return AddressBook::addLabelToAddress(std::get<0>(modeldata->at(index.row())));
|
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())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,22 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="grpRecurring">
|
||||||
|
<property name="title">
|
||||||
|
<string>Recurring Payment</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_2">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="lblRecurringDesc">
|
||||||
|
<property name="text">
|
||||||
|
<string>TextLabel</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||||||
rpc->checkForUpdate(false);
|
rpc->checkForUpdate(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Recurring payments
|
||||||
|
QObject::connect(ui->action_Recurring_Payments, &QAction::triggered, [=]() {
|
||||||
|
Recurring::getInstance()->showRecurringDialog(this);
|
||||||
|
});
|
||||||
|
|
||||||
// Request zcash
|
// Request zcash
|
||||||
QObject::connect(ui->actionRequest_zcash, &QAction::triggered, [=]() {
|
QObject::connect(ui->actionRequest_zcash, &QAction::triggered, [=]() {
|
||||||
RequestDialog::showRequestZcash(this);
|
RequestDialog::showRequestZcash(this);
|
||||||
@@ -603,7 +608,7 @@ void MainWindow::addressBook() {
|
|||||||
|
|
||||||
void MainWindow::donate() {
|
void MainWindow::donate() {
|
||||||
// Set up a donation to me :)
|
// Set up a donation to me :)
|
||||||
removeExtraAddresses();
|
clearSendForm();
|
||||||
|
|
||||||
ui->Address1->setText(Settings::getDonationAddr(
|
ui->Address1->setText(Settings::getDonationAddr(
|
||||||
Settings::getInstance()->isSaplingAddress(ui->inputsCombo->currentText())));
|
Settings::getInstance()->isSaplingAddress(ui->inputsCombo->currentText())));
|
||||||
@@ -778,6 +783,8 @@ void MainWindow::balancesReady() {
|
|||||||
pendingURIPayment = "";
|
pendingURIPayment = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute any pending Recurring payments
|
||||||
|
Recurring::getInstance()->processPending(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event filter for MacOS specific handling of payment URIs
|
// Event filter for MacOS specific handling of payment URIs
|
||||||
@@ -799,7 +806,7 @@ bool MainWindow::eventFilter(QObject *object, QEvent *event) {
|
|||||||
// the transaction.
|
// the transaction.
|
||||||
void MainWindow::payZcashURI(QString uri, QString myAddr) {
|
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 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";
|
qDebug() << "Payment UI not ready, waiting for UI to pay URI";
|
||||||
pendingURIPayment = uri;
|
pendingURIPayment = uri;
|
||||||
return;
|
return;
|
||||||
@@ -825,7 +832,8 @@ void MainWindow::payZcashURI(QString uri, QString myAddr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now, set the fields on the send tab
|
// Now, set the fields on the send tab
|
||||||
removeExtraAddresses();
|
clearSendForm();
|
||||||
|
|
||||||
if (!myAddr.isEmpty()) {
|
if (!myAddr.isEmpty()) {
|
||||||
ui->inputsCombo->setCurrentText(myAddr);
|
ui->inputsCombo->setCurrentText(myAddr);
|
||||||
}
|
}
|
||||||
@@ -1035,7 +1043,7 @@ void MainWindow::setupBalancesTab() {
|
|||||||
// If there's a to address, add that as well
|
// If there's a to address, add that as well
|
||||||
if (!to.isEmpty()) {
|
if (!to.isEmpty()) {
|
||||||
// Remember to clear any existing address fields, because we are creating a new transaction.
|
// Remember to clear any existing address fields, because we are creating a new transaction.
|
||||||
this->removeExtraAddresses();
|
this->clearSendForm();
|
||||||
ui->Address1->setText(to);
|
ui->Address1->setText(to);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1433,6 +1441,7 @@ MainWindow::~MainWindow()
|
|||||||
delete rpc;
|
delete rpc;
|
||||||
delete labelCompleter;
|
delete labelCompleter;
|
||||||
|
|
||||||
|
delete sendTxRecurringInfo;
|
||||||
delete amtValidator;
|
delete amtValidator;
|
||||||
delete feesValidator;
|
delete feesValidator;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
#define MAINWINDOW_H
|
#define MAINWINDOW_H
|
||||||
|
|
||||||
#include "precompiled.h"
|
#include "precompiled.h"
|
||||||
|
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
|
#include "recurring.h"
|
||||||
|
|
||||||
// Forward declare to break circular dependency.
|
// Forward declare to break circular dependency.
|
||||||
class RPC;
|
class RPC;
|
||||||
@@ -60,6 +62,12 @@ public:
|
|||||||
void updateTAddrCombo(bool checked);
|
void updateTAddrCombo(bool checked);
|
||||||
void updateFromCombo();
|
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;
|
Ui::MainWindow* ui;
|
||||||
|
|
||||||
QLabel* statusLabel;
|
QLabel* statusLabel;
|
||||||
@@ -83,11 +91,11 @@ private:
|
|||||||
void setupTurnstileDialog();
|
void setupTurnstileDialog();
|
||||||
void setupSettingsModal();
|
void setupSettingsModal();
|
||||||
void setupStatusBar();
|
void setupStatusBar();
|
||||||
|
|
||||||
void removeExtraAddresses();
|
void clearSendForm();
|
||||||
|
|
||||||
Tx createTxFromSendPage();
|
Tx createTxFromSendPage();
|
||||||
bool confirmTx(Tx tx);
|
bool confirmTx(Tx tx, RecurringPaymentInfo* rpi);
|
||||||
|
|
||||||
void turnstileDoMigration(QString fromAddr = "");
|
void turnstileDoMigration(QString fromAddr = "");
|
||||||
void turnstileProgress();
|
void turnstileProgress();
|
||||||
@@ -134,6 +142,8 @@ private:
|
|||||||
QRegExpValidator* amtValidator = nullptr;
|
QRegExpValidator* amtValidator = nullptr;
|
||||||
QRegExpValidator* feesValidator = nullptr;
|
QRegExpValidator* feesValidator = nullptr;
|
||||||
|
|
||||||
|
RecurringPaymentInfo* sendTxRecurringInfo = nullptr;
|
||||||
|
|
||||||
QMovie* loadingMovie;
|
QMovie* loadingMovie;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1086,6 +1086,7 @@
|
|||||||
</property>
|
</property>
|
||||||
<addaction name="action_Address_Book"/>
|
<addaction name="action_Address_Book"/>
|
||||||
<addaction name="actionSettings"/>
|
<addaction name="actionSettings"/>
|
||||||
|
<addaction name="action_Recurring_Payments"/>
|
||||||
</widget>
|
</widget>
|
||||||
<addaction name="menuFile"/>
|
<addaction name="menuFile"/>
|
||||||
<addaction name="menu_Edit"/>
|
<addaction name="menu_Edit"/>
|
||||||
@@ -1178,6 +1179,11 @@
|
|||||||
<string>Ctrl+M</string>
|
<string>Ctrl+M</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_Recurring_Payments">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Recurring Payments</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
<action name="actionRequest_zcash">
|
<action name="actionRequest_zcash">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Request zcash...</string>
|
<string>Request zcash...</string>
|
||||||
|
|||||||
@@ -14,43 +14,6 @@
|
|||||||
<string>Edit Schedule</string>
|
<string>Edit Schedule</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item row="5" column="2">
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QComboBox" name="cmbSchedule"/>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="horizontalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="10" column="2">
|
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="standardButtons">
|
|
||||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="1">
|
|
||||||
<widget class="QLabel" name="label_3">
|
|
||||||
<property name="text">
|
|
||||||
<string>Schedule</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="QLabel" name="label_7">
|
<widget class="QLabel" name="label_7">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
@@ -58,29 +21,12 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="2">
|
|
||||||
<widget class="QLabel" name="lblNextPayment">
|
|
||||||
<property name="text">
|
|
||||||
<string>TextLabel</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="1" colspan="2">
|
|
||||||
<widget class="Line" name="line">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="2">
|
<item row="3" column="2">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLineEdit" name="txtAmt">
|
<widget class="QLabel" name="lblAmt">
|
||||||
<property name="sizePolicy">
|
<property name="text">
|
||||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
<string notr="true"><Amount></string>
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -102,20 +48,24 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="2">
|
<item row="5" column="1">
|
||||||
<widget class="QLineEdit" name="txtDesc"/>
|
<widget class="QLabel" name="label_3">
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>From</string>
|
<string>Schedule</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="1">
|
<item row="1" column="2">
|
||||||
<widget class="QLabel" name="label_5">
|
<widget class="QLabel" name="lblFrom">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Number of payments</string>
|
<string notr="true"/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="8" column="1">
|
||||||
|
<widget class="QLabel" name="label_8">
|
||||||
|
<property name="text">
|
||||||
|
<string>Next Payment</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -126,21 +76,8 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="1">
|
<item row="0" column="2">
|
||||||
<spacer name="verticalSpacer">
|
<widget class="QLineEdit" name="txtDesc"/>
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="2">
|
|
||||||
<widget class="AddressCombo" name="cmbFromAddress"/>
|
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="2">
|
<item row="6" column="2">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
@@ -162,22 +99,18 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="1">
|
<item row="9" column="1">
|
||||||
<widget class="QLabel" name="label_8">
|
<spacer name="verticalSpacer">
|
||||||
<property name="text">
|
<property name="orientation">
|
||||||
<string>Next Payment</string>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
<property name="sizeHint" stdset="0">
|
||||||
</item>
|
<size>
|
||||||
<item row="2" column="2">
|
<width>20</width>
|
||||||
<widget class="QLineEdit" name="txtToAddr"/>
|
<height>40</height>
|
||||||
</item>
|
</size>
|
||||||
<item row="2" column="1">
|
|
||||||
<widget class="QLabel" name="label_2">
|
|
||||||
<property name="text">
|
|
||||||
<string>To</string>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="2">
|
<item row="4" column="2">
|
||||||
<widget class="QPlainTextEdit" name="txtMemo"/>
|
<widget class="QPlainTextEdit" name="txtMemo"/>
|
||||||
@@ -189,20 +122,82 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="7" column="1" colspan="2">
|
||||||
|
<widget class="Line" name="line">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>To</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>From</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="10" column="2">
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="8" column="2">
|
||||||
|
<widget class="QLabel" name="lblNextPayment">
|
||||||
|
<property name="text">
|
||||||
|
<string notr="true"/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="1">
|
||||||
|
<widget class="QLabel" name="label_5">
|
||||||
|
<property name="text">
|
||||||
|
<string>Number of payments</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="2">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="cmbSchedule"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QLabel" name="lblTo">
|
||||||
|
<property name="text">
|
||||||
|
<string notr="true"/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
|
||||||
<customwidget>
|
|
||||||
<class>AddressCombo</class>
|
|
||||||
<extends>QComboBox</extends>
|
|
||||||
<header>addresscombo.h</header>
|
|
||||||
</customwidget>
|
|
||||||
</customwidgets>
|
|
||||||
<tabstops>
|
<tabstops>
|
||||||
<tabstop>txtDesc</tabstop>
|
<tabstop>txtDesc</tabstop>
|
||||||
<tabstop>cmbFromAddress</tabstop>
|
|
||||||
<tabstop>txtToAddr</tabstop>
|
|
||||||
<tabstop>txtAmt</tabstop>
|
|
||||||
<tabstop>cmbCurrency</tabstop>
|
<tabstop>cmbCurrency</tabstop>
|
||||||
<tabstop>cmbSchedule</tabstop>
|
<tabstop>cmbSchedule</tabstop>
|
||||||
<tabstop>txtNumPayments</tabstop>
|
<tabstop>txtNumPayments</tabstop>
|
||||||
|
|||||||
@@ -4,45 +4,747 @@
|
|||||||
#include "rpc.h"
|
#include "rpc.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
#include "ui_newrecurring.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;
|
Ui_newRecurringDialog ui;
|
||||||
QDialog d(parent);
|
QDialog d(parent);
|
||||||
ui.setupUi(&d);
|
ui.setupUi(&d);
|
||||||
Settings::saveRestore(&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()) {
|
if (!tx.fromAddr.isEmpty()) {
|
||||||
ui.cmbFromAddress->setCurrentText(tx.fromAddr);
|
ui.lblFrom->setText(tx.fromAddr);
|
||||||
ui.cmbFromAddress->setEnabled(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.cmbCurrency->addItem(Settings::getTokenName());
|
|
||||||
ui.cmbCurrency->addItem("USD");
|
ui.cmbCurrency->addItem("USD");
|
||||||
|
ui.cmbCurrency->addItem(Settings::getTokenName());
|
||||||
|
|
||||||
if (tx.toAddrs.length() > 0) {
|
if (tx.toAddrs.length() > 0) {
|
||||||
ui.txtToAddr->setText(tx.toAddrs[0].addr);
|
ui.lblTo->setText(tx.toAddrs[0].addr);
|
||||||
ui.txtToAddr->setEnabled(false);
|
|
||||||
|
|
||||||
ui.txtAmt->setText(Settings::getDecimalString(tx.toAddrs[0].amount));
|
// Default is USD
|
||||||
ui.txtAmt->setEnabled(false);
|
ui.lblAmt->setText(Settings::getUSDFromZecAmount(tx.toAddrs[0].amount));
|
||||||
|
|
||||||
ui.txtMemo->setPlainText(tx.toAddrs[0].txtMemo);
|
ui.txtMemo->setPlainText(tx.toAddrs[0].txtMemo);
|
||||||
ui.txtMemo->setEnabled(false);
|
ui.txtMemo->setEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.cmbSchedule->addItem("Every Day", QVariant(Schedule::DAY));
|
// Wire up ZEC/USD toggle
|
||||||
ui.cmbSchedule->addItem("Every Week", QVariant(Schedule::WEEK));
|
QObject::connect(ui.cmbCurrency, QOverload<const QString&>::of(&QComboBox::currentIndexChanged), [&](QString c) {
|
||||||
ui.cmbSchedule->addItem("Every Month", QVariant(Schedule::MONTH));
|
if (tx.toAddrs.length() < 1)
|
||||||
ui.cmbSchedule->addItem("Every Year", QVariant(Schedule::YEAR));
|
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<int>::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");
|
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();
|
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<RecurringPaymentInfo::PaymentItem> 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<int> 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<int> 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<int> 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<void(QString, QString)> 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();
|
d.exec();
|
||||||
}
|
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();
|
||||||
|
}
|
||||||
|
|||||||
144
src/recurring.h
144
src/recurring.h
@@ -2,8 +2,14 @@
|
|||||||
#define RECURRING_H
|
#define RECURRING_H
|
||||||
|
|
||||||
#include "precompiled.h"
|
#include "precompiled.h"
|
||||||
|
#include "settings.h"
|
||||||
|
|
||||||
#include "mainwindow.h"
|
class MainWindow;
|
||||||
|
class Recurring;
|
||||||
|
class RecurringListViewModel;
|
||||||
|
class RecurringPaymentsListViewModel;
|
||||||
|
|
||||||
|
struct Tx;
|
||||||
|
|
||||||
enum Schedule {
|
enum Schedule {
|
||||||
DAY = 1,
|
DAY = 1,
|
||||||
@@ -12,25 +18,137 @@ enum Schedule {
|
|||||||
YEAR
|
YEAR
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RecurringPaymentInfo {
|
enum PaymentStatus {
|
||||||
QString desc;
|
NOT_STARTED = 0,
|
||||||
QString fromAddr;
|
PENDING,
|
||||||
QString toAddr;
|
SKIPPED,
|
||||||
double amt;
|
COMPLETED,
|
||||||
QString currency;
|
ERROR,
|
||||||
Schedule schedule;
|
UNKNOWN
|
||||||
int numPayments;
|
};
|
||||||
|
|
||||||
long startBlock;
|
QString schedule_desc(Schedule s);
|
||||||
int completedPayments;
|
|
||||||
|
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<PaymentItem> 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
|
class Recurring
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Recurring();
|
static Recurring* getInstance();
|
||||||
|
|
||||||
static void showEditDialog(QWidget* parent, MainWindow* main, Tx tx);
|
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<int> paymentNumber);
|
||||||
|
|
||||||
|
// Execute a Tx
|
||||||
|
void doSendTx(MainWindow* rpc, Tx tx, std::function<void(QString, QString)> cb);
|
||||||
|
|
||||||
|
bool updatePaymentItem(QString hash, int paymentNumber, QString txid, QString err, PaymentStatus status);
|
||||||
|
|
||||||
|
QList<RecurringPaymentInfo> getAsList() { return payments.values(); }
|
||||||
|
private:
|
||||||
|
Recurring() = default;
|
||||||
|
QMap<QString, RecurringPaymentInfo> 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;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QTableView* parent;
|
||||||
|
QStringList headers;
|
||||||
|
RecurringPaymentInfo rpi;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // RECURRING_H
|
#endif // RECURRING_H
|
||||||
@@ -15,26 +15,29 @@
|
|||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QTableView" name="tableView"/>
|
<widget class="QTableView" name="tableView">
|
||||||
|
<property name="selectionMode">
|
||||||
|
<enum>QAbstractItemView::SingleSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
|
<attribute name="horizontalHeaderStretchLastSection">
|
||||||
|
<bool>true</bool>
|
||||||
|
</attribute>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="pushButton">
|
<widget class="QPushButton" name="btnView">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Add</string>
|
<string>View</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="pushButton_2">
|
<widget class="QPushButton" name="btnDelete">
|
||||||
<property name="text">
|
|
||||||
<string>Edit</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="pushButton_3">
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Delete</string>
|
<string>Delete</string>
|
||||||
</property>
|
</property>
|
||||||
|
|||||||
178
src/recurringmultiple.ui
Normal file
178
src/recurringmultiple.ui
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>RecurringPending</class>
|
||||||
|
<widget class="QDialog" name="RecurringPending">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>883</width>
|
||||||
|
<height>801</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Dialog</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="14" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label_5">
|
||||||
|
<property name="text">
|
||||||
|
<string>No payments will be processed. You can manually pay them from the Recurring Payments Dialog box</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="15" column="0" colspan="2">
|
||||||
|
<widget class="Line" name="line_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="lblSchedule">
|
||||||
|
<property name="text">
|
||||||
|
<string>Schedule</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="8" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>How should ZecWallet proceed?</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="9" column="0" colspan="2">
|
||||||
|
<widget class="QRadioButton" name="rAll">
|
||||||
|
<property name="text">
|
||||||
|
<string>Pay All in 1 Tx</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="12" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label_4">
|
||||||
|
<property name="text">
|
||||||
|
<string>Only the latest pending payment will be processed. All previous pending payments will be skipped</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="11" column="0" colspan="2">
|
||||||
|
<widget class="QRadioButton" name="rLast">
|
||||||
|
<property name="text">
|
||||||
|
<string>Pay Latest Only</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="13" column="0" colspan="2">
|
||||||
|
<widget class="QRadioButton" name="rNone">
|
||||||
|
<property name="text">
|
||||||
|
<string>Pay None</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0" colspan="2">
|
||||||
|
<widget class="QTableView" name="tblPending">
|
||||||
|
<attribute name="horizontalHeaderStretchLastSection">
|
||||||
|
<bool>true</bool>
|
||||||
|
</attribute>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="7" column="0" colspan="2">
|
||||||
|
<widget class="Line" name="line">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="10" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>All pending payments collected, added up and paid in a single transaction</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="16" column="0" colspan="2">
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="lblDesc">
|
||||||
|
<property name="text">
|
||||||
|
<string>Description</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="lblTo">
|
||||||
|
<property name="text">
|
||||||
|
<string>To</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="Line" name="line_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>The following recurring payment has multiple payments pending</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>RecurringPending</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>RecurringPending</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
||||||
80
src/recurringpayments.ui
Normal file
80
src/recurringpayments.ui
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>RecurringPayments</class>
|
||||||
|
<widget class="QDialog" name="RecurringPayments">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>577</width>
|
||||||
|
<height>704</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Payments</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QTableView" name="tableView">
|
||||||
|
<property name="selectionMode">
|
||||||
|
<enum>QAbstractItemView::SingleSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
|
<attribute name="horizontalHeaderStretchLastSection">
|
||||||
|
<bool>true</bool>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="verticalHeaderStretchLastSection">
|
||||||
|
<bool>false</bool>
|
||||||
|
</attribute>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>RecurringPayments</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>RecurringPayments</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
||||||
@@ -73,7 +73,7 @@ void RequestDialog::showPaymentConfirmation(MainWindow* main, QString paymentURI
|
|||||||
req.txtFrom->setText(payInfo.addr);
|
req.txtFrom->setText(payInfo.addr);
|
||||||
req.txtMemo->setPlainText(payInfo.memo);
|
req.txtMemo->setPlainText(payInfo.memo);
|
||||||
req.txtAmount->setText(payInfo.amt);
|
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"));
|
req.buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Pay"));
|
||||||
|
|
||||||
@@ -112,9 +112,9 @@ void RequestDialog::showRequestZcash(MainWindow* main) {
|
|||||||
// Amount textbox
|
// Amount textbox
|
||||||
req.txtAmount->setValidator(main->getAmountValidator());
|
req.txtAmount->setValidator(main->getAmountValidator());
|
||||||
QObject::connect(req.txtAmount, &QLineEdit::textChanged, [=] (auto text) {
|
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->setAcceptButton(req.buttonBox->button(QDialogButtonBox::Ok));
|
||||||
req.txtMemo->setLenDisplayLabel(req.lblMemoLen);
|
req.txtMemo->setLenDisplayLabel(req.lblMemoLen);
|
||||||
|
|||||||
16
src/rpc.cpp
16
src/rpc.cpp
@@ -544,6 +544,10 @@ void RPC::getInfoThenRefresh(bool force) {
|
|||||||
Settings::getInstance()->setTestnet(reply["testnet"].get<json::boolean_t>());
|
Settings::getInstance()->setTestnet(reply["testnet"].get<json::boolean_t>());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Recurring pamynets are testnet only
|
||||||
|
if (!Settings::getInstance()->isTestnet())
|
||||||
|
main->disableRecurring();
|
||||||
|
|
||||||
// Connected, so display checkmark.
|
// Connected, so display checkmark.
|
||||||
QIcon i(":/icons/res/connected.gif");
|
QIcon i(":/icons/res/connected.gif");
|
||||||
main->statusIcon->setPixmap(i.pixmap(16, 16));
|
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.
|
// See if the turnstile migration has any steps that need to be done.
|
||||||
turnstile->executeMigrationStep();
|
turnstile->executeMigrationStep();
|
||||||
|
|
||||||
|
// See if recurring payments needs anything
|
||||||
|
Recurring::getInstance()->processPending(main);
|
||||||
|
|
||||||
refreshBalances();
|
refreshBalances();
|
||||||
refreshAddresses(); // This calls refreshZSentTransactions() and refreshReceivedZTrans()
|
refreshAddresses(); // This calls refreshZSentTransactions() and refreshReceivedZTrans()
|
||||||
refreshTransactions();
|
refreshTransactions();
|
||||||
@@ -643,7 +650,7 @@ void RPC::getInfoThenRefresh(bool force) {
|
|||||||
ui->lblSyncWarning->setVisible(isSyncing);
|
ui->lblSyncWarning->setVisible(isSyncing);
|
||||||
ui->lblSyncWarningReceive->setVisible(isSyncing);
|
ui->lblSyncWarningReceive->setVisible(isSyncing);
|
||||||
|
|
||||||
auto zecPrice = Settings::getUSDFormat(1);
|
auto zecPrice = Settings::getInstance()->getUSDFromZecAmount(1);
|
||||||
QString tooltip;
|
QString tooltip;
|
||||||
if (connections > 0) {
|
if (connections > 0) {
|
||||||
tooltip = QObject::tr("Connected to zcashd");
|
tooltip = QObject::tr("Connected to zcashd");
|
||||||
@@ -764,9 +771,10 @@ void RPC::refreshBalances() {
|
|||||||
ui->balTransparent->setText(Settings::getZECDisplayFormat(balT));
|
ui->balTransparent->setText(Settings::getZECDisplayFormat(balT));
|
||||||
ui->balTotal ->setText(Settings::getZECDisplayFormat(balTotal));
|
ui->balTotal ->setText(Settings::getZECDisplayFormat(balTotal));
|
||||||
|
|
||||||
ui->balSheilded ->setToolTip(Settings::getUSDFormat(balZ));
|
|
||||||
ui->balTransparent->setToolTip(Settings::getUSDFormat(balT));
|
ui->balSheilded ->setToolTip(Settings::getZECDisplayFormat(balZ));
|
||||||
ui->balTotal ->setToolTip(Settings::getUSDFormat(balTotal));
|
ui->balTransparent->setToolTip(Settings::getZECDisplayFormat(balT));
|
||||||
|
ui->balTotal ->setToolTip(Settings::getZECDisplayFormat(balTotal));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Get the UTXOs
|
// 2. Get the UTXOs
|
||||||
|
|||||||
147
src/sendtab.cpp
147
src/sendtab.cpp
@@ -62,7 +62,7 @@ void MainWindow::setupSendTab() {
|
|||||||
// Disable custom fees if settings say no
|
// Disable custom fees if settings say no
|
||||||
ui->minerFeeAmt->setReadOnly(!Settings::getInstance()->getAllowCustomFees());
|
ui->minerFeeAmt->setReadOnly(!Settings::getInstance()->getAllowCustomFees());
|
||||||
QObject::connect(ui->minerFeeAmt, &QLineEdit::textChanged, [=](auto txt) {
|
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()));
|
ui->minerFeeAmt->setText(Settings::getDecimalString(Settings::getMinerFee()));
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ void MainWindow::setupSendTab() {
|
|||||||
QObject::connect(ui->tabWidget, &QTabWidget::currentChanged, [=] (int pos) {
|
QObject::connect(ui->tabWidget, &QTabWidget::currentChanged, [=] (int pos) {
|
||||||
if (pos == 1) {
|
if (pos == 1) {
|
||||||
QString txt = ui->minerFeeAmt->text();
|
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
|
// Recurring button
|
||||||
QObject::connect(ui->chkRecurring, &QCheckBox::stateChanged, [=] (int checked) {
|
QObject::connect(ui->chkRecurring, &QCheckBox::stateChanged, [=] (int checked) {
|
||||||
if (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 {
|
} else {
|
||||||
ui->btnRecurSchedule->setEnabled(false);
|
ui->btnRecurSchedule->setEnabled(false);
|
||||||
ui->lblRecurDesc->setText("");
|
ui->lblRecurDesc->setText("");
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Recurring schedule button
|
// Recurring schedule button
|
||||||
QObject::connect(ui->btnRecurSchedule, &QPushButton::clicked, this, &MainWindow::editSchedule);
|
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
|
// 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() {
|
void MainWindow::editSchedule() {
|
||||||
// Open the edit schedule dialog
|
// Only on testnet for now
|
||||||
Recurring::showEditDialog(this, this, createTxFromSendPage());
|
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
|
||||||
|
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() {
|
void MainWindow::updateLabelsAutoComplete() {
|
||||||
@@ -192,7 +225,7 @@ void MainWindow::inputComboTextChanged(int index) {
|
|||||||
auto balFmt = Settings::getZECDisplayFormat(bal);
|
auto balFmt = Settings::getZECDisplayFormat(bal);
|
||||||
|
|
||||||
ui->sendAddressBalance->setText(balFmt);
|
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);
|
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
|
// Set focus into the address
|
||||||
Address1->setFocus();
|
Address1->setFocus();
|
||||||
|
|
||||||
@@ -297,7 +338,13 @@ void MainWindow::addressChanged(int itemNumber, const QString& text) {
|
|||||||
|
|
||||||
void MainWindow::amountChanged(int item, const QString& text) {
|
void MainWindow::amountChanged(int item, const QString& text) {
|
||||||
auto usd = ui->sendToWidgets->findChild<QLabel*>(QString("AmtUSD") % QString::number(item));
|
auto usd = ui->sendToWidgets->findChild<QLabel*>(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) {
|
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
|
// The last one is a spacer, so ignore that
|
||||||
int totalItems = ui->sendToWidgets->children().size() - 2;
|
int totalItems = ui->sendToWidgets->children().size() - 2;
|
||||||
|
|
||||||
@@ -395,9 +442,15 @@ void MainWindow::removeExtraAddresses() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset the recurring button
|
// Reset the recurring button
|
||||||
|
if (Settings::getInstance()->isTestnet()) {
|
||||||
|
ui->chkRecurring->setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
ui->chkRecurring->setCheckState(Qt::Unchecked);
|
ui->chkRecurring->setCheckState(Qt::Unchecked);
|
||||||
ui->btnRecurSchedule->setEnabled(false);
|
ui->btnRecurSchedule->setEnabled(false);
|
||||||
ui->lblRecurDesc->setText("");
|
ui->lblRecurDesc->setText("");
|
||||||
|
delete sendTxRecurringInfo;
|
||||||
|
sendTxRecurringInfo = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::maxAmountChecked(int checked) {
|
void MainWindow::maxAmountChecked(int checked) {
|
||||||
@@ -498,7 +551,7 @@ Tx MainWindow::createTxFromSendPage() {
|
|||||||
return tx;
|
return tx;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MainWindow::confirmTx(Tx tx) {
|
bool MainWindow::confirmTx(Tx tx, RecurringPaymentInfo* rpi) {
|
||||||
auto fnSplitAddressForWrap = [=] (const QString& a) -> QString {
|
auto fnSplitAddressForWrap = [=] (const QString& a) -> QString {
|
||||||
if (! Settings::isZAddress(a)) return a;
|
if (! Settings::isZAddress(a)) return a;
|
||||||
|
|
||||||
@@ -507,6 +560,10 @@ bool MainWindow::confirmTx(Tx tx) {
|
|||||||
return splitted;
|
return splitted;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update the recurring info with the latest Tx
|
||||||
|
if (rpi != nullptr) {
|
||||||
|
Recurring::getInstance()->updateInfoWithTx(rpi, tx);
|
||||||
|
}
|
||||||
|
|
||||||
// Show a confirmation dialog
|
// Show a confirmation dialog
|
||||||
QDialog d(this);
|
QDialog d(this);
|
||||||
@@ -561,7 +618,7 @@ bool MainWindow::confirmTx(Tx tx) {
|
|||||||
// Amount (USD)
|
// Amount (USD)
|
||||||
auto AmtUSD = new QLabel(confirm.sendToAddrs);
|
auto AmtUSD = new QLabel(confirm.sendToAddrs);
|
||||||
AmtUSD->setObjectName(QString("AmtUSD") % QString::number(i + 1));
|
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);
|
AmtUSD->setAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter);
|
||||||
confirm.gridLayout->addWidget(AmtUSD, row, 2, 1, 1);
|
confirm.gridLayout->addWidget(AmtUSD, row, 2, 1, 1);
|
||||||
|
|
||||||
@@ -612,7 +669,7 @@ bool MainWindow::confirmTx(Tx tx) {
|
|||||||
minerFeeUSD->setObjectName(QStringLiteral("minerFeeUSD"));
|
minerFeeUSD->setObjectName(QStringLiteral("minerFeeUSD"));
|
||||||
minerFeeUSD->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
|
minerFeeUSD->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
|
||||||
confirm.gridLayout->addWidget(minerFeeUSD, row, 2, 1, 1);
|
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()) {
|
if (Settings::getInstance()->getAllowCustomFees() && tx.fee != Settings::getMinerFee()) {
|
||||||
confirm.warningLabel->setVisible(true);
|
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
|
// Syncing warning
|
||||||
confirm.syncingWarning->setVisible(Settings::getInstance()->isSyncing());
|
confirm.syncingWarning->setVisible(Settings::getInstance()->isSyncing());
|
||||||
|
|
||||||
@@ -637,13 +703,7 @@ bool MainWindow::confirmTx(Tx tx) {
|
|||||||
confirm.sendFrom->setToolTip(tooltip);
|
confirm.sendFrom->setToolTip(tooltip);
|
||||||
|
|
||||||
// Show the dialog and submit it if the user confirms
|
// Show the dialog and submit it if the user confirms
|
||||||
if (d.exec() == QDialog::Accepted) {
|
return d.exec() == QDialog::Accepted;
|
||||||
// Then delete the additional fields from the sendTo tab
|
|
||||||
removeExtraAddresses();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send button clicked
|
// Send button clicked
|
||||||
@@ -661,23 +721,54 @@ void MainWindow::sendButton() {
|
|||||||
// abort the Tx
|
// abort the Tx
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show a dialog to confirm the Tx
|
// 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
|
// And send the Tx
|
||||||
rpc->executeTransaction(tx,
|
rpc->executeTransaction(tx,
|
||||||
|
// Submitted
|
||||||
[=] (QString opid) {
|
[=] (QString opid) {
|
||||||
ui->statusBar->showMessage(tr("Computing Tx: ") % opid);
|
ui->statusBar->showMessage(tr("Computing Tx: ") % opid);
|
||||||
},
|
},
|
||||||
|
// Accepted
|
||||||
[=] (QString, QString txid) {
|
[=] (QString, QString txid) {
|
||||||
ui->statusBar->showMessage(Settings::txidStatusMessage + " " + 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) {
|
[=] (QString opid, QString errStr) {
|
||||||
ui->statusBar->showMessage(QObject::tr(" Tx ") % opid % QObject::tr(" failed"), 15 * 1000);
|
ui->statusBar->showMessage(QObject::tr(" Tx ") % opid % QObject::tr(" failed"), 15 * 1000);
|
||||||
|
|
||||||
if (!opid.isEmpty())
|
if (!opid.isEmpty())
|
||||||
errStr = QObject::tr("The transaction with id ") % opid % QObject::tr(" failed. The error was") + ":\n\n" + errStr;
|
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);
|
QMessageBox::critical(this, QObject::tr("Transaction Error"), errStr, QMessageBox::Ok);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -705,6 +796,6 @@ QString MainWindow::doSendTxValidations(Tx tx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::cancelButton() {
|
void MainWindow::cancelButton() {
|
||||||
removeExtraAddresses();
|
clearSendForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -161,9 +161,15 @@ void Settings::saveRestore(QDialog* d) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString Settings::getUSDFormat(double bal) {
|
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 Settings::getDecimalString(double amt) {
|
||||||
QString f = QString::number(amt, 'f', 8);
|
QString f = QString::number(amt, 'f', 8);
|
||||||
|
|
||||||
@@ -182,9 +188,9 @@ QString Settings::getZECDisplayFormat(double bal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString Settings::getZECUSDDisplayFormat(double bal) {
|
QString Settings::getZECUSDDisplayFormat(double bal) {
|
||||||
auto usdFormat = getUSDFormat(bal);
|
auto usdFormat = getUSDFromZecAmount(bal);
|
||||||
if (!usdFormat.isEmpty())
|
if (!usdFormat.isEmpty())
|
||||||
return getZECDisplayFormat(bal) % " (" % getUSDFormat(bal) % ")";
|
return getZECDisplayFormat(bal) % " (" % usdFormat % ")";
|
||||||
else
|
else
|
||||||
return getZECDisplayFormat(bal);
|
return getZECDisplayFormat(bal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ public:
|
|||||||
static bool isTAddress(QString addr);
|
static bool isTAddress(QString addr);
|
||||||
|
|
||||||
static QString getDecimalString(double amt);
|
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 getZECDisplayFormat(double bal);
|
||||||
static QString getZECUSDDisplayFormat(double bal);
|
static QString getZECUSDDisplayFormat(double bal);
|
||||||
|
|
||||||
|
|||||||
@@ -246,6 +246,11 @@ void Turnstile::executeMigrationStep() {
|
|||||||
if (Settings::getInstance()->isSyncing())
|
if (Settings::getInstance()->isSyncing())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Also, process payments only when the Payments UI is ready, otherwise
|
||||||
|
// we might mess things up
|
||||||
|
if (!mainwindow->isPaymentsReady())
|
||||||
|
return;
|
||||||
|
|
||||||
auto plan = readMigrationPlan();
|
auto plan = readMigrationPlan();
|
||||||
|
|
||||||
//qDebug() << QString("Executing step");
|
//qDebug() << QString("Executing step");
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ void TxTableModel::updateAllData() {
|
|||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
case 2: return QDateTime::fromMSecsSinceEpoch(modeldata->at(index.row()).datetime * (qint64)1000).toLocalTime().toString();
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ HEADERS += \
|
|||||||
|
|
||||||
FORMS += \
|
FORMS += \
|
||||||
src/mainwindow.ui \
|
src/mainwindow.ui \
|
||||||
|
src/recurringpayments.ui \
|
||||||
src/settings.ui \
|
src/settings.ui \
|
||||||
src/about.ui \
|
src/about.ui \
|
||||||
src/confirm.ui \
|
src/confirm.ui \
|
||||||
@@ -100,7 +101,8 @@ FORMS += \
|
|||||||
src/createzcashconfdialog.ui \
|
src/createzcashconfdialog.ui \
|
||||||
src/recurringdialog.ui \
|
src/recurringdialog.ui \
|
||||||
src/newrecurring.ui \
|
src/newrecurring.ui \
|
||||||
src/requestdialog.ui
|
src/requestdialog.ui \
|
||||||
|
src/recurringmultiple.ui
|
||||||
|
|
||||||
|
|
||||||
TRANSLATIONS = res/zec_qt_wallet_es.ts \
|
TRANSLATIONS = res/zec_qt_wallet_es.ts \
|
||||||
|
|||||||
Reference in New Issue
Block a user