feat(data): period-earnings aggregation for the Balance tab
Adds src/data/earnings_series.h: a pure, header-only aggregator that 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, at "as seen" values. Attribution is best-effort: inflows map to the receiving address; in net mode a send debits from_address when present (a send with no from_address still counts in the wallet total). Includes a revision-keyed EarningsCache (mirrors ui::MiningEarningsCache) and a 13-assertion unit test covering inflows vs net, per-address attribution, period exclusion, period nesting, and cache invalidation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
#include "util/secure_vault.h"
|
||||
#include "daemon/lifecycle_adapters.h"
|
||||
#include "data/wallet_state.h"
|
||||
#include "data/earnings_series.h"
|
||||
#include "rpc/connection.h"
|
||||
#include "rpc/rpc_failure.h"
|
||||
#include "rpc/rpc_busy_gate.h"
|
||||
@@ -18808,6 +18809,83 @@ void testReadFileExact()
|
||||
fs::remove_all(dir, ec);
|
||||
}
|
||||
|
||||
// ---- Period-earnings aggregation (data/earnings_series.h) ----
|
||||
void testEarningsSeries() {
|
||||
using namespace dragonx::data;
|
||||
using dragonx::TransactionInfo;
|
||||
|
||||
const std::int64_t now = 1700000000; // fixed epoch; periods derived from the function under test
|
||||
|
||||
auto mk = [](const char* type, double amt, std::int64_t ts, const char* addr, const char* from) {
|
||||
TransactionInfo t; t.type = type; t.amount = amt; t.timestamp = ts;
|
||||
t.address = addr; t.from_address = from; return t;
|
||||
};
|
||||
|
||||
// Day period: inflows vs net, per-address attribution, yesterday excluded.
|
||||
{
|
||||
const std::int64_t dayStart = earningsPeriodStart(now, EarningsInterval::Day);
|
||||
EXPECT_TRUE(dayStart <= now);
|
||||
EXPECT_TRUE(now - dayStart < 24 * 3600);
|
||||
|
||||
std::vector<TransactionInfo> txs;
|
||||
txs.push_back(mk("receive", 2.0, dayStart + 10, "zA", ""));
|
||||
txs.push_back(mk("mined", 1.0, dayStart + 20, "zMine", ""));
|
||||
txs.push_back(mk("send", -0.5, dayStart + 30, "zB", "zA")); // stored negative; counterparty in address
|
||||
txs.push_back(mk("receive", 99.0, dayStart - 10, "zA", "")); // yesterday: excluded
|
||||
|
||||
EarningsResult inflow = computeEarnings(txs, now, EarningsInterval::Day, EarningsMode::Inflows);
|
||||
EXPECT_NEAR(inflow.total, 3.0, 1e-9);
|
||||
EXPECT_EQ(inflow.inflowCount, 2);
|
||||
EXPECT_NEAR(earningsForAddress(inflow, "zA"), 2.0, 1e-9);
|
||||
EXPECT_NEAR(earningsForAddress(inflow, "zMine"), 1.0, 1e-9);
|
||||
EXPECT_NEAR(earningsForAddress(inflow, "zB"), 0.0, 1e-9); // sends not attributed in Inflows
|
||||
|
||||
EarningsResult net = computeEarnings(txs, now, EarningsInterval::Day, EarningsMode::Net);
|
||||
EXPECT_NEAR(net.total, 2.5, 1e-9); // 3 - 0.5
|
||||
EXPECT_NEAR(earningsForAddress(net, "zA"), 1.5, 1e-9); // +2 received - 0.5 sent-from
|
||||
EXPECT_NEAR(earningsForAddress(net, "zMine"), 1.0, 1e-9);
|
||||
}
|
||||
|
||||
// Hour period excludes a tx from the previous hour.
|
||||
{
|
||||
const std::int64_t hourStart = earningsPeriodStart(now, EarningsInterval::Hour);
|
||||
EXPECT_TRUE(now - hourStart < 3600);
|
||||
std::vector<TransactionInfo> txs;
|
||||
txs.push_back(mk("receive", 5.0, hourStart + 1, "z1", ""));
|
||||
txs.push_back(mk("receive", 7.0, hourStart - 1, "z1", ""));
|
||||
EarningsResult r = computeEarnings(txs, now, EarningsInterval::Hour, EarningsMode::Inflows);
|
||||
EXPECT_NEAR(r.total, 5.0, 1e-9);
|
||||
}
|
||||
|
||||
// Period starts nest: year <= month <= day <= hour <= now.
|
||||
{
|
||||
std::int64_t y = earningsPeriodStart(now, EarningsInterval::Year);
|
||||
std::int64_t m = earningsPeriodStart(now, EarningsInterval::Month);
|
||||
std::int64_t d = earningsPeriodStart(now, EarningsInterval::Day);
|
||||
std::int64_t h = earningsPeriodStart(now, EarningsInterval::Hour);
|
||||
EXPECT_TRUE(y <= m); EXPECT_TRUE(m <= d); EXPECT_TRUE(d <= h); EXPECT_TRUE(h <= now);
|
||||
}
|
||||
|
||||
// Cache recomputes only on key change / period roll-over (not on a within-window re-get).
|
||||
{
|
||||
std::vector<TransactionInfo> txs;
|
||||
txs.push_back(mk("receive", 1.0, now, "z1", ""));
|
||||
EarningsCache cache;
|
||||
EarningsKey k;
|
||||
k.txRevision = 1; k.txCount = txs.size(); k.interval = EarningsInterval::Day;
|
||||
k.mode = EarningsMode::Inflows; k.periodStart = earningsPeriodStart(now, EarningsInterval::Day);
|
||||
cache.get(k, txs, now, 1000);
|
||||
cache.get(k, txs, now, 1500); // same key, within kMaxAgeMs -> no recompute
|
||||
EXPECT_EQ(cache.computeCount(), 1);
|
||||
EarningsKey k2 = k; k2.txRevision = 2; // tx changed -> recompute
|
||||
cache.get(k2, txs, now, 1600);
|
||||
EXPECT_EQ(cache.computeCount(), 2);
|
||||
EarningsKey k3 = k2; k3.periodStart += 86400; // period rolled over -> recompute
|
||||
cache.get(k3, txs, now, 1700);
|
||||
EXPECT_EQ(cache.computeCount(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
testConnectionConfig();
|
||||
@@ -19134,6 +19212,7 @@ int main()
|
||||
testCaptureExtent();
|
||||
testResolveExchangeSelection();
|
||||
testTransactionCsv();
|
||||
testEarningsSeries();
|
||||
|
||||
if (g_failures != 0) {
|
||||
std::cerr << g_failures << " test assertion(s) failed\n";
|
||||
|
||||
Reference in New Issue
Block a user