Skip to content

Commit

Permalink
feat: move network state to redux
Browse files Browse the repository at this point in the history
  • Loading branch information
BLuEScioN committed Feb 13, 2025
1 parent a1cd5e1 commit 6c0ac4a
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 54 deletions.
18 changes: 12 additions & 6 deletions src/app/_components/Providers.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { ReduxStateInitializer } from '@/common/state/ReduxStateInitializer';

Check warning on line 3 in src/app/_components/Providers.tsx

View check run for this annotation

Codecov / codecov/patch

src/app/_components/Providers.tsx#L3

Added line #L3 was not covered by tests
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useSearchParams } from 'next/navigation';
Expand Down Expand Up @@ -54,13 +55,18 @@ export const Providers = ({
<ChakraProvider value={system}>
<ColorModeProvider>
<ReduxProvider store={store}>
<AppConfig // TODO: rename to something else like SessionProvider
queryNetworkMode={queryNetworkMode}
queryApiUrl={queryApiUrl}
querySubnet={querySubnet}
<ReduxStateInitializer
addedCustomNetworksCookie={addedCustomNetworksCookie}
removedCustomNetworksCookie={removedCustomNetworksCookie}
>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</AppConfig>
<AppConfig // TODO: rename to something else like SessionProvider
queryNetworkMode={queryNetworkMode}
queryApiUrl={queryApiUrl}
querySubnet={querySubnet}
>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</AppConfig>
</ReduxStateInitializer>
</ReduxProvider>
</ColorModeProvider>
</ChakraProvider>
Expand Down
48 changes: 5 additions & 43 deletions src/common/context/GlobalContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -19,7 +18,6 @@ import {
NetworkModeUrlMap,
devnetNetwork,
mainnetNetwork,
oldTestnetNetwork,
testnetNetwork,
} from '../constants/network';
import { ONE_HOUR } from '../queries/query-stale-time';
Expand All @@ -36,10 +34,6 @@ function filterNetworks(
}

interface GlobalContext {
apiUrls: Record<NetworkModes, string>;
btcBlockBaseUrls: Record<NetworkModes, string>;
btcTxBaseUrls: Record<NetworkModes, string>;
btcAddressBaseUrls: Record<NetworkModes, string>;
activeNetwork: Network;
activeNetworkKey: string;
addCustomNetwork: (network: Network) => void;
Expand All @@ -50,10 +44,6 @@ interface GlobalContext {
}

export const GlobalContext = createContext<GlobalContext>({
apiUrls: NetworkModeUrlMap,
btcBlockBaseUrls: NetworkModeBtcBlockBaseUrlMap,
btcTxBaseUrls: NetworkModeBtcTxBaseUrlMap,
btcAddressBaseUrls: NetworkModeBtcAddressBaseUrlMap,
activeNetwork: mainnetNetwork,
activeNetworkKey: NetworkModeUrlMap[NetworkModes.Mainnet],
addCustomNetwork: (network: Network) => {},
Expand All @@ -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');
Expand All @@ -81,19 +70,14 @@ 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;
const queryBtcTxBaseUrl = Array.isArray(btcTxBaseUrl) ? btcTxBaseUrl[0] : btcTxBaseUrl;
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<string, Network> = JSON.parse(
addedCustomNetworksCookie || '{}'
Expand All @@ -104,32 +88,13 @@ export const GlobalContextProvider: FC<{
const [_, setAddedCustomNetworksCookie] = useCookies(['addedCustomNetworks']);
const [__, setRemovedCustomNetworksCookie] = useCookies(['removedCustomNetworks']);

const isUrlPassedSubnet = !!querySubnet;

const [networks, setNetworks] = useState<Record<string, Network>>(
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
)
Expand Down Expand Up @@ -263,23 +228,20 @@ export const GlobalContextProvider: FC<{
<GlobalContext.Provider
value={{
activeNetwork: networks[activeNetworkKey] || {},
activeNetworkKey,
apiUrls: NetworkModeUrlMap, // TODO: If this is a constant, why is it in context?
btcBlockBaseUrls: NetworkModeBtcBlockBaseUrlMap, // TODO: If this is a constant, why is it in context?
btcTxBaseUrls: NetworkModeBtcTxBaseUrlMap, // TODO: If this is a constant, why is it in context?
btcAddressBaseUrls: NetworkModeBtcAddressBaseUrlMap, // TODO: If this is a constant, why is it in context?
activeNetworkKey, // TODO: rename this to activeNetworkUrl. activeNetwork should be used instead of activeNetworkKey as having the information in both places is redundant
addCustomNetwork,
removeCustomNetwork,
networks,
apiClient: getApiClient(activeNetworkKey),

stacksApiSocketClientInfo: {
client: stacksApiSocketClient,
connect: connectStacksApiSocket,
disconnect: disconnectStacksApiSocket,
},
apiClient: getApiClient(activeNetworkKey),
}}
>
{children}
</GlobalContext.Provider>
);
};
};
10 changes: 5 additions & 5 deletions src/common/context/__tests__/GlobalContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('GlobalContext', () => {
},
} as any);
render(
<GlobalContextProvider headerCookies={''}>
<GlobalContextProvider addedCustomNetworksCookie={''} removedCustomNetworksCookie={''}>
<GlobalContextTestComponent />
</GlobalContextProvider>
);
Expand All @@ -72,22 +72,22 @@ describe('GlobalContext', () => {
},
} as any);
render(
<GlobalContextProvider headerCookies={''}>
<GlobalContextProvider addedCustomNetworksCookie={''} removedCustomNetworksCookie={''}>
<GlobalContextTestComponent />
</GlobalContextProvider>
);

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);
});

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);
});
});
});
});
44 changes: 44 additions & 0 deletions src/common/state/ReduxStateInitializer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useSearchParams } from 'next/navigation';
import { ReactNode, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';

Check warning on line 3 in src/common/state/ReduxStateInitializer.tsx

View check run for this annotation

Codecov / codecov/patch

src/common/state/ReduxStateInitializer.tsx#L1-L3

Added lines #L1 - L3 were not covered by tests

import { initializeNetworkState, initializeNetworks } from './slices/network-slice';

Check warning on line 5 in src/common/state/ReduxStateInitializer.tsx

View check run for this annotation

Codecov / codecov/patch

src/common/state/ReduxStateInitializer.tsx#L5

Added line #L5 was not covered by tests

export const ReduxStateInitializer = ({

Check warning on line 7 in src/common/state/ReduxStateInitializer.tsx

View check run for this annotation

Codecov / codecov/patch

src/common/state/ReduxStateInitializer.tsx#L7

Added line #L7 was not covered by tests
children,
addedCustomNetworksCookie,
removedCustomNetworksCookie,
}: {
children: ReactNode;
addedCustomNetworksCookie: string | undefined;
removedCustomNetworksCookie: string | undefined;
}) => {
const dispatch = useDispatch();
const searchParams = useSearchParams();
const [isInitialized, setIsInitialized] = useState(false);

Check warning on line 18 in src/common/state/ReduxStateInitializer.tsx

View check run for this annotation

Codecov / codecov/patch

src/common/state/ReduxStateInitializer.tsx#L15-L18

Added lines #L15 - L18 were not covered by tests

useEffect(() => {
const initNetworks = async () => {
const networkState = await initializeNetworkState(

Check warning on line 22 in src/common/state/ReduxStateInitializer.tsx

View check run for this annotation

Codecov / codecov/patch

src/common/state/ReduxStateInitializer.tsx#L20-L22

Added lines #L20 - L22 were not covered by tests
searchParams,
addedCustomNetworksCookie,
removedCustomNetworksCookie
);

if (!isInitialized) {
dispatch(initializeNetworks(networkState));
setIsInitialized(true);

Check warning on line 30 in src/common/state/ReduxStateInitializer.tsx

View check run for this annotation

Codecov / codecov/patch

src/common/state/ReduxStateInitializer.tsx#L29-L30

Added lines #L29 - L30 were not covered by tests
}
};

initNetworks();

Check warning on line 34 in src/common/state/ReduxStateInitializer.tsx

View check run for this annotation

Codecov / codecov/patch

src/common/state/ReduxStateInitializer.tsx#L34

Added line #L34 was not covered by tests
}, [
dispatch,
addedCustomNetworksCookie,
removedCustomNetworksCookie,
searchParams,
isInitialized,
]);

return <>{children}</>;
};
143 changes: 143 additions & 0 deletions src/common/state/slices/network-slice.ts
Original file line number Diff line number Diff line change
@@ -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<string, Network>;
}

