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 { useVoteCallback, useUserVotes } from '../../state/governance/hooks'
|
||||||
import { ExternalLink } from '../../theme/components'
|
import { ExternalLink } from '../../theme/components'
|
||||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
|
||||||
import { Trans } from '@lingui/macro'
|
import { Trans } from '@lingui/macro'
|
||||||
|
|
||||||
const ContentWrapper = styled(AutoColumn)`
|
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
|
voteCallback: (proposalId: string | undefined, support: boolean) => Promise<string> | undefined
|
||||||
} = useVoteCallback()
|
} = useVoteCallback()
|
||||||
const availableVotes: CurrencyAmount<Token> | undefined = useUserVotes()
|
const { votes: availableVotes } = useUserVotes()
|
||||||
|
|
||||||
// monitor call to help UI loading state
|
// monitor call to help UI loading state
|
||||||
const [hash, setHash] = useState<string | undefined>()
|
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_FACTORY_ADDRESSES: AddressMap = constructSameAddressMap(V2_FACTORY_ADDRESS)
|
||||||
export const V2_ROUTER_ADDRESS: AddressMap = constructSameAddressMap('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D')
|
export const V2_ROUTER_ADDRESS: AddressMap = constructSameAddressMap('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D')
|
||||||
|
|
||||||
// most current governance contract address should always be the 0 index
|
/**
|
||||||
// only support governance on mainnet
|
* The older V0 governance account
|
||||||
export const GOVERNANCE_ADDRESSES: AddressMap[] = [
|
*/
|
||||||
{
|
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]: '0xC4e172459f1E7939D522503B81AFAaC1014CE6F6',
|
||||||
},
|
}
|
||||||
{
|
|
||||||
[SupportedChainId.MAINNET]: '0x5e4be8Bc9637f0EAA1A755019e06A68ce081D58F',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
export const TIMELOCK_ADDRESS: AddressMap = constructSameAddressMap('0x1a9C8182C09F50C8318d769245beA52c32BE35BC')
|
export const TIMELOCK_ADDRESS: AddressMap = constructSameAddressMap('0x1a9C8182C09F50C8318d769245beA52c32BE35BC')
|
||||||
|
|
||||||
export const MERKLE_DISTRIBUTOR_ADDRESS: AddressMap = {
|
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'
|
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 }> = {
|
export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }> = {
|
||||||
[SupportedChainId.MAINNET]: {
|
[SupportedChainId.MAINNET]: {
|
||||||
[UNI_ADDRESS[SupportedChainId.MAINNET]]: 'UNI',
|
[UNI_ADDRESS[SupportedChainId.MAINNET]]: 'UNI',
|
||||||
[TIMELOCK_ADDRESS[SupportedChainId.MAINNET]]: 'Timelock',
|
[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_CORE_FACTORY_ADDRESSES,
|
||||||
V3_MIGRATOR_ADDRESSES,
|
V3_MIGRATOR_ADDRESSES,
|
||||||
ARGENT_WALLET_DETECTOR_ADDRESS,
|
ARGENT_WALLET_DETECTOR_ADDRESS,
|
||||||
GOVERNANCE_ADDRESSES,
|
|
||||||
MERKLE_DISTRIBUTOR_ADDRESS,
|
MERKLE_DISTRIBUTOR_ADDRESS,
|
||||||
MULTICALL2_ADDRESSES,
|
MULTICALL2_ADDRESSES,
|
||||||
V2_ROUTER_ADDRESS,
|
V2_ROUTER_ADDRESS,
|
||||||
ENS_REGISTRAR_ADDRESSES,
|
ENS_REGISTRAR_ADDRESSES,
|
||||||
SOCKS_CONTROLLER_ADDRESSES,
|
SOCKS_CONTROLLER_ADDRESSES,
|
||||||
|
GOVERNANCE_ALPHA_V0_ADDRESSES,
|
||||||
|
GOVERNANCE_ALPHA_V1_ADDRESSES,
|
||||||
} from 'constants/addresses'
|
} from 'constants/addresses'
|
||||||
import { abi as NFTPositionManagerABI } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
|
import { abi as NFTPositionManagerABI } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
@ -116,25 +117,16 @@ export function useMerkleDistributorContract() {
|
|||||||
return useContract(MERKLE_DISTRIBUTOR_ADDRESS, MERKLE_DISTRIBUTOR_ABI, true)
|
return useContract(MERKLE_DISTRIBUTOR_ADDRESS, MERKLE_DISTRIBUTOR_ABI, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGovernanceContracts(): (Contract | null)[] {
|
export function useGovernanceV0Contract(): Contract | null {
|
||||||
const { library, account, chainId } = useActiveWeb3React()
|
return useContract(GOVERNANCE_ALPHA_V0_ADDRESSES, GOVERNANCE_ABI, true)
|
||||||
|
|
||||||
return useMemo(() => {
|
|
||||||
if (!library || !chainId) {
|
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return GOVERNANCE_ADDRESSES.filter((addressMap) => Boolean(addressMap[chainId])).map((addressMap) => {
|
export function useGovernanceV1Contract(): Contract | null {
|
||||||
try {
|
return useContract(GOVERNANCE_ALPHA_V1_ADDRESSES, GOVERNANCE_ABI, true)
|
||||||
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 const useLatestGovernanceContract = useGovernanceV1Contract
|
||||||
|
|
||||||
export function useUniContract() {
|
export function useUniContract() {
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
return useContract(chainId ? UNI[chainId]?.address : undefined, UNI_ABI, true)
|
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 ApplicationUpdater from './state/application/updater'
|
||||||
import ListsUpdater from './state/lists/updater'
|
import ListsUpdater from './state/lists/updater'
|
||||||
import MulticallUpdater from './state/multicall/updater'
|
import MulticallUpdater from './state/multicall/updater'
|
||||||
|
import LogsUpdater from './state/logs/updater'
|
||||||
import TransactionUpdater from './state/transactions/updater'
|
import TransactionUpdater from './state/transactions/updater'
|
||||||
import UserUpdater from './state/user/updater'
|
import UserUpdater from './state/user/updater'
|
||||||
import ThemeProvider, { ThemedGlobalStyle } from './theme'
|
import ThemeProvider, { ThemedGlobalStyle } from './theme'
|
||||||
@ -57,6 +58,7 @@ function Updaters() {
|
|||||||
<ApplicationUpdater />
|
<ApplicationUpdater />
|
||||||
<TransactionUpdater />
|
<TransactionUpdater />
|
||||||
<MulticallUpdater />
|
<MulticallUpdater />
|
||||||
|
<LogsUpdater />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -86,9 +86,9 @@ const AutonomousProposalCTA = styled.div`
|
|||||||
export default function CreateProposal() {
|
export default function CreateProposal() {
|
||||||
const { account, chainId } = useActiveWeb3React()
|
const { account, chainId } = useActiveWeb3React()
|
||||||
|
|
||||||
const latestProposalId = useLatestProposalId(account ?? '0x0000000000000000000000000000000000000000') ?? '0'
|
const latestProposalId = useLatestProposalId(account ?? undefined) ?? '0'
|
||||||
const latestProposalData = useProposalData(0, latestProposalId)
|
const latestProposalData = useProposalData(0, latestProposalId)
|
||||||
const availableVotes: CurrencyAmount<Token> | undefined = useUserVotes()
|
const { votes: availableVotes } = useUserVotes()
|
||||||
const proposalThreshold: CurrencyAmount<Token> | undefined = useProposalThreshold()
|
const proposalThreshold: CurrencyAmount<Token> | undefined = useProposalThreshold()
|
||||||
|
|
||||||
const [modalOpen, setModalOpen] = useState(false)
|
const [modalOpen, setModalOpen] = useState(false)
|
||||||
@ -261,7 +261,7 @@ ${bodyValue}
|
|||||||
<CreateProposalButton
|
<CreateProposalButton
|
||||||
proposalThreshold={proposalThreshold}
|
proposalThreshold={proposalThreshold}
|
||||||
hasActiveOrPendingProposal={
|
hasActiveOrPendingProposal={
|
||||||
latestProposalData?.status === ProposalState.Active || latestProposalData?.status === ProposalState.Pending
|
latestProposalData?.status === ProposalState.ACTIVE || latestProposalData?.status === ProposalState.PENDING
|
||||||
}
|
}
|
||||||
hasEnoughVote={hasEnoughVote}
|
hasEnoughVote={hasEnoughVote}
|
||||||
isFormInvalid={isFormInvalid}
|
isFormInvalid={isFormInvalid}
|
||||||
|
@ -46,13 +46,14 @@ const PageWrapper = styled(AutoColumn)`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const ProposalInfo = styled(AutoColumn)`
|
const ProposalInfo = styled(AutoColumn)`
|
||||||
border: 1px solid ${({ theme }) => theme.bg4};
|
background: ${({ theme }) => theme.bg0};
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ArrowWrapper = styled(StyledInternalLink)`
|
const ArrowWrapper = styled(StyledInternalLink)`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -174,7 +175,7 @@ export default function VotePage({
|
|||||||
availableVotes &&
|
availableVotes &&
|
||||||
JSBI.greaterThan(availableVotes.quotient, JSBI.BigInt(0)) &&
|
JSBI.greaterThan(availableVotes.quotient, JSBI.BigInt(0)) &&
|
||||||
proposalData &&
|
proposalData &&
|
||||||
proposalData.status === ProposalState.Active
|
proposalData.status === ProposalState.ACTIVE
|
||||||
|
|
||||||
const uniBalance: CurrencyAmount<Token> | undefined = useTokenBalance(
|
const uniBalance: CurrencyAmount<Token> | undefined = useTokenBalance(
|
||||||
account ?? undefined,
|
account ?? undefined,
|
||||||
@ -211,9 +212,7 @@ export default function VotePage({
|
|||||||
<ArrowLeft size={20} /> All Proposals
|
<ArrowLeft size={20} /> All Proposals
|
||||||
</Trans>
|
</Trans>
|
||||||
</ArrowWrapper>
|
</ArrowWrapper>
|
||||||
{proposalData && (
|
{proposalData && <ProposalStatus status={proposalData.status} />}
|
||||||
<ProposalStatus status={proposalData.status}>{ProposalState[proposalData.status]}</ProposalStatus>
|
|
||||||
)}
|
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
<AutoColumn gap="10px" style={{ width: '100%' }}>
|
<AutoColumn gap="10px" style={{ width: '100%' }}>
|
||||||
<TYPE.largeHeader style={{ marginBottom: '.5rem' }}>{proposalData?.title}</TYPE.largeHeader>
|
<TYPE.largeHeader style={{ marginBottom: '.5rem' }}>{proposalData?.title}</TYPE.largeHeader>
|
||||||
@ -228,7 +227,7 @@ export default function VotePage({
|
|||||||
)}
|
)}
|
||||||
</TYPE.main>
|
</TYPE.main>
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
{proposalData && proposalData.status === ProposalState.Active && !showVotingButtons && (
|
{proposalData && proposalData.status === ProposalState.ACTIVE && !showVotingButtons && (
|
||||||
<GreyCard>
|
<GreyCard>
|
||||||
<TYPE.black>
|
<TYPE.black>
|
||||||
<Trans>
|
<Trans>
|
||||||
|
@ -11,13 +11,7 @@ import { ButtonPrimary } from '../../components/Button'
|
|||||||
import { Button } from 'rebass/styled-components'
|
import { Button } from 'rebass/styled-components'
|
||||||
import { darken } from 'polished'
|
import { darken } from 'polished'
|
||||||
import { CardBGImage, CardNoise, CardSection, DataCard } from '../../components/earn/styled'
|
import { CardBGImage, CardNoise, CardSection, DataCard } from '../../components/earn/styled'
|
||||||
import {
|
import { ProposalData, useAllProposalData, useUserDelegatee, useUserVotes } from '../../state/governance/hooks'
|
||||||
ProposalData,
|
|
||||||
ProposalState,
|
|
||||||
useAllProposalData,
|
|
||||||
useUserDelegatee,
|
|
||||||
useUserVotes,
|
|
||||||
} from '../../state/governance/hooks'
|
|
||||||
import DelegateModal from '../../components/vote/DelegateModal'
|
import DelegateModal from '../../components/vote/DelegateModal'
|
||||||
import { useTokenBalance } from '../../state/wallet/hooks'
|
import { useTokenBalance } from '../../state/wallet/hooks'
|
||||||
import { useActiveWeb3React } from '../../hooks/web3'
|
import { useActiveWeb3React } from '../../hooks/web3'
|
||||||
@ -119,10 +113,10 @@ export default function Vote() {
|
|||||||
const toggleDelegateModal = useToggleDelegateModal()
|
const toggleDelegateModal = useToggleDelegateModal()
|
||||||
|
|
||||||
// get data to list all proposals
|
// get data to list all proposals
|
||||||
const allProposals: ProposalData[] = useAllProposalData()
|
const { data: allProposals, loading: loadingProposals } = useAllProposalData()
|
||||||
|
|
||||||
// user data
|
// user data
|
||||||
const availableVotes: CurrencyAmount<Token> | undefined = useUserVotes()
|
const { loading: loadingAvailableVotes, votes: availableVotes } = useUserVotes()
|
||||||
const uniBalance: CurrencyAmount<Token> | undefined = useTokenBalance(
|
const uniBalance: CurrencyAmount<Token> | undefined = useTokenBalance(
|
||||||
account ?? undefined,
|
account ?? undefined,
|
||||||
chainId ? UNI[chainId] : undefined
|
chainId ? UNI[chainId] : undefined
|
||||||
@ -134,8 +128,6 @@ export default function Vote() {
|
|||||||
uniBalance && JSBI.notEqual(uniBalance.quotient, JSBI.BigInt(0)) && userDelegatee === ZERO_ADDRESS
|
uniBalance && JSBI.notEqual(uniBalance.quotient, JSBI.BigInt(0)) && userDelegatee === ZERO_ADDRESS
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxGovernorIndex = allProposals.reduce((max, p) => Math.max(p.governorIndex, max), 0)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageWrapper gap="lg" justify="center">
|
<PageWrapper gap="lg" justify="center">
|
||||||
@ -184,7 +176,7 @@ export default function Vote() {
|
|||||||
<Trans>Proposals</Trans>
|
<Trans>Proposals</Trans>
|
||||||
</TYPE.mediumHeader>
|
</TYPE.mediumHeader>
|
||||||
<AutoRow gap="6px" justify="flex-end">
|
<AutoRow gap="6px" justify="flex-end">
|
||||||
{(!allProposals || allProposals.length === 0) && !availableVotes && <Loader />}
|
{loadingProposals || loadingAvailableVotes ? <Loader /> : null}
|
||||||
{showUnlockVoting ? (
|
{showUnlockVoting ? (
|
||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
style={{ width: 'fit-content' }}
|
style={{ width: 'fit-content' }}
|
||||||
@ -263,10 +255,10 @@ export default function Vote() {
|
|||||||
return (
|
return (
|
||||||
<Proposal as={Link} to={`/vote/${p.governorIndex}/${p.id}`} key={`${p.governorIndex}${p.id}`}>
|
<Proposal as={Link} to={`/vote/${p.governorIndex}/${p.id}`} key={`${p.governorIndex}${p.id}`}>
|
||||||
<ProposalNumber>
|
<ProposalNumber>
|
||||||
{maxGovernorIndex - p.governorIndex}.{p.id}
|
{p.governorIndex}.{p.id}
|
||||||
</ProposalNumber>
|
</ProposalNumber>
|
||||||
<ProposalTitle>{p.title}</ProposalTitle>
|
<ProposalTitle>{p.title}</ProposalTitle>
|
||||||
<ProposalStatus status={p.status}>{ProposalState[p.status]}</ProposalStatus>
|
<ProposalStatus status={p.status} />
|
||||||
</Proposal>
|
</Proposal>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -1,25 +1,49 @@
|
|||||||
|
import { Trans } from '@lingui/macro'
|
||||||
import styled, { DefaultTheme } from 'styled-components'
|
import styled, { DefaultTheme } from 'styled-components'
|
||||||
import { ProposalState } from '../../state/governance/hooks'
|
import { ProposalState } from '../../state/governance/hooks'
|
||||||
|
|
||||||
const handleColorType = (status: ProposalState, theme: DefaultTheme) => {
|
const handleColorType = (status: ProposalState, theme: DefaultTheme) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ProposalState.Pending:
|
case ProposalState.PENDING:
|
||||||
case ProposalState.Active:
|
case ProposalState.ACTIVE:
|
||||||
return theme.blue1
|
return theme.blue1
|
||||||
case ProposalState.Succeeded:
|
case ProposalState.SUCCEEDED:
|
||||||
case ProposalState.Executed:
|
case ProposalState.EXECUTED:
|
||||||
return theme.green1
|
return theme.green1
|
||||||
case ProposalState.Defeated:
|
case ProposalState.DEFEATED:
|
||||||
return theme.red1
|
return theme.red1
|
||||||
case ProposalState.Queued:
|
case ProposalState.QUEUED:
|
||||||
case ProposalState.Canceled:
|
case ProposalState.CANCELED:
|
||||||
case ProposalState.Expired:
|
case ProposalState.EXPIRED:
|
||||||
default:
|
default:
|
||||||
return theme.text3
|
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-size: 0.825rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
@ -30,3 +54,11 @@ export const ProposalStatus = styled.span<{ status: ProposalState }>`
|
|||||||
justify-self: flex-end;
|
justify-self: flex-end;
|
||||||
text-transform: uppercase;
|
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 { TransactionResponse } from '@ethersproject/providers'
|
||||||
|
import { t } from '@lingui/macro'
|
||||||
import { abi as GOV_ABI } from '@uniswap/governance/build/GovernorAlpha.json'
|
import { abi as GOV_ABI } from '@uniswap/governance/build/GovernorAlpha.json'
|
||||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
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 { UNISWAP_GRANTS_PROPOSAL_DESCRIPTION } from 'constants/proposals/uniswap_grants_proposal_description'
|
||||||
import { BigNumber, ethers, utils } from 'ethers'
|
import { Contract } from 'ethers'
|
||||||
import { isAddress } from 'ethers/lib/utils'
|
import { defaultAbiCoder, formatUnits, Interface, isAddress } from 'ethers/lib/utils'
|
||||||
import { useGovernanceContracts, useUniContract } from 'hooks/useContract'
|
import {
|
||||||
|
useGovernanceV0Contract,
|
||||||
|
useGovernanceV1Contract,
|
||||||
|
useLatestGovernanceContract,
|
||||||
|
useUniContract,
|
||||||
|
} from 'hooks/useContract'
|
||||||
import { useActiveWeb3React } from 'hooks/web3'
|
import { useActiveWeb3React } from 'hooks/web3'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { calculateGasMargin } from 'utils/calculateGasMargin'
|
import { calculateGasMargin } from 'utils/calculateGasMargin'
|
||||||
|
import { SupportedChainId } from '../../constants/chains'
|
||||||
import { UNISWAP_GRANTS_START_BLOCK } from '../../constants/proposals'
|
import { UNISWAP_GRANTS_START_BLOCK } from '../../constants/proposals'
|
||||||
import { UNI } from '../../constants/tokens'
|
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 { useTransactionAdder } from '../transactions/hooks'
|
||||||
import { t } from '@lingui/macro'
|
|
||||||
|
|
||||||
interface ProposalDetail {
|
interface ProposalDetail {
|
||||||
target: string
|
target: string
|
||||||
@ -45,88 +50,50 @@ export interface CreateProposalData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum ProposalState {
|
export enum ProposalState {
|
||||||
Undetermined = -1,
|
UNDETERMINED = -1,
|
||||||
Pending,
|
PENDING,
|
||||||
Active,
|
ACTIVE,
|
||||||
Canceled,
|
CANCELED,
|
||||||
Defeated,
|
DEFEATED,
|
||||||
Succeeded,
|
SUCCEEDED,
|
||||||
Queued,
|
QUEUED,
|
||||||
Expired,
|
EXPIRED,
|
||||||
Executed,
|
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
|
// get count of all proposals made in the latest governor contract
|
||||||
function useLatestProposalCount(): number | undefined {
|
function useProposalCount(contract: Contract | null): number | undefined {
|
||||||
const govContracts = useGovernanceContracts()
|
const { result } = useSingleCallResult(contract, 'proposalCount')
|
||||||
|
|
||||||
const res = useSingleCallResult(govContracts[0], 'proposalCount')
|
return result?.[0]?.toNumber()
|
||||||
|
|
||||||
if (res?.result?.[0]) {
|
|
||||||
return (res.result[0] as BigNumber).toNumber()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined
|
interface FormattedProposalLog {
|
||||||
|
description: string
|
||||||
|
details: { target: string; functionSig: string; callData: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Need proposal events to get description data emitted from
|
* Need proposal events to get description data emitted from
|
||||||
* new proposal event.
|
* new proposal event.
|
||||||
*/
|
*/
|
||||||
function useDataFromEventLogs():
|
function useFormattedProposalCreatedLogs(contract: Contract | null): FormattedProposalLog[] | undefined {
|
||||||
| {
|
|
||||||
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()
|
|
||||||
|
|
||||||
// create filters for ProposalCreated events
|
// create filters for ProposalCreated events
|
||||||
const filters = useMemo(
|
const filter = useMemo(() => contract?.filters?.ProposalCreated(), [contract])
|
||||||
() =>
|
|
||||||
govContracts?.filter((govContract) => !!govContract)?.length > 0
|
|
||||||
? govContracts
|
|
||||||
.filter((govContract): govContract is ethers.Contract => !!govContract)
|
|
||||||
.map((contract) => ({
|
|
||||||
...contract.filters.ProposalCreated(),
|
|
||||||
fromBlock: 10861678, // TODO could optimize this on a per-contract basis, this is the safe value
|
|
||||||
toBlock: 'latest',
|
|
||||||
}))
|
|
||||||
: undefined,
|
|
||||||
[govContracts]
|
|
||||||
)
|
|
||||||
|
|
||||||
// clear logs on chainId change
|
const useLogsResult = useLogs(filter)
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
setFormattedEvents(undefined)
|
|
||||||
}
|
|
||||||
}, [chainId])
|
|
||||||
|
|
||||||
useEffect(() => {
|
return useMemo(() => {
|
||||||
if (!filters || !library) return
|
return useLogsResult?.logs?.map((log) => {
|
||||||
let stale = false
|
const parsed = GovernanceInterface.parseLog(log).args
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
description: eventParsed.description,
|
description: parsed.description,
|
||||||
details: eventParsed.targets.map((target: string, i: number) => {
|
details: parsed.targets.map((target: string, i: number) => {
|
||||||
const signature = eventParsed.signatures[i]
|
const signature = parsed.signatures[i]
|
||||||
const [name, types] = signature.substr(0, signature.length - 1).split('(')
|
const [name, types] = signature.substr(0, signature.length - 1).split('(')
|
||||||
const calldata = eventParsed.calldatas[i]
|
const calldata = parsed.calldatas[i]
|
||||||
const decoded = utils.defaultAbiCoder.decode(types.split(','), calldata)
|
const decoded = defaultAbiCoder.decode(types.split(','), calldata)
|
||||||
return {
|
return {
|
||||||
target,
|
target,
|
||||||
functionSig: name,
|
functionSig: name,
|
||||||
@ -135,118 +102,96 @@ function useDataFromEventLogs():
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}, [useLogsResult])
|
||||||
|
|
||||||
setFormattedEvents(formattedEventData)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (stale) return
|
|
||||||
|
|
||||||
console.error('Failed to fetch proposals', error)
|
|
||||||
setFormattedEvents(undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
stale = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
const V0_PROPOSAL_IDS = [[1], [2], [3], [4]]
|
||||||
}, [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
|
// get data for all past and active proposals
|
||||||
export function useAllProposalData(): ProposalData[] {
|
export function useAllProposalData(): { data: ProposalData[]; loading: boolean } {
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
const proposalCount = useLatestProposalCount()
|
const gov0 = useGovernanceV0Contract()
|
||||||
|
const gov1 = useGovernanceV1Contract()
|
||||||
|
|
||||||
const addresses = useMemo(() => {
|
const proposalCount0 = useProposalCount(gov0)
|
||||||
return chainId === SupportedChainId.MAINNET ? GOVERNANCE_ADDRESSES.map((addressMap) => addressMap[chainId]) : []
|
const proposalCount1 = useProposalCount(gov1)
|
||||||
}, [chainId])
|
|
||||||
|
|
||||||
const proposalIndexes = useMemo(() => {
|
const gov0ProposalIndexes = useMemo(() => {
|
||||||
return chainId === SupportedChainId.MAINNET
|
return chainId === SupportedChainId.MAINNET ? V0_PROPOSAL_IDS : countToIndices(proposalCount0)
|
||||||
? [
|
}, [chainId, proposalCount0])
|
||||||
typeof proposalCount === 'number' ? new Array(proposalCount).fill(0).map((_, i) => [i + 1]) : [], // dynamic for current governor alpha
|
const gov1ProposalIndexes = useMemo(() => {
|
||||||
[[1], [2], [3], [4]], // hardcoded for governor alpha V0
|
return countToIndices(proposalCount1)
|
||||||
]
|
}, [proposalCount1])
|
||||||
: []
|
|
||||||
}, [chainId, proposalCount])
|
|
||||||
|
|
||||||
// get all proposal entities
|
const proposalsV0 = useSingleContractMultipleData(gov0, 'proposals', gov0ProposalIndexes)
|
||||||
const allProposalsCallData = useMultipleContractMultipleData(
|
const proposalsV1 = useSingleContractMultipleData(gov1, 'proposals', gov1ProposalIndexes)
|
||||||
addresses,
|
|
||||||
GovernanceInterface,
|
|
||||||
'proposals',
|
|
||||||
proposalIndexes
|
|
||||||
)
|
|
||||||
|
|
||||||
// get all proposal states
|
// get all proposal states
|
||||||
const allProposalStatesCallData = useMultipleContractMultipleData(
|
const proposalStatesV0 = useSingleContractMultipleData(gov0, 'state', gov0ProposalIndexes)
|
||||||
addresses,
|
const proposalStatesV1 = useSingleContractMultipleData(gov1, 'state', gov1ProposalIndexes)
|
||||||
GovernanceInterface,
|
|
||||||
'state',
|
|
||||||
proposalIndexes
|
|
||||||
)
|
|
||||||
|
|
||||||
// get metadata from past events
|
// get metadata from past events
|
||||||
const allFormattedEvents = useDataFromEventLogs()
|
const formattedLogsV0 = useFormattedProposalCreatedLogs(gov0)
|
||||||
|
const formattedLogsV1 = useFormattedProposalCreatedLogs(gov1)
|
||||||
|
|
||||||
// early return until events are fetched
|
// early return until events are fetched
|
||||||
if (!allFormattedEvents) return []
|
return useMemo(() => {
|
||||||
|
const proposalsCallData = proposalsV0.concat(proposalsV1)
|
||||||
const results: ProposalData[][] = []
|
const proposalStatesCallData = proposalStatesV0.concat(proposalStatesV1)
|
||||||
|
const formattedLogs = (formattedLogsV0 ?? []).concat(formattedLogsV1 ?? [])
|
||||||
for (
|
|
||||||
let governanceContractIndex = 0;
|
|
||||||
governanceContractIndex < allProposalsCallData.length;
|
|
||||||
governanceContractIndex++
|
|
||||||
) {
|
|
||||||
const proposalsCallData = allProposalsCallData[governanceContractIndex]
|
|
||||||
const proposalStatesCallData = allProposalStatesCallData[governanceContractIndex]
|
|
||||||
const formattedEvents = allFormattedEvents[governanceContractIndex]
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!proposalsCallData?.every((p) => Boolean(p.result)) ||
|
proposalsCallData.some((p) => p.loading) ||
|
||||||
!proposalStatesCallData?.every((p) => Boolean(p.result)) ||
|
proposalStatesCallData.some((p) => p.loading) ||
|
||||||
!formattedEvents?.every((p) => Boolean(p))
|
(gov0 && !formattedLogsV0) ||
|
||||||
|
(gov1 && !formattedLogsV1)
|
||||||
) {
|
) {
|
||||||
results.push([])
|
return { data: [], loading: true }
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push(
|
return {
|
||||||
proposalsCallData.map((proposal, i) => {
|
data: proposalsCallData.map((proposal, i) => {
|
||||||
let description = formattedEvents[i].description
|
let description = formattedLogs[i]?.description
|
||||||
const startBlock = parseInt(proposal?.result?.startBlock?.toString())
|
const startBlock = parseInt(proposal?.result?.startBlock?.toString())
|
||||||
if (startBlock === UNISWAP_GRANTS_START_BLOCK) {
|
if (startBlock === UNISWAP_GRANTS_START_BLOCK) {
|
||||||
description = UNISWAP_GRANTS_PROPOSAL_DESCRIPTION
|
description = UNISWAP_GRANTS_PROPOSAL_DESCRIPTION
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: proposal?.result?.id.toString(),
|
id: proposal?.result?.id.toString(),
|
||||||
title: description?.split(/# |\n/g)[1] ?? 'Untitled',
|
title: description?.split(/# |\n/g)[1] ?? t`Untitled`,
|
||||||
description: description ?? 'No description.',
|
description: description ?? t`No description.`,
|
||||||
proposer: proposal?.result?.proposer,
|
proposer: proposal?.result?.proposer,
|
||||||
status: proposalStatesCallData[i]?.result?.[0] ?? ProposalState.Undetermined,
|
status: proposalStatesCallData[i]?.result?.[0] ?? ProposalState.UNDETERMINED,
|
||||||
forCount: parseFloat(ethers.utils.formatUnits(proposal?.result?.forVotes.toString(), 18)),
|
forCount: parseFloat(formatUnits(proposal?.result?.forVotes?.toString() ?? 0, 18)),
|
||||||
againstCount: parseFloat(ethers.utils.formatUnits(proposal?.result?.againstVotes.toString(), 18)),
|
againstCount: parseFloat(formatUnits(proposal?.result?.againstVotes?.toString() ?? 0, 18)),
|
||||||
startBlock,
|
startBlock,
|
||||||
endBlock: parseInt(proposal?.result?.endBlock?.toString()),
|
endBlock: parseInt(proposal?.result?.endBlock?.toString()),
|
||||||
details: formattedEvents[i].details,
|
details: formattedLogs[i]?.details,
|
||||||
governorIndex: governanceContractIndex,
|
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 {
|
export function useProposalData(governorIndex: number, id: string): ProposalData | undefined {
|
||||||
const allProposalData = useAllProposalData()
|
const { data } = useAllProposalData()
|
||||||
return allProposalData?.filter((p) => p.governorIndex === governorIndex)?.find((p) => p.id === id)
|
return data.filter((p) => p.governorIndex === governorIndex)?.find((p) => p.id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the users delegatee if it exists
|
// get the users delegatee if it exists
|
||||||
@ -258,14 +203,16 @@ export function useUserDelegatee(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// gets the users current votes
|
// 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 { account, chainId } = useActiveWeb3React()
|
||||||
const uniContract = useUniContract()
|
const uniContract = useUniContract()
|
||||||
|
|
||||||
// check for available votes
|
// check for available votes
|
||||||
|
const { result, loading } = useSingleCallResult(uniContract, 'getCurrentVotes', [account ?? undefined])
|
||||||
|
return useMemo(() => {
|
||||||
const uni = chainId ? UNI[chainId] : undefined
|
const uni = chainId ? UNI[chainId] : undefined
|
||||||
const votes = useSingleCallResult(uniContract, 'getCurrentVotes', [account ?? undefined])?.result?.[0]
|
return { loading, votes: uni && result ? CurrencyAmount.fromRawAmount(uni, result?.[0]) : undefined }
|
||||||
return votes && uni ? CurrencyAmount.fromRawAmount(uni, votes) : undefined
|
}, [chainId, loading, result])
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch available votes as of block (usually proposal start block)
|
// 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) })
|
.delegate(...args, { value: null, gasLimit: calculateGasMargin(estimatedGasLimit) })
|
||||||
.then((response: TransactionResponse) => {
|
.then((response: TransactionResponse) => {
|
||||||
addTransaction(response, {
|
addTransaction(response, {
|
||||||
summary: `Delegated votes`,
|
summary: t`Delegated votes`,
|
||||||
})
|
})
|
||||||
return response.hash
|
return response.hash
|
||||||
})
|
})
|
||||||
@ -311,8 +258,8 @@ export function useVoteCallback(): {
|
|||||||
} {
|
} {
|
||||||
const { account } = useActiveWeb3React()
|
const { account } = useActiveWeb3React()
|
||||||
|
|
||||||
const govContracts = useGovernanceContracts()
|
const latestGovernanceContract = useLatestGovernanceContract()
|
||||||
const latestGovernanceContract = govContracts ? govContracts[0] : null
|
|
||||||
const addTransaction = useTransactionAdder()
|
const addTransaction = useTransactionAdder()
|
||||||
|
|
||||||
const voteCallback = useCallback(
|
const voteCallback = useCallback(
|
||||||
@ -340,11 +287,10 @@ export function useCreateProposalCallback(): (
|
|||||||
) => undefined | Promise<string> {
|
) => undefined | Promise<string> {
|
||||||
const { account } = useActiveWeb3React()
|
const { account } = useActiveWeb3React()
|
||||||
|
|
||||||
const govContracts = useGovernanceContracts()
|
const latestGovernanceContract = useLatestGovernanceContract()
|
||||||
const latestGovernanceContract = govContracts ? govContracts[0] : null
|
|
||||||
const addTransaction = useTransactionAdder()
|
const addTransaction = useTransactionAdder()
|
||||||
|
|
||||||
const createProposalCallback = useCallback(
|
return useCallback(
|
||||||
(createProposalData: CreateProposalData | undefined) => {
|
(createProposalData: CreateProposalData | undefined) => {
|
||||||
if (!account || !latestGovernanceContract || !createProposalData) return undefined
|
if (!account || !latestGovernanceContract || !createProposalData) return undefined
|
||||||
|
|
||||||
@ -369,27 +315,19 @@ export function useCreateProposalCallback(): (
|
|||||||
},
|
},
|
||||||
[account, addTransaction, latestGovernanceContract]
|
[account, addTransaction, latestGovernanceContract]
|
||||||
)
|
)
|
||||||
|
|
||||||
return createProposalCallback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLatestProposalId(address: string): string | undefined {
|
export function useLatestProposalId(address: string | undefined): string | undefined {
|
||||||
const govContracts = useGovernanceContracts()
|
const govContractV1 = useGovernanceV1Contract()
|
||||||
const latestGovernanceContract = govContracts ? govContracts[0] : null
|
const res = useSingleCallResult(govContractV1, 'latestProposalIds', [address])
|
||||||
const res = useSingleCallResult(latestGovernanceContract, 'latestProposalIds', [address])
|
|
||||||
|
|
||||||
if (res?.result?.[0]) {
|
return res?.result?.[0]?.toString()
|
||||||
return (res.result[0] as BigNumber).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useProposalThreshold(): CurrencyAmount<Token> | undefined {
|
export function useProposalThreshold(): CurrencyAmount<Token> | undefined {
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
|
|
||||||
const govContracts = useGovernanceContracts()
|
const latestGovernanceContract = useLatestGovernanceContract()
|
||||||
const latestGovernanceContract = govContracts ? govContracts[0] : null
|
|
||||||
const res = useSingleCallResult(latestGovernanceContract, 'proposalThreshold')
|
const res = useSingleCallResult(latestGovernanceContract, 'proposalThreshold')
|
||||||
const uni = chainId ? UNI[chainId] : undefined
|
const uni = chainId ? UNI[chainId] : undefined
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import mintV3 from './mint/v3/reducer'
|
|||||||
import lists from './lists/reducer'
|
import lists from './lists/reducer'
|
||||||
import burn from './burn/reducer'
|
import burn from './burn/reducer'
|
||||||
import burnV3 from './burn/v3/reducer'
|
import burnV3 from './burn/v3/reducer'
|
||||||
|
import logs from './logs/slice'
|
||||||
import multicall from './multicall/reducer'
|
import multicall from './multicall/reducer'
|
||||||
import { api } from './data/slice'
|
import { api } from './data/slice'
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ const store = configureStore({
|
|||||||
burnV3,
|
burnV3,
|
||||||
multicall,
|
multicall,
|
||||||
lists,
|
lists,
|
||||||
|
logs,
|
||||||
[api.reducerPath]: api.reducer,
|
[api.reducerPath]: api.reducer,
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) =>
|
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('actions', () => {
|
||||||
describe('#parseCallKey', () => {
|
describe('#parseCallKey', () => {
|
||||||
@ -11,9 +11,6 @@ describe('actions', () => {
|
|||||||
callData: 'abc',
|
callData: 'abc',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('throws for invalid format', () => {
|
|
||||||
expect(() => parseCallKey('abc')).toThrow('Invalid call key: abc')
|
|
||||||
})
|
|
||||||
it('throws for uppercase calldata', () => {
|
it('throws for uppercase calldata', () => {
|
||||||
expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcD')).toEqual({
|
expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcD')).toEqual({
|
||||||
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||||
@ -29,25 +26,6 @@ describe('actions', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('#toCallKey', () => {
|
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', () => {
|
it('concatenates address to data', () => {
|
||||||
expect(toCallKey({ address: '0x6b175474e89094c44da98b954eedeac495271d0f', callData: '0xabcd' })).toEqual(
|
expect(toCallKey({ address: '0x6b175474e89094c44da98b954eedeac495271d0f', callData: '0xabcd' })).toEqual(
|
||||||
'0x6b175474e89094c44da98b954eedeac495271d0f-0xabcd'
|
'0x6b175474e89094c44da98b954eedeac495271d0f-0xabcd'
|
||||||
|
@ -1,41 +1,5 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit'
|
import { createAction } from '@reduxjs/toolkit'
|
||||||
|
import { Call } from './utils'
|
||||||
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]) } : {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListenerOptions {
|
export interface ListenerOptions {
|
||||||
// how often this data should be fetched, by default 1
|
// 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 { useAppDispatch, useAppSelector } from 'state/hooks'
|
||||||
import { useActiveWeb3React } from '../../hooks/web3'
|
import { useActiveWeb3React } from '../../hooks/web3'
|
||||||
import { useBlockNumber } from '../application/hooks'
|
import { useBlockNumber } from '../application/hooks'
|
||||||
import {
|
import { addMulticallListeners, ListenerOptions, removeMulticallListeners } from './actions'
|
||||||
addMulticallListeners,
|
import { Call, parseCallKey, toCallKey } from './utils'
|
||||||
Call,
|
|
||||||
ListenerOptions,
|
|
||||||
parseCallKey,
|
|
||||||
removeMulticallListeners,
|
|
||||||
toCallKey,
|
|
||||||
} from './actions'
|
|
||||||
|
|
||||||
export interface Result extends ReadonlyArray<any> {
|
export interface Result extends ReadonlyArray<any> {
|
||||||
readonly [key: string]: any
|
readonly [key: string]: any
|
||||||
@ -234,63 +228,6 @@ export function useMultipleContractSingleData(
|
|||||||
}, [fragment, results, contractInterface, latestBlockNumber])
|
}, [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(
|
export function useSingleCallResult(
|
||||||
contract: Contract | null | undefined,
|
contract: Contract | null | undefined,
|
||||||
methodName: string,
|
methodName: string,
|
||||||
|
@ -4,9 +4,9 @@ import {
|
|||||||
errorFetchingMulticallResults,
|
errorFetchingMulticallResults,
|
||||||
fetchingMulticallResults,
|
fetchingMulticallResults,
|
||||||
removeMulticallListeners,
|
removeMulticallListeners,
|
||||||
toCallKey,
|
|
||||||
updateMulticallResults,
|
updateMulticallResults,
|
||||||
} from './actions'
|
} from './actions'
|
||||||
|
import { toCallKey } from './utils'
|
||||||
|
|
||||||
export interface MulticallState {
|
export interface MulticallState {
|
||||||
callListeners?: {
|
callListeners?: {
|
||||||
|
@ -8,14 +8,9 @@ import { retry, RetryableError } from '../../utils/retry'
|
|||||||
import { updateBlockNumber } from '../application/actions'
|
import { updateBlockNumber } from '../application/actions'
|
||||||
import { useBlockNumber } from '../application/hooks'
|
import { useBlockNumber } from '../application/hooks'
|
||||||
import { AppState } from '../index'
|
import { AppState } from '../index'
|
||||||
import {
|
import { errorFetchingMulticallResults, fetchingMulticallResults, updateMulticallResults } from './actions'
|
||||||
Call,
|
|
||||||
errorFetchingMulticallResults,
|
|
||||||
fetchingMulticallResults,
|
|
||||||
parseCallKey,
|
|
||||||
updateMulticallResults,
|
|
||||||
} from './actions'
|
|
||||||
import { useAppDispatch, useAppSelector } from 'state/hooks'
|
import { useAppDispatch, useAppSelector } from 'state/hooks'
|
||||||
|
import { Call, parseCallKey } from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches a chunk of calls, enforcing a minimum block number constraint
|
* 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