feat(mining): live pool fee, saved/custom pool rows, and payout-address fix
Several related mining-tab pool improvements:
- Report the default pool fee correctly: pool.dragonx.is is 1%, not 0%.
The registry constant was hardcoded to 0. It now also fetches the live
poolFee from the pool's /api/stats alongside hashrate (no extra
request), so the displayed fee self-corrects and falls back to the
compile-time value only when the fetch hasn't landed.
- Show fractional fees: new FormatFeePercent trims trailing zeros so
whole fees read "1%" and fractional ones keep their decimals ("1.5%").
- Surface saved + custom pools in the pool list card: the list is now
the union of the official pools, the user's saved favorites, and the
currently-mined pool (effectivePools), each a selectable, endpoint-
deduped row. Previously the card only showed the hardcoded knownPools().
- Fix the xmrig "user" field: the "Payout Address" field now drives the
pool login rewards are credited to (resolveMiningUserAddress), instead
of being written only to "pass" while "user" was auto-derived from the
wallet's own first z-address -- which silently ignored a configured
payout address and could route rewards to the wrong address.
Unit tests cover parsePoolFee, FormatFeePercent, effectivePools, and
resolveMiningUserAddress; full app + ObsidianDragonTests build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
if (!addr.address.empty()) { firstShielded = addr.address; break; }
|
||||
}
|
||||
for (const auto& addr : state_.addresses) {
|
||||
if (addr.type == "transparent" && !addr.address.empty()) {
|
||||
firstTransparent = 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;
|
||||
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