// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "validate_address_dialog.h" #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" #include "../theme.h" #include "imgui.h" namespace dragonx { namespace ui { // Static member initialization bool ValidateAddressDialog::s_open = false; bool ValidateAddressDialog::s_validated = false; bool ValidateAddressDialog::s_validating = false; 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; void ValidateAddressDialog::show() { s_open = true; s_validated = false; s_validating = false; s_address_input[0] = '\0'; s_is_valid = false; s_is_mine = false; s_is_mining = false; s_ownership_known = true; s_address_type.clear(); s_error_message.clear(); } void ValidateAddressDialog::sweepShowMiningResult(const char* address) { show(); snprintf(s_address_input, sizeof(s_address_input), "%s", address); s_validated = true; s_is_valid = true; s_is_mine = true; s_is_mining = true; s_address_type = TR("validate_mining_type"); } bool ValidateAddressDialog::isOpen() { return s_open; } void ValidateAddressDialog::hide() { s_open = false; } void ValidateAddressDialog::render(App* app) { if (!s_open) return; auto& S = schema::UI(); auto win = S.window("dialogs.validate-address"); auto valBtn = S.button("dialogs.validate-address", "validate-button"); auto pasteBtn = S.button("dialogs.validate-address", "paste-button"); auto lbl = S.label("dialogs.validate-address", "label"); auto closeBtn = S.button("dialogs.validate-address", "close-button"); material::OverlayDialogSpec ov; ov.title = TR("validate_title"); ov.p_open = &s_open; ov.style = material::OverlayStyle::BlurFloat; ov.cardWidth = win.width; ov.idSuffix = "validateaddr"; if (material::BeginOverlayDialog(ov)) { ImGui::TextWrapped("%s", TR("validate_description")); ImGui::Spacing(); // Address input bool enter_pressed = material::LabeledInput(TR("address_label"), "##ValidateAddr", s_address_input, sizeof(s_address_input), -1.0f, nullptr, ImGuiInputTextFlags_EnterReturnsTrue); ImGui::Spacing(); // Validate button // Classification + checksum are LOCAL, so allow validating offline (the is-mine check needs the node). bool can_validate = strlen(s_address_input) > 0 && !s_validating; if (!can_validate) { ImGui::BeginDisabled(); } if (material::TactileButton(TR("validate_btn"), ImVec2(valBtn.width, 0), S.resolveFont(valBtn.font)) || (enter_pressed && can_validate)) { s_validating = true; s_validated = false; s_error_message.clear(); std::string address(s_address_input); // Trim whitespace/newlines a pasted address often carries (the daemon would reject it). 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(); // 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() || !app->isConnected()) { // No node to ask (lite, or full-node offline): the checksum verdict is the answer; ownership // is unknown. The result block shows why. 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; } s_validated = true; s_validating = false; }; }); } else { s_validating = false; // no worker: don't leave the button disabled forever } } if (!can_validate) { ImGui::EndDisabled(); } ImGui::SameLine(); if (material::TactileButton(TR("paste"), ImVec2(pasteBtn.width, 0), S.resolveFont(pasteBtn.font))) { const char* clipboard = ImGui::GetClipboardText(); if (clipboard) { strncpy(s_address_input, clipboard, sizeof(s_address_input) - 1); s_address_input[sizeof(s_address_input) - 1] = '\0'; s_validated = false; } } if (s_validating) { ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", TR("validating")); } ImGui::Spacing(); // Results if (s_validated) { ImGui::Text("%s", TR("validate_results")); ImGui::Spacing(); if (!s_error_message.empty()) { ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), TR("error_format"), s_error_message.c_str()); } else { // Valid/Invalid indicator ImGui::Text("%s", TR("validate_status")); ImGui::SameLine(lbl.position); if (s_is_valid) { ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("validate_valid")); } else { ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("validate_invalid")); } if (s_is_valid) { // Address type ImGui::Text("%s", TR("validate_type")); ImGui::SameLine(lbl.position); ImGui::Text("%s", s_address_type.c_str()); // Is mine? When no node was asked (lite / offline) say why ownership is unknown rather // than silently omitting it. ImGui::Text("%s", TR("validate_ownership")); ImGui::SameLine(lbl.position); if (!s_ownership_known) { ImGui::TextDisabled("%s", TR(app->isLiteBuild() ? "validate_ownership_unknown_lite" : "validate_ownership_unknown_offline")); } else 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(); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Warning())); ImGui::TextWrapped("%s", TR("mining_addr_explainer")); ImGui::PopStyleColor(); } } } } else { // Pre-interaction placeholder: keeps the results area from reading as a large // dead gap between the Validate/Paste row and the Close button until a result exists. ImGui::Spacing(); float cw = ImGui::GetContentRegionAvail().x; ImVec2 ts = ImGui::CalcTextSize(TR("validate_results_placeholder")); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (cw - ts.x) * 0.5f); ImGui::TextDisabled("%s", TR("validate_results_placeholder")); } // Close button at bottom — centered via the shared footer helper (no separator, matching // the prior hand-rolled placement). float button_width = closeBtn.width; material::BeginOverlayDialogFooter(button_width, /*drawSeparator=*/false); if (material::TactileButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) { s_open = false; } material::EndOverlayDialog(); } } } // namespace ui } // namespace dragonx