rpcconsole.cpp raw
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 }
1380