fix(sweep): keep the UI sweep away from real wallet data

The full UI sweep (dev builds only) ran its demo surfaces against the
real profile. Its teardown deleted wallet-{cold,trading,mining,donations,
2023,payroll}.dat from the real datadir, it created zero-filled wallet
files and picker-demo/extra-wallets folders, its demo wallet-index upsert
re-minted the active wallet's scopeId (hiding "w:"-scoped contacts), the
Contacts tab could re-attach legacy-scoped contacts to such a throwaway
id in addressbook.json, and it persisted the demo active wallet file and
Mining pool worker.

- Preflight: a full sweep is refused (toast + log line) while a real
  workflow is in flight: a pending seed_migration_* setting or open
  migrate dialog, a wallet switch, an encryption/import workflow, an
  encrypt/PIN/lock operation, the seed-backup dialog, or a bootstrap.
  The DRAGONX_FULL_SWEEP hook then exits as for an empty catalog.
- Persistence latches: AddressBook (snapshot on the first sweepBegin,
  save/load latched while a demo-contacts surface is shown), WalletIndex
  and Settings (active wallet file, contacts view mode, pool URL/worker)
  for the whole sweep. Settings::sweepEnd() is the last statement of
  clearDemoWalletData and flushes once only if a save was suppressed; a
  quit mid-sweep writes nothing to the profile (the 0700 sandbox, with
  zero-filled demo files only, may stay in the temp dir).
- Scope ids: during a full sweep activeWalletScopeId() returns the
  persisted id or "" and never mints one, so the Contacts tab cannot
  move legacy-scoped contacts to a throwaway id. The only address-book
  write a sweep can still cause is the one-time legacy re-attach to the
  wallet's persisted id, which opening Contacts does anyway.
- Sandbox: demo wallet files and folders go to a fresh random 0700 dir
  under the temp dir (exclusive create, a pre-existing path or symlink
  is refused), removed at sweep end. The teardown that deleted those six
  wallet files is gone.
- Cleanup: the migrate-to-seed state, wizard phase, demo chat
  conversations and a fabricated demo chat identity are reset at sweep
  end; the seed-backup surface always shows the demo phrase and a real
  z_exportmnemonic result is dropped while the sweep runs.
- i18n: one new key (screenshot_sweep_refused) in i18n.cpp and the 8
  language files.

Tests: the address book snapshots only on the first sweepBegin, keeps
addressbook.json byte-identical while latched and restores the real
book; the wallet index keeps wallets.json and the real scope id, also
with the background writer attached; Settings restores the latched
values and flushes once only when a save was suppressed; the sandbox
helper refuses a pre-existing path or symlink and removes only a
prefixed dir, never a symlink's target.

Release note: v2.0.x UI sweeps could delete wallet-{cold,trading,mining,
donations,2023,payroll}.dat from the data directory and re-scope
contacts; this cannot be undone automatically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 13:22:00 -05:00
co-authored by Claude Opus 5.5
parent 3ac46abe1a
commit 62a5577491
22 changed files with 625 additions and 40 deletions
+37
View File
@@ -586,7 +586,44 @@ public:
bool needsUpgradeSave() const { return needs_upgrade_save_; }
void clearUpgradeSave() { needs_upgrade_save_ = false; }
// UI-sweep ONLY. sweepBegin() snapshots the values the full sweep's demo surfaces change: the active
// wallet file, the contacts view mode, and the pool URL/worker (the Mining tab defaults an empty
// worker to the first DEMO z-address). Until sweepEnd(), save() returns true without writing, so
// neither those nor the demo portfolio can reach disk, even through the shutdown save if the app
// quits mid-sweep. sweepEnd() restores the snapshot and, only if a save was suppressed, saves once
// so a real change made meanwhile still lands. The caller restores everything else it changed
// (the portfolio) BEFORE sweepEnd(). Both are idempotent.
void sweepBegin() {
if (sweep_active_) return;
sweep_active_wallet_file_ = active_wallet_file_;
sweep_contacts_view_mode_ = contacts_view_mode_;
sweep_pool_url_ = pool_url_;
sweep_pool_worker_ = pool_worker_;
sweep_suppressed_save_ = false;
sweep_active_ = true;
}
void sweepEnd() {
if (!sweep_active_) return;
sweep_active_ = false;
active_wallet_file_ = sweep_active_wallet_file_;
contacts_view_mode_ = sweep_contacts_view_mode_;
pool_url_ = sweep_pool_url_;
pool_worker_ = sweep_pool_worker_;
if (sweep_suppressed_save_) {
sweep_suppressed_save_ = false;
save();
}
}
bool sweepActive() const { return sweep_active_; }
private:
// UI-sweep latch (see sweepBegin()).
bool sweep_active_ = false;
bool sweep_suppressed_save_ = false;
std::string sweep_active_wallet_file_;
int sweep_contacts_view_mode_ = 0;
std::string sweep_pool_url_, sweep_pool_worker_;
std::string settings_path_;
util::CoalescingWriter* writer_ = nullptr; // null -> synchronous saves (tests, tools, early startup)
// Armed by the fund/security-critical setters (seed-migration stage, encryption_pending, pin_enabled,