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",