transactiontablemodel.cpp raw

   1  // Copyright (c) 2011-2022 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 <qt/transactiontablemodel.h>
   6  
   7  #include <qt/addresstablemodel.h>
   8  #include <qt/limenkaunits.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, LimenkaUnit unit, const QFont& font_for_money)
 235      {
 236          return TransactionDesc::toHTML(node, wallet, rec, unit, font_for_money);
 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") << LimenkaUnits::getAmountColumnTitle(walletModel->getOptionsModel()->getDisplayUnit());
 259      priv->refreshWallet(walletModel->wallet());
 260  
 261      connect(walletModel->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &TransactionTableModel::updateDisplayUnit);
 262      connect(walletModel->getOptionsModel(), &OptionsModel::fontForMoneyChanged, this, &TransactionTableModel::updateDisplayUnit);
 263  }
 264  
 265  TransactionTableModel::~TransactionTableModel()
 266  {
 267      unsubscribeFromCoreSignals();
 268      delete priv;
 269  }
 270  
 271  /** Updates the column title to "Amount (DisplayUnit)" and emits headerDataChanged() signal for table headers to react. */
 272  void TransactionTableModel::updateAmountColumnTitle()
 273  {
 274      columns[Amount] = LimenkaUnits::getAmountColumnTitle(walletModel->getOptionsModel()->getDisplayUnit());
 275      Q_EMIT headerDataChanged(Qt::Horizontal,Amount,Amount);
 276  }
 277  
 278  void TransactionTableModel::updateTransaction(const QString &hash, int status, bool showTransaction)
 279  {
 280      uint256 updated;
 281      updated.SetHexDeprecated(hash.toStdString());
 282  
 283      priv->updateWallet(walletModel->wallet(), updated, status, showTransaction);
 284  }
 285  
 286  void TransactionTableModel::updateConfirmations()
 287  {
 288      // Blocks came in since last poll.
 289      // Invalidate status (number of confirmations) and (possibly) description
 290      //  for all rows. Qt is smart enough to only actually request the data for the
 291      //  visible rows.
 292      Q_EMIT dataChanged(index(0, Status), index(priv->size()-1, Status));
 293      Q_EMIT dataChanged(index(0, ToAddress), index(priv->size()-1, ToAddress));
 294  }
 295  
 296  int TransactionTableModel::rowCount(const QModelIndex &parent) const
 297  {
 298      if (parent.isValid()) {
 299          return 0;
 300      }
 301      return priv->size();
 302  }
 303  
 304  int TransactionTableModel::columnCount(const QModelIndex &parent) const
 305  {
 306      if (parent.isValid()) {
 307          return 0;
 308      }
 309      return columns.length();
 310  }
 311  
 312  QString TransactionTableModel::formatTxStatus(const TransactionRecord *wtx) const
 313  {
 314      QString status;
 315  
 316      switch(wtx->status.status)
 317      {
 318      case TransactionStatus::Unconfirmed:
 319          status = tr("Unconfirmed");
 320          break;
 321      case TransactionStatus::Abandoned:
 322          status = tr("Abandoned");
 323          break;
 324      case TransactionStatus::AssumedConfirmed:
 325          status = tr("Unconfirmed (%1 confirmations pending verification of historical blocks)").arg(wtx->status.depth);
 326          break;
 327      case TransactionStatus::Confirming:
 328          status = tr("Confirming (%1 of %2 recommended confirmations)").arg(wtx->status.depth).arg(TransactionRecord::RecommendedNumConfirmations);
 329          break;
 330      case TransactionStatus::Confirmed:
 331          status = tr("Confirmed (%1 confirmations)").arg(wtx->status.depth);
 332          break;
 333      case TransactionStatus::Conflicted:
 334          status = tr("Conflicted");
 335          break;
 336      case TransactionStatus::Immature:
 337          status = tr("Immature (%1 confirmations, will be available after %2)").arg(wtx->status.depth).arg(wtx->status.depth + wtx->status.matures_in);
 338          break;
 339      case TransactionStatus::NotAccepted:
 340          status = tr("Generated but not accepted");
 341          break;
 342      }
 343  
 344      return status;
 345  }
 346  
 347  QString TransactionTableModel::formatTxDate(const TransactionRecord *wtx) const
 348  {
 349      if(wtx->time)
 350      {
 351          return GUIUtil::dateTimeStr(wtx->time);
 352      }
 353      return QString();
 354  }
 355  
 356  /* Look up address in address book, if found return label (address)
 357     otherwise just return (address)
 358   */
 359  QString TransactionTableModel::lookupAddress(const std::string &address, bool tooltip) const
 360  {
 361      QString label = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(address));
 362      QString description;
 363      if(!label.isEmpty())
 364      {
 365          description += label;
 366      }
 367      if(label.isEmpty() || walletModel->getOptionsModel()->getDisplayAddresses() || tooltip)
 368      {
 369          description += QString(" (") + QString::fromStdString(address) + QString(")");
 370      }
 371      return description;
 372  }
 373  
 374  QString TransactionTableModel::formatTxType(const TransactionRecord *wtx) const
 375  {
 376      switch(wtx->type)
 377      {
 378      case TransactionRecord::RecvWithAddress:
 379          return tr("Received with");
 380      case TransactionRecord::RecvFromOther:
 381          return tr("Received from");
 382      case TransactionRecord::SendToAddress:
 383      case TransactionRecord::SendToOther:
 384          return tr("Sent to");
 385      case TransactionRecord::Generated:
 386          return tr("Mined");
 387      default:
 388          return QString();
 389      }
 390  }
 391  
 392  QVariant TransactionTableModel::txAddressDecoration(const TransactionRecord *wtx) const
 393  {
 394      switch(wtx->type)
 395      {
 396      case TransactionRecord::Generated:
 397          return QIcon(":/icons/tx_mined");
 398      case TransactionRecord::RecvWithAddress:
 399      case TransactionRecord::RecvFromOther:
 400          return QIcon(":/icons/tx_input");
 401      case TransactionRecord::SendToAddress:
 402      case TransactionRecord::SendToOther:
 403          return QIcon(":/icons/tx_output");
 404      default:
 405          return QIcon(":/icons/tx_inout");
 406      }
 407  }
 408  
 409  QString TransactionTableModel::formatTxToAddress(const TransactionRecord *wtx, bool tooltip) const
 410  {
 411      QString watchAddress;
 412      if (tooltip && wtx->involvesWatchAddress) {
 413          // Mark transactions involving watch-only addresses by adding " (watch-only)"
 414          watchAddress = QLatin1String(" (") + tr("watch-only") + QLatin1Char(')');
 415      }
 416  
 417      switch(wtx->type)
 418      {
 419      case TransactionRecord::RecvFromOther:
 420          return QString::fromStdString(wtx->address) + watchAddress;
 421      case TransactionRecord::RecvWithAddress:
 422      case TransactionRecord::SendToAddress:
 423      case TransactionRecord::Generated:
 424          return lookupAddress(wtx->address, tooltip) + watchAddress;
 425      case TransactionRecord::SendToOther:
 426          return QString::fromStdString(wtx->address) + watchAddress;
 427      default:
 428          return tr("(n/a)") + watchAddress;
 429      }
 430  }
 431  
 432  QVariant TransactionTableModel::addressColor(const TransactionRecord *wtx) const
 433  {
 434      // Show addresses without label in a less visible color
 435      switch(wtx->type)
 436      {
 437      case TransactionRecord::RecvWithAddress:
 438      case TransactionRecord::SendToAddress:
 439      case TransactionRecord::Generated:
 440          {
 441          QString label = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(wtx->address));
 442          if(label.isEmpty())
 443              return COLOR_BAREADDRESS;
 444          } break;
 445      default:
 446          break;
 447      }
 448      return QVariant();
 449  }
 450  
 451  QString TransactionTableModel::formatTxAmount(const TransactionRecord *wtx, bool showUnconfirmed, LimenkaUnits::SeparatorStyle separators) const
 452  {
 453      QString str = LimenkaUnits::format(walletModel->getOptionsModel()->getDisplayUnit(), wtx->credit + wtx->debit, false, separators);
 454      if(showUnconfirmed)
 455      {
 456          if(!wtx->status.countsForBalance)
 457          {
 458              str = QString("[") + str + QString("]");
 459          }
 460      }
 461      return QString(str);
 462  }
 463  
 464  QVariant TransactionTableModel::txStatusDecoration(const TransactionRecord *wtx) const
 465  {
 466      switch(wtx->status.status)
 467      {
 468      case TransactionStatus::Unconfirmed:
 469      case TransactionStatus::AssumedConfirmed:
 470          return QIcon(":/icons/transaction_0");
 471      case TransactionStatus::Abandoned:
 472          return QIcon(":/icons/transaction_abandoned");
 473      case TransactionStatus::Confirming:
 474          switch (wtx->status.depth * 6 / TransactionRecord::RecommendedNumConfirmations)
 475          {
 476          case 1: return QIcon(":/icons/transaction_1");
 477          case 2: return QIcon(":/icons/transaction_2");
 478          case 3: return QIcon(":/icons/transaction_3");
 479          case 4: return QIcon(":/icons/transaction_4");
 480          default: return QIcon(":/icons/transaction_5");
 481          };
 482      case TransactionStatus::Confirmed:
 483          return QIcon(":/icons/transaction_confirmed");
 484      case TransactionStatus::Conflicted:
 485          return QIcon(":/icons/transaction_conflicted");
 486      case TransactionStatus::Immature: {
 487          int total = wtx->status.depth + wtx->status.matures_in;
 488          int part = (wtx->status.depth * 4 / total) + 1;
 489          return QIcon(QString(":/icons/transaction_%1").arg(part));
 490          }
 491      case TransactionStatus::NotAccepted:
 492          return QIcon(":/icons/transaction_0");
 493      default:
 494          return COLOR_BLACK;
 495      }
 496  }
 497  
 498  QVariant TransactionTableModel::txWatchonlyDecoration(const TransactionRecord *wtx) const
 499  {
 500      if (wtx->involvesWatchAddress)
 501          return QIcon(":/icons/eye");
 502      else
 503          return QVariant();
 504  }
 505  
 506  QString TransactionTableModel::formatTooltip(const TransactionRecord *rec) const
 507  {
 508      QString tooltip = formatTxStatus(rec) + QString("\n") + formatTxType(rec);
 509      if(rec->type==TransactionRecord::RecvFromOther || rec->type==TransactionRecord::SendToOther ||
 510         rec->type==TransactionRecord::SendToAddress || rec->type==TransactionRecord::RecvWithAddress)
 511      {
 512          tooltip += QString(" ") + formatTxToAddress(rec, true);
 513      }
 514      return tooltip;
 515  }
 516  
 517  QVariant TransactionTableModel::data(const QModelIndex &index, int role) const
 518  {
 519      if(!index.isValid())
 520          return QVariant();
 521      TransactionRecord *rec = static_cast<TransactionRecord*>(index.internalPointer());
 522  
 523      const auto column = static_cast<ColumnIndex>(index.column());
 524      switch (role) {
 525      case RawDecorationRole:
 526          switch (column) {
 527          case Status:
 528              return txStatusDecoration(rec);
 529          case Watchonly:
 530              return txWatchonlyDecoration(rec);
 531          case Date: return {};
 532          case Type: return {};
 533          case ToAddress:
 534              return txAddressDecoration(rec);
 535          case Amount: return {};
 536          } // no default case, so the compiler can warn about missing cases
 537          assert(false);
 538      case Qt::DecorationRole:
 539      {
 540          QIcon icon = qvariant_cast<QIcon>(index.data(RawDecorationRole));
 541          return platformStyle->TextColorIcon(icon);
 542      }
 543      case Qt::DisplayRole:
 544          switch (column) {
 545          case Status: return {};
 546          case Watchonly: return {};
 547          case Date:
 548              return formatTxDate(rec);
 549          case Type:
 550              return formatTxType(rec);
 551          case ToAddress:
 552              return formatTxToAddress(rec, false);
 553          case Amount:
 554              return formatTxAmount(rec, true, LimenkaUnits::SeparatorStyle::ALWAYS);
 555          } // no default case, so the compiler can warn about missing cases
 556          assert(false);
 557      case Qt::EditRole:
 558          // Edit role is used for sorting, so return the unformatted values
 559          switch (column) {
 560          case Status:
 561              return QString::fromStdString(rec->status.sortKey);
 562          case Date:
 563              return QString::fromStdString(strprintf("%020s-%s", rec->time, rec->status.sortKey));
 564          case Type:
 565              return formatTxType(rec);
 566          case Watchonly:
 567              return (rec->involvesWatchAddress ? 1 : 0);
 568          case ToAddress:
 569              return formatTxToAddress(rec, true);
 570          case Amount:
 571              return qint64(rec->credit + rec->debit);
 572          } // no default case, so the compiler can warn about missing cases
 573          assert(false);
 574      case Qt::FontRole:
 575          if (column == Amount) {
 576              const LimenkaUnit display_unit = walletModel->getOptionsModel()->getDisplayUnit();
 577              return walletModel->getOptionsModel()->getFontForMoney(display_unit);
 578          }
 579          break;
 580      case Qt::ToolTipRole:
 581          return formatTooltip(rec);
 582      case Qt::TextAlignmentRole:
 583          return column_alignments[index.column()];
 584      case Qt::ForegroundRole:
 585          // Use the "danger" color for abandoned transactions
 586          if(rec->status.status == TransactionStatus::Abandoned)
 587          {
 588              return COLOR_TX_STATUS_DANGER;
 589          }
 590          // Non-confirmed (but not immature) as transactions are grey
 591          if(!rec->status.countsForBalance && rec->status.status != TransactionStatus::Immature)
 592          {
 593              return COLOR_UNCONFIRMED;
 594          }
 595          if(index.column() == Amount && (rec->credit+rec->debit) < 0)
 596          {
 597              return COLOR_NEGATIVE;
 598          }
 599          if(index.column() == ToAddress)
 600          {
 601              return addressColor(rec);
 602          }
 603          break;
 604      case TypeRole:
 605          return rec->type;
 606      case DateRole:
 607          return QDateTime::fromSecsSinceEpoch(rec->time);
 608      case WatchonlyRole:
 609          return rec->involvesWatchAddress;
 610      case WatchonlyDecorationRole:
 611          return txWatchonlyDecoration(rec);
 612      case LongDescriptionRole:
 613      {
 614          const LimenkaUnit display_unit = walletModel->getOptionsModel()->getDisplayUnit();
 615          const QFont font_for_money = walletModel->getOptionsModel()->getFontForMoney(display_unit);
 616          return priv->describe(walletModel->node(), walletModel->wallet(), rec, display_unit, font_for_money);
 617      }
 618      case AddressRole:
 619          return QString::fromStdString(rec->address);
 620      case LabelRole:
 621          return walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(rec->address));
 622      case AmountRole:
 623          return qint64(rec->credit + rec->debit);
 624      case TxHashRole:
 625          return rec->getTxHash();
 626      case TxHexRole:
 627          return priv->getTxHex(walletModel->wallet(), rec);
 628      case TxPlainTextRole:
 629          {
 630              QString details;
 631              QDateTime date = QDateTime::fromSecsSinceEpoch(rec->time);
 632              QString txLabel = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(rec->address));
 633  
 634              details.append(date.toString("M/d/yy HH:mm"));
 635              details.append(" ");
 636              details.append(formatTxStatus(rec));
 637              details.append(". ");
 638              if(!formatTxType(rec).isEmpty()) {
 639                  details.append(formatTxType(rec));
 640                  details.append(" ");
 641              }
 642              if(!rec->address.empty()) {
 643                  if(txLabel.isEmpty())
 644                      details.append(tr("(no label)") + " ");
 645                  else {
 646                      details.append("(");
 647                      details.append(txLabel);
 648                      details.append(") ");
 649                  }
 650                  details.append(QString::fromStdString(rec->address));
 651                  details.append(" ");
 652              }
 653              details.append(formatTxAmount(rec, false, LimenkaUnits::SeparatorStyle::NEVER));
 654              return details;
 655          }
 656      case ConfirmedRole:
 657          return rec->status.status == TransactionStatus::Status::Confirming || rec->status.status == TransactionStatus::Status::Confirmed;
 658      case FormattedAmountRole:
 659          // Used for copy/export, so don't include separators
 660          return formatTxAmount(rec, false, LimenkaUnits::SeparatorStyle::NEVER);
 661      case StatusRole:
 662          return rec->status.status;
 663      }
 664      return QVariant();
 665  }
 666  
 667  QVariant TransactionTableModel::headerData(int section, Qt::Orientation orientation, int role) const
 668  {
 669      if(orientation == Qt::Horizontal)
 670      {
 671          if(role == Qt::DisplayRole)
 672          {
 673              return columns[section];
 674          }
 675          else if (role == Qt::TextAlignmentRole)
 676          {
 677              return column_alignments[section];
 678          } else if (role == Qt::ToolTipRole)
 679          {
 680              switch(section)
 681              {
 682              case Status:
 683                  return tr("Transaction status. Hover over this field to show number of confirmations.");
 684              case Date:
 685                  return tr("Date and time that the transaction was received.");
 686              case Type:
 687                  return tr("Type of transaction.");
 688              case Watchonly:
 689                  return tr("Whether or not a watch-only address is involved in this transaction.");
 690              case ToAddress:
 691                  return tr("User-defined intent/purpose of the transaction.");
 692              case Amount:
 693                  return tr("Amount removed from or added to balance.");
 694              }
 695          }
 696      }
 697      return QVariant();
 698  }
 699  
 700  QModelIndex TransactionTableModel::index(int row, int column, const QModelIndex &parent) const
 701  {
 702      Q_UNUSED(parent);
 703      TransactionRecord* data = priv->index(walletModel->wallet(), walletModel->getLastBlockProcessed(), row);
 704      if(data)
 705      {
 706          return createIndex(row, column, data);
 707      }
 708      return QModelIndex();
 709  }
 710  
 711  void TransactionTableModel::updateDisplayUnit()
 712  {
 713      // emit dataChanged to update Amount column with the current unit
 714      updateAmountColumnTitle();
 715      Q_EMIT dataChanged(index(0, Amount), index(priv->size()-1, Amount));
 716  }
 717  
 718  void TransactionTablePriv::NotifyTransactionChanged(const uint256 &hash, ChangeType status)
 719  {
 720      // Find transaction in wallet
 721      // Determine whether to show transaction or not (determine this here so that no relocking is needed in GUI thread)
 722      bool showTransaction = TransactionRecord::showTransaction();
 723  
 724      TransactionNotification notification(hash, status, showTransaction);
 725  
 726      if (!m_loaded || m_loading)
 727      {
 728          vQueueNotifications.push_back(notification);
 729          return;
 730      }
 731      notification.invoke(parent);
 732  }
 733  
 734  void TransactionTablePriv::DispatchNotifications()
 735  {
 736      if (!m_loaded || m_loading) return;
 737  
 738      if (vQueueNotifications.size() > 10) { // prevent balloon spam, show maximum 10 balloons
 739          bool invoked = QMetaObject::invokeMethod(parent, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, true));
 740          assert(invoked);
 741      }
 742      for (unsigned int i = 0; i < vQueueNotifications.size(); ++i)
 743      {
 744          if (vQueueNotifications.size() - i <= 10) {
 745              bool invoked = QMetaObject::invokeMethod(parent, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, false));
 746              assert(invoked);
 747          }
 748  
 749          vQueueNotifications[i].invoke(parent);
 750      }
 751      vQueueNotifications.clear();
 752  }
 753  
 754  void TransactionTableModel::subscribeToCoreSignals()
 755  {
 756      // Connect signals to wallet
 757      m_handler_transaction_changed = walletModel->wallet().handleTransactionChanged(std::bind(&TransactionTablePriv::NotifyTransactionChanged, priv, std::placeholders::_1, std::placeholders::_2));
 758      m_handler_show_progress = walletModel->wallet().handleShowProgress([this](const std::string&, int progress) {
 759          priv->m_loading = progress < 100;
 760          priv->DispatchNotifications();
 761      });
 762  }
 763  
 764  void TransactionTableModel::unsubscribeFromCoreSignals()
 765  {
 766      // Disconnect signals from wallet
 767      m_handler_transaction_changed->disconnect();
 768      m_handler_show_progress->disconnect();
 769  }
 770