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