feat(shield): pick the mining address from a dropdown + show a confirm preview

Shield-coinbase replaced its free-text "*"-wildcard address box with a dropdown
of the wallet's mining (transparent) addresses and their balances ("All mining
addresses" default), and the confirm step now previews "Shield up to X DRGX from
N mining address(es)… to <label>? Network fee: Y" instead of only relabeling the
button. Also blocks submitting from an empty source or one with no funds. The
z_shieldcoinbase call (args/fee/limit/destination) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 13:09:41 -05:00
co-authored by Claude Opus 4.8
parent bcfc324426
commit 572b98cbcf
2 changed files with 63 additions and 3 deletions
+60 -2
View File
@@ -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();
+3 -1
View File
@@ -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";