refactor(audit): batch 7 — RPC/settings/lite/effects mechanical dedups

Six behavior-preserving consolidations from the audit:

- RPCClient::call() overloads share a private performCall() (payload dump +
  curl_easy_perform + http code) and a static parseRpcResult() (error->RpcError
  extraction). The timeout overload restores the prior timeout in both the
  success and catch paths, exactly as before.
- The six UnifiedCallback methods delegate to one splitUnified() that builds
  the (Callback, ErrorCallback) pair once (null-check preserved).
- One liteTrimCopy() in lite_connection_service replaces 5 file-local
  trim-copy helpers across the lite slice.
- lite_wallet_controller's inline height JSON parse now routes through the
  tested parseLiteHeightResponse().
- theme_effects.cpp: unpackRGB()/scaledAlpha() replace 14 RGB-unpack + 5
  alpha-scale copy-paste blocks.
- settings.cpp load() uses loadScalar()/loadClamped() helpers for ~44 scalar
  fields. Besides removing boilerplate this HARDENS loading: ~44 fields that
  previously only checked contains() now also verify the JSON type, so a
  malformed value in settings.json falls back to the default instead of
  throwing/misreading. Custom cases (enum parses, legacy migrations, arrays)
  stay inline; save() is unchanged.

Full-node + Lite build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 19:56:54 -05:00
parent 4635a56e89
commit dcd09dc542
11 changed files with 245 additions and 328 deletions

View File

