334 lines
12 KiB
JavaScript
334 lines
12 KiB
JavaScript
import { useEffect, useState, useMemo } from "react";
|
|
|
|
import {
|
|
Box,
|
|
Paper,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Typography,
|
|
Link,
|
|
LinearProgress,
|
|
} from "@mui/material";
|
|
import { useTheme } from "@mui/material/styles";
|
|
|
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
|
import WarningIcon from '@mui/icons-material/Warning';
|
|
import CancelIcon from '@mui/icons-material/Cancel';
|
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
|
|
import GhostStyledIcon from "../../components/Icon/GhostIcon";
|
|
import InfoTooltip from "../../components/Tooltip/InfoTooltip";
|
|
import { PrimaryButton } from "../../components/Button";
|
|
|
|
import { GHOST_CONNECT } from "../../constants/ecosystem";
|
|
|
|
export const ValidatorTable = ({
|
|
currentTime,
|
|
currentBlock,
|
|
latestCommits,
|
|
isVerySmallScreen,
|
|
bridgeStability,
|
|
providerDetail,
|
|
}) => {
|
|
const theme = useTheme();
|
|
const [sortedBy, setsortedBy] = useState(undefined);
|
|
|
|
const stabilityColor = useMemo(() => {
|
|
const red = Math.round(255 * (1 - bridgeStability / 100));
|
|
const green = Math.round(255 * (bridgeStability / 100));
|
|
return `rgb(${red}, ${green}, 0)`;
|
|
}, [bridgeStability]);
|
|
|
|
const sortedCommits = useMemo(() => {
|
|
return latestCommits?.sort((a, b) => {
|
|
let one = 0;
|
|
let two = 0;
|
|
switch (sortedBy) {
|
|
case -1:
|
|
one = Number(b.lastUpdated ?? 0n);
|
|
two = Number(a.lastUpdated ?? 0n);
|
|
break;
|
|
case 1:
|
|
one = Number(a.lastUpdated ?? 0n);
|
|
two = Number(b.lastUpdated ?? 0n);
|
|
break;
|
|
case -2:
|
|
one = Number(b.lastStoredBlock ?? 0n);
|
|
two = Number(a.lastStoredBlock ?? 0n);
|
|
break;
|
|
case 2:
|
|
one = Number(a.lastStoredBlock ?? 0n);
|
|
two = Number(b.lastStoredBlock ?? 0n);
|
|
break;
|
|
case -3:
|
|
one = Number(b.lastStoredBlock ?? 0n);
|
|
two = Number(a.lastStoredBlock ?? 0n);
|
|
break;
|
|
case 3:
|
|
one = Number(a.lastStoredBlock ?? 0n);
|
|
two = Number(b.lastStoredBlock ?? 0n);
|
|
break;
|
|
case -4:
|
|
one = b.disabled ? 1 : 0;
|
|
two = a.disabled ? 1 : 0;
|
|
break;
|
|
case 4:
|
|
one = a.disabled ? 1 : 0;
|
|
two = b.disabled ? 1 : 0;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return one - two;
|
|
}) ?? [];
|
|
}, [latestCommits, sortedBy]);
|
|
|
|
const changeSort = (prevValue, newValue) => {
|
|
if (prevValue === undefined) {
|
|
return newValue;
|
|
}
|
|
|
|
const prevValueAbs = Math.abs(prevValue);
|
|
const newValueAbs = Math.abs(newValue);
|
|
|
|
if (prevValueAbs === newValueAbs) {
|
|
return prevValue * -1;
|
|
} else {
|
|
return newValue;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Box width="100%" height="340px" display="flex" flexDirection="column" justifyContent="space-between" gap="10px">
|
|
{!providerDetail && <Box sx={{ borderRadius: "15px", background: theme.colors.paper.background }} width="100%" height="100%" display="flex" justifyContent="center">
|
|
<Box padding="20px 30px" display="flex" flexDirection="column" justifyContent="space-around" alignItems="center">
|
|
<Typography sx={{ textAlign: "center" }} variant="h6">GHOST Connect is not detected on your browser!</Typography>
|
|
<Typography sx={{ textAlign: "center" }} variant="body2">Download GHOST Connect browser extension for real-time visibility into validator status and related transaction risks.</Typography>
|
|
<Typography sx={{ textAlign: "center" }} variant="body2"><b>Important:</b> The GHOST Connect is optional, but be aware that your bridge transaction will succeed or fail irreversibly based on the condition of the validators.</Typography>
|
|
<PrimaryButton onClick={() => window.open(GHOST_CONNECT, '_blank', 'noopener,noreferrer')}>
|
|
Get GHOST Connect
|
|
</PrimaryButton>
|
|
</Box>
|
|
</Box>}
|
|
{providerDetail && <TableContainer
|
|
component={Paper}
|
|
className="custom-scrollbar"
|
|
sx={{
|
|
height: "310px",
|
|
overflowY: 'scroll',
|
|
msOverflowStyle: "thin !important",
|
|
scrollbarWidth: "thin !important",
|
|
}}
|
|
>
|
|
<Table sx={{ marginTop: "0px" }} stickyHeader aria-label="sticky available validators">
|
|
<TableHead>
|
|
<TableRow sx={{ height: "40px" }}>
|
|
<BridgeHeaderTableCell index={0} changeSort={() => setsortedBy(undefined)} value="Validator" background={theme.colors.paper.cardHover} borderTopLeftRadius="3px" />
|
|
<BridgeHeaderTableCell index={1} sortedBy={sortedBy} changeSort={() => setsortedBy(prev => changeSort(prev, 1))} value="Last Acitve" background={theme.colors.paper.cardHover} tooltip="GHOST Validators must submit block commitments every 10 minutes to remain active." />
|
|
<BridgeHeaderTableCell index={2} sortedBy={sortedBy} changeSort={() => setsortedBy(prev => changeSort(prev, 2))} value="Block Height" background={theme.colors.paper.cardHover} tooltip="The latest EVM block height reported by the Validator." />
|
|
<BridgeHeaderTableCell index={3} sortedBy={sortedBy} changeSort={() => setsortedBy(prev => changeSort(prev, 3))} value="Block Delayed" background={theme.colors.paper.cardHover} tooltip="Block delays under 4 hours are safe. Block delays over 4 hours risk bridge transaction failure." />
|
|
<BridgeHeaderTableCell index={4} sortedBy={sortedBy} changeSort={() => setsortedBy(prev => changeSort(prev, 4))} value="Status" background={theme.colors.paper.cardHover} borderTopRightRadius="3px" tooltip="Active and disabled validators fore the current GHOST Epoch." />
|
|
</TableRow>
|
|
</TableHead>
|
|
|
|
<TableBody>
|
|
{latestCommits?.map((commit, index) => {
|
|
return (
|
|
<ValidatorRow
|
|
key={index}
|
|
colors={theme.colors}
|
|
currentTime={currentTime}
|
|
currentBlock={currentBlock}
|
|
index={index}
|
|
commit={commit}
|
|
/>
|
|
)
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>}
|
|
<Box marginTop="5px" display="flex" flexDirection="row" justifyContent="center" alignItems="center">
|
|
<Link
|
|
underline="always"
|
|
fontSize="12px"
|
|
lineHeight="15px"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
href="https://telemetry.ghostchain.io"
|
|
>
|
|
See Validator Telemetry
|
|
</Link>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
const BridgeHeaderTableCell = ({
|
|
index=0,
|
|
sortedBy=undefined,
|
|
changeSort,
|
|
align="center",
|
|
borderTopRightRadius="0px",
|
|
borderTopLeftRadius="0px",
|
|
borderBottomRightRadius="0px",
|
|
borderBottomLeftRadius="0px",
|
|
background="transparent",
|
|
fontSize="12px",
|
|
padding="0px",
|
|
tooltip,
|
|
value
|
|
}) => {
|
|
return (
|
|
<TableCell
|
|
align={align}
|
|
style={{
|
|
borderTopRightRadius,
|
|
borderTopLeftRadius,
|
|
borderBottomRightRadius,
|
|
borderBottomLeftRadius,
|
|
background,
|
|
fontSize,
|
|
padding,
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
<Box onClick={changeSort} display="flex" justifyContent="center" alignItems="center">
|
|
{value}
|
|
{tooltip
|
|
? Math.abs(sortedBy) !== index
|
|
? <InfoTooltip message={tooltip} />
|
|
: <GhostStyledIcon
|
|
sx={{ width: "12px", height: "12px" }}
|
|
viewBox="0 0 25 25"
|
|
component={sortedBy > 0 ? KeyboardArrowDownIcon : KeyboardArrowUpIcon}
|
|
/>
|
|
: <></>
|
|
}
|
|
</Box>
|
|
</TableCell>
|
|
)
|
|
}
|
|
|
|
const BridgeTableCell = ({
|
|
align="center",
|
|
borderTopRightRadius="0px",
|
|
borderTopLeftRadius="0px",
|
|
borderBottomRightRadius="0px",
|
|
borderBottomLeftRadius="0px",
|
|
background="transparent",
|
|
fontSize="10px",
|
|
padding="0px",
|
|
value
|
|
}) => {
|
|
return (
|
|
<TableCell
|
|
align={align}
|
|
style={{
|
|
borderTopRightRadius,
|
|
borderTopLeftRadius,
|
|
borderBottomRightRadius,
|
|
borderBottomLeftRadius,
|
|
background,
|
|
fontSize,
|
|
padding,
|
|
}}
|
|
>
|
|
{value}
|
|
</TableCell>
|
|
)
|
|
}
|
|
|
|
const ValidatorRow = ({
|
|
colors,
|
|
index,
|
|
currentTime,
|
|
currentBlock,
|
|
commit,
|
|
}) => {
|
|
const background = index % 2 === 1 ? "" : colors.gray[750];
|
|
return (
|
|
<TableRow
|
|
sx={{ height: "30px" }}
|
|
id={index + `--vaidator`}
|
|
data-testid={index + `--vaidator`}
|
|
>
|
|
<BridgeTableCell align="center" value={sliceString(commit.validator, 10, 45)} background={background} />
|
|
<BridgeTableCell align="center" value={getTimeAgo(currentTime - (Number(commit?.lastUpdated) ?? 0))} background={background} />
|
|
<BridgeTableCell align="center" value={(commit?.lastStoredBlock ?? 0n).toLocaleString('en-US')} background={background} />
|
|
<BridgeTableCell align="center" value={blockDelayIcon(colors, currentBlock - (commit?.lastStoredBlock ?? 0n))} background={background} />
|
|
<BridgeTableCell align="center" value={statusIcon(colors, commit?.disabled)} background={background} />
|
|
</TableRow>
|
|
)
|
|
}
|
|
|
|
const sliceString = (string, first, second) => {
|
|
if (!string) return "";
|
|
return string.slice(0, first) + "..." + string.slice(second);
|
|
}
|
|
|
|
const getTimeAgo = (timestampDiff) => {
|
|
const seconds = Math.floor(timestampDiff / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
|
|
if (seconds < 60) return `${seconds}s ago`;
|
|
if (minutes < 60) return `${minutes}m ago`;
|
|
if (hours < 24) return `${hours}h ago`;
|
|
|
|
return "long ago";
|
|
}
|
|
|
|
const blockDelayIcon = (colors, timestampDiff) => {
|
|
let color = colors.feedback.error;
|
|
let icon = CancelIcon;
|
|
|
|
if (timestampDiff < 900000n) {
|
|
color = colors.feedback.success;
|
|
icon = CheckCircleIcon;
|
|
} else if (timestampDiff < 2700000n) {
|
|
color = colors.feedback.warning;
|
|
icon = WarningIcon;
|
|
}
|
|
|
|
return (
|
|
<GhostStyledIcon
|
|
sx={{
|
|
marginLeft: "5px",
|
|
width: "16px",
|
|
height: "16px",
|
|
fill: color
|
|
}}
|
|
viewBox="0 0 25 25"
|
|
component={icon}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const statusIcon = (colors, disabled) => {
|
|
let color = colors.feedback.success;
|
|
let icon = CheckCircleIcon;
|
|
|
|
if (disabled === true) {
|
|
color = colors.feedback.error;
|
|
icon = CancelIcon;
|
|
}
|
|
|
|
return (
|
|
<GhostStyledIcon
|
|
sx={{
|
|
marginLeft: "5px",
|
|
width: "16px",
|
|
height: "16px",
|
|
fill: color
|
|
}}
|
|
viewBox="0 0 25 25"
|
|
component={icon}
|
|
/>
|
|
)
|
|
}
|