435 lines
19 KiB
JavaScript
435 lines
19 KiB
JavaScript
import { useEffect, useState, useMemo, useCallback } from "react";
|
|
import ReactGA from "react-ga4";
|
|
|
|
import {
|
|
Box,
|
|
Grid,
|
|
Container,
|
|
Typography,
|
|
Skeleton,
|
|
useMediaQuery,
|
|
} from "@mui/material";
|
|
import { decodeAddress } from "@polkadot/util-crypto";
|
|
import { getBlockNumber } from "@wagmi/core";
|
|
import { useTransaction } from "wagmi";
|
|
import { keccak256 } from "viem";
|
|
import { u32, u64, u128 } from "scale-ts";
|
|
|
|
import PendingActionsIcon from '@mui/icons-material/PendingActions';
|
|
import ArrowBack from '@mui/icons-material/ArrowBack';
|
|
|
|
import PageTitle from "../../components/PageTitle/PageTitle";
|
|
import Paper from "../../components/Paper/Paper";
|
|
import GhostStyledIcon from "../../components/Icon/GhostIcon";
|
|
|
|
import { networkAvgBlockSpeed } from "../../constants";
|
|
import { timeConverter } from "../../helpers";
|
|
|
|
import { useTokenSymbol, useBalance } from "../../hooks/tokens";
|
|
import { useGatekeeperAddress } from "../../hooks/staking";
|
|
|
|
import {
|
|
useEvmNetwork,
|
|
useApplauseDetails,
|
|
useAuthorities,
|
|
useValidators,
|
|
useBlockCommitments,
|
|
useDisabledValidators,
|
|
useCurrentIndex,
|
|
useUnstableProvider,
|
|
useApplauseThreshold,
|
|
useMetadata,
|
|
useCurrentSlot,
|
|
useGenesisSlot,
|
|
useErasTotalStake,
|
|
} from "../../hooks/ghost";
|
|
|
|
import { ValidatorTable } from "./ValidatorTable";
|
|
import { BridgeModal, BridgeConfirmModal } from "./BridgeModal";
|
|
import { BridgeHeader } from "./BridgeHeader";
|
|
import { BridgeCardAction, BridgeCardHistory } from "./BridgeCard";
|
|
|
|
const STORAGE_PREFIX = "storedTransactions"
|
|
|
|
const Bridge = ({ chainId, address, config, connect }) => {
|
|
const isBigScreen = useMediaQuery("(max-width: 980px)")
|
|
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
|
const isSemiSmallScreen = useMediaQuery("(max-width: 540px)");
|
|
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
|
|
|
const [bridgeModalOpen, setBridgeModalOpen] = useState(false);
|
|
const [isConfirmed, setIsConfirmed] = useState(false);
|
|
const [activeTxIndex, setActiveTxIndex] = useState(-1);
|
|
const [blockNumber, setBlockNumber] = useState(0n);
|
|
const [bridgeAction, setBridgeAction] = useState(true);
|
|
const [currentTime, setCurrentTime] = useState(Date.now());
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => setCurrentTime(Date.now()), 1000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const initialStoredTransactions = localStorage.getItem(STORAGE_PREFIX);
|
|
const [storedTransactions, setStoredTransactions] = useState(
|
|
initialStoredTransactions ? JSON.parse(initialStoredTransactions) : []
|
|
);
|
|
|
|
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
|
|
const gatekeeperAddressEmpty = useMemo(() => {
|
|
if (gatekeeperAddress === "0x0000000000000000000000000000000000000000") {
|
|
return true;
|
|
}
|
|
return false;
|
|
}, [gatekeeperAddress]);
|
|
|
|
const { providerDetail } = useUnstableProvider();
|
|
const metadata = useMetadata();
|
|
|
|
const watchTransaction = useMemo(() => {
|
|
if (activeTxIndex < 0 || activeTxIndex >= storedTransactions?.length) {
|
|
return undefined
|
|
}
|
|
return storedTransactions?.at(activeTxIndex)
|
|
}, [activeTxIndex, storedTransactions]);
|
|
|
|
const { data: watchTransactionInfo } = useTransaction({
|
|
hash: watchTransaction?.transactionHash
|
|
});
|
|
|
|
const hashedArguments = useMemo(() => {
|
|
if (!watchTransaction) return undefined
|
|
|
|
const networkIdEncoded = u64.enc(BigInt(chainId));
|
|
const amountEncoded = u128.enc(BigInt(watchTransaction.amount));
|
|
const addressEncoded = decodeAddress(watchTransaction.receiverAddress, false, 1996);
|
|
const blockNumber = u64.enc(watchTransactionInfo?.blockNumber ?? 0n);
|
|
|
|
const clapArgsStr = new Uint8Array([
|
|
...addressEncoded,
|
|
...amountEncoded,
|
|
...blockNumber,
|
|
...networkIdEncoded
|
|
]);
|
|
return keccak256(clapArgsStr)
|
|
}, [watchTransaction, watchTransactionInfo])
|
|
|
|
const currentSlot = useCurrentSlot();
|
|
const genesisSlot = useGenesisSlot();
|
|
const currentSession = useCurrentIndex();
|
|
const applauseThreshold = useApplauseThreshold();
|
|
const evmNetwork = useEvmNetwork({ evmChainId: chainId });
|
|
const totalStakedAmount = useErasTotalStake({
|
|
eraIndex: Math.floor((watchTransaction?.sessionIndex ?? currentSession) / 6)
|
|
});
|
|
const authorities = useAuthorities({
|
|
currentSession: watchTransaction?.sessionIndex ?? currentSession
|
|
});
|
|
const validators = useValidators({
|
|
currentSession: watchTransaction?.sessionIndex ?? currentSession
|
|
});
|
|
const blockCommitments = useBlockCommitments({ evmChainId: chainId });
|
|
const disabledValidators = useDisabledValidators();
|
|
const transactionApplaused = useApplauseDetails({
|
|
currentSession: watchTransaction?.sessionIndex ?? currentSession,
|
|
argsHash: hashedArguments
|
|
});
|
|
|
|
const finalityDelay = Number(evmNetwork?.finality_delay ?? 0n);
|
|
|
|
getBlockNumber(config).then(block => setBlockNumber(block));
|
|
|
|
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
|
|
|
useEffect(() => {
|
|
ReactGA.send({ hitType: "pageview", page: "/bridge" });
|
|
}, []);
|
|
|
|
const chainExplorerUrl = useMemo(() => {
|
|
const client = config?.getClient();
|
|
return client?.chain?.blockExplorers?.default?.url;
|
|
}, [config]);
|
|
|
|
const currentRecord = useMemo(() => {
|
|
if (!watchTransaction) return undefined
|
|
|
|
const countOnesInBigInt = (n) => {
|
|
let count = 0;
|
|
let tempN = n;
|
|
while (tempN > 0n) {
|
|
tempN &= (tempN - 1n);
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
const numberOfClaps = transactionApplaused?.authorities.buckets.reduce((sum, bucketPair) => {
|
|
const bigIntValue = bucketPair.at(1);
|
|
return sum + countOnesInBigInt(bigIntValue);
|
|
}, 0);
|
|
|
|
const finalization = Math.max(0, (finalityDelay + watchTransaction.blockNumber) - Number(blockNumber));
|
|
const applaused = transactionApplaused?.finalized ?? false;
|
|
const clappedAmount = transactionApplaused?.clapped_amount ?? 0n;
|
|
const clappedPercentage = clappedAmount * 100n / (totalStakedAmount ?? 1n);
|
|
const step = finalization > 0
|
|
? 0
|
|
: applaused ? 2 : 1;
|
|
|
|
return {
|
|
...watchTransaction,
|
|
finalization,
|
|
applaused,
|
|
numberOfClaps,
|
|
clappedAmount,
|
|
clappedPercentage,
|
|
step,
|
|
}
|
|
}, [
|
|
transactionApplaused,
|
|
finalityDelay,
|
|
watchTransaction,
|
|
blockNumber,
|
|
totalStakedAmount
|
|
]);
|
|
|
|
const filteredStoredTransactions = useMemo(() => {
|
|
return storedTransactions.filter(obj => obj.chainId === chainId);
|
|
}, [storedTransactions, chainId]);
|
|
|
|
const latestCommits = useMemo(() => {
|
|
return validators?.map((validator, index) => {
|
|
return {
|
|
validator: validator,
|
|
lastActive: currentTime - Number(blockCommitments?.at(index)?.last_updated ?? 0),
|
|
lastUpdated: blockCommitments?.at(index)?.last_updated,
|
|
lastStoredBlock: blockCommitments?.at(index)?.last_stored_block,
|
|
storedBlockTime: (blockCommitments?.at(index)?.last_stored_block ?? 0n) * networkAvgBlockSpeed(chainId),
|
|
disabled: disabledValidators?.includes(index),
|
|
}
|
|
})
|
|
}, [blockCommitments, disabledValidators, validators, chainId]);
|
|
|
|
const latestUpdate = useMemo(() => {
|
|
const validCommits = latestCommits?.filter(commit =>
|
|
!commit.disabled && commit.lastActive != null
|
|
) || [];
|
|
|
|
if (validCommits.length === 0) return null;
|
|
|
|
return Math.min(...validCommits.map(commit => commit.lastActive));
|
|
}, [latestCommits])
|
|
|
|
const slowestEvmBlock = useMemo(() => {
|
|
if (latestCommits?.length === 0) {
|
|
return 0n;
|
|
}
|
|
const slowestValidator = latestCommits?.reduce((min, commit) => {
|
|
return commit.lastStoredBlock < min.lastStoredBlock && !commit.disabled ? commit : min
|
|
});
|
|
if (!blockNumber || !slowestValidator) {
|
|
return 0n;
|
|
}
|
|
return blockNumber * networkAvgBlockSpeed(chainId) - slowestValidator?.storedBlockTime;
|
|
}, [latestCommits, blockNumber, chainId]);
|
|
|
|
const bridgeStability = useMemo(() => {
|
|
const length = latestCommits?.length ?? 0;
|
|
if (length === 0) {
|
|
return 0;
|
|
}
|
|
|
|
const blocksInFourHours = 14400n / networkAvgBlockSpeed(chainId);
|
|
let certainty = 0n;
|
|
for (let i = 0; i < length; i++) {
|
|
const commit = latestCommits.at(i);
|
|
if (commit.disabled || blockNumber < blocksInFourHours) {
|
|
continue;
|
|
}
|
|
certainty += (commit?.lastStoredBlock ?? 0n) - (blockNumber - blocksInFourHours);
|
|
}
|
|
return Math.max(Number(certainty * 100n / (blocksInFourHours * BigInt(length))), 0);
|
|
}, [latestCommits, blockNumber]);
|
|
|
|
const timeToNextEpoch = useMemo(() => {
|
|
if (!currentSession || !genesisSlot || !currentSlot) {
|
|
return undefined;
|
|
}
|
|
|
|
const valueOrDefault = (value) => value ?? 0n;
|
|
const blocks = (BigInt(currentSession ?? 0) + 1n) * 2400n + valueOrDefault(genesisSlot) - valueOrDefault(currentSlot);
|
|
return blocks * 6n;
|
|
}, [currentSession, genesisSlot, currentSlot, currentTime]);
|
|
|
|
const removeStoredRecord = useCallback(() => {
|
|
const newStoredTransactions = storedTransactions.filter((_, index) => index !== activeTxIndex)
|
|
setStoredTransactions(newStoredTransactions);
|
|
localStorage.setItem(storagePrefix, JSON.stringify(newStoredTransactions));
|
|
setActiveTxIndex(-1);
|
|
}, [storedTransactions, activeTxIndex, setStoredTransactions, setActiveTxIndex]);
|
|
|
|
const handleButtonProceed = () => {
|
|
setBridgeModalOpen(false);
|
|
setIsConfirmed(true);
|
|
}
|
|
|
|
const storeTransactionHash = (txHash, receiver, amount) => {
|
|
const transaction = {
|
|
sessionIndex: currentSession ?? 0,
|
|
transactionHash: txHash,
|
|
receiverAddress: receiver,
|
|
amount: amount,
|
|
chainId: chainId,
|
|
blockNumber: Number(blockNumber),
|
|
bridgeStability: bridgeStability,
|
|
timestamp: Date.now()
|
|
}
|
|
|
|
const newStoredTransactions = [transaction, ...storedTransactions];
|
|
setStoredTransactions(newStoredTransactions);
|
|
localStorage.setItem(STORAGE_PREFIX, JSON.stringify(newStoredTransactions));
|
|
setActiveTxIndex(newStoredTransactions.length - 1)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<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",
|
|
}}
|
|
>
|
|
<BridgeConfirmModal
|
|
bridgeStability={bridgeStability}
|
|
isOpen={bridgeModalOpen}
|
|
setClose={() => setBridgeModalOpen(false)}
|
|
handleButtonProceed={handleButtonProceed}
|
|
/>
|
|
<BridgeModal
|
|
currentRecord={currentRecord}
|
|
activeTxIndex={activeTxIndex}
|
|
setActiveTxIndex={setActiveTxIndex}
|
|
authorities={authorities}
|
|
ghstSymbol={ghstSymbol}
|
|
hashedArguments={hashedArguments}
|
|
chainExplorerUrl={chainExplorerUrl}
|
|
removeStoredRecord={removeStoredRecord}
|
|
/>
|
|
<Box minHeight="calc(100vh - 135px)" display="flex" flexDirection="column" justifyContent="center" sx={{ mt: "15px" }}>
|
|
<Grid container spacing={1}>
|
|
<Grid item xs={12} height="100%">
|
|
<Paper
|
|
sx={{ height: isBigScreen ? "100%" : "150px", paddingBottom: "0 !important", marginBottom: "0 !important" }}
|
|
fullWidth
|
|
enableBackground
|
|
>
|
|
<BridgeHeader
|
|
totalValidators={validators?.length}
|
|
disabledValidators={disabledValidators?.length}
|
|
bridgeStability={bridgeStability ? Number(bridgeStability) : undefined}
|
|
transactionEta={slowestEvmBlock ? Number(slowestEvmBlock) : undefined}
|
|
timeToNextEpoch={timeToNextEpoch ? Number(timeToNextEpoch) : undefined}
|
|
isSmallScreen={isSmallScreen}
|
|
/>
|
|
</Paper>
|
|
</Grid>
|
|
<Grid container spacing={2} style={{ marginTop: '10px' }}>
|
|
<Grid item xs={isBigScreen ? 12 : 5} style={{ paddingLeft: "23px" }}>
|
|
<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 && (
|
|
<Box alignItems="center" justifyContent="center" display="flex" height="100%">
|
|
<GhostStyledIcon
|
|
component={PendingActionsIcon}
|
|
viewBox="0 0 23 23"
|
|
style={{ display: "flex", alignItems: "center", cursor: "pointer" }}
|
|
onClick={() => setBridgeAction(!bridgeAction)}
|
|
/>
|
|
</Box>
|
|
)}
|
|
style={{ height: "434px" }}
|
|
fullWidth
|
|
enableBackground
|
|
>
|
|
{bridgeAction
|
|
? <BridgeCardAction
|
|
isVerySmallScreen={isVerySmallScreen}
|
|
isSemiSmallScreen={isSemiSmallScreen}
|
|
chainId={chainId}
|
|
address={address}
|
|
ghstSymbol={ghstSymbol}
|
|
gatekeeperAddressEmpty={gatekeeperAddressEmpty}
|
|
gatekeeperAddress={gatekeeperAddress}
|
|
evmNetwork={evmNetwork}
|
|
connect={connect}
|
|
isConfirmed={isConfirmed}
|
|
setIsConfirmed={setIsConfirmed}
|
|
openBridgeModal={() => setBridgeModalOpen(true)}
|
|
storeTransactionHash={storeTransactionHash}
|
|
/>
|
|
: <BridgeCardHistory
|
|
isSemiSmallScreen={isSemiSmallScreen}
|
|
isBigScreen={isBigScreen}
|
|
filteredStoredTransactions={filteredStoredTransactions}
|
|
ghstSymbol={ghstSymbol}
|
|
blockNumber={blockNumber}
|
|
finalityDelay={finalityDelay}
|
|
setActiveTxIndex={setActiveTxIndex}
|
|
/>
|
|
}
|
|
</Paper>
|
|
</Grid>
|
|
{!isSemiSmallScreen && (<Grid item xs={isBigScreen ? 12 : 7} style={{ paddingLeft: isBigScreen ? "23px": "" }}>
|
|
<Paper
|
|
headerContent={
|
|
<Box alignItems="center" justifyContent="start" display="flex" width="100%">
|
|
<Typography variant="h4">Real-Time Bridge Stats</Typography>
|
|
</Box>
|
|
}
|
|
topRight={
|
|
<Box alignItems="center" justifyContent="center" display="flex" height="100%" width="180px">
|
|
<Typography variant="body1">{latestUpdate
|
|
? `Last update: ${timeConverter(Math.floor(latestUpdate / 1000))}`
|
|
: <Skeleton width="180px" />
|
|
}</Typography>
|
|
</Box>
|
|
}
|
|
style={{ height: "434px" }}
|
|
enableBackground
|
|
fullWidth
|
|
>
|
|
<ValidatorTable
|
|
currentTime={currentTime}
|
|
currentBlock={blockNumber}
|
|
latestCommits={latestCommits}
|
|
bridgeStability={bridgeStability}
|
|
isVerySmallScreen={isVerySmallScreen}
|
|
providerDetail={providerDetail}
|
|
/>
|
|
</Paper>
|
|
</Grid>)}
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
</Container>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default Bridge;
|