// SPDX-License-Identifier: BSD-3-Clause // SPDX-FileCopyrightText: 2020-2023 The Monero Project #include "TransactionHistory.h" #include "utils/Utils.h" #include "utils/AppData.h" #include "utils/config.h" #include "constants.h" #include "WalletManager.h" #include "Transfer.h" #include "Ring.h" bool TransactionHistory::transaction(int index, std::function callback) { QReadLocker locker(&m_lock); if (index < 0 || index >= m_rows.size()) { qCritical("%s: no transaction info for index %d", __FUNCTION__, index); qCritical("%s: there's %d transactions in backend", __FUNCTION__, this->count()); return false; } callback(*m_rows.value(index)); return true; } TransactionRow* TransactionHistory::transaction(const QString &id) { QReadLocker locker(&m_lock); auto itr = std::find_if(m_rows.begin(), m_rows.end(), [&](const TransactionRow * ti) { return ti->hash() == id; }); return itr != m_rows.end() ? *itr : nullptr; } TransactionRow* TransactionHistory::transaction(int index) { if (index < 0 || index >= m_rows.size()) { return nullptr; } return m_rows[index]; } void TransactionHistory::refresh() { QDateTime firstDateTime = QDate(2014, 4, 18).startOfDay(); QDateTime lastDateTime = QDateTime::currentDateTime().addDays(1); // tomorrow (guard against jitter and timezones) emit refreshStarted(); { QWriteLocker locker(&m_lock); clearRows(); quint64 lastTxHeight = 0; m_locked = false; m_minutesToUnlock = 0; uint64_t min_height = 0; uint64_t max_height = (uint64_t)-1; uint64_t wallet_height = m_wallet->blockChainHeight(); uint32_t account = m_wallet->currentSubaddressAccount(); // transactions are stored in wallet2: // - confirmed_transfer_details - out transfers // - unconfirmed_transfer_details - pending out transfers // - payment_details - input transfers // payments are "input transactions"; // one input transaction contains only one transfer. e.g. - <100XMR> std::list> in_payments; m_wallet2->get_payments(in_payments, min_height, max_height); for (std::list>::const_iterator i = in_payments.begin(); i != in_payments.end(); ++i) { const tools::wallet2::payment_details &pd = i->second; if (pd.m_subaddr_index.major != account) { continue; } std::string payment_id = epee::string_tools::pod_to_hex(i->first); if (payment_id.substr(16).find_first_not_of('0') == std::string::npos) payment_id = payment_id.substr(0,16); auto* t = new TransactionRow(); t->m_paymentId = QString::fromStdString(payment_id); t->m_coinbase = pd.m_coinbase; t->m_amount = pd.m_amount; t->m_balanceDelta = pd.m_amount; t->m_fee = pd.m_fee; t->m_direction = TransactionRow::Direction_In; t->m_hash = QString::fromStdString(epee::string_tools::pod_to_hex(pd.m_tx_hash)); t->m_blockHeight = pd.m_block_height; t->m_description = QString::fromStdString(m_wallet2->get_tx_note(pd.m_tx_hash)); t->m_subaddrIndex = { pd.m_subaddr_index.minor }; t->m_subaddrAccount = pd.m_subaddr_index.major; t->m_label = QString::fromStdString(m_wallet2->get_subaddress_label(pd.m_subaddr_index)); t->m_timestamp = QDateTime::fromSecsSinceEpoch(pd.m_timestamp); t->m_confirmations = (wallet_height > pd.m_block_height) ? wallet_height - pd.m_block_height : 0; t->m_unlockTime = pd.m_unlock_time; m_rows.append(t); } // confirmed output transactions // one output transaction may contain more than one money transfer, e.g. // : // transfer1: 100XMR to // transfer2: 50XMR to // fee: fee charged per transaction // std::list> out_payments; m_wallet2->get_payments_out(out_payments, min_height, max_height); for (std::list>::const_iterator i = out_payments.begin(); i != out_payments.end(); ++i) { const crypto::hash &hash = i->first; const tools::wallet2::confirmed_transfer_details &pd = i->second; if (pd.m_subaddr_account != account) { continue; } uint64_t change = pd.m_change == (uint64_t)-1 ? 0 : pd.m_change; // change may not be known uint64_t fee = pd.m_amount_in - pd.m_amount_out; std::string payment_id = epee::string_tools::pod_to_hex(i->second.m_payment_id); if (payment_id.substr(16).find_first_not_of('0') == std::string::npos) payment_id = payment_id.substr(0,16); auto* t = new TransactionRow(); t->m_paymentId = QString::fromStdString(payment_id); t->m_amount = pd.m_amount_out - change; t->m_balanceDelta = change - pd.m_amount_in; t->m_fee = fee; t->m_direction = TransactionRow::Direction_Out; t->m_hash = QString::fromStdString(epee::string_tools::pod_to_hex(hash)); t->m_blockHeight = pd.m_block_height; t->m_description = QString::fromStdString(m_wallet2->get_tx_note(hash)); t->m_subaddrAccount = pd.m_subaddr_account; t->m_label = QString::fromStdString(pd.m_subaddr_indices.size() == 1 ? m_wallet2->get_subaddress_label({pd.m_subaddr_account, *pd.m_subaddr_indices.begin()}) : ""); t->m_timestamp = QDateTime::fromSecsSinceEpoch(pd.m_timestamp); t->m_confirmations = (wallet_height > pd.m_block_height) ? wallet_height - pd.m_block_height : 0; for (uint32_t idx : t->subaddrIndex()) { t->m_subaddrIndex.insert(idx); } // single output transaction might contain multiple transfers for (auto const &d: pd.m_dests) { Transfer *transfer = new Transfer(d.amount, QString::fromStdString(d.address(m_wallet2->nettype(), pd.m_payment_id)), this); t->m_transfers.append(transfer); } for (auto const &r: pd.m_rings) { Ring *ring = new Ring(QString::fromStdString(epee::string_tools::pod_to_hex(r.first)), cryptonote::relative_output_offsets_to_absolute(r.second), this); t->m_rings.append(ring); } m_rows.append(t); } // unconfirmed output transactions std::list> upayments_out; m_wallet2->get_unconfirmed_payments_out(upayments_out); for (std::list>::const_iterator i = upayments_out.begin(); i != upayments_out.end(); ++i) { const tools::wallet2::unconfirmed_transfer_details &pd = i->second; if (pd.m_subaddr_account != account) { continue; } const crypto::hash &hash = i->first; uint64_t amount = pd.m_amount_in; uint64_t fee = amount - pd.m_amount_out; uint64_t change = pd.m_change == (uint64_t)-1 ? 0 : pd.m_change; std::string payment_id = epee::string_tools::pod_to_hex(i->second.m_payment_id); if (payment_id.substr(16).find_first_not_of('0') == std::string::npos) payment_id = payment_id.substr(0,16); bool is_failed = pd.m_state == tools::wallet2::unconfirmed_transfer_details::failed; auto *t = new TransactionRow(); t->m_paymentId = QString::fromStdString(payment_id); t->m_amount = pd.m_amount_out - change; t->m_balanceDelta = change - pd.m_amount_in; t->m_fee = fee; t->m_direction = TransactionRow::Direction_Out; t->m_failed = is_failed; t->m_pending = true; t->m_hash = QString::fromStdString(epee::string_tools::pod_to_hex(hash)); t->m_description = QString::fromStdString(m_wallet2->get_tx_note(hash)); t->m_subaddrAccount = pd.m_subaddr_account; t->m_label = QString::fromStdString(pd.m_subaddr_indices.size() == 1 ? m_wallet2->get_subaddress_label({pd.m_subaddr_account, *pd.m_subaddr_indices.begin()}) : ""); t->m_timestamp = QDateTime::fromSecsSinceEpoch(pd.m_timestamp); t->m_confirmations = 0; for (uint32_t idx : t->subaddrIndex()) { t->m_subaddrIndex.insert(idx); } for (auto const &d: pd.m_dests) { Transfer *transfer = new Transfer(d.amount, QString::fromStdString(d.address(m_wallet2->nettype(), pd.m_payment_id)), this); t->m_transfers.append(transfer); } for (auto const &r: pd.m_rings) { Ring *ring = new Ring(QString::fromStdString(epee::string_tools::pod_to_hex(r.first)), cryptonote::relative_output_offsets_to_absolute(r.second), this); t->m_rings.append(ring); } m_rows.append(t); } // unconfirmed payments (tx pool) std::list> upayments; m_wallet2->get_unconfirmed_payments(upayments); for (std::list>::const_iterator i = upayments.begin(); i != upayments.end(); ++i) { const tools::wallet2::payment_details &pd = i->second.m_pd; if (pd.m_subaddr_index.major != account) { continue; } std::string payment_id = epee::string_tools::pod_to_hex(i->first); if (payment_id.substr(16).find_first_not_of('0') == std::string::npos) payment_id = payment_id.substr(0,16); auto *t = new TransactionRow(); t->m_paymentId = QString::fromStdString(payment_id); t->m_amount = pd.m_amount; t->m_balanceDelta = pd.m_amount; t->m_direction = TransactionRow::Direction_In; t->m_hash = QString::fromStdString(epee::string_tools::pod_to_hex(pd.m_tx_hash)); t->m_blockHeight = pd.m_block_height; t->m_description = QString::fromStdString(m_wallet2->get_tx_note(pd.m_tx_hash)); t->m_pending = true; t->m_subaddrIndex = { pd.m_subaddr_index.minor }; t->m_subaddrAccount = pd.m_subaddr_index.major; t->m_label = QString::fromStdString(m_wallet2->get_subaddress_label(pd.m_subaddr_index)); t->m_timestamp = QDateTime::fromSecsSinceEpoch(pd.m_timestamp); t->m_confirmations = 0; m_rows.append(t); LOG_PRINT_L1(__FUNCTION__ << ": Unconfirmed payment found " << pd.m_amount); } } emit refreshFinished(); } void TransactionHistory::setTxNote(const QString &txid, const QString ¬e) { cryptonote::blobdata txid_data; if(!epee::string_tools::parse_hexstr_to_binbuff(txid.toStdString(), txid_data) || txid_data.size() != sizeof(crypto::hash)) return; const crypto::hash htxid = *reinterpret_cast(txid_data.data()); m_wallet2->set_tx_note(htxid, note.toStdString()); refresh(); emit txNoteChanged(); } quint64 TransactionHistory::count() const { QReadLocker locker(&m_lock); return m_rows.length(); } 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(Wallet *wallet, tools::wallet2 *wallet2, QObject *parent) : QObject(parent) , m_wallet(wallet) , m_wallet2(wallet2) , 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) } void TransactionHistory::clearRows() { qDeleteAll(m_rows); m_rows.clear(); } bool TransactionHistory::writeCSV(const QString &path) { QString data; QReadLocker locker(&m_lock); auto transactions = m_rows; std::sort(m_rows.begin(), m_rows.end(), [](const TransactionRow *info1, const TransactionRow *info2){ return info1->blockHeight() < info2->blockHeight(); }); for (const auto &info : transactions) { // collect column data QDateTime timeStamp = info->timestamp(); double amount = info->amount(); // calc historical fiat price QString fiatAmount; QString preferredFiatSymbol = conf()->get(Config::preferredFiatCurrency).toString(); const double usd_price = appData()->txFiatHistory->get(timeStamp.toString("yyyyMMdd")); double fiat_price = usd_price * amount; if (preferredFiatSymbol != "USD") fiat_price = appData()->prices.convert("USD", preferredFiatSymbol, fiat_price); double fiat_rounded = ceil(Utils::roundSignificant(fiat_price, 3) * 100.0) / 100.0; if (usd_price != 0) fiatAmount = QString::number(fiat_rounded); else fiatAmount = "\"?\""; QString direction = QString(""); if (info->direction() == TransactionRow::Direction_In) direction = QString("in"); else if (info->direction() == TransactionRow::Direction_Out) direction = QString("out"); else continue; // skip TransactionInfo::Direction_Both QString displayAmount = info->displayAmount(); QString paymentId = info->paymentId(); if (paymentId == "0000000000000000") { paymentId = ""; } QString date = QString("%1T%2Z").arg(info->date(), info->time()); // ISO 8601 QString balanceDelta = WalletManager::displayAmount(info->balanceDelta()); if (info->direction() == TransactionRow::Direction_Out) { balanceDelta = "-" + balanceDelta; } // format and write QString line = QString("\n%1,%2,\"%3\",%4,\"%5\",%6,%7,%8,\"%9\",\"%10\",\"%11\",%12,\"%13\"") .arg(QString::number(info->blockHeight()), QString::number(timeStamp.toSecsSinceEpoch()), date, QString::number(info->subaddrAccount()), direction, balanceDelta, info->displayAmount(), info->fee(), info->hash(), info->description(), paymentId, fiatAmount, preferredFiatSymbol); data += line; } if (data.isEmpty()) return false; data = QString("blockHeight,timestamp,date,accountIndex,direction,balanceDelta,amount,fee,txid,description,paymentId,fiatAmount,fiatCurrency%1").arg(data); return Utils::fileWrite(path, data); }