// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "key_export_dialog.h" #include "../../app.h" #include "../../wallet/lite_wallet_controller.h" #include #include "../../rpc/rpc_client.h" #include "../../rpc/rpc_worker.h" #include "../../util/i18n.h" #include "../schema/ui_schema.h" #include "../material/draw_helpers.h" #include "../material/type.h" #include "../material/colors.h" #include "../layout.h" #include "../widgets/qr_code.h" #include "../widgets/copy_field.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" namespace dragonx { namespace ui { // Static member initialization bool KeyExportDialog::s_open = false; bool KeyExportDialog::s_fetching = false; bool KeyExportDialog::s_show_key = false; bool KeyExportDialog::s_show_qr = false; std::uintptr_t KeyExportDialog::s_qr_tex = 0; std::string KeyExportDialog::s_qr_cached; KeyExportDialog::KeyType KeyExportDialog::s_key_type = KeyExportDialog::KeyType::Private; std::string KeyExportDialog::s_address; std::string KeyExportDialog::s_key; std::string KeyExportDialog::s_error; void KeyExportDialog::releaseQr() { if (s_qr_tex) { FreeQRTexture(s_qr_tex); s_qr_tex = 0; } s_qr_cached.clear(); s_show_qr = false; } void KeyExportDialog::show(const std::string& address, KeyType type) { s_open = true; s_fetching = false; s_show_key = false; releaseQr(); s_key_type = type; s_address = address; s_key.clear(); s_error.clear(); } bool KeyExportDialog::isOpen() { return s_open; } void KeyExportDialog::render(App* app) { if (!s_open) return; const char* title = (s_key_type == KeyType::Private) ? TR("export_private_key") : TR("export_viewing_key"); auto& S = schema::UI(); auto warningBox = S.drawElement("dialogs.key-export", "warning-box"); auto addrInput = S.input("dialogs.key-export", "address-input"); auto revealBtn = S.button("dialogs.key-export", "reveal-button"); auto keyDisplay = S.drawElement("dialogs.key-export", "key-display"); auto copyBtn = S.button("dialogs.key-export", "copy-button"); auto closeBtn = S.button("dialogs.key-export", "close-button"); (void)addrInput; (void)keyDisplay; (void)copyBtn; // Modal scales to 85% of the window width. BeginOverlayDialog multiplies the width by // Layout::dpiScale(); divide it out here so the final card is exactly 85% at any font scale. const float cardW = (0.85f * ImGui::GetMainViewport()->Size.x) / Layout::dpiScale(); if (material::BeginOverlayDialog(title, &s_open, cardW, 0.94f)) { ImGui::Spacing(); // Warning section with colored background ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.2f, 0.2f, 0.3f)); // Auto-size the warning box to its (wrapping, font-scaled) text so it never clips it. (void)warningBox; ImGui::BeginChild("WarningBox", ImVec2(-1, 0), ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Borders); // Heading — warning icon (drawn with the icon font, not the text font, so it isn't tofu) + // larger h6 text, centered, for emphasis. { ImFont* iconF = material::Type().iconMed(); ImFont* textF = material::Type().h6(); const char* icon = ICON_MD_WARNING; const char* txt = TR("warning_upper"); ImGui::PushFont(iconF); const float iw = ImGui::CalcTextSize(icon).x; ImGui::PopFont(); ImGui::PushFont(textF); const float tw = ImGui::CalcTextSize(txt).x; ImGui::PopFont(); const float sp = ImGui::GetStyle().ItemSpacing.x; const float wa = ImGui::GetContentRegionAvail().x; const float total = iw + sp + tw; ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (wa > total ? (wa - total) * 0.5f : 0.0f)); const ImVec4 red(1.0f, 0.42f, 0.42f, 1.0f); ImGui::PushFont(iconF); ImGui::TextColored(red, "%s", icon); ImGui::PopFont(); ImGui::SameLine(0, sp); ImGui::PushFont(textF); ImGui::TextColored(red, "%s", txt); ImGui::PopFont(); } ImGui::Spacing(); // Body — centered wrapped text. { const char* body = (s_key_type == KeyType::Private) ? TR("key_export_private_warning") : TR("key_export_viewing_warning"); ImDrawList* wdl = ImGui::GetWindowDrawList(); ImFont* wf = ImGui::GetFont(); const float wfs = ImGui::GetFontSize(); const float wa = ImGui::GetContentRegionAvail().x; const ImVec2 wp = ImGui::GetCursorScreenPos(); const float bh = widgets::DrawCenteredWrapped(wdl, wf, wfs, wp, wa, material::OnSurface(), body); ImGui::Dummy(ImVec2(wa, bh)); } ImGui::EndChild(); ImGui::PopStyleColor(); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Address — click-to-copy field (chunked, width-clamped, centered). ImGui::Text("%s", TR("address_label")); widgets::AddressCopyField("##addrcopy", s_address); ImGui::Spacing(); // Key display section const char* key_label = (s_key_type == KeyType::Private) ? TR("key_export_private_key") : TR("key_export_viewing_key"); ImGui::Text("%s", key_label); if (s_fetching) { ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", TR("key_export_fetching")); } else if (!s_error.empty()) { ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), TR("error_format"), s_error.c_str()); } else if (s_key.empty()) { // Show button to fetch key if (material::StyledButton(TR("key_export_reveal"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) { s_fetching = true; // Check if z-address or t-address bool is_zaddress = (s_address.length() > 50 || s_address[0] == 'z'); if (auto* lite = app->liteWallet()) { // Lite wallet: there is no daemon RPC — export the key locally via the SDXL // backend (a local, network-free op) instead of the z_exportkey/dumpprivkey // RPCs, which fail with "Not connected" when no full node is present. const bool wantViewing = (s_key_type != KeyType::Private); if (wantViewing && !is_zaddress) { s_error = TR("key_export_viewing_keys_zonly"); s_fetching = false; } else { auto r = lite->exportPrivateKeys(s_address); std::string found; if (r.ok) { try { auto j = nlohmann::json::parse(r.privateKeysJson); const nlohmann::json* entry = nullptr; if (j.is_array()) { for (auto& e : j) if (e.contains("address") && e["address"] == s_address) { entry = &e; break; } if (!entry && !j.empty()) entry = &j.front(); } else if (j.is_object()) { entry = &j; } const char* field = wantViewing ? "viewing_key" : "private_key"; if (entry && entry->contains(field) && (*entry)[field].is_string()) found = (*entry)[field].get(); } catch (...) {} wallet::secureWipeLiteSecret(r.privateKeysJson); } if (!found.empty()) { s_key = found; s_show_key = wantViewing; // viewing keys are less sensitive } else { s_error = r.ok ? std::string("Key not available for this address") : r.error; } wallet::secureWipeLiteSecret(found); s_fetching = false; } } else if (s_key_type == KeyType::Private) { // Export private key std::string addr = s_address; std::string method = is_zaddress ? "z_exportkey" : "dumpprivkey"; if (app->worker()) { app->worker()->post([rpc = app->rpc(), addr, method]() -> rpc::RPCWorker::MainCb { std::string key; std::string error; try { rpc::RPCClient::TraceScope trace("Settings / Export key"); auto result = rpc->call(method, {addr}); key = result.get(); } catch (const std::exception& e) { error = e.what(); } return [key, error]() { if (error.empty()) { s_key = key; s_show_key = false; // Don't show by default } else { s_error = error; } s_fetching = false; }; }); } } else { // Export viewing key (only for z-addresses) if (is_zaddress) { std::string addr = s_address; if (app->worker()) { app->worker()->post([rpc = app->rpc(), addr]() -> rpc::RPCWorker::MainCb { std::string key; std::string error; try { rpc::RPCClient::TraceScope trace("Settings / Export viewing key"); auto result = rpc->call("z_exportviewingkey", {addr}); key = result.get(); } catch (const std::exception& e) { error = e.what(); } return [key, error]() { if (error.empty()) { s_key = key; s_show_key = true; // Viewing keys are less sensitive } else { s_error = error; } s_fetching = false; }; }); } } else { s_error = TR("key_export_viewing_keys_zonly"); s_fetching = false; } } } ImGui::SameLine(); ImGui::TextDisabled("%s", TR("key_export_click_retrieve")); } else { // Key fetched. Layout: [ key text (click-to-copy) + Show/Hide below ] [ QR | square ]. const float gap = 12.0f; const float avail = ImGui::GetContentRegionAvail().x; // Larger, responsive QR: ~30% of the content width, clamped to a comfortable range. float qrSize = avail * 0.30f; if (qrSize < 200.0f) qrSize = 200.0f; if (qrSize > 340.0f) qrSize = 340.0f; float keyW = avail - qrSize - gap; const bool sideBySide = keyW >= 200.0f; // too narrow -> stack the QR under the key if (!sideBySide) keyW = avail; // Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact // rather than slicing it into 4s — only the part from the '1' separator on is chunked. auto chunkKey = [](const std::string& s) -> std::string { const size_t one = s.find('1'); if (one != std::string::npos && one > 0) { bool isHrp = true; for (size_t i = 0; i < one; ++i) { const char c = s[i]; if (!((c >= 'a' && c <= 'z') || c == '-')) { isHrp = false; break; } } if (isHrp) return s.substr(0, one) + " " + widgets::ChunkString(s.substr(one), 4); } return widgets::ChunkString(s, 4); }; const std::string shown = s_show_key ? s_key : std::string(s_key.size(), '*'); const std::string keyText = chunkKey(shown); // Left column: key text (click-to-copy), with the Show/Hide button just below it. ImGui::BeginGroup(); if (widgets::CopyField("##keycopy", keyText, keyW, 0.0f, /*center=*/true)) { app->copySecretToClipboard(s_key); // auto-clearing clipboard } ImGui::Spacing(); if (material::StyledButton(s_show_key ? TR("hide") : TR("show"), ImVec2(0, 0), material::Type().subtitle1())) { s_show_key = !s_show_key; } ImGui::EndGroup(); if (sideBySide) ImGui::SameLine(0, gap); else ImGui::Spacing(); // QR to the right of the key when revealed; an empty placeholder square while hidden. const ImVec2 sq0 = ImGui::GetCursorScreenPos(); if (s_show_key && !s_key.empty()) { if (s_qr_cached != s_key) { // (re)generate the QR texture when the key changes if (s_qr_tex) { FreeQRTexture(s_qr_tex); s_qr_tex = 0; } int qw = 0, qh = 0; s_qr_tex = GenerateQRTexture(s_key.c_str(), &qw, &qh); s_qr_cached = s_key; } if (s_qr_tex) { // White quiet-zone backing so the code stays scannable on dark themes. ImGui::GetWindowDrawList()->AddRectFilled( sq0, ImVec2(sq0.x + qrSize, sq0.y + qrSize), IM_COL32_WHITE, 4.0f); RenderQRCode(s_qr_tex, qrSize); } else { ImGui::Dummy(ImVec2(qrSize, qrSize)); } } else { // Empty placeholder square — same footprint the QR will occupy once revealed. ImGui::Dummy(ImVec2(qrSize, qrSize)); ImDrawList* dl = ImGui::GetWindowDrawList(); const ImVec2 sq1(sq0.x + qrSize, sq0.y + qrSize); dl->AddRectFilled(sq0, sq1, material::WithAlpha(material::OnSurface(), 8), 6.0f); dl->AddRect(sq0, sq1, material::WithAlpha(material::OnSurface(), 45), 6.0f, 0, 1.0f); } } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Close button float button_width = closeBtn.width; float avail_width = ImGui::GetContentRegionAvail().x; ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail_width - button_width) / 2.0f); if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) { s_open = false; // Clear sensitive data s_key.clear(); s_show_key = false; releaseQr(); } material::EndOverlayDialog(); } // Dialog dismissed any other way (scrim click / Esc): drop the key + its QR texture. if (!s_open) { if (!s_key.empty()) s_key.clear(); s_show_key = false; releaseQr(); } } } // namespace ui } // namespace dragonx