diff --git a/src/ui/windows/validate_address_dialog.cpp b/src/ui/windows/validate_address_dialog.cpp index dadac36..4c8daa0 100644 --- a/src/ui/windows/validate_address_dialog.cpp +++ b/src/ui/windows/validate_address_dialog.cpp @@ -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(); diff --git a/src/ui/windows/validate_address_dialog.h b/src/ui/windows/validate_address_dialog.h index 0d6fcd2..5698cf7 100644 --- a/src/ui/windows/validate_address_dialog.h +++ b/src/ui/windows/validate_address_dialog.h @@ -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; }; diff --git a/src/util/address_validation.cpp b/src/util/address_validation.cpp index 7abd144..e44cd91 100644 --- a/src/util/address_validation.cpp +++ b/src/util/address_validation.cpp @@ -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 diff --git a/src/util/address_validation.h b/src/util/address_validation.h index 48542a1..b3eede2 100644 --- a/src/util/address_validation.h +++ b/src/util/address_validation.h @@ -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 diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index bf02a68..8044bca 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -12642,6 +12642,13 @@ void testAddressChecksumValidation() EXPECT_FALSE(isTransparentAddress("Up3W7uVYkLxCfH91APxjSpkkGBWJyBrm3tt1bCz64V5fpZK9ef3C")); EXPECT_FALSE(isDragonXAddress("R9NXAVJezHiBnT3ijTpg3JUZre7PxhJWtX")); // flipped checksum char EXPECT_FALSE(isValidRecipientAddress("")); + using dragonx::util::validateRpcMethod; + EXPECT_EQ(std::string(validateRpcMethod(classifyRecipient(zaddr))), std::string("z_validateaddress")); + EXPECT_EQ(std::string(validateRpcMethod(classifyRecipient(p2pkh))), std::string("validateaddress")); + EXPECT_EQ(std::string(validateRpcMethod(classifyRecipient(p2sh))), std::string("validateaddress")); + EXPECT_TRUE(validateRpcMethod(classifyRecipient("x")) == nullptr); + EXPECT_TRUE(validateRpcMethod(classifyRecipient("zivks1qqqsyqcyq5rqwzqfpg9scrgwpugpzysn")) == nullptr); // a key is never an address -> no RPC + EXPECT_TRUE(validateRpcMethod(classifyRecipient("")) == nullptr); } // Import-key recognition: the client gate must accept every real DragonX key form and reject