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