Commit 091876a3 authored by Noah Zinsmeister's avatar Noah Zinsmeister Committed by GitHub

feat: add `Queue` and `Execute` buttons (#3905)

* add queue and execute buttons

* eta is timestamp not block number

* address comments

* add execute text

* address comments
parent d0e4aa83
...@@ -17,7 +17,9 @@ import { ...@@ -17,7 +17,9 @@ import {
DepositLiquidityStakingTransactionInfo, DepositLiquidityStakingTransactionInfo,
ExactInputSwapTransactionInfo, ExactInputSwapTransactionInfo,
ExactOutputSwapTransactionInfo, ExactOutputSwapTransactionInfo,
ExecuteTransactionInfo,
MigrateV2LiquidityToV3TransactionInfo, MigrateV2LiquidityToV3TransactionInfo,
QueueTransactionInfo,
RemoveLiquidityV3TransactionInfo, RemoveLiquidityV3TransactionInfo,
SubmitProposalTransactionInfo, SubmitProposalTransactionInfo,
TransactionInfo, TransactionInfo,
...@@ -126,6 +128,16 @@ function VoteSummary({ info }: { info: VoteTransactionInfo }) { ...@@ -126,6 +128,16 @@ function VoteSummary({ info }: { info: VoteTransactionInfo }) {
} }
} }
function QueueSummary({ info }: { info: QueueTransactionInfo }) {
const proposalKey = `${info.governorAddress}/${info.proposalId}`
return <Trans>Queue proposal {proposalKey}.</Trans>
}
function ExecuteSummary({ info }: { info: ExecuteTransactionInfo }) {
const proposalKey = `${info.governorAddress}/${info.proposalId}`
return <Trans>Execute proposal {proposalKey}.</Trans>
}
function DelegateSummary({ info: { delegatee } }: { info: DelegateTransactionInfo }) { function DelegateSummary({ info: { delegatee } }: { info: DelegateTransactionInfo }) {
const { ENSName } = useENSName(delegatee) const { ENSName } = useENSName(delegatee)
return <Trans>Delegate voting power to {ENSName ?? delegatee}</Trans> return <Trans>Delegate voting power to {ENSName ?? delegatee}</Trans>
...@@ -339,6 +351,12 @@ export function TransactionSummary({ info }: { info: TransactionInfo }) { ...@@ -339,6 +351,12 @@ export function TransactionSummary({ info }: { info: TransactionInfo }) {
case TransactionType.REMOVE_LIQUIDITY_V3: case TransactionType.REMOVE_LIQUIDITY_V3:
return <RemoveLiquidityV3Summary info={info} /> return <RemoveLiquidityV3Summary info={info} />
case TransactionType.QUEUE:
return <QueueSummary info={info} />
case TransactionType.EXECUTE:
return <ExecuteSummary info={info} />
case TransactionType.SUBMIT_PROPOSAL: case TransactionType.SUBMIT_PROPOSAL:
return <SubmitProposalTransactionSummary info={info} /> return <SubmitProposalTransactionSummary info={info} />
} }
......
...@@ -42,7 +42,7 @@ export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: Staki ...@@ -42,7 +42,7 @@ export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: Staki
const [hash, setHash] = useState<string | undefined>() const [hash, setHash] = useState<string | undefined>()
const [attempting, setAttempting] = useState(false) const [attempting, setAttempting] = useState(false)
function wrappedOndismiss() { function wrappedOnDismiss() {
setHash(undefined) setHash(undefined)
setAttempting(false) setAttempting(false)
onDismiss() onDismiss()
...@@ -79,14 +79,14 @@ export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: Staki ...@@ -79,14 +79,14 @@ export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: Staki
} }
return ( return (
<Modal isOpen={isOpen} onDismiss={wrappedOndismiss} maxHeight={90}> <Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
{!attempting && !hash && ( {!attempting && !hash && (
<ContentWrapper gap="lg"> <ContentWrapper gap="lg">
<RowBetween> <RowBetween>
<ThemedText.MediumHeader> <ThemedText.MediumHeader>
<Trans>Withdraw</Trans> <Trans>Withdraw</Trans>
</ThemedText.MediumHeader> </ThemedText.MediumHeader>
<CloseIcon onClick={wrappedOndismiss} /> <CloseIcon onClick={wrappedOnDismiss} />
</RowBetween> </RowBetween>
{stakingInfo?.stakedAmount && ( {stakingInfo?.stakedAmount && (
<AutoColumn justify="center" gap="md"> <AutoColumn justify="center" gap="md">
...@@ -117,7 +117,7 @@ export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: Staki ...@@ -117,7 +117,7 @@ export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: Staki
</ContentWrapper> </ContentWrapper>
)} )}
{attempting && !hash && ( {attempting && !hash && (
<LoadingView onDismiss={wrappedOndismiss}> <LoadingView onDismiss={wrappedOnDismiss}>
<AutoColumn gap="12px" justify={'center'}> <AutoColumn gap="12px" justify={'center'}>
<ThemedText.Body fontSize={20}> <ThemedText.Body fontSize={20}>
<Trans>Withdrawing {stakingInfo?.stakedAmount?.toSignificant(4)} UNI-V2</Trans> <Trans>Withdrawing {stakingInfo?.stakedAmount?.toSignificant(4)} UNI-V2</Trans>
...@@ -129,7 +129,7 @@ export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: Staki ...@@ -129,7 +129,7 @@ export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: Staki
</LoadingView> </LoadingView>
)} )}
{hash && ( {hash && (
<SubmittedView onDismiss={wrappedOndismiss} hash={hash}> <SubmittedView onDismiss={wrappedOnDismiss} hash={hash}>
<AutoColumn gap="12px" justify={'center'}> <AutoColumn gap="12px" justify={'center'}>
<ThemedText.LargeHeader> <ThemedText.LargeHeader>
<Trans>Transaction Submitted</Trans> <Trans>Transaction Submitted</Trans>
......
...@@ -66,7 +66,7 @@ export default function DelegateModal({ isOpen, onDismiss, title }: VoteModalPro ...@@ -66,7 +66,7 @@ export default function DelegateModal({ isOpen, onDismiss, title }: VoteModalPro
const [attempting, setAttempting] = useState(false) const [attempting, setAttempting] = useState(false)
// wrapper to reset state on modal close // wrapper to reset state on modal close
function wrappedOndismiss() { function wrappedOnDismiss() {
setHash(undefined) setHash(undefined)
setAttempting(false) setAttempting(false)
onDismiss() onDismiss()
...@@ -90,13 +90,13 @@ export default function DelegateModal({ isOpen, onDismiss, title }: VoteModalPro ...@@ -90,13 +90,13 @@ export default function DelegateModal({ isOpen, onDismiss, title }: VoteModalPro
} }
return ( return (
<Modal isOpen={isOpen} onDismiss={wrappedOndismiss} maxHeight={90}> <Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
{!attempting && !hash && ( {!attempting && !hash && (
<ContentWrapper gap="lg"> <ContentWrapper gap="lg">
<AutoColumn gap="lg" justify="center"> <AutoColumn gap="lg" justify="center">
<RowBetween> <RowBetween>
<ThemedText.MediumHeader fontWeight={500}>{title}</ThemedText.MediumHeader> <ThemedText.MediumHeader fontWeight={500}>{title}</ThemedText.MediumHeader>
<StyledClosed stroke="black" onClick={wrappedOndismiss} /> <StyledClosed stroke="black" onClick={wrappedOnDismiss} />
</RowBetween> </RowBetween>
<ThemedText.Body> <ThemedText.Body>
<Trans>Earned UNI tokens represent voting shares in Uniswap governance.</Trans> <Trans>Earned UNI tokens represent voting shares in Uniswap governance.</Trans>
...@@ -119,7 +119,7 @@ export default function DelegateModal({ isOpen, onDismiss, title }: VoteModalPro ...@@ -119,7 +119,7 @@ export default function DelegateModal({ isOpen, onDismiss, title }: VoteModalPro
</ContentWrapper> </ContentWrapper>
)} )}
{attempting && !hash && ( {attempting && !hash && (
<LoadingView onDismiss={wrappedOndismiss}> <LoadingView onDismiss={wrappedOnDismiss}>
<AutoColumn gap="12px" justify={'center'}> <AutoColumn gap="12px" justify={'center'}>
<ThemedText.LargeHeader> <ThemedText.LargeHeader>
{usingDelegate ? <Trans>Delegating votes</Trans> : <Trans>Unlocking Votes</Trans>} {usingDelegate ? <Trans>Delegating votes</Trans> : <Trans>Unlocking Votes</Trans>}
...@@ -129,7 +129,7 @@ export default function DelegateModal({ isOpen, onDismiss, title }: VoteModalPro ...@@ -129,7 +129,7 @@ export default function DelegateModal({ isOpen, onDismiss, title }: VoteModalPro
</LoadingView> </LoadingView>
)} )}
{hash && ( {hash && (
<SubmittedView onDismiss={wrappedOndismiss} hash={hash}> <SubmittedView onDismiss={wrappedOnDismiss} hash={hash}>
<AutoColumn gap="12px" justify={'center'}> <AutoColumn gap="12px" justify={'center'}>
<ThemedText.LargeHeader> <ThemedText.LargeHeader>
<Trans>Transaction Submitted</Trans> <Trans>Transaction Submitted</Trans>
......
import { Trans } from '@lingui/macro'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useContext, useState } from 'react'
import { ArrowUpCircle, X } from 'react-feather'
import styled, { ThemeContext } from 'styled-components/macro'
import Circle from '../../assets/images/blue-loader.svg'
import { useExecuteCallback } from '../../state/governance/hooks'
import { CustomLightSpinner, ThemedText } from '../../theme'
import { ExternalLink } from '../../theme'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import { ButtonPrimary } from '../Button'
import { AutoColumn, ColumnCenter } from '../Column'
import Modal from '../Modal'
import { RowBetween } from '../Row'
const ContentWrapper = styled(AutoColumn)`
width: 100%;
padding: 24px;
`
const StyledClosed = styled(X)`
:hover {
cursor: pointer;
}
`
const ConfirmOrLoadingWrapper = styled.div`
width: 100%;
padding: 24px;
`
const ConfirmedIcon = styled(ColumnCenter)`
padding: 60px 0;
`
interface ExecuteModalProps {
isOpen: boolean
onDismiss: () => void
proposalId: string | undefined // id for the proposal to execute
}
export default function ExecuteModal({ isOpen, onDismiss, proposalId }: ExecuteModalProps) {
const { chainId } = useActiveWeb3React()
const executeCallback = useExecuteCallback()
// monitor call to help UI loading state
const [hash, setHash] = useState<string | undefined>()
const [attempting, setAttempting] = useState<boolean>(false)
// get theme for colors
const theme = useContext(ThemeContext)
// wrapper to reset state on modal close
function wrappedOnDismiss() {
setHash(undefined)
setAttempting(false)
onDismiss()
}
async function onExecute() {
setAttempting(true)
// if callback not returned properly ignore
if (!executeCallback) return
// try delegation and store hash
const hash = await executeCallback(proposalId)?.catch((error) => {
setAttempting(false)
console.log(error)
})
if (hash) {
setHash(hash)
}
}
return (
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
{!attempting && !hash && (
<ContentWrapper gap="lg">
<AutoColumn gap="lg" justify="center">
<RowBetween>
<ThemedText.MediumHeader fontWeight={500}>
<Trans>Execute Proposal {proposalId}</Trans>
</ThemedText.MediumHeader>
<StyledClosed onClick={wrappedOnDismiss} />
</RowBetween>
<RowBetween>
<ThemedText.Body>
<Trans>Executing this proposal will enact the calldata on-chain.</Trans>
</ThemedText.Body>
</RowBetween>
<ButtonPrimary onClick={onExecute}>
<ThemedText.MediumHeader color="white">
<Trans>Execute</Trans>
</ThemedText.MediumHeader>
</ButtonPrimary>
</AutoColumn>
</ContentWrapper>
)}
{attempting && !hash && (
<ConfirmOrLoadingWrapper>
<RowBetween>
<div />
<StyledClosed onClick={wrappedOnDismiss} />
</RowBetween>
<ConfirmedIcon>
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
</ConfirmedIcon>
<AutoColumn gap="100px" justify={'center'}>
<AutoColumn gap="12px" justify={'center'}>
<ThemedText.LargeHeader>
<Trans>Executing</Trans>
</ThemedText.LargeHeader>
</AutoColumn>
<ThemedText.SubHeader>
<Trans>Confirm this transaction in your wallet</Trans>
</ThemedText.SubHeader>
</AutoColumn>
</ConfirmOrLoadingWrapper>
)}
{hash && (
<ConfirmOrLoadingWrapper>
<RowBetween>
<div />
<StyledClosed onClick={wrappedOnDismiss} />
</RowBetween>
<ConfirmedIcon>
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
</ConfirmedIcon>
<AutoColumn gap="100px" justify={'center'}>
<AutoColumn gap="12px" justify={'center'}>
<ThemedText.LargeHeader>
<Trans>Execution Submitted</Trans>
</ThemedText.LargeHeader>
</AutoColumn>
{chainId && (
<ExternalLink
href={getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)}
style={{ marginLeft: '4px' }}
>
<ThemedText.SubHeader>
<Trans>View transaction on Explorer</Trans>
</ThemedText.SubHeader>
</ExternalLink>
)}
</AutoColumn>
</ConfirmOrLoadingWrapper>
)}
</Modal>
)
}
import { Trans } from '@lingui/macro'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useContext, useState } from 'react'
import { ArrowUpCircle, X } from 'react-feather'
import styled, { ThemeContext } from 'styled-components/macro'
import Circle from '../../assets/images/blue-loader.svg'
import { useQueueCallback } from '../../state/governance/hooks'
import { CustomLightSpinner, ThemedText } from '../../theme'
import { ExternalLink } from '../../theme'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import { ButtonPrimary } from '../Button'
import { AutoColumn, ColumnCenter } from '../Column'
import Modal from '../Modal'
import { RowBetween } from '../Row'
const ContentWrapper = styled(AutoColumn)`
width: 100%;
padding: 24px;
`
const StyledClosed = styled(X)`
:hover {
cursor: pointer;
}
`
const ConfirmOrLoadingWrapper = styled.div`
width: 100%;
padding: 24px;
`
const ConfirmedIcon = styled(ColumnCenter)`
padding: 60px 0;
`
interface QueueModalProps {
isOpen: boolean
onDismiss: () => void
proposalId: string | undefined // id for the proposal to queue
}
export default function QueueModal({ isOpen, onDismiss, proposalId }: QueueModalProps) {
const { chainId } = useActiveWeb3React()
const queueCallback = useQueueCallback()
// monitor call to help UI loading state
const [hash, setHash] = useState<string | undefined>()
const [attempting, setAttempting] = useState<boolean>(false)
// get theme for colors
const theme = useContext(ThemeContext)
// wrapper to reset state on modal close
function wrappedOnDismiss() {
setHash(undefined)
setAttempting(false)
onDismiss()
}
async function onQueue() {
setAttempting(true)
// if callback not returned properly ignore
if (!queueCallback) return
// try delegation and store hash
const hash = await queueCallback(proposalId)?.catch((error) => {
setAttempting(false)
console.log(error)
})
if (hash) {
setHash(hash)
}
}
return (
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
{!attempting && !hash && (
<ContentWrapper gap="lg">
<AutoColumn gap="lg" justify="center">
<RowBetween>
<ThemedText.MediumHeader fontWeight={500}>
<Trans>Queue Proposal {proposalId}</Trans>
</ThemedText.MediumHeader>
<StyledClosed onClick={wrappedOnDismiss} />
</RowBetween>
<RowBetween>
<ThemedText.Body>
<Trans>Adding this proposal to the queue will allow it to be executed, after a delay.</Trans>
</ThemedText.Body>
</RowBetween>
<ButtonPrimary onClick={onQueue}>
<ThemedText.MediumHeader color="white">
<Trans>Queue</Trans>
</ThemedText.MediumHeader>
</ButtonPrimary>
</AutoColumn>
</ContentWrapper>
)}
{attempting && !hash && (
<ConfirmOrLoadingWrapper>
<RowBetween>
<div />
<StyledClosed onClick={wrappedOnDismiss} />
</RowBetween>
<ConfirmedIcon>
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
</ConfirmedIcon>
<AutoColumn gap="100px" justify={'center'}>
<AutoColumn gap="12px" justify={'center'}>
<ThemedText.LargeHeader>
<Trans>Queueing</Trans>
</ThemedText.LargeHeader>
</AutoColumn>
<ThemedText.SubHeader>
<Trans>Confirm this transaction in your wallet</Trans>
</ThemedText.SubHeader>
</AutoColumn>
</ConfirmOrLoadingWrapper>
)}
{hash && (
<ConfirmOrLoadingWrapper>
<RowBetween>
<div />
<StyledClosed onClick={wrappedOnDismiss} />
</RowBetween>
<ConfirmedIcon>
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
</ConfirmedIcon>
<AutoColumn gap="100px" justify={'center'}>
<AutoColumn gap="12px" justify={'center'}>
<ThemedText.LargeHeader>
<Trans>Transaction Submitted</Trans>
</ThemedText.LargeHeader>
</AutoColumn>
{chainId && (
<ExternalLink
href={getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)}
style={{ marginLeft: '4px' }}
>
<ThemedText.SubHeader>
<Trans>View transaction on Explorer</Trans>
</ThemedText.SubHeader>
</ExternalLink>
)}
</AutoColumn>
</ConfirmOrLoadingWrapper>
)}
</Modal>
)
}
...@@ -45,7 +45,7 @@ interface VoteModalProps { ...@@ -45,7 +45,7 @@ interface VoteModalProps {
export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }: VoteModalProps) { export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }: VoteModalProps) {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const { voteCallback } = useVoteCallback() const voteCallback = useVoteCallback()
const { votes: availableVotes } = useUserVotes() const { votes: availableVotes } = useUserVotes()
// monitor call to help UI loading state // monitor call to help UI loading state
...@@ -56,7 +56,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }: ...@@ -56,7 +56,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }:
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
// wrapper to reset state on modal close // wrapper to reset state on modal close
function wrappedOndismiss() { function wrappedOnDismiss() {
setHash(undefined) setHash(undefined)
setAttempting(false) setAttempting(false)
onDismiss() onDismiss()
...@@ -80,7 +80,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }: ...@@ -80,7 +80,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }:
} }
return ( return (
<Modal isOpen={isOpen} onDismiss={wrappedOndismiss} maxHeight={90}> <Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
{!attempting && !hash && ( {!attempting && !hash && (
<ContentWrapper gap="lg"> <ContentWrapper gap="lg">
<AutoColumn gap="lg" justify="center"> <AutoColumn gap="lg" justify="center">
...@@ -94,7 +94,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }: ...@@ -94,7 +94,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }:
<Trans>Vote to abstain on proposal {proposalId}</Trans> <Trans>Vote to abstain on proposal {proposalId}</Trans>
)} )}
</ThemedText.MediumHeader> </ThemedText.MediumHeader>
<StyledClosed stroke="black" onClick={wrappedOndismiss} /> <StyledClosed onClick={wrappedOnDismiss} />
</RowBetween> </RowBetween>
<ThemedText.LargeHeader> <ThemedText.LargeHeader>
<Trans>{formatCurrencyAmount(availableVotes, 4)} Votes</Trans> <Trans>{formatCurrencyAmount(availableVotes, 4)} Votes</Trans>
...@@ -117,7 +117,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }: ...@@ -117,7 +117,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }:
<ConfirmOrLoadingWrapper> <ConfirmOrLoadingWrapper>
<RowBetween> <RowBetween>
<div /> <div />
<StyledClosed onClick={wrappedOndismiss} /> <StyledClosed onClick={wrappedOnDismiss} />
</RowBetween> </RowBetween>
<ConfirmedIcon> <ConfirmedIcon>
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} /> <CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
...@@ -138,7 +138,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }: ...@@ -138,7 +138,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }:
<ConfirmOrLoadingWrapper> <ConfirmOrLoadingWrapper>
<RowBetween> <RowBetween>
<div /> <div />
<StyledClosed onClick={wrappedOndismiss} /> <StyledClosed onClick={wrappedOnDismiss} />
</RowBetween> </RowBetween>
<ConfirmedIcon> <ConfirmedIcon>
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} /> <ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
......
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { CurrencyAmount, Fraction, Token } from '@uniswap/sdk-core' import { CurrencyAmount, Fraction, Token } from '@uniswap/sdk-core'
import ExecuteModal from 'components/vote/ExecuteModal'
import QueueModal from 'components/vote/QueueModal'
import { useActiveLocale } from 'hooks/useActiveLocale' import { useActiveLocale } from 'hooks/useActiveLocale'
import useActiveWeb3React from 'hooks/useActiveWeb3React' import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp' import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import useBlockNumber from 'lib/hooks/useBlockNumber' import useBlockNumber from 'lib/hooks/useBlockNumber'
import ms from 'ms.macro'
import { useState } from 'react' import { useState } from 'react'
import { ArrowLeft } from 'react-feather' import { ArrowLeft } from 'react-feather'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
...@@ -27,7 +30,13 @@ import { ...@@ -27,7 +30,13 @@ import {
} from '../../constants/governance' } from '../../constants/governance'
import { ZERO_ADDRESS } from '../../constants/misc' import { ZERO_ADDRESS } from '../../constants/misc'
import { UNI } from '../../constants/tokens' import { UNI } from '../../constants/tokens'
import { useModalOpen, useToggleDelegateModal, useToggleVoteModal } from '../../state/application/hooks' import {
useModalOpen,
useToggleDelegateModal,
useToggleExecuteModal,
useToggleQueueModal,
useToggleVoteModal,
} from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer' import { ApplicationModal } from '../../state/application/reducer'
import { import {
ProposalData, ProposalData,
...@@ -134,7 +143,7 @@ function getDateFromBlock( ...@@ -134,7 +143,7 @@ function getDateFromBlock(
date.setTime( date.setTime(
currentTimestamp currentTimestamp
.add(BigNumber.from(averageBlockTimeInSeconds).mul(BigNumber.from(targetBlock - currentBlock))) .add(BigNumber.from(averageBlockTimeInSeconds).mul(BigNumber.from(targetBlock - currentBlock)))
.toNumber() * 1000 .toNumber() * ms`1 second`
) )
return date return date
} }
...@@ -166,6 +175,14 @@ export default function VotePage({ ...@@ -166,6 +175,14 @@ export default function VotePage({
const showDelegateModal = useModalOpen(ApplicationModal.DELEGATE) const showDelegateModal = useModalOpen(ApplicationModal.DELEGATE)
const toggleDelegateModal = useToggleDelegateModal() const toggleDelegateModal = useToggleDelegateModal()
// toggle for showing queue modal
const showQueueModal = useModalOpen(ApplicationModal.QUEUE)
const toggleQueueModal = useToggleQueueModal()
// toggle for showing execute modal
const showExecuteModal = useModalOpen(ApplicationModal.EXECUTE)
const toggleExecuteModal = useToggleExecuteModal()
// get and format date from data // get and format date from data
const currentTimestamp = useCurrentBlockTimestamp() const currentTimestamp = useCurrentBlockTimestamp()
const currentBlock = useBlockNumber() const currentBlock = useBlockNumber()
...@@ -191,6 +208,8 @@ export default function VotePage({ ...@@ -191,6 +208,8 @@ export default function VotePage({
minute: 'numeric', minute: 'numeric',
timeZoneName: 'short', timeZoneName: 'short',
} }
// convert the eta to milliseconds before it's a date
const eta = proposalData?.eta ? new Date(proposalData.eta.mul(ms`1 second`).toNumber()) : undefined
// get total votes and format percentages for UI // get total votes and format percentages for UI
const totalVotes = proposalData?.forCount?.add(proposalData.againstCount) const totalVotes = proposalData?.forCount?.add(proposalData.againstCount)
...@@ -209,6 +228,12 @@ export default function VotePage({ ...@@ -209,6 +228,12 @@ export default function VotePage({
proposalData && proposalData &&
proposalData.status === ProposalState.ACTIVE proposalData.status === ProposalState.ACTIVE
// we only show the button if there's an account connected and the proposal state is correct
const showQueueButton = account && proposalData?.status === ProposalState.SUCCEEDED
// we only show the button if there's an account connected and the proposal state is correct
const showExecuteButton = account && proposalData?.status === ProposalState.QUEUED
const uniBalance: CurrencyAmount<Token> | undefined = useTokenBalance( const uniBalance: CurrencyAmount<Token> | undefined = useTokenBalance(
account ?? undefined, account ?? undefined,
chainId ? UNI[chainId] : undefined chainId ? UNI[chainId] : undefined
...@@ -242,6 +267,8 @@ export default function VotePage({ ...@@ -242,6 +267,8 @@ export default function VotePage({
voteOption={voteOption} voteOption={voteOption}
/> />
<DelegateModal isOpen={showDelegateModal} onDismiss={toggleDelegateModal} title={<Trans>Unlock Votes</Trans>} /> <DelegateModal isOpen={showDelegateModal} onDismiss={toggleDelegateModal} title={<Trans>Unlock Votes</Trans>} />
<QueueModal isOpen={showQueueModal} onDismiss={toggleQueueModal} proposalId={proposalData?.id} />
<ExecuteModal isOpen={showExecuteModal} onDismiss={toggleExecuteModal} proposalId={proposalData?.id} />
<ProposalInfo gap="lg" justify="start"> <ProposalInfo gap="lg" justify="start">
<RowBetween style={{ width: '100%' }}> <RowBetween style={{ width: '100%' }}>
<ArrowWrapper to="/vote"> <ArrowWrapper to="/vote">
...@@ -289,7 +316,7 @@ export default function VotePage({ ...@@ -289,7 +316,7 @@ export default function VotePage({
</GreyCard> </GreyCard>
)} )}
</AutoColumn> </AutoColumn>
{showVotingButtons ? ( {showVotingButtons && (
<RowFixed style={{ width: '100%', gap: '12px' }}> <RowFixed style={{ width: '100%', gap: '12px' }}>
<ButtonPrimary <ButtonPrimary
padding="8px" padding="8px"
...@@ -312,8 +339,43 @@ export default function VotePage({ ...@@ -312,8 +339,43 @@ export default function VotePage({
<Trans>Vote Against</Trans> <Trans>Vote Against</Trans>
</ButtonPrimary> </ButtonPrimary>
</RowFixed> </RowFixed>
) : ( )}
'' {showQueueButton && (
<RowFixed style={{ width: '100%', gap: '12px' }}>
<ButtonPrimary
padding="8px"
$borderRadius="8px"
onClick={() => {
toggleQueueModal()
}}
>
<Trans>Queue</Trans>
</ButtonPrimary>
</RowFixed>
)}
{showExecuteButton && (
<>
{eta && (
<RowBetween>
<ThemedText.Black>
<Trans>This proposal may be executed after {eta.toLocaleString(locale, dateFormat)}.</Trans>
</ThemedText.Black>
</RowBetween>
)}
<RowFixed style={{ width: '100%', gap: '12px' }}>
<ButtonPrimary
padding="8px"
$borderRadius="8px"
onClick={() => {
toggleExecuteModal()
}}
// can't execute until the eta has arrived
disabled={!currentTimestamp || !proposalData?.eta || currentTimestamp.lt(proposalData.eta)}
>
<Trans>Execute</Trans>
</ButtonPrimary>
</RowFixed>
</>
)} )}
<CardWrapper> <CardWrapper>
<StyledDataCard> <StyledDataCard>
......
...@@ -44,6 +44,14 @@ export function useToggleVoteModal(): () => void { ...@@ -44,6 +44,14 @@ export function useToggleVoteModal(): () => void {
return useToggleModal(ApplicationModal.VOTE) return useToggleModal(ApplicationModal.VOTE)
} }
export function useToggleQueueModal(): () => void {
return useToggleModal(ApplicationModal.QUEUE)
}
export function useToggleExecuteModal(): () => void {
return useToggleModal(ApplicationModal.EXECUTE)
}
export function useTogglePrivacyPolicy(): () => void { export function useTogglePrivacyPolicy(): () => void {
return useToggleModal(ApplicationModal.PRIVACY_POLICY) return useToggleModal(ApplicationModal.PRIVACY_POLICY)
} }
......
...@@ -26,6 +26,8 @@ export enum ApplicationModal { ...@@ -26,6 +26,8 @@ export enum ApplicationModal {
SETTINGS, SETTINGS,
VOTE, VOTE,
WALLET, WALLET,
QUEUE,
EXECUTE,
} }
type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }> type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>
......
import { defaultAbiCoder, Interface } from '@ethersproject/abi' import { defaultAbiCoder, Interface } from '@ethersproject/abi'
import { isAddress } from '@ethersproject/address' import { isAddress } from '@ethersproject/address'
import { BigNumber } from '@ethersproject/bignumber'
import { Contract } from '@ethersproject/contracts' import { Contract } from '@ethersproject/contracts'
import { TransactionResponse } from '@ethersproject/providers' import { TransactionResponse } from '@ethersproject/providers'
import { toUtf8String, Utf8ErrorFuncs, Utf8ErrorReason } from '@ethersproject/strings' import { toUtf8String, Utf8ErrorFuncs, Utf8ErrorReason } from '@ethersproject/strings'
...@@ -73,6 +74,7 @@ export interface ProposalData { ...@@ -73,6 +74,7 @@ export interface ProposalData {
againstCount: CurrencyAmount<Token> againstCount: CurrencyAmount<Token>
startBlock: number startBlock: number
endBlock: number endBlock: number
eta: BigNumber
details: ProposalDetail[] details: ProposalDetail[]
governorIndex: number // index in the governance address array for which this proposal pertains governorIndex: number // index in the governance address array for which this proposal pertains
} }
...@@ -301,6 +303,7 @@ export function useAllProposalData(): { data: ProposalData[]; loading: boolean } ...@@ -301,6 +303,7 @@ export function useAllProposalData(): { data: ProposalData[]; loading: boolean }
againstCount: CurrencyAmount.fromRawAmount(uni, proposal?.result?.againstVotes), againstCount: CurrencyAmount.fromRawAmount(uni, proposal?.result?.againstVotes),
startBlock, startBlock,
endBlock: parseInt(proposal?.result?.endBlock?.toString()), endBlock: parseInt(proposal?.result?.endBlock?.toString()),
eta: proposal?.result?.eta,
details: formattedLogs[i]?.details, details: formattedLogs[i]?.details,
governorIndex: i >= proposalsV0.length + proposalsV1.length ? 2 : i >= proposalsV0.length ? 1 : 0, governorIndex: i >= proposalsV0.length + proposalsV1.length ? 2 : i >= proposalsV0.length ? 1 : 0,
} }
...@@ -407,16 +410,15 @@ export function useDelegateCallback(): (delegatee: string | undefined) => undefi ...@@ -407,16 +410,15 @@ export function useDelegateCallback(): (delegatee: string | undefined) => undefi
) )
} }
export function useVoteCallback(): { export function useVoteCallback(): (
voteCallback: (proposalId: string | undefined, voteOption: VoteOption) => undefined | Promise<string> proposalId: string | undefined,
} { voteOption: VoteOption
) => undefined | Promise<string> {
const { account, chainId } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
const latestGovernanceContract = useLatestGovernanceContract() const latestGovernanceContract = useLatestGovernanceContract()
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
const voteCallback = useCallback( return useCallback(
(proposalId: string | undefined, voteOption: VoteOption) => { (proposalId: string | undefined, voteOption: VoteOption) => {
if (!account || !latestGovernanceContract || !proposalId || !chainId) return if (!account || !latestGovernanceContract || !proposalId || !chainId) return
const args = [proposalId, voteOption === VoteOption.Against ? 0 : voteOption === VoteOption.For ? 1 : 2] const args = [proposalId, voteOption === VoteOption.Against ? 0 : voteOption === VoteOption.For ? 1 : 2]
...@@ -437,14 +439,64 @@ export function useVoteCallback(): { ...@@ -437,14 +439,64 @@ export function useVoteCallback(): {
}, },
[account, addTransaction, latestGovernanceContract, chainId] [account, addTransaction, latestGovernanceContract, chainId]
) )
return { voteCallback } }
export function useQueueCallback(): (proposalId: string | undefined) => undefined | Promise<string> {
const { account, chainId } = useActiveWeb3React()
const latestGovernanceContract = useLatestGovernanceContract()
const addTransaction = useTransactionAdder()
return useCallback(
(proposalId: string | undefined) => {
if (!account || !latestGovernanceContract || !proposalId || !chainId) return
const args = [proposalId]
return latestGovernanceContract.estimateGas.queue(...args, {}).then((estimatedGasLimit) => {
return latestGovernanceContract
.queue(...args, { value: null, gasLimit: calculateGasMargin(estimatedGasLimit) })
.then((response: TransactionResponse) => {
addTransaction(response, {
type: TransactionType.QUEUE,
governorAddress: latestGovernanceContract.address,
proposalId: parseInt(proposalId),
})
return response.hash
})
})
},
[account, addTransaction, latestGovernanceContract, chainId]
)
}
export function useExecuteCallback(): (proposalId: string | undefined) => undefined | Promise<string> {
const { account, chainId } = useActiveWeb3React()
const latestGovernanceContract = useLatestGovernanceContract()
const addTransaction = useTransactionAdder()
return useCallback(
(proposalId: string | undefined) => {
if (!account || !latestGovernanceContract || !proposalId || !chainId) return
const args = [proposalId]
return latestGovernanceContract.estimateGas.execute(...args, {}).then((estimatedGasLimit) => {
return latestGovernanceContract
.execute(...args, { value: null, gasLimit: calculateGasMargin(estimatedGasLimit) })
.then((response: TransactionResponse) => {
addTransaction(response, {
type: TransactionType.EXECUTE,
governorAddress: latestGovernanceContract.address,
proposalId: parseInt(proposalId),
})
return response.hash
})
})
},
[account, addTransaction, latestGovernanceContract, chainId]
)
} }
export function useCreateProposalCallback(): ( export function useCreateProposalCallback(): (
createProposalData: CreateProposalData | undefined createProposalData: CreateProposalData | undefined
) => undefined | Promise<string> { ) => undefined | Promise<string> {
const { account, chainId } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
const latestGovernanceContract = useLatestGovernanceContract() const latestGovernanceContract = useLatestGovernanceContract()
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
......
...@@ -19,20 +19,22 @@ interface SerializableTransactionReceipt { ...@@ -19,20 +19,22 @@ interface SerializableTransactionReceipt {
*/ */
export enum TransactionType { export enum TransactionType {
APPROVAL = 0, APPROVAL = 0,
SWAP = 1, SWAP,
DEPOSIT_LIQUIDITY_STAKING = 2, DEPOSIT_LIQUIDITY_STAKING,
WITHDRAW_LIQUIDITY_STAKING = 3, WITHDRAW_LIQUIDITY_STAKING,
CLAIM = 4, CLAIM,
VOTE = 5, VOTE,
DELEGATE = 6, DELEGATE,
WRAP = 7, WRAP,
CREATE_V3_POOL = 8, CREATE_V3_POOL,
ADD_LIQUIDITY_V3_POOL = 9, ADD_LIQUIDITY_V3_POOL,
ADD_LIQUIDITY_V2_POOL = 10, ADD_LIQUIDITY_V2_POOL,
MIGRATE_LIQUIDITY_V3 = 11, MIGRATE_LIQUIDITY_V3,
COLLECT_FEES = 12, COLLECT_FEES,
REMOVE_LIQUIDITY_V3 = 13, REMOVE_LIQUIDITY_V3,
SUBMIT_PROPOSAL = 14, SUBMIT_PROPOSAL,
QUEUE,
EXECUTE,
} }
export interface BaseTransactionInfo { export interface BaseTransactionInfo {
...@@ -47,6 +49,18 @@ export interface VoteTransactionInfo extends BaseTransactionInfo { ...@@ -47,6 +49,18 @@ export interface VoteTransactionInfo extends BaseTransactionInfo {
reason: string reason: string
} }
export interface QueueTransactionInfo extends BaseTransactionInfo {
type: TransactionType.QUEUE
governorAddress: string
proposalId: number
}
export interface ExecuteTransactionInfo extends BaseTransactionInfo {
type: TransactionType.EXECUTE
governorAddress: string
proposalId: number
}
export interface DelegateTransactionInfo extends BaseTransactionInfo { export interface DelegateTransactionInfo extends BaseTransactionInfo {
type: TransactionType.DELEGATE type: TransactionType.DELEGATE
delegatee: string delegatee: string
...@@ -158,6 +172,8 @@ export type TransactionInfo = ...@@ -158,6 +172,8 @@ export type TransactionInfo =
| ExactInputSwapTransactionInfo | ExactInputSwapTransactionInfo
| ClaimTransactionInfo | ClaimTransactionInfo
| VoteTransactionInfo | VoteTransactionInfo
| QueueTransactionInfo
| ExecuteTransactionInfo
| DelegateTransactionInfo | DelegateTransactionInfo
| DepositLiquidityStakingTransactionInfo | DepositLiquidityStakingTransactionInfo
| WithdrawLiquidityStakingTransactionInfo | WithdrawLiquidityStakingTransactionInfo
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment