feat(wallet): per-wallet data isolation foundation (P1)

First phase of multi-wallet support: keep each wallet's data separate so loading
a different wallet no longer shows the previous one's data, and lay the metadata
groundwork for the wallet-files list.

- Address book scoped per wallet: AddressBookEntry gains a "scope" ("global" or a
  wallet-identity hash); the Contacts tab shows global + current-wallet contacts
  (App::activeWalletIdentityHash) with a "show in every wallet" toggle + globe
  badge. Legacy entries migrate to "global". Scope-aware de-dup allows the same
  address across different wallets.
- Wallet metadata index (data/wallet_index -> wallets.json): file-keyed cache of
  balance / address count / identity / size / last-opened / synced-here, since
  those can't be read off a wallet.dat without loading it. Populated on connect +
  address refresh (change-detecting upsert). Plus an active_wallet_file setting
  for the -wallet=<name> switch coming in P2.

Unit-tested (testAddressBookScope, testWalletIndex): migration, visibility filter,
scope-aware de-dup, upsert change-detection, save/reload round-trip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 15:57:23 -05:00
co-authored by Claude Opus 4.8
parent 82e61da62f
commit 72bf59149d
13 changed files with 524 additions and 14 deletions
+68
View File
@@ -0,0 +1,68 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <vector>
namespace dragonx {
namespace data {
/**
* @brief Cached, file-keyed metadata for one wallet file.
*
* A wallet.dat on disk only reveals its size + mtime; balance and address count require the daemon
* to have loaded it. So those are cached here after each load and shown in the wallet-files list
* (P2) even when the wallet isn't the active one. walletIdentityHash bridges file -> per-wallet data
* (address book, tx history) so the right caches can be selected before/after a switch.
*/
struct WalletIndexEntry {
std::string fileName; // plain wallet filename in the datadir (e.g. "wallet.dat")
std::string displayName; // user-facing name (defaults to fileName)
std::string walletIdentityHash; // address-derived identity of the last load; "" = unknown
double cachedBalance = -1.0; // last-known total balance; < 0 = unknown (never opened)
long long cachedAddressCount = -1;// < 0 = unknown
long long lastOpenedEpoch = 0; // unix seconds of last open; 0 = never opened
long long sizeBytesAtLastOpen = 0;
bool syncedHere = false; // loaded in this datadir before -> catch-up on switch (no full rescan)
};
/**
* @brief Persistent wallet-files metadata index (wallets.json, in the config dir).
*
* Keyed by wallet file name. Populated after each load (P1b) and read by the wallet-files list (P2).
* Also remembers extra folders the user added to scan for wallet files. Not encrypted — it holds
* only file names + coarse metadata (no keys/addresses).
*/
class WalletIndex {
public:
bool load();
bool save();
static std::string getDefaultPath();
const std::vector<WalletIndexEntry>& entries() const { return entries_; }
const std::vector<std::string>& extraFolders() const { return extra_folders_; }
/**
* @brief Insert or update the entry for e.fileName.
* @return true if a new entry was added or any field changed (so the caller can skip save()).
*/
bool upsert(const WalletIndexEntry& e);
/** @brief Find the entry for a wallet file, or nullptr. */
const WalletIndexEntry* find(const std::string& fileName) const;
/** @brief Add/remove an extra scan folder. Returns true if the set changed. */
bool addExtraFolder(const std::string& dir);
bool removeExtraFolder(const std::string& dir);
private:
std::vector<WalletIndexEntry> entries_;
std::vector<std::string> extra_folders_;
std::string file_path_;
};
} // namespace data
} // namespace dragonx