Commit ffbd2d10 authored by Justin Domingue's avatar Justin Domingue Committed by GitHub

feat(pool): add liquidity distribution range slider and update add liquidity layout (#1829)

* first iteration of useTicks and useActiveLiquidity

* feat(pools): add liquidity depth chart (#1835)

* cleanup

* use area chart instead of bar chart

* check for undefined rather than falsy

* feat: range buttons based on fee amount (#1870)

* range buttons based on fee amount

* hardcode percentages as ticks

* increase blocksperfect

* feat: optimize add liquidity charts (#1880)

* ignore syncing state

* remove surrounding ticks

* avoid processing price1 as it is unused

* cleanup

* feat: add zoom buttons to liquidity depth chart (#1882)

* ignore syncing state

* remove surrounding ticks

* avoid processing price1 as it is unused

* cleanup

* first pass at +/- zoom buttons

* remove console.log, cleanup

* use real price for price line

* updated brush handles to latest spec

* added % labels to handles

* round tick to nearest usable tick

* first pass at brushable area chart with d3

* first pass at brushable area chart with d3

* rework

* address PR comments

* add brush handles

* address PR comments

* further improvements

* feat(pools): improve full range support + capital efficiency warning (#1903)

* handle min and max prices in add liquidity

* cleaned up

* use flag to denote full range

* reset inputs on fee select

* fixed merge conflict

* handle full range in positions preview

* fixed invalid range when tokens are reversed

* use formatTickData

* updated layout

* cleaned up layout

* fixed address

* avoid re-rendering deposit amounts

* added zoom behavior and more styling

* renamed chart

* renamed main chart file;

* add brush tooltips

* remove chart title

* added accents to brush handles

* more work

* moved to file

* modularize chart components

* fix maximum depth

* added brush labels

* cleanup

* cleanup

* set up zoom

* added new components

* improved brush and zoom integration

* cleaned up clip path

* fixed clip paths

* integrated with the graph changes

* adjust fee selector

* fix data error

* add bar chart

* polish

* merged

* clean up error

* cleaned up after merge

* visual improvements

* moved +/- buttons to the right/left

* removed margin bottom

* removed unsused

* fix brush labels % change

* use d3.axisBottom

* updated labels

* improve brush range

* fix one brush change only

* adjust zoom and clippath

* use bars

* use area

* adjust axis bottom to mocks;

* improved bars

* show bar colors

* better handle full range

* adjust colors for light mode

* updated to mocks

* adjusted handles for visibility

* switch to area

* add react ga events

* adjusted to mocks

* memo brush domain to avoid re-renders

* fix inputstepcounter color

* adjust handles

* rely on the graph sorting tickidx

* use curvestepafter

* updated polish

* merged main

* add clamping and other fixes

* highlight selected area using a mask

* use price instead of % for labels

* delete unused

* refine ux

* relayout

* improve hooks

* adjust layout for mobile

* fixed card color

* adjust padding

* preent tick overflow

* flip handles sooner

* delete bars.tsx
parent 2db29f20
...@@ -153,7 +153,6 @@ export const ButtonOutlined = styled(Base)` ...@@ -153,7 +153,6 @@ export const ButtonOutlined = styled(Base)`
border: 1px solid ${({ theme }) => theme.bg2}; border: 1px solid ${({ theme }) => theme.bg2};
background-color: transparent; background-color: transparent;
color: ${({ theme }) => theme.text1}; color: ${({ theme }) => theme.text1};
&:focus { &:focus {
box-shadow: 0 0 0 1px ${({ theme }) => theme.bg4}; box-shadow: 0 0 0 1px ${({ theme }) => theme.bg4};
} }
...@@ -169,6 +168,27 @@ export const ButtonOutlined = styled(Base)` ...@@ -169,6 +168,27 @@ export const ButtonOutlined = styled(Base)`
} }
` `
export const ButtonYellow = styled(Base)`
background-color: ${({ theme }) => theme.yellow3};
color: white;
&:focus {
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, theme.yellow3)};
background-color: ${({ theme }) => darken(0.05, theme.yellow3)};
}
&:hover {
background-color: ${({ theme }) => darken(0.05, theme.yellow3)};
}
&:active {
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, theme.yellow3)};
background-color: ${({ theme }) => darken(0.1, theme.yellow3)};
}
&:disabled {
background-color: ${({ theme }) => theme.yellow3};
opacity: 50%;
cursor: auto;
}
`
export const ButtonEmpty = styled(Base)` export const ButtonEmpty = styled(Base)`
background-color: transparent; background-color: transparent;
color: ${({ theme }) => theme.primary1}; color: ${({ theme }) => theme.primary1};
......
...@@ -173,7 +173,7 @@ export default function FeeSelector({ ...@@ -173,7 +173,7 @@ export default function FeeSelector({
onClick={() => handleFeePoolSelectWithEvent(FeeAmount.LOW)} onClick={() => handleFeePoolSelectWithEvent(FeeAmount.LOW)}
> >
<AutoColumn gap="sm" justify="flex-start"> <AutoColumn gap="sm" justify="flex-start">
<AutoColumn justify="flex-start" gap="4px"> <AutoColumn justify="flex-start" gap="6px">
<ResponsiveText> <ResponsiveText>
<Trans>0.05% fee</Trans> <Trans>0.05% fee</Trans>
</ResponsiveText> </ResponsiveText>
......
import { useState, useCallback, useEffect, ReactNode } from 'react' import { useState, useCallback, useEffect, ReactNode } from 'react'
import { LightCard } from 'components/Card' import { OutlineCard } from 'components/Card'
import { RowBetween } from 'components/Row'
import { Input as NumericalInput } from '../NumericalInput' import { Input as NumericalInput } from '../NumericalInput'
import styled, { keyframes } from 'styled-components/macro' import styled, { keyframes } from 'styled-components/macro'
import { TYPE } from 'theme' import { TYPE } from 'theme'
import { AutoColumn } from 'components/Column' import { AutoColumn } from 'components/Column'
import { ButtonPrimary } from 'components/Button' import { ButtonGray } from 'components/Button'
import { FeeAmount } from '@uniswap/v3-sdk' import { FeeAmount } from '@uniswap/v3-sdk'
import { formattedFeeAmount } from 'utils'
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Plus, Minus } from 'react-feather'
const pulse = (color: string) => keyframes` const pulse = (color: string) => keyframes`
0% { 0% {
...@@ -24,25 +23,29 @@ const pulse = (color: string) => keyframes` ...@@ -24,25 +23,29 @@ const pulse = (color: string) => keyframes`
} }
` `
const SmallButton = styled(ButtonPrimary)` const InputRow = styled.div`
/* background-color: ${({ theme }) => theme.bg2}; */ display: grid;
grid-template-columns: 30px 1fr 30px;
`
const SmallButton = styled(ButtonGray)`
border-radius: 8px; border-radius: 8px;
padding: 4px 6px; padding: 4px;
width: 48%;
` `
const FocusedOutlineCard = styled(LightCard)<{ active?: boolean; pulsing?: boolean }>` const FocusedOutlineCard = styled(OutlineCard)<{ active?: boolean; pulsing?: boolean }>`
border-color: ${({ active, theme }) => active && theme.blue1}; border-color: ${({ active, theme }) => active && theme.blue1};
padding: 12px; padding: 12px;
animation: ${({ pulsing, theme }) => pulsing && pulse(theme.blue1)} 0.8s linear; animation: ${({ pulsing, theme }) => pulsing && pulse(theme.blue1)} 0.8s linear;
` `
const StyledInput = styled(NumericalInput)<{ usePercent?: boolean }>` const StyledInput = styled(NumericalInput)<{ usePercent?: boolean }>`
/* background-color: ${({ theme }) => theme.bg0}; */ background-color: transparent;
text-align: center; text-align: center;
margin-right: 12px;
width: 100%; width: 100%;
font-weight: 500; font-weight: 500;
padding: 0 10px;
` `
const InputTitle = styled(TYPE.small)` const InputTitle = styled(TYPE.small)`
...@@ -51,11 +54,17 @@ const InputTitle = styled(TYPE.small)` ...@@ -51,11 +54,17 @@ const InputTitle = styled(TYPE.small)`
font-weight: 500; font-weight: 500;
` `
const ButtonLabel = styled(TYPE.white)<{ disabled: boolean }>`
color: ${({ theme, disabled }) => (disabled ? theme.text2 : theme.text1)} !important;
`
interface StepCounterProps { interface StepCounterProps {
value: string value: string
onUserInput: (value: string) => void onUserInput: (value: string) => void
decrement: () => string decrement: () => string
increment: () => string increment: () => string
decrementDisabled?: boolean
incrementDisabled?: boolean
feeAmount?: FeeAmount feeAmount?: FeeAmount
label?: string label?: string
width?: string width?: string
...@@ -69,7 +78,8 @@ const StepCounter = ({ ...@@ -69,7 +78,8 @@ const StepCounter = ({
value, value,
decrement, decrement,
increment, increment,
feeAmount, decrementDisabled = false,
incrementDisabled = false,
width, width,
locked, locked,
onUserInput, onUserInput,
...@@ -87,9 +97,6 @@ const StepCounter = ({ ...@@ -87,9 +97,6 @@ const StepCounter = ({
// animation if parent value updates local value // animation if parent value updates local value
const [pulsing, setPulsing] = useState<boolean>(false) const [pulsing, setPulsing] = useState<boolean>(false)
// format fee amount
const feeAmountFormatted = feeAmount ? formattedFeeAmount(feeAmount * 2) : ''
const handleOnFocus = () => { const handleOnFocus = () => {
setUseLocalValue(true) setUseLocalValue(true)
setActive(true) setActive(true)
...@@ -126,39 +133,45 @@ const StepCounter = ({ ...@@ -126,39 +133,45 @@ const StepCounter = ({
return ( return (
<FocusedOutlineCard pulsing={pulsing} active={active} onFocus={handleOnFocus} onBlur={handleOnBlur} width={width}> <FocusedOutlineCard pulsing={pulsing} active={active} onFocus={handleOnFocus} onBlur={handleOnBlur} width={width}>
<AutoColumn gap="6px" style={{ marginBottom: '12px' }}> <AutoColumn gap="6px">
<InputTitle fontSize={12} textAlign="center"> <InputTitle fontSize={12} textAlign="center">
{title} {title}
</InputTitle> </InputTitle>
<StyledInput
className="rate-input-0" <InputRow>
value={localValue} {!locked && (
fontSize="20px" <SmallButton onClick={handleDecrement} disabled={decrementDisabled}>
disabled={locked} <ButtonLabel disabled={decrementDisabled} fontSize="12px">
onUserInput={(val) => { <Minus size={18} />
setLocalValue(val) </ButtonLabel>
}} </SmallButton>
/> )}
<StyledInput
className="rate-input-0"
value={localValue}
fontSize="20px"
disabled={locked}
onUserInput={(val) => {
setLocalValue(val)
}}
/>
{!locked && (
<SmallButton onClick={handleIncrement} disabled={incrementDisabled}>
<ButtonLabel disabled={incrementDisabled} fontSize="12px">
<Plus size={18} />
</ButtonLabel>
</SmallButton>
)}
</InputRow>
<InputTitle fontSize={12} textAlign="center"> <InputTitle fontSize={12} textAlign="center">
<Trans> <Trans>
{tokenB} per {tokenA} {tokenB} per {tokenA}
</Trans> </Trans>
</InputTitle> </InputTitle>
</AutoColumn> </AutoColumn>
{!locked ? (
<RowBetween>
<SmallButton onClick={handleDecrement}>
<TYPE.white fontSize="12px">
<Trans>-{feeAmountFormatted}%</Trans>
</TYPE.white>
</SmallButton>
<SmallButton onClick={handleIncrement}>
<TYPE.white fontSize="12px">
<Trans>+{feeAmountFormatted}%</Trans>
</TYPE.white>
</SmallButton>
</RowBetween>
) : null}
</FocusedOutlineCard> </FocusedOutlineCard>
) )
} }
......
...@@ -13,6 +13,8 @@ import { resetMintState } from 'state/mint/actions' ...@@ -13,6 +13,8 @@ import { resetMintState } from 'state/mint/actions'
import { resetMintState as resetMintV3State } from 'state/mint/v3/actions' import { resetMintState as resetMintV3State } from 'state/mint/v3/actions'
import { TYPE } from 'theme' import { TYPE } from 'theme'
import useTheme from 'hooks/useTheme' import useTheme from 'hooks/useTheme'
import { ReactNode } from 'react'
import { Box } from 'rebass'
const Tabs = styled.div` const Tabs = styled.div`
${({ theme }) => theme.flexRowNoWrap} ${({ theme }) => theme.flexRowNoWrap}
...@@ -49,6 +51,15 @@ const StyledNavLink = styled(NavLink).attrs({ ...@@ -49,6 +51,15 @@ const StyledNavLink = styled(NavLink).attrs({
} }
` `
const StyledHistoryLink = styled(HistoryLink)<{ flex: string | undefined }>`
flex: ${({ flex }) => flex ?? 'none'};
${({ theme }) => theme.mediaWidth.upToMedium`
flex: none;
margin-right: 10px;
`};
`
const ActiveText = styled.div` const ActiveText = styled.div`
font-weight: 500; font-weight: 500;
font-size: 20px; font-size: 20px;
...@@ -91,11 +102,14 @@ export function AddRemoveTabs({ ...@@ -91,11 +102,14 @@ export function AddRemoveTabs({
creating, creating,
defaultSlippage, defaultSlippage,
positionID, positionID,
children,
}: { }: {
adding: boolean adding: boolean
creating: boolean creating: boolean
defaultSlippage: Percent defaultSlippage: Percent
positionID?: string | undefined positionID?: string | undefined
showBackLink?: boolean
children?: ReactNode | undefined
}) { }) {
const theme = useTheme() const theme = useTheme()
// reset states on back // reset states on back
...@@ -110,7 +124,7 @@ export function AddRemoveTabs({ ...@@ -110,7 +124,7 @@ export function AddRemoveTabs({
return ( return (
<Tabs> <Tabs>
<RowBetween style={{ padding: '1rem 1rem 0 1rem' }}> <RowBetween style={{ padding: '1rem 1rem 0 1rem' }}>
<HistoryLink <StyledHistoryLink
to={poolLink} to={poolLink}
onClick={() => { onClick={() => {
if (adding) { if (adding) {
...@@ -119,10 +133,15 @@ export function AddRemoveTabs({ ...@@ -119,10 +133,15 @@ export function AddRemoveTabs({
dispatch(resetMintV3State()) dispatch(resetMintV3State())
} }
}} }}
flex={children ? '1' : undefined}
> >
<StyledArrowLeft stroke={theme.text2} /> <StyledArrowLeft stroke={theme.text2} />
</HistoryLink> </StyledHistoryLink>
<TYPE.mediumHeader fontWeight={500} fontSize={20}> <TYPE.mediumHeader
fontWeight={500}
fontSize={20}
style={{ flex: '1', margin: 'auto', textAlign: children ? 'start' : 'center' }}
>
{creating ? ( {creating ? (
<Trans>Create a pair</Trans> <Trans>Create a pair</Trans>
) : adding ? ( ) : adding ? (
...@@ -131,6 +150,7 @@ export function AddRemoveTabs({ ...@@ -131,6 +150,7 @@ export function AddRemoveTabs({
<Trans>Remove Liquidity</Trans> <Trans>Remove Liquidity</Trans>
)} )}
</TYPE.mediumHeader> </TYPE.mediumHeader>
<Box style={{ marginRight: '.5rem' }}>{children}</Box>
<SettingsTab placeholderSlippage={defaultSlippage} /> <SettingsTab placeholderSlippage={defaultSlippage} />
</RowBetween> </RowBetween>
</Tabs> </Tabs>
......
...@@ -9,7 +9,7 @@ import styled from 'styled-components/macro' ...@@ -9,7 +9,7 @@ import styled from 'styled-components/macro'
import { HideSmall, MEDIA_WIDTHS, SmallOnly } from 'theme' import { HideSmall, MEDIA_WIDTHS, SmallOnly } from 'theme'
import { PositionDetails } from 'types/position' import { PositionDetails } from 'types/position'
import { Price, Token, Percent } from '@uniswap/sdk-core' import { Price, Token, Percent } from '@uniswap/sdk-core'
import { formatPrice } from 'utils/formatCurrencyAmount' import { formatTickPrice } from 'utils/formatTickPrice'
import Loader from 'components/Loader' import Loader from 'components/Loader'
import { unwrappedToken } from 'utils/unwrappedToken' import { unwrappedToken } from 'utils/unwrappedToken'
import RangeBadge from 'components/Badge/RangeBadge' import RangeBadge from 'components/Badge/RangeBadge'
...@@ -17,6 +17,8 @@ import { RowFixed } from 'components/Row' ...@@ -17,6 +17,8 @@ import { RowFixed } from 'components/Row'
import HoverInlineText from 'components/HoverInlineText' import HoverInlineText from 'components/HoverInlineText'
import { DAI, USDC, USDT, WBTC, WETH9_EXTENDED } from '../../constants/tokens' import { DAI, USDC, USDT, WBTC, WETH9_EXTENDED } from '../../constants/tokens'
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import useIsTickAtLimit from 'hooks/useIsTickAtLimit'
import { Bound } from 'state/mint/v3/actions'
const LinkRow = styled(Link)` const LinkRow = styled(Link)`
align-items: center; align-items: center;
...@@ -201,6 +203,8 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr ...@@ -201,6 +203,8 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
return undefined return undefined
}, [liquidity, pool, tickLower, tickUpper]) }, [liquidity, pool, tickLower, tickUpper])
const tickAtLimit = useIsTickAtLimit(feeAmount, tickLower, tickUpper)
// prices // prices
const { priceLower, priceUpper, quote, base } = getPriceOrderingFromPositionForUI(position) const { priceLower, priceUpper, quote, base } = getPriceOrderingFromPositionForUI(position)
...@@ -239,8 +243,8 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr ...@@ -239,8 +243,8 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
<Trans>Min: </Trans> <Trans>Min: </Trans>
</ExtentsText> </ExtentsText>
<Trans> <Trans>
{formatPrice(priceLower, 5)} <HoverInlineText text={currencyQuote?.symbol} /> per{' '} {formatTickPrice(priceLower, tickAtLimit, Bound.LOWER)} <HoverInlineText text={currencyQuote?.symbol} />{' '}
<HoverInlineText text={currencyBase?.symbol ?? ''} /> per <HoverInlineText text={currencyBase?.symbol ?? ''} />
</Trans> </Trans>
</RangeText>{' '} </RangeText>{' '}
<HideSmall> <HideSmall>
...@@ -254,8 +258,8 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr ...@@ -254,8 +258,8 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
<Trans>Max:</Trans> <Trans>Max:</Trans>
</ExtentsText> </ExtentsText>
<Trans> <Trans>
{formatPrice(priceUpper, 5)} <HoverInlineText text={currencyQuote?.symbol} /> per{' '} {formatTickPrice(priceUpper, tickAtLimit, Bound.UPPER)} <HoverInlineText text={currencyQuote?.symbol} />{' '}
<HoverInlineText maxCharacters={10} text={currencyBase?.symbol} /> per <HoverInlineText maxCharacters={10} text={currencyBase?.symbol} />
</Trans> </Trans>
</RangeText> </RangeText>
</RangeLineItem> </RangeLineItem>
......
...@@ -14,17 +14,21 @@ import DoubleCurrencyLogo from 'components/DoubleLogo' ...@@ -14,17 +14,21 @@ import DoubleCurrencyLogo from 'components/DoubleLogo'
import RangeBadge from 'components/Badge/RangeBadge' import RangeBadge from 'components/Badge/RangeBadge'
import { ThemeContext } from 'styled-components/macro' import { ThemeContext } from 'styled-components/macro'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { Bound } from 'state/mint/v3/actions'
import { formatTickPrice } from 'utils/formatTickPrice'
export const PositionPreview = ({ export const PositionPreview = ({
position, position,
title, title,
inRange, inRange,
baseCurrencyDefault, baseCurrencyDefault,
ticksAtLimit,
}: { }: {
position: Position position: Position
title?: ReactNode title?: ReactNode
inRange: boolean inRange: boolean
baseCurrencyDefault?: Currency | undefined baseCurrencyDefault?: Currency | undefined
ticksAtLimit: { [bound: string]: boolean | undefined }
}) => { }) => {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
...@@ -121,7 +125,11 @@ export const PositionPreview = ({ ...@@ -121,7 +125,11 @@ export const PositionPreview = ({
<TYPE.main fontSize="12px"> <TYPE.main fontSize="12px">
<Trans>Min Price</Trans> <Trans>Min Price</Trans>
</TYPE.main> </TYPE.main>
<TYPE.mediumHeader textAlign="center">{`${priceLower.toSignificant(5)}`}</TYPE.mediumHeader> <TYPE.mediumHeader textAlign="center">{`${formatTickPrice(
priceLower,
ticksAtLimit,
Bound.LOWER
)}`}</TYPE.mediumHeader>
<TYPE.main textAlign="center" fontSize="12px"> <TYPE.main textAlign="center" fontSize="12px">
<Trans> <Trans>
{quoteCurrency.symbol} per {baseCurrency.symbol} {quoteCurrency.symbol} per {baseCurrency.symbol}
...@@ -138,7 +146,11 @@ export const PositionPreview = ({ ...@@ -138,7 +146,11 @@ export const PositionPreview = ({
<TYPE.main fontSize="12px"> <TYPE.main fontSize="12px">
<Trans>Max Price</Trans> <Trans>Max Price</Trans>
</TYPE.main> </TYPE.main>
<TYPE.mediumHeader textAlign="center">{`${priceUpper.toSignificant(5)}`}</TYPE.mediumHeader> <TYPE.mediumHeader textAlign="center">{`${formatTickPrice(
priceUpper,
ticksAtLimit,
Bound.UPPER
)}`}</TYPE.mediumHeader>
<TYPE.main textAlign="center" fontSize="12px"> <TYPE.main textAlign="center" fontSize="12px">
<Trans> <Trans>
{quoteCurrency.symbol} per {baseCurrency.symbol} {quoteCurrency.symbol} per {baseCurrency.symbol}
......
import React from 'react'
import { ButtonOutlined } from 'components/Button'
import { AutoRow } from 'components/Row'
import { TYPE } from 'theme'
import styled from 'styled-components/macro'
import { Trans } from '@lingui/macro'
import { FeeAmount } from '@uniswap/v3-sdk'
import ReactGA from 'react-ga'
const Button = styled(ButtonOutlined).attrs(() => ({
padding: '4px',
borderRadius: '8px',
}))`
color: ${({ theme }) => theme.text1};
flex: 1;
background-color: ${({ theme }) => theme.bg2};
`
const RANGES = {
[FeeAmount.LOW]: [
{ label: '0.05', ticks: 5 },
{ label: '0.1', ticks: 10 },
{ label: '0.2', ticks: 20 },
],
[FeeAmount.MEDIUM]: [
{ label: '1', ticks: 100 },
{ label: '10', ticks: 953 },
{ label: '50', ticks: 4055 },
],
[FeeAmount.HIGH]: [
{ label: '2', ticks: 198 },
{ label: '10', ticks: 953 },
{ label: '80', ticks: 5878 },
],
}
interface PresetsButtonProps {
feeAmount: FeeAmount | undefined
setRange: (numTicks: number) => void
setFullRange: () => void
}
const PresetButton = ({
values: { label, ticks },
setRange,
}: {
values: {
label: string
ticks: number
}
setRange: (numTicks: number) => void
}) => (
<Button
onClick={() => {
setRange(ticks)
ReactGA.event({
category: 'Liquidity',
action: 'Preset clicked',
label: label,
})
}}
>
<TYPE.body fontSize={12}>
<Trans>+/- {label}%</Trans>
</TYPE.body>
</Button>
)
export default function PresetsButtons({ feeAmount, setRange, setFullRange }: PresetsButtonProps) {
feeAmount = feeAmount ?? FeeAmount.LOW
return (
<AutoRow gap="4px" width="auto">
<PresetButton values={RANGES[feeAmount][0]} setRange={setRange} />
<PresetButton values={RANGES[feeAmount][1]} setRange={setRange} />
<PresetButton values={RANGES[feeAmount][2]} setRange={setRange} />
<Button onClick={() => setFullRange()}>
<TYPE.body fontSize={12}>
<Trans>Full Range</Trans>
</TYPE.body>
</Button>
</AutoRow>
)
}
...@@ -2,6 +2,9 @@ import { Trans } from '@lingui/macro' ...@@ -2,6 +2,9 @@ import { Trans } from '@lingui/macro'
import { Currency, Price, Token } from '@uniswap/sdk-core' import { Currency, Price, Token } from '@uniswap/sdk-core'
import StepCounter from 'components/InputStepCounter/InputStepCounter' import StepCounter from 'components/InputStepCounter/InputStepCounter'
import { RowBetween } from 'components/Row' import { RowBetween } from 'components/Row'
import { AutoColumn } from 'components/Column'
import { Bound } from 'state/mint/v3/actions'
import { formatTickPrice } from 'utils/formatTickPrice'
// currencyA is the base token // currencyA is the base token
export default function RangeSelector({ export default function RangeSelector({
...@@ -16,6 +19,7 @@ export default function RangeSelector({ ...@@ -16,6 +19,7 @@ export default function RangeSelector({
currencyA, currencyA,
currencyB, currencyB,
feeAmount, feeAmount,
ticksAtLimit,
}: { }: {
priceLower?: Price<Token, Token> priceLower?: Price<Token, Token>
priceUpper?: Price<Token, Token> priceUpper?: Price<Token, Token>
...@@ -28,6 +32,7 @@ export default function RangeSelector({ ...@@ -28,6 +32,7 @@ export default function RangeSelector({
currencyA?: Currency | null currencyA?: Currency | null
currencyB?: Currency | null currencyB?: Currency | null
feeAmount?: number feeAmount?: number
ticksAtLimit: { [bound in Bound]?: boolean | undefined }
}) { }) {
const tokenA = (currencyA ?? undefined)?.wrapped const tokenA = (currencyA ?? undefined)?.wrapped
const tokenB = (currencyB ?? undefined)?.wrapped const tokenB = (currencyB ?? undefined)?.wrapped
...@@ -37,31 +42,37 @@ export default function RangeSelector({ ...@@ -37,31 +42,37 @@ export default function RangeSelector({
const rightPrice = isSorted ? priceUpper : priceLower?.invert() const rightPrice = isSorted ? priceUpper : priceLower?.invert()
return ( return (
<RowBetween> <AutoColumn gap="md">
<StepCounter <RowBetween>
value={leftPrice?.toSignificant(5) ?? ''} <StepCounter
onUserInput={onLeftRangeInput} value={formatTickPrice(leftPrice, ticksAtLimit, Bound.LOWER, '')}
width="48%" onUserInput={onLeftRangeInput}
decrement={isSorted ? getDecrementLower : getIncrementUpper} width="48%"
increment={isSorted ? getIncrementLower : getDecrementUpper} decrement={isSorted ? getDecrementLower : getIncrementUpper}
feeAmount={feeAmount} increment={isSorted ? getIncrementLower : getDecrementUpper}
label={leftPrice ? `${currencyB?.symbol}` : '-'} decrementDisabled={ticksAtLimit[Bound.LOWER]}
title={<Trans>Min Price</Trans>} incrementDisabled={ticksAtLimit[Bound.LOWER]}
tokenA={currencyA?.symbol} feeAmount={feeAmount}
tokenB={currencyB?.symbol} label={leftPrice ? `${currencyB?.symbol}` : '-'}
/> title={<Trans>Min Price</Trans>}
<StepCounter tokenA={currencyA?.symbol}
value={rightPrice?.toSignificant(5) ?? ''} tokenB={currencyB?.symbol}
onUserInput={onRightRangeInput} />
width="48%" <StepCounter
decrement={isSorted ? getDecrementUpper : getIncrementLower} value={formatTickPrice(rightPrice, ticksAtLimit, Bound.UPPER, '')}
increment={isSorted ? getIncrementUpper : getDecrementLower} onUserInput={onRightRangeInput}
feeAmount={feeAmount} width="48%"
label={rightPrice ? `${currencyB?.symbol}` : '-'} decrement={isSorted ? getDecrementUpper : getIncrementLower}
tokenA={currencyA?.symbol} increment={isSorted ? getIncrementUpper : getDecrementLower}
tokenB={currencyB?.symbol} incrementDisabled={ticksAtLimit[Bound.UPPER]}
title={<Trans>Max Price</Trans>} decrementDisabled={ticksAtLimit[Bound.UPPER]}
/> feeAmount={feeAmount}
</RowBetween> label={rightPrice ? `${currencyB?.symbol}` : '-'}
tokenA={currencyA?.symbol}
tokenB={currencyB?.symbol}
title={<Trans>Max Price</Trans>}
/>
</RowBetween>
</AutoColumn>
) )
} }
...@@ -22,10 +22,10 @@ export default function RateToggle({ ...@@ -22,10 +22,10 @@ export default function RateToggle({
<div style={{ width: 'fit-content', display: 'flex', alignItems: 'center' }} onClick={handleRateToggle}> <div style={{ width: 'fit-content', display: 'flex', alignItems: 'center' }} onClick={handleRateToggle}>
<ToggleWrapper width="fit-content"> <ToggleWrapper width="fit-content">
<ToggleElement isActive={isSorted} fontSize="12px"> <ToggleElement isActive={isSorted} fontSize="12px">
<Trans>{isSorted ? currencyA.symbol : currencyB.symbol} price</Trans> <Trans>{isSorted ? currencyA.symbol : currencyB.symbol}</Trans>
</ToggleElement> </ToggleElement>
<ToggleElement isActive={!isSorted} fontSize="12px"> <ToggleElement isActive={!isSorted} fontSize="12px">
<Trans>{isSorted ? currencyB.symbol : currencyA.symbol} price</Trans> <Trans>{isSorted ? currencyB.symbol : currencyA.symbol}</Trans>
</ToggleElement> </ToggleElement>
</ToggleWrapper> </ToggleWrapper>
</div> </div>
......
import { FeeAmount, nearestUsableTick, TickMath, TICK_SPACINGS } from '@uniswap/v3-sdk'
import { useMemo } from 'react'
import { Bound } from 'state/mint/v3/actions'
export default function useIsTickAtLimit(
feeAmount: FeeAmount | undefined,
tickLower: number | undefined,
tickUpper: number | undefined
) {
return useMemo(
() => ({
[Bound.LOWER]:
feeAmount && tickLower
? tickLower === nearestUsableTick(TickMath.MIN_TICK, TICK_SPACINGS[feeAmount as FeeAmount])
: undefined,
[Bound.UPPER]:
feeAmount && tickUpper
? tickUpper === nearestUsableTick(TickMath.MAX_TICK, TICK_SPACINGS[feeAmount as FeeAmount])
: undefined,
}),
[feeAmount, tickLower, tickUpper]
)
}
import { Field } from '../../state/mint/v3/actions' import { Bound, Field } from '../../state/mint/v3/actions'
import { AutoColumn } from 'components/Column' import { AutoColumn } from 'components/Column'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { Currency, CurrencyAmount, Price } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Price } from '@uniswap/sdk-core'
...@@ -12,6 +12,7 @@ const Wrapper = styled.div` ...@@ -12,6 +12,7 @@ const Wrapper = styled.div`
export function Review({ export function Review({
position, position,
outOfRange, outOfRange,
ticksAtLimit,
}: { }: {
position?: Position position?: Position
existingPosition?: Position existingPosition?: Position
...@@ -19,11 +20,19 @@ export function Review({ ...@@ -19,11 +20,19 @@ export function Review({
priceLower?: Price<Currency, Currency> priceLower?: Price<Currency, Currency>
priceUpper?: Price<Currency, Currency> priceUpper?: Price<Currency, Currency>
outOfRange: boolean outOfRange: boolean
ticksAtLimit: { [bound in Bound]?: boolean | undefined }
}) { }) {
return ( return (
<Wrapper> <Wrapper>
<AutoColumn gap="lg"> <AutoColumn gap="lg">
{position ? <PositionPreview position={position} inRange={!outOfRange} title={'Selected Range'} /> : null} {position ? (
<PositionPreview
position={position}
inRange={!outOfRange}
ticksAtLimit={ticksAtLimit}
title={'Selected Range'}
/>
) : null}
</AutoColumn> </AutoColumn>
</Wrapper> </Wrapper>
) )
......
import { useCallback, useContext, useMemo, useState } from 'react' import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { TransactionResponse } from '@ethersproject/providers' import { TransactionResponse } from '@ethersproject/providers'
import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { AlertTriangle, AlertCircle } from 'react-feather' import { AlertTriangle, AlertCircle } from 'react-feather'
...@@ -11,12 +11,12 @@ import { useV3NFTPositionManagerContract } from '../../hooks/useContract' ...@@ -11,12 +11,12 @@ import { useV3NFTPositionManagerContract } from '../../hooks/useContract'
import { RouteComponentProps } from 'react-router-dom' import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ThemeContext } from 'styled-components/macro' import { ThemeContext } from 'styled-components/macro'
import { ButtonError, ButtonLight, ButtonPrimary, ButtonText } from '../../components/Button' import { ButtonError, ButtonLight, ButtonPrimary, ButtonText, ButtonYellow } from '../../components/Button'
import { YellowCard, OutlineCard, BlueCard, LightCard } from '../../components/Card' import { YellowCard, OutlineCard, BlueCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column' import { AutoColumn } from '../../components/Column'
import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal' import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel' import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { RowBetween, RowFixed } from '../../components/Row' import Row, { RowBetween, RowFixed, AutoRow } from '../../components/Row'
import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported' import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported'
import { useUSDCValue } from '../../hooks/useUSDCPrice' import { useUSDCValue } from '../../hooks/useUSDCPrice'
import approveAmountCalldata from '../../utils/approveAmountCalldata' import approveAmountCalldata from '../../utils/approveAmountCalldata'
...@@ -33,11 +33,23 @@ import { useTransactionAdder } from '../../state/transactions/hooks' ...@@ -33,11 +33,23 @@ import { useTransactionAdder } from '../../state/transactions/hooks'
import { useIsExpertMode, useUserSlippageToleranceWithDefault } from '../../state/user/hooks' import { useIsExpertMode, useUserSlippageToleranceWithDefault } from '../../state/user/hooks'
import { TYPE, ExternalLink } from '../../theme' import { TYPE, ExternalLink } from '../../theme'
import { maxAmountSpend } from '../../utils/maxAmountSpend' import { maxAmountSpend } from '../../utils/maxAmountSpend'
import AppBody from '../AppBody'
import { Dots } from '../Pool/styleds' import { Dots } from '../Pool/styleds'
import { currencyId } from '../../utils/currencyId' import { currencyId } from '../../utils/currencyId'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter' import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import { DynamicSection, CurrencyDropdown, StyledInput, Wrapper, ScrollablePage } from './styled' import {
DynamicSection,
CurrencyDropdown,
StyledInput,
Wrapper,
ScrollablePage,
ResponsiveTwoColumns,
PageWrapper,
StackedContainer,
StackedItem,
RightContainer,
MediumOnly,
HideMedium,
} from './styled'
import { Trans, t } from '@lingui/macro' import { Trans, t } from '@lingui/macro'
import { import {
useV3MintState, useV3MintState,
...@@ -56,6 +68,8 @@ import { BigNumber } from '@ethersproject/bignumber' ...@@ -56,6 +68,8 @@ import { BigNumber } from '@ethersproject/bignumber'
import { AddRemoveTabs } from 'components/NavigationTabs' import { AddRemoveTabs } from 'components/NavigationTabs'
import HoverInlineText from 'components/HoverInlineText' import HoverInlineText from 'components/HoverInlineText'
import { SwitchLocaleLink } from 'components/SwitchLocaleLink' import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
import PresetsButtons from 'components/RangeSelector/PresetsButtons'
import LiquidityChartRangeInput from 'components/LiquidityChartRangeInput'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
const DEFAULT_ADD_IN_RANGE_SLIPPAGE_TOLERANCE = new Percent(50, 10_000) const DEFAULT_ADD_IN_RANGE_SLIPPAGE_TOLERANCE = new Percent(50, 10_000)
...@@ -118,6 +132,7 @@ export default function AddLiquidity({ ...@@ -118,6 +132,7 @@ export default function AddLiquidity({
depositADisabled, depositADisabled,
depositBDisabled, depositBDisabled,
invertPrice, invertPrice,
ticksAtLimit,
} = useV3DerivedMintInfo( } = useV3DerivedMintInfo(
currencyA ?? undefined, currencyA ?? undefined,
currencyB ?? undefined, currencyB ?? undefined,
...@@ -135,6 +150,11 @@ export default function AddLiquidity({ ...@@ -135,6 +150,11 @@ export default function AddLiquidity({
const [showConfirm, setShowConfirm] = useState<boolean>(false) const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
// capital efficiency warning
const [showCapitalEfficiencyWarning, setShowCapitalEfficiencyWarning] = useState(false)
useEffect(() => setShowCapitalEfficiencyWarning(false), [currencyA, currencyB, feeAmount])
// txn values // txn values
const deadline = useTransactionDeadline() // custom from users settings const deadline = useTransactionDeadline() // custom from users settings
...@@ -399,9 +419,11 @@ export default function AddLiquidity({ ...@@ -399,9 +419,11 @@ export default function AddLiquidity({
const handleFeePoolSelect = useCallback( const handleFeePoolSelect = useCallback(
(newFeeAmount: FeeAmount) => { (newFeeAmount: FeeAmount) => {
onLeftRangeInput('')
onRightRangeInput('')
history.push(`/add/${currencyIdA}/${currencyIdB}/${newFeeAmount}`) history.push(`/add/${currencyIdA}/${currencyIdB}/${newFeeAmount}`)
}, },
[currencyIdA, currencyIdB, history] [currencyIdA, currencyIdB, history, onLeftRangeInput, onRightRangeInput]
) )
const handleDismissConfirmation = useCallback(() => { const handleDismissConfirmation = useCallback(() => {
...@@ -428,14 +450,8 @@ export default function AddLiquidity({ ...@@ -428,14 +450,8 @@ export default function AddLiquidity({
const { [Bound.LOWER]: tickLower, [Bound.UPPER]: tickUpper } = ticks const { [Bound.LOWER]: tickLower, [Bound.UPPER]: tickUpper } = ticks
const { [Bound.LOWER]: priceLower, [Bound.UPPER]: priceUpper } = pricesAtTicks const { [Bound.LOWER]: priceLower, [Bound.UPPER]: priceUpper } = pricesAtTicks
const { getDecrementLower, getIncrementLower, getDecrementUpper, getIncrementUpper } = useRangeHopCallbacks( const { getDecrementLower, getIncrementLower, getDecrementUpper, getIncrementUpper, getSetRange, getSetFullRange } =
baseCurrency ?? undefined, useRangeHopCallbacks(baseCurrency ?? undefined, quoteCurrency ?? undefined, feeAmount, tickLower, tickUpper, pool)
quoteCurrency ?? undefined,
feeAmount,
tickLower,
tickUpper,
pool
)
// we need an existence check on parsed amounts for single-asset deposits // we need an existence check on parsed amounts for single-asset deposits
const showApprovalA = const showApprovalA =
...@@ -443,6 +459,80 @@ export default function AddLiquidity({ ...@@ -443,6 +459,80 @@ export default function AddLiquidity({
const showApprovalB = const showApprovalB =
!argentWalletContract && approvalB !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_B] !argentWalletContract && approvalB !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_B]
const Buttons = () =>
addIsUnsupported ? (
<ButtonPrimary disabled={true} $borderRadius="12px" padding={'12px'}>
<TYPE.main mb="4px">
<Trans>Unsupported Asset</Trans>
</TYPE.main>
</ButtonPrimary>
) : !account ? (
<ButtonLight onClick={toggleWalletModal} $borderRadius="12px" padding={'12px'}>
<Trans>Connect wallet</Trans>
</ButtonLight>
) : (
<AutoColumn gap={'md'}>
{(approvalA === ApprovalState.NOT_APPROVED ||
approvalA === ApprovalState.PENDING ||
approvalB === ApprovalState.NOT_APPROVED ||
approvalB === ApprovalState.PENDING) &&
isValid && (
<RowBetween>
{showApprovalA && (
<ButtonPrimary
onClick={approveACallback}
disabled={approvalA === ApprovalState.PENDING}
width={showApprovalB ? '48%' : '100%'}
>
{approvalA === ApprovalState.PENDING ? (
<Dots>
<Trans>Approving {currencies[Field.CURRENCY_A]?.symbol}</Trans>
</Dots>
) : (
<Trans>Approve {currencies[Field.CURRENCY_A]?.symbol}</Trans>
)}
</ButtonPrimary>
)}
{showApprovalB && (
<ButtonPrimary
onClick={approveBCallback}
disabled={approvalB === ApprovalState.PENDING}
width={showApprovalA ? '48%' : '100%'}
>
{approvalB === ApprovalState.PENDING ? (
<Dots>
<Trans>Approving {currencies[Field.CURRENCY_B]?.symbol}</Trans>
</Dots>
) : (
<Trans>Approve {currencies[Field.CURRENCY_B]?.symbol}</Trans>
)}
</ButtonPrimary>
)}
</RowBetween>
)}
{mustCreateSeparately && (
<ButtonError onClick={onCreate}>
<Text fontWeight={500}>
<Trans>Create</Trans>
</Text>
</ButtonError>
)}
<ButtonError
onClick={() => {
expertMode ? onAdd() : setShowConfirm(true)
}}
disabled={
mustCreateSeparately ||
!isValid ||
(!argentWalletContract && approvalA !== ApprovalState.APPROVED && !depositADisabled) ||
(!argentWalletContract && approvalB !== ApprovalState.APPROVED && !depositBDisabled)
}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
>
<Text fontWeight={500}>{errorMessage ? errorMessage : <Trans>Preview</Trans>}</Text>
</ButtonError>
</AutoColumn>
)
// flag for whether pool creation must be a separate tx // flag for whether pool creation must be a separate tx
const mustCreateSeparately = const mustCreateSeparately =
noLiquidity && (chainId === SupportedChainId.OPTIMISM || chainId === SupportedChainId.OPTIMISTIC_KOVAN) noLiquidity && (chainId === SupportedChainId.OPTIMISM || chainId === SupportedChainId.OPTIMISTIC_KOVAN)
...@@ -468,6 +558,7 @@ export default function AddLiquidity({ ...@@ -468,6 +558,7 @@ export default function AddLiquidity({
priceLower={priceLower} priceLower={priceLower}
priceUpper={priceUpper} priceUpper={priceUpper}
outOfRange={outOfRange} outOfRange={outOfRange}
ticksAtLimit={ticksAtLimit}
/> />
)} )}
bottomContent={() => ( bottomContent={() => (
...@@ -481,369 +572,397 @@ export default function AddLiquidity({ ...@@ -481,369 +572,397 @@ export default function AddLiquidity({
)} )}
pendingText={pendingText} pendingText={pendingText}
/> />
<AppBody> <PageWrapper wide={!hasExistingPosition}>
<AddRemoveTabs <AddRemoveTabs
creating={false} creating={false}
adding={true} adding={true}
positionID={tokenId} positionID={tokenId}
defaultSlippage={DEFAULT_ADD_IN_RANGE_SLIPPAGE_TOLERANCE} defaultSlippage={DEFAULT_ADD_IN_RANGE_SLIPPAGE_TOLERANCE}
/> showBackLink={!hasExistingPosition}
>
{!hasExistingPosition && (
<Row justifyContent="flex-end" style={{ width: 'fit-content', minWidth: 'fit-content' }}>
<MediumOnly>
<ButtonText onClick={clearAll} margin="0 15px 0 0">
<TYPE.blue fontSize="12px">
<Trans>Clear All</Trans>
</TYPE.blue>
</ButtonText>
</MediumOnly>
{baseCurrency && quoteCurrency ? (
<RateToggle
currencyA={baseCurrency}
currencyB={quoteCurrency}
handleRateToggle={() => {
onLeftRangeInput('')
onRightRangeInput('')
history.push(
`/add/${currencyIdB as string}/${currencyIdA as string}${feeAmount ? '/' + feeAmount : ''}`
)
}}
/>
) : null}
</Row>
)}
</AddRemoveTabs>
<Wrapper> <Wrapper>
<AutoColumn gap="32px"> <ResponsiveTwoColumns wide={!hasExistingPosition}>
{!hasExistingPosition && ( <AutoColumn gap="lg">
<> {!hasExistingPosition && (
<AutoColumn gap="md"> <>
<RowBetween paddingBottom="20px"> <AutoColumn gap="md">
<TYPE.label> <RowBetween paddingBottom="20px">
<Trans>Select pair</Trans> <TYPE.label>
</TYPE.label> <Trans>Select Pair</Trans>
<ButtonText onClick={clearAll}> </TYPE.label>
<TYPE.blue fontSize="12px"> </RowBetween>
<Trans>Clear All</Trans> <RowBetween>
</TYPE.blue> <CurrencyDropdown
</ButtonText> value={formattedAmounts[Field.CURRENCY_A]}
</RowBetween> onUserInput={onFieldAInput}
<RowBetween> hideInput={true}
<CurrencyDropdown onMax={() => {
value={formattedAmounts[Field.CURRENCY_A]} onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '')
onUserInput={onFieldAInput} }}
hideInput={true} onCurrencySelect={handleCurrencyASelect}
onMax={() => { showMaxButton={!atMaxAmounts[Field.CURRENCY_A]}
onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '') currency={currencies[Field.CURRENCY_A]}
}} id="add-liquidity-input-tokena"
onCurrencySelect={handleCurrencyASelect} showCommonBases
showMaxButton={!atMaxAmounts[Field.CURRENCY_A]} />
currency={currencies[Field.CURRENCY_A]} <div style={{ width: '12px' }} />
id="add-liquidity-input-tokena"
showCommonBases <CurrencyDropdown
/> value={formattedAmounts[Field.CURRENCY_B]}
<div style={{ width: '12px' }} /> hideInput={true}
onUserInput={onFieldBInput}
<CurrencyDropdown onCurrencySelect={handleCurrencyBSelect}
value={formattedAmounts[Field.CURRENCY_B]} onMax={() => {
hideInput={true} onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '')
onUserInput={onFieldBInput} }}
onCurrencySelect={handleCurrencyBSelect} showMaxButton={!atMaxAmounts[Field.CURRENCY_B]}
onMax={() => { currency={currencies[Field.CURRENCY_B]}
onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '') id="add-liquidity-input-tokenb"
}} showCommonBases
showMaxButton={!atMaxAmounts[Field.CURRENCY_B]} />
currency={currencies[Field.CURRENCY_B]} </RowBetween>
id="add-liquidity-input-tokenb"
showCommonBases <FeeSelector
disabled={!currencyB || !currencyA}
feeAmount={feeAmount}
handleFeePoolSelect={handleFeePoolSelect}
token0={currencyA?.wrapped}
token1={currencyB?.wrapped}
/> />
</RowBetween>
<FeeSelector
disabled={!currencyB || !currencyA}
feeAmount={feeAmount}
handleFeePoolSelect={handleFeePoolSelect}
token0={currencyA?.wrapped}
token1={currencyB?.wrapped}
/>
</AutoColumn>{' '}
</>
)}
{hasExistingPosition && existingPosition ? ( {noLiquidity && (
<PositionPreview
position={existingPosition}
title={<Trans>Selected Range</Trans>}
inRange={!outOfRange}
/>
) : (
<>
{noLiquidity && (
<DynamicSection disabled={!currencyA || !currencyB}>
<AutoColumn gap="md">
<RowBetween>
<TYPE.label>
<Trans>Set Starting Price</Trans>
</TYPE.label>
{baseCurrency && quoteCurrency ? (
<RateToggle
currencyA={baseCurrency}
currencyB={quoteCurrency}
handleRateToggle={() => {
onLeftRangeInput('')
onRightRangeInput('')
history.push(
`/add/${currencyIdB as string}/${currencyIdA as string}${
feeAmount ? '/' + feeAmount : ''
}`
)
}}
/>
) : null}
</RowBetween>
<OutlineCard padding="12px">
<StyledInput
className="start-price-input"
value={startPriceTypedValue}
onUserInput={onStartPriceInput}
/>
</OutlineCard>
<RowBetween style={{ backgroundColor: theme.bg1, padding: '12px', borderRadius: '12px' }}>
<TYPE.main>
<Trans>Current {baseCurrency?.symbol} Price:</Trans>
</TYPE.main>
<TYPE.main>
{price ? (
<TYPE.main>
<RowFixed>
<HoverInlineText
maxCharacters={20}
text={invertPrice ? price?.invert()?.toSignificant(5) : price?.toSignificant(5)}
/>{' '}
<span style={{ marginLeft: '4px' }}>{quoteCurrency?.symbol}</span>
</RowFixed>
</TYPE.main>
) : (
'-'
)}
</TYPE.main>
</RowBetween>
<BlueCard <BlueCard
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
padding: ' 1.5rem 1.25rem', padding: '1rem 1rem',
}} }}
> >
<AlertCircle color={theme.text1} size={32} style={{ marginBottom: '12px', opacity: 0.8 }} /> <div style={{ marginRight: '12px', width: '30px', height: '30px' }}>
<AlertCircle color={theme.primaryText1} size={30} />
</div>
<TYPE.body <TYPE.body
fontSize={14} fontSize={14}
style={{ marginBottom: 8, fontWeight: 500, opacity: 0.8 }} style={{ marginBottom: 8, fontWeight: 500 }}
textAlign="center" textAlign="center"
color={theme.primaryText1}
> >
You are the first liquidity provider for this Uniswap V3 pool. <Trans>
</TYPE.body> You are the first liquidity provider for this Uniswap V3 pool.The transaction cost will be
much higher as it includes the gas to create the pool.
<TYPE.body fontWeight={500} textAlign="center" fontSize={14} style={{ opacity: 0.8 }}> </Trans>
The transaction cost will be much higher as it includes the gas to create the pool.
</TYPE.body> </TYPE.body>
</BlueCard> </BlueCard>
</AutoColumn> )}
</DynamicSection> </AutoColumn>{' '}
)} </>
)}
<DynamicSection {hasExistingPosition && existingPosition && (
gap="md" <PositionPreview
disabled={!feeAmount || invalidPool || (noLiquidity && !startPriceTypedValue)} position={existingPosition}
> title={<Trans>Selected Range</Trans>}
<RowBetween> inRange={!outOfRange}
<TYPE.label> ticksAtLimit={ticksAtLimit}
<Trans>Set Price Range</Trans> />
</TYPE.label> )}
</AutoColumn>
{baseCurrency && quoteCurrency ? (
<RateToggle <div>
currencyA={baseCurrency} <DynamicSection
currencyB={quoteCurrency} disabled={tickLower === undefined || tickUpper === undefined || invalidPool || invalidRange}
handleRateToggle={() => { >
onLeftRangeInput('') <AutoColumn gap="md">
onRightRangeInput('') <TYPE.label>
history.push( {hasExistingPosition ? <Trans>Add more liquidity</Trans> : <Trans>Deposit Amounts</Trans>}
`/add/${currencyIdB as string}/${currencyIdA as string}${ </TYPE.label>
feeAmount ? '/' + feeAmount : ''
}` <CurrencyInputPanel
) value={formattedAmounts[Field.CURRENCY_A]}
}} onUserInput={onFieldAInput}
/> onMax={() => {
) : null} onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '')
</RowBetween> }}
<TYPE.main fontSize={14} fontWeight={400} style={{ marginBottom: '.5rem', lineHeight: '125%' }}> showMaxButton={!atMaxAmounts[Field.CURRENCY_A]}
<Trans> currency={currencies[Field.CURRENCY_A]}
Your liquidity will only earn fees when the market price of the pair is within your range.{' '} id="add-liquidity-input-tokena"
<ExternalLink fiatValue={usdcValues[Field.CURRENCY_A]}
href={ showCommonBases
'https://docs.uniswap.org/protocol/concepts/introduction/liquidity-user-guide#4-set-price-range' locked={depositADisabled}
}
style={{ fontSize: '14px' }}
>
Need help picking a range?
</ExternalLink>
</Trans>
</TYPE.main>
<RangeSelector
priceLower={priceLower}
priceUpper={priceUpper}
getDecrementLower={getDecrementLower}
getIncrementLower={getIncrementLower}
getDecrementUpper={getDecrementUpper}
getIncrementUpper={getIncrementUpper}
onLeftRangeInput={onLeftRangeInput}
onRightRangeInput={onRightRangeInput}
currencyA={baseCurrency}
currencyB={quoteCurrency}
feeAmount={feeAmount}
/> />
{price && baseCurrency && quoteCurrency && !noLiquidity && ( <CurrencyInputPanel
<LightCard style={{ padding: '12px' }}> value={formattedAmounts[Field.CURRENCY_B]}
<AutoColumn gap="4px"> onUserInput={onFieldBInput}
<TYPE.main fontWeight={500} textAlign="center" fontSize={12}> onMax={() => {
<Trans>Current Price</Trans> onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '')
</TYPE.main> }}
<TYPE.body fontWeight={500} textAlign="center" fontSize={20}> showMaxButton={!atMaxAmounts[Field.CURRENCY_B]}
<HoverInlineText fiatValue={usdcValues[Field.CURRENCY_B]}
maxCharacters={20} currency={currencies[Field.CURRENCY_B]}
text={invertPrice ? price.invert().toSignificant(6) : price.toSignificant(6)} id="add-liquidity-input-tokenb"
/>{' '} showCommonBases
</TYPE.body> locked={depositBDisabled}
<TYPE.main fontWeight={500} textAlign="center" fontSize={12}> />
<Trans> </AutoColumn>
</DynamicSection>
</div>
{!hasExistingPosition ? (
<>
<HideMedium>
<Buttons />
</HideMedium>
<RightContainer gap="lg">
<DynamicSection gap="md" disabled={!feeAmount || invalidPool}>
<RowBetween>
<TYPE.label>
<Trans>Set your Price Range</Trans>
</TYPE.label>
</RowBetween>
{price && baseCurrency && quoteCurrency && !noLiquidity && (
<AutoRow gap="4px" justify="center" style={{ marginTop: '0.5rem' }}>
<Trans>
<TYPE.main fontWeight={500} textAlign="center" fontSize={12} color="text1">
Current Price:
</TYPE.main>
<TYPE.body fontWeight={500} textAlign="center" fontSize={12} color="text1">
<HoverInlineText
maxCharacters={20}
text={invertPrice ? price.invert().toSignificant(6) : price.toSignificant(6)}
/>
</TYPE.body>
<TYPE.body color="text2" fontSize={12}>
{quoteCurrency?.symbol} per {baseCurrency.symbol} {quoteCurrency?.symbol} per {baseCurrency.symbol}
</Trans> </TYPE.body>
</TYPE.main> </Trans>
</AutoColumn> </AutoRow>
</LightCard> )}
)}
{outOfRange ? (
<YellowCard padding="8px 12px" $borderRadius="12px">
<RowBetween>
<AlertTriangle stroke={theme.yellow3} size="16px" />
<TYPE.yellow ml="12px" fontSize="12px">
<Trans>
Your position will not earn fees or be used in trades until the market price moves into
your range.
</Trans>
</TYPE.yellow>
</RowBetween>
</YellowCard>
) : null}
{invalidRange ? (
<YellowCard padding="8px 12px" $borderRadius="12px">
<RowBetween>
<AlertTriangle stroke={theme.yellow3} size="16px" />
<TYPE.yellow ml="12px" fontSize="12px">
<Trans>Invalid range selected. The min price must be lower than the max price.</Trans>
</TYPE.yellow>
</RowBetween>
</YellowCard>
) : null}
</DynamicSection>
</>
)}
<DynamicSection <LiquidityChartRangeInput
disabled={tickLower === undefined || tickUpper === undefined || invalidPool || invalidRange} currencyA={baseCurrency ?? undefined}
> currencyB={quoteCurrency ?? undefined}
<AutoColumn gap="md"> feeAmount={feeAmount}
<TYPE.label>{hasExistingPosition ? 'Add more liquidity' : t`Deposit Amounts`}</TYPE.label> ticksAtLimit={ticksAtLimit}
price={price ? parseFloat((invertPrice ? price.invert() : price).toSignificant(8)) : undefined}
<CurrencyInputPanel priceLower={priceLower}
value={formattedAmounts[Field.CURRENCY_A]} priceUpper={priceUpper}
onUserInput={onFieldAInput} onLeftRangeInput={onLeftRangeInput}
onMax={() => { onRightRangeInput={onRightRangeInput}
onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '') interactive={!hasExistingPosition}
}} />
showMaxButton={!atMaxAmounts[Field.CURRENCY_A]}
currency={currencies[Field.CURRENCY_A]}
id="add-liquidity-input-tokena"
fiatValue={usdcValues[Field.CURRENCY_A]}
showCommonBases
locked={depositADisabled}
/>
<CurrencyInputPanel {noLiquidity && (
value={formattedAmounts[Field.CURRENCY_B]} <AutoColumn gap="md">
onUserInput={onFieldBInput} <RowBetween>
onMax={() => { <TYPE.label>
onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '') <Trans>Set Starting Price</Trans>
}} </TYPE.label>
showMaxButton={!atMaxAmounts[Field.CURRENCY_B]} {baseCurrency && quoteCurrency ? (
fiatValue={usdcValues[Field.CURRENCY_B]} <RateToggle
currency={currencies[Field.CURRENCY_B]} currencyA={baseCurrency}
id="add-liquidity-input-tokenb" currencyB={quoteCurrency}
showCommonBases handleRateToggle={() => {
locked={depositBDisabled} onLeftRangeInput('')
/> onRightRangeInput('')
</AutoColumn> history.push(
</DynamicSection> `/add/${currencyIdB as string}/${currencyIdA as string}${
<div> feeAmount ? '/' + feeAmount : ''
{addIsUnsupported ? ( }`
<ButtonPrimary disabled={true} $borderRadius="12px" padding={'12px'}> )
<TYPE.main mb="4px"> }}
<Trans>Unsupported Asset</Trans> />
</TYPE.main> ) : null}
</ButtonPrimary> </RowBetween>
) : !account ? (
<ButtonLight onClick={toggleWalletModal} $borderRadius="12px" padding={'12px'}> <OutlineCard padding="12px">
<Trans>Connect wallet</Trans> <StyledInput
</ButtonLight> className="start-price-input"
) : ( value={startPriceTypedValue}
<AutoColumn gap={'md'}> onUserInput={onStartPriceInput}
{(approvalA === ApprovalState.NOT_APPROVED || />
approvalA === ApprovalState.PENDING || </OutlineCard>
approvalB === ApprovalState.NOT_APPROVED || <RowBetween style={{ backgroundColor: theme.bg1, padding: '12px', borderRadius: '12px' }}>
approvalB === ApprovalState.PENDING) && <TYPE.main>
isValid && ( <Trans>Current {baseCurrency?.symbol} Price:</Trans>
<RowBetween> </TYPE.main>
{showApprovalA && ( <TYPE.main>
<ButtonPrimary {price ? (
onClick={approveACallback} <TYPE.main>
disabled={approvalA === ApprovalState.PENDING} <RowFixed>
width={showApprovalB ? '48%' : '100%'} <HoverInlineText
> maxCharacters={20}
{approvalA === ApprovalState.PENDING ? ( text={invertPrice ? price?.invert()?.toSignificant(5) : price?.toSignificant(5)}
<Dots> />{' '}
<Trans>Approving {currencies[Field.CURRENCY_A]?.symbol}</Trans> <span style={{ marginLeft: '4px' }}>{quoteCurrency?.symbol}</span>
</Dots> </RowFixed>
) : ( </TYPE.main>
<Trans>Approve {currencies[Field.CURRENCY_A]?.symbol}</Trans>
)}
</ButtonPrimary>
)}
{showApprovalB && (
<ButtonPrimary
onClick={approveBCallback}
disabled={approvalB === ApprovalState.PENDING}
width={showApprovalA ? '48%' : '100%'}
>
{approvalB === ApprovalState.PENDING ? (
<Dots>
<Trans>Approving {currencies[Field.CURRENCY_B]?.symbol}</Trans>
</Dots>
) : ( ) : (
<Trans>Approve {currencies[Field.CURRENCY_B]?.symbol}</Trans> '-'
)} )}
</ButtonPrimary> </TYPE.main>
)} </RowBetween>
</RowBetween> </AutoColumn>
)} )}
{mustCreateSeparately && ( </DynamicSection>
<ButtonError onClick={onCreate}>
<Text fontWeight={500}> <DynamicSection
<Trans>Create</Trans> gap="md"
</Text> disabled={!feeAmount || invalidPool || (noLiquidity && !startPriceTypedValue)}
</ButtonError>
)}
<ButtonError
onClick={() => {
expertMode ? onAdd() : setShowConfirm(true)
}}
disabled={
mustCreateSeparately ||
!isValid ||
(!argentWalletContract && approvalA !== ApprovalState.APPROVED && !depositADisabled) ||
(!argentWalletContract && approvalB !== ApprovalState.APPROVED && !depositBDisabled)
}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
> >
<Text fontWeight={500}>{errorMessage ? errorMessage : <Trans>Add</Trans>}</Text> <StackedContainer>
</ButtonError> <StackedItem style={{ opacity: showCapitalEfficiencyWarning ? '0.05' : 1 }}>
</AutoColumn> <AutoColumn gap="md">
)} {!noLiquidity && (
</div> <PresetsButtons
</AutoColumn> feeAmount={feeAmount}
setRange={(numTicks: number) => {
const [range1, range2] = getSetRange(numTicks)
onLeftRangeInput(invertPrice ? range2 : range1)
onRightRangeInput(invertPrice ? range1 : range2)
}}
setFullRange={() => {
setShowCapitalEfficiencyWarning(true)
}}
/>
)}
<RangeSelector
priceLower={priceLower}
priceUpper={priceUpper}
getDecrementLower={getDecrementLower}
getIncrementLower={getIncrementLower}
getDecrementUpper={getDecrementUpper}
getIncrementUpper={getIncrementUpper}
onLeftRangeInput={onLeftRangeInput}
onRightRangeInput={onRightRangeInput}
currencyA={baseCurrency}
currencyB={quoteCurrency}
feeAmount={feeAmount}
ticksAtLimit={ticksAtLimit}
/>
</AutoColumn>
</StackedItem>
{showCapitalEfficiencyWarning && (
<StackedItem zIndex={1}>
<YellowCard
padding="15px"
$borderRadius="12px"
height="100%"
style={{
borderColor: theme.yellow3,
border: '1px solid',
}}
>
<AutoColumn gap="8px" style={{ height: '100%' }}>
<RowFixed>
<AlertTriangle stroke={theme.yellow3} size="16px" />
<TYPE.yellow ml="12px" fontSize="15px">
<Trans>Efficiency Comparison</Trans>
</TYPE.yellow>
</RowFixed>
<RowFixed>
<TYPE.yellow ml="12px" fontSize="13px" margin={0} fontWeight={400}>
<Trans>
On Uniswap V3, setting a range across all prices like V2 is less capital efficient
than a concentrated one. Learn more{' '}
<ExternalLink
style={{ color: theme.yellow3, textDecoration: 'underline' }}
href={''}
>
here
</ExternalLink>
.
</Trans>
</TYPE.yellow>
</RowFixed>
<Row>
<ButtonYellow
padding="8px"
marginRight="8px"
$borderRadius="8px"
width="auto"
onClick={() => {
setShowCapitalEfficiencyWarning(false)
getSetFullRange()
}}
>
<TYPE.black fontSize={13} color="black">
<Trans>I Understand</Trans>
</TYPE.black>
</ButtonYellow>
</Row>
</AutoColumn>
</YellowCard>
</StackedItem>
)}
</StackedContainer>
{outOfRange ? (
<YellowCard padding="8px 12px" $borderRadius="12px">
<RowBetween>
<AlertTriangle stroke={theme.yellow3} size="16px" />
<TYPE.yellow ml="12px" fontSize="12px">
<Trans>
Your position will not earn fees or be used in trades until the market price moves into
your range.
</Trans>
</TYPE.yellow>
</RowBetween>
</YellowCard>
) : null}
{invalidRange ? (
<YellowCard padding="8px 12px" $borderRadius="12px">
<RowBetween>
<AlertTriangle stroke={theme.yellow3} size="16px" />
<TYPE.yellow ml="12px" fontSize="12px">
<Trans>Invalid range selected. The min price must be lower than the max price.</Trans>
</TYPE.yellow>
</RowBetween>
</YellowCard>
) : null}
</DynamicSection>
<MediumOnly>
<Buttons />
</MediumOnly>
</RightContainer>
</>
) : (
<Buttons />
)}
</ResponsiveTwoColumns>
</Wrapper> </Wrapper>
</AppBody> </PageWrapper>
{addIsUnsupported && ( {addIsUnsupported && (
<UnsupportedCurrencyFooter <UnsupportedCurrencyFooter
show={addIsUnsupported} show={addIsUnsupported}
......
...@@ -2,10 +2,22 @@ import styled from 'styled-components/macro' ...@@ -2,10 +2,22 @@ import styled from 'styled-components/macro'
import { AutoColumn } from 'components/Column' import { AutoColumn } from 'components/Column'
import CurrencyInputPanel from 'components/CurrencyInputPanel' import CurrencyInputPanel from 'components/CurrencyInputPanel'
import Input from 'components/NumericalInput' import Input from 'components/NumericalInput'
import { BodyWrapper } from 'pages/AppBody'
export const PageWrapper = styled(BodyWrapper)<{ wide: boolean }>`
max-width: ${({ wide }) => (wide ? '880px' : '480px')};
width: 100%;
padding: ${({ wide }) => (wide ? '10px' : '0')};
${({ theme }) => theme.mediaWidth.upToMedium`
max-width: 480px;
`};
`
export const Wrapper = styled.div` export const Wrapper = styled.div`
position: relative; position: relative;
padding: 20px; padding: 26px 16px;
min-width: 480px; min-width: 480px;
${({ theme }) => theme.mediaWidth.upToSmall` ${({ theme }) => theme.mediaWidth.upToSmall`
...@@ -38,3 +50,59 @@ export const StyledInput = styled(Input)` ...@@ -38,3 +50,59 @@ export const StyledInput = styled(Input)`
font-size: 18px; font-size: 18px;
width: 100%; width: 100%;
` `
/* two-column layout where DepositAmount is moved at the very end on mobile. */
export const ResponsiveTwoColumns = styled.div<{ wide: boolean }>`
display: grid;
grid-column-gap: 50px;
grid-row-gap: 15px;
grid-template-columns: ${({ wide }) => (wide ? '1fr 1fr' : '1fr')};
grid-template-rows: max-content;
grid-auto-flow: row;
padding-top: 20px;
border-top: 1px solid ${({ theme }) => theme.bg2};
${({ theme }) => theme.mediaWidth.upToMedium`
grid-template-columns: 1fr;
margin-top: 0;
`};
`
export const RightContainer = styled(AutoColumn)`
grid-row: 1 / 3;
grid-column: 2;
height: fit-content;
${({ theme }) => theme.mediaWidth.upToMedium`
grid-row: 2 / 3;
grid-column: 1;
`};
`
export const StackedContainer = styled.div`
display: grid;
`
export const StackedItem = styled.div<{ zIndex?: number }>`
grid-column: 1;
grid-row: 1;
height: 100%;
z-index: ${({ zIndex }) => zIndex};
`
export const MediumOnly = styled.div`
${({ theme }) => theme.mediaWidth.upToMedium`
display: none;
`};
`
export const HideMedium = styled.div`
display: none;
${({ theme }) => theme.mediaWidth.upToMedium`
display: block;
`};
`
...@@ -176,7 +176,7 @@ function V2PairMigration({ ...@@ -176,7 +176,7 @@ function V2PairMigration({
// the following is a small hack to get access to price range data/input handlers // the following is a small hack to get access to price range data/input handlers
const [baseToken, setBaseToken] = useState(token0) const [baseToken, setBaseToken] = useState(token0)
const { ticks, pricesAtTicks, invertPrice, invalidRange, outOfRange } = useV3DerivedMintInfo( const { ticks, pricesAtTicks, invertPrice, invalidRange, outOfRange, ticksAtLimit } = useV3DerivedMintInfo(
token0, token0,
token1, token1,
feeAmount, feeAmount,
...@@ -543,6 +543,7 @@ function V2PairMigration({ ...@@ -543,6 +543,7 @@ function V2PairMigration({
currencyA={invertPrice ? currency1 : currency0} currencyA={invertPrice ? currency1 : currency0}
currencyB={invertPrice ? currency0 : currency1} currencyB={invertPrice ? currency0 : currency1}
feeAmount={feeAmount} feeAmount={feeAmount}
ticksAtLimit={ticksAtLimit}
/> />
{outOfRange ? ( {outOfRange ? (
......
...@@ -41,6 +41,9 @@ import { SwitchLocaleLink } from '../../components/SwitchLocaleLink' ...@@ -41,6 +41,9 @@ import { SwitchLocaleLink } from '../../components/SwitchLocaleLink'
import useUSDCPrice from 'hooks/useUSDCPrice' import useUSDCPrice from 'hooks/useUSDCPrice'
import Loader from 'components/Loader' import Loader from 'components/Loader'
import Toggle from 'components/Toggle' import Toggle from 'components/Toggle'
import { Bound } from 'state/mint/v3/actions'
import useIsTickAtLimit from 'hooks/useIsTickAtLimit'
import { formatTickPrice } from 'utils/formatTickPrice'
const PageWrapper = styled.div` const PageWrapper = styled.div`
min-width: 800px; min-width: 800px;
...@@ -282,6 +285,26 @@ function NFT({ image, height: targetHeight }: { image: string; height: number }) ...@@ -282,6 +285,26 @@ function NFT({ image, height: targetHeight }: { image: string; height: number })
) )
} }
const useInverter = (
priceLower?: Price<Token, Token>,
priceUpper?: Price<Token, Token>,
quote?: Token,
base?: Token,
invert?: boolean
): {
priceLower?: Price<Token, Token>
priceUpper?: Price<Token, Token>
quote?: Token
base?: Token
} => {
return {
priceUpper: invert ? priceUpper?.invert() : priceUpper,
priceLower: invert ? priceLower?.invert() : priceLower,
quote,
base,
}
}
export function PositionPage({ export function PositionPage({
match: { match: {
params: { tokenId: tokenIdFromUrl }, params: { tokenId: tokenIdFromUrl },
...@@ -325,12 +348,20 @@ export function PositionPage({ ...@@ -325,12 +348,20 @@ export function PositionPage({
return undefined return undefined
}, [liquidity, pool, tickLower, tickUpper]) }, [liquidity, pool, tickLower, tickUpper])
let { priceLower, priceUpper, base, quote } = getPriceOrderingFromPositionForUI(position) const tickAtLimit = useIsTickAtLimit(feeAmount, tickLower, tickUpper)
const pricesFromPosition = getPriceOrderingFromPositionForUI(position)
const [manuallyInverted, setManuallyInverted] = useState(false) const [manuallyInverted, setManuallyInverted] = useState(false)
// handle manual inversion // handle manual inversion
if (manuallyInverted) { const { priceLower, priceUpper, base } = useInverter(
;[priceLower, priceUpper, base, quote] = [priceUpper?.invert(), priceLower?.invert(), quote, base] pricesFromPosition.priceUpper,
} pricesFromPosition.priceLower,
pricesFromPosition.quote,
pricesFromPosition.base,
manuallyInverted
)
const inverted = token1 ? base?.equals(token1) : undefined const inverted = token1 ? base?.equals(token1) : undefined
const currencyQuote = inverted ? currency0 : currency1 const currencyQuote = inverted ? currency0 : currency1
const currencyBase = inverted ? currency1 : currency0 const currencyBase = inverted ? currency1 : currency0
...@@ -358,6 +389,31 @@ export function PositionPage({ ...@@ -358,6 +389,31 @@ export function PositionPage({
const isCollectPending = useIsTransactionPending(collectMigrationHash ?? undefined) const isCollectPending = useIsTransactionPending(collectMigrationHash ?? undefined)
const [showConfirm, setShowConfirm] = useState(false) const [showConfirm, setShowConfirm] = useState(false)
// usdc prices always in terms of tokens
const price0 = useUSDCPrice(token0 ?? undefined)
const price1 = useUSDCPrice(token1 ?? undefined)
const fiatValueOfFees: CurrencyAmount<Currency> | null = useMemo(() => {
if (!price0 || !price1 || !feeValue0 || !feeValue1) return null
// we wrap because it doesn't matter, the quote returns a USDC amount
const feeValue0Wrapped = feeValue0?.wrapped
const feeValue1Wrapped = feeValue1?.wrapped
if (!feeValue0Wrapped || !feeValue1Wrapped) return null
const amount0 = price0.quote(feeValue0Wrapped)
const amount1 = price1.quote(feeValue1Wrapped)
return amount0.add(amount1)
}, [price0, price1, feeValue0, feeValue1])
const fiatValueOfLiquidity: CurrencyAmount<Token> | null = useMemo(() => {
if (!price0 || !price1 || !position) return null
const amount0 = price0.quote(position.amount0)
const amount1 = price1.quote(position.amount1)
return amount0.add(amount1)
}, [price0, price1, position])
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
const positionManager = useV3NFTPositionManagerContract() const positionManager = useV3NFTPositionManagerContract()
const collect = useCallback(() => { const collect = useCallback(() => {
...@@ -414,31 +470,6 @@ export function PositionPage({ ...@@ -414,31 +470,6 @@ export function PositionPage({
const owner = useSingleCallResult(!!tokenId ? positionManager : null, 'ownerOf', [tokenId]).result?.[0] const owner = useSingleCallResult(!!tokenId ? positionManager : null, 'ownerOf', [tokenId]).result?.[0]
const ownsNFT = owner === account || positionDetails?.operator === account const ownsNFT = owner === account || positionDetails?.operator === account
// usdc prices always in terms of tokens
const price0 = useUSDCPrice(token0 ?? undefined)
const price1 = useUSDCPrice(token1 ?? undefined)
const fiatValueOfFees: CurrencyAmount<Currency> | null = useMemo(() => {
if (!price0 || !price1 || !feeValue0 || !feeValue1) return null
// we wrap because it doesn't matter, the quote returns a USDC amount
const feeValue0Wrapped = feeValue0?.wrapped
const feeValue1Wrapped = feeValue1?.wrapped
if (!feeValue0Wrapped || !feeValue1Wrapped) return null
const amount0 = price0.quote(feeValue0Wrapped)
const amount1 = price1.quote(feeValue1Wrapped)
return amount0.add(amount1)
}, [price0, price1, feeValue0, feeValue1])
const fiatValueOfLiquidity: CurrencyAmount<Token> | null = useMemo(() => {
if (!price0 || !price1 || !position) return null
const amount0 = price0.quote(position.amount0)
const amount1 = price1.quote(position.amount1)
return amount0.add(amount1)
}, [price0, price1, position])
const feeValueUpper = inverted ? feeValue0 : feeValue1 const feeValueUpper = inverted ? feeValue0 : feeValue1
const feeValueLower = inverted ? feeValue1 : feeValue0 const feeValueLower = inverted ? feeValue1 : feeValue0
...@@ -779,7 +810,9 @@ export function PositionPage({ ...@@ -779,7 +810,9 @@ export function PositionPage({
<ExtentsText> <ExtentsText>
<Trans>Min price</Trans> <Trans>Min price</Trans>
</ExtentsText> </ExtentsText>
<TYPE.mediumHeader textAlign="center">{priceLower?.toSignificant(5)}</TYPE.mediumHeader> <TYPE.mediumHeader textAlign="center">
{formatTickPrice(priceLower, tickAtLimit, Bound.LOWER)}
</TYPE.mediumHeader>
<ExtentsText> <ExtentsText>
{' '} {' '}
<Trans> <Trans>
...@@ -801,7 +834,9 @@ export function PositionPage({ ...@@ -801,7 +834,9 @@ export function PositionPage({
<ExtentsText> <ExtentsText>
<Trans>Max price</Trans> <Trans>Max price</Trans>
</ExtentsText> </ExtentsText>
<TYPE.mediumHeader textAlign="center">{priceUpper?.toSignificant(5)}</TYPE.mediumHeader> <TYPE.mediumHeader textAlign="center">
{formatTickPrice(priceUpper, tickAtLimit, Bound.UPPER)}
</TYPE.mediumHeader>
<ExtentsText> <ExtentsText>
{' '} {' '}
<Trans> <Trans>
......
...@@ -16,3 +16,4 @@ export const typeStartPriceInput = createAction<{ typedValue: string }>('mintV3/ ...@@ -16,3 +16,4 @@ export const typeStartPriceInput = createAction<{ typedValue: string }>('mintV3/
export const typeLeftRangeInput = createAction<{ typedValue: string }>('mintV3/typeLeftRangeInput') export const typeLeftRangeInput = createAction<{ typedValue: string }>('mintV3/typeLeftRangeInput')
export const typeRightRangeInput = createAction<{ typedValue: string }>('mintV3/typeRightRangeInput') export const typeRightRangeInput = createAction<{ typedValue: string }>('mintV3/typeRightRangeInput')
export const resetMintState = createAction<void>('mintV3/resetMintState') export const resetMintState = createAction<void>('mintV3/resetMintState')
export const setFullRange = createAction<void>('mintV3/setFullRange')
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
tickToPrice, tickToPrice,
TICK_SPACINGS, TICK_SPACINGS,
encodeSqrtRatioX96, encodeSqrtRatioX96,
nearestUsableTick,
} from '@uniswap/v3-sdk/dist/' } from '@uniswap/v3-sdk/dist/'
import { Currency, Token, CurrencyAmount, Price, Rounding } from '@uniswap/sdk-core' import { Currency, Token, CurrencyAmount, Price, Rounding } from '@uniswap/sdk-core'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
...@@ -19,7 +20,15 @@ import { useActiveWeb3React } from '../../../hooks/web3' ...@@ -19,7 +20,15 @@ import { useActiveWeb3React } from '../../../hooks/web3'
import { AppState } from '../../index' import { AppState } from '../../index'
import { tryParseAmount } from '../../swap/hooks' import { tryParseAmount } from '../../swap/hooks'
import { useCurrencyBalances } from '../../wallet/hooks' import { useCurrencyBalances } from '../../wallet/hooks'
import { Field, Bound, typeInput, typeStartPriceInput, typeLeftRangeInput, typeRightRangeInput } from './actions' import {
Field,
Bound,
typeInput,
typeStartPriceInput,
typeLeftRangeInput,
typeRightRangeInput,
setFullRange,
} from './actions'
import { tryParseTick } from './utils' import { tryParseTick } from './utils'
import { usePool } from 'hooks/usePools' import { usePool } from 'hooks/usePools'
import { useAppDispatch, useAppSelector } from 'state/hooks' import { useAppDispatch, useAppSelector } from 'state/hooks'
...@@ -109,6 +118,7 @@ export function useV3DerivedMintInfo( ...@@ -109,6 +118,7 @@ export function useV3DerivedMintInfo(
depositADisabled: boolean depositADisabled: boolean
depositBDisabled: boolean depositBDisabled: boolean
invertPrice: boolean invertPrice: boolean
ticksAtLimit: { [bound in Bound]?: boolean | undefined }
} { } {
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
...@@ -207,6 +217,17 @@ export function useV3DerivedMintInfo( ...@@ -207,6 +217,17 @@ export function useV3DerivedMintInfo(
// if pool exists use it, if not use the mock pool // if pool exists use it, if not use the mock pool
const poolForPosition: Pool | undefined = pool ?? mockPool const poolForPosition: Pool | undefined = pool ?? mockPool
// lower and upper limits in the tick space for `feeAmount`
const tickSpaceLimits: {
[bound in Bound]: number | undefined
} = useMemo(
() => ({
[Bound.LOWER]: feeAmount ? nearestUsableTick(TickMath.MIN_TICK, TICK_SPACINGS[feeAmount]) : undefined,
[Bound.UPPER]: feeAmount ? nearestUsableTick(TickMath.MAX_TICK, TICK_SPACINGS[feeAmount]) : undefined,
}),
[feeAmount]
)
// parse typed range values and determine closest ticks // parse typed range values and determine closest ticks
// lower should always be a smaller tick // lower should always be a smaller tick
const ticks: { const ticks: {
...@@ -216,20 +237,44 @@ export function useV3DerivedMintInfo( ...@@ -216,20 +237,44 @@ export function useV3DerivedMintInfo(
[Bound.LOWER]: [Bound.LOWER]:
typeof existingPosition?.tickLower === 'number' typeof existingPosition?.tickLower === 'number'
? existingPosition.tickLower ? existingPosition.tickLower
: (invertPrice && typeof rightRangeTypedValue === 'boolean') ||
(!invertPrice && typeof leftRangeTypedValue === 'boolean')
? tickSpaceLimits[Bound.LOWER]
: invertPrice : invertPrice
? tryParseTick(token1, token0, feeAmount, rightRangeTypedValue) ? tryParseTick(token1, token0, feeAmount, rightRangeTypedValue.toString())
: tryParseTick(token0, token1, feeAmount, leftRangeTypedValue), : tryParseTick(token0, token1, feeAmount, leftRangeTypedValue.toString()),
[Bound.UPPER]: [Bound.UPPER]:
typeof existingPosition?.tickUpper === 'number' typeof existingPosition?.tickUpper === 'number'
? existingPosition.tickUpper ? existingPosition.tickUpper
: (!invertPrice && typeof rightRangeTypedValue === 'boolean') ||
(invertPrice && typeof leftRangeTypedValue === 'boolean')
? tickSpaceLimits[Bound.UPPER]
: invertPrice : invertPrice
? tryParseTick(token1, token0, feeAmount, leftRangeTypedValue) ? tryParseTick(token1, token0, feeAmount, leftRangeTypedValue.toString())
: tryParseTick(token0, token1, feeAmount, rightRangeTypedValue), : tryParseTick(token0, token1, feeAmount, rightRangeTypedValue.toString()),
} }
}, [existingPosition, feeAmount, invertPrice, leftRangeTypedValue, rightRangeTypedValue, token0, token1]) }, [
existingPosition,
feeAmount,
invertPrice,
leftRangeTypedValue,
rightRangeTypedValue,
token0,
token1,
tickSpaceLimits,
])
const { [Bound.LOWER]: tickLower, [Bound.UPPER]: tickUpper } = ticks || {} const { [Bound.LOWER]: tickLower, [Bound.UPPER]: tickUpper } = ticks || {}
// specifies whether the lower and upper ticks is at the exteme bounds
const ticksAtLimit = useMemo(
() => ({
[Bound.LOWER]: feeAmount && tickLower === tickSpaceLimits.LOWER,
[Bound.UPPER]: feeAmount && tickUpper === tickSpaceLimits.UPPER,
}),
[tickSpaceLimits, tickLower, tickUpper, feeAmount]
)
// mark invalid range // mark invalid range
const invalidRange = Boolean(typeof tickLower === 'number' && typeof tickUpper === 'number' && tickLower >= tickUpper) const invalidRange = Boolean(typeof tickLower === 'number' && typeof tickUpper === 'number' && tickLower >= tickUpper)
...@@ -428,6 +473,7 @@ export function useV3DerivedMintInfo( ...@@ -428,6 +473,7 @@ export function useV3DerivedMintInfo(
depositADisabled, depositADisabled,
depositBDisabled, depositBDisabled,
invertPrice, invertPrice,
ticksAtLimit,
} }
} }
...@@ -439,6 +485,8 @@ export function useRangeHopCallbacks( ...@@ -439,6 +485,8 @@ export function useRangeHopCallbacks(
tickUpper: number | undefined, tickUpper: number | undefined,
pool?: Pool | undefined | null pool?: Pool | undefined | null
) { ) {
const dispatch = useAppDispatch()
const baseToken = useMemo(() => baseCurrency?.wrapped, [baseCurrency]) const baseToken = useMemo(() => baseCurrency?.wrapped, [baseCurrency])
const quoteToken = useMemo(() => quoteCurrency?.wrapped, [quoteCurrency]) const quoteToken = useMemo(() => quoteCurrency?.wrapped, [quoteCurrency])
...@@ -494,5 +542,34 @@ export function useRangeHopCallbacks( ...@@ -494,5 +542,34 @@ export function useRangeHopCallbacks(
return '' return ''
}, [baseToken, quoteToken, tickUpper, feeAmount, pool]) }, [baseToken, quoteToken, tickUpper, feeAmount, pool])
return { getDecrementLower, getIncrementLower, getDecrementUpper, getIncrementUpper } const getSetRange = useCallback(
(numTicks: number) => {
if (baseToken && quoteToken && feeAmount && pool) {
// calculate range around current price given `numTicks`
const newPriceLower = tickToPrice(
baseToken,
quoteToken,
Math.max(TickMath.MIN_TICK, pool.tickCurrent - numTicks)
)
const newPriceUpper = tickToPrice(
baseToken,
quoteToken,
Math.min(TickMath.MAX_TICK, pool.tickCurrent + numTicks)
)
return [
newPriceLower.toSignificant(5, undefined, Rounding.ROUND_UP),
newPriceUpper.toSignificant(5, undefined, Rounding.ROUND_UP),
]
}
return ['', '']
},
[baseToken, quoteToken, feeAmount, pool]
)
const getSetFullRange = useCallback(() => {
dispatch(setFullRange())
}, [dispatch])
return { getDecrementLower, getIncrementLower, getDecrementUpper, getIncrementUpper, getSetRange, getSetFullRange }
} }
...@@ -2,18 +2,21 @@ import { createReducer } from '@reduxjs/toolkit' ...@@ -2,18 +2,21 @@ import { createReducer } from '@reduxjs/toolkit'
import { import {
Field, Field,
resetMintState, resetMintState,
setFullRange,
typeInput, typeInput,
typeStartPriceInput, typeStartPriceInput,
typeLeftRangeInput, typeLeftRangeInput,
typeRightRangeInput, typeRightRangeInput,
} from './actions' } from './actions'
export type FullRange = true
interface MintState { interface MintState {
readonly independentField: Field readonly independentField: Field
readonly typedValue: string readonly typedValue: string
readonly startPriceTypedValue: string // for the case when there's no liquidity readonly startPriceTypedValue: string // for the case when there's no liquidity
readonly leftRangeTypedValue: string readonly leftRangeTypedValue: string | FullRange
readonly rightRangeTypedValue: string readonly rightRangeTypedValue: string | FullRange
} }
const initialState: MintState = { const initialState: MintState = {
...@@ -27,6 +30,13 @@ const initialState: MintState = { ...@@ -27,6 +30,13 @@ const initialState: MintState = {
export default createReducer<MintState>(initialState, (builder) => export default createReducer<MintState>(initialState, (builder) =>
builder builder
.addCase(resetMintState, () => initialState) .addCase(resetMintState, () => initialState)
.addCase(setFullRange, (state) => {
return {
...state,
leftRangeTypedValue: true,
rightRangeTypedValue: true,
}
})
.addCase(typeStartPriceInput, (state, { payload: { typedValue } }) => { .addCase(typeStartPriceInput, (state, { payload: { typedValue } }) => {
return { return {
...state, ...state,
......
import { Bound } from '../state/mint/v3/actions'
import { Price, Token } from '@uniswap/sdk-core'
import { formatPrice } from './formatCurrencyAmount'
export function formatTickPrice(
price: Price<Token, Token> | undefined,
atLimit: { [bound in Bound]?: boolean | undefined },
direction: Bound,
placeholder?: string
) {
if (atLimit[direction]) {
return direction === Bound.LOWER ? '0' : ''
}
if (!price && placeholder !== undefined) {
return placeholder
}
return formatPrice(price, 5)
}
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