// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "address_book.h" #include #include #include #include "../util/logger.h" #include "../util/platform.h" namespace fs = std::filesystem; using json = nlohmann::json; namespace dragonx { namespace data { AddressBook::AddressBook() = default; AddressBook::~AddressBook() = default; std::string AddressBook::getDefaultPath() { // Co-locate with settings.json in the per-variant config dir (Lite -> ObsidianDragonLite/). // util::Platform::getConfigDir() owns the per-platform + per-variant path in one place. const std::string dir = util::Platform::getConfigDir(); fs::create_directories(dir); return (fs::path(dir) / "addressbook.json").string(); } bool AddressBook::load() { file_path_ = getDefaultPath(); std::ifstream file(file_path_); if (!file.is_open()) { // No file yet - that's OK return true; } try { json j; file >> j; entries_.clear(); if (j.contains("entries") && j["entries"].is_array()) { for (const auto& entry : j["entries"]) { AddressBookEntry e; e.label = entry.value("label", ""); e.address = entry.value("address", ""); e.notes = entry.value("notes", ""); // Legacy entries (no "scope") migrate to "global" so nothing disappears when // multi-wallet scoping lands — a contact you already had stays visible everywhere. e.scope = entry.value("scope", "global"); e.avatar = entry.value("avatar", ""); if (!e.address.empty()) { entries_.push_back(e); } } } DEBUG_LOGF("Address book loaded: %zu entries\n", entries_.size()); return true; } catch (const std::exception& e) { DEBUG_LOGF("Error loading address book: %s\n", e.what()); return false; } } bool AddressBook::save() { if (file_path_.empty()) { file_path_ = getDefaultPath(); } try { json j; j["entries"] = json::array(); for (const auto& entry : entries_) { json e; e["label"] = entry.label; e["address"] = entry.address; e["notes"] = entry.notes; e["scope"] = entry.scope.empty() ? std::string("global") : entry.scope; if (!entry.avatar.empty()) e["avatar"] = entry.avatar; j["entries"].push_back(e); } // Atomic + durable: temp file + fsync + rename, so a crash mid-write can't // truncate addressbook.json (which is fully rewritten on every entry change). // Owner-only (0600) — it holds the user's saved contacts. if (!util::Platform::writeFileAtomically(file_path_, j.dump(2), /*restrictPermissions=*/true)) { DEBUG_LOGF("Could not write address book: %s\n", file_path_.c_str()); return false; } DEBUG_LOGF("Address book saved: %zu entries\n", entries_.size()); return true; } catch (const std::exception& e) { DEBUG_LOGF("Error saving address book: %s\n", e.what()); return false; } } bool AddressBook::addEntry(const AddressBookEntry& entry) { // Reject a duplicate only within the same visible set (same wallet or global) — the same // address may legitimately be a contact in two different wallets. if (hasVisibleDuplicate(entry.address, entry.scope)) { return false; } entries_.push_back(entry); return save(); } bool AddressBook::updateEntry(size_t index, const AddressBookEntry& entry) { if (index >= entries_.size()) { return false; } // Check for a duplicate visible alongside this entry's scope (excluding the entry being edited) if (hasVisibleDuplicate(entry.address, entry.scope, static_cast(index))) { return false; } entries_[index] = entry; return save(); } bool AddressBook::removeEntry(size_t index) { if (index >= entries_.size()) { return false; } entries_.erase(entries_.begin() + index); return save(); } int AddressBook::reattachLegacyScopes(const std::string& scopeId) { if (scopeId.empty()) return 0; int rescoped = 0; for (auto& e : entries_) { if (e.isGlobal()) continue; // global stays global if (e.scope.rfind("w:", 0) == 0) continue; // already a stable scope e.scope = scopeId; ++rescoped; } if (rescoped > 0) save(); return rescoped; } int AddressBook::findByAddress(const std::string& address) const { for (size_t i = 0; i < entries_.size(); i++) { if (entries_[i].address == address) { return static_cast(i); } } return -1; } bool AddressBook::hasVisibleDuplicate(const std::string& address, const std::string& scope, int excludeIndex) const { AddressBookEntry probe; probe.scope = scope; // reuse the isGlobal()/scope logic for (size_t i = 0; i < entries_.size(); i++) { if (static_cast(i) == excludeIndex) continue; const auto& e = entries_[i]; if (e.address != address) continue; // Collides if they'd ever be shown together: same wallet scope, or either is global. if (e.isGlobal() || probe.isGlobal() || e.scope == scope) return true; } return false; } } // namespace data } // namespace dragonx