fix(contacts): stable per-wallet scope so contacts don't vanish (address-hash drift)
Contacts were scoped by activeWalletIdentityHash() — a hash of the wallet's
ENTIRE address set. Creating a new receive address grows the set, changing the
hash, so every contact stamped with the old hash falls out of the scope filter
(contacts_tab.cpp:915) while still being counted — the "3 saved, 1 showing"
symptom, where only the one set to global (which bypasses the scope) survives.
It also hid scoped contacts on every startup before the daemon connected (hash
empty until addresses load).
Introduce a stable per-wallet scope id: WalletIndexEntry.scopeId ("w:"+random
hex), generated once and persisted in the wallet index (keyed by wallet file),
never recomputed from the mutable address set — so creating addresses, locking,
or disconnecting never changes it. App::activeWalletScopeId() establishes it on
first use. Contacts now scope + filter on this instead of the drifting hash. The
tx-history-cache identity (the hash's real purpose) is untouched.
Recovery for already-orphaned contacts:
- AddressBook::reattachLegacyScopes() re-attaches non-global, non-"w:" contacts
to the active wallet's stable id; run once when there's a single known wallet
(unambiguous attribution). Idempotent.
- The scope filter fails OPEN for legacy scopes (multi-wallet case where recovery
can't attribute them) so no contact is ever hidden; stable "w:" scopes still
match strictly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1004,6 +1004,34 @@ std::string App::activeWalletIdentityHash() const
|
||||
return data::TransactionHistoryCache::walletIdentityHash(identity);
|
||||
}
|
||||
|
||||
std::string App::activeWalletScopeId()
|
||||
{
|
||||
if (!settings_) return std::string();
|
||||
const std::string file = settings_->getActiveWalletFile();
|
||||
if (file.empty()) return std::string();
|
||||
|
||||
// Reuse the persisted id if we have one for this wallet file.
|
||||
if (const auto* existing = wallet_index_.find(file)) {
|
||||
if (!existing->scopeId.empty()) return existing->scopeId;
|
||||
}
|
||||
|
||||
// Establish one now: 16 random bytes, "w:"-prefixed so it's distinguishable from the legacy
|
||||
// 64-hex address-hash scopes we migrate away from.
|
||||
unsigned char rnd[16];
|
||||
randombytes_buf(rnd, sizeof(rnd));
|
||||
static const char* kHex = "0123456789abcdef";
|
||||
std::string id = "w:";
|
||||
for (unsigned char b : rnd) { id.push_back(kHex[b >> 4]); id.push_back(kHex[b & 0x0F]); }
|
||||
|
||||
data::WalletIndexEntry e;
|
||||
if (const auto* existing = wallet_index_.find(file)) e = *existing;
|
||||
e.fileName = file;
|
||||
if (e.displayName.empty()) e.displayName = file;
|
||||
e.scopeId = id;
|
||||
if (wallet_index_.upsert(e)) wallet_index_.save();
|
||||
return id;
|
||||
}
|
||||
|
||||
void App::updateWalletIndexForActiveWallet(bool markOpened)
|
||||
{
|
||||
if (!settings_) return;
|
||||
|
||||
Reference in New Issue
Block a user