From a3b4f534d5daf1f41c246d758dc8a7dfef8e63dc Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 19 Oct 2026 22:43:49 -0500 Subject: [PATCH] feat(recovery): import keys from a legacy wallet backup file (z_importwallet) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app.cpp | 85 +++++++++++++++++++++++++++++++++- src/app.h | 10 ++++ src/app_network.cpp | 42 +++++++++++++++++ src/ui/pages/settings_page.cpp | 3 ++ src/util/i18n.cpp | 10 ++++ 5 files changed, 149 insertions(+), 1 deletion(-) diff --git a/src/app.cpp b/src/app.cpp index f4f704a..c1eb5a9 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -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; diff --git a/src/app.h b/src/app.h index fa04a4a..998e57e 100644 --- a/src/app.h +++ b/src/app.h @@ -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 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) diff --git a/src/app_network.cpp b/src/app_network.cpp index c013dd2..838968e 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -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 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 diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index f393893..7e910dc 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -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; diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index bf8d9a6..884ad28 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -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";