/ src / qt / transactiontablemodel.cpp
transactiontablemodel.cpp
  1  // Copyright (c) 2011-present The Bitcoin Core developers
  2  // Distributed under the MIT software license, see the accompanying
  3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
  4  
  5  #include <qt/transactiontablemodel.h>
  6  
  7  #include <qt/addresstablemodel.h>
  8  #include <qt/bitcoinunits.h>
  9  #include <qt/clientmodel.h>
 10  #include <qt/guiconstants.h>
 11  #include <qt/guiutil.h>
 12  #include <qt/optionsmodel.h>
 13  #include <qt/platformstyle.h>
 14  #include <qt/transactiondesc.h>
 15  #include <qt/transactionrecord.h>
 16  #include <qt/walletmodel.h>
 17  
 18  #include <core_io.h>
 19  #include <interfaces/handler.h>
 20  #include <tinyformat.h>
 21  #include <uint256.h>
 22  
 23  #include <algorithm>
 24  #include <functional>
 25  
 26  #include <QColor>
 27  #include <QDateTime>
 28  #include <QDebug>
 29  #include <QIcon>
 30  #include <QLatin1Char>
 31  #include <QLatin1String>
 32  #include <QList>
 33  
 34  
 35  // Amount column is right-aligned it contains numbers
 36  static int column_alignments[] = {
 37          Qt::AlignLeft|Qt::AlignVCenter, /*status=*/
 38          Qt::AlignLeft|Qt::AlignVCenter, /*date=*/
 39          Qt::AlignLeft|Qt::AlignVCenter, /*type=*/
 40          Qt::AlignLeft|Qt::AlignVCenter, /*address=*/
 41          Qt::AlignRight|Qt::AlignVCenter /* amount */
 42      };
 43  
 44  // Comparison operator for sort/binary search of model tx list
 45  struct TxLessThan
 46  {
 47      bool operator()(const TransactionRecord &a, const TransactionRecord &b) const
 48      {
 49          return a.hash < b.hash;
 50      }
 51      bool operator()(const TransactionRecord &a, const Txid &b) const
 52      {
 53          return a.hash < b;
 54      }
 55      bool operator()(const Txid &a, const TransactionRecord &b) const
 56      {
 57          return a < b.hash;
 58      }
 59  };
 60  
 61  // queue notifications to show a non freezing progress dialog e.g. for rescan
 62  struct TransactionNotification
 63  {
 64  public:
 65      TransactionNotification() = default;
 66      TransactionNotification(Txid _hash, ChangeType _status, bool _showTransaction):
 67          hash(_hash), status(_status), showTransaction(_showTransaction) {}
 68  
 69      void invoke(QObject *ttm)
 70      {
 71          QString strHash = QString::fromStdString(hash.GetHex());
 72          qDebug() << "NotifyTransactionChanged: " + strHash + " status= " + QString::number(status);
 73          bool invoked = QMetaObject::invokeMethod(ttm, "updateTransaction", Qt::QueuedConnection,
 74                                    Q_ARG(QString, strHash),
 75                                    Q_ARG(int, status),
 76                                    Q_ARG(bool, showTransaction));
 77          assert(invoked);
 78      }
 79  private:
 80      Txid hash;
 81      ChangeType status;
 82      bool showTransaction;
 83  };
 84  
 85  // Private implementation
 86  class TransactionTablePriv
 87  {
 88  public:
 89      explicit TransactionTablePriv(TransactionTableModel *_parent) :
 90          parent(_parent)
 91      {
 92      }
 93  
 94      TransactionTableModel *parent;
 95  
 96      //! Local cache of wallet sorted by transaction hash
 97      QList<TransactionRecord> cachedWallet;
 98  
 99      /** True when model finishes loading all wallet transactions on start */
100      bool m_loaded = false;
101      /** True when transactions are being notified, for instance when scanning */
102      bool m_loading = false;
103      std::vector< TransactionNotification > vQueueNotifications;
104  
105      void NotifyTransactionChanged(const Txid& hash, ChangeType status);
106      void DispatchNotifications();
107  
108      /* Query entire wallet anew from core.
109       */
110      void refreshWallet(interfaces::Wallet& wallet)
111      {
112          assert(!m_loaded);
113          {
114              for (const auto& wtx : wallet.getWalletTxs()) {
115                  if (TransactionRecord::showTransaction()) {
116                      cachedWallet.append(TransactionRecord::decomposeTransaction(wtx));
117                  }
118              }
119          }
120          m_loaded = true;
121          DispatchNotifications();
122      }
123  
124      /* Update our model of the wallet incrementally, to synchronize our model of the wallet
125         with that of the core.
126  
127         Call with transaction that was added, removed or changed.
128       */
129      void updateWallet(interfaces::Wallet& wallet, const Txid& hash, int status, bool showTransaction)
130      {
131          qDebug() << "TransactionTablePriv::updateWallet: " + QString::fromStdString(hash.ToString()) + " " + QString::number(status);
132  
133          // Find bounds of this transaction in model
134          QList<TransactionRecord>::iterator lower = std::lower_bound(
135              cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan());
136          QList<TransactionRecord>::iterator upper = std::upper_bound(
137              cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan());
138          int lowerIndex = (lower - cachedWallet.begin());
139          int upperIndex = (upper - cachedWallet.begin());
140          bool inModel = (lower != upper);
141  
142          if(status == CT_UPDATED)
143          {
144              if(showTransaction && !inModel)
145                  status = CT_NEW; /* Not in model, but want to show, treat as new */
146              if(!showTransaction && inModel)
147                  status = CT_DELETED; /* In model, but want to hide, treat as deleted */
148          }
149  
150          qDebug() << "    inModel=" + QString::number(inModel) +
151                      " Index=" + QString::number(lowerIndex) + "-" + QString::number(upperIndex) +
152                      " showTransaction=" + QString::number(showTransaction) + " derivedStatus=" + QString::number(status);
153  
154          switch(status)
155          {
156          case CT_NEW:
157              if(inModel)
158              {
159                  qWarning() << "TransactionTablePriv::updateWallet: Warning: Got CT_NEW, but transaction is already in model";
160                  break;
161              }
162              if(showTransaction)
163              {
164                  // Find transaction in wallet
165                  interfaces::WalletTx wtx = wallet.getWalletTx(hash);
166                  if(!wtx.tx)
167                  {
168                      qWarning() << "TransactionTablePriv::updateWallet: Warning: Got CT_NEW, but transaction is not in wallet";
169                      break;
170                  }
171                  // Added -- insert at the right position
172                  QList<TransactionRecord> toInsert =
173                          TransactionRecord::decomposeTransaction(wtx);
174                  if(!toInsert.isEmpty()) /* only if something to insert */
175                  {
176                      parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex+toInsert.size()-1);
177                      int insert_idx = lowerIndex;
178                      for (const TransactionRecord &rec : toInsert)
179                      {
180                          cachedWallet.insert(insert_idx, rec);
181                          insert_idx += 1;
182                      }
183                      parent->endInsertRows();
184                  }
185              }
186              break;
187          case CT_DELETED:
188              if(!inModel)
189              {
190                  qWarning() << "TransactionTablePriv::updateWallet: Warning: Got CT_DELETED, but transaction is not in model";
191                  break;
192              }
193              // Removed -- remove entire transaction from table
194              parent->beginRemoveRows(QModelIndex(), lowerIndex, upperIndex-1);
195              cachedWallet.erase(lower, upper);
196              parent->endRemoveRows();
197              break;
198          case CT_UPDATED:
199              // Miscellaneous updates -- nothing to do, status update will take care of this, and is only computed for
200              // visible transactions.
201              for (int i = lowerIndex; i < upperIndex; i++) {
202                  TransactionRecord *rec = &cachedWallet[i];
203                  rec->status.needsUpdate = true;
204              }
205              break;
206          }
207      }
208  
209      int size()
210      {
211          return cachedWallet.size();
212      }
213  
214      TransactionRecord* index(interfaces::Wallet& wallet, const uint256& cur_block_hash, const int idx)
215      {
216          if (idx >= 0 && idx < cachedWallet.size()) {
217              TransactionRecord *rec = &cachedWallet[idx];
218  
219              // If a status update is needed (blocks came in since last check),
220              // try to update the status of this transaction from the wallet.
221              // Otherwise, simply reuse the cached status.
222              interfaces::WalletTxStatus wtx;
223              int numBlocks;
224              int64_t block_time;
225              if (!cur_block_hash.IsNull() && rec->statusUpdateNeeded(cur_block_hash) && wallet.tryGetTxStatus(rec->hash, wtx, numBlocks, block_time)) {
226                  rec->updateStatus(wtx, cur_block_hash, numBlocks, block_time);
227              }
228              return rec;
229          }
230          return nullptr;
231      }
232  
233      QString describe(interfaces::Node& node, interfaces::Wallet& wallet, TransactionRecord* rec, BitcoinUnit unit)
234      {
235          return TransactionDesc::toHTML(node, wallet, rec, unit);
236      }
237  
238      QString getTxHex(interfaces::Wallet& wallet, TransactionRecord *rec)
239      {
240          auto tx = wallet.getTx(rec->hash);
241          if (tx) {
242              std::string strHex = EncodeHexTx(*tx);
243              return QString::fromStdString(strHex);
244          }
245          return QString();
246      }
247  };
248  
249  TransactionTableModel::TransactionTableModel(const PlatformStyle *_platformStyle, WalletModel *parent):
250          QAbstractTableModel(parent),
251          walletModel(parent),
252          priv(new TransactionTablePriv(this)),
253          platformStyle(_platformStyle)
254  {
255      subscribeToCoreSignals();
256  
257      columns << QString() << tr("Date") << tr("Type") << tr("Label") << BitcoinUnits::getAmountColumnTitle(walletModel->getOptionsModel()->getDisplayUnit());
258      priv->refreshWallet(walletModel->wallet());
259  
260      connect(walletModel->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &TransactionTableModel::updateDisplayUnit);
261  }
262  
263  TransactionTableModel::~TransactionTableModel()
264  {
265      unsubscribeFromCoreSignals();
266      delete priv;
267  }
268  
269  /** Updates the column title to "Amount (DisplayUnit)" and emits headerDataChanged() signal for table headers to react. */
270  void TransactionTableModel::updateAmountColumnTitle()
271  {
272      columns[Amount] = BitcoinUnits::getAmountColumnTitle(walletModel->getOptionsModel()->getDisplayUnit());
273      Q_EMIT headerDataChanged(Qt::Horizontal,Amount,Amount);
274  }
275  
276  void TransactionTableModel::updateTransaction(const QString &hash, int status, bool showTransaction)
277  {
278      Txid updated = Txid::FromHex(hash.toStdString()).value();
279  
280      priv->updateWallet(walletModel->wallet(), updated, status, showTransaction);
281  }
282  
283  void TransactionTableModel::updateConfirmations()
284  {
285      // Blocks came in since last poll.
286      // Invalidate status (number of confirmations) and (possibly) description
287      //  for all rows. Qt is smart enough to only actually request the data for the
288      //  visible rows.
289      Q_EMIT dataChanged(index(0, Status), index(priv->size()-1, Status));
290      Q_EMIT dataChanged(index(0, ToAddress), index(priv->size()-1, ToAddress));
291  }
292  
293  int TransactionTableModel::rowCount(const QModelIndex &parent) const
294  {
295      if (parent.isValid()) {
296          return 0;
297      }
298      return priv->size();
299  }
300  
301  int TransactionTableModel::columnCount(const QModelIndex &parent) const
302  {
303      if (parent.isValid()) {
304          return 0;
305      }
306      return columns.length();
307  }
308  
309  QString TransactionTableModel::formatTxStatus(const TransactionRecord *wtx) const
310  {
311      QString status;
312  
313      switch(wtx->status.status)
314      {
315      case TransactionStatus::Unconfirmed:
316          status = tr("Unconfirmed");
317          break;
318      case TransactionStatus::Abandoned:
319          status = tr("Abandoned");
320          break;
321      case TransactionStatus::Confirming:
322          status = tr("Confirming (%1 of %2 recommended confirmations)").arg(wtx->status.depth).arg(TransactionRecord::RecommendedNumConfirmations);
323          break;
324      case TransactionStatus::Confirmed:
325          status = tr("Confirmed (%1 confirmations)").arg(wtx->status.depth);
326          break;
327      case TransactionStatus::Conflicted:
328          status = tr("Conflicted");
329          break;
330      case TransactionStatus::Immature:
331          status = tr("Immature (%1 confirmations, will be available after %2)").arg(wtx->status.depth).arg(wtx->status.depth + wtx->status.matures_in);
332          break;
333      case TransactionStatus::NotAccepted:
334          status = tr("Generated but not accepted");
335          break;
336      }
337  
338      return status;
339  }
340  
341  QString TransactionTableModel::formatTxDate(const TransactionRecord *wtx) const
342  {
343      if(wtx->time)
344      {
345          return GUIUtil::dateTimeStr(wtx->time);
346      }
347      return QString();
348  }
349  
350  /* Look up address in address book, if found return label (address)
351     otherwise just return (address)
352   */
353  QString TransactionTableModel::lookupAddress(const std::string &address, bool tooltip) const
354  {
355      QString label = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(address));
356      QString description;
357      if(!label.isEmpty())
358      {
359          description += label;
360      }
361      if(label.isEmpty() || tooltip)
362      {
363          description += QString(" (") + QString::fromStdString(address) + QString(")");
364      }
365      return description;
366  }
367  
368  QString TransactionTableModel::formatTxType(const TransactionRecord *wtx) const
369  {
370      switch(wtx->type)
371      {
372      case TransactionRecord::RecvWithAddress:
373          return tr("Received with");
374      case TransactionRecord::RecvFromOther:
375          return tr("Received from");
376      case TransactionRecord::SendToAddress:
377      case TransactionRecord::SendToOther:
378          return tr("Sent to");
379      case TransactionRecord::Generated:
380          return tr("Mined");
381      default:
382          return QString();
383      }
384  }
385  
386  QVariant TransactionTableModel::txAddressDecoration(const TransactionRecord *wtx) const
387  {
388      switch(wtx->type)
389      {
390      case TransactionRecord::Generated:
391          return QIcon(":/icons/tx_mined");
392      case TransactionRecord::RecvWithAddress:
393      case TransactionRecord::RecvFromOther:
394          return QIcon(":/icons/tx_input");
395      case TransactionRecord::SendToAddress:
396      case TransactionRecord::SendToOther:
397          return QIcon(":/icons/tx_output");
398      default:
399          return QIcon(":/icons/tx_inout");
400      }
401  }
402  
403  QString TransactionTableModel::formatTxToAddress(const TransactionRecord *wtx, bool tooltip) const
404  {
405      switch(wtx->type)
406      {
407      case TransactionRecord::RecvFromOther:
408          return QString::fromStdString(wtx->address);
409      case TransactionRecord::RecvWithAddress:
410      case TransactionRecord::SendToAddress:
411      case TransactionRecord::Generated:
412          return lookupAddress(wtx->address, tooltip);
413      case TransactionRecord::SendToOther:
414          return QString::fromStdString(wtx->address);
415      default:
416          return tr("(n/a)");
417      }
418  }
419  
420  QVariant TransactionTableModel::addressColor(const TransactionRecord *wtx) const
421  {
422      // Show addresses without label in a less visible color
423      switch(wtx->type)
424      {
425      case TransactionRecord::RecvWithAddress:
426      case TransactionRecord::SendToAddress:
427      case TransactionRecord::Generated:
428          {
429          QString label = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(wtx->address));
430          if(label.isEmpty())
431              return COLOR_BAREADDRESS;
432          } break;
433      default:
434          break;
435      }
436      return QVariant();
437  }
438  
439  QString TransactionTableModel::formatTxAmount(const TransactionRecord *wtx, bool showUnconfirmed, BitcoinUnits::SeparatorStyle separators) const
440  {
441      QString str = BitcoinUnits::format(walletModel->getOptionsModel()->getDisplayUnit(), wtx->credit + wtx->debit, false, separators);
442      if(showUnconfirmed)
443      {
444          if(!wtx->status.countsForBalance)
445          {
446              str = QString("[") + str + QString("]");
447          }
448      }
449      return QString(str);
450  }
451  
452  QVariant TransactionTableModel::txStatusDecoration(const TransactionRecord *wtx) const
453  {
454      switch(wtx->status.status)
455      {
456      case TransactionStatus::Unconfirmed:
457          return QIcon(":/icons/transaction_0");
458      case TransactionStatus::Abandoned:
459          return QIcon(":/icons/transaction_abandoned");
460      case TransactionStatus::Confirming:
461          switch(wtx->status.depth)
462          {
463          case 1: return QIcon(":/icons/transaction_1");
464          case 2: return QIcon(":/icons/transaction_2");
465          case 3: return QIcon(":/icons/transaction_3");
466          case 4: return QIcon(":/icons/transaction_4");
467          default: return QIcon(":/icons/transaction_5");
468          };
469      case TransactionStatus::Confirmed:
470          return QIcon(":/icons/transaction_confirmed");
471      case TransactionStatus::Conflicted:
472          return QIcon(":/icons/transaction_conflicted");
473      case TransactionStatus::Immature: {
474          int total = wtx->status.depth + wtx->status.matures_in;
475          int part = (wtx->status.depth * 4 / total) + 1;
476          return QIcon(QString(":/icons/transaction_%1").arg(part));
477          }
478      case TransactionStatus::NotAccepted:
479          return QIcon(":/icons/transaction_0");
480      } // no default case, so the compiler can warn about missing cases
481      assert(false);
482  }
483  
484  QString TransactionTableModel::formatTooltip(const TransactionRecord *rec) const
485  {
486      QString tooltip = formatTxStatus(rec) + QString("\n") + formatTxType(rec);
487      if(rec->type==TransactionRecord::RecvFromOther || rec->type==TransactionRecord::SendToOther ||
488         rec->type==TransactionRecord::SendToAddress || rec->type==TransactionRecord::RecvWithAddress)
489      {
490          tooltip += QString(" ") + formatTxToAddress(rec, true);
491      }
492      return tooltip;
493  }
494  
495  QVariant TransactionTableModel::data(const QModelIndex &index, int role) const
496  {
497      if(!index.isValid())
498          return QVariant();
499      TransactionRecord *rec = static_cast<TransactionRecord*>(index.internalPointer());
500  
501      const auto column = static_cast<ColumnIndex>(index.column());
502      switch (role) {
503      case RawDecorationRole:
504          switch (column) {
505          case Status:
506              return txStatusDecoration(rec);
507          case Date: return {};
508          case Type: return {};
509          case ToAddress:
510              return txAddressDecoration(rec);
511          case Amount: return {};
512          } // no default case, so the compiler can warn about missing cases
513          assert(false);
514      case Qt::DecorationRole:
515      {
516          QIcon icon = qvariant_cast<QIcon>(index.data(RawDecorationRole));
517          return platformStyle->TextColorIcon(icon);
518      }
519      case Qt::DisplayRole:
520          switch (column) {
521          case Status: return {};
522          case Date:
523              return formatTxDate(rec);
524          case Type:
525              return formatTxType(rec);
526          case ToAddress:
527              return formatTxToAddress(rec, false);
528          case Amount:
529              return formatTxAmount(rec, true, BitcoinUnits::SeparatorStyle::ALWAYS);
530          } // no default case, so the compiler can warn about missing cases
531          assert(false);
532      case Qt::EditRole:
533          // Edit role is used for sorting, so return the unformatted values
534          switch (column) {
535          case Status:
536              return QString::fromStdString(rec->status.sortKey);
537          case Date:
538              return QString::fromStdString(strprintf("%020s-%s", rec->time, rec->status.sortKey));
539          case Type:
540              return formatTxType(rec);
541          case ToAddress:
542              return formatTxToAddress(rec, true);
543          case Amount:
544              return qint64(rec->credit + rec->debit);
545          } // no default case, so the compiler can warn about missing cases
546          assert(false);
547      case Qt::ToolTipRole:
548          return formatTooltip(rec);
549      case Qt::TextAlignmentRole:
550          return column_alignments[index.column()];
551      case Qt::ForegroundRole:
552          // Use the "danger" color for abandoned transactions
553          if(rec->status.status == TransactionStatus::Abandoned)
554          {
555              return COLOR_TX_STATUS_DANGER;
556          }
557          // Non-confirmed (but not immature) as transactions are grey
558          if(!rec->status.countsForBalance && rec->status.status != TransactionStatus::Immature)
559          {
560              return COLOR_UNCONFIRMED;
561          }
562          if(index.column() == Amount && (rec->credit+rec->debit) < 0)
563          {
564              return COLOR_NEGATIVE;
565          }
566          if(index.column() == ToAddress)
567          {
568              return addressColor(rec);
569          }
570          break;
571      case TypeRole:
572          return rec->type;
573      case DateRole:
574          return QDateTime::fromSecsSinceEpoch(rec->time);
575      case LongDescriptionRole:
576          return priv->describe(walletModel->node(), walletModel->wallet(), rec, walletModel->getOptionsModel()->getDisplayUnit());
577      case AddressRole:
578          return QString::fromStdString(rec->address);
579      case LabelRole:
580          return walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(rec->address));
581      case AmountRole:
582          return qint64(rec->credit + rec->debit);
583      case TxHashRole:
584          return rec->getTxHash();
585      case TxHexRole:
586          return priv->getTxHex(walletModel->wallet(), rec);
587      case TxPlainTextRole:
588          {
589              QString details;
590              QDateTime date = QDateTime::fromSecsSinceEpoch(rec->time);
591              QString txLabel = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(rec->address));
592  
593              details.append(date.toString("M/d/yy HH:mm"));
594              details.append(" ");
595              details.append(formatTxStatus(rec));
596              details.append(". ");
597              if(!formatTxType(rec).isEmpty()) {
598                  details.append(formatTxType(rec));
599                  details.append(" ");
600              }
601              if(!rec->address.empty()) {
602                  if(txLabel.isEmpty())
603                      details.append(tr("(no label)") + " ");
604                  else {
605                      details.append("(");
606                      details.append(txLabel);
607                      details.append(") ");
608                  }
609                  details.append(QString::fromStdString(rec->address));
610                  details.append(" ");
611              }
612              details.append(formatTxAmount(rec, false, BitcoinUnits::SeparatorStyle::NEVER));
613              return details;
614          }
615      case ConfirmedRole:
616          return rec->status.status == TransactionStatus::Status::Confirming || rec->status.status == TransactionStatus::Status::Confirmed;
617      case FormattedAmountRole:
618          // Used for copy/export, so don't include separators
619          return formatTxAmount(rec, false, BitcoinUnits::SeparatorStyle::NEVER);
620      case StatusRole:
621          return rec->status.status;
622      }
623      return QVariant();
624  }
625  
626  QVariant TransactionTableModel::headerData(int section, Qt::Orientation orientation, int role) const
627  {
628      if(orientation == Qt::Horizontal)
629      {
630          if(role == Qt::DisplayRole)
631          {
632              return columns[section];
633          }
634          else if (role == Qt::TextAlignmentRole)
635          {
636              return column_alignments[section];
637          } else if (role == Qt::ToolTipRole)
638          {
639              switch(section)
640              {
641              case Status:
642                  return tr("Transaction status. Hover over this field to show number of confirmations.");
643              case Date:
644                  return tr("Date and time that the transaction was received.");
645              case Type:
646                  return tr("Type of transaction.");
647              case ToAddress:
648                  return tr("User-defined intent/purpose of the transaction.");
649              case Amount:
650                  return tr("Amount removed from or added to balance.");
651              }
652          }
653      }
654      return QVariant();
655  }
656  
657  QModelIndex TransactionTableModel::index(int row, int column, const QModelIndex &parent) const
658  {
659      Q_UNUSED(parent);
660      TransactionRecord* data = priv->index(walletModel->wallet(), walletModel->getLastBlockProcessed(), row);
661      if(data)
662      {
663          return createIndex(row, column, data);
664      }
665      return QModelIndex();
666  }
667  
668  void TransactionTableModel::updateDisplayUnit()
669  {
670      // emit dataChanged to update Amount column with the current unit
671      updateAmountColumnTitle();
672      Q_EMIT dataChanged(index(0, Amount), index(priv->size()-1, Amount));
673  }
674  
675  void TransactionTablePriv::NotifyTransactionChanged(const Txid& hash, ChangeType status)
676  {
677      // Find transaction in wallet
678      // Determine whether to show transaction or not (determine this here so that no relocking is needed in GUI thread)
679      bool showTransaction = TransactionRecord::showTransaction();
680  
681      TransactionNotification notification(hash, status, showTransaction);
682  
683      if (!m_loaded || m_loading)
684      {
685          vQueueNotifications.push_back(notification);
686          return;
687      }
688      notification.invoke(parent);
689  }
690  
691  void TransactionTablePriv::DispatchNotifications()
692  {
693      if (!m_loaded || m_loading) return;
694  
695      if (vQueueNotifications.size() > 10) { // prevent balloon spam, show maximum 10 balloons
696          bool invoked = QMetaObject::invokeMethod(parent, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, true));
697          assert(invoked);
698      }
699      for (unsigned int i = 0; i < vQueueNotifications.size(); ++i)
700      {
701          if (vQueueNotifications.size() - i <= 10) {
702              bool invoked = QMetaObject::invokeMethod(parent, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, false));
703              assert(invoked);
704          }
705  
706          vQueueNotifications[i].invoke(parent);
707      }
708      vQueueNotifications.clear();
709  }
710  
711  void TransactionTableModel::subscribeToCoreSignals()
712  {
713      // Connect signals to wallet
714      m_handler_transaction_changed = walletModel->wallet().handleTransactionChanged([this](const Txid& hash, ChangeType status) {
715          priv->NotifyTransactionChanged(hash, status);
716      });
717      m_handler_show_progress = walletModel->wallet().handleShowProgress([this](const std::string&, int progress) {
718          priv->m_loading = progress < 100;
719          priv->DispatchNotifications();
720      });
721  }
722  
723  void TransactionTableModel::unsubscribeFromCoreSignals()
724  {
725      // Disconnect signals from wallet
726      m_handler_transaction_changed->disconnect();
727      m_handler_show_progress->disconnect();
728  }