feat(recovery): import keys from a legacy wallet backup file (z_importwallet)
Phase 3: for a wallet.dat the current daemon can't open, recover its keys from the OLD wallet's export file. Settings → "Import From Backup File…" opens a dialog that takes the path to a z_exportwallet / dumpwallet dump (or SilentDragon's "Export all private keys") and calls z_importwallet — the daemon reads every key and does ONE rescan, far faster and simpler than importing keys one at a time. On success it nudges the user to shield any mined coinbase from the Shield screen. - New App::importWalletFile(path, cb): runs z_importwallet on the long-call lane (no client timeout — the rescan can take a long time), then refreshAddresses + markWalletMutated. Full-node only. - Only the PATH touches GUI memory, never the keys themselves (the daemon reads the file) — less secret exposure than a pasted-key field. - New renderImportWalletFileDialog (typed path + Paste/Clear, progress, result) + a Settings card. Reuses the existing long-call + z_importwallet infra; the careful single-key import dialog is untouched. No send/sweep/encrypt call changed. Build + ctest green; the import/rescan itself is a daemon RPC, not pure-testable, and wasn't exercised end-to-end here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+84
-1
@@ -2582,7 +2582,11 @@ void App::render()
|
||||
if (show_import_key_) {
|
||||
renderImportKeyDialog();
|
||||
}
|
||||
|
||||
|
||||
if (show_import_wallet_file_) {
|
||||
renderImportWalletFileDialog();
|
||||
}
|
||||
|
||||
if (show_export_key_) {
|
||||
renderExportKeyDialog();
|
||||
}
|
||||
@@ -3949,6 +3953,85 @@ static void RenderScanHeightControl(char* buf, size_t bufSize, int tipHeight, bo
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
void App::renderImportWalletFileDialog()
|
||||
{
|
||||
namespace m = ui::material;
|
||||
float dp = ui::Layout::dpiScale();
|
||||
|
||||
m::OverlayDialogSpec ov;
|
||||
ov.title = TR("import_wallet_file_title");
|
||||
ov.p_open = &show_import_wallet_file_;
|
||||
ov.style = m::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 640.0f;
|
||||
ov.idSuffix = "importwalletfile";
|
||||
if (!m::BeginOverlayDialog(ov)) return;
|
||||
|
||||
if (ImGui::IsWindowAppearing()) {
|
||||
import_wallet_file_done_ = false;
|
||||
import_wallet_file_ok_ = false;
|
||||
import_wallet_file_status_.clear();
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape) && !import_wallet_file_busy_) show_import_wallet_file_ = false;
|
||||
|
||||
m::DialogWarningHeader(TR("import_wallet_file_note"));
|
||||
ImGui::Spacing();
|
||||
if (!state_.connected) {
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceMedium()), "%s", TR("import_key_need_node"));
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
ImGui::TextUnformatted(TR("import_wallet_file_path_label"));
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
ImGui::BeginDisabled(import_wallet_file_busy_);
|
||||
ImGui::InputText("##importwalletpath", import_wallet_file_path_, sizeof(import_wallet_file_path_));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (!import_wallet_file_busy_) {
|
||||
if (m::TactileButton(TR("paste"), ImVec2(104 * dp, 0))) {
|
||||
const char* clip = ImGui::GetClipboardText();
|
||||
if (clip) {
|
||||
std::string t = dragonx::util::trimmedClipboardText(clip);
|
||||
snprintf(import_wallet_file_path_, sizeof(import_wallet_file_path_), "%s", t.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (m::TactileButton(TR("clear"), ImVec2(104 * dp, 0))) import_wallet_file_path_[0] = '\0';
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
if (import_wallet_file_busy_) {
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceMedium()), "%s%s",
|
||||
TR("import_wallet_file_importing"), m::LoadingDots());
|
||||
ImGui::Spacing();
|
||||
} else if (import_wallet_file_done_) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(
|
||||
import_wallet_file_ok_ ? m::Success() : m::ReadableError()));
|
||||
ImGui::TextWrapped("%s", import_wallet_file_status_.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
ImGui::BeginDisabled(import_wallet_file_busy_ || !state_.connected || import_wallet_file_path_[0] == '\0');
|
||||
if (m::TactileButton(TR("import_wallet_file_import_btn"), ImVec2(190 * dp, 0))) {
|
||||
import_wallet_file_busy_ = true;
|
||||
import_wallet_file_done_ = false;
|
||||
import_wallet_file_status_.clear();
|
||||
importWalletFile(import_wallet_file_path_, [this](bool ok, const std::string& err) {
|
||||
import_wallet_file_busy_ = false;
|
||||
import_wallet_file_done_ = true;
|
||||
import_wallet_file_ok_ = ok;
|
||||
import_wallet_file_status_ = ok ? std::string(TR("import_wallet_file_ok")) : err;
|
||||
});
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(import_wallet_file_busy_);
|
||||
if (m::TactileButton(TR("close"), ImVec2(110 * dp, 0))) show_import_wallet_file_ = false;
|
||||
ImGui::EndDisabled();
|
||||
|
||||
m::EndOverlayDialog();
|
||||
}
|
||||
|
||||
void App::renderImportKeyDialog()
|
||||
{
|
||||
namespace m = ui::material;
|
||||
|
||||
@@ -650,6 +650,9 @@ public:
|
||||
// Dialog triggers (used by settings page to open modal dialogs)
|
||||
void showImportKeyDialog() { import_view_mode_ = false; show_import_key_ = true; } // spending
|
||||
void showImportViewingKeyDialog() { import_view_mode_ = true; show_import_key_ = true; } // watch-only
|
||||
void showImportWalletFileDialog() { show_import_wallet_file_ = true; } // import a whole legacy key dump
|
||||
// Import every key from an old wallet's z_exportwallet/dumpwallet dump file (z_importwallet, one rescan).
|
||||
void importWalletFile(const std::string& path, std::function<void(bool, const std::string&)> callback);
|
||||
void showExportKeyDialog() { show_export_key_ = true; }
|
||||
void showBackupDialog() { show_backup_ = true; }
|
||||
void showSeedBackupDialog() { show_seed_backup_ = true; }
|
||||
@@ -1419,6 +1422,12 @@ private:
|
||||
bool show_about_ = false;
|
||||
bool show_faq_ = false;
|
||||
bool show_import_key_ = false;
|
||||
bool show_import_wallet_file_ = false; // the "import keys from a backup file" modal is open
|
||||
char import_wallet_file_path_[1024] = {0}; // typed path to the old wallet's key dump (not a secret)
|
||||
bool import_wallet_file_busy_ = false; // a z_importwallet long call is in flight
|
||||
bool import_wallet_file_done_ = false; // terminal result reached (ok/err in import_wallet_file_ok_)
|
||||
bool import_wallet_file_ok_ = false;
|
||||
std::string import_wallet_file_status_; // result / error text shown in the modal
|
||||
bool show_export_key_ = false;
|
||||
bool show_backup_ = false;
|
||||
bool show_seed_backup_ = false;
|
||||
@@ -2060,6 +2069,7 @@ private:
|
||||
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
|
||||
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
|
||||
void renderImportKeyDialog();
|
||||
void renderImportWalletFileDialog(); // typed-path import of a legacy key dump (z_importwallet)
|
||||
void renderExportKeyDialog();
|
||||
void renderBackupDialog();
|
||||
void renderSeedBackupDialog(); // full-node "Back up seed phrase" modal (z_exportmnemonic)
|
||||
|
||||
@@ -5746,6 +5746,48 @@ void App::importPrivateKey(const std::string& rawKey, int startHeight,
|
||||
if (!key.empty()) sodium_memzero(&key[0], key.size()); // scrub the calling-frame copy
|
||||
}
|
||||
|
||||
// Import a whole wallet-key dump file (the output of the OLD wallet's z_exportwallet / dumpwallet, or
|
||||
// SilentDragon's "export all private keys") via z_importwallet: the daemon reads every key from the
|
||||
// file and does ONE rescan — the right tool for recovering a legacy wallet.dat the new client can't open,
|
||||
// and far faster than importing keys one by one. Only the PATH touches our memory, never the keys. Runs on
|
||||
// the long-call lane (no client timeout: the rescan can take a long time). Full-node only.
|
||||
void App::importWalletFile(const std::string& path, std::function<void(bool, const std::string&)> callback)
|
||||
{
|
||||
if (!state_.connected || !rpc_ || !worker_) {
|
||||
if (callback) callback(false, TR("import_key_need_node"));
|
||||
return;
|
||||
}
|
||||
std::string p(path);
|
||||
while (!p.empty() && (p.front()==' '||p.front()=='\t'||p.front()=='\n'||p.front()=='\r')) p.erase(p.begin());
|
||||
while (!p.empty() && (p.back()==' '||p.back()=='\t'||p.back()=='\n'||p.back()=='\r')) p.pop_back();
|
||||
if (p.empty()) { if (callback) callback(false, TR("import_wallet_file_need_path")); return; }
|
||||
{
|
||||
std::string why; const char* trKey = nullptr;
|
||||
if (longWalletOpRefused(&why, &trKey)) { if (callback) callback(false, TR(trKey)); return; }
|
||||
}
|
||||
const bool started = runLongWalletTask([this, p, callback](rpc::RPCClient& c) mutable -> rpc::RPCWorker::MainCb {
|
||||
std::string err;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Import wallet file");
|
||||
c.call("z_importwallet", {p}, 0L); // reads the dump + one rescan; 0 = no client timeout
|
||||
} catch (const std::exception& e) {
|
||||
err = e.what();
|
||||
if (!err.empty() && err.find("Cannot open") != std::string::npos)
|
||||
err = TR("import_wallet_file_not_found"); // the most common cause: wrong/missing path
|
||||
} catch (...) {
|
||||
err = "Import failed (unknown error)";
|
||||
}
|
||||
return [this, err, callback]() {
|
||||
if (!err.empty()) { if (callback) callback(false, err); return; }
|
||||
invalidateAddressValidationCache();
|
||||
refreshAddresses();
|
||||
markWalletMutated(/*forceBalance=*/true); // keys added: balance changes with no tip/mempool signal
|
||||
if (callback) callback(true, "");
|
||||
};
|
||||
});
|
||||
if (!started && callback) callback(false, TR("appx_wallet_op_running"));
|
||||
}
|
||||
|
||||
// Sweep a spending key: import it (a full rescan populates its UTXOs/notes — the stock node has no
|
||||
// address index, so there is no way to enumerate them without importing) then z_sendmany ALL of the
|
||||
// key's funds (balance − fee) to a destination the user already controls. The imported key is left in
|
||||
|
||||
@@ -1490,6 +1490,9 @@ void RenderSettingsPage(App* app) {
|
||||
last = cy + bh; cy += bh + bgp;
|
||||
if (cardBtn(cx, cy, cw, "##imp_vk", TR("settings_import_viewkey"), ICON_MD_VISIBILITY)) app->showImportViewingKeyDialog();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_import_viewkey"));
|
||||
last = cy + bh; cy += bh + bgp;
|
||||
if (cardBtn(cx, cy, cw, "##imp_file", TR("settings_import_wallet_file"), ICON_MD_UPLOAD_FILE)) app->showImportWalletFileDialog();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_import_wallet_file"));
|
||||
last = cy + bh;
|
||||
if (fullNode) {
|
||||
cy += bh + bgp;
|
||||
|
||||
@@ -630,6 +630,15 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["settings_merge_to_address"] = "Merge to Address…";
|
||||
strings_["settings_clear_ztx"] = "Clear Z-Tx History";
|
||||
strings_["settings_import_key"] = "Import Private Key…";
|
||||
strings_["settings_import_wallet_file"] = "Import From Backup File…";
|
||||
strings_["import_wallet_file_title"] = "Import keys from a backup file";
|
||||
strings_["import_wallet_file_note"] = "Imports every private key from an old wallet's export file (its z_exportwallet / dumpwallet output, or SilentDragon's \"Export all private keys\") and rescans once — the fastest way to recover a legacy wallet.dat this client can't open.";
|
||||
strings_["import_wallet_file_path_label"] = "Path to the key dump file:";
|
||||
strings_["import_wallet_file_import_btn"] = "Import and rescan";
|
||||
strings_["import_wallet_file_importing"] = "Importing keys and rescanning — this can take a while";
|
||||
strings_["import_wallet_file_ok"] = "Keys imported. Once the rescan finishes, shield any mined (coinbase) transparent funds from the Shield screen to make them spendable.";
|
||||
strings_["import_wallet_file_need_path"] = "Enter the path to the key dump file.";
|
||||
strings_["import_wallet_file_not_found"] = "Couldn't open that file — check the path is correct and the file is readable.";
|
||||
strings_["settings_import_viewkey"] = "Import Viewing Key…";
|
||||
strings_["settings_export_key"] = "Export Key…";
|
||||
strings_["settings_export_all"] = "Export All…";
|
||||
@@ -1099,6 +1108,7 @@ void I18n::loadBuiltinEnglish()
|
||||
|
||||
// Settings: additional tooltips (keys/data row)
|
||||
strings_["tt_import_key"] = "Import a private key (zkey or tkey) into this wallet";
|
||||
strings_["tt_import_wallet_file"] = "Import all keys from an old wallet's exported dump file (one rescan)";
|
||||
strings_["tt_import_viewkey"] = "Import a shielded viewing key to watch an address (read-only)";
|
||||
strings_["tt_export_key"] = "Export the private key for the selected address";
|
||||
strings_["tt_export_all"] = "Export all private keys to a file";
|
||||
|
||||
Reference in New Issue
Block a user