Skip to content

fix: react to Freighter account drift and fix stale account on network switch (#25) - #35

Merged
abayomicornelius merged 9 commits into
StellarSend:mainfrom
boluwacodes:fix/25-walletcontext-freighter-sync
Jul 22, 2026
Merged

fix: react to Freighter account drift and fix stale account on network switch (#25)#35
abayomicornelius merged 9 commits into
StellarSend:mainfrom
boluwacodes:fix/25-walletcontext-freighter-sync

Conversation

@boluwacodes

Copy link
Copy Markdown
Contributor

Summary

Closes #25.

WalletContext detected the Freighter connection exactly once, on
mount, and never again. If the user switched accounts inside
Freighter's own popup UI (a normal, supported action this app can't
prevent), wallet.publicKey kept pointing at the stale account
indefinitely - which would then be used as accountToSign for any new
transaction. Separately, setNetwork() only updated network and
localStorage; it didn't clear the stale account or force an
immediate refetch, so switching the app's network selector while
connected could briefly show the previous network's balances under
the new network's label until the next periodic refresh tick (up to
stellarsend_refresh_interval seconds, default 30s, away).

  • Added a polling reconciliation effect (distinct interval from the
    balance-refresh timer, since @stellar/freighter-api v1.7.1 exposes
    no account/network-change event - only getPublicKey()/getNetwork()
    to poll) that compares Freighter's currently-reported public key
    against app state every 3s while connected, and forces a clear
    "please reconnect" state (WALLET_CHANGED) on mismatch rather than
    continuing to sign with a stale key.
  • setNetwork() now synchronously clears the stale account
    (dispatch({ type: 'SET_ACCOUNT', account: null })) before the new
    network's data arrives, closing the stale-balance display window.
  • Added wallet.network explicitly to the refresh-account effect's
    dependency array, documenting (rather than just implicitly relying
    on) the invariant that a network switch while connected triggers an
    immediate refetch.
  • Network-side drift (Freighter's own active network vs. this app's
    independent network selector) was intentionally left out of the
    polling check: wallet.network is an app-level selector that's
    allowed to differ from whatever network Freighter itself is
    currently pointed at, since signTransaction already passes an
    explicit networkPassphrase override rather than relying on
    Freighter's own active network. Conflating the two risked forcing
    disconnects on a legitimate, independent use of the app's selector.
  • Added src/context/WalletContext.test.tsx (previously untested):
    connect/disconnect coverage, the setNetwork stale-clear + refetch
    behavior (using a manually-controlled promise to prove the account
    clears before the new fetch resolves), and the account-drift
    polling reconciliation - including the two negative cases (no
    spurious disconnect on a matching key or a transient Freighter
    error).

Test plan

  • npm test - 21/21 suites, 80/80 tests pass
  • npm run type-check - clean
  • npm run lint - clean (0 warnings)
  • npm run build - succeeds
  • Manually traced @stellar/freighter-api's type defs (v1.7.1 as
    installed) to confirm no event/subscription API exists, only
    getPublicKey()/getNetwork() polling - per the issue's own
    "flagging as spike" framing to investigate this first

Adds a dedicated action for reconciling a stale identity: it clears
publicKey and account and moves status to 'error' with an explanatory
message, rather than continuing to hold a publicKey that no longer
matches what Freighter itself reports. Also widens SET_ACCOUNT's
payload to accept null so callers can synchronously clear a stale
account before a fresh one arrives.

Not yet wired to anything - the polling and setNetwork changes that
dispatch it land in the next few commits.
Preparatory change for the account-drift polling effect landing next:
a setInterval callback registered by a status-only effect closes over
whatever wallet.publicKey was at the time the effect last ran, not
its current value, so the poll needs a ref it can read fresh on every
tick instead.
Closes the core gap in StellarSend#25: WalletContext previously detected the
Freighter connection exactly once, on mount, so switching accounts
inside Freighter's own extension UI (a normal, supported action this
app can't prevent) left wallet.publicKey pointing at a stale account
indefinitely - which would then be used as accountToSign for any new
transaction.

Adds a lightweight interval, distinct from the balance-refresh timer,
that compares Freighter's currently-reported public key against
publicKeyRef on each tick and dispatches WALLET_CHANGED on a mismatch.
Transient Freighter errors (e.g. momentarily locked) are swallowed
rather than treated as a drift signal.
)

setNetwork() only updated the network field and localStorage - it left
the previous network's account/balances in state until the next
refresh tick (up to stellarsend_refresh_interval seconds, default 30s
away), so the UI could briefly show e.g. testnet balances labeled as
mainnet right after the user flips the selector. Dispatching
SET_ACCOUNT with null in the same tick as SET_NETWORK closes that
window; the existing refresh-account effect refetches fresh data for
the new network right after.
refreshAccount's own useCallback deps already include wallet.network,
so this effect technically already re-ran on a network switch via that
transitive identity change - but that's an indirect, easy-to-break
invariant. Listing wallet.network explicitly here documents the
requirement in the one place a future reader would look, per StellarSend#25's
suggested fix, without changing behavior.
…end#25)

Adds the test file with the mock scaffolding (Freighter API functions,
fetchAccountFromHorizon) and a connectWallet() helper that drives the
provider through connect() the same way the real UI would. No test
cases yet - those land in the next few commits, split by the behavior
each one covers per StellarSend#25's testing strategy.
Baseline coverage for a file that previously had zero tests: connect()
reaches 'connected' with Freighter's reported public key, and
disconnect() clears both publicKey and account.
Exercises exactly the scenario StellarSend#25's testing strategy calls out:
setNetwork() must clear the previous network's account synchronously
(observable before the new network's fetch has resolved, using a
manually-controlled promise to prove the ordering) and trigger a
refetch scoped to the new network.
Matches StellarSend#25's testing strategy: mocks Freighter's getPublicKey()
returning a different key than what's in state and asserts the app
transitions to a clear "please reconnect" error state with publicKey
and account both cleared, rather than continuing on the stale key.
Also covers the negative cases - no state change when Freighter
keeps reporting the same key, and a transient Freighter error doesn't
get mistaken for a real account switch.
@abayomicornelius
abayomicornelius merged commit eaef6d7 into StellarSend:main Jul 22, 2026
4 checks passed
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.

WalletContext never reacts to Freighter-side account or network switches, and setNetwork doesn't invalidate stale account/passphrase state

2 participants