15c3012199
* explorer-api: add nym node endpoints + UI to show nym-nodes and account balances * explorer-api: add endpoints to get operator rewards explorer-ui: show delegations on nym-nodes, show operator rewards, bug fixes * explorer-ui: change summary screen to only show nym-node stats * explorer-api: add unstable routes to get legacy mixnodes and gateways from the contract instead of the Nym API explorer-ui: adapt front-end to show less information in legacy nodes with plain bond types * explorer-ui: fix up source of legacy mixnode data * explorer-ui: add more account page null and undefined checks * explorer-ui: filter out null gateway versions * explorer-ui: sanitise gateway versions * explorer-ui: add more guards on the balance parts to check that greater than 0 * explorer-api: make /tmp/unstable/gateways endpoint compatible with the current Harbour Master API * explorer-ui: fix typo * cargo fmt * Add node-id, total stake and links to nodes list --------- Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com> Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
298 lines
8.2 KiB
TypeScript
298 lines
8.2 KiB
TypeScript
'use client'
|
|
|
|
import * as React from 'react'
|
|
import { PaletteMode } from '@mui/material'
|
|
import {
|
|
ApiState,
|
|
BlockResponse,
|
|
CountryDataResponse,
|
|
GatewayResponse,
|
|
MixNodeResponse,
|
|
MixnodeStatus,
|
|
SummaryOverviewResponse,
|
|
ValidatorsResponse,
|
|
Environment,
|
|
DirectoryServiceProvider,
|
|
} from '@/app/typeDefs/explorer-api'
|
|
import { EnumFilterKey } from '@/app/typeDefs/filters'
|
|
import { Api, getEnvironment } from '@/app/api'
|
|
import { toPercentIntegerString } from '@/app/utils'
|
|
import { NavOptionType, originalNavOptions } from './nav'
|
|
|
|
interface StateData {
|
|
summaryOverview?: ApiState<SummaryOverviewResponse>
|
|
block?: ApiState<BlockResponse>
|
|
countryData?: ApiState<CountryDataResponse>
|
|
gateways?: ApiState<GatewayResponse>
|
|
globalError?: string | undefined
|
|
mixnodes?: ApiState<MixNodeResponse>
|
|
nodes?: ApiState<any>
|
|
mode: PaletteMode
|
|
validators?: ApiState<ValidatorsResponse>
|
|
environment?: Environment
|
|
serviceProviders?: ApiState<DirectoryServiceProvider[]>
|
|
}
|
|
|
|
interface StateApi {
|
|
fetchMixnodes: (
|
|
status?: MixnodeStatus
|
|
) => Promise<MixNodeResponse | undefined>
|
|
filterMixnodes: (filters: any, status: any) => void
|
|
fetchNodes: () => Promise<any>
|
|
fetchNodeById: (id: number) => Promise<any>
|
|
fetchAccountById: (accountAddr: string) => Promise<any>
|
|
toggleMode: () => void
|
|
}
|
|
|
|
type State = StateData & StateApi
|
|
|
|
export const MainContext = React.createContext<State>({
|
|
mode: 'dark',
|
|
toggleMode: () => undefined,
|
|
filterMixnodes: () => null,
|
|
fetchMixnodes: () => Promise.resolve(undefined),
|
|
fetchNodes: async () => undefined,
|
|
fetchNodeById: async () => undefined,
|
|
fetchAccountById: async () => undefined,
|
|
})
|
|
|
|
export const useMainContext = (): React.ContextType<typeof MainContext> =>
|
|
React.useContext<State>(MainContext)
|
|
|
|
export const MainContextProvider: FCWithChildren = ({ children }) => {
|
|
// network explorer environment
|
|
const [environment, setEnvironment] = React.useState<Environment>()
|
|
|
|
// light/dark mode
|
|
const [mode, setMode] = React.useState<PaletteMode>('dark')
|
|
|
|
// global / banner error messaging
|
|
const [globalError] = React.useState<string>()
|
|
|
|
// various APIs for Overview page
|
|
const [summaryOverview, setSummaryOverview] =
|
|
React.useState<ApiState<SummaryOverviewResponse>>()
|
|
const [nodes, setNodes] = React.useState<ApiState<any>>()
|
|
const [mixnodes, setMixnodes] = React.useState<ApiState<MixNodeResponse>>()
|
|
const [gateways, setGateways] = React.useState<ApiState<GatewayResponse>>()
|
|
const [validators, setValidators] =
|
|
React.useState<ApiState<ValidatorsResponse>>()
|
|
const [block, setBlock] = React.useState<ApiState<BlockResponse>>()
|
|
const [countryData, setCountryData] =
|
|
React.useState<ApiState<CountryDataResponse>>()
|
|
const [serviceProviders, setServiceProviders] =
|
|
React.useState<ApiState<DirectoryServiceProvider[]>>()
|
|
|
|
const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark'))
|
|
|
|
const fetchOverviewSummary = async () => {
|
|
try {
|
|
const data = await Api.fetchOverviewSummary()
|
|
setSummaryOverview({ data, isLoading: false })
|
|
} catch (error) {
|
|
setSummaryOverview({
|
|
error:
|
|
error instanceof Error
|
|
? error
|
|
: new Error('Overview summary api fail'),
|
|
isLoading: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
const fetchMixnodes = async () => {
|
|
let data
|
|
setMixnodes((d) => ({ ...d, isLoading: true }))
|
|
try {
|
|
data = await Api.fetchMixnodes()
|
|
setMixnodes({ data, isLoading: false })
|
|
} catch (error) {
|
|
setMixnodes({
|
|
error: error instanceof Error ? error : new Error('Mixnode api fail'),
|
|
isLoading: false,
|
|
})
|
|
}
|
|
return data
|
|
}
|
|
|
|
const filterMixnodes = async (
|
|
filters: { [key in EnumFilterKey]: number[] },
|
|
status?: MixnodeStatus
|
|
) => {
|
|
setMixnodes((d) => ({ ...d, isLoading: true }))
|
|
const mxns = await Api.fetchMixnodes()
|
|
|
|
const filtered = mxns?.filter(
|
|
(m) =>
|
|
+m.profit_margin_percent >= filters.profitMargin[0] / 100 &&
|
|
+m.profit_margin_percent <= filters.profitMargin[1] / 100 &&
|
|
m.stake_saturation >= filters.stakeSaturation[0] &&
|
|
m.stake_saturation <= filters.stakeSaturation[1] &&
|
|
m.avg_uptime >= filters.routingScore[0] &&
|
|
m.avg_uptime <= filters.routingScore[1]
|
|
)
|
|
|
|
setMixnodes({ data: filtered, isLoading: false })
|
|
}
|
|
|
|
const fetchGateways = async () => {
|
|
setGateways((d) => ({ ...d, isLoading: true }))
|
|
try {
|
|
const data = await Api.fetchGateways()
|
|
setGateways({ data, isLoading: false })
|
|
} catch (error) {
|
|
setGateways({
|
|
error: error instanceof Error ? error : new Error('Gateways api fail'),
|
|
isLoading: false,
|
|
})
|
|
}
|
|
}
|
|
const fetchValidators = async () => {
|
|
try {
|
|
const data = await Api.fetchValidators()
|
|
setValidators({ data, isLoading: false })
|
|
} catch (error) {
|
|
setValidators({
|
|
error:
|
|
error instanceof Error ? error : new Error('Validators api fail'),
|
|
isLoading: false,
|
|
})
|
|
}
|
|
}
|
|
const fetchBlock = async () => {
|
|
try {
|
|
const data = await Api.fetchBlock()
|
|
setBlock({ data, isLoading: false })
|
|
} catch (error) {
|
|
setBlock({
|
|
error: error instanceof Error ? error : new Error('Block api fail'),
|
|
isLoading: false,
|
|
})
|
|
}
|
|
}
|
|
const fetchCountryData = async () => {
|
|
setCountryData({ data: undefined, isLoading: true })
|
|
try {
|
|
const res = await Api.fetchCountryData()
|
|
setCountryData({ data: res, isLoading: false })
|
|
} catch (error) {
|
|
setCountryData({
|
|
error:
|
|
error instanceof Error ? error : new Error('Country Data api fail'),
|
|
isLoading: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
const fetchServiceProviders = async () => {
|
|
setServiceProviders({ data: undefined, isLoading: true })
|
|
try {
|
|
const res = await Api.fetchServiceProviders()
|
|
const resWithRoutingScorePercentage = res.map((item) => ({
|
|
...item,
|
|
routing_score: item.routing_score
|
|
? `${toPercentIntegerString(item.routing_score.toString())}%`
|
|
: item.routing_score,
|
|
}))
|
|
setServiceProviders({
|
|
data: resWithRoutingScorePercentage,
|
|
isLoading: false,
|
|
})
|
|
} catch (error) {
|
|
setServiceProviders({
|
|
error:
|
|
error instanceof Error
|
|
? error
|
|
: new Error('Service provider api fail'),
|
|
isLoading: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
const fetchNodes = async () => {
|
|
setNodes({ data: undefined, isLoading: true })
|
|
try {
|
|
const res = await Api.fetchNodes();
|
|
res.forEach((node: any) => node.total_stake =
|
|
Math.round(Number.parseFloat(node.rewarding_details?.operator || "0")
|
|
+ Number.parseFloat(node.rewarding_details?.delegates || "0"))
|
|
);
|
|
setNodes({
|
|
data: res.sort((a: any, b: any) => b.total_stake - a.total_stake),
|
|
isLoading: false,
|
|
})
|
|
} catch (error) {
|
|
setNodes({
|
|
error:
|
|
error instanceof Error
|
|
? error
|
|
: new Error('Service provider api fail'),
|
|
isLoading: false,
|
|
})
|
|
} };
|
|
|
|
const fetchNodeById = async (id: number) => {
|
|
const res = await Api.fetchNodeById(id);
|
|
return res;
|
|
};
|
|
|
|
const fetchAccountById = async (id: string) => {
|
|
const res = await Api.fetchAccountById(id);
|
|
return res;
|
|
};
|
|
|
|
React.useEffect(() => {
|
|
if (environment === 'mainnet') {
|
|
fetchServiceProviders()
|
|
}
|
|
}, [environment])
|
|
|
|
React.useEffect(() => {
|
|
setEnvironment(getEnvironment())
|
|
Promise.all([
|
|
fetchOverviewSummary(),
|
|
fetchGateways(),
|
|
fetchValidators(),
|
|
fetchBlock(),
|
|
fetchCountryData(),
|
|
])
|
|
}, [])
|
|
|
|
const state = React.useMemo<State>(
|
|
() => ({
|
|
environment,
|
|
block,
|
|
countryData,
|
|
gateways,
|
|
globalError,
|
|
mixnodes,
|
|
mode,
|
|
nodes,
|
|
summaryOverview,
|
|
validators,
|
|
serviceProviders,
|
|
toggleMode,
|
|
fetchMixnodes,
|
|
filterMixnodes,
|
|
fetchNodes,
|
|
fetchNodeById,
|
|
fetchAccountById,
|
|
}),
|
|
[
|
|
environment,
|
|
block,
|
|
countryData,
|
|
gateways,
|
|
globalError,
|
|
mixnodes,
|
|
mode,
|
|
nodes,
|
|
summaryOverview,
|
|
validators,
|
|
serviceProviders,
|
|
]
|
|
)
|
|
|
|
return <MainContext.Provider value={state}>{children}</MainContext.Provider>
|
|
}
|