feat(recovery): inline unlock in the migrate-to-seed sweep step

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 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 22:28:35 -05:00
co-authored by Claude Opus 4.8
parent ae4012adb7
commit 293b633a24
2 changed files with 18 additions and 0 deletions
+16
View File
@@ -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)))
+2
View File
@@ -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.";