Skip to content

Commit

Permalink
Reimplement click-to-pay links. Add OSX support.
Browse files Browse the repository at this point in the history
Switch to using Qt's QLocalServer/QLocalSocket to handle bitcoin
payment links (bitcoin:... URIs)

Reason for switch: the boost::interprocess mechanism seemed flaky,
and doesn't mesh as well with "The Qt Way"

qtipcserver.cpp/h is replaced by paymentserver.cpp/h

Click-to-pay now also works on OSX, with a custom Info.plist
that registers Bitcoin-Qt as a handler for bitcoin: URLs and
an event listener on the main QApplication that handles
QFileOpenEvents (Qt translates 'url clicked' AppleEvents into
QFileOpenEvents automagically).
  • Loading branch information
gavinandresen committed Feb 12, 2013
1 parent 2f0fa79 commit 8269a09
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 204 deletions.
6 changes: 4 additions & 2 deletions bitcoin-qt.pro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ TEMPLATE = app
TARGET = bitcoin-qt
VERSION = 0.8.0
INCLUDEPATH += src src/json src/qt
QT += network
DEFINES += QT_GUI BOOST_THREAD_USE_LIB BOOST_SPIRIT_THREADSAFE
CONFIG += no_include_pwd
CONFIG += thread
Expand Down Expand Up @@ -195,7 +196,7 @@ HEADERS += src/qt/bitcoingui.h \
src/qt/askpassphrasedialog.h \
src/protocol.h \
src/qt/notificator.h \
src/qt/qtipcserver.h \
src/qt/paymentserver.h \
src/allocators.h \
src/ui_interface.h \
src/qt/rpcconsole.h \
Expand Down Expand Up @@ -264,7 +265,7 @@ SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \
src/qt/askpassphrasedialog.cpp \
src/protocol.cpp \
src/qt/notificator.cpp \
src/qt/qtipcserver.cpp \
src/qt/paymentserver.cpp \
src/qt/rpcconsole.cpp \
src/noui.cpp \
src/leveldb.cpp \
Expand Down Expand Up @@ -385,6 +386,7 @@ macx:TARGET = "Bitcoin-Qt"
macx:QMAKE_CFLAGS_THREAD += -pthread
macx:QMAKE_LFLAGS_THREAD += -pthread
macx:QMAKE_CXXFLAGS_THREAD += -pthread
macx:QMAKE_INFO_PLIST = share/qt/Info.plist

# Set libraries and includes at end, to use platform-defined defaults if not overridden
INCLUDEPATH += $$BOOST_INCLUDE_PATH $$BDB_INCLUDE_PATH $$OPENSSL_INCLUDE_PATH $$QRENCODE_INCLUDE_PATH
Expand Down
31 changes: 31 additions & 0 deletions share/qt/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
<plist version="0.9">
<dict>
<key>CFBundleIconFile</key>
<string>bitcoin.icns</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleGetInfoString</key>
<string>Bitcoin-Qt</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleExecutable</key>
<string>Bitcoin-Qt</string>
<key>CFBundleIdentifier</key>
<string>org.bitcoinfoundation.Bitcoin-Qt</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>org.bitcoinfoundation.BitcoinPayment</string>
<key>CFBundleURLSchemes</key>
<array>
<string>bitcoin</string>
</array>
</dict>
</array>
</dict>
</plist>
33 changes: 12 additions & 21 deletions src/qt/bitcoin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
#include "guiconstants.h"
#include "init.h"
#include "ui_interface.h"
#include "qtipcserver.h"
#include "paymentserver.h"

#include <QApplication>
#include <QMessageBox>
#include <QTextCodec>
#include <QLocale>
#include <QTimer>
#include <QTranslator>
#include <QSplashScreen>
#include <QLibraryInfo>
Expand Down Expand Up @@ -70,15 +71,6 @@ static bool ThreadSafeAskFee(int64 nFeeRequired)
return payFee;
}

static void ThreadSafeHandleURI(const std::string& strURI)
{
if(!guiref)
return;

QMetaObject::invokeMethod(guiref, "handleURI", GUIUtil::blockingGUIThreadConnection(),
Q_ARG(QString, QString::fromStdString(strURI)));
}

static void InitMessage(const std::string &message)
{
if(splashref)
Expand Down Expand Up @@ -117,21 +109,19 @@ int main(int argc, char *argv[])
// Command-line options take precedence:
ParseParameters(argc, argv);

if(GetBoolArg("-testnet")) // Separate message queue name for testnet
strBitcoinURIQueueName = BITCOINURI_QUEUE_NAME_TESTNET;
else
strBitcoinURIQueueName = BITCOINURI_QUEUE_NAME_MAINNET;

// Do this early as we don't want to bother initializing if we are just calling IPC
ipcScanRelay(argc, argv);

// Internal string conversion is all UTF-8
QTextCodec::setCodecForTr(QTextCodec::codecForName("UTF-8"));
QTextCodec::setCodecForCStrings(QTextCodec::codecForTr());

Q_INIT_RESOURCE(bitcoin);
QApplication app(argc, argv);

// Do this early as we don't want to bother initializing if we are just calling IPC
// ... but do it after creating app, so QCoreApplication::arguments is initialized:
if (PaymentServer::ipcSendCommandLine())
exit(0);
PaymentServer* paymentServer = new PaymentServer(&app);

// Install global event filter that makes sure that long tooltips can be word-wrapped
app.installEventFilter(new GUIUtil::ToolTipToRichTextFilter(TOOLTIP_WRAP_THRESHOLD, &app));

Expand Down Expand Up @@ -188,7 +178,6 @@ int main(int argc, char *argv[])
// Subscribe to global signals from core
uiInterface.ThreadSafeMessageBox.connect(ThreadSafeMessageBox);
uiInterface.ThreadSafeAskFee.connect(ThreadSafeAskFee);
uiInterface.ThreadSafeHandleURI.connect(ThreadSafeHandleURI);
uiInterface.InitMessage.connect(InitMessage);
uiInterface.QueueShutdown.connect(QueueShutdown);
uiInterface.Translate.connect(Translate);
Expand Down Expand Up @@ -249,8 +238,10 @@ int main(int argc, char *argv[])
window.show();
}

