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;