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.
Problem
src/context/WalletContext.tsxdetects the Freighter connection state exactly once, on mount: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_CONNECTEDfires with an initialpublicKey, this app's state never updates again unless the user manually disconnects/reconnects through this app's own UI.Separately,
setNetwork:only updates the
networkfield in state andlocalStorage— it doesn't clear the staleaccount(fetched against the previous network) or force an immediaterefreshAccount(). The existinguseEffectthat callsrefreshAccount()is keyed on[wallet.status, refreshAccount], not onwallet.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 periodicsetInterval(refreshAccount, interval * 1000)tick, which can be up tostellarsend_refresh_intervalseconds away, default 30s) is stale data from the network the user just switched away from.Why it matters
publicKey, old balances, and would build/sign any new transaction (signTransaction) using the stalewallet.publicKeyasaccountToSign— Freighter itself would likely reject a mismatchedaccountToSign, 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.refresh intervalseconds) 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 fromwallet.network:buildPaymentTransaction/buildBatchPaymentTransaction/etc., all of which also take anetworkparameter derived fromwallet.networkat 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
wallet.publicKeyin this app's state does not update.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-apiactually exposes for this)@stellar/freighter-apiexposes any event/subscription for account or network changes (some wallet-connector APIs pollgetPublicKey()/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.setNetworksynchronously clear the staleaccount(dispatch({ type: 'SET_ACCOUNT', account: null })or equivalent) and immediately invokerefreshAccount()rather than waiting for the existing interval.useEffectdependency onwallet.network(not justwallet.status) for the refresh-account effect, so a network switch while connected triggers an immediate refetch.Testing strategy
setNetworkdispatching the network change and assertingrefreshAccount(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).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.