From 65376da08b05c824fab567e2f2c0ba123c0106ab Mon Sep 17 00:00:00 2001 From: Ben Wolski <570819+benwolski@users.noreply.github.com> Date: Wed, 14 Jan 2026 18:12:39 -0500 Subject: [PATCH 1/4] add wallet address mismatch detection to prevent stale sessions after soft refresh --- packages/frontend/app/contexts/AppContext.tsx | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/packages/frontend/app/contexts/AppContext.tsx b/packages/frontend/app/contexts/AppContext.tsx index 1a6a8c136..c7a367264 100644 --- a/packages/frontend/app/contexts/AppContext.tsx +++ b/packages/frontend/app/contexts/AppContext.tsx @@ -4,6 +4,7 @@ import React, { useContext, useEffect, useState, + useRef, type Dispatch, type SetStateAction, } from 'react'; @@ -113,6 +114,107 @@ export const AppProvider: React.FC = ({ children }) => { initializePythPriceService(); }, []); + // Workaround for Nightly wallet bug: after a soft refresh (Cmd-R), the session + // may be restored with a cached wallet address that differs from the currently + // selected address in the wallet extension. This effect detects the mismatch + // by checking the wallet's connection state and triggers session end if needed. + useEffect(() => { + if (!isEstablished(sessionState)) { + return; + } + + const sessionAddress = sessionState.walletPublicKey.toString(); + const wallet = sessionState.solanaWallet; + + // Check for wallet connection issues + const checkWalletMismatch = () => { + // Check if wallet is disconnected + const isWalletConnected = wallet?.connected; + console.log('[AppContext] Checking wallet state:', { + isWalletConnected, + walletPublicKey: wallet?.publicKey?.toBase58?.(), + sessionAddress, + }); + + // If wallet is not connected, end the session + if (isWalletConnected === false) { + console.warn( + '[AppContext] Wallet not connected. Ending session.', + ); + sessionState.endSession(); + return; + } + + // Check if wallet has no publicKey + const walletPublicKey = wallet?.publicKey?.toBase58?.(); + if (!walletPublicKey) { + console.warn( + '[AppContext] Wallet has no publicKey. Ending session.', + ); + sessionState.endSession(); + return; + } + + // Check if wallet's current address differs from session + if (walletPublicKey !== sessionAddress) { + console.warn( + '[AppContext] Wallet address mismatch. ' + + `Session: ${sessionAddress}, Wallet: ${walletPublicKey}. Ending session.`, + ); + sessionState.endSession(); + return; + } + + // Also check wallet standard accounts array + // @ts-expect-error wallet.wallet types are not fully typed + const accounts = wallet?.wallet?.accounts; + if (Array.isArray(accounts) && accounts.length > 0) { + const currentAddress = accounts[0]?.address; + if (currentAddress && currentAddress !== sessionAddress) { + console.warn( + '[AppContext] Wallet standard address mismatch. ' + + `Session: ${sessionAddress}, Wallet: ${currentAddress}. Ending session.`, + ); + sessionState.endSession(); + } + } + }; + + // Check after a short delay to allow wallet extension to update + const timeoutId = setTimeout(checkWalletMismatch, 500); + + // Also listen for wallet standard "change" events + let removeListener: (() => void) | undefined; + try { + // @ts-expect-error wallet.wallet types are not fully typed + const eventsFeature = wallet?.wallet?.features?.['standard:events']; + if (eventsFeature?.on) { + removeListener = eventsFeature.on( + 'change', + (event: { accounts?: Array<{ address: string }> }) => { + if (event.accounts && event.accounts.length > 0) { + const newAddress = event.accounts[0]?.address; + if (newAddress && newAddress !== sessionAddress) { + console.warn( + '[AppContext] Wallet address changed. ' + + `Session: ${sessionAddress}, New: ${newAddress}. Ending session.`, + ); + sessionState.endSession(); + } + } + }, + ); + } + } catch { + // Wallet doesn't support standard:events + } + + return () => { + clearTimeout(timeoutId); + removeListener?.(); + }; + }, [sessionState]); + return ( Date: Wed, 14 Jan 2026 18:31:29 -0500 Subject: [PATCH 2/4] simplify wallet mismatch detection to only check for disconnected state --- packages/frontend/app/contexts/AppContext.tsx | 89 ++++--------------- 1 file changed, 16 insertions(+), 73 deletions(-) diff --git a/packages/frontend/app/contexts/AppContext.tsx b/packages/frontend/app/contexts/AppContext.tsx index c7a367264..d9e028ba2 100644 --- a/packages/frontend/app/contexts/AppContext.tsx +++ b/packages/frontend/app/contexts/AppContext.tsx @@ -4,7 +4,6 @@ import React, { useContext, useEffect, useState, - useRef, type Dispatch, type SetStateAction, } from 'react'; @@ -128,90 +127,34 @@ export const AppProvider: React.FC = ({ children }) => { // Check for wallet connection issues const checkWalletMismatch = () => { - // Check if wallet is disconnected - const isWalletConnected = wallet?.connected; - console.log('[AppContext] Checking wallet state:', { - isWalletConnected, - walletPublicKey: wallet?.publicKey?.toBase58?.(), - sessionAddress, - }); - - // If wallet is not connected, end the session - if (isWalletConnected === false) { - console.warn( - '[AppContext] Wallet not connected. Ending session.', - ); - sessionState.endSession(); - return; - } - - // Check if wallet has no publicKey - const walletPublicKey = wallet?.publicKey?.toBase58?.(); - if (!walletPublicKey) { - console.warn( - '[AppContext] Wallet has no publicKey. Ending session.', - ); - sessionState.endSession(); - return; - } - - // Check if wallet's current address differs from session - if (walletPublicKey !== sessionAddress) { - console.warn( - '[AppContext] Wallet address mismatch. ' + - `Session: ${sessionAddress}, Wallet: ${walletPublicKey}. Ending session.`, - ); - sessionState.endSession(); - return; - } - - // Also check wallet standard accounts array - // @ts-expect-error wallet.wallet types are not fully typed - const accounts = wallet?.wallet?.accounts; - if (Array.isArray(accounts) && accounts.length > 0) { - const currentAddress = accounts[0]?.address; - if (currentAddress && currentAddress !== sessionAddress) { + try { + const isWalletConnected = wallet?.connected; + const walletPublicKey = wallet?.publicKey?.toBase58?.(); + + // Only treat it as an error if the underlying wallet is actually + // disconnected / has no active public key while the session claims + // to be established (the "stuck" state after Cmd-R). + if (isWalletConnected === false || !walletPublicKey) { console.warn( - '[AppContext] Wallet standard address mismatch. ' + - `Session: ${sessionAddress}, Wallet: ${currentAddress}. Ending session.`, + '[AppContext] Session established but wallet is disconnected or missing publicKey. Ending session.', + { + sessionAddress, + isWalletConnected, + walletPublicKey, + }, ); sessionState.endSession(); } + } catch { + // Best-effort only } }; // Check after a short delay to allow wallet extension to update const timeoutId = setTimeout(checkWalletMismatch, 500); - // Also listen for wallet standard "change" events - let removeListener: (() => void) | undefined; - try { - // @ts-expect-error wallet.wallet types are not fully typed - const eventsFeature = wallet?.wallet?.features?.['standard:events']; - if (eventsFeature?.on) { - removeListener = eventsFeature.on( - 'change', - (event: { accounts?: Array<{ address: string }> }) => { - if (event.accounts && event.accounts.length > 0) { - const newAddress = event.accounts[0]?.address; - if (newAddress && newAddress !== sessionAddress) { - console.warn( - '[AppContext] Wallet address changed. ' + - `Session: ${sessionAddress}, New: ${newAddress}. Ending session.`, - ); - sessionState.endSession(); - } - } - }, - ); - } - } catch { - // Wallet doesn't support standard:events - } - return () => { clearTimeout(timeoutId); - removeListener?.(); }; }, [sessionState]); From 644635a4ccd3f85620573da9f9b55f88ff2d3e8c Mon Sep 17 00:00:00 2001 From: Ben Wolski <570819+benwolski@users.noreply.github.com> Date: Wed, 14 Jan 2026 18:41:44 -0500 Subject: [PATCH 3/4] add continuous wallet mismatch monitoring with focus and visibility change detection --- packages/frontend/app/contexts/AppContext.tsx | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/frontend/app/contexts/AppContext.tsx b/packages/frontend/app/contexts/AppContext.tsx index d9e028ba2..2a4860695 100644 --- a/packages/frontend/app/contexts/AppContext.tsx +++ b/packages/frontend/app/contexts/AppContext.tsx @@ -3,17 +3,14 @@ import React, { createContext, useContext, useEffect, - useState, type Dispatch, type SetStateAction, } from 'react'; -import { useCallback } from 'react'; import { useLocation } from 'react-router'; import { useDebugStore } from '~/stores/DebugStore'; import { useTradeDataStore } from '~/stores/TradeDataStore'; import { useUserDataStore } from '~/stores/UserDataStore'; import { initializePythPriceService } from '~/stores/PythPriceStore'; -import { debugWallets } from '~/utils/Constants'; interface AppContextType { isUserConnected: boolean; @@ -32,8 +29,6 @@ export interface AppProviderProps { } export const AppProvider: React.FC = ({ children }) => { - const [isUserConnected, setIsUserConnected] = useState(false); - const { isDebugWalletActive, debugWallet, @@ -49,6 +44,7 @@ export const AppProvider: React.FC = ({ children }) => { const { resetUserData } = useTradeDataStore(); const sessionState = useSession(); + const isSessionEstablished = isEstablished(sessionState); const location = useLocation(); // Drive userAddress from URL parameter, session, or debug settings @@ -118,7 +114,7 @@ export const AppProvider: React.FC = ({ children }) => { // selected address in the wallet extension. This effect detects the mismatch // by checking the wallet's connection state and triggers session end if needed. useEffect(() => { - if (!isEstablished(sessionState)) { + if (!isSessionEstablished) { return; } @@ -150,13 +146,26 @@ export const AppProvider: React.FC = ({ children }) => { } }; - // Check after a short delay to allow wallet extension to update const timeoutId = setTimeout(checkWalletMismatch, 500); + const intervalId = setInterval(checkWalletMismatch, 1000); + + const handleVisibilityOrFocus = () => { + checkWalletMismatch(); + }; + + window.addEventListener('focus', handleVisibilityOrFocus); + document.addEventListener('visibilitychange', handleVisibilityOrFocus); return () => { clearTimeout(timeoutId); + clearInterval(intervalId); + window.removeEventListener('focus', handleVisibilityOrFocus); + document.removeEventListener( + 'visibilitychange', + handleVisibilityOrFocus, + ); }; - }, [sessionState]); + }, [isSessionEstablished, sessionState]); return ( Date: Wed, 14 Jan 2026 18:54:53 -0500 Subject: [PATCH 4/4] refactor wallet mismatch detection to use ref-based session state access and eliminate dependency array --- packages/frontend/app/contexts/AppContext.tsx | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/frontend/app/contexts/AppContext.tsx b/packages/frontend/app/contexts/AppContext.tsx index 2a4860695..30c8d5332 100644 --- a/packages/frontend/app/contexts/AppContext.tsx +++ b/packages/frontend/app/contexts/AppContext.tsx @@ -44,7 +44,6 @@ export const AppProvider: React.FC = ({ children }) => { const { resetUserData } = useTradeDataStore(); const sessionState = useSession(); - const isSessionEstablished = isEstablished(sessionState); const location = useLocation(); // Drive userAddress from URL parameter, session, or debug settings @@ -113,16 +112,22 @@ export const AppProvider: React.FC = ({ children }) => { // may be restored with a cached wallet address that differs from the currently // selected address in the wallet extension. This effect detects the mismatch // by checking the wallet's connection state and triggers session end if needed. + const sessionStateRef = React.useRef(sessionState); useEffect(() => { - if (!isSessionEstablished) { - return; - } + sessionStateRef.current = sessionState; + }, [sessionState]); + + useEffect(() => { + const checkSessionHealth = () => { + const currentSessionState = sessionStateRef.current; + if (!isEstablished(currentSessionState)) { + return; + } - const sessionAddress = sessionState.walletPublicKey.toString(); - const wallet = sessionState.solanaWallet; + const sessionAddress = + currentSessionState.walletPublicKey.toString(); + const wallet = currentSessionState.solanaWallet; - // Check for wallet connection issues - const checkWalletMismatch = () => { try { const isWalletConnected = wallet?.connected; const walletPublicKey = wallet?.publicKey?.toBase58?.(); @@ -139,18 +144,18 @@ export const AppProvider: React.FC = ({ children }) => { walletPublicKey, }, ); - sessionState.endSession(); + currentSessionState.endSession(); } } catch { // Best-effort only } }; - const timeoutId = setTimeout(checkWalletMismatch, 500); - const intervalId = setInterval(checkWalletMismatch, 1000); + const timeoutId = setTimeout(checkSessionHealth, 500); + const intervalId = setInterval(checkSessionHealth, 1000); const handleVisibilityOrFocus = () => { - checkWalletMismatch(); + checkSessionHealth(); }; window.addEventListener('focus', handleVisibilityOrFocus); @@ -165,7 +170,7 @@ export const AppProvider: React.FC = ({ children }) => { handleVisibilityOrFocus, ); }; - }, [isSessionEstablished, sessionState]); + }, []); return (