Skip to content

fix(accounts): serialize account-mutating auth actions - #1066

Open
aristidesstaffieri wants to merge 1 commit into
feature/add-wallet-chooserfrom
fix/serialize-account-mutations
Open

aristidesstaffieri wants to merge 1 commit into
feature/add-wallet-chooserfrom
fix/serialize-account-mutations

Conversation

@aristidesstaffieri

@aristidesstaffieri aristidesstaffieri commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes stellar/freighter-private#112

What

Ports stellar/freighter#3086 to mobile. The auth store actions that change accounts, sources or the session now run one at a time behind a promise-chain lock (helpers/accountMutationLock.ts). That covers sign-up, phrase import, sign-in, create account, add recovery phrase, secret key import, rename, account switch, and the storage-clearing part of a wipe. Each action holds the lock only around its module function in ducks/auth.ts, and the reads that follow (fetchActiveAccount, getAllAccounts) run after it releases. The lock is not reentrant, so it is never taken inside a holder.

Mobile has no background message listener, so the lock sits at the store action instead. The plain lock path of logout and softLock stay off the lock, so locking the app never waits on a Scrypt import. The wipe waits only for its storage clears. The WalletConnect and WebView cleanup before them makes network calls and holds no account state, so it runs outside.

Suggested reading order: helpers/accountMutationLock.ts, then the store actions at the bottom of ducks/auth.ts, then cachePrivateKey and withFundedLookupTimeout.

  • helpers/accountMutationLock.ts (new): withAccountMutationLock runs its callback once every earlier holder has settled. A rejection releases the lock and still reaches the caller.
  • signUp, importWallet, signIn, createAccount, addRecoveryPhraseSource, importSecretKey, renameAccount, selectAccount: each wraps its module call in the lock. signIn also clears the persisted LOCKED marker inside it.
  • logout(true): the storage clears, auto-lock reset and biometrics clear run under the lock, so an add, import or switch in flight finishes first and never writes its account back into the wiped wallet.
  • verifyAndCreateExistingAccountsOnNetwork: each Horizon lookup during a phrase import resolves to unfunded after 5s, so a stalled request can't hold the lock and block a wipe. loadAccount takes no abort signal, so only the wait ends.
  • cachePrivateKey (new): when getActiveAccount lazily loads a missing private key, it now writes it under the lock onto a fresh read of the temporary store. Before, it wrote back the copy it read first, which could drop a key or phrase that an add saved in between.
  • selectAccount: drops the loaded account if a soft-lock landed during the switch, as signIn already does, so the private key never goes back into a locked store.

Not ported from #3086: the subscription removal (mobile never had subscribeAccount), the active-key mismatch exemptions and sign-out refusal handling (one surface, and logout never refuses), and the auto-lock alarm waiting on the lock (softLock stays immediate).

Why

Each of these actions reads the account list, derivation indices, source records and encrypted temporary store, then writes them back whole. Two of them overlapping (a double tap on Add account, a load caching a key while an add finishes, a wipe while an import is still storing) let the later write drop the earlier one's account, name, index or phrase, gave two adds on one phrase the same index, or left an account in a wallet that had just been wiped.

Known limitations

Screenshots / video

N/A, no UI change.

Test plan

  • accountMutationLock.test.ts covers a second holder waiting for the first, and a rejected holder releasing the lock and passing its error on.
  • auth.test.ts covers two overlapping adds on one phrase taking indices 1 and 2, a wipe during an add leaving no account list or indices behind, a soft-lock during a switch leaving no account in the store, the lazy key cache keeping a key another add wrote since the first read, and a stalled funded lookup cut off at 5s so the import finishes.
  • Each new auth.test.ts case fails with its fix removed.
  • yarn test (5527 passed, 14 skipped) and tsc --noEmit pass.
  • Manual check on iOS: two quick adds, switch and rename, and a wipe during an add.
  • Manual check on Android.
  • CI green.

Checklist

PR structure

  • This PR does not mix refactoring changes with feature changes (break it down into smaller PRs if not).
  • This PR has reasonably narrow scope (break it down into smaller PRs if not).
  • This PR includes relevant before and after screenshots/videos highlighting these changes.
  • I took the time to review my own PR.

Testing

  • These changes have been tested and confirmed to work as intended on Android.
  • These changes have been tested and confirmed to work as intended on iOS.
  • These changes have been tested and confirmed to work as intended on small iOS screens.
  • These changes have been tested and confirmed to work as intended on small Android screens.
  • I have tried to break these changes while extensively testing them.
  • This PR adds tests for the new functionality or fixes.

Release

  • This is not a breaking change.
  • This PR updates existing JSDocs when applicable.
  • This PR adds JSDocs to new functionalities.
  • I've checked with the product team if we should add metrics to these changes.
  • I've shared relevant before and after screenshots/videos highlighting these changes with the design team and they've approved the changes.

  Port of stellar/freighter#3086. Sign-up, import, sign-in, add, rename,
  switch and the wipe's storage clear now run one at a time behind a
  promise-chain lock. A lazily loaded private key is written to a fresh
  read of the temporary store under the lock. A switch drops its account
  if a soft-lock lands mid-switch. Funded-account lookups during a
  recovery-phrase import time out after 5s.
@aristidesstaffieri aristidesstaffieri self-assigned this Nov 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-d421891485493c1638d2
Backend: sandbox (aristidesstaffieri). SDF collaborators only — install instructions in the release description.

@github-actions

Copy link
Copy Markdown
Contributor

Android Emulator preview APK is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-d1a771bcd2a4ff9af07d
Backend: sandbox (aristidesstaffieri). SDF collaborators only — install instructions in the release description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant