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 }