fix(settings): scope portfolio groups by wallet identity
Market-tab portfolio groups were tied to a wallet by a hash of its address set. That hash changed whenever an address was added, so a group built for a wallet could silently drop out of view once the wallet gained a new address. - Groups are now scoped by the wallet's stable id, which is persisted and never derived from the address set, so adding an address can no longer orphan a group. - An older build's address-hash scope fails open (the group stays visible) and is migrated once per wallet per session to the stable id. A legacy group is claimed when its old hash matches, when one of its own addresses belongs to this wallet, or when only one wallet is known; anything ambiguous is left legacy and stays visible. - Scope filtering waits until the wallet's addresses are loaded, so a group never flashes an empty 0.0000 balance and Add stays disabled until the wallet is resolved. This works on Lite, which never stamps an address-update time. Behaviour: an older build opened after this change hides groups tagged with a stable "w:" scope (they are not deleted and return on the next upgrade) - worth a release note. Tests: group visibility across global, stable and legacy scopes with the wallet resolved and not; the migration rules (hash match, address overlap, single-wallet claim, ambiguous left alone, idempotent); and a save/load round trip where an address-set change cannot orphan the scope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -371,30 +371,30 @@ static void pfCommitIfNeeded(App* app)
|
||||
if (existing) { base = entries[s_pfEdit.sel]; if (pfWorkingMatches(base)) return; }
|
||||
auto e = pfBuildWorking(base);
|
||||
if (e.scope.empty()) {
|
||||
const std::string h = app->activeWalletIdentityHash();
|
||||
if (!h.empty()) e.scope = h;
|
||||
const std::string sid = app->portfolioScopeId();
|
||||
if (!sid.empty()) e.scope = sid;
|
||||
}
|
||||
if (existing) entries[s_pfEdit.sel] = e;
|
||||
else { entries.push_back(e); s_pfEdit.sel = (int)entries.size() - 1; }
|
||||
pfPersist(settings, entries);
|
||||
}
|
||||
|
||||
// A group is visible to the active wallet when it carries the wallet's scope, is unscoped (legacy —
|
||||
// shown everywhere), or the wallet identity isn't resolved yet. The editor mirrors the Market summary's
|
||||
// scope filter so it only lists/edits the current wallet's groups.
|
||||
// A group is visible to the active wallet when it carries the wallet's stable scope id, is unscoped (global,
|
||||
// shown everywhere), or still carries a legacy address-hash scope (fail-open until migrated). Nothing but
|
||||
// global groups is visible while the wallet id is unresolved. The editor mirrors the Market summary's filter
|
||||
// so it only lists/edits the current wallet's groups.
|
||||
static bool pfIndexVisible(App* app, int i)
|
||||
{
|
||||
const auto& es = app->settings()->getPortfolioEntries();
|
||||
if (i < 0 || i >= (int)es.size()) return false;
|
||||
const std::string h = app->activeWalletIdentityHash();
|
||||
return es[i].scope.empty() || (!h.empty() && es[i].scope == h);
|
||||
return es[i].visibleInWallet(app->portfolioScopeId());
|
||||
}
|
||||
static int pfFirstVisibleIndex(App* app)
|
||||
{
|
||||
const auto& es = app->settings()->getPortfolioEntries();
|
||||
const std::string h = app->activeWalletIdentityHash();
|
||||
const std::string sid = app->portfolioScopeId();
|
||||
for (int i = 0; i < (int)es.size(); i++)
|
||||
if (es[i].scope.empty() || (!h.empty() && es[i].scope == h)) return i;
|
||||
if (es[i].visibleInWallet(sid)) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -831,12 +831,12 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImDrawList* mdl = ImGui::GetWindowDrawList();
|
||||
// Only list groups scoped to the active wallet (or legacy/unscoped) — same filter the
|
||||
// Market summary uses. `i` stays the storage index (selection/delete operate on storage).
|
||||
const std::string activeHash = app->activeWalletIdentityHash();
|
||||
const std::string scopeId = app->portfolioScopeId();
|
||||
std::vector<int> vis;
|
||||
for (int i = 0; i < (int)entries.size(); i++)
|
||||
// Global groups always; this wallet's groups only when the identity is known — never
|
||||
// another wallet's scoped groups during the switch/first-load window.
|
||||
if (entries[i].scope.empty() || (!activeHash.empty() && entries[i].scope == activeHash))
|
||||
// Global groups always; this wallet's (and not-yet-migrated legacy) groups only once the wallet
|
||||
// is resolved - never another wallet's scoped groups during the switch/first-load window.
|
||||
if (entries[i].visibleInWallet(scopeId))
|
||||
vis.push_back(i);
|
||||
if (vis.empty())
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
||||
@@ -935,10 +935,10 @@ static void RenderPortfolioEditor(App* app)
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
// Add immediately creates a persisted "Untitled" group scoped to this wallet — so it's disabled
|
||||
// while the wallet identity is unknown (switch / first-load window), otherwise a new group would
|
||||
// Add immediately creates a persisted "Untitled" group scoped to this wallet - so it's disabled
|
||||
// while the wallet is unresolved (switch / first-load window), otherwise a new group would
|
||||
// get an empty scope and show under every wallet.
|
||||
const bool pfIdentityKnown = !app->activeWalletIdentityHash().empty();
|
||||
const bool pfIdentityKnown = !app->portfolioScopeId().empty();
|
||||
ImGui::BeginDisabled(!pfIdentityKnown);
|
||||
if (material::TactileButton(TR("portfolio_add_entry"), ImVec2(masterW, addH))) {
|
||||
// Auto-save the current group, then start a fresh one selected for editing.
|
||||
@@ -946,7 +946,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
auto es = settings->getPortfolioEntries();
|
||||
config::Settings::PortfolioEntry ne;
|
||||
ne.label = TR("portfolio_untitled");
|
||||
ne.scope = app->activeWalletIdentityHash(); // new group defaults to this wallet
|
||||
ne.scope = app->portfolioScopeId(); // new group defaults to this wallet (stable id)
|
||||
es.push_back(ne);
|
||||
pfPersist(settings, es);
|
||||
s_pfEdit.sel = (int)es.size() - 1;
|
||||
@@ -2113,14 +2113,15 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
// arrows switch styles (handled in RenderMarketTab). Click a row to edit it. ----
|
||||
{
|
||||
const auto& allEntries = app->settings()->getPortfolioEntries();
|
||||
const std::string activeHash = app->activeWalletIdentityHash();
|
||||
// Visible = this wallet's groups + legacy (empty scope). Keep storage indices for edit targets.
|
||||
const std::string scopeId = app->portfolioScopeId();
|
||||
// Visible = this wallet's groups + global (empty scope) + not-yet-migrated legacy. Keep storage indices
|
||||
// for edit targets.
|
||||
std::vector<int> vis;
|
||||
for (int i = 0; i < (int)allEntries.size(); i++) {
|
||||
const auto& e = allEntries[i];
|
||||
// Global groups always; this wallet's only when the identity is known (never another
|
||||
// wallet's scoped groups during the switch/first-load window).
|
||||
if (e.scope.empty() || (!activeHash.empty() && e.scope == activeHash)) vis.push_back(i);
|
||||
// Global groups always; this wallet's only once the wallet is resolved (never another wallet's
|
||||
// scoped groups during the switch/first-load window).
|
||||
if (e.visibleInWallet(scopeId)) vis.push_back(i);
|
||||
}
|
||||
|
||||
int style = app->settings() ? app->settings()->getPortfolioStyle() : 0;
|
||||
@@ -2315,11 +2316,11 @@ void RenderMarketTab(App* app)
|
||||
+ Layout::spacingMd(); // gap before the group cards
|
||||
// Content height = full-width rows in the selected style, confined to a bounded area (up to
|
||||
// pfMaxVisibleRows) with an internal scroll when the visible groups overflow. Only groups
|
||||
// visible in the active wallet (own scope or legacy/global) count toward the height.
|
||||
const std::string pfActiveHash = app->activeWalletIdentityHash();
|
||||
// visible in the active wallet (own scope, global or not-yet-migrated legacy) count toward the height.
|
||||
const std::string pfScopeId = app->portfolioScopeId();
|
||||
int pfVisN = 0;
|
||||
for (const auto& e : pfEntriesGeo)
|
||||
if (e.scope.empty() || (!pfActiveHash.empty() && e.scope == pfActiveHash)) pfVisN++;
|
||||
if (e.visibleInWallet(pfScopeId)) pfVisN++;
|
||||
int pfStyle = app->settings()->getPortfolioStyle();
|
||||
float pfRowH = pfRowHeight(pfStyle, mktDp);
|
||||
float pfRowGap = pfRowGapFor(pfStyle, mktDp); // Table rows abut
|
||||
|
||||
Reference in New Issue
Block a user