diff --git a/src/constants/addresses.ts b/src/constants/addresses.ts index ca88ca8181..d1c118049a 100644 --- a/src/constants/addresses.ts +++ b/src/constants/addresses.ts @@ -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', } diff --git a/src/constants/governance.ts b/src/constants/governance.ts index 7aa0f4298c..e7f3b80637 100644 --- a/src/constants/governance.ts +++ b/src/constants/governance.ts @@ -6,7 +6,9 @@ const governanceContracts = (): Record => 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})` + }`, }), {} ) diff --git a/src/constants/proposals/index.ts b/src/constants/proposals/index.ts index 7b4d73c057..3a1ad3cafd 100644 --- a/src/constants/proposals/index.ts +++ b/src/constants/proposals/index.ts @@ -1,2 +1 @@ export const UNISWAP_GRANTS_START_BLOCK = 11473815 -export const EDUCATION_FUND_1_START_BLOCK = 12620175 diff --git a/src/hooks/useContract.ts b/src/hooks/useContract.ts index b855b227f5..ddbbdbb8f0 100644 --- a/src/hooks/useContract.ts +++ b/src/hooks/useContract.ts @@ -46,34 +46,19 @@ import { useActiveWeb3React } from './web3' // returns null on errors export function useContract( - 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 - } - try { - return GOVERNANCE_ADDRESSES.filter((addressMap) => Boolean(addressMap[chainId])).map((addressMap) => - getContract(addressMap[chainId], GOVERNANCE_ABI, library, account ? account : undefined) - ) - } catch (error) { - console.error('Failed to get contract', error) - return null + return [] } + + return GOVERNANCE_ADDRESSES.filter((addressMap) => Boolean(addressMap[chainId])).map((addressMap) => { + try { + return getContract(addressMap[chainId], GOVERNANCE_ABI, library, account ? account : undefined) + } catch (error) { + console.error('Failed to get contract', error) + return null + } + }) }, [library, chainId, account]) } diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 95d42533dc..dfc84a0502 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -87,7 +87,7 @@ export default function App() { - + diff --git a/src/pages/Vote/VotePage.tsx b/src/pages/Vote/VotePage.tsx index 744c9b8e03..20e76bee36 100644 --- a/src/pages/Vote/VotePage.tsx +++ b/src/pages/Vote/VotePage.tsx @@ -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(true) diff --git a/src/pages/Vote/index.tsx b/src/pages/Vote/index.tsx index 02dcebbec6..e537f2b171 100644 --- a/src/pages/Vote/index.tsx +++ b/src/pages/Vote/index.tsx @@ -248,9 +248,9 @@ export default function Vote() { )} - {allProposals?.reverse().map((p: ProposalData, i) => { + {allProposals?.reverse()?.map((p: ProposalData) => { return ( - + {p.id} {p.title} {ProposalState[p.status]} diff --git a/src/state/governance/hooks.ts b/src/state/governance/hooks.ts index b3d27e2078..a453152d65 100644 --- a/src/state/governance/hooks.ts +++ b/src/state/governance/hooks.ts @@ -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,84 +48,95 @@ export enum ProposalState { } const GovernanceInterface = new ethers.utils.Interface(GOV_ABI) -// get count of all proposals made -export function useProposalCounts(): Record | undefined { - const { chainId } = useActiveWeb3React() - const addresses = useMemo(() => { - if (!chainId) { - return [] - } - 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]) +// 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 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) => ({ - ...contract.filters.ProposalCreated(), - fromBlock: 10861678, - toBlock: 'latest', - })) + govContracts?.filter((govContract) => !!govContract)?.length > 0 + ? govContracts + .filter((govContract): govContract is ethers.Contract => !!govContract) + .map((contract) => ({ + ...contract.filters.ProposalCreated(), + 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 eventParsed = GovernanceInterface.parseLog(event).args - return { - description: eventParsed.description, - details: eventParsed.targets.map((target: string, i: number) => { - const signature = eventParsed.signatures[i] - const [name, types] = signature.substr(0, signature.length - 1).split('(') - const calldata = eventParsed.calldatas[i] - const decoded = utils.defaultAbiCoder.decode(types.split(','), calldata) - return { - target, - functionSig: name, - callData: decoded.join(', '), - } - }), - } + + const formattedEventData = governanceContractsProposalEvents.map((proposalEvents) => { + return proposalEvents.map((event) => { + const eventParsed = GovernanceInterface.parseLog(event).args + return { + description: eventParsed.description, + details: eventParsed.targets.map((target: string, i: number) => { + const signature = eventParsed.signatures[i] + const [name, types] = signature.substr(0, signature.length - 1).split('(') + const calldata = eventParsed.calldatas[i] + const decoded = utils.defaultAbiCoder.decode(types.split(','), calldata) + return { + target, + functionSig: name, + callData: decoded.join(', '), + } + }), + } + }) }) + 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,44 +180,64 @@ export function useAllProposalData() { GovernanceInterface, 'state', proposalIndexes - ).flat() + ) - if ( - !allProposalsCallData?.every((p) => Boolean(p.result)) || - !allProposalStatesCallData?.every((p) => Boolean(p.result)) || - !formattedEvents?.every((p) => Boolean(p)) + // 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++ ) { - return [] + const proposalsCallData = allProposalsCallData[governanceContractIndex] + const proposalStatesCallData = allProposalStatesCallData[governanceContractIndex] + const formattedEvents = allFormattedEvents[governanceContractIndex] + + if ( + !proposalsCallData?.every((p) => Boolean(p.result)) || + !proposalStatesCallData?.every((p) => Boolean(p.result)) || + !formattedEvents?.every((p) => Boolean(p)) + ) { + results.push([]) + continue + } + + results.push( + proposalsCallData.map((proposal, i) => { + let description = formattedEvents[i].description + const startBlock = parseInt(proposal?.result?.startBlock?.toString()) + if (startBlock === UNISWAP_GRANTS_START_BLOCK) { + description = UNISWAP_GRANTS_PROPOSAL_DESCRIPTION + } + return { + id: proposal?.result?.id.toString(), + title: description?.split(/# |\n/g)[1] ?? 'Untitled', + description: description ?? 'No description.', + proposer: proposal?.result?.proposer, + 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, + } + }) + ) } - const omittedProposalStartBlocks = [EDUCATION_FUND_1_START_BLOCK] - - return allProposalsCallData - .map((proposal, i) => { - let description = formattedEvents[i].description - const startBlock = parseInt(proposal?.result?.startBlock?.toString()) - if (startBlock === UNISWAP_GRANTS_START_BLOCK) { - description = UNISWAP_GRANTS_PROPOSAL_DESCRIPTION - } - return { - id: proposal?.result?.id.toString(), - title: description?.split(/# |\n/g)[1] ?? 'Untitled', - description: description ?? 'No description.', - proposer: proposal?.result?.proposer, - status: allProposalStatesCallData[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, - } - }) - .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