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:
2026-07-13 12:58:36 -05:00
parent 8293f02e36
commit 46f3001360
15 changed files with 274 additions and 88 deletions

View File

@@ -2921,10 +2921,11 @@ void App::exportAllKeys(std::function<void(const std::string&, int, int)> callba
}
}
void App::importPrivateKey(const std::string& rawKey, std::function<void(bool, const std::string&)> callback)
void App::importPrivateKey(const std::string& rawKey,
std::function<void(bool, const std::string&, const std::string&)> callback)
{
if (!state_.connected || !rpc_ || !worker_) {
if (callback) callback(false, "Not connected");
if (callback) callback(false, "Not connected", "");
return;
}
@@ -2934,23 +2935,33 @@ void App::importPrivateKey(const std::string& rawKey, std::function<void(bool, c
while (!key.empty() && (key.front()==' '||key.front()=='\t'||key.front()=='\n'||key.front()=='\r')) key.erase(key.begin());
while (!key.empty() && (key.back()==' '||key.back()=='\t'||key.back()=='\n'||key.back()=='\r')) key.pop_back();
// Reject anything that doesn't look like a Z/T private key before handing it to the daemon (the
// dialog's indicator and this guard now share isRecognizedPrivateKey, so they can't disagree).
if (!services::WalletSecurityController::isRecognizedPrivateKey(key)) {
if (callback) callback(false, "Unrecognized private-key format.");
// Reject anything that isn't a recognized Z/T private key or shielded viewing key before handing
// it to the daemon (the dialog's indicator and this guard share isRecognizedImportKey).
if (!services::WalletSecurityController::isRecognizedImportKey(key)) {
if (callback) callback(false, "Unrecognized key format.", "");
return;
}
const bool viewing = services::WalletSecurityController::isViewingKey(key);
const bool shielded = services::WalletSecurityController::classifyPrivateKey(key)
== services::WalletSecurityController::KeyKind::Shielded;
// Run on the worker thread — import requests a full rescan (rescan=true), so the
// synchronous curl call can take many seconds; never block the UI thread on it.
worker_->post([this, key, shielded, callback]() -> rpc::RPCWorker::MainCb {
std::string err;
worker_->post([this, key, viewing, shielded, callback]() -> rpc::RPCWorker::MainCb {
std::string err, addr;
try {
rpc::RPCClient::TraceScope trace("Settings / Import private key");
if (shielded) rpc_->call("z_importkey", {key, "yes"}); // rescan
else rpc_->call("importprivkey", {key, "", true}); // label "", rescan
rpc::RPCClient::TraceScope trace("Settings / Import key");
std::string method;
nlohmann::json params;
if (viewing) { method = "z_importviewingkey"; params = {key, "yes"}; } // watch-only
else if (shielded) { method = "z_importkey"; params = {key, "yes"}; }
else { method = "importprivkey"; params = {key, "", true}; }
nlohmann::json r = rpc_->call(method, params);
// z_import* return {type,address}; importprivkey returns the t-address string.
if (r.is_object() && r.contains("address") && r["address"].is_string())
addr = r["address"].get<std::string>();
else if (r.is_string())
addr = r.get<std::string>();
} catch (const std::exception& e) {
err = e.what();
} catch (...) {
@@ -2958,16 +2969,14 @@ void App::importPrivateKey(const std::string& rawKey, std::function<void(bool, c
// below would never run, leaving a stuck "Importing…" spinner.
err = "Import failed (unknown error)";
}
return [this, shielded, err, callback]() {
return [this, err, addr, callback]() {
if (!err.empty()) {
if (callback) callback(false, err);
if (callback) callback(false, err, "");
return;
}
invalidateAddressValidationCache();
refreshAddresses();
if (callback) callback(true, services::WalletSecurityController::importSuccessMessage(
shielded ? services::WalletSecurityController::KeyKind::Shielded
: services::WalletSecurityController::KeyKind::Transparent));
if (callback) callback(true, "", addr);
};
});
}