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:
parent
abfd87c517
commit
27f756107e
@ -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[] = [
|
||||
{
|
||||
[SupportedChainId.MAINNET]: '0xC4e172459f1E7939D522503B81AFAaC1014CE6F6',
|
||||
},
|
||||
{
|
||||
[SupportedChainId.MAINNET]: '0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F',
|
||||
},
|
||||
]
|
||||
/**
|
||||
* 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',
|
||||
}
|
||||
|
||||
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,208 +50,148 @@ 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 {
|
||||
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
|
||||
return useMemo(() => {
|
||||
return useLogsResult?.logs?.map((log) => {
|
||||
const parsed = GovernanceInterface.parseLog(log).args
|
||||
return {
|
||||
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 = parsed.calldatas[i]
|
||||
const decoded = defaultAbiCoder.decode(types.split(','), calldata)
|
||||
return {
|
||||
target,
|
||||
functionSig: name,
|
||||
callData: decoded.join(', '),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [useLogsResult])
|
||||
}
|
||||
|
||||
return
|
||||
}, [filters, library, formattedEvents, chainId])
|
||||
const V0_PROPOSAL_IDS = [[1], [2], [3], [4]]
|
||||
|
||||
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,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return results.reverse().flat()
|
||||
}),
|
||||
loading: false,
|
||||
}
|
||||
}, [
|
||||
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 uni = chainId ? UNI[chainId] : undefined
|
||||
const votes = useSingleCallResult(uniContract, 'getCurrentVotes', [account ?? undefined])?.result?.[0]
|
||||
return votes && uni ? CurrencyAmount.fromRawAmount(uni, votes) : undefined
|
||||
const { result, loading } = useSingleCallResult(uniContract, 'getCurrentVotes', [account ?? undefined])
|
||||
return useMemo(() => {
|
||||
const uni = chainId ? UNI[chainId] : 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
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
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
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
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
|
||||
|
28
src/state/multicall/utils.ts
Normal file
28
src/state/multicall/utils.ts
Normal file
@ -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]) } : {}),
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user