/ src / qt / rpcconsole.cpp
rpcconsole.cpp
   1  // Copyright (c) 2011-2022 The Bitcoin Core developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
   4  
   5  #if defined(HAVE_CONFIG_H)
   6  #include <config/bitcoin-config.h>
   7  #endif
   8  
   9  #include <qt/rpcconsole.h>
  10  #include <qt/forms/ui_debugwindow.h>
  11  
  12  #include <chainparams.h>
  13  #include <common/system.h>
  14  #include <interfaces/node.h>
  15  #include <node/connection_types.h>
  16  #include <qt/bantablemodel.h>
  17  #include <qt/clientmodel.h>
  18  #include <qt/guiutil.h>
  19  #include <qt/peertablesortproxy.h>
  20  #include <qt/platformstyle.h>
  21  #include <qt/walletmodel.h>
  22  #include <rpc/client.h>
  23  #include <rpc/server.h>
  24  #include <util/strencodings.h>
  25  #include <util/string.h>
  26  #include <util/threadnames.h>
  27  
  28  #include <univalue.h>
  29  
  30  #include <QAbstractButton>
  31  #include <QAbstractItemModel>
  32  #include <QDateTime>
  33  #include <QFont>
  34  #include <QKeyEvent>
  35  #include <QKeySequence>
  36  #include <QLatin1String>
  37  #include <QLocale>
  38  #include <QMenu>
  39  #include <QMessageBox>
  40  #include <QScreen>
  41  #include <QScrollBar>
  42  #include <QSettings>
  43  #include <QString>
  44  #include <QStringList>
  45  #include <QStyledItemDelegate>
  46  #include <QTime>
  47  #include <QTimer>
  48  #include <QVariant>
  49  
  50  #include <chrono>
  51  
  52  const int CONSOLE_HISTORY = 50;
  53  const int INITIAL_TRAFFIC_GRAPH_MINS = 30;
  54  const QSize FONT_RANGE(4, 40);
  55  const char fontSizeSettingsKey[] = "consoleFontSize";
  56  
  57  const struct {
  58      const char *url;
  59      const char *source;
  60  } ICON_MAPPING[] = {
  61      {"cmd-request", ":/icons/tx_input"},
  62      {"cmd-reply", ":/icons/tx_output"},
  63      {"cmd-error", ":/icons/tx_output"},
  64      {"misc", ":/icons/tx_inout"},
  65      {nullptr, nullptr}
  66  };
  67  
  68  namespace {
  69  
  70  // don't add private key handling cmd's to the history
  71  const QStringList historyFilter = QStringList()
  72      << "importprivkey"
  73      << "importmulti"
  74      << "sethdseed"
  75      << "signmessagewithprivkey"
  76      << "signrawtransactionwithkey"
  77      << "walletpassphrase"
  78      << "walletpassphrasechange"
  79      << "encryptwallet";
  80  
  81  }
  82  
  83  /* Object for executing console RPC commands in a separate thread.
  84  */
  85  class RPCExecutor : public QObject
  86  {
  87      Q_OBJECT
  88  public:
  89      explicit RPCExecutor(interfaces::Node& node) : m_node(node) {}
  90  
  91  public Q_SLOTS:
  92      void request(const QString &command, const WalletModel* wallet_model);
  93  
  94  Q_SIGNALS:
  95      void reply(int category, const QString &command);
  96  
  97  private:
  98      interfaces::Node& m_node;
  99  };
 100  
 101  /** Class for handling RPC timers
 102   * (used for e.g. re-locking the wallet after a timeout)
 103   */
 104  class QtRPCTimerBase: public QObject, public RPCTimerBase
 105  {
 106      Q_OBJECT
 107  public:
 108      QtRPCTimerBase(std::function<void()>& _func, int64_t millis):
 109          func(_func)
 110      {
 111          timer.setSingleShot(true);
 112          connect(&timer, &QTimer::timeout, [this]{ func(); });
 113          timer.start(millis);
 114      }
 115      ~QtRPCTimerBase() = default;
 116  private:
 117      QTimer timer;
 118      std::function<void()> func;
 119  };
 120  
 121  class QtRPCTimerInterface: public RPCTimerInterface
 122  {
 123  public:
 124      ~QtRPCTimerInterface() = default;
 125      const char *Name() override { return "Qt"; }
 126      RPCTimerBase* NewTimer(std::function<void()>& func, int64_t millis) override
 127      {
 128          return new QtRPCTimerBase(func, millis);
 129      }
 130  };
 131  
 132  class PeerIdViewDelegate : public QStyledItemDelegate
 133  {
 134      Q_OBJECT
 135  public:
 136      explicit PeerIdViewDelegate(QObject* parent = nullptr)
 137          : QStyledItemDelegate(parent) {}
 138  
 139      QString displayText(const QVariant& value, const QLocale& locale) const override
 140      {
 141          // Additional spaces should visually separate right-aligned content
 142          // from the next column to the right.
 143          return value.toString() + QLatin1String("   ");
 144      }
 145  };
 146  
 147  #include <qt/rpcconsole.moc>
 148  
 149  /**
 150   * Split shell command line into a list of arguments and optionally execute the command(s).
 151   * Aims to emulate \c bash and friends.
 152   *
 153   * - Command nesting is possible with parenthesis; for example: validateaddress(getnewaddress())
 154   * - Arguments are delimited with whitespace or comma
 155   * - Extra whitespace at the beginning and end and between arguments will be ignored
 156   * - Text can be "double" or 'single' quoted
 157   * - The backslash \c \ is used as escape character
 158   *   - Outside quotes, any character can be escaped
 159   *   - Within double quotes, only escape \c " and backslashes before a \c " or another backslash
 160   *   - Within single quotes, no escaping is possible and no special interpretation takes place
 161   *
 162   * @param[in]    node    optional node to execute command on
 163   * @param[out]   strResult   stringified result from the executed command(chain)
 164   * @param[in]    strCommand  Command line to split
 165   * @param[in]    fExecute    set true if you want the command to be executed
 166   * @param[out]   pstrFilteredOut  Command line, filtered to remove any sensitive data
 167   */
 168  
 169  bool RPCConsole::RPCParseCommandLine(interfaces::Node* node, std::string &strResult, const std::string &strCommand, const bool fExecute, std::string * const pstrFilteredOut, const WalletModel* wallet_model)
 170  {
 171      std::vector< std::vector<std::string> > stack;
 172      stack.emplace_back();
 173  
 174      enum CmdParseState
 175      {
 176          STATE_EATING_SPACES,
 177          STATE_EATING_SPACES_IN_ARG,
 178          STATE_EATING_SPACES_IN_BRACKETS,
 179          STATE_ARGUMENT,
 180          STATE_SINGLEQUOTED,
 181          STATE_DOUBLEQUOTED,
 182          STATE_ESCAPE_OUTER,
 183          STATE_ESCAPE_DOUBLEQUOTED,
 184          STATE_COMMAND_EXECUTED,
 185          STATE_COMMAND_EXECUTED_INNER
 186      } state = STATE_EATING_SPACES;
 187      std::string curarg;
 188      UniValue lastResult;
 189      unsigned nDepthInsideSensitive = 0;
 190      size_t filter_begin_pos = 0, chpos;
 191      std::vector<std::pair<size_t, size_t>> filter_ranges;
 192  
 193      auto add_to_current_stack = [&](const std::string& strArg) {
 194          if (stack.back().empty() && (!nDepthInsideSensitive) && historyFilter.contains(QString::fromStdString(strArg), Qt::CaseInsensitive)) {
 195              nDepthInsideSensitive = 1;
 196              filter_begin_pos = chpos;
 197          }
 198          // Make sure stack is not empty before adding something
 199          if (stack.empty()) {
 200              stack.emplace_back();
 201          }
 202          stack.back().push_back(strArg);
 203      };
 204  
 205      auto close_out_params = [&]() {
 206          if (nDepthInsideSensitive) {
 207              if (!--nDepthInsideSensitive) {
 208                  assert(filter_begin_pos);
 209                  filter_ranges.emplace_back(filter_begin_pos, chpos);
 210                  filter_begin_pos = 0;
 211              }
 212          }
 213          stack.pop_back();
 214      };
 215  
 216      std::string strCommandTerminated = strCommand;
 217      if (strCommandTerminated.back() != '\n')
 218          strCommandTerminated += "\n";
 219      for (chpos = 0; chpos < strCommandTerminated.size(); ++chpos)
 220      {
 221          char ch = strCommandTerminated[chpos];
 222          switch(state)
 223          {
 224              case STATE_COMMAND_EXECUTED_INNER:
 225              case STATE_COMMAND_EXECUTED:
 226              {
 227                  bool breakParsing = true;
 228                  switch(ch)
 229                  {
 230                      case '[': curarg.clear(); state = STATE_COMMAND_EXECUTED_INNER; break;
 231                      default:
 232                          if (state == STATE_COMMAND_EXECUTED_INNER)
 233                          {
 234                              if (ch != ']')
 235                              {
 236                                  // append char to the current argument (which is also used for the query command)
 237                                  curarg += ch;
 238                                  break;
 239                              }
 240                              if (curarg.size() && fExecute)
 241                              {
 242                                  // if we have a value query, query arrays with index and objects with a string key
 243                                  UniValue subelement;
 244                                  if (lastResult.isArray())
 245                                  {
 246                                      const auto parsed{ToIntegral<size_t>(curarg)};
 247                                      if (!parsed) {
 248                                          throw std::runtime_error("Invalid result query");
 249                                      }
 250                                      subelement = lastResult[parsed.value()];
 251                                  }
 252                                  else if (lastResult.isObject())
 253                                      subelement = lastResult.find_value(curarg);
 254                                  else
 255                                      throw std::runtime_error("Invalid result query"); //no array or object: abort
 256                                  lastResult = subelement;
 257                              }
 258  
 259                              state = STATE_COMMAND_EXECUTED;
 260                              break;
 261                          }
 262                          // don't break parsing when the char is required for the next argument
 263                          breakParsing = false;
 264  
 265                          // pop the stack and return the result to the current command arguments
 266                          close_out_params();
 267  
 268                          // don't stringify the json in case of a string to avoid doublequotes
 269                          if (lastResult.isStr())
 270                              curarg = lastResult.get_str();
 271                          else
 272                              curarg = lastResult.write(2);
 273  
 274                          // if we have a non empty result, use it as stack argument otherwise as general result
 275                          if (curarg.size())
 276                          {
 277                              if (stack.size())
 278                                  add_to_current_stack(curarg);
 279                              else
 280                                  strResult = curarg;
 281                          }
 282                          curarg.clear();
 283                          // assume eating space state
 284                          state = STATE_EATING_SPACES;
 285                  }
 286                  if (breakParsing)
 287                      break;
 288                  [[fallthrough]];
 289              }
 290              case STATE_ARGUMENT: // In or after argument
 291              case STATE_EATING_SPACES_IN_ARG:
 292              case STATE_EATING_SPACES_IN_BRACKETS:
 293              case STATE_EATING_SPACES: // Handle runs of whitespace
 294                  switch(ch)
 295              {
 296                  case '"': state = STATE_DOUBLEQUOTED; break;
 297                  case '\'': state = STATE_SINGLEQUOTED; break;
 298                  case '\\': state = STATE_ESCAPE_OUTER; break;
 299                  case '(': case ')': case '\n':
 300                      if (state == STATE_EATING_SPACES_IN_ARG)
 301                          throw std::runtime_error("Invalid Syntax");
 302                      if (state == STATE_ARGUMENT)
 303                      {
 304                          if (ch == '(' && stack.size() && stack.back().size() > 0)
 305                          {
 306                              if (nDepthInsideSensitive) {
 307                                  ++nDepthInsideSensitive;
 308                              }
 309                              stack.emplace_back();
 310                          }
 311  
 312                          // don't allow commands after executed commands on baselevel
 313                          if (!stack.size())
 314                              throw std::runtime_error("Invalid Syntax");
 315  
 316                          add_to_current_stack(curarg);
 317                          curarg.clear();
 318                          state = STATE_EATING_SPACES_IN_BRACKETS;
 319                      }
 320                      if ((ch == ')' || ch == '\n') && stack.size() > 0)
 321                      {
 322                          if (fExecute) {
 323                              // Convert argument list to JSON objects in method-dependent way,
 324                              // and pass it along with the method name to the dispatcher.
 325                              UniValue params = RPCConvertValues(stack.back()[0], std::vector<std::string>(stack.back().begin() + 1, stack.back().end()));
 326                              std::string method = stack.back()[0];
 327                              std::string uri;
 328  #ifdef ENABLE_WALLET
 329                              if (wallet_model) {
 330                                  QByteArray encodedName = QUrl::toPercentEncoding(wallet_model->getWalletName());
 331                                  uri = "/wallet/"+std::string(encodedName.constData(), encodedName.length());
 332                              }
 333  #endif
 334                              assert(node);
 335                              lastResult = node->executeRpc(method, params, uri);
 336                          }
 337  
 338                          state = STATE_COMMAND_EXECUTED;
 339                          curarg.clear();
 340                      }
 341                      break;
 342                  case ' ': case ',': case '\t':
 343                      if(state == STATE_EATING_SPACES_IN_ARG && curarg.empty() && ch == ',')
 344                          throw std::runtime_error("Invalid Syntax");
 345  
 346                      else if(state == STATE_ARGUMENT) // Space ends argument
 347                      {
 348                          add_to_current_stack(curarg);
 349                          curarg.clear();
 350                      }
 351                      if ((state == STATE_EATING_SPACES_IN_BRACKETS || state == STATE_ARGUMENT) && ch == ',')
 352                      {
 353                          state = STATE_EATING_SPACES_IN_ARG;
 354                          break;
 355                      }
 356                      state = STATE_EATING_SPACES;
 357                      break;
 358                  default: curarg += ch; state = STATE_ARGUMENT;
 359              }
 360                  break;
 361              case STATE_SINGLEQUOTED: // Single-quoted string
 362                  switch(ch)
 363              {
 364                  case '\'': state = STATE_ARGUMENT; break;
 365                  default: curarg += ch;
 366              }
 367                  break;
 368              case STATE_DOUBLEQUOTED: // Double-quoted string
 369                  switch(ch)
 370              {
 371                  case '"': state = STATE_ARGUMENT; break;
 372                  case '\\': state = STATE_ESCAPE_DOUBLEQUOTED; break;
 373                  default: curarg += ch;
 374              }
 375                  break;
 376              case STATE_ESCAPE_OUTER: // '\' outside quotes
 377                  curarg += ch; state = STATE_ARGUMENT;
 378                  break;
 379              case STATE_ESCAPE_DOUBLEQUOTED: // '\' in double-quoted text
 380                  if(ch != '"' && ch != '\\') curarg += '\\'; // keep '\' for everything but the quote and '\' itself
 381                  curarg += ch; state = STATE_DOUBLEQUOTED;
 382                  break;
 383          }
 384      }
 385      if (pstrFilteredOut) {
 386          if (STATE_COMMAND_EXECUTED == state) {
 387              assert(!stack.empty());
 388              close_out_params();
 389          }
 390          *pstrFilteredOut = strCommand;
 391          for (auto i = filter_ranges.rbegin(); i != filter_ranges.rend(); ++i) {
 392              pstrFilteredOut->replace(i->first, i->second - i->first, "(…)");
 393          }
 394      }
 395      switch(state) // final state
 396      {
 397          case STATE_COMMAND_EXECUTED:
 398              if (lastResult.isStr())
 399                  strResult = lastResult.get_str();
 400              else
 401                  strResult = lastResult.write(2);
 402              [[fallthrough]];
 403          case STATE_ARGUMENT:
 404          case STATE_EATING_SPACES:
 405              return true;
 406          default: // ERROR to end in one of the other states
 407              return false;
 408      }
 409  }
 410  
 411  void RPCExecutor::request(const QString &command, const WalletModel* wallet_model)
 412  {
 413      try
 414      {
 415          std::string result;
 416          std::string executableCommand = command.toStdString() + "\n";
 417  
 418          // Catch the console-only-help command before RPC call is executed and reply with help text as-if a RPC reply.
 419          if(executableCommand == "help-console\n") {
 420              Q_EMIT reply(RPCConsole::CMD_REPLY, QString(("\n"
 421                  "This console accepts RPC commands using the standard syntax.\n"
 422                  "   example:    getblockhash 0\n\n"
 423  
 424                  "This console can also accept RPC commands using the parenthesized syntax.\n"
 425                  "   example:    getblockhash(0)\n\n"
 426  
 427                  "Commands may be nested when specified with the parenthesized syntax.\n"
 428                  "   example:    getblock(getblockhash(0) 1)\n\n"
 429  
 430                  "A space or a comma can be used to delimit arguments for either syntax.\n"
 431                  "   example:    getblockhash 0\n"
 432                  "               getblockhash,0\n\n"
 433  
 434                  "Named results can be queried with a non-quoted key string in brackets using the parenthesized syntax.\n"
 435                  "   example:    getblock(getblockhash(0) 1)[tx]\n\n"
 436  
 437                  "Results without keys can be queried with an integer in brackets using the parenthesized syntax.\n"
 438                  "   example:    getblock(getblockhash(0),1)[tx][0]\n\n")));
 439              return;
 440          }
 441          if (!RPCConsole::RPCExecuteCommandLine(m_node, result, executableCommand, nullptr, wallet_model)) {
 442              Q_EMIT reply(RPCConsole::CMD_ERROR, QString("Parse error: unbalanced ' or \""));
 443              return;
 444          }
 445  
 446          Q_EMIT reply(RPCConsole::CMD_REPLY, QString::fromStdString(result));
 447      }
 448      catch (UniValue& objError)
 449      {
 450          try // Nice formatting for standard-format error
 451          {
 452              int code = objError.find_value("code").getInt<int>();
 453              std::string message = objError.find_value("message").get_str();
 454              Q_EMIT reply(RPCConsole::CMD_ERROR, QString::fromStdString(message) + " (code " + QString::number(code) + ")");
 455          }
 456          catch (const std::runtime_error&) // raised when converting to invalid type, i.e. missing code or message
 457          {   // Show raw JSON object
 458              Q_EMIT reply(RPCConsole::CMD_ERROR, QString::fromStdString(objError.write()));
 459          }
 460      }
 461      catch (const std::exception& e)
 462      {
 463          Q_EMIT reply(RPCConsole::CMD_ERROR, QString("Error: ") + QString::fromStdString(e.what()));
 464      }
 465  }
 466  
 467  RPCConsole::RPCConsole(interfaces::Node& node, const PlatformStyle *_platformStyle, QWidget *parent) :
 468      QWidget(parent),
 469      m_node(node),
 470      ui(new Ui::RPCConsole),
 471      platformStyle(_platformStyle)
 472  {
 473      ui->setupUi(this);
 474      QSettings settings;
 475  #ifdef ENABLE_WALLET
 476      if (WalletModel::isWalletEnabled()) {
 477          // RPCConsole widget is a window.
 478          if (!restoreGeometry(settings.value("RPCConsoleWindowGeometry").toByteArray())) {
 479              // Restore failed (perhaps missing setting), center the window
 480              move(QGuiApplication::primaryScreen()->availableGeometry().center() - frameGeometry().center());
 481          }
 482          ui->splitter->restoreState(settings.value("RPCConsoleWindowPeersTabSplitterSizes").toByteArray());
 483      } else
 484  #endif // ENABLE_WALLET
 485      {
 486          // RPCConsole is a child widget.
 487          ui->splitter->restoreState(settings.value("RPCConsoleWidgetPeersTabSplitterSizes").toByteArray());
 488      }
 489  
 490      m_peer_widget_header_state = settings.value("PeersTabPeerHeaderState").toByteArray();
 491      m_banlist_widget_header_state = settings.value("PeersTabBanlistHeaderState").toByteArray();
 492  
 493      constexpr QChar nonbreaking_hyphen(8209);
 494      const std::vector<QString> CONNECTION_TYPE_DOC{
 495          //: Explanatory text for an inbound peer connection.
 496          tr("Inbound: initiated by peer"),
 497          /*: Explanatory text for an outbound peer connection that
 498              relays all network information. This is the default behavior for
 499              outbound connections. */
 500          tr("Outbound Full Relay: default"),
 501          /*: Explanatory text for an outbound peer connection that relays
 502              network information about blocks and not transactions or addresses. */
 503          tr("Outbound Block Relay: does not relay transactions or addresses"),
 504          /*: Explanatory text for an outbound peer connection that was
 505              established manually through one of several methods. The numbered
 506              arguments are stand-ins for the methods available to establish
 507              manual connections. */
 508          tr("Outbound Manual: added using RPC %1 or %2/%3 configuration options")
 509              .arg("addnode")
 510              .arg(QString(nonbreaking_hyphen) + "addnode")
 511              .arg(QString(nonbreaking_hyphen) + "connect"),
 512          /*: Explanatory text for a short-lived outbound peer connection that
 513              is used to test the aliveness of known addresses. */
 514          tr("Outbound Feeler: short-lived, for testing addresses"),
 515          /*: Explanatory text for a short-lived outbound peer connection that is used
 516              to request addresses from a peer. */
 517          tr("Outbound Address Fetch: short-lived, for soliciting addresses")};
 518      const QString connection_types_list{"<ul><li>" + Join(CONNECTION_TYPE_DOC, QString("</li><li>")) + "</li></ul>"};
 519      ui->peerConnectionTypeLabel->setToolTip(ui->peerConnectionTypeLabel->toolTip().arg(connection_types_list));
 520      const std::vector<QString> TRANSPORT_TYPE_DOC{
 521          //: Explanatory text for "detecting" transport type.
 522          tr("detecting: peer could be v1 or v2"),
 523          //: Explanatory text for v1 transport type.
 524          tr("v1: unencrypted, plaintext transport protocol"),
 525          //: Explanatory text for v2 transport type.
 526          tr("v2: BIP324 encrypted transport protocol")};
 527      const QString transport_types_list{"<ul><li>" + Join(TRANSPORT_TYPE_DOC, QString("</li><li>")) + "</li></ul>"};
 528      ui->peerTransportTypeLabel->setToolTip(ui->peerTransportTypeLabel->toolTip().arg(transport_types_list));
 529      const QString hb_list{"<ul><li>\""
 530          + ts.to + "\" – " + tr("we selected the peer for high bandwidth relay") + "</li><li>\""
 531          + ts.from + "\" – " + tr("the peer selected us for high bandwidth relay") + "</li><li>\""
 532          + ts.no + "\" – " + tr("no high bandwidth relay selected") + "</li></ul>"};
 533      ui->peerHighBandwidthLabel->setToolTip(ui->peerHighBandwidthLabel->toolTip().arg(hb_list));
 534      ui->dataDir->setToolTip(ui->dataDir->toolTip().arg(QString(nonbreaking_hyphen) + "datadir"));
 535      ui->blocksDir->setToolTip(ui->blocksDir->toolTip().arg(QString(nonbreaking_hyphen) + "blocksdir"));
 536      ui->openDebugLogfileButton->setToolTip(ui->openDebugLogfileButton->toolTip().arg(PACKAGE_NAME));
 537  
 538      if (platformStyle->getImagesOnButtons()) {
 539          ui->openDebugLogfileButton->setIcon(platformStyle->SingleColorIcon(":/icons/export"));
 540      }
 541      ui->clearButton->setIcon(platformStyle->SingleColorIcon(":/icons/remove"));
 542  
 543      ui->fontBiggerButton->setIcon(platformStyle->SingleColorIcon(":/icons/fontbigger"));
 544      //: Main shortcut to increase the RPC console font size.
 545      ui->fontBiggerButton->setShortcut(tr("Ctrl++"));
 546      //: Secondary shortcut to increase the RPC console font size.
 547      GUIUtil::AddButtonShortcut(ui->fontBiggerButton, tr("Ctrl+="));
 548  
 549      ui->fontSmallerButton->setIcon(platformStyle->SingleColorIcon(":/icons/fontsmaller"));
 550      //: Main shortcut to decrease the RPC console font size.
 551      ui->fontSmallerButton->setShortcut(tr("Ctrl+-"));
 552      //: Secondary shortcut to decrease the RPC console font size.
 553      GUIUtil::AddButtonShortcut(ui->fontSmallerButton, tr("Ctrl+_"));
 554  
 555      ui->promptIcon->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/prompticon")));
 556  
 557      // Install event filter for up and down arrow
 558      ui->lineEdit->installEventFilter(this);
 559      ui->lineEdit->setMaxLength(16 * 1024 * 1024);
 560      ui->messagesWidget->installEventFilter(this);
 561  
 562      connect(ui->clearButton, &QAbstractButton::clicked, [this] { clear(); });
 563      connect(ui->fontBiggerButton, &QAbstractButton::clicked, this, &RPCConsole::fontBigger);
 564      connect(ui->fontSmallerButton, &QAbstractButton::clicked, this, &RPCConsole::fontSmaller);
 565      connect(ui->btnClearTrafficGraph, &QPushButton::clicked, ui->trafficGraph, &TrafficGraphWidget::clear);
 566  
 567      // disable the wallet selector by default
 568      ui->WalletSelector->setVisible(false);
 569      ui->WalletSelectorLabel->setVisible(false);
 570  
 571      // Register RPC timer interface
 572      rpcTimerInterface = new QtRPCTimerInterface();
 573      // avoid accidentally overwriting an existing, non QTThread
 574      // based timer interface
 575      m_node.rpcSetTimerInterfaceIfUnset(rpcTimerInterface);
 576  
 577      setTrafficGraphRange(INITIAL_TRAFFIC_GRAPH_MINS);
 578      updateDetailWidget();
 579  
 580      consoleFontSize = settings.value(fontSizeSettingsKey, QFont().pointSize()).toInt();
 581      clear();
 582  
 583      GUIUtil::handleCloseWindowShortcut(this);
 584  
 585      updateWindowTitle();
 586  }
 587  
 588  RPCConsole::~RPCConsole()
 589  {
 590      QSettings settings;
 591  #ifdef ENABLE_WALLET
 592      if (WalletModel::isWalletEnabled()) {
 593          // RPCConsole widget is a window.
 594          settings.setValue("RPCConsoleWindowGeometry", saveGeometry());
 595          settings.setValue("RPCConsoleWindowPeersTabSplitterSizes", ui->splitter->saveState());
 596      } else
 597  #endif // ENABLE_WALLET
 598      {
 599          // RPCConsole is a child widget.
 600          settings.setValue("RPCConsoleWidgetPeersTabSplitterSizes", ui->splitter->saveState());
 601      }
 602  
 603      settings.setValue("PeersTabPeerHeaderState", m_peer_widget_header_state);
 604      settings.setValue("PeersTabBanlistHeaderState", m_banlist_widget_header_state);
 605  
 606      m_node.rpcUnsetTimerInterface(rpcTimerInterface);
 607      delete rpcTimerInterface;
 608      delete ui;
 609  }
 610  
 611  bool RPCConsole::eventFilter(QObject* obj, QEvent *event)
 612  {
 613      if(event->type() == QEvent::KeyPress) // Special key handling
 614      {
 615          QKeyEvent *keyevt = static_cast<QKeyEvent*>(event);
 616          int key = keyevt->key();
 617          Qt::KeyboardModifiers mod = keyevt->modifiers();
 618          switch(key)
 619          {
 620          case Qt::Key_Up: if(obj == ui->lineEdit) { browseHistory(-1); return true; } break;
 621          case Qt::Key_Down: if(obj == ui->lineEdit) { browseHistory(1); return true; } break;
 622          case Qt::Key_PageUp: /* pass paging keys to messages widget */
 623          case Qt::Key_PageDown:
 624              if (obj == ui->lineEdit) {
 625                  QApplication::sendEvent(ui->messagesWidget, keyevt);
 626                  return true;
 627              }
 628              break;
 629          case Qt::Key_Return:
 630          case Qt::Key_Enter:
 631              // forward these events to lineEdit
 632              if (obj == autoCompleter->popup()) {
 633                  QApplication::sendEvent(ui->lineEdit, keyevt);
 634                  autoCompleter->popup()->hide();
 635                  return true;
 636              }
 637              break;
 638          default:
 639              // Typing in messages widget brings focus to line edit, and redirects key there
 640              // Exclude most combinations and keys that emit no text, except paste shortcuts
 641              if(obj == ui->messagesWidget && (
 642                    (!mod && !keyevt->text().isEmpty() && key != Qt::Key_Tab) ||
 643                    ((mod & Qt::ControlModifier) && key == Qt::Key_V) ||
 644                    ((mod & Qt::ShiftModifier) && key == Qt::Key_Insert)))
 645              {
 646                  ui->lineEdit->setFocus();
 647                  QApplication::sendEvent(ui->lineEdit, keyevt);
 648                  return true;
 649              }
 650          }
 651      }
 652      return QWidget::eventFilter(obj, event);
 653  }
 654  
 655  void RPCConsole::setClientModel(ClientModel *model, int bestblock_height, int64_t bestblock_date, double verification_progress)
 656  {
 657      clientModel = model;
 658  
 659      bool wallet_enabled{false};
 660  #ifdef ENABLE_WALLET
 661      wallet_enabled = WalletModel::isWalletEnabled();
 662  #endif // ENABLE_WALLET
 663      if (model && !wallet_enabled) {
 664          // Show warning, for example if this is a prerelease version
 665          connect(model, &ClientModel::alertsChanged, this, &RPCConsole::updateAlerts);
 666          updateAlerts(model->getStatusBarWarnings());
 667      }
 668  
 669      ui->trafficGraph->setClientModel(model);
 670      if (model && clientModel->getPeerTableModel() && clientModel->getBanTableModel()) {
 671          // Keep up to date with client
 672          setNumConnections(model->getNumConnections());
 673          connect(model, &ClientModel::numConnectionsChanged, this, &RPCConsole::setNumConnections);
 674  
 675          setNumBlocks(bestblock_height, QDateTime::fromSecsSinceEpoch(bestblock_date), verification_progress, SyncType::BLOCK_SYNC);
 676          connect(model, &ClientModel::numBlocksChanged, this, &RPCConsole::setNumBlocks);
 677  
 678          updateNetworkState();
 679          connect(model, &ClientModel::networkActiveChanged, this, &RPCConsole::setNetworkActive);
 680  
 681          interfaces::Node& node = clientModel->node();
 682          updateTrafficStats(node.getTotalBytesRecv(), node.getTotalBytesSent());
 683          connect(model, &ClientModel::bytesChanged, this, &RPCConsole::updateTrafficStats);
 684  
 685          connect(model, &ClientModel::mempoolSizeChanged, this, &RPCConsole::setMempoolSize);
 686  
 687          // set up peer table
 688          ui->peerWidget->setModel(model->peerTableSortProxy());
 689          ui->peerWidget->verticalHeader()->hide();
 690          ui->peerWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
 691          ui->peerWidget->setSelectionMode(QAbstractItemView::ExtendedSelection);
 692          ui->peerWidget->setContextMenuPolicy(Qt::CustomContextMenu);
 693  
 694          if (!ui->peerWidget->horizontalHeader()->restoreState(m_peer_widget_header_state)) {
 695              ui->peerWidget->setColumnWidth(PeerTableModel::Address, ADDRESS_COLUMN_WIDTH);
 696              ui->peerWidget->setColumnWidth(PeerTableModel::Subversion, SUBVERSION_COLUMN_WIDTH);
 697              ui->peerWidget->setColumnWidth(PeerTableModel::Ping, PING_COLUMN_WIDTH);
 698          }
 699          ui->peerWidget->horizontalHeader()->setSectionResizeMode(PeerTableModel::Age, QHeaderView::ResizeToContents);
 700          ui->peerWidget->horizontalHeader()->setStretchLastSection(true);
 701          ui->peerWidget->setItemDelegateForColumn(PeerTableModel::NetNodeId, new PeerIdViewDelegate(this));
 702  
 703          // create peer table context menu
 704          peersTableContextMenu = new QMenu(this);
 705          //: Context menu action to copy the address of a peer.
 706          peersTableContextMenu->addAction(tr("&Copy address"), [this] {
 707              GUIUtil::copyEntryData(ui->peerWidget, PeerTableModel::Address, Qt::DisplayRole);
 708          });
 709          peersTableContextMenu->addSeparator();
 710          peersTableContextMenu->addAction(tr("&Disconnect"), this, &RPCConsole::disconnectSelectedNode);
 711          peersTableContextMenu->addAction(ts.ban_for + " " + tr("1 &hour"), [this] { banSelectedNode(60 * 60); });
 712          peersTableContextMenu->addAction(ts.ban_for + " " + tr("1 d&ay"), [this] { banSelectedNode(60 * 60 * 24); });
 713          peersTableContextMenu->addAction(ts.ban_for + " " + tr("1 &week"), [this] { banSelectedNode(60 * 60 * 24 * 7); });
 714          peersTableContextMenu->addAction(ts.ban_for + " " + tr("1 &year"), [this] { banSelectedNode(60 * 60 * 24 * 365); });
 715          connect(ui->peerWidget, &QTableView::customContextMenuRequested, this, &RPCConsole::showPeersTableContextMenu);
 716  
 717          // peer table signal handling - update peer details when selecting new node
 718          connect(ui->peerWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &RPCConsole::updateDetailWidget);
 719          connect(model->getPeerTableModel(), &QAbstractItemModel::dataChanged, [this] { updateDetailWidget(); });
 720  
 721          // set up ban table
 722          ui->banlistWidget->setModel(model->getBanTableModel());
 723          ui->banlistWidget->verticalHeader()->hide();
 724          ui->banlistWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
 725          ui->banlistWidget->setSelectionMode(QAbstractItemView::SingleSelection);
 726          ui->banlistWidget->setContextMenuPolicy(Qt::CustomContextMenu);
 727  
 728          if (!ui->banlistWidget->horizontalHeader()->restoreState(m_banlist_widget_header_state)) {
 729              ui->banlistWidget->setColumnWidth(BanTableModel::Address, BANSUBNET_COLUMN_WIDTH);
 730              ui->banlistWidget->setColumnWidth(BanTableModel::Bantime, BANTIME_COLUMN_WIDTH);
 731          }
 732          ui->banlistWidget->horizontalHeader()->setSectionResizeMode(BanTableModel::Address, QHeaderView::ResizeToContents);
 733          ui->banlistWidget->horizontalHeader()->setStretchLastSection(true);
 734  
 735          // create ban table context menu
 736          banTableContextMenu = new QMenu(this);
 737          /*: Context menu action to copy the IP/Netmask of a banned peer.
 738              IP/Netmask is the combination of a peer's IP address and its Netmask.
 739              For IP address, see: https://en.wikipedia.org/wiki/IP_address. */
 740          banTableContextMenu->addAction(tr("&Copy IP/Netmask"), [this] {
 741              GUIUtil::copyEntryData(ui->banlistWidget, BanTableModel::Address, Qt::DisplayRole);
 742          });
 743          banTableContextMenu->addSeparator();
 744          banTableContextMenu->addAction(tr("&Unban"), this, &RPCConsole::unbanSelectedNode);
 745          connect(ui->banlistWidget, &QTableView::customContextMenuRequested, this, &RPCConsole::showBanTableContextMenu);
 746  
 747          // ban table signal handling - clear peer details when clicking a peer in the ban table
 748          connect(ui->banlistWidget, &QTableView::clicked, this, &RPCConsole::clearSelectedNode);
 749          // ban table signal handling - ensure ban table is shown or hidden (if empty)
 750          connect(model->getBanTableModel(), &BanTableModel::layoutChanged, this, &RPCConsole::showOrHideBanTableIfRequired);
 751          showOrHideBanTableIfRequired();
 752  
 753          // Provide initial values
 754          ui->clientVersion->setText(model->formatFullVersion());
 755          ui->clientUserAgent->setText(model->formatSubVersion());
 756          ui->dataDir->setText(model->dataDir());
 757          ui->blocksDir->setText(model->blocksDir());
 758          ui->startupTime->setText(model->formatClientStartupTime());
 759          ui->networkName->setText(QString::fromStdString(Params().GetChainTypeString()));
 760  
 761          //Setup autocomplete and attach it
 762          QStringList wordList;
 763          std::vector<std::string> commandList = m_node.listRpcCommands();
 764          for (size_t i = 0; i < commandList.size(); ++i)
 765          {
 766              wordList << commandList[i].c_str();
 767              wordList << ("help " + commandList[i]).c_str();
 768          }
 769  
 770          wordList << "help-console";
 771          wordList.sort();
 772          autoCompleter = new QCompleter(wordList, this);
 773          autoCompleter->setModelSorting(QCompleter::CaseSensitivelySortedModel);
 774          // ui->lineEdit is initially disabled because running commands is only
 775          // possible from now on.
 776          ui->lineEdit->setEnabled(true);
 777          ui->lineEdit->setCompleter(autoCompleter);
 778          autoCompleter->popup()->installEventFilter(this);
 779          // Start thread to execute RPC commands.
 780          startExecutor();
 781      }
 782      if (!model) {
 783          // Client model is being set to 0, this means shutdown() is about to be called.
 784          thread.quit();
 785          thread.wait();
 786      }
 787  }
 788  
 789  #ifdef ENABLE_WALLET
 790  void RPCConsole::addWallet(WalletModel * const walletModel)
 791  {
 792      // use name for text and wallet model for internal data object (to allow to move to a wallet id later)
 793      ui->WalletSelector->addItem(walletModel->getDisplayName(), QVariant::fromValue(walletModel));
 794      if (ui->WalletSelector->count() == 2) {
 795          // First wallet added, set to default to match wallet RPC behavior
 796          ui->WalletSelector->setCurrentIndex(1);
 797      }
 798      if (ui->WalletSelector->count() > 2) {
 799          ui->WalletSelector->setVisible(true);
 800          ui->WalletSelectorLabel->setVisible(true);
 801      }
 802  }
 803  
 804  void RPCConsole::removeWallet(WalletModel * const walletModel)
 805  {
 806      ui->WalletSelector->removeItem(ui->WalletSelector->findData(QVariant::fromValue(walletModel)));
 807      if (ui->WalletSelector->count() == 2) {
 808          ui->WalletSelector->setVisible(false);
 809          ui->WalletSelectorLabel->setVisible(false);
 810      }
 811  }
 812  
 813  void RPCConsole::setCurrentWallet(WalletModel* const wallet_model)
 814  {
 815      QVariant data = QVariant::fromValue(wallet_model);
 816      ui->WalletSelector->setCurrentIndex(ui->WalletSelector->findData(data));
 817  }
 818  #endif
 819  
 820  static QString categoryClass(int category)
 821  {
 822      switch(category)
 823      {
 824      case RPCConsole::CMD_REQUEST:  return "cmd-request"; break;
 825      case RPCConsole::CMD_REPLY:    return "cmd-reply"; break;
 826      case RPCConsole::CMD_ERROR:    return "cmd-error"; break;
 827      default:                       return "misc";
 828      }
 829  }
 830  
 831  void RPCConsole::fontBigger()
 832  {
 833      setFontSize(consoleFontSize+1);
 834  }
 835  
 836  void RPCConsole::fontSmaller()
 837  {
 838      setFontSize(consoleFontSize-1);
 839  }
 840  
 841  void RPCConsole::setFontSize(int newSize)
 842  {
 843      QSettings settings;
 844  
 845      //don't allow an insane font size
 846      if (newSize < FONT_RANGE.width() || newSize > FONT_RANGE.height())
 847          return;
 848  
 849      // temp. store the console content
 850      QString str = ui->messagesWidget->toHtml();
 851  
 852      // replace font tags size in current content
 853      str.replace(QString("font-size:%1pt").arg(consoleFontSize), QString("font-size:%1pt").arg(newSize));
 854  
 855      // store the new font size
 856      consoleFontSize = newSize;
 857      settings.setValue(fontSizeSettingsKey, consoleFontSize);
 858  
 859      // clear console (reset icon sizes, default stylesheet) and re-add the content
 860      float oldPosFactor = 1.0 / ui->messagesWidget->verticalScrollBar()->maximum() * ui->messagesWidget->verticalScrollBar()->value();
 861      clear(/*keep_prompt=*/true);
 862      ui->messagesWidget->setHtml(str);
 863      ui->messagesWidget->verticalScrollBar()->setValue(oldPosFactor * ui->messagesWidget->verticalScrollBar()->maximum());
 864  }
 865  
 866  void RPCConsole::clear(bool keep_prompt)
 867  {
 868      ui->messagesWidget->clear();
 869      if (!keep_prompt) ui->lineEdit->clear();
 870      ui->lineEdit->setFocus();
 871  
 872      // Add smoothly scaled icon images.
 873      // (when using width/height on an img, Qt uses nearest instead of linear interpolation)
 874      for(int i=0; ICON_MAPPING[i].url; ++i)
 875      {
 876          ui->messagesWidget->document()->addResource(
 877                      QTextDocument::ImageResource,
 878                      QUrl(ICON_MAPPING[i].url),
 879                      platformStyle->SingleColorImage(ICON_MAPPING[i].source).scaled(QSize(consoleFontSize*2, consoleFontSize*2), Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
 880      }
 881  
 882      // Set default style sheet
 883  #ifdef Q_OS_MACOS
 884      QFontInfo fixedFontInfo(GUIUtil::fixedPitchFont(/*use_embedded_font=*/true));
 885  #else
 886      QFontInfo fixedFontInfo(GUIUtil::fixedPitchFont());
 887  #endif
 888      ui->messagesWidget->document()->setDefaultStyleSheet(
 889          QString(
 890                  "table { }"
 891                  "td.time { color: #808080; font-size: %2; padding-top: 3px; } "
 892                  "td.message { font-family: %1; font-size: %2; white-space:pre-wrap; } "
 893                  "td.cmd-request { color: #006060; } "
 894                  "td.cmd-error { color: red; } "
 895                  ".secwarning { color: red; }"
 896                  "b { color: #006060; } "
 897              ).arg(fixedFontInfo.family(), QString("%1pt").arg(consoleFontSize))
 898          );
 899  
 900      static const QString welcome_message =
 901          /*: RPC console welcome message.
 902              Placeholders %7 and %8 are style tags for the warning content, and
 903              they are not space separated from the rest of the text intentionally. */
 904          tr("Welcome to the %1 RPC console.\n"
 905             "Use up and down arrows to navigate history, and %2 to clear screen.\n"
 906             "Use %3 and %4 to increase or decrease the font size.\n"
 907             "Type %5 for an overview of available commands.\n"
 908             "For more information on using this console, type %6.\n"
 909             "\n"
 910             "%7WARNING: Scammers have been active, telling users to type"
 911             " commands here, stealing their wallet contents. Do not use this console"
 912             " without fully understanding the ramifications of a command.%8")
 913              .arg(PACKAGE_NAME,
 914                   "<b>" + ui->clearButton->shortcut().toString(QKeySequence::NativeText) + "</b>",
 915                   "<b>" + ui->fontBiggerButton->shortcut().toString(QKeySequence::NativeText) + "</b>",
 916                   "<b>" + ui->fontSmallerButton->shortcut().toString(QKeySequence::NativeText) + "</b>",
 917                   "<b>help</b>",
 918                   "<b>help-console</b>",
 919                   "<span class=\"secwarning\">",
 920                   "<span>");
 921  
 922      message(CMD_REPLY, welcome_message, true);
 923  }
 924  
 925  void RPCConsole::keyPressEvent(QKeyEvent *event)
 926  {
 927      if (windowType() != Qt::Widget && GUIUtil::IsEscapeOrBack(event->key())) {
 928          close();
 929      }
 930  }
 931  
 932  void RPCConsole::changeEvent(QEvent* e)
 933  {
 934      if (e->type() == QEvent::PaletteChange) {
 935          ui->clearButton->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/remove")));
 936          ui->fontBiggerButton->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/fontbigger")));
 937          ui->fontSmallerButton->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/fontsmaller")));
 938          ui->promptIcon->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/prompticon")));
 939  
 940          for (int i = 0; ICON_MAPPING[i].url; ++i) {
 941              ui->messagesWidget->document()->addResource(
 942                  QTextDocument::ImageResource,
 943                  QUrl(ICON_MAPPING[i].url),
 944                  platformStyle->SingleColorImage(ICON_MAPPING[i].source).scaled(QSize(consoleFontSize * 2, consoleFontSize * 2), Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
 945          }
 946      }
 947  
 948      QWidget::changeEvent(e);
 949  }
 950  
 951  void RPCConsole::message(int category, const QString &message, bool html)
 952  {
 953      QTime time = QTime::currentTime();
 954      QString timeString = time.toString();
 955      QString out;
 956      out += "<table><tr><td class=\"time\" width=\"65\">" + timeString + "</td>";
 957      out += "<td class=\"icon\" width=\"32\"><img src=\"" + categoryClass(category) + "\"></td>";
 958      out += "<td class=\"message " + categoryClass(category) + "\" valign=\"middle\">";
 959      if(html)
 960          out += message;
 961      else
 962          out += GUIUtil::HtmlEscape(message, false);
 963      out += "</td></tr></table>";
 964      ui->messagesWidget->append(out);
 965  }
 966  
 967  void RPCConsole::updateNetworkState()
 968  {
 969      if (!clientModel) return;
 970      QString connections = QString::number(clientModel->getNumConnections()) + " (";
 971      connections += tr("In:") + " " + QString::number(clientModel->getNumConnections(CONNECTIONS_IN)) + " / ";
 972      connections += tr("Out:") + " " + QString::number(clientModel->getNumConnections(CONNECTIONS_OUT)) + ")";
 973  
 974      if(!clientModel->node().getNetworkActive()) {
 975          connections += " (" + tr("Network activity disabled") + ")";
 976      }
 977  
 978      ui->numberOfConnections->setText(connections);
 979  }
 980  
 981  void RPCConsole::setNumConnections(int count)
 982  {
 983      if (!clientModel)
 984          return;
 985  
 986      updateNetworkState();
 987  }
 988  
 989  void RPCConsole::setNetworkActive(bool networkActive)
 990  {
 991      updateNetworkState();
 992  }
 993  
 994  void RPCConsole::setNumBlocks(int count, const QDateTime& blockDate, double nVerificationProgress, SyncType synctype)
 995  {
 996      if (synctype == SyncType::BLOCK_SYNC) {
 997          ui->numberOfBlocks->setText(QString::number(count));
 998          ui->lastBlockTime->setText(blockDate.toString());
 999      }
1000  }
1001  
1002  void RPCConsole::setMempoolSize(long numberOfTxs, size_t dynUsage)
1003  {
1004      ui->mempoolNumberTxs->setText(QString::number(numberOfTxs));
1005  
1006      if (dynUsage < 1000000) {
1007          ui->mempoolSize->setText(QObject::tr("%1 kB").arg(dynUsage / 1000.0, 0, 'f', 2));
1008      } else {
1009          ui->mempoolSize->setText(QObject::tr("%1 MB").arg(dynUsage / 1000000.0, 0, 'f', 2));
1010      }
1011  }
1012  
1013  void RPCConsole::on_lineEdit_returnPressed()
1014  {
1015      QString cmd = ui->lineEdit->text().trimmed();
1016  
1017      if (cmd.isEmpty()) {
1018          return;
1019      }
1020  
1021      std::string strFilteredCmd;
1022      try {
1023          std::string dummy;
1024          if (!RPCParseCommandLine(nullptr, dummy, cmd.toStdString(), false, &strFilteredCmd)) {
1025              // Failed to parse command, so we cannot even filter it for the history
1026              throw std::runtime_error("Invalid command line");
1027          }
1028      } catch (const std::exception& e) {
1029          QMessageBox::critical(this, "Error", QString("Error: ") + QString::fromStdString(e.what()));
1030          return;
1031      }
1032  
1033      // A special case allows to request shutdown even a long-running command is executed.
1034      if (cmd == QLatin1String("stop")) {
1035          std::string dummy;
1036          RPCExecuteCommandLine(m_node, dummy, cmd.toStdString());
1037          return;
1038      }
1039  
1040      if (m_is_executing) {
1041          return;
1042      }
1043  
1044      ui->lineEdit->clear();
1045  
1046      WalletModel* wallet_model{nullptr};
1047  #ifdef ENABLE_WALLET
1048      wallet_model = ui->WalletSelector->currentData().value<WalletModel*>();
1049  
1050      if (m_last_wallet_model != wallet_model) {
1051          if (wallet_model) {
1052              message(CMD_REQUEST, tr("Executing command using \"%1\" wallet").arg(wallet_model->getWalletName()));
1053          } else {
1054              message(CMD_REQUEST, tr("Executing command without any wallet"));
1055          }
1056          m_last_wallet_model = wallet_model;
1057      }
1058  #endif // ENABLE_WALLET
1059  
1060      message(CMD_REQUEST, QString::fromStdString(strFilteredCmd));
1061      //: A console message indicating an entered command is currently being executed.
1062      message(CMD_REPLY, tr("Executing…"));
1063      m_is_executing = true;
1064  
1065      QMetaObject::invokeMethod(m_executor, [this, cmd, wallet_model] {
1066          m_executor->request(cmd, wallet_model);
1067      });
1068  
1069      cmd = QString::fromStdString(strFilteredCmd);
1070  
1071      // Remove command, if already in history
1072      history.removeOne(cmd);
1073      // Append command to history
1074      history.append(cmd);
1075      // Enforce maximum history size
1076      while (history.size() > CONSOLE_HISTORY) {
1077          history.removeFirst();
1078      }
1079      // Set pointer to end of history
1080      historyPtr = history.size();
1081  
1082      // Scroll console view to end
1083      scrollToEnd();
1084  }
1085  
1086  void RPCConsole::browseHistory(int offset)
1087  {
1088      // store current text when start browsing through the history
1089      if (historyPtr == history.size()) {
1090          cmdBeforeBrowsing = ui->lineEdit->text();
1091      }
1092  
1093      historyPtr += offset;
1094      if(historyPtr < 0)
1095          historyPtr = 0;
1096      if(historyPtr > history.size())
1097          historyPtr = history.size();
1098      QString cmd;
1099      if(historyPtr < history.size())
1100          cmd = history.at(historyPtr);
1101      else if (!cmdBeforeBrowsing.isNull()) {
1102          cmd = cmdBeforeBrowsing;
1103      }
1104      ui->lineEdit->setText(cmd);
1105  }
1106  
1107  void RPCConsole::startExecutor()
1108  {
1109      m_executor = new RPCExecutor(m_node);
1110      m_executor->moveToThread(&thread);
1111  
1112      // Replies from executor object must go to this object
1113      connect(m_executor, &RPCExecutor::reply, this, [this](int category, const QString& command) {
1114          // Remove "Executing…" message.
1115          ui->messagesWidget->undo();
1116          message(category, command);
1117          scrollToEnd();
1118          m_is_executing = false;
1119      });
1120  
1121      // Make sure executor object is deleted in its own thread
1122      connect(&thread, &QThread::finished, m_executor, &RPCExecutor::deleteLater);
1123  
1124      // Default implementation of QThread::run() simply spins up an event loop in the thread,
1125      // which is what we want.
1126      thread.start();
1127      QTimer::singleShot(0, m_executor, []() {
1128          util::ThreadRename("qt-rpcconsole");
1129      });
1130  }
1131  
1132  void RPCConsole::on_tabWidget_currentChanged(int index)
1133  {
1134      if (ui->tabWidget->widget(index) == ui->tab_console) {
1135          ui->lineEdit->setFocus();
1136      }
1137  }
1138  
1139  void RPCConsole::on_openDebugLogfileButton_clicked()
1140  {
1141      GUIUtil::openDebugLogfile();
1142  }
1143  
1144  void RPCConsole::scrollToEnd()
1145  {
1146      QScrollBar *scrollbar = ui->messagesWidget->verticalScrollBar();
1147      scrollbar->setValue(scrollbar->maximum());
1148  }
1149  
1150  void RPCConsole::on_sldGraphRange_valueChanged(int value)
1151  {
1152      const int multiplier = 5; // each position on the slider represents 5 min
1153      int mins = value * multiplier;
1154      setTrafficGraphRange(mins);
1155  }
1156  
1157  void RPCConsole::setTrafficGraphRange(int mins)
1158  {
1159      ui->trafficGraph->setGraphRange(std::chrono::minutes{mins});
1160      ui->lblGraphRange->setText(GUIUtil::formatDurationStr(std::chrono::minutes{mins}));
1161  }
1162  
1163  void RPCConsole::updateTrafficStats(quint64 totalBytesIn, quint64 totalBytesOut)
1164  {
1165      ui->lblBytesIn->setText(GUIUtil::formatBytes(totalBytesIn));
1166      ui->lblBytesOut->setText(GUIUtil::formatBytes(totalBytesOut));
1167  }
1168  
1169  void RPCConsole::updateDetailWidget()
1170  {
1171      const QList<QModelIndex> selected_peers = GUIUtil::getEntryData(ui->peerWidget, PeerTableModel::NetNodeId);
1172      if (!clientModel || !clientModel->getPeerTableModel() || selected_peers.size() != 1) {
1173          ui->peersTabRightPanel->hide();
1174          ui->peerHeading->setText(tr("Select a peer to view detailed information."));
1175          return;
1176      }
1177      const auto stats = selected_peers.first().data(PeerTableModel::StatsRole).value<CNodeCombinedStats*>();
1178      // update the detail ui with latest node information
1179      QString peerAddrDetails(QString::fromStdString(stats->nodeStats.m_addr_name) + " ");
1180      peerAddrDetails += tr("(peer: %1)").arg(QString::number(stats->nodeStats.nodeid));
1181      if (!stats->nodeStats.addrLocal.empty())
1182          peerAddrDetails += "<br />" + tr("via %1").arg(QString::fromStdString(stats->nodeStats.addrLocal));
1183      ui->peerHeading->setText(peerAddrDetails);
1184      QString bip152_hb_settings;
1185      if (stats->nodeStats.m_bip152_highbandwidth_to) bip152_hb_settings = ts.to;
1186      if (stats->nodeStats.m_bip152_highbandwidth_from) bip152_hb_settings += (bip152_hb_settings.isEmpty() ? ts.from : QLatin1Char('/') + ts.from);
1187      if (bip152_hb_settings.isEmpty()) bip152_hb_settings = ts.no;
1188      ui->peerHighBandwidth->setText(bip152_hb_settings);
1189      const auto time_now{GetTime<std::chrono::seconds>()};
1190      ui->peerConnTime->setText(GUIUtil::formatDurationStr(time_now - stats->nodeStats.m_connected));
1191      ui->peerLastBlock->setText(TimeDurationField(time_now, stats->nodeStats.m_last_block_time));
1192      ui->peerLastTx->setText(TimeDurationField(time_now, stats->nodeStats.m_last_tx_time));
1193      ui->peerLastSend->setText(TimeDurationField(time_now, stats->nodeStats.m_last_send));
1194      ui->peerLastRecv->setText(TimeDurationField(time_now, stats->nodeStats.m_last_recv));
1195      ui->peerBytesSent->setText(GUIUtil::formatBytes(stats->nodeStats.nSendBytes));
1196      ui->peerBytesRecv->setText(GUIUtil::formatBytes(stats->nodeStats.nRecvBytes));
1197      ui->peerPingTime->setText(GUIUtil::formatPingTime(stats->nodeStats.m_last_ping_time));
1198      ui->peerMinPing->setText(GUIUtil::formatPingTime(stats->nodeStats.m_min_ping_time));
1199      ui->timeoffset->setText(GUIUtil::formatTimeOffset(stats->nodeStats.nTimeOffset));
1200      if (stats->nodeStats.nVersion) {
1201          ui->peerVersion->setText(QString::number(stats->nodeStats.nVersion));
1202      }
1203      if (!stats->nodeStats.cleanSubVer.empty()) {
1204          ui->peerSubversion->setText(QString::fromStdString(stats->nodeStats.cleanSubVer));
1205      }
1206      ui->peerConnectionType->setText(GUIUtil::ConnectionTypeToQString(stats->nodeStats.m_conn_type, /*prepend_direction=*/true));
1207      ui->peerTransportType->setText(QString::fromStdString(TransportTypeAsString(stats->nodeStats.m_transport_type)));
1208      if (stats->nodeStats.m_transport_type == TransportProtocolType::V2) {
1209          ui->peerSessionIdLabel->setVisible(true);
1210          ui->peerSessionId->setVisible(true);
1211          ui->peerSessionId->setText(QString::fromStdString(stats->nodeStats.m_session_id));
1212      } else {
1213          ui->peerSessionIdLabel->setVisible(false);
1214          ui->peerSessionId->setVisible(false);
1215      }
1216      ui->peerNetwork->setText(GUIUtil::NetworkToQString(stats->nodeStats.m_network));
1217      if (stats->nodeStats.m_permission_flags == NetPermissionFlags::None) {
1218          ui->peerPermissions->setText(ts.na);
1219      } else {
1220          QStringList permissions;
1221          for (const auto& permission : NetPermissions::ToStrings(stats->nodeStats.m_permission_flags)) {
1222              permissions.append(QString::fromStdString(permission));
1223          }
1224          ui->peerPermissions->setText(permissions.join(" & "));
1225      }
1226      ui->peerMappedAS->setText(stats->nodeStats.m_mapped_as != 0 ? QString::number(stats->nodeStats.m_mapped_as) : ts.na);
1227  
1228      // This check fails for example if the lock was busy and
1229      // nodeStateStats couldn't be fetched.
1230      if (stats->fNodeStateStatsAvailable) {
1231          ui->peerServices->setText(GUIUtil::formatServicesStr(stats->nodeStateStats.their_services));
1232          // Sync height is init to -1
1233          if (stats->nodeStateStats.nSyncHeight > -1) {
1234              ui->peerSyncHeight->setText(QString("%1").arg(stats->nodeStateStats.nSyncHeight));
1235          } else {
1236              ui->peerSyncHeight->setText(ts.unknown);
1237          }
1238          // Common height is init to -1
1239          if (stats->nodeStateStats.nCommonHeight > -1) {
1240              ui->peerCommonHeight->setText(QString("%1").arg(stats->nodeStateStats.nCommonHeight));
1241          } else {
1242              ui->peerCommonHeight->setText(ts.unknown);
1243          }
1244          ui->peerHeight->setText(QString::number(stats->nodeStateStats.m_starting_height));
1245          ui->peerPingWait->setText(GUIUtil::formatPingTime(stats->nodeStateStats.m_ping_wait));
1246          ui->peerAddrRelayEnabled->setText(stats->nodeStateStats.m_addr_relay_enabled ? ts.yes : ts.no);
1247          ui->peerAddrProcessed->setText(QString::number(stats->nodeStateStats.m_addr_processed));
1248          ui->peerAddrRateLimited->setText(QString::number(stats->nodeStateStats.m_addr_rate_limited));
1249          ui->peerRelayTxes->setText(stats->nodeStateStats.m_relay_txs ? ts.yes : ts.no);
1250      }
1251  
1252      ui->peersTabRightPanel->show();
1253  }
1254  
1255  void RPCConsole::resizeEvent(QResizeEvent *event)
1256  {
1257      QWidget::resizeEvent(event);
1258  }
1259  
1260  void RPCConsole::showEvent(QShowEvent *event)
1261  {
1262      QWidget::showEvent(event);
1263  
1264      if (!clientModel || !clientModel->getPeerTableModel())
1265          return;
1266  
1267      // start PeerTableModel auto refresh
1268      clientModel->getPeerTableModel()->startAutoRefresh();
1269  }
1270  
1271  void RPCConsole::hideEvent(QHideEvent *event)
1272  {
1273      // It is too late to call QHeaderView::saveState() in ~RPCConsole(), as all of
1274      // the columns of QTableView child widgets will have zero width at that moment.
1275      m_peer_widget_header_state = ui->peerWidget->horizontalHeader()->saveState();
1276      m_banlist_widget_header_state = ui->banlistWidget->horizontalHeader()->saveState();
1277  
1278      QWidget::hideEvent(event);
1279  
1280      if (!clientModel || !clientModel->getPeerTableModel())
1281          return;
1282  
1283      // stop PeerTableModel auto refresh
1284      clientModel->getPeerTableModel()->stopAutoRefresh();
1285  }
1286  
1287  void RPCConsole::showPeersTableContextMenu(const QPoint& point)
1288  {
1289      QModelIndex index = ui->peerWidget->indexAt(point);
1290      if (index.isValid())
1291          peersTableContextMenu->exec(QCursor::pos());
1292  }
1293  
1294  void RPCConsole::showBanTableContextMenu(const QPoint& point)
1295  {
1296      QModelIndex index = ui->banlistWidget->indexAt(point);
1297      if (index.isValid())
1298          banTableContextMenu->exec(QCursor::pos());
1299  }
1300  
1301  void RPCConsole::disconnectSelectedNode()
1302  {
1303      // Get selected peer addresses
1304      QList<QModelIndex> nodes = GUIUtil::getEntryData(ui->peerWidget, PeerTableModel::NetNodeId);
1305      for(int i = 0; i < nodes.count(); i++)
1306      {
1307          // Get currently selected peer address
1308          NodeId id = nodes.at(i).data().toLongLong();
1309          // Find the node, disconnect it and clear the selected node
1310          if(m_node.disconnectById(id))
1311              clearSelectedNode();
1312      }
1313  }
1314  
1315  void RPCConsole::banSelectedNode(int bantime)
1316  {
1317      if (!clientModel)
1318          return;
1319  
1320      for (const QModelIndex& peer : GUIUtil::getEntryData(ui->peerWidget, PeerTableModel::NetNodeId)) {
1321          // Find possible nodes, ban it and clear the selected node
1322          const auto stats = peer.data(PeerTableModel::StatsRole).value<CNodeCombinedStats*>();
1323          if (stats) {
1324              m_node.ban(stats->nodeStats.addr, bantime);
1325              m_node.disconnectByAddress(stats->nodeStats.addr);
1326          }
1327      }
1328      clearSelectedNode();
1329      clientModel->getBanTableModel()->refresh();
1330  }
1331  
1332  void RPCConsole::unbanSelectedNode()
1333  {
1334      if (!clientModel)
1335          return;
1336  
1337      // Get selected ban addresses
1338      QList<QModelIndex> nodes = GUIUtil::getEntryData(ui->banlistWidget, BanTableModel::Address);
1339      BanTableModel* ban_table_model{clientModel->getBanTableModel()};
1340      bool unbanned{false};
1341      for (const auto& node_index : nodes) {
1342          unbanned |= ban_table_model->unban(node_index);
1343      }
1344      if (unbanned) {
1345          ban_table_model->refresh();
1346      }
1347  }
1348  
1349  void RPCConsole::clearSelectedNode()
1350  {
1351      ui->peerWidget->selectionModel()->clearSelection();
1352      cachedNodeids.clear();
1353      updateDetailWidget();
1354  }
1355  
1356  void RPCConsole::showOrHideBanTableIfRequired()
1357  {
1358      if (!clientModel)
1359          return;
1360  
1361      bool visible = clientModel->getBanTableModel()->shouldShow();
1362      ui->banlistWidget->setVisible(visible);
1363      ui->banHeading->setVisible(visible);
1364  }
1365  
1366  void RPCConsole::setTabFocus(enum TabTypes tabType)
1367  {
1368      ui->tabWidget->setCurrentIndex(int(tabType));
1369  }
1370  
1371  QString RPCConsole::tabTitle(TabTypes tab_type) const
1372  {
1373      return ui->tabWidget->tabText(int(tab_type));
1374  }
1375  
1376  QKeySequence RPCConsole::tabShortcut(TabTypes tab_type) const
1377  {
1378      switch (tab_type) {
1379      case TabTypes::INFO: return QKeySequence(tr("Ctrl+I"));
1380      case TabTypes::CONSOLE: return QKeySequence(tr("Ctrl+T"));
1381      case TabTypes::GRAPH: return QKeySequence(tr("Ctrl+N"));
1382      case TabTypes::PEERS: return QKeySequence(tr("Ctrl+P"));
1383      } // no default case, so the compiler can warn about missing cases
1384  
1385      assert(false);
1386  }
1387  
1388  void RPCConsole::updateAlerts(const QString& warnings)
1389  {
1390      this->ui->label_alerts->setVisible(!warnings.isEmpty());
1391      this->ui->label_alerts->setText(warnings);
1392  }
1393  
1394  void RPCConsole::updateWindowTitle()
1395  {
1396      const ChainType chain = Params().GetChainType();
1397      if (chain == ChainType::MAIN) return;
1398  
1399      const QString chainType = QString::fromStdString(Params().GetChainTypeString());
1400      const QString title = tr("Node window - [%1]").arg(chainType);
1401      this->setWindowTitle(title);
1402  }