// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "import_key_dialog.h" #include "../../app.h" #include "../../rpc/rpc_client.h" #include "../../rpc/rpc_worker.h" #include "../../util/i18n.h" #include "../notifications.h" #include "../schema/ui_schema.h" #include "../material/draw_helpers.h" #include "../material/type.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include #include #include namespace dragonx { namespace ui { using json = nlohmann::json; // Static state static bool s_open = false; static char s_key_input[4096] = ""; static bool s_rescan = true; static int s_rescan_height = 0; // 0 = full rescan static bool s_importing = false; static std::string s_status; static int s_total_keys = 0; static int s_imported_keys = 0; static int s_failed_keys = 0; static bool s_paste_previewing = false; static std::string s_preview_text; // Helper to detect key type static std::string detectKeyType(const std::string& key) { if (key.empty()) return "unknown"; // Z-address spending keys start with "secret-extended-key-" or "SK" prefix patterns if (key.substr(0, 20) == "secret-extended-key-") { return "z-spending"; } // Legacy z-addr keys (SK prefix) if (key.length() >= 2 && key[0] == 'S' && key[1] == 'K') { return "z-spending"; } // T-address private keys (WIF format) - start with 5, K, or L for Bitcoin-derived // DragonX/HUSH uses different prefixes if (key.length() >= 51 && key.length() <= 52) { char first = key[0]; if (first == '5' || first == 'K' || first == 'L' || first == 'U') { return "t-privkey"; } } return "unknown"; } // Helper to split input into individual keys static std::vector splitKeys(const std::string& input) { std::vector keys; std::istringstream stream(input); std::string line; while (std::getline(stream, line)) { // Trim whitespace size_t start = line.find_first_not_of(" \t\r\n"); size_t end = line.find_last_not_of(" \t\r\n"); if (start != std::string::npos && end != std::string::npos) { std::string key = line.substr(start, end - start + 1); if (!key.empty() && key[0] != '#') { // Skip comments keys.push_back(key); } } } return keys; } void ImportKeyDialog::show() { s_open = true; s_key_input[0] = '\0'; s_rescan = true; s_rescan_height = 0; s_importing = false; s_status.clear(); s_total_keys = 0; s_imported_keys = 0; s_failed_keys = 0; s_paste_previewing = false; s_preview_text.clear(); } bool ImportKeyDialog::isOpen() { return s_open; } void ImportKeyDialog::render(App* app) { if (!s_open) return; auto& S = schema::UI(); auto win = S.window("dialogs.import-key"); auto keyInput = S.input("dialogs.import-key", "key-input"); auto rescanInput = S.input("dialogs.import-key", "rescan-height-input"); auto importBtn = S.button("dialogs.import-key", "import-button"); auto closeBtn = S.button("dialogs.import-key", "close-button"); if (material::BeginOverlayDialog(TR("import_key_title"), &s_open, win.width, 0.94f)) { // Warning ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.0f, 1.0f)); ImGui::PushFont(material::Type().iconSmall()); ImGui::Text(ICON_MD_WARNING); ImGui::PopFont(); ImGui::SameLine(0, 4.0f); ImGui::TextWrapped("%s", TR("import_key_warning")); ImGui::PopStyleColor(); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Key input ImGui::Text("%s", TR("import_key_label")); ImGui::SameLine(); ImGui::TextDisabled("(?)"); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", TR("import_key_tooltip")); } // Validation indicator inline with title — check preview text during hover, // otherwise check the actual input { const char* checkSrc = s_key_input; if (s_paste_previewing && !s_preview_text.empty()) checkSrc = s_preview_text.c_str(); if (checkSrc[0] != '\0') { auto previewKeys = splitKeys(checkSrc); int pz = 0, pt = 0, pu = 0; for (const auto& k : previewKeys) { std::string kt = detectKeyType(k); if (kt == "z-spending") pz++; else if (kt == "t-privkey") pt++; else pu++; } if (pz > 0 || pt > 0) { ImGui::SameLine(); char vbuf[128]; if (pz > 0 && pt > 0) snprintf(vbuf, sizeof(vbuf), "%d shielded, %d transparent", pz, pt); else if (pz > 0) snprintf(vbuf, sizeof(vbuf), "%d shielded key(s)", pz); else snprintf(vbuf, sizeof(vbuf), "%d transparent key(s)", pt); material::Type().textColored(material::TypeStyle::Caption, material::Success(), vbuf); if (pu > 0) { ImGui::SameLine(); snprintf(vbuf, sizeof(vbuf), "(%d unrecognized)", pu); material::Type().textColored(material::TypeStyle::Caption, material::Error(), vbuf); } } else if (pu > 0 && !s_paste_previewing) { ImGui::SameLine(); material::Type().textColored(material::TypeStyle::Caption, material::Error(), "Unrecognized key format"); } } } if (s_importing) { ImGui::BeginDisabled(); } ImGui::SetNextItemWidth(-1); ImGui::InputTextMultiline("##KeyInput", s_key_input, sizeof(s_key_input), ImVec2(-1, keyInput.height > 0 ? keyInput.height : 150), ImGuiInputTextFlags_AllowTabInput); ImVec2 inputMin = ImGui::GetItemRectMin(); ImVec2 inputMax = ImGui::GetItemRectMax(); // Detect paste button hover before drawing it ImVec2 pasteBtnPos = ImGui::GetCursorScreenPos(); // Estimate button size for hover detection ImFont* btnFont = S.resolveFont(importBtn.font); ImVec2 pasteBtnSize = btnFont ? ImVec2(btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, TR("paste_from_clipboard")).x + ImGui::GetStyle().FramePadding.x * 2, ImGui::GetFrameHeight()) : ImVec2(150, ImGui::GetFrameHeight()); bool paste_hovered = material::IsRectHovered(pasteBtnPos, ImVec2(pasteBtnPos.x + pasteBtnSize.x, pasteBtnPos.y + pasteBtnSize.y)); // Handle preview state if (paste_hovered && !s_paste_previewing) { const char* clip = ImGui::GetClipboardText(); if (clip && clip[0] != '\0') { std::string trimmed(clip); while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' || trimmed.front() == '\n' || trimmed.front() == '\r')) trimmed.erase(trimmed.begin()); while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' || trimmed.back() == '\n' || trimmed.back() == '\r')) trimmed.pop_back(); if (!trimmed.empty() && s_key_input[0] == '\0') { s_preview_text = trimmed; s_paste_previewing = true; } } } else if (!paste_hovered && s_paste_previewing) { s_paste_previewing = false; s_preview_text.clear(); } // Draw transparent preview text overlay on the input field if (s_paste_previewing && !s_preview_text.empty()) { ImVec2 textPos(inputMin.x + ImGui::GetStyle().FramePadding.x, inputMin.y + ImGui::GetStyle().FramePadding.y); ImVec4 previewCol = ImGui::GetStyleColorVec4(ImGuiCol_Text); previewCol.w = S.drawElement("dialogs.import-key", "paste-preview-alpha").sizeOr(0.3f); size_t maxChars = (size_t)S.drawElement("dialogs.import-key", "paste-preview-max-chars").sizeOr(200.0f); // Clip to input rect ImGui::GetWindowDrawList()->PushClipRect(inputMin, inputMax, true); ImGui::GetWindowDrawList()->AddText(textPos, ImGui::ColorConvertFloat4ToU32(previewCol), s_preview_text.c_str(), s_preview_text.c_str() + std::min(s_preview_text.size(), maxChars)); ImGui::GetWindowDrawList()->PopClipRect(); } // Paste button if (material::StyledButton(TR("paste_from_clipboard"), ImVec2(0,0), S.resolveFont(importBtn.font))) { if (s_paste_previewing) { snprintf(s_key_input, sizeof(s_key_input), "%s", s_preview_text.c_str()); s_paste_previewing = false; s_preview_text.clear(); } else { const char* clipboard = ImGui::GetClipboardText(); if (clipboard) { std::string trimmed(clipboard); while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' || trimmed.front() == '\n' || trimmed.front() == '\r')) trimmed.erase(trimmed.begin()); while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' || trimmed.back() == '\n' || trimmed.back() == '\r')) trimmed.pop_back(); snprintf(s_key_input, sizeof(s_key_input), "%s", trimmed.c_str()); } } } ImGui::SameLine(); if (material::StyledButton(TR("clear"), ImVec2(0,0), S.resolveFont(importBtn.font))) { s_key_input[0] = '\0'; s_paste_previewing = false; s_preview_text.clear(); } ImGui::Spacing(); // Rescan options ImGui::Checkbox(TR("import_key_rescan"), &s_rescan); if (s_rescan) { ImGui::Indent(); ImGui::Text("%s", TR("import_key_start_height")); ImGui::SameLine(); ImGui::SetNextItemWidth(rescanInput.width); ImGui::InputInt("##RescanHeight", &s_rescan_height); if (s_rescan_height < 0) s_rescan_height = 0; ImGui::SameLine(); ImGui::TextDisabled("%s", TR("import_key_full_rescan")); ImGui::Unindent(); } if (s_importing) { ImGui::EndDisabled(); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Import button if (s_importing) { ImGui::BeginDisabled(); } if (material::StyledButton(TR("import_key_btn"), ImVec2(importBtn.width, 0), S.resolveFont(importBtn.font))) { auto keys = splitKeys(s_key_input); if (keys.empty()) { Notifications::instance().warning(TR("import_key_no_valid")); } else if (!app->rpc() || !app->rpc()->isConnected()) { Notifications::instance().error(TR("not_connected")); } else { s_importing = true; s_total_keys = static_cast(keys.size()); s_imported_keys = 0; s_failed_keys = 0; s_status = "Importing..."; // Import keys on worker thread to avoid freezing UI bool rescan = s_rescan; if (app->worker()) { app->worker()->post([rpc = app->rpc(), keys, rescan]() -> rpc::RPCWorker::MainCb { int imported = 0; int failed = 0; for (const auto& key : keys) { std::string keyType = detectKeyType(key); try { if (keyType == "z-spending") { rpc->call("z_importkey", {key, rescan ? "yes" : "no"}); imported++; } else if (keyType == "t-privkey") { rpc->call("importprivkey", {key, "", rescan}); imported++; } else { failed++; } } catch (...) { failed++; } } return [imported, failed]() { s_imported_keys = imported; s_failed_keys = failed; s_importing = false; char buf[128]; snprintf(buf, sizeof(buf), "Import complete: %d success, %d failed", imported, failed); s_status = buf; if (imported > 0) { Notifications::instance().success(TR("import_key_success")); } }; }); } } } if (s_importing) { ImGui::EndDisabled(); ImGui::SameLine(); ImGui::TextDisabled(TR("import_key_progress"), s_imported_keys + s_failed_keys, s_total_keys); } ImGui::SameLine(); if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) { s_open = false; } // Status if (!s_status.empty()) { ImGui::Spacing(); bool success = s_failed_keys == 0 && !s_importing; ImGui::TextColored( success ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) : ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", s_status.c_str() ); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Help text ImGui::TextDisabled("%s", TR("import_key_formats")); ImGui::BulletText("%s", TR("import_key_z_format")); ImGui::BulletText("%s", TR("import_key_t_format")); material::EndOverlayDialog(); } } } // namespace ui } // namespace dragonx