import { BigNumber } from 'ethers' import { DateTime } from 'luxon' import React, { useEffect, useState } from 'react' import { ChevronRight } from 'react-feather' import ReactMarkdown from 'react-markdown' import { useDispatch } from 'react-redux' import { RouteComponentProps, withRouter } from 'react-router-dom' import styled from 'styled-components' import { ButtonError } from '../../components/Button' import { AVERAGE_BLOCK_TIME_IN_SECS, BIG_INT_ZERO } from '../../constants' import { useActiveWeb3React } from '../../hooks' import useCurrentBlockTimestamp from '../../hooks/useCurrentBlockTimestamp' import { BodyWrapper } from '../../pages/AppBody' import { AppDispatch } from '../../state' import { ApplicationModal } from '../../state/application/actions' import { useBlockNumber, useModalOpen, useToggleModal } from '../../state/application/hooks' import { useActiveProtocol, useProposalData, useProposalStatus, useUserVotes } from '../../state/governance/hooks' import { SUPPORTED_PROTOCOLS } from '../../state/governance/reducer' import { useAllIdentities } from '../../state/social/hooks' import { ExternalLink, TYPE } from '../../theme' import { getEtherscanLink, isAddress } from '../../utils' import { nameOrAddress } from '../../utils/getName' import { AutoColumn } from '../Column' import { RowBetween, RowFixed } from '../Row' import VoteModal from '../vote/VoteModal' import { ProposalStatus } from './styled' import VoterList from './VoterList' const Wrapper = styled.div<{ backgroundColor?: string }>`` const ProposalInfo = styled(AutoColumn)` border-radius: 12px; position: relative; ` const ArrowWrapper = styled.div` display: flex; align-items: center; gap: 8px; color: ${({ theme }) => theme.text1}; a { color: ${({ theme }) => theme.text1}; text-decoration: none; } :hover { text-decoration: none; cursor: pointer; } ` const CardWrapper = styled.div` display: grid; grid-template-columns: 1fr 1fr; gap: 12px; width: 100%; ${({ theme }) => theme.mediaWidth.upToSmall` grid-template-columns: 1fr; margin: 0; padding: 0; `}; ` export const CardSection = styled(AutoColumn)<{ disabled?: boolean }>` padding: 1rem; z-index: 1; opacity: ${({ disabled }) => disabled && '0.4'}; ` const DetailText = styled.div` word-break: break-all; ` const MarkDownWrapper = styled.div` overflow: scroll; ${({ theme }) => theme.mediaWidth.upToSmall` max-width: 400px; `}; ` const AddressWrapper = styled.div` ${({ theme }) => theme.mediaWidth.upToSmall` max-width: 300px; `}; ` function ProposalDetails({ match: { params: { protocolID, proposalID }, }, history, }: RouteComponentProps<{ protocolID: string; proposalID: string }>) { const { chainId } = useActiveWeb3React() // if valid protocol id passed in, update global active protocol const dispatch = useDispatch<AppDispatch>() const [, setActiveProtocol] = useActiveProtocol() useEffect(() => { if (protocolID && Object.keys(SUPPORTED_PROTOCOLS).includes(protocolID)) { setActiveProtocol(SUPPORTED_PROTOCOLS[protocolID]) } }, [dispatch, protocolID, setActiveProtocol]) const proposalData = useProposalData(proposalID) const status = useProposalStatus(proposalID) // @TODO shoudlnt use spearate data for this // get and format data const currentTimestamp = useCurrentBlockTimestamp() const currentBlock = useBlockNumber() const endDate: DateTime | undefined = proposalData && currentTimestamp && currentBlock ? DateTime.fromSeconds( currentTimestamp .add(BigNumber.from(AVERAGE_BLOCK_TIME_IN_SECS).mul(BigNumber.from(proposalData.endBlock - currentBlock))) .toNumber() ) : undefined const now: DateTime = DateTime.local() // get total votes and format percentages for UI const totalVotes: number | undefined = proposalData?.forCount !== undefined && proposalData?.againstCount !== undefined ? proposalData.forCount + proposalData.againstCount : undefined const forPercentage: string = proposalData?.forCount !== undefined && totalVotes ? ((proposalData.forCount * 100) / totalVotes).toFixed(0) + '%' : '0%' const againstPercentage: string = proposalData?.againstCount !== undefined && totalVotes ? ((proposalData.againstCount * 100) / totalVotes).toFixed(0) + '%' : '0%' const [allIdentities] = useAllIdentities() // show links in propsoal details if content is an address const linkIfAddress = (content: string) => { if (isAddress(content) && chainId) { return ( <ExternalLink href={getEtherscanLink(chainId, content, 'address')}> {nameOrAddress(content, allIdentities)} </ExternalLink> ) } return <span>{nameOrAddress(content, allIdentities)}</span> } const [support, setSupport] = useState(false) const toggleVoteModal = useToggleModal(ApplicationModal.VOTE) const voteModalOpen = useModalOpen(ApplicationModal.VOTE) const voteModelToggle = useToggleModal(ApplicationModal.VOTE) const userAvailableVotes = useUserVotes() // only show voting if user has > 0 votes at proposal start block and proposal is active, const showVotingButtons = userAvailableVotes && userAvailableVotes.greaterThan(BIG_INT_ZERO) && proposalData && proposalData.status === 'active' return ( <BodyWrapper> <VoteModal isOpen={voteModalOpen} onDismiss={voteModelToggle} support={support} proposalId={proposalID} proposalTitle={proposalData?.title} /> <Wrapper> <ProposalInfo gap="lg" justify="start"> <RowBetween style={{ width: '100%', alignItems: 'flex-start' }}> <RowFixed> <ArrowWrapper onClick={() => { history?.length === 1 ? history.push('/') : history.goBack() }} style={{ alignItems: 'flex-start' }} > <TYPE.body fontWeight="600">Proposals</TYPE.body> </ArrowWrapper> <ChevronRight size={16} /> <TYPE.body>{'Proposal #' + proposalID}</TYPE.body> </RowFixed> {proposalData && <ProposalStatus status={status ?? ''}>{status}</ProposalStatus>} </RowBetween> <AutoColumn gap="10px" style={{ width: '100%' }}> <TYPE.largeHeader style={{ marginBottom: '.5rem' }}>{proposalData?.title}</TYPE.largeHeader> <RowBetween> <TYPE.main> {endDate && endDate < now ? 'Voting ended ' + (endDate && endDate.toLocaleString(DateTime.DATETIME_FULL)) : proposalData ? 'Voting ends approximately ' + (endDate && endDate.toLocaleString(DateTime.DATETIME_FULL)) : ''} </TYPE.main> </RowBetween> </AutoColumn> <CardWrapper> {proposalData && ( <> <VoterList title="For" amount={proposalData?.forCount} percentage={forPercentage} voters={proposalData?.forVotes.slice(0, Math.min(10, Object.keys(proposalData?.forVotes)?.length))} support="for" id={proposalData?.id} /> <VoterList title="Against" amount={proposalData?.againstCount} percentage={againstPercentage} voters={proposalData?.againstVotes.slice( 0, Math.min(10, Object.keys(proposalData?.againstVotes)?.length) )} support={'against'} id={proposalData?.id} /> </> )} {showVotingButtons && ( <> <ButtonError style={{ flexGrow: 1, fontSize: 16, padding: '8px 12px', width: 'unset' }} onClick={() => { setSupport(true) toggleVoteModal() }} > Support Proposal </ButtonError> <ButtonError error style={{ flexGrow: 1, fontSize: 16, padding: '8px 12px', width: 'unset' }} onClick={() => { setSupport(false) toggleVoteModal() }} > Reject Proposal </ButtonError> </> )} </CardWrapper> <AutoColumn gap="md"> <TYPE.mediumHeader fontWeight={600}>Details</TYPE.mediumHeader> {proposalData?.details?.map((d, i) => { return ( <DetailText key={i}> {i + 1}: {linkIfAddress(d.target)}.{d.functionSig}( {d.callData.split(',').map((content, i) => { return ( <span key={i}> {linkIfAddress(content)} {d.callData.split(',').length - 1 === i ? '' : ','} </span> ) })} ) </DetailText> ) })} </AutoColumn> <AutoColumn gap="md"> <MarkDownWrapper> <ReactMarkdown source={proposalData?.description} disallowedTypes={['code']} /> </MarkDownWrapper> </AutoColumn> <AutoColumn gap="md"> <TYPE.mediumHeader fontWeight={600}>Proposer</TYPE.mediumHeader> <AddressWrapper> <ExternalLink href={ proposalData?.proposer && chainId ? getEtherscanLink(chainId, proposalData?.proposer, 'address') : '' } style={{ wordWrap: 'break-word' }} > <TYPE.blue fontWeight={500}>{nameOrAddress(proposalData?.proposer, allIdentities)}</TYPE.blue> </ExternalLink> </AddressWrapper> </AutoColumn> </ProposalInfo> </Wrapper> </BodyWrapper> ) } export default withRouter(ProposalDetails)