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;