Files
ObsidianDragon/src/ui/windows/key_export_dialog.cpp
dan_s 3e1b60b0f5 feat(keys): improve the key-export modal — auto-clear copy, inline QR, cleaner actions
- Auto-clear: the Copy button now routes through App::copySecretToClipboard, so a copied
  private/viewing key is wiped from the clipboard after ~45s (same protection as the seed) with
  a "auto-clears" notice — instead of the raw SetClipboardText that left it indefinitely.
- QR: once the key is revealed, a Show/Hide QR toggle renders the key's QR inline (via the same
  GenerateQRTexture/RenderQRCode widget the Receive tab uses) for scanning into another wallet.
  The QR texture is cached, regenerated on key change, and freed on hide/close/dismiss; hiding the
  key also hides its QR.
- Actions row tightened to Show/Hide · Copy · QR, and the key + QR texture are now cleared on any
  dismissal (Close button, scrim click, Esc), not just the Close button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:59:06 -05:00

285 lines
11 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "key_export_dialog.h"
#include "../../app.h"
#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 "../widgets/qr_code.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 win = S.window("dialogs.key-export");
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 toggleBtn = S.button("dialogs.key-export", "toggle-button");
auto copyBtn = S.button("dialogs.key-export", "copy-button");
auto closeBtn = S.button("dialogs.key-export", "close-button");
if (material::BeginOverlayDialog(title, &s_open, win.width, 0.94f)) {
ImGui::Spacing();
// Warning section with colored background
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.2f, 0.2f, 0.3f));
ImGui::BeginChild("WarningBox", ImVec2(-1, warningBox.height > 0 ? warningBox.height : 80), true);
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), " %s", TR("warning_upper"));
ImGui::Spacing();
if (s_key_type == KeyType::Private) {
ImGui::TextWrapped(" %s", TR("key_export_private_warning"));
} else {
ImGui::TextWrapped(" %s", TR("key_export_viewing_warning"));
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Address display
ImGui::Text("%s", TR("address_label"));
// Determine if it's a z-address (longer) or t-address
bool is_zaddr = s_address.length() > 50;
if (is_zaddr) {
// Use multiline for z-addresses
char addr_buf[512];
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
addr_buf[sizeof(addr_buf) - 1] = '\0';
ImGui::InputTextMultiline("##Address", addr_buf, sizeof(addr_buf),
ImVec2(-1, addrInput.height > 0 ? addrInput.height : 60), ImGuiInputTextFlags_ReadOnly);
} else {
char addr_buf[128];
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
addr_buf[sizeof(addr_buf) - 1] = '\0';
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
}
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 (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<std::string>();
} 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<std::string>();
} 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 has been fetched - display it
if (s_show_key) {
// Show the actual key
char key_buf[1024];
strncpy(key_buf, s_key.c_str(), sizeof(key_buf) - 1);
key_buf[sizeof(key_buf) - 1] = '\0';
ImGui::InputTextMultiline("##Key", key_buf, sizeof(key_buf),
ImVec2(-1, keyDisplay.height > 0 ? keyDisplay.height : 80), ImGuiInputTextFlags_ReadOnly);
} else {
// Show masked
std::string masked(s_key.length(), '*');
char masked_buf[1024];
strncpy(masked_buf, masked.c_str(), sizeof(masked_buf) - 1);
masked_buf[sizeof(masked_buf) - 1] = '\0';
ImGui::InputTextMultiline("##Key", masked_buf, sizeof(masked_buf),
ImVec2(-1, keyDisplay.height > 0 ? keyDisplay.height : 80), ImGuiInputTextFlags_ReadOnly);
}
// Action row: Show/Hide · Copy · QR
ImGui::Spacing();
if (material::StyledButton(s_show_key ? TR("hide") : TR("show"), ImVec2(toggleBtn.width, 0), S.resolveFont(toggleBtn.font))) {
s_show_key = !s_show_key;
if (!s_show_key) s_show_qr = false; // hiding the key also hides its QR
}
ImGui::SameLine();
if (material::StyledButton(TR("copy_to_clipboard"), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
// Auto-clearing clipboard: the key (as sensitive as the seed) is wiped after ~45s.
app->copySecretToClipboard(s_key);
}
// QR (only once revealed) — for scanning the key into another wallet.
if (s_show_key) {
ImGui::SameLine();
if (material::StyledButton(s_show_qr ? TR("hide_qr") : TR("show_qr"),
ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
s_show_qr = !s_show_qr;
}
}
if (s_show_qr && 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) {
ImGui::Spacing();
const float qrSize = 180.0f;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - qrSize) * 0.5f);
RenderQRCode(s_qr_tex, qrSize);
}
}
}
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