import { useMemo, useState, useCallback, useEffect } from "react"; import { Box, Typography, Link, Checkbox, FormControlLabel, useTheme } from "@mui/material"; import { useNavigate } from 'react-router-dom'; import { getBlockNumber } from "@wagmi/core"; import { useConfig } from "wagmi"; import { ss58Decode } from "@polkadot-labs/hdkd-helpers"; import { toHex } from "@polkadot-api/utils"; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material"; import Metric from "../../components/Metric/Metric"; import Modal from "../../components/Modal/Modal"; import SwapCard from "../../components/Swap/SwapCard"; import Token from "../../components/Token/Token"; import GhostStyledIcon from "../../components/Icon/GhostIcon"; import { PrimaryButton, SecondaryButton } from "../../components/Button"; import { GATEKEEPER_ADDRESSES, EMPTY_ADDRESS } from "../../constants/addresses"; import { GHOST_CONNECT } from "../../constants/ecosystem"; import { useLocalStorage } from "../../hooks/localstorage"; import { useBreakoutModal } from "../../hooks/breakoutModal"; import { useTokenSymbol, useCirculatingSupply } from "../../hooks/tokens"; import { useEpoch, useGatekeeperApy, useGatekeeperAddress } from "../../hooks/staking"; import { useEvmNetwork, useCurrentIndex, useUnstableProvider } from "../../hooks/ghost"; import { formatNumber, shorten } from "../../helpers"; import { prettifySecondsInDays } from "../../helpers/timeUtil"; import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; const BreakoutModal = ({ chainId, address }) => { const [step, setStep] = useState(0); const [receiver, setReceiver] = useState(""); const [convertedReceiver, setConvertedReceiver] = useState(undefined); const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST"); const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO"); const evmNetwork = useEvmNetwork({ evmChainId: chainId }); const { isOpened, closeModal: closeModalInner, isStakingOpened, isClaimBondOpened, warmupPeriod, setActiveTxIndex, defaultFunction, executableFunction, estimatedAmount } = useBreakoutModal(); const incomingFee = useMemo(() => { return new DecimalBigNumber( evmNetwork ? evmNetwork.incoming_fee : 100000000, 7 ); }, [evmNetwork]); const closeModal = () => { setActiveTxIndex(-1); closeModalPure(); } const closeModalPure = () => { setStep(0); setReceiver(""); closeModalInner(); } const header = useMemo(() => { if (isStakingOpened && warmupPeriod <= 0) return "Stake Warmed-up" if (isClaimBondOpened && warmupPeriod <= 0) return "Bond Warmed-up" if (isStakingOpened && warmupPeriod > 0) return "Stake in Warm-up" if (isClaimBondOpened && warmupPeriod > 0) return "Bond in Warm-up" }, [isStakingOpened, isClaimBondOpened, warmupPeriod]); const bridgeNumbers = useMemo(() => { const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length; const number = 1 + connectedNetworks * 3; return `(${number}, ${number})`; }, [chainId]); 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 ( {step === 0 ? header : step === 1 ? `Start ${bridgeNumbers} ${"Stake\u00B2"}` : "Bridge Confirmation"} } open={isOpened} onClose={closeModal} maxWidth="380px" minHeight="200px" > {step === 0 ? setStep(1)} closeModal={closeModal} /> : step === 1 ? setStep(2)} convertedReceiver={convertedReceiver} setConvertedReceiver={setConvertedReceiver} /> : } ) } const BridgeView = ({ chainId, receiver, setReceiver, convertedReceiver, setConvertedReceiver, bridgeNumbers, ghstSymbol, estimatedAmount, goNext, incomingFee }) => { const theme = useTheme(); const config = useConfig(); const { gatekeeperAddress } = useGatekeeperAddress(chainId); const chainExplorerUrl = useMemo(() => { const client = config?.getClient(); return client?.chain?.blockExplorers?.default?.url; }, [config]); return ( <> Bridge to start earning {bridgeNumbers} {"Stake\u00B2"} on your {ghstSymbol} balance: {formatNumber(estimatedAmount, 5)} {ghstSymbol} Generate a unique address for per tx with GHOST Connect for privacy. setReceiver(convertedReceiver ? "" : event.currentTarget.value)} inputProps={{ "data-testid": "fromInput" }} placeholder="GHOST address (sf prefixed)" endString={convertedReceiver ? : undefined } type="text" maxWidth="100%" /> Gatekeeper {shorten(gatekeeperAddress, 10, -8)} Bridge Fee {formatNumber(incomingFee, 4)}% Est. Time 20 mins Proceed ) } const WelcomeView = ({ bridgeNumbers, goNext, isStakingOpened, chainId, warmupPeriod, ghstSymbol, ftsoSymbol, defaultFunction, closeModal }) => { const [isPending, setIsPending] = useState(false); const { epoch } = useEpoch(chainId); const { gatekeeperAddress } = useGatekeeperAddress(chainId); const circulatingSupply = useCirculatingSupply(chainId, "STNK"); const gatekeepedApy = useGatekeeperApy(chainId); const { isExtensionMissing } = useUnstableProvider(); const getConnect = () => { window.open(GHOST_CONNECT, '_blank', 'noopener,noreferrer'); closeModal(); } const apyInner = useMemo(() => { let apy = Infinity; if (circulatingSupply._value > 0n) { const value = epoch.distribute.div(circulatingSupply); apy = 100 * (Math.pow(1 + parseFloat(value.toString()), 1095) - 1); if (apy === 0) apy = Infinity; } return apy; }, [circulatingSupply, epoch]); const callDefaultFunction = useCallback(async () => { setIsPending(true); await defaultFunction()(); setIsPending(false); closeModal(); }, [defaultFunction]); return ( <> {warmupPeriod <= 0 ? `You've succesfully warmed-up your ${isStakingOpened ? " " : "bonded "}${ftsoSymbol} ${isStakingOpened ? "(3, 3)" : "(1, 1)"} Staked at:` : `${isStakingOpened ? "Stake" : "Bond"} is in warm-up${isStakingOpened ? "" : ", which extends with each purchase"}. Your ${ftsoSymbol} ${isStakingOpened ? "(3, 3)" : "(1, 1)"} is Staked at:` } {formatNumber(apyInner, 2)}% APY callDefaultFunction()} disabled={isPending || warmupPeriod > 0} loading={isPending} fullWidth > {warmupPeriod > 0 ? `Warm-up ends in ${prettifySecondsInDays(epoch.length * warmupPeriod)}` : `${isPending ? "Claiming..." : "Claim"} ${isStakingOpened ? "(3, 3) Stake" : "(1, 1) Bond"}` }
OR
Skip the Warm-up Now! {`Bridge your ${ghstSymbol} to GHOST Chain and start ${bridgeNumbers} ${"Stake\u00B2"} at:`} {formatNumber(apyInner * gatekeepedApy, 2)}% APY {isExtensionMissing ? "Get GHOST Connect" : `Start ${bridgeNumbers} ${"Stake\u00B2"}`} ) } const ConfirmStep = ({ chainId, address, receiver, convertedReceiver, executableFunction, ghstSymbol, bridgeNumbers, estimatedAmount, bridgingRisk, incomingFee, setActiveTxIndex, closeModal, evmNetwork }) => { const config = useConfig(); const navigate = useNavigate(); const currentSession = useCurrentIndex(); const { getStorageValue, setStorageValue } = useLocalStorage(); const [blockNumber, setBlockNumber] = useState(0n); const [isPending, setIsPending] = useState(false); const [acknowledgeBridgingRisk, setAcknowledgeBridgingRisk] = useState(false); const [acknowledgeWalletCustody, setAcknowledgeWalletCustody] = useState(false); getBlockNumber(config).then(block => setBlockNumber(block)); const nativeSymbol = useMemo(() => config?.getClient()?.chain?.nativeCurrency?.symbol, [config]); const networkName= useMemo(() => config?.getClient()?.chain?.name.toLowerCase(), [config]); const receivedEstimation = useMemo(() => { const decimals = incomingFee._decimals + 2; const afterFee = new DecimalBigNumber( BigInt(Math.pow(10, decimals) - incomingFee._value), decimals ); return estimatedAmount.mul(afterFee); }, [incomingFee, estimatedAmount]); const execute = useCallback(async () => { setIsPending(true); try { const txHash = await executableFunction()(convertedReceiver); if (txHash) { const expectedSessionIndex = (currentSession ?? 0) + (evmNetwork ? Number((evmNetwork.avg_block_speed * evmNetwork.finality_delay) / (1000n * 14400n)) : 0); const transaction = { receiverAddress: receiver, amount: estimatedAmount._value.toString(), sessionIndex: expectedSessionIndex, transactionHash: txHash, blockNumber: blockNumber, chainId: chainId, bridgeStability: 69, // TODO: avoid stability timestamp: Date.now() } const storedTransactions = getStorageValue(chainId, address, "bridge-txs", []); const newStoredTransactions = [transaction, ...storedTransactions]; setStorageValue(chainId, address, "bridge-txs", newStoredTransactions); setActiveTxIndex(0); navigate(`${networkName}/bridge`); } } finally { setIsPending(false); closeModal(); } }, [ executableFunction, convertedReceiver, receiver, networkName, chainId, address, blockNumber, evmNetwork, currentSession, ]); return ( <> {ghstSymbol} {ghstSymbol} {`You are bridging to GHOST Chain now to claim ${bridgeNumbers} ${"Stake\u00B2"} rewards.`}
setAcknowledgeBridgingRisk(event.target.checked)} icon={} checkedIcon={} /> } label={ {`I acknowledge decentralized bridging risk.`}  Learn more. } sx={{ '& .MuiFormControlLabel-label': { textAlign: "justify" } }} /> setAcknowledgeWalletCustody(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" } }} /> execute()} loading={isPending} disabled={isPending || !acknowledgeWalletCustody || !acknowledgeBridgingRisk} fullWidth > {isPending ? "Confirming..." : "I Confirm"} ) } export default BreakoutModal;