// SPDX-License-Identifier: BSD-3-Clause // SPDX-FileCopyrightText: 2020-2024 The Monero Project #include "TransactionHistoryModel.h" #include "TransactionHistory.h" #include "constants.h" #include "utils/config.h" #include "utils/ColorScheme.h" #include "utils/Icons.h" #include "utils/AppData.h" #include "utils/Utils.h" #include "libwalletqt/rows/TransactionRow.h" TransactionHistoryModel::TransactionHistoryModel(QObject *parent) : QAbstractTableModel(parent), m_transactionHistory(nullptr) { } void TransactionHistoryModel::setTransactionHistory(TransactionHistory *th) { beginResetModel(); m_transactionHistory = th; endResetModel(); connect(m_transactionHistory, &TransactionHistory::refreshStarted, this, &TransactionHistoryModel::beginResetModel); connect(m_transactionHistory, &TransactionHistory::refreshFinished, this, &TransactionHistoryModel::endResetModel); emit transactionHistoryChanged(); } TransactionHistory *TransactionHistoryModel::transactionHistory() const { return m_transactionHistory; } TransactionRow* TransactionHistoryModel::entryFromIndex(const QModelIndex &index) const { Q_ASSERT(index.isValid() && index.row() < m_transactionHistory->count()); return m_transactionHistory->transaction(index.row()); } int TransactionHistoryModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } else { return m_transactionHistory ? m_transactionHistory->count() : 0; } } int TransactionHistoryModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return Column::COUNT; } QVariant TransactionHistoryModel::data(const QModelIndex &index, int role) const { if (!m_transactionHistory) { return QVariant(); } if (!index.isValid() || index.row() < 0 || static_cast(index.row()) >= m_transactionHistory->count()) return QVariant(); QVariant result; bool found = m_transactionHistory->transaction(index.row(), [this, &index, &result, &role](const TransactionRow &tInfo) { if(role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole) { result = parseTransactionInfo(tInfo, index.column(), role); } else if (role == Qt::TextAlignmentRole) { switch (index.column()) { case Column::Amount: case Column::FiatAmount: result = Qt::AlignRight; } } else if (role == Qt::DecorationRole) { switch (index.column()) { case Column::Date: { if (tInfo.isFailed()) result = QVariant(icons()->icon("warning.png")); else if (tInfo.isPending()) result = QVariant(icons()->icon("unconfirmed.png")); else if (tInfo.confirmations() <= (1.0/5.0 * tInfo.confirmationsRequired())) result = QVariant(icons()->icon("clock1.png")); else if (tInfo.confirmations() <= (2.0/5.0 * tInfo.confirmationsRequired())) result = QVariant(icons()->icon("clock2.png")); else if (tInfo.confirmations() <= (3.0/5.0 * tInfo.confirmationsRequired())) result = QVariant(icons()->icon("clock3.png")); else if (tInfo.confirmations() <= (4.0/5.0 * tInfo.confirmationsRequired())) result = QVariant(icons()->icon("clock4.png")); else if (tInfo.confirmations() < tInfo.confirmationsRequired()) result = QVariant(icons()->icon("clock5.png")); else if (tInfo.confirmations()) result = QVariant(icons()->icon("confirmed.svg")); } } } else if (role == Qt::ToolTipRole) { switch(index.column()) { case Column::Date: { if (tInfo.isFailed()) result = "Transaction failed"; else if (tInfo.confirmations() < tInfo.confirmationsRequired()) result = QString("%1/%2 confirmations").arg(QString::number(tInfo.confirmations()), QString::number(tInfo.confirmationsRequired())); else result = QString("%1 confirmations").arg(QString::number(tInfo.confirmations())); } } } else if (role == Qt::ForegroundRole) { switch(index.column()) { case Column::FiatAmount: case Column::Amount: { if (tInfo.balanceDelta() < 0) { result = QVariant(QColor("#BC1E1E")); } } } } else if (role == Qt::FontRole) { switch(index.column()) { case Column::TxID: { result = Utils::getMonospaceFont(); } } } }); if (!found) { qCritical("%s: internal error: no transaction info for index %d", __FUNCTION__, index.row()); } return result; } QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionRow &tInfo, int column, int role) const { switch (column) { case Column::Date: { if (role == Qt::UserRole) { if (tInfo.blockHeight() > 0) { return tInfo.blockHeight(); } return tInfo.timestamp().toMSecsSinceEpoch(); } return tInfo.timestamp().toString(QString("%1 %2 ").arg(conf()->get(Config::dateFormat).toString(), conf()->get(Config::timeFormat).toString())); } case Column::Description: return tInfo.description(); case Column::Amount: { if (role == Qt::UserRole) { return tInfo.balanceDelta(); } QString amount = QString::number(tInfo.balanceDelta() / constants::cdiv, 'f', conf()->get(Config::amountPrecision).toInt()); amount = (tInfo.balanceDelta() < 0) ? amount : "+" + amount; return amount; } case Column::TxID: { if (conf()->get(Config::historyShowFullTxid).toBool()) { return tInfo.hash(); } return Utils::displayAddress(tInfo.hash(), 1); } case Column::FiatAmount: { double usd_price = appData()->txFiatHistory->get(tInfo.timestamp().toString("yyyyMMdd")); if (usd_price == 0.0) { return QString("?"); } double usd_amount = usd_price * (abs(tInfo.balanceDelta()) / constants::cdiv); QString preferredFiatCurrency = conf()->get(Config::preferredFiatCurrency).toString(); if (preferredFiatCurrency != "USD") { usd_amount = appData()->prices.convert("USD", preferredFiatCurrency, usd_amount); } if (role == Qt::UserRole) { return usd_amount; } if (usd_amount == 0.0) { return QString("?"); } double fiat_rounded = ceil(Utils::roundSignificant(usd_amount, 3) * 100.0) / 100.0; return QString("%1").arg(Utils::amountToCurrencyString(fiat_rounded, preferredFiatCurrency)); } default: { qCritical() << "Unimplemented role"; return {}; } } } QVariant TransactionHistoryModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(orientation) if (role != Qt::DisplayRole) { return QVariant(); } if (orientation == Qt::Horizontal) { switch(section) { case Column::Date: return QString("Date"); case Column::Description: return QString("Description"); case Column::Amount: return QString("Amount"); case Column::TxID: return QString("Txid"); case Column::FiatAmount: return QString("Fiat"); default: return QVariant(); } } return QVariant(); } bool TransactionHistoryModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.isValid() && role == Qt::EditRole) { QString hash; switch (index.column()) { case Column::Description: { m_transactionHistory->transaction(index.row(), [this, &hash, &value](const TransactionRow &tInfo){ hash = tInfo.hash(); }); m_transactionHistory->setTxNote(hash, value.toString()); break; } default: return false; } emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole}); return true; } return false; } Qt::ItemFlags TransactionHistoryModel::flags(const QModelIndex &index) const { if (!index.isValid()) return Qt::ItemIsEnabled; if (index.column() == Description) return QAbstractTableModel::flags(index) | Qt::ItemIsEditable; return QAbstractTableModel::flags(index); }