diff --git a/application.qrc b/application.qrc index e7e9b2c..c7201af 100644 --- a/application.qrc +++ b/application.qrc @@ -5,6 +5,7 @@ res/connected.gif res/loading.gif + res/paymentreq.gif res/icon.ico diff --git a/res/paymentreq.gif b/res/paymentreq.gif new file mode 100644 index 0000000..f451982 Binary files /dev/null and b/res/paymentreq.gif differ diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 01573e4..82ba03c 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -804,12 +804,6 @@ void MainWindow::payZcashURI(QString uri) { return; } - // Error to display if something goes wrong. - auto payZcashURIError = [=] (QString errorDetail = "") { - QMessageBox::critical(this, tr("Error paying zcash URI"), - tr("URI should be of the form 'zcash:?amt=x&memo=y") + "\n" + errorDetail); - }; - // If there was no URI passed, ask the user for one. if (uri.isEmpty()) { uri = QInputDialog::getText(this, tr("Paste Zcash URI"), @@ -820,70 +814,21 @@ void MainWindow::payZcashURI(QString uri) { if (uri.isEmpty()) return; - // URI should be of the form zcash://address?amt=x&memo=y - if (!uri.startsWith("zcash:")) { - payZcashURIError(); - return; - } - // Extract the address qDebug() << "Recieved URI " << uri; - uri = uri.right(uri.length() - QString("zcash:").length()); - - QRegExp re("([a-zA-Z0-9]+)"); - int pos; - if ( (pos = re.indexIn(uri)) == -1 ) { - payZcashURIError(); - return; - } - - QString addr = re.cap(1); - if (!Settings::isValidAddress(addr)) { - payZcashURIError(tr("Could not understand address")); + PaymentURI paymentInfo = Settings::parseURI(uri); + if (!paymentInfo.error.isEmpty()) { + QMessageBox::critical(this, tr("Error paying zcash URI"), + tr("URI should be of the form 'zcash:?amt=x&memo=y") + "\n" + paymentInfo.error); return; } - uri = uri.right(uri.length() - addr.length()); - - double amount = 0.0; - QString memo = ""; - - if (!uri.isEmpty()) { - uri = uri.right(uri.length() - 1); // Eat the "?" - - QStringList args = uri.split("&"); - for (QString arg: args) { - QStringList kv = arg.split("="); - if (kv.length() != 2) { - payZcashURIError(); - return; - } - - if (kv[0].toLower() == "amt" || kv[0].toLower() == "amount") { - amount = kv[1].toDouble(); - } else if (kv[0].toLower() == "memo" || kv[0].toLower() == "message" || kv[0].toLower() == "msg") { - memo = kv[1]; - // Test if this is hex - - QRegularExpression hexMatcher("^[0-9A-F]+$", - QRegularExpression::CaseInsensitiveOption); - QRegularExpressionMatch match = hexMatcher.match(memo); - if (match.hasMatch()) { - // Encoded as hex, convert to string - memo = QByteArray::fromHex(memo.toUtf8()); - } - } else { - payZcashURIError(tr("Unknown field in URI:") + kv[0]); - return; - } - } - } // Now, set the fields on the send tab removeExtraAddresses(); - ui->Address1->setText(addr); + ui->Address1->setText(paymentInfo.addr); ui->Address1->setCursorPosition(0); - ui->Amount1->setText(QString::number(amount)); - ui->MemoTxt1->setText(memo); + ui->Amount1->setText(Settings::getDecimalString(paymentInfo.amt.toDouble())); + ui->MemoTxt1->setText(paymentInfo.memo); // And switch to the send tab. ui->tabWidget->setCurrentIndex(1); @@ -891,7 +836,7 @@ void MainWindow::payZcashURI(QString uri) { // And click the send button if the amount is > 0, to validate everything. If everything is OK, it will show the confirm box // else, show the error message; - if (amount > 0) { + if (paymentInfo.amt > 0) { sendButton(); } } @@ -1214,8 +1159,16 @@ void MainWindow::setupTransactionsTab() { QDesktopServices::openUrl(QUrl(url)); }); + // Payment Request + if (!memo.isEmpty() && memo.startsWith("zcash:")) { + menu.addAction(tr("View Payment Request"), [=] () { + RequestDialog::showPaymentConfirmation(this, memo); + }); + } + + // View Memo if (!memo.isEmpty()) { - menu.addAction(tr("View Memo"), [=] () { + menu.addAction(tr("View Memo"), [=] () { QMessageBox mb(QMessageBox::Information, tr("Memo"), memo, QMessageBox::Ok, this); mb.setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); mb.exec(); diff --git a/src/requestdialog.cpp b/src/requestdialog.cpp index abfe170..da95c3d 100644 --- a/src/requestdialog.cpp +++ b/src/requestdialog.cpp @@ -2,6 +2,10 @@ #include "ui_requestdialog.h" #include "settings.h" #include "addressbook.h" +#include "mainwindow.h" +#include "rpc.h" +#include "settings.h" + RequestDialog::RequestDialog(QWidget *parent) : QDialog(parent), @@ -15,6 +19,51 @@ RequestDialog::~RequestDialog() delete ui; } +void RequestDialog::setupDialog(QDialog* d, Ui_RequestDialog* req) { + req->setupUi(d); + Settings::saveRestore(d); + + // Setup + req->txtMemo->setLenDisplayLabel(req->lblMemoLen); + req->lblAmount->setText(req->lblAmount->text() + Settings::getTokenName()); +} + +// Static method that shows an incoming payment request and prompts the user to pay it +void RequestDialog::showPaymentConfirmation(MainWindow* main, QString paymentURI) { + PaymentURI payInfo = Settings::parseURI(paymentURI); + if (!payInfo.error.isEmpty()) { + QMessageBox::critical(main, tr("Error paying zcash URI"), + tr("URI should be of the form 'zcash:?amt=x&memo=y") + "\n" + payInfo.error); + return; + } + + QDialog d(main); + Ui_RequestDialog req; + setupDialog(&d, &req); + + // In the view mode, all fields are read-only + req.txtAmount->setReadOnly(true); + req.txtFrom->setReadOnly(true); + req.txtMemo->setReadOnly(true); + + // Payment is "to" + req.lblAddress->setText(tr("Pay To")); + + // No Addressbook + req.btnAddressBook->setVisible(false); + + 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.buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Pay")); + + if (d.exec() == QDialog::Accepted) { + main->payZcashURI(paymentURI); + } +} + // Static method that shows the request dialog void RequestDialog::showRequestZcash(MainWindow* main) { QDialog d(main); @@ -55,9 +104,15 @@ void RequestDialog::showRequestZcash(MainWindow* main) { if (d.exec() == QDialog::Accepted) { // Construct a zcash Payment URI with the data and pay it immediately. - QString paymentURI = "zcash:" + AddressBook::addressFromAddressLabel(req.txtFrom->text()) + QString memoURI = "zcash:" + main->getRPC()->getDefaultSaplingAddress() + "?amt=" + Settings::getDecimalString(req.txtAmount->text().toDouble()) + "&memo=" + QUrl::toPercentEncoding(req.txtMemo->toPlainText()); - main->payZcashURI(paymentURI); + + QString sendURI = "zcash:" + AddressBook::addressFromAddressLabel(req.txtFrom->text()) + + "?amt=0.0001" + + "&memo=" + QUrl::toPercentEncoding(memoURI); + + qDebug() << "Paying " << sendURI; + main->payZcashURI(sendURI); } -} \ No newline at end of file +} diff --git a/src/requestdialog.h b/src/requestdialog.h index eca2e5d..a05b3c7 100644 --- a/src/requestdialog.h +++ b/src/requestdialog.h @@ -3,6 +3,7 @@ #include #include "mainwindow.h" +#include "ui_requestdialog.h" namespace Ui { class RequestDialog; @@ -17,7 +18,8 @@ public: ~RequestDialog(); static void showRequestZcash(MainWindow* main); - + static void showPaymentConfirmation(MainWindow* main, QString paymentURI); + static void setupDialog(QDialog* d, Ui_RequestDialog* req); private: Ui::RequestDialog *ui; }; diff --git a/src/requestdialog.ui b/src/requestdialog.ui index dd3614f..f188f8b 100644 --- a/src/requestdialog.ui +++ b/src/requestdialog.ui @@ -6,41 +6,41 @@ 0 0 - 714 - 524 + 1052 + 509 - Dialog + Payment Request + + + + Qt::Vertical + + + + 20 + 40 + + + + - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter Amount - - - - color: red; - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - + Qt::Horizontal @@ -67,8 +67,38 @@ - + + + + + 0 + 0 + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + z address + + + + + + + Request From + + + + + + + + AddressBook + + + @@ -82,35 +112,25 @@ - - - - AddressBook - - - - - - - - 0 - 0 - + + + + color: red; - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + - - z address + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - + - Request From + @@ -120,24 +140,37 @@ Amount USD - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - + - Amount in ZEC + Amount in - + Qt::Horizontal + + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -147,6 +180,11 @@
memoedit.h
+ + txtFrom + txtAmount + txtMemo + diff --git a/src/settings.cpp b/src/settings.cpp index 6810d51..c291247 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -161,10 +161,7 @@ void Settings::saveRestore(QDialog* d) { } QString Settings::getUSDFormat(double bal) { - if (!Settings::getInstance()->isTestnet() && Settings::getInstance()->getZECPrice() > 0) - return "$" + QLocale(QLocale::English).toString(bal * Settings::getInstance()->getZECPrice(), 'f', 2); - else - return QString(); + return "$" + QLocale(QLocale::English).toString(bal * Settings::getInstance()->getZECPrice(), 'f', 2); } QString Settings::getDecimalString(double amt) { @@ -289,4 +286,60 @@ bool Settings::isValidAddress(QString addr) { ztsexp.exactMatch(addr) || zsexp.exactMatch(addr); } +// Get a pretty string representation of this Payment URI +QString Settings::paymentURIPretty(PaymentURI uri) { + return QString() + "Payment Request\n" + "Pay: " + uri.addr + "\nAmount: " + getZECDisplayFormat(uri.amt.toDouble()) + + "\nMemo:" + QUrl::fromPercentEncoding(uri.memo.toUtf8()); +} + +// Parse a payment URI string into its components +PaymentURI Settings::parseURI(QString uri) { + PaymentURI ans; + + if (!uri.startsWith("zcash:")) { + ans.error = "Not a zcash payment URI"; + return ans; + } + + uri = uri.right(uri.length() - QString("zcash:").length()); + + QRegExp re("([a-zA-Z0-9]+)"); + int pos; + if ( (pos = re.indexIn(uri)) == -1 ) { + ans.error = "Couldn't find an address"; + return ans; + } + + ans.addr = re.cap(1); + if (!Settings::isValidAddress(ans.addr)) { + ans.error = "Could not understand address"; + return ans; + } + uri = uri.right(uri.length() - ans.addr.length()); + + if (!uri.isEmpty()) { + uri = uri.right(uri.length() - 1); // Eat the "?" + + QStringList args = uri.split("&"); + for (QString arg: args) { + QStringList kv = arg.split("="); + if (kv.length() != 2) { + ans.error = "No value argument was seen"; + return ans; + } + + if (kv[0].toLower() == "amt" || kv[0].toLower() == "amount") { + ans.amt = kv[1]; + } else if (kv[0].toLower() == "memo" || kv[0].toLower() == "message" || kv[0].toLower() == "msg") { + ans.memo = QUrl::fromPercentEncoding(kv[1].toUtf8()); + } else { + ans.error = "Unknown field in URI:" + kv[0]; + return ans; + } + } + } + + return ans; +} + const QString Settings::labelRegExp("[a-zA-Z0-9\\-_]{0,40}"); diff --git a/src/settings.h b/src/settings.h index 03ba530..ee3a053 100644 --- a/src/settings.h +++ b/src/settings.h @@ -13,6 +13,15 @@ struct Config { struct ToFields; struct Tx; +struct PaymentURI { + QString addr; + QString amt; + QString memo; + + // Any errors are stored here + QString error; +}; + class Settings { public: @@ -68,6 +77,9 @@ public: static void saveRestore(QDialog* d); + static PaymentURI parseURI(QString paymentURI); + static QString paymentURIPretty(PaymentURI); + static bool isZAddress(QString addr); static bool isTAddress(QString addr); diff --git a/src/txtablemodel.cpp b/src/txtablemodel.cpp index cc099e7..f563921 100644 --- a/src/txtablemodel.cpp +++ b/src/txtablemodel.cpp @@ -138,8 +138,14 @@ void TxTableModel::updateAllData() { if (role == Qt::ToolTipRole) { switch (index.column()) { - case 0: return modeldata->at(index.row()).type + - (dat.memo.isEmpty() ? "" : " tx memo: \"" + dat.memo + "\""); + case 0: { + if (dat.memo.startsWith("zcash:")) { + return Settings::paymentURIPretty(Settings::parseURI(dat.memo)); + } else { + return modeldata->at(index.row()).type + + (dat.memo.isEmpty() ? "" : " tx memo: \"" + dat.memo + "\""); + } + } case 1: { auto addr = modeldata->at(index.row()).address; if (addr.trimmed().isEmpty()) @@ -154,9 +160,15 @@ void TxTableModel::updateAllData() { if (role == Qt::DecorationRole && index.column() == 0) { if (!dat.memo.isEmpty()) { - // Return the info pixmap to indicate memo - QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxInformation); - return QVariant(icon.pixmap(16, 16)); + // If the memo is a Payment URI, then show a payment request icon + if (dat.memo.startsWith("zcash:")) { + QIcon icon(":/icons/res/paymentreq.gif"); + return QVariant(icon.pixmap(16, 16)); + } else { + // Return the info pixmap to indicate memo + QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxInformation); + return QVariant(icon.pixmap(16, 16)); + } } else { // Empty pixmap to make it align QPixmap p(16, 16);