460 lines
24 KiB
JavaScript
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
|
|
<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}%.`}
|
|
<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>
|
|
)
|
|
}
|