// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "wallet_index.h" #include #include #include #include #include "../util/logger.h" #include "../util/platform.h" namespace fs = std::filesystem; using json = nlohmann::json; namespace dragonx { namespace data { namespace { // A field-wise "did anything change" check so we only rewrite wallets.json when needed. bool sameEntry(const WalletIndexEntry& a, const WalletIndexEntry& b) { return a.fileName == b.fileName && a.displayName == b.displayName && a.walletIdentityHash == b.walletIdentityHash && a.scopeId == b.scopeId && a.cachedBalance == b.cachedBalance && a.cachedAddressCount == b.cachedAddressCount && a.lastOpenedEpoch == b.lastOpenedEpoch && a.sizeBytesAtLastOpen == b.sizeBytesAtLastOpen && a.syncedHere == b.syncedHere; } } // namespace std::string WalletIndex::getDefaultPath() { // Co-located with settings.json / addressbook.json in the per-variant config dir. const std::string dir = util::Platform::getConfigDir(); fs::create_directories(dir); return (fs::path(dir) / "wallets.json").string(); } bool WalletIndex::load() { file_path_ = getDefaultPath(); entries_.clear(); extra_folders_.clear(); std::ifstream file(file_path_); if (!file.is_open()) return true; // no file yet is fine try { json j; file >> j; if (j.contains("entries") && j["entries"].is_array()) { for (const auto& e : j["entries"]) { WalletIndexEntry w; w.fileName = e.value("file", ""); if (w.fileName.empty()) continue; w.displayName = e.value("name", w.fileName); w.walletIdentityHash = e.value("identity", ""); w.scopeId = e.value("scopeId", ""); w.cachedBalance = e.value("balance", -1.0); w.cachedAddressCount = e.value("addresses", (long long)-1); w.lastOpenedEpoch = e.value("lastOpened", (long long)0); w.sizeBytesAtLastOpen = e.value("size", (long long)0); w.syncedHere = e.value("syncedHere", false); entries_.push_back(std::move(w)); } } if (j.contains("extraFolders") && j["extraFolders"].is_array()) { for (const auto& d : j["extraFolders"]) { if (d.is_string() && !d.get().empty()) extra_folders_.push_back(d.get()); } } DEBUG_LOGF("Wallet index loaded: %zu wallets, %zu extra folders\n", entries_.size(), extra_folders_.size()); return true; } catch (const std::exception& e) { DEBUG_LOGF("Error loading wallet index: %s\n", e.what()); return false; } } bool WalletIndex::save() { if (file_path_.empty()) file_path_ = getDefaultPath(); try { json j; j["entries"] = json::array(); for (const auto& w : entries_) { json e; e["file"] = w.fileName; e["name"] = w.displayName; e["identity"] = w.walletIdentityHash; e["scopeId"] = w.scopeId; e["balance"] = w.cachedBalance; e["addresses"] = w.cachedAddressCount; e["lastOpened"] = w.lastOpenedEpoch; e["size"] = w.sizeBytesAtLastOpen; e["syncedHere"] = w.syncedHere; j["entries"].push_back(std::move(e)); } j["extraFolders"] = extra_folders_; // No secrets here (file names + coarse metadata) but keep it owner-only for consistency. if (!util::Platform::writeFileAtomically(file_path_, j.dump(2), /*restrictPermissions=*/true)) { DEBUG_LOGF("Could not write wallet index: %s\n", file_path_.c_str()); return false; } return true; } catch (const std::exception& e) { DEBUG_LOGF("Error saving wallet index: %s\n", e.what()); return false; } } bool WalletIndex::upsert(const WalletIndexEntry& e) { for (auto& w : entries_) { if (w.fileName == e.fileName) { if (sameEntry(w, e)) return false; // nothing changed -> caller can skip save() w = e; return true; } } entries_.push_back(e); return true; } const WalletIndexEntry* WalletIndex::find(const std::string& fileName) const { for (const auto& w : entries_) { if (w.fileName == fileName) return &w; } return nullptr; } bool WalletIndex::addExtraFolder(const std::string& dir) { if (dir.empty()) return false; if (std::find(extra_folders_.begin(), extra_folders_.end(), dir) != extra_folders_.end()) return false; extra_folders_.push_back(dir); return true; } bool WalletIndex::removeExtraFolder(const std::string& dir) { auto it = std::find(extra_folders_.begin(), extra_folders_.end(), dir); if (it == extra_folders_.end()) return false; extra_folders_.erase(it); return true; } } // namespace data } // namespace dragonx