rpcconsole.cpp raw

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