diff --git a/package.json b/package.json index abc8a32..f0fa95c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ghost-dao-interface", "private": true, - "version": "0.3.11", + "version": "0.4.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/Sidebar/NavContent.jsx b/src/components/Sidebar/NavContent.jsx index 79aa61a..8cc1c07 100644 --- a/src/components/Sidebar/NavContent.jsx +++ b/src/components/Sidebar/NavContent.jsx @@ -112,6 +112,10 @@ const NavContent = ({ chainId, addressChainId }) => { {isNetworkAvailable(chainId, addressChainId) && <> + {isNetworkLegacy(chainId) + ? + : + } { } /> - - {isNetworkLegacy(chainId) - ? - : - } - { } /> + + diff --git a/src/components/Swap/SwapCard.jsx b/src/components/Swap/SwapCard.jsx index 376379b..4f7b416 100644 --- a/src/components/Swap/SwapCard.jsx +++ b/src/components/Swap/SwapCard.jsx @@ -90,7 +90,7 @@ const SwapCard = ({ )} - + { +const SwapCollection = ({ UpperSwapCard, LowerSwapCard, arrowOnClick, iconNotNeeded, maxWidth}) => { const theme = useTheme(); return ( - + {UpperSwapCard} {!iconNotNeeded && ( { return icon; } -const Token = ({ name, viewBox = "0 0 260 260", fontSize = "large", ...props }) => { +const Token = ({ chainTokenName, name, viewBox = "0 0 260 260", fontSize = "large", ...props }) => { return ( - + + + {chainTokenName && ( + + )} + ); }; diff --git a/src/constants.ts b/src/constants.ts index 41b2656..a5b306b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -37,3 +37,21 @@ export const isNetworkLegacy = (chainId) => { } return exists; } + +export const networkAvgBlockSpeed = (chainId) => { + let blockSpeed = 12n; + switch (chainId) { + case 11155111: + blockSpeed = 12n + break; + case 560048: + blockSpeed = 12n + break; + case 63: + blockSpeed = 13n + break; + default: + break; + } + return blockSpeed +} diff --git a/src/containers/Bridge/Bridge.jsx b/src/containers/Bridge/Bridge.jsx index f9431a0..73e78f9 100644 --- a/src/containers/Bridge/Bridge.jsx +++ b/src/containers/Bridge/Bridge.jsx @@ -3,94 +3,85 @@ import ReactGA from "react-ga4"; import { Box, + Grid, Container, Typography, - Link, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - TableContainer, + Skeleton, useMediaQuery, - useTheme } from "@mui/material"; -import { ss58Decode, ss58Address } from "@polkadot-labs/hdkd-helpers"; -import { toHex } from "@polkadot-api/utils"; import { decodeAddress } from "@polkadot/util-crypto"; -import { useTransactionConfirmations } from "wagmi"; import { getBlockNumber } from "@wagmi/core"; +import { useTransaction } from "wagmi"; import { keccak256 } from "viem"; -import { u64, u128 } from "scale-ts"; +import { u32, u64, u128 } from "scale-ts"; -import ContentPasteIcon from '@mui/icons-material/ContentPaste'; -import PendingIcon from '@mui/icons-material/Pending'; import PendingActionsIcon from '@mui/icons-material/PendingActions'; import ArrowBack from '@mui/icons-material/ArrowBack'; -import ArrowRightIcon from '@mui/icons-material/ArrowRight'; -import HourglassBottomIcon from '@mui/icons-material/HourglassBottom'; -import ThumbUpIcon from '@mui/icons-material/ThumbUp'; -import ThumbDownAltIcon from '@mui/icons-material/ThumbDownAlt'; -import HandshakeIcon from '@mui/icons-material/Handshake'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -import CheckIcon from '@mui/icons-material/Check'; import PageTitle from "../../components/PageTitle/PageTitle"; import Paper from "../../components/Paper/Paper"; -import SwapCard from "../../components/Swap/SwapCard"; -import SwapCollection from "../../components/Swap/SwapCollection"; -import TokenStack from "../../components/TokenStack/TokenStack"; import GhostStyledIcon from "../../components/Icon/GhostIcon"; -import Modal from "../../components/Modal/Modal"; -import InfoTooltip from "../../components/Tooltip/InfoTooltip"; -import { PrimaryButton } from "../../components/Button"; -import { GATEKEEPER_ADDRESSES } from "../../constants/addresses"; -import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; -import { formatCurrency } from "../../helpers"; +import { networkAvgBlockSpeed } from "../../constants"; +import { timeConverter } from "../../helpers"; import { useTokenSymbol, useBalance } from "../../hooks/tokens"; -import { useGatekeeperAddress, ghost } from "../../hooks/staking"; +import { useGatekeeperAddress } from "../../hooks/staking"; + import { useEvmNetwork, - useClapsInSession, - useApplauseThreshold, - useReceivedClaps, - useApplausesForTransaction, + useApplauseDetails, useAuthorities, + useValidators, + useBlockCommitments, + useDisabledValidators, useCurrentIndex, useUnstableProvider, - useMetadata + 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 theme = useTheme(); + 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 [copiedIndex, setCopiedIndex] = useState(null); - const [isPending, setIsPending] = useState(false); - const [bridgeAction, setBridgeAction] = useState(true); + const [bridgeModalOpen, setBridgeModalOpen] = useState(false); + const [isConfirmed, setIsConfirmed] = useState(false); const [activeTxIndex, setActiveTxIndex] = useState(-1); - const [receiver, setReceiver] = useState(""); - const [convertedReceiver, setConvertedReceiver] = useState(undefined); - const [amount, setAmount] = useState(""); - const [rotation, setRotation] = useState(0); - const [blockNumber, setBlockNumber] = useState(0); + const [blockNumber, setBlockNumber] = useState(0n); + const [bridgeAction, setBridgeAction] = useState(true); + const [currentTime, setCurrentTime] = useState(Date.now()); - const sliceString = (string, first, second) => { - if (!string) return ""; - return string.slice(0, first) + "..." + string.slice(second); - } + 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(); @@ -99,210 +90,208 @@ const Bridge = ({ chainId, address, config, connect }) => { return undefined } return storedTransactions?.at(activeTxIndex) - }, [activeTxIndex, storedTransactions]) + }, [activeTxIndex, storedTransactions]); + + const { data: watchTransactionInfo } = useTransaction({ + hash: watchTransaction?.transactionHash + }); const hashedArguments = useMemo(() => { if (!watchTransaction) return undefined - const amountEncoded = u128.enc(BigInt(watchTransaction.amount)); 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]) + }, [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 clapsInSession = useClapsInSession({ + const validators = useValidators({ currentSession: watchTransaction?.sessionIndex ?? currentSession }); - const appluseThreshold = useApplauseThreshold(); - const receivedClaps = useReceivedClaps({ + const blockCommitments = useBlockCommitments({ evmChainId: chainId }); + const disabledValidators = useDisabledValidators(); + const transactionApplaused = useApplauseDetails({ currentSession: watchTransaction?.sessionIndex ?? currentSession, - txHash: watchTransaction?.transactionHash, - argsHash: hashedArguments - }); - const transactionApplaused = useApplausesForTransaction({ - currentSession: watchTransaction?.sessionIndex ?? currentSession, - txHash: watchTransaction?.transactionHash, argsHash: hashedArguments }); const finalityDelay = Number(evmNetwork?.finality_delay ?? 0n); - const incomingFee = Number(evmNetwork?.incoming_fee ?? 0n) / 10000000; getBlockNumber(config).then(block => setBlockNumber(block)); - const { gatekeeperAddress } = useGatekeeperAddress(chainId); const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST"); - const { - balance: ghstBalance, - refetch: ghstBalanceRefetch - } = useBalance(chainId, "GHST", address); useEffect(() => { ReactGA.send({ hitType: "pageview", page: "/bridge" }); }, []); - useEffect(() => { - const interval = setInterval(() => { - setRotation((prevRotation) => prevRotation > 0 ? 0 : 180) - }, 2000); - return () => clearInterval(interval); - }, [setRotation]) - - useEffect(() => { - try { - const [publicKey, prefix] = ss58Decode(receiver); - if (prefix !== 1995 && prefix !== 1996) { - throw new Error("bad prefix"); - } - setConvertedReceiver(toHex(publicKey)); - } catch { - setConvertedReceiver(undefined); - } - }, [receiver]) - - const clapsInSessionLength = useMemo(() => { - const disabledIndexes = new Set(clapsInSession?.filter(item => item.at(1).disabled).map(item => item.at(0))); - return authorities?.filter((_, idx) => !disabledIndexes.has(idx)).length ?? 0; - }, [authorities, clapsInSession]); - const chainExplorerUrl = useMemo(() => { const client = config?.getClient(); return client?.chain?.blockExplorers?.default?.url; }, [config]); - const chainName = useMemo(() => { - const client = config?.getClient(); - return client?.chain?.name; - }, [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 receivedClapsLength = receivedClaps?.length ?? 0; - const clapsNeeded = Math.floor(clapsInSessionLength * appluseThreshold / 100); + const applaused = transactionApplaused?.finalized ?? false; + const clappedAmount = transactionApplaused?.clapped_amount ?? 0n; + const clappedPercentage = clappedAmount * 100n / (totalStakedAmount ?? 1n); const step = finalization > 0 ? 0 - : receivedClapsLength < clapsNeeded && !transactionApplaused - ? 1 - : !transactionApplaused - ? 2 - : 3; + : applaused ? 2 : 1; return { ...watchTransaction, finalization, + applaused, + numberOfClaps, + clappedAmount, + clappedPercentage, step, } }, [ transactionApplaused, - receivedClaps, - appluseThreshold, - clapsInSessionLength, finalityDelay, watchTransaction, - blockNumber + blockNumber, + totalStakedAmount ]); - const gatekeeperAddressEmpty = useMemo(() => { - if (gatekeeperAddress === "0x0000000000000000000000000000000000000000") { - return true; - } - return false; - }, [gatekeeperAddress]); - - const preparedAmount = useMemo(() => { - try { - const result = BigInt(parseFloat(amount) * Math.pow(10, 18)); - if (result > ghstBalance._value) { - return ghstBalance._value; - } - return result; - } catch { - return 0n; - } - }, [amount]) - const filteredStoredTransactions = useMemo(() => { return storedTransactions.filter(obj => obj.chainId === chainId); }, [storedTransactions, chainId]); - const selfApplauseUrl = useMemo(() => { - if (!currentRecord) return ''; + 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 amount = new DecimalBigNumber(BigInt(currentRecord.amount), 18).toString(); - let url = "https://lite.ghostchain.io/#/applause?"; - url += `networkId=${currentRecord.chainId}&`; - url += `sessionIndex=${currentRecord.sessionIndex}&`; - url += `amount=${amount}&`; - url += `receiver=${currentRecord.receiverAddress}&`; - url += `transactionHash=${currentRecord.transactionHash}`; + const latestUpdate = useMemo(() => { + const validCommits = latestCommits?.filter(commit => + !commit.disabled && commit.lastActive != null + ) || []; - return url; - }, [currentRecord]); + 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) { + continue; + } + certainty += (commit?.lastStoredBlock ?? 0n) - (blockNumber - blocksInFourHours); + } + return Number(certainty * 100n / (blocksInFourHours * BigInt(length))); + }, [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(STORAGE_PREFIX, JSON.stringify(newStoredTransactions)); + localStorage.setItem(storagePrefix, JSON.stringify(newStoredTransactions)); setActiveTxIndex(-1); }, [storedTransactions, activeTxIndex, setStoredTransactions, setActiveTxIndex]); - const copyToClipboard = (text, index) => { - navigator.clipboard.writeText(text).then(() => { - setCopiedIndex(index); - setTimeout(() => setCopiedIndex(null) , 800); - }); - }; + const handleButtonProceed = () => { + setBridgeModalOpen(false); + setIsConfirmed(true); + } - const ghostOrConnect = async () => { - if (address === "") { - connect(); - } else { - setIsPending(true); - - try { - const txHash = await ghost(chainId, address, convertedReceiver, preparedAmount); - - const transaction = { - sessionIndex: currentSession ?? 0, - transactionHash: txHash, - receiverAddress: receiver, - amount: preparedAmount.toString(), - chainId: chainId, - blockNumber: Number(blockNumber), - timestamp: Date.now() - } - - const newStoredTransactions = [...storedTransactions, transaction]; - setStoredTransactions(newStoredTransactions); - localStorage.setItem(STORAGE_PREFIX, JSON.stringify(newStoredTransactions)); - - if (providerDetail) { - setActiveTxIndex(newStoredTransactions.length - 1) - } - } finally { - await ghstBalanceRefetch(); - setReceiver(""); - setAmount(""); - setIsPending(false); - } + 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 = [...storedTransactions, transaction]; + setStoredTransactions(newStoredTransactions); + localStorage.setItem(STORAGE_PREFIX, JSON.stringify(newStoredTransactions)); + setActiveTxIndex(newStoredTransactions.length - 1) } return ( - + <> { display: "flex", justifyContent: "center", alignItems: "center", - height: "calc(100vh - 153px)" }} > - - - TX Hash  - - {currentRecord ? sliceString(currentRecord.transactionHash, 10, -8) : ""} - - - - } - open={activeTxIndex >= 0} - onClose={() => setActiveTxIndex(-1)} - minHeight={"100px"} - > - - - 0 && theme.colors.primary[300] - }} - width="120px" - display="flex" - flexDirection="column" - justifyContent="start" - alignItems="center" - > - 0 - ? `rotate(${rotation}deg)` - : "rotate(0deg)" - }} - viewBox="0 0 25 25" - component={HourglassBottomIcon} - /> - Finalization - - {(currentRecord?.finalization ?? 0).toString()} blocks left - - - - - - 1 && theme.colors.primary[300] - }} - width="120px" - display="flex" - flexDirection="column" - justifyContent="start" - alignItems="center" - > - - {currentRecord?.step <= 1 - ? ( - <> - - - - ) - : ( - - ) - } - - - Slow Claps - {receivedClaps?.length ?? 0} / {clapsInSessionLength} - - - - - - = 2 && "scale(1.2)", - color: currentRecord?.step >= 2 && theme.colors.primary[300] - }} - width="120px" - display="flex" - flexDirection="column" - justifyContent="start" - alignItems="center" - > - - Applaused - { - currentRecord?.step === 3 ? "Check Receiver" : "Waiting Room" - } - - - - {(currentRecord?.step ?? 3) < 3 && (currentSession && currentRecord && currentSession > (currentRecord.sessionIndex ?? 0) + 2) && - - window.open( - selfApplauseUrl, - '_blank', - 'noopener,noreferrer' - )} - > - Self Applause - - - - Your transaction seems to be stuck, possibly because of a problem with some inactive validators on the network. - - - } - - - - - Session Index: - - Transaction Watchmen - - {authorities?.map((authority, idx) => { - const authorityAddress = ss58Address(authority.asHex(), 1996); - const disabled = clapsInSession?.find((info => info.at(0) === idx))?.at(1)?.disabled; - const clapped = receivedClaps?.some(authId => authId === idx); - - return ( - - - {authorityAddress} - - - ) - })} - - } /> - - {currentRecord?.sessionIndex} - - - Receiver Address: - copyToClipboard(currentRecord ? currentRecord.receiverAddress : "", 0)} - > - - {currentRecord ? sliceString(currentRecord.receiverAddress, 14, -5) : ""} - - - - - - Sent Amount: - {formatCurrency( - new DecimalBigNumber( - BigInt(currentRecord ? currentRecord.amount : "0"), - 18 - ).toString(), 9, ghstSymbol) - } - - - Executed at: - { - new Date(currentRecord ? currentRecord.timestamp : 0).toLocaleString('en-US') - } - -
- - Transaction Hash: - copyToClipboard(currentRecord ? currentRecord.transactionHash : "", 1)} - > - - {currentRecord ? sliceString(currentRecord.transactionHash, 10, -8) : "0x"} - - - - - - - Arguments Hash: - - - copyToClipboard(hashedArguments ? hashedArguments : "", 2)} - > - - {hashedArguments ? sliceString(hashedArguments, 10, -8) : "0x"} - - - - -
- - - - setBridgeModalOpen(false)} + handleButtonProceed={handleButtonProceed} + /> + + + + + removeStoredRecord()} + enableBackground > - Erase Record - - - - This will remove the transaction record from the session storage, but it will not cancel the bridge transaction. - - -
- - - - {!bridgeAction && ( setBridgeAction(!bridgeAction)} - />)} - { - bridgeAction ? `Bridge $${ghstSymbol}` : "Transaction History" - } - - } - topRight={bridgeAction && ( setBridgeAction(!bridgeAction)} - />)} - enableBackground - fullWidth - > - - {bridgeAction && ( - <> - setReceiver(event.currentTarget.value)} - inputProps={{ "data-testid": "fromInput" }} - placeholder="Ghost address (sf prefixed)" - type="text" - maxWidth="446px" - />} - LowerSwapCard={ setAmount(event.currentTarget.value)} - inputProps={{ "data-testid": "fromInput" }} - endString={"Max"} - endStringOnClick={() => setAmount(ghstBalance.toString())} - maxWidth="446px" - />} + + + + + + + {!bridgeAction && ( setBridgeAction(!bridgeAction)} + />)} + { + bridgeAction ? `Bridge $${ghstSymbol}` : "Transaction History" + } + + } + topRight={bridgeAction && ( + + setBridgeAction(!bridgeAction)} + /> + + )} + fullWidth + enableBackground + > + {bridgeAction + ? setBridgeModalOpen(true)} + storeTransactionHash={storeTransactionHash} + /> + : + } + + + {!isSemiSmallScreen && ( + + Real-Time Bridge Stats +
+ } + topRight={ + + {latestUpdate + ? `Last update: ${timeConverter(Math.floor(latestUpdate / 1000))}` + : + } + + } + enableBackground + fullWidth + > + - - - {gatekeeperAddressEmpty && ( - - - - There is no connected gatekeeper on {chainName} network. Propose gatekeeper on this network to make authorities listen to it. - - - - )} - {!gatekeeperAddressEmpty && ( - <> - {!isVerySmallScreen && Gatekeeper:} - - {sliceString(gatekeeperAddress, 10, -8)} - - - )} - - - {!providerDetail - ? ( - - - GHOST Wallet is not detected on your browser. Download  - - GHOST Wallet -   to see full detalization for bridge transaction. - - - ) - : metadata - ? ( - - - {!isVerySmallScreen && Estimated Fee:} - {incomingFee.toFixed(4)}% - - - {!isVerySmallScreen && - Finality Delay: - - } - {finalityDelay} blocks - -
- - {!isVerySmallScreen && Current GHOST Epoch:} - {currentSession ?? 0} - - - {!isVerySmallScreen && - Current Validators: - - Validators - - {authorities?.map((authority, idx) => { - const authorityAddress = ss58Address(authority.asHex(), 1996); - const clapInfo = clapsInSession?.find((info => info.at(0) === idx))?.at(1); - - return ( - -
- {authorityAddress} -
-
{clapInfo?.claps ?? 0}
-
- ) - })} -
- } /> -
} - {clapsInSessionLength} / {authorities?.length ?? 0} -
-
- ) - : ( - - Downloading chain metadata, wait please... - - ) - } -
-
- ghostOrConnect()} - > - {address === "" ? "Connect" : "Bridge" } - - - )} - {!bridgeAction && ( - - {!isSemiSmallScreen && ( - Amount - Datetime - Status - )} - - {filteredStoredTransactions - .map((obj, idx) => ( - setActiveTxIndex(idx)} - > - - - {formatCurrency( - new DecimalBigNumber(BigInt(obj.amount), 18).toString(), - isSemiSmallScreen ? 3 : 8, - ghstSymbol - )} - - - {sliceString( - obj.receiverAddress, - isSemiSmallScreen ? 5 : 10, - isSemiSmallScreen ? -3 : -8 - )} - - - - - - {new Date(obj.timestamp).toLocaleDateString('en-US')} - - - {new Date(obj.timestamp).toLocaleTimeString('en-US')} - - - - - - {Number(blockNumber) - (obj.blockNumber + finalityDelay) < 0 ? - - : - - } - - - - ))} - - - )} -
- + + )} + +
-
+ ) } diff --git a/src/containers/Bridge/BridgeCard.jsx b/src/containers/Bridge/BridgeCard.jsx new file mode 100644 index 0000000..ccf33a6 --- /dev/null +++ b/src/containers/Bridge/BridgeCard.jsx @@ -0,0 +1,366 @@ +import { useMemo, useState, useEffect } from "react"; +import { + Box, + Typography, + Link, + Skeleton, + TableContainer, + Table, + Paper, + TableHead, + TableBody, + TableRow, + TableCell, + Modal, + useTheme, +} from "@mui/material"; + +import { useConfig } from "wagmi"; +import { ss58Decode } from "@polkadot-labs/hdkd-helpers"; +import { toHex } from "@polkadot-api/utils"; + +import { PrimaryButton } from "../../components/Button"; +import GhostStyledIcon from "../../components/Icon/GhostIcon"; +import SwapCard from "../../components/Swap/SwapCard"; +import SwapCollection from "../../components/Swap/SwapCollection"; +import InfoTooltip from "../../components/Tooltip/InfoTooltip"; + +import { ghost } from "../../hooks/staking"; +import { useBalance } from "../../hooks/tokens"; + +import CheckIcon from '@mui/icons-material/Check'; +import HourglassBottomIcon from '@mui/icons-material/HourglassBottom'; + +import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; +import { formatNumber, formatCurrency, timeConverter } from "../../helpers"; + +import { BridgeRoute } from "./BridgeRoute"; + +const sliceString = (string, first, second) => { + if (!string) return ""; + return string.slice(0, first) + "..." + string.slice(second); +} + +export const BridgeCardAction = ({ + isVerySmallScreen, + isSemiSmallScreen, + chainId, + address, + ghstSymbol, + gatekeeperAddressEmpty, + gatekeeperAddress, + evmNetwork, + connect, + isConfirmed, + setIsConfirmed, + openBridgeModal, + storeTransactionHash, +}) => { + const [isPending, setIsPending] = useState(false); + const [receiver, setReceiver] = useState(""); + const [convertedReceiver, setConvertedReceiver] = useState(undefined); + const [amount, setAmount] = useState(""); + + const config = useConfig(); + const incomingFee = Number(evmNetwork?.incoming_fee ?? 0n) / 10000000; + + const { + balance: ghstBalance, + refetch: ghstBalanceRefetch + } = useBalance(chainId, "GHST", address); + + const chainName = useMemo(() => { + const client = config?.getClient(); + return client?.chain?.name; + }, [config]); + + const chainNativeCurrency = useMemo(() => { + const client = config?.getClient(); + return client?.chain?.nativeCurrency?.symbol; + }, [config]); + + const chainExplorerUrl = useMemo(() => { + const client = config?.getClient(); + return client?.chain?.blockExplorers?.default?.url; + }, [config]); + + const preparedAmount = useMemo(() => { + try { + const result = BigInt(parseFloat(amount) * Math.pow(10, 18)); + if (result > ghstBalance._value) { + return ghstBalance._value; + } + return result; + } catch { + return 0n; + } + }, [amount]); + + const amountAfterFee = useMemo(() => { + const convertedAmount = parseFloat(amount); + if (!convertedAmount) { + return 0; + } + return convertedAmount * (1 - incomingFee / 100); + }, [amount, incomingFee]); + + const isDisabled = useMemo(() => { + let isDisabled = isPending || gatekeeperAddressEmpty; + if (address !== "") { + isDisabled = isDisabled + || !convertedReceiver + || preparedAmount === 0n + || ghstBalance._value < preparedAmount; + } + return isDisabled; + }, [isPending, gatekeeperAddressEmpty, address, convertedReceiver, preparedAmount, ghstBalance]); + + const ghostFunds = async () => { + setIsPending(true); + try { + const txHash = await ghost(chainId, address, convertedReceiver, preparedAmount); + if (txHash) { + storeTransactionHash(txHash, receiver, preparedAmount.toString()); + } + } finally { + await ghstBalanceRefetch(); + setReceiver(""); + setAmount(""); + setIsPending(false); + } + } + + useEffect(() => { + if (isConfirmed) { + setIsConfirmed(false); + ghostFunds(); + } + }, [isConfirmed]); + + const ghostOrConnect = async () => { + if (address === "") { + connect(); + } else if (!isConfirmed) { + openBridgeModal(); + } + } + + useEffect(() => { + try { + const [publicKey, prefix] = ss58Decode(receiver); + if (prefix !== 1995 && prefix !== 1996) { + throw new Error("bad prefix"); + } + setConvertedReceiver(toHex(publicKey)); + } catch { + setConvertedReceiver(undefined); + } + }, [receiver]) + + return ( + + setReceiver(event.currentTarget.value)} + inputProps={{ "data-testid": "fromInput" }} + placeholder="GHOST address (sf prefixed)" + type="text" + maxWidth="100%" + />} + LowerSwapCard={ setAmount(event.currentTarget.value)} + inputProps={{ "data-testid": "fromInput" }} + endString={"Max"} + endStringOnClick={() => setAmount(ghstBalance.toString())} + maxWidth="100%" + />} + /> + + + {gatekeeperAddressEmpty && ( + + + + There is no connected gatekeeper on {chainName} network. Propose gatekeeper on this network to make authorities listen to it. + + + + )} + {!gatekeeperAddressEmpty && ( + <> + {!isVerySmallScreen && Gatekeeper:} + + {sliceString(gatekeeperAddress, 10, -8)} + + + )} + + + + + {!isVerySmallScreen && Bridge Fee:} + {incomingFee + ? {`${incomingFee.toFixed(4)}%`} + : + } + + + {!isVerySmallScreen && You will get:} + {incomingFee + ? {amountAfterFee.toFixed(4)} {ghstSymbol} + : + } + + + + + + ghostOrConnect()} + > + {address === "" ? "Connect" : "Bridge" } + + + ) +} + +export const BridgeCardHistory = ({ + isSemiSmallScreen, + filteredStoredTransactions, + ghstSymbol, + blockNumber, + finalityDelay, + setActiveTxIndex +}) => { + const theme = useTheme(); + const background = (index) => { + return index % 2 === 1 ? "" : theme.colors.gray[750]; + } + + return ( + + + + + + + Transaction + + + Datetime + + + Status + + + + + {filteredStoredTransactions?.map((obj, idx) => ( + setActiveTxIndex(idx)} + > + + + + {formatCurrency( + new DecimalBigNumber(BigInt(obj.amount), 18).toString(), + isSemiSmallScreen ? 3 : 8, + ghstSymbol + )} + + + {sliceString( + obj.receiverAddress, + isSemiSmallScreen ? 5 : 10, + isSemiSmallScreen ? -3 : -8 + )} + + + + + + + + {new Date(obj.timestamp).toLocaleDateString('en-US')} + + + {new Date(obj.timestamp).toLocaleTimeString('en-US')} + + + + + + + + {Number(blockNumber) - (obj.blockNumber + finalityDelay) < 0 ? + + : + + } + + + + + ))} + +
+
+
+ ) +} diff --git a/src/containers/Bridge/BridgeHeader.jsx b/src/containers/Bridge/BridgeHeader.jsx new file mode 100644 index 0000000..f20306b --- /dev/null +++ b/src/containers/Bridge/BridgeHeader.jsx @@ -0,0 +1,133 @@ +import { useMemo } from "react"; +import { Box, Paper, Grid, Typography, LinearProgress, useTheme } from "@mui/material" + +import Metric from "../../components/Metric/Metric"; +import Countdown from "../../components/Countdown/Countdown"; +import { formatNumber } from "../../helpers"; + +export const BridgeHeader = ({ + totalValidators, + disabledValidators, + bridgeStability, + transactionEta, + timeToNextEpoch, + isSmallScreen +}) => { + const theme = useTheme(); + const disabledPercentage = useMemo(() => { + if (totalValidators === undefined || disabledValidators === undefined) { + return 0; + } + return ((totalValidators - disabledValidators) / totalValidators) * 100; + }, [totalValidators, disabledValidators]); + + const validatorsColor = useMemo(() => { + if (disabledPercentage < 50) { + return theme.colors.validatorsColor.red; + } + return theme.colors.validatorsColor.green; + }, [disabledPercentage, theme]); + + const stabilityColor = useMemo(() => { + if (bridgeStability > 80) { + return theme.colors.bridgeProgress.success; + } else if (bridgeStability > 50) { + return theme.colors.bridgeProgress.warning; + } else { + return theme.colors.bridgeProgress.error; + } + }, [bridgeStability, theme]); + + const progressBarPostfix = useMemo(() => { + if (bridgeStability > 90) { + return "✅ Safe"; + } else if (bridgeStability > 80) { + return "✅ Moderate Risk"; + } else if (bridgeStability > 70) { + return "⚠️ High Risk"; + } else if (bridgeStability > 50) { + return "⚠️ Critical Risk"; + } else { + return "❌ Do NOT Bridge"; + } + }, [bridgeStability]); + + const formatTime = (totalSeconds) => { + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const secs = Math.floor(totalSeconds % 60); + + if (hours > 0) { + return `${hours} hours ${minutes} mins`; + } else if (minutes > 0) { + return `${minutes} mins`; + } else { + return `${secs} secs`; + } + } + + return ( + + + + {totalValidators} ({formatNumber(disabledPercentage, 0)}% active) + + } + label="Total Validators" + tooltip="Active and disabled GHOST Validators in the current GHOST Epoch." + /> + + + + + + + + + + + + + Bridge Stability {bridgeStability + ? `${formatNumber(bridgeStability, 0)}% ${progressBarPostfix}` + : "Unknown" + } + + + + + + ) +} diff --git a/src/containers/Bridge/BridgeModal.jsx b/src/containers/Bridge/BridgeModal.jsx new file mode 100644 index 0000000..cf9cfbd --- /dev/null +++ b/src/containers/Bridge/BridgeModal.jsx @@ -0,0 +1,410 @@ +import { useState, useEffect } from "react"; + +import { Box, Typography, Link, FormControlLabel, Checkbox, useTheme } from "@mui/material"; +import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material"; + +import ThumbUpIcon from '@mui/icons-material/ThumbUp'; +import ThumbDownAltIcon from '@mui/icons-material/ThumbDownAlt'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import CheckIcon from '@mui/icons-material/Check'; +import AssuredWorkloadIcon from '@mui/icons-material/AssuredWorkload'; + +import HourglassBottomIcon from '@mui/icons-material/HourglassBottom'; +import ArrowRightIcon from '@mui/icons-material/ArrowRight'; +import HandshakeIcon from '@mui/icons-material/Handshake'; +import PendingIcon from '@mui/icons-material/Pending'; +import ContentPasteIcon from '@mui/icons-material/ContentPaste'; + +import InfoTooltip from "../../components/Tooltip/InfoTooltip"; +import Modal from "../../components/Modal/Modal"; +import GhostStyledIcon from "../../components/Icon/GhostIcon"; +import { PrimaryButton } from "../../components/Button"; + +import { formatCurrency } from "../../helpers"; +import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; + +export const BridgeModal = ({ + currentRecord, + activeTxIndex, + setActiveTxIndex, + authorities, + ghstSymbol, + hashedArguments, + chainExplorerUrl, + removeStoredRecord, +}) => { + const theme = useTheme(); + const [copiedIndex, setCopiedIndex] = useState(null); + + const sliceString = (string, first, second) => { + if (!string) return ""; + return string.slice(0, first) + "..." + string.slice(second); + } + + const copyToClipboard = (text, index) => { + navigator.clipboard.writeText(text).then(() => { + setCopiedIndex(index); + setTimeout(() => setCopiedIndex(null) , 800); + }); + }; + + return ( + + + TX Hash  + + {currentRecord ? sliceString(currentRecord.transactionHash, 10, -8) : ""} + + +
+ } + open={activeTxIndex >= 0} + onClose={() => setActiveTxIndex(-1)} + minHeight={"100px"} + > + + + 0 && theme.colors.primary[300] + }} + width="120px" + display="flex" + flexDirection="column" + justifyContent="start" + alignItems="center" + > + + Finalization + + {(currentRecord?.finalization ?? 0).toString()} blocks left + + + + + + 1 && theme.colors.primary[300] + }} + width="120px" + display="flex" + flexDirection="column" + justifyContent="start" + alignItems="center" + > + + {currentRecord?.step <= 1 + ? ( + <> + + + + ) + : ( + + ) + } + + + Slow Claps + {currentRecord?.numberOfClaps ?? 0} / {authorities?.length ?? 0} + + + + + + + {currentRecord?.applaused + ? <> + + Applaused + Check Receiver + + : <> + + Capital Backed + + {(currentRecord?.clappedAmount ?? 0n) / 10n**18n} {ghstSymbol} ({currentRecord?.clappedPercentage ?? 0}%) + + + } + + + + + + GHOST Epoch: + {currentRecord?.sessionIndex} + + + Accepted Bridge Risk: + {currentRecord?.bridgeStability}% + + + + Arguments Hash: + + + copyToClipboard(hashedArguments ? hashedArguments : "", 2)} + > + + {hashedArguments ? sliceString(hashedArguments, 10, -8) : "0x"} + + + + +
+ + Receiver Address: + copyToClipboard(currentRecord ? currentRecord.receiverAddress : "", 0)} + > + + {currentRecord ? sliceString(currentRecord.receiverAddress, 14, -5) : ""} + + + + + + Bridged Amount: + {formatCurrency( + new DecimalBigNumber( + BigInt(currentRecord ? currentRecord.amount : "0"), + 18 + ).toString(), 9, ghstSymbol) + } + + + Executed at: + { + new Date(currentRecord ? currentRecord.timestamp : 0).toLocaleString('en-US') + } + +
+ + + removeStoredRecord()} + > + Erase Record + + + + This will permanently remove the bridge transaction record from the session storage, but it will not cancel the bridge transaction. + + +
+ + ) +} + +export const BridgeConfirmModal = ({ + bridgeStability, + isOpen, + setClose, + handleButtonProceed +}) => { + const [isBridgingRiskChecked, setIsBridgingRiskChecked] = useState(false); + const [isBridgingRecipientChecked, setIsBridgingRecipientChecked] = useState(false); + + const handleProceed = () => { + setIsBridgingRiskChecked(false); + setIsBridgingRecipientChecked(false); + handleButtonProceed(); + } + + return ( + + + + setIsBridgingRiskChecked(event.target.checked)} + icon={} + checkedIcon={} + /> + } + label={ + + {`I acknowledge bridging risk at ${bridgeStability}%.`}  + + Learn more. + + + } + /> +
+ setIsBridgingRecipientChecked(event.target.checked)} + icon={} + checkedIcon={} + /> + } + label="I confirm that recipient address is a self-custodial wallet, not an exchange, third party service, or smart-contract." + sx={{ '& .MuiFormControlLabel-label': { textAlign: "justify" } }} + /> +
+ + + Proceed Bridge + +
+
+ ) +} diff --git a/src/containers/Bridge/BridgeRoute.jsx b/src/containers/Bridge/BridgeRoute.jsx new file mode 100644 index 0000000..8cc7fd8 --- /dev/null +++ b/src/containers/Bridge/BridgeRoute.jsx @@ -0,0 +1,45 @@ +import React from "react"; +import { Box, Typography, useTheme } from "@mui/material"; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; + +import GhostStyledIcon from "../../components/Icon/GhostIcon"; +import Token from "../../components/Token/Token"; + +export const BridgeRoute = ({ coinName, chainTokenName, tokens }) => { + const theme = useTheme(); + return ( + + Route: + + {tokens?.map((token, index) => { + return ( + + + + + ) + })} + + + + + ) +} + +const RouteHop = ({ theme, token, chainTokenName, arrowNeeded }) => { + return ( + + + + {token} + + + ) +} diff --git a/src/containers/Bridge/ValidatorTable.jsx b/src/containers/Bridge/ValidatorTable.jsx new file mode 100644 index 0000000..7653e22 --- /dev/null +++ b/src/containers/Bridge/ValidatorTable.jsx @@ -0,0 +1,242 @@ +import { useEffect, useState, useMemo } from "react"; + +import { + Box, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + LinearProgress, +} from "@mui/material"; +import { useTheme } from "@mui/material/styles"; + +import WarningIcon from '@mui/icons-material/Warning'; +import CancelIcon from '@mui/icons-material/Cancel'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; + +import GhostStyledIcon from "../../components/Icon/GhostIcon"; +import InfoTooltip from "../../components/Tooltip/InfoTooltip"; +import { PrimaryButton } from "../../components/Button"; + +export const ValidatorTable = ({ + currentTime, + currentBlock, + latestCommits, + isVerySmallScreen, + bridgeStability, + providerDetail, +}) => { + const theme = useTheme(); + + const stabilityColor = useMemo(() => { + const red = Math.round(255 * (1 - bridgeStability / 100)); + const green = Math.round(255 * (bridgeStability / 100)); + return `rgb(${red}, ${green}, 0)`; + }, [bridgeStability]); + + return ( + + {!providerDetail && + + GHOST Wallet is not detected on your browser! + Download GHOST Wallet Extension to see real-time validator stats for bridging transaction. + window.open('https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases', '_blank', 'noopener,noreferrer')}> + Get GHOST Extension + + + } + {providerDetail && + + + + + + + + + + + + + {latestCommits?.map((commit, index) => { + return ( + + ) + })} + +
+
} +
+ ) +} + +const BridgeHeaderTableCell = ({ + align="center", + borderTopRightRadius="0px", + borderTopLeftRadius="0px", + borderBottomRightRadius="0px", + borderBottomLeftRadius="0px", + background="transparent", + fontSize="12px", + padding="0px", + tooltip, + value +}) => { + return ( + + + {value} + {tooltip && } + + + ) +} + +const BridgeTableCell = ({ + align="center", + borderTopRightRadius="0px", + borderTopLeftRadius="0px", + borderBottomRightRadius="0px", + borderBottomLeftRadius="0px", + background="transparent", + fontSize="10px", + padding="0px", + value +}) => { + return ( + + {value} + + ) +} + +const ValidatorRow = ({ + colors, + index, + currentTime, + currentBlock, + commit, +}) => { + const background = index % 2 === 1 ? "" : colors.gray[750]; + return ( + + + + + + + + ) +} + +const sliceString = (string, first, second) => { + if (!string) return ""; + return string.slice(0, first) + "..." + string.slice(second); +} + +const getTimeAgo = (timestampDiff) => { + const seconds = Math.floor(timestampDiff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (seconds < 60) return `${seconds}s ago`; + if (minutes < 60) return `${minutes}m ago`; + if (hours < 24) return `${hours}h ago`; + + return "long ago"; +} + +const blockDelayIcon = (colors, timestampDiff) => { + let color = colors.feedback.error; + let icon = CancelIcon; + + if (timestampDiff < 900000n) { + color = colors.feedback.success; + icon = CheckCircleIcon; + } else if (timestampDiff < 2700000n) { + color = colors.feedback.warning; + icon = WarningIcon; + } + + return ( + + ) +} + +const statusIcon = (colors, disabled) => { + let color = colors.feedback.success; + let icon = CheckCircleIcon; + + if (disabled === true) { + color = colors.feedback.error; + icon = CancelIcon; + } + + return ( + + ) +} diff --git a/src/helpers/index.js b/src/helpers/index.js index 7678ce8..a6d4410 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -41,3 +41,10 @@ export const formatNumber = (number, precision = 0) => { export const sortBondsByDiscount = (bonds) => { return Array.from(bonds).filter((bond) => !bond.isSoldOut).sort((a, b) => (a.discount.gt(b.discount) ? -1 : 1)); }; + +export const timeConverter = (time) => { + const seconds = Number(time); + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}m ${secs < 10 ? '0' : ''}${secs}s`; +} diff --git a/src/hooks/ghost/UnstableProvider.jsx b/src/hooks/ghost/UnstableProvider.jsx index 3ceb85c..d14b7e2 100644 --- a/src/hooks/ghost/UnstableProvider.jsx +++ b/src/hooks/ghost/UnstableProvider.jsx @@ -4,7 +4,7 @@ import { createClient } from "@polkadot-api/substrate-client" import { getObservableClient } from "@polkadot-api/observable-client" import useSWR from "swr" -const DEFAULT_CHAIN_ID = "0xa217f4ee58a944470e9633ca5bd6d28a428ed64cd9b6f3e413565f359f89af90" +const DEFAULT_CHAIN_ID = "0x5e1190682f1a6409cdfd691c0b23a6db792864d8994e591e9c19a31d8163989f" const UnstableProvider = createContext(null) export const useUnstableProvider = () => useContext(UnstableProvider) diff --git a/src/hooks/ghost/index.js b/src/hooks/ghost/index.js index da4f253..d76ab9c 100644 --- a/src/hooks/ghost/index.js +++ b/src/hooks/ghost/index.js @@ -6,4 +6,9 @@ export * from "./useClapsInSession"; export * from "./useApplauseThreshold"; export * from "./useReceivedClaps"; export * from "./useAuthorities"; -export * from "./useApplausesForTransaction"; +export * from "./useValidators"; +export * from "./useDisabledValidators"; +export * from "./useBlockCommitments"; +export * from "./useApplauseDetails"; +export * from "./useBabeSlots"; +export * from "./useErasTotalStaked"; diff --git a/src/hooks/ghost/useApplausesForTransaction.js b/src/hooks/ghost/useApplauseDetails.js similarity index 57% rename from src/hooks/ghost/useApplausesForTransaction.js rename to src/hooks/ghost/useApplauseDetails.js index f956581..c19c44c 100644 --- a/src/hooks/ghost/useApplausesForTransaction.js +++ b/src/hooks/ghost/useApplauseDetails.js @@ -6,42 +6,41 @@ import { fromHex } from "@polkadot-api/utils"; import { useUnstableProvider } from "./UnstableProvider" import { useMetadata } from "./MetadataProvider" -export const useApplausesForTransaction = ({ currentSession, txHash, argsHash }) => { +export const useApplauseDetails = ({ currentSession, argsHash }) => { const { chainHead$, chainId } = useUnstableProvider() const metadata = useMetadata() - const { data: applausesForTransaction } = useSWRSubscription( - chainHead$ && txHash && argsHash && currentSession && chainId && metadata - ? ["applausesForTransaction", chainHead$, txHash, argsHash, currentSession, chainId, metadata] + const { data: applauseDetails } = useSWRSubscription( + chainHead$ && argsHash && currentSession && chainId && metadata + ? ["applauseDetails", chainHead$, argsHash, currentSession, chainId, metadata] : null, - ([_, chainHead$, txHash, argsHash, currentSession, chainId, metadata], { next }) => { + ([_, chainHead$, argsHash, currentSession, chainId, metadata], { next }) => { const { finalized$, storage$ } = chainHead$ const subscription = finalized$.pipe( filter(Boolean), mergeMap((blockInfo) => { const builder = getDynamicBuilder(getLookupFn(metadata)) - const applausesForTransaction = builder.buildStorage("GhostSlowClaps", "ApplausesForTransaction") + const applauseDetails = builder.buildStorage("GhostSlowClaps", "ApplauseDetails") return storage$(blockInfo?.hash, "value", () => - applausesForTransaction?.keys.enc( + applauseDetails?.keys.enc( currentSession, - { asBytes: () => fromHex(txHash) }, { asBytes: () => fromHex(argsHash) }, ) ).pipe( filter(Boolean), distinct(), - map((value) => applausesForTransaction?.value.dec(value)) + map((value) => applauseDetails?.value.dec(value)) ) }), ) .subscribe({ - next(applausesForTransaction) { - next(null, applausesForTransaction) + next(applauseDetails) { + next(null, applauseDetails) }, error: next, }) return () => subscription.unsubscribe() } ) - return applausesForTransaction + return applauseDetails } diff --git a/src/hooks/ghost/useBabeSlots.js b/src/hooks/ghost/useBabeSlots.js new file mode 100644 index 0000000..7d97a7b --- /dev/null +++ b/src/hooks/ghost/useBabeSlots.js @@ -0,0 +1,76 @@ +import useSWRSubscription from "swr/subscription" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import { distinct, filter, map, mergeMap } from "rxjs" + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useGenesisSlot = () => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: genesisSlot } = useSWRSubscription( + chainHead$ && chainId && metadata + ? ["genesisSlot", chainHead$, chainId, metadata] + : null, + ([_, chainHead$, chainId, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const genesisSlot = builder.buildStorage("Babe", "GenesisSlot") + return storage$(blockInfo?.hash, "value", () => + genesisSlot?.keys.enc() + ).pipe( + filter(Boolean), + distinct(), + map((value) => genesisSlot?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(genesisSlot) { + next(null, genesisSlot) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return genesisSlot +} + +export const useCurrentSlot = () => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: currentSlot } = useSWRSubscription( + chainHead$ && chainId && metadata + ? ["currentSlot", chainHead$, chainId, metadata] + : null, + ([_, chainHead$, chainId, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const currentSlot = builder.buildStorage("Babe", "CurrentSlot") + return storage$(blockInfo?.hash, "value", () => + currentSlot?.keys.enc() + ).pipe( + filter(Boolean), + distinct(), + map((value) => currentSlot?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(currentSlot) { + next(null, currentSlot) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return currentSlot +} diff --git a/src/hooks/ghost/useBlockCommitments.js b/src/hooks/ghost/useBlockCommitments.js new file mode 100644 index 0000000..f44817c --- /dev/null +++ b/src/hooks/ghost/useBlockCommitments.js @@ -0,0 +1,44 @@ +import useSWRSubscription from "swr/subscription" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import { distinct, filter, map, mergeMap } from "rxjs" + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useBlockCommitments = ({ evmChainId }) => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: blockCommitments } = useSWRSubscription( + chainHead$ && chainId && metadata + ? ["blockCommitments", chainHead$, evmChainId, chainId, metadata] + : null, + ([_, chainHead$, evmChainId, chainId, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const blockCommitments = builder.buildStorage("GhostSlowClaps", "BlockCommitments") + return storage$(blockInfo?.hash, "value", () => + blockCommitments?.keys.enc(BigInt(evmChainId)) + ).pipe( + filter(Boolean), + distinct(), + map((value) => blockCommitments?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(blockCommitments) { + next(null, blockCommitments) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return blockCommitments?.reduce((acc, [index, obj]) => { + acc[index] = obj; + return acc; + }, []); +} diff --git a/src/hooks/ghost/useDisabledValidators.js b/src/hooks/ghost/useDisabledValidators.js new file mode 100644 index 0000000..0c887ab --- /dev/null +++ b/src/hooks/ghost/useDisabledValidators.js @@ -0,0 +1,41 @@ +import useSWRSubscription from "swr/subscription" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import { distinct, filter, map, mergeMap } from "rxjs" + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useDisabledValidators = () => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: disabledIndexes, error } = useSWRSubscription( + chainHead$ && chainId && metadata + ? ["disabledIndexes", chainHead$, chainId, metadata] + : null, + ([_, chainHead$, chainId, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const disabledIndexes = builder.buildStorage("Session", "DisabledValidators") + return storage$(blockInfo?.hash, "value", () => + disabledIndexes?.keys.enc() + ).pipe( + filter(Boolean), + distinct(), + map((value) => disabledIndexes?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(disabledIndexes) { + next(null, disabledIndexes) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return disabledIndexes ? disabledIndexes : [] +} diff --git a/src/hooks/ghost/useErasTotalStaked.js b/src/hooks/ghost/useErasTotalStaked.js new file mode 100644 index 0000000..f08ee1e --- /dev/null +++ b/src/hooks/ghost/useErasTotalStaked.js @@ -0,0 +1,41 @@ +import useSWRSubscription from "swr/subscription" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import { distinct, filter, map, mergeMap } from "rxjs" + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useErasTotalStake = ({ eraIndex }) => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: eraTotalStake } = useSWRSubscription( + chainHead$ && chainId && metadata + ? ["eraTotalStake", chainHead$, eraIndex, chainId, metadata] + : null, + ([_, chainHead$, eraIndex, chainId, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const eraTotalStake = builder.buildStorage("Staking", "ErasTotalStake") + return storage$(blockInfo?.hash, "value", () => + eraTotalStake?.keys.enc(eraIndex) + ).pipe( + filter(Boolean), + distinct(), + map((value) => eraTotalStake?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(eraTotalStake) { + next(null, eraTotalStake) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return eraTotalStake; +} diff --git a/src/hooks/ghost/useValidators.js b/src/hooks/ghost/useValidators.js new file mode 100644 index 0000000..fbc18e7 --- /dev/null +++ b/src/hooks/ghost/useValidators.js @@ -0,0 +1,41 @@ +import useSWRSubscription from "swr/subscription" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import { distinct, filter, map, mergeMap } from "rxjs" + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useValidators = ({ currentSession }) => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: slowClapValidators } = useSWRSubscription( + chainHead$ && chainId && metadata + ? ["slowClapValidators", chainHead$, currentSession, chainId, metadata] + : null, + ([_, chainHead$, currentSession, chainId, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const slowClapValidators = builder.buildStorage("GhostSlowClaps", "Validators") + return storage$(blockInfo?.hash, "value", () => + slowClapValidators?.keys.enc(currentSession) + ).pipe( + filter(Boolean), + distinct(), + map((value) => slowClapValidators?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(slowClapValidators) { + next(null, slowClapValidators) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return slowClapValidators +} diff --git a/src/style.scss b/src/style.scss index 637b18b..9ca57cf 100644 --- a/src/style.scss +++ b/src/style.scss @@ -128,3 +128,48 @@ a:hover svg { .tooltip { z-index: 9999999; } + +.custom-scrollbar { + overflow: auto; + max-height: 400px; + + /* For Chrome, Safari, Edge */ + &::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + &::-webkit-scrollbar-track { + background: transparent; /* Hide track */ + } + + &::-webkit-scrollbar-thumb { + background: #888; /* Only visible part */ + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb:hover { + background: #fff; + } + + /* This definitely hides arrows in Chrome/Safari/Edge */ + &::-webkit-scrollbar-button { + display: none; /* ← THIS WORKS */ + } + + &::-webkit-scrollbar-corner { + background: transparent; /* Hide corner */ + } + + /* For Firefox */ + scrollbar-width: thin; /* auto | thin | none */ + scrollbar-color: #fff transparent; /* thumb track */ +} + +input:-webkit-autofill { + -webkit-box-shadow: 0 0 0 1000px transparent inset !important; + box-shadow: 0 0 0 1000px transparent inset !important; + transition: background-color 5000s ease-in-out 0s; + color: #ffffff !important; + -webkit-text-fill-color: #ffffff !important; +} diff --git a/src/themes/darkPalette.js b/src/themes/darkPalette.js index ebe1a59..09df1aa 100644 --- a/src/themes/darkPalette.js +++ b/src/themes/darkPalette.js @@ -18,9 +18,18 @@ export const darkPalette = { success: "#60C45B", // idk where this is - done userFeedback: "#49A1F2", // idk where this is error: "#F06F73", // red negative % - done - warning: "#49A1F2", // idk where this is - done + warning: "#ed6c02", // idk where this is - done pnlGain: "#60C45B", // green positive % - done }, + bridgeProgress: { + error: "#F06F73", + warning: "#ed6c02", + success: "#60C45B", + }, + validatorsColor: { + red: "#fd9b9e", + green: "#60c45b", + }, gray: { 800: "#1F4671", // active menu - done 700: "#50759E", // menu background color - done diff --git a/src/themes/lightPalette.js b/src/themes/lightPalette.js index 180fc41..99fdbaf 100644 --- a/src/themes/lightPalette.js +++ b/src/themes/lightPalette.js @@ -6,11 +6,20 @@ export const lightPalette = { }, background: "linear-gradient(180.37deg, #B3BFC5 0.49%, #D1D5D4 26.3%, #EEEAE3 99.85%)", feedback: { - success: "#94B9A1", - userFeedback: "#49A1F2", - error: "#FF6767", - warning: "#FC8E5F", - pnlGain: "#3D9C70", + success: "#60C45B", // idk where this is - done + userFeedback: "#49A1F2", // idk where this is + error: "#F06F73", // red negative % - done + warning: "#ed6c02", // idk where this is - done + pnlGain: "#60C45B", // green positive % - done + }, + bridgeProgress: { + error: "#F06F73", + warning: "#ed6c02", + success: "#60C45B", + }, + validatorsColor: { + red: "#fd9b9e", + green: "#60c45b", }, gray: { 700: "#FAFAFB",