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()
|
||||
|
||||
@@ -300,7 +300,10 @@ public:
|
||||
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
|
||||
// callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export.
|
||||
void exportAllKeys(std::function<void(const std::string&, int, int)> callback);
|
||||
void importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback);
|
||||
// callback(success, errorOrEmpty, importedAddress). address is "" on failure or when the RPC
|
||||
// returns none; the import routes to z_importviewingkey / z_importkey / importprivkey by key type.
|
||||
void importPrivateKey(const std::string& key,
|
||||
std::function<void(bool, const std::string&, const std::string&)> callback);
|
||||
|
||||
// Wallet backup
|
||||
void backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback);
|
||||
@@ -802,6 +805,9 @@ private:
|
||||
std::string export_address_;
|
||||
std::string import_status_;
|
||||
bool import_success_ = false;
|
||||
bool import_key_reveal_ = false; // show the key in plaintext (default masked)
|
||||
bool import_in_progress_ = false; // an import + rescan is running (disable/spinner)
|
||||
std::string import_result_address_; // address imported on success (shown as a copy field)
|
||||
std::string backup_status_;
|
||||
bool backup_success_ = false;
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -101,10 +101,18 @@ WalletSecurityController::KeyKind WalletSecurityController::classifyPrivateKey(c
|
||||
// (The old `key[0]=='s'` test misrouted an uppercase "SK..." shielded key to the transparent RPC.)
|
||||
if (key.rfind("secret-extended-key-", 0) == 0) return KeyKind::Shielded;
|
||||
if (key.size() >= 2 && key[0] == 'S' && key[1] == 'K') return KeyKind::Shielded;
|
||||
if (!key.empty() && key[0] == 's') return KeyKind::Shielded;
|
||||
// A "z"-prefixed key is a shielded viewing key (zxview…); "s"-prefixed is a legacy Sprout key.
|
||||
if (!key.empty() && (key[0] == 's' || key[0] == 'z')) return KeyKind::Shielded;
|
||||
return KeyKind::Transparent;
|
||||
}
|
||||
|
||||
bool WalletSecurityController::isViewingKey(const std::string& key)
|
||||
{
|
||||
// Sapling extended full viewing key (mainnet HRP "zxviews"; "zxview" also matches the prefix the
|
||||
// lite backend recognizes). Watch-only: reveals the address's funds but cannot spend them.
|
||||
return key.rfind("zxview", 0) == 0;
|
||||
}
|
||||
|
||||
bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key)
|
||||
{
|
||||
if (key.rfind("secret-extended-key-", 0) == 0) return true; // Sapling z spending key
|
||||
@@ -115,6 +123,11 @@ bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WalletSecurityController::isRecognizedImportKey(const std::string& key)
|
||||
{
|
||||
return isRecognizedPrivateKey(key) || isViewingKey(key);
|
||||
}
|
||||
|
||||
const char* WalletSecurityController::importSuccessMessage(KeyKind kind)
|
||||
{
|
||||
return kind == KeyKind::Shielded
|
||||
|
||||
@@ -74,9 +74,13 @@ public:
|
||||
std::size_t minLength = 4);
|
||||
static KeyKind classifyAddress(const std::string& address);
|
||||
static KeyKind classifyPrivateKey(const std::string& key);
|
||||
// True if `key` is a shielded viewing key (extended full viewing key, "zxview…" — watch-only).
|
||||
static bool isViewingKey(const std::string& key);
|
||||
// True if `key` looks like a recognized Z (Sapling/Sprout spending) or T (WIF) private key.
|
||||
// Single source of truth for the import dialog's indicator AND its submit guard.
|
||||
static bool isRecognizedPrivateKey(const std::string& key);
|
||||
// As above, but also accepts a recognized shielded viewing key — the import dialog auto-detects
|
||||
// both, so this is the single source of truth for its indicator AND its submit guard.
|
||||
static bool isRecognizedImportKey(const std::string& key);
|
||||
static const char* importSuccessMessage(KeyKind kind);
|
||||
static std::string decryptExportFileName(std::uint64_t timestampSeconds);
|
||||
static void secureClear(std::string& value);
|
||||
|
||||
@@ -1346,6 +1346,18 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["import_key_tooltip"] = "Enter one or more private keys, one per line.\nSupports both z-address and t-address keys.\nLines starting with # are treated as comments.";
|
||||
strings_["import_key_warning"] = "Warning: Never share your private keys! Importing keys from untrusted sources can compromise your wallet.";
|
||||
strings_["import_key_z_format"] = "Z-address spending keys (secret-extended-key-...)";
|
||||
strings_["import_key_warn"] = "Only import a key you own \xE2\x80\x94 it grants access to its funds.";
|
||||
strings_["import_key_need_node"] = "Connect a running node to import a key.";
|
||||
strings_["import_key_field"] = "Key";
|
||||
strings_["import_key_reveal_tip"] = "Show/hide the key";
|
||||
strings_["import_key_type_tkey"] = "Transparent private key";
|
||||
strings_["import_key_type_zspend"] = "Shielded spending key";
|
||||
strings_["import_key_type_zview"] = "Shielded viewing key (watch-only)";
|
||||
strings_["import_key_type_unknown"] = "Unrecognized key format";
|
||||
strings_["import_key_rescanning"] = "Importing & rescanning \xE2\x80\x94 this can take several minutes";
|
||||
strings_["import_key_done"] = "Imported. Wallet is rescanning.";
|
||||
strings_["import_key_address"] = "Address:";
|
||||
strings_["import_key_import"] = "Import";
|
||||
|
||||
// --- Key Export Dialog ---
|
||||
strings_["key_export_fetching"] = "Fetching key from wallet...";
|
||||
|
||||
Reference in New Issue
Block a user