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:
2026-02-26 02:31:52 -06:00
commit 3aee55b49c
306 changed files with 177789 additions and 0 deletions

View 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