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

@@ -499,7 +499,11 @@ void App::onConnected()
daemon_start_error_shown_ = false;
daemon_last_seen_crashes_ = 0; // (onConnected resets the daemon's crash count too)
connection_status_ = TR("connected");
// Stamp the active wallet as opened in the index (last-opened + size + synced-here). Balance +
// address count fill in on the first address refresh (addresses aren't loaded yet here).
updateWalletIndexForActiveWallet(/*markOpened=*/true);
// Reset crash counter on successful connection
if (daemon_controller_) {
daemon_controller_->resetCrashCount();
@@ -959,6 +963,50 @@ std::string App::transactionHistoryCacheWalletIdentity() const
shieldedAddresses, transparentAddresses);
}
std::string App::activeWalletIdentityHash() const
{
const std::string identity = transactionHistoryCacheWalletIdentity();
if (identity.empty()) return std::string();
return data::TransactionHistoryCache::walletIdentityHash(identity);
}
void App::updateWalletIndexForActiveWallet(bool markOpened)
{
if (!settings_) return;
const std::string file = settings_->getActiveWalletFile();
if (file.empty()) return;
// Start from the existing entry so we preserve fields we're not updating this call.
data::WalletIndexEntry e;
if (const auto* existing = wallet_index_.find(file)) e = *existing;
e.fileName = file;
if (e.displayName.empty()) e.displayName = file;
// Balance + address count are only meaningful once the wallet's addresses are known (identity
// non-empty); at connect time they're still 0, so don't cache bogus zeros then.
const std::string idHash = state_.connected ? activeWalletIdentityHash() : std::string();
if (!idHash.empty()) {
e.walletIdentityHash = idHash;
e.cachedBalance = state_.totalBalance;
e.cachedAddressCount = static_cast<long long>(state_.z_addresses.size() + state_.t_addresses.size());
}
// File size is always readable from disk.
std::error_code ec;
const std::string path = util::Platform::getDragonXDataDir() + "/" + file;
if (std::filesystem::exists(path, ec)) {
auto sz = std::filesystem::file_size(path, ec);
if (!ec) e.sizeBytesAtLastOpen = static_cast<long long>(sz);
}
if (markOpened) {
e.lastOpenedEpoch = static_cast<long long>(std::time(nullptr));
e.syncedHere = true; // we've loaded it in this datadir -> catch-up (no full rescan) on switch
}
if (wallet_index_.upsert(e)) wallet_index_.save();
}
void App::wipePendingTransactionHistoryCachePassphrase()
{
if (!pending_transaction_history_cache_passphrase_.empty()) {
@@ -1337,6 +1385,9 @@ void App::refreshAddressData()
if (state_.transactions.empty()) loadTransactionHistoryCacheIfAvailable();
else storeTransactionHistoryCacheIfAvailable();
maybeFinishTransactionSendProgress();
// Addresses (hence identity + count) are now known — refresh the wallet index's cached
// metadata for the active wallet. Throttled: upsert() only writes on a real change.
updateWalletIndexForActiveWallet(/*markOpened=*/false);
};
}, 3);
if (!enqueued.enqueued) return;