ghost-dao-interface/src/containers/Bridge/Bridge.jsx
Uncle Fatso cffaad6973
small fixes for more comfortable applause detection
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2025-11-12 19:37:23 +03:00

939 lines
55 KiB
JavaScript

import { useEffect, useState, useMemo, useCallback } from "react";
import ReactGA from "react-ga4";
import {
Box,
Container,
Typography,
Link,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
TableContainer,
useMediaQuery,
useTheme
} from "@mui/material";
import { ss58Decode, ss58Address } from "@polkadot-labs/hdkd-helpers";
import { toHex } from "@polkadot-api/utils";
import { decodeAddress } from "@polkadot/util-crypto";
import { useTransactionConfirmations } from "wagmi";
import { getBlockNumber } from "@wagmi/core";
import { keccak256 } from "viem";
import { u64, u128 } from "scale-ts";
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import PendingIcon from '@mui/icons-material/Pending';
import PendingActionsIcon from '@mui/icons-material/PendingActions';
import ArrowBack from '@mui/icons-material/ArrowBack';
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
import HourglassBottomIcon from '@mui/icons-material/HourglassBottom';
import ThumbUpIcon from '@mui/icons-material/ThumbUp';
import ThumbDownAltIcon from '@mui/icons-material/ThumbDownAlt';
import HandshakeIcon from '@mui/icons-material/Handshake';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import CheckIcon from '@mui/icons-material/Check';
import PageTitle from "../../components/PageTitle/PageTitle";
import Paper from "../../components/Paper/Paper";
import SwapCard from "../../components/Swap/SwapCard";
import SwapCollection from "../../components/Swap/SwapCollection";
import TokenStack from "../../components/TokenStack/TokenStack";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import Modal from "../../components/Modal/Modal";
import InfoTooltip from "../../components/Tooltip/InfoTooltip";
import { PrimaryButton } from "../../components/Button";
import { GATEKEEPER_ADDRESSES } from "../../constants/addresses";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { formatCurrency } from "../../helpers";
import { useTokenSymbol, useBalance } from "../../hooks/tokens";
import { useGatekeeperAddress, ghost } from "../../hooks/staking";
import {
useEvmNetwork,
useClapsInSession,
useApplauseThreshold,
useReceivedClaps,
useApplausesForTransaction,
useAuthorities,
useCurrentIndex,
useUnstableProvider,
useMetadata
} from "../../hooks/ghost";
const STORAGE_PREFIX = "storedTransactions"
const Bridge = ({ chainId, address, config, connect }) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isSemiSmallScreen = useMediaQuery("(max-width: 540px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
const [copiedIndex, setCopiedIndex] = useState(null);
const [isPending, setIsPending] = useState(false);
const [bridgeAction, setBridgeAction] = useState(true);
const [activeTxIndex, setActiveTxIndex] = useState(-1);
const [receiver, setReceiver] = useState("");
const [convertedReceiver, setConvertedReceiver] = useState(undefined);
const [amount, setAmount] = useState("");
const [rotation, setRotation] = useState(0);
const [blockNumber, setBlockNumber] = useState(0);
const sliceString = (string, first, second) => {
if (!string) return "";
return string.slice(0, first) + "..." + string.slice(second);
}
const initialStoredTransactions = localStorage.getItem(STORAGE_PREFIX);
const [storedTransactions, setStoredTransactions] = useState(
initialStoredTransactions ? JSON.parse(initialStoredTransactions) : []
);
const { providerDetail } = useUnstableProvider();
const metadata = useMetadata();
const watchTransaction = useMemo(() => {
if (activeTxIndex < 0 || activeTxIndex >= storedTransactions?.length) {
return undefined
}
return storedTransactions?.at(activeTxIndex)
}, [activeTxIndex, storedTransactions])
const hashedArguments = useMemo(() => {
if (!watchTransaction) return undefined
const amountEncoded = u128.enc(BigInt(watchTransaction.amount));
const networkIdEncoded = u64.enc(BigInt(chainId));
const addressEncoded = decodeAddress(watchTransaction.receiverAddress, false, 1996);
const clapArgsStr = new Uint8Array([
...addressEncoded,
...amountEncoded,
...networkIdEncoded
]);
return keccak256(clapArgsStr)
}, [watchTransaction])
const currentSession = useCurrentIndex();
const evmNetwork = useEvmNetwork({ evmChainId: chainId });
const authorities = useAuthorities({
currentSession: watchTransaction?.sessionIndex ?? currentSession
});
const clapsInSession = useClapsInSession({
currentSession: watchTransaction?.sessionIndex ?? currentSession
});
const appluseThreshold = useApplauseThreshold();
const receivedClaps = useReceivedClaps({
currentSession: watchTransaction?.sessionIndex ?? currentSession,
txHash: watchTransaction?.transactionHash,
argsHash: hashedArguments
});
const transactionApplaused = useApplausesForTransaction({
currentSession: watchTransaction?.sessionIndex ?? currentSession,
txHash: watchTransaction?.transactionHash,
argsHash: hashedArguments
});
const finalityDelay = Number(evmNetwork?.finality_delay ?? 0n);
const incomingFee = Number(evmNetwork?.incoming_fee ?? 0n) / 10000000;
getBlockNumber(config).then(block => setBlockNumber(block));
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const {
balance: ghstBalance,
refetch: ghstBalanceRefetch
} = useBalance(chainId, "GHST", address);
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: "/bridge" });
}, []);
useEffect(() => {
const interval = setInterval(() => {
setRotation((prevRotation) => prevRotation > 0 ? 0 : 180)
}, 2000);
return () => clearInterval(interval);
}, [setRotation])
useEffect(() => {
try {
const [publicKey, prefix] = ss58Decode(receiver);
if (prefix !== 1995 && prefix !== 1996) {
throw new Error("bad prefix");
}
setConvertedReceiver(toHex(publicKey));
} catch {
setConvertedReceiver(undefined);
}
}, [receiver])
const clapsInSessionLength = useMemo(() => {
const disabledIndexes = new Set(clapsInSession?.filter(item => item.at(1).disabled).map(item => item.at(0)));
return authorities?.filter((_, idx) => !disabledIndexes.has(idx)).length ?? 0;
}, [authorities, clapsInSession]);
const chainExplorerUrl = useMemo(() => {
const client = config?.getClient();
return client?.chain?.blockExplorers?.default?.url;
}, [config]);
const chainName = useMemo(() => {
const client = config?.getClient();
return client?.chain?.name;
}, [config]);
const currentRecord = useMemo(() => {
if (!watchTransaction) return undefined
const finalization = Math.max(0, (finalityDelay + watchTransaction.blockNumber) - Number(blockNumber));
const receivedClapsLength = receivedClaps?.length ?? 0;
const clapsNeeded = Math.floor(clapsInSessionLength * appluseThreshold / 100);
const step = finalization > 0
? 0
: receivedClapsLength < clapsNeeded && !transactionApplaused
? 1
: !transactionApplaused
? 2
: 3;
return {
...watchTransaction,
finalization,
step,
}
}, [
transactionApplaused,
receivedClaps,
appluseThreshold,
clapsInSessionLength,
finalityDelay,
watchTransaction,
blockNumber
]);
const gatekeeperAddressEmpty = useMemo(() => {
if (gatekeeperAddress === "0x0000000000000000000000000000000000000000") {
return true;
}
return false;
}, [gatekeeperAddress]);
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 filteredStoredTransactions = useMemo(() => {
return storedTransactions.filter(obj => obj.chainId === chainId);
}, [storedTransactions, chainId]);
const selfApplauseUrl = useMemo(() => {
if (!currentRecord) return '';
const amount = new DecimalBigNumber(BigInt(currentRecord.amount), 18).toString();
let url = "https://lite.ghostchain.io/#/applause?";
url += `networkId=${currentRecord.chainId}&`;
url += `sessionIndex=${currentRecord.sessionIndex}&`;
url += `amount=${amount}&`;
url += `receiver=${currentRecord.receiverAddress}&`;
url += `transactionHash=${currentRecord.transactionHash}`;
return url;
}, [currentRecord]);
const removeStoredRecord = useCallback(() => {
const newStoredTransactions = storedTransactions.filter((_, index) => index !== activeTxIndex)
setStoredTransactions(newStoredTransactions);
localStorage.setItem(STORAGE_PREFIX, JSON.stringify(newStoredTransactions));
setActiveTxIndex(-1);
}, [storedTransactions, activeTxIndex, setStoredTransactions, setActiveTxIndex]);
const copyToClipboard = (text, index) => {
navigator.clipboard.writeText(text).then(() => {
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null) , 800);
});
};
const ghostOrConnect = async () => {
if (address === "") {
connect();
} else {
setIsPending(true);
try {
const txHash = await ghost(chainId, address, convertedReceiver, preparedAmount);
const transaction = {
sessionIndex: currentSession ?? 0,
transactionHash: txHash,
receiverAddress: receiver,
amount: preparedAmount.toString(),
chainId: chainId,
blockNumber: Number(blockNumber),
timestamp: Date.now()
}
const newStoredTransactions = [...storedTransactions, transaction];
setStoredTransactions(newStoredTransactions);
localStorage.setItem(STORAGE_PREFIX, JSON.stringify(newStoredTransactions));
if (providerDetail) {
setActiveTxIndex(newStoredTransactions.length - 1)
}
} finally {
await ghstBalanceRefetch();
setReceiver("");
setAmount("");
setIsPending(false);
}
}
}
return (
<Box height="calc(100vh - 43px)">
<PageTitle name="GHOST Bridge" subtitle="The only pure Web3 decentralized bridge." />
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
paddingRight: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "calc(100vh - 153px)"
}}
>
<Modal
data-testid="transaction-details-modal"
maxWidth="476px"
headerContent={
<Box display="flex" flexDirection="row">
<Typography variant="h5">
TX Hash&nbsp;
<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.2s 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",
transition: "transform 0.7s ease-in-out",
transform: (currentRecord?.finalization ?? 0) > 0
? `rotate(${rotation}deg)`
: "rotate(0deg)"
}}
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.2s ease", opacity: currentRecord?.step < 1 && "0.2" }}
component={ArrowRightIcon}
/>
<Box
sx={{
transition: "all 0.2s 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",
transition: "transform 0.7s ease-in-out",
transform: `rotateX(${currentRecord.step == 1 ? rotation : 0}deg)`
}}
viewBox="0 0 25 25"
component={ThumbUpIcon}
/>
<GhostStyledIcon
sx={{
width: "35px",
height: "35px",
transition: "transform 0.7s ease-in-out",
transform: `rotateX(${currentRecord.step == 1 ? rotation : 0}deg)`
}}
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">{receivedClaps?.length ?? 0} / {clapsInSessionLength}</Typography>
</Box>
</Box>
<GhostStyledIcon
sx={{
transition: "all 0.2s ease",
opacity: currentRecord?.step < 2 && "0.2"
}}
component={ArrowRightIcon}
/>
<Box
sx={{
transition: "all 0.2s 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"
>
<GhostStyledIcon
sx={{ width: "35px", height: "35px" }}
viewBox="0 0 25 25"
component={currentRecord?.step === 3 ? CheckCircleIcon : PendingIcon}
/>
<Typography variant="caption">Applaused</Typography>
<Typography variant="caption">{
currentRecord?.step === 3 ? "Check Receiver" : "Waiting Room"
}</Typography>
</Box>
</Box>
{(currentRecord?.step ?? 3) < 3 && (currentSession && currentRecord && currentSession > (currentRecord.sessionIndex ?? 0) + 2) &&
<Box display="flex" flexDirection="column" gap="5px">
<PrimaryButton
fullWidth
onClick={() => window.open(
selfApplauseUrl,
'_blank',
'noopener,noreferrer'
)}
>
Self Applause
</PrimaryButton>
<Typography variant="body2" sx={{ fontStyle: "italic" }}>
Your transaction seems to be stuck, possibly because of a problem with some inactive validators on the network.
</Typography>
</Box>
}
<Box display="flex" flexDirection="column" gap="5px" padding="0.6rem 0">
<Box display="flex" flexDirection="row" justifyContent="space-between">
<Box display="flex" flexDirection="row">
<Typography variant="body2">Session Index:</Typography>
<InfoTooltip message={<>
<Typography variant="subtitle1">Transaction Watchmen</Typography>
<Box
width="280px"
height="250px"
margin="0"
padding="0"
marginTop="5px"
display="flex"
flexDirection="column"
sx={{ overflowY: "auto" }}
>
{authorities?.map((authority, idx) => {
const authorityAddress = ss58Address(authority.asHex(), 1996);
const disabled = clapsInSession?.find((info => info.at(0) === idx))?.at(1)?.disabled;
const clapped = receivedClaps?.some(authId => authId === idx);
return (
<Box
key={idx}
display="flex"
flexDirection="row"
justifyContent="space-between"
alignItems="center"
>
<Typography
sx={{
fontSize: "10px",
width: "275px",
overflow: 'hidden',
whiteSpace: 'normal',
textOverflow: "ellipsis",
color: clapped
? theme.colors.primary[300]
: disabled
? theme.colors.feedback.error
: theme.colors.gray[10]
}}
>
{authorityAddress}
</Typography>
</Box>
)
})}
</Box>
</>} />
</Box>
<Typography variant="body2">{currentRecord?.sessionIndex}</Typography>
</Box>
<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">Sent 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>
<hr style={{ width: "100%" }} />
<Box display="flex" flexDirection="row" justifyContent="space-between">
<Typography variant="body2">Transaction Hash:</Typography>
<Link
style={{ display: "flex", flexDirection: "row", justifyContent: "center", alignItems: "center" }}
onClick={() => copyToClipboard(currentRecord ? currentRecord.transactionHash : "", 1)}
>
<Typography variant="body2">
{currentRecord ? sliceString(currentRecord.transactionHash, 10, -8) : "0x"}
</Typography>
<GhostStyledIcon
sx={{ marginLeft: "5px", width: "12px", height: "12px" }}
viewBox="0 0 25 25"
component={copiedIndex === 1 ? CheckIcon : ContentPasteIcon}
/>
</Link>
</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, 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>
</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 remove the transaction record from the session storage, but it will not cancel the bridge transaction.
</Typography>
</Box>
</Box>
</Modal>
<Box width="100%" maxWidth="506px" display="flex" alignItems="center" justifyContent="center" flexDirection="column">
<Paper
headerContent={
<Box alignItems="center" justifyContent="start" display="flex" width="100%">
{!bridgeAction && (<GhostStyledIcon
component={ArrowBack}
viewBox="0 0 23 23"
style={{ marginRight: "20px", display: "flex", alignSelf: "center", cursor: "pointer" }}
onClick={() => setBridgeAction(!bridgeAction)}
/>)}
<Typography variant="h4">{
bridgeAction ? `Bridge $${ghstSymbol}` : "Transaction History"
}</Typography>
</Box>
}
topRight={bridgeAction && (<GhostStyledIcon
component={PendingActionsIcon}
viewBox="0 0 23 23"
style={{ display: "flex", alignItems: "center", cursor: "pointer" }}
onClick={() => setBridgeAction(!bridgeAction)}
/>)}
enableBackground
fullWidth
>
<Box minHeight="300px" display="flex" flexDirection="column" gap="5px">
{bridgeAction && (
<>
<SwapCollection
iconNotNeeded
UpperSwapCard={<SwapCard
id={`bridge-token-receiver`}
inputWidth={isVerySmallScreen ? "180px" : isSemiSmallScreen ? "280px" : "516px"}
value={receiver}
onChange={event => setReceiver(event.currentTarget.value)}
inputProps={{ "data-testid": "fromInput" }}
placeholder="Ghost address (sf prefixed)"
type="text"
maxWidth="446px"
/>}
LowerSwapCard={<SwapCard
id={`bridge-token-amount`}
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "280px"}
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="446px"
/>}
/>
<Box
mb="20px"
mt="20px"
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 maxWidth="506px" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
{!providerDetail
? (
<Typography mr="10px" variant="body2" color="textSecondary">
<em>
GHOST Wallet is not detected on your browser. Download&nbsp;
<Link
underline="always"
target="_blank"
rel="noopener noreferrer"
href="https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases"
>
GHOST Wallet
</Link>&nbsp; to see full detalization for bridge transaction.
</em>
</Typography>
)
: metadata
? (
<Box width="100%" display="flex" flexDirection="column" gap="0px">
<Box maxWidth="506px" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">Estimated Fee:</Typography>}
<Typography fontSize="12px" lineHeight="15px">{incomingFee.toFixed(4)}%</Typography>
</Box>
<Box maxWidth="506px" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
{!isVerySmallScreen && <Box display="flex" flexDirection="row">
<Typography fontSize="12px" lineHeight="15px">Finality Delay:</Typography>
<InfoTooltip message="The finality delay period guarantees that a bridging transaction becomes permanent and immutable on the GHOST Chain ledger." />
</Box>}
<Typography fontSize="12px" lineHeight="15px">{finalityDelay} blocks</Typography>
</Box>
<hr width="100%" />
<Box maxWidth="506px" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
{!isVerySmallScreen && <Typography fontSize="12px" lineHeight="15px">Current GHOST Epoch:</Typography>}
<Typography fontSize="12px" lineHeight="15px">{currentSession ?? 0}</Typography>
</Box>
<Box maxWidth="506px" display="flex" justifyContent={isVerySmallScreen ? "end" : "space-between"}>
{!isVerySmallScreen && <Box display="flex" flexDirection="row">
<Typography fontSize="12px" lineHeight="15px">Current Validators:</Typography>
<InfoTooltip message={<>
<Typography variant="subtitle1">Validators</Typography>
<Box
width="280px"
height="250px"
margin="0"
padding="0"
marginTop="5px"
display="flex"
flexDirection="column"
sx={{ overflowY: "auto" }}
>
{authorities?.map((authority, idx) => {
const authorityAddress = ss58Address(authority.asHex(), 1996);
const clapInfo = clapsInSession?.find((info => info.at(0) === idx))?.at(1);
return (
<Box
key={idx}
display="flex"
flexDirection="row"
justifyContent="space-between"
alignItems="center"
sx={{
fontSize: "10px",
color: !clapInfo?.disabled
? theme.colors.primary[300]
: theme.colors.feedback.error
}}
>
<div
style={{
width: "250px",
overflow: 'hidden',
whiteSpace: 'normal',
textOverflow: "ellipsis"
}}
>
{authorityAddress}
</div>
<div>{clapInfo?.claps ?? 0}</div>
</Box>
)
})}
</Box>
</>} />
</Box>}
<Typography fontSize="12px" lineHeight="15px">{clapsInSessionLength} / {authorities?.length ?? 0}</Typography>
</Box>
</Box>
)
: (
<Box display="flex" flexDirection="column" justifyContent="center">
<Typography fontSize="12px" lineHeight="15px">Downloading chain metadata, wait please...</Typography>
</Box>
)
}
</Box>
</Box>
<PrimaryButton
fullWidth
disabled={
isPending || address === "" || gatekeeperAddressEmpty || !convertedReceiver ||
preparedAmount === 0n || ghstBalance._value < preparedAmount
}
loading={isPending}
onClick={() => ghostOrConnect()}
>
{address === "" ? "Connect" : "Bridge" }
</PrimaryButton>
</>
)}
{!bridgeAction && (
<Box>
{!isSemiSmallScreen && (<Box display="grid" gridTemplateColumns="3fr 2fr 60px" sx={{ padding: "0.6rem", borderBottom: "1px solid" }}>
<Typography variant="subtitle1" sx={{ fontWeight: "bold" }}>Amount</Typography>
<Typography variant="subtitle1" sx={{ fontWeight: "bold" }}>Datetime</Typography>
<Typography variant="subtitle1" sx={{ justifySelf: "center", fontWeight: "bold" }}>Status</Typography>
</Box>)}
<Box display="flex" flexDirection="column" sx={{
height: "250px",
overflowY: "scroll",
msOverflowStyle: "thin !important",
scrollbarWidth: "thin !important",
}}>
{filteredStoredTransactions
.map((obj, idx) => (
<Box
display="grid"
gridTemplateColumns="3fr 2fr 60px"
sx={{
backgroundColor: theme.colors.gray[750],
borderBottom: "1px solid",
padding: "0.6rem",
paddingTop: "1.2rem",
cursor: 'pointer',
'&:hover': {
background: theme.colors.paper.cardHover
}
}}
key={obj.transactionHash}
onClick={() => setActiveTxIndex(idx)}
>
<Box display="flex" flexDirection="column" justifyContent="center">
<Typography variant={!isSemiSmallScreen ? "subtitle2" : "caption"}>
{formatCurrency(
new DecimalBigNumber(BigInt(obj.amount), 18).toString(),
isSemiSmallScreen ? 3 : 8,
ghstSymbol
)}
</Typography>
<Typography variant={!isSemiSmallScreen ? "subtitle2" : "caption"}>
{sliceString(
obj.receiverAddress,
isSemiSmallScreen ? 5 : 10,
isSemiSmallScreen ? -3 : -8
)}
</Typography>
</Box>
<Box display="flex" flexDirection="column" paddingLeft="5px">
<Typography variant={!isSemiSmallScreen ? "subtitle2" : "caption"}>
{new Date(obj.timestamp).toLocaleDateString('en-US')}
</Typography>
<Typography variant={!isSemiSmallScreen ? "subtitle2" : "caption"}>
{new Date(obj.timestamp).toLocaleTimeString('en-US')}
</Typography>
</Box>
<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>
</Box>
))}
</Box>
</Box>
)}
</Box>
</Paper>
</Box>
</Container>
</Box>
)
}
export default Bridge;