diff --git a/src/ui/windows/shield_dialog.cpp b/src/ui/windows/shield_dialog.cpp index fb4071a..1f88b6c 100644 --- a/src/ui/windows/shield_dialog.cpp +++ b/src/ui/windows/shield_dialog.cpp @@ -204,6 +204,25 @@ static double srcAmount() { return s_src == 0 ? s_t_amount : s_src == 1 ? s_z_am static std::string fmtAmt(double v) { char b[48]; std::snprintf(b, sizeof(b), "%.4f", v); return b; } +// Shield-coinbase source helpers. "*" = all spendable mining (transparent) addresses; else one address. +// Balances are DISPLAY totals (may include immature/non-coinbase), so the UI wording says "up to". +static double miningBalance(const WalletState& st, const char* from) { + if (std::strcmp(from, "*") == 0) { + double sum = 0.0; + for (const auto& a : st.t_addresses) if (a.isSpendable()) sum += a.balance; + return sum; + } + const int i = addressIndexByString(st.t_addresses, from); + return i >= 0 ? st.t_addresses[i].balance : 0.0; +} +static int miningAddrCount(const WalletState& st, const char* from) { + if (std::strcmp(from, "*") == 0) { + int n = 0; for (const auto& a : st.t_addresses) if (a.isSpendable() && a.balance > 0.0) ++n; return n; + } + const int i = addressIndexByString(st.t_addresses, from); + return (i >= 0 && st.t_addresses[i].balance > 0.0) ? 1 : 0; +} + // A caption-style line that wraps at the dialog edge (Type().textColored doesn't wrap). static void wrappedCaption(ImU32 color, const char* text) { @@ -567,8 +586,32 @@ void ShieldDialog::render(App* app) // ── Shield coinbase: from address ─────────────────────────────────────────────────────────── if (!isMerge && !opInFlight) { - material::LabeledInput(TR("shield_from_address"), "##FromAddr", s_from_address, sizeof(s_from_address)); - ImGui::TextDisabled("%s", TR("shield_wildcard_hint")); + // Pick the mining (transparent) address to shield from a dropdown with balances, instead of a + // free-text box with a "*" wildcard. "*" shields all of them. + ImGui::TextUnformatted(TR("shield_from_address")); + const bool allFrom = std::strcmp(s_from_address, "*") == 0; + const std::string fromLbl = allFrom ? std::string(TR("shield_all_mining_addrs")) + : (app->getAddressLabel(s_from_address).empty() ? shortAddr(s_from_address) + : app->getAddressLabel(s_from_address)); + ImGui::SetNextItemWidth(-1); + if (ImGui::BeginCombo("##FromAddr", fromLbl.c_str())) { + if (ImGui::Selectable(TR("shield_all_mining_addrs"), allFrom)) + strncpy(s_from_address, "*", sizeof(s_from_address)); + for (const auto& a : state.t_addresses) { + if (!a.isSpendable()) continue; + const std::string lbl = app->getAddressLabel(a.address); + char item[192]; + std::snprintf(item, sizeof(item), "%s (%s DRGX)", + lbl.empty() ? shortAddr(a.address).c_str() : lbl.c_str(), + fmtAmt(a.balance).c_str()); + const bool sel = std::strcmp(s_from_address, a.address.c_str()) == 0; + if (ImGui::Selectable(item, sel)) { + strncpy(s_from_address, a.address.c_str(), sizeof(s_from_address) - 1); + s_from_address[sizeof(s_from_address) - 1] = '\0'; + } + } + ImGui::EndCombo(); + } ImGui::Spacing(); } @@ -833,9 +876,24 @@ void ShieldDialog::render(App* app) fmtAmt(srcAmount()).c_str(), srcCount(), shortAddr(s_to_address).c_str()); ImGui::TextWrapped("%s", cb); ImGui::Spacing(); + } else if (s_confirm && !isMerge) { + // Shield-coinbase preview: amount / address count / fee / destination (no amount/count was shown before). + const bool allFrom = std::strcmp(s_from_address, "*") == 0; + const std::string toLbl = app->getAddressLabel(s_to_address); + char cb[360]; + std::snprintf(cb, sizeof(cb), TR("shield_confirm_fmt"), + fmtAmt(miningBalance(state, s_from_address)).c_str(), + allFrom ? miningAddrCount(state, s_from_address) : 1, + s_utxo_limit, + toLbl.empty() ? shortAddr(s_to_address).c_str() : toLbl.c_str(), + fmtAmt(s_fee).c_str()); + ImGui::TextWrapped("%s", cb); + ImGui::Spacing(); } bool can_submit = haveDest && connected && !syncing; + // Block a shield from an empty source or one with no funds (the free-text box allowed both). + if (!isMerge && (s_from_address[0] == '\0' || miningBalance(state, s_from_address) <= 0.0)) can_submit = false; const bool scopeLoaded = s_scan.phase == services::ScopePhase::Loaded; if (isMerge && scopeLoaded && srcCount() == 0) can_submit = false; if (perAddress) can_submit = connected && !syncing && scopeLoaded && !selectedGroups().empty(); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 6447834..a6e8e93 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -2621,7 +2621,9 @@ void I18n::loadBuiltinEnglish() strings_["shield_check_status"] = "Check Status"; strings_["shield_completed"] = "Operation completed successfully!"; strings_["shield_description"] = "Shield your mining rewards by sending coinbase outputs from transparent addresses to a shielded address. This improves privacy by hiding your mining income."; - strings_["shield_from_address"] = "From Address:"; + strings_["shield_from_address"] = "From mining address:"; + strings_["shield_all_mining_addrs"] = "All mining addresses"; + strings_["shield_confirm_fmt"] = "Shield up to %s DRGX from %d mining address(es) (at most %d rewards per run) to %s? Network fee: %s DRGX."; strings_["shield_funds"] = "Shield Funds"; strings_["shield_in_progress"] = "Operation in progress..."; strings_["shield_max_utxos"] = "Max UTXOs per operation";