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)`
border: 1px solid ${({ theme }) => theme.bg2};
background-color: transparent;
color: ${({ theme }) => theme.text1};
&:focus {
box-shadow: 0 0 0 1px ${({ theme }) => theme.bg4};
}
......@@ -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)`
background-color: transparent;
color: ${({ theme }) => theme.primary1};
......
......@@ -173,7 +173,7 @@ export default function FeeSelector({
onClick={() => handleFeePoolSelectWithEvent(FeeAmount.LOW)}
>
<AutoColumn gap="sm" justify="flex-start">
<AutoColumn justify="flex-start" gap="4px">
<AutoColumn justify="flex-start" gap="6px">
<ResponsiveText>
<Trans>0.05% fee</Trans>
</ResponsiveText>
......
import { useState, useCallback, useEffect, ReactNode } from 'react'
import { LightCard } from 'components/Card'
import { RowBetween } from 'components/Row'
import { OutlineCard } from 'components/Card'
import { Input as NumericalInput } from '../NumericalInput'
import styled, { keyframes } from 'styled-components/macro'
import { TYPE } from 'theme'
import { AutoColumn } from 'components/Column'
import { ButtonPrimary } from 'components/Button'
import { ButtonGray } from 'components/Button'
import { FeeAmount } from '@uniswap/v3-sdk'
import { formattedFeeAmount } from 'utils'
import { Trans } from '@lingui/macro'
import { Plus, Minus } from 'react-feather'
const pulse = (color: string) => keyframes`
0% {
......@@ -24,25 +23,29 @@ const pulse = (color: string) => keyframes`
}
`
const SmallButton = styled(ButtonPrimary)`
/* background-color: ${({ theme }) => theme.bg2}; */
const InputRow = styled.div`
display: grid;
grid-template-columns: 30px 1fr 30px;
`
const SmallButton = styled(ButtonGray)`
border-radius: 8px;
padding: 4px 6px;
width: 48%;
padding: 4px;
`
const FocusedOutlineCard = styled(LightCard)<{ active?: boolean; pulsing?: boolean }>`
const FocusedOutlineCard = styled(OutlineCard)<{ active?: boolean; pulsing?: boolean }>`
border-color: ${({ active, theme }) => active && theme.blue1};
padding: 12px;
animation: ${({ pulsing, theme }) => pulsing && pulse(theme.blue1)} 0.8s linear;
`
const StyledInput = styled(NumericalInput)<{ usePercent?: boolean }>`
/* background-color: ${({ theme }) => theme.bg0}; */
background-color: transparent;
text-align: center;
margin-right: 12px;
width: 100%;
font-weight: 500;
padding: 0 10px;
`
const InputTitle = styled(TYPE.small)`
......@@ -51,11 +54,17 @@ const InputTitle = styled(TYPE.small)`
font-weight: 500;
`
const ButtonLabel = styled(TYPE.white)<{ disabled: boolean }>`
color: ${({ theme, disabled }) => (disabled ? theme.text2 : theme.text1)} !important;
`
interface StepCounterProps {
value: string
onUserInput: (value: string) => void
decrement: () => string
increment: () => string
decrementDisabled?: boolean
incrementDisabled?: boolean
feeAmount?: FeeAmount
label?: string
width?: string
......@@ -69,7 +78,8 @@ const StepCounter = ({
value,
decrement,
increment,
feeAmount,
decrementDisabled = false,
incrementDisabled = false,
width,
locked,
onUserInput,
......@@ -87,9 +97,6 @@ const StepCounter = ({
// animation if parent value updates local value
const [pulsing, setPulsing] = useState<boolean>(false)
// format fee amount
const feeAmountFormatted = feeAmount ? formattedFeeAmount(feeAmount * 2) : ''
const handleOnFocus = () => {
setUseLocalValue(true)
setActive(true)
......@@ -126,39 +133,45 @@ const StepCounter = ({
return (
<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">
{title}
</InputTitle>
<StyledInput
className="rate-input-0"
value={localValue}
fontSize="20px"
disabled={locked}
onUserInput={(val) => {
setLocalValue(val)
}}
/>
<InputRow>
{!locked && (
<SmallButton onClick={handleDecrement} disabled={decrementDisabled}>
<ButtonLabel disabled={decrementDisabled} fontSize="12px">
<Minus size={18} />
</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">
<Trans>
{tokenB} per {tokenA}
</Trans>
</InputTitle>
</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>
)
}
......
......@@ -13,6 +13,8 @@ import { resetMintState } from 'state/mint/actions'
import { resetMintState as resetMintV3State } from 'state/mint/v3/actions'
import { TYPE } from 'theme'
import useTheme from 'hooks/useTheme'
import { ReactNode } from 'react'
import { Box } from 'rebass'
const Tabs = styled.div`
${({ theme }) => theme.flexRowNoWrap}
......@@ -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`
font-weight: 500;
font-size: 20px;
......@@ -91,11 +102,14 @@ export function AddRemoveTabs({
creating,
defaultSlippage,
positionID,
children,
}: {
adding: boolean
creating: boolean
defaultSlippage: Percent
positionID?: string | undefined
showBackLink?: boolean
children?: ReactNode | undefined
}) {
const theme = useTheme()
// reset states on back
......@@ -110,7 +124,7 @@ export function AddRemoveTabs({
return (
<Tabs>
<RowBetween style={{ padding: '1rem 1rem 0 1rem' }}>
<HistoryLink
<StyledHistoryLink
to={poolLink}
onClick={() => {
if (adding) {
......@@ -119,10 +133,15 @@ export function AddRemoveTabs({
dispatch(resetMintV3State())
}
}}
flex={children ? '1' : undefined}
>
<StyledArrowLeft stroke={theme.text2} />
</HistoryLink>
<TYPE.mediumHeader fontWeight={500} fontSize={20}>
</StyledHistoryLink>
<TYPE.mediumHeader
fontWeight={500}
fontSize={20}
style={{ flex: '1', margin: 'auto', textAlign: children ? 'start' : 'center' }}
>
{creating ? (
<Trans>Create a pair</Trans>
) : adding ? (
......@@ -131,6 +150,7 @@ export function AddRemoveTabs({
<Trans>Remove Liquidity</Trans>
)}
</TYPE.mediumHeader>
<Box style={{ marginRight: '.5rem' }}>{children}</Box>
<SettingsTab placeholderSlippage={defaultSlippage} />
</RowBetween>
</Tabs>
......
......@@ -9,7 +9,7 @@ import styled from 'styled-components/macro'
import { HideSmall, MEDIA_WIDTHS, SmallOnly } from 'theme'
import { PositionDetails } from 'types/position'
import { Price, Token, Percent } from '@uniswap/sdk-core'
import { formatPrice } from 'utils/formatCurrencyAmount'
import { formatTickPrice } from 'utils/formatTickPrice'
import Loader from 'components/Loader'
import { unwrappedToken } from 'utils/unwrappedToken'
import RangeBadge from 'components/Badge/RangeBadge'
......@@ -17,6 +17,8 @@ import { RowFixed } from 'components/Row'
import HoverInlineText from 'components/HoverInlineText'
import { DAI, USDC, USDT, WBTC, WETH9_EXTENDED } from '../../constants/tokens'
import { Trans } from '@lingui/macro'
import useIsTickAtLimit from 'hooks/useIsTickAtLimit'
import { Bound } from 'state/mint/v3/actions'
const LinkRow = styled(Link)`
align-items: center;
......@@ -201,6 +203,8 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
return undefined
}, [liquidity, pool, tickLower, tickUpper])
const tickAtLimit = useIsTickAtLimit(feeAmount, tickLower, tickUpper)
// prices
const { priceLower, priceUpper, quote, base } = getPriceOrderingFromPositionForUI(position)
......@@ -239,8 +243,8 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
<Trans>Min: </Trans>
</ExtentsText>
<Trans>
{formatPrice(priceLower, 5)} <HoverInlineText text={currencyQuote?.symbol} /> per{' '}
<HoverInlineText text={currencyBase?.symbol ?? ''} />
{formatTickPrice(priceLower, tickAtLimit, Bound.LOWER)} <HoverInlineText text={currencyQuote?.symbol} />{' '}
per <HoverInlineText text={currencyBase?.symbol ?? ''} />
</Trans>
</RangeText>{' '}
<HideSmall>
......@@ -254,8 +258,8 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
<Trans>Max:</Trans>
</ExtentsText>
<Trans>
{formatPrice(priceUpper, 5)} <HoverInlineText text={currencyQuote?.symbol} /> per{' '}
<HoverInlineText maxCharacters={10} text={currencyBase?.symbol} />
{formatTickPrice(priceUpper, tickAtLimit, Bound.UPPER)} <HoverInlineText text={currencyQuote?.symbol} />{' '}
per <HoverInlineText maxCharacters={10} text={currencyBase?.symbol} />
</Trans>
</RangeText>
</RangeLineItem>
......
......@@ -14,17 +14,21 @@ import DoubleCurrencyLogo from 'components/DoubleLogo'
import RangeBadge from 'components/Badge/RangeBadge'
import { ThemeContext } from 'styled-components/macro'
import JSBI from 'jsbi'
import { Bound } from 'state/mint/v3/actions'
import { formatTickPrice } from 'utils/formatTickPrice'
export const PositionPreview = ({
position,
title,
inRange,
baseCurrencyDefault,
ticksAtLimit,
}: {
position: Position
title?: ReactNode
inRange: boolean
baseCurrencyDefault?: Currency | undefined
ticksAtLimit: { [bound: string]: boolean | undefined }
}) => {
const theme = useContext(ThemeContext)
......@@ -121,7 +125,11 @@ export const PositionPreview = ({
<TYPE.main fontSize="12px">
<Trans>Min Price</Trans>
</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">
<Trans>
{quoteCurrency.symbol} per {baseCurrency.symbol}
......@@ -138,7 +146,11 @@ export const PositionPreview = ({
<TYPE.main fontSize="12px">
<Trans>Max Price</Trans>
</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">
<Trans>
{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'
import { Currency, Price, Token } from '@uniswap/sdk-core'
import StepCounter from 'components/InputStepCounter/InputStepCounter'
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
export default function RangeSelector({
......@@ -16,6 +19,7 @@ export default function RangeSelector({
currencyA,
currencyB,
feeAmount,
ticksAtLimit,
}: {
priceLower?: Price<Token, Token>
priceUpper?: Price<Token, Token>
......@@ -28,6 +32,7 @@ export default function RangeSelector({
currencyA?: Currency | null
currencyB?: Currency | null
feeAmount?: number
ticksAtLimit: { [bound in Bound]?: boolean | undefined }
}) {
const tokenA = (currencyA ?? undefined)?.wrapped
const tokenB = (currencyB ?? undefined)?.wrapped
......@@ -37,31 +42,37 @@ export default function RangeSelector({
const rightPrice = isSorted ? priceUpper : priceLower?.invert()
return (
<RowBetween>
<StepCounter
value={leftPrice?.toSignificant(5) ?? ''}
onUserInput={onLeftRangeInput}
width="48%"
decrement={isSorted ? getDecrementLower : getIncrementUpper}
increment={isSorted ? getIncrementLower : getDecrementUpper}
feeAmount={feeAmount}
label={leftPrice ? `${currencyB?.symbol}` : '-'}
title={<Trans>Min Price</Trans>}
tokenA={currencyA?.symbol}
tokenB={currencyB?.symbol}
/>
<StepCounter
value={rightPrice?.toSignificant(5) ?? ''}
onUserInput={onRightRangeInput}
width="48%"
decrement={isSorted ? getDecrementUpper : getIncrementLower}
increment={isSorted ? getIncrementUpper : getDecrementLower}
feeAmount={feeAmount}
label={rightPrice ? `${currencyB?.symbol}` : '-'}
tokenA={currencyA?.symbol}
tokenB={currencyB?.symbol}
title={<Trans>Max Price</Trans>}
/>
</RowBetween>
<AutoColumn gap="md">
<RowBetween>
<StepCounter
value={formatTickPrice(leftPrice, ticksAtLimit, Bound.LOWER, '')}
onUserInput={onLeftRangeInput}
width="48%"
decrement={isSorted ? getDecrementLower : getIncrementUpper}
increment={isSorted ? getIncrementLower : getDecrementUpper}
decrementDisabled={ticksAtLimit[Bound.LOWER]}
incrementDisabled={ticksAtLimit[Bound.LOWER]}
feeAmount={feeAmount}
label={leftPrice ? `${currencyB?.symbol}` : '-'}
title={<Trans>Min Price</Trans>}
tokenA={currencyA?.symbol}
tokenB={currencyB?.symbol}
/>
<StepCounter
value={formatTickPrice(rightPrice, ticksAtLimit, Bound.UPPER, '')}
onUserInput={onRightRangeInput}
width="48%"
decrement={isSorted ? getDecrementUpper : getIncrementLower}
increment={isSorted ? getIncrementUpper : getDecrementLower}
incrementDisabled={ticksAtLimit[Bound.UPPER]}
decrementDisabled={ticksAtLimit[Bound.UPPER]}
feeAmount={feeAmount}
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({
<div style={{ width: 'fit-content', display: 'flex', alignItems: 'center' }} onClick={handleRateToggle}>
<ToggleWrapper width="fit-content">
<ToggleElement isActive={isSorted} fontSize="12px">
<Trans>{isSorted ? currencyA.symbol : currencyB.symbol} price</Trans>
<Trans>{isSorted ? currencyA.symbol : currencyB.symbol}</Trans>
</ToggleElement>
<ToggleElement isActive={!isSorted} fontSize="12px">
<Trans>{isSorted ? currencyB.symbol : currencyA.symbol} price</Trans>
<Trans>{isSorted ? currencyB.symbol : currencyA.symbol}</Trans>
</ToggleElement>
</ToggleWrapper>
</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 styled from 'styled-components/macro'
import { Currency, CurrencyAmount, Price } from '@uniswap/sdk-core'
......@@ -12,6 +12,7 @@ const Wrapper = styled.div`
export function Review({
position,
outOfRange,
ticksAtLimit,
}: {
position?: Position
existingPosition?: Position
......@@ -19,11 +20,19 @@ export function Review({
priceLower?: Price<Currency, Currency>
priceUpper?: Price<Currency, Currency>
outOfRange: boolean
ticksAtLimit: { [bound in Bound]?: boolean | undefined }
}) {
return (
<Wrapper>
<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>
</Wrapper>
)
......
This diff is collapsed.
......@@ -2,10 +2,22 @@ import styled from 'styled-components/macro'
import { AutoColumn } from 'components/Column'
import CurrencyInputPanel from 'components/CurrencyInputPanel'
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`
position: relative;
padding: 20px;
padding: 26px 16px;
min-width: 480px;
${({ theme }) => theme.mediaWidth.upToSmall`
......@@ -38,3 +50,59 @@ export const StyledInput = styled(Input)`
font-size: 18px;
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({
// the following is a small hack to get access to price range data/input handlers
const [baseToken, setBaseToken] = useState(token0)
const { ticks, pricesAtTicks, invertPrice, invalidRange, outOfRange } = useV3DerivedMintInfo(
const { ticks, pricesAtTicks, invertPrice, invalidRange, outOfRange, ticksAtLimit } = useV3DerivedMintInfo(
token0,
token1,
feeAmount,
......@@ -543,6 +543,7 @@ function V2PairMigration({
currencyA={invertPrice ? currency1 : currency0}
currencyB={invertPrice ? currency0 : currency1}
feeAmount={feeAmount}
ticksAtLimit={ticksAtLimit}
/>
{outOfRange ? (
......
......@@ -41,6 +41,9 @@ import { SwitchLocaleLink } from '../../components/SwitchLocaleLink'
import useUSDCPrice from 'hooks/useUSDCPrice'
import Loader from 'components/Loader'
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`
min-width: 800px;
......@@ -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({
match: {
params: { tokenId: tokenIdFromUrl },
......@@ -325,12 +348,20 @@ export function PositionPage({
return undefined
}, [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)
// handle manual inversion
if (manuallyInverted) {
;[priceLower, priceUpper, base, quote] = [priceUpper?.invert(), priceLower?.invert(), quote, base]
}
const { priceLower, priceUpper, base } = useInverter(
pricesFromPosition.priceUpper,
pricesFromPosition.priceLower,
pricesFromPosition.quote,
pricesFromPosition.base,
manuallyInverted
)
const inverted = token1 ? base?.equals(token1) : undefined
const currencyQuote = inverted ? currency0 : currency1
const currencyBase = inverted ? currency1 : currency0
......@@ -358,6 +389,31 @@ export function PositionPage({
const isCollectPending = useIsTransactionPending(collectMigrationHash ?? undefined)
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 positionManager = useV3NFTPositionManagerContract()
const collect = useCallback(() => {
......@@ -414,31 +470,6 @@ export function PositionPage({
const owner = useSingleCallResult(!!tokenId ? positionManager : null, 'ownerOf', [tokenId]).result?.[0]
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 feeValueLower = inverted ? feeValue1 : feeValue0
......@@ -779,7 +810,9 @@ export function PositionPage({
<ExtentsText>
<Trans>Min price</Trans>
</ExtentsText>
<TYPE.mediumHeader textAlign="center">{priceLower?.toSignificant(5)}</TYPE.mediumHeader>
<TYPE.mediumHeader textAlign="center">
{formatTickPrice(priceLower, tickAtLimit, Bound.LOWER)}
</TYPE.mediumHeader>
<ExtentsText>
{' '}
<Trans>
......@@ -801,7 +834,9 @@ export function PositionPage({
<ExtentsText>
<Trans>Max price</Trans>
</ExtentsText>
<TYPE.mediumHeader textAlign="center">{priceUpper?.toSignificant(5)}</TYPE.mediumHeader>
<TYPE.mediumHeader textAlign="center">
{formatTickPrice(priceUpper, tickAtLimit, Bound.UPPER)}
</TYPE.mediumHeader>
<ExtentsText>
{' '}
<Trans>
......
......@@ -16,3 +16,4 @@ export const typeStartPriceInput = createAction<{ typedValue: string }>('mintV3/
export const typeLeftRangeInput = createAction<{ typedValue: string }>('mintV3/typeLeftRangeInput')
export const typeRightRangeInput = createAction<{ typedValue: string }>('mintV3/typeRightRangeInput')
export const resetMintState = createAction<void>('mintV3/resetMintState')
export const setFullRange = createAction<void>('mintV3/setFullRange')
......@@ -12,6 +12,7 @@ import {
tickToPrice,
TICK_SPACINGS,
encodeSqrtRatioX96,
nearestUsableTick,
} from '@uniswap/v3-sdk/dist/'
import { Currency, Token, CurrencyAmount, Price, Rounding } from '@uniswap/sdk-core'
import { useCallback, useMemo } from 'react'
......@@ -19,7 +20,15 @@ import { useActiveWeb3React } from '../../../hooks/web3'
import { AppState } from '../../index'
import { tryParseAmount } from '../../swap/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 { usePool } from 'hooks/usePools'
import { useAppDispatch, useAppSelector } from 'state/hooks'
......@@ -109,6 +118,7 @@ export function useV3DerivedMintInfo(
depositADisabled: boolean
depositBDisabled: boolean
invertPrice: boolean
ticksAtLimit: { [bound in Bound]?: boolean | undefined }
} {
const { account } = useActiveWeb3React()
......@@ -207,6 +217,17 @@ export function useV3DerivedMintInfo(
// if pool exists use it, if not use the mock pool
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
// lower should always be a smaller tick
const ticks: {
......@@ -216,20 +237,44 @@ export function useV3DerivedMintInfo(
[Bound.LOWER]:
typeof existingPosition?.tickLower === 'number'
? existingPosition.tickLower
: (invertPrice && typeof rightRangeTypedValue === 'boolean') ||
(!invertPrice && typeof leftRangeTypedValue === 'boolean')
? tickSpaceLimits[Bound.LOWER]
: invertPrice
? tryParseTick(token1, token0, feeAmount, rightRangeTypedValue)
: tryParseTick(token0, token1, feeAmount, leftRangeTypedValue),
? tryParseTick(token1, token0, feeAmount, rightRangeTypedValue.toString())
: tryParseTick(token0, token1, feeAmount, leftRangeTypedValue.toString()),
[Bound.UPPER]:
typeof existingPosition?.tickUpper === 'number'
? existingPosition.tickUpper
: (!invertPrice && typeof rightRangeTypedValue === 'boolean') ||
(invertPrice && typeof leftRangeTypedValue === 'boolean')
? tickSpaceLimits[Bound.UPPER]
: invertPrice
? tryParseTick(token1, token0, feeAmount, leftRangeTypedValue)
: tryParseTick(token0, token1, feeAmount, rightRangeTypedValue),
? tryParseTick(token1, token0, feeAmount, leftRangeTypedValue.toString())
: 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 || {}
// 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
const invalidRange = Boolean(typeof tickLower === 'number' && typeof tickUpper === 'number' && tickLower >= tickUpper)
......@@ -428,6 +473,7 @@ export function useV3DerivedMintInfo(
depositADisabled,
depositBDisabled,
invertPrice,
ticksAtLimit,
}
}
......@@ -439,6 +485,8 @@ export function useRangeHopCallbacks(
tickUpper: number | undefined,
pool?: Pool | undefined | null
) {
const dispatch = useAppDispatch()
const baseToken = useMemo(() => baseCurrency?.wrapped, [baseCurrency])
const quoteToken = useMemo(() => quoteCurrency?.wrapped, [quoteCurrency])
......@@ -494,5 +542,34 @@ export function useRangeHopCallbacks(
return ''
}, [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'
import {
Field,
resetMintState,
setFullRange,
typeInput,
typeStartPriceInput,
typeLeftRangeInput,
typeRightRangeInput,
} from './actions'
export type FullRange = true
interface MintState {
readonly independentField: Field
readonly typedValue: string
readonly startPriceTypedValue: string // for the case when there's no liquidity
readonly leftRangeTypedValue: string
readonly rightRangeTypedValue: string
readonly leftRangeTypedValue: string | FullRange
readonly rightRangeTypedValue: string | FullRange
}
const initialState: MintState = {
......@@ -27,6 +30,13 @@ const initialState: MintState = {
export default createReducer<MintState>(initialState, (builder) =>
builder
.addCase(resetMintState, () => initialState)
.addCase(setFullRange, (state) => {
return {
...state,
leftRangeTypedValue: true,
rightRangeTypedValue: true,
}
})
.addCase(typeStartPriceInput, (state, { payload: { typedValue } }) => {
return {
...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