changeset 2403:0ca103ed05d5 draft

Merge pull request #1075 from laanwj/2012_04_consoleui Add UI RPC console / debug window
author Gregory Maxwell <gmaxwell@gmail.com>
date Tue, 08 May 2012 12:26:49 -0700
parents 474f3a86e09a (current diff) 413c38b8c6b7 (diff)
children 6a2c351fa65b
files bitcoin-qt.pro src/bitcoinrpc.cpp
diffstat 10 files changed, 834 insertions(+), 80 deletions(-) [+]
line wrap: on
line diff
--- a/bitcoin-qt.pro
+++ b/bitcoin-qt.pro
@@ -158,7 +158,8 @@
     src/qt/notificator.h \
     src/qt/qtipcserver.h \
     src/allocators.h \
-    src/ui_interface.h
+    src/ui_interface.h \
+    src/qt/rpcconsole.h
 
 SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \
     src/qt/transactiontablemodel.cpp \
@@ -212,7 +213,8 @@
     src/qt/askpassphrasedialog.cpp \
     src/protocol.cpp \
     src/qt/notificator.cpp \
-    src/qt/qtipcserver.cpp
+    src/qt/qtipcserver.cpp \
+    src/qt/rpcconsole.cpp
 
 RESOURCES += \
     src/qt/bitcoin.qrc
@@ -226,7 +228,8 @@
     src/qt/forms/transactiondescdialog.ui \
     src/qt/forms/overviewpage.ui \
     src/qt/forms/sendcoinsentry.ui \
-    src/qt/forms/askpassphrasedialog.ui
+    src/qt/forms/askpassphrasedialog.ui \
+    src/qt/forms/rpcconsole.ui
 
 contains(USE_QRCODE, 1) {
 HEADERS += src/qt/qrcodedialog.h
--- a/src/bitcoinrpc.cpp
+++ b/src/bitcoinrpc.cpp
@@ -2507,34 +2507,11 @@
             else
                 throw JSONRPCError(-32600, "Params must be an array");
 
-            // Find method
-            const CRPCCommand *pcmd = tableRPC[strMethod];
-            if (!pcmd)
-                throw JSONRPCError(-32601, "Method not found");
-
-            // Observe safe mode
-            string strWarning = GetWarnings("rpc");
-            if (strWarning != "" && !GetBoolArg("-disablesafemode") &&
-                !pcmd->okSafeMode)
-                throw JSONRPCError(-2, string("Safe mode: ") + strWarning);
-
-            try
-            {
-                // Execute
-                Value result;
-                {
-                    LOCK2(cs_main, pwalletMain->cs_wallet);
-                    result = pcmd->actor(params, false);
-                }
-
-                // Send reply
-                string strReply = JSONRPCReply(result, Value::null, id);
-                stream << HTTPReply(200, strReply) << std::flush;
-            }
-            catch (std::exception& e)
-            {
-                ErrorReply(stream, JSONRPCError(-1, e.what()), id);
-            }
+            Value result = tableRPC.execute(strMethod, params);
+
+            // Send reply
+            string strReply = JSONRPCReply(result, Value::null, id);
+            stream << HTTPReply(200, strReply) << std::flush;
         }
         catch (Object& objError)
         {
@@ -2547,7 +2524,34 @@
     }
 }
 
-
+json_spirit::Value CRPCTable::execute(const std::string &strMethod, const json_spirit::Array &params) const
+{
+    // Find method
+    const CRPCCommand *pcmd = tableRPC[strMethod];
+    if (!pcmd)
+        throw JSONRPCError(-32601, "Method not found");
+
+    // Observe safe mode
+    string strWarning = GetWarnings("rpc");
+    if (strWarning != "" && !GetBoolArg("-disablesafemode") &&
+        !pcmd->okSafeMode)
+        throw JSONRPCError(-2, string("Safe mode: ") + strWarning);
+
+    try
+    {
+        // Execute
+        Value result;
+        {
+            LOCK2(cs_main, pwalletMain->cs_wallet);
+            result = pcmd->actor(params, false);
+        }
+        return result;
+    }
+    catch (std::exception& e)
+    {
+        throw JSONRPCError(-1, e.what());
+    }
+}
 
 
 Object CallRPC(const string& strMethod, const Array& params)
@@ -2621,6 +2625,60 @@
     }
 }
 
+// Convert strings to command-specific RPC representation
+Array RPCConvertValues(const std::string &strMethod, const std::vector<std::string> &strParams)
+{
+    Array params;
+    BOOST_FOREACH(const std::string &param, strParams)
+        params.push_back(param);
+
+    int n = params.size();
+
+    //
+    // Special case non-string parameter types
+    //
+    if (strMethod == "setgenerate"            && n > 0) ConvertTo<bool>(params[0]);
+    if (strMethod == "setgenerate"            && n > 1) ConvertTo<boost::int64_t>(params[1]);
+    if (strMethod == "sendtoaddress"          && n > 1) ConvertTo<double>(params[1]);
+    if (strMethod == "settxfee"               && n > 0) ConvertTo<double>(params[0]);
+    if (strMethod == "getreceivedbyaddress"   && n > 1) ConvertTo<boost::int64_t>(params[1]);
+    if (strMethod == "getreceivedbyaccount"   && n > 1) ConvertTo<boost::int64_t>(params[1]);
+    if (strMethod == "listreceivedbyaddress"  && n > 0) ConvertTo<boost::int64_t>(params[0]);
+    if (strMethod == "listreceivedbyaddress"  && n > 1) ConvertTo<bool>(params[1]);
+    if (strMethod == "listreceivedbyaccount"  && n > 0) ConvertTo<boost::int64_t>(params[0]);
+    if (strMethod == "listreceivedbyaccount"  && n > 1) ConvertTo<bool>(params[1]);
+    if (strMethod == "getbalance"             && n > 1) ConvertTo<boost::int64_t>(params[1]);
+    if (strMethod == "getblockhash"           && n > 0) ConvertTo<boost::int64_t>(params[0]);
+    if (strMethod == "move"                   && n > 2) ConvertTo<double>(params[2]);
+    if (strMethod == "move"                   && n > 3) ConvertTo<boost::int64_t>(params[3]);
+    if (strMethod == "sendfrom"               && n > 2) ConvertTo<double>(params[2]);
+    if (strMethod == "sendfrom"               && n > 3) ConvertTo<boost::int64_t>(params[3]);
+    if (strMethod == "listtransactions"       && n > 1) ConvertTo<boost::int64_t>(params[1]);
+    if (strMethod == "listtransactions"       && n > 2) ConvertTo<boost::int64_t>(params[2]);
+    if (strMethod == "listaccounts"           && n > 0) ConvertTo<boost::int64_t>(params[0]);
+    if (strMethod == "walletpassphrase"       && n > 1) ConvertTo<boost::int64_t>(params[1]);
+    if (strMethod == "listsinceblock"         && n > 1) ConvertTo<boost::int64_t>(params[1]);
+    if (strMethod == "sendmany"               && n > 1)
+    {
+        string s = params[1].get_str();
+        Value v;
+        if (!read_string(s, v) || v.type() != obj_type)
+            throw runtime_error("type mismatch");
+        params[1] = v.get_obj();
+    }
+    if (strMethod == "sendmany"                && n > 2) ConvertTo<boost::int64_t>(params[2]);
+    if (strMethod == "addmultisigaddress"      && n > 0) ConvertTo<boost::int64_t>(params[0]);
+    if (strMethod == "addmultisigaddress"      && n > 1)
+    {
+        string s = params[1].get_str();
+        Value v;
+        if (!read_string(s, v) || v.type() != array_type)
+            throw runtime_error("type mismatch "+s);
+        params[1] = v.get_array();
+    }
+    return params;
+}
+
 int CommandLineRPC(int argc, char *argv[])
 {
     string strPrint;
@@ -2640,53 +2698,8 @@
         string strMethod = argv[1];
 
         // Parameters default to strings
-        Array params;
-        for (int i = 2; i < argc; i++)
-            params.push_back(argv[i]);
-        int n = params.size();
-
-        //
-        // Special case non-string parameter types
-        //
-        if (strMethod == "setgenerate"            && n > 0) ConvertTo<bool>(params[0]);
-        if (strMethod == "setgenerate"            && n > 1) ConvertTo<boost::int64_t>(params[1]);
-        if (strMethod == "sendtoaddress"          && n > 1) ConvertTo<double>(params[1]);
-        if (strMethod == "settxfee"               && n > 0) ConvertTo<double>(params[0]);
-        if (strMethod == "getreceivedbyaddress"   && n > 1) ConvertTo<boost::int64_t>(params[1]);
-        if (strMethod == "getreceivedbyaccount"   && n > 1) ConvertTo<boost::int64_t>(params[1]);
-        if (strMethod == "listreceivedbyaddress"  && n > 0) ConvertTo<boost::int64_t>(params[0]);
-        if (strMethod == "listreceivedbyaddress"  && n > 1) ConvertTo<bool>(params[1]);
-        if (strMethod == "listreceivedbyaccount"  && n > 0) ConvertTo<boost::int64_t>(params[0]);
-        if (strMethod == "listreceivedbyaccount"  && n > 1) ConvertTo<bool>(params[1]);
-        if (strMethod == "getbalance"             && n > 1) ConvertTo<boost::int64_t>(params[1]);
-        if (strMethod == "getblockhash"           && n > 0) ConvertTo<boost::int64_t>(params[0]);
-        if (strMethod == "move"                   && n > 2) ConvertTo<double>(params[2]);
-        if (strMethod == "move"                   && n > 3) ConvertTo<boost::int64_t>(params[3]);
-        if (strMethod == "sendfrom"               && n > 2) ConvertTo<double>(params[2]);
-        if (strMethod == "sendfrom"               && n > 3) ConvertTo<boost::int64_t>(params[3]);
-        if (strMethod == "listtransactions"       && n > 1) ConvertTo<boost::int64_t>(params[1]);
-        if (strMethod == "listtransactions"       && n > 2) ConvertTo<boost::int64_t>(params[2]);
-        if (strMethod == "listaccounts"           && n > 0) ConvertTo<boost::int64_t>(params[0]);
-        if (strMethod == "walletpassphrase"       && n > 1) ConvertTo<boost::int64_t>(params[1]);
-        if (strMethod == "listsinceblock"         && n > 1) ConvertTo<boost::int64_t>(params[1]);
-        if (strMethod == "sendmany"               && n > 1)
-        {
-            string s = params[1].get_str();
-            Value v;
-            if (!read_string(s, v) || v.type() != obj_type)
-                throw runtime_error("type mismatch");
-            params[1] = v.get_obj();
-        }
-        if (strMethod == "sendmany"                && n > 2) ConvertTo<boost::int64_t>(params[2]);
-        if (strMethod == "addmultisigaddress"      && n > 0) ConvertTo<boost::int64_t>(params[0]);
-        if (strMethod == "addmultisigaddress"      && n > 1)
-        {
-            string s = params[1].get_str();
-            Value v;
-            if (!read_string(s, v) || v.type() != array_type)
-                throw runtime_error("type mismatch "+s);
-            params[1] = v.get_array();
-        }
+        std::vector<std::string> strParams(&argv[2], &argv[argc]);
+        Array params = RPCConvertValues(strMethod, strParams);
 
         // Execute
         Object reply = CallRPC(strMethod, params);
--- a/src/bitcoinrpc.h
+++ b/src/bitcoinrpc.h
@@ -16,6 +16,9 @@
 void ThreadRPCServer(void* parg);
 int CommandLineRPC(int argc, char *argv[]);
 
+/** Convert parameter values for RPC call from strings to command-specific JSON objects. */
+json_spirit::Array RPCConvertValues(const std::string &strMethod, const std::vector<std::string> &strParams);
+
 typedef json_spirit::Value(*rpcfn_type)(const json_spirit::Array& params, bool fHelp);
 
 class CRPCCommand
@@ -26,6 +29,9 @@
     bool okSafeMode;
 };
 
+/**
+ * Bitcoin RPC command dispatcher.
+ */
 class CRPCTable
 {
 private:
@@ -34,6 +40,15 @@
     CRPCTable();
     const CRPCCommand* operator[](std::string name) const;
     std::string help(std::string name) const;
+
+    /**
+     * Execute a method.
+     * @param method   Method to execute
+     * @param params   Array of arguments (JSON objects)
+     * @returns Result of the call.
+     * @throws an exception (json_spirit::Value) when an error happens.
+     */
+    json_spirit::Value execute(const std::string &method, const json_spirit::Array &params) const;
 };
 
 extern const CRPCTable tableRPC;
--- a/src/qt/bitcoingui.cpp
+++ b/src/qt/bitcoingui.cpp
@@ -24,6 +24,7 @@
 #include "askpassphrasedialog.h"
 #include "notificator.h"
 #include "guiutil.h"
+#include "rpcconsole.h"
 
 #ifdef Q_WS_MAC
 #include "macdockiconhandler.h"
@@ -64,7 +65,8 @@
     changePassphraseAction(0),
     aboutQtAction(0),
     trayIcon(0),
-    notificator(0)
+    notificator(0),
+    rpcConsole(0)
 {
     resize(850, 550);
     setWindowTitle(tr("Bitcoin Wallet"));
@@ -158,6 +160,9 @@
     // Doubleclicking on a transaction on the transaction history page shows details
     connect(transactionView, SIGNAL(doubleClicked(QModelIndex)), transactionView, SLOT(showDetails()));
 
+    rpcConsole = new RPCConsole(this);
+    connect(openRPCConsoleAction, SIGNAL(triggered()), rpcConsole, SLOT(show()));
+
     gotoOverviewPage();
 }
 
@@ -248,6 +253,8 @@
     backupWalletAction->setToolTip(tr("Backup wallet to another location"));
     changePassphraseAction = new QAction(QIcon(":/icons/key"), tr("&Change Passphrase"), this);
     changePassphraseAction->setToolTip(tr("Change the passphrase used for wallet encryption"));
+    openRPCConsoleAction = new QAction(tr("&Debug window"), this);
+    openRPCConsoleAction->setToolTip(tr("Open debugging and diagnostic console"));
 
     connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit()));
     connect(optionsAction, SIGNAL(triggered()), this, SLOT(optionsClicked()));
@@ -286,6 +293,8 @@
     settings->addAction(optionsAction);
 
     QMenu *help = appMenuBar->addMenu(tr("&Help"));
+    help->addAction(openRPCConsoleAction);
+    help->addSeparator();
     help->addAction(aboutAction);
     help->addAction(aboutQtAction);
 }
@@ -338,6 +347,8 @@
 
         // Report errors from network/worker thread
         connect(clientModel, SIGNAL(error(QString,QString, bool)), this, SLOT(error(QString,QString,bool)));
+
+        rpcConsole->setClientModel(clientModel);
     }
 }
 
--- a/src/qt/bitcoingui.h
+++ b/src/qt/bitcoingui.h
@@ -13,6 +13,7 @@
 class SendCoinsDialog;
 class MessagePage;
 class Notificator;
+class RPCConsole;
 
 QT_BEGIN_NAMESPACE
 class QLabel;
@@ -87,10 +88,12 @@
     QAction *backupWalletAction;
     QAction *changePassphraseAction;
     QAction *aboutQtAction;
+    QAction *openRPCConsoleAction;
 
     QSystemTrayIcon *trayIcon;
     Notificator *notificator;
     TransactionView *transactionView;
+    RPCConsole *rpcConsole;
 
     QMovie *syncIconMovie;
 
--- a/src/qt/clientmodel.cpp
+++ b/src/qt/clientmodel.cpp
@@ -93,3 +93,8 @@
 {
     return QString::fromStdString(CLIENT_DATE);
 }
+
+QString ClientModel::clientName() const
+{
+    return QString::fromStdString(CLIENT_NAME);
+}
--- a/src/qt/clientmodel.h
+++ b/src/qt/clientmodel.h
@@ -38,6 +38,7 @@
 
     QString formatFullVersion() const;
     QString formatBuildDate() const;
+    QString clientName() const;
 
 private:
     OptionsModel *optionsModel;
new file mode 100644
--- /dev/null
+++ b/src/qt/forms/rpcconsole.ui
@@ -0,0 +1,323 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>RPCConsole</class>
+ <widget class="QDialog" name="RPCConsole">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>706</width>
+    <height>382</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Bitcoin debug window</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <item>
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="tab">
+      <attribute name="title">
+       <string>Information</string>
+      </attribute>
+      <layout class="QGridLayout" name="gridLayout" columnstretch="0,1">
+       <property name="horizontalSpacing">
+        <number>12</number>
+       </property>
+       <item row="1" column="0">
+        <widget class="QLabel" name="label_5">
+         <property name="text">
+          <string>Client name</string>
+         </property>
+        </widget>
+       </item>
+       <item row="1" column="1">
+        <widget class="QLabel" name="clientName">
+         <property name="text">
+          <string>N/A</string>
+         </property>
+         <property name="textFormat">
+          <enum>Qt::PlainText</enum>
+         </property>
+         <property name="textInteractionFlags">
+          <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+         </property>
+        </widget>
+       </item>
+       <item row="2" column="0">
+        <widget class="QLabel" name="label_6">
+         <property name="text">
+          <string>Client version</string>
+         </property>
+        </widget>
+       </item>
+       <item row="2" column="1">
+        <widget class="QLabel" name="clientVersion">
+         <property name="text">
+          <string>N/A</string>
+         </property>
+         <property name="textFormat">
+          <enum>Qt::PlainText</enum>
+         </property>
+         <property name="textInteractionFlags">
+          <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="0">
+        <widget class="QLabel" name="label_9">
+         <property name="font">
+          <font>
+           <weight>75</weight>
+           <bold>true</bold>
+          </font>
+         </property>
+         <property name="text">
+          <string>Version</string>
+         </property>
+        </widget>
+       </item>
+       <item row="4" column="0">
+        <widget class="QLabel" name="label_11">
+         <property name="font">
+          <font>
+           <weight>75</weight>
+           <bold>true</bold>
+          </font>
+         </property>
+         <property name="text">
+          <string>Network</string>
+         </property>
+        </widget>
+       </item>
+       <item row="5" column="0">
+        <widget class="QLabel" name="label_7">
+         <property name="text">
+          <string>Number of connections</string>
+         </property>
+        </widget>
+       </item>
+       <item row="5" column="1">
+        <widget class="QLabel" name="numberOfConnections">
+         <property name="text">
+          <string>N/A</string>
+         </property>
+         <property name="textFormat">
+          <enum>Qt::PlainText</enum>
+         </property>
+         <property name="textInteractionFlags">
+          <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+         </property>
+        </widget>
+       </item>
+       <item row="6" column="0">
+        <widget class="QLabel" name="label_8">
+         <property name="text">
+          <string>On testnet</string>
+         </property>
+        </widget>
+       </item>
+       <item row="6" column="1">
+        <widget class="QCheckBox" name="isTestNet">
+         <property name="enabled">
+          <bool>false</bool>
+         </property>
+         <property name="text">
+          <string/>
+         </property>
+        </widget>
+       </item>
+       <item row="7" column="0">
+        <widget class="QLabel" name="label_10">
+         <property name="font">
+          <font>
+           <weight>75</weight>
+           <bold>true</bold>
+          </font>
+         </property>
+         <property name="text">
+          <string>Block chain</string>
+         </property>
+        </widget>
+       </item>
+       <item row="8" column="0">
+        <widget class="QLabel" name="label_3">
+         <property name="text">
+          <string>Current number of blocks</string>
+         </property>
+        </widget>
+       </item>
+       <item row="8" column="1">
+        <widget class="QLabel" name="numberOfBlocks">
+         <property name="text">
+          <string>N/A</string>
+         </property>
+         <property name="textFormat">
+          <enum>Qt::PlainText</enum>
+         </property>
+         <property name="textInteractionFlags">
+          <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+         </property>
+        </widget>
+       </item>
+       <item row="9" column="0">
+        <widget class="QLabel" name="label_4">
+         <property name="text">
+          <string>Estimated total blocks</string>
+         </property>
+        </widget>
+       </item>
+       <item row="9" column="1">
+        <widget class="QLabel" name="totalBlocks">
+         <property name="text">
+          <string>N/A</string>
+         </property>
+         <property name="textFormat">
+          <enum>Qt::PlainText</enum>
+         </property>
+         <property name="textInteractionFlags">
+          <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+         </property>
+        </widget>
+       </item>
+       <item row="10" column="0">
+        <widget class="QLabel" name="label_2">
+         <property name="text">
+          <string>Last block time</string>
+         </property>
+        </widget>
+       </item>
+       <item row="10" column="1">
+        <widget class="QLabel" name="lastBlockTime">
+         <property name="text">
+          <string>N/A</string>
+         </property>
+         <property name="textFormat">
+          <enum>Qt::PlainText</enum>
+         </property>
+         <property name="textInteractionFlags">
+          <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+         </property>
+        </widget>
+       </item>
+       <item row="11" column="0">
+        <spacer name="verticalSpacer">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item row="3" column="0">
+        <widget class="QLabel" name="label_12">
+         <property name="text">
+          <string>Build date</string>
+         </property>
+        </widget>
+       </item>
+       <item row="3" column="1">
+        <widget class="QLabel" name="buildDate">
+         <property name="text">
+          <string>N/A</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tab_2">
+      <attribute name="title">
+       <string>Console</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_3">
+       <property name="spacing">
+        <number>3</number>
+       </property>
+       <item>
+        <widget class="QTableWidget" name="messagesWidget">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>100</height>
+          </size>
+         </property>
+         <property name="tabKeyNavigation">
+          <bool>false</bool>
+         </property>
+         <property name="selectionBehavior">
+          <enum>QAbstractItemView::SelectRows</enum>
+         </property>
+         <property name="columnCount">
+          <number>2</number>
+         </property>
+         <attribute name="horizontalHeaderVisible">
+          <bool>false</bool>
+         </attribute>
+         <attribute name="verticalHeaderVisible">
+          <bool>false</bool>
+         </attribute>
+         <column/>
+         <column/>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout">
+         <property name="spacing">
+          <number>3</number>
+         </property>
+         <item>
+          <widget class="QLabel" name="label">
+           <property name="text">
+            <string>&gt;</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLineEdit" name="lineEdit"/>
+         </item>
+         <item>
+          <widget class="QPushButton" name="clearButton">
+           <property name="maximumSize">
+            <size>
+             <width>24</width>
+             <height>24</height>
+            </size>
+           </property>
+           <property name="toolTip">
+            <string>Clear console</string>
+           </property>
+           <property name="text">
+            <string/>
+           </property>
+           <property name="icon">
+            <iconset resource="../bitcoin.qrc">
+             <normaloff>:/icons/remove</normaloff>:/icons/remove</iconset>
+           </property>
+           <property name="shortcut">
+            <string notr="true">Ctrl+L</string>
+           </property>
+           <property name="autoDefault">
+            <bool>false</bool>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources>
+  <include location="../bitcoin.qrc"/>
+ </resources>
+ <connections/>
+</ui>
new file mode 100644
--- /dev/null
+++ b/src/qt/rpcconsole.cpp
@@ -0,0 +1,316 @@
+#include "rpcconsole.h"
+#include "ui_rpcconsole.h"
+
+#include "clientmodel.h"
+#include "bitcoinrpc.h"
+#include "guiutil.h"
+
+#include <QTime>
+#include <QTimer>
+#include <QThread>
+#include <QTextEdit>
+#include <QKeyEvent>
+
+#include <boost/tokenizer.hpp>
+
+// TODO: make it possible to filter out categories (esp debug messages when implemented)
+// TODO: receive errors and debug messages through ClientModel
+
+const int CONSOLE_SCROLLBACK = 50;
+const int CONSOLE_HISTORY = 50;
+
+/* Object for executing console RPC commands in a separate thread.
+*/
+class RPCExecutor: public QObject
+{
+    Q_OBJECT
+public slots:
+    void start();
+    void request(const QString &command);
+signals:
+    void reply(int category, const QString &command);
+};
+
+#include "rpcconsole.moc"
+
+void RPCExecutor::start()
+{
+   // Nothing to do
+}
+
+void RPCExecutor::request(const QString &command)
+{
+    // Parse shell-like command line into separate arguments
+    boost::escaped_list_separator<char> els('\\',' ','\"');
+    std::string strCommand = command.toStdString();
+    boost::tokenizer<boost::escaped_list_separator<char> > tok(strCommand, els);
+
+    std::string strMethod;
+    std::vector<std::string> strParams;
+    int n = 0;
+    for(boost::tokenizer<boost::escaped_list_separator<char> >::iterator beg=tok.begin(); beg!=tok.end();++beg,++n)
+    {
+        if(n == 0) // First parameter is the command
+            strMethod = *beg;
+        else
+            strParams.push_back(*beg);
+    }
+
+    try {
+        std::string strPrint;
+        json_spirit::Value result = tableRPC.execute(strMethod, RPCConvertValues(strMethod, strParams));
+
+        // Format result reply
+        if (result.type() == json_spirit::null_type)
+            strPrint = "";
+        else if (result.type() == json_spirit::str_type)
+            strPrint = result.get_str();
+        else
+            strPrint = write_string(result, true);
+
+        emit reply(RPCConsole::CMD_REPLY, QString::fromStdString(strPrint));
+    }
+    catch (json_spirit::Object& objError)
+    {
+        emit reply(RPCConsole::CMD_ERROR, QString::fromStdString(write_string(json_spirit::Value(objError), false)));
+    }
+    catch (std::exception& e)
+    {
+        emit reply(RPCConsole::CMD_ERROR, QString("Error: ") + QString::fromStdString(e.what()));
+    }
+}
+
+RPCConsole::RPCConsole(QWidget *parent) :
+    QDialog(parent),
+    ui(new Ui::RPCConsole),
+    firstLayout(true),
+    historyPtr(0)
+{
+    ui->setupUi(this);
+    ui->messagesWidget->horizontalHeader()->setResizeMode(1, QHeaderView::Stretch);
+    ui->messagesWidget->setContextMenuPolicy(Qt::ActionsContextMenu);
+
+    // Install event filter for up and down arrow
+    ui->lineEdit->installEventFilter(this);
+
+    // Add "Copy message" to context menu explicitly
+    QAction *copyMessageAction = new QAction(tr("&Copy"), this);
+    copyMessageAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_C));
+    copyMessageAction->setShortcutContext(Qt::WidgetShortcut);
+    connect(copyMessageAction, SIGNAL(triggered()), this, SLOT(copyMessage()));
+    ui->messagesWidget->addAction(copyMessageAction);
+
+    connect(ui->clearButton, SIGNAL(clicked()), this, SLOT(clear()));
+
+    startExecutor();
+
+    clear();
+}
+
+RPCConsole::~RPCConsole()
+{
+    emit stopExecutor();
+    delete ui;
+}
+
+bool RPCConsole::event(QEvent *event)
+{
+   int returnValue = QWidget::event(event);
+
+   if (event->type() == QEvent::LayoutRequest && firstLayout)
+   {
+       // Work around QTableWidget issue:
+       // Call resizeRowsToContents on first Layout request with widget visible,
+       // to make sure multiline messages that were added before the console was shown
+       // have the right height.
+       if(ui->messagesWidget->isVisible())
+       {
+           firstLayout = false;
+           ui->messagesWidget->resizeRowsToContents();
+       }
+       return true;
+   }
+
+   return returnValue;
+}
+
+bool RPCConsole::eventFilter(QObject* obj, QEvent *event)
+{
+    if(obj == ui->lineEdit)
+    {
+        if(event->type() == QEvent::KeyPress)
+        {
+            QKeyEvent *key = static_cast<QKeyEvent*>(event);
+            switch(key->key())
+            {
+            case Qt::Key_Up: browseHistory(-1); return true;
+            case Qt::Key_Down: browseHistory(1); return true;
+            }
+        }
+    }
+    return QDialog::eventFilter(obj, event);
+}
+
+void RPCConsole::setClientModel(ClientModel *model)
+{
+    this->clientModel = model;
+    if(model)
+    {
+        // Subscribe to information, replies, messages, errors
+        connect(model, SIGNAL(numConnectionsChanged(int)), this, SLOT(setNumConnections(int)));
+        connect(model, SIGNAL(numBlocksChanged(int)), this, SLOT(setNumBlocks(int)));
+
+        // Provide initial values
+        ui->clientVersion->setText(model->formatFullVersion());
+        ui->clientName->setText(model->clientName());
+        ui->buildDate->setText(model->formatBuildDate());
+
+        setNumConnections(model->getNumConnections());
+        ui->isTestNet->setChecked(model->isTestNet());
+
+        setNumBlocks(model->getNumBlocks());
+    }
+}
+
+static QColor categoryColor(int category)
+{
+    switch(category)
+    {
+    case RPCConsole::MC_ERROR:     return QColor(255,0,0); break;
+    case RPCConsole::MC_DEBUG:     return QColor(192,192,192); break;
+    case RPCConsole::CMD_REQUEST:  return QColor(128,128,128); break;
+    case RPCConsole::CMD_REPLY:    return QColor(128,255,128); break;
+    case RPCConsole::CMD_ERROR:    return QColor(255,128,128); break;
+    default:           return QColor(0,0,0);
+    }
+}
+
+void RPCConsole::clear()
+{
+    ui->messagesWidget->clear();
+    ui->messagesWidget->setRowCount(0);
+    ui->lineEdit->clear();
+    ui->lineEdit->setFocus();
+
+    message(CMD_REPLY, tr("Welcome to the bitcoin RPC console.")+"\n"+
+                       tr("Use up and down arrows to navigate history, and Ctrl-L to clear screen.")+"\n"+
+                       tr("Type \"help\" for an overview of available commands."));
+}
+
+void RPCConsole::message(int category, const QString &message)
+{
+    // Add row to messages widget
+    int row = ui->messagesWidget->rowCount();
+    ui->messagesWidget->setRowCount(row+1);
+
+    QTime time = QTime::currentTime();
+    QTableWidgetItem *newTime = new QTableWidgetItem(time.toString());
+    newTime->setData(Qt::DecorationRole, categoryColor(category));
+    newTime->setForeground(QColor(128,128,128));
+    newTime->setFlags(Qt::ItemIsSelectable|Qt::ItemIsEnabled); // make non-editable
+
+    int numLines = message.count("\n") + 1;
+    // As Qt doesn't like very tall cells (they break scrolling) keep only short messages in
+    // the cell text, longer messages trigger a display widget with scroll bar
+    if(numLines < 5)
+    {
+        QTableWidgetItem *newItem = new QTableWidgetItem(message);
+        newItem->setFlags(Qt::ItemIsSelectable|Qt::ItemIsEnabled); // make non-editable
+        if(category == CMD_ERROR) // Coloring error messages in red
+            newItem->setForeground(QColor(255,16,16));
+        ui->messagesWidget->setItem(row, 1, newItem);
+    } else {
+        QTextEdit *newWidget = new QTextEdit;
+        newWidget->setText(message);
+        newWidget->setMaximumHeight(100);
+        newWidget->setReadOnly(true);
+        ui->messagesWidget->setCellWidget(row, 1, newWidget);
+    }
+
+    ui->messagesWidget->setItem(row, 0, newTime);
+    ui->messagesWidget->resizeRowToContents(row);
+    // Preserve only limited scrollback buffer
+    while(ui->messagesWidget->rowCount() > CONSOLE_SCROLLBACK)
+        ui->messagesWidget->removeRow(0);
+    // Scroll to bottom after table is updated
+    QTimer::singleShot(0, ui->messagesWidget, SLOT(scrollToBottom()));
+}
+
+void RPCConsole::setNumConnections(int count)
+{
+    ui->numberOfConnections->setText(QString::number(count));
+}
+
+void RPCConsole::setNumBlocks(int count)
+{
+    ui->numberOfBlocks->setText(QString::number(count));
+    if(clientModel)
+    {
+        ui->totalBlocks->setText(QString::number(clientModel->getNumBlocksOfPeers()));
+        ui->lastBlockTime->setText(clientModel->getLastBlockDate().toString());
+    }
+}
+
+void RPCConsole::on_lineEdit_returnPressed()
+{
+    QString cmd = ui->lineEdit->text();
+    ui->lineEdit->clear();
+
+    if(!cmd.isEmpty())
+    {
+        message(CMD_REQUEST, cmd);
+        emit cmdRequest(cmd);
+        // Truncate history from current position
+        history.erase(history.begin() + historyPtr, history.end());
+        // Append command to history
+        history.append(cmd);
+        // Enforce maximum history size
+        while(history.size() > CONSOLE_HISTORY)
+            history.removeFirst();
+        // Set pointer to end of history
+        historyPtr = history.size();
+    }
+}
+
+void RPCConsole::browseHistory(int offset)
+{
+    historyPtr += offset;
+    if(historyPtr < 0)
+        historyPtr = 0;
+    if(historyPtr > history.size())
+        historyPtr = history.size();
+    QString cmd;
+    if(historyPtr < history.size())
+        cmd = history.at(historyPtr);
+    ui->lineEdit->setText(cmd);
+}
+
+void RPCConsole::startExecutor()
+{
+    QThread* thread = new QThread;
+    RPCExecutor *executor = new RPCExecutor();
+    executor->moveToThread(thread);
+
+    // Notify executor when thread started (in executor thread)
+    connect(thread, SIGNAL(started()), executor, SLOT(start()));
+    // Replies from executor object must go to this object
+    connect(executor, SIGNAL(reply(int,QString)), this, SLOT(message(int,QString)));
+    // Requests from this object must go to executor
+    connect(this, SIGNAL(cmdRequest(QString)), executor, SLOT(request(QString)));
+    // On stopExecutor signal
+    // - queue executor for deletion (in execution thread)
+    // - quit the Qt event loop in the execution thread
+    connect(this, SIGNAL(stopExecutor()), executor, SLOT(deleteLater()));
+    connect(this, SIGNAL(stopExecutor()), thread, SLOT(quit()));
+    // Queue the thread for deletion (in this thread) when it is finished
+    connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
+
+    // Default implementation of QThread::run() simply spins up an event loop in the thread,
+    // which is what we want.
+    thread->start();
+}
+
+void RPCConsole::copyMessage()
+{
+    GUIUtil::copyEntryData(ui->messagesWidget, 1, Qt::EditRole);
+}
new file mode 100644
--- /dev/null
+++ b/src/qt/rpcconsole.h
@@ -0,0 +1,64 @@
+#ifndef RPCCONSOLE_H
+#define RPCCONSOLE_H
+
+#include <QDialog>
+
+namespace Ui {
+    class RPCConsole;
+}
+class ClientModel;
+
+/** Local bitcoin RPC console. */
+class RPCConsole: public QDialog
+{
+    Q_OBJECT
+
+public:
+    explicit RPCConsole(QWidget *parent = 0);
+    ~RPCConsole();
+
+    void setClientModel(ClientModel *model);
+
+    enum MessageClass {
+        MC_ERROR,
+        MC_DEBUG,
+        CMD_REQUEST,
+        CMD_REPLY,
+        CMD_ERROR
+    };
+
+protected:
+    virtual bool event(QEvent *event);
+    virtual bool eventFilter(QObject* obj, QEvent *event);
+
+private slots:
+    void on_lineEdit_returnPressed();
+
+public slots:
+    void clear();
+    void message(int category, const QString &message);
+    /** Set number of connections shown in the UI */
+    void setNumConnections(int count);
+    /** Set number of blocks shown in the UI */
+    void setNumBlocks(int count);
+    /** Go forward or back in history */
+    void browseHistory(int offset);
+    /** Copy currently selected message to clipboard */
+    void copyMessage();
+
+signals:
+    // For RPC command executor
+    void stopExecutor();
+    void cmdRequest(const QString &command);
+
+private:
+    Ui::RPCConsole *ui;
+    ClientModel *clientModel;
+    bool firstLayout;
+    QStringList history;
+    int historyPtr;
+
+    void startExecutor();
+};
+
+#endif // RPCCONSOLE_H