{selectedAssets.slice(0, 3).map((asset) => {
- const token = items.find((item) => getTokenKey(item) === asset);
+ const token = items.find((item) => getMarketFilterAssetSelectionKey(item) === asset);
return token ?
{renderTokenIcon(token, 14)} : null;
})}
{selectedAssets.length > 3 &&
+{selectedAssets.length - 3}}
@@ -143,7 +142,7 @@ export default function AssetFilter({
role="listbox"
>
{filteredItems.map((token) => {
- const tokenKey = getTokenKey(token);
+ const tokenKey = getMarketFilterAssetSelectionKey(token);
const isSelected = selectedAssets.includes(tokenKey);
return (
{
+ return token.networks.map((network) => toChainAssetKey(network.address, network.chain.id)).join('|');
+};
+
+export const marketFilterSelectionIncludesAsset = (selectionKey: string, address: string, chainId: number): boolean => {
+ return selectionKey.split('|').includes(toChainAssetKey(address, chainId));
+};
+
+const matchesTokenSelector = (token: MarketFilterAsset, selector: MarketFilterTokenSelector): boolean => {
+ if (selector.kind === 'symbol') {
+ return token.symbol.toLowerCase() === selector.symbol;
+ }
+
+ return token.networks.some((network) => network.chain.id === selector.chainId && network.address.toLowerCase() === selector.address);
+};
+
+export const resolveMarketFilterTokenSelectors = (selectors: MarketFilterTokenSelector[], items: MarketFilterAsset[]): string[] => {
+ const selectedKeys = new Set();
+
+ for (const selector of selectors) {
+ for (const item of items) {
+ if (matchesTokenSelector(item, selector)) {
+ selectedKeys.add(getMarketFilterAssetSelectionKey(item));
+ }
+ }
+ }
+
+ return [...selectedKeys];
+};
diff --git a/src/features/markets/market-filter-url-state.ts b/src/features/markets/market-filter-url-state.ts
new file mode 100644
index 00000000..64e4dad2
--- /dev/null
+++ b/src/features/markets/market-filter-url-state.ts
@@ -0,0 +1,100 @@
+import { SupportedNetworks, isSupportedNetwork } from '@/utils/supported-networks';
+
+type SearchParamsLike = Pick;
+
+export type MarketFilterUrlState = {
+ selectedNetwork?: SupportedNetworks | null;
+ signature: string | null;
+};
+
+const CLEAR_VALUES = new Set(['all', 'clear', 'none']);
+
+const NETWORK_ALIASES: Record = {
+ arb: SupportedNetworks.Arbitrum,
+ arbitrum: SupportedNetworks.Arbitrum,
+ base: SupportedNetworks.Base,
+ etherlink: SupportedNetworks.Etherlink,
+ ethereum: SupportedNetworks.Mainnet,
+ eth: SupportedNetworks.Mainnet,
+ 'hyper-evm': SupportedNetworks.HyperEVM,
+ hyperevm: SupportedNetworks.HyperEVM,
+ mainnet: SupportedNetworks.Mainnet,
+ monad: SupportedNetworks.Monad,
+ op: SupportedNetworks.Optimism,
+ optimism: SupportedNetworks.Optimism,
+ polygon: SupportedNetworks.Polygon,
+ unichain: SupportedNetworks.Unichain,
+};
+
+const REF_NETWORK_ALIASES: Record = {
+ etherlink: SupportedNetworks.Etherlink,
+};
+
+const normalizeParamValue = (value: string): string => value.trim().toLowerCase();
+
+export const resolveSupportedNetworkPreference = (rawValue: string): SupportedNetworks | null | undefined => {
+ const normalizedValue = normalizeParamValue(rawValue);
+
+ if (!normalizedValue) {
+ return undefined;
+ }
+
+ if (CLEAR_VALUES.has(normalizedValue)) {
+ return null;
+ }
+
+ if (/^\d+$/.test(normalizedValue)) {
+ const chainId = Number(normalizedValue);
+ return isSupportedNetwork(chainId) ? chainId : undefined;
+ }
+
+ return NETWORK_ALIASES[normalizedValue];
+};
+
+const parseExplicitNetworkPreference = (searchParams: SearchParamsLike): SupportedNetworks | null | undefined => {
+ for (const key of ['network', 'chain']) {
+ if (!searchParams.has(key)) {
+ continue;
+ }
+
+ const value = searchParams.get(key);
+ if (!value) {
+ continue;
+ }
+
+ const resolvedNetwork = resolveSupportedNetworkPreference(value);
+ if (resolvedNetwork !== undefined) {
+ return resolvedNetwork;
+ }
+ }
+
+ return undefined;
+};
+
+const resolveReferralNetworkPreference = (searchParams: SearchParamsLike): SupportedNetworks | undefined => {
+ const refValue = searchParams.get('ref');
+ if (!refValue) {
+ return undefined;
+ }
+
+ return REF_NETWORK_ALIASES[normalizeParamValue(refValue)];
+};
+
+const createMarketFilterUrlStateSignature = (state: Omit): string | null => {
+ if (state.selectedNetwork === undefined) {
+ return null;
+ }
+
+ return `network:${state.selectedNetwork ?? 'all'}`;
+};
+
+export const parseMarketFilterUrlState = (searchParams: SearchParamsLike): MarketFilterUrlState => {
+ const nextState: Omit = {
+ selectedNetwork: parseExplicitNetworkPreference(searchParams) ?? resolveReferralNetworkPreference(searchParams),
+ };
+
+ return {
+ ...nextState,
+ signature: createMarketFilterUrlStateSignature(nextState),
+ };
+};
diff --git a/src/features/markets/markets-view.tsx b/src/features/markets/markets-view.tsx
index 70c95fdd..643b1c94 100644
--- a/src/features/markets/markets-view.tsx
+++ b/src/features/markets/markets-view.tsx
@@ -1,12 +1,14 @@
'use client';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { Chain } from 'viem';
import Header from '@/components/layout/header/Header';
import { Breadcrumbs } from '@/components/shared/breadcrumbs';
+import { parseMarketFilterUrlState } from '@/features/markets/market-filter-url-state';
import { useFilteredMarkets } from '@/hooks/useFilteredMarkets';
import { useTokensQuery } from '@/hooks/queries/useTokensQuery';
import { useMarketsQuery } from '@/hooks/queries/useMarketsQuery';
+import { useMarketFilterPreferences } from '@/stores/useMarketFilterPreferences';
import { useMarketsFilters } from '@/stores/useMarketsFilters';
import { usePagination } from '@/hooks/usePagination';
import { useStyledToast } from '@/hooks/useStyledToast';
@@ -16,14 +18,21 @@ import type { ERC20Token, UnknownERC20Token } from '@/utils/tokens';
import { CompactFilterBar } from './components/filters/compact-filter-bar';
import MarketsTable from './components/table/markets-table';
-export default function Markets() {
+type MarketsProps = {
+ initialSearchParams: string;
+};
+
+export default function Markets({ initialSearchParams }: MarketsProps) {
const toast = useStyledToast();
+ const appliedUrlSignatureRef = useRef(null);
// Data fetching with React Query
const { data: rawMarkets, isLoading: loading, refetch } = useMarketsQuery();
const { markets, isLoading: filteredMarketsLoading, isWhitelistUnavailable } = useFilteredMarkets();
const filters = useMarketsFilters();
+ const persistedFilters = useMarketFilterPreferences();
+ const urlFilterState = useMemo(() => parseMarketFilterUrlState(new URLSearchParams(initialSearchParams)), [initialSearchParams]);
// UI state
const [uniqueCollaterals, setUniqueCollaterals] = useState<(ERC20Token | UnknownERC20Token)[]>([]);
@@ -115,13 +124,30 @@ export default function Markets() {
setUniqueLoanAssets(processTokens(loanList));
}, [rawMarkets, includeUnknownTokens, allTokens]);
+ useLayoutEffect(() => {
+ if (!urlFilterState.signature) {
+ appliedUrlSignatureRef.current = null;
+ return;
+ }
+
+ if (appliedUrlSignatureRef.current === urlFilterState.signature) {
+ return;
+ }
+
+ if (urlFilterState.selectedNetwork !== undefined && urlFilterState.selectedNetwork !== persistedFilters.selectedNetwork) {
+ persistedFilters.applySelections({ selectedNetwork: urlFilterState.selectedNetwork });
+ }
+
+ appliedUrlSignatureRef.current = urlFilterState.signature;
+ }, [persistedFilters, urlFilterState.selectedNetwork, urlFilterState.signature]);
+
// Reset page when filters change
useEffect(() => {
resetPage();
}, [
- filters.selectedNetwork,
- filters.selectedCollaterals,
- filters.selectedLoanAssets,
+ persistedFilters.selectedNetwork,
+ persistedFilters.selectedCollaterals,
+ persistedFilters.selectedLoanAssets,
filters.selectedOracles,
filters.searchQuery,
resetPage,
@@ -149,6 +175,11 @@ export default function Markets() {
refetch().then(() => toast.success('Markets refreshed', 'Markets refreshed successfully'));
}, [refetch, toast]);
+ const handleClearAll = useCallback(() => {
+ persistedFilters.reset();
+ filters.resetFilters();
+ }, [filters, persistedFilters]);
+
return (
<>
@@ -168,18 +199,18 @@ export default function Markets() {
diff --git a/src/hooks/useFilteredMarkets.ts b/src/hooks/useFilteredMarkets.ts
index b825c905..5b76c0ff 100644
--- a/src/hooks/useFilteredMarkets.ts
+++ b/src/hooks/useFilteredMarkets.ts
@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { useProcessedMarkets } from '@/hooks/useProcessedMarkets';
+import { useMarketFilterPreferences } from '@/stores/useMarketFilterPreferences';
import { useMorphoWhitelistStatusQuery } from '@/hooks/queries/useMorphoWhitelistStatusQuery';
import { useAllOracleMetadata } from '@/hooks/useOracleMetadata';
import { useMarketsFilters } from '@/stores/useMarketsFilters';
@@ -21,6 +22,7 @@ type UseFilteredMarketsResult = {
export const useFilteredMarkets = (): UseFilteredMarketsResult => {
const preferences = useMarketPreferences();
+ const persistedFilters = useMarketFilterPreferences();
const { allMarkets, whitelistedMarkets } = useProcessedMarkets();
const { whitelistLookup, isLoading: whitelistLoading, isFetching: whitelistFetching } = useMorphoWhitelistStatusQuery();
const { data: oracleMetadataMap } = useAllOracleMetadata();
@@ -41,12 +43,12 @@ export const useFilteredMarkets = (): UseFilteredMarketsResult => {
if (filteredMarkets.length === 0) return [];
filteredMarkets = filterMarkets(filteredMarkets, {
- selectedNetwork: filters.selectedNetwork,
+ selectedNetwork: persistedFilters.selectedNetwork,
showUnknownTokens: preferences.includeUnknownTokens,
showUnknownOracle: preferences.showUnknownOracle,
showLockedMarkets: preferences.showLockedMarkets,
- selectedCollaterals: filters.selectedCollaterals,
- selectedLoanAssets: filters.selectedLoanAssets,
+ selectedCollaterals: persistedFilters.selectedCollaterals,
+ selectedLoanAssets: persistedFilters.selectedLoanAssets,
selectedOracles: filters.selectedOracles,
usdFilters: {
minSupply: {
@@ -154,6 +156,7 @@ export const useFilteredMarkets = (): UseFilteredMarketsResult => {
shouldBlockWhitelistedFiltering,
showUnwhitelistedMarkets,
filters,
+ persistedFilters,
preferences,
trustedVaults,
findToken,
diff --git a/src/stores/useMarketFilterPreferences.ts b/src/stores/useMarketFilterPreferences.ts
new file mode 100644
index 00000000..8d2de3d4
--- /dev/null
+++ b/src/stores/useMarketFilterPreferences.ts
@@ -0,0 +1,58 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+import type { SupportedNetworks } from '@/utils/supported-networks';
+
+export type MarketFilterPreferencesState = {
+ selectedCollaterals: string[];
+ selectedLoanAssets: string[];
+ selectedNetwork: SupportedNetworks | null;
+};
+
+type MarketFilterPreferencesActions = {
+ applySelections: (state: Partial