Refactor app services and stabilize refresh/UI flows
- Add refresh scheduler and network refresh service boundaries for typed refresh results, ordered RPC collectors, applicators, and price parsing. - Add daemon lifecycle and wallet security workflow helpers while preserving App-owned command RPC, decrypt, cancellation, and UI handoff behavior. - Split balance, console, mining, amount formatting, and async task logic into focused modules with expanded Phase 4 test coverage. - Fix market price loading by triggering price refresh immediately, avoiding queue-pressure drops, tracking loading/error state, and adding translations. - Polish send, explorer, peers, settings, theme/schema, and related tab UI. - Replace checked-in generated language headers with build-generated resources. - Document the cleanup audit, UI static-state guidance, and architecture updates.
This commit is contained in:
@@ -6,11 +6,14 @@
|
||||
#include "../config/version.h"
|
||||
#include "../resources/embedded_resources.h"
|
||||
|
||||
#include <sodium.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
#include "../util/logger.h"
|
||||
|
||||
@@ -26,6 +29,56 @@ namespace fs = std::filesystem;
|
||||
namespace dragonx {
|
||||
namespace rpc {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string generateSecureRandomString(size_t length)
|
||||
{
|
||||
static constexpr char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
static constexpr uint32_t charsetSize = static_cast<uint32_t>(sizeof(charset) - 1);
|
||||
|
||||
if (sodium_init() < 0) {
|
||||
DEBUG_LOGF("Failed to initialize libsodium for RPC credential generation\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result;
|
||||
result.reserve(length);
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
result.push_back(charset[randombytes_uniform(charsetSize)]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string lowercase(std::string value)
|
||||
{
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
bool parseBoolValue(const std::string& value)
|
||||
{
|
||||
std::string lowered = lowercase(value);
|
||||
return lowered == "1" || lowered == "true" || lowered == "yes" || lowered == "on";
|
||||
}
|
||||
|
||||
bool applyCookieAuth(ConnectionConfig& config, const std::string& dataDir)
|
||||
{
|
||||
std::string cookieUser, cookiePass;
|
||||
if (!Connection::readAuthCookie(dataDir, cookieUser, cookiePass)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
config.rpcuser = cookieUser;
|
||||
config.rpcpassword = cookiePass;
|
||||
config.auth_source = AuthSource::Cookie;
|
||||
if (config.hush_dir.empty()) config.hush_dir = dataDir;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Connection::Connection() = default;
|
||||
Connection::~Connection() = default;
|
||||
|
||||
@@ -140,8 +193,14 @@ ConnectionConfig Connection::parseConfFile(const std::string& path)
|
||||
config.host = value;
|
||||
} else if (key == "proxy") {
|
||||
config.proxy = value;
|
||||
} else if (key == "rpctls" || key == "rpcssl" || key == "use_tls" || key == "rpcuse_tls") {
|
||||
config.use_tls = parseBoolValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.rpcuser.empty() || !config.rpcpassword.empty()) {
|
||||
config.auth_source = AuthSource::ConfigFile;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -177,10 +236,7 @@ ConnectionConfig Connection::autoDetectConfig()
|
||||
|
||||
// If rpcpassword is empty, the daemon may be using .cookie auth
|
||||
if (config.rpcpassword.empty()) {
|
||||
std::string cookieUser, cookiePass;
|
||||
if (readAuthCookie(data_dir, cookieUser, cookiePass)) {
|
||||
config.rpcuser = cookieUser;
|
||||
config.rpcpassword = cookiePass;
|
||||
if (applyCookieAuth(config, data_dir)) {
|
||||
DEBUG_LOGF("Using .cookie authentication (no rpcpassword in config)\n");
|
||||
}
|
||||
}
|
||||
@@ -196,23 +252,57 @@ ConnectionConfig Connection::autoDetectConfig()
|
||||
return config;
|
||||
}
|
||||
|
||||
bool Connection::buildCookieAuthConfig(const ConnectionConfig& base, ConnectionConfig& cookieConfig)
|
||||
{
|
||||
if (base.auth_source == AuthSource::Cookie) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string dataDir = base.hush_dir.empty() ? getDefaultDataDir() : base.hush_dir;
|
||||
ConnectionConfig fallback = base;
|
||||
if (!applyCookieAuth(fallback, dataDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cookieConfig = std::move(fallback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Connection::isLocalHost(const std::string& host)
|
||||
{
|
||||
std::string lowered = lowercase(host);
|
||||
if (!lowered.empty() && lowered.front() == '[' && lowered.back() == ']') {
|
||||
lowered = lowered.substr(1, lowered.size() - 2);
|
||||
}
|
||||
|
||||
return lowered == "localhost" || lowered == "localhost." ||
|
||||
lowered == "::1" || lowered == "0:0:0:0:0:0:0:1" ||
|
||||
lowered == "127.0.0.1" || lowered.rfind("127.", 0) == 0;
|
||||
}
|
||||
|
||||
bool Connection::usesPlaintextRemote(const ConnectionConfig& config)
|
||||
{
|
||||
return !config.use_tls && !isLocalHost(config.host);
|
||||
}
|
||||
|
||||
const char* Connection::authSourceName(AuthSource source)
|
||||
{
|
||||
switch (source) {
|
||||
case AuthSource::ConfigFile: return "config";
|
||||
case AuthSource::Cookie: return "cookie";
|
||||
case AuthSource::Missing: return "missing";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
bool Connection::createDefaultConfig(const std::string& path)
|
||||
{
|
||||
// Generate random rpcuser/rpcpassword
|
||||
auto generateRandomString = [](int length) -> std::string {
|
||||
const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
std::string result;
|
||||
result.reserve(length);
|
||||
|
||||
std::srand(static_cast<unsigned>(std::time(nullptr)));
|
||||
for (int i = 0; i < length; i++) {
|
||||
result += charset[std::rand() % (sizeof(charset) - 1)];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
std::string rpcuser = generateRandomString(16);
|
||||
std::string rpcpassword = generateRandomString(32);
|
||||
std::string rpcuser = generateSecureRandomString(16);
|
||||
std::string rpcpassword = generateSecureRandomString(32);
|
||||
if (rpcuser.empty() || rpcpassword.empty()) {
|
||||
DEBUG_LOGF("Failed to generate secure RPC credentials for config file: %s\n", path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream file(path);
|
||||
if (!file.is_open()) {
|
||||
|
||||
@@ -12,6 +12,12 @@ namespace rpc {
|
||||
/**
|
||||
* @brief Connection configuration
|
||||
*/
|
||||
enum class AuthSource {
|
||||
Missing,
|
||||
ConfigFile,
|
||||
Cookie
|
||||
};
|
||||
|
||||
struct ConnectionConfig {
|
||||
std::string host = "127.0.0.1";
|
||||
std::string port = "21769";
|
||||
@@ -20,6 +26,8 @@ struct ConnectionConfig {
|
||||
std::string hush_dir;
|
||||
std::string proxy; // SOCKS5 proxy for Tor
|
||||
bool use_embedded = true;
|
||||
bool use_tls = false;
|
||||
AuthSource auth_source = AuthSource::Missing;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -96,6 +104,23 @@ public:
|
||||
*/
|
||||
static bool readAuthCookie(const std::string& dataDir, std::string& user, std::string& password);
|
||||
|
||||
/**
|
||||
* @brief Build a cookie-auth retry config from a failed config-auth attempt
|
||||
*/
|
||||
static bool buildCookieAuthConfig(const ConnectionConfig& base, ConnectionConfig& cookieConfig);
|
||||
|
||||
/**
|
||||
* @brief Whether a host is local enough for plaintext HTTP RPC
|
||||
*/
|
||||
static bool isLocalHost(const std::string& host);
|
||||
|
||||
/**
|
||||
* @brief Whether this config would send RPC credentials over plaintext to a remote host
|
||||
*/
|
||||
static bool usesPlaintextRemote(const ConnectionConfig& config);
|
||||
|
||||
static const char* authSourceName(AuthSource source);
|
||||
|
||||
private:
|
||||
};
|
||||
|
||||
|
||||
@@ -60,6 +60,13 @@ RPCClient::~RPCClient() = default;
|
||||
|
||||
bool RPCClient::connect(const std::string& host, const std::string& port,
|
||||
const std::string& user, const std::string& password)
|
||||
{
|
||||
return connect(host, port, user, password, false);
|
||||
}
|
||||
|
||||
bool RPCClient::connect(const std::string& host, const std::string& port,
|
||||
const std::string& user, const std::string& password,
|
||||
bool useTls)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||
host_ = host;
|
||||
@@ -69,8 +76,7 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
||||
std::string credentials = user + ":" + password;
|
||||
auth_ = util::base64_encode(credentials);
|
||||
|
||||
// Build URL - use HTTP for localhost RPC (TLS not always enabled)
|
||||
impl_->url = "http://" + host + ":" + port + "/";
|
||||
impl_->url = std::string(useTls ? "https://" : "http://") + host + ":" + port + "/";
|
||||
VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str());
|
||||
|
||||
// Clean up previous curl handle/headers to avoid leaks on retries
|
||||
|
||||
@@ -43,6 +43,10 @@ public:
|
||||
bool connect(const std::string& host, const std::string& port,
|
||||
const std::string& user, const std::string& password);
|
||||
|
||||
bool connect(const std::string& host, const std::string& port,
|
||||
const std::string& user, const std::string& password,
|
||||
bool useTls);
|
||||
|
||||
/**
|
||||
* @brief Disconnect from dragonxd
|
||||
*/
|
||||
|
||||
@@ -97,6 +97,18 @@ bool RPCWorker::hasPendingResults() const
|
||||
return !results_.empty();
|
||||
}
|
||||
|
||||
std::size_t RPCWorker::pendingTaskCount() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(taskMtx_);
|
||||
return tasks_.size();
|
||||
}
|
||||
|
||||
std::size_t RPCWorker::pendingResultCount() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(resultMtx_);
|
||||
return results_.size();
|
||||
}
|
||||
|
||||
void RPCWorker::run()
|
||||
{
|
||||
while (true) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
@@ -69,6 +70,8 @@ public:
|
||||
|
||||
/// True when there are completed results waiting for the main thread.
|
||||
bool hasPendingResults() const;
|
||||
std::size_t pendingTaskCount() const;
|
||||
std::size_t pendingResultCount() const;
|
||||
|
||||
/// True when the worker thread is running.
|
||||
bool isRunning() const { return running_.load(std::memory_order_relaxed); }
|
||||
@@ -80,7 +83,7 @@ private:
|
||||
std::atomic<bool> running_{false};
|
||||
|
||||
// ---- Task queue (produced by main thread, consumed by worker) ----
|
||||
std::mutex taskMtx_;
|
||||
mutable std::mutex taskMtx_;
|
||||
std::condition_variable taskCv_;
|
||||
std::deque<WorkFn> tasks_;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user