function filterNetworks(
networks: Record<string, Network>,
removedNetworks: Record<string, Network>

Check warning on line 28 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L28

Added line #L28 was not covered by tests
) {
return Object.fromEntries(
Object.entries(networks).filter(([key]) => !Object.keys(removedNetworks).includes(key))

Check warning on line 31 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L30-L31

Added lines #L30 - L31 were not covered by tests
);
}

// Helper function to initialize the network state based on URL parameters
export const initializeNetworkState = async (
searchParams: URLSearchParams,
addedCustomNetworksCookie: string | undefined,
removedCustomNetworksCookie: string | undefined

Check warning on line 39 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L39

Added line #L39 was not covered by tests
) => {
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');

Check warning on line 45 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L41-L45

Added lines #L41 - L45 were not covered by tests

// 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<string, Network> = JSON.parse(

Check warning on line 54 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L54

Added line #L54 was not covered by tests
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<string, Network> = JSON.parse(

Check warning on line 62 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L62

Added line #L62 was not covered by tests
removedCustomNetworksCookie || '{}'
);

// Initialize base networks
let networks: Record<string, Network> = filterNetworks(

Check warning on line 67 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L67

Added line #L67 was not covered by tests
{
[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);

Check warning on line 80 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L79-L80

Added lines #L79 - L80 were not covered by tests
if (networkId) {
networks[apiUrlFromSearchParams] = {

Check warning on line 82 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L82

Added line #L82 was not covered by tests
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 {

Check warning on line 97 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L97

Added line #L97 was not covered by tests
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<Network>) => {
state.activeNetwork = action.payload;

Check warning on line 117 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L116-L117

Added lines #L116 - L117 were not covered by tests
},
addNetwork: (state, action: PayloadAction<Network>) => {
state.networks[action.payload.url] = {

Check warning on line 120 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L119-L120

Added lines #L119 - L120 were not covered by tests
...action.payload,
isCustomNetwork: true,
};
},
removeNetwork: (state, action: PayloadAction<string>) => {
const { [action.payload]: _, ...remainingNetworks } = state.networks;
state.networks = remainingNetworks;

Check warning on line 127 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L125-L127

Added lines #L125 - L127 were not covered by tests
},
initializeNetworks: (state, action: PayloadAction<NetworkState>) => {
state.networks = action.payload.networks;
state.activeNetwork = action.payload.activeNetwork;

Check warning on line 131 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L129-L131

Added lines #L129 - L131 were not covered by tests
},
},
});

export const { setActiveNetwork, addNetwork, removeNetwork, initializeNetworks } =

Check warning on line 136 in src/common/state/slices/network-slice.ts

View check run for this annotation

Codecov / codecov/patch

src/common/state/slices/network-slice.ts#L136

Added line #L136 was not covered by tests
networkSlice.actions;

// Selector hooks
export const useActiveNetwork = () => useAppSelector(state => state.network.activeNetwork);
export const useNetworks = () => useAppSelector(state => state.network.networks);

export default networkSlice.reducer;
Loading

0 comments on commit 6c0ac4a

Please sign in to comment.