Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/1441.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add more configuration options for paratimes
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ REACT_APP_STAGING_URLS=https://explorer.stg.oasis.io
REACT_APP_SHOW_BUILD_BANNERS=true
# REACT_APP_FIXED_NETWORK=testnet
# REACT_APP_FIXED_LAYER=sapphire
# REACT_APP_SKIP_GRAPH=true
REACT_APP_SHOW_FIAT_VALUES=true
1 change: 1 addition & 0 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ REACT_APP_STAGING_URLS=https://explorer.stg.oasis.io
REACT_APP_SHOW_BUILD_BANNERS=true
# REACT_APP_FIXED_NETWORK=testnet
# REACT_APP_FIXED_LAYER=sapphire
# REACT_APP_SKIP_GRAPH=true
REACT_APP_SHOW_FIAT_VALUES=true
2 changes: 1 addition & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import { Preview } from '@storybook/react'
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'
import '../src/locales/i18n'
import { withDefaultTheme } from '../src/app/components/ThemeByNetwork'
import { withDefaultTheme } from '../src/app/components/ThemeByScope'
import { initialize, mswLoader } from 'msw-storybook-addon'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { handlers } from '../internals/mocks/msw-handlers'
Expand Down
6 changes: 3 additions & 3 deletions src/app/components/AnalyticsConsent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Button from '@mui/material/Button'
import Link from '@mui/material/Link'
import { Trans, useTranslation } from 'react-i18next'
import * as matomo from './initializeMatomo'
import { ThemeByNetwork } from '../ThemeByNetwork'
import { ThemeByScope } from '../ThemeByScope'
import { Network } from '../../../types/network'
import { AnalyticsIsBlocked } from './AnalyticsIsBlocked'
import { AnalyticsDialogLayout } from './AnalyticsDialogLayout'
Expand Down Expand Up @@ -63,7 +63,7 @@ export const AnalyticsConsentProvider = (props: { children: React.ReactNode }) =
>
{props.children}
{/* Theme is needed because AnalyticsConsentProvider is outside network-themed routes */}
<ThemeByNetwork isRootTheme={false} network={Network.mainnet}>
<ThemeByScope isRootTheme={false} network={Network.mainnet}>
<AnalyticsConsentView
isOpen={hasAccepted === 'not-chosen'}
onAccept={async () => {
Expand All @@ -80,7 +80,7 @@ export const AnalyticsConsentProvider = (props: { children: React.ReactNode }) =
onReload={() => window.location.reload()}
onClose={() => setHasAccepted('timed_out_matomo_not_loaded')}
/>
</ThemeByNetwork>
</ThemeByScope>
</AnalyticsContext.Provider>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import { FC, ReactNode } from 'react'
import { Network } from '../../../types/network'
import { ThemeProvider } from '@mui/material/styles'
import { getThemesForNetworks } from '../../../styles/theme'
import { getThemeForScope } from '../../../styles/theme'
import CssBaseline from '@mui/material/CssBaseline'
import { fixedNetwork } from '../../utils/route-utils'
import { Layer } from '../../../oasis-nexus/api'

export const ThemeByNetwork: FC<{ network: Network; isRootTheme: boolean; children: React.ReactNode }> = ({
network,
isRootTheme,
children,
}) => (
<ThemeProvider theme={getThemesForNetworks()[network]}>
export const ThemeByScope: FC<{
network: Network
layer?: Layer
isRootTheme: boolean
children: React.ReactNode
}> = ({ network, layer, isRootTheme, children }) => (
<ThemeProvider theme={getThemeForScope(network, layer)}>
{isRootTheme && <CssBaseline />}
{children}
</ThemeProvider>
)

export const withDefaultTheme = (node: ReactNode, alwaysMainnet = false) => (
<ThemeByNetwork
<ThemeByScope
isRootTheme={true}
network={alwaysMainnet ? Network.mainnet : fixedNetwork ?? Network.mainnet}
>
{node}
</ThemeByNetwork>
</ThemeByScope>
)
6 changes: 3 additions & 3 deletions src/app/pages/HomePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useTranslation } from 'react-i18next'
import { ParaTimeSelectorStep } from './Graph/types'
import { BuildBanner } from '../../components/BuildBanner'
import { useSearchQueryNetworkParam } from '../../hooks/useSearchQueryNetworkParam'
import { ThemeByNetwork } from '../../components/ThemeByNetwork'
import { ThemeByScope } from '../../components/ThemeByScope'
import { NetworkOfflineBanner } from '../../components/OfflineBanner'
import { useIsApiReachable } from '../../components/OfflineBanner/hook'

Expand Down Expand Up @@ -168,7 +168,7 @@ export const HomePage: FC = () => {
</InfoScreenBtn>
)}
</SearchInputContainer>
<ThemeByNetwork isRootTheme={false} network={network}>
<ThemeByScope isRootTheme={false} network={network}>
<Box sx={{ zIndex: zIndexHomePage.paraTimeSelector }}>
<ParaTimeSelector
step={step}
Expand All @@ -179,7 +179,7 @@ export const HomePage: FC = () => {
onGraphZoomedIn={setIsGraphZoomedIn}
/>
</Box>
</ThemeByNetwork>
</ThemeByScope>
</Content>

<FooterStyled>
Expand Down
6 changes: 3 additions & 3 deletions src/app/pages/RoutingErrorPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import Divider from '@mui/material/Divider'
import { PageLayout } from '../../components/PageLayout'
import { ErrorDisplay } from '../../components/ErrorDisplay'
import { useRouteError } from 'react-router-dom'
import { ThemeByNetwork } from '../../components/ThemeByNetwork'
import { ThemeByScope } from '../../components/ThemeByScope'
import { useScopeParam } from '../../hooks/useScopeParam'
import { Network } from '../../../types/network'

export const RoutingErrorPage: FC = () => {
const scope = useScopeParam()
return (
<ThemeByNetwork isRootTheme={true} network={scope?.network ?? Network.mainnet}>
<ThemeByScope isRootTheme={true} network={scope?.network ?? Network.mainnet} layer={scope?.layer}>
<PageLayout>
<Divider variant="layout" />
<ErrorDisplay error={useRouteError()} />
</PageLayout>
</ThemeByNetwork>
</ThemeByScope>
)
}
5 changes: 2 additions & 3 deletions src/app/pages/SearchResultsPage/GlobalSearchResultsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
Network,
} from '../../../types/network'
import { HideMoreResults, ShowMoreResults } from './notifications'
import { getThemesForNetworks } from '../../../styles/theme'
import { getThemeForScope } from '../../../styles/theme'
import { orderByLayer } from '../../../types/layers'
import { useRedirectIfSingleResult } from './useRedirectIfSingleResult'
import { SearchParams } from '../../components/Search/search-utils'
Expand All @@ -30,7 +30,6 @@ export const GlobalSearchResultsView: FC<{
const [othersOpen, setOthersOpen] = useState(false)
useRedirectIfSingleResult(undefined, searchParams, searchResults)

const themes = getThemesForNetworks()
const networkNames = getNetworkNames(t)
const { searchTerm } = searchParams

Expand All @@ -51,7 +50,7 @@ export const GlobalSearchResultsView: FC<{
}

const otherNetworks = RouteUtils.getEnabledNetworks().filter(isNotMainnet)
const notificationTheme = themes[Network.testnet]
const notificationTheme = getThemeForScope(Network.testnet)
const mainnetResults = searchResults.filter(isOnMainnet).sort(orderByLayer)
const otherResults = searchResults.filter(isNotOnMainnet).sort(orderByLayer)

Expand Down
7 changes: 4 additions & 3 deletions src/app/pages/SearchResultsPage/ScopedSearchResultsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
getInverseFilterForScope,
SearchScope,
} from '../../../types/searchScope'
import { getThemesForNetworks } from '../../../styles/theme'
import { getThemeForScope } from '../../../styles/theme'
import { RouteUtils } from '../../utils/route-utils'
import { SearchResults } from './hooks'
import { SearchResultsList } from './SearchResultsList'
Expand All @@ -27,12 +27,13 @@ export const ScopedSearchResultsView: FC<{
const { t } = useTranslation()
const [othersOpen, setOthersOpen] = useState(false)
const networkNames = getNetworkNames(t)
const themes = getThemesForNetworks()
const isInWantedScope = getFilterForScope(wantedScope)
const isNotInWantedScope = getInverseFilterForScope(wantedScope)
const wantedResults = searchResults.filter(isInWantedScope)
const otherResults = searchResults.filter(isNotInWantedScope)
const notificationTheme = themes[otherResults.some(isOnMainnet) ? Network.mainnet : Network.testnet]
const notificationTheme = getThemeForScope(
otherResults.some(isOnMainnet) ? Network.mainnet : Network.testnet,
)

useRedirectIfSingleResult(wantedScope, searchParams, searchResults)

Expand Down
4 changes: 2 additions & 2 deletions src/app/pages/SearchResultsPage/SearchResultsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
TokenResult,
TransactionResult,
} from './hooks'
import { getThemesForNetworks } from '../../../styles/theme'
import { getThemeForScope } from '../../../styles/theme'
import { Network } from '../../../types/network'
import { SubPageCard } from '../../components/SubPageCard'
import { AllTokenPrices } from '../../../coin-gecko/api'
Expand Down Expand Up @@ -44,7 +44,7 @@ export const SearchResultsList: FC<{
if (!numberOfResults) {
return null
}
const theme = getThemesForNetworks()[networkForTheme]
const theme = getThemeForScope(networkForTheme)

return (
<ResultListFrame theme={theme}>
Expand Down
2 changes: 1 addition & 1 deletion src/app/utils/renderWithProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MemoryRouter } from 'react-router-dom'
import { render } from '@testing-library/react'
import { withDefaultTheme } from '../components/ThemeByNetwork'
import { withDefaultTheme } from '../components/ThemeByScope'
import React from 'react'
import { useIsApiReachable, useRuntimeFreshness } from '../components/OfflineBanner/hook'

Expand Down
108 changes: 67 additions & 41 deletions src/app/utils/route-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { AppError, AppErrors } from '../../types/errors'
import { EvmTokenType, Layer } from '../../oasis-nexus/api'
import { Network } from '../../types/network'
import { SearchScope } from '../../types/searchScope'
import { isStableDeploy } from '../../config'
import { isStableDeploy, specialScopePaths } from '../../config'
import { getSearchTermFromRequest } from '../components/Search/search-utils'
import type { HasLayer } from '../../types/layers'

export const fixedNetwork = process.env.REACT_APP_FIXED_NETWORK as Network | undefined
export const fixedLayer = process.env.REACT_APP_FIXED_LAYER as Layer | undefined
export const skipGraph = !!fixedLayer || !!(process.env.REACT_APP_SKIP_GRAPH as boolean | undefined)

export type ScopeFreedom =
| 'network' // We can select only the network
Expand All @@ -37,6 +38,33 @@ export type SpecifiedPerEnabledLayer<T = any, ExcludeLayers = never> = {

export type SpecifiedPerEnabledRuntime<T = any> = SpecifiedPerEnabledLayer<T, typeof Layer.consensus>

export const specialScopeRecognition: Partial<Record<string, Partial<Record<string, SearchScope>>>> = {}

function invertSpecialScopePaths() {
const networks = Object.keys(specialScopePaths) as Network[]

networks.forEach(network => {
const networkPaths = specialScopePaths[network]!
const layers = Object.keys(networkPaths) as Layer[]
layers.forEach(layer => {
const [word1, word2] = networkPaths[layer]!
if (!specialScopeRecognition[word1]) {
specialScopeRecognition[word1] = {}
}
if (specialScopeRecognition[word1]![word2]) {
const other = specialScopeRecognition[word1]![word2]!
console.warn(
`Wrong config: conflicting special scope paths ${word1}/${word2} definitions used both for ${other.network}/${other.layer} and ${network}/${layer} `,
)
} else {
specialScopeRecognition[word1]![word2] = { network, layer }
}
})
})
}

invertSpecialScopePaths()

export const hiddenLayers: Layer[] = [Layer.pontusxdev]

export const isLayerHidden = (layer: Layer): boolean => hiddenLayers.includes(layer)
Expand Down Expand Up @@ -65,38 +93,38 @@ export abstract class RouteUtils {
},
} satisfies Record<Network, Record<Layer, boolean>>

static getDashboardRoute = ({ network, layer }: SearchScope) => {
return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}`
static getScopeRoute = ({ network, layer }: SearchScope) => {
const specialPath = specialScopePaths[network]?.[layer]
const result = specialPath
? `/${specialPath[0]}/${specialPath[1]}`
: `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}`
return result
}

static getLatestTransactionsRoute = ({ network, layer }: SearchScope) => {
return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/tx`
static getDashboardRoute = (scope: SearchScope) => this.getScopeRoute(scope)

static getLatestTransactionsRoute = (scope: SearchScope) => {
return `${this.getScopeRoute(scope)}/tx`
}

static getTopTokensRoute = ({ network, layer }: SearchScope) => {
return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token`
static getTopTokensRoute = (scope: SearchScope) => {
return `${this.getScopeRoute(scope)}/token`
}

static getLatestBlocksRoute = ({ network, layer }: SearchScope) => {
return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/block`
static getLatestBlocksRoute = (scope: SearchScope) => {
return `${this.getScopeRoute(scope)}/block`
}

static getBlockRoute = ({ network, layer }: SearchScope, blockHeight: number) => {
return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/block/${encodeURIComponent(
blockHeight,
)}`
static getBlockRoute = (scope: SearchScope, blockHeight: number) => {
return `${this.getScopeRoute(scope)}/block/${encodeURIComponent(blockHeight)}`
}

static getTransactionRoute = (scope: SearchScope, txHash: string) => {
return `/${encodeURIComponent(scope.network)}/${encodeURIComponent(scope.layer)}/tx/${encodeURIComponent(
txHash,
)}`
return `${this.getScopeRoute(scope)}/tx/${encodeURIComponent(txHash)}`
}

static getAccountRoute = ({ network, layer }: SearchScope, account: string) => {
return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/address/${encodeURIComponent(
account,
)}`
static getAccountRoute = (scope: SearchScope, accountAddress: string) => {
return `${this.getScopeRoute(scope)}/address/${encodeURIComponent(accountAddress)}`
}

static getAccountsRoute = (network: Network) => {
Expand Down Expand Up @@ -128,28 +156,20 @@ export abstract class RouteUtils {

static getSearchRoute = (scope: SearchScope | undefined, searchTerm: string) => {
return scope
? `/${encodeURIComponent(scope.network)}/${encodeURIComponent(scope.layer)}/search?q=${encodeURIComponent(searchTerm)}`
? `${this.getScopeRoute(scope)}/search?q=${encodeURIComponent(searchTerm)}`
: `/search?q=${encodeURIComponent(searchTerm)}`
}

static getTokenRoute = ({ network, layer }: SearchScope, tokenAddress: string) => {
return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token/${encodeURIComponent(
tokenAddress,
)}`
static getTokenRoute = (scope: SearchScope, tokenAddress: string) => {
return `${this.getScopeRoute(scope)}/token/${encodeURIComponent(tokenAddress)}`
}

static getTokenHoldersRoute = ({ network, layer }: SearchScope, tokenAddress: string) => {
return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token/${encodeURIComponent(
tokenAddress,
)}/holders`
static getTokenHoldersRoute = (scope: SearchScope, tokenAddress: string) => {
return `${this.getScopeRoute(scope)}/token/${encodeURIComponent(tokenAddress)}/holders`
}

static getNFTInstanceRoute = (
{ network, layer }: SearchScope,
contractAddress: string,
instanceId: string,
): string =>
`/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token/${encodeURIComponent(
static getNFTInstanceRoute = (scope: SearchScope, contractAddress: string, instanceId: string): string =>
`${this.getScopeRoute(scope)}/token/${encodeURIComponent(
contractAddress,
)}/instance/${encodeURIComponent(instanceId)}`

Expand Down Expand Up @@ -315,19 +335,25 @@ export const runtimeTransactionParamLoader = async ({ params }: LoaderFunctionAr
return validateRuntimeTxHashParam(params.hash!)
}

export const assertEnabledScope = ({
network,
layer,
}: {
export const assertEnabledScope = (params: {
network: string | undefined
layer: string | undefined
}): SearchScope => {
if (!network || !RouteUtils.getEnabledNetworks().includes(network as Network)) {
const { network: networkLike, layer: layerLike } = params
if (!networkLike || !layerLike) {
throw new AppError(AppErrors.InvalidUrl)
}

const { network, layer } = specialScopeRecognition[networkLike]?.[layerLike] ?? {
network: networkLike as Network,
layer: layerLike as Layer,
}

if (!RouteUtils.getEnabledNetworks().includes(network as Network)) {
throw new AppError(AppErrors.InvalidUrl)
}

if (
!layer || // missing param
!RouteUtils.getAllLayersForNetwork(network as Network).enabled.includes(layer as Layer) // unsupported on network
) {
throw new AppError(AppErrors.UnsupportedLayer)
Expand Down
Loading