feat(portfolio): persisted portfolio entries + pure sum helpers
Foundation for the Market tab's configurable portfolio (custom labels tied to
groups of addresses):
- Settings gains a PortfolioEntry {label, addresses[]} list, persisted in
settings.json as a JSON array-of-objects (same mechanism as lite_servers_),
with get/setPortfolioEntries().
- data/portfolio.h (header-only, ImGui-free): SumPortfolioBalance() sums a
group's DRGX balance against the live per-address list (unknown addresses
contribute 0), plus PortfolioEntryContains/Add/Remove group helpers.
- Unit-tested (testPortfolioHelpers): empty/known/unknown-address sums, and the
add/remove/contains membership semantics.
UI (editor dialog, Market card rework, Overview right-click) comes next.
Full-node + lite build clean; ctest green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -294,6 +294,19 @@ bool Settings::load(const std::string& path)
|
||||
for (const auto& w : j["saved_pool_workers"])
|
||||
if (w.is_string()) saved_pool_workers_.push_back(w.get<std::string>());
|
||||
}
|
||||
if (j.contains("portfolio_entries") && j["portfolio_entries"].is_array()) {
|
||||
portfolio_entries_.clear();
|
||||
for (const auto& e : j["portfolio_entries"]) {
|
||||
if (!e.is_object()) continue;
|
||||
PortfolioEntry entry;
|
||||
if (e.contains("label") && e["label"].is_string())
|
||||
entry.label = e["label"].get<std::string>();
|
||||
if (e.contains("addresses") && e["addresses"].is_array())
|
||||
for (const auto& a : e["addresses"])
|
||||
if (a.is_string()) entry.addresses.push_back(a.get<std::string>());
|
||||
if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
if (j.contains("font_scale") && j["font_scale"].is_number())
|
||||
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>()));
|
||||
if (j.contains("window_width") && j["window_width"].is_number_integer())
|
||||
@@ -437,6 +450,14 @@ bool Settings::save(const std::string& path)
|
||||
j["saved_pool_workers"] = json::array();
|
||||
for (const auto& w : saved_pool_workers_)
|
||||
j["saved_pool_workers"].push_back(w);
|
||||
j["portfolio_entries"] = json::array();
|
||||
for (const auto& e : portfolio_entries_) {
|
||||
json entry;
|
||||
entry["label"] = e.label;
|
||||
entry["addresses"] = json::array();
|
||||
for (const auto& a : e.addresses) entry["addresses"].push_back(a);
|
||||
j["portfolio_entries"].push_back(std::move(entry));
|
||||
}
|
||||
j["font_scale"] = font_scale_;
|
||||
j["settings_version"] = std::string(DRAGONX_VERSION);
|
||||
if (window_width_ > 0 && window_height_ > 0) {
|
||||
|
||||
@@ -73,6 +73,13 @@ public:
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
// A user-defined portfolio entry: a custom label tied to a group of wallet addresses.
|
||||
// The Market tab's portfolio card sums these addresses' balances under the label.
|
||||
struct PortfolioEntry {
|
||||
std::string label;
|
||||
std::vector<std::string> addresses;
|
||||
};
|
||||
|
||||
// Theme
|
||||
std::string getTheme() const { return theme_; }
|
||||
void setTheme(const std::string& theme) { theme_ = theme; }
|
||||
@@ -251,6 +258,10 @@ public:
|
||||
const std::vector<LiteServerPreference>& getLiteServers() const { return lite_servers_; }
|
||||
void setLiteServers(const std::vector<LiteServerPreference>& servers) { lite_servers_ = servers; }
|
||||
|
||||
// User-defined portfolio entries (Market tab). "All funds" is implicit, not stored here.
|
||||
const std::vector<PortfolioEntry>& getPortfolioEntries() const { return portfolio_entries_; }
|
||||
void setPortfolioEntries(const std::vector<PortfolioEntry>& entries) { portfolio_entries_ = entries; }
|
||||
|
||||
// Lite servers the user has hidden from the Network tab (kept by URL, shown via a toggle).
|
||||
const std::set<std::string>& getLiteHiddenServers() const { return lite_hidden_servers_; }
|
||||
bool isLiteServerHidden(const std::string& url) const { return lite_hidden_servers_.count(url) > 0; }
|
||||
@@ -432,6 +443,8 @@ private:
|
||||
};
|
||||
std::set<std::string> lite_hidden_servers_; // server URLs hidden from the Network tab
|
||||
|
||||
std::vector<PortfolioEntry> portfolio_entries_; // Market tab custom portfolio groups
|
||||
|
||||
bool verbose_logging_ = false;
|
||||
std::set<std::string> debug_categories_;
|
||||
bool theme_effects_enabled_ = true;
|
||||
|
||||
61
src/data/portfolio.h
Normal file
61
src/data/portfolio.h
Normal file
@@ -0,0 +1,61 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Portfolio helpers — pure balance math for the Market tab's configurable portfolio.
|
||||
// A portfolio entry is a user label tied to a group of wallet addresses (persisted in
|
||||
// Settings as PortfolioEntry); these helpers sum/query that group against the live
|
||||
// per-address balance list. No I/O, no ImGui — unit-testable.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "wallet_state.h" // AddressInfo
|
||||
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace data {
|
||||
|
||||
// Sum the DRGX balance of `entryAddresses` by looking them up in the wallet's per-address
|
||||
// list. Addresses not present in the wallet contribute 0 (e.g. removed/rescanned). Pure.
|
||||
inline double SumPortfolioBalance(const std::vector<std::string>& entryAddresses,
|
||||
const std::vector<AddressInfo>& walletAddresses)
|
||||
{
|
||||
if (entryAddresses.empty()) return 0.0;
|
||||
std::unordered_set<std::string> want(entryAddresses.begin(), entryAddresses.end());
|
||||
double sum = 0.0;
|
||||
for (const auto& a : walletAddresses)
|
||||
if (want.count(a.address)) sum += a.balance;
|
||||
return sum;
|
||||
}
|
||||
|
||||
// Whether `address` is part of the entry's address group.
|
||||
inline bool PortfolioEntryContains(const std::vector<std::string>& entryAddresses,
|
||||
const std::string& address)
|
||||
{
|
||||
for (const auto& a : entryAddresses)
|
||||
if (a == address) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add `address` to the group if absent (returns true if it was added).
|
||||
inline bool PortfolioEntryAdd(std::vector<std::string>& entryAddresses, const std::string& address)
|
||||
{
|
||||
if (address.empty() || PortfolioEntryContains(entryAddresses, address)) return false;
|
||||
entryAddresses.push_back(address);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove `address` from the group if present (returns true if it was removed).
|
||||
inline bool PortfolioEntryRemove(std::vector<std::string>& entryAddresses, const std::string& address)
|
||||
{
|
||||
for (auto it = entryAddresses.begin(); it != entryAddresses.end(); ++it) {
|
||||
if (*it == address) { entryAddresses.erase(it); return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
} // namespace dragonx
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "wallet/lite_wallet_state_mapper.h"
|
||||
#include "config/settings.h"
|
||||
#include "data/wallet_state.h"
|
||||
#include "data/portfolio.h"
|
||||
#include "fake_lite_backend.h"
|
||||
|
||||
#include <chrono>
|
||||
@@ -2581,6 +2582,46 @@ void testConsoleModel()
|
||||
}
|
||||
}
|
||||
|
||||
// Portfolio helpers — sum/query a labeled address group against per-address balances.
|
||||
void testPortfolioHelpers()
|
||||
{
|
||||
using dragonx::data::AddressInfo;
|
||||
using dragonx::data::SumPortfolioBalance;
|
||||
using dragonx::data::PortfolioEntryContains;
|
||||
using dragonx::data::PortfolioEntryAdd;
|
||||
using dragonx::data::PortfolioEntryRemove;
|
||||
|
||||
std::vector<AddressInfo> wallet;
|
||||
auto mk = [](const char* addr, double bal, const char* type) {
|
||||
AddressInfo a; a.address = addr; a.balance = bal; a.type = type; return a;
|
||||
};
|
||||
wallet.push_back(mk("R-t1", 1.5, "transparent"));
|
||||
wallet.push_back(mk("R-t2", 2.0, "transparent"));
|
||||
wallet.push_back(mk("zs-s1", 10.0, "shielded"));
|
||||
wallet.push_back(mk("zs-s2", 0.25, "shielded"));
|
||||
|
||||
// Empty group sums to 0.
|
||||
EXPECT_NEAR(SumPortfolioBalance({}, wallet), 0.0, 1e-9);
|
||||
// A group of two known addresses.
|
||||
EXPECT_NEAR(SumPortfolioBalance({"R-t1", "zs-s1"}, wallet), 11.5, 1e-9);
|
||||
// Unknown addresses contribute 0 (not present in the wallet).
|
||||
EXPECT_NEAR(SumPortfolioBalance({"R-t2", "does-not-exist"}, wallet), 2.0, 1e-9);
|
||||
// All shielded.
|
||||
EXPECT_NEAR(SumPortfolioBalance({"zs-s1", "zs-s2"}, wallet), 10.25, 1e-9);
|
||||
|
||||
// Membership + add/remove semantics.
|
||||
std::vector<std::string> group = {"R-t1"};
|
||||
EXPECT_TRUE(PortfolioEntryContains(group, "R-t1"));
|
||||
EXPECT_FALSE(PortfolioEntryContains(group, "R-t2"));
|
||||
EXPECT_TRUE(PortfolioEntryAdd(group, "R-t2")); // added
|
||||
EXPECT_FALSE(PortfolioEntryAdd(group, "R-t2")); // already present -> no-op
|
||||
EXPECT_EQ(group.size(), static_cast<size_t>(2));
|
||||
EXPECT_TRUE(PortfolioEntryRemove(group, "R-t1")); // removed
|
||||
EXPECT_FALSE(PortfolioEntryRemove(group, "R-t1"));// gone -> no-op
|
||||
EXPECT_EQ(group.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(group[0], std::string("R-t2"));
|
||||
}
|
||||
|
||||
// ComputeConsoleFoldSpans — brace/bracket matching over pretty-printed JSON lines.
|
||||
void testConsoleFoldSpans()
|
||||
{
|
||||
@@ -5311,6 +5352,7 @@ int main()
|
||||
testDaemonLifecycleAdapters();
|
||||
testConsoleTextLayout();
|
||||
testConsoleModel();
|
||||
testPortfolioHelpers();
|
||||
testConsoleFoldSpans();
|
||||
testConsoleScrollController();
|
||||
testConsoleSelectionController();
|
||||
|
||||
Reference in New Issue
Block a user