// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // settings.cpp — JSON settings persistence. Loads/saves user preferences // to ~/.config/ObsidianDragon/settings.json (Linux/macOS) or %APPDATA% (Windows). #include "settings.h" #include "version.h" #include #include #include #include "../util/logger.h" #ifdef _WIN32 #include #else #include #include #endif namespace fs = std::filesystem; using json = nlohmann::json; namespace dragonx { namespace config { Settings::Settings() = default; Settings::~Settings() = default; std::string Settings::getDefaultPath() { #ifdef _WIN32 char path[MAX_PATH]; if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) { std::string dir = std::string(path) + "\\ObsidianDragon"; fs::create_directories(dir); return dir + "\\settings.json"; } return "settings.json"; #elif defined(__APPLE__) const char* home = getenv("HOME"); if (!home) { struct passwd* pw = getpwuid(getuid()); home = pw->pw_dir; } std::string dir = std::string(home) + "/Library/Application Support/ObsidianDragon"; fs::create_directories(dir); return dir + "/settings.json"; #else const char* home = getenv("HOME"); if (!home) { struct passwd* pw = getpwuid(getuid()); home = pw->pw_dir; } std::string dir = std::string(home) + "/.config/ObsidianDragon"; fs::create_directories(dir); return dir + "/settings.json"; #endif } bool Settings::load() { return load(getDefaultPath()); } bool Settings::load(const std::string& path) { settings_path_ = path; std::ifstream file(path); if (!file.is_open()) { return false; } try { json j; file >> j; if (j.contains("theme")) theme_ = j["theme"].get(); if (j.contains("save_ztxs")) save_ztxs_ = j["save_ztxs"].get(); if (j.contains("auto_shield")) auto_shield_ = j["auto_shield"].get(); if (j.contains("use_tor")) use_tor_ = j["use_tor"].get(); if (j.contains("allow_custom_fees")) allow_custom_fees_ = j["allow_custom_fees"].get(); if (j.contains("default_fee")) default_fee_ = j["default_fee"].get(); if (j.contains("fetch_prices")) fetch_prices_ = j["fetch_prices"].get(); if (j.contains("tx_explorer_url")) tx_explorer_url_ = j["tx_explorer_url"].get(); if (j.contains("address_explorer_url")) address_explorer_url_ = j["address_explorer_url"].get(); if (j.contains("language")) language_ = j["language"].get(); if (j.contains("skin_id")) skin_id_ = j["skin_id"].get(); if (j.contains("acrylic_enabled")) acrylic_enabled_ = j["acrylic_enabled"].get(); if (j.contains("acrylic_quality")) acrylic_quality_ = j["acrylic_quality"].get(); if (j.contains("blur_multiplier")) blur_multiplier_ = j["blur_multiplier"].get(); if (j.contains("noise_opacity")) noise_opacity_ = j["noise_opacity"].get(); if (j.contains("gradient_background")) gradient_background_ = j["gradient_background"].get(); // Migrate legacy reduced_transparency bool -> ui_opacity float if (j.contains("ui_opacity")) { ui_opacity_ = j["ui_opacity"].get(); } else if (j.contains("reduced_transparency") && j["reduced_transparency"].get()) { ui_opacity_ = 1.0f; // legacy: reduced = fully opaque } if (j.contains("window_opacity")) window_opacity_ = j["window_opacity"].get(); if (j.contains("balance_layout")) { if (j["balance_layout"].is_string()) balance_layout_ = j["balance_layout"].get(); else if (j["balance_layout"].is_number_integer()) { // Legacy migration: convert old int index to string ID static const char* legacyIds[] = { "classic","donut","consolidated","dashboard", "vertical-stack","shield","timeline","two-row","minimal" }; int idx = j["balance_layout"].get(); if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx]; } } if (j.contains("scanline_enabled")) scanline_enabled_ = j["scanline_enabled"].get(); if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) { hidden_addresses_.clear(); for (const auto& a : j["hidden_addresses"]) if (a.is_string()) hidden_addresses_.insert(a.get()); } if (j.contains("favorite_addresses") && j["favorite_addresses"].is_array()) { favorite_addresses_.clear(); for (const auto& a : j["favorite_addresses"]) if (a.is_string()) favorite_addresses_.insert(a.get()); } if (j.contains("address_meta") && j["address_meta"].is_object()) { address_meta_.clear(); for (auto& [addr, meta] : j["address_meta"].items()) { AddressMeta m; if (meta.contains("label") && meta["label"].is_string()) m.label = meta["label"].get(); if (meta.contains("icon") && meta["icon"].is_string()) m.icon = meta["icon"].get(); if (meta.contains("order") && meta["order"].is_number_integer()) m.sortOrder = meta["order"].get(); if (meta.contains("mining") && meta["mining"].is_boolean()) m.mining = meta["mining"].get(); address_meta_[addr] = m; } } if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get(); if (j.contains("auto_lock_timeout")) auto_lock_timeout_ = j["auto_lock_timeout"].get(); if (j.contains("unlock_duration")) unlock_duration_ = j["unlock_duration"].get(); if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get(); if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get(); if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get(); if (j.contains("max_connections")) max_connections_ = j["max_connections"].get(); if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get(); if (j.contains("debug_categories") && j["debug_categories"].is_array()) { debug_categories_.clear(); for (const auto& c : j["debug_categories"]) if (c.is_string()) debug_categories_.insert(c.get()); } if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get(); if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get(); if (j.contains("reduce_motion")) reduce_motion_ = j["reduce_motion"].get(); if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get(); if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get(); if (j.contains("pool_url")) pool_url_ = j["pool_url"].get(); // Migrate old default pool URL that was missing the stratum port if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433"; if (j.contains("pool_algo")) pool_algo_ = j["pool_algo"].get(); if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get(); if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get(); if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get(); if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get(); if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get(); if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get(); if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get()); if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get(); if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get(); if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get(); if (j.contains("idle_gpu_aware")) idle_gpu_aware_ = j["idle_gpu_aware"].get(); if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) { saved_pool_urls_.clear(); for (const auto& u : j["saved_pool_urls"]) if (u.is_string()) saved_pool_urls_.push_back(u.get()); } if (j.contains("saved_pool_workers") && j["saved_pool_workers"].is_array()) { saved_pool_workers_.clear(); for (const auto& w : j["saved_pool_workers"]) if (w.is_string()) saved_pool_workers_.push_back(w.get()); } if (j.contains("font_scale") && j["font_scale"].is_number()) font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get())); if (j.contains("window_width") && j["window_width"].is_number_integer()) window_width_ = j["window_width"].get(); if (j.contains("window_height") && j["window_height"].is_number_integer()) window_height_ = j["window_height"].get(); // Version tracking — detect upgrades so we can re-save with new defaults if (j.contains("settings_version")) settings_version_ = j["settings_version"].get(); if (settings_version_ != DRAGONX_VERSION) { DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n", settings_version_.empty() ? "(none)" : settings_version_.c_str(), DRAGONX_VERSION); needs_upgrade_save_ = true; } return true; } catch (const std::exception& e) { DEBUG_LOGF("Failed to parse settings: %s\n", e.what()); return false; } } bool Settings::save() { if (settings_path_.empty()) { settings_path_ = getDefaultPath(); } return save(settings_path_); } bool Settings::save(const std::string& path) { json j; j["theme"] = theme_; j["save_ztxs"] = save_ztxs_; j["auto_shield"] = auto_shield_; j["use_tor"] = use_tor_; j["allow_custom_fees"] = allow_custom_fees_; j["default_fee"] = default_fee_; j["fetch_prices"] = fetch_prices_; j["tx_explorer_url"] = tx_explorer_url_; j["address_explorer_url"] = address_explorer_url_; j["language"] = language_; j["skin_id"] = skin_id_; j["acrylic_enabled"] = acrylic_enabled_; j["acrylic_quality"] = acrylic_quality_; j["blur_multiplier"] = blur_multiplier_; j["noise_opacity"] = noise_opacity_; j["gradient_background"] = gradient_background_; j["ui_opacity"] = ui_opacity_; j["window_opacity"] = window_opacity_; j["balance_layout"] = balance_layout_; // saved as string ID j["scanline_enabled"] = scanline_enabled_; j["hidden_addresses"] = json::array(); for (const auto& addr : hidden_addresses_) j["hidden_addresses"].push_back(addr); j["favorite_addresses"] = json::array(); for (const auto& addr : favorite_addresses_) j["favorite_addresses"].push_back(addr); { json meta_obj = json::object(); for (const auto& [addr, m] : address_meta_) { if (m.label.empty() && m.icon.empty() && m.sortOrder < 0 && !m.mining) continue; json entry = json::object(); if (!m.label.empty()) entry["label"] = m.label; if (!m.icon.empty()) entry["icon"] = m.icon; if (m.sortOrder >= 0) entry["order"] = m.sortOrder; if (m.mining) entry["mining"] = true; meta_obj[addr] = entry; } j["address_meta"] = meta_obj; } j["wizard_completed"] = wizard_completed_; j["auto_lock_timeout"] = auto_lock_timeout_; j["unlock_duration"] = unlock_duration_; j["pin_enabled"] = pin_enabled_; j["keep_daemon_running"] = keep_daemon_running_; j["stop_external_daemon"] = stop_external_daemon_; j["max_connections"] = max_connections_; j["verbose_logging"] = verbose_logging_; j["debug_categories"] = json::array(); for (const auto& cat : debug_categories_) j["debug_categories"].push_back(cat); j["theme_effects_enabled"] = theme_effects_enabled_; j["low_spec_mode"] = low_spec_mode_; j["reduce_motion"] = reduce_motion_; j["selected_exchange"] = selected_exchange_; j["selected_pair"] = selected_pair_; j["pool_url"] = pool_url_; j["pool_algo"] = pool_algo_; j["pool_worker"] = pool_worker_; j["pool_threads"] = pool_threads_; j["pool_tls"] = pool_tls_; j["pool_hugepages"] = pool_hugepages_; j["pool_mode"] = pool_mode_; j["mine_when_idle"] = mine_when_idle_; j["mine_idle_delay"]= mine_idle_delay_; j["idle_thread_scaling"] = idle_thread_scaling_; j["idle_threads_active"] = idle_threads_active_; j["idle_threads_idle"] = idle_threads_idle_; j["idle_gpu_aware"] = idle_gpu_aware_; j["saved_pool_urls"] = json::array(); for (const auto& u : saved_pool_urls_) j["saved_pool_urls"].push_back(u); j["saved_pool_workers"] = json::array(); for (const auto& w : saved_pool_workers_) j["saved_pool_workers"].push_back(w); j["font_scale"] = font_scale_; j["settings_version"] = std::string(DRAGONX_VERSION); if (window_width_ > 0 && window_height_ > 0) { j["window_width"] = window_width_; j["window_height"] = window_height_; } try { // Ensure directory exists fs::path p(path); fs::create_directories(p.parent_path()); std::ofstream file(path); if (!file.is_open()) { return false; } file << j.dump(4); return true; } catch (const std::exception& e) { DEBUG_LOGF("Failed to save settings: %s\n", e.what()); return false; } } } // namespace config } // namespace dragonx