aboutsummaryrefslogtreecommitdiff
path: root/src/qt
diff options
context:
space:
mode:
authorGavin Andresen <gavinandresen@gmail.com>2013-07-22 16:50:39 +1000
committerGavin Andresen <gavinandresen@gmail.com>2013-08-22 16:18:25 +1000
commita41d5fe01947f2f878c055670986a165af800f9a (patch)
tree40eeada1ebc180f8e3669a7d164104686dc0c618 /src/qt
parent47d0534368fbf0e3fb2cad7d05d60501d29f62aa (diff)
downloadbitcoin-a41d5fe01947f2f878c055670986a165af800f9a.tar.xz
Payment Protocol: X509-validated payment requests
Add support for a Payment Protocol to Bitcoin-Qt. Payment messages are protocol-buffer encoded and communicated over http(s), so this adds a dependency on the Google protocol buffer library, and requires Qt with OpenSSL support.
Diffstat (limited to 'src/qt')
-rw-r--r--src/qt/addresstablemodel.cpp27
-rw-r--r--src/qt/bitcoin.cpp16
-rw-r--r--src/qt/bitcoinamountfield.cpp5
-rw-r--r--src/qt/bitcoinamountfield.h3
-rw-r--r--src/qt/bitcoingui.cpp20
-rw-r--r--src/qt/bitcoingui.h5
-rw-r--r--src/qt/forms/sendcoinsentry.ui792
-rw-r--r--src/qt/guiconstants.h3
-rw-r--r--src/qt/optionsmodel.cpp11
-rw-r--r--src/qt/optionsmodel.h1
-rw-r--r--src/qt/paymentrequest.proto46
-rw-r--r--src/qt/paymentrequestplus.cpp204
-rw-r--r--src/qt/paymentrequestplus.h41
-rw-r--r--src/qt/paymentserver.cpp527
-rw-r--r--src/qt/paymentserver.h64
-rw-r--r--src/qt/sendcoinsdialog.cpp47
-rw-r--r--src/qt/sendcoinsdialog.h3
-rw-r--r--src/qt/sendcoinsentry.cpp35
-rw-r--r--src/qt/sendcoinsentry.h14
-rw-r--r--src/qt/test/paymentrequestdata.h307
-rw-r--r--src/qt/test/paymentservertests.cpp109
-rw-r--r--src/qt/test/paymentservertests.h29
-rw-r--r--src/qt/test/test_main.cpp37
-rw-r--r--src/qt/transactiondesc.cpp16
-rw-r--r--src/qt/walletframe.cpp4
-rw-r--r--src/qt/walletframe.h5
-rw-r--r--src/qt/walletmodel.cpp96
-rw-r--r--src/qt/walletmodel.h16
-rw-r--r--src/qt/walletstack.cpp4
-rw-r--r--src/qt/walletstack.h3
-rw-r--r--src/qt/walletview.cpp4
-rw-r--r--src/qt/walletview.h3
32 files changed, 2231 insertions, 266 deletions
diff --git a/src/qt/addresstablemodel.cpp b/src/qt/addresstablemodel.cpp
index 823730c4ca..dcc70222cc 100644
--- a/src/qt/addresstablemodel.cpp
+++ b/src/qt/addresstablemodel.cpp
@@ -62,9 +62,19 @@ public:
BOOST_FOREACH(const PAIRTYPE(CTxDestination, CAddressBookData)& item, wallet->mapAddressBook)
{
const CBitcoinAddress& address = item.first;
+
+ AddressTableEntry::Type addressType;
+ const std::string& strPurpose = item.second.purpose;
+ if (strPurpose == "send") addressType = AddressTableEntry::Sending;
+ else if (strPurpose == "receive") addressType = AddressTableEntry::Receiving;
+ else if (strPurpose == "unknown") {
+ bool fMine = IsMine(*wallet, address.Get());
+ addressType = (fMine ? AddressTableEntry::Receiving : AddressTableEntry::Sending);
+ }
+ else continue; // "refund" addresses aren't shown, and change addresses aren't in mapAddressBook at all.
+
const std::string& strName = item.second.name;
- bool fMine = IsMine(*wallet, address.Get());
- cachedAddressTable.append(AddressTableEntry(fMine ? AddressTableEntry::Receiving : AddressTableEntry::Sending,
+ cachedAddressTable.append(AddressTableEntry(addressType,
QString::fromStdString(strName),
QString::fromStdString(address.ToString())));
}
@@ -215,7 +225,7 @@ bool AddressTableModel::setData(const QModelIndex &index, const QVariant &value,
if(!index.isValid())
return false;
AddressTableEntry *rec = static_cast<AddressTableEntry*>(index.internalPointer());
-
+ std::string strPurpose = (rec->type == AddressTableEntry::Sending ? "send" : "receive");
editStatus = OK;
if(role == Qt::EditRole)
@@ -229,7 +239,7 @@ bool AddressTableModel::setData(const QModelIndex &index, const QVariant &value,
editStatus = NO_CHANGES;
return false;
}
- wallet->SetAddressBookName(CBitcoinAddress(rec->address.toStdString()).Get(), value.toString().toStdString());
+ wallet->SetAddressBook(CBitcoinAddress(rec->address.toStdString()).Get(), value.toString().toStdString(), strPurpose);
break;
case Address:
// Do nothing, if old address == new address
@@ -257,9 +267,9 @@ bool AddressTableModel::setData(const QModelIndex &index, const QVariant &value,
{
LOCK(wallet->cs_wallet);
// Remove old entry
- wallet->DelAddressBookName(CBitcoinAddress(rec->address.toStdString()).Get());
+ wallet->DelAddressBook(CBitcoinAddress(rec->address.toStdString()).Get());
// Add new entry with new address
- wallet->SetAddressBookName(CBitcoinAddress(value.toString().toStdString()).Get(), rec->label.toStdString());
+ wallet->SetAddressBook(CBitcoinAddress(value.toString().toStdString()).Get(), rec->label.toStdString(), strPurpose);
}
}
break;
@@ -368,7 +378,8 @@ QString AddressTableModel::addRow(const QString &type, const QString &label, con
// Add entry
{
LOCK(wallet->cs_wallet);
- wallet->SetAddressBookName(CBitcoinAddress(strAddress).Get(), strLabel);
+ wallet->SetAddressBook(CBitcoinAddress(strAddress).Get(), strLabel,
+ (type == Send ? "send" : "receive"));
}
return QString::fromStdString(strAddress);
}
@@ -385,7 +396,7 @@ bool AddressTableModel::removeRows(int row, int count, const QModelIndex &parent
}
{
LOCK(wallet->cs_wallet);
- wallet->DelAddressBookName(CBitcoinAddress(rec->address.toStdString()).Get());
+ wallet->DelAddressBook(CBitcoinAddress(rec->address.toStdString()).Get());
}
return true;
}
diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp
index 5ab5d1f7ee..e7cf440044 100644
--- a/src/qt/bitcoin.cpp
+++ b/src/qt/bitcoin.cpp
@@ -213,7 +213,7 @@ int main(int argc, char *argv[])
// Do this early as we don't want to bother initializing if we are just calling IPC
// ... but do it after creating app and setting up translations, so errors are
// translated properly.
- if (PaymentServer::ipcSendCommandLine())
+ if (PaymentServer::ipcSendCommandLine(argc, argv))
exit(0);
// Now that translations are initialized check for errors and allow a translatable error message
@@ -299,6 +299,9 @@ int main(int argc, char *argv[])
optionsModel.Upgrade(); // Must be done after AppInit2
+ PaymentServer::LoadRootCAs();
+ paymentServer->initNetManager(optionsModel);
+
if (splashref)
splash.finish(&window);
@@ -320,8 +323,15 @@ int main(int argc, char *argv[])
}
// Now that initialization/startup is done, process any command-line
- // bitcoin: URIs
- QObject::connect(paymentServer, SIGNAL(receivedURI(QString)), &window, SLOT(handleURI(QString)));
+ // bitcoin: URIs or payment requests:
+ QObject::connect(paymentServer, SIGNAL(receivedPaymentRequest(SendCoinsRecipient)),
+ &window, SLOT(handlePaymentRequest(SendCoinsRecipient)));
+ QObject::connect(&walletModel, SIGNAL(coinsSent(CWallet*,SendCoinsRecipient,QByteArray)),
+ paymentServer, SLOT(fetchPaymentACK(CWallet*,const SendCoinsRecipient&,QByteArray)));
+ QObject::connect(paymentServer, SIGNAL(receivedPaymentACK(QString)),
+ &window, SLOT(showPaymentACK(QString)));
+ QObject::connect(paymentServer, SIGNAL(reportError(QString, QString, unsigned int)),
+ guiref, SLOT(message(QString, QString, unsigned int)));
QTimer::singleShot(100, paymentServer, SLOT(uiReady()));
app.exec();
diff --git a/src/qt/bitcoinamountfield.cpp b/src/qt/bitcoinamountfield.cpp
index eeb6fe89bb..d9d4e3b23d 100644
--- a/src/qt/bitcoinamountfield.cpp
+++ b/src/qt/bitcoinamountfield.cpp
@@ -130,6 +130,11 @@ void BitcoinAmountField::setValue(qint64 value)
setText(BitcoinUnits::format(currentUnit, value));
}
+void BitcoinAmountField::setReadOnly(bool fReadeOnly)
+{
+ // TODO ...
+}
+
void BitcoinAmountField::unitChanged(int idx)
{
// Use description tooltip for current unit for the combobox
diff --git a/src/qt/bitcoinamountfield.h b/src/qt/bitcoinamountfield.h
index dacbb395ec..9c8be5a26a 100644
--- a/src/qt/bitcoinamountfield.h
+++ b/src/qt/bitcoinamountfield.h
@@ -22,6 +22,9 @@ public:
qint64 value(bool *valid=0) const;
void setValue(qint64 value);
+ /** Make read-only **/
+ void setReadOnly(bool fReadOnly);
+
/** Mark current value as invalid in UI. */
void setValid(bool valid);
/** Perform input validation, mark field as invalid if entered value is not valid. */
diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp
index 8ec2f03fad..ad32c9ea68 100644
--- a/src/qt/bitcoingui.cpp
+++ b/src/qt/bitcoingui.cpp
@@ -45,6 +45,7 @@
#include <QDragEnterEvent>
#if QT_VERSION < 0x050000
#include <QUrl>
+#include <QTextDocument>
#endif
#include <QMimeData>
#include <QStyle>
@@ -707,7 +708,8 @@ void BitcoinGUI::dropEvent(QDropEvent *event)
QList<QUrl> uris = event->mimeData()->urls();
foreach(const QUrl &uri, uris)
{
- if (walletFrame->handleURI(uri.toString()))
+ SendCoinsRecipient r;
+ if (GUIUtil::parseBitcoinURI(uri, &r) && walletFrame->handlePaymentRequest(r))
nValidUrisFound++;
}
@@ -734,12 +736,18 @@ bool BitcoinGUI::eventFilter(QObject *object, QEvent *event)
return QMainWindow::eventFilter(object, event);
}
-void BitcoinGUI::handleURI(QString strURI)
+void BitcoinGUI::handlePaymentRequest(const SendCoinsRecipient& recipient)
{
- // URI has to be valid
- if (!walletFrame->handleURI(strURI))
- message(tr("URI handling"), tr("URI can not be parsed! This can be caused by an invalid Bitcoin address or malformed URI parameters."),
- CClientUIInterface::ICON_WARNING);
+ walletFrame->handlePaymentRequest(recipient);
+}
+
+void BitcoinGUI::showPaymentACK(QString msg)
+{
+#if QT_VERSION < 0x050000
+ message(tr("Payment acknowledged"), Qt::escape(msg), CClientUIInterface::MODAL);
+#else
+ message(tr("Payment acknowledged"), msg.toHtmlEscaped(), CClientUIInterface::MODAL);
+#endif
}
void BitcoinGUI::setEncryptionStatus(int status)
diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h
index 6b9161539c..fc25e867fc 100644
--- a/src/qt/bitcoingui.h
+++ b/src/qt/bitcoingui.h
@@ -15,6 +15,7 @@ class TransactionView;
class OverviewPage;
class AddressBookPage;
class SendCoinsDialog;
+class SendCoinsRecipient;
class SignVerifyMessageDialog;
class Notificator;
class RPCConsole;
@@ -151,7 +152,9 @@ public slots:
@param[out] payFee true to pay the fee, false to not pay the fee
*/
void askFee(qint64 nFeeRequired, bool *payFee);
- void handleURI(QString strURI);
+
+ void handlePaymentRequest(const SendCoinsRecipient& recipient);
+ void showPaymentACK(QString msg);
/** Show incoming transaction notification for new transactions. */
void incomingTransaction(const QString& date, int unit, qint64 amount, const QString& type, const QString& address);
diff --git a/src/qt/forms/sendcoinsentry.ui b/src/qt/forms/sendcoinsentry.ui
index 28553c5508..a2ef9a0a38 100644
--- a/src/qt/forms/sendcoinsentry.ui
+++ b/src/qt/forms/sendcoinsentry.ui
@@ -1,143 +1,685 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SendCoinsEntry</class>
- <widget class="QFrame" name="SendCoinsEntry">
+ <widget class="QStackedWidget" name="SendCoinsEntry">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>729</width>
- <height>136</height>
+ <height>150</height>
</rect>
</property>
<property name="windowTitle">
- <string>Form</string>
+ <string>StackedWidget</string>
</property>
- <property name="frameShape">
- <enum>QFrame::StyledPanel</enum>
+ <property name="autoFillBackground">
+ <bool>false</bool>
</property>
- <property name="frameShadow">
- <enum>QFrame::Sunken</enum>
+ <property name="currentIndex">
+ <number>1</number>
</property>
- <layout class="QGridLayout" name="gridLayout">
- <property name="spacing">
- <number>12</number>
+ <widget class="QFrame" name="SendCoinsInsecure">
+ <property name="windowTitle">
+ <string>Form</string>
</property>
- <item row="5" column="0">
- <widget class="QLabel" name="label">
- <property name="text">
- <string>A&amp;mount:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- <property name="buddy">
- <cstring>payAmount</cstring>
- </property>
- </widget>
- </item>
- <item row="3" column="0">
- <widget class="QLabel" name="label_2">
- <property name="text">
- <string>Pay &amp;To:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- <property name="buddy">
- <cstring>payTo</cstring>
- </property>
- </widget>
- </item>
- <item row="5" column="1">
- <widget class="BitcoinAmountField" name="payAmount"/>
- </item>
- <item row="4" column="0">
- <widget class="QLabel" name="label_4">
- <property name="text">
- <string>&amp;Label:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- <property name="buddy">
- <cstring>addAsLabel</cstring>
- </property>
- </widget>
- </item>
- <item row="3" column="1">
- <layout class="QHBoxLayout" name="payToLayout">
- <property name="spacing">
- <number>0</number>
- </property>
- <item>
- <widget class="QValidatedLineEdit" name="payTo">
- <property name="toolTip">
- <string>The address to send the payment to (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L)</string>
- </property>
- <property name="maxLength">
- <number>34</number>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="addressBookButton">
- <property name="toolTip">
- <string>Choose address from address book</string>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="icon">
- <iconset resource="../bitcoin.qrc">
- <normaloff>:/icons/address-book</normaloff>:/icons/address-book</iconset>
- </property>
- <property name="shortcut">
- <string>Alt+A</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="pasteButton">
- <property name="toolTip">
- <string>Paste address from clipboard</string>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="icon">
- <iconset resource="../bitcoin.qrc">
- <normaloff>:/icons/editpaste</normaloff>:/icons/editpaste</iconset>
- </property>
- <property name="shortcut">
- <string>Alt+P</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="deleteButton">
- <property name="toolTip">
- <string>Remove this recipient</string>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="icon">
- <iconset resource="../bitcoin.qrc">
- <normaloff>:/icons/remove</normaloff>:/icons/remove</iconset>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="4" column="1">
- <widget class="QValidatedLineEdit" name="addAsLabel">
- <property name="toolTip">
- <string>Enter a label for this address to add it to your address book</string>
- </property>
- </widget>
- </item>
- </layout>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <property name="spacing">
+ <number>12</number>
+ </property>
+ <item row="5" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>A&amp;mount:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>payAmount</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Pay &amp;To:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>payTo</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="BitcoinAmountField" name="payAmount"/>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>&amp;Label:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>addAsLabel</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <layout class="QHBoxLayout" name="payToLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QValidatedLineEdit" name="payTo">
+ <property name="toolTip">
+ <string>The address to send the payment to (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L)</string>
+ </property>
+ <property name="maxLength">
+ <number>34</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="addressBookButton">
+ <property name="toolTip">
+ <string>Choose address from address book</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="../bitcoin.qrc">
+ <normaloff>:/icons/address-book</normaloff>:/icons/address-book</iconset>
+ </property>
+ <property name="shortcut">
+ <string>Alt+A</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="pasteButton">
+ <property name="toolTip">
+ <string>Paste address from clipboard</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="../bitcoin.qrc">
+ <normaloff>:/icons/editpaste</normaloff>:/icons/editpaste</iconset>
+ </property>
+ <property name="shortcut">
+ <string>Alt+P</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="deleteButton">
+ <property name="toolTip">
+ <string>Remove this recipient</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="../bitcoin.qrc">
+ <normaloff>:/icons/remove</normaloff>:/icons/remove</iconset>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="4" column="1">
+ <widget class="QValidatedLineEdit" name="addAsLabel">
+ <property name="toolTip">
+ <string>Enter a label for this address to add it to your address book</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QFrame" name="SendCoinsSecure">
+ <property name="palette">
+ <palette>
+ <active>
+ <colorrole role="WindowText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Button">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>140</red>
+ <green>232</green>
+ <blue>119</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Light">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>230</red>
+ <green>255</green>
+ <blue>224</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Midlight">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>185</red>
+ <green>243</green>
+ <blue>171</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Dark">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>70</red>
+ <green>116</green>
+ <blue>59</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Mid">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>93</red>
+ <green>155</green>
+ <blue>79</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Text">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="BrightText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>155</red>
+ <green>255</green>
+ <blue>147</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="ButtonText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Base">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>119</red>
+ <green>255</green>
+ <blue>233</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Window">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>140</red>
+ <green>232</green>
+ <blue>119</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Shadow">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="AlternateBase">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>197</red>
+ <green>243</green>
+ <blue>187</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="NoRole">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>125</red>
+ <green>194</green>
+ <blue>122</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="ToolTipBase">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>255</red>
+ <green>255</green>
+ <blue>220</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="ToolTipText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ </active>
+ <inactive>
+ <colorrole role="WindowText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Button">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>140</red>
+ <green>232</green>
+ <blue>119</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Light">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>230</red>
+ <green>255</green>
+ <blue>224</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Midlight">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>185</red>
+ <green>243</green>
+ <blue>171</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Dark">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>70</red>
+ <green>116</green>
+ <blue>59</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Mid">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>93</red>
+ <green>155</green>
+ <blue>79</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Text">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="BrightText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>155</red>
+ <green>255</green>
+ <blue>147</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="ButtonText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Base">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>119</red>
+ <green>255</green>
+ <blue>233</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Window">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>140</red>
+ <green>232</green>
+ <blue>119</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Shadow">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="AlternateBase">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>197</red>
+ <green>243</green>
+ <blue>187</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="NoRole">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>125</red>
+ <green>194</green>
+ <blue>122</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="ToolTipBase">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>255</red>
+ <green>255</green>
+ <blue>220</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="ToolTipText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ </inactive>
+ <disabled>
+ <colorrole role="WindowText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>70</red>
+ <green>116</green>
+ <blue>59</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Button">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>140</red>
+ <green>232</green>
+ <blue>119</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Light">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>230</red>
+ <green>255</green>
+ <blue>224</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Midlight">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>185</red>
+ <green>243</green>
+ <blue>171</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Dark">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>70</red>
+ <green>116</green>
+ <blue>59</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Mid">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>93</red>
+ <green>155</green>
+ <blue>79</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Text">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>70</red>
+ <green>116</green>
+ <blue>59</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="BrightText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>155</red>
+ <green>255</green>
+ <blue>147</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="ButtonText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>70</red>
+ <green>116</green>
+ <blue>59</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Base">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>140</red>
+ <green>232</green>
+ <blue>119</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Window">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>140</red>
+ <green>232</green>
+ <blue>119</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Shadow">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="AlternateBase">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>140</red>
+ <green>232</green>
+ <blue>119</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="NoRole">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>125</red>
+ <green>194</green>
+ <blue>122</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="ToolTipBase">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>255</red>
+ <green>255</green>
+ <blue>220</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="ToolTipText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ </disabled>
+ </palette>
+ </property>
+ <property name="windowTitle">
+ <string>SecureSend</string>
+ </property>
+ <property name="autoFillBackground">
+ <bool>true</bool>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_s">
+ <property name="spacing">
+ <number>12</number>
+ </property>
+ <item row="4" column="0">
+ <widget class="QLabel" name="label_s4">
+ <property name="text">
+ <string>Memo:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>addAsLabel</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <widget class="QLabel" name="label_s1">
+ <property name="text">
+ <string>A&amp;mount:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>payAmount</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_s2">
+ <property name="text">
+ <string>Pay &amp;To:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>payTo_s</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="2">
+ <widget class="BitcoinAmountField" name="payAmount_s">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="acceptDrops">
+ <bool>false</bool>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="2">
+ <layout class="QHBoxLayout" name="payToLayout_s">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="payTo_s">
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="4" column="2">
+ <widget class="QLabel" name="memo_s">
+ <property name="text">
+ <string>message from merchant</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</widget>
<customwidgets>
<customwidget>
diff --git a/src/qt/guiconstants.h b/src/qt/guiconstants.h
index 92417834ec..2c72c01021 100644
--- a/src/qt/guiconstants.h
+++ b/src/qt/guiconstants.h
@@ -28,6 +28,9 @@ static const int TOOLTIP_WRAP_THRESHOLD = 80;
/* Maximum allowed URI length */
static const int MAX_URI_LENGTH = 255;
+/* Maximum somewhat-sane size of a payment request file */
+static const int MAX_PAYMENT_REQUEST_SIZE = 50000; // bytes
+
/* QRCodeDialog -- size of exported QR Code image */
#define EXPORT_IMAGE_SIZE 256
diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp
index 7ebe5b4755..d93a60e1bc 100644
--- a/src/qt/optionsmodel.cpp
+++ b/src/qt/optionsmodel.cpp
@@ -290,3 +290,14 @@ qint64 OptionsModel::getTransactionFee()
{
return nTransactionFee;
}
+
+bool OptionsModel::getProxySettings(QString& proxyIP, quint16 &proxyPort) const
+{
+ std::string proxy = GetArg("-proxy", "");
+ if (proxy.empty()) return false;
+
+ CService addrProxy(proxy);
+ proxyIP = QString(addrProxy.ToStringIP().c_str());
+ proxyPort = addrProxy.GetPort();
+ return true;
+}
diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h
index d25d898d9c..173db0fe0a 100644
--- a/src/qt/optionsmodel.h
+++ b/src/qt/optionsmodel.h
@@ -49,6 +49,7 @@ public:
int getDisplayUnit() { return nDisplayUnit; }
bool getDisplayAddresses() { return bDisplayAddresses; }
QString getLanguage() { return language; }
+ bool getProxySettings(QString& proxyIP, quint16 &proxyPort) const;
private:
int nDisplayUnit;
diff --git a/src/qt/paymentrequest.proto b/src/qt/paymentrequest.proto
new file mode 100644
index 0000000000..b2281c4c7b
--- /dev/null
+++ b/src/qt/paymentrequest.proto
@@ -0,0 +1,46 @@
+//
+// Simple Bitcoin Payment Protocol messages
+//
+// Use fields 100+ for extensions;
+// to avoid conflicts, register extensions at:
+// https://en.bitcoin.it/wiki/Payment_Request
+//
+
+package payments;
+option java_package = "org.bitcoin.protocols.payments";
+option java_outer_classname = "Protos";
+
+// Generalized form of "send payment to this/these bitcoin addresses"
+message Output {
+ optional uint64 amount = 1 [default = 0]; // amount is integer-number-of-satoshis
+ required bytes script = 2; // usually one of the standard Script forms
+}
+message PaymentDetails {
+ optional string network = 1 [default = "main"]; // "main" or "test"
+ repeated Output outputs = 2; // Where payment should be sent
+ required uint64 time = 3; // Timestamp; when payment request created
+ optional uint64 expires = 4; // Timestamp; when this request should be considered invalid
+ optional string memo = 5; // Human-readable description of request for the customer
+ optional string payment_url = 6; // URL to send Payment and get PaymentACK
+ optional bytes merchant_data = 7; // Arbitrary data to include in the Payment message
+}
+message PaymentRequest {
+ optional uint32 payment_details_version = 1 [default = 1];
+ optional string pki_type = 2 [default = "none"]; // none / x509+sha256 / x509+sha1
+ optional bytes pki_data = 3; // depends on pki_type
+ required bytes serialized_payment_details = 4; // PaymentDetails
+ optional bytes signature = 5; // pki-dependent signature
+}
+message X509Certificates {
+ repeated bytes certificate = 1; // DER-encoded X.509 certificate chain
+}
+message Payment {
+ optional bytes merchant_data = 1; // From PaymentDetails.merchant_data
+ repeated bytes transactions = 2; // Signed transactions that satisfy PaymentDetails.outputs
+ repeated Output refund_to = 3; // Where to send refunds, if a refund is necessary
+ optional string memo = 4; // Human-readable message for the merchant
+}
+message PaymentACK {
+ required Payment payment = 1; // Payment message that triggered this ACK
+ optional string memo = 2; // human-readable message for customer
+}
diff --git a/src/qt/paymentrequestplus.cpp b/src/qt/paymentrequestplus.cpp
new file mode 100644
index 0000000000..289ddabb92
--- /dev/null
+++ b/src/qt/paymentrequestplus.cpp
@@ -0,0 +1,204 @@
+//
+// Wraps dumb protocol buffer paymentRequest
+// with some extra methods
+//
+
+#include <QDateTime>
+#include <QDebug>
+#include <QSslCertificate>
+
+#include <openssl/x509.h>
+#include <openssl/x509_vfy.h>
+
+#include <stdexcept>
+
+#include "paymentrequestplus.h"
+
+class SSLVerifyError : public std::runtime_error
+{
+public:
+ SSLVerifyError(std::string err) : std::runtime_error(err) { }
+};
+
+bool PaymentRequestPlus::parse(const QByteArray& data)
+{
+ bool parseOK = paymentRequest.ParseFromArray(data.data(), data.size());
+ if (!parseOK) {
+ qDebug() << "Error parsing payment request";
+ return false;
+ }
+ if (paymentRequest.payment_details_version() > 1) {
+ qDebug() << "Received up-version payment details, version=" << paymentRequest.payment_details_version();
+ return false;
+ }
+
+ parseOK = details.ParseFromString(paymentRequest.serialized_payment_details());
+ if (!parseOK)
+ {
+ qDebug() << "Error parsing payment details";
+ paymentRequest.Clear();
+ return false;
+ }
+ return true;
+}
+
+bool PaymentRequestPlus::SerializeToString(string* output) const
+{
+ return paymentRequest.SerializeToString(output);
+}
+
+bool PaymentRequestPlus::IsInitialized() const
+{
+ return paymentRequest.IsInitialized();
+}
+
+QString PaymentRequestPlus::getPKIType() const
+{
+ if (!IsInitialized()) return QString("none");
+ return QString::fromStdString(paymentRequest.pki_type());
+}
+
+bool PaymentRequestPlus::getMerchant(X509_STORE* certStore, QString& merchant) const
+{
+ merchant.clear();
+
+ if (!IsInitialized())
+ return false;
+
+ // One day we'll support more PKI types, but just
+ // x509 for now:
+ const EVP_MD* digestAlgorithm = NULL;
+ if (paymentRequest.pki_type() == "x509+sha256") {
+ digestAlgorithm = EVP_sha256();
+ }
+ else if (paymentRequest.pki_type() == "x509+sha1") {
+ digestAlgorithm = EVP_sha1();
+ }
+ else if (paymentRequest.pki_type() == "none") {
+ if (fDebug) qDebug() << "PaymentRequest: pki_type == none";
+ return false;
+ }
+ else {
+ qDebug() << "PaymentRequest: unknown pki_type " << paymentRequest.pki_type().c_str();
+ return false;
+ }
+
+ payments::X509Certificates certChain;
+ if (!certChain.ParseFromString(paymentRequest.pki_data())) {
+ qDebug() << "PaymentRequest: error parsing pki_data";
+ return false;
+ }
+
+ std::vector<X509*> certs;
+ const QDateTime currentTime = QDateTime::currentDateTime();
+ for (int i = 0; i < certChain.certificate_size(); i++) {
+ QByteArray certData(certChain.certificate(i).data(), certChain.certificate(i).size());
+ QSslCertificate qCert(certData, QSsl::Der);
+ if (currentTime < qCert.effectiveDate() || currentTime > qCert.expiryDate()) {
+ qDebug() << "PaymentRequest: certificate expired or not yet active: " << qCert;
+ return false;
+ }
+#if QT_VERSION >= 0x050000
+ if (qCert.isBlacklisted()) {
+ qDebug() << "PaymentRequest: certificate blacklisted: " << qCert;
+ return false;
+ }
+#endif
+ const unsigned char *data = (const unsigned char *)certChain.certificate(i).data();
+ X509 *cert = d2i_X509(NULL, &data, certChain.certificate(i).size());
+ if (cert)
+ certs.push_back(cert);
+ }
+ if (certs.empty()) {
+ qDebug() << "PaymentRequest: empty certificate chain";
+ return false;
+ }
+
+ // The first cert is the signing cert, the rest are untrusted certs that chain
+ // to a valid root authority. OpenSSL needs them separately.
+ STACK_OF(X509) *chain = sk_X509_new_null();
+ for (int i = certs.size()-1; i > 0; i--) {
+ sk_X509_push(chain, certs[i]);
+ }
+ X509 *signing_cert = certs[0];
+
+ // Now create a "store context", which is a single use object for checking,
+ // load the signing cert into it and verify.
+ X509_STORE_CTX *store_ctx = X509_STORE_CTX_new();
+ if (!store_ctx) {
+ qDebug() << "PaymentRequest: error creating X509_STORE_CTX";
+ return false;
+ }
+
+ char *website = NULL;
+ bool fResult = true;
+ try
+ {
+ if (!X509_STORE_CTX_init(store_ctx, certStore, signing_cert, chain))
+ {
+ int error = X509_STORE_CTX_get_error(store_ctx);
+ throw SSLVerifyError(X509_verify_cert_error_string(error));
+ }
+
+ // Now do the verification!
+ int result = X509_verify_cert(store_ctx);
+ if (result != 1) {
+ int error = X509_STORE_CTX_get_error(store_ctx);
+ throw SSLVerifyError(X509_verify_cert_error_string(error));
+ }
+ X509_NAME *certname = X509_get_subject_name(signing_cert);
+
+ // Valid cert; check signature:
+ payments::PaymentRequest rcopy(paymentRequest); // Copy
+ rcopy.set_signature(std::string(""));
+ std::string data_to_verify; // Everything but the signature
+ rcopy.SerializeToString(&data_to_verify);
+
+ EVP_MD_CTX ctx;
+ EVP_PKEY *pubkey = X509_get_pubkey(signing_cert);
+ EVP_MD_CTX_init(&ctx);
+ if (!EVP_VerifyInit_ex(&ctx, digestAlgorithm, NULL) ||
+ !EVP_VerifyUpdate(&ctx, data_to_verify.data(), data_to_verify.size()) ||
+ !EVP_VerifyFinal(&ctx, (const unsigned char*)paymentRequest.signature().data(), paymentRequest.signature().size(), pubkey)) {
+
+ throw SSLVerifyError("Bad signature, invalid PaymentRequest.");
+ }
+
+ // OpenSSL API for getting human printable strings from certs is baroque.
+ int textlen = X509_NAME_get_text_by_NID(certname, NID_commonName, NULL, 0);
+ website = new char[textlen + 1];
+ if (X509_NAME_get_text_by_NID(certname, NID_commonName, website, textlen + 1) == textlen && textlen > 0) {
+ merchant = website;
+ }
+ else {
+ throw SSLVerifyError("Bad certificate, missing common name");
+ }
+ // TODO: detect EV certificates and set merchant = business name instead of unfriendly NID_commonName ?
+ }
+ catch (SSLVerifyError& err)
+ {
+ fResult = false;
+ qDebug() << "PaymentRequestPlus::getMerchant SSL err: " << err.what();
+ }
+
+ if (website)
+ delete[] website;
+ X509_STORE_CTX_free(store_ctx);
+ for (unsigned int i = 0; i < certs.size(); i++)
+ X509_free(certs[i]);
+
+ return fResult;
+}
+
+QList<std::pair<CScript,qint64> > PaymentRequestPlus::getPayTo() const
+{
+ QList<std::pair<CScript,qint64> > result;
+ for (int i = 0; i < details.outputs_size(); i++)
+ {
+ const unsigned char* scriptStr = (const unsigned char*)details.outputs(i).script().data();
+ CScript s(scriptStr, scriptStr+details.outputs(i).script().size());
+
+ result.append(make_pair(s, details.outputs(i).amount()));
+ }
+ return result;
+}
diff --git a/src/qt/paymentrequestplus.h b/src/qt/paymentrequestplus.h
new file mode 100644
index 0000000000..846d256aca
--- /dev/null
+++ b/src/qt/paymentrequestplus.h
@@ -0,0 +1,41 @@
+#ifndef PAYMENTREQUESTPLUS_H
+#define PAYMENTREQUESTPLUS_H
+
+#include <QByteArray>
+#include <QList>
+#include <QString>
+
+#include "base58.h"
+#include "paymentrequest.pb.h"
+
+//
+// Wraps dumb protocol buffer paymentRequest
+// with extra methods
+//
+
+class PaymentRequestPlus
+{
+public:
+ PaymentRequestPlus() { }
+
+ bool parse(const QByteArray& data);
+ bool SerializeToString(string* output) const;
+
+ bool IsInitialized() const;
+ QString getPKIType() const;
+ // Returns true if merchant's identity is authenticated, and
+ // returns human-readable merchant identity in merchant
+ bool getMerchant(X509_STORE* certStore, QString& merchant) const;
+
+ // Returns list of outputs, amount
+ QList<std::pair<CScript,qint64> > getPayTo() const;
+
+ const payments::PaymentDetails& getDetails() const { return details; }
+
+private:
+ payments::PaymentRequest paymentRequest;
+ payments::PaymentDetails details;
+};
+
+#endif // PAYMENTREQUESTPLUS_H
+
diff --git a/src/qt/paymentserver.cpp b/src/qt/paymentserver.cpp
index 0d31f24a13..a9f71315a9 100644
--- a/src/qt/paymentserver.cpp
+++ b/src/qt/paymentserver.cpp
@@ -2,30 +2,63 @@
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
-#include "paymentserver.h"
-
-#include "guiconstants.h"
-#include "ui_interface.h"
-#include "util.h"
-
#include <QApplication>
#include <QByteArray>
#include <QDataStream>
+#include <QDateTime>
#include <QDebug>
+#include <QFile>
#include <QFileOpenEvent>
#include <QHash>
+#include <QList>
#include <QLocalServer>
#include <QLocalSocket>
#include <QStringList>
+#include <QTextDocument>
+#include <QNetworkAccessManager>
+#include <QNetworkProxy>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QSslCertificate>
+#include <QSslError>
+#include <QSslSocket>
#if QT_VERSION < 0x050000
#include <QUrl>
+#else
+#include <QUrlQuery>
#endif
+#include <cstdlib>
+
+#include <openssl/x509.h>
+#include <openssl/x509_vfy.h>
+
+#include "base58.h"
+#include "bitcoinunits.h"
+#include "guiconstants.h"
+#include "guiutil.h"
+#include "optionsmodel.h"
+#include "paymentserver.h"
+#include "ui_interface.h"
+#include "util.h"
+#include "wallet.h"
+#include "walletmodel.h"
+
using namespace boost;
const int BITCOIN_IPC_CONNECT_TIMEOUT = 1000; // milliseconds
const QString BITCOIN_IPC_PREFIX("bitcoin:");
+X509_STORE* PaymentServer::certStore = NULL;
+void PaymentServer::freeCertStore()
+{
+ if (PaymentServer::certStore != NULL)
+ {
+ X509_STORE_free(PaymentServer::certStore);
+ PaymentServer::certStore = NULL;
+ }
+}
+
//
// Create a name that is unique for:
// testnet / non-testnet
@@ -45,11 +78,99 @@ static QString ipcServerName()
}
//
-// This stores payment requests received before
+// We store payment URLs and requests received before
// the main GUI window is up and ready to ask the user
// to send payment.
+
+static QList<QString> savedPaymentRequests;
+
+static void ReportInvalidCertificate(const QSslCertificate& cert)
+{
+ if (fDebug) {
+ qDebug() << "Invalid certificate: " << cert.subjectInfo(QSslCertificate::CommonName);
+ }
+}
+
+//
+// Load openSSL's list of root certificate authorities
//
-static QStringList savedPaymentRequests;
+void PaymentServer::LoadRootCAs(X509_STORE* _store)
+{
+ if (PaymentServer::certStore == NULL)
+ atexit(PaymentServer::freeCertStore);
+ else
+ freeCertStore();
+
+ // Unit tests mostly use this, to pass in fake root CAs:
+ if (_store)
+ {
+ PaymentServer::certStore = _store;
+ return;
+ }
+
+ // Normal execution, use either -rootcertificates or system certs:
+ PaymentServer::certStore = X509_STORE_new();
+
+ // Note: use "-system-" default here so that users can pass -rootcertificates=""
+ // and get 'I don't like X.509 certificates, don't trust anybody' behavior:
+ QString certFile = QString::fromStdString(GetArg("-rootcertificates", "-system-"));
+
+ if (certFile.isEmpty())
+ return; // Empty store
+
+ QList<QSslCertificate> certList;
+
+ if (certFile != "-system-")
+ {
+ certList = QSslCertificate::fromPath(certFile);
+ // Use those certificates when fetching payment requests, too:
+ QSslSocket::setDefaultCaCertificates(certList);
+ }
+ else
+ certList = QSslSocket::systemCaCertificates ();
+
+ int nRootCerts = 0;
+ const QDateTime currentTime = QDateTime::currentDateTime();
+ foreach (const QSslCertificate& cert, certList)
+ {
+ if (currentTime < cert.effectiveDate() || currentTime > cert.expiryDate()) {
+ ReportInvalidCertificate(cert);
+ continue;
+ }
+#if QT_VERSION >= 0x050000
+ if (cert.isBlacklisted()) {
+ ReportInvalidCertificate(cert);
+ continue;
+ }
+#endif
+ QByteArray certData = cert.toDer();
+ const unsigned char *data = (const unsigned char *)certData.data();
+
+ X509* x509 = d2i_X509(0, &data, certData.size());
+ if (x509 && X509_STORE_add_cert( PaymentServer::certStore, x509))
+ {
+ // Note: X509_STORE_free will free the X509* objects when
+ // the PaymentServer is destroyed
+ ++nRootCerts;
+ }
+ else
+ {
+ ReportInvalidCertificate(cert);
+ continue;
+ }
+ }
+ if (fDebug)
+ qDebug() << "PaymentServer: loaded " << nRootCerts << " root certificates";
+
+ // Project for another day:
+ // Fetch certificate revocation lists, and add them to certStore.
+ // Issues to consider:
+ // performance (start a thread to fetch in background?)
+ // privacy (fetch through tor/proxy so IP address isn't revealed)
+ // would it be easier to just use a compiled-in blacklist?
+ // or use Qt's blacklist?
+ // "certificate stapling" with server-side caching is more efficient
+}
//
// Sending to the server is done synchronously, at startup.
@@ -57,19 +178,54 @@ static QStringList savedPaymentRequests;
// and the items in savedPaymentRequest will be handled
// when uiReady() is called.
//
-bool PaymentServer::ipcSendCommandLine()
+bool PaymentServer::ipcSendCommandLine(int argc, char* argv[])
{
bool fResult = false;
- const QStringList& args = qApp->arguments();
- for (int i = 1; i < args.size(); i++)
+ for (int i = 1; i < argc; i++)
{
- if (!args[i].startsWith(BITCOIN_IPC_PREFIX, Qt::CaseInsensitive))
+ QString arg(argv[i]);
+ if (arg.startsWith("-"))
continue;
- savedPaymentRequests.append(args[i]);
+
+ if (arg.startsWith(BITCOIN_IPC_PREFIX, Qt::CaseInsensitive)) // bitcoin:
+ {
+ savedPaymentRequests.append(arg);
+
+ SendCoinsRecipient r;
+ if (GUIUtil::parseBitcoinURI(arg, &r))
+ {
+ CBitcoinAddress address(r.address.toStdString());
+
+ SelectParams(CChainParams::MAIN);
+ if (!address.IsValid())
+ {
+ SelectParams(CChainParams::TESTNET);
+ }
+ }
+ }
+ else if (QFile::exists(arg)) // Filename
+ {
+ savedPaymentRequests.append(arg);
+
+ PaymentRequestPlus request;
+ if (readPaymentRequest(arg, request))
+ {
+ if (request.getDetails().network() == "main")
+ SelectParams(CChainParams::MAIN);
+ else
+ SelectParams(CChainParams::TESTNET);
+ }
+ }
+ else
+ {
+ qDebug() << "Payment request file does not exist: " << argv[i];
+ // Printing to debug.log is about the best we can do here, the
+ // GUI hasn't started yet so we can't pop up a message box.
+ }
}
- foreach (const QString& arg, savedPaymentRequests)
+ foreach (const QString& r, savedPaymentRequests)
{
QLocalSocket* socket = new QLocalSocket();
socket->connectToServer(ipcServerName(), QIODevice::WriteOnly);
@@ -79,7 +235,7 @@ bool PaymentServer::ipcSendCommandLine()
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_4_0);
- out << arg;
+ out << r;
out.device()->seek(0);
socket->write(block);
socket->flush();
@@ -92,50 +248,148 @@ bool PaymentServer::ipcSendCommandLine()
return fResult;
}
-PaymentServer::PaymentServer(QApplication* parent) : QObject(parent), saveURIs(true)
+PaymentServer::PaymentServer(QObject* parent,
+ bool startLocalServer) : QObject(parent), saveURIs(true)
{
+ // Verify that the version of the library that we linked against is
+ // compatible with the version of the headers we compiled against.
+ GOOGLE_PROTOBUF_VERIFY_VERSION;
+
// Install global event filter to catch QFileOpenEvents on the mac (sent when you click bitcoin: links)
- parent->installEventFilter(this);
+ if (parent)
+ parent->installEventFilter(this);
QString name = ipcServerName();
// Clean up old socket leftover from a crash:
QLocalServer::removeServer(name);
- uriServer = new QLocalServer(this);
+ if (startLocalServer)
+ {
+ uriServer = new QLocalServer(this);
- if (!uriServer->listen(name))
- qDebug() << tr("Cannot start bitcoin: click-to-pay handler");
- else
- connect(uriServer, SIGNAL(newConnection()), this, SLOT(handleURIConnection()));
+ if (!uriServer->listen(name))
+ qDebug() << "Cannot start bitcoin: click-to-pay handler";
+ else
+ connect(uriServer, SIGNAL(newConnection()), this, SLOT(handleURIConnection()));
+ }
+
+ // netManager is null until uiReady() is called
+ netManager = NULL;
}
-bool PaymentServer::eventFilter(QObject *object, QEvent *event)
+PaymentServer::~PaymentServer()
+{
+ google::protobuf::ShutdownProtobufLibrary();
+}
+
+//
+// OSX-specific way of handling bitcoin uris and
+// PaymentRequest mime types
+//
+bool PaymentServer::eventFilter(QObject *, QEvent *event)
{
// clicking on bitcoin: URLs creates FileOpen events on the Mac:
if (event->type() == QEvent::FileOpen)
{
QFileOpenEvent* fileEvent = static_cast<QFileOpenEvent*>(event);
- if (!fileEvent->url().isEmpty())
- {
- if (saveURIs) // Before main window is ready:
- savedPaymentRequests.append(fileEvent->url().toString());
- else
- emit receivedURI(fileEvent->url().toString());
- return true;
- }
+ if (!fileEvent->file().isEmpty())
+ handleURIOrFile(fileEvent->file());
+ else if (!fileEvent->url().isEmpty())
+ handleURIOrFile(fileEvent->url().toString());
+
+ return true;
}
return false;
}
+void PaymentServer::initNetManager(const OptionsModel& options)
+{
+ if (netManager != NULL)
+ delete netManager;
+
+ // netManager is used to fetch paymentrequests given in bitcoin: URI's
+ netManager = new QNetworkAccessManager(this);
+
+ // Use proxy settings from options:
+ QString proxyIP;
+ quint16 proxyPort;
+ if (options.getProxySettings(proxyIP, proxyPort))
+ {
+ QNetworkProxy proxy;
+ proxy.setType(QNetworkProxy::Socks5Proxy);
+ proxy.setHostName(proxyIP);
+ proxy.setPort(proxyPort);
+ netManager->setProxy(proxy);
+ }
+
+ connect(netManager, SIGNAL(finished(QNetworkReply*)),
+ this, SLOT(netRequestFinished(QNetworkReply*)));
+ connect(netManager, SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError> &)),
+ this, SLOT(reportSslErrors(QNetworkReply*, const QList<QSslError> &)));
+}
+
void PaymentServer::uiReady()
{
+ assert(netManager != NULL); // Must call initNetManager before uiReady()
+
saveURIs = false;
foreach (const QString& s, savedPaymentRequests)
- emit receivedURI(s);
+ {
+ handleURIOrFile(s);
+ }
savedPaymentRequests.clear();
}
+void PaymentServer::handleURIOrFile(const QString& s)
+{
+ if (saveURIs)
+ {
+ savedPaymentRequests.append(s);
+ return;
+ }
+
+ if (s.startsWith(BITCOIN_IPC_PREFIX, Qt::CaseInsensitive)) // bitcoin:
+ {
+#if QT_VERSION >= 0x050000
+ QUrlQuery url((QUrl(s)));
+#else
+ QUrl url(s);
+#endif
+ if (url.hasQueryItem("request"))
+ {
+ QByteArray temp; temp.append(url.queryItemValue("request"));
+ QString decoded = QUrl::fromPercentEncoding(temp);
+ QUrl fetchUrl(decoded, QUrl::StrictMode);
+
+ if (fDebug) qDebug() << "PaymentServer::fetchRequest " << fetchUrl;
+
+ if (fetchUrl.isValid())
+ fetchRequest(fetchUrl);
+ else
+ qDebug() << "PaymentServer: invalid url: " << fetchUrl;
+ return;
+ }
+
+ SendCoinsRecipient recipient;
+ if (GUIUtil::parseBitcoinURI(s, &recipient))
+ emit receivedPaymentRequest(recipient);
+ return;
+ }
+
+ if (QFile::exists(s))
+ {
+ PaymentRequestPlus request;
+ QList<SendCoinsRecipient> recipients;
+ if (readPaymentRequest(s, request) && processPaymentRequest(request, recipients)) {
+ foreach (const SendCoinsRecipient& recipient, recipients){
+ emit receivedPaymentRequest(recipient);
+ }
+ }
+ return;
+ }
+}
+
void PaymentServer::handleURIConnection()
{
QLocalSocket *clientConnection = uriServer->nextPendingConnection();
@@ -154,8 +408,209 @@ void PaymentServer::handleURIConnection()
QString message;
in >> message;
- if (saveURIs)
- savedPaymentRequests.append(message);
- else
- emit receivedURI(message);
+ handleURIOrFile(message);
+}
+
+bool PaymentServer::readPaymentRequest(const QString& filename, PaymentRequestPlus& request)
+{
+ QFile f(filename);
+ if (!f.open(QIODevice::ReadOnly))
+ {
+ qDebug() << "PaymentServer::readPaymentRequest fail to open " << filename;
+ return false;
+ }
+
+ if (f.size() > MAX_PAYMENT_REQUEST_SIZE)
+ {
+ qDebug() << "PaymentServer::readPaymentRequest " << filename << " too large";
+ return false;
+ }
+
+ QByteArray data = f.readAll();
+
+ return request.parse(data);
+}
+
+bool
+PaymentServer::processPaymentRequest(PaymentRequestPlus& request,
+ QList<SendCoinsRecipient>& recipients)
+{
+ QList<std::pair<CScript,qint64> > sendingTos = request.getPayTo();
+ qint64 totalAmount = 0;
+ foreach(const PAIRTYPE(CScript, qint64)& sendingTo, sendingTos) {
+ CTxOut txOut(sendingTo.second, sendingTo.first);
+ if (txOut.IsDust(CTransaction::nMinRelayTxFee)) {
+ QString message = QObject::tr("Requested payment amount (%1) too small")
+ .arg(BitcoinUnits::formatWithUnit(BitcoinUnits::BTC, sendingTo.second));
+ qDebug() << message;
+ emit reportError(tr("Payment request error"), message, CClientUIInterface::MODAL);
+ return false;
+ }
+
+ totalAmount += sendingTo.second;
+ }
+
+ recipients.append(SendCoinsRecipient());
+
+ if (request.getMerchant(PaymentServer::certStore, recipients[0].authenticatedMerchant)) {
+ recipients[0].paymentRequest = request;
+ recipients[0].amount = totalAmount;
+ if (fDebug) qDebug() << "PaymentRequest from " << recipients[0].authenticatedMerchant;
+ }
+ else {
+ recipients.clear();
+ // Insecure payment requests may turn into more than one recipient if
+ // the merchant is requesting payment to more than one address.
+ for (int i = 0; i < sendingTos.size(); i++) {
+ std::pair<CScript, qint64>& sendingTo = sendingTos[i];
+ recipients.append(SendCoinsRecipient());
+ recipients[i].amount = sendingTo.second;
+ QString memo = QString::fromStdString(request.getDetails().memo());
+#if QT_VERSION < 0x050000
+ recipients[i].label = Qt::escape(memo);
+#else
+ recipients[i].label = memo.toHtmlEscaped();
+#endif
+ CTxDestination dest;
+ if (ExtractDestination(sendingTo.first, dest)) {
+ if (i == 0) // Tie request to first pay-to, we don't want multiple ACKs
+ recipients[i].paymentRequest = request;
+ recipients[i].address = QString::fromStdString(CBitcoinAddress(dest).ToString());
+ if (fDebug) qDebug() << "PaymentRequest, insecure " << recipients[i].address;
+ }
+ else {
+ // Insecure payments to custom bitcoin addresses are not supported
+ // (there is no good way to tell the user where they are paying in a way
+ // they'd have a chance of understanding).
+ emit reportError(tr("Payment request error"),
+ tr("Insecure requests to custom payment scripts unsupported"),
+ CClientUIInterface::MODAL);
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+void
+PaymentServer::fetchRequest(const QUrl& url)
+{
+ QNetworkRequest netRequest;
+ netRequest.setAttribute(QNetworkRequest::User, "PaymentRequest");
+ netRequest.setUrl(url);
+ netRequest.setRawHeader("User-Agent", CLIENT_NAME.c_str());
+ netManager->get(netRequest);
+}
+
+void
+PaymentServer::fetchPaymentACK(CWallet* wallet, SendCoinsRecipient recipient, QByteArray transaction)
+{
+ const payments::PaymentDetails& details = recipient.paymentRequest.getDetails();
+ if (!details.has_payment_url())
+ return;
+
+ QNetworkRequest netRequest;
+ netRequest.setAttribute(QNetworkRequest::User, "PaymentACK");
+ netRequest.setUrl(QString::fromStdString(details.payment_url()));
+ netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/bitcoin-payment");
+ netRequest.setRawHeader("User-Agent", CLIENT_NAME.c_str());
+
+ payments::Payment payment;
+ payment.set_merchant_data(details.merchant_data());
+ payment.add_transactions(transaction.data(), transaction.size());
+
+ // Create a new refund address, or re-use:
+ QString account = tr("Refund from") + QString(" ") + recipient.authenticatedMerchant;
+ std::string strAccount = account.toStdString();
+ set<CTxDestination> refundAddresses = wallet->GetAccountAddresses(strAccount);
+ if (!refundAddresses.empty()) {
+ CScript s; s.SetDestination(*refundAddresses.begin());
+ payments::Output* refund_to = payment.add_refund_to();
+ refund_to->set_script(&s[0], s.size());
+ }
+ else {
+ CPubKey newKey;
+ if (wallet->GetKeyFromPool(newKey, false)) {
+ CKeyID keyID = newKey.GetID();
+ wallet->SetAddressBook(keyID, strAccount, "refund");
+
+ CScript s; s.SetDestination(keyID);
+ payments::Output* refund_to = payment.add_refund_to();
+ refund_to->set_script(&s[0], s.size());
+ }
+ else {
+ // This should never happen, because sending coins should have just unlocked the wallet
+ // and refilled the keypool
+ qDebug() << "Error getting refund key, refund_to not set";
+ }
+ }
+
+ int length = payment.ByteSize();
+ netRequest.setHeader(QNetworkRequest::ContentLengthHeader, length);
+ QByteArray serData(length, '\0');
+ if (payment.SerializeToArray(serData.data(), length)) {
+ netManager->post(netRequest, serData);
+ }
+ else {
+ // This should never happen, either:
+ qDebug() << "Error serializing payment message";
+ }
+}
+
+void
+PaymentServer::netRequestFinished(QNetworkReply* reply)
+{
+ reply->deleteLater();
+ if (reply->error() != QNetworkReply::NoError)
+ {
+ QString message = QObject::tr("Error communicating with %1: %2")
+ .arg(reply->request().url().toString())
+ .arg(reply->errorString());
+ qDebug() << message;
+ emit reportError(tr("Network request error"), message, CClientUIInterface::MODAL);
+ return;
+ }
+
+ QByteArray data = reply->readAll();
+
+ QString requestType = reply->request().attribute(QNetworkRequest::User).toString();
+ if (requestType == "PaymentRequest")
+ {
+ PaymentRequestPlus request;
+ QList<SendCoinsRecipient> recipients;
+ if (request.parse(data) && processPaymentRequest(request, recipients)) {
+ foreach (const SendCoinsRecipient& recipient, recipients){
+ emit receivedPaymentRequest(recipient);
+ }
+ }
+ else
+ qDebug() << "PaymentServer::netRequestFinished: error processing PaymentRequest";
+ return;
+ }
+ else if (requestType == "PaymentACK")
+ {
+ payments::PaymentACK paymentACK;
+ if (!paymentACK.ParseFromArray(data.data(), data.size()))
+ {
+ QString message = QObject::tr("Bad response from server %1")
+ .arg(reply->request().url().toString());
+ qDebug() << message;
+ emit reportError(tr("Network request error"), message, CClientUIInterface::MODAL);
+ }
+ else {
+ emit receivedPaymentACK(QString::fromStdString(paymentACK.memo()));
+ }
+ }
+}
+
+void
+PaymentServer::reportSslErrors(QNetworkReply* reply, const QList<QSslError> &errs)
+{
+ QString errString;
+ foreach (const QSslError& err, errs) {
+ qDebug() << err;
+ errString += err.errorString() + "\n";
+ }
+ emit reportError(tr("Network request error"), errString, CClientUIInterface::MODAL);
}
diff --git a/src/qt/paymentserver.h b/src/qt/paymentserver.h
index 1f2fdd4664..7c6b2eabf0 100644
--- a/src/qt/paymentserver.h
+++ b/src/qt/paymentserver.h
@@ -31,37 +31,87 @@
#include <QObject>
#include <QString>
+#include "paymentrequestplus.h"
+#include "walletmodel.h"
+
+class CWallet;
+class OptionsModel;
class QApplication;
+class QByteArray;
class QLocalServer;
+class QNetworkAccessManager;
+class QNetworkReply;
+class QSslError;
+class QUrl;
class PaymentServer : public QObject
{
Q_OBJECT
-private:
- bool saveURIs;
- QLocalServer* uriServer;
-
public:
// Returns true if there were URIs on the command line
// which were successfully sent to an already-running
// process.
- static bool ipcSendCommandLine();
+ // Note: if a payment request is given, SelectParams(MAIN/TESTNET)
+ // will be called so we startup in the right mode.
+ static bool ipcSendCommandLine(int argc, char *argv[]);
- PaymentServer(QApplication* parent);
+ PaymentServer(QObject* parent, // parent should be QApplication object
+ bool startLocalServer=true);
+ ~PaymentServer();
+ // Load root certificate authorities. Pass NULL (default)
+ // to read from the file specified in the -rootcertificates setting,
+ // or, if that's not set, to use the system default root certificates.
+ // If you pass in a store, you should not X509_STORE_free it: it will be
+ // freed either at exit or when another set of CAs are loaded.
+ static void LoadRootCAs(X509_STORE* store=NULL);
+
+ // Return certificate store
+ static X509_STORE* getCertStore() { return certStore; }
+
+ // Setup networking (options is used to get proxy settings)
+ void initNetManager(const OptionsModel& options);
+
+ // Constructor registers this on the parent QApplication to
+ // receive QEvent::FileOpen events
bool eventFilter(QObject *object, QEvent *event);
signals:
- void receivedURI(QString);
+ // Fired when a valid payment request is received
+ void receivedPaymentRequest(SendCoinsRecipient);
+
+ // Fired when a valid PaymentACK is received
+ void receivedPaymentACK(QString);
+
+ // Fired when an error should be reported to the user
+ void reportError(QString, QString, unsigned int);
public slots:
// Signal this when the main window's UI is ready
// to display payment requests to the user
void uiReady();
+ // Submit Payment message to a merchant, get back PaymentACK:
+ void fetchPaymentACK(CWallet* wallet, SendCoinsRecipient recipient, QByteArray transaction);
+
private slots:
void handleURIConnection();
+ void netRequestFinished(QNetworkReply*);
+ void reportSslErrors(QNetworkReply*, const QList<QSslError> &);
+
+private:
+ static bool readPaymentRequest(const QString& filename, PaymentRequestPlus& request);
+ bool processPaymentRequest(PaymentRequestPlus& request, QList<SendCoinsRecipient>& recipients);
+ void handleURIOrFile(const QString& s);
+ void fetchRequest(const QUrl& url);
+
+ bool saveURIs; // true during startup
+ QLocalServer* uriServer;
+ static X509_STORE* certStore; // Trusted root certificates
+ static void freeCertStore();
+
+ QNetworkAccessManager* netManager; // Used to fetch payment requests
};
#endif // PAYMENTSERVER_H
diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp
index 2c7bab2045..09d83b1a29 100644
--- a/src/qt/sendcoinsdialog.cpp
+++ b/src/qt/sendcoinsdialog.cpp
@@ -93,11 +93,26 @@ void SendCoinsDialog::on_sendButton_clicked()
QStringList formatted;
foreach(const SendCoinsRecipient &rcp, recipients)
{
+ QString amount = BitcoinUnits::formatWithUnit(BitcoinUnits::BTC, rcp.amount);
+ if (rcp.authenticatedMerchant.isEmpty())
+ {
+ QString address = rcp.address;
#if QT_VERSION < 0x050000
- formatted.append(tr("<b>%1</b> to %2 (%3)").arg(BitcoinUnits::formatWithUnit(BitcoinUnits::BTC, rcp.amount), Qt::escape(rcp.label), rcp.address));
+ QString to = Qt::escape(rcp.label);
#else
- formatted.append(tr("<b>%1</b> to %2 (%3)").arg(BitcoinUnits::formatWithUnit(BitcoinUnits::BTC, rcp.amount), rcp.label.toHtmlEscaped(), rcp.address));
+ QString to = rcp.label.toHtmlEscaped();
#endif
+ formatted.append(tr("<b>%1</b> to %2 (%3)").arg(amount, to, address));
+ }
+ else
+ {
+#if QT_VERSION < 0x050000
+ QString merchant = Qt::escape(rcp.authenticatedMerchant);
+#else
+ QString merchant = rcp.authenticatedMerchant.toHtmlEscaped();
+#endif
+ formatted.append(tr("<b>%1</b> to %2").arg(amount, merchant));
+ }
}
fNewRecipientAllowed = false;
@@ -292,20 +307,30 @@ void SendCoinsDialog::pasteEntry(const SendCoinsRecipient &rv)
entry->setValue(rv);
}
-bool SendCoinsDialog::handleURI(const QString &uri)
+bool SendCoinsDialog::handlePaymentRequest(const SendCoinsRecipient &rv)
{
- SendCoinsRecipient rv;
- // URI has to be valid
- if (GUIUtil::parseBitcoinURI(uri, &rv))
- {
+ if (!rv.authenticatedMerchant.isEmpty()) {
+ // Expired payment request?
+ const payments::PaymentDetails& details = rv.paymentRequest.getDetails();
+ if (details.has_expires() && (int64)details.expires() < GetTime())
+ {
+ QMessageBox::warning(this, tr("Send Coins"),
+ tr("Payment request expired"));
+ return false;
+ }
+ }
+ else {
CBitcoinAddress address(rv.address.toStdString());
- if (!address.IsValid())
+ if (!address.IsValid()) {
+ QString strAddress(address.ToString().c_str());
+ QMessageBox::warning(this, tr("Send Coins"),
+ tr("Invalid payment address %1").arg(strAddress));
return false;
- pasteEntry(rv);
- return true;
+ }
}
- return false;
+ pasteEntry(rv);
+ return true;
}
void SendCoinsDialog::setBalance(qint64 balance, qint64 unconfirmedBalance, qint64 immatureBalance)
diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h
index 043dfdcb40..e75a003ba1 100644
--- a/src/qt/sendcoinsdialog.h
+++ b/src/qt/sendcoinsdialog.h
@@ -2,6 +2,7 @@
#define SENDCOINSDIALOG_H
#include <QDialog>
+#include <QVariant>
namespace Ui {
class SendCoinsDialog;
@@ -31,7 +32,7 @@ public:
void setAddress(const QString &address);
void pasteEntry(const SendCoinsRecipient &rv);
- bool handleURI(const QString &uri);
+ bool handlePaymentRequest(const SendCoinsRecipient &recipient);
public slots:
void clear();
diff --git a/src/qt/sendcoinsentry.cpp b/src/qt/sendcoinsentry.cpp
index 1c54850a03..75610f199e 100644
--- a/src/qt/sendcoinsentry.cpp
+++ b/src/qt/sendcoinsentry.cpp
@@ -12,12 +12,14 @@
#include <QClipboard>
SendCoinsEntry::SendCoinsEntry(QWidget *parent) :
- QFrame(parent),
+ QStackedWidget(parent),
ui(new Ui::SendCoinsEntry),
model(0)
{
ui->setupUi(this);
+ setCurrentWidget(ui->SendCoinsInsecure);
+
#ifdef Q_OS_MAC
ui->payToLayout->setSpacing(4);
#endif
@@ -101,6 +103,9 @@ bool SendCoinsEntry::validate()
// Check input validity
bool retval = true;
+ if (!recipient.authenticatedMerchant.isEmpty())
+ return retval;
+
if(!ui->payTo->hasAcceptableInput() ||
(model && !model->validateAddress(ui->payTo->text())))
{
@@ -124,13 +129,15 @@ bool SendCoinsEntry::validate()
SendCoinsRecipient SendCoinsEntry::getValue()
{
- SendCoinsRecipient rv;
+ if (!recipient.authenticatedMerchant.isEmpty())
+ return recipient;
- rv.address = ui->payTo->text();
- rv.label = ui->addAsLabel->text();
- rv.amount = ui->payAmount->value();
+ // User-entered or non-authenticated:
+ recipient.address = ui->payTo->text();
+ recipient.label = ui->addAsLabel->text();
+ recipient.amount = ui->payAmount->value();
- return rv;
+ return recipient;
}
QWidget *SendCoinsEntry::setupTabChain(QWidget *prev)
@@ -145,9 +152,22 @@ QWidget *SendCoinsEntry::setupTabChain(QWidget *prev)
void SendCoinsEntry::setValue(const SendCoinsRecipient &value)
{
+ recipient = value;
+
ui->payTo->setText(value.address);
ui->addAsLabel->setText(value.label);
ui->payAmount->setValue(value.amount);
+
+ if (!recipient.authenticatedMerchant.isEmpty())
+ {
+ const payments::PaymentDetails& details = value.paymentRequest.getDetails();
+
+ ui->payTo_s->setText(value.authenticatedMerchant);
+ ui->memo_s->setTextFormat(Qt::PlainText);
+ ui->memo_s->setText(QString::fromStdString(details.memo()));
+ ui->payAmount_s->setValue(value.amount);
+ setCurrentWidget(ui->SendCoinsSecure);
+ }
}
void SendCoinsEntry::setAddress(const QString &address)
@@ -158,7 +178,7 @@ void SendCoinsEntry::setAddress(const QString &address)
bool SendCoinsEntry::isClear()
{
- return ui->payTo->text().isEmpty();
+ return ui->payTo->text().isEmpty() && ui->payTo_s->text().isEmpty();
}
void SendCoinsEntry::setFocus()
@@ -172,5 +192,6 @@ void SendCoinsEntry::updateDisplayUnit()
{
// Update payAmount with the current unit
ui->payAmount->setDisplayUnit(model->getOptionsModel()->getDisplayUnit());
+ ui->payAmount_s->setDisplayUnit(model->getOptionsModel()->getDisplayUnit());
}
}
diff --git a/src/qt/sendcoinsentry.h b/src/qt/sendcoinsentry.h
index ec5f3410c1..9c7bfe9521 100644
--- a/src/qt/sendcoinsentry.h
+++ b/src/qt/sendcoinsentry.h
@@ -1,16 +1,21 @@
#ifndef SENDCOINSENTRY_H
#define SENDCOINSENTRY_H
-#include <QFrame>
+#include <QStackedWidget>
+
+#include "walletmodel.h"
namespace Ui {
class SendCoinsEntry;
}
class WalletModel;
-class SendCoinsRecipient;
-/** A single entry in the dialog for sending bitcoins. */
-class SendCoinsEntry : public QFrame
+/**
+ * A single entry in the dialog for sending bitcoins.
+ * Stacked widget, with different UIs for payment requests
+ * with a strong payee identity.
+ */
+class SendCoinsEntry : public QStackedWidget
{
Q_OBJECT
@@ -49,6 +54,7 @@ private slots:
void updateDisplayUnit();
private:
+ SendCoinsRecipient recipient;
Ui::SendCoinsEntry *ui;
WalletModel *model;
};
diff --git a/src/qt/test/paymentrequestdata.h b/src/qt/test/paymentrequestdata.h
new file mode 100644
index 0000000000..49558165f0
--- /dev/null
+++ b/src/qt/test/paymentrequestdata.h
@@ -0,0 +1,307 @@
+//
+// Data for paymentservertests.cpp
+//
+
+// Base64/DER-encoded fake certificate authority certificate.
+// Convert pem to base64/der with:
+// cat file.pem | openssl x509 -inform PEM -outform DER | openssl enc -base64
+//
+// Serial Number: 10302349811211485352 (0x8ef94c91b112c0a8)
+// Issuer: CN=PaymentRequest Test CA
+// Subject: CN=PaymentRequest Test CA
+// Not Valid After : Dec 8 16:37:24 2022 GMT
+//
+const char* caCert_BASE64 =
+"\
+MIIB0DCCATmgAwIBAgIJAI75TJGxEsCoMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNV\
+BAMTFlBheW1lbnRSZXF1ZXN0IFRlc3QgQ0EwHhcNMTIxMjEwMTYzNzI0WhcNMjIx\
+MjA4MTYzNzI0WjAhMR8wHQYDVQQDExZQYXltZW50UmVxdWVzdCBUZXN0IENBMIGf\
+MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCvua59nX9radoqDYyplcns5qdVDTN1\
+7tmcGixmMYOYU3UYMU55VSsJs0dWKnMm3COQDY+N63c0XSbRqarBcsLTkaNASuPX\
+FCv1VWuEKSyy5xe4zeoDU7CVSzlxtQD9wbZW/s3ISjgaXBpwn6eVmntb0JwYxxPc\
+M1u/hrMD8BDbSQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA\
+A4GBADSaRgK5xe47XxycXBhHhr0Wgl4pAsFsufqA9aB9r8KNEHJ0yUvvbD/jaJJM\
+RtQcf0AJ9olzUMY4syehxbzUJP6aeXhZEYiMvdvcv9D55clq6+WLLlNT3jBgAaVn\
+p3waRjPD4bUX3nv+ojz5s4puw7Qq5QUZlhGsMzPvwDGCmZkL\
+";
+
+//
+// This payment request validates directly against the
+// above certificate authority.
+//
+const char* paymentrequest1_BASE64 =
+"\
+Egt4NTA5K3NoYTI1NhrxAwruAzCCAeowggFToAMCAQICAQEwDQYJKoZIhvcNAQEL\
+BQAwITEfMB0GA1UEAxMWUGF5bWVudFJlcXVlc3QgVGVzdCBDQTAeFw0xMjEyMTAx\
+NjM3MjRaFw0yMjEyMDgxNjM3MjRaMEMxGTAXBgNVBAMMEHRlc3RtZXJjaGFudC5v\
+cmcxJjAkBgNVBAoMHVBheW1lbnQgUmVxdWVzdCBUZXN0IE1lcmNoYW50MIGfMA0G\
+CSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHkMy8W1u6HsWlSqdWTmMKf54gICxNfxbY\
++rcMtAftr62hCYx2d2QiSRd1pCUzmo12IiSX3WxSHwaTnT3MFD6jRx6+zM6XdGar\
+I2zpYle11ANzu4gAthN17uRQHV2O5QxVtzNaMdKeJLXT2L9tfEdyL++9ZUqoQmdA\
+YG9ix330hQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GB\
+AIkyO99KC68bi9PFRyQQ7nvn5GlQEb3Ca1bRG5+AKN9N5vc8rZ9G2hejtM8wEXni\
+eGBP+chVMsbTPEHKLrwREn7IvcyCcbAStaklPC3w0B/2idQSHskb6P3X13OR2bTH\
+a2+6wuhsOZRUrVNr24rM95DKx/eCC6JN1VW+qRPU6fqzIjQSHwiw2wYSGXapFJVg\
+igPI+6XpExtNLO/i1WFV8ZmoiKwYsuHFiwUqC1VuaXRUZXN0T25lKoABS0j59iMU\
+Uc9MdIfwsO1BskIET0eJSGNZ7eXb9N62u+qf831PMpEHkmlGpk8rHy92nPcgua/U\
+Yt8oZMn3QaTZ5A6HjJbc3A73eLylp1a0SwCl+KDMEvDQhqMn1jAVu2v92AH3uB7n\
+SiWVbw0tX/68iSQEGGfh9n6ee/8Myb3ICdw=\
+";
+
+//
+// Signed, but expired, merchant cert in the request
+//
+const char* paymentrequest2_BASE64 =
+"\
+Egt4NTA5K3NoYTI1NhrsAwrpAzCCAeUwggFOoAMCAQICAQMwDQYJKoZIhvcNAQEL\
+BQAwITEfMB0GA1UEAxMWUGF5bWVudFJlcXVlc3QgVGVzdCBDQTAeFw0xMzAyMjMy\
+MTI2NDNaFw0xMzAyMjQyMTI2NDNaMD4xHDAaBgNVBAMME2V4cGlyZWRtZXJjaGFu\
+dC5vcmcxHjAcBgNVBAoMFUV4cGlyZWQgVGVzdCBNZXJjaGFudDCBnzANBgkqhkiG\
+9w0BAQEFAAOBjQAwgYkCgYEAx5DMvFtbuh7FpUqnVk5jCn+eICAsTX8W2Pq3DLQH\
+7a+toQmMdndkIkkXdaQlM5qNdiIkl91sUh8Gk509zBQ+o0cevszOl3RmqyNs6WJX\
+tdQDc7uIALYTde7kUB1djuUMVbczWjHSniS109i/bXxHci/vvWVKqEJnQGBvYsd9\
+9IUCAwEAAaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQAaU137\
+j53rvSjlmYZpZ4RWTP7EdD6fl5ZxBeXHytN6DQL33H0eD7OFHt+ofc7E6D7keubl\
+UfCu+jOvt/MvvPUmtCI9yXZ0dNC4sjyETv+wQpxO0UNZwOM4uegdCzlo6Bi3pD4/\
+KKLdMkWuUfuPBmoammny74lZaOVr5deKXztTuCI0Eh8IsNsGEhl2qRSVYIoDyPul\
+6RMbTSzv4tVhVfGZqIisGLLhxYsFKgtVbml0VGVzdFR3byqAAXHuo4nZEPniLpkd\
+y30TkwBxVgprWJ18a9z/7Py35Qss/JMbOXbnBhJtmJCdIowHRI0aa+zqt3KKKAXi\
+mm+V4seMgxTcxMS+eDDkiTcB/RtWWSyRcS2ANjFeY0T4SLMwiCL9qWPi03hr8j96\
+tejrSPOBNSJ3Mi/q5u2Yl4gJZY2b\
+";
+
+//
+// 10-long chain, all intermediates valid
+//
+const char* paymentrequest3_BASE64 =
+"\
+Egt4NTA5K3NoYTI1Nhq8JAr/AzCCAfswggFkoAMCAQICAQEwDQYJKoZIhvcNAQEL\
+BQAwPzEUMBIGA1UEAwwLdGVzdGNhOC5vcmcxJzAlBgNVBAoMHlBheW1lbnQgUmVx\
+dWVzdCBJbnRlcm1lZGlhdGUgODAeFw0xMzAyMjMyMjQyMzFaFw0yMzAyMjEyMjQy\
+MzFaMDYxGjAYBgNVBAMMEXRlc3RtZXJjaGFudDgub3JnMRgwFgYDVQQKDA9UZXN0\
+IE1lcmNoYW50IDgwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMMCHA3hiHbS\
+TKZ5K9jHRwE8NxkGp3IOx56PDB2diNkldG8XweTcRq7bBm7pdiBt4IVggtfs+6hE\
+hDYIOecyoAnVzPFTdvQ7KQdQ/fD9YLe6lk+o0edOqutPMyrxLFjSluXxEQyk7fdt\
+URloMMYfp3p1/hFCboA1rAsQ2RW38hR5AgMBAAGjEDAOMAwGA1UdEwQFMAMBAf8w\
+DQYJKoZIhvcNAQELBQADgYEAPsdFatnc2RJSpvZsw+nCiPVsllycw5ELglq9vfJz\
+nJJucRxgzmqI2iuas1ugwbXn0BEIRLK7vMF/qBzQR6M/nTxttah+KEu+okjps9vJ\
+cIyhfTyGPC5xkHaHZ7sG+UHOFhPw0/kXn0x+pbVgBZ5315axqcp1R+DTSj/whMAr\
+n0AKiAQwggIEMIIBbaADAgECAgECMA0GCSqGSIb3DQEBCwUAMD8xFDASBgNVBAMM\
+C3Rlc3RjYTcub3JnMScwJQYDVQQKDB5QYXltZW50IFJlcXVlc3QgSW50ZXJtZWRp\
+YXRlIDcwHhcNMTMwMjIzMjI0MjMxWhcNMjMwMjIxMjI0MjMxWjA/MRQwEgYDVQQD\
+DAt0ZXN0Y2E4Lm9yZzEnMCUGA1UECgweUGF5bWVudCBSZXF1ZXN0IEludGVybWVk\
+aWF0ZSA4MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDexUFfxb1sThvabp7u\
+dZz59ciThGmmAW0nP4tjrgEACgvWIInr2dZpTHbiQNF34ycsk0le1JD93D7Qb8rd\
+25OrpaO8XS2Li2zjR9cleixXjSLwV/zv8zJ8yPl/27XL++PDTKBXVpJ8/Syp+9Ty\
+plV1BqDhqtIHb/QSHEkTQXjeYQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqG\
+SIb3DQEBCwUAA4GBACMooQVbkbIZ2DaPwHDc4ULwguG3VI2Kzj50UdExmHtzm2S4\
+MQei+n+HEPjtJAx5OY520+10nfuP+12H2DRLQmWmdvDpeQ/Cv0yavlw4ZRejRFo7\
+KS83C0wo5rd+qTvvOmAN4UTArWkzYcEUulPdiXnRamb0WQHTeVdIbHVkMormCogE\
+MIICBDCCAW2gAwIBAgIBAjANBgkqhkiG9w0BAQsFADA/MRQwEgYDVQQDDAt0ZXN0\
+Y2E2Lm9yZzEnMCUGA1UECgweUGF5bWVudCBSZXF1ZXN0IEludGVybWVkaWF0ZSA2\
+MB4XDTEzMDIyMzIyNDIzMVoXDTIzMDIyMTIyNDIzMVowPzEUMBIGA1UEAwwLdGVz\
+dGNhNy5vcmcxJzAlBgNVBAoMHlBheW1lbnQgUmVxdWVzdCBJbnRlcm1lZGlhdGUg\
+NzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtjBRazrkebXAhXsbjimrMIRm\
+W/f9SwAHwXfc042keNtl0t2z6XE6UPcR2v/KrssXuCZgodeYxz6IM6lWosCM1xot\
+C3ChKKFBfVO30reuKBRUxXfKAFqxaG0YOAEzdZkkY9AGhqWloeSmgxpIfhInU0EF\
+JjCwrJ6IkijBatGoAAECAwEAAaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B\
+AQsFAAOBgQDBRTi1MolmOA0niHYX0A2lN5QWHkCfX0A7GwyoMA3dvM45m/NYd4WB\
+X+HwfnfYcI6X9jOgNo5OWmc4GGsld0HlxwMYEKISBS9PbSHPBrb3TBOlw5ztQpXZ\
+91+bOhLux52Fr03sK7v9qExmBM12M8UR2ltpzAMiUgLLMHyPfiWkvQqIBDCCAgQw\
+ggFtoAMCAQICAQIwDQYJKoZIhvcNAQELBQAwPzEUMBIGA1UEAwwLdGVzdGNhNS5v\
+cmcxJzAlBgNVBAoMHlBheW1lbnQgUmVxdWVzdCBJbnRlcm1lZGlhdGUgNTAeFw0x\
+MzAyMjMyMjQyMzBaFw0yMzAyMjEyMjQyMzBaMD8xFDASBgNVBAMMC3Rlc3RjYTYu\
+b3JnMScwJQYDVQQKDB5QYXltZW50IFJlcXVlc3QgSW50ZXJtZWRpYXRlIDYwgZ8w\
+DQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANJSH3xivX1t9olIdHsznI1aE9SD7t9i\
+SZJsIB0otoETHZRVv9M9LvyzBNK98ZV+kTOlST7PJgC0d9BQM9sgYApSRq5oqKDM\
+9FXbOm/yaReAbU3mkFNFw5roTlJ5ThEy0yOGT/DS0YBRaGIvRPRj2DiqDVdCZZ+w\
+4jo1IYHkZt4FAgMBAAGjEDAOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQAD\
+gYEATm6+J1OmbrothO60xALKonWMBKr6hudb4amkFBqKbA9wMeM3jl+I/yKfz/Uf\
+xWuJ071IhiNv6Gxx5YwNvhUe1xMhUqHv0gpyK1Z47bD+kYS2se5sWNPNo3Y9qZDG\
+IXiGQxwHmrzaFk79Uy1xsmvsEz42w6hr25Yaw7HkIgrFveoKiAQwggIEMIIBbaAD\
+AgECAgECMA0GCSqGSIb3DQEBCwUAMD8xFDASBgNVBAMMC3Rlc3RjYTQub3JnMScw\
+JQYDVQQKDB5QYXltZW50IFJlcXVlc3QgSW50ZXJtZWRpYXRlIDQwHhcNMTMwMjIz\
+MjI0MjMwWhcNMjMwMjIxMjI0MjMwWjA/MRQwEgYDVQQDDAt0ZXN0Y2E1Lm9yZzEn\
+MCUGA1UECgweUGF5bWVudCBSZXF1ZXN0IEludGVybWVkaWF0ZSA1MIGfMA0GCSqG\
+SIb3DQEBAQUAA4GNADCBiQKBgQC7vVUFpxHzz2Tr/xij3k58s8d/BPA0R6D5RXTV\
+vmhAzc1Zuin4zUKRFs/aCj/0yED8Wu/COfNGF4tVlRNMdl9EcFsxa8XGEL4eAZa+\
+H/rOHH+7/1EINrrVWhZlUecyhilN8jmCZmqEM3ecuD0NAViqyMrgmaiFmsLoQZpE\
+GepDUQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GBAEdJ\
+Ss8jWiooja3WZzHXeF95QkBJNjIlpDLGcpl4opOYLSuEl9Uxp//LaQQiXuzpj4/I\
+pkWGQmMy5HOyH1lqDyiMgXpcG8PE0jEQAoEUGZ0QEqB1mZ6BCrYvmUuf/5aSVd8Y\
+6lKMR3WzFDYU9Zy0nzuHB/3nvp6MeDRQeRMtYvz4CogEMIICBDCCAW2gAwIBAgIB\
+AjANBgkqhkiG9w0BAQsFADA/MRQwEgYDVQQDDAt0ZXN0Y2EzLm9yZzEnMCUGA1UE\
+CgweUGF5bWVudCBSZXF1ZXN0IEludGVybWVkaWF0ZSAzMB4XDTEzMDIyMzIyNDIy\
+OVoXDTIzMDIyMTIyNDIyOVowPzEUMBIGA1UEAwwLdGVzdGNhNC5vcmcxJzAlBgNV\
+BAoMHlBheW1lbnQgUmVxdWVzdCBJbnRlcm1lZGlhdGUgNDCBnzANBgkqhkiG9w0B\
+AQEFAAOBjQAwgYkCgYEAxYYo3w2UXiYg6O8b4QgwN/vgreTkiW122Ep/z2TiDrhV\
+MhfOOiKdwYESPflfnXnVaQQzCGexYTQqsvqvzHSyna5hL0zPTRJxSKmTVrXRsWtp\
+dCRhjxCGipS3tlQBDi7vb+7SNRIBK4dBjjGzALNk7gMCpy+yM8f6I043jTlmGb0C\
+AwEAAaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQDU+IQxt3Oh\
+KqaUYWC23+cB2gekvWqwMBnrCNrX/Dp+kjoJKUoR2Fs3qw53raHES4SIhpGT9l9l\
+rppNQgFe/JMHeYqOZMZO+6kuU0olJanBJ14tPIc7zlMTQ9OfmZ6v07IpyFbsQDtR\
+hpe80DpuvSFPfJ4fh0WrQf6kn3KDVpGDnAqIBDCCAgQwggFtoAMCAQICAQIwDQYJ\
+KoZIhvcNAQELBQAwPzEUMBIGA1UEAwwLdGVzdGNhMi5vcmcxJzAlBgNVBAoMHlBh\
+eW1lbnQgUmVxdWVzdCBJbnRlcm1lZGlhdGUgMjAeFw0xMzAyMjMyMjQyMjlaFw0y\
+MzAyMjEyMjQyMjlaMD8xFDASBgNVBAMMC3Rlc3RjYTMub3JnMScwJQYDVQQKDB5Q\
+YXltZW50IFJlcXVlc3QgSW50ZXJtZWRpYXRlIDMwgZ8wDQYJKoZIhvcNAQEBBQAD\
+gY0AMIGJAoGBANzgVP99Qg98e6NsKEz1v5KqRB7NTBRRsYnBvb/TSWipvMQaCYuE\
+yk1xG57x++QuASKeR3QHRQJOoAhQaj9JLUhSSv9GQ5PrFLLsOFv7L1tpzXHh2dOB\
+IW92X2yFRW2s39q+Q21yvN+N8uoKdqXhzRA+dDoXh3cavaVeHX1G+IrlAgMBAAGj\
+EDAOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADgYEASTwg84cX+1UhOG9s\
+ejFV3m34QuI1hPZ+qhqVJlRYUtego8Wng1BburDSwqVAv4ch2wi3c2s4e8J7AXyL\
+tzSbSQG4RN0oZi0mR8EtTTN+Mix/hBIk79dMZg85+I29uFA6Zj2d9oAhQv2qkHhc\
+6tcaheNvkQRlCyH68k3iF1Fqf+4KiAQwggIEMIIBbaADAgECAgECMA0GCSqGSIb3\
+DQEBCwUAMD8xFDASBgNVBAMMC3Rlc3RjYTEub3JnMScwJQYDVQQKDB5QYXltZW50\
+IFJlcXVlc3QgSW50ZXJtZWRpYXRlIDEwHhcNMTMwMjIzMjI0MjI5WhcNMjMwMjIx\
+MjI0MjI5WjA/MRQwEgYDVQQDDAt0ZXN0Y2EyLm9yZzEnMCUGA1UECgweUGF5bWVu\
+dCBSZXF1ZXN0IEludGVybWVkaWF0ZSAyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB\
+iQKBgQDaV8zhfyQuSf/f+fauMfgs3g/RnWy9yxxUkvQneQQPH3uZzCyk3A6q72ip\
+TtwNqiibG9455L9A7SaUjGtnpUz0NKT/VWUdqbfCl1PqXjEZbDobbAQ5hxLGOTyL\
+RQhLIcgeq2/BnmeCqHsC4md04nUp+nBo1HwKyygvK+9sMbCp/wIDAQABoxAwDjAM\
+BgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GBACvYyE+PPmWFkbjyRu9LAt8D\
+crtyYYLRClKSg6tVvutwukLG2l//kDOohYkJtgTqr6LnCIIIwYdXN+4wxugmw4cn\
+PIZmP6kovxjhhVM95okilor1zniTAo3RN7JDIfTGNgxLdGu1btt7DOFL4zTbeSJM\
+b8M1JpPftehH+x/VLyuUCuoDMIIB5jCCAU+gAwIBAgIBBTANBgkqhkiG9w0BAQsF\
+ADAhMR8wHQYDVQQDExZQYXltZW50UmVxdWVzdCBUZXN0IENBMB4XDTEzMDIyMzIy\
+NDIyOFoXDTIzMDIyMTIyNDIyOFowPzEUMBIGA1UEAwwLdGVzdGNhMS5vcmcxJzAl\
+BgNVBAoMHlBheW1lbnQgUmVxdWVzdCBJbnRlcm1lZGlhdGUgMTCBnzANBgkqhkiG\
+9w0BAQEFAAOBjQAwgYkCgYEAo5Vy9H3nA/OOkF5Ap89yfVNSiTay/LYCaB0eALpc\
+U690U75O9Q3w2M+2AN8wpbbHsJHZMIjEeBRoQfjlYXW1ucQTxWKyT+liu0D25mGX\
+X27CBXBd4iXTxVII/iX+u3lcjORjoHOBy7QgeIDIIS9y0vYu8eArpjh7m4thrVgI\
+RtMCAwEAAaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQB9LKcV\
+JK9sjASNzpQlpUp7nCiw5FSjVY+XMRIKK/kavzlKjZ+InsmmyRVGjDoZi9GrqG9P\
+VHgLBxi2VtVjmokZoNPqao3OfhqORAubC+JR/JLepM7aDaxDdTHVhSUk4lgNAvi2\
+6dGY7nZMsnHlPQ2tPp/HvRRiMq1oDjlylc8VTCI2Eh8IsNsGEhl2qRSVYIoDyPul\
+6RMbTSzv4tVhVfGZqIisGLLhxYsFKg1Vbml0VGVzdFRocmVlKoABn2HTsUQtMNI4\
+yNvkfkFNka3pRvTUTydJrvyfmEeLzImfM1BWddZjnywku9RToNFZZNgow5QnljmF\
+chhR/aHOuEMTxmc12K4rNlgYtHCsxLP9zd+6u0cva3TucZ6EzS8PKEib/+r12/52\
+664NuWA9WtsK7QCFrK2K95PnVCRmWl0=\
+";
+
+//
+// Long chain, with an invalid (expired) cert in the middle
+//
+const char* paymentrequest4_BASE64 =
+"\
+Egt4NTA5K3NoYTI1NhqeJAr/AzCCAfswggFkoAMCAQICAQEwDQYJKoZIhvcNAQEL\
+BQAwPzEUMBIGA1UEAwwLdGVzdGNhOC5vcmcxJzAlBgNVBAoMHlBheW1lbnQgUmVx\
+dWVzdCBJbnRlcm1lZGlhdGUgODAeFw0xMzAyMjMyMjQyMzFaFw0yMzAyMjEyMjQy\
+MzFaMDYxGjAYBgNVBAMMEXRlc3RtZXJjaGFudDgub3JnMRgwFgYDVQQKDA9UZXN0\
+IE1lcmNoYW50IDgwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMMCHA3hiHbS\
+TKZ5K9jHRwE8NxkGp3IOx56PDB2diNkldG8XweTcRq7bBm7pdiBt4IVggtfs+6hE\
+hDYIOecyoAnVzPFTdvQ7KQdQ/fD9YLe6lk+o0edOqutPMyrxLFjSluXxEQyk7fdt\
+URloMMYfp3p1/hFCboA1rAsQ2RW38hR5AgMBAAGjEDAOMAwGA1UdEwQFMAMBAf8w\
+DQYJKoZIhvcNAQELBQADgYEAPsdFatnc2RJSpvZsw+nCiPVsllycw5ELglq9vfJz\
+nJJucRxgzmqI2iuas1ugwbXn0BEIRLK7vMF/qBzQR6M/nTxttah+KEu+okjps9vJ\
+cIyhfTyGPC5xkHaHZ7sG+UHOFhPw0/kXn0x+pbVgBZ5315axqcp1R+DTSj/whMAr\
+n0AKiAQwggIEMIIBbaADAgECAgECMA0GCSqGSIb3DQEBCwUAMD8xFDASBgNVBAMM\
+C3Rlc3RjYTcub3JnMScwJQYDVQQKDB5QYXltZW50IFJlcXVlc3QgSW50ZXJtZWRp\
+YXRlIDcwHhcNMTMwMjIzMjI0MjMxWhcNMjMwMjIxMjI0MjMxWjA/MRQwEgYDVQQD\
+DAt0ZXN0Y2E4Lm9yZzEnMCUGA1UECgweUGF5bWVudCBSZXF1ZXN0IEludGVybWVk\
+aWF0ZSA4MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDexUFfxb1sThvabp7u\
+dZz59ciThGmmAW0nP4tjrgEACgvWIInr2dZpTHbiQNF34ycsk0le1JD93D7Qb8rd\
+25OrpaO8XS2Li2zjR9cleixXjSLwV/zv8zJ8yPl/27XL++PDTKBXVpJ8/Syp+9Ty\
+plV1BqDhqtIHb/QSHEkTQXjeYQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqG\
+SIb3DQEBCwUAA4GBACMooQVbkbIZ2DaPwHDc4ULwguG3VI2Kzj50UdExmHtzm2S4\
+MQei+n+HEPjtJAx5OY520+10nfuP+12H2DRLQmWmdvDpeQ/Cv0yavlw4ZRejRFo7\
+KS83C0wo5rd+qTvvOmAN4UTArWkzYcEUulPdiXnRamb0WQHTeVdIbHVkMormCogE\
+MIICBDCCAW2gAwIBAgIBAjANBgkqhkiG9w0BAQsFADA/MRQwEgYDVQQDDAt0ZXN0\
+Y2E2Lm9yZzEnMCUGA1UECgweUGF5bWVudCBSZXF1ZXN0IEludGVybWVkaWF0ZSA2\
+MB4XDTEzMDIyMzIyNDIzMVoXDTIzMDIyMTIyNDIzMVowPzEUMBIGA1UEAwwLdGVz\
+dGNhNy5vcmcxJzAlBgNVBAoMHlBheW1lbnQgUmVxdWVzdCBJbnRlcm1lZGlhdGUg\
+NzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtjBRazrkebXAhXsbjimrMIRm\
+W/f9SwAHwXfc042keNtl0t2z6XE6UPcR2v/KrssXuCZgodeYxz6IM6lWosCM1xot\
+C3ChKKFBfVO30reuKBRUxXfKAFqxaG0YOAEzdZkkY9AGhqWloeSmgxpIfhInU0EF\
+JjCwrJ6IkijBatGoAAECAwEAAaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B\
+AQsFAAOBgQDBRTi1MolmOA0niHYX0A2lN5QWHkCfX0A7GwyoMA3dvM45m/NYd4WB\
+X+HwfnfYcI6X9jOgNo5OWmc4GGsld0HlxwMYEKISBS9PbSHPBrb3TBOlw5ztQpXZ\
+91+bOhLux52Fr03sK7v9qExmBM12M8UR2ltpzAMiUgLLMHyPfiWkvQqIBDCCAgQw\
+ggFtoAMCAQICAQIwDQYJKoZIhvcNAQELBQAwPzEUMBIGA1UEAwwLdGVzdGNhNS5v\
+cmcxJzAlBgNVBAoMHlBheW1lbnQgUmVxdWVzdCBJbnRlcm1lZGlhdGUgNTAeFw0x\
+MzAyMjMyMjQyMzBaFw0yMzAyMjEyMjQyMzBaMD8xFDASBgNVBAMMC3Rlc3RjYTYu\
+b3JnMScwJQYDVQQKDB5QYXltZW50IFJlcXVlc3QgSW50ZXJtZWRpYXRlIDYwgZ8w\
+DQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANJSH3xivX1t9olIdHsznI1aE9SD7t9i\
+SZJsIB0otoETHZRVv9M9LvyzBNK98ZV+kTOlST7PJgC0d9BQM9sgYApSRq5oqKDM\
+9FXbOm/yaReAbU3mkFNFw5roTlJ5ThEy0yOGT/DS0YBRaGIvRPRj2DiqDVdCZZ+w\
+4jo1IYHkZt4FAgMBAAGjEDAOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQAD\
+gYEATm6+J1OmbrothO60xALKonWMBKr6hudb4amkFBqKbA9wMeM3jl+I/yKfz/Uf\
+xWuJ071IhiNv6Gxx5YwNvhUe1xMhUqHv0gpyK1Z47bD+kYS2se5sWNPNo3Y9qZDG\
+IXiGQxwHmrzaFk79Uy1xsmvsEz42w6hr25Yaw7HkIgrFveoK6gMwggHmMIIBT6AD\
+AgECAgEGMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMTFlBheW1lbnRSZXF1ZXN0\
+IFRlc3QgQ0EwHhcNMTMwMjIzMjI1OTUxWhcNMTMwMjI0MjI1OTUxWjA/MRQwEgYD\
+VQQDDAt0ZXN0Y2E1Lm9yZzEnMCUGA1UECgweUGF5bWVudCBSZXF1ZXN0IEludGVy\
+bWVkaWF0ZSA1MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7vVUFpxHzz2Tr\
+/xij3k58s8d/BPA0R6D5RXTVvmhAzc1Zuin4zUKRFs/aCj/0yED8Wu/COfNGF4tV\
+lRNMdl9EcFsxa8XGEL4eAZa+H/rOHH+7/1EINrrVWhZlUecyhilN8jmCZmqEM3ec\
+uD0NAViqyMrgmaiFmsLoQZpEGepDUQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0G\
+CSqGSIb3DQEBCwUAA4GBAEmcUEnhua/oiXy1fwScLgMqt+jk9mHRpE6SVsIop23Q\
+CY2JfpG6RxhMMzzzhGklEGN6cxG0HCi6B3HJx6PYrFEfTB0rW4K6m0Tvx3WpS9mN\
+uoEuJHLy18ausI/sYAPDHCL+SfBVcqorpaIG2sSpZouRBjRHAyqFAYlwlW87uq5n\
+CogEMIICBDCCAW2gAwIBAgIBAjANBgkqhkiG9w0BAQsFADA/MRQwEgYDVQQDDAt0\
+ZXN0Y2EzLm9yZzEnMCUGA1UECgweUGF5bWVudCBSZXF1ZXN0IEludGVybWVkaWF0\
+ZSAzMB4XDTEzMDIyMzIyNDIyOVoXDTIzMDIyMTIyNDIyOVowPzEUMBIGA1UEAwwL\
+dGVzdGNhNC5vcmcxJzAlBgNVBAoMHlBheW1lbnQgUmVxdWVzdCBJbnRlcm1lZGlh\
+dGUgNDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAxYYo3w2UXiYg6O8b4Qgw\
+N/vgreTkiW122Ep/z2TiDrhVMhfOOiKdwYESPflfnXnVaQQzCGexYTQqsvqvzHSy\
+na5hL0zPTRJxSKmTVrXRsWtpdCRhjxCGipS3tlQBDi7vb+7SNRIBK4dBjjGzALNk\
+7gMCpy+yM8f6I043jTlmGb0CAwEAAaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG\
+9w0BAQsFAAOBgQDU+IQxt3OhKqaUYWC23+cB2gekvWqwMBnrCNrX/Dp+kjoJKUoR\
+2Fs3qw53raHES4SIhpGT9l9lrppNQgFe/JMHeYqOZMZO+6kuU0olJanBJ14tPIc7\
+zlMTQ9OfmZ6v07IpyFbsQDtRhpe80DpuvSFPfJ4fh0WrQf6kn3KDVpGDnAqIBDCC\
+AgQwggFtoAMCAQICAQIwDQYJKoZIhvcNAQELBQAwPzEUMBIGA1UEAwwLdGVzdGNh\
+Mi5vcmcxJzAlBgNVBAoMHlBheW1lbnQgUmVxdWVzdCBJbnRlcm1lZGlhdGUgMjAe\
+Fw0xMzAyMjMyMjQyMjlaFw0yMzAyMjEyMjQyMjlaMD8xFDASBgNVBAMMC3Rlc3Rj\
+YTMub3JnMScwJQYDVQQKDB5QYXltZW50IFJlcXVlc3QgSW50ZXJtZWRpYXRlIDMw\
+gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANzgVP99Qg98e6NsKEz1v5KqRB7N\
+TBRRsYnBvb/TSWipvMQaCYuEyk1xG57x++QuASKeR3QHRQJOoAhQaj9JLUhSSv9G\
+Q5PrFLLsOFv7L1tpzXHh2dOBIW92X2yFRW2s39q+Q21yvN+N8uoKdqXhzRA+dDoX\
+h3cavaVeHX1G+IrlAgMBAAGjEDAOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEL\
+BQADgYEASTwg84cX+1UhOG9sejFV3m34QuI1hPZ+qhqVJlRYUtego8Wng1BburDS\
+wqVAv4ch2wi3c2s4e8J7AXyLtzSbSQG4RN0oZi0mR8EtTTN+Mix/hBIk79dMZg85\
++I29uFA6Zj2d9oAhQv2qkHhc6tcaheNvkQRlCyH68k3iF1Fqf+4KiAQwggIEMIIB\
+baADAgECAgECMA0GCSqGSIb3DQEBCwUAMD8xFDASBgNVBAMMC3Rlc3RjYTEub3Jn\
+MScwJQYDVQQKDB5QYXltZW50IFJlcXVlc3QgSW50ZXJtZWRpYXRlIDEwHhcNMTMw\
+MjIzMjI0MjI5WhcNMjMwMjIxMjI0MjI5WjA/MRQwEgYDVQQDDAt0ZXN0Y2EyLm9y\
+ZzEnMCUGA1UECgweUGF5bWVudCBSZXF1ZXN0IEludGVybWVkaWF0ZSAyMIGfMA0G\
+CSqGSIb3DQEBAQUAA4GNADCBiQKBgQDaV8zhfyQuSf/f+fauMfgs3g/RnWy9yxxU\
+kvQneQQPH3uZzCyk3A6q72ipTtwNqiibG9455L9A7SaUjGtnpUz0NKT/VWUdqbfC\
+l1PqXjEZbDobbAQ5hxLGOTyLRQhLIcgeq2/BnmeCqHsC4md04nUp+nBo1HwKyygv\
+K+9sMbCp/wIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GB\
+ACvYyE+PPmWFkbjyRu9LAt8DcrtyYYLRClKSg6tVvutwukLG2l//kDOohYkJtgTq\
+r6LnCIIIwYdXN+4wxugmw4cnPIZmP6kovxjhhVM95okilor1zniTAo3RN7JDIfTG\
+NgxLdGu1btt7DOFL4zTbeSJMb8M1JpPftehH+x/VLyuUCuoDMIIB5jCCAU+gAwIB\
+AgIBBTANBgkqhkiG9w0BAQsFADAhMR8wHQYDVQQDExZQYXltZW50UmVxdWVzdCBU\
+ZXN0IENBMB4XDTEzMDIyMzIyNDIyOFoXDTIzMDIyMTIyNDIyOFowPzEUMBIGA1UE\
+AwwLdGVzdGNhMS5vcmcxJzAlBgNVBAoMHlBheW1lbnQgUmVxdWVzdCBJbnRlcm1l\
+ZGlhdGUgMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAo5Vy9H3nA/OOkF5A\
+p89yfVNSiTay/LYCaB0eALpcU690U75O9Q3w2M+2AN8wpbbHsJHZMIjEeBRoQfjl\
+YXW1ucQTxWKyT+liu0D25mGXX27CBXBd4iXTxVII/iX+u3lcjORjoHOBy7QgeIDI\
+IS9y0vYu8eArpjh7m4thrVgIRtMCAwEAAaMQMA4wDAYDVR0TBAUwAwEB/zANBgkq\
+hkiG9w0BAQsFAAOBgQB9LKcVJK9sjASNzpQlpUp7nCiw5FSjVY+XMRIKK/kavzlK\
+jZ+InsmmyRVGjDoZi9GrqG9PVHgLBxi2VtVjmokZoNPqao3OfhqORAubC+JR/JLe\
+pM7aDaxDdTHVhSUk4lgNAvi26dGY7nZMsnHlPQ2tPp/HvRRiMq1oDjlylc8VTCI1\
+Eh8IsNsGEhl2qRSVYIoDyPul6RMbTSzv4tVhVfGZqIisGLLhxYsFKgxVbml0VGVz\
+dEZvdXIqgAEBE1PP93Tkpif35F+dYmXn9kLA/1djcPjCs2o2rwRMM4Uk356O5dgu\
+HXQjsfdR58qZQS9CS5DAtRUf0R8+43/wijO/hb49VNaNXmY+/cPHMkahP2aV3tZi\
+FAyZblLik9A7ZvF+UsjeFQiHB5wzWQvbqk5wQ4yabHIXoYv/E0q+eQ==\
+";
+
+const char* paymentrequest5_BASE64 =
+"\
+Egt4NTA5K3NoYTI1NhrxAwruAzCCAeowggFToAMCAQICAQEwDQYJKoZIhvcNAQEL\
+BQAwITEfMB0GA1UEAxMWUGF5bWVudFJlcXVlc3QgVGVzdCBDQTAeFw0xMzA0MTkx\
+NzIwMDZaFw0yMzA0MTcxNzIwMDZaMEMxGTAXBgNVBAMMEHRlc3RtZXJjaGFudC5v\
+cmcxJjAkBgNVBAoMHVBheW1lbnQgUmVxdWVzdCBUZXN0IE1lcmNoYW50MIGfMA0G\
+CSqGSIb3DQEBAQUAA4GNADCBiQKBgQDhV6Yn47aEEmbl50YLvXoqGEJA51I/40wr\
+Z6VQGdXYaRqYktagrWDlgYY9h0JQ1bQhm8HgW7ju0R4NaDTXUqxg4HjprF0z3Mfm\
+/6mmebkLOOptfkVD7ceAteNI7cyuqWGIAZA7D9mV97mXoCAtTlBUycvkmoiClCCS\
+h0EpF/UTaQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GB\
+AGIRwW7I0QvLga+RnJoJSZNZQbtu4rQW3xmoz8WfZMBYXX3QBYg5ftycbdK+/IbP\
+qozfjGW2AS6DNArvpveSPDTK9+GJBNo1paiNtVqwXkC3Ddscv5AIms1eZGiIOQNC\
+mUvdLkpoXo48WAer3EGsZ3B15GyNEELc0q9W5yUebba1IjUSHwiw2wYSGXapFJVg\
+igPI+6XpExtNLO/i1WFV8ZmoiKwYuPvFiwUqDFVuaXRUZXN0Rml2ZSqAAXdsMgdG\
+ssymvca1S/1KeM3n8Ydi2fi1JUzAAr59xPvNJRUeqCLP9upHn5z7br3P12Oz9A20\
+5/4wL4ClPRPVnOHgij0bEg+y0tGESqmF1rfOfXDszlo2U92wCxS07kq79YAZJ1Zo\
+XYh860/Q4wvc7lfiTe+dXBzPKAKhMy91yETY\
+";
diff --git a/src/qt/test/paymentservertests.cpp b/src/qt/test/paymentservertests.cpp
new file mode 100644
index 0000000000..2e26ab0c9b
--- /dev/null
+++ b/src/qt/test/paymentservertests.cpp
@@ -0,0 +1,109 @@
+#include <QCoreApplication>
+#include <QDebug>
+#include <QTemporaryFile>
+#include <QVariant>
+
+#include <openssl/x509.h>
+#include <openssl/x509_vfy.h>
+
+#include "optionsmodel.h"
+#include "paymentservertests.h"
+#include "paymentrequestdata.h"
+#include "util.h"
+
+
+
+X509 *parse_b64der_cert(const char* cert_data)
+{
+ std::vector<unsigned char> data = DecodeBase64(cert_data);
+ assert(data.size() > 0);
+ const unsigned char* dptr = &data[0];
+ X509 *cert = d2i_X509(NULL, &dptr, data.size());
+ assert(cert);
+ return cert;
+}
+
+
+//
+// Test payment request handling
+//
+
+static SendCoinsRecipient handleRequest(PaymentServer* server, std::vector<unsigned char>& data)
+{
+ RecipientCatcher sigCatcher;
+ QObject::connect(server, SIGNAL(receivedPaymentRequest(SendCoinsRecipient)),
+ &sigCatcher, SLOT(getRecipient(SendCoinsRecipient)));
+
+ // Write data to a temp file:
+ QTemporaryFile f;
+ f.open();
+ f.write((const char*)&data[0], data.size());
+ f.close();
+
+ // Create a FileOpenEvent and send it directly to the server's event filter:
+ QFileOpenEvent event(f.fileName());
+ server->eventFilter(NULL, &event);
+
+ QObject::disconnect(server, SIGNAL(receivedPaymentRequest(SendCoinsRecipient)),
+ &sigCatcher, SLOT(getRecipient(SendCoinsRecipient)));
+
+ // Return results from sigCatcher
+ return sigCatcher.recipient;
+}
+
+void PaymentServerTests::paymentServerTests()
+{
+ OptionsModel optionsModel;
+ PaymentServer* server = new PaymentServer(NULL, false);
+ X509_STORE* caStore = X509_STORE_new();
+ X509_STORE_add_cert(caStore, parse_b64der_cert(caCert_BASE64));
+ PaymentServer::LoadRootCAs(caStore);
+ server->initNetManager(optionsModel);
+ server->uiReady();
+
+ // Now feed PaymentRequests to server, and observe signals it produces:
+ std::vector<unsigned char> data = DecodeBase64(paymentrequest1_BASE64);
+ SendCoinsRecipient r = handleRequest(server, data);
+ QString merchant;
+ r.paymentRequest.getMerchant(caStore, merchant);
+ QCOMPARE(merchant, QString("testmerchant.org"));
+
+ // Version of the above, with an expired certificate:
+ data = DecodeBase64(paymentrequest2_BASE64);
+ r = handleRequest(server, data);
+ r.paymentRequest.getMerchant(caStore, merchant);
+ QCOMPARE(merchant, QString(""));
+
+ // Long certificate chain:
+ data = DecodeBase64(paymentrequest3_BASE64);
+ r = handleRequest(server, data);
+ r.paymentRequest.getMerchant(caStore, merchant);
+ QCOMPARE(merchant, QString("testmerchant8.org"));
+
+ // Long certificate chain, with an expired certificate in the middle:
+ data = DecodeBase64(paymentrequest4_BASE64);
+ r = handleRequest(server, data);
+ r.paymentRequest.getMerchant(caStore, merchant);
+ QCOMPARE(merchant, QString(""));
+
+ // Validly signed, but by a CA not in our root CA list:
+ data = DecodeBase64(paymentrequest5_BASE64);
+ r = handleRequest(server, data);
+ r.paymentRequest.getMerchant(caStore, merchant);
+ QCOMPARE(merchant, QString(""));
+
+ // Try again with no root CA's, verifiedMerchant should be empty:
+ caStore = X509_STORE_new();
+ PaymentServer::LoadRootCAs(caStore);
+ data = DecodeBase64(paymentrequest1_BASE64);
+ r = handleRequest(server, data);
+ r.paymentRequest.getMerchant(caStore, merchant);
+ QCOMPARE(merchant, QString(""));
+
+ delete server;
+}
+
+void RecipientCatcher::getRecipient(SendCoinsRecipient r)
+{
+ recipient = r;
+}
diff --git a/src/qt/test/paymentservertests.h b/src/qt/test/paymentservertests.h
new file mode 100644
index 0000000000..5aa24ebaf2
--- /dev/null
+++ b/src/qt/test/paymentservertests.h
@@ -0,0 +1,29 @@
+#ifndef PAYMENTSERVERTESTS_H
+#define PAYMENTSERVERTESTS_H
+
+#include <QTest>
+#include <QObject>
+
+#include "../paymentserver.h"
+
+class PaymentServerTests : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void paymentServerTests();
+};
+
+// Dummy class to receive paymentserver signals.
+// If SendCoinsRecipient was a proper QObject, then we could use
+// QSignalSpy... but it's not.
+class RecipientCatcher : public QObject
+{
+ Q_OBJECT
+public slots:
+ void getRecipient(SendCoinsRecipient r);
+public:
+ SendCoinsRecipient recipient;
+};
+
+#endif // PAYMENTSERVERTESTS_H
diff --git a/src/qt/test/test_main.cpp b/src/qt/test/test_main.cpp
index af2d358fc4..dae4e60412 100644
--- a/src/qt/test/test_main.cpp
+++ b/src/qt/test/test_main.cpp
@@ -1,16 +1,21 @@
-#include <QTest>
-#include <QObject>
-
-#include "uritests.h"
-
-// This is all you need to run all the tests
-int main(int argc, char *argv[])
-{
- bool fInvalid = false;
-
- URITests test1;
- if (QTest::qExec(&test1) != 0)
- fInvalid = true;
-
- return fInvalid;
-}
+#include <QTest>
+#include <QObject>
+
+#include "uritests.h"
+#include "paymentservertests.h"
+
+// This is all you need to run all the tests
+int main(int argc, char *argv[])
+{
+ bool fInvalid = false;
+
+ URITests test1;
+ if (QTest::qExec(&test1) != 0)
+ fInvalid = true;
+
+ PaymentServerTests test2;
+ if (QTest::qExec(&test2) != 0)
+ fInvalid = true;
+
+ return fInvalid;
+}
diff --git a/src/qt/transactiondesc.cpp b/src/qt/transactiondesc.cpp
index e9f3eb478d..25ff3623c0 100644
--- a/src/qt/transactiondesc.cpp
+++ b/src/qt/transactiondesc.cpp
@@ -7,6 +7,7 @@
#include "db.h"
#include "ui_interface.h"
#include "base58.h"
+#include "paymentserver.h"
#include <string>
@@ -216,6 +217,21 @@ QString TransactionDesc::toHTML(CWallet *wallet, CWalletTx &wtx)
strHTML += "<b>" + tr("Transaction ID") + ":</b> " + wtx.GetHash().ToString().c_str() + "<br>";
+ //
+ // PaymentRequest info:
+ //
+ foreach (const PAIRTYPE(string, string)& r, wtx.vOrderForm)
+ {
+ if (r.first == "PaymentRequest")
+ {
+ PaymentRequestPlus req;
+ req.parse(QByteArray::fromRawData(r.second.c_str(), r.second.size()));
+ QString merchant;
+ if (req.getMerchant(PaymentServer::getCertStore(), merchant))
+ strHTML += "<b>" + tr("Merchant") + ":</b> " + GUIUtil::HtmlEscape(merchant) + "<br>";
+ }
+ }
+
if (wtx.IsCoinBase())
strHTML += "<br>" + tr("Generated coins must mature 120 blocks before they can be spent. When you generated this block, it was broadcast to the network to be added to the block chain. If it fails to get into the chain, its state will change to \"not accepted\" and it won't be spendable. This may occasionally happen if another node generates a block within a few seconds of yours.") + "<br>";
diff --git a/src/qt/walletframe.cpp b/src/qt/walletframe.cpp
index 50c03ac62d..ed2723a0f7 100644
--- a/src/qt/walletframe.cpp
+++ b/src/qt/walletframe.cpp
@@ -52,9 +52,9 @@ void WalletFrame::removeAllWallets()
walletStack->removeAllWallets();
}
-bool WalletFrame::handleURI(const QString &uri)
+bool WalletFrame::handlePaymentRequest(const SendCoinsRecipient &recipient)
{
- return walletStack->handleURI(uri);
+ return walletStack->handlePaymentRequest(recipient);
}
void WalletFrame::showOutOfSyncWarning(bool fShow)
diff --git a/src/qt/walletframe.h b/src/qt/walletframe.h
index d7092f9879..d5aeb6d859 100644
--- a/src/qt/walletframe.h
+++ b/src/qt/walletframe.h
@@ -11,6 +11,7 @@
class BitcoinGUI;
class ClientModel;
+class SendCoinsRecipient;
class WalletModel;
class WalletStack;
@@ -29,7 +30,7 @@ public:
void removeAllWallets();
- bool handleURI(const QString &uri);
+ bool handlePaymentRequest(const SendCoinsRecipient& recipient);
void showOutOfSyncWarning(bool fShow);
@@ -71,4 +72,4 @@ public slots:
void setEncryptionStatus();
};
-#endif // WALLETFRAME_H \ No newline at end of file
+#endif // WALLETFRAME_H
diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp
index d3c3ddcab1..61357647b7 100644
--- a/src/qt/walletmodel.cpp
+++ b/src/qt/walletmodel.cpp
@@ -127,31 +127,60 @@ bool WalletModel::validateAddress(const QString &address)
WalletModel::SendCoinsReturn WalletModel::sendCoins(const QList<SendCoinsRecipient> &recipients)
{
qint64 total = 0;
- QSet<QString> setAddress;
- QString hex;
+ std::vector<std::pair<CScript, int64> > vecSend;
+ QByteArray transaction;
if(recipients.empty())
{
return OK;
}
+ QSet<QString> setAddress; // Used to detect duplicates
+ int nAddresses = 0;
+
// Pre-check input data for validity
foreach(const SendCoinsRecipient &rcp, recipients)
{
- if(!validateAddress(rcp.address))
- {
- return InvalidAddress;
+ if (rcp.paymentRequest.IsInitialized())
+ { // PaymentRequest...
+ int64 subtotal = 0;
+ const payments::PaymentDetails& details = rcp.paymentRequest.getDetails();
+ for (int i = 0; i < details.outputs_size(); i++)
+ {
+ const payments::Output& out = details.outputs(i);
+ if (out.amount() <= 0) continue;
+ subtotal += out.amount();
+ const unsigned char* scriptStr = (const unsigned char*)out.script().data();
+ CScript scriptPubKey(scriptStr, scriptStr+out.script().size());
+ vecSend.push_back(std::pair<CScript, int64>(scriptPubKey, out.amount()));
+ }
+ if (subtotal <= 0)
+ {
+ return InvalidAmount;
+ }
+ total += subtotal;
}
- setAddress.insert(rcp.address);
+ else
+ { // User-entered bitcoin address / amount:
+ if(!validateAddress(rcp.address))
+ {
+ return InvalidAddress;
+ }
+ if(rcp.amount <= 0)
+ {
+ return InvalidAmount;
+ }
+ setAddress.insert(rcp.address);
+ ++nAddresses;
- if(rcp.amount <= 0)
- {
- return InvalidAmount;
+ CScript scriptPubKey;
+ scriptPubKey.SetDestination(CBitcoinAddress(rcp.address.toStdString()).Get());
+ vecSend.push_back(std::pair<CScript, int64>(scriptPubKey, rcp.amount));
+
+ total += rcp.amount;
}
- total += rcp.amount;
}
-
- if(recipients.size() > setAddress.size())
+ if(setAddress.size() != nAddresses)
{
return DuplicateAddress;
}
@@ -169,19 +198,10 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(const QList<SendCoinsRecipie
{
LOCK2(cs_main, wallet->cs_wallet);
- // Sendmany
- std::vector<std::pair<CScript, int64> > vecSend;
- foreach(const SendCoinsRecipient &rcp, recipients)
- {
- CScript scriptPubKey;
- scriptPubKey.SetDestination(CBitcoinAddress(rcp.address.toStdString()).Get());
- vecSend.push_back(make_pair(scriptPubKey, rcp.amount));
- }
-
- CWalletTx wtx;
CReserveKey keyChange(wallet);
int64 nFeeRequired = 0;
std::string strFailReason;
+ CWalletTx wtx;
bool fCreated = wallet->CreateTransaction(vecSend, wtx, keyChange, nFeeRequired, strFailReason);
if(!fCreated)
@@ -194,6 +214,18 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(const QList<SendCoinsRecipie
CClientUIInterface::MSG_ERROR);
return TransactionCreationFailed;
}
+ // Store PaymentRequests in wtx.vOrderForm in wallet.
+ foreach(const SendCoinsRecipient &rcp, recipients)
+ {
+ if (rcp.paymentRequest.IsInitialized())
+ {
+ std::string key("PaymentRequest");
+ std::string value;
+ rcp.paymentRequest.SerializeToString(&value);
+ wtx.vOrderForm.push_back(make_pair(key, value));
+ }
+ }
+
if(!uiInterface.ThreadSafeAskFee(nFeeRequired))
{
return Aborted;
@@ -202,10 +234,15 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(const QList<SendCoinsRecipie
{
return TransactionCommitFailed;
}
- hex = QString::fromStdString(wtx.GetHash().GetHex());
+
+ CTransaction* t = (CTransaction*)&wtx;
+ CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
+ ssTx << *t;
+ transaction.append(&(ssTx[0]), ssTx.size());
}
- // Add addresses / update labels that we've sent to to the address book
+ // Add addresses / update labels that we've sent to to the address book,
+ // and emit coinsSent signal
foreach(const SendCoinsRecipient &rcp, recipients)
{
std::string strAddress = rcp.address.toStdString();
@@ -217,14 +254,19 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(const QList<SendCoinsRecipie
std::map<CTxDestination, CAddressBookData>::iterator mi = wallet->mapAddressBook.find(dest);
// Check if we have a new address or an updated label
- if (mi == wallet->mapAddressBook.end() || mi->second.name != strLabel)
+ if (mi == wallet->mapAddressBook.end())
+ {
+ wallet->SetAddressBook(dest, strLabel, "send");
+ }
+ else if (mi->second.name != strLabel)
{
- wallet->SetAddressBookName(dest, strLabel);
+ wallet->SetAddressBook(dest, strLabel, ""); // "" means don't change purpose
}
}
+ emit coinsSent(wallet, rcp, transaction);
}
- return SendCoinsReturn(OK, 0, hex);
+ return SendCoinsReturn(OK, 0);
}
OptionsModel *WalletModel::getOptionsModel()
diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h
index f14b09effe..8cba10f5d2 100644
--- a/src/qt/walletmodel.h
+++ b/src/qt/walletmodel.h
@@ -4,6 +4,7 @@
#include <QObject>
#include "allocators.h" /* for SecureString */
+#include "paymentrequestplus.h"
class OptionsModel;
class AddressTableModel;
@@ -17,9 +18,15 @@ QT_END_NAMESPACE
class SendCoinsRecipient
{
public:
+ SendCoinsRecipient() : amount(0) { }
+
QString address;
QString label;
qint64 amount;
+
+ // If from a payment request, paymentRequest.IsInitialized() will be true
+ PaymentRequestPlus paymentRequest;
+ QString authenticatedMerchant; // Empty if no authentication or invalid signature/cert/etc.
};
/** Interface to Bitcoin wallet from Qt view code. */
@@ -68,12 +75,10 @@ public:
struct SendCoinsReturn
{
SendCoinsReturn(StatusCode status,
- qint64 fee=0,
- QString hex=QString()):
- status(status), fee(fee), hex(hex) {}
+ qint64 fee=0):
+ status(status), fee(fee) {}
StatusCode status;
qint64 fee; // is used in case status is "AmountWithFeeExceedsBalance"
- QString hex; // is filled with the transaction hash if status is "OK"
};
// Send coins to a list of recipients
@@ -151,6 +156,9 @@ signals:
// Asynchronous message notification
void message(const QString &title, const QString &message, unsigned int style);
+ // Coins sent: from wallet, to recipient, in (serialized) transaction:
+ void coinsSent(CWallet* wallet, SendCoinsRecipient recipient, QByteArray transaction);
+
public slots:
/* Wallet status might have changed */
void updateStatus();
diff --git a/src/qt/walletstack.cpp b/src/qt/walletstack.cpp
index 9e3060e86b..6cc73358a4 100644
--- a/src/qt/walletstack.cpp
+++ b/src/qt/walletstack.cpp
@@ -54,12 +54,12 @@ void WalletStack::removeAllWallets()
mapWalletViews.clear();
}
-bool WalletStack::handleURI(const QString &uri)
+bool WalletStack::handlePaymentRequest(const SendCoinsRecipient &recipient)
{
WalletView *walletView = (WalletView*)currentWidget();
if (!walletView) return false;
- return walletView->handleURI(uri);
+ return walletView->handlePaymentRequest(recipient);
}
void WalletStack::showOutOfSyncWarning(bool fShow)
diff --git a/src/qt/walletstack.h b/src/qt/walletstack.h
index 506d595c0f..5c84cb6e1e 100644
--- a/src/qt/walletstack.h
+++ b/src/qt/walletstack.h
@@ -20,6 +20,7 @@ class TransactionView;
class OverviewPage;
class AddressBookPage;
class SendCoinsDialog;
+class SendCoinsRecipient;
class SignVerifyMessageDialog;
class Notificator;
class RPCConsole;
@@ -54,7 +55,7 @@ public:
void removeAllWallets();
- bool handleURI(const QString &uri);
+ bool handlePaymentRequest(const SendCoinsRecipient &recipient);
void showOutOfSyncWarning(bool fShow);
diff --git a/src/qt/walletview.cpp b/src/qt/walletview.cpp
index 277c056693..0a6a4028a0 100644
--- a/src/qt/walletview.cpp
+++ b/src/qt/walletview.cpp
@@ -201,10 +201,10 @@ void WalletView::gotoVerifyMessageTab(QString addr)
signVerifyMessageDialog->setAddress_VM(addr);
}
-bool WalletView::handleURI(const QString& strURI)
+bool WalletView::handlePaymentRequest(const SendCoinsRecipient& recipient)
{
// URI has to be valid
- if (sendCoinsPage->handleURI(strURI))
+ if (sendCoinsPage->handlePaymentRequest(recipient))
{
gotoSendCoinsPage();
return true;
diff --git a/src/qt/walletview.h b/src/qt/walletview.h
index 6bcd70baf9..97e8f93df9 100644
--- a/src/qt/walletview.h
+++ b/src/qt/walletview.h
@@ -16,6 +16,7 @@ class TransactionView;
class OverviewPage;
class AddressBookPage;
class SendCoinsDialog;
+class SendCoinsRecipient;
class SignVerifyMessageDialog;
class RPCConsole;
@@ -49,7 +50,7 @@ public:
*/
void setWalletModel(WalletModel *walletModel);
- bool handleURI(const QString &uri);
+ bool handlePaymentRequest(const SendCoinsRecipient& recipient);
void showOutOfSyncWarning(bool fShow);