ghost-dao-interface/src/containers/Bridge/Bridge.jsx
Uncle Fatso d1d4313851
apply corrections from @neptune and @ghost_7
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2025-12-15 18:27:32 +03:00

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;