fix(lite): lock/unlock parity — shared attempt-lockout, immediate unlock state, no dead PIN, surfaced encrypt failure

Lite-parity audit, FIX C (gating/validation/presentation only — no encrypt/unlock/send backend call changed):
- Lite unlock went straight to the backend with no lockout. New App::unlockLiteWallet() runs the SAME
  escalating, persisted attempt-lockout as the full node (restoreLockoutState/applyUnlockFailure), wired into
  the Settings unlock, the send-time prompt and the lock screen. A wrong-passphrase failure counts; an
  already-unlocked/not-encrypted result does not.
- state_.locked is now cleared immediately on a successful unlock (the ~2s status poll lagged, so the UI
  stayed "locked" after a correct passphrase).
- PIN quick-unlock controls are hidden in lite Settings — lite has no PIN vault, so they were dead.
- A create/restore whose passphrase encryption FAILS now raises a one-shot visible error
  (takeEncryptionFailureNotice -> lite_encrypt_failed_unencrypted) instead of only a log line; the wallet is
  left open but unencrypted. Fixed the flag on BOTH the lifecycle and the direct create/restore encrypt paths.
- A background sync save is skipped while an encrypted wallet is unlocked (the backend save re-locks it),
  resuming once locked.

Compile- and unit-verified (testLiteWalletControllerLockUnlockParity covers the save-deferral + encrypt-failure
notice, run twice for the flaky guard); the lock-screen lockout/PIN-hiding UX itself is runtime/GUI-only and
not exercised here — worth a manual lite GUI pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 15:33:28 -05:00
co-authored by Claude Opus 4.8
parent 0cb1df3bfe
commit 16a1ca02e8
10 changed files with 199 additions and 10 deletions
+3
View File
@@ -52,6 +52,7 @@ inline std::atomic<bool> g_liteFakeBlockBalance{false}; // "balance" blocks (a
inline std::atomic<bool> g_liteFakeBalanceError{false}; // "balance" returns the backend's plain-text sync error
inline std::atomic<long> g_liteFakeBalanceEntered{0}; // calls that reached "balance"
inline std::atomic<bool> g_liteFakeBlockEncrypt{false}; // "encrypt" blocks (before changing state)
inline std::atomic<bool> g_liteFakeEncryptFails{false}; // "encrypt" returns an error and changes no state
inline std::atomic<bool> g_liteFakeBlockImport{false}; // "import"/"timport" block
inline std::atomic<bool> g_liteFakeImportFails{false}; // "import"/"timport" reject the key (real do_import_key shape)
inline std::atomic<bool> g_liteFakeImportEncrypted{false}; // "import"/"timport" refuse: the wallet is encrypted
@@ -261,6 +262,8 @@ inline char* liteFakeExecute(const char* command, const char* args)
// unlock/lock toggle locked; decrypt clears it; encryptionstatus reports {encrypted,locked}.
if (std::strcmp(c, "encrypt") == 0) {
liteFakeWaitWhile(g_liteFakeBlockEncrypt);
if (g_liteFakeEncryptFails.load())
return liteFakeDup("{\"result\":\"error\",\"error\":\"encrypt failed\"}");
g_liteFakeEncrypted = true; g_liteFakeLocked = true;
return liteFakeDup("{\"result\":\"success\"}");
}