ghost-dao-interface/src/containers/Bridge/ValidatorTable.jsx
Uncle Fatso a9f34987f2
change url to the ghost-connect
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-05-03 17:18:48 +03:00

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}
/>
)
}