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>
) )
......
This diff is collapsed.
...@@ -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