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;