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
// only support governance on mainnet
export const GOVERNANCE_ADDRESSES: AddressMap[] = [
{
[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 = {
[SupportedChainId.MAINNET]: '0x090D4613473dEE047c3f2706764f49E0821D256e',
}

@ -6,7 +6,9 @@ const governanceContracts = (): Record<string, string> =>
GOVERNANCE_ADDRESSES.reduce(
(acc, addressMap, i) => ({
...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 EDUCATION_FUND_1_START_BLOCK = 12620175

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

@ -87,7 +87,7 @@ export default function App() {
<Web3ReactManager>
<Switch>
<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="/uni" component={Earn} />
<Route exact strict path="/uni/:currencyIdA/:currencyIdB" component={Manage} />

@ -121,13 +121,13 @@ const ProposerAddressLink = styled(ExternalLink)`
export default function VotePage({
match: {
params: { id },
params: { governorIndex, id },
},
}: RouteComponentProps<{ id: string }>) {
}: RouteComponentProps<{ governorIndex: string; id: string }>) {
const { chainId, account } = useActiveWeb3React()
// 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
const [support, setSupport] = useState<boolean>(true)

@ -248,9 +248,9 @@ export default function Vote() {
</TYPE.subHeader>
</EmptyProposals>
)}
{allProposals?.reverse().map((p: ProposalData, i) => {
{allProposals?.reverse()?.map((p: ProposalData) => {
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>
<ProposalTitle>{p.title}</ProposalTitle>
<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 { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { GOVERNANCE_ADDRESSES } from 'constants/addresses'
import { SupportedChainId } from 'constants/chains'
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 { useGovernanceContracts, useUniContract } from 'hooks/useContract'
import { useActiveWeb3React } from 'hooks/web3'
import { useCallback, useEffect, useMemo, useState } from 'react'
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 { useMultipleContractMultipleData, useMultipleContractSingleData, useSingleCallResult } from '../multicall/hooks'
import { useMultipleContractMultipleData, useSingleCallResult } from '../multicall/hooks'
import { useTransactionAdder } from '../transactions/hooks'
interface ProposalDetail {
@ -31,6 +32,7 @@ export interface ProposalData {
startBlock: number
endBlock: number
details: ProposalDetail[]
governorIndex: number // index in the governance address array for which this proposal pertains
}
export enum ProposalState {
@ -46,63 +48,68 @@ export enum ProposalState {
}
const GovernanceInterface = new ethers.utils.Interface(GOV_ABI)
// get count of all proposals made
export function useProposalCounts(): Record<string, number> | undefined {
const { chainId } = useActiveWeb3React()
const addresses = useMemo(() => {
if (!chainId) {
return []
// get count of all proposals made in the latest governor contract
function useLatestProposalCount(): number | undefined {
const govContracts = useGovernanceContracts()
const res = useSingleCallResult(govContracts[0], 'proposalCount')
if (res?.result?.[0]) {
return (res.result[0] as BigNumber).toNumber()
}
return GOVERNANCE_ADDRESSES.map((addressMap) => addressMap[chainId])
}, [chainId])
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])
return undefined
}
/**
* Need proposal events to get description data emitted from
* new proposal event.
*/
export function useDataFromEventLogs() {
function useDataFromEventLogs():
| {
description: string
details: { target: string; functionSig: string; callData: string }[]
}[][]
| undefined {
const { library, chainId } = useActiveWeb3React()
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()
// create filter for these specific events
// create filters for ProposalCreated events
const filters = useMemo(
() =>
govContracts
? govContracts.map((contract) => ({
govContracts?.filter((govContract) => !!govContract)?.length > 0
? govContracts
.filter((govContract): govContract is ethers.Contract => !!govContract)
.map((contract) => ({
...contract.filters.ProposalCreated(),
fromBlock: 10861678,
fromBlock: 10861678, // TODO could optimize this on a per-contract basis, this is the safe value
toBlock: 'latest',
}))
: undefined,
[govContracts]
)
// clear logs on chainId change
useEffect(() => {
return () => {
setFormattedEvents(undefined)
}
}, [chainId])
useEffect(() => {
if (!filters || !library) return
let stale = false
if (!formattedEvents) {
const filterRequests = filters.map((filter) => library.getLogs(filter))
Promise.all(filterRequests)
.then((events) => events.flat())
Promise.all(filters.map((filter) => library.getLogs(filter)))
.then((governanceContractsProposalEvents) => {
if (stale) return
const formattedEventData = governanceContractsProposalEvents.map((event) => {
const formattedEventData = governanceContractsProposalEvents.map((proposalEvents) => {
return proposalEvents.map((event) => {
const eventParsed = GovernanceInterface.parseLog(event).args
return {
description: eventParsed.description,
@ -119,11 +126,17 @@ export function useDataFromEventLogs() {
}),
}
})
})
setFormattedEvents(formattedEventData)
})
.catch((error) => {
if (stale) return
console.error('Failed to fetch proposals', error)
setFormattedEvents(undefined)
})
return () => {
stale = true
}
@ -136,41 +149,22 @@ export function useDataFromEventLogs() {
}
// get data for all past and active proposals
export function useAllProposalData() {
export function useAllProposalData(): ProposalData[] {
const { chainId } = useActiveWeb3React()
const proposalCounts = useProposalCounts()
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 proposalCount = useLatestProposalCount()
const addresses = useMemo(() => {
if (!chainId) {
return []
}
return GOVERNANCE_ADDRESSES.map((addressMap) => addressMap[chainId]).filter(
(address) => proposalCounts && proposalCounts[address] > 0
)
}, [chainId, proposalCounts])
return chainId === SupportedChainId.MAINNET ? GOVERNANCE_ADDRESSES.map((addressMap) => addressMap[chainId]) : []
}, [chainId])
// get metadata from past events
const formattedEvents = useDataFromEventLogs()
const proposalIndexes = useMemo(() => {
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
const allProposalsCallData = useMultipleContractMultipleData(
@ -178,7 +172,7 @@ export function useAllProposalData() {
GovernanceInterface,
'proposals',
proposalIndexes
).flat()
)
// get all proposal states
const allProposalStatesCallData = useMultipleContractMultipleData(
@ -186,20 +180,36 @@ export function useAllProposalData() {
GovernanceInterface,
'state',
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 (
!allProposalsCallData?.every((p) => Boolean(p.result)) ||
!allProposalStatesCallData?.every((p) => Boolean(p.result)) ||
!proposalsCallData?.every((p) => Boolean(p.result)) ||
!proposalStatesCallData?.every((p) => Boolean(p.result)) ||
!formattedEvents?.every((p) => Boolean(p))
) {
return []
results.push([])
continue
}
const omittedProposalStartBlocks = [EDUCATION_FUND_1_START_BLOCK]
return allProposalsCallData
.map((proposal, i) => {
results.push(
proposalsCallData.map((proposal, i) => {
let description = formattedEvents[i].description
const startBlock = parseInt(proposal?.result?.startBlock?.toString())
if (startBlock === UNISWAP_GRANTS_START_BLOCK) {
@ -210,20 +220,24 @@ export function useAllProposalData() {
title: description?.split(/# |\n/g)[1] ?? 'Untitled',
description: description ?? 'No description.',
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)),
againstCount: parseFloat(ethers.utils.formatUnits(proposal?.result?.againstVotes.toString(), 18)),
startBlock,
endBlock: parseInt(proposal?.result?.endBlock?.toString()),
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()
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