import { useEffect, useState, useMemo } from "react"; import ReactGA from "react-ga4"; import { useParams } from 'react-router-dom'; 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 { convertStatusToTemplate, convertStatusToLabel } from "./helpers"; import { parseFunctionCalldata } from "./components/functions"; import { networkAvgBlockSpeed } from "../../constants"; import { useTokenSymbol, usePastTotalSupply, usePastVotes, useBalance } from "../../hooks/tokens"; import { 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 { id } = useParams(); const proposalId = BigInt(id); const [isPending, setIsPending] = 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 { 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: `/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; const value = (totalVotes?._value ?? 0n) * 100n / (totalSupply?._value ?? 1n); return new DecimalBigNumber(value, 0); }, [totalVotes, totalSupply]); const forPercentage = useMemo(() => { if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2); const value = (forVotes?._value ?? 0n) * 10000n / (totalSupply?._value ?? 1n); return new DecimalBigNumber(value, 2); }, [forVotes, totalSupply]); const againstPercentage= useMemo(() => { if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2); const value = (againstVotes?._value ?? 0n) * 10000n / (totalSupply?._value ?? 1n); return new DecimalBigNumber(value, 2); }, [againstVotes, totalSupply]); const voteWeightPercentage = useMemo(() => { if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2); const value = (pastVotes?._value ?? 0n) * 10000n / (totalSupply?._value ?? 1n); return new DecimalBigNumber(value, 2); }, [pastVotes, totalSupply]); const voteValue = useMemo(() => { if (totalVotes?._value == 0n) { return 0; } return Number(forVotes._value * 100n / totalVotes._value); }, [forVotes, totalVotes]); const voteTarget = useMemo(() => { const first = (5n * againstVotes._value + forVotes._value); const second = BigInt(Math.floor(Math.sqrt(Number(totalVotes._value)))); const bias = 3n * first + second; const denominator = totalVotes._value + bias; if (denominator === 0n) { return 80; } return Number(totalVotes?._value * 100n / denominator); }, [againstVotes, forVotes, totalVotes]); 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 handleVote = async (against) => { setIsPending(true); const support = against ? 0 : 1; const result = await castVote(chainId, address, proposalId, support); if (result) { const toStore = JSON.stringify(proposalId.toString()); localStorage.setItem(VOTED_PROPOSALS_PREFIX, toStore); } setIsPending(true); } const handleExecute = async () => { setIsPending(true); await executeProposal(chainId, address, proposalId); setIsPending(true); } const handleRelease = async (proposalId) => { setIsPending(true); await releaseLocked(chainId, address, proposalId); setIsPending(true); } return ( Proposal details, need more in-depth description } /> Progress } topRight={ View Forum } > For: {formatNumber(forVotes.toString(), 2)} ({formatNumber(forPercentage?.toString(), 1)}%) Against: {formatNumber(againstVotes.toString(), 2)} ({formatNumber(againstPercentage?.toString(), 1)}%) Proposer {`${shorten(proposalProposer)}`} Locked {formatCurrency(proposalLocked, 4, ghstSymbol)}
Quorum {formatNumber(proposalQuorum.toString(), 4)} ({formatNumber(quorumPercentage, 1)}%) Total {formatNumber(totalSupply.toString(), 4)} ({formatNumber(votePercentage, 1)}%) Votes {formatNumber(pastVotes.toString(), 4)} ({formatNumber(voteWeightPercentage, 1)}%)
{address === undefined || address === "" ? connect()}>Connect : voteOf === 0n ? <> handleVote(1)} > For handleVote(0)} > Against : {`Voted ${voteOf === 1n ? "Against" : "For"}`} }
Timeline
} >
Executable Code } > Function Target Calldata Value {proposalDetails?.map((metadata, index) => { return parseFunctionCalldata(metadata, index, chainId, nativeCurrency); })}
) } const VotingTimeline = ({ connect, handleExecute, handleRelease, proposalLocked, proposalId, chainId, state, address, isProposer }) => { 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 && address === "" ? connect() : handleRelease()} > {address === "" ? "Connect" : "Release"} } address === "" ? connect() : handleExecute()} > {address === "" ? "Connect" : "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;