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>
142 lines
4.4 KiB
C++
142 lines
4.4 KiB
C++
// 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 A single address book entry
|
|
*/
|
|
struct AddressBookEntry {
|
|
std::string label;
|
|
std::string address;
|
|
std::string notes;
|
|
// Per-wallet visibility scope: "global" (shown in every wallet) or a wallet-identity hash
|
|
// (shown only when that wallet is the active one). Empty is treated as "global" so legacy
|
|
// entries — written before multi-wallet scoping — keep showing everywhere.
|
|
std::string scope = "global";
|
|
// Contact avatar: "" = default type badge (Z/T), "icon:<name>" = a Material wallet-icon,
|
|
// "img:<path>" = a custom image (copied into <config>/contact-avatars/).
|
|
std::string avatar;
|
|
|
|
AddressBookEntry() = default;
|
|
AddressBookEntry(const std::string& l, const std::string& a, const std::string& n = "",
|
|
const std::string& s = "global")
|
|
: label(l), address(a), notes(n), scope(s) {}
|
|
|
|
bool isGlobal() const { return scope.empty() || scope == "global"; }
|
|
// Visible under the wallet whose identity hash is walletHash (or globally).
|
|
bool visibleInWallet(const std::string& walletHash) const {
|
|
return isGlobal() || scope == walletHash;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @brief Address book manager
|
|
*
|
|
* Stores labeled addresses for easy access when sending.
|
|
*/
|
|
class AddressBook {
|
|
public:
|
|
AddressBook();
|
|
~AddressBook();
|
|
|
|
/**
|
|
* @brief Load address book from disk
|
|
* @return true if loaded successfully
|
|
*/
|
|
bool load();
|
|
|
|
/**
|
|
* @brief Save address book to disk
|
|
* @return true if saved successfully
|
|
*/
|
|
bool save();
|
|
|
|
/**
|
|
* @brief Get default address book file path
|
|
*/
|
|
static std::string getDefaultPath();
|
|
|
|
/**
|
|
* @brief Add a new entry
|
|
* @param entry The entry to add
|
|
* @return true if added (false if duplicate address)
|
|
*/
|
|
bool addEntry(const AddressBookEntry& entry);
|
|
|
|
/**
|
|
* @brief Update an existing entry
|
|
* @param index Index of entry to update
|
|
* @param entry New entry data
|
|
* @return true if updated
|
|
*/
|
|
bool updateEntry(size_t index, const AddressBookEntry& entry);
|
|
|
|
/**
|
|
* @brief Remove an entry
|
|
* @param index Index of entry to remove
|
|
* @return true if removed
|
|
*/
|
|
bool removeEntry(size_t index);
|
|
|
|
/**
|
|
* @brief Re-attach contacts stuck on a legacy (drifting address-hash) scope to a stable wallet id.
|
|
* Rewrites every non-global entry whose scope is NOT already a stable "w:"-prefixed id to
|
|
* `scopeId`, and saves once if anything changed. Recovery for contacts orphaned when the
|
|
* old address-set-hash scope shifted (e.g. after creating a new address).
|
|
* @return number of entries re-scoped.
|
|
*/
|
|
int reattachLegacyScopes(const std::string& scopeId);
|
|
|
|
/**
|
|
* @brief Find entry by address (any scope). Used for contact-label lookups.
|
|
* @param address Address to search for
|
|
* @return Index or -1 if not found
|
|
*/
|
|
int findByAddress(const std::string& address) const;
|
|
|
|
/**
|
|
* @brief Is there an existing entry with this address that would be visible ALONGSIDE a new
|
|
* entry of the given scope? (Same wallet, or either side global.) Used to reject
|
|
* duplicates within a wallet while still allowing the same address in different wallets.
|
|
* @param excludeIndex Storage index to skip (for edit), or -1.
|
|
*/
|
|
bool hasVisibleDuplicate(const std::string& address, const std::string& scope,
|
|
int excludeIndex = -1) const;
|
|
|
|
/**
|
|
* @brief Get all entries
|
|
*/
|
|
const std::vector<AddressBookEntry>& entries() const { return entries_; }
|
|
|
|
/**
|
|
* @brief Get entry count
|
|
*/
|
|
size_t size() const { return entries_.size(); }
|
|
|
|
/**
|
|
* @brief UI-sweep ONLY: replace the in-memory entries WITHOUT persisting to disk, so the sweep can
|
|
* seed demo contacts and restore the real book without a disk write. Do not use outside the sweep.
|
|
*/
|
|
void sweepSetEntries(std::vector<AddressBookEntry> e) { entries_ = std::move(e); }
|
|
|
|
/**
|
|
* @brief Check if empty
|
|
*/
|
|
bool empty() const { return entries_.empty(); }
|
|
|
|
private:
|
|
std::vector<AddressBookEntry> entries_;
|
|
std::string file_path_;
|
|
};
|
|
|
|
} // namespace data
|
|
} // namespace dragonx
|