Harden wallet lifecycle: remove implicit auto-init, enforce explicit identity boundary - #77
Harden wallet lifecycle: remove implicit auto-init, enforce explicit identity boundary#77nulllpc wants to merge 10 commits into
Conversation
- unlock(walletId) now requires an explicit walletId; no more implicit fallback to the store's activeWalletId - Add switchWallet(walletId) as an atomic lock() + unlock() convenience, guaranteeing the previous wallet's state is cleared before the next one loads - Remove setActiveWalletId: an unguarded setter with no internal callers - Stop persisting activeWalletId - identity is caller-owned, so no wallet id should silently survive a session/user change - READY status now requires walletLoadingState.identifier to match activeWalletId, not just isWorkletInitialized, so a mismatched identity reports LOCKED instead of a false READY
- Refactor tests for `useWalletManager` to remove dependency on `useWdkApp` and its associated context wrapper, simplifying the test setup - Remove redundant and filler tests that do not add much value
- `lock` now shares the same operation mutex as unlock/switchWallet/createWallet/restoreWallet instead of running unprotected. - `createWallet` and restoreWallet now throw if a wallet is already ready, matching the guard already enforced by unlock. - Guard `createWallet` and `restoreWallet` with mutex
orchestrator test - useWalletManager: drop dead/padding tests (deleted setActiveWalletId, thin pass-through delegation/error tests); add coverage for switchWallet atomicity, lock/unlock/createWallet/restoreWallet mutex races, and the "must lock before switching identity" guards - useWalletOrchestrator: add identity-mismatch READY-vs-LOCKED test; fix an existing test that silently broke when READY started requiring a matching walletLoadingState identifier
- Set activeWalletId to null in walletStore during rehydration - Adjust tests accordingly
- Unexported, unused by any hook/component - a leftover parallel wallet-switching path from an earlier architecture - Missing the reset-before-switch discipline enforced everywhere else in useWalletManager - Strips its two tests out of raceConditions.test.ts (they only used it as a vehicle to exercise the generic mutex, not because the service itself needed dedicated coverage) - Fixes a stale JSDoc example in operationMutex.ts that referenced it
|
It started as cleanup of a deprecated prop and turned up a real identity-boundary bug along the way. So I did a deeper refactoring and the result was pretty satisfying. Here's a detailed report of what I found: Motivation
Approach
Net result: all wallet identity mutation lives in one place ( Breaking changes
|
- Improve state orchestration to distinguish between "no wallets exist" and "wallets exist but are locked" - Update documentation and type definitions to clarify that LOCKED status may contain an optional walletId
- README: fix stale useWdkApp/useWalletManager snippets (isReady, loadWallet, hasWallet() never existed as shown), add a Wallet Lifecycle section covering caller-owned identity, the lock-before-switching rule, and status meanings - docs/quick-start.md: rewrite the example to use state.status and explicit unlock(userId)/createWallet(userId) - docs/architecture.md: remove the deleted WalletSwitchingService and "consolidated effect" description, replace with the actual mutex-serialized useWalletManager model; drop dead WALLET_STATE_MACHINE.md link; fix useWallet -> useWalletManager - docs/troubleshooting.md: remove reference to the removed retry() method
Breaking Changes
Also Fixed
|
|
Please rebase onto main before merge. This branch is 8 commits behind main (missing PR #78 swidge protocol support, the beta.14 release/version bump, and a dependabot postcss bump). As-is, this diff shows package.json going from 1.0.0-beta.15 → 1.0.0-beta.13, @tetherto/pear-wrk-wdk downgrading from beta.10 → beta.8, and 'swidge' being silently dropped from useProtocol.ts's protocolType union. None of that is intentional — it's just staleness — but merging as-is would revert those changes. |
| await WorkletLifecycleService.ensureWorkletStarted() | ||
| const performUnlock = useCallback( | ||
| async (walletId: string) => { | ||
| if (walletStore.getState().walletLoadingState.type === 'ready') { |
There was a problem hiding this comment.
On unlock(): this only checks whether something is ready (walletLoadingState.type === 'ready'), not whether it's the requested wallet — so unlock('wallet-b') while wallet-a is active silently resolves without switching, leaving wallet-a active. That's confirmed intentional by the new test (should not switch identity when unlock is called directly while a different wallet is ready), but it's inconsistent with createWallet/restoreWallet, which now throw in the identical scenario. A caller has no way to distinguish "already where I wanted to be" from "my request was silently dropped." Could we either make unlock throw the same way when a different wallet is active, or at minimum only no-op when walletLoadingState.identifier === walletId?
On restoreWallet/createWallet: nice, this correctly closes the same gap unlock used to have — throwing instead of silently overwriting an active session. It'd be good if unlock matched this pattern too (see comment above).
| ``` | ||
|
|
||
| ## Wallet Lifecycle | ||
|
|
There was a problem hiding this comment.
"unlock, createWallet, and restoreWallet all reject if a wallet is already active" — this isn't accurate for unlock. Per the actual implementation (and the test should not switch identity when unlock is called directly while a different wallet is ready), unlock silently no-ops and resolves rather than rejecting. Either the doc needs to say "silently no-ops" instead of "reject," or unlock's behavior should be changed to match (see the useWalletManager.ts comment above) — but the two should agree either way.
Summary
This PR removes the reactive, implicit wallet auto-initialization model and replaces it with an explicit, race-free lifecycle centered entirely in
useWalletManager