@@ -12,6 +12,8 @@
#include <fstream> #include <fstream>
#include <filesystem> #include <filesystem>
#include <ctime> #include <ctime>
#include <algorithm>
#include <type_traits>
#include "../util/logger.h" #include "../util/logger.h"
#include "../util/platform.h" #include "../util/platform.h"
@@ -76,6 +78,34 @@ const char* poolSelectModeName(Settings::PoolSelectMode mode)
return "manual"; return "manual";
} }
// True if j[key] exists and holds a JSON value convertible to T. Guards every scalar
// read so a malformed value (wrong type / missing key) leaves the field's default
// instead of throwing out of the whole load().
template <typename T>
bool jsonHasType(const json& v)
{
if constexpr (std::is_same_v<T, bool>) return v.is_boolean();
else if constexpr (std::is_same_v<T, std::string>) return v.is_string();
else if constexpr (std::is_floating_point_v<T>) return v.is_number();
else if constexpr (std::is_integral_v<T>) return v.is_number_integer();
else return false;
}
// Reads j[key] into field only when present AND of the matching type.
template <typename T>
void loadScalar(const json& j, const char* key, T& field)
{
if (j.contains(key) && jsonHasType<T>(j[key])) field = j[key].get<T>();
}
// Same as loadScalar but clamps the read value into [lo, hi].
template <typename T>
void loadClamped(const json& j, const char* key, T& field, T lo, T hi)
{
if (j.contains(key) && jsonHasType<T>(j[key]))
field = std::max(lo, std::min(hi, j[key].get<T>()));
}
} // namespace } // namespace
std::string Settings::getDefaultPath() std::string Settings::getDefaultPath()
@@ -127,29 +157,29 @@ bool Settings::load(const std::string& path)
json j; json j;
file >> j; file >> j;
if (j.contains("theme")) theme_ = j["theme"].get<std::string>(); loadScalar(j, "theme", theme_);
if (j.contains("save_ztxs")) save_ztxs_ = j["save_ztxs"].get<bool>(); loadScalar(j, "save_ztxs", save_ztxs_);
if (j.contains("auto_shield")) auto_shield_ = j["auto_shield"].get<bool>(); loadScalar(j, "auto_shield", auto_shield_);
if (j.contains("use_tor")) use_tor_ = j["use_tor"].get<bool>(); loadScalar(j, "use_tor", use_tor_);
if (j.contains("allow_custom_fees")) allow_custom_fees_ = j["allow_custom_fees"].get<bool>(); loadScalar(j, "allow_custom_fees", allow_custom_fees_);
if (j.contains("default_fee")) default_fee_ = j["default_fee"].get<double>(); loadScalar(j, "default_fee", default_fee_);
if (j.contains("fetch_prices")) fetch_prices_ = j["fetch_prices"].get<bool>(); loadScalar(j, "fetch_prices", fetch_prices_);
if (j.contains("tx_explorer_url")) tx_explorer_url_ = j["tx_explorer_url"].get<std::string>(); loadScalar(j, "tx_explorer_url", tx_explorer_url_);
if (j.contains("address_explorer_url")) address_explorer_url_ = j["address_explorer_url"].get<std::string>(); loadScalar(j, "address_explorer_url", address_explorer_url_);
if (j.contains("language")) language_ = j["language"].get<std::string>(); loadScalar(j, "language", language_);
if (j.contains("skin_id")) skin_id_ = j["skin_id"].get<std::string>(); loadScalar(j, "skin_id", skin_id_);
if (j.contains("acrylic_enabled")) acrylic_enabled_ = j["acrylic_enabled"].get<bool>(); loadScalar(j, "acrylic_enabled", acrylic_enabled_);
if (j.contains("acrylic_quality")) acrylic_quality_ = j["acrylic_quality"].get<int>(); loadScalar(j, "acrylic_quality", acrylic_quality_);
if (j.contains("blur_multiplier")) blur_multiplier_ = j["blur_multiplier"].get<float>(); loadScalar(j, "blur_multiplier", blur_multiplier_);
if (j.contains("noise_opacity")) noise_opacity_ = j["noise_opacity"].get<float>(); loadScalar(j, "noise_opacity", noise_opacity_);
if (j.contains("gradient_background")) gradient_background_ = j["gradient_background"].get<bool>(); loadScalar(j, "gradient_background", gradient_background_);
// Migrate legacy reduced_transparency bool -> ui_opacity float // Migrate legacy reduced_transparency bool -> ui_opacity float
if (j.contains("ui_opacity")) { if (j.contains("ui_opacity")) {
ui_opacity_ = j["ui_opacity"].get<float>(); ui_opacity_ = j["ui_opacity"].get<float>();
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) { } else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) {
ui_opacity_ = 1.0f; // legacy: reduced = fully opaque ui_opacity_ = 1.0f; // legacy: reduced = fully opaque
} }
if (j.contains("window_opacity")) window_opacity_ = j["window_opacity"].get<float>(); loadScalar(j, "window_opacity", window_opacity_);
if (j.contains("balance_layout")) { if (j.contains("balance_layout")) {
if (j["balance_layout"].is_string()) if (j["balance_layout"].is_string())
balance_layout_ = j["balance_layout"].get<std::string>(); balance_layout_ = j["balance_layout"].get<std::string>();
@@ -163,7 +193,7 @@ bool Settings::load(const std::string& path)
if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx]; if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
} }
} }
if (j.contains("scanline_enabled")) scanline_enabled_ = j["scanline_enabled"].get<bool>(); loadScalar(j, "scanline_enabled", scanline_enabled_);
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) { if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
hidden_addresses_.clear(); hidden_addresses_.clear();
for (const auto& a : j["hidden_addresses"]) for (const auto& a : j["hidden_addresses"])
@@ -189,13 +219,13 @@ bool Settings::load(const std::string& path)
address_meta_[addr] = m; address_meta_[addr] = m;
} }
} }
if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get<bool>(); loadScalar(j, "wizard_completed", wizard_completed_);
if (j.contains("auto_lock_timeout")) auto_lock_timeout_ = j["auto_lock_timeout"].get<int>(); loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
if (j.contains("unlock_duration")) unlock_duration_ = j["unlock_duration"].get<int>(); loadScalar(j, "unlock_duration", unlock_duration_);
if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get<bool>(); loadScalar(j, "pin_enabled", pin_enabled_);
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get<bool>(); loadScalar(j, "keep_daemon_running", keep_daemon_running_);
if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get<bool>(); loadScalar(j, "stop_external_daemon", stop_external_daemon_);
if (j.contains("max_connections")) max_connections_ = j["max_connections"].get<int>(); loadScalar(j, "max_connections", max_connections_);
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) { if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
const auto& lite = j["lite_wallet"]; const auto& lite = j["lite_wallet"];
if (lite.contains("server_selection_mode")) { if (lite.contains("server_selection_mode")) {
@@ -256,34 +286,36 @@ bool Settings::load(const std::string& path)
if (u.is_string()) lite_hidden_servers_.insert(u.get<std::string>()); if (u.is_string()) lite_hidden_servers_.insert(u.get<std::string>());
} }
} }
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>(); loadScalar(j, "verbose_logging", verbose_logging_);
if (j.contains("debug_categories") && j["debug_categories"].is_array()) { if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
debug_categories_.clear(); debug_categories_.clear();
for (const auto& c : j["debug_categories"]) for (const auto& c : j["debug_categories"])
if (c.is_string()) debug_categories_.insert(c.get<std::string>()); if (c.is_string()) debug_categories_.insert(c.get<std::string>());
} }
if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get<bool>(); loadScalar(j, "theme_effects_enabled", theme_effects_enabled_);
if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get<bool>(); loadScalar(j, "low_spec_mode", low_spec_mode_);
if (j.contains("reduce_motion")) reduce_motion_ = j["reduce_motion"].get<bool>(); loadScalar(j, "reduce_motion", reduce_motion_);
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>(); loadScalar(j, "selected_exchange", selected_exchange_);
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>(); loadScalar(j, "selected_pair", selected_pair_);
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>(); loadScalar(j, "pool_url", pool_url_);
// Migrate old default pool URL that was missing the stratum port // Migrate old default pool URL that was missing the stratum port
if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433"; if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
if (j.contains("pool_algo")) pool_algo_ = j["pool_algo"].get<std::string>(); loadScalar(j, "pool_algo", pool_algo_);
if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get<std::string>(); loadScalar(j, "pool_worker", pool_worker_);
if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get<int>(); loadScalar(j, "pool_threads", pool_threads_);
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>(); loadScalar(j, "pool_tls", pool_tls_);
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>(); loadScalar(j, "pool_hugepages", pool_hugepages_);
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>(); loadScalar(j, "pool_mode", pool_mode_);
if (j.contains("pool_select_mode")) pool_select_mode_ = parsePoolSelectMode(j["pool_select_mode"]); if (j.contains("pool_select_mode")) pool_select_mode_ = parsePoolSelectMode(j["pool_select_mode"]);
if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>(); loadScalar(j, "mine_when_idle", mine_when_idle_);
if (j.contains("xmrig_version")) xmrig_version_ = j["xmrig_version"].get<std::string>(); loadScalar(j, "xmrig_version", xmrig_version_);
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>()); // Lower-bounded only (min 30s), matching setMineIdleDelay; now type-guarded too.
if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get<bool>(); if (j.contains("mine_idle_delay") && j["mine_idle_delay"].is_number_integer())
if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get<int>(); mine_idle_delay_ = std::max(30, j["mine_idle_delay"].get<int>());
if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get<int>(); loadScalar(j, "idle_thread_scaling", idle_thread_scaling_);
if (j.contains("idle_gpu_aware")) idle_gpu_aware_ = j["idle_gpu_aware"].get<bool>(); loadScalar(j, "idle_threads_active", idle_threads_active_);
loadScalar(j, "idle_threads_idle", idle_threads_idle_);
loadScalar(j, "idle_gpu_aware", idle_gpu_aware_);
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) { if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
saved_pool_urls_.clear(); saved_pool_urls_.clear();
for (const auto& u : j["saved_pool_urls"]) for (const auto& u : j["saved_pool_urls"])
@@ -307,15 +339,12 @@ bool Settings::load(const std::string& path)
if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry)); if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry));
} }
} }
if (j.contains("font_scale") && j["font_scale"].is_number()) loadClamped(j, "font_scale", font_scale_, 1.0f, 1.5f);
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>())); loadScalar(j, "window_width", window_width_);
if (j.contains("window_width") && j["window_width"].is_number_integer()) loadScalar(j, "window_height", window_height_);
window_width_ = j["window_width"].get<int>();
if (j.contains("window_height") && j["window_height"].is_number_integer())
window_height_ = j["window_height"].get<int>();
// Version tracking — detect upgrades so we can re-save with new defaults // Version tracking — detect upgrades so we can re-save with new defaults
if (j.contains("settings_version")) settings_version_ = j["settings_version"].get<std::string>(); loadScalar(j, "settings_version", settings_version_);
if (settings_version_ != DRAGONX_VERSION) { if (settings_version_ != DRAGONX_VERSION) {
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n", DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
settings_version_.empty() ? "(none)" : settings_version_.c_str(), settings_version_.empty() ? "(none)" : settings_version_.c_str(),

View File

@@ -290,13 +290,8 @@ json RPCClient::makePayload(const std::string& method, const json& params)
}; };
} }
json RPCClient::call(const std::string& method, const json& params) std::string RPCClient::performCall(const std::string& method, const json& params, long& httpCodeOut)
{ {
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
if (!impl_->curl) {
throw std::runtime_error("Not connected");
}
emitRpcTrace(method); emitRpcTrace(method);
json payload = makePayload(method, params); json payload = makePayload(method, params);
@@ -310,22 +305,27 @@ json RPCClient::call(const std::string& method, const json& params)
// Perform request // Perform request
CURLcode res = curl_easy_perform(impl_->curl); CURLcode res = curl_easy_perform(impl_->curl);
if (res != CURLE_OK) { if (res != CURLE_OK) {
throw std::runtime_error("RPC request failed: " + std::string(curl_easy_strerror(res))); throw std::runtime_error("RPC request failed: " + std::string(curl_easy_strerror(res)));
} }
// Check HTTP response code // Check HTTP response code
long http_code = 0; httpCodeOut = 0;
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &http_code); curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &httpCodeOut);
return response_data;
}
json RPCClient::parseRpcResult(long httpCode, const std::string& body)
{
// Bitcoin/Hush RPC returns HTTP 500 for application-level errors // Bitcoin/Hush RPC returns HTTP 500 for application-level errors
// (insufficient funds, bad params, etc.) with a valid JSON body. // (insufficient funds, bad params, etc.) with a valid JSON body.
// Parse the body first to extract the real error message. // Parse the body first to extract the real error message.
if (http_code != 200) { if (httpCode != 200) {
int errCode = 0; int errCode = 0;
try { try {
json response = json::parse(response_data); json response = json::parse(body);
if (response.contains("error") && response["error"].is_object()) { if (response.contains("error") && response["error"].is_object()) {
if (response["error"].contains("code") && response["error"]["code"].is_number_integer()) if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
errCode = response["error"]["code"].get<int>(); errCode = response["error"]["code"].get<int>();
@@ -337,10 +337,10 @@ json RPCClient::call(const std::string& method, const json& params)
} catch (const json::exception&) { } catch (const json::exception&) {
// Body wasn't valid JSON — fall through to generic HTTP error // Body wasn't valid JSON — fall through to generic HTTP error
} }
throw RpcError(errCode, "RPC error: HTTP " + std::to_string(http_code)); throw RpcError(errCode, "RPC error: HTTP " + std::to_string(httpCode));
} }
json response = json::parse(response_data); json response = json::parse(body);
if (response.contains("error") && !response["error"].is_null()) { if (response.contains("error") && !response["error"].is_null()) {
int errCode = 0; int errCode = 0;
@@ -358,6 +358,18 @@ json RPCClient::call(const std::string& method, const json& params)
return response["result"]; return response["result"];
} }
json RPCClient::call(const std::string& method, const json& params)
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
if (!impl_->curl) {
throw std::runtime_error("Not connected");
}
long http_code = 0;
std::string response_data = performCall(method, params, http_code);
return parseRpcResult(http_code, response_data);
}
json RPCClient::call(const std::string& method, const json& params, long timeoutSec) json RPCClient::call(const std::string& method, const json& params, long timeoutSec)
{ {
std::lock_guard<std::recursive_mutex> lk(curl_mutex_); std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
@@ -365,65 +377,16 @@ json RPCClient::call(const std::string& method, const json& params, long timeout
throw std::runtime_error("Not connected"); throw std::runtime_error("Not connected");
} }
emitRpcTrace(method);
// Temporarily override timeout // Temporarily override timeout
long prevTimeout = 30L; long prevTimeout = 30L;
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec); curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec);
try { try {
// Unlock before calling to avoid recursive lock issues — but we already hold it, long http_code = 0;
// and call() also locks with recursive_mutex, so just delegate to the body directly. std::string response_data = performCall(method, params, http_code);
json payload = makePayload(method, params);
std::string body = payload.dump();
std::string response_data;
curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDS, body.c_str());
curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDSIZE, (long)body.size());
curl_easy_setopt(impl_->curl, CURLOPT_WRITEDATA, &response_data);
CURLcode res = curl_easy_perform(impl_->curl);
// Restore original timeout // Restore original timeout
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout); curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
return parseRpcResult(http_code, response_data);
if (res != CURLE_OK) {
throw std::runtime_error("RPC request failed: " + std::string(curl_easy_strerror(res)));
}
long http_code = 0;
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &http_code);
if (http_code != 200) {
int errCode = 0;
try {
json response = json::parse(response_data);
if (response.contains("error") && response["error"].is_object()) {
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
errCode = response["error"]["code"].get<int>();
if (response["error"].contains("message") && response["error"]["message"].is_string())
throw RpcError(errCode, response["error"]["message"].get<std::string>());
throw RpcError(errCode, "RPC error: " + response["error"].dump());
}
} catch (const json::exception&) {}
throw RpcError(errCode, "RPC error: HTTP " + std::to_string(http_code));
}
json response = json::parse(response_data);
if (response.contains("error") && !response["error"].is_null()) {
int errCode = 0;
std::string err_msg;
if (response["error"].is_object()) {
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
errCode = response["error"]["code"].get<int>();
if (response["error"].contains("message") && response["error"]["message"].is_string())
err_msg = response["error"]["message"].get<std::string>();
}
if (err_msg.empty()) err_msg = response["error"].dump();
throw RpcError(errCode, "RPC error: " + err_msg);
}
return response["result"];
} catch (...) { } catch (...) {
// Ensure timeout is always restored // Ensure timeout is always restored
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout); curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
@@ -762,43 +725,40 @@ void RPCClient::z_listReceivedByAddress(const std::string& address, int minconf,
doRPC("z_listreceivedbyaddress", {address, minconf}, cb, err); doRPC("z_listreceivedbyaddress", {address, minconf}, cb, err);
} }
// Unified callback versions // Adapts a UnifiedCallback into the (Callback, ErrorCallback) pair doRPC expects: the
void RPCClient::getInfo(UnifiedCallback cb) // success lambda forwards (result, ""), the error lambda forwards ({}, error). The cb
// null-check is preserved on each invocation.
std::pair<Callback, ErrorCallback> RPCClient::splitUnified(UnifiedCallback cb)
{ {
doRPC("getinfo", {}, return {
[cb](const json& result) { [cb](const json& result) {
if (cb) cb(result, ""); if (cb) cb(result, "");
}, },
[cb](const std::string& error) { [cb](const std::string& error) {
if (cb) cb(json{}, error); if (cb) cb(json{}, error);
} }
); };
}
// Unified callback versions
void RPCClient::getInfo(UnifiedCallback cb)
{
auto handlers = splitUnified(std::move(cb));
doRPC("getinfo", {}, handlers.first, handlers.second);
} }
void RPCClient::rescanBlockchain(int startHeight, UnifiedCallback cb) void RPCClient::rescanBlockchain(int startHeight, UnifiedCallback cb)
{ {
// hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain". // hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain".
doRPC("rescan", {startHeight}, auto handlers = splitUnified(std::move(cb));
[cb](const json& result) { doRPC("rescan", {startHeight}, handlers.first, handlers.second);
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
} }
void RPCClient::z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr, void RPCClient::z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr,
double fee, int limit, UnifiedCallback cb) double fee, int limit, UnifiedCallback cb)
{ {
doRPC("z_shieldcoinbase", {fromAddr, toAddr, fee, limit}, auto handlers = splitUnified(std::move(cb));
[cb](const json& result) { doRPC("z_shieldcoinbase", {fromAddr, toAddr, fee, limit}, handlers.first, handlers.second);
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
} }
void RPCClient::z_mergeToAddress(const std::vector<std::string>& fromAddrs, const std::string& toAddr, void RPCClient::z_mergeToAddress(const std::vector<std::string>& fromAddrs, const std::string& toAddr,
@@ -808,14 +768,8 @@ void RPCClient::z_mergeToAddress(const std::vector<std::string>& fromAddrs, cons
for (const auto& addr : fromAddrs) { for (const auto& addr : fromAddrs) {
addrs.push_back(addr); addrs.push_back(addr);
} }
doRPC("z_mergetoaddress", {addrs, toAddr, fee, 0, limit}, auto handlers = splitUnified(std::move(cb));
[cb](const json& result) { doRPC("z_mergetoaddress", {addrs, toAddr, fee, 0, limit}, handlers.first, handlers.second);
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
} }
void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, UnifiedCallback cb) void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, UnifiedCallback cb)
@@ -824,30 +778,18 @@ void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, Unif
for (const auto& id : opids) { for (const auto& id : opids) {
ids.push_back(id); ids.push_back(id);
} }
doRPC("z_getoperationstatus", {ids}, auto handlers = splitUnified(std::move(cb));
[cb](const json& result) { doRPC("z_getoperationstatus", {ids}, handlers.first, handlers.second);
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
} }
void RPCClient::getBlock(int height, UnifiedCallback cb) void RPCClient::getBlock(int height, UnifiedCallback cb)
{ {
// First get block hash, then get block // First get block hash, then get block
getBlockHash(height, getBlockHash(height,
[this, cb](const json& hashResult) { [this, cb](const json& hashResult) {
std::string hash = hashResult.get<std::string>(); std::string hash = hashResult.get<std::string>();
getBlock(hash, auto handlers = splitUnified(cb);
[cb](const json& blockResult) { getBlock(hash, handlers.first, handlers.second);
if (cb) cb(blockResult, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
}, },
[cb](const std::string& error) { [cb](const std::string& error) {
if (cb) cb(json{}, error); if (cb) cb(json{}, error);

View File

@@ -245,6 +245,17 @@ private:
json makePayload(const std::string& method, const json& params = json::array()); json makePayload(const std::string& method, const json& params = json::array());
void doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err); void doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err);
// Shared blocking-call core: dumps the payload, performs the request (throwing on a
// curl transport error) and returns the response body plus HTTP status. Caller must
// hold curl_mutex_ and have verified impl_->curl.
std::string performCall(const std::string& method, const json& params, long& httpCodeOut);
// Centralizes the HTTP-code check and JSON error->RpcError extraction, returning
// response["result"] on success.
static json parseRpcResult(long httpCode, const std::string& body);
// Splits a UnifiedCallback into the (Callback, ErrorCallback) pair used by doRPC.
static std::pair<Callback, ErrorCallback> splitUnified(UnifiedCallback cb);
std::string host_; std::string host_;
std::string port_; std::string port_;
std::string auth_; // Base64 encoded "user:password" std::string auth_; // Base64 encoded "user:password"

View File

@@ -15,6 +15,23 @@ namespace dragonx {
namespace ui { namespace ui {
namespace effects { namespace effects {
namespace {
// Colors are packed little-endian RGBA (R at bit 0, G at 8, B at 16, A at 24) — the
// same layout IM_COL32 produces. unpackRGB pulls out the three color channels (alpha is
// handled separately at each site).
struct RGB { int r, g, b; };
static inline RGB unpackRGB(ImU32 c) {
return RGB{ (int)(c & 0xFF), (int)((c >> 8) & 0xFF), (int)((c >> 16) & 0xFF) };
}
// Scales a 0..1 base alpha by the background opacity into a clamped 0..255 int.
static inline int scaledAlpha(float base, float bgOpacity) {
return std::clamp((int)(base * bgOpacity * 255.0f), 0, 255);
}
} // namespace
// ============================================================================ // ============================================================================
// Singleton // Singleton
// ============================================================================ // ============================================================================
@@ -327,16 +344,13 @@ void ThemeEffects::drawShimmer(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
if (clLeft >= clRight) return; if (clLeft >= clRight) return;
float mid = (clLeft + clRight) * 0.5f; float mid = (clLeft + clRight) * 0.5f;
int peakA = (int)(shimmer_.alpha * bgOpacity_ * 255.0f); int peakA = scaledAlpha(shimmer_.alpha, bgOpacity_);
peakA = std::clamp(peakA, 0, 255);
// Extract shimmer color RGB (ignore alpha, use shimmer_.alpha) // Extract shimmer color RGB (ignore alpha, use shimmer_.alpha)
int sR = shimmer_.color & 0xFF; RGB s = unpackRGB(shimmer_.color);
int sG = (shimmer_.color >> 8) & 0xFF;
int sB = (shimmer_.color >> 16) & 0xFF;
ImU32 clear = IM_COL32(sR, sG, sB, 0); ImU32 clear = IM_COL32(s.r, s.g, s.b, 0);
ImU32 peak = IM_COL32(sR, sG, sB, peakA); ImU32 peak = IM_COL32(s.r, s.g, s.b, peakA);
// Inset clip rect by corner rounding to prevent shimmer bleeding into rounded corners // Inset clip rect by corner rounding to prevent shimmer bleeding into rounded corners
float cr = std::min(rounding, std::min(w, h) * 0.5f) * 0.3f; float cr = std::min(rounding, std::min(w, h) * 0.5f) * 0.3f;
@@ -374,9 +388,7 @@ void ThemeEffects::drawSpecularGlare(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
float minDim = std::min(w, h); float minDim = std::min(w, h);
float glareR = minDim * specular_glare_.radius; float glareR = minDim * specular_glare_.radius;
int cR = specular_glare_.color & 0xFF; RGB c = unpackRGB(specular_glare_.color);
int cG = (specular_glare_.color >> 8) & 0xFF;
int cB = (specular_glare_.color >> 16) & 0xFF;
// Per-panel seed from position — unique drift per panel // Per-panel seed from position — unique drift per panel
float seed = pMin.x * 0.0073f + pMin.y * 0.0137f; float seed = pMin.x * 0.0073f + pMin.y * 0.0137f;
@@ -410,7 +422,7 @@ void ThemeEffects::drawSpecularGlare(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
if (a <= 0) continue; if (a <= 0) continue;
dl->AddCircleFilled(ImVec2(cx, cy), ringRadius, dl->AddCircleFilled(ImVec2(cx, cy), ringRadius,
IM_COL32(cR, cG, cB, a), 32); IM_COL32(c.r, c.g, c.b, a), 32);
} }
} }
@@ -445,18 +457,14 @@ ImU32 ThemeEffects::tintByPosition(ImU32 baseColor, float screenY) const {
float s = positional_hue_.strength; float s = positional_hue_.strength;
float inv = 1.0f - s; float inv = 1.0f - s;
int bR = baseColor & 0xFF; RGB base = unpackRGB(baseColor);
int bG = (baseColor >> 8) & 0xFF;
int bB = (baseColor >> 16) & 0xFF;
int bA = (baseColor >> 24) & 0xFF; int bA = (baseColor >> 24) & 0xFF;
int tR = tint & 0xFF; RGB t = unpackRGB(tint);
int tG = (tint >> 8) & 0xFF;
int tB = (tint >> 16) & 0xFF;
int r = (int)(bR * inv + tR * s); int r = (int)(base.r * inv + t.r * s);
int g = (int)(bG * inv + tG * s); int g = (int)(base.g * inv + t.g * s);
int b = (int)(bB * inv + tB * s); int b = (int)(base.b * inv + t.b * s);
return IM_COL32(r, g, b, bA); return IM_COL32(r, g, b, bA);
} }
@@ -474,10 +482,8 @@ void ThemeEffects::drawGlowPulse(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
float alpha = glow_pulse_.minAlpha + phase * (glow_pulse_.maxAlpha - glow_pulse_.minAlpha); float alpha = glow_pulse_.minAlpha + phase * (glow_pulse_.maxAlpha - glow_pulse_.minAlpha);
if (alpha <= 0.001f) return; if (alpha <= 0.001f) return;
int a = std::clamp((int)(alpha * bgOpacity_ * 255.0f), 0, 255); int a = scaledAlpha(alpha, bgOpacity_);
int cR = glow_pulse_.color & 0xFF; RGB c = unpackRGB(glow_pulse_.color);
int cG = (glow_pulse_.color >> 8) & 0xFF;
int cB = (glow_pulse_.color >> 16) & 0xFF;
// Border-only glow: concentric outlines with smooth Gaussian falloff. // Border-only glow: concentric outlines with smooth Gaussian falloff.
// Use many fine sub-rings (4× the pixel radius) to avoid visible // Use many fine sub-rings (4× the pixel radius) to avoid visible
@@ -496,7 +502,7 @@ void ThemeEffects::drawGlowPulse(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
dl->AddRect( dl->AddRect(
ImVec2(pMin.x - expand, pMin.y - expand), ImVec2(pMin.x - expand, pMin.y - expand),
ImVec2(pMax.x + expand, pMax.y + expand), ImVec2(pMax.x + expand, pMax.y + expand),
IM_COL32(cR, cG, cB, ringAlpha), IM_COL32(c.r, c.g, c.b, ringAlpha),
rounding + expand * 0.5f, 0, thickness); rounding + expand * 0.5f, 0, thickness);
} }
} }
@@ -513,17 +519,13 @@ void ThemeEffects::drawGradientBorderShift(ImDrawList* dl, ImVec2 pMin, ImVec2 p
float phase = std::sin(time_ * gradient_border_.speed * 2.0f * 3.14159265f) * 0.5f + 0.5f; float phase = std::sin(time_ * gradient_border_.speed * 2.0f * 3.14159265f) * 0.5f + 0.5f;
// Extract RGBA from both colors and lerp // Extract RGBA from both colors and lerp
int aR = gradient_border_.colorA & 0xFF; RGB ca = unpackRGB(gradient_border_.colorA);
int aG = (gradient_border_.colorA >> 8) & 0xFF; RGB cb = unpackRGB(gradient_border_.colorB);
int aB = (gradient_border_.colorA >> 16) & 0xFF;
int bR = gradient_border_.colorB & 0xFF;
int bG = (gradient_border_.colorB >> 8) & 0xFF;
int bB = (gradient_border_.colorB >> 16) & 0xFF;
int r = aR + (int)((bR - aR) * phase); int r = ca.r + (int)((cb.r - ca.r) * phase);
int g = aG + (int)((bG - aG) * phase); int g = ca.g + (int)((cb.g - ca.g) * phase);
int b = aB + (int)((bB - aB) * phase); int b = ca.b + (int)((cb.b - ca.b) * phase);
int a = std::clamp((int)(gradient_border_.alpha * bgOpacity_ * 255.0f), 0, 255); int a = scaledAlpha(gradient_border_.alpha, bgOpacity_);
// Draw the shifting border // Draw the shifting border
dl->AddRect(pMin, pMax, IM_COL32(r, g, b, a), dl->AddRect(pMin, pMax, IM_COL32(r, g, b, a),
@@ -640,9 +642,7 @@ void ThemeEffects::drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
float headPos = std::fmod(time_ * edge_trace_.speed, 1.0f); float headPos = std::fmod(time_ * edge_trace_.speed, 1.0f);
float traceLen = edge_trace_.length; float traceLen = edge_trace_.length;
int cR = edge_trace_.color & 0xFF; RGB c = unpackRGB(edge_trace_.color);
int cG = (edge_trace_.color >> 8) & 0xFF;
int cB = (edge_trace_.color >> 16) & 0xFF;
// Draw the trace as connected line segments with fading alpha. // Draw the trace as connected line segments with fading alpha.
// Use enough segments to make curved corners smooth. // Use enough segments to make curved corners smooth.
@@ -667,16 +667,16 @@ void ThemeEffects::drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
aFrac = aFrac * aFrac; // quadratic falloff for natural fade aFrac = aFrac * aFrac; // quadratic falloff for natural fade
int a = (int)(edge_trace_.alpha * aFrac * bgOpacity_ * 255.0f); int a = (int)(edge_trace_.alpha * aFrac * bgOpacity_ * 255.0f);
if (a > 0) { if (a > 0) {
dl->AddLine(prev, pt, IM_COL32(cR, cG, cB, a), edge_trace_.thickness); dl->AddLine(prev, pt, IM_COL32(c.r, c.g, c.b, a), edge_trace_.thickness);
} }
prev = pt; prev = pt;
} }
// Bright dot at the head // Bright dot at the head
int headA = (int)(edge_trace_.alpha * bgOpacity_ * 255.0f); int headA = scaledAlpha(edge_trace_.alpha, bgOpacity_);
ImVec2 headPt = perimeterPoint(pMin, pMax, headPos, rounding); ImVec2 headPt = perimeterPoint(pMin, pMax, headPos, rounding);
dl->AddCircleFilled(headPt, edge_trace_.thickness * 1.5f, dl->AddCircleFilled(headPt, edge_trace_.thickness * 1.5f,
IM_COL32(cR, cG, cB, headA), 8); IM_COL32(c.r, c.g, c.b, headA), 8);
} }
// ============================================================================ // ============================================================================
@@ -690,9 +690,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
float h = pMax.y - pMin.y; float h = pMax.y - pMin.y;
if (w <= 0 || h <= 0) return; if (w <= 0 || h <= 0) return;
int cR = ember_rise_.color & 0xFF; RGB c = unpackRGB(ember_rise_.color);
int cG = (ember_rise_.color >> 8) & 0xFF;
int cB = (ember_rise_.color >> 16) & 0xFF;
for (int i = 0; i < ember_rise_.count; i++) { for (int i = 0; i < ember_rise_.count; i++) {
// Golden-ratio spacing gives evenly distributed phases // Golden-ratio spacing gives evenly distributed phases
@@ -723,11 +721,11 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
// Warm core // Warm core
dl->AddCircleFilled(ImVec2(x, y), size, dl->AddCircleFilled(ImVec2(x, y), size,
IM_COL32(cR, cG, cB, a), 6); IM_COL32(c.r, c.g, c.b, a), 6);
// Softer outer glow // Softer outer glow
if (a > 30) { if (a > 30) {
dl->AddCircleFilled(ImVec2(x, y), size * 2.0f, dl->AddCircleFilled(ImVec2(x, y), size * 2.0f,
IM_COL32(cR, cG, cB, a / 4), 6); IM_COL32(c.r, c.g, c.b, a / 4), 6);
} }
} }
} }
@@ -746,9 +744,7 @@ void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
float vpY = vp->WorkPos.y; float vpY = vp->WorkPos.y;
if (vpW <= 0 || vpH <= 0) return; if (vpW <= 0 || vpH <= 0) return;
int cR = ember_rise_.color & 0xFF; RGB c = unpackRGB(ember_rise_.color);
int cG = (ember_rise_.color >> 8) & 0xFF;
int cB = (ember_rise_.color >> 16) & 0xFF;
// Viewport embers: more particles, spread across the whole screen // Viewport embers: more particles, spread across the whole screen
int vpCount = ember_rise_.count * 3; int vpCount = ember_rise_.count * 3;
@@ -777,10 +773,10 @@ void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
if (a <= 0) continue; if (a <= 0) continue;
dl->AddCircleFilled(ImVec2(x, y), size, dl->AddCircleFilled(ImVec2(x, y), size,
IM_COL32(cR, cG, cB, a), 6); IM_COL32(c.r, c.g, c.b, a), 6);
if (a > 20) { if (a > 20) {
dl->AddCircleFilled(ImVec2(x, y), size * 2.5f, dl->AddCircleFilled(ImVec2(x, y), size * 2.5f,
IM_COL32(cR, cG, cB, a / 5), 6); IM_COL32(c.r, c.g, c.b, a / 5), 6);
} }
} }
} }
@@ -799,9 +795,7 @@ void ThemeEffects::drawSandstorm(ImDrawList* dl) const {
float vpY = vp->WorkPos.y; float vpY = vp->WorkPos.y;
if (vpW <= 0 || vpH <= 0) return; if (vpW <= 0 || vpH <= 0) return;
int cR = sandstorm_.color & 0xFF; RGB c = unpackRGB(sandstorm_.color);
int cG = (sandstorm_.color >> 8) & 0xFF;
int cB = (sandstorm_.color >> 16) & 0xFF;
// Wind direction vector from angle (degrees from horizontal) // Wind direction vector from angle (degrees from horizontal)
float windRad = sandstorm_.windAngle * 3.14159265f / 180.0f; float windRad = sandstorm_.windAngle * 3.14159265f / 180.0f;
@@ -875,21 +869,21 @@ void ThemeEffects::drawSandstorm(ImDrawList* dl) const {
dl->AddLine( dl->AddLine(
ImVec2(x, y), ImVec2(x, y),
ImVec2(x + sx, y + sy), ImVec2(x + sx, y + sy),
IM_COL32(cR, cG, cB, a), IM_COL32(c.r, c.g, c.b, a),
size * 0.7f); size * 0.7f);
// Bright head dot // Bright head dot
dl->AddCircleFilled(ImVec2(x, y), size * 0.5f, dl->AddCircleFilled(ImVec2(x, y), size * 0.5f,
IM_COL32(cR, cG, cB, std::min(255, a * 3 / 2)), 5); IM_COL32(c.r, c.g, c.b, std::min(255, a * 3 / 2)), 5);
} else { } else {
// Small/slow particles: simple circle // Small/slow particles: simple circle
dl->AddCircleFilled(ImVec2(x, y), size, dl->AddCircleFilled(ImVec2(x, y), size,
IM_COL32(cR, cG, cB, a), 6); IM_COL32(c.r, c.g, c.b, a), 6);
} }
// Occasional larger dust puff (every ~8th particle, double size, half alpha) // Occasional larger dust puff (every ~8th particle, double size, half alpha)
if (i % 8 == 0 && a > 15) { if (i % 8 == 0 && a > 15) {
dl->AddCircleFilled(ImVec2(x, y), size * 2.5f, dl->AddCircleFilled(ImVec2(x, y), size * 2.5f,
IM_COL32(cR, cG, cB, a / 4), 8); IM_COL32(c.r, c.g, c.b, a / 4), 8);
} }
} }
} }
@@ -916,9 +910,7 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
float headPos = std::fmod(time_ * edge_trace_.speed + posKey, 1.0f); float headPos = std::fmod(time_ * edge_trace_.speed + posKey, 1.0f);
float traceLen = edge_trace_.length; float traceLen = edge_trace_.length;
int cR = edge_trace_.color & 0xFF; RGB c = unpackRGB(edge_trace_.color);
int cG = (edge_trace_.color >> 8) & 0xFF;
int cB = (edge_trace_.color >> 16) & 0xFF;
// Panel edge trace is subtler than sidebar — 60% alpha // Panel edge trace is subtler than sidebar — 60% alpha
float panelAlpha = edge_trace_.alpha * 0.6f * bgOpacity_; float panelAlpha = edge_trace_.alpha * 0.6f * bgOpacity_;
@@ -935,7 +927,7 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
aFrac = aFrac * aFrac; aFrac = aFrac * aFrac;
int a = (int)(panelAlpha * aFrac * 255.0f); int a = (int)(panelAlpha * aFrac * 255.0f);
if (a > 0) { if (a > 0) {
dl->AddLine(prev, pt, IM_COL32(cR, cG, cB, a), dl->AddLine(prev, pt, IM_COL32(c.r, c.g, c.b, a),
edge_trace_.thickness * 0.8f); edge_trace_.thickness * 0.8f);
} }
prev = pt; prev = pt;
@@ -945,7 +937,7 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
int headA = (int)(panelAlpha * 255.0f); int headA = (int)(panelAlpha * 255.0f);
ImVec2 headPt = perimeterPoint(pMin, pMax, headPos, rounding); ImVec2 headPt = perimeterPoint(pMin, pMax, headPos, rounding);
dl->AddCircleFilled(headPt, edge_trace_.thickness * 1.2f, dl->AddCircleFilled(headPt, edge_trace_.thickness * 1.2f,
IM_COL32(cR, cG, cB, headA), 6); IM_COL32(c.r, c.g, c.b, headA), 6);
} }
} }
@@ -958,9 +950,7 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
int panelCount = std::max(2, ember_rise_.count / 3); int panelCount = std::max(2, ember_rise_.count / 3);
float panelAlpha = ember_rise_.alpha * 0.5f * bgOpacity_; float panelAlpha = ember_rise_.alpha * 0.5f * bgOpacity_;
int cR = ember_rise_.color & 0xFF; RGB c = unpackRGB(ember_rise_.color);
int cG = (ember_rise_.color >> 8) & 0xFF;
int cB = (ember_rise_.color >> 16) & 0xFF;
// Use panel position as seed for unique particle distribution // Use panel position as seed for unique particle distribution
float seed = pMin.x * 0.013f + pMin.y * 0.031f; float seed = pMin.x * 0.013f + pMin.y * 0.031f;
@@ -987,7 +977,7 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
if (a <= 0) continue; if (a <= 0) continue;
dl->AddCircleFilled(ImVec2(x, y), size, dl->AddCircleFilled(ImVec2(x, y), size,
IM_COL32(cR, cG, cB, a), 6); IM_COL32(c.r, c.g, c.b, a), 6);
} }
dl->PopClipRect(); dl->PopClipRect();
@@ -1074,14 +1064,12 @@ void ThemeEffects::drawViewportOverlay(ImDrawList* dl) const {
// === Vignette: edge darkening/tinting (4 gradient strips) === // === Vignette: edge darkening/tinting (4 gradient strips) ===
// Overlapping strips naturally darken corners more than edges — cinematic look // Overlapping strips naturally darken corners more than edges — cinematic look
if (viewport_overlay_.vignetteEnabled) { if (viewport_overlay_.vignetteEnabled) {
int cR = viewport_overlay_.vignetteColor & 0xFF; RGB c = unpackRGB(viewport_overlay_.vignetteColor);
int cG = (viewport_overlay_.vignetteColor >> 8) & 0xFF; int maxA = scaledAlpha(viewport_overlay_.vignetteAlpha, bgOpacity_);
int cB = (viewport_overlay_.vignetteColor >> 16) & 0xFF;
int maxA = std::clamp((int)(viewport_overlay_.vignetteAlpha * bgOpacity_ * 255.0f), 0, 255);
if (maxA > 0) { if (maxA > 0) {
ImU32 edgeCol = IM_COL32(cR, cG, cB, maxA); ImU32 edgeCol = IM_COL32(c.r, c.g, c.b, maxA);
ImU32 clearCol = IM_COL32(cR, cG, cB, 0); ImU32 clearCol = IM_COL32(c.r, c.g, c.b, 0);
float fadeW = vpSize.x * viewport_overlay_.vignetteRadius; float fadeW = vpSize.x * viewport_overlay_.vignetteRadius;
float fadeH = vpSize.y * viewport_overlay_.vignetteRadius; float fadeH = vpSize.y * viewport_overlay_.vignetteRadius;

View File

@@ -13,17 +13,6 @@ namespace wallet {
namespace { namespace {
std::string trimCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
bool startsWith(const std::string& value, const char* prefix) bool startsWith(const std::string& value, const char* prefix)
{ {
const std::string prefixValue(prefix); const std::string prefixValue(prefix);
@@ -34,7 +23,7 @@ bool startsWith(const std::string& value, const char* prefix)
LiteServerEndpoint trimmedEndpoint(const LiteServerEndpoint& endpoint) LiteServerEndpoint trimmedEndpoint(const LiteServerEndpoint& endpoint)
{ {
LiteServerEndpoint normalized = endpoint; LiteServerEndpoint normalized = endpoint;
normalized.url = trimCopy(normalized.url); normalized.url = liteTrimCopy(normalized.url);
return normalized; return normalized;
} }
@@ -52,12 +41,23 @@ std::vector<std::pair<std::size_t, LiteServerEndpoint>> usableConfiguredServers(
std::string normalizedChainName(const std::string& chainName) std::string normalizedChainName(const std::string& chainName)
{ {
const std::string normalized = trimCopy(chainName); const std::string normalized = liteTrimCopy(chainName);
return normalized.empty() ? kDragonXLiteChainName : normalized; return normalized.empty() ? kDragonXLiteChainName : normalized;
} }
} // namespace } // namespace
std::string liteTrimCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
std::vector<LiteServerEndpoint> defaultDragonXLiteServers() std::vector<LiteServerEndpoint> defaultDragonXLiteServers()
{ {
return { return {
@@ -83,13 +83,13 @@ LiteConnectionSettings defaultLiteConnectionSettings()
bool isLiteServerUrlUsable(const std::string& serverUrl) bool isLiteServerUrlUsable(const std::string& serverUrl)
{ {
const std::string normalized = trimCopy(serverUrl); const std::string normalized = liteTrimCopy(serverUrl);
return startsWith(normalized, "https://") || startsWith(normalized, "http://"); return startsWith(normalized, "https://") || startsWith(normalized, "http://");
} }
bool isOfficialLiteServer(const std::string& serverUrl) bool isOfficialLiteServer(const std::string& serverUrl)
{ {
const std::string url = trimCopy(serverUrl); const std::string url = liteTrimCopy(serverUrl);
for (const auto& s : defaultDragonXLiteServers()) for (const auto& s : defaultDragonXLiteServers())
if (s.url == url) return true; if (s.url == url) return true;
return false; return false;
@@ -126,7 +126,7 @@ const char* liteConnectionAvailabilityName(LiteConnectionAvailability availabili
LiteServerSelectionResult selectLiteServer(const LiteConnectionSettings& settings) LiteServerSelectionResult selectLiteServer(const LiteConnectionSettings& settings)
{ {
auto usableServers = usableConfiguredServers(settings); auto usableServers = usableConfiguredServers(settings);
const std::string stickyServerUrl = trimCopy(settings.stickyServerUrl); const std::string stickyServerUrl = liteTrimCopy(settings.stickyServerUrl);
if (settings.selectionMode == LiteServerSelectionMode::Sticky && if (settings.selectionMode == LiteServerSelectionMode::Sticky &&
isLiteServerUrlUsable(stickyServerUrl)) { isLiteServerUrlUsable(stickyServerUrl)) {

View File

@@ -84,6 +84,8 @@ struct LiteServerHealthCheckResult {
std::string error; std::string error;
}; };
// Trims leading/trailing whitespace, returning a copy. Shared across the lite slice.
std::string liteTrimCopy(const std::string& value);
std::vector<LiteServerEndpoint> defaultDragonXLiteServers(); std::vector<LiteServerEndpoint> defaultDragonXLiteServers();
LiteConnectionSettings defaultLiteConnectionSettings(); LiteConnectionSettings defaultLiteConnectionSettings();
bool isLiteServerUrlUsable(const std::string& serverUrl); bool isLiteServerUrlUsable(const std::string& serverUrl);

View File

@@ -1,24 +1,12 @@
#include "wallet/lite_sync_service.h" #include "wallet/lite_sync_service.h"
#include <algorithm> #include <algorithm>
#include <cctype>
#include <limits> #include <limits>
#include <utility> #include <utility>
namespace dragonx::wallet { namespace dragonx::wallet {
namespace { namespace {
std::string trimSyncCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
std::uint64_t effectiveStagnantThreshold(std::uint64_t threshold) std::uint64_t effectiveStagnantThreshold(std::uint64_t threshold)
{ {
return threshold == 0 ? 1 : threshold; return threshold == 0 ? 1 : threshold;
@@ -283,7 +271,7 @@ LiteSyncStatusResult LiteSyncService::pollSyncStatus(const LiteSyncStatusRequest
LiteServerSelectionResult LiteSyncService::selectServerForRequest(const std::string& serverUrl) const LiteServerSelectionResult LiteSyncService::selectServerForRequest(const std::string& serverUrl) const
{ {
const auto overrideUrl = trimSyncCopy(serverUrl); const auto overrideUrl = liteTrimCopy(serverUrl);
if (!overrideUrl.empty()) { if (!overrideUrl.empty()) {
if (!isLiteServerUrlUsable(overrideUrl)) { if (!isLiteServerUrlUsable(overrideUrl)) {
return LiteServerSelectionResult{false, {}, 0, false, "lite sync server URL is not usable"}; return LiteServerSelectionResult{false, {}, 0, false, "lite sync server URL is not usable"};

View File

@@ -5,6 +5,7 @@
#include "lite_wallet_controller.h" #include "lite_wallet_controller.h"
#include "lite_diagnostics.h" #include "lite_diagnostics.h"
#include "lite_result_parsers.h"
#include "../data/wallet_state.h" #include "../data/wallet_state.h"
#include <algorithm> #include <algorithm>
@@ -695,19 +696,14 @@ std::optional<LiteWalletAppRefreshModel> LiteWalletController::refreshModel()
if (bridge_) { if (bridge_) {
const auto h = bridge_->execute("height", ""); const auto h = bridge_->execute("height", "");
if (h.ok) { if (h.ok) {
try { const auto parsed = parseLiteHeightResponse(h.value);
const auto j = nlohmann::json::parse(h.value); if (parsed.ok && parsed.height.height > 0) {
if (j.is_object() && j.contains("height") && j["height"].is_number_unsigned()) { model.hasSyncStatus = true;
const unsigned long long height = j["height"].get<unsigned long long>(); model.sync.walletHeight = parsed.height.height;
if (height > 0) { model.sync.chainHeight = parsed.height.height; // synced: wallet height == chain tip
model.hasSyncStatus = true; model.sync.complete = true;
model.sync.walletHeight = height; model.sync.progress = 1.0;
model.sync.chainHeight = height; // synced: wallet height == chain tip }
model.sync.complete = true;
model.sync.progress = 1.0;
}
}
} catch (...) { /* leave the mapped sync fields as-is */ }
} }
} }
return model; return model;

View File

@@ -1,22 +1,10 @@
#include "wallet/lite_wallet_gateway.h" #include "wallet/lite_wallet_gateway.h"
#include <cctype>
#include <utility> #include <utility>
namespace dragonx::wallet { namespace dragonx::wallet {
namespace { namespace {
std::string trimGatewayCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
WalletBackendStatus makeGatewayStatus(WalletBackendState state, std::string message) WalletBackendStatus makeGatewayStatus(WalletBackendState state, std::string message)
{ {
WalletBackendStatus status; WalletBackendStatus status;
@@ -262,7 +250,7 @@ LiteWalletRefreshResult LiteWalletGateway::refresh(const LiteWalletRefreshReques
LiteServerSelectionResult LiteWalletGateway::selectServerForRequest(const std::string& serverUrl) const LiteServerSelectionResult LiteWalletGateway::selectServerForRequest(const std::string& serverUrl) const
{ {
const auto overrideUrl = trimGatewayCopy(serverUrl); const auto overrideUrl = liteTrimCopy(serverUrl);
if (!overrideUrl.empty()) { if (!overrideUrl.empty()) {
if (!isLiteServerUrlUsable(overrideUrl)) { if (!isLiteServerUrlUsable(overrideUrl)) {
return LiteServerSelectionResult{false, {}, 0, false, "lite gateway server URL is not usable"}; return LiteServerSelectionResult{false, {}, 0, false, "lite gateway server URL is not usable"};

View File

@@ -4,7 +4,6 @@
#include "lite_wallet_lifecycle_service.h" #include "lite_wallet_lifecycle_service.h"
#include <cctype>
#include <utility> #include <utility>
namespace dragonx { namespace dragonx {
@@ -12,17 +11,6 @@ namespace wallet {
namespace { namespace {
std::string trimLifecycleCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
LiteRedactedPrivateData redactedField(LitePrivateDataKind kind, const std::string& value) LiteRedactedPrivateData redactedField(LitePrivateDataKind kind, const std::string& value)
{ {
return LiteRedactedPrivateData{kind, !value.empty(), redactLitePrivateDataValue(value)}; return LiteRedactedPrivateData{kind, !value.empty(), redactLitePrivateDataValue(value)};
@@ -172,7 +160,7 @@ LiteWalletLifecyclePlan LiteWalletLifecycleService::planOpenWallet(
LiteWalletLifecyclePlan LiteWalletLifecycleService::planRestoreWallet( LiteWalletLifecyclePlan LiteWalletLifecycleService::planRestoreWallet(
const LiteWalletRestoreRequest& request) const const LiteWalletRestoreRequest& request) const
{ {
const std::string validationError = trimLifecycleCopy(request.seedPhrase).empty() const std::string validationError = liteTrimCopy(request.seedPhrase).empty()
? "restore seed phrase is required" ? "restore seed phrase is required"
: std::string(); : std::string();
return makePlan(LiteWalletLifecycleOperation::RestoreFromSeed, return makePlan(LiteWalletLifecycleOperation::RestoreFromSeed,
@@ -220,7 +208,7 @@ LiteWalletLifecycleResult LiteWalletLifecycleService::restoreWallet(
LiteServerSelectionResult LiteWalletLifecycleService::selectServerForRequest( LiteServerSelectionResult LiteWalletLifecycleService::selectServerForRequest(
const std::string& serverUrl) const const std::string& serverUrl) const
{ {
const std::string overrideUrl = trimLifecycleCopy(serverUrl); const std::string overrideUrl = liteTrimCopy(serverUrl);
if (!overrideUrl.empty()) { if (!overrideUrl.empty()) {
if (!isLiteServerUrlUsable(overrideUrl)) { if (!isLiteServerUrlUsable(overrideUrl)) {
return LiteServerSelectionResult{false, {}, 0, false, "lite lifecycle server URL is not usable"}; return LiteServerSelectionResult{false, {}, 0, false, "lite lifecycle server URL is not usable"};

View File

@@ -1,25 +1,10 @@
#include "lite_wallet_server_selection_adapter.h" #include "lite_wallet_server_selection_adapter.h"
#include <algorithm>
#include <cctype>
namespace dragonx { namespace dragonx {
namespace wallet { namespace wallet {
namespace { namespace {
std::string trimCopy(const std::string& value)
{
const auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) {
return std::isspace(ch) != 0;
});
const auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) {
return std::isspace(ch) != 0;
}).base();
if (first >= last) return {};
return std::string(first, last);
}
LiteServerSelectionMode walletModeFromSettings( LiteServerSelectionMode walletModeFromSettings(
config::Settings::LiteServerSelectionPreferenceMode mode) config::Settings::LiteServerSelectionPreferenceMode mode)
{ {
@@ -52,14 +37,14 @@ LiteConnectionSettings liteConnectionSettingsFromAppSettings(const config::Setti
connectionSettings.servers.clear(); connectionSettings.servers.clear();
for (const auto& server : settings.getLiteServers()) { for (const auto& server : settings.getLiteServers()) {
connectionSettings.servers.push_back(LiteServerEndpoint{ connectionSettings.servers.push_back(LiteServerEndpoint{
trimCopy(server.url), liteTrimCopy(server.url),
server.label, server.label,
server.enabled server.enabled
}); });
} }
connectionSettings.selectionMode = walletModeFromSettings(settings.getLiteServerSelectionMode()); connectionSettings.selectionMode = walletModeFromSettings(settings.getLiteServerSelectionMode());
connectionSettings.stickyServerUrl = trimCopy(settings.getLiteStickyServerUrl()); connectionSettings.stickyServerUrl = liteTrimCopy(settings.getLiteStickyServerUrl());
connectionSettings.chainName = trimCopy(settings.getLiteChainName()); connectionSettings.chainName = liteTrimCopy(settings.getLiteChainName());
if (connectionSettings.chainName.empty()) connectionSettings.chainName = kDragonXLiteChainName; if (connectionSettings.chainName.empty()) connectionSettings.chainName = kDragonXLiteChainName;
connectionSettings.randomSelectionSeed = settings.getLiteRandomSelectionSeed(); connectionSettings.randomSelectionSeed = settings.getLiteRandomSelectionSeed();
return connectionSettings; return connectionSettings;
@@ -72,24 +57,24 @@ void applyLiteConnectionSettingsToAppSettings(config::Settings& settings,
servers.reserve(connectionSettings.servers.size()); servers.reserve(connectionSettings.servers.size());
for (const auto& server : connectionSettings.servers) { for (const auto& server : connectionSettings.servers) {
servers.push_back(config::Settings::LiteServerPreference{ servers.push_back(config::Settings::LiteServerPreference{
trimCopy(server.url), liteTrimCopy(server.url),
server.label, server.label,
server.enabled server.enabled
}); });
} }
settings.setLiteServerSelectionMode(settingsModeFromWallet(connectionSettings.selectionMode)); settings.setLiteServerSelectionMode(settingsModeFromWallet(connectionSettings.selectionMode));
settings.setLiteStickyServerUrl(trimCopy(connectionSettings.stickyServerUrl)); settings.setLiteStickyServerUrl(liteTrimCopy(connectionSettings.stickyServerUrl));
settings.setLiteChainName(trimCopy(connectionSettings.chainName).empty() settings.setLiteChainName(liteTrimCopy(connectionSettings.chainName).empty()
? kDragonXLiteChainName ? kDragonXLiteChainName
: trimCopy(connectionSettings.chainName)); : liteTrimCopy(connectionSettings.chainName));
settings.setLiteRandomSelectionSeed(connectionSettings.randomSelectionSeed); settings.setLiteRandomSelectionSeed(connectionSettings.randomSelectionSeed);
settings.setLiteServers(servers); settings.setLiteServers(servers);
} }
std::string redactLiteServerSelectionValue(const std::string& value) std::string redactLiteServerSelectionValue(const std::string& value)
{ {
const std::string trimmed = trimCopy(value); const std::string trimmed = liteTrimCopy(value);
if (trimmed.empty()) return "<empty>"; if (trimmed.empty()) return "<empty>";
const auto scheme = trimmed.find("://"); const auto scheme = trimmed.find("://");
if (scheme == std::string::npos) return "<redacted>"; if (scheme == std::string::npos) return "<redacted>";