Skip to content

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

Description

@abayomicornelius

Problem

src/context/WalletContext.tsx detects the Freighter connection state exactly once, on mount:

useEffect(() => {
  const detect = async () => {
    try {
      const connected = await isConnected()
      dispatch({ type: 'SET_FREIGHTER_INSTALLED', installed: true })
      if (connected) {
        const allowed = await isAllowed()
        if (allowed) {
          const pubKey = await getPublicKey()
          if (pubKey) dispatch({ type: 'SET_CONNECTED', publicKey: pubKey })
        }
      }
    } catch {
      dispatch({ type: 'SET_FREIGHTER_INSTALLED', installed: false })
    }
  }
  detect()
}, [])

There is no listener for Freighter's own account-changed or network-changed events (the extension supports switching between multiple imported accounts, and switching its own network preference, entirely from its own UI, independent of this app). Once SET_CONNECTED fires with an initial publicKey, this app's state never updates again unless the user manually disconnects/reconnects through this app's own UI.

Separately, setNetwork:

const setNetwork = useCallback((network: Network) => {
  dispatch({ type: 'SET_NETWORK', network })
  localStorage.setItem('stellarsend_network', network)
}, [])

only updates the network field in state and localStorage — it doesn't clear the stale account (fetched against the previous network) or force an immediate refreshAccount(). The existing useEffect that calls refreshAccount() is keyed on [wallet.status, refreshAccount], not on wallet.network, so switching networks while already 'connected' does not by itself trigger a refetch — the account (and its balances) shown to the user briefly (until the next periodic setInterval(refreshAccount, interval * 1000) tick, which can be up to stellarsend_refresh_interval seconds away, default 30s) is stale data from the network the user just switched away from.

Why it matters

  • Stale identity risk: if the user switches accounts inside the Freighter extension (a normal, supported Freighter action, not something this app can prevent), this app keeps showing the old publicKey, old balances, and would build/sign any new transaction (signTransaction) using the stale wallet.publicKey as accountToSign — Freighter itself would likely reject a mismatched accountToSign, so this may "just" surface as a confusing signature failure rather than a silent wrong-account transaction, but that's still a broken, unexplained failure mode from the user's perspective with no guidance to reconnect.
  • Stale network / balance display: switching this app's own network selector while connected briefly (up to refresh interval seconds) displays account data fetched for the previous network under the new network's label — e.g. showing testnet XLM balances momentarily labeled as mainnet, which is a materially misleading thing to show around money movement, even if transient.
  • signTransaction's network passphrase is computed fresh at call time from wallet.network:
    const networkPassphrase = wallet.network === 'testnet' ? 'Test SDF Network ; September 2015' : 'Public Global Stellar Network ; September 2015'
    If a transaction was built (via buildPaymentTransaction/buildBatchPaymentTransaction/etc., all of which also take a network parameter derived from wallet.network at build time) just before the user flips the network selector, and signed just after, the built XDR's embedded network context and the passphrase used to sign it could disagree — Horizon/Freighter will likely reject this outright rather than silently misapply it, but the failure is opaque to the user ("signature invalid") with nothing in this codebase explaining or guarding against the race.

Reproduction

  • Connect the wallet, then switch accounts inside the Freighter extension's own popup UI (not through this app) — observe wallet.publicKey in this app's state does not update.
  • Toggle the app's network selector while connected and immediately inspect wallet.account — it still reflects the previous network's balances until the next periodic refresh fires.

Suggested fix (flagging as spike — the right approach depends on what @stellar/freighter-api actually exposes for this)

  • Investigate whether @stellar/freighter-api exposes any event/subscription for account or network changes (some wallet-connector APIs poll getPublicKey()/getNetwork() periodically instead of pushing events) — if only polling is available, add a lightweight periodic check (distinct from the existing balance-refresh interval) that compares the currently-reported Freighter public key/network against this app's state and reconciles (disconnect + re-prompt, or silently resync) on mismatch.
  • Make setNetwork synchronously clear the stale account (dispatch({ type: 'SET_ACCOUNT', account: null }) or equivalent) and immediately invoke refreshAccount() rather than waiting for the existing interval.
  • Add a useEffect dependency on wallet.network (not just wallet.status) for the refresh-account effect, so a network switch while connected triggers an immediate refetch.

Testing strategy

  • Unit test setNetwork dispatching the network change and asserting refreshAccount (or an equivalent immediate refetch) is called synchronously, and that stale account data is cleared before new data arrives (no flash of old-network balances under the new label).
  • If a polling-based reconciliation is implemented, unit test it against a mocked Freighter API returning a different public key than the one in state, asserting the app transitions to a clear "wallet changed — please reconnect" state rather than continuing to use the stale key.

Related issues in this batch

The signature-mismatch race described above is a specific case of the same "network/asset assumptions drift across an async boundary" theme as the always-zero History volume chart and hardcoded-mainnet-Horizon issues filed elsewhere in this batch.

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26bugSomething isn't workingsecuritySecurity concernvery hardVery difficult / senior-level bounty issue

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions