939 lines
55 KiB
JavaScript
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
|
|
<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
|
|
<Link
|
|
underline="always"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
href="https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases"
|
|
>
|
|
GHOST Wallet
|
|
</Link> 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;
|