Add a full-node daemon updater (util/DaemonUpdater + daemon_download_dialog) reachable from Settings -> NODE & SECURITY: downloads/verifies (SHA-256 + enforced ed25519 signature) and atomically installs the latest dragonxd from the project Gitea, with a "Restart daemon now" step. Add a shared "Browse all releases..." picker (release_list_view) to both the miner and daemon updaters so users can pin older/pre-release builds. Pure no-I/O cores (daemon_updater_core / xmrig_updater_core) are unit-tested; sign-daemon-release.sh signs release archives offline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
207 lines
7.3 KiB
C++
207 lines
7.3 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
//
|
|
// Pure (no-I/O) core of the daemon updater: release-JSON parsing, platform/asset matching, the
|
|
// markdown checksum-table parser, version normalization, and signature-asset selection. Split from
|
|
// daemon_updater.cpp (the libcurl/miniz worker) so it can be unit-tested without curl/miniz. The
|
|
// generic SHA-256 / ed25519 primitives are reused from the miner updater (xmrig_updater_core.cpp).
|
|
|
|
#include "daemon_updater.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <sstream>
|
|
|
|
using json = nlohmann::json;
|
|
|
|
namespace dragonx {
|
|
namespace util {
|
|
|
|
namespace {
|
|
|
|
std::string toLower(std::string s)
|
|
{
|
|
std::transform(s.begin(), s.end(), s.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
return s;
|
|
}
|
|
|
|
bool isHex64(const std::string& s)
|
|
{
|
|
if (s.size() != 64) return false;
|
|
for (unsigned char c : s)
|
|
if (!std::isxdigit(c)) return false;
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace {
|
|
// Fill a DaemonRelease from one Gitea release JSON object (ok=true iff it has a tag).
|
|
DaemonRelease parseOneDaemonRelease(const json& j)
|
|
{
|
|
DaemonRelease r;
|
|
if (!j.is_object()) return r;
|
|
if (j.contains("tag_name") && j["tag_name"].is_string())
|
|
r.tag = j["tag_name"].get<std::string>();
|
|
if (j.contains("name") && j["name"].is_string())
|
|
r.name = j["name"].get<std::string>();
|
|
if (j.contains("body") && j["body"].is_string())
|
|
r.body = j["body"].get<std::string>();
|
|
if (j.contains("prerelease") && j["prerelease"].is_boolean())
|
|
r.prerelease = j["prerelease"].get<bool>();
|
|
if (j.contains("published_at") && j["published_at"].is_string())
|
|
r.publishedAt = j["published_at"].get<std::string>();
|
|
if (j.contains("assets") && j["assets"].is_array()) {
|
|
for (const auto& a : j["assets"]) {
|
|
if (!a.is_object()) continue;
|
|
DaemonReleaseAsset asset;
|
|
if (a.contains("name") && a["name"].is_string())
|
|
asset.name = a["name"].get<std::string>();
|
|
if (a.contains("browser_download_url") && a["browser_download_url"].is_string())
|
|
asset.downloadUrl = a["browser_download_url"].get<std::string>();
|
|
if (a.contains("size") && a["size"].is_number_integer())
|
|
asset.size = a["size"].get<long long>();
|
|
if (!asset.name.empty() && !asset.downloadUrl.empty())
|
|
r.assets.push_back(std::move(asset));
|
|
}
|
|
}
|
|
if (!r.tag.empty()) r.ok = true;
|
|
return r;
|
|
}
|
|
} // namespace
|
|
|
|
DaemonRelease parseDaemonRelease(const std::string& jsonStr)
|
|
{
|
|
DaemonRelease r;
|
|
try {
|
|
r = parseOneDaemonRelease(json::parse(jsonStr));
|
|
if (!r.ok) r.error = "release JSON has no tag_name";
|
|
} catch (const std::exception& e) {
|
|
r.error = std::string("failed to parse release JSON: ") + e.what();
|
|
}
|
|
return r;
|
|
}
|
|
|
|
std::vector<DaemonRelease> parseDaemonReleaseList(const std::string& jsonStr)
|
|
{
|
|
std::vector<DaemonRelease> out;
|
|
try {
|
|
const json j = json::parse(jsonStr);
|
|
if (!j.is_array()) return out;
|
|
for (const auto& e : j) {
|
|
if (e.contains("draft") && e["draft"].is_boolean() && e["draft"].get<bool>())
|
|
continue; // skip drafts (not meant for end users)
|
|
DaemonRelease r = parseOneDaemonRelease(e);
|
|
if (r.ok) out.push_back(std::move(r));
|
|
}
|
|
} catch (const std::exception&) {
|
|
// malformed list -> empty (caller treats as "could not load releases")
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string currentDaemonPlatformToken()
|
|
{
|
|
#if defined(_WIN32)
|
|
return "win64";
|
|
#elif defined(__APPLE__)
|
|
return "macos"; // single macOS archive (no arm/x86 split in the release naming)
|
|
#elif defined(__linux__)
|
|
#if defined(__aarch64__)
|
|
return "linux-arm64"; // no arm64 build published yet -> resolves to Unavailable
|
|
#else
|
|
return "linux-amd64";
|
|
#endif
|
|
#else
|
|
return "";
|
|
#endif
|
|
}
|
|
|
|
int selectDaemonAsset(const DaemonRelease& release, const std::string& platformToken)
|
|
{
|
|
if (platformToken.empty()) return -1;
|
|
const std::string needle = "-" + toLower(platformToken) + ".zip";
|
|
for (std::size_t i = 0; i < release.assets.size(); ++i) {
|
|
const std::string n = toLower(release.assets[i].name);
|
|
if (n.size() >= needle.size() &&
|
|
n.compare(n.size() - needle.size(), needle.size(), needle) == 0)
|
|
return static_cast<int>(i);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
std::map<std::string, std::string> parseDaemonChecksums(const std::string& body)
|
|
{
|
|
// The release body publishes checksums as a markdown table:
|
|
// | File | SHA-256 |
|
|
// |------|---------|
|
|
// | dragonx-1.0.2-linux-amd64.zip | `85f1dd…16` |
|
|
// Per line: blank out the table/code delimiters ('|' and '`'), then find the 64-hex token (the
|
|
// hash) and a token ending in ".zip" (the archive name). Header/separator/prose rows lack one
|
|
// or the other and are skipped, so this is robust to surrounding text and column order.
|
|
std::map<std::string, std::string> out;
|
|
std::istringstream in(body);
|
|
std::string line;
|
|
while (std::getline(in, line)) {
|
|
for (char& c : line)
|
|
if (c == '|' || c == '`') c = ' ';
|
|
std::istringstream ls(line);
|
|
std::string tok, hash, name;
|
|
while (ls >> tok) {
|
|
if (hash.empty() && isHex64(tok)) { hash = toLower(tok); continue; }
|
|
if (name.empty()) {
|
|
const std::string low = toLower(tok);
|
|
// Key by the lowercased name so the lookup (also lowercased) is case-insensitive,
|
|
// in case the markdown table and the JSON asset names differ in case.
|
|
if (low.size() >= 4 && low.compare(low.size() - 4, 4, ".zip") == 0) name = low;
|
|
}
|
|
}
|
|
if (!hash.empty() && !name.empty()) out[name] = hash;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::vector<std::string> daemonExtractBasenames(const std::string& platformToken)
|
|
{
|
|
if (platformToken.rfind("win", 0) == 0)
|
|
return {"dragonxd.exe", "dragonx-cli.exe", "dragonx-tx.exe"};
|
|
return {"dragonxd", "dragonx-cli", "dragonx-tx"};
|
|
}
|
|
|
|
int selectDaemonSignatureAsset(const DaemonRelease& release, const std::string& archiveName)
|
|
{
|
|
if (archiveName.empty()) return -1;
|
|
for (std::size_t i = 0; i < release.assets.size(); ++i) {
|
|
const std::string& n = release.assets[i].name;
|
|
if (n == archiveName + ".sig" || n == archiveName + ".minisig")
|
|
return static_cast<int>(i);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
std::string daemonVersionCore(const std::string& version)
|
|
{
|
|
// Extract a leading "v?MAJOR.MINOR.PATCH" run, ignoring any "-<commit>" / build suffix. Hand-
|
|
// rolled (no <regex>) to stay light and dependency-free.
|
|
const std::size_t start = version.find_first_of("0123456789");
|
|
if (start == std::string::npos) return version;
|
|
std::size_t i = start;
|
|
int dots = 0;
|
|
for (; i < version.size(); ++i) {
|
|
const char c = version[i];
|
|
if (c >= '0' && c <= '9') continue;
|
|
if (c == '.' && dots < 2) { ++dots; continue; }
|
|
break;
|
|
}
|
|
if (dots < 2) return version; // not a full N.N.N — leave as-is
|
|
const bool hasV = start > 0 && (version[start - 1] == 'v' || version[start - 1] == 'V');
|
|
return (hasV ? "v" : "") + version.substr(start, i - start);
|
|
}
|
|
|
|
} // namespace util
|
|
} // namespace dragonx
|