restrict governance UI to mainnet only

fix governor name bug

revert useContract change

add governorIndex to vote page

only fetch latest useLatestProposalCount

fix useDataFromEventLogs

hardcode proposalIndexes for old governors
This commit is contained in:
Noah Zinsmeister 2021-06-16 17:22:11 -04:00
parent d9bd392e6d
commit b763659788
No known key found for this signature in database
GPG Key ID: 83022DD49188C9F2
8 changed files with 162 additions and 155 deletions

@ -18,13 +18,19 @@ export const V2_ROUTER_ADDRESS: AddressMap = constructSameAddressMap(
) )
// most current governance contract address should always be the 0 index // most current governance contract address should always be the 0 index
// only support governance on mainnet
export const GOVERNANCE_ADDRESSES: AddressMap[] = [ export const GOVERNANCE_ADDRESSES: AddressMap[] = [
{ {
[SupportedChainId.MAINNET]: '0xC4e172459f1E7939D522503B81AFAaC1014CE6F6', [SupportedChainId.MAINNET]: '0xC4e172459f1E7939D522503B81AFAaC1014CE6F6',
}, },
constructSameAddressMap('0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F', false), {
[SupportedChainId.MAINNET]: '0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F',
},
] ]
export const TIMELOCK_ADDRESS: AddressMap = constructSameAddressMap('0x1a9C8182C09F50C8318d769245beA52c32BE35BC', false) export const TIMELOCK_ADDRESS: AddressMap = {
[SupportedChainId.MAINNET]: '0x1a9C8182C09F50C8318d769245beA52c32BE35BC',
}
export const MERKLE_DISTRIBUTOR_ADDRESS: AddressMap = { export const MERKLE_DISTRIBUTOR_ADDRESS: AddressMap = {
[SupportedChainId.MAINNET]: '0x090D4613473dEE047c3f2706764f49E0821D256e', [SupportedChainId.MAINNET]: '0x090D4613473dEE047c3f2706764f49E0821D256e',
} }

@ -6,7 +6,9 @@ const governanceContracts = (): Record<string, string> =>
GOVERNANCE_ADDRESSES.reduce( GOVERNANCE_ADDRESSES.reduce(
(acc, addressMap, i) => ({ (acc, addressMap, i) => ({
...acc, ...acc,
[addressMap[SupportedChainId.MAINNET]]: `Governance${i === GOVERNANCE_ADDRESSES.length - 1 ? '' : ` (V${i})`}`, [addressMap[SupportedChainId.MAINNET]]: `Governance${
i === 0 ? '' : ` (V${GOVERNANCE_ADDRESSES.length - 1 - i})`
}`,
}), }),
{} {}
) )

@ -1,2 +1 @@
export const UNISWAP_GRANTS_START_BLOCK = 11473815 export const UNISWAP_GRANTS_START_BLOCK = 11473815
export const EDUCATION_FUND_1_START_BLOCK = 12620175

