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 ( <> setBridgeModalOpen(false)} handleButtonProceed={handleButtonProceed} /> {!bridgeAction && ( setBridgeAction(!bridgeAction)} />)} { bridgeAction ? `Bridge $${ghstSymbol}` : "Transaction History" } } topRight={bridgeAction && ( setBridgeAction(!bridgeAction)} /> )} style={{ height: "434px" }} fullWidth enableBackground > {bridgeAction ? setBridgeModalOpen(true)} storeTransactionHash={storeTransactionHash} /> : } {!isSemiSmallScreen && ( Real-Time Bridge Stats } topRight={ {latestUpdate ? `Last update: ${timeConverter(Math.floor(latestUpdate / 1000))}` : } } style={{ height: "434px" }} enableBackground fullWidth > )} ) } export default Bridge;