feat(wallet): migrate-to-seed, Phase 2 — sweep + adopt (fund-safety gated)

Second half of "migrate a legacy wallet to a seed wallet": sweep all funds
into the new seed wallet, then adopt it as the primary wallet.

- Sweep (beginSweepToSeedWallet): z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"]
  -> the new wallet's z-address, at the default fee, limits 0,0. The opid is
  tracked via the existing send-callback pipeline; the txid is persisted.
- Confirming gate (fund-safety): after the sweep, pollSweepStatus() polls the
  tx confirmations + legacy balance. "Make this my wallet" only unlocks once
  the sweep is MINED (>= 1 conf) AND the old wallet is empty — so wallet.dat is
  never swapped while the funds could still bounce back or a remainder is left.
  A too-big-for-one-tx remainder offers a "Sweep remaining" re-sweep.
- Adopt (beginAdoptSeedWallet, background): stop daemon -> move legacy
  wallet.dat to a timestamped .bak (never deleted; restored on failure) ->
  install the new wallet -> restart with -rescan. Exception-safe;
  daemon_restarting_ gates tryConnect + is always cleared; skips the restart
  while shutting down; beginShutdown joins the task first.
- Resume: a pending migration reopens at Sweep, or at Confirming if the sweep
  txid is already recorded (re-derived from the chain) — never re-sweeps blind.
- Secret hygiene: the seed is wiped leaving ShowSeed, on any modal dismiss
  (detected after BeginOverlayDialog), and in App::~App() (same fix applied to
  the seed-backup dialog).

Built both variants; tests + hygiene pass. Two rounds of parallel adversarial
review (12 findings fixed, then a clean re-verify) gate this fund-moving code.
The migration modal uses English literals (as renderBackupDialog does); it gets
translated once the flow is finalized. Live sweep test against a funded wallet
is the next step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 15:04:40 -05:00
parent 7ee20bb497
commit e66f2b6c8c
5 changed files with 427 additions and 19 deletions

View File

@@ -353,7 +353,7 @@ public:
void showExportKeyDialog() { show_export_key_ = true; }
void showBackupDialog() { show_backup_ = true; }
void showSeedBackupDialog() { show_seed_backup_ = true; }
void showSeedMigrationDialog() { show_seed_migration_ = true; seed_migration_step_ = SeedMigrationStep::Intro; }
void showSeedMigrationDialog(); // opens the migration modal (resumes a pending one at Sweep)
void showAboutDialog() { show_about_ = true; }
// Legacy tab compat — maps int to NavPage
@@ -616,6 +616,11 @@ private:
// Seed-wallet migration (Phase 1: create a new mnemonic wallet in isolation, no funds moved).
void beginCreateSeedWallet(); // starts the isolated create on a background thread
void pumpSeedMigration(); // main thread: pick up background progress/result each frame
// Phase 2: sweep all legacy funds into the new wallet, then adopt it as the primary wallet.
void refreshSeedMigrationBalance(); // query the legacy total (shown on the Sweep step)
void beginSweepToSeedWallet(); // z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"] -> dest
void pollSweepStatus(); // Confirming step: poll sweep confirmations + legacy balance
void beginAdoptSeedWallet(); // stop daemon -> swap wallet.dat -> restart with -rescan
void provisionChatIdentityFromSecret(std::string secret);
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr for chat
std::string generateChatLocalId(const char* prefix, int numBytes) const; // unique echo id / cid
@@ -684,22 +689,32 @@ private:
bool seed_backup_no_mnemonic_ = false;
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
// --- Seed-wallet migration (Phase 1: isolated seed-wallet creation) ---
enum class SeedMigrationStep { Intro, Working, ShowSeed, Error };
// --- Seed-wallet migration (Phase 1: create; Phase 2: sweep + adopt) ---
enum class SeedMigrationStep { Intro, Working, ShowSeed, Sweep, Sweeping, Confirming, Adopting, Done, Error };
bool show_seed_migration_ = false;
SeedMigrationStep seed_migration_step_ = SeedMigrationStep::Intro;
bool seed_migration_in_flight_ = false; // main-thread guard while the bg task runs
bool seed_migration_in_flight_ = false; // main-thread guard while the bg create task runs
std::string seed_migration_seed_; // SECRET — the revealed phrase, wiped on close
std::string seed_migration_dest_; // new shielded z-address (Phase 2 sweep target)
std::string seed_migration_temp_dir_; // temp datadir holding the new wallet (kept)
std::string seed_migration_temp_dir_; // temp datadir root holding the new wallet (kept)
bool seed_migration_backed_up_ = false; // "I've written it down" confirmation
std::string seed_migration_status_; // main-thread display (progress / error text)
double seed_migration_balance_ = 0.0; // legacy total to sweep (shown on the Sweep step)
std::string seed_migration_sweep_txid_; // the z_mergetoaddress sweep transaction id
int seed_migration_sweep_confs_ = 0; // confirmations of the sweep tx (adopt gate: >= 1)
double seed_migration_legacy_remaining_ = -1.0; // legacy balance after sweep (-1 = unknown)
float seed_migration_poll_timer_ = 0.0f; // throttles the Confirming-step poll
bool seed_migration_confirm_in_flight_ = false; // guards the confirm poll
// Cross-thread handoff from the background create task (guarded by seed_migration_mutex_).
std::mutex seed_migration_mutex_;
std::string seed_migration_progress_; // latest progress line
bool seed_migration_done_ = false; // result ready to consume
bool seed_migration_done_ = false; // create result ready to consume
bool seed_migration_ok_ = false;
std::string seed_migration_r_seed_, seed_migration_r_dest_, seed_migration_r_tmp_, seed_migration_r_err_;
// Cross-thread handoff from the background adopt task (guarded by seed_migration_mutex_).
bool seed_migration_adopt_done_ = false;
bool seed_migration_adopt_ok_ = false;
std::string seed_migration_adopt_err_;
// Embedded daemon state
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());