// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include #include #include 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:" = a Material wallet-icon, // "img:" = a custom image (copied into /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& entries() const { return entries_; } /** * @brief Get entry count */ size_t size() const { return entries_.size(); } /** * @brief Monotonic counter bumped on every content change (add/update/remove/load/sweep). Consumers * (e.g. the chat conversation-list memo) key their caches off this so an IN-PLACE edit — a rename or * address change that keeps size() constant — still invalidates them. */ std::uint64_t revision() const { return revision_; } /** * @brief UI-sweep ONLY: show `demo` instead of the real book. The first call snapshots the real * entries; later calls (the sweep re-runs its setup every frame) only swap the demo list. While * active, save() and load() return true without touching disk or the snapshot, so nothing (an * add/update/remove, reattachLegacyScopes) can persist demo contacts. Do not use outside the sweep. */ void sweepBegin(std::vector demo) { if (!sweep_active_) { sweep_backup_ = std::move(entries_); sweep_active_ = true; } entries_ = std::move(demo); ++revision_; } /** @brief UI-sweep ONLY: put the real book back (no disk write). Idempotent. */ void sweepEnd() { if (!sweep_active_) return; entries_ = std::move(sweep_backup_); sweep_backup_.clear(); sweep_active_ = false; ++revision_; } bool sweepActive() const { return sweep_active_; } /** * @brief Check if empty */ bool empty() const { return entries_.empty(); } private: std::vector entries_; std::uint64_t revision_ = 0; std::string file_path_; std::vector sweep_backup_; // the real book while a UI sweep shows demo contacts bool sweep_active_ = false; }; } // namespace data } // namespace dragonx