feat(mining): explain solo/pool, add a payout-address create path, surface the miner download

New-user mining cluster from the UX audit:
- Solo/Pool toggle: hover tooltips on each half explaining the trade-off
  (solo = no fees but long waits on a small CPU; pool = frequent small payouts,
  needs the miner + a shielded payout address).
- Payout field: when it's empty (new wallets have no z-address, and a pool
  payout must be shielded) the bookmark button is replaced by a one-click
  "Generate a shielded payout address" button (createNewZAddress → fills the
  field, persists, toasts), instead of only a hover hint to the Receive tab.
- Miner updater pill: when no miner is installed it becomes a prominent accent
  "Download miner" call-to-action with a "pool mining needs DRG-XMRig" tooltip,
  instead of a subtle grey "Update" pill that's easy to miss.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 09:03:24 -05:00
co-authored by Claude Opus 4.8
parent 8eee351faa
commit 7d2972f315
3 changed files with 63 additions and 16 deletions
+44 -12
View File
@@ -139,6 +139,8 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
if (soloMiningAvailable && poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (poolHov && soloMiningActive && !s_pool_mode) {
material::Tooltip("%s", TR("mining_stop_solo_for_pool"));
} else if (poolHov && !soloMiningActive) {
material::Tooltip("%s", TR("mining_pool_tip"));
}
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
@@ -439,21 +441,51 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
}
}
// --- Worker: Bookmark button ---
// --- Worker: Generate (when empty) / Bookmark button ---
// A new wallet has no z-address, and a pool payout MUST be shielded. When the field is empty
// show a one-click "generate a shielded address" button in place of the (useless-when-empty)
// bookmark, instead of only a hover hint pointing at the Receive tab.
ImGui::SameLine(0, 0);
{
std::string currentWorker(s_pool_worker);
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolWorkers(), currentWorker);
material::IconButtonStyle st;
st.color = alreadySaved ? Primary() : OnSurfaceMedium();
st.hoverBg = StateHover();
st.bgRounding = 4.0f * dp;
st.tooltip = alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address");
if (material::IconButton("##SaveWorkerAddr", alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER,
Type().iconSmall(), ImVec2(iconBtnW, inputFrameH2), st)
&& !currentWorker.empty() && currentWorker != "x" && !alreadySaved) {
app->settings()->addSavedPoolWorker(currentWorker);
app->settings()->save();
const bool noAddr = currentWorker.empty() || currentWorker == "x";
if (noAddr) {
material::IconButtonStyle st;
st.color = Primary();
st.hoverBg = StateHover();
st.bgRounding = 4.0f * dp;
st.tooltip = TR("mining_generate_z_address");
if (material::IconButton("##GenZAddr", ICON_MD_ADD, Type().iconSmall(),
ImVec2(iconBtnW, inputFrameH2), st)) {
if (!app->isLiteBuild() && !app->isConnected()) {
ui::Notifications::instance().warning(TR("toast_not_connected_daemon"));
} else {
char* wp = &s_pool_worker[0]; const size_t wn = sizeof(s_pool_worker);
app->createNewZAddress([app, wp, wn](const std::string& addr) {
if (addr.empty()) {
ui::Notifications::instance().error(TR("mining_generate_z_failed"));
return;
}
snprintf(wp, wn, "%s", addr.c_str()); // wp → the static s_pool_worker storage
app->settings()->setPoolWorker(addr);
app->settings()->save();
ui::Notifications::instance().success(TR("mining_generate_z_ok"));
});
}
}
} else {
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolWorkers(), currentWorker);
material::IconButtonStyle st;
st.color = alreadySaved ? Primary() : OnSurfaceMedium();
st.hoverBg = StateHover();
st.bgRounding = 4.0f * dp;
st.tooltip = alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address");
if (material::IconButton("##SaveWorkerAddr", alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER,
Type().iconSmall(), ImVec2(iconBtnW, inputFrameH2), st)
&& !alreadySaved) {
app->settings()->addSavedPoolWorker(currentWorker);
app->settings()->save();
}
}
}