diff --git a/app/markets/page.tsx b/app/markets/page.tsx index b3d979d0..8b03984b 100644 --- a/app/markets/page.tsx +++ b/app/markets/page.tsx @@ -1,5 +1,6 @@ import { generateMetadata } from '@/utils/generateMetadata'; import MarketContent from '@/features/markets/markets-view'; +import { serializeSearchParamsRecord, type SearchParamsRecord } from '@/utils/search-params'; export const metadata = generateMetadata({ title: 'Markets | Monarch', @@ -8,6 +9,11 @@ export const metadata = generateMetadata({ pathname: '', }); -export default function MarketPage() { - return ; +type MarketPageProps = { + searchParams: Promise; +}; + +export default async function MarketPage({ searchParams }: MarketPageProps) { + const resolvedSearchParams = await searchParams; + return ; } diff --git a/src/features/markets/components/filters/asset-filter.tsx b/src/features/markets/components/filters/asset-filter.tsx index ec819782..5462d35b 100644 --- a/src/features/markets/components/filters/asset-filter.tsx +++ b/src/features/markets/components/filters/asset-filter.tsx @@ -2,8 +2,9 @@ import { useState, useRef, useEffect } from 'react'; import { ChevronDownIcon, TrashIcon } from '@radix-ui/react-icons'; import Image from 'next/image'; +import { getMarketFilterAssetSelectionKey } from '@/features/markets/market-filter-selection'; import { cn } from '@/utils/components'; -import { type ERC20Token, type UnknownERC20Token, infoToKey } from '@/utils/tokens'; +import type { ERC20Token, UnknownERC20Token } from '@/utils/tokens'; type AssetFilterProps = { label: string; @@ -40,12 +41,10 @@ export default function AssetFilter({ return () => document.removeEventListener('mousedown', handleClickOutside); }, []); - const getTokenKey = (token: ERC20Token | UnknownERC20Token) => token.networks.map((n) => infoToKey(n.address, n.chain.id)).join('|'); - const toggleDropdown = () => setIsOpen(!isOpen); const selectOption = (token: ERC20Token | UnknownERC20Token) => { - const tokenKey = getTokenKey(token); + const tokenKey = getMarketFilterAssetSelectionKey(token); if (selectedAssets.includes(tokenKey)) { setSelectedAssets(selectedAssets.filter((asset) => asset !== tokenKey)); } else { @@ -67,7 +66,7 @@ export default function AssetFilter({ const newSelection = updateFromSearch .map((symbol) => items.find((item) => item.symbol.toLowerCase() === symbol.toLowerCase())) .filter(Boolean) - .map((token) => getTokenKey(token!)); + .map((token) => getMarketFilterAssetSelectionKey(token!)); setSelectedAssets(newSelection); } }, [updateFromSearch, items, setSelectedAssets]); @@ -115,7 +114,7 @@ export default function AssetFilter({ ) : selectedAssets.length > 0 ? (
{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) => void; + reset: () => void; + setSelectedCollaterals: (collaterals: string[]) => void; + setSelectedLoanAssets: (assets: string[]) => void; + setSelectedNetwork: (network: SupportedNetworks | null) => void; +}; + +type MarketFilterPreferencesStore = MarketFilterPreferencesState & MarketFilterPreferencesActions; + +const dedupeSelections = (values: string[]) => [...new Set(values)]; + +const DEFAULT_STATE: MarketFilterPreferencesState = { + selectedCollaterals: [], + selectedLoanAssets: [], + selectedNetwork: null, +}; + +export const useMarketFilterPreferences = create()( + persist( + (set) => ({ + ...DEFAULT_STATE, + + setSelectedCollaterals: (collaterals) => set({ selectedCollaterals: dedupeSelections(collaterals) }), + setSelectedLoanAssets: (assets) => set({ selectedLoanAssets: dedupeSelections(assets) }), + setSelectedNetwork: (network) => set({ selectedNetwork: network }), + applySelections: (state) => + set((currentState) => ({ + selectedNetwork: 'selectedNetwork' in state ? (state.selectedNetwork ?? null) : currentState.selectedNetwork, + selectedLoanAssets: + 'selectedLoanAssets' in state ? dedupeSelections(state.selectedLoanAssets ?? []) : currentState.selectedLoanAssets, + selectedCollaterals: + 'selectedCollaterals' in state ? dedupeSelections(state.selectedCollaterals ?? []) : currentState.selectedCollaterals, + })), + reset: () => set(DEFAULT_STATE), + }), + { + name: 'monarch_store_marketFilterPreferences', + version: 1, + migrate: (state) => { + if (!state || typeof state !== 'object') { + return DEFAULT_STATE; + } + return { ...DEFAULT_STATE, ...(state as Partial) }; + }, + }, + ), +); diff --git a/src/stores/useMarketsFilters.ts b/src/stores/useMarketsFilters.ts index dfb23fa6..667fc2a8 100644 --- a/src/stores/useMarketsFilters.ts +++ b/src/stores/useMarketsFilters.ts @@ -1,18 +1,14 @@ import { create } from 'zustand'; -import type { SupportedNetworks } from '@/utils/networks'; import type { PriceFeedVendors } from '@/utils/oracle'; /** - * Temporary filter state for markets page (resets on refresh for lightning-fast UX). + * Session-only filter state for the markets page. * - * Separation from useMarketPreferences: - * - useMarketPreferences: UI preferences (sort, view mode, USD thresholds, column visibility) - persisted - * - useMarketsFilters: Data filters (which assets/network to view, search query) - temporary + * Persisted market selections (network, loan asset, collateral) live in + * useMarketFilterPreferences. This store holds the transient filters that + * should reset naturally between sessions or when the user clears the page. */ type MarketsFiltersState = { - selectedCollaterals: string[]; - selectedLoanAssets: string[]; - selectedNetwork: SupportedNetworks | null; selectedOracles: PriceFeedVendors[]; searchQuery: string; trendingMode: boolean; // Official trending filter (backend-computed) @@ -21,9 +17,6 @@ type MarketsFiltersState = { }; type MarketsFiltersActions = { - setSelectedCollaterals: (collaterals: string[]) => void; - setSelectedLoanAssets: (assets: string[]) => void; - setSelectedNetwork: (network: SupportedNetworks | null) => void; setSelectedOracles: (oracles: PriceFeedVendors[]) => void; setSearchQuery: (query: string) => void; toggleTrendingMode: () => void; @@ -35,9 +28,6 @@ type MarketsFiltersActions = { type MarketsFiltersStore = MarketsFiltersState & MarketsFiltersActions; const DEFAULT_STATE: MarketsFiltersState = { - selectedCollaterals: [], - selectedLoanAssets: [], - selectedNetwork: null, selectedOracles: [], searchQuery: '', trendingMode: false, @@ -50,16 +40,12 @@ const DEFAULT_STATE: MarketsFiltersState = { * * @example * ```tsx - * const { selectedCollaterals, setSelectedCollaterals } = useMarketsFilters(); * const { searchQuery, setSearchQuery, resetFilters } = useMarketsFilters(); * ``` */ export const useMarketsFilters = create()((set) => ({ ...DEFAULT_STATE, - setSelectedCollaterals: (collaterals) => set({ selectedCollaterals: [...new Set(collaterals)] }), - setSelectedLoanAssets: (assets) => set({ selectedLoanAssets: [...new Set(assets)] }), - setSelectedNetwork: (network) => set({ selectedNetwork: network }), setSelectedOracles: (oracles) => set({ selectedOracles: oracles }), setSearchQuery: (query) => set({ searchQuery: query }), toggleTrendingMode: () => set((state) => ({ trendingMode: !state.trendingMode })), diff --git a/src/utils/chain-asset-key.ts b/src/utils/chain-asset-key.ts new file mode 100644 index 00000000..aff23359 --- /dev/null +++ b/src/utils/chain-asset-key.ts @@ -0,0 +1,3 @@ +export const toChainAssetKey = (address: string, chainId: number): string => { + return `${address.toLowerCase()}-${chainId}`; +}; diff --git a/src/utils/marketFilters.ts b/src/utils/marketFilters.ts index edf3a50f..df02b551 100644 --- a/src/utils/marketFilters.ts +++ b/src/utils/marketFilters.ts @@ -7,6 +7,7 @@ import { LOCKED_MARKET_APY_THRESHOLD } from '@/constants/markets'; import type { OracleMetadataRecord } from '@/hooks/useOracleMetadata'; +import { marketFilterSelectionIncludesAsset } from '@/features/markets/market-filter-selection'; import { parseNumericThreshold } from '@/utils/markets'; import type { SupportedNetworks } from '@/utils/networks'; import { type PriceFeedVendors, getOracleType, getOracleVendorInfo, OracleType } from '@/utils/oracle'; @@ -135,8 +136,8 @@ export const createCollateralFilter = (selectedCollaterals: string[]): MarketFil return () => true; } return (market) => { - return selectedCollaterals.some((combinedKey) => - combinedKey.split('|').includes(`${market.collateralAsset.address.toLowerCase()}-${market.morphoBlue.chain.id}`), + return selectedCollaterals.some((selectionKey) => + marketFilterSelectionIncludesAsset(selectionKey, market.collateralAsset.address, market.morphoBlue.chain.id), ); }; }; @@ -149,8 +150,8 @@ export const createLoanAssetFilter = (selectedLoanAssets: string[]): MarketFilte return () => true; } return (market) => { - return selectedLoanAssets.some((combinedKey) => - combinedKey.split('|').includes(`${market.loanAsset.address.toLowerCase()}-${market.morphoBlue.chain.id}`), + return selectedLoanAssets.some((selectionKey) => + marketFilterSelectionIncludesAsset(selectionKey, market.loanAsset.address, market.morphoBlue.chain.id), ); }; }; diff --git a/src/utils/networks.ts b/src/utils/networks.ts index a9e0bb3f..246d4892 100644 --- a/src/utils/networks.ts +++ b/src/utils/networks.ts @@ -10,8 +10,10 @@ import { unichain, hyperEvm as hyperEvmOld, } from 'viem/chains'; +import { isSupportedNetwork as isSupportedNetworkValue, SupportedNetworks as SupportedNetworkId } from './supported-networks'; import { v2AgentsBase } from './monarch-agent'; import type { AgentMetadata } from './types'; +export { ALL_SUPPORTED_NETWORKS, SupportedNetworks, isSupportedNetwork } from './supported-networks'; const alchemyKey = process.env.NEXT_PUBLIC_ALCHEMY_API_KEY; const rpcPriority = process.env.NEXT_PUBLIC_RPC_PRIORITY; @@ -37,30 +39,6 @@ const getRpcUrl = (specificRpcUrl: string | undefined, alchemySubdomain?: string return targetRpc ?? alchemyUrl ?? ''; }; -export enum SupportedNetworks { - Mainnet = 1, - Optimism = 10, - Base = 8453, - Polygon = 137, - Unichain = 130, - Arbitrum = 42_161, - Etherlink = 42_793, - HyperEVM = 999, - Monad = 143, -} - -export const ALL_SUPPORTED_NETWORKS = [ - SupportedNetworks.Mainnet, - SupportedNetworks.Optimism, - SupportedNetworks.Base, - SupportedNetworks.Polygon, - SupportedNetworks.Unichain, - SupportedNetworks.Arbitrum, - SupportedNetworks.Etherlink, - SupportedNetworks.HyperEVM, - SupportedNetworks.Monad, -]; - // use hyperevm as custom chain export const hyperEvm = defineChain({ ...hyperEvmOld, @@ -80,7 +58,7 @@ type VaultAgentConfig = { }; type NetworkConfig = { - network: SupportedNetworks; + network: SupportedNetworkId; logo: string; name: string; chain: Chain; @@ -101,7 +79,7 @@ type NetworkConfig = { export const networks: NetworkConfig[] = [ { - network: SupportedNetworks.Mainnet, + network: SupportedNetworkId.Mainnet, logo: require('../imgs/chains/eth.svg') as string, name: 'Mainnet', chain: mainnet, @@ -112,7 +90,7 @@ export const networks: NetworkConfig[] = [ wrappedNativeToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', }, { - network: SupportedNetworks.Optimism, + network: SupportedNetworkId.Optimism, logo: require('../imgs/chains/op.svg') as string, name: 'Optimism', chain: optimism, @@ -123,7 +101,7 @@ export const networks: NetworkConfig[] = [ wrappedNativeToken: '0x4200000000000000000000000000000000000006', }, { - network: SupportedNetworks.Base, + network: SupportedNetworkId.Base, logo: require('../imgs/chains/base.webp') as string, name: 'Base', chain: base, @@ -140,7 +118,7 @@ export const networks: NetworkConfig[] = [ wrappedNativeToken: '0x4200000000000000000000000000000000000006', }, { - network: SupportedNetworks.Polygon, + network: SupportedNetworkId.Polygon, chain: polygon, logo: require('../imgs/chains/polygon.png') as string, name: 'Polygon', @@ -152,7 +130,7 @@ export const networks: NetworkConfig[] = [ wrappedNativeToken: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', }, { - network: SupportedNetworks.Unichain, + network: SupportedNetworkId.Unichain, chain: unichain, logo: require('../imgs/chains/unichain.svg') as string, defaultRPC: getRpcUrl(process.env.NEXT_PUBLIC_UNICHAIN_RPC, 'unichain-mainnet'), @@ -163,7 +141,7 @@ export const networks: NetworkConfig[] = [ wrappedNativeToken: '0x4200000000000000000000000000000000000006', }, { - network: SupportedNetworks.Arbitrum, + network: SupportedNetworkId.Arbitrum, chain: arbitrum, logo: require('../imgs/chains/arbitrum.png') as string, name: 'Arbitrum', @@ -174,7 +152,7 @@ export const networks: NetworkConfig[] = [ wrappedNativeToken: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', }, { - network: SupportedNetworks.Etherlink, + network: SupportedNetworkId.Etherlink, chain: etherlinkChain, logo: require('../imgs/chains/etherlink.svg') as string, name: 'Etherlink', @@ -186,7 +164,7 @@ export const networks: NetworkConfig[] = [ wrappedNativeToken: '0xc9B53AB2679f573e480d01e0f49e2B5CFB7a3EAb', }, { - network: SupportedNetworks.HyperEVM, + network: SupportedNetworkId.HyperEVM, chain: hyperEvm, logo: require('../imgs/chains/hyperevm.png') as string, name: 'HyperEVM', @@ -198,7 +176,7 @@ export const networks: NetworkConfig[] = [ explorerUrl: 'https://hyperevmscan.io', }, { - network: SupportedNetworks.Monad, + network: SupportedNetworkId.Monad, chain: monad, logo: require('../imgs/chains/monad.svg') as string, name: 'Monad', @@ -212,26 +190,26 @@ export const networks: NetworkConfig[] = [ ]; export const isSupportedChain = (chainId: number) => { - return Object.values(SupportedNetworks).includes(chainId); + return isSupportedNetworkValue(chainId); }; -export const getNetworkConfig = (chainId: SupportedNetworks): NetworkConfig => { +export const getNetworkConfig = (chainId: SupportedNetworkId): NetworkConfig => { return networks.find((network) => network.network === chainId) as NetworkConfig; }; -export const getViemChain = (chainId: SupportedNetworks): Chain => { +export const getViemChain = (chainId: SupportedNetworkId): Chain => { return getNetworkConfig(chainId).chain; }; -export const getDefaultRPC = (chainId: SupportedNetworks): string => { +export const getDefaultRPC = (chainId: SupportedNetworkId): string => { return getNetworkConfig(chainId).defaultRPC; }; -export const getBlocktime = (chainId: SupportedNetworks): number => { +export const getBlocktime = (chainId: SupportedNetworkId): number => { return getNetworkConfig(chainId).blocktime; }; -export const getMaxBlockDelay = (chainId: SupportedNetworks): number => { +export const getMaxBlockDelay = (chainId: SupportedNetworkId): number => { return getNetworkConfig(chainId).maxBlockDelay || 0; }; @@ -242,7 +220,7 @@ export const isAgentAvailable = (chainId: number): boolean => { return true; }; -export const getAgentConfig = (chainId: SupportedNetworks): VaultAgentConfig | undefined => { +export const getAgentConfig = (chainId: SupportedNetworkId): VaultAgentConfig | undefined => { const network = getNetworkConfig(chainId); return network?.vaultConfig; }; @@ -257,15 +235,15 @@ export const getNetworkName = (chainId: number) => { return target?.name; }; -export const getExplorerUrl = (chainId: SupportedNetworks): string => { +export const getExplorerUrl = (chainId: SupportedNetworkId): string => { return getNetworkConfig(chainId).explorerUrl ?? 'https://etherscan.io'; }; -export const getNativeTokenSymbol = (chainId: SupportedNetworks): string => { +export const getNativeTokenSymbol = (chainId: SupportedNetworkId): string => { return getNetworkConfig(chainId).nativeTokenSymbol ?? 'ETH'; }; -export const getWrappedNativeToken = (chainId: SupportedNetworks): Address | undefined => { +export const getWrappedNativeToken = (chainId: SupportedNetworkId): Address | undefined => { return getNetworkConfig(chainId).wrappedNativeToken; }; diff --git a/src/utils/search-params.ts b/src/utils/search-params.ts new file mode 100644 index 00000000..814bf789 --- /dev/null +++ b/src/utils/search-params.ts @@ -0,0 +1,22 @@ +export type SearchParamsRecord = Record; + +export const serializeSearchParamsRecord = (searchParams: SearchParamsRecord): string => { + const nextSearchParams = new URLSearchParams(); + + for (const [key, value] of Object.entries(searchParams)) { + if (value === undefined) { + continue; + } + + if (Array.isArray(value)) { + for (const entry of value) { + nextSearchParams.append(key, entry); + } + continue; + } + + nextSearchParams.set(key, value); + } + + return nextSearchParams.toString(); +}; diff --git a/src/utils/supported-networks.ts b/src/utils/supported-networks.ts new file mode 100644 index 00000000..8d765caa --- /dev/null +++ b/src/utils/supported-networks.ts @@ -0,0 +1,27 @@ +export enum SupportedNetworks { + Mainnet = 1, + Optimism = 10, + Base = 8453, + Polygon = 137, + Unichain = 130, + Arbitrum = 42_161, + Etherlink = 42_793, + HyperEVM = 999, + Monad = 143, +} + +export const ALL_SUPPORTED_NETWORKS = [ + SupportedNetworks.Mainnet, + SupportedNetworks.Optimism, + SupportedNetworks.Base, + SupportedNetworks.Polygon, + SupportedNetworks.Unichain, + SupportedNetworks.Arbitrum, + SupportedNetworks.Etherlink, + SupportedNetworks.HyperEVM, + SupportedNetworks.Monad, +]; + +export const isSupportedNetwork = (chainId: number): chainId is SupportedNetworks => { + return ALL_SUPPORTED_NETWORKS.includes(chainId as SupportedNetworks); +}; diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index 7bac3b1f..9399c807 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -1,4 +1,5 @@ import { type Chain, arbitrum, base, etherlink, mainnet, monad, optimism, polygon, unichain } from 'viem/chains'; +import { toChainAssetKey } from './chain-asset-key'; import { getWrappedNativeToken, hyperEvm } from './networks'; export type TokenSource = 'local' | 'external' | 'unknown'; @@ -1004,9 +1005,7 @@ const findToken = (address: string, chainId: number) => { ); }; -const infoToKey = (address: string, chainId: number) => { - return `${address.toLowerCase()}-${chainId}`; -}; +const infoToKey = toChainAssetKey; const findTokenWithKey = (key: string) => { // key: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48-1|0x833589fcd6edb6e08f4c7c32d4f71b54bda02913-8453'