ghost-dao-interface/src/containers/Bridge/BridgeModal.jsx
Uncle Fatso ad55c04525
governance rev.5
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-02-18 11:17:08 +03:00

460 lines
24 KiB
JavaScript

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 AccountBalanceIcon from '@mui/icons-material/AccountBalance';
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, TertiaryButton } from "../../components/Button";
import { formatCurrency } from "../../helpers";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
export const BridgeModal = ({
providerDetail,
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&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">
{!providerDetail && <Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<TertiaryButton
fullWidth
onClick={() => window.open('https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases', '_blank', 'noopener,noreferrer')}
>
Get GHOST Connect
</TertiaryButton>
</Box>}
{providerDetail && <Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
{currentRecord?.finalization > 0 && (
<>
<Box
sx={{
transition: "all 0.8s ease",
transform: currentRecord?.finalization > 0 && "scale(1.2)",
color: currentRecord?.finalization === 0 && theme.colors.primary[300]
}}
width="120px"
display="flex"
flexDirection="column"
justifyContent="start"
alignItems="center"
>
<GhostStyledIcon
sx={{
width: "35px",
height: "35px",
animation: currentRecord?.finalization > 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?.finalization > 0 && "0.2"
}}
component={ArrowRightIcon}
/>
</>
)}
<Box
sx={{
transition: "all 0.3s ease",
opacity: currentRecord?.finalization > 0 && "0.2",
transform: currentRecord?.finalization === 0 && currentRecord?.clappedPercentage < 50n && "scale(1.2)",
color: currentRecord?.clappedPercentage > 50n && theme.colors.primary[300]
}}
width="120px"
display="flex"
flexDirection="column"
justifyContent="start"
alignItems="center"
>
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center">
{currentRecord?.clappedPercentage < 50n
? <GhostStyledIcon
sx={{
width: "35px",
height: "35px",
animation: currentRecord?.finalization === 0 && 'bounce 2s ease-in-out infinite',
'@keyframes bounce': {
'0%, 20%, 50%, 80%, 75%, 100%': { transform: 'translateY(0)' },
'40%': { transform: 'translateY(-4px)' },
'60%': { transform: 'translateY(-2px)' },
},
}}
viewBox="0 0 25 25"
component={AccountBalanceIcon}
/>
: <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>
<GhostStyledIcon
sx={{ transition: "all 0.3s ease", opacity: currentRecord?.clappedPercentage < 50n && "0.2" }}
component={ArrowRightIcon}
/>
<Box
sx={{
transition: "all 0.3s ease",
opacity: currentRecord?.finalization > 0 && "0.2",
transform: currentRecord?.finalization === 0 && currentRecord?.clapsPercentage < 50 && "scale(1.2)",
color: currentRecord?.clapsPercentage > 50 && theme.colors.primary[300]
}}
width="120px"
display="flex"
flexDirection="column"
justifyContent="start"
alignItems="center"
>
<Box display="flex" flexDirection="row" justifyContent="center" alignItems="center">
{currentRecord?.clapsPercentage < 50
? (
<>
<GhostStyledIcon
sx={{
width: "35px",
height: "35px",
animation: currentRecord?.finalization === 0 && '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?.finalization === 0 && '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>
{currentRecord?.finalization === 0 && (
<>
<GhostStyledIcon
sx={{ transition: "all 0.3s ease", opacity: !currentRecord?.applaused && "0.2" }}
component={ArrowRightIcon}
/>
<Box
sx={{
transition: "all 0.3s ease",
opacity: !currentRecord?.applaused && "0.2",
transform: currentRecord?.applaused && "scale(1.2)",
color: currentRecord?.applaused && 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={CheckCircleIcon}
/>
<Typography variant="caption">Applaused</Typography>
<Typography variant="caption">Check Receiver</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">
<Box display="flex" flexDirection="row">
<Typography variant="body2">Executed at:</Typography>
<InfoTooltip message="The timestamp when the bridge transaction was submitted." />
</Box>
<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}%.`}&nbsp;
<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://ghostchain.io/bridge-disclaimer"
>
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>
)
}