fix(ui): validate shielded addresses with the correct RPC

The Validate address dialog chose its RPC from the first character of
the input, then asked the transparent-only validateaddress for every
case. That RPC is Base58-only, so a valid Sapling (z) address came back
isvalid=false and was reported invalid.

- The input is now classified with the checksum-verified recognizers,
  and a shielded address is sent to z_validateaddress while a
  transparent (mining) address keeps validateaddress.
- Anything that is not an address - a typo, garbage, or a pasted
  viewing/spending key - is judged invalid on the spot and never sent to
  the node, so a key pasted into the field is not leaked to an RPC.
- On Lite there is no node to ask, so the dialog shows the checksum
  verdict alone and hides the "owned by this wallet" row.

Tests: validateRpcMethod maps shielded to z_validateaddress, transparent
to validateaddress, and a key or empty string to no RPC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 21:42:23 -05:00
co-authored by Claude Opus 4.8
parent abc82136c4
commit 579accd0c6
5 changed files with 88 additions and 64 deletions
+64 -64
View File
@@ -6,6 +6,7 @@
#include "../../app.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/address_validation.h"
#include "../../util/i18n.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
@@ -23,6 +24,7 @@ char ValidateAddressDialog::s_address_input[512] = "";
bool ValidateAddressDialog::s_is_valid = false;
bool ValidateAddressDialog::s_is_mine = false;
bool ValidateAddressDialog::s_is_mining = false;
bool ValidateAddressDialog::s_ownership_known = true;
std::string ValidateAddressDialog::s_address_type;
std::string ValidateAddressDialog::s_error_message;
@@ -35,6 +37,7 @@ void ValidateAddressDialog::show()
s_is_valid = false;
s_is_mine = false;
s_is_mining = false;
s_ownership_known = true;
s_address_type.clear();
s_error_message.clear();
}
@@ -104,65 +107,60 @@ void ValidateAddressDialog::render(App* app)
while (!address.empty() && (address.front()==' '||address.front()=='\t'||address.front()=='\n'||address.front()=='\r')) address.erase(address.begin());
while (!address.empty() && (address.back()==' '||address.back()=='\t'||address.back()=='\n'||address.back()=='\r')) address.pop_back();
// Determine if z-address or t-address
bool is_zaddr = !address.empty() && address[0] == 'z';
if (is_zaddr) {
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), address]() -> rpc::RPCWorker::MainCb {
bool valid = false, mine = false;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Receive tab / Validate address");
auto result = rpc->call("validateaddress", {address});
valid = result.value("isvalid", false);
mine = result.value("ismine", false);
} catch (const std::exception& e) {
error = e.what();
// Classify with the checksum-verified recognizers, not address[0]=='z': the daemon's validateaddress
// is transparent-only (Base58), so a Sapling address must go to z_validateaddress.
const util::RecipientKind kind = util::classifyRecipient(address);
const char* method = util::validateRpcMethod(kind);
const bool mining = (kind == util::RecipientKind::MiningOnly);
if (!method) {
// Not an address (typo, a pasted key, garbage): INVALID without asking the node, so a viewing
// or spending key pasted here is never sent to an RPC.
s_is_valid = false;
s_is_mine = false;
s_is_mining = false;
s_ownership_known = true;
s_address_type.clear();
s_validated = true;
s_validating = false;
} else if (app->isLiteBuild()) {
// Lite has no node to ask: the checksum verdict is the answer, ownership is unknown (hidden).
s_is_valid = true;
s_is_mine = false;
s_ownership_known = false;
s_is_mining = mining;
s_address_type = TR(mining ? "validate_mining_type" : "validate_shielded_type");
s_validated = true;
s_validating = false;
} else if (app->worker()) {
app->worker()->post([rpc = app->rpc(), address, method = std::string(method), mining]() -> rpc::RPCWorker::MainCb {
bool valid = false, mine = false;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Receive tab / Validate address");
auto result = rpc->call(method, {address});
valid = result.value("isvalid", false);
mine = result.value("ismine", false);
} catch (const std::exception& e) {
error = e.what();
}
return [valid, mine, error, mining]() {
if (error.empty()) {
s_is_valid = valid;
s_is_mine = valid && mine;
s_ownership_known = true;
s_address_type = TR(mining ? "validate_mining_type" : "validate_shielded_type");
s_is_mining = mining;
} else {
s_error_message = error;
s_is_valid = false;
}
return [valid, mine, error]() {
if (error.empty()) {
s_is_valid = valid;
s_is_mine = mine;
s_address_type = TR("validate_shielded_type");
s_is_mining = false;
} else {
s_error_message = error;
s_is_valid = false;
}
s_validated = true;
s_validating = false;
};
});
}
s_validated = true;
s_validating = false;
};
});
} else {
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), address]() -> rpc::RPCWorker::MainCb {
bool valid = false, mine = false;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Receive tab / Validate address");
auto result = rpc->call("validateaddress", {address});
valid = result.value("isvalid", false);
mine = result.value("ismine", false);
} catch (const std::exception& e) {
error = e.what();
}
return [valid, mine, error]() {
if (error.empty()) {
s_is_valid = valid;
s_is_mine = mine;
s_address_type = TR("validate_mining_type");
s_is_mining = true;
} else {
s_error_message = error;
s_is_valid = false;
}
s_validated = true;
s_validating = false;
};
});
}
s_validating = false; // no worker: don't leave the button disabled forever
}
}
@@ -211,13 +209,15 @@ void ValidateAddressDialog::render(App* app)
ImGui::SameLine(lbl.position);
ImGui::Text("%s", s_address_type.c_str());
// Is mine?
ImGui::Text("%s", TR("validate_ownership"));
ImGui::SameLine(lbl.position);
if (s_is_mine) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("validate_is_mine"));
} else {
ImGui::TextDisabled("%s", TR("validate_not_mine"));
// Is mine? (not shown when no node was asked - lite)
if (s_ownership_known) {
ImGui::Text("%s", TR("validate_ownership"));
ImGui::SameLine(lbl.position);
if (s_is_mine) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("validate_is_mine"));
} else {
ImGui::TextDisabled("%s", TR("validate_not_mine"));
}
}
if (s_is_mining) { // valid, but not for payments
ImGui::Spacing();
+1
View File
@@ -51,6 +51,7 @@ private:
static bool s_is_valid;
static bool s_is_mine;
static bool s_is_mining; // a valid transparent (mining) address: explain it can't be paid
static bool s_ownership_known; // false when no node was asked (lite): the ownership row is hidden
static std::string s_address_type;
static std::string s_error_message;
};
+10
View File
@@ -198,5 +198,15 @@ RecipientKind classifyRecipient(const std::string& s)
return RecipientKind::Invalid;
}
const char* validateRpcMethod(RecipientKind kind)
{
switch (kind) {
case RecipientKind::Shielded: return "z_validateaddress";
case RecipientKind::MiningOnly: return "validateaddress";
case RecipientKind::Invalid: break;
}
return nullptr;
}
} // namespace util
} // namespace dragonx
+6
View File
@@ -58,5 +58,11 @@ bool isValidRecipientAddress(const std::string& s);
enum class RecipientKind { Invalid, Shielded, MiningOnly };
RecipientKind classifyRecipient(const std::string& s);
// The daemon RPC that can validate an address of this kind: "z_validateaddress" for a shielded (Sapling)
// address, "validateaddress" for a transparent one (z_validateaddress reports isvalid=false for those, and
// validateaddress is Base58-only so it reports false for Sapling), nullptr for Invalid (not an address:
// answer client-side, send nothing to the node).
const char* validateRpcMethod(RecipientKind kind);
} // namespace util
} // namespace dragonx