From 661ccee14a1ae3eb349a6ae33b7cb1d153a16442 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Thu, 13 Feb 2025 00:03:37 -0600 Subject: [PATCH] feat: move network state to redux --- src/app/_components/NavBar/NetworkLabel.tsx | 12 +- src/app/_components/Providers.tsx | 18 ++- src/common/context/GlobalContextProvider.tsx | 46 +----- .../context/__tests__/GlobalContext.test.tsx | 8 +- src/common/state/ReduxStateInitializer.tsx | 44 ++++++ src/common/state/slices/network-slice.ts | 143 ++++++++++++++++++ src/common/state/store.ts | 3 + 7 files changed, 214 insertions(+), 60 deletions(-) create mode 100644 src/common/state/ReduxStateInitializer.tsx create mode 100644 src/common/state/slices/network-slice.ts diff --git a/src/app/_components/NavBar/NetworkLabel.tsx b/src/app/_components/NavBar/NetworkLabel.tsx index 67f7f584b..275c24e94 100644 --- a/src/app/_components/NavBar/NetworkLabel.tsx +++ b/src/app/_components/NavBar/NetworkLabel.tsx @@ -1,8 +1,8 @@ +import { DEFAULT_MAINNET_SERVER, DEFAULT_TESTNET_SERVER } from '@/common/constants/env'; import { Badge, Flex, Icon, Spinner, Stack } from '@chakra-ui/react'; import { Check, Trash } from '@phosphor-icons/react'; import { FC, useMemo } from 'react'; -// import { Badge } from '../../../common/components/Badge'; import { DEFAULT_DEVNET_SERVER } from '../../../common/constants/constants'; import { useGlobalContext } from '../../../common/context/useGlobalContext'; import { useCustomNetworkApiInfo } from '../../../common/queries/useCustomNetworkApiInfo'; @@ -19,13 +19,9 @@ const ellipsisStyle: React.CSSProperties = { }; export const NetworkLabel: FC<{ network: Network }> = ({ network }) => { - const { - activeNetwork, - removeCustomNetwork, - apiUrls: { mainnet, testnet }, - } = useGlobalContext(); - const isMainnet = network.url === mainnet; - const isTestnet = network.url === testnet; + const { activeNetwork, removeCustomNetwork } = useGlobalContext(); + const isMainnet = network.url === DEFAULT_MAINNET_SERVER; + const isTestnet = network.url === DEFAULT_TESTNET_SERVER; const isDevnet = network.url === DEFAULT_DEVNET_SERVER; const isDefault = isMainnet || isTestnet; diff --git a/src/app/_components/Providers.tsx b/src/app/_components/Providers.tsx index 7081e5da3..e0b1631fa 100644 --- a/src/app/_components/Providers.tsx +++ b/src/app/_components/Providers.tsx @@ -1,5 +1,6 @@ 'use client'; +import { ReduxStateInitializer } from '@/common/state/ReduxStateInitializer'; import { ChakraProvider } from '@chakra-ui/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useSearchParams } from 'next/navigation'; @@ -54,13 +55,18 @@ export const Providers = ({ - - {children} - + + {children} + + diff --git a/src/common/context/GlobalContextProvider.tsx b/src/common/context/GlobalContextProvider.tsx index 651726162..adb7430a9 100644 --- a/src/common/context/GlobalContextProvider.tsx +++ b/src/common/context/GlobalContextProvider.tsx @@ -10,7 +10,6 @@ import { useStacksApiSocketClient, } from '../../app/_components/BlockList/Sockets/use-stacks-api-socket-client'; import { buildCustomNetworkUrl, fetchCustomNetworkId } from '../components/modals/AddNetwork/utils'; -import { IS_BROWSER } from '../constants/constants'; import { NetworkIdModeMap, NetworkModeBtcAddressBaseUrlMap, @@ -19,7 +18,6 @@ import { NetworkModeUrlMap, devnetNetwork, mainnetNetwork, - oldTestnetNetwork, testnetNetwork, } from '../constants/network'; import { ONE_HOUR } from '../queries/query-stale-time'; @@ -36,10 +34,6 @@ function filterNetworks( } interface GlobalContext { - apiUrls: Record; - btcBlockBaseUrls: Record; - btcTxBaseUrls: Record; - btcAddressBaseUrls: Record; activeNetwork: Network; activeNetworkKey: string; addCustomNetwork: (network: Network) => void; @@ -50,10 +44,6 @@ interface GlobalContext { } export const GlobalContext = createContext({ - apiUrls: NetworkModeUrlMap, - btcBlockBaseUrls: NetworkModeBtcBlockBaseUrlMap, - btcTxBaseUrls: NetworkModeBtcTxBaseUrlMap, - btcAddressBaseUrls: NetworkModeBtcAddressBaseUrlMap, activeNetwork: mainnetNetwork, activeNetworkKey: NetworkModeUrlMap[NetworkModes.Mainnet], addCustomNetwork: (network: Network) => {}, @@ -72,7 +62,6 @@ export const GlobalContextProvider: FC<{ const searchParams = useSearchParams(); const chain = searchParams?.get('chain'); const api = searchParams?.get('api'); - const subnet = searchParams?.get('subnet'); const btcBlockBaseUrl = searchParams?.get('btcBlockBaseUrl'); const btcTxBaseUrl = searchParams?.get('btcTxBaseUrl'); const btcAddressBaseUrl = searchParams?.get('btcAddressBaseUrl'); @@ -81,7 +70,6 @@ export const GlobalContextProvider: FC<{ const queryNetworkMode = ((Array.isArray(chain) ? chain[0] : chain) || NetworkModes.Mainnet) as NetworkModes; const queryApiUrl = removeTrailingSlash(Array.isArray(api) ? api[0] : api); - const querySubnet = Array.isArray(subnet) ? subnet[0] : subnet; const queryBtcBlockBaseUrl = Array.isArray(btcBlockBaseUrl) ? btcBlockBaseUrl[0] : btcBlockBaseUrl; @@ -89,11 +77,7 @@ export const GlobalContextProvider: FC<{ const queryBtcAddressBaseUrl = Array.isArray(btcAddressBaseUrl) ? btcAddressBaseUrl[0] : btcAddressBaseUrl; - const activeNetworkKey = querySubnet || queryApiUrl || NetworkModeUrlMap[queryNetworkMode]; - - // TODO: is this needed anymore? - if (IS_BROWSER && (window as any)?.location?.search?.includes('err=1')) - throw new Error('test error'); + const activeNetworkKey = queryApiUrl || NetworkModeUrlMap[queryNetworkMode]; const addedCustomNetworks: Record = JSON.parse( addedCustomNetworksCookie || '{}' @@ -104,32 +88,13 @@ export const GlobalContextProvider: FC<{ const [_, setAddedCustomNetworksCookie] = useCookies(['addedCustomNetworks']); const [__, setRemovedCustomNetworksCookie] = useCookies(['removedCustomNetworks']); - const isUrlPassedSubnet = !!querySubnet; - const [networks, setNetworks] = useState>( filterNetworks( { [mainnetNetwork.url]: mainnetNetwork, [testnetNetwork.url]: testnetNetwork, - [oldTestnetNetwork.url]: oldTestnetNetwork, [devnetNetwork.url]: devnetNetwork, ...addedCustomNetworks, - ...(isUrlPassedSubnet - ? { - [querySubnet]: { - isSubnet: true, - url: querySubnet, - btcBlockBaseUrl: - queryBtcBlockBaseUrl || NetworkModeBtcBlockBaseUrlMap[NetworkModes.Mainnet], - btcTxBaseUrl: queryBtcTxBaseUrl || NetworkModeBtcTxBaseUrlMap[NetworkModes.Mainnet], - btcAddressBaseUrl: - queryBtcAddressBaseUrl || NetworkModeBtcAddressBaseUrlMap[NetworkModes.Mainnet], - label: 'subnet', - networkId: 1, - mode: NetworkModes.Mainnet, - } as Network, - } - : {}), }, removedCustomNetworks ) @@ -263,20 +228,17 @@ export const GlobalContextProvider: FC<{ {children} diff --git a/src/common/context/__tests__/GlobalContext.test.tsx b/src/common/context/__tests__/GlobalContext.test.tsx index c695fa99b..e156785c9 100644 --- a/src/common/context/__tests__/GlobalContext.test.tsx +++ b/src/common/context/__tests__/GlobalContext.test.tsx @@ -55,7 +55,7 @@ describe('GlobalContext', () => { }, } as any); render( - + ); @@ -72,13 +72,13 @@ describe('GlobalContext', () => { }, } as any); render( - + ); const networks = getContextField('networks'); - expect(Object.keys(networks).length).toBe(4); + expect(Object.keys(networks).length).toBe(3); await waitFor(() => { expect(fetchCustomNetworkId).toHaveBeenCalledWith(customApiUrl, false); @@ -86,7 +86,7 @@ describe('GlobalContext', () => { await waitFor(() => { const updatedNetworks = getContextField('networks'); - expect(Object.keys(updatedNetworks).length).toBe(5); + expect(Object.keys(updatedNetworks).length).toBe(4); expect(updatedNetworks[customApiUrl].isCustomNetwork).toBe(true); }); }); diff --git a/src/common/state/ReduxStateInitializer.tsx b/src/common/state/ReduxStateInitializer.tsx new file mode 100644 index 000000000..4e84041c2 --- /dev/null +++ b/src/common/state/ReduxStateInitializer.tsx @@ -0,0 +1,44 @@ +import { useSearchParams } from 'next/navigation'; +import { ReactNode, useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { initializeNetworkState, initializeNetworks } from './slices/network-slice'; + +export const ReduxStateInitializer = ({ + children, + addedCustomNetworksCookie, + removedCustomNetworksCookie, +}: { + children: ReactNode; + addedCustomNetworksCookie: string | undefined; + removedCustomNetworksCookie: string | undefined; +}) => { + const dispatch = useDispatch(); + const searchParams = useSearchParams(); + const [isInitialized, setIsInitialized] = useState(false); + + useEffect(() => { + const initNetworks = async () => { + const networkState = await initializeNetworkState( + searchParams, + addedCustomNetworksCookie, + removedCustomNetworksCookie + ); + + if (!isInitialized) { + dispatch(initializeNetworks(networkState)); + setIsInitialized(true); + } + }; + + initNetworks(); + }, [ + dispatch, + addedCustomNetworksCookie, + removedCustomNetworksCookie, + searchParams, + isInitialized, + ]); + + return <>{children}; +}; diff --git a/src/common/state/slices/network-slice.ts b/src/common/state/slices/network-slice.ts new file mode 100644 index 000000000..86674e60f --- /dev/null +++ b/src/common/state/slices/network-slice.ts @@ -0,0 +1,143 @@ +import { Network, NetworkModes } from '@/common/types/network'; +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; + +import { + buildCustomNetworkUrl, + fetchCustomNetworkId, +} from '../../components/modals/AddNetwork/utils'; +import { + NetworkIdModeMap, + NetworkModeBtcAddressBaseUrlMap, + NetworkModeBtcBlockBaseUrlMap, + NetworkModeBtcTxBaseUrlMap, + NetworkModeUrlMap, + devnetNetwork, + mainnetNetwork, + testnetNetwork, +} from '../../constants/network'; +import { useAppSelector } from '../../state/hooks'; +import { removeTrailingSlash } from '../../utils/utils'; + +export interface NetworkState { + activeNetwork: Network; + networks: Record; +} + +function filterNetworks( + networks: Record, + removedNetworks: Record +) { + return Object.fromEntries( + Object.entries(networks).filter(([key]) => !Object.keys(removedNetworks).includes(key)) + ); +} + +// Helper function to initialize the network state based on URL parameters +export const initializeNetworkState = async ( + searchParams: URLSearchParams, + addedCustomNetworksCookie: string | undefined, + removedCustomNetworksCookie: string | undefined +) => { + const chain = searchParams.get('chain'); + const api = searchParams.get('api'); + const btcBlockBaseUrl = searchParams.get('btcBlockBaseUrl'); + const btcTxBaseUrl = searchParams.get('btcTxBaseUrl'); + const btcAddressBaseUrl = searchParams.get('btcAddressBaseUrl'); + + // Derive network mode and URLs from params + const networkModeFromSearchParams = (chain || NetworkModes.Mainnet) as NetworkModes; + const apiUrlFromSearchParams = removeTrailingSlash(api || '') || null; + + // Determine active network URL + const activeNetworkUrl = apiUrlFromSearchParams || NetworkModeUrlMap[networkModeFromSearchParams]; + + const addedCustomNetworks: Record = JSON.parse( + addedCustomNetworksCookie || '{}' + ); + + // This is used to keep track of default networks that have been removed. By default, default networks + // are included in the networks list. Therefore, we need to remember that the default network has been removed + // so that we can remove it from the networks list. If we don't allow users to remove the default networks, + // which we should do, then we could remove this logic. + const removedCustomNetworks: Record = JSON.parse( + removedCustomNetworksCookie || '{}' + ); + + // Initialize base networks + let networks: Record = filterNetworks( + { + [mainnetNetwork.url]: mainnetNetwork, + [testnetNetwork.url]: testnetNetwork, + [devnetNetwork.url]: devnetNetwork, + ...addedCustomNetworks, + }, + removedCustomNetworks + ); + + // Add custom API network if provided in the URL + if (apiUrlFromSearchParams && !networks[apiUrlFromSearchParams]) { + const networkUrl = buildCustomNetworkUrl(apiUrlFromSearchParams); + const networkId = await fetchCustomNetworkId(networkUrl, false); + if (networkId) { + networks[apiUrlFromSearchParams] = { + label: apiUrlFromSearchParams, + url: networkUrl, + btcBlockBaseUrl: btcBlockBaseUrl || NetworkModeBtcBlockBaseUrlMap[NetworkModes.Mainnet], + btcTxBaseUrl: btcTxBaseUrl || NetworkModeBtcTxBaseUrlMap[NetworkModes.Mainnet], + btcAddressBaseUrl: + btcAddressBaseUrl || NetworkModeBtcAddressBaseUrlMap[NetworkModes.Mainnet], + networkId, + mode: NetworkIdModeMap[networkId], + isCustomNetwork: true, + isSubnet: false, + }; + } + } + + return { + networks, + activeNetwork: networks[activeNetworkUrl] || mainnetNetwork, + }; +}; + +export const initialState: NetworkState = { + activeNetwork: mainnetNetwork, + networks: { + [mainnetNetwork.url]: mainnetNetwork, + [testnetNetwork.url]: testnetNetwork, + [devnetNetwork.url]: devnetNetwork, + }, +}; + +export const networkSlice = createSlice({ + name: 'network', + initialState, + reducers: { + setActiveNetwork: (state, action: PayloadAction) => { + state.activeNetwork = action.payload; + }, + addNetwork: (state, action: PayloadAction) => { + state.networks[action.payload.url] = { + ...action.payload, + isCustomNetwork: true, + }; + }, + removeNetwork: (state, action: PayloadAction) => { + const { [action.payload]: _, ...remainingNetworks } = state.networks; + state.networks = remainingNetworks; + }, + initializeNetworks: (state, action: PayloadAction) => { + state.networks = action.payload.networks; + state.activeNetwork = action.payload.activeNetwork; + }, + }, +}); + +export const { setActiveNetwork, addNetwork, removeNetwork, initializeNetworks } = + networkSlice.actions; + +// Selector hooks +export const useActiveNetwork = () => useAppSelector(state => state.network.activeNetwork); +export const useNetworks = () => useAppSelector(state => state.network.networks); + +export default networkSlice.reducer; diff --git a/src/common/state/store.ts b/src/common/state/store.ts index dcabad245..ffdb1deab 100644 --- a/src/common/state/store.ts +++ b/src/common/state/store.ts @@ -9,6 +9,7 @@ import { filterAndSortReducers, } from '../../features/txsFilterAndSort/txsFilterAndSortSlice'; import { ModalState, modalSlice } from '../components/modals/modal-slice'; +import { NetworkState, networkSlice } from './slices/network-slice'; import { TransactionValueFilterState, activeTransactionValueFilterSlice, @@ -19,6 +20,7 @@ const rootReducer = combineReducers({ search: searchSlice.reducer, connect: sandboxSlice.reducer, activeTransactionValueFilter: activeTransactionValueFilterSlice.reducer, + network: networkSlice.reducer, ...filterAndSortReducers, }); @@ -42,6 +44,7 @@ export interface RootState extends TxFilters { search: SearchState; connect: ConnectState; activeTransactionValueFilter: TransactionValueFilterState; + network: NetworkState; } export type AppDispatch = ReturnType['dispatch'];