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