Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More configuration options for paratimes #1441

Merged
merged 10 commits into from
Aug 13, 2024
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'
csillag marked this conversation as resolved.
Show resolved Hide resolved
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
Loading