#pragma once // Pure period-earnings aggregation for the Balance tab's inline earnings figures. Header-only and // dependency-free (beyond TransactionInfo + the std lib) so it can be unit-tested without the GUI. // // It sums wallet INFLOWS (received + mined) over the CURRENT calendar period (hour / day / month / // year), optionally NET of sends, both per receiving address and wallet-wide. Values are "as seen" // (no confirmation/maturity gate), matching the balances the wallet already shows. // // Attribution is best-effort: inflows attribute to TransactionInfo::address (our receiving address); // in Net mode a send debits TransactionInfo::from_address when present (our source address). A send // with no from_address still counts in the wallet TOTAL but not in any per-address figure, so the // per-address numbers can be less complete than the total in Net mode. #include #include #include #include #include #include #include #include "data/wallet_state.h" // TransactionInfo namespace dragonx { namespace data { enum class EarningsInterval { Hour = 0, Day = 1, Month = 2, Year = 3 }; enum class EarningsMode { Inflows = 0, Net = 1 }; // Start (Unix seconds, local calendar) of the current period containing `nowSec` for `interval`: // Hour = top of the current clock hour; Day = local midnight; Month/Year = the 1st of the local // calendar month / Jan 1 of the local year. DST quirks follow std::mktime (as LocalDayStartMemo does). inline std::int64_t earningsPeriodStart(std::int64_t nowSec, EarningsInterval interval) { std::time_t t = (std::time_t)nowSec; std::tm lt{}; #if defined(_WIN32) localtime_s(<, &t); #else localtime_r(&t, <); #endif lt.tm_sec = 0; lt.tm_min = 0; // all intervals zero out seconds + minutes if (interval != EarningsInterval::Hour) lt.tm_hour = 0; // Day/Month/Year if (interval == EarningsInterval::Month || interval == EarningsInterval::Year) lt.tm_mday = 1; if (interval == EarningsInterval::Year) lt.tm_mon = 0; // January lt.tm_isdst = -1; // let mktime resolve DST for the normalized time return (std::int64_t)std::mktime(<); } struct EarningsResult { double total = 0.0; // wallet-wide earnings for the period (mode applied) std::map perAddress; // per receiving (Inflows) / source-debited (Net) address int inflowCount = 0; // inflow txs counted in the period }; // Sum earnings over the current `interval` period. Inflows = received + mined (>= 0). Net also // subtracts sends (so the total can be negative). `nowSec` is passed in for testability. inline EarningsResult computeEarnings(const std::vector& txs, std::int64_t nowSec, EarningsInterval interval, EarningsMode mode) { EarningsResult r; const std::int64_t start = earningsPeriodStart(nowSec, interval); const bool net = (mode == EarningsMode::Net); double totalIn = 0.0, totalOut = 0.0; for (const auto& tx : txs) { if (tx.timestamp < start) continue; if (tx.type == "receive" || tx.type == "mined") { const double a = std::fabs(tx.amount); totalIn += a; r.inflowCount++; if (!tx.address.empty()) r.perAddress[tx.address] += a; } else if (net && tx.type == "send") { const double a = std::fabs(tx.amount); totalOut += a; if (!tx.from_address.empty()) r.perAddress[tx.from_address] -= a; } } r.total = net ? (totalIn - totalOut) : totalIn; return r; } // Convenience: the earnings for a single address in a result (0 if the address earned nothing). inline double earningsForAddress(const EarningsResult& r, const std::string& address) { auto it = r.perAddress.find(address); return (it != r.perAddress.end()) ? it->second : 0.0; } // Every input the result depends on: equal keys => the cached result is still valid. `periodStart` is // computed by the caller (earningsPeriodStart) so a period roll-over invalidates the cache. struct EarningsKey { std::uint64_t txRevision = 0; // WalletState::tx_revision (bumped by every write to transactions) std::size_t txCount = 0; // belt: a writer that skips the bump but changes the row count EarningsInterval interval = EarningsInterval::Day; EarningsMode mode = EarningsMode::Inflows; std::int64_t periodStart = 0; bool operator==(const EarningsKey& o) const { return txRevision == o.txRevision && txCount == o.txCount && interval == o.interval && mode == o.mode && periodStart == o.periodStart; } }; // Revision-keyed cache (mirrors ui::MiningEarningsCache): the O(transactions) pass reruns only when an // input changes, plus a staleness safety net for a future writer that skips the revision bump. class EarningsCache { public: static constexpr std::int64_t kMaxAgeMs = 2000; const EarningsResult& get(const EarningsKey& key, const std::vector& txs, std::int64_t nowSec, std::int64_t nowMs) { if (!valid_ || !(key_ == key) || (nowMs - computedAtMs_) > kMaxAgeMs) { result_ = computeEarnings(txs, nowSec, key.interval, key.mode); key_ = key; valid_ = true; computedAtMs_ = nowMs; computes_++; } return result_; } int computeCount() const { return computes_; } // tests private: bool valid_ = false; EarningsKey key_; std::int64_t computedAtMs_ = 0; EarningsResult result_; int computes_ = 0; }; } // namespace data } // namespace dragonx