feat: Add proposal creation interface (#1799)
* Make address input panel more generalized * Add TextInput component * Allow adjusting AppBody maxwidth * Add create proposal layout page * Fix style consistency * create proposal ui goes brrr * various fixes * minor tweaks and bug fixes * merge conflict clean up + bug fix * i18n * always show create proposal button * adjust proposal title top margin and show ButtonError styling * use button disable instead of button error when form is not filled * revert mistaken css change * default params for address input panel * Refactor & fixes - Add missing i18n - Refactor how some styled-components is done - ButtonError is now disabled when proposal is true in create proposal interface - Edit copywriting - Minor styling adjustments * Fixed create proposal padding on medium screen * refactor * refactor pt. 2 * single column styling * change AddressInputPanel font color * Design adjustments * change route for create proposal * Add autonomous proposal CTA * cta text edit * Add link to docs for custom action * small cleanup * work with latest governor Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
This commit is contained in:
parent
c2fe17615f
commit
bb542ef0fb
@ -40,7 +40,7 @@ const Input = styled.input<{ error?: boolean }>`
|
||||
width: 0;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
||||
color: ${({ error, theme }) => (error ? theme.red1 : theme.primary1)};
|
||||
color: ${({ error, theme }) => (error ? theme.red1 : theme.text1)};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
@ -67,10 +67,16 @@ const Input = styled.input<{ error?: boolean }>`
|
||||
|
||||
export default function AddressInputPanel({
|
||||
id,
|
||||
className = 'recipient-address-input',
|
||||
label = 'Recipient',
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
id?: string
|
||||
className?: string
|
||||
label?: string
|
||||
placeholder?: string
|
||||
// the typed string value
|
||||
value: string
|
||||
// triggers whenever the typed value changes
|
||||
@ -99,7 +105,7 @@ export default function AddressInputPanel({
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
|
||||
Recipient
|
||||
{label}
|
||||
</TYPE.black>
|
||||
{address && chainId && (
|
||||
<ExternalLink
|
||||
@ -111,13 +117,13 @@ export default function AddressInputPanel({
|
||||
)}
|
||||
</RowBetween>
|
||||
<Input
|
||||
className="recipient-address-input"
|
||||
className={className}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
placeholder="Wallet Address or ENS name"
|
||||
placeholder={placeholder ?? 'Wallet Address or ENS name'}
|
||||
error={error}
|
||||
pattern="^(0x[a-fA-F0-9]{40})$"
|
||||
onChange={handleInput}
|
||||
|
@ -160,6 +160,8 @@ interface CurrencyInputPanelProps {
|
||||
priceImpact?: Percent
|
||||
id: string
|
||||
showCommonBases?: boolean
|
||||
showCurrencyAmount?: boolean
|
||||
disableNonToken?: boolean
|
||||
renderBalance?: (amount: CurrencyAmount<Currency>) => ReactNode
|
||||
locked?: boolean
|
||||
}
|
||||
@ -174,6 +176,8 @@ export default function CurrencyInputPanel({
|
||||
otherCurrency,
|
||||
id,
|
||||
showCommonBases,
|
||||
showCurrencyAmount,
|
||||
disableNonToken,
|
||||
renderBalance,
|
||||
fiatValue,
|
||||
priceImpact,
|
||||
@ -298,6 +302,8 @@ export default function CurrencyInputPanel({
|
||||
selectedCurrency={currency}
|
||||
otherSelectedCurrency={otherCurrency}
|
||||
showCommonBases={showCommonBases}
|
||||
showCurrencyAmount={showCurrencyAmount}
|
||||
disableNonToken={disableNonToken}
|
||||
/>
|
||||
)}
|
||||
</InputPanel>
|
||||
|
@ -6,7 +6,7 @@ import { NavLink, Link as HistoryLink, useLocation } from 'react-router-dom'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { RowBetween } from '../Row'
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import SettingsTab from '../Settings'
|
||||
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
@ -137,3 +137,16 @@ export function AddRemoveTabs({
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
export function CreateProposalTabs() {
|
||||
return (
|
||||
<Tabs>
|
||||
<Row style={{ padding: '1rem 1rem 0 1rem' }}>
|
||||
<HistoryLink to="/vote">
|
||||
<StyledArrowLeft />
|
||||
</HistoryLink>
|
||||
<ActiveText style={{ marginLeft: 'auto', marginRight: 'auto' }}>Create Proposal</ActiveText>
|
||||
</Row>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
@ -104,12 +104,14 @@ function CurrencyRow({
|
||||
isSelected,
|
||||
otherSelected,
|
||||
style,
|
||||
showCurrencyAmount,
|
||||
}: {
|
||||
currency: Currency
|
||||
onSelect: () => void
|
||||
isSelected: boolean
|
||||
otherSelected: boolean
|
||||
style: CSSProperties
|
||||
showCurrencyAmount?: boolean
|
||||
}) {
|
||||
const { account } = useActiveWeb3React()
|
||||
const key = currencyKey(currency)
|
||||
@ -141,9 +143,11 @@ function CurrencyRow({
|
||||
</TYPE.darkGray>
|
||||
</Column>
|
||||
<TokenTags currency={currency} />
|
||||
<RowFixed style={{ justifySelf: 'flex-end' }}>
|
||||
{balance ? <Balance balance={balance} /> : account ? <Loader /> : null}
|
||||
</RowFixed>
|
||||
{showCurrencyAmount && (
|
||||
<RowFixed style={{ justifySelf: 'flex-end' }}>
|
||||
{balance ? <Balance balance={balance} /> : account ? <Loader /> : null}
|
||||
</RowFixed>
|
||||
)}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
@ -189,6 +193,7 @@ export default function CurrencyList({
|
||||
fixedListRef,
|
||||
showImportView,
|
||||
setImportToken,
|
||||
showCurrencyAmount,
|
||||
}: {
|
||||
height: number
|
||||
currencies: Currency[]
|
||||
@ -199,6 +204,7 @@ export default function CurrencyList({
|
||||
fixedListRef?: MutableRefObject<FixedSizeList | undefined>
|
||||
showImportView: () => void
|
||||
setImportToken: (token: Token) => void
|
||||
showCurrencyAmount?: boolean
|
||||
}) {
|
||||
const itemData: (Currency | BreakLine)[] = useMemo(() => {
|
||||
if (otherListTokens && otherListTokens?.length > 0) {
|
||||
@ -237,13 +243,22 @@ export default function CurrencyList({
|
||||
isSelected={isSelected}
|
||||
onSelect={handleSelect}
|
||||
otherSelected={otherSelected}
|
||||
showCurrencyAmount={showCurrencyAmount}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
[currencies.length, onCurrencySelect, otherCurrency, selectedCurrency, setImportToken, showImportView]
|
||||
[
|
||||
currencies.length,
|
||||
onCurrencySelect,
|
||||
otherCurrency,
|
||||
selectedCurrency,
|
||||
setImportToken,
|
||||
showImportView,
|
||||
showCurrencyAmount,
|
||||
]
|
||||
)
|
||||
|
||||
const itemKey = useCallback((index: number, data: typeof itemData) => {
|
||||
|
@ -48,6 +48,8 @@ interface CurrencySearchProps {
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherSelectedCurrency?: Currency | null
|
||||
showCommonBases?: boolean
|
||||
showCurrencyAmount?: boolean
|
||||
disableNonToken?: boolean
|
||||
showManageView: () => void
|
||||
showImportView: () => void
|
||||
setImportToken: (token: Token) => void
|
||||
@ -58,6 +60,8 @@ export function CurrencySearch({
|
||||
onCurrencySelect,
|
||||
otherSelectedCurrency,
|
||||
showCommonBases,
|
||||
showCurrencyAmount,
|
||||
disableNonToken,
|
||||
onDismiss,
|
||||
isOpen,
|
||||
showManageView,
|
||||
@ -203,7 +207,7 @@ export function CurrencySearch({
|
||||
{({ height }) => (
|
||||
<CurrencyList
|
||||
height={height}
|
||||
currencies={filteredSortedTokensWithETH}
|
||||
currencies={disableNonToken ? filteredSortedTokens : filteredSortedTokensWithETH}
|
||||
otherListTokens={filteredInactiveTokens}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
otherCurrency={otherSelectedCurrency}
|
||||
@ -211,6 +215,7 @@ export function CurrencySearch({
|
||||
fixedListRef={fixedList}
|
||||
showImportView={showImportView}
|
||||
setImportToken={setImportToken}
|
||||
showCurrencyAmount={showCurrencyAmount}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
@ -17,6 +17,8 @@ interface CurrencySearchModalProps {
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherSelectedCurrency?: Currency | null
|
||||
showCommonBases?: boolean
|
||||
showCurrencyAmount?: boolean
|
||||
disableNonToken?: boolean
|
||||
}
|
||||
|
||||
export enum CurrencyModalView {
|
||||
@ -33,6 +35,8 @@ export default function CurrencySearchModal({
|
||||
selectedCurrency,
|
||||
otherSelectedCurrency,
|
||||
showCommonBases = false,
|
||||
showCurrencyAmount = true,
|
||||
disableNonToken = false,
|
||||
}: CurrencySearchModalProps) {
|
||||
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.manage)
|
||||
const lastOpen = useLast(isOpen)
|
||||
@ -74,6 +78,8 @@ export default function CurrencySearchModal({
|
||||
selectedCurrency={selectedCurrency}
|
||||
otherSelectedCurrency={otherSelectedCurrency}
|
||||
showCommonBases={showCommonBases}
|
||||
showCurrencyAmount={showCurrencyAmount}
|
||||
disableNonToken={disableNonToken}
|
||||
showImportView={() => setModalView(CurrencyModalView.importToken)}
|
||||
setImportToken={setImportToken}
|
||||
showManageView={() => setModalView(CurrencyModalView.manage)}
|
||||
|
146
src/components/TextInput/index.tsx
Normal file
146
src/components/TextInput/index.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import React, { memo, useCallback, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const Input = styled.input<{ error?: boolean; fontSize?: string }>`
|
||||
font-size: ${({ fontSize }) => fontSize || '1.25rem'};
|
||||
outline: none;
|
||||
border: none;
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
||||
color: ${({ error, theme }) => (error ? theme.red1 : theme.text1)};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
-webkit-appearance: textfield;
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-outer-spin-button,
|
||||
::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.text4};
|
||||
}
|
||||
`
|
||||
|
||||
const TextAreaInput = styled.textarea<{ error?: boolean; fontSize?: string }>`
|
||||
font-size: ${({ fontSize }) => fontSize || '1.25rem'};
|
||||
outline: none;
|
||||
border: none;
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
resize: none;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
||||
color: ${({ error, theme }) => (error ? theme.red1 : theme.text1)};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
width: 100%;
|
||||
line-height: 1.2;
|
||||
padding: 0px;
|
||||
-webkit-appearance: textfield;
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-outer-spin-button,
|
||||
::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.text4};
|
||||
}
|
||||
`
|
||||
|
||||
export const TextInput = ({
|
||||
className,
|
||||
value,
|
||||
onUserInput,
|
||||
placeholder,
|
||||
fontSize,
|
||||
}: {
|
||||
className?: string
|
||||
value: string
|
||||
onUserInput: (value: string) => void
|
||||
placeholder: string
|
||||
fontSize: string
|
||||
}) => {
|
||||
const handleInput = useCallback(
|
||||
(event) => {
|
||||
onUserInput(event.target.value)
|
||||
},
|
||||
[onUserInput]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
placeholder={placeholder || ''}
|
||||
onChange={handleInput}
|
||||
value={value}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ResizingTextArea = memo(
|
||||
({
|
||||
className,
|
||||
value,
|
||||
onUserInput,
|
||||
placeholder,
|
||||
fontSize,
|
||||
}: {
|
||||
className?: string
|
||||
value: string
|
||||
onUserInput: (value: string) => void
|
||||
placeholder: string
|
||||
fontSize: string
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLTextAreaElement>(document.createElement('textarea'))
|
||||
|
||||
const handleInput = useCallback(
|
||||
(event) => {
|
||||
inputRef.current.style.height = 'auto'
|
||||
inputRef.current.style.height = inputRef.current.scrollHeight + 'px'
|
||||
onUserInput(event.target.value)
|
||||
},
|
||||
[onUserInput]
|
||||
)
|
||||
|
||||
return (
|
||||
<TextAreaInput
|
||||
style={{ height: 'auto', minHeight: '500px' }}
|
||||
className={className}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
placeholder={placeholder || ''}
|
||||
onChange={handleInput}
|
||||
value={value}
|
||||
fontSize={fontSize}
|
||||
ref={inputRef}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ResizingTextArea.displayName = 'ResizingTextArea'
|
@ -29,6 +29,7 @@ import { RedirectDuplicateTokenIdsV2 } from './AddLiquidityV2/redirects'
|
||||
import { PositionPage } from './Pool/PositionPage'
|
||||
import AddLiquidity from './AddLiquidity'
|
||||
import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader'
|
||||
import CreateProposal from './CreateProposal'
|
||||
|
||||
const AppWrapper = styled.div`
|
||||
display: flex;
|
||||
@ -40,14 +41,13 @@ const BodyWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding-top: 120px;
|
||||
padding: 120px 16px 0px 16px;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
z-index: 1;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
padding: 16px;
|
||||
padding-top: 6rem;
|
||||
padding: 6rem 16px 16px 16px;
|
||||
`};
|
||||
`
|
||||
|
||||
@ -122,6 +122,7 @@ export default function App() {
|
||||
<Route exact strict path="/migrate/v2" component={MigrateV2} />
|
||||
<Route exact strict path="/migrate/v2/:address" component={MigrateV2Pair} />
|
||||
|
||||
<Route exact strict path="/create-proposal" component={CreateProposal} />
|
||||
<Route component={RedirectPathToSwapOnly} />
|
||||
</Switch>
|
||||
</Web3ReactManager>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
export const BodyWrapper = styled.div<{ margin?: string }>`
|
||||
export const BodyWrapper = styled.div<{ margin?: string; maxWidth?: string }>`
|
||||
position: relative;
|
||||
margin-top: ${({ margin }) => margin ?? '0px'};
|
||||
max-width: 480px;
|
||||
max-width: ${({ maxWidth }) => maxWidth ?? '480px'};
|
||||
width: 100%;
|
||||
background: ${({ theme }) => theme.bg0};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
|
83
src/pages/CreateProposal/ProposalActionDetail.tsx
Normal file
83
src/pages/CreateProposal/ProposalActionDetail.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import React from 'react'
|
||||
import AddressInputPanel from 'components/AddressInputPanel'
|
||||
import CurrencyInputPanel from 'components/CurrencyInputPanel'
|
||||
import styled from 'styled-components'
|
||||
import { ProposalAction } from './ProposalActionSelector'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
|
||||
enum ProposalActionDetailField {
|
||||
ADDRESS,
|
||||
CURRENCY,
|
||||
}
|
||||
|
||||
const ProposalActionDetailContainer = styled.div`
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
grid-gap: 10px;
|
||||
`
|
||||
|
||||
export const ProposalActionDetail = ({
|
||||
className,
|
||||
proposalAction,
|
||||
currency,
|
||||
amount,
|
||||
toAddress,
|
||||
onCurrencySelect,
|
||||
onAmountInput,
|
||||
onToAddressInput,
|
||||
}: {
|
||||
className?: string
|
||||
proposalAction: ProposalAction
|
||||
currency: Currency | undefined
|
||||
amount: string
|
||||
toAddress: string
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
onAmountInput: (amount: string) => void
|
||||
onToAddressInput: (address: string) => void
|
||||
}) => {
|
||||
const proposalActionsData = {
|
||||
[ProposalAction.TRANSFER_TOKEN]: [
|
||||
{
|
||||
type: ProposalActionDetailField.ADDRESS,
|
||||
label: 'To',
|
||||
},
|
||||
{
|
||||
type: ProposalActionDetailField.CURRENCY,
|
||||
},
|
||||
],
|
||||
[ProposalAction.APPROVE_TOKEN]: [
|
||||
{
|
||||
type: ProposalActionDetailField.ADDRESS,
|
||||
label: 'To',
|
||||
},
|
||||
{
|
||||
type: ProposalActionDetailField.CURRENCY,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return (
|
||||
<ProposalActionDetailContainer className={className}>
|
||||
{proposalActionsData[proposalAction].map((field, i) =>
|
||||
field.type === ProposalActionDetailField.ADDRESS ? (
|
||||
<AddressInputPanel key={i} label={field.label} value={toAddress} onChange={onToAddressInput} />
|
||||
) : field.type === ProposalActionDetailField.CURRENCY ? (
|
||||
<CurrencyInputPanel
|
||||
key={i}
|
||||
value={amount}
|
||||
currency={currency}
|
||||
onUserInput={(amount: string) => onAmountInput(amount)}
|
||||
onCurrencySelect={(currency: Currency) => onCurrencySelect(currency)}
|
||||
showMaxButton={false}
|
||||
showCommonBases={false}
|
||||
showCurrencyAmount={false}
|
||||
disableNonToken={true}
|
||||
hideBalance={true}
|
||||
id="currency-input"
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</ProposalActionDetailContainer>
|
||||
)
|
||||
}
|
129
src/pages/CreateProposal/ProposalActionSelector.tsx
Normal file
129
src/pages/CreateProposal/ProposalActionSelector.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Text } from 'rebass'
|
||||
import { CloseIcon } from 'theme'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import Column from 'components/Column'
|
||||
import Modal from 'components/Modal'
|
||||
import { RowBetween } from 'components/Row'
|
||||
import { MenuItem, PaddedColumn, Separator } from 'components/SearchModal/styleds'
|
||||
import { ButtonDropdown } from 'components/Button'
|
||||
|
||||
export enum ProposalAction {
|
||||
TRANSFER_TOKEN = 'Transfer Token',
|
||||
APPROVE_TOKEN = 'Approve Token',
|
||||
}
|
||||
|
||||
interface ProposalActionSelectorModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
onProposalActionSelect: (proposalAction: ProposalAction) => void
|
||||
}
|
||||
|
||||
const ContentWrapper = styled(Column)`
|
||||
width: 100%;
|
||||
flex: 1 1;
|
||||
position: relative;
|
||||
`
|
||||
const ActionSelectorHeader = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
`
|
||||
|
||||
const ActionDropdown = styled(ButtonDropdown)`
|
||||
padding: 0px;
|
||||
background-color: transparent;
|
||||
color: ${({ theme }) => theme.text1}
|
||||
font-size: 1.25rem;
|
||||
|
||||
:hover,
|
||||
:active,
|
||||
:focus {
|
||||
outline: 0px;
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
const ProposalActionSelectorFlex = styled.div`
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
border-radius: 20px;
|
||||
border: 1px solid ${({ theme }) => theme.bg2};
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
`
|
||||
|
||||
const ProposalActionSelectorContainer = styled.div`
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: 10px;
|
||||
`
|
||||
|
||||
export const ProposalActionSelector = ({
|
||||
className,
|
||||
onClick,
|
||||
proposalAction,
|
||||
}: {
|
||||
className?: string
|
||||
onClick: () => void
|
||||
proposalAction: ProposalAction
|
||||
}) => {
|
||||
return (
|
||||
<ProposalActionSelectorFlex>
|
||||
<ProposalActionSelectorContainer className={className}>
|
||||
<ActionSelectorHeader>
|
||||
<Trans>Proposed Action</Trans>
|
||||
</ActionSelectorHeader>
|
||||
<ActionDropdown onClick={onClick}>{proposalAction}</ActionDropdown>
|
||||
</ProposalActionSelectorContainer>
|
||||
</ProposalActionSelectorFlex>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProposalActionSelectorModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onProposalActionSelect,
|
||||
}: ProposalActionSelectorModalProps) {
|
||||
const handleProposalActionSelect = useCallback(
|
||||
(proposalAction: ProposalAction) => {
|
||||
onProposalActionSelect(proposalAction)
|
||||
onDismiss()
|
||||
},
|
||||
[onDismiss, onProposalActionSelect]
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss}>
|
||||
<ContentWrapper>
|
||||
<PaddedColumn gap="16px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
<Trans>Select an action</Trans>
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<Separator />
|
||||
<MenuItem onClick={() => handleProposalActionSelect(ProposalAction.TRANSFER_TOKEN)}>
|
||||
<Column>
|
||||
<Text fontWeight={500}>
|
||||
<Trans>Transfer Token</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => handleProposalActionSelect(ProposalAction.APPROVE_TOKEN)}>
|
||||
<Column>
|
||||
<Text fontWeight={500}>
|
||||
<Trans>Approve Token</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
</MenuItem>
|
||||
</ContentWrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
63
src/pages/CreateProposal/ProposalEditor.tsx
Normal file
63
src/pages/CreateProposal/ProposalEditor.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React, { memo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Text } from 'rebass'
|
||||
import { ResizingTextArea, TextInput } from 'components/TextInput'
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
|
||||
const ProposalEditorHeader = styled(Text)`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
`
|
||||
|
||||
const ProposalTitle = memo(styled(TextInput)`
|
||||
margin-top: 10.5px;
|
||||
margin-bottom: 7.5px;
|
||||
`)
|
||||
|
||||
const ProposalEditorContainer = styled.div`
|
||||
margin-top: 10px;
|
||||
padding: 0.75rem 1rem 0.75rem 1rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid ${({ theme }) => theme.bg2};
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
`
|
||||
|
||||
export const ProposalEditor = ({
|
||||
className,
|
||||
title,
|
||||
body,
|
||||
onTitleInput,
|
||||
onBodyInput,
|
||||
}: {
|
||||
className?: string
|
||||
title: string
|
||||
body: string
|
||||
onTitleInput: (title: string) => void
|
||||
onBodyInput: (body: string) => void
|
||||
}) => {
|
||||
const bodyPlaceholder = `## Summary
|
||||
|
||||
Insert your summary here
|
||||
|
||||
## Methodology
|
||||
|
||||
Insert your methodology here
|
||||
|
||||
## Conclusion
|
||||
|
||||
Insert your conclusion here
|
||||
|
||||
`
|
||||
|
||||
return (
|
||||
<ProposalEditorContainer className={className}>
|
||||
<ProposalEditorHeader>
|
||||
<Trans>Proposal</Trans>
|
||||
</ProposalEditorHeader>
|
||||
<ProposalTitle value={title} onUserInput={onTitleInput} placeholder={t`Proposal Title`} fontSize="1.25rem" />
|
||||
<hr />
|
||||
<ResizingTextArea value={body} onUserInput={onBodyInput} placeholder={bodyPlaceholder} fontSize="1rem" />
|
||||
</ProposalEditorContainer>
|
||||
)
|
||||
}
|
57
src/pages/CreateProposal/ProposalSubmissionModal.tsx
Normal file
57
src/pages/CreateProposal/ProposalSubmissionModal.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { Text } from 'rebass'
|
||||
import { ExternalLink, TYPE } from 'theme'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import Modal from 'components/Modal'
|
||||
import { LoadingView, SubmittedView } from 'components/ModalViews'
|
||||
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Trans } from '@lingui/macro'
|
||||
|
||||
export const ProposalSubmissionModal = ({
|
||||
isOpen,
|
||||
hash,
|
||||
onDismiss,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
hash: string | undefined
|
||||
onDismiss: () => void
|
||||
}) => {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss}>
|
||||
{!hash ? (
|
||||
<LoadingView onDismiss={onDismiss}>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<TYPE.largeHeader>
|
||||
<Trans>Submitting Proposal</Trans>
|
||||
</TYPE.largeHeader>
|
||||
</AutoColumn>
|
||||
</LoadingView>
|
||||
) : (
|
||||
<SubmittedView onDismiss={onDismiss} hash={hash}>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<Text fontWeight={500} fontSize={20} textAlign="center">
|
||||
<Trans>Proposal Submitted</Trans>
|
||||
</Text>
|
||||
{hash && (
|
||||
<ExternalLink href={getExplorerLink(1, hash, ExplorerDataType.TRANSACTION)}>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||
<Trans>View on Etherscan</Trans>
|
||||
</Text>
|
||||
</ExternalLink>
|
||||
)}
|
||||
<ButtonPrimary as={Link} to="/vote" onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
<Trans>Return</Trans>
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</AutoColumn>
|
||||
</SubmittedView>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
285
src/pages/CreateProposal/index.tsx
Normal file
285
src/pages/CreateProposal/index.tsx
Normal file
@ -0,0 +1,285 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import JSBI from 'jsbi'
|
||||
import styled from 'styled-components'
|
||||
import { utils } from 'ethers'
|
||||
import { ExternalLink, TYPE } from 'theme'
|
||||
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { UNI } from '../../constants/tokens'
|
||||
import AppBody from '../AppBody'
|
||||
import { CreateProposalTabs } from '../../components/NavigationTabs'
|
||||
import { ButtonError } from 'components/Button'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { BlueCard } from 'components/Card'
|
||||
import { Wrapper } from 'pages/Pool/styleds'
|
||||
import { ProposalAction, ProposalActionSelector, ProposalActionSelectorModal } from './ProposalActionSelector'
|
||||
import { ProposalEditor } from './ProposalEditor'
|
||||
import { ProposalActionDetail } from './ProposalActionDetail'
|
||||
import { ProposalSubmissionModal } from './ProposalSubmissionModal'
|
||||
import { useActiveWeb3React } from 'hooks/web3'
|
||||
import {
|
||||
CreateProposalData,
|
||||
ProposalState,
|
||||
useCreateProposalCallback,
|
||||
useLatestProposalId,
|
||||
useProposalData,
|
||||
useProposalThreshold,
|
||||
useUserVotes,
|
||||
} from 'state/governance/hooks'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { tryParseAmount } from 'state/swap/hooks'
|
||||
import { getAddress } from '@ethersproject/address'
|
||||
|
||||
const CreateProposalButton = ({
|
||||
proposalThreshold,
|
||||
hasActiveOrPendingProposal,
|
||||
hasEnoughVote,
|
||||
isFormInvalid,
|
||||
handleCreateProposal,
|
||||
}: {
|
||||
proposalThreshold?: CurrencyAmount<Token>
|
||||
hasActiveOrPendingProposal: boolean
|
||||
hasEnoughVote: boolean
|
||||
isFormInvalid: boolean
|
||||
handleCreateProposal: () => void
|
||||
}) => {
|
||||
const formattedProposalThreshold = proposalThreshold
|
||||
? JSBI.divide(
|
||||
proposalThreshold.quotient,
|
||||
JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(proposalThreshold.currency.decimals))
|
||||
).toLocaleString()
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<ButtonError
|
||||
style={{ marginTop: '18px' }}
|
||||
error={hasActiveOrPendingProposal || !hasEnoughVote}
|
||||
disabled={isFormInvalid || hasActiveOrPendingProposal || !hasEnoughVote}
|
||||
onClick={handleCreateProposal}
|
||||
>
|
||||
{hasActiveOrPendingProposal ? (
|
||||
<Trans>You already have an active or pending proposal</Trans>
|
||||
) : !hasEnoughVote ? (
|
||||
<>
|
||||
{formattedProposalThreshold ? (
|
||||
<Trans>You must have {formattedProposalThreshold} votes to submit a proposal</Trans>
|
||||
) : (
|
||||
<Trans>You don't have enough votes to submit a proposal</Trans>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Trans>Create Proposal</Trans>
|
||||
)}
|
||||
</ButtonError>
|
||||
)
|
||||
}
|
||||
|
||||
const CreateProposalWrapper = styled(Wrapper)`
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
`
|
||||
|
||||
const AutonomousProposalCTA = styled.div`
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
`
|
||||
|
||||
export default function CreateProposal() {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const latestProposalId = useLatestProposalId(account ?? '0x0000000000000000000000000000000000000000') ?? '0'
|
||||
const latestProposalData = useProposalData(0, latestProposalId)
|
||||
const availableVotes: CurrencyAmount<Token> | undefined = useUserVotes()
|
||||
const proposalThreshold: CurrencyAmount<Token> | undefined = useProposalThreshold()
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [hash, setHash] = useState<string | undefined>()
|
||||
const [attempting, setAttempting] = useState(false)
|
||||
const [proposalAction, setProposalAction] = useState(ProposalAction.TRANSFER_TOKEN)
|
||||
const [toAddressValue, setToAddressValue] = useState('')
|
||||
const [currencyValue, setCurrencyValue] = useState<Currency>(UNI[chainId ?? 1])
|
||||
const [amountValue, setAmountValue] = useState('')
|
||||
const [titleValue, setTitleValue] = useState('')
|
||||
const [bodyValue, setBodyValue] = useState('')
|
||||
|
||||
const handleActionSelectorClick = useCallback(() => {
|
||||
setModalOpen(true)
|
||||
}, [setModalOpen])
|
||||
|
||||
const handleActionChange = useCallback(
|
||||
(proposalAction: ProposalAction) => {
|
||||
setProposalAction(proposalAction)
|
||||
},
|
||||
[setProposalAction]
|
||||
)
|
||||
|
||||
const handleDismissActionSelector = useCallback(() => {
|
||||
setModalOpen(false)
|
||||
}, [setModalOpen])
|
||||
|
||||
const handleDismissSubmissionModal = useCallback(() => {
|
||||
setHash(undefined)
|
||||
setAttempting(false)
|
||||
}, [setHash, setAttempting])
|
||||
|
||||
const handleToAddressInput = useCallback(
|
||||
(toAddress: string) => {
|
||||
setToAddressValue(toAddress)
|
||||
},
|
||||
[setToAddressValue]
|
||||
)
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency) => {
|
||||
setCurrencyValue(currency)
|
||||
},
|
||||
[setCurrencyValue]
|
||||
)
|
||||
|
||||
const handleAmountInput = useCallback(
|
||||
(amount: string) => {
|
||||
setAmountValue(amount)
|
||||
},
|
||||
[setAmountValue]
|
||||
)
|
||||
|
||||
const handleTitleInput = useCallback(
|
||||
(title: string) => {
|
||||
setTitleValue(title)
|
||||
},
|
||||
[setTitleValue]
|
||||
)
|
||||
|
||||
const handleBodyInput = useCallback(
|
||||
(body: string) => {
|
||||
setBodyValue(body)
|
||||
},
|
||||
[setBodyValue]
|
||||
)
|
||||
|
||||
const isFormInvalid = useMemo(
|
||||
() =>
|
||||
Boolean(
|
||||
!proposalAction ||
|
||||
!utils.isAddress(toAddressValue) ||
|
||||
!currencyValue?.isToken ||
|
||||
amountValue === '' ||
|
||||
titleValue === '' ||
|
||||
bodyValue === ''
|
||||
),
|
||||
[proposalAction, toAddressValue, currencyValue, amountValue, titleValue, bodyValue]
|
||||
)
|
||||
|
||||
const hasEnoughVote = Boolean(
|
||||
availableVotes && proposalThreshold && JSBI.greaterThanOrEqual(availableVotes.quotient, proposalThreshold.quotient)
|
||||
)
|
||||
|
||||
const createProposalCallback = useCreateProposalCallback()
|
||||
|
||||
const handleCreateProposal = async () => {
|
||||
setAttempting(true)
|
||||
|
||||
const createProposalData: CreateProposalData = {} as CreateProposalData
|
||||
|
||||
if (!createProposalCallback || !proposalAction || !currencyValue.isToken) return
|
||||
|
||||
const tokenAmount = tryParseAmount(amountValue, currencyValue)
|
||||
if (!tokenAmount) return
|
||||
|
||||
createProposalData.targets = [currencyValue.address]
|
||||
createProposalData.values = ['0']
|
||||
createProposalData.description = `# ${titleValue}
|
||||
|
||||
${bodyValue}
|
||||
`
|
||||
|
||||
let types: string[][]
|
||||
let values: string[][]
|
||||
switch (proposalAction) {
|
||||
case ProposalAction.TRANSFER_TOKEN: {
|
||||
types = [['address', 'uint256']]
|
||||
values = [[getAddress(toAddressValue), tokenAmount.quotient.toString()]]
|
||||
createProposalData.signatures = [`transfer(${types[0].join(',')})`]
|
||||
break
|
||||
}
|
||||
|
||||
case ProposalAction.APPROVE_TOKEN: {
|
||||
types = [['address', 'uint256']]
|
||||
values = [[getAddress(toAddressValue), tokenAmount.quotient.toString()]]
|
||||
createProposalData.signatures = [`approve(${types[0].join(',')})`]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
createProposalData.calldatas = []
|
||||
for (let i = 0; i < createProposalData.signatures.length; i++) {
|
||||
createProposalData.calldatas[i] = utils.defaultAbiCoder.encode(types[i], values[i])
|
||||
}
|
||||
|
||||
const hash = await createProposalCallback(createProposalData ?? undefined)?.catch(() => {
|
||||
setAttempting(false)
|
||||
})
|
||||
|
||||
if (hash) setHash(hash)
|
||||
}
|
||||
|
||||
return (
|
||||
<AppBody {...{ maxWidth: '800px' }}>
|
||||
<CreateProposalTabs />
|
||||
<CreateProposalWrapper>
|
||||
<BlueCard>
|
||||
<AutoColumn gap="10px">
|
||||
<TYPE.link fontWeight={400} color={'primaryText1'}>
|
||||
<Trans>
|
||||
<strong>Tip:</strong> Select an action and describe your proposal for the community. The proposal cannot
|
||||
be modified after submission, so please verify all information before submitting. The voting period will
|
||||
begin immediately and last for 7 days. To propose a custom action,{' '}
|
||||
<ExternalLink href="https://uniswap.org/docs/v2/governance/governance-reference/#propose">
|
||||
read the docs
|
||||
</ExternalLink>
|
||||
.
|
||||
</Trans>
|
||||
</TYPE.link>
|
||||
</AutoColumn>
|
||||
</BlueCard>
|
||||
|
||||
<ProposalActionSelector onClick={handleActionSelectorClick} proposalAction={proposalAction} />
|
||||
<ProposalActionDetail
|
||||
proposalAction={proposalAction}
|
||||
currency={currencyValue}
|
||||
amount={amountValue}
|
||||
toAddress={toAddressValue}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
onAmountInput={handleAmountInput}
|
||||
onToAddressInput={handleToAddressInput}
|
||||
/>
|
||||
<ProposalEditor
|
||||
title={titleValue}
|
||||
body={bodyValue}
|
||||
onTitleInput={handleTitleInput}
|
||||
onBodyInput={handleBodyInput}
|
||||
/>
|
||||
<CreateProposalButton
|
||||
proposalThreshold={proposalThreshold}
|
||||
hasActiveOrPendingProposal={
|
||||
latestProposalData?.status === ProposalState.Active || latestProposalData?.status === ProposalState.Pending
|
||||
}
|
||||
hasEnoughVote={hasEnoughVote}
|
||||
isFormInvalid={isFormInvalid}
|
||||
handleCreateProposal={handleCreateProposal}
|
||||
/>
|
||||
{!hasEnoughVote ? (
|
||||
<AutonomousProposalCTA>
|
||||
Don’t have 2.5M votes? Anyone can create an autonomous proposal using{' '}
|
||||
<ExternalLink href="https://fish.vote">fish.vote</ExternalLink>
|
||||
</AutonomousProposalCTA>
|
||||
) : null}
|
||||
</CreateProposalWrapper>
|
||||
<ProposalActionSelectorModal
|
||||
isOpen={modalOpen}
|
||||
onDismiss={handleDismissActionSelector}
|
||||
onProposalActionSelect={(proposalAction: ProposalAction) => handleActionChange(proposalAction)}
|
||||
/>
|
||||
<ProposalSubmissionModal isOpen={attempting} hash={hash} onDismiss={handleDismissSubmissionModal} />
|
||||
</AppBody>
|
||||
)
|
||||
}
|
@ -4,7 +4,7 @@ import styled from 'styled-components/macro'
|
||||
import { SwitchLocaleLink } from '../../components/SwitchLocaleLink'
|
||||
import { UNI } from '../../constants/tokens'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { RowBetween, RowFixed } from '../../components/Row'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { getExplorerLink, ExplorerDataType } from '../../utils/getExplorerLink'
|
||||
import { ProposalStatus } from './styled'
|
||||
@ -182,34 +182,44 @@ export default function Vote() {
|
||||
<TYPE.mediumHeader style={{ margin: '0.5rem 0.5rem 0.5rem 0', flexShrink: 0 }}>
|
||||
<Trans>Proposals</Trans>
|
||||
</TYPE.mediumHeader>
|
||||
{(!allProposals || allProposals.length === 0) && !availableVotes && <Loader />}
|
||||
{showUnlockVoting ? (
|
||||
<AutoRow gap="6px" justify="flex-end">
|
||||
{(!allProposals || allProposals.length === 0) && !availableVotes && <Loader />}
|
||||
{showUnlockVoting ? (
|
||||
<ButtonPrimary
|
||||
style={{ width: 'fit-content' }}
|
||||
padding="8px"
|
||||
borderRadius="8px"
|
||||
onClick={toggleDelegateModal}
|
||||
>
|
||||
<Trans>Unlock Voting</Trans>
|
||||
</ButtonPrimary>
|
||||
) : availableVotes && JSBI.notEqual(JSBI.BigInt(0), availableVotes?.quotient) ? (
|
||||
<TYPE.body fontWeight={500} mr="6px">
|
||||
<Trans>
|
||||
<FormattedCurrencyAmount currencyAmount={availableVotes} /> Votes
|
||||
</Trans>
|
||||
</TYPE.body>
|
||||
) : uniBalance &&
|
||||
userDelegatee &&
|
||||
userDelegatee !== ZERO_ADDRESS &&
|
||||
JSBI.notEqual(JSBI.BigInt(0), uniBalance?.quotient) ? (
|
||||
<TYPE.body fontWeight={500} mr="6px">
|
||||
<Trans>
|
||||
<FormattedCurrencyAmount currencyAmount={uniBalance} /> Votes
|
||||
</Trans>
|
||||
</TYPE.body>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<ButtonPrimary
|
||||
style={{ width: 'fit-content' }}
|
||||
as={Link}
|
||||
to="/create-proposal"
|
||||
style={{ width: 'fit-content', borderRadius: '8px' }}
|
||||
padding="8px"
|
||||
borderRadius="8px"
|
||||
onClick={toggleDelegateModal}
|
||||
>
|
||||
<Trans>Unlock Voting</Trans>
|
||||
<Trans>Create Proposal</Trans>
|
||||
</ButtonPrimary>
|
||||
) : availableVotes && JSBI.notEqual(JSBI.BigInt(0), availableVotes?.quotient) ? (
|
||||
<TYPE.body fontWeight={500} mr="6px">
|
||||
<Trans>
|
||||
<FormattedCurrencyAmount currencyAmount={availableVotes} /> Votes
|
||||
</Trans>
|
||||
</TYPE.body>
|
||||
) : uniBalance &&
|
||||
userDelegatee &&
|
||||
userDelegatee !== ZERO_ADDRESS &&
|
||||
JSBI.notEqual(JSBI.BigInt(0), uniBalance?.quotient) ? (
|
||||
<TYPE.body fontWeight={500} mr="6px">
|
||||
<Trans>
|
||||
<FormattedCurrencyAmount currencyAmount={uniBalance} /> Votes
|
||||
</Trans>
|
||||
</TYPE.body>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</AutoRow>
|
||||
</WrapSmall>
|
||||
{!showUnlockVoting && (
|
||||
<RowBetween>
|
||||
|
@ -14,6 +14,7 @@ import { UNISWAP_GRANTS_START_BLOCK } from '../../constants/proposals'
|
||||
import { UNI } from '../../constants/tokens'
|
||||
import { useMultipleContractMultipleData, useSingleCallResult } from '../multicall/hooks'
|
||||
import { useTransactionAdder } from '../transactions/hooks'
|
||||
import { t } from '@lingui/macro'
|
||||
|
||||
interface ProposalDetail {
|
||||
target: string
|
||||
@ -35,6 +36,14 @@ export interface ProposalData {
|
||||
governorIndex: number // index in the governance address array for which this proposal pertains
|
||||
}
|
||||
|
||||
export interface CreateProposalData {
|
||||
targets: string[]
|
||||
values: string[]
|
||||
signatures: string[]
|
||||
calldatas: string[]
|
||||
description: string
|
||||
}
|
||||
|
||||
export enum ProposalState {
|
||||
Undetermined = -1,
|
||||
Pending,
|
||||
@ -325,3 +334,68 @@ export function useVoteCallback(): {
|
||||
)
|
||||
return { voteCallback }
|
||||
}
|
||||
|
||||
export function useCreateProposalCallback(): (
|
||||
createProposalData: CreateProposalData | undefined
|
||||
) => undefined | Promise<string> {
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const govContracts = useGovernanceContracts()
|
||||
const latestGovernanceContract = govContracts ? govContracts[0] : null
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
const createProposalCallback = useCallback(
|
||||
(createProposalData: CreateProposalData | undefined) => {
|
||||
if (!account || !latestGovernanceContract || !createProposalData) return undefined
|
||||
|
||||
const args = [
|
||||
createProposalData.targets,
|
||||
createProposalData.values,
|
||||
createProposalData.signatures,
|
||||
createProposalData.calldatas,
|
||||
createProposalData.description,
|
||||
]
|
||||
|
||||
return latestGovernanceContract.estimateGas.propose(...args).then((estimatedGasLimit) => {
|
||||
return latestGovernanceContract
|
||||
.propose(...args, { gasLimit: calculateGasMargin(estimatedGasLimit) })
|
||||
.then((response: TransactionResponse) => {
|
||||
addTransaction(response, {
|
||||
summary: t`Submitted new proposal`,
|
||||
})
|
||||
return response.hash
|
||||
})
|
||||
})
|
||||
},
|
||||
[account, addTransaction, latestGovernanceContract]
|
||||
)
|
||||
|
||||
return createProposalCallback
|
||||
}
|
||||
|
||||
export function useLatestProposalId(address: string): string | undefined {
|
||||
const govContracts = useGovernanceContracts()
|
||||
const latestGovernanceContract = govContracts ? govContracts[0] : null
|
||||
const res = useSingleCallResult(latestGovernanceContract, 'latestProposalIds', [address])
|
||||
|
||||
if (res?.result?.[0]) {
|
||||
return (res.result[0] as BigNumber).toString()
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function useProposalThreshold(): CurrencyAmount<Token> | undefined {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const govContracts = useGovernanceContracts()
|
||||
const latestGovernanceContract = govContracts ? govContracts[0] : null
|
||||
const res = useSingleCallResult(latestGovernanceContract, 'proposalThreshold')
|
||||
const uni = chainId ? UNI[chainId] : undefined
|
||||
|
||||
if (res?.result?.[0] && uni) {
|
||||
return CurrencyAmount.fromRawAmount(uni, res.result[0])
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user