import { useEffect, useState, useMemo, useCallback } from "react"; import { useLocation } from "react-router-dom"; import { ss58Decode } from "@polkadot-labs/hdkd-helpers"; import { toHex } from "@polkadot-api/utils"; import ReactGA from "react-ga4"; import useSWR from 'swr'; import { Box, Container, Typography, Skeleton, useMediaQuery, useTheme, } from "@mui/material"; import SettingsIcon from '@mui/icons-material/Settings'; import Paper from "../../components/Paper/Paper"; import Metric from "../../components/Metric/Metric"; import PageTitle from "../../components/PageTitle/PageTitle"; import GhostStyledIcon from "../../components/Icon/GhostIcon"; import MetricCollection from "../../components/Metric/MetricCollection"; import LinearProgressBar from "../../components/Progress/LinearProgressBar"; import SwapCard from "../../components/Swap/SwapCard"; import SwapCollection from "../../components/Swap/SwapCollection"; import TokenStack from "../../components/TokenStack/TokenStack"; import DataRow from "../../components/DataRow/DataRow"; import { PrimaryButton } from "../../components/Button"; import { BridgeInSettingsModal } from "./BridgeSettingsModal"; import { ghost, useGatekeeperApy, useGatekeeperAddress, useGatekeeperMinimum, } from "../../hooks/staking"; import { useTokenSymbol, useBalance } from "../../hooks/tokens"; import { useUnstableProvider } from "../../hooks/ghost"; import { useLocalStorage } from "../../hooks/localstorage"; import { prettifySeconds } from "../../helpers/timeUtil"; import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; import { formatNumber, formatCurrency } from "../../helpers"; import { GATEKEEPER_ADDRESSES } from "../../constants/addresses"; import { networkAvgBlockSpeed } from "../../constants"; const DECIMALS_PREFIX = "bridge-in-decimals"; const BridgeStatusCard = ({ totalValidators, pendingValidators, tapestryDraft }) => { const theme = useTheme(); const progressPercent = totalValidators > 0 ? (pendingValidators / totalValidators) * 100 : 0; return ( {tapestryDraft ? "Round 2: Merkle Root Verification" : "Round 1: Block Consensus"} {tapestryDraft ? `Block: ${formatNumber(tapestryDraft.median)} (${pendingValidators} out of ${totalValidators} validators)` : `Blocks Pending... (${pendingValidators} out of ${totalValidators} validators)` } ); }; const BridgeIn = ({ chainId, address, config, connect }) => { const location = useLocation(); const { useStorageValue, setStorageValue, tryStoreInHistory, CONSTANTS } = useLocalStorage(); const isSmallScreen = useMediaQuery("(max-width: 650px)"); const isSemiSmallScreen = useMediaQuery("(max-width: 480px)"); const isVerySmallScreen = useMediaQuery("(max-width: 379px)"); const { casperApi, client, account } = useUnstableProvider(); const { gatekeepedApy } = useGatekeeperApy(chainId); const [nextEraIn, setNextEraIn] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); const [isPending, setIsPending] = useState(false); const [amount, setAmount] = useState(""); const [receiver, setReceiver] = useState(account); const [customReceiver, setCustomReceiver] = useState(account); const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST"); const { balance: ghstBalance, refetch: ghstBalanceRefetch } = useBalance(chainId, "GHST", address); const { gatekeeperAddress } = useGatekeeperAddress(chainId); const { existentialDeposit } = useGatekeeperMinimum(chainId); const { data: formatDecimals } = useStorageValue(chainId, address, DECIMALS_PREFIX, "5"); useEffect(() => { ReactGA.send({ hitType: "pageview", page: location.pathname }); }, [location]); const bridgeNumbers = useMemo(() => { const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length; const number = 1 + connectedNetworks * 3; return `(${number}, ${number})`; }, [chainId]); const { data: chainProperties } = useSWR( client ? ['chainProperties', client] : null, () => client.getChainSpecData(), { revalidateOnFocus: true } ); const { data: activeEraStart } = useSWR( casperApi ? ['activeEra', casperApi] : null, () => casperApi.query.Staking.ActiveEra.getValue(), { refreshInterval: 12000, revalidateOnFocus: true } ); const { data: weaverAuthorities } = useSWR( casperApi ? ['weaverAuthorities', casperApi] : null, () => casperApi.query.GhostWeaver.Authorities.getValue(), { revalidateOnFocus: true } ); const { data: networkData } = useSWR( casperApi ? ['networkData', casperApi, chainId] : null, async () => { const key = BigInt(chainId); const res = await casperApi.query.GhostNetworks.Networks.getValue(key); return res; }, { revalidateOnFocus: true } ); const { data: totalStaked } = useSWR( casperApi && activeEraStart && chainProperties ? ['erasTotalStake', casperApi, activeEraStart, chainProperties] : null, async () => { const decimals = chainProperties.properties.tokenDecimals; const total = await casperApi.query.Staking.ErasTotalStake.getValue(activeEraStart.index); return new DecimalBigNumber(total, decimals); }, { revalidateOnFocus: true } ); const { data: networkAttestations } = useSWR( casperApi ? ['networkAttestations', casperApi, chainId] : null, async () => { const key = BigInt(chainId); const res = await casperApi.query.GhostWeaver.NetworkAttestations.getValue(key); return res; }, { refreshInterval: 12000, revalidateOnFocus: true } ); const { data: tapestryDraft } = useSWR( casperApi ? ['tapestryDraft', casperApi, chainId] : null, async () => { const key = BigInt(chainId); const res = await casperApi.query.GhostWeaver.TapestryDrafts.getValue(key); return res; }, { refreshInterval: 12000, revalidateOnFocus: true } ); const { data: tapestryStrand } = useSWR( casperApi ? ['tapestryStrand', casperApi, chainId] : null, async () => { const key = BigInt(chainId); const res = await casperApi.query.GhostWeaver.TapestryStrands.getValue(key); return res; }, { refreshInterval: 12000, revalidateOnFocus: true } ); const tapestryParticipants = useMemo(() => { if (!tapestryStrand) return { element: "", onesCount: 0 }; let maxOnesCount = 0; let bestElement = null; tapestryStrand.forEach(item => { const dataArray = item[1]?.data || []; const totalOnes = dataArray.reduce((sum, num) => { const onesInNumber = num.toString(2).split('1').length - 1; return sum + onesInNumber; }, 0); if (totalOnes > maxOnesCount) { maxOnesCount = totalOnes; bestElement = item[0]; } }); return { element: bestElement, onesCount: maxOnesCount }; }, [tapestryStrand]); useEffect(() => { if (!activeEraStart || !activeEraStart.start) return; const calculateTime = () => { const nowMs = Date.now(); const diffMs = nowMs - Number(activeEraStart.start); return prettifySeconds(Math.max(0, diffMs / 1000)); }; setNextEraIn(calculateTime()); const intervalId = setInterval(() => { setNextEraIn(calculateTime()); }, 1000); return () => clearInterval(intervalId); }, [activeEraStart]); const blockDelayMetric = useMemo(() => { if (!networkData) return null; const blockSpeed = networkAvgBlockSpeed(chainId); const finalityDelay = networkData.finality_delay; const estimatedMins = (blockSpeed * finalityDelay) / 60n; return { finalityDelay, text: `~${estimatedMins} mins` } }, [chainId, networkData]); const nativeSymbol = useMemo(() => { const client = config?.getClient(); return client?.chain?.nativeCurrency?.symbol; }, [config]); const estimatedOutAmount = useMemo(() => { const hundredPercent = 1000000000; const incomingShare = networkData ? networkData.incoming_share : 0; const result = hundredPercent - incomingShare; return amount * result / hundredPercent }, [amount, networkData]); const gatekeepersDifferent = useMemo(() => { if (!networkData) return false; return gatekeeperAddress.toUpperCase() == toHex(networkData.gatekeeper).toUpperCase(); }, [gatekeeperAddress, networkData]); const preparedAmount = useMemo(() => { return new DecimalBigNumber(amount, ghstBalance._decimals); }, [amount, ghstBalance]); const receiverHex = useMemo(() => { const finalReceiver = customReceiver ? customReceiver : receiver; try { const [publicKey, prefix] = ss58Decode(finalReceiver); if (prefix !== 1995 && prefix !== 1996) return undefined; return toHex(publicKey); } catch { return undefined; } }, [receiver, customReceiver]); const actionDisabled = useMemo(() => { if (!networkData) return true; const differentGatekeepers = gatekeeperAddress.toUpperCase() !== toHex(networkData.gatekeeper).toUpperCase(); const smallAmount = existentialDeposit.gt(preparedAmount); const insufficientAmount = preparedAmount.gt(ghstBalance); return isPending || differentGatekeepers || smallAmount || insufficientAmount || !receiverHex }, [isPending, networkData, gatekeeperAddress, existentialDeposit, preparedAmount, ghstBalance, receiverHex]); const setFormatDecimals = useCallback((value) => { if (Number(value) <= 17) { setStorageValue(chainId, address, DECIMALS_PREFIX, value); } }, [chainId, address]); const startGhostBridge = async () => { setIsPending(true); try { const txHash = await ghost(chainId, address, receiverHex, preparedAmount._value); if (txHash) tryStoreInHistory(chainId, address, CONSTANTS.bridgeIn.key, txHash, CONSTANTS.bridgeIn.name); } finally { await ghstBalanceRefetch(); setAmount(""); setIsPending(false); } } return ( setSettingsOpen(false)} decimals={formatDecimals} setDecimals={setFormatDecimals} receiver={customReceiver} setReceiver={setCustomReceiver} /> Next Era in {nextEraIn ? {nextEraIn} : } } > v !== undefined).length : 0 } /> setSettingsOpen(true)} /> } headerContent={ Bridge-In } > } tokenName={ghstSymbol} info={formatCurrency(ghstBalance.toString(), formatDecimals, ghstSymbol)} endString="Max" value={amount} endStringOnClick={() => setAmount(ghstBalance.toString())} onChange={event => setAmount(event.currentTarget.value)} inputProps={{ "data-testid": "fromInput" }} />} LowerSwapCard={ } tokenName={"GHST"} value={estimatedOutAmount} inputProps={{ "data-testid": "toInput" }} /> } /> {formatCurrency(existentialDeposit.toString(), formatDecimals, ghstSymbol)}} /> {isPending ? "Pending..." : "Bridge-In"} ) } export default BridgeIn;