import { useEffect, useState, useMemo, useCallback } from "react"; import ReactGA from "react-ga4"; import { useParams, useLocation } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { useBlock, useBlockNumber } from "wagmi"; import { Box, Container, Typography, Link, TableContainer, Table, TableHead, TableRow, TableCell, TableBody, useMediaQuery, useTheme } from "@mui/material"; import Paper from "../../components/Paper/Paper"; import PageTitle from "../../components/PageTitle/PageTitle"; import LinearProgressBar from "../../components/Progress/LinearProgressBar"; import InfoTooltip from "../../components/Tooltip/InfoTooltip"; import Chip from "../../components/Chip/Chip"; import { PrimaryButton, SecondaryButton } from "../../components/Button"; import GhostStyledIcon from "../../components/Icon/GhostIcon"; import ArrowUpIcon from "../../assets/icons/arrow-up.svg?react"; import { formatNumber, formatCurrency, shorten } from "../../helpers"; import { prettifySecondsInDays } from "../../helpers/timeUtil"; import { DecimalBigNumber, } from "../../helpers/DecimalBigNumber"; import { convertStatusToColor, convertStatusToLabel } from "./helpers"; import { parseFunctionCalldata } from "./components/functions"; import { networkAvgBlockSpeed } from "../../constants"; import { useLocalStorage } from "../../hooks/localstorage"; import { useTokenSymbol, usePastTotalSupply, usePastVotes, useBalance } from "../../hooks/tokens"; import { getVoteValue, getVoteTarget, useProposalState, useProposalProposer, useProposalLocked, useProposalQuorum, useProposalVoteOf, useProposalVotes, useProposalDetails, useProposalSnapshot, useProposalDeadline, useProposalVotingDelay, castVote, executeProposal, releaseLocked } from "../../hooks/governance"; import { VOTED_PROPOSALS_PREFIX } from "./helpers"; /////////////////////////////////////////////////////// import Timeline from '@mui/lab/Timeline'; import TimelineItem from '@mui/lab/TimelineItem'; import TimelineSeparator from '@mui/lab/TimelineSeparator'; import TimelineConnector from '@mui/lab/TimelineConnector'; import TimelineContent from '@mui/lab/TimelineContent'; import TimelineOppositeContent from '@mui/lab/TimelineOppositeContent'; import TimelineDot from '@mui/lab/TimelineDot'; /////////////////////////// import FastfoodIcon from '@mui/icons-material/Fastfood'; import LaptopMacIcon from '@mui/icons-material/LaptopMac'; import HotelIcon from '@mui/icons-material/Hotel'; import RepeatIcon from '@mui/icons-material/Repeat'; const HUNDRED = new DecimalBigNumber(100n, 0); const ProposalDetails = ({ chainId, address, connect, config }) => { const location = useLocation(); const { id } = useParams(); const proposalId = BigInt(id); const [isPendingVote, setIsPendingVote] = useState(-1); const [isPendingExecute, setIsPendingExecute] = useState(false); const [isPendingRelease, setIsPendingRelease] = useState(false); const [selectedDiscussionUrl, setSelectedDiscussionUrl] = useState(undefined); const isSemiSmallScreen = useMediaQuery("(max-width: 745px)"); const isSmallScreen = useMediaQuery("(max-width: 650px)"); const isVerySmallScreen = useMediaQuery("(max-width: 379px)"); const theme = useTheme(); const queryClient = useQueryClient(); const { getStorageValue, setStorageValue } = useLocalStorage(); const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST"); const { balance } = useBalance(chainId, "GHST", address); const { state: proposalState } = useProposalState(chainId, proposalId); const { proposer: proposalProposer } = useProposalProposer(chainId, proposalId); const { locked: proposalLocked } = useProposalLocked(chainId, proposalId); const { quorum: proposalQuorum } = useProposalQuorum(chainId, proposalId); const { voteOf } = useProposalVoteOf(chainId, proposalId, address); const { forVotes, againstVotes, totalVotes } = useProposalVotes(chainId, proposalId); const { proposalDetails } = useProposalDetails(chainId, proposalId); const { snapshot: proposalSnapshot } = useProposalSnapshot(chainId, proposalId); const { pastTotalSupply: totalSupply } = usePastTotalSupply(chainId, "GHST", proposalSnapshot); const { pastVotes } = usePastVotes(chainId, "GHST", proposalSnapshot, address); useEffect(() => { ReactGA.send({ hitType: "pageview", page: `${chainId}/governance/${id}` }); }, []); const quorumPercentage = useMemo(() => { if (totalSupply._value === 0n) return 0; return proposalQuorum / totalSupply * HUNDRED; }, [proposalQuorum, totalSupply]); const votePercentage = useMemo(() => { if (totalSupply._value === 0n) return 0; return totalVotes * HUNDRED / totalSupply; }, [totalVotes, totalSupply]); const forPercentage = useMemo(() => { if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2); return forVotes * HUNDRED / totalSupply; }, [forVotes, totalSupply]); const againstPercentage= useMemo(() => { if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2); return againstVotes * HUNDRED / totalSupply; }, [againstVotes, totalSupply]); const voteWeightPercentage = useMemo(() => { if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2); return pastVotes * HUNDRED / totalSupply; }, [pastVotes, totalSupply]); const nativeCurrency = useMemo(() => { const client = config?.getClient(); return client?.chain?.nativeCurrency?.symbol; }, [config]); const etherscanLink = useMemo(() => { const client = config.getClient(); let url = client?.chain?.blockExplorers?.default?.url; if (url) { url = url + `/address/${proposalProposer}`; } return url; }, [proposalProposer, config]); const isPending = useMemo(() => { return isPendingExecute || isPendingRelease || isPendingVote > -1; }, [isPendingExecute, isPendingRelease, isPendingVote]); const handleVote = useCallback(async (support) => { setIsPendingVote(support); const result = await castVote(chainId, address, proposalId, support); if (result) { const storedVotedProposals = getStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, []); const proposals = JSON.parse(storedVotedProposals || "[]").map(id => BigInt(id)); proposals.push(proposalId); setStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, proposals.map(id => id.toString())); await queryClient.invalidateQueries(); } setIsPendingVote(-1); }, [chainId, address, proposalId]); const handleExecute = async () => { setIsPendingExecute(true); await executeProposal(chainId, address, proposalId); await queryClient.invalidateQueries(); setIsPendingExecute(false); } const handleRelease = async () => { setIsPendingRelease(true); await releaseLocked(chainId, address, proposalId); await queryClient.invalidateQueries(); setIsPendingRelease(false); } return ( {`Cast ${ghstSymbol} to shape this proposal's outcome`} } /> {!isSmallScreen && Progress } } topRight={ View Forum } > For: {formatNumber(forVotes.toString(), 2)} ({formatNumber(forPercentage?.toString(), 2)}%) Against: {formatNumber(againstVotes.toString(), 2)} ({formatNumber(againstPercentage?.toString(), 2)}%) Proposer {`${shorten(proposalProposer)}`} Locked {formatCurrency(proposalLocked, 5, ghstSymbol)}
Quorum {formatNumber(proposalQuorum.toString(), 5)} ({formatNumber(quorumPercentage, 2)}%) Total {formatNumber(totalSupply.toString(), 5)} ({formatNumber(votePercentage, 2)}%) Votes {formatNumber(pastVotes.toString(), 5)} ({formatNumber(voteWeightPercentage, 2)}%)
{address === undefined || address === "" ? connect()}>Connect : voteOf === 0n ? <> handleVote(1)} > {isPendingVote === 1 ? "Voting For..." : "For"} handleVote(0)} > {isPendingVote === 0 ? "Voting Against..." : "Against"} : {`Voted ${voteOf === 1n ? "Against" : "For"}`} }
Timeline
} >
Executable Code } > {isSmallScreen ? {proposalDetails?.map((metadata, index) => { return parseFunctionCalldata({ metadata, index, chainId, nativeCoin: nativeCurrency, isTable: false, }); })} : Function Target Calldata Value {proposalDetails?.map((metadata, index) => { return parseFunctionCalldata({ metadata, index, chainId, nativeCoin: nativeCurrency, }); })}
}
) } const VotingTimeline = ({ connect, handleExecute, handleRelease, proposalLocked, proposalId, chainId, state, address, isProposer, isPending, isPendingExecute, isPendingRelease, }) => { const { delay: propsalVotingDelay } = useProposalVotingDelay(chainId, proposalId); const { snapshot: proposalSnapshot } = useProposalSnapshot(chainId, proposalId); const { deadline: proposalDeadline } = useProposalDeadline(chainId, proposalId); const voteStarted = useMemo(() => { if (proposalSnapshot && propsalVotingDelay) { return proposalSnapshot > propsalVotingDelay ? proposalSnapshot - propsalVotingDelay : 0; } return 0n; }, [proposalSnapshot, propsalVotingDelay]); return ( {(isProposer && (proposalLocked?._value ?? 0n) > 0n) && address === "" ? connect() : handleRelease()} > {address === "" ? "Connect" : isPendingRelease ? "Releasing..." : "Release"} } address === "" ? connect() : handleExecute()} > {address === "" ? "Connect" : state !== 4 ? convertStatusToLabel(state) : isPendingExecute ? "Executing..." : "Execute" } ) } const VotingTimelineItem = ({ position, isFirst, isLast, chainId, occured, message }) => { const { data: blockNumber } = useBlockNumber({ chainId, watch: true }); const { data: blockInfo } = useBlock({ chainId: chainId, blockNumber: occured, query: { enabled: Boolean(occured) } }); const timestamp = useMemo(() => { if (blockInfo && blockNumber > occured) { const timestamp = Number(blockInfo?.timestamp ?? 0n) * 1000; return new Date(timestamp).toLocaleString(); } const blocksRemaining = Number(occured) - Number(blockNumber); const secondsRemaining = blocksRemaining * Number(networkAvgBlockSpeed(chainId)); const predictedTimestamp = Math.floor(Date.now() / 1000) + secondsRemaining; return new Date(predictedTimestamp * 1000).toLocaleString(); }, [chainId, occured, blockInfo, blockNumber]); return ( {message} {timestamp} ) } export default ProposalDetails;