ObsidianDragon - DragonX ImGui Wallet
Full-node GUI wallet for DragonX cryptocurrency. Built with Dear ImGui, SDL3, and OpenGL3/DX11. Features: - Send/receive shielded and transparent transactions - Autoshield with merged transaction display - Built-in CPU mining (xmrig) - Peer management and network monitoring - Wallet encryption with PIN lock - QR code generation for receive addresses - Transaction history with pagination - Console for direct RPC commands - Cross-platform (Linux, Windows)
This commit is contained in:
291
src/ui/windows/import_key_dialog.cpp
Normal file
291
src/ui/windows/import_key_dialog.cpp
Normal file
@@ -0,0 +1,291 @@
|
||||
// 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 "../theme.h"
|
||||
#include "../effects/imgui_acrylic.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
|
||||
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;
|
||||
|
||||
// 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<std::string> splitKeys(const std::string& input)
|
||||
{
|
||||
std::vector<std::string> 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;
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
|
||||
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
|
||||
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowFocus();
|
||||
|
||||
const auto& acrylicTheme = GetCurrentAcrylicTheme();
|
||||
ImGui::OpenPopup("Import Private Key");
|
||||
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Import Private Key", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
|
||||
// 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("Warning: Never share your private keys! "
|
||||
"Importing keys from untrusted sources can compromise your wallet.");
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Key input
|
||||
ImGui::Text("Private Key(s):");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(?)");
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Enter one or more private keys, one per line.\n"
|
||||
"Supports both z-address and t-address keys.\n"
|
||||
"Lines starting with # are treated as comments.");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Paste button
|
||||
if (material::StyledButton("Paste from Clipboard", ImVec2(0,0), S.resolveFont(importBtn.font))) {
|
||||
const char* clipboard = ImGui::GetClipboardText();
|
||||
if (clipboard) {
|
||||
strncpy(s_key_input, clipboard, sizeof(s_key_input) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Clear", ImVec2(0,0), S.resolveFont(importBtn.font))) {
|
||||
s_key_input[0] = '\0';
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Rescan options
|
||||
ImGui::Checkbox("Rescan blockchain after import", &s_rescan);
|
||||
|
||||
if (s_rescan) {
|
||||
ImGui::Indent();
|
||||
ImGui::Text("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("(0 = 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("Import Key(s)", ImVec2(importBtn.width, 0), S.resolveFont(importBtn.font))) {
|
||||
auto keys = splitKeys(s_key_input);
|
||||
|
||||
if (keys.empty()) {
|
||||
Notifications::instance().warning("No valid keys found in input");
|
||||
} else if (!app->rpc() || !app->rpc()->isConnected()) {
|
||||
Notifications::instance().error("Not connected to daemon");
|
||||
} else {
|
||||
s_importing = true;
|
||||
s_total_keys = static_cast<int>(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("Keys imported successfully");
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s_importing) {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Importing %d/%d...", s_imported_keys + s_failed_keys, s_total_keys);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("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("Supported key formats:");
|
||||
ImGui::BulletText("Z-address spending keys (secret-extended-key-...)");
|
||||
ImGui::BulletText("T-address WIF private keys");
|
||||
}
|
||||
effects::ImGuiAcrylic::EndAcrylicPopup();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
Reference in New Issue
Block a user