3 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 7d2972f315 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>
2026-10-18 09:03:24 -05:00
DanSandClaude Opus 4.8 8eee351faa fix(history): show shielding moves as neutral "Shielded", not red "Sent"
A coinbase/transparent→shielded move (often auto-shield) was drawn with a red
"Sent" label and send arrow yet a green "+X" amount — an internally
contradictory row that reads as "money leaving" to anyone who auto-shielded. It
now renders as a neutral "Shielded" row (shield icon + the shielded accent
colour for label and amount), and since a shield is your own funds going
private — not a spend — it's excluded from the Sent card total and the Sent
filter (it merges into one row visible under "All").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 08:57:38 -05:00
DanSandClaude Opus 4.8 09f39f9183 feat(i18n,errors): route node-action toasts through i18n + friendlyRpcError
Finish the error-surfacing foundation across app_network.cpp (+ one in
app_wizard.cpp): ~21 toasts that were hardcoded English — or raw daemon/RPC
error text concatenated after an English prefix — now go through TR() keys, and
every raw-error one maps the detail through util::friendlyRpcError() (warming
up / locked / method-not-found / insufficient / unreachable → plain guidance,
raw text as fallback). Covers mining start/stop, pool start, peer ban/unban/
clear, key export, rescan, wallet-switch guards, and the lite-unavailable
notices.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 08:55:29 -05:00
6 changed files with 133 additions and 55 deletions
+28 -27
View File
@@ -1584,28 +1584,28 @@ void App::updateWalletIndexForActiveWallet(bool markOpened)
void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed, bool salvage)
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("toast_lite_lifecycle_unavailable"));
return;
}
if (walletFile.empty() || !settings_) return;
if (settings_->getActiveWalletFile() == walletFile) {
ui::Notifications::instance().info("That wallet is already open.");
ui::Notifications::instance().info(TR("toast_wallet_already_open"));
return;
}
if (daemon_restarting_) {
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
ui::Notifications::instance().warning(TR("toast_node_busy_restarting"));
return;
}
// Don't race the other daemon-lifecycle operations. rescan/repair set state_.sync.rescanning; the
// seed-migration flow drives its own daemon stop/start; an encryption restart sets daemon_restarting_.
if (state_.sync.rescanning) {
ui::Notifications::instance().warning("A rescan or repair is in progress — try again once it finishes.");
ui::Notifications::instance().warning(TR("toast_rescan_in_progress"));
return;
}
// W3-4: block switching while a migration is PENDING, not only while its dialog is open — closing
// the dialog via "Later" mid-migration leaves the pending state but previously dropped this guard.
if (show_seed_migration_ || (settings_ && settings_->getSeedMigrationPending())) {
ui::Notifications::instance().warning("Finish or cancel the seed migration before switching wallets.");
ui::Notifications::instance().warning(TR("toast_finish_migration_first"));
return;
}
if (consolidation_.active()) {
@@ -1613,11 +1613,11 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed
return;
}
if (!isUsingEmbeddedDaemon()) {
ui::Notifications::instance().warning("Switching wallets needs the embedded daemon — stop any external dragonxd first.");
ui::Notifications::instance().warning(TR("toast_switch_needs_embedded"));
return;
}
if (hasTransactionSendProgress()) {
ui::Notifications::instance().warning("Finish or cancel the pending send before switching wallets.");
ui::Notifications::instance().warning(TR("toast_finish_send_first"));
return;
}
if (refuseIfNodeBusy()) return;
@@ -2234,7 +2234,7 @@ void App::refreshCoreData(bool allowBalance)
last_tx_block_height_ = -1;
invalidateShieldedHistoryScanProgress(true);
if (user_initiated_rescan_) {
ui::Notifications::instance().success("Blockchain rescan complete");
ui::Notifications::instance().success(TR("toast_rescan_complete"));
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
}
}
@@ -3517,7 +3517,7 @@ void App::startMining(int threads)
{
if (!supportsSoloMining()) {
(void)threads;
ui::Notifications::instance().warning("Solo mining is unavailable in lite build");
ui::Notifications::instance().warning(TR("toast_solo_unavailable_lite"));
return;
}
if (!state_.connected || !rpc_ || !worker_) return;
@@ -3545,7 +3545,7 @@ void App::startMining(int threads)
DEBUG_LOGF("Mining started with %d threads\n", threads);
} else {
DEBUG_LOGF("Failed to start mining: %s\n", errMsg.c_str());
ui::Notifications::instance().error("Mining failed: " + errMsg);
ui::Notifications::instance().error(std::string(TR("toast_mining_failed")) + util::friendlyRpcError(errMsg));
}
};
});
@@ -3577,7 +3577,7 @@ void App::stopMining()
} else {
// Don't silently leave generate=true as if it worked: tell the user and let the next
// getmininginfo refresh reconcile the true daemon state. (M-07)
ui::Notifications::instance().error("Failed to stop mining: " + errMsg);
ui::Notifications::instance().error(std::string(TR("toast_stop_mining_failed")) + util::friendlyRpcError(errMsg));
}
};
});
@@ -3587,7 +3587,7 @@ void App::startPoolMining(int threads)
{
if (!supportsPoolMining()) {
(void)threads;
ui::Notifications::instance().warning("Pool mining is unavailable in this build");
ui::Notifications::instance().warning(TR("toast_pool_unavailable"));
return;
}
// Clamp to [1, logical cores] before the count reaches xmrig (M-06/M-08 pool path).
@@ -3664,7 +3664,7 @@ void App::postPoolMinerJob(int threads, bool retargetOnly)
if (cfg.wallet_address.empty()) {
DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n");
ui::Notifications::instance().error("No wallet address available — generate a Z address in the Receive tab");
ui::Notifications::instance().error(TR("toast_no_address_generate_z"));
if (retargetOnly) stopPoolMining();
return;
}
@@ -3707,7 +3707,7 @@ void App::postPoolMinerJob(int threads, bool retargetOnly)
if (ok) {
// Miner spawned — it still needs a few seconds to connect to the pool and start hashing.
pool_starting_.store(true, std::memory_order_relaxed);
ui::Notifications::instance().info("Starting pool miner — connecting to the pool…");
ui::Notifications::instance().info(TR("toast_pool_connecting"));
} else {
DEBUG_LOGF("[ERROR] Pool mining: %s\n", err.c_str());
// Windows Defender blocking (error 225 = ERROR_VIRUS_INFECTED)
@@ -3718,7 +3718,7 @@ void App::postPoolMinerJob(int threads, bool retargetOnly)
pending_antivirus_dialog_ = true;
#endif
} else {
ui::Notifications::instance().error("Failed to start pool miner: " + err);
ui::Notifications::instance().error(std::string(TR("toast_pool_start_failed")) + util::friendlyRpcError(err));
}
}
};
@@ -3754,7 +3754,7 @@ void App::banPeer(const std::string& ip, int duration_seconds)
err = e.what();
}
return [this, err]() {
if (!err.empty()) ui::Notifications::instance().error("Ban failed: " + err);
if (!err.empty()) ui::Notifications::instance().error(std::string(TR("toast_ban_failed")) + util::friendlyRpcError(err));
else refreshPeerInfo();
};
});
@@ -3772,7 +3772,7 @@ void App::unbanPeer(const std::string& ip)
err = e.what();
}
return [this, err]() {
if (!err.empty()) ui::Notifications::instance().error("Unban failed: " + err);
if (!err.empty()) ui::Notifications::instance().error(std::string(TR("toast_unban_failed")) + util::friendlyRpcError(err));
else refreshPeerInfo();
};
});
@@ -3790,7 +3790,7 @@ void App::clearBans()
err = e.what();
}
return [this, err]() {
if (!err.empty()) { ui::Notifications::instance().error("Clear bans failed: " + err); return; }
if (!err.empty()) { ui::Notifications::instance().error(std::string(TR("toast_clear_bans_failed")) + util::friendlyRpcError(err)); return; }
state_.banned_peers.clear();
refreshPeerInfo();
};
@@ -4101,7 +4101,7 @@ void App::exportPrivateKey(const std::string& address, std::function<void(const
return [callback, key, err]() {
if (!err.empty()) {
DEBUG_LOGF("Export key error: %s\n", err.c_str());
ui::Notifications::instance().error("Key export failed: " + err);
ui::Notifications::instance().error(std::string(TR("toast_key_export_failed")) + util::friendlyRpcError(err));
if (callback) callback("");
} else if (callback) {
callback(key);
@@ -6657,7 +6657,7 @@ void App::beginAdoptSeedWallet()
void App::restoreOriginalWallet()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("toast_lite_lifecycle_unavailable"));
return;
}
if (daemon_restarting_) {
@@ -6832,7 +6832,7 @@ bool App::walletRebuildAvailable() const { return !findWalletRebuildHelper().emp
void App::rebuildWalletDatabase()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("toast_lite_lifecycle_unavailable"));
return;
}
if (daemon_restarting_) { ui::Notifications::instance().warning(TR("wallet_restore_busy")); return; }
@@ -7354,11 +7354,11 @@ void App::detectLowestAvailableBlockHeight(std::function<void(bool, int, bool)>
bool App::runtimeRescan(int startHeight)
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("toast_lite_lifecycle_unavailable"));
return false;
}
if (!state_.connected || !rpc_ || !rpc_->isConnected() || !worker_) {
ui::Notifications::instance().warning("Not connected to daemon");
ui::Notifications::instance().warning(TR("toast_not_connected_daemon"));
return false;
}
if (runtime_rescan_active_) return false;
@@ -7395,8 +7395,9 @@ bool App::runtimeRescan(int startHeight)
last_tx_block_height_ = -1;
invalidateShieldedHistoryScanProgress(true);
ui::Notifications::instance().info("Rescanning blockchain from block " + std::to_string(startHeight) +
" — this can take a while.");
char rescanMsg[192];
snprintf(rescanMsg, sizeof(rescanMsg), TR("toast_rescan_from_fmt"), static_cast<int>(startHeight));
ui::Notifications::instance().info(rescanMsg);
const bool started = runLongWalletTask([this, startHeight](rpc::RPCClient& c) -> rpc::RPCWorker::MainCb {
bool ok = false;
@@ -7417,11 +7418,11 @@ bool App::runtimeRescan(int startHeight)
noteWalletMutation();
last_tx_block_height_ = -1;
invalidateShieldedHistoryScanProgress(true);
ui::Notifications::instance().success("Blockchain rescan complete");
ui::Notifications::instance().success(TR("toast_rescan_complete"));
refreshData();
} else {
state_.sync.rescan_progress = 0.0f;
ui::Notifications::instance().error("Rescan failed: " + err);
ui::Notifications::instance().error(std::string(TR("toast_rescan_failed")) + util::friendlyRpcError(err));
}
};
});
+1 -1
View File
@@ -73,7 +73,7 @@ WizardUiState s_wizardUi;
void App::restartWizard()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Lite wallet lifecycle requests are available from Settings as dry-run readiness checks");
ui::Notifications::instance().warning(TR("toast_wizard_lite_unavailable"));
return;
}
+12 -4
View File
@@ -579,6 +579,9 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
std::string curVer = s_live_miner_ver;
if (curVer.empty()) curVer = app->poolMiningInstalledVersion();
if (curVer.empty()) curVer = app->settings()->getXmrigVersion();
// No miner installed at all → pool mining can't run. Make this a prominent
// "Download miner" call-to-action instead of a subtle grey "Update" pill.
const bool minerMissing = curVer.empty();
// Version state: subtle GREEN + "xmrig releases" when up to date, subtle ORANGE +
// "Update <latest>" when an update is available, neutral when either version is unknown.
auto verNum = [](std::string s) -> std::vector<int> {
@@ -609,7 +612,9 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
const ImU32 subtleOrange = IM_COL32(214, 158, 74, 255);
char xbtn[64];
if (upToDate)
if (minerMissing)
snprintf(xbtn, sizeof(xbtn), "%s", TR("xmrig_download_miner"));
else if (upToDate)
snprintf(xbtn, sizeof(xbtn), "%s", TR("xmrig_releases"));
else if (!s_xmrig_latest_tag.empty())
snprintf(xbtn, sizeof(xbtn), "%s %s", TR("xmrig_update_short"), s_xmrig_latest_tag.c_str());
@@ -629,18 +634,21 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
bool xhov = ImGui::IsItemHovered();
bool xclk = ImGui::IsItemClicked();
ImU32 xpill = minerBusy ? WithAlpha(OnSurface(), 12)
: (xhov ? StateHover() : WithAlpha(OnSurface(), 28));
: (minerMissing ? WithAlpha(Primary(), xhov ? 70 : 45)
: (xhov ? StateHover() : WithAlpha(OnSurface(), 28)));
dl->AddRectFilled(ImVec2(xbtnX, xbtnY), ImVec2(xbtnX + xbtnW, xbtnY + xbtnH),
xpill, xbtnH * 0.3f);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(xbtnX + xpadX, xbtnY + (xbtnH - xlblSz.y) * 0.5f),
minerBusy ? OnSurfaceDisabled()
: (upToDate ? subtleGreen : outdated ? subtleOrange : OnSurfaceMedium()),
: (minerMissing ? Primary()
: (upToDate ? subtleGreen : outdated ? subtleOrange : OnSurfaceMedium())),
xbtn);
if (xhov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", minerBusy ? TR("xmrig_stop_mining_first")
: TR("xmrig_update_title"));
: (minerMissing ? TR("xmrig_download_miner_tip")
: TR("xmrig_update_title")));
}
if (xclk && !minerBusy) XmrigDownloadDialog::show(app);
+48 -13
View File
@@ -120,7 +120,10 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
CancelMiningBenchmark(app, false); // don't leave a pool benchmark running after switching to solo (L-11)
app->stopPoolMining();
}
if (soloHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (soloHov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_solo_tip"));
}
}
ImGui::SetCursorScreenPos(poolMin);
@@ -136,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));
@@ -436,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();
}
}
}
+11 -10
View File
@@ -98,7 +98,7 @@ static void DrawTxIcon(ImDrawList* dl, const std::string& type,
} else if (type == "receive") {
icon = ICON_MD_CALL_RECEIVED;
} else if (type == "shield") {
icon = ICON_MD_CALL_MADE;
icon = ICON_MD_SHIELD; // a shield, not an outgoing arrow — it's an internal privacy move
} else {
icon = ICON_MD_CONSTRUCTION;
}
@@ -185,6 +185,9 @@ void RenderTransactionsTab(App* app)
ImU32 greenCol = Success();
ImU32 redCol = Error();
ImU32 goldCol = Warning();
// Neutral colour for a shielding move (your own funds going transparent→private): neither the
// red of a spend nor the green of income, so a shield never reads as "money leaving".
ImU32 shieldCol = schema::UI().resolveColor("var(--accent-shielded)", OnSurfaceMedium());
std::string txLoadingText = app->transactionRefreshProgressText();
bool txLoading = !txLoadingText.empty();
@@ -248,11 +251,9 @@ void RenderTransactionsTab(App* app)
if (send_i >= 0 && recv_i >= 0) {
isShieldLeg[send_i] = true;
isShieldLeg[recv_i] = true;
// The list shows the merged shield under the "Sent" filter, so count it there too —
// otherwise the Sent card reads 0 while the Sent list is non-empty. Use the shielded
// (receive-leg) amount, which is what the merged row displays.
sendCount++;
sendTotal += std::abs(state.transactions[recv_i].amount);
// A shield moves your OWN funds transparent→private — it isn't a spend, so it is NOT
// counted toward the Sent card/total (and no longer appears under the Sent filter).
// It merges into one neutral "Shielded" row, visible under "All".
}
}
@@ -543,7 +544,7 @@ void RenderTransactionsTab(App* app)
for (size_t i = 0; i < display_txns.size(); i++) {
const auto& dtx = display_txns[i];
if (type_filter != 0) {
if (type_filter == 1 && dtx.display_type != "send" && dtx.display_type != "shield") continue;
if (type_filter == 1 && dtx.display_type != "send") continue; // Sent = real spends; shields aren't sends
if (type_filter == 2 && dtx.display_type != "receive") continue;
if (type_filter == 3 && dtx.display_type != "generate" && dtx.display_type != "immature" && dtx.display_type != "mined") continue;
if (type_filter == 4 && !chatStore.hasChatTxid(dtx.txid)) continue; // chat-only
@@ -722,7 +723,7 @@ void RenderTransactionsTab(App* app)
ImU32 iconCol;
const char* typeStr;
if (shieldedDisplay) {
iconCol = redCol; typeStr = TR("sent_type");
iconCol = shieldCol; typeStr = TR("shielded_self_type");
} else if (tx.display_type == "receive") {
iconCol = greenCol; typeStr = TR("recv_type");
} else if (tx.display_type == "send") {
@@ -750,7 +751,7 @@ void RenderTransactionsTab(App* app)
float cy = rowPos.y + Layout::spacingMd();
// Icon
DrawTxIcon(dl, shieldedDisplay ? "send" : tx.display_type,
DrawTxIcon(dl, shieldedDisplay ? "shield" : tx.display_type,
cx + rowIconSz, cy + body2->LegacySize * 0.5f, rowIconSz, iconCol);
// Type label
@@ -769,7 +770,7 @@ void RenderTransactionsTab(App* app)
OnSurfaceMedium(), addr_display.c_str());
// Amount (right-aligned, first line)
ImU32 amtCol = (tx.amount >= 0) ? greenCol : redCol;
ImU32 amtCol = shieldedDisplay ? shieldCol : ((tx.amount >= 0) ? greenCol : redCol);
if (tx.amount >= 0)
snprintf(buf, sizeof(buf), "+%.8f", tx.amount);
else
+33
View File
@@ -1207,6 +1207,7 @@ void I18n::loadBuiltinEnglish()
strings_["tx_chat_badge"] = "Message";
strings_["recv_type"] = "Recv";
strings_["sent_type"] = "Sent";
strings_["shielded_self_type"] = "Shielded";
strings_["immature_type"] = "Immature";
strings_["mined_type"] = "Mined";
strings_["mature"] = "Mature";
@@ -1919,6 +1920,31 @@ void I18n::loadBuiltinEnglish()
strings_["err_insufficient"] = "Not enough spendable balance to cover the amount and fee.";
strings_["err_cant_reach"] = "Can't reach the node. Check that it's running and try again.";
strings_["err_generic"] = "Something went wrong. Please try again.";
// Node/wallet action toasts (were hardcoded English in app_network.cpp). The *_failed keys are
// prefixes: the code appends a friendlyRpcError() detail after them.
strings_["toast_lite_lifecycle_unavailable"] = "Full-node actions aren't available in the lite wallet.";
strings_["toast_wallet_already_open"] = "That wallet is already open.";
strings_["toast_node_busy_restarting"] = "The node is busy restarting — try again in a moment.";
strings_["toast_rescan_in_progress"] = "A rescan or repair is in progress — try again once it finishes.";
strings_["toast_finish_migration_first"] = "Finish or cancel the seed migration before switching wallets.";
strings_["toast_switch_needs_embedded"] = "Switching wallets needs the embedded node — stop any external dragonxd first.";
strings_["toast_finish_send_first"] = "Finish or cancel the pending send before switching wallets.";
strings_["toast_rescan_complete"] = "Blockchain rescan complete.";
strings_["toast_rescan_from_fmt"] = "Rescanning the blockchain from block %d — this can take a while.";
strings_["toast_solo_unavailable_lite"] = "Solo mining isn't available in the lite wallet.";
strings_["toast_pool_unavailable"] = "Pool mining isn't available in this build.";
strings_["toast_no_address_generate_z"] = "No wallet address yet — generate a shielded (Z) address in the Receive tab.";
strings_["toast_pool_connecting"] = "Starting pool miner — connecting to the pool…";
strings_["toast_not_connected_daemon"] = "Not connected to the node.";
strings_["toast_mining_failed"] = "Couldn't start mining: ";
strings_["toast_stop_mining_failed"] = "Couldn't stop mining: ";
strings_["toast_pool_start_failed"] = "Couldn't start the pool miner: ";
strings_["toast_ban_failed"] = "Couldn't ban the peer: ";
strings_["toast_unban_failed"] = "Couldn't unban the peer: ";
strings_["toast_clear_bans_failed"] = "Couldn't clear bans: ";
strings_["toast_key_export_failed"] = "Key export failed: ";
strings_["toast_rescan_failed"] = "Rescan failed: ";
strings_["toast_wizard_lite_unavailable"] = "Wallet setup actions aren't available in the lite wallet (see Settings for readiness checks).";
strings_["console_capturing_output"] = "Capturing daemon output...";
strings_["console_clear"] = "Clear";
strings_["console_clear_console"] = "Clear Console";
@@ -2263,6 +2289,9 @@ void I18n::loadBuiltinEnglish()
strings_["validate_mining_type"] = "Mining address (transparent)";
strings_["mining_est_daily_pool_sub"] = "rough solo-equivalent, before pool fee";
strings_["mining_generate_z_address_hint"] = "Generate a Z address in the Receive tab to use as your payout address";
strings_["mining_generate_z_address"] = "Generate a shielded payout address";
strings_["mining_generate_z_ok"] = "Created a shielded payout address.";
strings_["mining_generate_z_failed"] = "Couldn't create a shielded address — try the Receive tab.";
strings_["mining_pool_needs_payout_tooltip"] = "Enter a payout address first (generate a Z address)";
strings_["mining_pool"] = "Pool";
strings_["mining_payout_foreign"] = "⚠ This payout address isn't in your current wallet — mined rewards would go to a different wallet. Update it if you switched wallets.";
@@ -2302,6 +2331,8 @@ void I18n::loadBuiltinEnglish()
strings_["mining_starting_tooltip"] = "Miner is starting...";
strings_["mining_stop"] = "Stop";
strings_["mining_stop_solo_for_pool"] = "Stop solo mining before starting pool mining";
strings_["mining_solo_tip"] = "Solo: your node mines on its own. No pool fees, but a small CPU may wait a long time between rewards.";
strings_["mining_pool_tip"] = "Pool: team up with other miners for smaller, more frequent payouts. Needs the DRG-XMRig miner and a shielded payout address.";
strings_["mining_stop_solo_for_pool_settings"] = "Stop solo mining to change pool settings";
strings_["mining_stopping"] = "Stopping...";
strings_["mining_stopping_tooltip"] = "Miner is stopping...";
@@ -2313,6 +2344,8 @@ void I18n::loadBuiltinEnglish()
// --- Miner (xmrig) updater ---
strings_["xmrig_update_button"] = "Update miner…";
strings_["xmrig_download_miner"] = "Download miner";
strings_["xmrig_download_miner_tip"] = "Pool mining needs the DRG-XMRig miner. Click to download and install it.";
strings_["xmrig_update_short"] = "Update";
strings_["xmrig_releases"] = "xmrig releases";
strings_["xmrig_current"] = "Current:";