feat(settings): redesign the Import Key dialog
Rework the full-node Import Key dialog into a safer, Material-consistent, more capable flow: - Security: the key is masked by default (Password) with a reveal (eye) toggle, a DialogWarningHeader "only import a key you own" note, and the buffer is wiped with sodium_memzero on close and on successful import. - Progress: import triggers a blocking full-chain rescan; the dialog now shows "Importing & rescanning — this can take several minutes" with LoadingDots and disables Import while it runs (no double-submit). Offline shows a "connect a running node" note and disables Import up front. - Viewing keys: the classifier gains isViewingKey / isRecognizedImportKey; importPrivateKey auto-detects a shielded viewing key (zxviews…) and routes to z_importviewingkey (watch-only) vs z_importkey / importprivkey. The live type indicator shows Transparent / Shielded spending / Shielded viewing (watch-only). - Result: on success the imported address is captured from the RPC result and shown via AddressCopyField. - Material restyle (OverlayDialogSpec/BlurFloat + TactileButton + Esc) and full i18n (12 new keys x 8 languages; CJK subset rebuilt). Verified: rendered on dark + light skins; a seeded zxviews… key shows the masked field + "Shielded viewing key (watch-only)" indicator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
184
src/app.cpp
184
src/app.cpp
@@ -66,6 +66,7 @@
|
||||
#include "ui/material/type.h"
|
||||
#include "ui/material/typography.h"
|
||||
#include "ui/material/draw_helpers.h"
|
||||
#include "ui/widgets/copy_field.h"
|
||||
#include "ui/notifications.h"
|
||||
#include "util/i18n.h"
|
||||
#include "util/platform.h"
|
||||
@@ -2755,106 +2756,151 @@ void App::renderLiteUnlockPrompt()
|
||||
|
||||
void App::renderImportKeyDialog()
|
||||
{
|
||||
auto dlg = ui::schema::UI().drawElement("inline-dialogs", "import-key");
|
||||
auto dlgF = [&](const char* key, float fb) -> float {
|
||||
auto it = dlg.extraFloats.find(key);
|
||||
return it != dlg.extraFloats.end() ? it->second : fb;
|
||||
};
|
||||
|
||||
int btnFont = (int)dlgF("button-font", 1);
|
||||
float btnW = dlgF("button-width", 120.0f);
|
||||
|
||||
if (!ui::material::BeginOverlayDialog("Import Private Key", &show_import_key_, dlgF("width", 500.0f), 0.94f)) {
|
||||
return;
|
||||
namespace m = ui::material;
|
||||
float dp = ui::Layout::dpiScale();
|
||||
|
||||
m::OverlayDialogSpec ov;
|
||||
ov.title = TR("import_key_title");
|
||||
ov.p_open = &show_import_key_;
|
||||
ov.style = m::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 520.0f;
|
||||
ov.idSuffix = "importkey";
|
||||
if (!m::BeginOverlayDialog(ov)) return;
|
||||
|
||||
// Fresh dialog on each open (the key buffer was already wiped on the last close).
|
||||
if (ImGui::IsWindowAppearing()) {
|
||||
import_success_ = false;
|
||||
import_status_.clear();
|
||||
import_result_address_.clear();
|
||||
import_key_reveal_ = false;
|
||||
}
|
||||
|
||||
ImGui::TextWrapped("Enter a private key to import. The wallet will rescan the blockchain for transactions.");
|
||||
// Esc dismisses (this overlay has no built-in Esc handling).
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) show_import_key_ = false;
|
||||
|
||||
// Handle-with-care note.
|
||||
m::DialogWarningHeader(TR("import_key_warn"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Private Key:");
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_));
|
||||
|
||||
// Paste & Clear buttons
|
||||
if (ui::material::StyledButton(TR("paste"), ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
|
||||
|
||||
// A running node is required — say so up front instead of failing only after Import.
|
||||
if (!state_.connected) {
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceMedium()), "%s", TR("import_key_need_node"));
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// Masked key field with a reveal (eye) toggle + Paste/Clear.
|
||||
ImGui::TextUnformatted(TR("import_key_field"));
|
||||
float eyeW = ImGui::GetFrameHeight();
|
||||
ImGui::SetNextItemWidth(-(eyeW + ui::Layout::spacingSm()));
|
||||
ImGuiInputTextFlags flags = import_key_reveal_ ? 0 : ImGuiInputTextFlags_Password;
|
||||
ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_), flags);
|
||||
ImGui::SameLine(0, ui::Layout::spacingSm());
|
||||
if (m::TactileButton(import_key_reveal_ ? ICON_MD_VISIBILITY_OFF : ICON_MD_VISIBILITY,
|
||||
ImVec2(eyeW, eyeW), m::Type().iconSmall()))
|
||||
import_key_reveal_ = !import_key_reveal_;
|
||||
if (ImGui::IsItemHovered()) m::Tooltip("%s", TR("import_key_reveal_tip"));
|
||||
|
||||
if (m::TactileButton(TR("paste"))) {
|
||||
const char* clipboard = ImGui::GetClipboardText();
|
||||
if (clipboard) {
|
||||
snprintf(import_key_input_, sizeof(import_key_input_), "%s", clipboard);
|
||||
// Trim whitespace
|
||||
std::string trimmed(import_key_input_);
|
||||
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();
|
||||
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(import_key_input_, sizeof(import_key_input_), "%s", trimmed.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton(TR("clear"), ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
|
||||
memset(import_key_input_, 0, sizeof(import_key_input_));
|
||||
}
|
||||
|
||||
// Trimmed view for validation (importPrivateKey trims again before the RPC). The indicator and the
|
||||
// Import-button guard below both derive from the SAME shared classifier, so they can't disagree.
|
||||
if (m::TactileButton(TR("clear"))) sodium_memzero(import_key_input_, sizeof(import_key_input_));
|
||||
|
||||
// Trimmed view + classification. The indicator and the Import guard share isRecognizedImportKey.
|
||||
std::string keyTrim(import_key_input_);
|
||||
while (!keyTrim.empty() && (keyTrim.front()==' '||keyTrim.front()=='\t'||keyTrim.front()=='\n'||keyTrim.front()=='\r')) keyTrim.erase(keyTrim.begin());
|
||||
while (!keyTrim.empty() && (keyTrim.back()==' '||keyTrim.back()=='\t'||keyTrim.back()=='\n'||keyTrim.back()=='\r')) keyTrim.pop_back();
|
||||
bool keyRecognized = services::WalletSecurityController::isRecognizedPrivateKey(keyTrim);
|
||||
bool keyIsZ = services::WalletSecurityController::classifyPrivateKey(keyTrim)
|
||||
== services::WalletSecurityController::KeyKind::Shielded;
|
||||
bool recognized = services::WalletSecurityController::isRecognizedImportKey(keyTrim);
|
||||
bool viewing = services::WalletSecurityController::isViewingKey(keyTrim);
|
||||
bool shielded = services::WalletSecurityController::classifyPrivateKey(keyTrim)
|
||||
== services::WalletSecurityController::KeyKind::Shielded;
|
||||
|
||||
// Key validation indicator
|
||||
if (!keyTrim.empty()) {
|
||||
if (keyRecognized) {
|
||||
ImGui::PushFont(ui::material::Type().iconSmall());
|
||||
ImGui::TextColored(ui::material::SuccessVec4(), ICON_MD_CHECK_CIRCLE);
|
||||
if (recognized) {
|
||||
const char* label = viewing ? TR("import_key_type_zview")
|
||||
: shielded ? TR("import_key_type_zspend")
|
||||
: TR("import_key_type_tkey");
|
||||
ImGui::PushFont(m::Type().iconSmall());
|
||||
ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 4.0f);
|
||||
ImGui::TextColored(ui::material::SuccessVec4(), "%s",
|
||||
keyIsZ ? "Shielded spending key" : "Transparent private key");
|
||||
ImGui::SameLine(0, 4.0f * dp);
|
||||
ImGui::TextColored(m::SuccessVec4(), "%s", label);
|
||||
} else {
|
||||
ImGui::PushFont(ui::material::Type().iconSmall());
|
||||
ImGui::PushFont(m::Type().iconSmall());
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), ICON_MD_HELP);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 4.0f);
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "Unrecognized key format");
|
||||
ImGui::SameLine(0, 4.0f * dp);
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "%s", TR("import_key_type_unknown"));
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
if (!import_status_.empty()) {
|
||||
if (import_success_) {
|
||||
ImGui::TextColored(ui::material::SuccessVec4(), "%s", import_status_.c_str());
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", import_status_.c_str());
|
||||
|
||||
// Progress / result / error.
|
||||
if (import_in_progress_) {
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::Primary()), "%s%s",
|
||||
TR("import_key_rescanning"), m::LoadingDots());
|
||||
} else if (import_success_) {
|
||||
ImGui::PushFont(m::Type().iconSmall());
|
||||
ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 4.0f * dp);
|
||||
ImGui::TextColored(m::SuccessVec4(), "%s", TR("import_key_done"));
|
||||
if (!import_result_address_.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceMedium()), "%s", TR("import_key_address"));
|
||||
ui::widgets::AddressCopyField("##importedaddr", import_result_address_);
|
||||
}
|
||||
} else if (!import_status_.empty()) {
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", import_status_.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::BeginDisabled(!keyRecognized); // only a recognized Z/T key can be imported
|
||||
if (ui::material::StyledButton("Import", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
|
||||
importPrivateKey(std::string(import_key_input_), [this](bool success, const std::string& msg) {
|
||||
import_success_ = success;
|
||||
import_status_ = msg;
|
||||
if (success) {
|
||||
memset(import_key_input_, 0, sizeof(import_key_input_));
|
||||
ImGui::Spacing();
|
||||
|
||||
float btnW = 130.0f * dp;
|
||||
bool canImport = recognized && state_.connected && !import_in_progress_;
|
||||
ImGui::BeginDisabled(!canImport);
|
||||
if (m::TactileButton(TR("import_key_import"), ImVec2(btnW, 0))) {
|
||||
import_in_progress_ = true;
|
||||
import_success_ = false;
|
||||
import_status_.clear();
|
||||
import_result_address_.clear();
|
||||
importPrivateKey(std::string(import_key_input_),
|
||||
[this](bool ok, const std::string& err, const std::string& addr) {
|
||||
import_in_progress_ = false;
|
||||
import_success_ = ok;
|
||||
if (ok) {
|
||||
import_result_address_ = addr;
|
||||
import_status_.clear();
|
||||
sodium_memzero(import_key_input_, sizeof(import_key_input_)); // wipe on success
|
||||
} else {
|
||||
import_status_ = err;
|
||||
}
|
||||
});
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
|
||||
show_import_key_ = false;
|
||||
if (m::TactileButton(TR("close"), ImVec2(btnW, 0))) show_import_key_ = false;
|
||||
|
||||
// Wipe the key the moment the dialog closes (Close button OR outside-click, which clears
|
||||
// show_import_key_ mid-frame). in_progress is left to the callback to clear.
|
||||
if (!show_import_key_) {
|
||||
sodium_memzero(import_key_input_, sizeof(import_key_input_));
|
||||
import_status_.clear();
|
||||
memset(import_key_input_, 0, sizeof(import_key_input_));
|
||||
import_result_address_.clear();
|
||||
import_success_ = false;
|
||||
import_key_reveal_ = false;
|
||||
}
|
||||
|
||||
ui::material::EndOverlayDialog();
|
||||
|
||||
m::EndOverlayDialog();
|
||||
}
|
||||
|
||||
void App::renderExportKeyDialog()
|
||||
|
||||
Reference in New Issue
Block a user