refactor: logs hook (#1941)

* feat(logs): add an infrastructure for fetching logs in a declarative way

* use the logs hook in the vote page, first pass

* fix comment

* bit of cleanup

* unused imports

* improve loading indicator on vote page

* some testnet behavior improvements

* fix loader state

* loading state nit

* show correct indexes

* remove the unnecessary retry code

* first pass at the slice

* no throws

* loading indicator should go away if not connected

* use the logs slice for the logs hook state

* style changes per cal's request
This commit is contained in:
Moody Salem 2021-07-01 14:44:02 -05:00 committed by GitHub
parent abfd87c517
commit 27f756107e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 517 additions and 395 deletions

@ -13,7 +13,6 @@ import Circle from '../../assets/images/blue-loader.svg'
import { useVoteCallback, useUserVotes } from '../../state/governance/hooks'
import { ExternalLink } from '../../theme/components'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { Trans } from '@lingui/macro'
const ContentWrapper = styled(AutoColumn)`
@ -50,7 +49,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, support }: Vo
}: {
voteCallback: (proposalId: string | undefined, support: boolean) => Promise<string> | undefined
} = useVoteCallback()
const availableVotes: CurrencyAmount<Token> | undefined = useUserVotes()
const { votes: availableVotes } = useUserVotes()
// monitor call to help UI loading state
const [hash, setHash] = useState<string | undefined>()

@ -14,16 +14,19 @@ export const MULTICALL2_ADDRESSES: AddressMap = {
export const V2_FACTORY_ADDRESSES: AddressMap = constructSameAddressMap(V2_FACTORY_ADDRESS)
export const V2_ROUTER_ADDRESS: AddressMap = constructSameAddressMap('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D')
// most current governance contract address should always be the 0 index
// only support governance on mainnet
export const GOVERNANCE_ADDRESSES: AddressMap[] = [
{
/**
* The older V0 governance account
*/
export const GOVERNANCE_ALPHA_V0_ADDRESSES: AddressMap = constructSameAddressMap(
'0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F'
)
/**
* The latest governor alpha that is currently admin of timelock
*/
export const GOVERNANCE_ALPHA_V1_ADDRESSES: AddressMap = {
[SupportedChainId.MAINNET]: '0xC4e172459f1E7939D522503B81AFAaC1014CE6F6',
},
{
[SupportedChainId.MAINNET]: '0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F',
},
]
}
export const TIMELOCK_ADDRESS: AddressMap = constructSameAddressMap('0x1a9C8182C09F50C8318d769245beA52c32BE35BC')
export const MERKLE_DISTRIBUTOR_ADDRESS: AddressMap = {

@ -1,23 +1,17 @@
import { GOVERNANCE_ADDRESSES, TIMELOCK_ADDRESS, UNI_ADDRESS } from './addresses'
import {
GOVERNANCE_ALPHA_V0_ADDRESSES,
GOVERNANCE_ALPHA_V1_ADDRESSES,
TIMELOCK_ADDRESS,
UNI_ADDRESS,
} from './addresses'
import { SupportedChainId } from './chains'
// returns { [address]: `Governance (V${n})`} for each address in GOVERNANCE_ADDRESSES except the current, which gets no version indicator
const governanceContracts = (): Record<string, string> =>
GOVERNANCE_ADDRESSES.reduce(
(acc, addressMap, i) => ({
...acc,
[addressMap[SupportedChainId.MAINNET]]: `Governance${
i === 0 ? '' : ` (V${GOVERNANCE_ADDRESSES.length - 1 - i})`
}`,
}),
{}
)
export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }> = {
[SupportedChainId.MAINNET]: {
[UNI_ADDRESS[SupportedChainId.MAINNET]]: 'UNI',
[TIMELOCK_ADDRESS[SupportedChainId.MAINNET]]: 'Timelock',
...governanceContracts(),
[GOVERNANCE_ALPHA_V0_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance (V0)',
[GOVERNANCE_ALPHA_V1_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance',
},
}

@ -27,12 +27,13 @@ import {
V3_CORE_FACTORY_ADDRESSES,
V3_MIGRATOR_ADDRESSES,
ARGENT_WALLET_DETECTOR_ADDRESS,
GOVERNANCE_ADDRESSES,
MERKLE_DISTRIBUTOR_ADDRESS,
MULTICALL2_ADDRESSES,
V2_ROUTER_ADDRESS,
ENS_REGISTRAR_ADDRESSES,
SOCKS_CONTROLLER_ADDRESSES,
GOVERNANCE_ALPHA_V0_ADDRESSES,
GOVERNANCE_ALPHA_V1_ADDRESSES,
} from 'constants/addresses'
import { abi as NFTPositionManagerABI } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
import { useMemo } from 'react'
@ -116,25 +117,16 @@ export function useMerkleDistributorContract() {
return useContract(MERKLE_DISTRIBUTOR_ADDRESS, MERKLE_DISTRIBUTOR_ABI, true)
}
export function useGovernanceContracts(): (Contract | null)[] {
const { library, account, chainId } = useActiveWeb3React()
return useMemo(() => {
if (!library || !chainId) {
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])
export function useGovernanceV0Contract(): Contract | null {
return useContract(GOVERNANCE_ALPHA_V0_ADDRESSES, GOVERNANCE_ABI, true)
}
export function useGovernanceV1Contract(): Contract | null {
return useContract(GOVERNANCE_ALPHA_V1_ADDRESSES, GOVERNANCE_ABI, true)
}
export const useLatestGovernanceContract = useGovernanceV1Contract
export function useUniContract() {
const { chainId } = useActiveWeb3React()
return useContract(chainId ? UNI[chainId]?.address : undefined, UNI_ABI, true)

@ -16,6 +16,7 @@ import * as serviceWorkerRegistration from './serviceWorkerRegistration'
import ApplicationUpdater from './state/application/updater'
import ListsUpdater from './state/lists/updater'
import MulticallUpdater from './state/multicall/updater'
import LogsUpdater from './state/logs/updater'
import TransactionUpdater from './state/transactions/updater'
import UserUpdater from './state/user/updater'
import ThemeProvider, { ThemedGlobalStyle } from './theme'
@ -57,6 +58,7 @@ function Updaters() {
<ApplicationUpdater />
<TransactionUpdater />
<MulticallUpdater />
<LogsUpdater />
</>
)
}

@ -86,9 +86,9 @@ const AutonomousProposalCTA = styled.div`
export default function CreateProposal() {
const { account, chainId } = useActiveWeb3React()
const latestProposalId = useLatestProposalId(account ?? '0x0000000000000000000000000000000000000000') ?? '0'
const latestProposalId = useLatestProposalId(account ?? undefined) ?? '0'
const latestProposalData = useProposalData(0, latestProposalId)
const availableVotes: CurrencyAmount<Token> | undefined = useUserVotes()
const { votes: availableVotes } = useUserVotes()
const proposalThreshold: CurrencyAmount<Token> | undefined = useProposalThreshold()
const [modalOpen, setModalOpen] = useState(false)
@ -261,7 +261,7 @@ ${bodyValue}
<CreateProposalButton
proposalThreshold={proposalThreshold}
hasActiveOrPendingProposal={
latestProposalData?.status === ProposalState.Active || latestProposalData?.status === ProposalState.Pending
latestProposalData?.status === ProposalState.ACTIVE || latestProposalData?.status === ProposalState.PENDING
}
hasEnoughVote={hasEnoughVote}
isFormInvalid={isFormInvalid}

@ -46,13 +46,14 @@ const PageWrapper = styled(AutoColumn)`
`
const ProposalInfo = styled(AutoColumn)`
border: 1px solid ${({ theme }) => theme.bg4};
background: ${({ theme }) => theme.bg0};
border-radius: 12px;
padding: 1.5rem;
position: relative;
max-width: 640px;
width: 100%;
`
const ArrowWrapper = styled(StyledInternalLink)`
display: flex;
align-items: center;
@ -174,7 +175,7 @@ export default function VotePage({
availableVotes &&
JSBI.greaterThan(availableVotes.quotient, JSBI.BigInt(0)) &&
proposalData &&
proposalData.status === ProposalState.Active
proposalData.status === ProposalState.ACTIVE
const uniBalance: CurrencyAmount<Token> | undefined = useTokenBalance(
account ?? undefined,
@ -211,9 +212,7 @@ export default function VotePage({
<ArrowLeft size={20} /> All Proposals
</Trans>
</ArrowWrapper>
{proposalData && (
<ProposalStatus status={proposalData.status}>{ProposalState[proposalData.status]}</ProposalStatus>
)}
{proposalData && <ProposalStatus status={proposalData.status} />}
</RowBetween>
<AutoColumn gap="10px" style={{ width: '100%' }}>
<TYPE.largeHeader style={{ marginBottom: '.5rem' }}>{proposalData?.title}</TYPE.largeHeader>
@ -228,7 +227,7 @@ export default function VotePage({
)}
</TYPE.main>
</RowBetween>
{proposalData && proposalData.status === ProposalState.Active && !showVotingButtons && (
{proposalData && proposalData.status === ProposalState.ACTIVE && !showVotingButtons && (
<GreyCard>
<TYPE.black>
<Trans>

@ -11,13 +11,7 @@ import { ButtonPrimary } from '../../components/Button'
import { Button } from 'rebass/styled-components'
import { darken } from 'polished'
import { CardBGImage, CardNoise, CardSection, DataCard } from '../../components/earn/styled'
import {
ProposalData,
ProposalState,
useAllProposalData,
useUserDelegatee,
useUserVotes,
} from '../../state/governance/hooks'
import { ProposalData, useAllProposalData, useUserDelegatee, useUserVotes } from '../../state/governance/hooks'
import DelegateModal from '../../components/vote/DelegateModal'
import { useTokenBalance } from '../../state/wallet/hooks'
import { useActiveWeb3React } from '../../hooks/web3'
@ -119,10 +113,10 @@ export default function Vote() {
const toggleDelegateModal = useToggleDelegateModal()
// get data to list all proposals
const allProposals: ProposalData[] = useAllProposalData()
const { data: allProposals, loading: loadingProposals } = useAllProposalData()
// user data
const availableVotes: CurrencyAmount<Token> | undefined = useUserVotes()
const { loading: loadingAvailableVotes, votes: availableVotes } = useUserVotes()
const uniBalance: CurrencyAmount<Token> | undefined = useTokenBalance(
account ?? undefined,
chainId ? UNI[chainId] : undefined
@ -134,8 +128,6 @@ export default function Vote() {
uniBalance && JSBI.notEqual(uniBalance.quotient, JSBI.BigInt(0)) && userDelegatee === ZERO_ADDRESS
)
const maxGovernorIndex = allProposals.reduce((max, p) => Math.max(p.governorIndex, max), 0)
return (
<>
<PageWrapper gap="lg" justify="center">
@ -184,7 +176,7 @@ export default function Vote() {
<Trans>Proposals</Trans>
</TYPE.mediumHeader>
<AutoRow gap="6px" justify="flex-end">
{(!allProposals || allProposals.length === 0) && !availableVotes && <Loader />}
{loadingProposals || loadingAvailableVotes ? <Loader /> : null}
{showUnlockVoting ? (
<ButtonPrimary
style={{ width: 'fit-content' }}
@ -263,10 +255,10 @@ export default function Vote() {
return (
<Proposal as={Link} to={`/vote/${p.governorIndex}/${p.id}`} key={`${p.governorIndex}${p.id}`}>
<ProposalNumber>
{maxGovernorIndex - p.governorIndex}.{p.id}
{p.governorIndex}.{p.id}
</ProposalNumber>
<ProposalTitle>{p.title}</ProposalTitle>
<ProposalStatus status={p.status}>{ProposalState[p.status]}</ProposalStatus>
<ProposalStatus status={p.status} />
</Proposal>
)
})}

@ -1,25 +1,49 @@
import { Trans } from '@lingui/macro'
import styled, { DefaultTheme } from 'styled-components'
import { ProposalState } from '../../state/governance/hooks'
const handleColorType = (status: ProposalState, theme: DefaultTheme) => {
switch (status) {
case ProposalState.Pending:
case ProposalState.Active:
case ProposalState.PENDING:
case ProposalState.ACTIVE:
return theme.blue1
case ProposalState.Succeeded:
case ProposalState.Executed:
case ProposalState.SUCCEEDED:
case ProposalState.EXECUTED:
return theme.green1
case ProposalState.Defeated:
case ProposalState.DEFEATED:
return theme.red1
case ProposalState.Queued:
case ProposalState.Canceled:
case ProposalState.Expired:
case ProposalState.QUEUED:
case ProposalState.CANCELED:
case ProposalState.EXPIRED:
default:
return theme.text3
}
}
export const ProposalStatus = styled.span<{ status: ProposalState }>`
function StatusText({ status }: { status: ProposalState }) {
switch (status) {
case ProposalState.PENDING:
return <Trans>Pending</Trans>
case ProposalState.ACTIVE:
return <Trans>Active</Trans>
case ProposalState.SUCCEEDED:
return <Trans>Succeeded</Trans>
case ProposalState.EXECUTED:
return <Trans>Executed</Trans>
case ProposalState.DEFEATED:
return <Trans>Defeated</Trans>
case ProposalState.QUEUED:
return <Trans>Queued</Trans>
case ProposalState.CANCELED:
return <Trans>Canceled</Trans>
case ProposalState.EXPIRED:
return <Trans>Expired</Trans>
default:
return <Trans>Undetermined</Trans>
}
}
const StyledProposalContainer = styled.span<{ status: ProposalState }>`
font-size: 0.825rem;
font-weight: 600;
padding: 0.5rem;
@ -30,3 +54,11 @@ export const ProposalStatus = styled.span<{ status: ProposalState }>`
justify-self: flex-end;
text-transform: uppercase;
`
export function ProposalStatus({ status }: { status: ProposalState }) {
return (
<StyledProposalContainer status={status}>
<StatusText status={status} />
</StyledProposalContainer>
)
}

@ -1,20 +1,25 @@
import { TransactionResponse } from '@ethersproject/providers'
import { t } from '@lingui/macro'
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 { BigNumber, ethers, utils } from 'ethers'
import { isAddress } from 'ethers/lib/utils'
import { useGovernanceContracts, useUniContract } from 'hooks/useContract'
import { Contract } from 'ethers'
import { defaultAbiCoder, formatUnits, Interface, isAddress } from 'ethers/lib/utils'
import {
useGovernanceV0Contract,
useGovernanceV1Contract,
useLatestGovernanceContract,
useUniContract,
} from 'hooks/useContract'
import { useActiveWeb3React } from 'hooks/web3'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useMemo } from 'react'
import { calculateGasMargin } from 'utils/calculateGasMargin'
import { SupportedChainId } from '../../constants/chains'
import { UNISWAP_GRANTS_START_BLOCK } from '../../constants/proposals'
import { UNI } from '../../constants/tokens'
import { useMultipleContractMultipleData, useSingleCallResult } from '../multicall/hooks'
import { useLogs } from '../logs/hooks'
import { useSingleCallResult, useSingleContractMultipleData } from '../multicall/hooks'
import { useTransactionAdder } from '../transactions/hooks'
import { t } from '@lingui/macro'
interface ProposalDetail {
target: string
@ -45,88 +50,50 @@ export interface CreateProposalData {
}
export enum ProposalState {
Undetermined = -1,
Pending,
Active,
Canceled,
Defeated,
Succeeded,
Queued,
Expired,
Executed,
UNDETERMINED = -1,
PENDING,
ACTIVE,
CANCELED,
DEFEATED,
SUCCEEDED,
QUEUED,
EXPIRED,
EXECUTED,
}
const GovernanceInterface = new ethers.utils.Interface(GOV_ABI)
const GovernanceInterface = new Interface(GOV_ABI)
// get count of all proposals made in the latest governor contract
function useLatestProposalCount(): number | undefined {
const govContracts = useGovernanceContracts()
function useProposalCount(contract: Contract | null): number | undefined {
const { result } = useSingleCallResult(contract, 'proposalCount')
const res = useSingleCallResult(govContracts[0], 'proposalCount')
if (res?.result?.[0]) {
return (res.result[0] as BigNumber).toNumber()
}
return undefined
return result?.[0]?.toNumber()
}
interface FormattedProposalLog {
description: string
details: { target: string; functionSig: string; callData: string }[]
}
/**
* Need proposal events to get description data emitted from
* new proposal event.
*/
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 }[] }[][]>()
const govContracts = useGovernanceContracts()
function useFormattedProposalCreatedLogs(contract: Contract | null): FormattedProposalLog[] | undefined {
// create filters for ProposalCreated events
const filters = useMemo(
() =>
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]
)
const filter = useMemo(() => contract?.filters?.ProposalCreated(), [contract])
// clear logs on chainId change
useEffect(() => {
return () => {
setFormattedEvents(undefined)
}
}, [chainId])
const useLogsResult = useLogs(filter)
useEffect(() => {
if (!filters || !library) return
let stale = false
if (!formattedEvents) {
Promise.all(filters.map((filter) => library.getLogs(filter)))
.then((governanceContractsProposalEvents) => {
if (stale) return
const formattedEventData = governanceContractsProposalEvents.map((proposalEvents) => {
return proposalEvents.map((event) => {
const eventParsed = GovernanceInterface.parseLog(event).args
return useMemo(() => {
return useLogsResult?.logs?.map((log) => {
const parsed = GovernanceInterface.parseLog(log).args
return {
description: eventParsed.description,
details: eventParsed.targets.map((target: string, i: number) => {
const signature = eventParsed.signatures[i]
description: parsed.description,
details: parsed.targets.map((target: string, i: number) => {
const signature = parsed.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)
const calldata = parsed.calldatas[i]
const decoded = defaultAbiCoder.decode(types.split(','), calldata)
return {
target,
functionSig: name,
@ -135,118 +102,96 @@ function useDataFromEventLogs():
}),
}
})
})
}, [useLogsResult])
}
setFormattedEvents(formattedEventData)
})
.catch((error) => {
if (stale) return
const V0_PROPOSAL_IDS = [[1], [2], [3], [4]]
console.error('Failed to fetch proposals', error)
setFormattedEvents(undefined)
})
return () => {
stale = true
}
}
return
}, [filters, library, formattedEvents, chainId])
return formattedEvents
function countToIndices(count: number | undefined) {
return typeof count === 'number' ? new Array(count).fill(0).map((_, i) => [i + 1]) : []
}
// get data for all past and active proposals
export function useAllProposalData(): ProposalData[] {
export function useAllProposalData(): { data: ProposalData[]; loading: boolean } {
const { chainId } = useActiveWeb3React()
const proposalCount = useLatestProposalCount()
const gov0 = useGovernanceV0Contract()
const gov1 = useGovernanceV1Contract()
const addresses = useMemo(() => {
return chainId === SupportedChainId.MAINNET ? GOVERNANCE_ADDRESSES.map((addressMap) => addressMap[chainId]) : []
}, [chainId])
const proposalCount0 = useProposalCount(gov0)
const proposalCount1 = useProposalCount(gov1)
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])
const gov0ProposalIndexes = useMemo(() => {
return chainId === SupportedChainId.MAINNET ? V0_PROPOSAL_IDS : countToIndices(proposalCount0)
}, [chainId, proposalCount0])
const gov1ProposalIndexes = useMemo(() => {
return countToIndices(proposalCount1)
}, [proposalCount1])
// get all proposal entities
const allProposalsCallData = useMultipleContractMultipleData(
addresses,
GovernanceInterface,
'proposals',
proposalIndexes
)
const proposalsV0 = useSingleContractMultipleData(gov0, 'proposals', gov0ProposalIndexes)
const proposalsV1 = useSingleContractMultipleData(gov1, 'proposals', gov1ProposalIndexes)
// get all proposal states
const allProposalStatesCallData = useMultipleContractMultipleData(
addresses,
GovernanceInterface,
'state',
proposalIndexes
)
const proposalStatesV0 = useSingleContractMultipleData(gov0, 'state', gov0ProposalIndexes)
const proposalStatesV1 = useSingleContractMultipleData(gov1, 'state', gov1ProposalIndexes)
// get metadata from past events
const allFormattedEvents = useDataFromEventLogs()
const formattedLogsV0 = useFormattedProposalCreatedLogs(gov0)
const formattedLogsV1 = useFormattedProposalCreatedLogs(gov1)
// 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]
return useMemo(() => {
const proposalsCallData = proposalsV0.concat(proposalsV1)
const proposalStatesCallData = proposalStatesV0.concat(proposalStatesV1)
const formattedLogs = (formattedLogsV0 ?? []).concat(formattedLogsV1 ?? [])
if (
!proposalsCallData?.every((p) => Boolean(p.result)) ||
!proposalStatesCallData?.every((p) => Boolean(p.result)) ||
!formattedEvents?.every((p) => Boolean(p))
proposalsCallData.some((p) => p.loading) ||
proposalStatesCallData.some((p) => p.loading) ||
(gov0 && !formattedLogsV0) ||
(gov1 && !formattedLogsV1)
) {
results.push([])
continue
return { data: [], loading: true }
}
results.push(
proposalsCallData.map((proposal, i) => {
let description = formattedEvents[i].description
return {
data: proposalsCallData.map((proposal, i) => {
let description = formattedLogs[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.',
title: description?.split(/# |\n/g)[1] ?? t`Untitled`,
description: description ?? t`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)),
status: proposalStatesCallData[i]?.result?.[0] ?? ProposalState.UNDETERMINED,
forCount: parseFloat(formatUnits(proposal?.result?.forVotes?.toString() ?? 0, 18)),
againstCount: parseFloat(formatUnits(proposal?.result?.againstVotes?.toString() ?? 0, 18)),
startBlock,
endBlock: parseInt(proposal?.result?.endBlock?.toString()),
details: formattedEvents[i].details,
governorIndex: governanceContractIndex,
details: formattedLogs[i]?.details,
governorIndex: i >= gov0ProposalIndexes.length ? 1 : 0,
}
})
)
}),
loading: false,
}
return results.reverse().flat()
}, [
formattedLogsV0,
formattedLogsV1,
gov0,
gov0ProposalIndexes.length,
gov1,
proposalStatesV0,
proposalStatesV1,
proposalsV0,
proposalsV1,
])
}
export function useProposalData(governorIndex: number, id: string): ProposalData | undefined {
const allProposalData = useAllProposalData()
return allProposalData?.filter((p) => p.governorIndex === governorIndex)?.find((p) => p.id === id)
const { data } = useAllProposalData()
return data.filter((p) => p.governorIndex === governorIndex)?.find((p) => p.id === id)
}
// get the users delegatee if it exists
@ -258,14 +203,16 @@ export function useUserDelegatee(): string {
}
// gets the users current votes
export function useUserVotes(): CurrencyAmount<Token> | undefined {
export function useUserVotes(): { loading: boolean; votes: CurrencyAmount<Token> | undefined } {
const { account, chainId } = useActiveWeb3React()
const uniContract = useUniContract()
// check for available votes
const { result, loading } = useSingleCallResult(uniContract, 'getCurrentVotes', [account ?? undefined])
return useMemo(() => {
const uni = chainId ? UNI[chainId] : undefined
const votes = useSingleCallResult(uniContract, 'getCurrentVotes', [account ?? undefined])?.result?.[0]
return votes && uni ? CurrencyAmount.fromRawAmount(uni, votes) : undefined
return { loading, votes: uni && result ? CurrencyAmount.fromRawAmount(uni, result?.[0]) : undefined }
}, [chainId, loading, result])
}
// fetch available votes as of block (usually proposal start block)
@ -296,7 +243,7 @@ export function useDelegateCallback(): (delegatee: string | undefined) => undefi
.delegate(...args, { value: null, gasLimit: calculateGasMargin(estimatedGasLimit) })
.then((response: TransactionResponse) => {
addTransaction(response, {
summary: `Delegated votes`,
summary: t`Delegated votes`,
})
return response.hash
})
@ -311,8 +258,8 @@ export function useVoteCallback(): {
} {
const { account } = useActiveWeb3React()
const govContracts = useGovernanceContracts()
const latestGovernanceContract = govContracts ? govContracts[0] : null
const latestGovernanceContract = useLatestGovernanceContract()
const addTransaction = useTransactionAdder()
const voteCallback = useCallback(
@ -340,11 +287,10 @@ export function useCreateProposalCallback(): (
) => undefined | Promise<string> {
const { account } = useActiveWeb3React()
const govContracts = useGovernanceContracts()
const latestGovernanceContract = govContracts ? govContracts[0] : null
const latestGovernanceContract = useLatestGovernanceContract()
const addTransaction = useTransactionAdder()
const createProposalCallback = useCallback(
return useCallback(
(createProposalData: CreateProposalData | undefined) => {
if (!account || !latestGovernanceContract || !createProposalData) return undefined
@ -369,27 +315,19 @@ export function useCreateProposalCallback(): (
},
[account, addTransaction, latestGovernanceContract]
)
return createProposalCallback
}
export function useLatestProposalId(address: string): string | undefined {
const govContracts = useGovernanceContracts()
const latestGovernanceContract = govContracts ? govContracts[0] : null
const res = useSingleCallResult(latestGovernanceContract, 'latestProposalIds', [address])
export function useLatestProposalId(address: string | undefined): string | undefined {
const govContractV1 = useGovernanceV1Contract()
const res = useSingleCallResult(govContractV1, 'latestProposalIds', [address])
if (res?.result?.[0]) {
return (res.result[0] as BigNumber).toString()
}
return undefined
return res?.result?.[0]?.toString()
}
export function useProposalThreshold(): CurrencyAmount<Token> | undefined {
const { chainId } = useActiveWeb3React()
const govContracts = useGovernanceContracts()
const latestGovernanceContract = govContracts ? govContracts[0] : null
const latestGovernanceContract = useLatestGovernanceContract()
const res = useSingleCallResult(latestGovernanceContract, 'proposalThreshold')
const uni = chainId ? UNI[chainId] : undefined

@ -11,6 +11,7 @@ import mintV3 from './mint/v3/reducer'
import lists from './lists/reducer'
import burn from './burn/reducer'
import burnV3 from './burn/v3/reducer'
import logs from './logs/slice'
import multicall from './multicall/reducer'
import { api } from './data/slice'
@ -28,6 +29,7 @@ const store = configureStore({
burnV3,
multicall,
lists,
logs,
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>

76
src/state/logs/hooks.ts Normal file

@ -0,0 +1,76 @@
import { useBlockNumber } from '../application/hooks'
import { useEffect, useMemo } from 'react'
import { useActiveWeb3React } from '../../hooks/web3'
import { useAppDispatch, useAppSelector } from '../hooks'
import { addListener, removeListener } from './slice'
import { EventFilter, filterToKey, Log } from './utils'
enum LogsState {
// The filter is invalid
INVALID,
// The logs are being loaded
LOADING,
// Logs are from a previous block number
SYNCING,
// Tried to fetch logs but received an error
ERROR,
// Logs have been fetched as of the latest block number
SYNCED,
}
export interface UseLogsResult {
logs: Log[] | undefined
state: LogsState
}
/**
* 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.
*/
export function useLogs(filter: EventFilter | undefined): UseLogsResult {
const { chainId } = useActiveWeb3React()
const blockNumber = useBlockNumber()
const logs = useAppSelector((state) => state.logs)
const dispatch = useAppDispatch()
useEffect(() => {
if (!filter || !chainId) return
dispatch(addListener({ chainId, filter }))
return () => {
dispatch(removeListener({ chainId, filter }))
}
}, [chainId, dispatch, filter])
const filterKey = useMemo(() => (filter ? filterToKey(filter) : undefined), [filter])
return useMemo(() => {
if (!chainId || !filterKey || !blockNumber)
return {
logs: undefined,
state: LogsState.INVALID,
}
const state = logs[chainId]?.[filterKey]
const result = state?.results
if (!result) {
return {
state: LogsState.LOADING,
logs: undefined,
}
}
if (result.error) {
return {
state: LogsState.ERROR,
logs: undefined,
}
}
return {
state: result.blockNumber >= blockNumber ? LogsState.SYNCED : LogsState.SYNCING,
logs: result.logs,
}
}, [blockNumber, chainId, filterKey, logs])
}

87
src/state/logs/slice.ts Normal file

@ -0,0 +1,87 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { EventFilter, filterToKey, Log } from './utils'
export interface LogsState {
[chainId: number]: {
[filterKey: string]: {
listeners: number
fetchingBlockNumber?: number
results?:
| {
blockNumber: number
logs: Log[]
error?: undefined
}
| {
blockNumber: number
logs?: undefined
error: true
}
}
}
}
const slice = createSlice({
name: 'logs',
initialState: {} as LogsState,
reducers: {
addListener(state, { payload: { chainId, filter } }: PayloadAction<{ chainId: number; filter: EventFilter }>) {
if (!state[chainId]) state[chainId] = {}
const key = filterToKey(filter)
if (!state[chainId][key])
state[chainId][key] = {
listeners: 1,
}
else state[chainId][key].listeners++
},
fetchingLogs(
state,
{
payload: { chainId, filters, blockNumber },
}: PayloadAction<{ chainId: number; filters: EventFilter[]; blockNumber: number }>
) {
if (!state[chainId]) return
for (const filter of filters) {
const key = filterToKey(filter)
if (!state[chainId][key]) continue
state[chainId][key].fetchingBlockNumber = blockNumber
}
},
fetchedLogs(
state,
{
payload: { chainId, filter, results },
}: PayloadAction<{ chainId: number; filter: EventFilter; results: { blockNumber: number; logs: Log[] } }>
) {
if (!state[chainId]) return
const key = filterToKey(filter)
const fetchState = state[chainId][key]
if (!fetchState || (fetchState.results && fetchState.results.blockNumber > results.blockNumber)) return
fetchState.results = results
},
fetchedLogsError(
state,
{
payload: { chainId, filter, blockNumber },
}: PayloadAction<{ chainId: number; blockNumber: number; filter: EventFilter }>
) {
if (!state[chainId]) return
const key = filterToKey(filter)
const fetchState = state[chainId][key]
if (!fetchState || (fetchState.results && fetchState.results.blockNumber > blockNumber)) return
fetchState.results = {
blockNumber,
error: true,
}
},
removeListener(state, { payload: { chainId, filter } }: PayloadAction<{ chainId: number; filter: EventFilter }>) {
if (!state[chainId]) return
const key = filterToKey(filter)
if (!state[chainId][key]) return
state[chainId][key].listeners--
},
},
})
export default slice.reducer
export const { addListener, removeListener, fetchedLogs, fetchedLogsError, fetchingLogs } = slice.actions

66
src/state/logs/updater.ts Normal file

@ -0,0 +1,66 @@
import { useEffect, useMemo } from 'react'
import { useActiveWeb3React } from '../../hooks/web3'
import { useBlockNumber } from '../application/hooks'
import { useAppDispatch, useAppSelector } from '../hooks'
import { fetchedLogs, fetchedLogsError, fetchingLogs } from './slice'
import { EventFilter, keyToFilter } from './utils'
export default function Updater(): null {
const dispatch = useAppDispatch()
const state = useAppSelector((state) => state.logs)
const { chainId, library } = useActiveWeb3React()
const blockNumber = useBlockNumber()
const filtersNeedFetch: EventFilter[] = useMemo(() => {
if (!chainId || typeof blockNumber !== 'number') return []
const active = state[chainId]
if (!active) return []
return Object.keys(active)
.filter((key) => {
const { fetchingBlockNumber, results, listeners } = active[key]
if (listeners === 0) return false
if (typeof fetchingBlockNumber === 'number' && fetchingBlockNumber >= blockNumber) return false
if (results && typeof results.blockNumber === 'number' && results.blockNumber >= blockNumber) return false
return true
})
.map((key) => keyToFilter(key))
}, [blockNumber, chainId, state])
useEffect(() => {
if (!library || !chainId || typeof blockNumber !== 'number' || filtersNeedFetch.length === 0) return
dispatch(fetchingLogs({ chainId, filters: filtersNeedFetch, blockNumber }))
filtersNeedFetch.forEach((filter) => {
library
.getLogs({
...filter,
fromBlock: 0,
toBlock: blockNumber,
})
.then((logs) => {
dispatch(
fetchedLogs({
chainId,
filter,
results: { logs, blockNumber },
})
)
})
.catch((error) => {
console.error('Failed to get logs', filter, error)
dispatch(
fetchedLogsError({
chainId,
filter,
blockNumber,
})
)
})
})
}, [blockNumber, chainId, dispatch, filtersNeedFetch, library])
return null
}

38
src/state/logs/utils.ts Normal file

@ -0,0 +1,38 @@
export interface EventFilter {
address?: string
topics?: Array<string | Array<string> | null>
}
export interface Log {
topics: Array<string>
data: string
}
/**
* Converts a filter to the corresponding string key
* @param filter the filter to convert
*/
export function filterToKey(filter: EventFilter): string {
return `${filter.address ?? ''}:${
filter.topics?.map((topic) => (topic ? (Array.isArray(topic) ? topic.join(';') : topic) : '\0'))?.join('-') ?? ''
}`
}
/**
* Convert a filter key to the corresponding filter
* @param key key to convert
*/
export function keyToFilter(key: string): EventFilter {
const pcs = key.split(':')
const address = pcs[0]
const topics = pcs[1].split('-').map((topic) => {
const parts = topic.split(';')
if (parts.length === 1) return parts[0]
return parts
})
return {
address: address.length === 0 ? undefined : address,
topics,
}
}

@ -1,4 +1,4 @@
import { parseCallKey, toCallKey } from './actions'
import { parseCallKey, toCallKey } from './utils'
describe('actions', () => {
describe('#parseCallKey', () => {
@ -11,9 +11,6 @@ describe('actions', () => {
callData: 'abc',
})
})
it('throws for invalid format', () => {
expect(() => parseCallKey('abc')).toThrow('Invalid call key: abc')
})
it('throws for uppercase calldata', () => {
expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcD')).toEqual({
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
@ -29,25 +26,6 @@ describe('actions', () => {
})
describe('#toCallKey', () => {
it('throws for invalid address', () => {
expect(() => toCallKey({ callData: '0x', address: '0x' })).toThrow('Invalid address: 0x')
})
it('throws for invalid calldata', () => {
expect(() =>
toCallKey({
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
callData: 'abc',
})
).toThrow('Invalid hex: abc')
})
it('throws for uppercase hex', () => {
expect(() =>
toCallKey({
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
callData: '0xabcD',
})
).toThrow('Invalid hex: 0xabcD')
})
it('concatenates address to data', () => {
expect(toCallKey({ address: '0x6b175474e89094c44da98b954eedeac495271d0f', callData: '0xabcd' })).toEqual(
'0x6b175474e89094c44da98b954eedeac495271d0f-0xabcd'

@ -1,41 +1,5 @@
import { createAction } from '@reduxjs/toolkit'
export interface Call {
address: string
callData: string
gasRequired?: number
}
const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/
const LOWER_HEX_REGEX = /^0x[a-f0-9]*$/
export function toCallKey(call: Call): string {
if (!ADDRESS_REGEX.test(call.address)) {
throw new Error(`Invalid address: ${call.address}`)
}
if (!LOWER_HEX_REGEX.test(call.callData)) {
throw new Error(`Invalid hex: ${call.callData}`)
}
let key = `${call.address}-${call.callData}`
if (call.gasRequired) {
if (!Number.isSafeInteger(call.gasRequired)) {
throw new Error(`Invalid number: ${call.gasRequired}`)
}
key += `-${call.gasRequired}`
}
return key
}
export function parseCallKey(callKey: string): Call {
const pcs = callKey.split('-')
if (![2, 3].includes(pcs.length)) {
throw new Error(`Invalid call key: ${callKey}`)
}
return {
address: pcs[0],
callData: pcs[1],
...(pcs[2] ? { gasRequired: Number.parseInt(pcs[2]) } : {}),
}
}
import { Call } from './utils'
export interface ListenerOptions {
// how often this data should be fetched, by default 1

@ -5,14 +5,8 @@ import { useEffect, useMemo } from 'react'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { useActiveWeb3React } from '../../hooks/web3'
import { useBlockNumber } from '../application/hooks'
import {
addMulticallListeners,
Call,
ListenerOptions,
parseCallKey,
removeMulticallListeners,
toCallKey,
} from './actions'
import { addMulticallListeners, ListenerOptions, removeMulticallListeners } from './actions'
import { Call, parseCallKey, toCallKey } from './utils'
export interface Result extends ReadonlyArray<any> {
readonly [key: string]: any
@ -234,63 +228,6 @@ export function useMultipleContractSingleData(
}, [fragment, results, contractInterface, latestBlockNumber])
}
export function useMultipleContractMultipleData(
addresses: (string | undefined)[],
contractInterface: Interface,
methodName: string,
callInputs?: OptionalMethodInputs[][],
options?: ListenerOptions,
gasRequired?: number
): CallState[][] {
const fragment = useMemo(() => contractInterface.getFunction(methodName), [contractInterface, methodName])
const calls = useMemo(() => {
return addresses.map((address, i) => {
const passesChecks = address && contractInterface && fragment
if (!passesChecks) {
return []
}
return callInputs
? callInputs[i].map<Call | undefined>((inputs) => {
const callData: string | undefined = isValidMethodArgs(inputs)
? contractInterface.encodeFunctionData(fragment, inputs)
: undefined
return address && callData
? {
address,
callData,
...(gasRequired ? { gasRequired } : {}),
}
: undefined
})
: []
})
}, [addresses, callInputs, contractInterface, fragment, gasRequired])
const callResults = useCallsData(calls.flat(), options)
const latestBlockNumber = useBlockNumber()
return useMemo(() => {
const callInputLengths = callInputs ? callInputs.map((inputArray) => inputArray.length) : []
const unformatedResults = callResults.map((result) =>
toCallState(result, contractInterface, fragment, latestBlockNumber)
)
return callInputLengths.map((length) => {
let j = 0
const indexElements: any = []
while (j < length) {
indexElements.push(unformatedResults.shift())
j++
}
return indexElements
})
}, [fragment, callInputs, callResults, contractInterface, latestBlockNumber])
}
export function useSingleCallResult(
contract: Contract | null | undefined,
methodName: string,

@ -4,9 +4,9 @@ import {
errorFetchingMulticallResults,
fetchingMulticallResults,
removeMulticallListeners,
toCallKey,
updateMulticallResults,
} from './actions'
import { toCallKey } from './utils'
export interface MulticallState {
callListeners?: {

@ -8,14 +8,9 @@ import { retry, RetryableError } from '../../utils/retry'
import { updateBlockNumber } from '../application/actions'
import { useBlockNumber } from '../application/hooks'
import { AppState } from '../index'
import {
Call,
errorFetchingMulticallResults,
fetchingMulticallResults,
parseCallKey,
updateMulticallResults,
} from './actions'
import { errorFetchingMulticallResults, fetchingMulticallResults, updateMulticallResults } from './actions'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { Call, parseCallKey } from './utils'
/**
* Fetches a chunk of calls, enforcing a minimum block number constraint

@ -0,0 +1,28 @@
export interface Call {
address: string
callData: string
gasRequired?: number
}
export function toCallKey(call: Call): string {
let key = `${call.address}-${call.callData}`
if (call.gasRequired) {
if (!Number.isSafeInteger(call.gasRequired)) {
throw new Error(`Invalid number: ${call.gasRequired}`)
}
key += `-${call.gasRequired}`
}
return key
}
export function parseCallKey(callKey: string): Call {
const pcs = callKey.split('-')
if (![2, 3].includes(pcs.length)) {
throw new Error(`Invalid call key: ${callKey}`)
}
return {
address: pcs[0],
callData: pcs[1],
...(pcs[2] ? { gasRequired: Number.parseInt(pcs[2]) } : {}),
}
}