netwatch.cpp raw
1 // Copyright (c) 2017-2021 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/netwatch.h>
8
9 #include <qt/limenkaunits.h>
10 #include <qt/clientmodel.h>
11 #include <qt/guiconstants.h>
12 #include <qt/guiutil.h>
13 #include <qt/networkstyle.h>
14 #include <qt/optionsmodel.h>
15 #include <qt/platformstyle.h>
16
17 #include <chain.h>
18 #include <interfaces/node.h>
19 #include <key_io.h>
20 #include <node/blockstorage.h>
21 #include <node/context.h>
22 #include <primitives/transaction.h>
23 #include <pubkey.h>
24 #include <sync.h>
25 #include <util/time.h>
26 #include <validation.h>
27 #include <validationinterface.h>
28
29 #include <algorithm>
30 #include <memory>
31 #include <type_traits>
32
33 #include <QAbstractTableModel>
34 #include <QHBoxLayout>
35 #include <QLineEdit>
36 #include <QPushButton>
37 #include <QRegularExpression>
38 #include <QScrollBar>
39 #include <QVBoxLayout>
40 #include <QWidget>
41
42 namespace {
43
44 bool IsDatacarrier(const CTxOut& txout)
45 {
46 return (txout.scriptPubKey[0] == OP_RETURN && txout.nValue == 0);
47 }
48
49 size_t CountNonDatacarrierOutputs(const CTransactionRef& tx)
50 {
51 size_t count = 0;
52 for (const auto& txout : tx->vout) {
53 if (IsDatacarrier(txout)) continue;
54 ++count;
55 }
56 return count;
57 }
58
59 const CTxOut* GetNonDatacarrierOutput(const CTransactionRef& tx, const size_t txout_index)
60 {
61 size_t count = 0;
62 for (auto& txout : tx->vout) {
63 if (IsDatacarrier(txout)) continue;
64 if (count == txout_index) {
65 return &txout;
66 }
67 ++count;
68 }
69 return nullptr;
70 }
71
72 } // namespace
73
74 QString LogEntry::LogEntryTypeAbbreviation(const Type log_entry_type)
75 {
76 switch (log_entry_type) {
77 case Type::Block: return QObject::tr("Blk", "Tx Watch: Block type abbreviation");
78 case Type::Transaction: return QObject::tr("Txn", "Tx Watch: Transaction type abbreviation");
79 } // no default case, so the compiler can warn about missing cases
80
81 assert(false);
82 }
83
84 void LogEntry::init(const LogEntry& other)
85 {
86 if (!other.m_data) {
87 m_data = nullptr;
88 return;
89 }
90 const meta_t n = *(meta_t*)other.m_data;
91 const int32_t relTimestamp = n & rel_ts_mask;
92 switch (n >> 30) {
93 case 1: // CBlockIndex*
94 init(relTimestamp, other.getBlockIndex());
95 break;
96 case 2: // CTransactionRef
97 {
98 CTransactionWeakref ptx(*other.get<CTransactionRef>());
99 init(relTimestamp, ptx, false);
100 break;
101 }
102 case 3: // CTransactionWeakref
103 init(relTimestamp, *other.get<CTransactionWeakref>(), true);
104 break;
105 default: assert(false);
106 }
107 }
108
109 LogEntry::LogEntry(const LogEntry& other)
110 {
111 init(other);
112 }
113
114 void LogEntry::init(int32_t relTimestamp, const CBlockIndex& blockindex)
115 {
116 relTimestamp &= rel_ts_mask;
117 size_t alignment;
118 const size_t sz = data_sizeof<CBlockIndex*>(alignment);
119 m_data = (uint8_t*)::operator new(sz);
120 *((meta_t*)m_data) = relTimestamp | (1 << 30);
121 *((const CBlockIndex**)&m_data[alignment]) = &blockindex;
122 }
123
124 LogEntry::LogEntry(int32_t relTimestamp, const CBlockIndex& blockindex)
125 {
126 init(relTimestamp, blockindex);
127 }
128
129 void LogEntry::init(int32_t relTimestamp, const CTransactionWeakref& tx, bool weak)
130 {
131 relTimestamp &= rel_ts_mask;
132
133 // Allocate enough space for either, so we can convert between them
134 size_t alignment_shared, alignment_weak;
135 const size_t sz = std::max(data_sizeof<CTransactionRef>(alignment_shared), data_sizeof<CTransactionWeakref>(alignment_weak));
136 m_data = (uint8_t*)::operator new(sz);
137
138 uint32_t type;
139 if (weak) {
140 //std::allocator<std::weak_ptr<CTransaction>>::construct(m_data + alignment_weak, tx);
141 new (m_data + alignment_weak) CTransactionWeakref(tx);
142 type = 3;
143 } else {
144 new (m_data + alignment_shared) CTransactionRef(tx.lock());
145 type = 2;
146 }
147 *((meta_t*)m_data) = relTimestamp | (type << 30);
148 }
149
150 LogEntry::LogEntry(int32_t relTimestamp, const CTransactionWeakref& tx, bool weak)
151 {
152 init(relTimestamp, tx, weak);
153 }
154
155 LogEntry::LogEntry(int32_t relTimestamp, const CTransactionRef& tx, bool weak)
156 {
157 CTransactionWeakref ptx(tx);
158 init(relTimestamp, ptx, weak);
159 }
160
161 void LogEntry::clear()
162 {
163 if (!m_data) {
164 return;
165 }
166 const meta_t n = *(meta_t*)m_data;
167 switch (n >> 30) {
168 case 1: // CBlockIndex*
169 break;
170 case 2: // CTransactionRef
171 get<CTransactionRef>()->~CTransactionRef();
172 break;
173 case 3: // CTransactionWeakref
174 get<CTransactionWeakref>()->~CTransactionWeakref();
175 break;
176 default: assert(false);
177 }
178 delete m_data;
179 }
180
181 LogEntry::~LogEntry()
182 {
183 clear();
184 }
185
186 LogEntry& LogEntry::operator=(const LogEntry& other)
187 {
188 if (this != &other) {
189 clear();
190 init(other);
191 }
192 return *this;
193 }
194
195 LogEntry::operator bool() const
196 {
197 return m_data;
198 }
199
200 int32_t LogEntry::getRelTimestamp() const
201 {
202 const meta_t n = *(meta_t*)m_data;
203 return n & rel_ts_mask;
204 }
205
206 uint64_t LogEntry::getTimestamp(uint64_t now) const
207 {
208 uint64_t ts = (now & ~rel_ts_mask64) | getRelTimestamp();
209 if (ts > now) {
210 ts -= (rel_ts_mask64 + 1);
211 }
212 return ts;
213 }
214
215 LogEntry::Type LogEntry::getType() const
216 {
217 const meta_t n = *(meta_t*)m_data;
218 return (n >> 31) ? Type::Transaction : Type::Block;
219 }
220
221 const CBlockIndex& LogEntry::getBlockIndex() const
222 {
223 return **(get<const CBlockIndex*>());
224 }
225
226 CTransactionRef LogEntry::getTransactionRef() const
227 {
228 const meta_t n = *(meta_t*)m_data;
229 if ((n >> 30) & 1) {
230 return get<CTransactionWeakref>()->lock();
231 } else {
232 return *get<CTransactionRef>();
233 }
234 }
235
236 bool LogEntry::isWeak() const
237 {
238 const meta_t n = *(meta_t*)m_data;
239 return ((n >> 30) == 3);
240 }
241
242 bool LogEntry::expired() const
243 {
244 if (isWeak()) {
245 return get<CTransactionWeakref>()->expired();
246 }
247 return false;
248 }
249
250 void LogEntry::makeWeak()
251 {
252 const meta_t n = *(meta_t*)m_data;
253 if ((n >> 30) != 2) {
254 return;
255 }
256 CTransactionRef * const ptx_old = get<CTransactionRef>();
257 CTransactionRef tx = *ptx_old; // save a copy
258 ptx_old->~CTransactionRef();
259 CTransactionWeakref * const ptx_new = get<CTransactionWeakref>();
260 new (ptx_new) CTransactionWeakref(tx);
261 *((meta_t*)m_data) |= (3 << 30);
262 }
263
264 class NetWatchValidationInterface final : public CValidationInterface {
265 private:
266 NetWatchLogModel& model;
267
268 public:
269 explicit NetWatchValidationInterface(NetWatchLogModel& model_in) : model(model_in) {}
270 void ValidationInterfaceUnregistering() override;
271
272 void BlockConnected(ChainstateRole role, const std::shared_ptr<const CBlock>& block, const CBlockIndex* pindex) override;
273 void TransactionAddedToMempool(const NewMempoolTransactionInfo&, uint64_t mempool_sequence) override;
274 };
275
276 void NetWatchValidationInterface::ValidationInterfaceUnregistering()
277 {
278 model.OrphanedValidationInterface();
279 }
280
281 void NetWatchValidationInterface::BlockConnected(ChainstateRole role, const std::shared_ptr<const CBlock>& block, const CBlockIndex* pindex)
282 {
283 model.LogBlock(pindex, block);
284 }
285
286 void NetWatchValidationInterface::TransactionAddedToMempool(const NewMempoolTransactionInfo& txinfo, uint64_t mempool_sequence)
287 {
288 model.LogTransaction(txinfo.info.m_tx);
289 }
290
291 NetWatchLogModel::NetWatchLogModel(QWidget *parent) :
292 QAbstractTableModel(parent),
293 m_widget(parent)
294 {
295 }
296
297 NetWatchLogModel::~NetWatchLogModel()
298 {
299 LOCK(cs);
300 if (m_validation_interface) {
301 Assert(m_client_model && m_client_model->node().context()->validation_signals);
302 m_client_model->node().context()->validation_signals->UnregisterValidationInterface(m_validation_interface);
303 delete m_validation_interface;
304 m_validation_interface = nullptr;
305 }
306 }
307
308 void NetWatchLogModel::OrphanedValidationInterface()
309 {
310 LOCK(cs);
311 delete m_validation_interface;
312 m_validation_interface = nullptr;
313 }
314
315 int NetWatchLogModel::rowCount(const QModelIndex& parent) const
316 {
317 LOCK(cs);
318 return m_log.size() - m_logskip;
319 }
320
321 int NetWatchLogModel::columnCount(const QModelIndex& parent) const
322 {
323 return HeaderCount;
324 }
325
326 QVariant NetWatchLogModel::data(const CBlockIndex& blockindex, int txout_index, const Header header) const
327 {
328 switch (header) {
329 case Header::Type:
330 return LogEntry::LogEntryTypeAbbreviation(LogEntry::Type::Block);
331 case Header::Id:
332 return QString::fromStdString(blockindex.GetBlockHash().GetHex());
333 case Header::Address:
334 case Header::Value: {
335 if (blockindex.nTx == 0 || !(WITH_LOCK(::cs_main, return blockindex.nStatus) & BLOCK_HAVE_DATA)) {
336 return QVariant();
337 }
338 CBlock block;
339 Assert(m_client_model && m_client_model->node().context());
340 if (!m_client_model->node().context()->chainman->m_blockman.ReadBlock(block, blockindex)) {
341 // Indicate error somehow?
342 return QVariant();
343 }
344 assert(block.vtx.size());
345 return data(block.vtx[0], txout_index, header);
346 }
347 case Header::Time: // Not valid here
348 assert(false);
349 }
350 return QVariant();
351 }
352
353 QVariant NetWatchLogModel::data(const CTransactionRef& tx, int txout_index, const Header header) const
354 {
355 switch (header) {
356 case Header::Type:
357 return LogEntry::LogEntryTypeAbbreviation(LogEntry::Type::Transaction);
358 case Header::Id:
359 return QString::fromStdString(tx->GetHash().GetHex());
360 case Header::Address: {
361 const CTxOut *ptxout = GetNonDatacarrierOutput(tx, txout_index);
362 if (!ptxout) {
363 // Only datacarriers
364 ptxout = &tx->vout[0];
365 }
366 CTxDestination txdest;
367 if (ptxout->scriptPubKey[0] == OP_RETURN && ptxout->nValue > 0) {
368 return tr("(Burn)", "Tx Watch: Provably burned value in transaction");
369 } else if (!ExtractDestination(ptxout->scriptPubKey, txdest)) {
370 return tr("(Unknown)", "Tx Watch: Unknown transaction output type");
371 }
372 return QString::fromStdString(EncodeDestination(txdest));
373 }
374 case Header::Value: {
375 const CTxOut *ptxout = GetNonDatacarrierOutput(tx, txout_index);
376 if (!ptxout) {
377 ptxout = &tx->vout[0];
378 }
379 if (m_client_model) {
380 return LimenkaUnits::format(m_client_model->getOptionsModel()->getDisplayUnit(), ptxout->nValue);
381 } else {
382 return qlonglong(ptxout->nValue);
383 }
384 }
385 case Header::Time: // Not valid here
386 assert(false);
387 }
388 return QVariant();
389 }
390
391 const LogEntry& NetWatchLogModel::getLogEntryRow(int row) const
392 {
393 AssertLockHeld(cs);
394 size_t pos = (m_logpos + row) % m_log.size();
395 return m_log[pos];
396 }
397
398 LogEntry& NetWatchLogModel::getLogEntryRow(int row)
399 {
400 AssertLockHeld(cs);
401 size_t pos = (m_logpos + row) % m_log.size();
402 return m_log[pos];
403 }
404
405 bool NetWatchLogModel::isLogRowContinuation(int row) const
406 {
407 AssertLockHeld(cs);
408 return !getLogEntryRow(row);
409 }
410
411 const LogEntry& NetWatchLogModel::findLogEntry(int row, int& out_entry_row) const
412 {
413 AssertLockHeld(cs);
414 out_entry_row = 0;
415 while (row && isLogRowContinuation(row)) {
416 --row;
417 ++out_entry_row;
418 }
419 return getLogEntryRow(row);
420 }
421
422 // NOLINTNEXTLINE(misc-no-recursion)
423 QVariant NetWatchLogModel::data(const QModelIndex& index, int role) const
424 {
425 const Header header = Header(index.column());
426 switch (role) {
427 case Qt::DisplayRole:
428 break;
429 case Qt::BackgroundRole: {
430 if (!data(index, Qt::DisplayRole).isValid()) {
431 return m_widget->palette().brush(QPalette::WindowText);
432 }
433 LogEntry::Type type;
434 {
435 int entry_row;
436 LOCK(cs);
437 const LogEntry& le = findLogEntry(index.row(), entry_row);
438 type = le.getType();
439 }
440 if (type == LogEntry::Type::Block) {
441 return m_widget->palette().brush(QPalette::AlternateBase);
442 }
443 return QVariant();
444 }
445 case Qt::ForegroundRole: {
446 if (index.column() < 3) {
447 bool iscont;
448 {
449 LOCK(cs);
450 iscont = isLogRowContinuation(index.row());
451 }
452 if (iscont) {
453 QBrush brush = m_widget->palette().brush(QPalette::WindowText);
454 QColor color = brush.color();
455 color.setAlpha(color.alpha() / 2);
456 brush.setColor(color);
457 return brush;
458 }
459 }
460 return QVariant();
461 }
462 case Qt::TextAlignmentRole:
463 if (header == Header::Value) {
464 return QVariant(Qt::AlignRight | Qt::AlignVCenter);
465 }
466 return QVariant();
467 case Qt::FontRole:
468 if (header == Header::Id) {
469 return GUIUtil::fixedPitchFont();
470 }
471 if (header == Header::Value) {
472 const auto display_unit = m_client_model->getOptionsModel()->getDisplayUnit();
473 return m_client_model->getOptionsModel()->getFontForMoney(display_unit);
474 }
475 return QVariant();
476 default:
477 return QVariant();
478 }
479 int entry_row;
480 LOCK(cs);
481 const LogEntry& le = findLogEntry(index.row(), entry_row);
482 if (header == Header::Time) {
483 return GUIUtil::dateTimeStr(le.getTimestamp(GetTime()));
484 }
485 const LogEntry::Type type = le.getType();
486 if (type == LogEntry::Type::Block) {
487 return data(le.getBlockIndex(), entry_row, header);
488 }
489 if (header == Header::Type) {
490 return LogEntry::LogEntryTypeAbbreviation(LogEntry::Type::Transaction);
491 }
492
493 if (le.expired()) {
494 return QVariant();
495 }
496 return data(le.getTransactionRef(), entry_row, header);
497 }
498
499 QVariant NetWatchLogModel::headerData(int section, Qt::Orientation orientation, int role) const
500 {
501 if (orientation != Qt::Horizontal || role != Qt::DisplayRole) {
502 return QVariant();
503 }
504 switch (Header(section)) {
505 case Header::Time: return tr("Time" , "NetWatch: Time header");
506 case Header::Type: return tr("Type" , "NetWatch: Type header");
507 case Header::Id: return tr("Id" , "NetWatch: Block hash / Txid header");
508 case Header::Address: return tr("Address", "NetWatch: Address header");
509 case Header::Value:
510 if (m_client_model) {
511 return LimenkaUnits::getAmountColumnTitle(m_client_model->getOptionsModel()->getDisplayUnit());
512 } else {
513 // Used only for sizing of the column
514 return LimenkaUnits::getAmountColumnTitle(LimenkaUnits::Unit::mBTC);
515 }
516 }
517 return QVariant();
518 }
519
520 NetWatchLogSearch::NetWatchLogSearch(const QString& query, LimenkaUnit display_unit) :
521 m_query(query)
522 {
523 const QRegularExpression reHex("^[\\da-f]+$", QRegularExpression::CaseInsensitiveOption);
524 const QRegularExpression reType("^(T(xn?)?|B(lk?)?)$", QRegularExpression::CaseInsensitiveOption);
525
526 m_check_type = m_query.length() < 4 && reType.match(m_query).hasMatch();
527 m_check_id = m_query.length() <= 64 && reHex.match(m_query).hasMatch();
528 m_check_addr = m_query.length() <= LONGEST_BECH32_ADDRESS;
529 CAmount val;
530 m_check_value = LimenkaUnits::parse(display_unit, m_query, &val) && val >= 0 && val <= LimenkaUnits::maxMoney();
531 }
532
533 bool NetWatchLogSearch::match(const NetWatchLogModel& model, int row) const
534 {
535 if (model.data(model.index(row, int(NetWatchLogModel::Header::Time))).toString().contains(m_query)) {
536 return true;
537 } else if (m_check_type && model.data(model.index(row, int(NetWatchLogModel::Header::Type))).toString().contains(m_query, Qt::CaseInsensitive)) {
538 return true;
539 } else if (m_check_id && model.data(model.index(row, int(NetWatchLogModel::Header::Id))).toString().contains(m_query, Qt::CaseInsensitive)) {
540 return true;
541 } else if (m_check_addr && model.data(model.index(row, int(NetWatchLogModel::Header::Address))).toString().contains(m_query, Qt::CaseInsensitive)) {
542 return true;
543 } else if (m_check_value && model.data(model.index(row, int(NetWatchLogModel::Header::Value))).toString().contains(m_query)) {
544 return true;
545 }
546 return false;
547 }
548
549 void NetWatchLogModel::searchRows(const QString& query, QList<int>& results)
550 {
551 const auto currentUnit = m_client_model->getOptionsModel()->getDisplayUnit();
552 NetWatchLogSearch *newsearch = new NetWatchLogSearch(query, currentUnit);
553 LOCK(cs);
554 delete m_current_search;
555 m_current_search = newsearch;
556
557 bool fAdding = false;
558 const size_t rows_used = rowCount();
559 for (size_t row = 0; row < rows_used; ++row) {
560 if (isLogRowContinuation(row) || newsearch->m_check_addr || newsearch->m_check_value) {
561 // Check for a match
562 fAdding = newsearch->match(*this, row);
563 }
564 if (fAdding) {
565 results.append(row);
566 }
567 }
568 }
569
570 void NetWatchLogModel::searchDisable() {
571 LOCK(cs);
572 delete m_current_search;
573 m_current_search = nullptr;
574 }
575
576 void NetWatchLogModel::log_append(const LogEntry& le, size_t& rows_used)
577 {
578 AssertLockHeld(cs);
579 if (m_log.size() < logsizelimit) {
580 // Haven't filled up yet, so just push_back
581 // Ensure push_back will append the current circular buffer, not go in the middle somewhere
582 // NOTE: m_logpos and m_logskip can be non-zero here, when further outputs will be overwriting
583 assert(m_logpos == m_logskip);
584 m_log.push_back(le);
585 } else {
586 // Replace a deleted row
587 assert(m_logskip);
588 getLogEntryRow(rows_used) = le;
589 --m_logskip;
590 }
591 ++rows_used;
592 if (rows_used > max_nonweak_txouts) {
593 LogEntry& old_le = getLogEntryRow(rows_used - max_nonweak_txouts - 1);
594 if (old_le) {
595 old_le.makeWeak();
596 }
597 }
598 }
599
600 void NetWatchLogModel::LogAddEntry(const LogEntry& le, size_t vout_count)
601 {
602 if (vout_count < 1) {
603 vout_count = 1;
604 }
605 const QModelIndex dummy;
606 LOCK(cs);
607 size_t rows_to_remove = 0;
608 if (vout_count >= max_vout_per_tx) {
609 vout_count = max_vout_per_tx;
610 }
611 size_t rows_used = rowCount();
612 if (rows_used > logsizelimit - vout_count) {
613 rows_to_remove = (rows_used + vout_count) - logsizelimit;
614 }
615 if (rows_to_remove) {
616 // Don't orphan continuation entries
617 while (isLogRowContinuation(rows_to_remove)) {
618 ++rows_to_remove;
619 }
620
621 beginRemoveRows(dummy, 0, rows_to_remove - 1);
622 m_logpos = (m_logpos + rows_to_remove) % m_log.size();
623 m_logskip += rows_to_remove;
624 endRemoveRows();
625
626 rows_used = rowCount();
627 }
628
629 const LogEntry cont_le;
630 const int first_new_row = rows_used, last_new_row = rows_used + vout_count - 1;
631 beginInsertRows(dummy, first_new_row, last_new_row);
632 log_append(le, rows_used);
633 for (size_t i = 1; i < vout_count; ++i) {
634 log_append(cont_le, rows_used);
635 }
636 endInsertRows();
637
638 if (m_current_search) {
639 QList<int> new_matches;
640 if (m_current_search->m_check_addr || m_current_search->m_check_value) {
641 for (int row = first_new_row; row <= last_new_row; ++row) {
642 if (m_current_search->match(*this, row)) {
643 new_matches.append(row);
644 }
645 }
646 } else if (m_current_search->match(*this, first_new_row)) {
647 for (int row = first_new_row; row <= last_new_row; ++row) {
648 new_matches.append(row);
649 }
650 }
651 if (!new_matches.isEmpty()) {
652 Q_EMIT moreSearchResults(new_matches);
653 }
654 }
655 }
656
657 void NetWatchLogModel::LogBlock(const CBlockIndex* pblockindex, const std::shared_ptr<const CBlock>& block_cached)
658 {
659 std::shared_ptr<const CBlock> block = block_cached;
660 if (!block) {
661 std::shared_ptr<CBlock> pblock = std::make_shared<CBlock>();
662 Assert(m_client_model && m_client_model->node().context());
663 if (!m_client_model->node().context()->chainman->m_blockman.ReadBlock(*pblock, *pblockindex)) {
664 // Indicate error somehow?
665 return;
666 }
667 block = pblock;
668 }
669 assert(block->vtx.size());
670 const size_t vout_count = CountNonDatacarrierOutputs(block->vtx[0]);
671 LogAddEntry(LogEntry(GetTime(), *pblockindex), vout_count);
672 }
673
674 void NetWatchLogModel::LogTransaction(const CTransactionRef& tx)
675 {
676 const size_t vout_count = CountNonDatacarrierOutputs(tx);
677 LogAddEntry(LogEntry(GetTime(), tx), vout_count);
678 }
679
680 void NetWatchLogModel::setClientModel(ClientModel *model)
681 {
682 if (m_client_model) {
683 delete m_validation_interface;
684 m_validation_interface = nullptr;
685
686 disconnect(m_client_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &NetWatchLogModel::updateDisplayUnit);
687 disconnect(m_client_model->getOptionsModel(), &OptionsModel::fontForMoneyChanged, this, &NetWatchLogModel::updateDisplayUnit);
688 }
689 m_client_model = model;
690 if (model) {
691 connect(model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &NetWatchLogModel::updateDisplayUnit);
692 connect(model->getOptionsModel(), &OptionsModel::fontForMoneyChanged, this, &NetWatchLogModel::updateDisplayUnit);
693
694 Assert(model->node().context()->validation_signals);
695 m_validation_interface = new NetWatchValidationInterface(*this);
696 model->node().context()->validation_signals->RegisterValidationInterface(m_validation_interface);
697 }
698 updateDisplayUnit();
699 }
700
701 void NetWatchLogModel::updateDisplayUnit()
702 {
703 Q_EMIT headerDataChanged(Qt::Horizontal, int(Header::Value), int(Header::Value));
704 Q_EMIT dataChanged(index(0, int(Header::Value)), index(rowCount() - 1, int(Header::Value)));
705 }
706
707 int NetWatchLogTestModel::rowCount(const QModelIndex& parent) const
708 {
709 return 2;
710 }
711
712 QVariant NetWatchLogTestModel::data(const QModelIndex& index, int role) const
713 {
714 const NetWatchLogModel::Header header = NetWatchLogModel::Header(index.column());
715 if (role == Qt::FontRole && header == Header::Id) {
716 return GUIUtil::fixedPitchFont();
717 } else if (role != Qt::DisplayRole) {
718 return QVariant();
719 }
720 switch (header) {
721 case Header::Time:
722 return QString{GUIUtil::dateTimeStr(GetTime()) + "4"};
723 case Header::Type:
724 return LogEntry::LogEntryTypeAbbreviation(LogEntry::Type(index.row()));
725 case Header::Id:
726 return QString(64, '0');
727 case Header::Address:
728 if (index.row()) {
729 return QString{"bc1" + QString(LONGEST_BECH32_ADDRESS-3, 'x')};
730 } else {
731 return QString(LONGEST_BASE58_ADDRESS, 'W');
732 }
733 case Header::Value:
734 return "20000000.00000000";
735 }
736 return QVariant();
737 }
738
739 GuiNetWatch::GuiNetWatch(const PlatformStyle *platformStyle, const NetworkStyle *networkStyle, QWidget *parent) :
740 QWidget(parent)
741 {
742 QVBoxLayout * const layout = new QVBoxLayout(this);
743
744 m_search_editor = new QLineEdit(this);
745 m_search_editor->setPlaceholderText("Search");
746 layout->addWidget(m_search_editor);
747
748 m_log_view = new QTableView(this);
749 m_log_view->verticalHeader()->hide();
750 m_log_view->setSizePolicy(QSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored));
751 m_log_view->setSelectionBehavior(QAbstractItemView::SelectRows);
752 m_log_view->setTabKeyNavigation(false);
753 m_log_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
754
755 {
756 NetWatchLogTestModel testmodel;
757 m_log_view->setModel(&testmodel);
758 m_log_view->resizeColumnsToContents();
759
760 log_model = new NetWatchLogModel(this);
761 m_log_view->setModel(log_model);
762 }
763 layout->addWidget(m_log_view);
764
765 setWindowTitle(tr(CLIENT_NAME) + " - " + tr("Network Watch") + " " + networkStyle->getTitleAddText());
766 setMinimumSize(640, 480);
767 resize(layout->contentsMargins().left() + (m_log_view->frameWidth() * 2) + m_log_view->columnViewportPosition(NetWatchLogModel::HeaderCount-1) + m_log_view->columnWidth(NetWatchLogModel::HeaderCount-1) + m_log_view->verticalScrollBar()->size().width() + layout->contentsMargins().right(), 480);
768 setWindowIcon(networkStyle->getTrayAndWindowIcon());
769
770 connect(m_search_editor, &QLineEdit::textChanged, this, &GuiNetWatch::doSearch);
771
772 connect(log_model, &NetWatchLogModel::rowsRemoved, this, &GuiNetWatch::rowsRemoved);
773 connect(log_model, &NetWatchLogModel::rowsAboutToBeInserted, this, &GuiNetWatch::aboutToInsert);
774 connect(log_model, &NetWatchLogModel::rowsInserted, this, &GuiNetWatch::maybeScrollToBottom);
775 connect(log_model, &NetWatchLogModel::moreSearchResults, this, &GuiNetWatch::moreSearchResults);
776
777 connect(m_log_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &GuiNetWatch::maybeCancelSearch);
778
779 setLayout(layout);
780 }
781
782 void GuiNetWatch::setClientModel(ClientModel *model)
783 {
784 log_model->setClientModel(model);
785 }
786
787 void GuiNetWatch::rowsRemoved(const QModelIndex& parent, int start, int end)
788 {
789 if (m_log_view->verticalScrollBar()->value() >= m_log_view->verticalScrollBar()->maximum()) {
790 return;
791 }
792 // Maintain the current viewed entries in place
793 int scrollpos = m_log_view->verticalScrollBar()->value();
794 if (start < scrollpos) {
795 scrollpos -= std::max(0, 1 + end - start);
796 m_log_view->verticalScrollBar()->setValue(scrollpos);
797 }
798 }
799
800 void GuiNetWatch::aboutToInsert()
801 {
802 m_adjust_scroll = (m_log_view->verticalScrollBar()->value() >= m_log_view->verticalScrollBar()->maximum());
803 }
804
805 void GuiNetWatch::maybeScrollToBottom()
806 {
807 if (m_adjust_scroll) {
808 m_log_view->scrollToBottom();
809 }
810 }
811
812 void GuiNetWatch::doSearch(const QString& query)
813 {
814 if (query.isEmpty()) {
815 log_model->searchDisable();
816 m_search_editor->setStyleSheet("");
817 return;
818 }
819 QList<int> results;
820 log_model->searchRows(query, results);
821 if (results.isEmpty()) {
822 m_search_editor->setStyleSheet(STYLE_INVALID);
823 return;
824 }
825 m_search_editor->setStyleSheet(STYLE_ACTIVE);
826 QItemSelectionModel& sel = *m_log_view->selectionModel();
827 m_dont_cancel_search = true;
828 sel.clear();
829 Q_FOREACH (int row, results) {
830 sel.select(log_model->index(row, 0), QItemSelectionModel::Rows | QItemSelectionModel::Select);
831 }
832 m_dont_cancel_search = false;
833 m_log_view->scrollTo(log_model->index(results.back(), int(NetWatchLogModel::Header::Id)));
834 }
835
836 void GuiNetWatch::moreSearchResults(const QList<int>& rows)
837 {
838 m_search_editor->setStyleSheet(STYLE_ACTIVE);
839 QItemSelectionModel& sel = *m_log_view->selectionModel();
840 m_dont_cancel_search = true;
841 Q_FOREACH (int row, rows) {
842 sel.select(log_model->index(row, 0), QItemSelectionModel::Rows | QItemSelectionModel::Select);
843 }
844 m_dont_cancel_search = false;
845 }
846
847 void GuiNetWatch::maybeCancelSearch()
848 {
849 if (m_dont_cancel_search) {
850 return;
851 }
852 m_search_editor->setStyleSheet("");
853 log_model->searchDisable();
854 }
855