change of bridge logic in accordance with new ghost-node implementation
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
parent
cffaad6973
commit
39118c3b93
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ghost-dao-interface",
|
"name": "ghost-dao-interface",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.11",
|
"version": "0.4.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@ -112,6 +112,10 @@ const NavContent = ({ chainId, addressChainId }) => {
|
|||||||
{isNetworkAvailable(chainId, addressChainId) &&
|
{isNetworkAvailable(chainId, addressChainId) &&
|
||||||
<>
|
<>
|
||||||
<NavItem icon={DashboardIcon} label={`Dashboard`} to="/dashboard" />
|
<NavItem icon={DashboardIcon} label={`Dashboard`} to="/dashboard" />
|
||||||
|
{isNetworkLegacy(chainId)
|
||||||
|
? <NavItem icon={ShowerIcon} label={`Faucet`} to="/faucet" />
|
||||||
|
: <NavItem icon={WifiProtectedSetupIcon} label={`Wrapper`} to="/wrapper" />
|
||||||
|
}
|
||||||
<NavItem
|
<NavItem
|
||||||
defaultExpanded
|
defaultExpanded
|
||||||
icon={BondIcon}
|
icon={BondIcon}
|
||||||
@ -148,12 +152,6 @@ const NavContent = ({ chainId, addressChainId }) => {
|
|||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<NavItem icon={StakeIcon} label={`Stake`} to="/stake" />
|
|
||||||
{isNetworkLegacy(chainId)
|
|
||||||
? <NavItem icon={ShowerIcon} label={`Faucet`} to="/faucet" />
|
|
||||||
: <NavItem icon={WifiProtectedSetupIcon} label={`Wrapper`} to="/wrapper" />
|
|
||||||
}
|
|
||||||
<NavItem icon={PublicIcon} label={`Bridge`} to="/bridge" />
|
|
||||||
<NavItem
|
<NavItem
|
||||||
icon={CurrencyExchangeIcon}
|
icon={CurrencyExchangeIcon}
|
||||||
label={`Dex`}
|
label={`Dex`}
|
||||||
@ -178,6 +176,8 @@ const NavContent = ({ chainId, addressChainId }) => {
|
|||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<NavItem icon={StakeIcon} label={`Stake`} to="/stake" />
|
||||||
|
<NavItem icon={PublicIcon} label={`Bridge`} to="/bridge" />
|
||||||
<Box className="menu-divider">
|
<Box className="menu-divider">
|
||||||
<Divider />
|
<Divider />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -90,7 +90,7 @@ const SwapCard = ({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Box display="flex" flexDirection="row" marginTop="12px" justifyContent="space-between" alignItems="center">
|
<Box display="flex" flexDirection="row" marginTop="12px" justifyContent="space-between" alignItems="center">
|
||||||
<Box display="flex" flexDirection="row" alignItems="center">
|
<Box display="flex" flexDirection="row" alignItems="center" width={!info && !endString && !usdValue ? "100%" : inputWidth ? inputWidth : "136px"}>
|
||||||
<StyledInputBase
|
<StyledInputBase
|
||||||
id={id}
|
id={id}
|
||||||
sx={{
|
sx={{
|
||||||
@ -99,6 +99,7 @@ const SwapCard = ({
|
|||||||
padding: 0,
|
padding: 0,
|
||||||
height: "24px",
|
height: "24px",
|
||||||
maxWidth: inputWidth || "136px",
|
maxWidth: inputWidth || "136px",
|
||||||
|
width: !info && !endString && !usdValue ? "100%" : inputWidth ? inputWidth : "136px",
|
||||||
}}
|
}}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
type={inputType}
|
type={inputType}
|
||||||
|
|||||||
@ -28,11 +28,11 @@ const StyledArrow = styled(Box)(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const SwapCollection = ({ UpperSwapCard, LowerSwapCard, arrowOnClick, iconNotNeeded }) => {
|
const SwapCollection = ({ UpperSwapCard, LowerSwapCard, arrowOnClick, iconNotNeeded, maxWidth}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" flexDirection="column" maxWidth="476px">
|
<Box display="flex" flexDirection="column" maxWidth={maxWidth ? maxWidth : "476px"}>
|
||||||
{UpperSwapCard}
|
{UpperSwapCard}
|
||||||
<Box display="flex" flexDirection="row" justifyContent="center">
|
<Box display="flex" flexDirection="row" justifyContent="center">
|
||||||
{!iconNotNeeded && (<StyledArrow
|
{!iconNotNeeded && (<StyledArrow
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { SvgIcon } from "@mui/material";
|
import { SvgIcon, Box } from "@mui/material";
|
||||||
import { styled } from "@mui/material/styles";
|
import { styled } from "@mui/material/styles";
|
||||||
|
|
||||||
import FtsoIcon from "../../assets/tokens/FTSO.svg?react";
|
import FtsoIcon from "../../assets/tokens/FTSO.svg?react";
|
||||||
@ -69,14 +69,31 @@ export const parseKnownToken = (name) => {
|
|||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Token = ({ name, viewBox = "0 0 260 260", fontSize = "large", ...props }) => {
|
const Token = ({ chainTokenName, name, viewBox = "0 0 260 260", fontSize = "large", ...props }) => {
|
||||||
return (
|
return (
|
||||||
|
<Box display="flex" justifyContent="center" alignItems="center" position="relative">
|
||||||
<StyledSvgIcon
|
<StyledSvgIcon
|
||||||
inheritViewBox
|
inheritViewBox
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
component={parseKnownToken(name)}
|
component={parseKnownToken(name)}
|
||||||
{...props}
|
{...props}
|
||||||
></StyledSvgIcon>
|
></StyledSvgIcon>
|
||||||
|
{chainTokenName && (
|
||||||
|
<StyledSvgIcon
|
||||||
|
inheritViewBox
|
||||||
|
component={parseKnownToken(chainTokenName)}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
marginLeft: "70%",
|
||||||
|
marginTop: "45%",
|
||||||
|
width: "65%",
|
||||||
|
height: "65%",
|
||||||
|
border: "1px solid #fff",
|
||||||
|
borderRadius: "100%"
|
||||||
|
}}
|
||||||
|
></StyledSvgIcon>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -37,3 +37,21 @@ export const isNetworkLegacy = (chainId) => {
|
|||||||
}
|
}
|
||||||
return exists;
|
return exists;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const networkAvgBlockSpeed = (chainId) => {
|
||||||
|
let blockSpeed = 12n;
|
||||||
|
switch (chainId) {
|
||||||
|
case 11155111:
|
||||||
|
blockSpeed = 12n
|
||||||
|
break;
|
||||||
|
case 560048:
|
||||||
|
blockSpeed = 12n
|
||||||
|
break;
|
||||||
|
case 63:
|
||||||
|
blockSpeed = 13n
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return blockSpeed
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
366
src/containers/Bridge/BridgeCard.jsx
Normal file
366
src/containers/Bridge/BridgeCard.jsx
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
import { useMemo, useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Link,
|
||||||
|
Skeleton,
|
||||||
|
TableContainer,
|
||||||
|
Table,
|
||||||
|
Paper,
|
||||||
|
TableHead,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
Modal,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
import { useConfig } from "wagmi";
|
||||||
|
import { ss58Decode } from "@polkadot-labs/hdkd-helpers";
|
||||||
|
import { toHex } from "@polkadot-api/utils";
|
||||||
|
|
||||||
|
import { PrimaryButton } from "../../components/Button";
|
||||||
|
import GhostStyledIcon from "../../components/Icon/GhostIcon";
|
||||||
|
import SwapCard from "../../components/Swap/SwapCard";
|
||||||
|
import SwapCollection from "../../components/Swap/SwapCollection";
|
||||||
|
import InfoTooltip from "../../components/Tooltip/InfoTooltip";
|
||||||
|
|
||||||
|
import { ghost } from "../../hooks/staking";
|
||||||
|
import { useBalance } from "../../hooks/tokens";
|
||||||
|
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
import HourglassBottomIcon from '@mui/icons-material/HourglassBottom';
|
||||||
|
|
||||||
|
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||||
|
import { formatNumber, formatCurrency, timeConverter } from "../../helpers";
|
||||||
|
|
||||||
|
import { BridgeRoute } from "./BridgeRoute";
|
||||||
|
|
||||||
|
const sliceString = (string, first, second) => {
|
||||||
|
if (!string) return "";
|
||||||
|
return string.slice(0, first) + "..." + string.slice(second);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BridgeCardAction = ({
|
||||||
|
isVerySmallScreen,
|
||||||
|
isSemiSmallScreen,
|
||||||
|
chainId,
|
||||||
|
address,
|
||||||
|
ghstSymbol,
|
||||||
|
gatekeeperAddressEmpty,
|
||||||
|
gatekeeperAddress,
|
||||||
|
evmNetwork,
|
||||||
|
connect,
|
||||||
|
isConfirmed,
|
||||||
|
setIsConfirmed,
|
||||||
|
openBridgeModal,
|
||||||
|
storeTransactionHash,
|
||||||
|
}) => {
|
||||||
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
const [receiver, setReceiver] = useState("");
|
||||||
|
const [convertedReceiver, setConvertedReceiver] = useState(undefined);
|
||||||
|
const [amount, setAmount] = useState("");
|
||||||
|
|
||||||
|
const config = useConfig();
|
||||||
|
const incomingFee = Number(evmNetwork?.incoming_fee ?? 0n) / 10000000;
|
||||||
|
|
||||||
|
const {
|
||||||
|
balance: ghstBalance,
|
||||||
|
refetch: ghstBalanceRefetch
|
||||||
|
} = useBalance(chainId, "GHST", address);
|
||||||
|
|
||||||
|
const chainName = useMemo(() => {
|
||||||
|
const client = config?.getClient();
|
||||||
|
return client?.chain?.name;
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
const chainNativeCurrency = useMemo(() => {
|
||||||
|
const client = config?.getClient();
|
||||||
|
return client?.chain?.nativeCurrency?.symbol;
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
const chainExplorerUrl = useMemo(() => {
|
||||||
|
const client = config?.getClient();
|
||||||
|
return client?.chain?.blockExplorers?.default?.url;
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
const preparedAmount = useMemo(() => {
|
||||||
|
try {
|
||||||
|
const result = BigInt(parseFloat(amount) * Math.pow(10, 18));
|
||||||
|
if (result > ghstBalance._value) {
|
||||||
|
return ghstBalance._value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch {
|
||||||
|
return 0n;
|
||||||
|
}
|
||||||
|
}, [amount]);
|
||||||
|
|
||||||
|
const amountAfterFee = useMemo(() => {
|
||||||
|
const convertedAmount = parseFloat(amount);
|
||||||
|
if (!convertedAmount) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return convertedAmount * (1 - incomingFee / 100);
|
||||||
|
}, [amount, incomingFee]);
|
||||||
|
|
||||||
|
const isDisabled = useMemo(() => {
|
||||||
|
let isDisabled = isPending || gatekeeperAddressEmpty;
|
||||||
|
if (address !== "") {
|
||||||
|
isDisabled = isDisabled
|
||||||
|
|| !convertedReceiver
|
||||||
|
|| preparedAmount === 0n
|
||||||
|
|| ghstBalance._value < preparedAmount;
|
||||||
|
}
|
||||||
|
return isDisabled;
|
||||||
|
}, [isPending, gatekeeperAddressEmpty, address, convertedReceiver, preparedAmount, ghstBalance]);
|
||||||
|
|
||||||
|
const ghostFunds = async () => {
|
||||||
|
setIsPending(true);
|
||||||
|
try {
|
||||||
|
const txHash = await ghost(chainId, address, convertedReceiver, preparedAmount);
|
||||||
|
if (txHash) {
|
||||||
|
storeTransactionHash(txHash, receiver, preparedAmount.toString());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await ghstBalanceRefetch();
|
||||||
|
setReceiver("");
|
||||||
|
setAmount("");
|
||||||
|
setIsPending(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isConfirmed) {
|
||||||
|
setIsConfirmed(false);
|
||||||
|
ghostFunds();
|
||||||
|
}
|
||||||
|
}, [isConfirmed]);
|
||||||
|
|
||||||
|
const ghostOrConnect = async () => {
|
||||||
|
if (address === "") {
|
||||||
|
connect();
|
||||||
|
} else if (!isConfirmed) {
|
||||||
|
openBridgeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const [publicKey, prefix] = ss58Decode(receiver);
|
||||||
|
if (prefix !== 1995 && prefix !== 1996) {
|
||||||
|
throw new Error("bad prefix");
|
||||||
|
}
|
||||||
|
setConvertedReceiver(toHex(publicKey));
|
||||||
|
} catch {
|
||||||
|
setConvertedReceiver(undefined);
|
||||||
|
}
|
||||||
|
}, [receiver])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box width="100%" height="320px" display="flex" flexDirection="column" justifyContent="space-between">
|
||||||
|
<SwapCollection
|
||||||
|
iconNotNeeded
|
||||||
|
UpperSwapCard={<SwapCard
|
||||||
|
id={`bridge-token-receiver`}
|
||||||
|
inputWidth={"100%"}
|
||||||
|
value={receiver}
|
||||||
|
onChange={event => setReceiver(event.currentTarget.value)}
|
||||||
|
inputProps={{ "data-testid": "fromInput" }}
|
||||||
|
placeholder="GHOST address (sf prefixed)"
|
||||||
|
type="text"
|
||||||
|
maxWidth="100%"
|
||||||
|
/>}
|
||||||
|
LowerSwapCard={<SwapCard
|
||||||
|
id={`bridge-token-amount`}
|
||||||
|
inputWidth={"100%"}
|
||||||
|
info={`${formatCurrency(ghstBalance.toString(), 4, ghstSymbol)}`}
|
||||||
|
value={amount}
|
||||||
|
onChange={event => setAmount(event.currentTarget.value)}
|
||||||
|
inputProps={{ "data-testid": "fromInput" }}
|
||||||
|
endString={"Max"}
|
||||||
|
endStringOnClick={() => setAmount(ghstBalance.toString())}
|
||||||
|
maxWidth="100%"
|
||||||
|
/>}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
display="flex"
|
||||||
|
gap="10px"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Box display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||||
|
{gatekeeperAddressEmpty && (
|
||||||
|
<Box maxWidth="416px" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||||
|
<Typography mr="10px" variant="body2" color="textSecondary">
|
||||||
|
<em>
|
||||||
|
There is no connected gatekeeper on {chainName} network. Propose gatekeeper on this network to make authorities listen to it.
|
||||||
|
</em>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{!gatekeeperAddressEmpty && (
|
||||||
|
<>
|
||||||
|
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">Gatekeeper:</Typography>}
|
||||||
|
<Link
|
||||||
|
fontSize="12px"
|
||||||
|
lineHeight="15px"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={`${chainExplorerUrl}/token/${gatekeeperAddress}`}
|
||||||
|
>
|
||||||
|
{sliceString(gatekeeperAddress, 10, -8)}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box width="100%" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||||
|
<Box width="100%" display="flex" flexDirection="column" gap="0px">
|
||||||
|
<Box maxWidth="100%" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||||
|
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">Bridge Fee:</Typography>}
|
||||||
|
{incomingFee
|
||||||
|
? <Typography fontSize="12px" lineHeight="15px">{`${incomingFee.toFixed(4)}%`}</Typography>
|
||||||
|
: <Skeleton height={15} width="80px" />
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
<Box maxWidth="100%" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
|
||||||
|
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">You will get:</Typography>}
|
||||||
|
{incomingFee
|
||||||
|
? <Typography fontSize="12px" lineHeight="15px">{amountAfterFee.toFixed(4)} {ghstSymbol}</Typography>
|
||||||
|
: <Skeleton height={15} width="80px" />
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<BridgeRoute coinName={ghstSymbol} chainTokenName={chainNativeCurrency} tokens={[ghstSymbol]} />
|
||||||
|
</Box>
|
||||||
|
<PrimaryButton
|
||||||
|
fullWidth
|
||||||
|
disabled={isDisabled}
|
||||||
|
loading={isPending}
|
||||||
|
onClick={() => ghostOrConnect()}
|
||||||
|
>
|
||||||
|
{address === "" ? "Connect" : "Bridge" }
|
||||||
|
</PrimaryButton>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BridgeCardHistory = ({
|
||||||
|
isSemiSmallScreen,
|
||||||
|
filteredStoredTransactions,
|
||||||
|
ghstSymbol,
|
||||||
|
blockNumber,
|
||||||
|
finalityDelay,
|
||||||
|
setActiveTxIndex
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const background = (index) => {
|
||||||
|
return index % 2 === 1 ? "" : theme.colors.gray[750];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box height="320px">
|
||||||
|
<TableContainer
|
||||||
|
component={Paper}
|
||||||
|
className="custom-scrollbar"
|
||||||
|
sx={{
|
||||||
|
height: "320px",
|
||||||
|
overflowY: 'scroll',
|
||||||
|
msOverflowStyle: "thin !important",
|
||||||
|
scrollbarWidth: "thin !important",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table sx={{ marginTop: "0px" }} stickyHeader aria-label="sticky available transactions">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow sx={{ height: "40px" }}>
|
||||||
|
<TableCell align="left" style={{ padding: "0px", paddingLeft: "16px", fontSize: "12px", borderTopLeftRadius: "3px", background: theme.colors.paper.cardHover }}>
|
||||||
|
Transaction
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center" style={{ padding: "0px", fontSize: "12px", background: theme.colors.paper.cardHover }}>
|
||||||
|
Datetime
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center" style={{ padding: "0px", fontSize: "12px", borderTopRightRadius: "3px", background: theme.colors.paper.cardHover }}>
|
||||||
|
Status
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{filteredStoredTransactions?.map((obj, idx) => (
|
||||||
|
<TableRow
|
||||||
|
key={idx}
|
||||||
|
sx={{ cursor: "pointer", height: "30px" }}
|
||||||
|
id={idx + `--tx-history`}
|
||||||
|
data-testid={idx + `--tx-history`}
|
||||||
|
onClick={() => setActiveTxIndex(idx)}
|
||||||
|
>
|
||||||
|
<TableCell style={{ background: background(idx) }}>
|
||||||
|
<Box display="flex" flexDirection="column" justifyContent="center">
|
||||||
|
<Typography variant="caption">
|
||||||
|
{formatCurrency(
|
||||||
|
new DecimalBigNumber(BigInt(obj.amount), 18).toString(),
|
||||||
|
isSemiSmallScreen ? 3 : 8,
|
||||||
|
ghstSymbol
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{sliceString(
|
||||||
|
obj.receiverAddress,
|
||||||
|
isSemiSmallScreen ? 5 : 10,
|
||||||
|
isSemiSmallScreen ? -3 : -8
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell style={{ background: background(idx) }}>
|
||||||
|
<Box display="flex" flexDirection="column" alignItems="center" paddingLeft="5px">
|
||||||
|
<Typography variant="caption">
|
||||||
|
{new Date(obj.timestamp).toLocaleDateString('en-US')}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{new Date(obj.timestamp).toLocaleTimeString('en-US')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell style={{ background: background(idx) }}>
|
||||||
|
<Box display="flex" justifyContent="center" alignItems="center">
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
sx={{
|
||||||
|
width: "20px",
|
||||||
|
height: "20px",
|
||||||
|
background: Number(blockNumber) - (obj.blockNumber + finalityDelay) < 0
|
||||||
|
? theme.colors.feedback.warning
|
||||||
|
: theme.colors.feedback.success,
|
||||||
|
borderRadius: "100%",
|
||||||
|
boxShadow: "0px 0px 1px black"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Number(blockNumber) - (obj.blockNumber + finalityDelay) < 0 ?
|
||||||
|
<GhostStyledIcon
|
||||||
|
sx={{ width: "15px", height: "15px" }}
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
component={HourglassBottomIcon}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<GhostStyledIcon
|
||||||
|
sx={{ width: "15px", height: "15px" }}
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
component={CheckIcon}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
133
src/containers/Bridge/BridgeHeader.jsx
Normal file
133
src/containers/Bridge/BridgeHeader.jsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { Box, Paper, Grid, Typography, LinearProgress, useTheme } from "@mui/material"
|
||||||
|
|
||||||
|
import Metric from "../../components/Metric/Metric";
|
||||||
|
import Countdown from "../../components/Countdown/Countdown";
|
||||||
|
import { formatNumber } from "../../helpers";
|
||||||
|
|
||||||
|
export const BridgeHeader = ({
|
||||||
|
totalValidators,
|
||||||
|
disabledValidators,
|
||||||
|
bridgeStability,
|
||||||
|
transactionEta,
|
||||||
|
timeToNextEpoch,
|
||||||
|
isSmallScreen
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const disabledPercentage = useMemo(() => {
|
||||||
|
if (totalValidators === undefined || disabledValidators === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return ((totalValidators - disabledValidators) / totalValidators) * 100;
|
||||||
|
}, [totalValidators, disabledValidators]);
|
||||||
|
|
||||||
|
const validatorsColor = useMemo(() => {
|
||||||
|
if (disabledPercentage < 50) {
|
||||||
|
return theme.colors.validatorsColor.red;
|
||||||
|
}
|
||||||
|
return theme.colors.validatorsColor.green;
|
||||||
|
}, [disabledPercentage, theme]);
|
||||||
|
|
||||||
|
const stabilityColor = useMemo(() => {
|
||||||
|
if (bridgeStability > 80) {
|
||||||
|
return theme.colors.bridgeProgress.success;
|
||||||
|
} else if (bridgeStability > 50) {
|
||||||
|
return theme.colors.bridgeProgress.warning;
|
||||||
|
} else {
|
||||||
|
return theme.colors.bridgeProgress.error;
|
||||||
|
}
|
||||||
|
}, [bridgeStability, theme]);
|
||||||
|
|
||||||
|
const progressBarPostfix = useMemo(() => {
|
||||||
|
if (bridgeStability > 90) {
|
||||||
|
return "✅ Safe";
|
||||||
|
} else if (bridgeStability > 80) {
|
||||||
|
return "✅ Moderate Risk";
|
||||||
|
} else if (bridgeStability > 70) {
|
||||||
|
return "⚠️ High Risk";
|
||||||
|
} else if (bridgeStability > 50) {
|
||||||
|
return "⚠️ Critical Risk";
|
||||||
|
} else {
|
||||||
|
return "❌ Do NOT Bridge";
|
||||||
|
}
|
||||||
|
}, [bridgeStability]);
|
||||||
|
|
||||||
|
const formatTime = (totalSeconds) => {
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const secs = Math.floor(totalSeconds % 60);
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours} hours ${minutes} mins`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes} mins`;
|
||||||
|
} else {
|
||||||
|
return `${secs} secs`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={isSmallScreen ? 4 : 1}>
|
||||||
|
<Grid item xs={isSmallScreen ? 12 : 4}>
|
||||||
|
<Metric
|
||||||
|
isLoading={totalValidators === undefined || disabledValidators === undefined}
|
||||||
|
metric={
|
||||||
|
<Typography color={validatorsColor} fontSize="24px" fontWeight="700" lineHeight="33px">
|
||||||
|
{totalValidators} ({formatNumber(disabledPercentage, 0)}% active)
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
label="Total Validators"
|
||||||
|
tooltip="Active and disabled GHOST Validators in the current GHOST Epoch."
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={isSmallScreen ? 12 : 4}>
|
||||||
|
<Metric
|
||||||
|
isLoading={timeToNextEpoch === undefined}
|
||||||
|
metric={formatTime(timeToNextEpoch)}
|
||||||
|
label="Rotation in"
|
||||||
|
tooltip="Bridge Stability Index refreshes every 10 minutes with new validator blocks; resets each Era when the validator set is updated."
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={isSmallScreen ? 12 : 4}>
|
||||||
|
<Metric
|
||||||
|
isLoading={transactionEta === undefined}
|
||||||
|
metric={formatTime(transactionEta)}
|
||||||
|
label="Max Bridge ETA"
|
||||||
|
tooltip="Maximum estimated time for finalizing bridge transactions based on the latest update."
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item gap={2} xs={12} sx={{ marginTop: "20px" }}>
|
||||||
|
<Box position="relative" margin="4.5px 0">
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={bridgeStability ?? 0}
|
||||||
|
sx={{
|
||||||
|
borderRadius: "5px",
|
||||||
|
height: "40px",
|
||||||
|
'& .MuiLinearProgress-bar': {
|
||||||
|
backgroundColor: stabilityColor,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box sx={{
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 'bold' }}>
|
||||||
|
Bridge Stability {bridgeStability
|
||||||
|
? `${formatNumber(bridgeStability, 0)}% ${progressBarPostfix}`
|
||||||
|
: "Unknown"
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
410
src/containers/Bridge/BridgeModal.jsx
Normal file
410
src/containers/Bridge/BridgeModal.jsx
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import { Box, Typography, Link, FormControlLabel, Checkbox, useTheme } from "@mui/material";
|
||||||
|
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
|
||||||
|
|
||||||
|
import ThumbUpIcon from '@mui/icons-material/ThumbUp';
|
||||||
|
import ThumbDownAltIcon from '@mui/icons-material/ThumbDownAlt';
|
||||||
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
import AssuredWorkloadIcon from '@mui/icons-material/AssuredWorkload';
|
||||||
|
|
||||||
|
import HourglassBottomIcon from '@mui/icons-material/HourglassBottom';
|
||||||
|
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
|
||||||
|
import HandshakeIcon from '@mui/icons-material/Handshake';
|
||||||
|
import PendingIcon from '@mui/icons-material/Pending';
|
||||||
|
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
||||||
|
|
||||||
|
import InfoTooltip from "../../components/Tooltip/InfoTooltip";
|
||||||
|
import Modal from "../../components/Modal/Modal";
|
||||||
|
import GhostStyledIcon from "../../components/Icon/GhostIcon";
|
||||||
|
import { PrimaryButton } from "../../components/Button";
|
||||||
|
|
||||||
|
import { formatCurrency } from "../../helpers";
|
||||||
|
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||||
|
|
||||||
|
export const BridgeModal = ({
|
||||||
|
currentRecord,
|
||||||
|
activeTxIndex,
|
||||||
|
setActiveTxIndex,
|
||||||
|
authorities,
|
||||||
|
ghstSymbol,
|
||||||
|
hashedArguments,
|
||||||
|
chainExplorerUrl,
|
||||||
|
removeStoredRecord,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [copiedIndex, setCopiedIndex] = useState(null);
|
||||||
|
|
||||||
|
const sliceString = (string, first, second) => {
|
||||||
|
if (!string) return "";
|
||||||
|
return string.slice(0, first) + "..." + string.slice(second);
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyToClipboard = (text, index) => {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
setCopiedIndex(index);
|
||||||
|
setTimeout(() => setCopiedIndex(null) , 800);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
data-testid="transaction-details-modal"
|
||||||
|
maxWidth="476px"
|
||||||
|
headerContent={
|
||||||
|
<Box display="flex" flexDirection="row">
|
||||||
|
<Typography variant="h5">
|
||||||
|
TX Hash
|
||||||
|
<Link
|
||||||
|
sx={{
|
||||||
|
margin: "0px",
|
||||||
|
font: "inherit",
|
||||||
|
letterSpacing: "inherit",
|
||||||
|
textDecoration: "underline",
|
||||||
|
color: theme.colors.gray[10],
|
||||||
|
textUnderlineOffset: "0.23rem",
|
||||||
|
cursor: "pointer",
|
||||||
|
textDecorationThickness: "3px",
|
||||||
|
"&:hover": {
|
||||||
|
textDecoration: "underline",
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={currentRecord
|
||||||
|
? `${chainExplorerUrl}/tx/${currentRecord ? currentRecord.transactionHash : ""}`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{currentRecord ? sliceString(currentRecord.transactionHash, 10, -8) : ""}
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
open={activeTxIndex >= 0}
|
||||||
|
onClose={() => setActiveTxIndex(-1)}
|
||||||
|
minHeight={"100px"}
|
||||||
|
>
|
||||||
|
<Box display="flex" gap="1.5rem" flexDirection="column" marginTop=".8rem">
|
||||||
|
<Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
transition: "all 0.8s ease",
|
||||||
|
transform: currentRecord?.step === 0 && "scale(1.2)",
|
||||||
|
color: currentRecord?.step > 0 && theme.colors.primary[300]
|
||||||
|
}}
|
||||||
|
width="120px"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
justifyContent="start"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<GhostStyledIcon
|
||||||
|
sx={{
|
||||||
|
width: "35px",
|
||||||
|
height: "35px",
|
||||||
|
animation: currentRecord?.step === 0 && 'rotateHourGlass 2s ease-in-out infinite',
|
||||||
|
'@keyframes rotateHourGlass': {
|
||||||
|
'0%': { transform: 'rotate(0deg)' },
|
||||||
|
'15%': { transform: 'rotate(0deg)' },
|
||||||
|
'85%': { transform: 'rotate(180deg)' },
|
||||||
|
'100%': { transform: 'rotate(180deg)' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
component={HourglassBottomIcon}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption">Finalization</Typography>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{(currentRecord?.finalization ?? 0).toString()} blocks left
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<GhostStyledIcon
|
||||||
|
sx={{ transition: "all 0.3s ease", opacity: currentRecord?.step < 1 && "0.2" }}
|
||||||
|
component={ArrowRightIcon}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
transition: "all 0.3s ease",
|
||||||
|
opacity: currentRecord?.step < 1 && "0.2",
|
||||||
|
transform: currentRecord?.step === 1 && "scale(1.2)",
|
||||||
|
color: currentRecord?.step > 1 && theme.colors.primary[300]
|
||||||
|
}}
|
||||||
|
width="120px"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
justifyContent="start"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Box display="flex" flexDirection="row" justifyContent="center" alignItems="center">
|
||||||
|
{currentRecord?.step <= 1
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
<GhostStyledIcon
|
||||||
|
sx={{
|
||||||
|
width: "35px",
|
||||||
|
height: "35px",
|
||||||
|
animation: currentRecord?.step === 1 && 'rotateRightHand 2s ease-in-out infinite',
|
||||||
|
'@keyframes rotateRightHand': {
|
||||||
|
'0%': { transform: 'rotateX(360deg)' },
|
||||||
|
'15%': { transform: 'rotateX(360deg)' },
|
||||||
|
'50%': { transform: 'rotateX(180deg)' },
|
||||||
|
'85%': { transform: 'rotateX(0deg)' },
|
||||||
|
'100%': { transform: 'rotateX(0deg)' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
component={ThumbUpIcon}
|
||||||
|
/>
|
||||||
|
<GhostStyledIcon
|
||||||
|
sx={{
|
||||||
|
width: "35px",
|
||||||
|
height: "35px",
|
||||||
|
animation: currentRecord?.step === 1 && 'rotateRightHand 2s ease-in-out infinite',
|
||||||
|
'@keyframes rotateRightHand': {
|
||||||
|
'0%': { transform: 'rotateX(0deg)' },
|
||||||
|
'15%': { transform: 'rotateX(0deg)' },
|
||||||
|
'50%': { transform: 'rotateX(180deg)' },
|
||||||
|
'85%': { transform: 'rotateX(360deg)' },
|
||||||
|
'100%': { transform: 'rotateX(360deg)' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
component={ThumbDownAltIcon}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<GhostStyledIcon
|
||||||
|
sx={{
|
||||||
|
width: "35px",
|
||||||
|
height: "35px",
|
||||||
|
}}
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
component={HandshakeIcon}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center">
|
||||||
|
<Typography variant="caption">Slow Claps</Typography>
|
||||||
|
<Typography variant="caption">{currentRecord?.numberOfClaps ?? 0} / {authorities?.length ?? 0}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<GhostStyledIcon
|
||||||
|
sx={{
|
||||||
|
transition: "all 0.3s ease",
|
||||||
|
opacity: currentRecord?.step < 2 && "0.2"
|
||||||
|
}}
|
||||||
|
component={ArrowRightIcon}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
transition: "all 0.3s ease",
|
||||||
|
opacity: currentRecord?.step < 2 && "0.2",
|
||||||
|
transform: currentRecord?.step === 2 && "scale(1.2)",
|
||||||
|
color: currentRecord?.step === 2 && theme.colors.primary[300]
|
||||||
|
}}
|
||||||
|
width="120px"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
justifyContent="start"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
{currentRecord?.applaused
|
||||||
|
? <>
|
||||||
|
<GhostStyledIcon
|
||||||
|
sx={{ width: "35px", height: "35px" }}
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
component={CheckCircleIcon}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption">Applaused</Typography>
|
||||||
|
<Typography variant="caption">Check Receiver</Typography>
|
||||||
|
</>
|
||||||
|
: <>
|
||||||
|
<GhostStyledIcon
|
||||||
|
sx={{ width: "35px", height: "35px" }}
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
component={AssuredWorkloadIcon}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption">Capital Backed</Typography>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{(currentRecord?.clappedAmount ?? 0n) / 10n**18n} {ghstSymbol} ({currentRecord?.clappedPercentage ?? 0}%)
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box display="flex" flexDirection="column" gap="5px" padding="0.6rem 0">
|
||||||
|
<Box display="flex" flexDirection="row" justifyContent="space-between">
|
||||||
|
<Typography variant="body2">GHOST Epoch:</Typography>
|
||||||
|
<Typography variant="body2">{currentRecord?.sessionIndex}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" flexDirection="row" justifyContent="space-between">
|
||||||
|
<Typography variant="body2">Accepted Bridge Risk:</Typography>
|
||||||
|
<Typography variant="body2">{currentRecord?.bridgeStability}%</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" flexDirection="row" justifyContent="space-between">
|
||||||
|
<Box display="flex" flexDirection="row">
|
||||||
|
<Typography variant="body2">Arguments Hash:</Typography>
|
||||||
|
<InfoTooltip message="A unique identifier for transaction parameters, represented as a hash generated by keccak256(receiver, amount, blockNumber, chainId)." />
|
||||||
|
</Box>
|
||||||
|
<Link
|
||||||
|
style={{ display: "flex", flexDirection: "row", justifyContent: "center", alignItems: "center" }}
|
||||||
|
onClick={() => copyToClipboard(hashedArguments ? hashedArguments : "", 2)}
|
||||||
|
>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{hashedArguments ? sliceString(hashedArguments, 10, -8) : "0x"}
|
||||||
|
</Typography>
|
||||||
|
<GhostStyledIcon
|
||||||
|
sx={{ marginLeft: "5px", width: "12px", height: "12px" }}
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
component={copiedIndex === 2 ? CheckIcon : ContentPasteIcon}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
<hr style={{ width: "100%" }} />
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Typography variant="body2">Receiver Address:</Typography>
|
||||||
|
<Link
|
||||||
|
style={{ display: "flex", flexDirection: "row", justifyContent: "center", alignItems: "center" }}
|
||||||
|
onClick={() => copyToClipboard(currentRecord ? currentRecord.receiverAddress : "", 0)}
|
||||||
|
>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{currentRecord ? sliceString(currentRecord.receiverAddress, 14, -5) : ""}
|
||||||
|
</Typography>
|
||||||
|
<GhostStyledIcon
|
||||||
|
sx={{ marginLeft: "5px", width: "12px", height: "12px" }}
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
component={copiedIndex === 0 ? CheckIcon : ContentPasteIcon}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" flexDirection="row" justifyContent="space-between">
|
||||||
|
<Typography variant="body2">Bridged Amount:</Typography>
|
||||||
|
<Typography variant="body2">{formatCurrency(
|
||||||
|
new DecimalBigNumber(
|
||||||
|
BigInt(currentRecord ? currentRecord.amount : "0"),
|
||||||
|
18
|
||||||
|
).toString(), 9, ghstSymbol)
|
||||||
|
}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" flexDirection="row" justifyContent="space-between">
|
||||||
|
<Typography variant="body2">Executed at:</Typography>
|
||||||
|
<Typography variant="body2">{
|
||||||
|
new Date(currentRecord ? currentRecord.timestamp : 0).toLocaleString('en-US')
|
||||||
|
}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box display="flex" flexDirection="column" gap="5px">
|
||||||
|
<PrimaryButton
|
||||||
|
fullWidth
|
||||||
|
loading={false}
|
||||||
|
onClick={() => removeStoredRecord()}
|
||||||
|
>
|
||||||
|
Erase Record
|
||||||
|
</PrimaryButton>
|
||||||
|
|
||||||
|
<Typography variant="body2" sx={{ fontStyle: "italic" }}>
|
||||||
|
This will permanently remove the bridge transaction record from the session storage, but it will not cancel the bridge transaction.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BridgeConfirmModal = ({
|
||||||
|
bridgeStability,
|
||||||
|
isOpen,
|
||||||
|
setClose,
|
||||||
|
handleButtonProceed
|
||||||
|
}) => {
|
||||||
|
const [isBridgingRiskChecked, setIsBridgingRiskChecked] = useState(false);
|
||||||
|
const [isBridgingRecipientChecked, setIsBridgingRecipientChecked] = useState(false);
|
||||||
|
|
||||||
|
const handleProceed = () => {
|
||||||
|
setIsBridgingRiskChecked(false);
|
||||||
|
setIsBridgingRecipientChecked(false);
|
||||||
|
handleButtonProceed();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
maxWidth="450px"
|
||||||
|
minHeight="150px"
|
||||||
|
headerText="Bridge Confirmation"
|
||||||
|
open={isOpen}
|
||||||
|
onClose={setClose}
|
||||||
|
>
|
||||||
|
<Box gap="20px" display="flex" flexDirection="column" justifyContent="space-between" alignItems="center">
|
||||||
|
<Box width="100%" display="flex" flexDirection="column" alignItems="start">
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
data-testid="acknowledge-bridge-stability"
|
||||||
|
checked={isBridgingRiskChecked}
|
||||||
|
onChange={event => setIsBridgingRiskChecked(event.target.checked)}
|
||||||
|
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
|
||||||
|
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
{`I acknowledge bridging risk at ${bridgeStability}%.`}
|
||||||
|
<Link
|
||||||
|
sx={{
|
||||||
|
margin: "0px",
|
||||||
|
font: "inherit",
|
||||||
|
letterSpacing: "inherit",
|
||||||
|
textDecoration: "underline",
|
||||||
|
textUnderlineOffset: "0.23rem",
|
||||||
|
cursor: "pointer",
|
||||||
|
textDecorationThickness: "1px",
|
||||||
|
"&:hover": {
|
||||||
|
textDecoration: "underline",
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://google.com"
|
||||||
|
>
|
||||||
|
Learn more.
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<hr style={{ margin: "10px 0", width: "100%" }} />
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
data-testid="acknowledge-bridge-stability"
|
||||||
|
checked={isBridgingRecipientChecked}
|
||||||
|
onChange={event => setIsBridgingRecipientChecked(event.target.checked)}
|
||||||
|
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
|
||||||
|
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="I confirm that recipient address is a self-custodial wallet, not an exchange, third party service, or smart-contract."
|
||||||
|
sx={{ '& .MuiFormControlLabel-label': { textAlign: "justify" } }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<PrimaryButton fullWidth disabled={!isBridgingRiskChecked || !isBridgingRecipientChecked} onClick={handleProceed}>
|
||||||
|
Proceed Bridge
|
||||||
|
</PrimaryButton>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
45
src/containers/Bridge/BridgeRoute.jsx
Normal file
45
src/containers/Bridge/BridgeRoute.jsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Box, Typography, useTheme } from "@mui/material";
|
||||||
|
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
||||||
|
|
||||||
|
import GhostStyledIcon from "../../components/Icon/GhostIcon";
|
||||||
|
import Token from "../../components/Token/Token";
|
||||||
|
|
||||||
|
export const BridgeRoute = ({ coinName, chainTokenName, tokens }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Box width="100%" display="flex" alignItems="center" flexDirection="row" flexWrap="wrap">
|
||||||
|
<Typography>Route:</Typography>
|
||||||
|
<Box display="flex" marginLeft="20px" gap="5px" alignItems="center">
|
||||||
|
{tokens?.map((token, index) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<RouteHop key={index} theme={theme} token={token} chainTokenName={chainTokenName} />
|
||||||
|
<GhostStyledIcon sx={{ width: "12px", height: "12px" }} component={ArrowForwardIosIcon} />
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<RouteHop theme={theme} token={coinName} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const RouteHop = ({ theme, token, chainTokenName, arrowNeeded }) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
display="inline-flex"
|
||||||
|
sx={{ backgroundColor: theme.colors.gray[600] }}
|
||||||
|
borderRadius="6px"
|
||||||
|
paddingX="9px"
|
||||||
|
paddingY="6.5px"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Token chainTokenName={chainTokenName} name={token} sx={{ fontSize: "21px" }} />
|
||||||
|
<Typography fontSize="15px" lineHeight="24px" marginLeft="9px">
|
||||||
|
{token}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
242
src/containers/Bridge/ValidatorTable.jsx
Normal file
242
src/containers/Bridge/ValidatorTable.jsx
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
import { useEffect, useState, useMemo } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Paper,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Typography,
|
||||||
|
LinearProgress,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
export const ValidatorTable = ({
|
||||||
|
currentTime,
|
||||||
|
currentBlock,
|
||||||
|
latestCommits,
|
||||||
|
isVerySmallScreen,
|
||||||
|
bridgeStability,
|
||||||
|
providerDetail,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const stabilityColor = useMemo(() => {
|
||||||
|
const red = Math.round(255 * (1 - bridgeStability / 100));
|
||||||
|
const green = Math.round(255 * (bridgeStability / 100));
|
||||||
|
return `rgb(${red}, ${green}, 0)`;
|
||||||
|
}, [bridgeStability]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box width="100%" height="320px" display="flex" flexDirection="column" justifyContent="space-between">
|
||||||
|
{!providerDetail && <Box sx={{ borderRadius: "15px", background: theme.colors.paper.background, paddingTop: "40px" }} 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 Wallet is not detected on your browser!</Typography>
|
||||||
|
<Typography sx={{ textAlign: "center" }}>Download GHOST Wallet Extension to see real-time validator stats for bridging transaction.</Typography>
|
||||||
|
<PrimaryButton onClick={() => window.open('https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases', '_blank', 'noopener,noreferrer')}>
|
||||||
|
Get GHOST Extension
|
||||||
|
</PrimaryButton>
|
||||||
|
</Box>
|
||||||
|
</Box>}
|
||||||
|
{providerDetail && <TableContainer
|
||||||
|
component={Paper}
|
||||||
|
className="custom-scrollbar"
|
||||||
|
sx={{
|
||||||
|
height: "320px",
|
||||||
|
overflowY: 'scroll',
|
||||||
|
msOverflowStyle: "thin !important",
|
||||||
|
scrollbarWidth: "thin !important",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table sx={{ marginTop: "0px" }} stickyHeader aria-label="sticky available validators">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow sx={{ height: "40px" }}>
|
||||||
|
<BridgeHeaderTableCell value="Validator" background={theme.colors.paper.cardHover} borderTopLeftRadius="3px" />
|
||||||
|
<BridgeHeaderTableCell value="Last Acitve" background={theme.colors.paper.cardHover} tooltip="GHOST Validators must submit block commitments every 10 minutes to remain active." />
|
||||||
|
<BridgeHeaderTableCell value="Block Height" background={theme.colors.paper.cardHover} tooltip="The latest EVM block height reported by the Validator." />
|
||||||
|
<BridgeHeaderTableCell 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 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const BridgeHeaderTableCell = ({
|
||||||
|
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,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" justifyContent="center">
|
||||||
|
{value}
|
||||||
|
{tooltip && <InfoTooltip message={tooltip} />}
|
||||||
|
</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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -41,3 +41,10 @@ export const formatNumber = (number, precision = 0) => {
|
|||||||
export const sortBondsByDiscount = (bonds) => {
|
export const sortBondsByDiscount = (bonds) => {
|
||||||
return Array.from(bonds).filter((bond) => !bond.isSoldOut).sort((a, b) => (a.discount.gt(b.discount) ? -1 : 1));
|
return Array.from(bonds).filter((bond) => !bond.isSoldOut).sort((a, b) => (a.discount.gt(b.discount) ? -1 : 1));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const timeConverter = (time) => {
|
||||||
|
const seconds = Number(time);
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${mins}m ${secs < 10 ? '0' : ''}${secs}s`;
|
||||||
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { createClient } from "@polkadot-api/substrate-client"
|
|||||||
import { getObservableClient } from "@polkadot-api/observable-client"
|
import { getObservableClient } from "@polkadot-api/observable-client"
|
||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
|
|
||||||
const DEFAULT_CHAIN_ID = "0xa217f4ee58a944470e9633ca5bd6d28a428ed64cd9b6f3e413565f359f89af90"
|
const DEFAULT_CHAIN_ID = "0x5e1190682f1a6409cdfd691c0b23a6db792864d8994e591e9c19a31d8163989f"
|
||||||
const UnstableProvider = createContext(null)
|
const UnstableProvider = createContext(null)
|
||||||
export const useUnstableProvider = () => useContext(UnstableProvider)
|
export const useUnstableProvider = () => useContext(UnstableProvider)
|
||||||
|
|
||||||
|
|||||||
@ -6,4 +6,9 @@ export * from "./useClapsInSession";
|
|||||||
export * from "./useApplauseThreshold";
|
export * from "./useApplauseThreshold";
|
||||||
export * from "./useReceivedClaps";
|
export * from "./useReceivedClaps";
|
||||||
export * from "./useAuthorities";
|
export * from "./useAuthorities";
|
||||||
export * from "./useApplausesForTransaction";
|
export * from "./useValidators";
|
||||||
|
export * from "./useDisabledValidators";
|
||||||
|
export * from "./useBlockCommitments";
|
||||||
|
export * from "./useApplauseDetails";
|
||||||
|
export * from "./useBabeSlots";
|
||||||
|
export * from "./useErasTotalStaked";
|
||||||
|
|||||||
@ -6,42 +6,41 @@ import { fromHex } from "@polkadot-api/utils";
|
|||||||
import { useUnstableProvider } from "./UnstableProvider"
|
import { useUnstableProvider } from "./UnstableProvider"
|
||||||
import { useMetadata } from "./MetadataProvider"
|
import { useMetadata } from "./MetadataProvider"
|
||||||
|
|
||||||
export const useApplausesForTransaction = ({ currentSession, txHash, argsHash }) => {
|
export const useApplauseDetails = ({ currentSession, argsHash }) => {
|
||||||
const { chainHead$, chainId } = useUnstableProvider()
|
const { chainHead$, chainId } = useUnstableProvider()
|
||||||
const metadata = useMetadata()
|
const metadata = useMetadata()
|
||||||
const { data: applausesForTransaction } = useSWRSubscription(
|
const { data: applauseDetails } = useSWRSubscription(
|
||||||
chainHead$ && txHash && argsHash && currentSession && chainId && metadata
|
chainHead$ && argsHash && currentSession && chainId && metadata
|
||||||
? ["applausesForTransaction", chainHead$, txHash, argsHash, currentSession, chainId, metadata]
|
? ["applauseDetails", chainHead$, argsHash, currentSession, chainId, metadata]
|
||||||
: null,
|
: null,
|
||||||
([_, chainHead$, txHash, argsHash, currentSession, chainId, metadata], { next }) => {
|
([_, chainHead$, argsHash, currentSession, chainId, metadata], { next }) => {
|
||||||
const { finalized$, storage$ } = chainHead$
|
const { finalized$, storage$ } = chainHead$
|
||||||
const subscription = finalized$.pipe(
|
const subscription = finalized$.pipe(
|
||||||
filter(Boolean),
|
filter(Boolean),
|
||||||
mergeMap((blockInfo) => {
|
mergeMap((blockInfo) => {
|
||||||
const builder = getDynamicBuilder(getLookupFn(metadata))
|
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||||
const applausesForTransaction = builder.buildStorage("GhostSlowClaps", "ApplausesForTransaction")
|
const applauseDetails = builder.buildStorage("GhostSlowClaps", "ApplauseDetails")
|
||||||
|
|
||||||
return storage$(blockInfo?.hash, "value", () =>
|
return storage$(blockInfo?.hash, "value", () =>
|
||||||
applausesForTransaction?.keys.enc(
|
applauseDetails?.keys.enc(
|
||||||
currentSession,
|
currentSession,
|
||||||
{ asBytes: () => fromHex(txHash) },
|
|
||||||
{ asBytes: () => fromHex(argsHash) },
|
{ asBytes: () => fromHex(argsHash) },
|
||||||
)
|
)
|
||||||
).pipe(
|
).pipe(
|
||||||
filter(Boolean),
|
filter(Boolean),
|
||||||
distinct(),
|
distinct(),
|
||||||
map((value) => applausesForTransaction?.value.dec(value))
|
map((value) => applauseDetails?.value.dec(value))
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next(applausesForTransaction) {
|
next(applauseDetails) {
|
||||||
next(null, applausesForTransaction)
|
next(null, applauseDetails)
|
||||||
},
|
},
|
||||||
error: next,
|
error: next,
|
||||||
})
|
})
|
||||||
return () => subscription.unsubscribe()
|
return () => subscription.unsubscribe()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return applausesForTransaction
|
return applauseDetails
|
||||||
}
|
}
|
||||||
76
src/hooks/ghost/useBabeSlots.js
Normal file
76
src/hooks/ghost/useBabeSlots.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import useSWRSubscription from "swr/subscription"
|
||||||
|
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
||||||
|
import { distinct, filter, map, mergeMap } from "rxjs"
|
||||||
|
|
||||||
|
import { useUnstableProvider } from "./UnstableProvider"
|
||||||
|
import { useMetadata } from "./MetadataProvider"
|
||||||
|
|
||||||
|
export const useGenesisSlot = () => {
|
||||||
|
const { chainHead$, chainId } = useUnstableProvider()
|
||||||
|
const metadata = useMetadata()
|
||||||
|
const { data: genesisSlot } = useSWRSubscription(
|
||||||
|
chainHead$ && chainId && metadata
|
||||||
|
? ["genesisSlot", chainHead$, chainId, metadata]
|
||||||
|
: null,
|
||||||
|
([_, chainHead$, chainId, metadata], { next }) => {
|
||||||
|
const { finalized$, storage$ } = chainHead$
|
||||||
|
const subscription = finalized$.pipe(
|
||||||
|
filter(Boolean),
|
||||||
|
mergeMap((blockInfo) => {
|
||||||
|
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||||
|
const genesisSlot = builder.buildStorage("Babe", "GenesisSlot")
|
||||||
|
return storage$(blockInfo?.hash, "value", () =>
|
||||||
|
genesisSlot?.keys.enc()
|
||||||
|
).pipe(
|
||||||
|
filter(Boolean),
|
||||||
|
distinct(),
|
||||||
|
map((value) => genesisSlot?.value.dec(value))
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next(genesisSlot) {
|
||||||
|
next(null, genesisSlot)
|
||||||
|
},
|
||||||
|
error: next,
|
||||||
|
})
|
||||||
|
return () => subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return genesisSlot
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCurrentSlot = () => {
|
||||||
|
const { chainHead$, chainId } = useUnstableProvider()
|
||||||
|
const metadata = useMetadata()
|
||||||
|
const { data: currentSlot } = useSWRSubscription(
|
||||||
|
chainHead$ && chainId && metadata
|
||||||
|
? ["currentSlot", chainHead$, chainId, metadata]
|
||||||
|
: null,
|
||||||
|
([_, chainHead$, chainId, metadata], { next }) => {
|
||||||
|
const { finalized$, storage$ } = chainHead$
|
||||||
|
const subscription = finalized$.pipe(
|
||||||
|
filter(Boolean),
|
||||||
|
mergeMap((blockInfo) => {
|
||||||
|
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||||
|
const currentSlot = builder.buildStorage("Babe", "CurrentSlot")
|
||||||
|
return storage$(blockInfo?.hash, "value", () =>
|
||||||
|
currentSlot?.keys.enc()
|
||||||
|
).pipe(
|
||||||
|
filter(Boolean),
|
||||||
|
distinct(),
|
||||||
|
map((value) => currentSlot?.value.dec(value))
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next(currentSlot) {
|
||||||
|
next(null, currentSlot)
|
||||||
|
},
|
||||||
|
error: next,
|
||||||
|
})
|
||||||
|
return () => subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return currentSlot
|
||||||
|
}
|
||||||
44
src/hooks/ghost/useBlockCommitments.js
Normal file
44
src/hooks/ghost/useBlockCommitments.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import useSWRSubscription from "swr/subscription"
|
||||||
|
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
||||||
|
import { distinct, filter, map, mergeMap } from "rxjs"
|
||||||
|
|
||||||
|
import { useUnstableProvider } from "./UnstableProvider"
|
||||||
|
import { useMetadata } from "./MetadataProvider"
|
||||||
|
|
||||||
|
export const useBlockCommitments = ({ evmChainId }) => {
|
||||||
|
const { chainHead$, chainId } = useUnstableProvider()
|
||||||
|
const metadata = useMetadata()
|
||||||
|
const { data: blockCommitments } = useSWRSubscription(
|
||||||
|
chainHead$ && chainId && metadata
|
||||||
|
? ["blockCommitments", chainHead$, evmChainId, chainId, metadata]
|
||||||
|
: null,
|
||||||
|
([_, chainHead$, evmChainId, chainId, metadata], { next }) => {
|
||||||
|
const { finalized$, storage$ } = chainHead$
|
||||||
|
const subscription = finalized$.pipe(
|
||||||
|
filter(Boolean),
|
||||||
|
mergeMap((blockInfo) => {
|
||||||
|
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||||
|
const blockCommitments = builder.buildStorage("GhostSlowClaps", "BlockCommitments")
|
||||||
|
return storage$(blockInfo?.hash, "value", () =>
|
||||||
|
blockCommitments?.keys.enc(BigInt(evmChainId))
|
||||||
|
).pipe(
|
||||||
|
filter(Boolean),
|
||||||
|
distinct(),
|
||||||
|
map((value) => blockCommitments?.value.dec(value))
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next(blockCommitments) {
|
||||||
|
next(null, blockCommitments)
|
||||||
|
},
|
||||||
|
error: next,
|
||||||
|
})
|
||||||
|
return () => subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return blockCommitments?.reduce((acc, [index, obj]) => {
|
||||||
|
acc[index] = obj;
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
41
src/hooks/ghost/useDisabledValidators.js
Normal file
41
src/hooks/ghost/useDisabledValidators.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import useSWRSubscription from "swr/subscription"
|
||||||
|
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
||||||
|
import { distinct, filter, map, mergeMap } from "rxjs"
|
||||||
|
|
||||||
|
import { useUnstableProvider } from "./UnstableProvider"
|
||||||
|
import { useMetadata } from "./MetadataProvider"
|
||||||
|
|
||||||
|
export const useDisabledValidators = () => {
|
||||||
|
const { chainHead$, chainId } = useUnstableProvider()
|
||||||
|
const metadata = useMetadata()
|
||||||
|
const { data: disabledIndexes, error } = useSWRSubscription(
|
||||||
|
chainHead$ && chainId && metadata
|
||||||
|
? ["disabledIndexes", chainHead$, chainId, metadata]
|
||||||
|
: null,
|
||||||
|
([_, chainHead$, chainId, metadata], { next }) => {
|
||||||
|
const { finalized$, storage$ } = chainHead$
|
||||||
|
const subscription = finalized$.pipe(
|
||||||
|
filter(Boolean),
|
||||||
|
mergeMap((blockInfo) => {
|
||||||
|
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||||
|
const disabledIndexes = builder.buildStorage("Session", "DisabledValidators")
|
||||||
|
return storage$(blockInfo?.hash, "value", () =>
|
||||||
|
disabledIndexes?.keys.enc()
|
||||||
|
).pipe(
|
||||||
|
filter(Boolean),
|
||||||
|
distinct(),
|
||||||
|
map((value) => disabledIndexes?.value.dec(value))
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next(disabledIndexes) {
|
||||||
|
next(null, disabledIndexes)
|
||||||
|
},
|
||||||
|
error: next,
|
||||||
|
})
|
||||||
|
return () => subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return disabledIndexes ? disabledIndexes : []
|
||||||
|
}
|
||||||
41
src/hooks/ghost/useErasTotalStaked.js
Normal file
41
src/hooks/ghost/useErasTotalStaked.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import useSWRSubscription from "swr/subscription"
|
||||||
|
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
||||||
|
import { distinct, filter, map, mergeMap } from "rxjs"
|
||||||
|
|
||||||
|
import { useUnstableProvider } from "./UnstableProvider"
|
||||||
|
import { useMetadata } from "./MetadataProvider"
|
||||||
|
|
||||||
|
export const useErasTotalStake = ({ eraIndex }) => {
|
||||||
|
const { chainHead$, chainId } = useUnstableProvider()
|
||||||
|
const metadata = useMetadata()
|
||||||
|
const { data: eraTotalStake } = useSWRSubscription(
|
||||||
|
chainHead$ && chainId && metadata
|
||||||
|
? ["eraTotalStake", chainHead$, eraIndex, chainId, metadata]
|
||||||
|
: null,
|
||||||
|
([_, chainHead$, eraIndex, chainId, metadata], { next }) => {
|
||||||
|
const { finalized$, storage$ } = chainHead$
|
||||||
|
const subscription = finalized$.pipe(
|
||||||
|
filter(Boolean),
|
||||||
|
mergeMap((blockInfo) => {
|
||||||
|
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||||
|
const eraTotalStake = builder.buildStorage("Staking", "ErasTotalStake")
|
||||||
|
return storage$(blockInfo?.hash, "value", () =>
|
||||||
|
eraTotalStake?.keys.enc(eraIndex)
|
||||||
|
).pipe(
|
||||||
|
filter(Boolean),
|
||||||
|
distinct(),
|
||||||
|
map((value) => eraTotalStake?.value.dec(value))
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next(eraTotalStake) {
|
||||||
|
next(null, eraTotalStake)
|
||||||
|
},
|
||||||
|
error: next,
|
||||||
|
})
|
||||||
|
return () => subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return eraTotalStake;
|
||||||
|
}
|
||||||
41
src/hooks/ghost/useValidators.js
Normal file
41
src/hooks/ghost/useValidators.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import useSWRSubscription from "swr/subscription"
|
||||||
|
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
|
||||||
|
import { distinct, filter, map, mergeMap } from "rxjs"
|
||||||
|
|
||||||
|
import { useUnstableProvider } from "./UnstableProvider"
|
||||||
|
import { useMetadata } from "./MetadataProvider"
|
||||||
|
|
||||||
|
export const useValidators = ({ currentSession }) => {
|
||||||
|
const { chainHead$, chainId } = useUnstableProvider()
|
||||||
|
const metadata = useMetadata()
|
||||||
|
const { data: slowClapValidators } = useSWRSubscription(
|
||||||
|
chainHead$ && chainId && metadata
|
||||||
|
? ["slowClapValidators", chainHead$, currentSession, chainId, metadata]
|
||||||
|
: null,
|
||||||
|
([_, chainHead$, currentSession, chainId, metadata], { next }) => {
|
||||||
|
const { finalized$, storage$ } = chainHead$
|
||||||
|
const subscription = finalized$.pipe(
|
||||||
|
filter(Boolean),
|
||||||
|
mergeMap((blockInfo) => {
|
||||||
|
const builder = getDynamicBuilder(getLookupFn(metadata))
|
||||||
|
const slowClapValidators = builder.buildStorage("GhostSlowClaps", "Validators")
|
||||||
|
return storage$(blockInfo?.hash, "value", () =>
|
||||||
|
slowClapValidators?.keys.enc(currentSession)
|
||||||
|
).pipe(
|
||||||
|
filter(Boolean),
|
||||||
|
distinct(),
|
||||||
|
map((value) => slowClapValidators?.value.dec(value))
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next(slowClapValidators) {
|
||||||
|
next(null, slowClapValidators)
|
||||||
|
},
|
||||||
|
error: next,
|
||||||
|
})
|
||||||
|
return () => subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return slowClapValidators
|
||||||
|
}
|
||||||
@ -128,3 +128,48 @@ a:hover svg {
|
|||||||
.tooltip {
|
.tooltip {
|
||||||
z-index: 9999999;
|
z-index: 9999999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar {
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 400px;
|
||||||
|
|
||||||
|
/* For Chrome, Safari, Edge */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent; /* Hide track */
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #888; /* Only visible part */
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This definitely hides arrows in Chrome/Safari/Edge */
|
||||||
|
&::-webkit-scrollbar-button {
|
||||||
|
display: none; /* ← THIS WORKS */
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
background: transparent; /* Hide corner */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For Firefox */
|
||||||
|
scrollbar-width: thin; /* auto | thin | none */
|
||||||
|
scrollbar-color: #fff transparent; /* thumb track */
|
||||||
|
}
|
||||||
|
|
||||||
|
input:-webkit-autofill {
|
||||||
|
-webkit-box-shadow: 0 0 0 1000px transparent inset !important;
|
||||||
|
box-shadow: 0 0 0 1000px transparent inset !important;
|
||||||
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
|
color: #ffffff !important;
|
||||||
|
-webkit-text-fill-color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|||||||
@ -18,9 +18,18 @@ export const darkPalette = {
|
|||||||
success: "#60C45B", // idk where this is - done
|
success: "#60C45B", // idk where this is - done
|
||||||
userFeedback: "#49A1F2", // idk where this is
|
userFeedback: "#49A1F2", // idk where this is
|
||||||
error: "#F06F73", // red negative % - done
|
error: "#F06F73", // red negative % - done
|
||||||
warning: "#49A1F2", // idk where this is - done
|
warning: "#ed6c02", // idk where this is - done
|
||||||
pnlGain: "#60C45B", // green positive % - done
|
pnlGain: "#60C45B", // green positive % - done
|
||||||
},
|
},
|
||||||
|
bridgeProgress: {
|
||||||
|
error: "#F06F73",
|
||||||
|
warning: "#ed6c02",
|
||||||
|
success: "#60C45B",
|
||||||
|
},
|
||||||
|
validatorsColor: {
|
||||||
|
red: "#fd9b9e",
|
||||||
|
green: "#60c45b",
|
||||||
|
},
|
||||||
gray: {
|
gray: {
|
||||||
800: "#1F4671", // active menu - done
|
800: "#1F4671", // active menu - done
|
||||||
700: "#50759E", // menu background color - done
|
700: "#50759E", // menu background color - done
|
||||||
|
|||||||
@ -6,11 +6,20 @@ export const lightPalette = {
|
|||||||
},
|
},
|
||||||
background: "linear-gradient(180.37deg, #B3BFC5 0.49%, #D1D5D4 26.3%, #EEEAE3 99.85%)",
|
background: "linear-gradient(180.37deg, #B3BFC5 0.49%, #D1D5D4 26.3%, #EEEAE3 99.85%)",
|
||||||
feedback: {
|
feedback: {
|
||||||
success: "#94B9A1",
|
success: "#60C45B", // idk where this is - done
|
||||||
userFeedback: "#49A1F2",
|
userFeedback: "#49A1F2", // idk where this is
|
||||||
error: "#FF6767",
|
error: "#F06F73", // red negative % - done
|
||||||
warning: "#FC8E5F",
|
warning: "#ed6c02", // idk where this is - done
|
||||||
pnlGain: "#3D9C70",
|
pnlGain: "#60C45B", // green positive % - done
|
||||||
|
},
|
||||||
|
bridgeProgress: {
|
||||||
|
error: "#F06F73",
|
||||||
|
warning: "#ed6c02",
|
||||||
|
success: "#60C45B",
|
||||||
|
},
|
||||||
|
validatorsColor: {
|
||||||
|
red: "#fd9b9e",
|
||||||
|
green: "#60c45b",
|
||||||
},
|
},
|
||||||
gray: {
|
gray: {
|
||||||
700: "#FAFAFB",
|
700: "#FAFAFB",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user