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
parent 82e61da62f
commit 72bf59149d
13 changed files with 524 additions and 14 deletions

View File

@@ -16,6 +16,7 @@
#include <nlohmann/json_fwd.hpp>
#include "data/transaction_history_cache.h"
#include "data/address_book.h"
#include "data/wallet_index.h"
#include "data/wallet_state.h"
#include "rpc/connection.h"
#include "services/network_refresh_service.h"
@@ -197,6 +198,10 @@ public:
data::AddressBook& addressBook() { return address_book_; }
const data::AddressBook& addressBook() const { return address_book_; }
// Hash of the active wallet's identity (derived from its address list), used to scope
// per-wallet data (e.g. address-book contacts). Empty until addresses are known (pre-connect).
std::string activeWalletIdentityHash() const;
// Connection state (convenience wrappers)
bool isConnected() const { return state_.connected; }
int getBlockHeight() const { return state_.sync.blocks; }
@@ -555,6 +560,10 @@ private:
void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
std::string transactionHistoryCacheWalletIdentity() const;
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
// Record the active wallet's cached metadata (balance, address count, identity, size) into the
// wallet index (wallets.json). Cheap + throttled: only writes when a value changed. markOpened
// stamps last-opened + syncedHere (call once per connect).
void updateWalletIndexForActiveWallet(bool markOpened);
void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
// Shared main-thread continuations for a wallet unlock attempt, so the passphrase
// and PIN paths cannot drift. applyUnlockFailure applies the escalating lockout
@@ -965,6 +974,7 @@ private:
std::unique_ptr<util::SecureVault> vault_;
data::TransactionHistoryCache transaction_history_cache_;
data::AddressBook address_book_; // shared contact store; loaded once in init(), self-saves on mutation
data::WalletIndex wallet_index_; // per-wallet metadata cache (wallets.json); populated after each load
std::string pending_transaction_history_cache_passphrase_;
bool transaction_history_cache_loaded_ = false;