- Mining tab: sync s_selected_threads with actual thread count when idle thread scaling adjusts threads (solo via genproclimit, pool via threads_active), skipping sync during user drag - Auto-lock: bypass lock screen overlay when xmrig pool mining is active so the mining UI remains accessible - Import key dialog: add clipboard hover preview with transparent overlay on the input field, inline key type validation next to title (matching send tab paste button pattern), configurable via ui.toml
385 lines
15 KiB
C++
385 lines
15 KiB
C++
// 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 <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;
|
|
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<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;
|
|
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<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(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
|