// SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2014-2021, The Monero Project. #include "TransactionHistory.h" #include "TransactionInfo.h" #include "utils/utils.h" #include "appcontext.h" bool TransactionHistory::transaction(int index, std::function callback) { QReadLocker locker(&m_lock); if (index < 0 || index >= m_tinfo.size()) { qCritical("%s: no transaction info for index %d", __FUNCTION__, index); qCritical("%s: there's %d transactions in backend", __FUNCTION__, m_pimpl->count()); return false; } callback(*m_tinfo.value(index)); return true; } TransactionInfo* TransactionHistory::transaction(const QString &id) { QReadLocker locker(&m_lock); auto itr = std::find_if(m_tinfo.begin(), m_tinfo.end(), [&](const TransactionInfo * ti) { return ti->hash() == id; }); return itr != m_tinfo.end() ? *itr : nullptr; } void TransactionHistory::refresh(quint32 accountIndex) { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QDateTime firstDateTime = QDate(2014, 4, 18).startOfDay(); #else QDateTime firstDateTime = QDateTime(QDate(2014, 4, 18)); // the genesis block #endif QDateTime lastDateTime = QDateTime::currentDateTime().addDays(1); // tomorrow (guard against jitter and timezones) emit refreshStarted(); { QWriteLocker locker(&m_lock); qDeleteAll(m_tinfo); m_tinfo.clear(); quint64 lastTxHeight = 0; m_locked = false; m_minutesToUnlock = 0; m_pimpl->refresh(); for (const auto i : m_pimpl->getAll()) { if (i->subaddrAccount() != accountIndex) { continue; } m_tinfo.append(new TransactionInfo(i, this)); const TransactionInfo *ti = m_tinfo.back(); // looking for transactions timestamp scope if (ti->timestamp() >= lastDateTime) { lastDateTime = ti->timestamp(); } if (ti->timestamp() <= firstDateTime) { firstDateTime = ti->timestamp(); } quint64 requiredConfirmations = (ti->blockHeight() < ti->unlockTime()) ? ti->unlockTime() - ti->blockHeight() : 10; // store last tx height if (ti->confirmations() < requiredConfirmations && ti->blockHeight() >= lastTxHeight) { lastTxHeight = ti->blockHeight(); // TODO: Fetch block time and confirmations needed from wallet2? m_minutesToUnlock = (requiredConfirmations - ti->confirmations()) * 2; m_locked = true; } } } emit refreshFinished(); if (m_firstDateTime != firstDateTime) { m_firstDateTime = firstDateTime; emit firstDateTimeChanged(); } if (m_lastDateTime != lastDateTime) { m_lastDateTime = lastDateTime; emit lastDateTimeChanged(); } } void TransactionHistory::setTxNote(const QString &txid, const QString ¬e) { m_pimpl->setTxNote(txid.toStdString(), note.toStdString()); this->refresh(0); // todo: get actual account index emit txNoteChanged(); } quint64 TransactionHistory::count() const { QReadLocker locker(&m_lock); return m_tinfo.count(); } QDateTime TransactionHistory::firstDateTime() const { return m_firstDateTime; } QDateTime TransactionHistory::lastDateTime() const { return m_lastDateTime; } quint64 TransactionHistory::minutesToUnlock() const { return m_minutesToUnlock; } bool TransactionHistory::TransactionHistory::locked() const { return m_locked; } TransactionHistory::TransactionHistory(Monero::TransactionHistory *pimpl, QObject *parent) : QObject(parent), m_pimpl(pimpl), m_minutesToUnlock(0), m_locked(false) { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) m_firstDateTime = QDate(2014, 4, 18).startOfDay(); #else m_firstDateTime = QDateTime(QDate(2014, 4, 18)); // the genesis block #endif m_lastDateTime = QDateTime::currentDateTime().addDays(1); // tomorrow (guard against jitter and timezones) } bool TransactionHistory::writeCSV(const QString &path) { auto data = QString(""); QReadLocker locker(&m_lock); for (const auto &tx : m_pimpl->getAll()) { if (tx->subaddrAccount() != 0) { // only account 0 continue; } TransactionInfo info(tx, this); // collect column data QDateTime timeStamp = info.timestamp(); double amount = info.amount(); // calc historical fiat price QString fiatAmount; QString preferredFiatSymbol = config()->get(Config::preferredFiatCurrency).toString(); const double usd_price = AppContext::txFiatHistory->get(timeStamp.toString("yyyyMMdd")); double fiat_price = usd_price * amount; if(preferredFiatSymbol != "USD") fiat_price = AppContext::prices->convert("USD", preferredFiatSymbol, fiat_price); double fiat_rounded = ceil(Utils::roundSignificant(fiat_price, 3) * 100.0) / 100.0; if(fiat_price != 0) fiatAmount = QString("%1 %2").arg(QString::number(fiat_rounded)).arg(preferredFiatSymbol); // collect some more column data quint64 atomicAmount = info.atomicAmount(); quint32 subaddrAccount = info.subaddrAccount(); QString fee = info.fee(); QString direction = QString(""); TransactionInfo::Direction _direction = info.direction(); if(_direction == TransactionInfo::Direction_In) direction = QString("in"); else if(_direction == TransactionInfo::Direction_Out) direction = QString("out"); else continue; // skip TransactionInfo::Direction_Both QString label = info.label(); label.remove(QChar('"')); // reserved quint64 blockHeight = info.blockHeight(); QString date = info.date() + " " + info.time(); uint epoch = timeStamp.toTime_t(); QString displayAmount = info.displayAmount(); QString paymentId = info.paymentId(); if(paymentId == "0000000000000000") paymentId = ""; // format and write QString line = QString("%1,%2,%3,%4,%5,\"%6\",%7,%8,%9,\"%10\",%11,%12\n") .arg(QString::number(blockHeight), QString::number(epoch), date) .arg(direction, QString::number(amount), fiatAmount, QString::number(atomicAmount)) .arg(info.fee(), info.hash(), label, QString::number(subaddrAccount)) .arg(paymentId); data += line; } if(data.isEmpty()) return false; data = QString("blockHeight,epoch,date,direction,amount,fiat,atomicAmount,fee,txid,label,subaddrAccount,paymentId\n%1").arg(data); return Utils::fileWrite(path, data); } QJsonArray TransactionHistory::toJsonArray(){ QJsonArray return_array; for (const auto &tx : m_pimpl->getAll()) { if (tx->subaddrAccount() != 0) { // only account 0 continue; } TransactionInfo info(tx, this); // collect column data QDateTime timeStamp = info.timestamp(); double amount = info.amount(); // calc historical fiat price QString fiatAmount; QString preferredCur = config()->get(Config::preferredFiatCurrency).toString(); const double usd_price = AppContext::txFiatHistory->get(timeStamp.toString("yyyyMMdd")); double fiat_price = usd_price * amount; if(preferredCur != "USD") fiat_price = AppContext::prices->convert("USD", preferredCur, fiat_price); double fiat_rounded = ceil(Utils::roundSignificant(fiat_price, 3) * 100.0) / 100.0; if(fiat_price != 0) fiatAmount = QString("%1 %2").arg(QString::number(fiat_rounded)).arg(preferredCur); // collect some more column data quint64 atomicAmount = info.atomicAmount(); quint32 subaddrAccount = info.subaddrAccount(); QString fee = info.fee(); QString direction = QString(""); TransactionInfo::Direction _direction = info.direction(); if(_direction == TransactionInfo::Direction_In) direction = QString("in"); else if(_direction == TransactionInfo::Direction_Out) direction = QString("out"); else continue; // skip TransactionInfo::Direction_Both QString label = info.label(); quint64 blockHeight = info.blockHeight(); QString date = info.date() + " " + info.time(); uint epoch = timeStamp.toTime_t(); QString displayAmount = info.displayAmount(); QString paymentId = info.paymentId(); if(paymentId == "0000000000000000") paymentId = ""; QJsonObject tx_item; tx_item["timestamp"] = (int) epoch; tx_item["date"] = date; tx_item["preferred_currency"] = preferredCur; tx_item["direction"] = direction; tx_item["blockheight"] = (int) blockHeight; tx_item["description"] = label; tx_item["subaddress_account"] = (int) subaddrAccount; tx_item["payment_id"] = paymentId; tx_item["amount"] = amount; tx_item["amount_display"] = displayAmount; tx_item["amount_fiat"] = fiatAmount; tx_item["fiat_rounded"] = fiat_rounded; tx_item["fiat_price"] = fiat_price; tx_item["fee"] = fee; return_array.append(tx_item); } return return_array; }