// Place this here as guiref has to be defined if we don't want to lose URIs
ipcInit(argc, argv);
// Now that initialization/startup is done, process any command-line
// bitcoin: URIs
QObject::connect(paymentServer, SIGNAL(receivedURI(QString)), &window, SLOT(handleURI(QString)));
QTimer::singleShot(100, paymentServer, SLOT(uiReady()));

app.exec();

Expand Down
159 changes: 159 additions & 0 deletions src/qt/paymentserver.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright (c) 2009-2012 The Bitcoin developers
// 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 <QCoreApplication>
#include <QDataStream>
#include <QDebug>
#include <QFileOpenEvent>
#include <QHash>
#include <QLocalServer>
#include <QLocalSocket>
#include <QStringList>
#include <QUrl>

using namespace boost;

const int BITCOIN_IPC_CONNECT_TIMEOUT = 1000; // milliseconds
const QString BITCOIN_IPC_PREFIX("bitcoin:");

//
// Create a name that is unique for:
// testnet / non-testnet
// data directory
//
static QString ipcServerName()
{
QString name("BitcoinQt");

// Append a simple hash of the datadir
// Note that GetDataDir(true) returns a different path
// for -testnet versus main net
QString ddir(GetDataDir(true).string().c_str());
name.append(QString::number(qHash(ddir)));

return name;
}

//
// This stores payment requests received before
// the main GUI window is up and ready to ask the user
// to send payment.
//
static QStringList savedPaymentRequests;

//
// Sending to the server is done synchronously, at startup.
// If the server isn't already running, startup continues,
// and the items in savedPaymentRequest will be handled
// when uiReady() is called.
//
bool PaymentServer::ipcSendCommandLine()
{
bool fResult = false;

const QStringList& args = QCoreApplication::arguments();
for (int i = 1; i < args.size(); i++)
{
if (!args[i].startsWith(BITCOIN_IPC_PREFIX, Qt::CaseInsensitive))
continue;
savedPaymentRequests.append(args[i]);
}

foreach (const QString& arg, savedPaymentRequests)
{
QLocalSocket* socket = new QLocalSocket();
socket->connectToServer(ipcServerName(), QIODevice::WriteOnly);
if (!socket->waitForConnected(BITCOIN_IPC_CONNECT_TIMEOUT))
return false;

QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_4_0);
out << arg;
out.device()->seek(0);
socket->write(block);
socket->flush();

socket->waitForBytesWritten(BITCOIN_IPC_CONNECT_TIMEOUT);
socket->disconnectFromServer();
delete socket;
fResult = true;
}
return fResult;
}

PaymentServer::PaymentServer(QApplication* parent) : QObject(parent), saveURIs(true)
{
// Install global event filter to catch QFileOpenEvents on the mac (sent when you click bitcoin: links)
parent->installEventFilter(this);

QString name = ipcServerName();

// Clean up old socket leftover from a crash:
QLocalServer::removeServer(name);

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()));
}

bool PaymentServer::eventFilter(QObject *object, 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;
}
}
return false;
}

void PaymentServer::uiReady()
{
saveURIs = false;
foreach (const QString& s, savedPaymentRequests)
emit receivedURI(s);
savedPaymentRequests.clear();
}

void PaymentServer::handleURIConnection()
{
QLocalSocket *clientConnection = uriServer->nextPendingConnection();

while (clientConnection->bytesAvailable() < (int)sizeof(quint32))
clientConnection->waitForReadyRead();

connect(clientConnection, SIGNAL(disconnected()),
clientConnection, SLOT(deleteLater()));

QDataStream in(clientConnection);
in.setVersion(QDataStream::Qt_4_0);
if (clientConnection->bytesAvailable() < (int)sizeof(quint16)) {
return;
}
QString message;
in >> message;

if (saveURIs)
savedPaymentRequests.append(message);
else
emit receivedURI(message);
}
66 changes: 66 additions & 0 deletions src/qt/paymentserver.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#ifndef PAYMENTSERVER_H
#define PAYMENTSERVER_H

//
// This class handles payment requests from clicking on
// bitcoin: URIs
//
// This is somewhat tricky, because we have to deal with
// the situation where the user clicks on a link during
// startup/initialization, when the splash-screen is up
// but the main window (and the Send Coins tab) is not.
//
// So, the strategy is:
//
// Create the server, and register the event handler,
// when the application is created. Save any URIs
// received at or during startup in a list.
//
// When startup is finished and the main window is
// show, a signal is sent to slot uiReady(), which
// emits a receivedURL() signal for any payment
// requests that happened during startup.
//
// After startup, receivedURL() happens as usual.
//
// This class has one more feature: a static
// method that finds URIs passed in the command line
// and, if a server is running in another process,
// sends them to the server.
//
#include <QObject>
#include <QString>

class QApplication;
class QLocalServer;

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();

PaymentServer(QApplication* parent);

bool eventFilter(QObject *object, QEvent *event);

signals:
void receivedURI(QString);

public slots:
// Signal this when the main window's UI is ready
// to display payment requests to the user
void uiReady();

private slots:
void handleURIConnection();
};

#endif // PAYMENTSERVER_H
Loading

0 comments on commit 8269a09

Please sign in to comment.