@ -46,34 +46,19 @@ import { useActiveWeb3React } from './web3'
// returns null on errors // returns null on errors
export function useContract<T extends Contract = Contract>( export function useContract<T extends Contract = Contract>(
addressOrAddressMap: string | { [chainId: number]: string } | { [chainId: number]: string }[] | undefined, addressOrAddressMap: string | { [chainId: number]: string } | undefined,
ABI: any, ABI: any,
withSignerIfPossible = true withSignerIfPossible = true
): T | null { ): T | null {
const { library, account, chainId } = useActiveWeb3React() const { library, account, chainId } = useActiveWeb3React()
return useMemo(() => { return useMemo(() => {
if (!addressOrAddressMap || !ABI || !library || !chainId) { if (!addressOrAddressMap || !ABI || !library || !chainId) return null
return null
}
let address: string | undefined let address: string | undefined
if (typeof addressOrAddressMap === 'string') { if (typeof addressOrAddressMap === 'string') address = addressOrAddressMap
address = addressOrAddressMap else address = addressOrAddressMap[chainId]
} else if (!Array.isArray(addressOrAddressMap)) { if (!address) return null
address = addressOrAddressMap[chainId]
}
if (!address && !Array.isArray(addressOrAddressMap)) {
return null
}
try { try {
if (Array.isArray(addressOrAddressMap)) {
return addressOrAddressMap.map((addressMap) =>
getContract(addressMap[chainId], ABI, library, withSignerIfPossible && account ? account : undefined)
)
}
if (!address) {
return null
}
return getContract(address, ABI, library, withSignerIfPossible && account ? account : undefined) return getContract(address, ABI, library, withSignerIfPossible && account ? account : undefined)
} catch (error) { } catch (error) {
console.error('Failed to get contract', error) console.error('Failed to get contract', error)
@ -131,21 +116,22 @@ export function useMerkleDistributorContract() {
return useContract(MERKLE_DISTRIBUTOR_ADDRESS, MERKLE_DISTRIBUTOR_ABI, true) return useContract(MERKLE_DISTRIBUTOR_ADDRESS, MERKLE_DISTRIBUTOR_ABI, true)
} }
export function useGovernanceContracts(): Contract[] | null { export function useGovernanceContracts(): (Contract | null)[] {
const { library, account, chainId } = useActiveWeb3React() const { library, account, chainId } = useActiveWeb3React()
return useMemo(() => { return useMemo(() => {
if (!library || !chainId) { if (!library || !chainId) {
return null return []
} }
return GOVERNANCE_ADDRESSES.filter((addressMap) => Boolean(addressMap[chainId])).map((addressMap) => {
try { try {
return GOVERNANCE_ADDRESSES.filter((addressMap) => Boolean(addressMap[chainId])).map((addressMap) => return getContract(addressMap[chainId], GOVERNANCE_ABI, library, account ? account : undefined)
getContract(addressMap[chainId], GOVERNANCE_ABI, library, account ? account : undefined)
)
} catch (error) { } catch (error) {
console.error('Failed to get contract', error) console.error('Failed to get contract', error)
return null return null
} }
})
}, [library, chainId, account]) }, [library, chainId, account])
} }

@ -87,7 +87,7 @@ export default function App() {
<Web3ReactManager> <Web3ReactManager>
<Switch> <Switch>
<Route exact strict path="/vote" component={Vote} /> <Route exact strict path="/vote" component={Vote} />
<Route exact strict path="/vote/:id" component={VotePage} /> <Route exact strict path="/vote/:governorIndex/:id" component={VotePage} />
<Route exact strict path="/claim" component={OpenClaimAddressModalAndRedirectToSwap} /> <Route exact strict path="/claim" component={OpenClaimAddressModalAndRedirectToSwap} />
<Route exact strict path="/uni" component={Earn} /> <Route exact strict path="/uni" component={Earn} />
<Route exact strict path="/uni/:currencyIdA/:currencyIdB" component={Manage} /> <Route exact strict path="/uni/:currencyIdA/:currencyIdB" component={Manage} />

@ -121,13 +121,13 @@ const ProposerAddressLink = styled(ExternalLink)`
export default function VotePage({ export default function VotePage({
match: { match: {
params: { id }, params: { governorIndex, id },
}, },
}: RouteComponentProps<{ id: string }>) { }: RouteComponentProps<{ governorIndex: string; id: string }>) {
const { chainId, account } = useActiveWeb3React() const { chainId, account } = useActiveWeb3React()
// get data for this specific proposal // get data for this specific proposal
const proposalData: ProposalData | undefined = useProposalData(id) const proposalData: ProposalData | undefined = useProposalData(Number.parseInt(governorIndex), id)
// update support based on button interactions // update support based on button interactions
const [support, setSupport] = useState<boolean>(true) const [support, setSupport] = useState<boolean>(true)

@ -248,9 +248,9 @@ export default function Vote() {
</TYPE.subHeader> </TYPE.subHeader>
</EmptyProposals> </EmptyProposals>
)} )}
{allProposals?.reverse().map((p: ProposalData, i) => { {allProposals?.reverse()?.map((p: ProposalData) => {
return ( return (
<Proposal as={Link} to={'/vote/' + p.id} key={i}> <Proposal as={Link} to={`/vote/${p.governorIndex}/${p.id}`} key={`${p.governorIndex}${p.id}`}>
<ProposalNumber>{p.id}</ProposalNumber> <ProposalNumber>{p.id}</ProposalNumber>
<ProposalTitle>{p.title}</ProposalTitle> <ProposalTitle>{p.title}</ProposalTitle>
<ProposalStatus status={p.status}>{ProposalState[p.status]}</ProposalStatus> <ProposalStatus status={p.status}>{ProposalState[p.status]}</ProposalStatus>

@ -2,16 +2,17 @@ import { TransactionResponse } from '@ethersproject/providers'
import { abi as GOV_ABI } from '@uniswap/governance/build/GovernorAlpha.json' import { abi as GOV_ABI } from '@uniswap/governance/build/GovernorAlpha.json'
import { CurrencyAmount, Token } from '@uniswap/sdk-core' import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { GOVERNANCE_ADDRESSES } from 'constants/addresses' import { GOVERNANCE_ADDRESSES } from 'constants/addresses'
import { SupportedChainId } from 'constants/chains'
import { UNISWAP_GRANTS_PROPOSAL_DESCRIPTION } from 'constants/proposals/uniswap_grants_proposal_description' import { UNISWAP_GRANTS_PROPOSAL_DESCRIPTION } from 'constants/proposals/uniswap_grants_proposal_description'
import { ethers, utils } from 'ethers' import { BigNumber, ethers, utils } from 'ethers'
import { isAddress } from 'ethers/lib/utils' import { isAddress } from 'ethers/lib/utils'
import { useGovernanceContracts, useUniContract } from 'hooks/useContract' import { useGovernanceContracts, useUniContract } from 'hooks/useContract'
import { useActiveWeb3React } from 'hooks/web3' import { useActiveWeb3React } from 'hooks/web3'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { calculateGasMargin } from 'utils/calculateGasMargin' import { calculateGasMargin } from 'utils/calculateGasMargin'
import { EDUCATION_FUND_1_START_BLOCK, UNISWAP_GRANTS_START_BLOCK } from '../../constants/proposals' import { UNISWAP_GRANTS_START_BLOCK } from '../../constants/proposals'
import { UNI } from '../../constants/tokens' import { UNI } from '../../constants/tokens'
import { useMultipleContractMultipleData, useMultipleContractSingleData, useSingleCallResult } from '../multicall/hooks' import { useMultipleContractMultipleData, useSingleCallResult } from '../multicall/hooks'
import { useTransactionAdder } from '../transactions/hooks' import { useTransactionAdder } from '../transactions/hooks'
interface ProposalDetail { interface ProposalDetail {
@ -31,6 +32,7 @@ export interface ProposalData {
startBlock: number startBlock: number
endBlock: number endBlock: number
details: ProposalDetail[] details: ProposalDetail[]
governorIndex: number // index in the governance address array for which this proposal pertains
} }
export enum ProposalState { export enum ProposalState {
@ -46,63 +48,68 @@ export enum ProposalState {
} }
const GovernanceInterface = new ethers.utils.Interface(GOV_ABI) const GovernanceInterface = new ethers.utils.Interface(GOV_ABI)
// get count of all proposals made // get count of all proposals made in the latest governor contract
export function useProposalCounts(): Record<string, number> | undefined { function useLatestProposalCount(): number | undefined {
const { chainId } = useActiveWeb3React() const govContracts = useGovernanceContracts()
const addresses = useMemo(() => {
if (!chainId) { const res = useSingleCallResult(govContracts[0], 'proposalCount')
return []
if (res?.result?.[0]) {
return (res.result[0] as BigNumber).toNumber()
} }
return GOVERNANCE_ADDRESSES.map((addressMap) => addressMap[chainId])
}, [chainId]) return undefined
const responses = useMultipleContractSingleData(addresses, GovernanceInterface, 'proposalCount')
return useMemo(() => {
return responses.reduce((acc, response, i) => {
if (response.result && !response.loading) {
return {
...acc,
[addresses[i]]: parseInt(response.result[0]),
}
}
return acc
}, {})
}, [addresses, responses])
} }
/** /**
* Need proposal events to get description data emitted from * Need proposal events to get description data emitted from
* new proposal event. * new proposal event.
*/ */
export function useDataFromEventLogs() { function useDataFromEventLogs():
| {
description: string
details: { target: string; functionSig: string; callData: string }[]
}[][]
| undefined {
const { library, chainId } = useActiveWeb3React() const { library, chainId } = useActiveWeb3React()
const [formattedEvents, setFormattedEvents] = const [formattedEvents, setFormattedEvents] =
useState<{ description: string; details: { target: string; functionSig: string; callData: string }[] }[]>() useState<{ description: string; details: { target: string; functionSig: string; callData: string }[] }[][]>()
const govContracts = useGovernanceContracts() const govContracts = useGovernanceContracts()
// create filter for these specific events // create filters for ProposalCreated events
const filters = useMemo( const filters = useMemo(
() => () =>
govContracts govContracts?.filter((govContract) => !!govContract)?.length > 0
? govContracts.map((contract) => ({ ? govContracts
.filter((govContract): govContract is ethers.Contract => !!govContract)
.map((contract) => ({
...contract.filters.ProposalCreated(), ...contract.filters.ProposalCreated(),
fromBlock: 10861678, fromBlock: 10861678, // TODO could optimize this on a per-contract basis, this is the safe value
toBlock: 'latest', toBlock: 'latest',
})) }))
: undefined, : undefined,
[govContracts] [govContracts]
) )
// clear logs on chainId change
useEffect(() => {
return () => {
setFormattedEvents(undefined)
}
}, [chainId])
useEffect(() => { useEffect(() => {
if (!filters || !library) return if (!filters || !library) return
let stale = false let stale = false
if (!formattedEvents) { if (!formattedEvents) {
const filterRequests = filters.map((filter) => library.getLogs(filter)) Promise.all(filters.map((filter) => library.getLogs(filter)))
Promise.all(filterRequests)
.then((events) => events.flat())
.then((governanceContractsProposalEvents) => { .then((governanceContractsProposalEvents) => {
if (stale) return if (stale) return
const formattedEventData = governanceContractsProposalEvents.map((event) => {
const formattedEventData = governanceContractsProposalEvents.map((proposalEvents) => {
return proposalEvents.map((event) => {
const eventParsed = GovernanceInterface.parseLog(event).args const eventParsed = GovernanceInterface.parseLog(event).args
return { return {
description: eventParsed.description, description: eventParsed.description,
@ -119,11 +126,17 @@ export function useDataFromEventLogs() {
}), }),
} }
}) })
})
setFormattedEvents(formattedEventData) setFormattedEvents(formattedEventData)
}) })
.catch((error) => { .catch((error) => {
if (stale) return
console.error('Failed to fetch proposals', error) console.error('Failed to fetch proposals', error)
setFormattedEvents(undefined)
}) })
return () => { return () => {
stale = true stale = true
} }
@ -136,41 +149,22 @@ export function useDataFromEventLogs() {
} }
// get data for all past and active proposals // get data for all past and active proposals
export function useAllProposalData() { export function useAllProposalData(): ProposalData[] {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const proposalCounts = useProposalCounts() const proposalCount = useLatestProposalCount()
const proposalIndexes = useMemo(() => {
const results: number[][][] = []
const emptyState = new Array(GOVERNANCE_ADDRESSES.length).fill([], 0)
GOVERNANCE_ADDRESSES.forEach((addressMap, i) => {
results[i] = []
if (!chainId) {
return emptyState
}
const address = addressMap[chainId]
if (!proposalCounts || proposalCounts[address] === undefined) {
return emptyState
}
for (let j = 1; j <= proposalCounts[address]; j++) {
results[i].push([j])
}
return results
})
return results.filter((indexArray) => indexArray.length > 0)
}, [chainId, proposalCounts])
const addresses = useMemo(() => { const addresses = useMemo(() => {
if (!chainId) { return chainId === SupportedChainId.MAINNET ? GOVERNANCE_ADDRESSES.map((addressMap) => addressMap[chainId]) : []
return [] }, [chainId])
}
return GOVERNANCE_ADDRESSES.map((addressMap) => addressMap[chainId]).filter(
(address) => proposalCounts && proposalCounts[address] > 0
)
}, [chainId, proposalCounts])
// get metadata from past events const proposalIndexes = useMemo(() => {
const formattedEvents = useDataFromEventLogs() return chainId === SupportedChainId.MAINNET
? [
typeof proposalCount === 'number' ? new Array(proposalCount).fill(0).map((_, i) => [i + 1]) : [], // dynamic for current governor alpha
[[1], [2], [3], [4]], // hardcoded for governor alpha V0
]
: []
}, [chainId, proposalCount])
// get all proposal entities // get all proposal entities
const allProposalsCallData = useMultipleContractMultipleData( const allProposalsCallData = useMultipleContractMultipleData(
@ -178,7 +172,7 @@ export function useAllProposalData() {
GovernanceInterface, GovernanceInterface,
'proposals', 'proposals',
proposalIndexes proposalIndexes
).flat() )
// get all proposal states // get all proposal states
const allProposalStatesCallData = useMultipleContractMultipleData( const allProposalStatesCallData = useMultipleContractMultipleData(
@ -186,20 +180,36 @@ export function useAllProposalData() {
GovernanceInterface, GovernanceInterface,
'state', 'state',
proposalIndexes proposalIndexes
).flat() )
// get metadata from past events
const allFormattedEvents = useDataFromEventLogs()
// early return until events are fetched
if (!allFormattedEvents) return []
const results: ProposalData[][] = []
for (
let governanceContractIndex = 0;
governanceContractIndex < allProposalsCallData.length;
governanceContractIndex++
) {
const proposalsCallData = allProposalsCallData[governanceContractIndex]
const proposalStatesCallData = allProposalStatesCallData[governanceContractIndex]
const formattedEvents = allFormattedEvents[governanceContractIndex]
if ( if (
!allProposalsCallData?.every((p) => Boolean(p.result)) || !proposalsCallData?.every((p) => Boolean(p.result)) ||
!allProposalStatesCallData?.every((p) => Boolean(p.result)) || !proposalStatesCallData?.every((p) => Boolean(p.result)) ||
!formattedEvents?.every((p) => Boolean(p)) !formattedEvents?.every((p) => Boolean(p))
) { ) {
return [] results.push([])
continue
} }
const omittedProposalStartBlocks = [EDUCATION_FUND_1_START_BLOCK] results.push(
proposalsCallData.map((proposal, i) => {
return allProposalsCallData
.map((proposal, i) => {
let description = formattedEvents[i].description let description = formattedEvents[i].description
const startBlock = parseInt(proposal?.result?.startBlock?.toString()) const startBlock = parseInt(proposal?.result?.startBlock?.toString())
if (startBlock === UNISWAP_GRANTS_START_BLOCK) { if (startBlock === UNISWAP_GRANTS_START_BLOCK) {
@ -210,20 +220,24 @@ export function useAllProposalData() {
title: description?.split(/# |\n/g)[1] ?? 'Untitled', title: description?.split(/# |\n/g)[1] ?? 'Untitled',
description: description ?? 'No description.', description: description ?? 'No description.',
proposer: proposal?.result?.proposer, proposer: proposal?.result?.proposer,
status: allProposalStatesCallData[i]?.result?.[0] ?? ProposalState.Undetermined, status: proposalStatesCallData[i]?.result?.[0] ?? ProposalState.Undetermined,
forCount: parseFloat(ethers.utils.formatUnits(proposal?.result?.forVotes.toString(), 18)), forCount: parseFloat(ethers.utils.formatUnits(proposal?.result?.forVotes.toString(), 18)),
againstCount: parseFloat(ethers.utils.formatUnits(proposal?.result?.againstVotes.toString(), 18)), againstCount: parseFloat(ethers.utils.formatUnits(proposal?.result?.againstVotes.toString(), 18)),
startBlock, startBlock,
endBlock: parseInt(proposal?.result?.endBlock?.toString()), endBlock: parseInt(proposal?.result?.endBlock?.toString()),
details: formattedEvents[i].details, details: formattedEvents[i].details,
governorIndex: i,
} }
}) })
.filter((proposal) => !omittedProposalStartBlocks.includes(proposal.startBlock)) )
}
return results.flat()
} }
export function useProposalData(id: string): ProposalData | undefined { export function useProposalData(governorIndex: number, id: string): ProposalData | undefined {
const allProposalData = useAllProposalData() const allProposalData = useAllProposalData()
return allProposalData?.find((p) => p.id === id) return allProposalData?.filter((p) => p.governorIndex === governorIndex)?.find((p) => p.id === id)
} }
// get the users delegatee if it exists // get the users delegatee if it exists