diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index ff94f921b4..6cbb295b6b 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -11,7 +11,7 @@ use serde::Serialize; use std::sync::Arc; use std::time::SystemTime; use tokio::sync::RwLock; -use validator_client::models::SelectionChance; +use validator_client::models::{NodePerformance, SelectionChance}; #[derive(Clone, Debug, Serialize, JsonSchema, PartialEq)] #[serde(rename_all = "snake_case")] @@ -36,6 +36,7 @@ pub(crate) struct PrettyDetailedMixNodeBond { pub stake_saturation: f32, pub uncapped_saturation: f32, pub avg_uptime: u8, + pub node_performance: NodePerformance, pub estimated_operator_apy: f64, pub estimated_delegators_apy: f64, pub operating_cost: Coin, diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index c4483cf192..0cbc7b0167 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -153,6 +153,7 @@ impl ThreadsafeMixNodesCache { layer: node.mixnode_details.bond_information.layer, mix_node: node.mixnode_details.bond_information.mix_node.clone(), avg_uptime: node.performance.round_to_integer(), + node_performance: node.node_performance.clone(), stake_saturation: best_effort_small_dec_to_f64(node.stake_saturation) as f32, uncapped_saturation: best_effort_small_dec_to_f64(node.uncapped_stake_saturation) as f32, diff --git a/explorer/src/api/index.ts b/explorer/src/api/index.ts index 894dd3e179..f6a46f151d 100644 --- a/explorer/src/api/index.ts +++ b/explorer/src/api/index.ts @@ -62,6 +62,7 @@ export class Api { if (cachedMixnodes) { return cachedMixnodes; } + const res = await fetch(MIXNODES_API); const json = await res.json(); storeInCache('mixnodes', JSON.stringify(json)); diff --git a/explorer/src/components/DetailTable.tsx b/explorer/src/components/DetailTable.tsx index 80e7ce969b..d333f0eb59 100644 --- a/explorer/src/components/DetailTable.tsx +++ b/explorer/src/components/DetailTable.tsx @@ -1,5 +1,15 @@ import * as React from 'react'; -import { Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; +import { + Link, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TableCellProps, +} from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { Tooltip } from '@nymproject/react/tooltip/Tooltip'; import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; @@ -8,13 +18,13 @@ import { cellStyles } from './Universal-DataGrid'; import { unymToNym } from '../utils/currency'; import { GatewayEnrichedRowType } from './Gateways'; import { MixnodeRowType } from './MixNodes'; +import { StakeSaturationProgressBar } from './MixNodes/Economics/StakeSaturationProgressBar'; export type ColumnsType = { field: string; title: string; - headerAlign: string; - flex?: number; - width?: number; + headerAlign?: TableCellProps['align']; + width?: string | number; tooltipInfo?: string; }; @@ -50,6 +60,10 @@ function formatCellValues(val: string | number, field: string) { ); } + if (field === 'stake_saturation') { + return ; + } + return val; } @@ -61,11 +75,11 @@ export const DetailTable: FCWithChildren<{ const theme = useTheme(); return ( - +
- {columnsData?.map(({ field, title, flex, tooltipInfo }) => ( - + {columnsData?.map(({ field, title, width, tooltipInfo }) => ( + {tooltipInfo && ( diff --git a/explorer/src/components/MixNodes/Economics/Columns.ts b/explorer/src/components/MixNodes/Economics/Columns.ts index 3c20e0eebd..c3ac85baaf 100644 --- a/explorer/src/components/MixNodes/Economics/Columns.ts +++ b/explorer/src/components/MixNodes/Economics/Columns.ts @@ -4,57 +4,48 @@ export const EconomicsInfoColumns: ColumnsType[] = [ { field: 'estimatedTotalReward', title: 'Estimated Total Reward', - flex: 1, - headerAlign: 'left', + width: '15%', tooltipInfo: 'Estimated node reward (total for the operator and delegators) in the current epoch. There are roughly 24 epochs in a day.', }, { field: 'estimatedOperatorReward', title: 'Estimated Operator Reward', - flex: 1, - headerAlign: 'left', + width: '15%', tooltipInfo: "Estimated operator's reward (including PM and Operating Cost) in the current epoch. There are roughly 24 epochs in a day.", }, { field: 'selectionChance', title: 'Active Set Probability', - flex: 1, - headerAlign: 'left', + width: '12.5%', tooltipInfo: 'Probability of getting selected in the reward set (active and standby nodes) in the next epoch. The more your stake, the higher the chances to be selected.', }, - { - field: 'stakeSaturation', - title: 'Stake Saturation', - flex: 1, - headerAlign: 'left', - tooltipInfo: - 'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 730k NYM, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.', - }, { field: 'profitMargin', title: 'Profit Margin', - flex: 1, - headerAlign: 'left', + width: '12.5%', tooltipInfo: 'Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators.', }, { field: 'operatingCost', title: 'Operating Cost', - flex: 1, - headerAlign: 'left', + width: '10%', tooltipInfo: 'Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators.', }, { field: 'avgUptime', + title: 'Avg. Score', + width: '10%', + tooltipInfo: "Mixnode's average routing score in the last 24 hour", + }, + { + field: 'nodePerformance', title: 'Routing Score', - flex: 1, - headerAlign: 'left', tooltipInfo: - 'Node’s routing score is relative to that of the network. Each time a node is tested, the test packets have to go through the full path of the network (a gateway + 3 nodes). If a node in the path drop packets it will affect the score of other nodes in the test.', + "Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.", }, ]; diff --git a/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx b/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx index 9f8525a754..24db62d167 100644 --- a/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx +++ b/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx @@ -9,14 +9,14 @@ const parseToNumber = (value: number | undefined | string) => export const EconomicsProgress: FCWithChildren< LinearProgressProps & { threshold?: number; + color: string; } -> = ({ threshold, ...props }) => { +> = ({ threshold, color, ...props }) => { const theme = useTheme(); const { value } = props; const valueNumber: number = parseToNumber(value); const thresholdNumber: number = parseToNumber(threshold); - const percentageColor = valueNumber > (threshold || 100) ? 'warning' : 'inherit'; const percentageToDisplay = Math.min(valueNumber, thresholdNumber); return ( @@ -29,9 +29,9 @@ export const EconomicsProgress: FCWithChildren< ); diff --git a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx b/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx index aa3156ccf1..c9c82c0438 100644 --- a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx +++ b/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx @@ -14,9 +14,7 @@ const row: EconomicsInfoRowWithIndex = { selectionChance: { value: 'High', }, - avgUptime: { - value: '65 %', - }, + estimatedOperatorReward: { value: '80000.123456 NYM', }, @@ -29,9 +27,11 @@ const row: EconomicsInfoRowWithIndex = { operatingCost: { value: '11121 NYM', }, - stakeSaturation: { - value: '80 %', - progressBarValue: 80, + avgUptime: { + value: '-', + }, + nodePerformance: { + value: '-', }, }; @@ -55,9 +55,7 @@ const emptyRow: EconomicsInfoRowWithIndex = { value: '-', progressBarValue: 0, }, - avgUptime: { - value: '-', - }, + estimatedOperatorReward: { value: '-', }, @@ -70,9 +68,11 @@ const emptyRow: EconomicsInfoRowWithIndex = { operatingCost: { value: '-', }, - stakeSaturation: { + avgUptime: { + value: '-', + }, + nodePerformance: { value: '-', - progressBarValue: 0, }, }; diff --git a/explorer/src/components/MixNodes/Economics/Rows.ts b/explorer/src/components/MixNodes/Economics/Rows.ts index 61049a5ca3..a1387a03de 100644 --- a/explorer/src/components/MixNodes/Economics/Rows.ts +++ b/explorer/src/components/MixNodes/Economics/Rows.ts @@ -14,11 +14,15 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { currencyToString((economicDynamicsStats?.data?.estimated_total_node_reward || '').toString()) || '-'; const estimatedOperatorRewards = currencyToString((economicDynamicsStats?.data?.estimated_operator_reward || '').toString()) || '-'; - const stakeSaturation = economicDynamicsStats?.data?.uncapped_saturation || '-'; const profitMargin = mixNode?.data?.profit_margin_percent ? toPercentIntegerString(mixNode?.data?.profit_margin_percent) : '-'; - const avgUptime = economicDynamicsStats?.data?.current_interval_uptime; + const avgUptime = mixNode?.data?.node_performance + ? toPercentIntegerString(mixNode?.data?.node_performance.last_24h) + : '-'; + const nodePerformance = mixNode?.data?.node_performance + ? toPercentIntegerString(mixNode?.data?.node_performance.most_recent) + : '-'; const opCost = mixNode?.data?.operating_cost; @@ -33,10 +37,6 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { selectionChance: { value: selectionChance(economicDynamicsStats), }, - stakeSaturation: { - progressBarValue: typeof stakeSaturation === 'number' ? stakeSaturation * 100 : 0, - value: typeof stakeSaturation === 'number' ? `${(stakeSaturation * 100).toFixed(2)} %` : '-', - }, profitMargin: { value: profitMargin ? `${profitMargin} %` : '-', }, @@ -46,5 +46,8 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { avgUptime: { value: avgUptime ? `${avgUptime} %` : '-', }, + nodePerformance: { + value: nodePerformance, + }, }; }; diff --git a/explorer/src/components/MixNodes/Economics/StakeSaturationProgressBar.tsx b/explorer/src/components/MixNodes/Economics/StakeSaturationProgressBar.tsx new file mode 100644 index 0000000000..918d024643 --- /dev/null +++ b/explorer/src/components/MixNodes/Economics/StakeSaturationProgressBar.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Box, Typography } from '@mui/material'; +import { useIsMobile } from '../../../hooks/useIsMobile'; +import { EconomicsProgress } from './EconomicsProgress'; + +export const StakeSaturationProgressBar = ({ value, threshold }: { value: number; threshold: number }) => { + const isTablet = useIsMobile('lg'); + const percentageColor = value > (threshold || 100) ? 'warning' : 'inherit'; + const textColor = percentageColor === 'warning' ? 'warning.main' : 'nym.wallet.fee'; + + return ( + + + {value}% + + + + ); +}; diff --git a/explorer/src/components/MixNodes/Economics/Table.tsx b/explorer/src/components/MixNodes/Economics/Table.tsx index 929445254a..74bfa30251 100644 --- a/explorer/src/components/MixNodes/Economics/Table.tsx +++ b/explorer/src/components/MixNodes/Economics/Table.tsx @@ -1,70 +1,19 @@ import * as React from 'react'; import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material'; import { Box } from '@mui/system'; -import { useTheme, Theme } from '@mui/material/styles'; +import { useTheme } from '@mui/material/styles'; import { Tooltip } from '@nymproject/react/tooltip/Tooltip'; import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types'; -import { EconomicsProgress } from './EconomicsProgress'; -import { cellStyles } from '../../Universal-DataGrid'; import { UniversalTableProps } from '../../DetailTable'; -import { useIsMobile } from '../../../hooks/useIsMobile'; +import { textColour } from '../../../utils'; -const threshold = 100; - -const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => { - const progressBarValue = value?.progressBarValue || 0; - const fieldValue = value.value; - - if (progressBarValue > 100) { - return theme.palette.warning.main; - } - if (field === 'selectionChance') { - // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow - switch (fieldValue) { - case 'High': - case 'VeryHigh': - return theme.palette.nym.networkExplorer.selectionChance.overModerate; - case 'Good': - case 'Moderate': - return theme.palette.nym.networkExplorer.selectionChance.moderate; - case 'Low': - case 'VeryLow': - return theme.palette.nym.networkExplorer.selectionChance.underModerate; - default: - return theme.palette.nym.wallet.fee; - } - } - return theme.palette.nym.wallet.fee; -}; - -const formatCellValues = (value: EconomicsRowsType, field: string) => { - const isTablet = useIsMobile('lg'); - if (value.progressBarValue) { - return ( - - - {value.value} - - - - ); - } - return ( - - - {value.value} - - - ); -}; +const formatCellValues = (value: EconomicsRowsType, field: string) => ( + + + {value.value} + + +); export const DelegatorsInfoTable: FCWithChildren> = ({ tableName, @@ -78,21 +27,19 @@ export const DelegatorsInfoTable: FCWithChildren - {columnsData?.map(({ field, title, flex, tooltipInfo }) => ( - + {columnsData?.map(({ field, title, tooltipInfo, width }) => ( + {tooltipInfo && ( - - - + )} {title} @@ -106,19 +53,11 @@ export const DelegatorsInfoTable: FCWithChildren { const { field } = columnsData[index]; const value: EconomicsRowsType = (eachRow as any)[field]; - + console.log(value); return ( { const { mixNode, mixNodeRow, description, stats, status, uptimeStory, uniqDelegations } = useMixnodeContext(); - + console.log(mixNodeRow); return ( - <Grid container spacing={2} mt={1} mb={6}> <Grid item xs={12}> {mixNodeRow && description?.data && ( @@ -76,13 +75,11 @@ const PageMixnodeDetailWithState: FCWithChildren = () => { )} </Grid> </Grid> - <Grid container> <Grid item xs={12}> <DetailTable columnsData={columns} tableName="Mixnode detail table" rows={mixNodeRow ? [mixNodeRow] : []} /> </Grid> </Grid> - <Grid container spacing={2} mt={0}> <Grid item xs={12}> <DelegatorsInfoTable @@ -92,7 +89,6 @@ const PageMixnodeDetailWithState: FCWithChildren = () => { /> </Grid> </Grid> - <Grid container spacing={2} mt={0}> <Grid item xs={12}> <ContentCard title={`Stake Breakdown (${uniqDelegations?.data?.length} delegators)`}> @@ -100,7 +96,6 @@ const PageMixnodeDetailWithState: FCWithChildren = () => { </ContentCard> </Grid> </Grid> - <Grid container spacing={2} mt={0}> <Grid item xs={12} md={4}> <ContentCard title="Mixnode Stats"> @@ -144,7 +139,6 @@ const PageMixnodeDetailWithState: FCWithChildren = () => { )} </Grid> </Grid> - <Grid container spacing={2} mt={0}> <Grid item xs={12} md={4}> {status && ( diff --git a/explorer/src/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx index 50d85583b1..d42b99efef 100644 --- a/explorer/src/pages/Mixnodes/index.tsx +++ b/explorer/src/pages/Mixnodes/index.tsx @@ -168,7 +168,7 @@ export const PageMixnodes: FCWithChildren = () => { component={RRDLink} to={`/network-components/mixnode/${params.row.mix_id}`} > - {`${params.value.toFixed(2)} %`} + {`${params.value} %`} </MuiLink> ), }, @@ -238,13 +238,13 @@ export const PageMixnodes: FCWithChildren = () => { ), }, { - field: 'avg_uptime', + field: 'node_performance', headerName: 'Routing Score', disableColumnMenu: true, renderHeader: () => ( <CustomColumnHeading headingTitle="Routing Score" - tooltipInfo="Node’s routing score is relative to that of the network. Each time a node is tested, the test packets have to go through the full path of the network (a gateway + 3 nodes). If a node in the path drop packets it will affect the score of other nodes in the test." + tooltipInfo="Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test." /> ), headerClassName: 'MuiDataGrid-header-override', @@ -332,7 +332,6 @@ export const PageMixnodes: FCWithChildren = () => { const handlePageSize = (event: SelectChangeEvent<string>) => { setPageSize(event.target.value); }; - return ( <> <Title text="Mixnodes" /> diff --git a/explorer/src/typeDefs/explorer-api.ts b/explorer/src/typeDefs/explorer-api.ts index e430427aff..4d5b44854f 100644 --- a/explorer/src/typeDefs/explorer-api.ts +++ b/explorer/src/typeDefs/explorer-api.ts @@ -84,6 +84,7 @@ export interface MixNodeResponseItem { }; mix_node: MixNode; avg_uptime: number; + node_performance: NodePerformance; stake_saturation: number; uncapped_saturation: number; operating_cost: Amount; diff --git a/explorer/src/utils/index.ts b/explorer/src/utils/index.ts index 1065c000ae..4f922d870d 100644 --- a/explorer/src/utils/index.ts +++ b/explorer/src/utils/index.ts @@ -1,7 +1,9 @@ /* eslint-disable camelcase */ import { MutableRefObject } from 'react'; +import { Theme } from '@mui/material/styles'; import { registerLocale, getName } from 'i18n-iso-countries'; import { CountryData } from '../typeDefs/explorer-api'; +import { EconomicsRowsType } from '../components/MixNodes/Economics/types'; registerLocale(require('i18n-iso-countries/langs/en.json')); @@ -53,3 +55,29 @@ export const splice = (start: number, deleteCount: number, address?: string): st * @returns A stringified integer */ export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString(); + +export const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => { + const progressBarValue = value?.progressBarValue || 0; + const fieldValue = value.value; + + if (progressBarValue > 100) { + return theme.palette.warning.main; + } + if (field === 'selectionChance') { + // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow + switch (fieldValue) { + case 'High': + case 'VeryHigh': + return theme.palette.nym.networkExplorer.selectionChance.overModerate; + case 'Good': + case 'Moderate': + return theme.palette.nym.networkExplorer.selectionChance.moderate; + case 'Low': + case 'VeryLow': + return theme.palette.nym.networkExplorer.selectionChance.underModerate; + default: + return theme.palette.nym.wallet.fee; + } + } + return theme.palette.nym.wallet.fee; +};