Skip to content

feat(accounts): import an additional recovery phrase without wiping the wallet - #1067

Open
aristidesstaffieri wants to merge 1 commit into
fix/serialize-account-mutationsfrom
feature/import-additional-recovery-phrase
Open

aristidesstaffieri wants to merge 1 commit into
fix/serialize-account-mutationsfrom
feature/import-additional-recovery-phrase

Conversation

@aristidesstaffieri

@aristidesstaffieri aristidesstaffieri commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes stellar/freighter-private#55

What

Ports stellar/freighter#3084 to mobile. Add wallet now offers Import Recovery Phrase next to Create new wallet and Import Secret Key. The user types or pastes a 12 or 24-word phrase, confirms with the password (or Face ID), and the phrase joins the wallet as a new source with index 0 plus every funded index from 1 to 49 on mainnet. Index 0 becomes active. Existing sources, accounts, names, indices and the pin stay as they are. A phrase the wallet already holds, a phrase with an address already in the wallet, a wrong password or an invalid phrase is refused before anything is written.

The typed phrase is held in the same auth module slot that #1058 uses for a generated phrase, so it never shows up in store state or navigation params, and lock, log out and wipe already clear it. The screen passes only the hold's id to the password step. The import is split in two: the password check, held-phrase check and funded-account lookups run first, outside the account-mutation lock from #1066, and the checks and writes run under it. Up to 49 lookups at 5s each would otherwise hold the lock and keep a wipe waiting. If a lock or log out drops the held phrase while the lookups run, the import stores nothing.

Suggested reading order: prepareRecoveryPhraseImport and storeImportedRecoveryPhrase in ducks/auth.ts, then ImportRecoveryPhraseScreen.

Auth

  • importRecoveryPhrase (ducks/auth.ts, new): runs the prepare half, then the store half under the lock, then reloads the accounts, tracks the add-account event with source: "imported_phrase" and links the new source.
  • prepareRecoveryPhraseImport (new): validates the held phrase, checks the password against the active keystore, refuses a held phrase before any lookup, and finds the funded indices.
  • storeImportedRecoveryPhrase (new): checks the hold, the source and every address again, registers the source, then stores each account in order. If no account was listed, it removes the source and releases its indices. On any failure it puts the previous account back as active.
  • discoverFundedIndices (new): mainnet lookups with at most 10 in flight, each capped at 5s by the timeout from fix(accounts): serialize account-mutating auth actions #1066. A failed or timed-out lookup counts as unfunded.
  • holdPendingRecoveryPhrase (new): holds a typed phrase by id. generatePendingRecoveryPhrase now goes through it.
  • removeSecretSource (services/auth/secretSources.ts, new): drops one source record and moves the pin to the earliest remaining source if it was pinned. Only the import's rollback calls it.
  • isValidImportMnemonic (helpers/recoveryPhrase.ts, new): 12 or 24 words that pass the BIP-39 wordlist and checksum, without deriving the seed.

Screens

  • AddAnotherWallet: adds the Import Recovery Phrase option.
  • ImportRecoveryPhraseScreen (new): reuses onboarding's ImportRecoveryPhraseTemplate, which already has a Paste button on the secure clipboard. The extension left Paste out because extension pages can't read the clipboard. An invalid phrase shows the error under the field. The hold is dropped when the screen goes away.
  • VerifyPasswordScreen: takes an importPhrase target and shows Import on its button.
  • Strings added in en and pt.

Not ported from #3084: the shared password helper (mobile already shares getKeyFromKeyManager) and moving onboarding recovery onto the lookup helper (its lookups already run in parallel).

Why

The only way to import a phrase today is onboarding's Import Wallet, which deletes every account and source before storing the new phrase, so using it to add a second phrase wipes the first wallet. Onboarding keeps that behavior because it is also the forgot-password path.

Known limitations

Screenshots / video

[TODO: Add wallet with the new option, the import screen, the password step]

Test plan

  • auth.test.ts covers an import storing index 0 and a funded index under a new source, with index 0 active and the wallet's pin and phrase kept. It also covers refusals for a held phrase (before any lookup), an address already in the wallet, a wrong password and an invalid phrase.
  • auth.test.ts covers the rollback when no account is stored, a partial failure keeping the source and restoring the active account, two runs of the same import storing one set, a newer hold replacing an older one, and a lock during the lookups storing nothing.
  • The same-import overlap test fails without the lock.
  • recoveryPhrase.test.ts covers 12 and 24 words, stray case and whitespace, a failed checksum, and a valid 15-word phrase being refused. secretSources.test.ts covers removing a record, moving the pin, and an unknown id.
  • Screen tests cover the new chooser option, the import screen (Paste, the error, passing only the hold's id, dropping the hold on leave), and the password step's import target.
  • yarn test (5555 passed, 14 skipped) and tsc --noEmit pass.
  • Manual check on iOS: an unfunded phrase, a phrase with funded accounts, and each refusal.
  • 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.

…he wallet

  Port of stellar/freighter#3084. Add wallet now offers Import Recovery
  Phrase, which adds a typed 12- or 24-word phrase as a new source with
  index 0 and any funded indices up to 49 on mainnet. Index 0 becomes
  active and nothing already in the wallet changes. A held phrase, an
  address already in the wallet or a wrong password is refused before
  anything is written. The funded-account lookups run before the
  account-mutation lock is taken, and the checks and writes run under it.
  If no account gets stored, the source and its indices are removed again.
@aristidesstaffieri aristidesstaffieri self-assigned this Nov 12, 2026
@aristidesstaffieri aristidesstaffieri changed the title feat(accounts): import an additional recovery phrase without wiping t… feat(accounts): import an additional recovery phrase without wiping the wallet 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-986d85e93df7fd19167b
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-7761902fef6fa3f99072
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