import React from 'react' import { withRouter } from 'react-router-dom' import { BodyWrapper } from './AppBody' import { useActiveProtocol, useDelegateInfo, useGovernanceToken, useAllProposals, useAllProposalStates, useUserDelegatee, } from '../state/governance/hooks' import { RouteComponentProps } from 'react-router-dom' import { useActiveWeb3React } from '../hooks' import { ChainId, Token, JSBI } from '@uniswap/sdk' import { GreyCard, OutlineCard } from '../components/Card' import { useProtocolUpdate } from '../hooks/useProtocolUpdate' import styled from 'styled-components' import { RowBetween, AutoRow, RowFixed } from '../components/Row' import { CheckCircle, XCircle, ChevronRight } from 'react-feather' import { AutoColumn } from '../components/Column' import EmptyProfile from '../assets/images/emptyprofile.png' import { RoundedProfileImage, WrappedListLogo, ProposalStatusSmall, DelegateButton, } from '../components/governance/styled' import { getTwitterProfileLink, getEtherscanLink, shortenAddress, isAddress } from '../utils' import { TYPE, ExternalLink, GreenIcon, RedIcon, StyledInternalLink, OnlyAboveSmall } from '../theme' import { useIdentity, useTwitterProfileData, useAllIdentities } from '../state/social/hooks' import { useTokenBalance } from '../state/wallet/hooks' import Loader from '../components/Loader' import { enumerateProposalState } from '../data/governance' import CopyHelper from '../components/AccountDetails/Copy' import { useIsEOA } from '../hooks/useIsEOA' import { useIsAave } from '../hooks/useContract' import { useToggleModal, useModalDelegatee } from '../state/application/hooks' import { ApplicationModal } from '../state/application/actions' import { BIG_INT_ZERO } from '../constants' import useENS from '../hooks/useENS' import { nameOrAddress } from '../utils/getName' const ArrowWrapper = styled(StyledInternalLink)` display: flex; align-items: center; gap: 8px; height: 24px; color: ${({ theme }) => theme.text1}; a { color: ${({ theme }) => theme.text1}; text-decoration: none; } :hover { text-decoration: none; cursor: pointer; } ` const DataRow = styled.div` display: grid; grid-template-columns: 1fr 1fr 1fr; ${({ theme }) => theme.mediaWidth.upToSmall` grid-template-columns: 1fr 1fr; `}; ` export const Break = styled.div` width: 100%; background-color: ${({ theme }) => theme.bg4}; height: 1px; ` const ResponsiveDataText = styled(TYPE.black)` font-size: 20px; ${({ theme }) => theme.mediaWidth.upToSmall` font-size: 14px; `}; ` const ResponsiveBodyText = styled(TYPE.black)` font-size: 16px; ${({ theme }) => theme.mediaWidth.upToSmall` font-size: 12px; `}; ` function localNumber(val: number) { return parseFloat(parseFloat(val.toString()).toFixed(0)).toLocaleString() } function DelegateInfo({ match: { params: { protocolID, delegateAddress }, }, }: RouteComponentProps<{ protocolID?: string; delegateAddress?: string }>) { // if valid protocol id passed in, update global active protocol useProtocolUpdate(protocolID) const { chainId, account } = useActiveWeb3React() const [activeProtocol] = useActiveProtocol() const formattedAddress = isAddress(delegateAddress) // get governance data and format amounts const delegateInfo = useDelegateInfo(delegateAddress) const delegatedVotes = delegateInfo ? ( localNumber(delegateInfo.delegatedVotes) ) : delegateInfo === null ? ( '0' ) : ( <Loader /> ) const userDelegatee: string | undefined = useUserDelegatee(formattedAddress) const holdersRepresented = delegateInfo ? ( localNumber( delegateInfo.tokenHoldersRepresentedAmount - (userDelegatee && userDelegatee === formattedAddress ? 1 : 0) ) ) : delegateInfo === null ? ( '0' ) : ( <Loader /> ) const isEOA = useIsEOA(delegateAddress) // proposal data const proposalData = useAllProposals() const proposalStatuses = useAllProposalStates() // get gov token balance const govToken: Token | undefined = useGovernanceToken() const delegateTokenBalance = useTokenBalance(formattedAddress ? formattedAddress : undefined, govToken) // user gov data const isDelegatee = userDelegatee && delegateAddress ? userDelegatee.toLowerCase() === delegateAddress.toLowerCase() : false // don't show govToken balance for Aave until multi-token support implemented in Sybil const isAave = useIsAave() // get social data from Sybil list const identity = useIdentity(delegateAddress) const twitterHandle = identity?.twitter?.handle const twitterData = useTwitterProfileData(twitterHandle) const [allIdentities] = useAllIdentities() // ens name if they have it const ensName = useENS(formattedAddress ? formattedAddress : null)?.name const nameShortened = nameOrAddress( formattedAddress ? formattedAddress : undefined, allIdentities, true, delegateInfo?.autonomous, ensName ) // toggle for showing delegation modal with prefilled delegate const toggelDelegateModal = useToggleModal(ApplicationModal.DELEGATE) const [, setPrefilledDelegate] = useModalDelegatee() // detect if they can delegate const userTokenBalance = useTokenBalance(account ?? undefined, govToken) const showDelegateButton = Boolean(userTokenBalance && JSBI.greaterThan(userTokenBalance.raw, BIG_INT_ZERO)) // mainnet only if (chainId && chainId !== ChainId.MAINNET) { return ( <BodyWrapper> <OutlineCard>Please switch to Ethereum mainnet. </OutlineCard> </BodyWrapper> ) } return ( <BodyWrapper> {formattedAddress && chainId && delegateAddress ? ( <AutoColumn gap="lg"> <RowFixed style={{ width: '100%', height: '20px' }}> <ArrowWrapper to={'/delegates/' + activeProtocol?.id}> <TYPE.body fontSize="16px" fontWeight="600"> Top Delegates </TYPE.body> </ArrowWrapper> <ChevronRight size={16} /> <ExternalLink href={ twitterHandle ? getTwitterProfileLink(twitterHandle) : getEtherscanLink(chainId, formattedAddress, 'address') } > <TYPE.black>{nameShortened}</TYPE.black> </ExternalLink> </RowFixed> <GreyCard> <RowBetween> <AutoRow gap="10px"> {twitterData?.profileURL ? ( <RoundedProfileImage> <img src={twitterData.profileURL} alt="profile" /> </RoundedProfileImage> ) : ( <WrappedListLogo src={EmptyProfile} /> )} <AutoColumn gap="2px"> <RowFixed> <ExternalLink href={ twitterHandle ? getTwitterProfileLink(twitterHandle) : getEtherscanLink(chainId, formattedAddress, 'address') } > <TYPE.black> {nameShortened === formattedAddress ? ensName ?? formattedAddress : nameShortened} </TYPE.black> </ExternalLink> {!twitterHandle && !delegateInfo?.autonomous && <CopyHelper toCopy={formattedAddress} />} </RowFixed> {twitterHandle || delegateInfo?.autonomous || nameShortened !== shortenAddress(delegateAddress) ? ( <RowFixed> <ExternalLink href={getEtherscanLink(chainId, formattedAddress, 'address')}> <TYPE.black fontSize="12px">{shortenAddress(delegateAddress)}</TYPE.black> </ExternalLink> <CopyHelper toCopy={formattedAddress} /> </RowFixed> ) : ( <TYPE.black fontSize="12px"> {isEOA === true ? '👤 EOA' : isEOA === false && '📜 Smart Contract'} </TYPE.black> )} </AutoColumn> </AutoRow> <DelegateButton width="fit-content" disabled={!showDelegateButton || !account || isDelegatee} onClick={() => { setPrefilledDelegate(delegateAddress) toggelDelegateModal() }} > {isDelegatee ? 'Delegated' : 'Delegate'} </DelegateButton> </RowBetween> </GreyCard> <GreyCard> <DataRow> {!isAave && ( <AutoColumn gap="sm"> <TYPE.main fontSize="14px">{`${activeProtocol?.token.symbol} Balance`}</TYPE.main> <ResponsiveDataText> {delegateTokenBalance ? delegateTokenBalance?.toFixed(0) : <Loader />} </ResponsiveDataText> </AutoColumn> )} <AutoColumn gap="sm"> <TYPE.main fontSize="14px">Votes</TYPE.main> <ResponsiveDataText>{delegatedVotes}</ResponsiveDataText> </AutoColumn> <OnlyAboveSmall> <AutoColumn gap="sm"> <TYPE.main fontSize="14px">Token Holders Represented</TYPE.main> <ResponsiveDataText>{holdersRepresented}</ResponsiveDataText> </AutoColumn> </OnlyAboveSmall> </DataRow> </GreyCard> <GreyCard> <AutoColumn gap="lg"> <TYPE.main fontSize="16px">Voting History</TYPE.main> <Break /> {delegateInfo && proposalStatuses && delegateInfo.votes ? ( delegateInfo.votes ?.map((vote, i) => { const proposal = proposalData?.[vote.proposal] // need to offset by one because proposal ids start at 1 const index = proposal ? parseFloat(proposal?.id) - 1 : 0 const status = proposalStatuses[index] ? enumerateProposalState(proposalStatuses[index]) : 'loading' return ( proposal && ( <div key={i}> <RowBetween key={i + proposal.id} style={{ alignItems: 'flex-start' }}> <AutoColumn gap="sm" style={{ maxWidth: '500px' }} justify="flex-start"> <StyledInternalLink to={'/proposals/' + activeProtocol?.id + '/' + proposal.id}> <ResponsiveBodyText style={{ maxWidth: '240px' }}>{proposal.title}</ResponsiveBodyText> </StyledInternalLink> {status && ( <RowFixed> <ProposalStatusSmall status={status}>{status}</ProposalStatusSmall> </RowFixed> )} </AutoColumn> <AutoColumn gap="sm" justify="flex-start" style={{ height: '100%' }}> <RowFixed> <ResponsiveBodyText mr="6px" ml="6px" textAlign="right"> {`${localNumber(vote.votes)} votes ${vote.support ? 'in favor' : 'against'}`} </ResponsiveBodyText> {vote.support ? ( <GreenIcon> <CheckCircle /> </GreenIcon> ) : ( <RedIcon> <XCircle /> </RedIcon> )} </RowFixed> </AutoColumn> </RowBetween> {i !== 0 && <Break style={{ marginTop: '24px' }} />} </div> ) ) }) .reverse() ) : delegateInfo === null ? ( <TYPE.body>No past votes</TYPE.body> ) : ( <Loader /> )} {delegateInfo && delegateInfo?.votes?.length === 0 && <TYPE.body>No past votes</TYPE.body>} </AutoColumn> </GreyCard> </AutoColumn> ) : ( <Loader /> )} </BodyWrapper> ) } export default withRouter(DelegateInfo)