From 293b633a243dd5ab08383a6018fb6ee6d6ee7f8d Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 19 Oct 2026 22:28:35 -0500 Subject: [PATCH] feat(recovery): inline unlock in the migrate-to-seed sweep step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of legacy-wallet migration. The sweep (z_shieldcoinbase / z_mergetoaddress) needs an unlocked wallet, so an encrypted wallet left the "Sweep all funds" button silently disabled with no way forward. The Sweep step now, when the wallet is locked, shows a passphrase field + Unlock button that calls unlockWallet(pass, 600); once it unlocks, the (already-present) canSweep gate re-enables the sweep. The passphrase buffer is sodium_memzero'd immediately after submit. No fund-path change: this only unlocks the wallet (the daemon's own walletpassphrase), and the sweep/adopt logic is untouched. Batched-coinbase progress is already handled — the Confirming step shows the live remaining balance and the existing "Sweep remaining" flow ("too many inputs for one transaction") drains it across z_shieldcoinbase's 50-UTXO batches. Runtime/UI change (build + ctest green; not pure-testable). Still to verify on a real degraded+encrypted wallet end-to-end: unlock -> coinbase shield -> confirm -> adopt. Co-Authored-By: Claude Opus 4.8 --- src/app.cpp | 16 ++++++++++++++++ src/util/i18n.cpp | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/app.cpp b/src/app.cpp index 59037f1..f4f704a 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -5081,6 +5081,22 @@ void App::renderSeedMigrationDialog() { const bool canSweep = !state_.isLocked() && seed_migration_balance_ > 0.0; if (!canSweep) seed_migration_confirm_sweep_ = false; // never keep a stale armed state + // The sweep (z_shieldcoinbase / z_mergetoaddress) needs an unlocked wallet, so an encrypted + // wallet silently disabled the button. Offer an inline unlock instead of a dead end. + if (state_.isLocked()) { + static char s_mig_unlock_pass[128] = {0}; + ImGui::TextWrapped("%s", TR("mig_unlock_prompt")); + ImGui::SetNextItemWidth(220 * dp); + const bool submit = ImGui::InputText("##mig_unlock", s_mig_unlock_pass, sizeof(s_mig_unlock_pass), + ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue); + ImGui::SameLine(); + if ((ui::material::TactileButton(TR("mig_unlock_btn"), ImVec2(120 * dp, 0)) || submit) + && s_mig_unlock_pass[0]) { + unlockWallet(s_mig_unlock_pass, 600); + sodium_memzero(s_mig_unlock_pass, sizeof(s_mig_unlock_pass)); // don't retain the secret + } + ImGui::Spacing(); + } if (!seed_migration_confirm_sweep_) { if (!canSweep) ImGui::BeginDisabled(); if (ui::material::TactileButton(TR("mig_sweep_all"), ImVec2(170 * dp, 0))) diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 1f4ceab..bf8d9a6 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -512,6 +512,8 @@ void I18n::loadBuiltinEnglish() strings_["mig_to"] = "To: "; strings_["mig_unlock_first"] = "Unlock your wallet first to spend from it."; strings_["mig_sweep_all"] = "Sweep all funds"; + strings_["mig_unlock_prompt"] = "Your wallet is locked. Enter your passphrase to unlock it, then sweep."; + strings_["mig_unlock_btn"] = "Unlock"; strings_["mig_later"] = "Later"; strings_["mig_sweeping"] = "Sweeping your funds into the new wallet…"; strings_["mig_confirming"] = "Waiting for the sweep to confirm on-chain before switching wallets — this guarantees your funds have actually moved.";