Compare commits
1 Commits
master
...
45b652f514
| Author | SHA1 | Date | |
|---|---|---|---|
| 45b652f514 |
@@ -1,61 +0,0 @@
|
||||
**DragonX (DRGX) Lite Desktop Wallet — v1.0.0**
|
||||
|
||||
ObsidianDragonLite is a lightweight companion to the ObsidianDragon full-node wallet. It skips the embedded full node entirely — **no multi-gigabyte blockchain download and near-instant startup** — by connecting to a DragonX lite-wallet (lightwalletd-style) server, while keeping the same native ImGui interface and shielded-first workflow.
|
||||
|
||||
This is the **first release** of the Lite variant.
|
||||
|
||||
---
|
||||
|
||||
## Why Lite?
|
||||
|
||||
- **No blockchain to download.** Sync in seconds instead of hours; a few MB of state instead of many GB.
|
||||
- **Same wallet, lighter footprint.** The familiar ObsidianDragon UI, shielded and transparent addresses, and address book — without running a node or miner.
|
||||
- **Portable.** A single self-contained binary that stores its data in its own `ObsidianDragonLite` folder, so it coexists cleanly alongside the full-node wallet.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Fast, node-free operation** — connect to a DragonX lite-wallet server and sync in seconds; minimal disk and RAM.
|
||||
- **Wallet lifecycle** — create a new wallet, restore from a seed phrase, or open an existing one; wallets auto-open on startup, with a first-run welcome prompt and **guided seed backup** on creation.
|
||||
- **Shielded + transparent** — send, shield, and receive DRGX; per-address balances computed from unspent notes and UTXOs.
|
||||
- **Keys & seed** — export your seed phrase and keys, and import keys, from Settings.
|
||||
- **Passphrase encryption** — encrypt, unlock, lock, and decrypt the wallet; you're prompted to unlock at send time and on startup when the wallet is locked.
|
||||
- **Encrypted messaging (HushChat)** — built-in **Contacts** and **Chat** with a seed-derived, **SilentDragonXLite-compatible** identity and a seed-encrypted local message store, so you can message other DragonX users end-to-end.
|
||||
- **Server management** — a built-in **server browser with automatic failover**; switch servers with live reconnect/recovery, and a **"Redownload blocks"** action to rescan from the server.
|
||||
- **Status & diagnostics** — a **Network tab** showing connection and sync status, plus an **interactive Console** for running backend commands and copyable error output.
|
||||
- **Multi-language UI** — full internationalization covering 8 languages: German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese.
|
||||
- **Cross-platform** — native builds for Linux (AppImage + zip), Windows (portable exe + zip), and macOS (DMG + .app), x86-64.
|
||||
- QR code generation for receiving addresses
|
||||
- Extensive theme and appearance options
|
||||
|
||||
---
|
||||
|
||||
## How it differs from the full-node wallet
|
||||
|
||||
- **No embedded `dragonxd`, no mining, no local blockchain explorer.** Chain data comes from the lite-wallet server rather than being verified locally by a full node — a lighter footprint in exchange for trusting the server for chain state.
|
||||
- Everything key-related stays **on your device**: seed, keys, and (when enabled) the encryption passphrase never leave the machine.
|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
| File | SHA-256 |
|
||||
|------|---------|
|
||||
| ObsidianDragonLite-1.0.0.AppImage | `a1459d6081124a6b8df47aa898b60c0237875e8cbb132e4fd8ae637673055ed4` |
|
||||
| ObsidianDragonLite-1.0.0-Linux-x64.zip | `573af502a6372334572e4dc88bd26e0cf36ed543b9df1e835fd8e4abd295108d` |
|
||||
| ObsidianDragonLite-1.0.0.exe | `28659efb770fa573bd1b1d0fcab1c1a3f9c7e46574185c3916cc9f669684852c` |
|
||||
| ObsidianDragonLite-1.0.0-Windows-x64.zip | `f6434a931e873e5936ebe249b044d6c7a413694767c667374c91ee438259b50c` |
|
||||
| ObsidianDragonLite-1.0.0-macOS-x86_64.dmg | `10f976f1453e6b1978cb7a85026fd033c85a10c1e66f436475aa628379a298c7` |
|
||||
| ObsidianDragonLite-1.0.0-macOS-x86_64.app.zip | `3e7259c363f7be2e9027c5282c94b742f4be280c998203752dc0a0551f6ab68b` |
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Linux:** x86-64, glibc 2.31+ (Ubuntu 20.04+, Fedora 33+, etc.)
|
||||
- **Windows:** x86-64, Windows 10 or later
|
||||
- **macOS:** x86-64 (Intel; runs on Apple Silicon via Rosetta 2), macOS 10.15 Catalina or later
|
||||
- Network access to a DragonX lite-wallet server.
|
||||
|
||||
## License
|
||||
|
||||
Released under the **GPLv3**. See [LICENSE](https://git.dragonx.is/DragonX/ObsidianDragon/src/branch/master/LICENSE) for details.
|
||||
@@ -1,86 +0,0 @@
|
||||
**DragonX (DRGX) Full-Node Desktop Wallet — v2.0.0**
|
||||
|
||||
This is ObsidianDragon, a native ImGui-based wallet for the DragonX network. It ships with an embedded full-node daemon (`dragonxd`) and an integrated CPU miner (DRG-XMRig), giving users a complete, self-contained experience on Linux, Windows, and macOS.
|
||||
|
||||
---
|
||||
|
||||
## What's New in v2.0.0
|
||||
|
||||
v2.0.0 is a major release. It adds **end-to-end encrypted HushChat messaging** and **BIP39 seed-phrase wallets** (with migrate-to-seed for legacy wallets), lets the wallet **update its own node and miner** with each install cryptographically verified, and ships a **security-hardening pass** across the updater, RPC, and secret-storage surfaces — plus full support for the **new multi-threaded `dragonxd`** and a large stability and UX overhaul.
|
||||
|
||||
### New Features
|
||||
|
||||
- **Encrypted messaging (HushChat)** — new **Contacts** and **Chat** tabs bring DRGX-native, end-to-end encrypted messaging. Your chat identity is derived from your wallet seed, messages are encrypted with libsodium (XChaCha20-Poly1305 / secretstream) and stored in a **seed-encrypted local database**, and the wire format is interoperable with SilentDragonX / SilentDragonXLite.
|
||||
- **BIP39 seed-phrase wallets & migrate-to-seed** — new wallets are backed by a mnemonic **seed phrase** you can back up from Settings. Existing **legacy wallets can be migrated into a seed wallet** — the wallet mints a new mnemonic wallet, sweeps your funds to it, and adopts it only once the sweep is mined, keeping a timestamped `wallet.dat` backup throughout. (The bundled `dragonxd` supports the required mnemonic RPCs; older nodes degrade gracefully.)
|
||||
- **In-app daemon updater** — Settings → **Node & Security → Daemon binary → "Check for updates…"** downloads the latest `dragonxd` from the project Gitea, verifies its **SHA-256 and a detached ed25519 signature** before installing, and replaces the binary **atomically while the node keeps running** (the new build takes effect on the next daemon start). A two-pane version picker lets you pin, downgrade, or install any published/pre-release build.
|
||||
- **In-app miner updater** — an **"Update miner…"** action in the Mining tab fetches, verifies (SHA-256 + ed25519), and installs the latest DRG-XMRig, with the same version picker and a display of current vs. latest.
|
||||
- **Daemon binary management** — the wallet no longer auto-overwrites a node you've dropped in; a dedicated Settings panel shows the managed binary and gathers all node actions (restart, rescan, repair) onto one toolbar.
|
||||
- **Repair Wallet** — a one-click `-zapwallettxes=2` recovery in Settings, plus automatic wallet reconciliation after a bootstrap and runtime rescan support for pruned nodes.
|
||||
- **Explorer search** — fuzzy, live (debounced) filtering of the block list by partial hash or height.
|
||||
- **History sorting & fast load** — sort transactions by date or amount, a "Loading older history (N%)" indicator during the initial bulk load, and persisted history that surfaces pending sends immediately.
|
||||
- **Smarter mining** — the thread benchmark now measures **sustained (thermally-throttled) hashrate** instead of an inflated initial burst, with GPU-aware idle mining and corrected idle thread scaling.
|
||||
|
||||
### Security & Integrity
|
||||
|
||||
- **Mandatory signature verification** — both the daemon and miner updaters **require** a valid ed25519 signature (checked against a key pinned in the wallet) in addition to the SHA-256; an install is refused if the signature is missing or invalid.
|
||||
- **Hardened secret handling** — RPC credentials are wiped from memory after use, RPC responses are size-capped and scrubbed of secrets in logs, generated node config is written **owner-only (0600)**, and secrets copied to the clipboard **auto-clear**.
|
||||
- **Safe on-disk writes** — settings, address book, and secret files are written **atomically with owner-only permissions**; the PIN vault fsyncs its secure-delete overwrite; SQLite cache growth is bounded.
|
||||
- **Recipient validation** — sends validate recipient address checksums (Base58Check + Bech32) before building a transaction.
|
||||
- **Path-traversal protection** — archive extraction (chain bootstrap and the updaters) rejects any entry that would escape the target directory (zip-slip), and the bootstrap fails closed on a missing checksum rather than trusting an unverified archive.
|
||||
- **Robustness** — malformed RPC error JSON is guarded and sends are single-flight, preventing duplicate/ill-formed submissions.
|
||||
|
||||
### Improvements
|
||||
|
||||
- **New multi-threaded daemon support** — live Sapling note-witness rebuild progress, accurate sync-speed display, reliable rescan-completion detection, and RPC polling throttled during sync so block download isn't slowed.
|
||||
- **Networking** — DragonX DNS seed nodes, `-maxconnections` passed to the daemon, and a peer count that stays current on every tab.
|
||||
- **Node resilience** — non-blocking warmup so you can connect while the daemon is still initializing, a live daemon-console tail on the startup overlay, fast-failing connect probes, and mid-session disconnect detection that keeps the UI responsive (in-flight RPC calls abort cleanly on disconnect/shutdown).
|
||||
- **Address list** — modernized with drag-to-transfer, labels, and view-only handling.
|
||||
- **UI / DPI** — overlay dialogs scale correctly with the font/DPI setting, improved CJK font rendering, native language names in the language picker, and format-incompatible translations are rejected.
|
||||
- **Settings** — an "Open data folder" button and confirmation modals for rescan and restart-daemon.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
Extensive fixes across history (shielded-tx ordering, stuck "refreshing history" banners, unconfirmed-badge stickiness), sends (correct fee passed to `z_sendmany`, fast-lane worker restart on reconnect, note-selection fee-gap workaround), rescan (accurate completion detection, no false "complete", no per-second error flood), RPC (mid-session disconnect handling, stale-refresh invalidation), and UI layout/i18n. See the commit history for the full list.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Full-node wallet** — send, receive, and verify DRGX transactions with a bundled `dragonxd` daemon; no external setup required.
|
||||
- **Self-updating** — verify-and-install the latest node and miner from within the app (SHA-256 + ed25519 signature enforced).
|
||||
- **Encrypted messaging** — built-in HushChat with a seed-derived identity, end-to-end encryption, and a seed-encrypted local message store, interoperable with SilentDragonX / SilentDragonXLite.
|
||||
- **Seed-phrase wallets** — BIP39 mnemonic backup, and migrate-to-seed to upgrade a legacy wallet into a seed wallet.
|
||||
- **Built-in CPU mining** — start/stop DRG-XMRig from the Mining tab with real-time hashrate and pool statistics, mine-when-idle support with configurable delay, and sustained-hashrate benchmarking.
|
||||
- **Multi-language UI** — full internationalization covering 8 languages: German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese.
|
||||
- **Cross-platform** — native builds for Linux (AppImage + zip), Windows (portable exe + zip), and macOS (**universal** DMG + .app); Linux/Windows x86-64, macOS Intel + Apple Silicon.
|
||||
- Shielded (z-address) and transparent (t-address) send/receive
|
||||
- QR code generation for receiving addresses
|
||||
- Transaction history with memo support, date/amount sorting, and pending-send tracking
|
||||
- Blockchain explorer with live block/transaction search
|
||||
- Blockchain rescan and Sapling witness-rebuild with status-bar progress
|
||||
- Repair Wallet and daemon-binary management in Settings
|
||||
- Built-in console
|
||||
- Extensive theme and appearance options
|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
| File | SHA-256 |
|
||||
|------|---------|
|
||||
| ObsidianDragon-2.0.0.AppImage | `34c9ea57ec27bf415e59d2890d995ed9069bfce04d979d7d8d58aa18f4570aa1` |
|
||||
| ObsidianDragon-2.0.0-Linux-x64.zip | `563004b4e45650dbfa9411d62fa1c59a3ca2199bd43a647b1614f2683bbdb5fa` |
|
||||
| ObsidianDragon-2.0.0.exe | `56100ae59dc3c67445d015cac5412ca40465bf459ba182c5ad2477a3b95ff548` |
|
||||
| ObsidianDragon-2.0.0-Windows-x64.zip | `ce9d067f36c5296288a473d3c2cceca7fcf807672a93a2084afc53c3b6e780d9` |
|
||||
| ObsidianDragon-2.0.0-macOS-universal.dmg | `8a01b6c0c2b5bebd64a222ba4a5ad04a1b8851138f28458b5c7f767fbb65db66` |
|
||||
| ObsidianDragon-2.0.0-macOS-universal.app.zip | `1bce1e5be15d9a2f3c6f42c95b23a0e89df142a73d65895252e4d8f50d19f541` |
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Linux:** x86-64, glibc 2.31+ (Ubuntu 20.04+, Fedora 33+, etc.)
|
||||
- **Windows:** x86-64, Windows 10 or later
|
||||
- **macOS:** Intel & Apple Silicon (universal binary), macOS 10.15 Catalina or later
|
||||
|
||||
## License
|
||||
|
||||
Released under the **GPLv3**. See [LICENSE](https://git.dragonx.is/DragonX/ObsidianDragon/src/branch/master/LICENSE) for details.
|
||||
@@ -35,6 +35,7 @@
|
||||
#include "rpc/connection.h"
|
||||
#include "chat/chat_identity.h" // deriveChatIdentityFromSecret for HushChat identity provisioning
|
||||
#include "ui/windows/chat_tab.h" // ui::ResetChatTab — wipe chat UI plaintext on a wallet switch
|
||||
#include "ui/windows/mining_pool_panel.h" // ui::resolveMiningUserAddress
|
||||
#include <sodium.h> // sodium_memzero for wiping the fetched mnemonic
|
||||
#include <cctype>
|
||||
#include "config/settings.h"
|
||||
@@ -2288,27 +2289,23 @@ void App::startPoolMining(int threads)
|
||||
cfg.tls = settings_->getPoolTls();
|
||||
cfg.hugepages = settings_->getPoolHugepages();
|
||||
|
||||
// Use first shielded address as the mining wallet address, fall back to transparent
|
||||
// xmrig "user" is the pool login the block rewards are credited to. The user's
|
||||
// "Payout Address" field (cfg.worker_name = getPoolWorker) is exactly that, so it
|
||||
// takes priority — otherwise a payout address that differs from the wallet's own
|
||||
// first z-address is silently ignored and rewards go to the wrong address. Only when
|
||||
// no payout address is set do we fall back to the wallet's own first shielded, then
|
||||
// transparent, address (available even before the daemon is connected/synced).
|
||||
std::string firstShielded, firstTransparent;
|
||||
for (const auto& addr : state_.z_addresses) {
|
||||
if (!addr.address.empty()) {
|
||||
cfg.wallet_address = addr.address;
|
||||
break;
|
||||
if (!addr.address.empty()) { firstShielded = addr.address; break; }
|
||||
}
|
||||
}
|
||||
if (cfg.wallet_address.empty()) {
|
||||
for (const auto& addr : state_.addresses) {
|
||||
if (addr.type == "transparent" && !addr.address.empty()) {
|
||||
cfg.wallet_address = addr.address;
|
||||
firstTransparent = addr.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use pool worker address from settings (available even before
|
||||
// the daemon is connected or the blockchain is synced).
|
||||
if (cfg.wallet_address.empty() && !cfg.worker_name.empty()) {
|
||||
cfg.wallet_address = cfg.worker_name;
|
||||
}
|
||||
cfg.wallet_address = ui::resolveMiningUserAddress(cfg.worker_name, firstShielded, firstTransparent);
|
||||
|
||||
if (cfg.wallet_address.empty()) {
|
||||
DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n");
|
||||
|
||||
@@ -20,6 +20,17 @@ std::string defaultPoolWorkerAddress(const std::vector<AddressInfo>& addresses)
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string resolveMiningUserAddress(const std::string& payoutAddress,
|
||||
const std::string& firstShieldedAddress,
|
||||
const std::string& firstTransparentAddress)
|
||||
{
|
||||
// The configured payout address is the pool login rewards go to, so it wins over
|
||||
// the wallet's own addresses. "x" is the placeholder for an unset field.
|
||||
if (!payoutAddress.empty() && payoutAddress != "x") return payoutAddress;
|
||||
if (!firstShieldedAddress.empty()) return firstShieldedAddress;
|
||||
return firstTransparentAddress; // may be empty -> caller reports "no address"
|
||||
}
|
||||
|
||||
bool miningValueAlreadySaved(const std::vector<std::string>& savedValues,
|
||||
const std::string& value)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,14 @@ namespace ui {
|
||||
|
||||
bool shouldDefaultPoolWorker(const std::string& currentWorker, bool alreadyDefaulted);
|
||||
std::string defaultPoolWorkerAddress(const std::vector<AddressInfo>& addresses);
|
||||
|
||||
// The xmrig "user" — the pool login block rewards are credited to. The user-entered
|
||||
// payout address wins; otherwise fall back to the wallet's own first shielded, then
|
||||
// transparent, address. "x" is the empty-field placeholder and counts as unset. The
|
||||
// result may be empty (no address anywhere), which the caller treats as an error.
|
||||
std::string resolveMiningUserAddress(const std::string& payoutAddress,
|
||||
const std::string& firstShieldedAddress,
|
||||
const std::string& firstTransparentAddress);
|
||||
bool miningValueAlreadySaved(const std::vector<std::string>& savedValues,
|
||||
const std::string& value);
|
||||
const char* defaultPoolUrl();
|
||||
|
||||
@@ -251,11 +251,15 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
|
||||
}
|
||||
y += gap * 0.5f;
|
||||
|
||||
// The pool list = official pools ∪ user-saved favorites ∪ the current custom pool.
|
||||
const auto effective = util::effectivePools(app->settings()->getPoolUrl(),
|
||||
app->settings()->getSavedPoolUrls());
|
||||
|
||||
// --- POOLS (N) header + Refresh ---
|
||||
{
|
||||
char hdr[48];
|
||||
snprintf(hdr, sizeof(hdr), "%s (%d)", TR("mining_pools_header"),
|
||||
(int)util::knownPools().size());
|
||||
(int)effective.size());
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(x, y), OnSurfaceMedium(), hdr);
|
||||
|
||||
float btnS = ovFont->LegacySize + 6 * dp;
|
||||
@@ -278,11 +282,11 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
|
||||
{
|
||||
ImDrawList* cdl = ImGui::GetWindowDrawList();
|
||||
const auto snap = app->poolStatsSnapshot();
|
||||
const util::KnownPool* current = util::findKnownPoolByUrl(app->settings()->getPoolUrl());
|
||||
const util::KnownPool* current = util::findPoolByUrl(effective, app->settings()->getPoolUrl());
|
||||
const float childW = ImGui::GetContentRegionAvail().x;
|
||||
const float listRowH = capFont->LegacySize + 10 * dp;
|
||||
|
||||
for (const auto& kp : util::knownPools()) {
|
||||
for (const auto& kp : effective) {
|
||||
ImGui::PushID(kp.id.c_str());
|
||||
const bool isCurrent = current && current->id == kp.id;
|
||||
const auto it = snap.byId.find(kp.id);
|
||||
@@ -315,7 +319,17 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
|
||||
|
||||
char right[64];
|
||||
std::string hrStr = haveHr ? FormatHashrate(it->second.hashrateHs) : std::string("—");
|
||||
snprintf(right, sizeof(right), "%s %.0f%% fee", hrStr.c_str(), kp.feePercent);
|
||||
// Prefer the live fee the pool reports; fall back to the compile-time
|
||||
// KnownPool.feePercent. A synthetic user pool has an unknown (<0) fee, so
|
||||
// we show just its hashrate placeholder for it.
|
||||
double feePct = (it != snap.byId.end() && it->second.feePercent >= 0.0)
|
||||
? it->second.feePercent
|
||||
: kp.feePercent;
|
||||
if (feePct >= 0.0)
|
||||
snprintf(right, sizeof(right), "%s %s%% fee", hrStr.c_str(),
|
||||
FormatFeePercent(feePct).c_str());
|
||||
else
|
||||
snprintf(right, sizeof(right), "%s", hrStr.c_str());
|
||||
ImVec2 rSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, right);
|
||||
cdl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rMax.x - rSz.x - 6 * dp, textY), OnSurfaceMedium(), right);
|
||||
|
||||
@@ -41,6 +41,21 @@ std::string FormatHashrate(double hashrate)
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
std::string FormatFeePercent(double feePercent)
|
||||
{
|
||||
// Whole fees read "1"; fractional ones keep only their significant decimals
|
||||
// ("1.5", "0.9", "1.25") with no trailing zeros. Capped at 2 dp — finer than
|
||||
// any pool advertises, and the caller appends the "%".
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%.2f", feePercent);
|
||||
std::string s(buffer);
|
||||
if (s.find('.') != std::string::npos) {
|
||||
s.erase(s.find_last_not_of('0') + 1);
|
||||
if (!s.empty() && s.back() == '.') s.pop_back();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
double EstimateHoursToBlock(double localHashrate, double networkHashrate, double difficulty)
|
||||
{
|
||||
(void)difficulty;
|
||||
|
||||
@@ -9,6 +9,7 @@ int GetMaxMiningThreads();
|
||||
int ClampMiningThreads(int requestedThreads, int maxThreads);
|
||||
bool IsPoolMiningActive(bool poolMode, bool xmrigRunning, bool soloMiningRunning);
|
||||
std::string FormatHashrate(double hashrate);
|
||||
std::string FormatFeePercent(double feePercent);
|
||||
double EstimateHoursToBlock(double localHashrate, double networkHashrate, double difficulty);
|
||||
std::string FormatEstTime(double estimatedHours);
|
||||
|
||||
|
||||
@@ -45,16 +45,30 @@ struct PoolHashrate {
|
||||
std::string id;
|
||||
double hashrateHs = 0.0;
|
||||
bool ok = false;
|
||||
// Live pool fee (%) read from the same stats JSON. <0 means "not available" —
|
||||
// callers fall back to the compile-time KnownPool.feePercent.
|
||||
double feePercent = -1.0;
|
||||
};
|
||||
|
||||
// The built-in official pools (PPLNS only — never a SOLO pool, whose hashrate is
|
||||
// meaningless to balance against). Stable order.
|
||||
const std::vector<KnownPool>& knownPools();
|
||||
|
||||
// The known pool whose stratum matches `url` (host, and port when both specify one),
|
||||
// or nullptr. `url` may be a bare host, host:port, or carry a scheme/userinfo/path.
|
||||
// The pool in `pools` whose stratum matches `url` (host, and port when both specify
|
||||
// one), or nullptr. `url` may be a bare host, host:port, or carry a scheme/path.
|
||||
const KnownPool* findPoolByUrl(const std::vector<KnownPool>& pools, const std::string& url);
|
||||
|
||||
// Same, over the built-in official pools only.
|
||||
const KnownPool* findKnownPoolByUrl(const std::string& url);
|
||||
|
||||
// The full list the UI should show: the official knownPools(), plus a row for every
|
||||
// user-saved pool URL and for `currentPoolUrl` when it isn't one of those — so a
|
||||
// custom/bookmarked pool is a first-class, selectable row. Synthetic (user) rows are
|
||||
// official=false and carry no statsUrl (feePercent<0, no live hashrate), and endpoints
|
||||
// are de-duplicated so a saved URL that equals an official pool isn't listed twice.
|
||||
std::vector<KnownPool> effectivePools(const std::string& currentPoolUrl,
|
||||
const std::vector<std::string>& savedPoolUrls);
|
||||
|
||||
// The algo xmrig must use for `url`: the matching known pool's algo, else `fallback`.
|
||||
std::string resolvePoolAlgo(const std::string& url, const std::string& fallback);
|
||||
|
||||
@@ -64,6 +78,13 @@ std::string resolvePoolAlgo(const std::string& url, const std::string& fallback)
|
||||
double parsePoolHashrate(PoolStatsSchema schema, const std::string& json,
|
||||
const std::string& miningcorePoolId, bool& ok);
|
||||
|
||||
// Parse a pool's advertised fee (%) out of the same stats JSON (DragonXIs:
|
||||
// pools.<name>.poolFee; Miningcore: pools[id].poolFeePercent). Selects the same
|
||||
// pool entry as parsePoolHashrate. Sets ok=false and returns 0 when the field is
|
||||
// absent / malformed, so the caller keeps the compile-time fallback.
|
||||
double parsePoolFee(PoolStatsSchema schema, const std::string& json,
|
||||
const std::string& miningcorePoolId, bool& ok);
|
||||
|
||||
// Weighted-random pick among the usable (ok==true) pools: probability is inversely
|
||||
// proportional to hashrate (smaller pools favored), so miners spread out instead of
|
||||
// all stampeding to the single lowest pool. The current pool (`currentId`, may be
|
||||
|
||||
@@ -28,7 +28,7 @@ const std::vector<KnownPool>& knownPools()
|
||||
KnownPool{
|
||||
"dragonx-is", "pool.dragonx.is", "pool.dragonx.is:3433", "rx/hush",
|
||||
"https://pool.dragonx.is/api/stats", PoolStatsSchema::DragonXIs,
|
||||
/*miningcorePoolId=*/"", /*feePercent=*/0.0, /*official=*/true,
|
||||
/*miningcorePoolId=*/"", /*feePercent=*/1.0, /*official=*/true,
|
||||
},
|
||||
};
|
||||
return pools;
|
||||
@@ -83,13 +83,62 @@ bool sameEndpoint(const std::string& a, const std::string& b)
|
||||
return pa == pb;
|
||||
}
|
||||
|
||||
// Build a synthetic, selectable pool row for a user-supplied URL (a saved favorite
|
||||
// or the current custom pool). We don't know its stats API, so it carries no
|
||||
// statsUrl / live hashrate and an unknown (<0) fee — the UI falls back to "—".
|
||||
KnownPool makeUserPool(const std::string& url)
|
||||
{
|
||||
KnownPool p;
|
||||
const std::string hp = hostPortOf(url);
|
||||
std::string host, port;
|
||||
splitHostPort(hp, host, port);
|
||||
p.id = "user:" + trimmed(url); // stable + unique (used as the ImGui id)
|
||||
p.label = host.empty() ? hp : host;
|
||||
p.stratum = trimmed(url); // what the miner connects to / a row-click restores
|
||||
p.algo = ""; // unknown; xmrig resolves via resolvePoolAlgo's fallback
|
||||
p.statsUrl = ""; // no known stats endpoint -> no live hashrate/fee
|
||||
p.schema = PoolStatsSchema::DragonXIs;
|
||||
p.miningcorePoolId = "";
|
||||
p.feePercent = -1.0; // unknown fee
|
||||
p.official = false;
|
||||
return p;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const KnownPool* findPoolByUrl(const std::vector<KnownPool>& pools, const std::string& url)
|
||||
{
|
||||
for (const auto& p : pools)
|
||||
if (sameEndpoint(p.stratum, url)) return &p;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const KnownPool* findKnownPoolByUrl(const std::string& url)
|
||||
{
|
||||
for (const auto& p : knownPools())
|
||||
if (sameEndpoint(p.stratum, url)) return &p;
|
||||
return nullptr;
|
||||
return findPoolByUrl(knownPools(), url);
|
||||
}
|
||||
|
||||
std::vector<KnownPool> effectivePools(const std::string& currentPoolUrl,
|
||||
const std::vector<std::string>& savedPoolUrls)
|
||||
{
|
||||
std::vector<KnownPool> pools = knownPools();
|
||||
|
||||
// Skip anything whose endpoint already appears (official or an earlier user row).
|
||||
auto listed = [&](const std::string& url) {
|
||||
return findPoolByUrl(pools, url) != nullptr;
|
||||
};
|
||||
|
||||
for (const auto& url : savedPoolUrls) {
|
||||
if (trimmed(url).empty() || listed(url)) continue;
|
||||
pools.push_back(makeUserPool(url));
|
||||
}
|
||||
|
||||
// The pool currently being mined, if not already shown, so the active pool is
|
||||
// always visible even before it's bookmarked.
|
||||
if (!trimmed(currentPoolUrl).empty() && !listed(currentPoolUrl))
|
||||
pools.push_back(makeUserPool(currentPoolUrl));
|
||||
|
||||
return pools;
|
||||
}
|
||||
|
||||
std::string resolvePoolAlgo(const std::string& url, const std::string& fallback)
|
||||
@@ -159,6 +208,64 @@ double parsePoolHashrate(PoolStatsSchema schema, const std::string& jsonStr,
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double parsePoolFee(PoolStatsSchema schema, const std::string& jsonStr,
|
||||
const std::string& miningcorePoolId, bool& ok)
|
||||
{
|
||||
ok = false;
|
||||
try {
|
||||
const json j = json::parse(jsonStr);
|
||||
|
||||
if (schema == PoolStatsSchema::DragonXIs) {
|
||||
// { "pools": { "dragonx": { "poolFee": <num>, ... }, ... } }
|
||||
if (j.contains("pools") && j["pools"].is_object()) {
|
||||
const auto& pools = j["pools"];
|
||||
auto readFee = [&](const json& pool, double& out) -> bool {
|
||||
if (pool.is_object() && pool.contains("poolFee") &&
|
||||
pool["poolFee"].is_number()) {
|
||||
out = pool["poolFee"].get<double>();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
double fee = 0.0;
|
||||
if (pools.contains("dragonx") && readFee(pools["dragonx"], fee)) {
|
||||
ok = true;
|
||||
return fee;
|
||||
}
|
||||
for (auto it = pools.begin(); it != pools.end(); ++it) {
|
||||
if (readFee(it.value(), fee)) {
|
||||
ok = true;
|
||||
return fee;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // Miningcore: pools[id].poolFeePercent
|
||||
if (j.contains("pools") && j["pools"].is_array()) {
|
||||
const json* chosen = nullptr;
|
||||
for (const auto& pool : j["pools"]) {
|
||||
if (!pool.is_object()) continue;
|
||||
if (!miningcorePoolId.empty()) {
|
||||
if (pool.value("id", std::string{}) == miningcorePoolId) {
|
||||
chosen = &pool;
|
||||
break;
|
||||
}
|
||||
} else if (!chosen) {
|
||||
chosen = &pool; // first pool when no id requested
|
||||
}
|
||||
}
|
||||
if (chosen && chosen->contains("poolFeePercent") &&
|
||||
(*chosen)["poolFeePercent"].is_number()) {
|
||||
ok = true;
|
||||
return (*chosen)["poolFeePercent"].get<double>();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
// fall through — ok stays false
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
std::string chooseWeightedPool(const std::vector<PoolHashrate>& pools,
|
||||
const std::string& currentId,
|
||||
std::mt19937& rng)
|
||||
|
||||
@@ -94,6 +94,12 @@ void PoolStatsService::run(std::vector<KnownPool> pools)
|
||||
const double v = parsePoolHashrate(p.schema, body, p.miningcorePoolId, ok);
|
||||
hr.ok = ok;
|
||||
hr.hashrateHs = ok ? v : 0.0;
|
||||
|
||||
bool feeOk = false;
|
||||
const double fee = parsePoolFee(p.schema, body, p.miningcorePoolId, feeOk);
|
||||
// Only trust a sane fee; anything else leaves feePercent < 0 so the UI
|
||||
// falls back to the compile-time KnownPool.feePercent.
|
||||
if (feeOk && fee >= 0.0 && fee <= 100.0) hr.feePercent = fee;
|
||||
}
|
||||
results[p.id] = hr;
|
||||
}
|
||||
|
||||
@@ -3231,6 +3231,19 @@ void testRendererHelpers()
|
||||
EXPECT_EQ(dragonx::ui::defaultPoolWorkerAddress(poolAddresses), std::string("zs-default-worker"));
|
||||
EXPECT_TRUE(dragonx::ui::miningValueAlreadySaved({"pool-a", "pool-b"}, "pool-b"));
|
||||
EXPECT_FALSE(dragonx::ui::miningValueAlreadySaved({"pool-a"}, ""));
|
||||
|
||||
// resolveMiningUserAddress: the configured payout address is the xmrig "user"
|
||||
// (where rewards go) and must win over the wallet's own addresses.
|
||||
EXPECT_EQ(dragonx::ui::resolveMiningUserAddress("zs-payout", "zs-own", "R-own"),
|
||||
std::string("zs-payout")); // explicit payout wins
|
||||
EXPECT_EQ(dragonx::ui::resolveMiningUserAddress("", "zs-own", "R-own"),
|
||||
std::string("zs-own")); // unset -> own shielded
|
||||
EXPECT_EQ(dragonx::ui::resolveMiningUserAddress("x", "zs-own", "R-own"),
|
||||
std::string("zs-own")); // "x" placeholder counts as unset
|
||||
EXPECT_EQ(dragonx::ui::resolveMiningUserAddress("x", "", "R-own"),
|
||||
std::string("R-own")); // no shielded -> transparent
|
||||
EXPECT_EQ(dragonx::ui::resolveMiningUserAddress("", "", ""),
|
||||
std::string("")); // nothing anywhere -> caller errors
|
||||
EXPECT_EQ(std::string(dragonx::ui::defaultPoolUrl()), std::string("pool.dragonx.is:3433"));
|
||||
|
||||
dragonx::TransactionInfo tx;
|
||||
@@ -5828,6 +5841,94 @@ void testPoolHashrateParsing()
|
||||
EXPECT_FALSE(ok);
|
||||
}
|
||||
|
||||
// Schema-aware pool fee parsing (fed to the mining-tab "N% fee" display).
|
||||
void testPoolFeeParsing()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
bool ok = false;
|
||||
|
||||
// pool.dragonx.is custom schema: pools.dragonx.poolFee (a whole-percent number).
|
||||
const std::string isJson =
|
||||
R"({"pools":{"dragonx":{"hashrate":27670.14,"poolFee":1,"soloFee":3}}})";
|
||||
double fee = parsePoolFee(PoolStatsSchema::DragonXIs, isJson, "", ok);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_NEAR(fee, 1.0, 0.001);
|
||||
|
||||
// Fractional fees survive (display rounds, but the parse must not).
|
||||
const std::string isFrac = R"({"pools":{"dragonx":{"poolFee":1.5}}})";
|
||||
fee = parsePoolFee(PoolStatsSchema::DragonXIs, isFrac, "", ok);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_NEAR(fee, 1.5, 0.001);
|
||||
|
||||
// Miningcore schema: the requested pool id's poolFeePercent.
|
||||
const std::string ccJson =
|
||||
R"({"pools":[)"
|
||||
R"({"id":"dragonx-solo","poolFeePercent":2.0,"poolStats":{"poolHashrate":88780.0}},)"
|
||||
R"({"id":"dragonx-pplns","poolFeePercent":0.9,"poolStats":{"poolHashrate":1585.9}}]})";
|
||||
fee = parsePoolFee(PoolStatsSchema::Miningcore, ccJson, "dragonx-pplns", ok);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_NEAR(fee, 0.9, 0.001);
|
||||
|
||||
// Missing field / malformed / wrong-schema input all fail closed (caller keeps
|
||||
// the compile-time fallback rather than showing a bogus 0%).
|
||||
parsePoolFee(PoolStatsSchema::DragonXIs, R"({"pools":{"dragonx":{"hashrate":1.0}}})", "", ok);
|
||||
EXPECT_FALSE(ok); // no poolFee key
|
||||
parsePoolFee(PoolStatsSchema::DragonXIs, "not json", "", ok);
|
||||
EXPECT_FALSE(ok);
|
||||
parsePoolFee(PoolStatsSchema::Miningcore, ccJson, "does-not-exist", ok);
|
||||
EXPECT_FALSE(ok);
|
||||
parsePoolFee(PoolStatsSchema::DragonXIs, R"({"pools":{"dragonx":{"poolFee":"1"}}})", "", ok);
|
||||
EXPECT_FALSE(ok); // string, not number
|
||||
}
|
||||
|
||||
// The effective pool list = official pools ∪ saved favorites ∪ current custom pool,
|
||||
// endpoint-deduped, with synthetic user rows flagged official=false.
|
||||
void testEffectivePools()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
const int base = (int)knownPools().size();
|
||||
|
||||
// Current pool is the official one, nothing saved -> just the official pools.
|
||||
auto a = effectivePools("pool.dragonx.is:3433", {});
|
||||
EXPECT_EQ((int)a.size(), base);
|
||||
|
||||
// A custom current pool (neither official nor saved) appears as an extra row.
|
||||
auto b = effectivePools("my.pool.example:3333", {});
|
||||
EXPECT_EQ((int)b.size(), base + 1);
|
||||
const KnownPool* custom = findPoolByUrl(b, "my.pool.example:3333");
|
||||
EXPECT_TRUE(custom != nullptr);
|
||||
EXPECT_FALSE(custom->official);
|
||||
EXPECT_TRUE(custom->feePercent < 0.0); // unknown fee
|
||||
|
||||
// Saved pools are appended; an official one among them and a duplicate collapse.
|
||||
auto c = effectivePools("pool.dragonx.is:3433",
|
||||
{"pool.dragonx.is:3433", "alt.pool:1", "alt.pool:1"});
|
||||
EXPECT_EQ((int)c.size(), base + 1);
|
||||
EXPECT_TRUE(findPoolByUrl(c, "alt.pool:1") != nullptr);
|
||||
|
||||
// Current pool equal to a saved one is not listed twice.
|
||||
auto d = effectivePools("alt.pool:1", {"alt.pool:1"});
|
||||
EXPECT_EQ((int)d.size(), base + 1);
|
||||
|
||||
// Blank/whitespace URLs are ignored (no phantom rows).
|
||||
auto e = effectivePools(" ", {"", " "});
|
||||
EXPECT_EQ((int)e.size(), base);
|
||||
}
|
||||
|
||||
// Fee formatting: whole numbers stay clean, fractional fees keep their decimals.
|
||||
void testFormatFeePercent()
|
||||
{
|
||||
using dragonx::ui::FormatFeePercent;
|
||||
EXPECT_TRUE(FormatFeePercent(1.0) == "1");
|
||||
EXPECT_TRUE(FormatFeePercent(0.0) == "0");
|
||||
EXPECT_TRUE(FormatFeePercent(3.0) == "3");
|
||||
EXPECT_TRUE(FormatFeePercent(1.5) == "1.5");
|
||||
EXPECT_TRUE(FormatFeePercent(0.9) == "0.9");
|
||||
EXPECT_TRUE(FormatFeePercent(1.25) == "1.25");
|
||||
EXPECT_TRUE(FormatFeePercent(2.50) == "2.5"); // trailing zero trimmed
|
||||
EXPECT_TRUE(FormatFeePercent(100.0) == "100");
|
||||
}
|
||||
|
||||
// Weighted-random pool selection: smaller pools favored, incumbent sticky, fails safe.
|
||||
void testPoolWeightedSelection()
|
||||
{
|
||||
@@ -6590,6 +6691,9 @@ int main()
|
||||
testLiteOfficialServerDetection();
|
||||
testPoolRegistryLookup();
|
||||
testPoolHashrateParsing();
|
||||
testPoolFeeParsing();
|
||||
testEffectivePools();
|
||||
testFormatFeePercent();
|
||||
testPoolWeightedSelection();
|
||||
testAtomicFileWrite();
|
||||
testHushChatCrypto();
|
||||
|
||||
Reference in New Issue
Block a user