Commit db5a1438 authored by Noah Zinsmeister's avatar Noah Zinsmeister Committed by GitHub

perf: constrain log-fetching block ranges (#3846)

* first pass

* don't re-fetch historical logs

* hide cancelled proposals by default
parent 5dc7d366
import {
GOVERNANCE_ALPHA_V0_ADDRESSES,
GOVERNANCE_ALPHA_V1_ADDRESSES,
GOVERNANCE_BRAVO_ADDRESSES,
TIMELOCK_ADDRESS,
UNI_ADDRESS,
} from './addresses'
......@@ -11,7 +12,8 @@ export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }
[UNI_ADDRESS[SupportedChainId.MAINNET]]: 'UNI',
[TIMELOCK_ADDRESS[SupportedChainId.MAINNET]]: 'Timelock',
[GOVERNANCE_ALPHA_V0_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance (V0)',
[GOVERNANCE_ALPHA_V1_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance',
[GOVERNANCE_ALPHA_V1_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance (V1)',
[GOVERNANCE_BRAVO_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance',
'0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e': 'ENS Registry',
'0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41': 'ENS Public Resolver',
},
......
......@@ -7,16 +7,18 @@ import FormattedCurrencyAmount from 'components/FormattedCurrencyAmount'
import Loader from 'components/Loader'
import { AutoRow, RowBetween, RowFixed } from 'components/Row'
import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
import Toggle from 'components/Toggle'
import DelegateModal from 'components/vote/DelegateModal'
import ProposalEmptyState from 'components/vote/ProposalEmptyState'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import JSBI from 'jsbi'
import { darken } from 'polished'
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { Button } from 'rebass/styled-components'
import { useModalOpen, useToggleDelegateModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import { ProposalData } from 'state/governance/hooks'
import { ProposalData, ProposalState } from 'state/governance/hooks'
import { useAllProposalData, useUserDelegatee, useUserVotes } from 'state/governance/hooks'
import { useTokenBalance } from 'state/wallet/hooks'
import styled from 'styled-components/macro'
......@@ -107,6 +109,8 @@ const StyledExternalLink = styled(ExternalLink)`
export default function Landing() {
const { account, chainId } = useActiveWeb3React()
const [hideCancelled, setHideCancelled] = useState(true)
// toggle for showing delegation modal
const showDelegateModal = useModalOpen(ApplicationModal.DELEGATE)
const toggleDelegateModal = useToggleDelegateModal()
......@@ -237,10 +241,24 @@ export default function Landing() {
)}
</RowBetween>
)}
{allProposals?.length === 0 && <ProposalEmptyState />}
{allProposals?.length > 0 && (
<AutoColumn gap="md">
<RowBetween>
<ThemedText.Main>
<Trans>Show Cancelled</Trans>
</ThemedText.Main>
<Toggle isActive={!hideCancelled} toggle={() => setHideCancelled((hideCancelled) => !hideCancelled)} />
</RowBetween>
</AutoColumn>
)}
{allProposals
?.slice(0)
?.reverse()
?.filter((p: ProposalData) => (hideCancelled ? p.status !== ProposalState.CANCELED : true))
?.map((p: ProposalData) => {
return (
<Proposal as={Link} to={`/vote/${p.governorIndex}/${p.id}`} key={`${p.governorIndex}${p.id}`}>
......@@ -253,6 +271,7 @@ export default function Landing() {
)
})}
</TopSection>
<ThemedText.SubHeader color="text3">
<Trans>A minimum threshold of 0.25% of the total UNI supply is required to submit proposals</Trans>
</ThemedText.SubHeader>
......
......@@ -123,10 +123,20 @@ const FOUR_BYTES_DIR: { [sig: string]: string } = {
*/
function useFormattedProposalCreatedLogs(
contract: Contract | null,
indices: number[][]
indices: number[][],
fromBlock?: number,
toBlock?: number
): FormattedProposalLog[] | undefined {
// create filters for ProposalCreated events
const filter = useMemo(() => contract?.filters?.ProposalCreated(), [contract])
const filter = useMemo(() => {
const filter = contract?.filters?.ProposalCreated()
if (!filter) return undefined
return {
...filter,
fromBlock,
toBlock,
}
}, [contract, fromBlock, toBlock])
const useLogsResult = useLogs(filter)
......@@ -244,9 +254,9 @@ export function useAllProposalData(): { data: ProposalData[]; loading: boolean }
const proposalStatesV2 = useSingleContractMultipleData(gov2, 'state', gov2ProposalIndexes)
// get metadata from past events
const formattedLogsV0 = useFormattedProposalCreatedLogs(gov0, gov0ProposalIndexes)
const formattedLogsV1 = useFormattedProposalCreatedLogs(gov1, gov1ProposalIndexes)
const formattedLogsV2 = useFormattedProposalCreatedLogs(gov2, gov2ProposalIndexes)
const formattedLogsV0 = useFormattedProposalCreatedLogs(gov0, gov0ProposalIndexes, 11042287, 12563484)
const formattedLogsV1 = useFormattedProposalCreatedLogs(gov1, gov1ProposalIndexes, 12686656, 13059343)
const formattedLogsV2 = useFormattedProposalCreatedLogs(gov2, gov2ProposalIndexes, 13538153)
const uni = useMemo(() => (chainId ? UNI[chainId] : undefined), [chainId])
......
import { Filter } from '@ethersproject/providers'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import { useEffect, useMemo } from 'react'
import { useAppDispatch, useAppSelector } from '../hooks'
import { addListener, removeListener } from './slice'
import { EventFilter, filterToKey, Log } from './utils'
import { filterToKey, isHistoricalLog, Log } from './utils'
export enum LogsState {
// The filter is invalid
......@@ -26,10 +27,10 @@ export interface UseLogsResult {
/**
* Returns the logs for the given filter as of the latest block, re-fetching from the library every block.
* @param filter The logs filter, without `blockHash`, `fromBlock` or `toBlock` defined.
* @param filter The logs filter, with `fromBlock` or `toBlock` optionally specified.
* The filter parameter should _always_ be memoized, or else will trigger constant refetching
*/
export function useLogs(filter: EventFilter | undefined): UseLogsResult {
export function useLogs(filter: Filter | undefined): UseLogsResult {
const { chainId } = useActiveWeb3React()
const blockNumber = useBlockNumber()
......@@ -45,17 +46,16 @@ export function useLogs(filter: EventFilter | undefined): UseLogsResult {
}
}, [chainId, dispatch, filter])
const filterKey = useMemo(() => (filter ? filterToKey(filter) : undefined), [filter])
return useMemo(() => {
if (!chainId || !filterKey || !blockNumber)
if (!chainId || !filter || !blockNumber)
return {
logs: undefined,
state: LogsState.INVALID,
}
const state = logs[chainId]?.[filterKey]
const state = logs[chainId]?.[filterToKey(filter)]
const result = state?.results
if (!result) {
return {
state: LogsState.LOADING,
......@@ -71,8 +71,13 @@ export function useLogs(filter: EventFilter | undefined): UseLogsResult {
}
return {
state: result.blockNumber >= blockNumber ? LogsState.SYNCED : LogsState.SYNCING,
// if we're only fetching logs until a block that has already elapsed, we're synced regardless of result.blockNumber
state: isHistoricalLog(filter, blockNumber)
? LogsState.SYNCED
: result.blockNumber >= blockNumber
? LogsState.SYNCED
: LogsState.SYNCING,
logs: result.logs,
}
}, [blockNumber, chainId, filterKey, logs])
}, [blockNumber, chainId, filter, logs])
}
import { Filter } from '@ethersproject/providers'
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { EventFilter, filterToKey, Log } from './utils'
import { filterToKey, Log } from './utils'
export interface LogsState {
[chainId: number]: {
......@@ -26,7 +27,7 @@ const slice = createSlice({
name: 'logs',
initialState: {} as LogsState,
reducers: {
addListener(state, { payload: { chainId, filter } }: PayloadAction<{ chainId: number; filter: EventFilter }>) {
addListener(state, { payload: { chainId, filter } }: PayloadAction<{ chainId: number; filter: Filter }>) {
if (!state[chainId]) state[chainId] = {}
const key = filterToKey(filter)
if (!state[chainId][key])
......@@ -39,7 +40,7 @@ const slice = createSlice({
state,
{
payload: { chainId, filters, blockNumber },
}: PayloadAction<{ chainId: number; filters: EventFilter[]; blockNumber: number }>
}: PayloadAction<{ chainId: number; filters: Filter[]; blockNumber: number }>
) {
if (!state[chainId]) return
for (const filter of filters) {
......@@ -52,7 +53,7 @@ const slice = createSlice({
state,
{
payload: { chainId, filter, results },
}: PayloadAction<{ chainId: number; filter: EventFilter; results: { blockNumber: number; logs: Log[] } }>
}: PayloadAction<{ chainId: number; filter: Filter; results: { blockNumber: number; logs: Log[] } }>
) {
if (!state[chainId]) return
const key = filterToKey(filter)
......@@ -64,7 +65,7 @@ const slice = createSlice({
state,
{
payload: { chainId, filter, blockNumber },
}: PayloadAction<{ chainId: number; blockNumber: number; filter: EventFilter }>
}: PayloadAction<{ chainId: number; blockNumber: number; filter: Filter }>
) {
if (!state[chainId]) return
const key = filterToKey(filter)
......@@ -75,7 +76,7 @@ const slice = createSlice({
error: true,
}
},
removeListener(state, { payload: { chainId, filter } }: PayloadAction<{ chainId: number; filter: EventFilter }>) {
removeListener(state, { payload: { chainId, filter } }: PayloadAction<{ chainId: number; filter: Filter }>) {
if (!state[chainId]) return
const key = filterToKey(filter)
if (!state[chainId][key]) return
......
import { Filter } from '@ethersproject/providers'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import { useEffect, useMemo } from 'react'
import { useAppDispatch, useAppSelector } from '../hooks'
import { fetchedLogs, fetchedLogsError, fetchingLogs } from './slice'
import { EventFilter, keyToFilter } from './utils'
import { isHistoricalLog, keyToFilter } from './utils'
export default function Updater(): null {
const dispatch = useAppDispatch()
......@@ -13,7 +14,7 @@ export default function Updater(): null {
const blockNumber = useBlockNumber()
const filtersNeedFetch: EventFilter[] = useMemo(() => {
const filtersNeedFetch: Filter[] = useMemo(() => {
if (!chainId || typeof blockNumber !== 'number') return []
const active = state[chainId]
......@@ -25,6 +26,8 @@ export default function Updater(): null {
if (listeners === 0) return false
if (typeof fetchingBlockNumber === 'number' && fetchingBlockNumber >= blockNumber) return false
if (results && typeof results.blockNumber === 'number' && results.blockNumber >= blockNumber) return false
// this condition ensures that if a log is historical, and it's already fetched, we don't re-fetch it
if (isHistoricalLog(keyToFilter(key), blockNumber) && results?.logs !== undefined) return false
return true
})
.map((key) => keyToFilter(key))
......@@ -35,11 +38,16 @@ export default function Updater(): null {
dispatch(fetchingLogs({ chainId, filters: filtersNeedFetch, blockNumber }))
filtersNeedFetch.forEach((filter) => {
// provide defaults if {from,to}Block are missing
let fromBlock = filter.fromBlock ?? 0
let toBlock = filter.toBlock ?? blockNumber
if (typeof fromBlock === 'string') fromBlock = Number.parseInt(fromBlock)
if (typeof toBlock === 'string') toBlock = Number.parseInt(toBlock)
library
.getLogs({
...filter,
fromBlock: 0,
toBlock: blockNumber,
fromBlock,
toBlock,
})
.then((logs) => {
dispatch(
......
export interface EventFilter {
address?: string
topics?: Array<string | Array<string> | null>
}
import { Filter } from '@ethersproject/providers'
export interface Log {
topics: Array<string>
......@@ -15,17 +12,17 @@ export interface Log {
* Converts a filter to the corresponding string key
* @param filter the filter to convert
*/
export function filterToKey(filter: EventFilter): string {
export function filterToKey(filter: Filter): string {
return `${filter.address ?? ''}:${
filter.topics?.map((topic) => (topic ? (Array.isArray(topic) ? topic.join(';') : topic) : '\0'))?.join('-') ?? ''
}`
}:${filter.fromBlock ?? ''}:${filter.toBlock ?? ''}`
}
/**
* Convert a filter key to the corresponding filter
* @param key key to convert
*/
export function keyToFilter(key: string): EventFilter {
export function keyToFilter(key: string): Filter {
const pcs = key.split(':')
const address = pcs[0]
const topics = pcs[1].split('-').map((topic) => {
......@@ -34,9 +31,26 @@ export function keyToFilter(key: string): EventFilter {
if (parts.length === 1) return parts[0]
return parts
})
const fromBlock = pcs[2]
const toBlock = pcs[3]
return {
address: address.length === 0 ? undefined : address,
topics,
fromBlock: fromBlock.length === 0 ? undefined : fromBlock,
toBlock: toBlock.length === 0 ? undefined : toBlock,
}
}
/**
* Determines whether a filter is for a historical log that doesn't need to be re-fetched.
* @param filter The filter to check.
* @param blockNumber The current block number.
*/
export function isHistoricalLog(filter: Filter, blockNumber: number): boolean {
if (!filter.toBlock) return false
let toBlock = filter.toBlock
if (typeof toBlock === 'string') toBlock = Number.parseInt(toBlock)
return toBlock <= blockNumber
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment