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:
2026-10-12 21:42:23 -05:00
co-authored by Claude Opus 4.8
parent c3ffc97b31
commit 8255923f33
8 changed files with 159 additions and 32 deletions
+10
View File
@@ -343,6 +343,15 @@ public:
// except during a full UI sweep (capture_mode_), where it returns the existing id or "".
std::string activeWalletScopeId();
// Stable scope for the Market-tab portfolio groups: activeWalletScopeId() once this wallet's addresses are
// loaded, else "" (so a group never shows a 0.0000 balance from an empty list, and Add stays disabled
// until the wallet is known - the same window the old identity-hash gate covered). Never the address hash.
// In a full UI sweep (capture_mode_) a fixed demo id so the sweep can still exercise Add.
std::string portfolioScopeId();
// One-time (per wallet per session) migration of legacy hash-scoped portfolio groups to the stable id.
// Called from update(); a no-op until the wallet's addresses are loaded.
void reattachLegacyPortfolioScopes();
data::WalletIndex& walletIndex() { return wallet_index_; }
const data::WalletIndex& walletIndex() const { return wallet_index_; }
@@ -1792,6 +1801,7 @@ private:
// Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan
// that reconciles the preserved wallet.dat against the freshly-imported chain.
bool post_bootstrap_rescan_pending_ = false;
std::string portfolio_scope_migrated_for_; // scope id the legacy-portfolio-scope migration last ran for
bool post_bootstrap_probe_in_flight_ = false; // its detectLowestAvailableBlockHeight() is on worker_
int post_bootstrap_probe_attempts_ = 0; // probes that could not tell; gives up after 6
std::chrono::steady_clock::time_point post_bootstrap_probe_retry_at_{};