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>
)
......
import { useCallback, useContext, useMemo, useState } from 'react'
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { TransactionResponse } from '@ethersproject/providers'
import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { AlertTriangle, AlertCircle } from 'react-feather'
......@@ -11,12 +11,12 @@ import { useV3NFTPositionManagerContract } from '../../hooks/useContract'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components/macro'
import { ButtonError, ButtonLight, ButtonPrimary, ButtonText } from '../../components/Button'
import { YellowCard, OutlineCard, BlueCard, LightCard } from '../../components/Card'
import { ButtonError, ButtonLight, ButtonPrimary, ButtonText, ButtonYellow } from '../../components/Button'
import { YellowCard, OutlineCard, BlueCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal'
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 { useUSDCValue } from '../../hooks/useUSDCPrice'
import approveAmountCalldata from '../../utils/approveAmountCalldata'
......@@ -33,11 +33,23 @@ import { useTransactionAdder } from '../../state/transactions/hooks'
import { useIsExpertMode, useUserSlippageToleranceWithDefault } from '../../state/user/hooks'
import { TYPE, ExternalLink } from '../../theme'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import AppBody from '../AppBody'
import { Dots } from '../Pool/styleds'
import { currencyId } from '../../utils/currencyId'
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 {
useV3MintState,
......@@ -56,6 +68,8 @@ import { BigNumber } from '@ethersproject/bignumber'
import { AddRemoveTabs } from 'components/NavigationTabs'
import HoverInlineText from 'components/HoverInlineText'
import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
import PresetsButtons from 'components/RangeSelector/PresetsButtons'
import LiquidityChartRangeInput from 'components/LiquidityChartRangeInput'
import { SupportedChainId } from 'constants/chains'
const DEFAULT_ADD_IN_RANGE_SLIPPAGE_TOLERANCE = new Percent(50, 10_000)
......@@ -118,6 +132,7 @@ export default function AddLiquidity({
depositADisabled,
depositBDisabled,
invertPrice,
ticksAtLimit,
} = useV3DerivedMintInfo(
currencyA ?? undefined,
currencyB ?? undefined,
......@@ -135,6 +150,11 @@ export default function AddLiquidity({
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
// capital efficiency warning
const [showCapitalEfficiencyWarning, setShowCapitalEfficiencyWarning] = useState(false)
useEffect(() => setShowCapitalEfficiencyWarning(false), [currencyA, currencyB, feeAmount])
// txn values
const deadline = useTransactionDeadline() // custom from users settings
......@@ -399,9 +419,11 @@ export default function AddLiquidity({
const handleFeePoolSelect = useCallback(
(newFeeAmount: FeeAmount) => {
onLeftRangeInput('')
onRightRangeInput('')
history.push(`/add/${currencyIdA}/${currencyIdB}/${newFeeAmount}`)
},
[currencyIdA, currencyIdB, history]
[currencyIdA, currencyIdB, history, onLeftRangeInput, onRightRangeInput]
)
const handleDismissConfirmation = useCallback(() => {
......@@ -428,14 +450,8 @@ export default function AddLiquidity({
const { [Bound.LOWER]: tickLower, [Bound.UPPER]: tickUpper } = ticks
const { [Bound.LOWER]: priceLower, [Bound.UPPER]: priceUpper } = pricesAtTicks
const { getDecrementLower, getIncrementLower, getDecrementUpper, getIncrementUpper } = useRangeHopCallbacks(
baseCurrency ?? undefined,
quoteCurrency ?? undefined,
feeAmount,
tickLower,
tickUpper,
pool
)
const { getDecrementLower, getIncrementLower, getDecrementUpper, getIncrementUpper, getSetRange, getSetFullRange } =
useRangeHopCallbacks(baseCurrency ?? undefined, quoteCurrency ?? undefined, feeAmount, tickLower, tickUpper, pool)
// we need an existence check on parsed amounts for single-asset deposits
const showApprovalA =
......@@ -443,6 +459,80 @@ export default function AddLiquidity({
const showApprovalB =
!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
const mustCreateSeparately =
noLiquidity && (chainId === SupportedChainId.OPTIMISM || chainId === SupportedChainId.OPTIMISTIC_KOVAN)
......@@ -468,6 +558,7 @@ export default function AddLiquidity({
priceLower={priceLower}
priceUpper={priceUpper}
outOfRange={outOfRange}
ticksAtLimit={ticksAtLimit}
/>
)}
bottomContent={() => (
......@@ -481,369 +572,397 @@ export default function AddLiquidity({
)}
pendingText={pendingText}
/>
<AppBody>
<PageWrapper wide={!hasExistingPosition}>
<AddRemoveTabs
creating={false}
adding={true}
positionID={tokenId}
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>
<AutoColumn gap="32px">
{!hasExistingPosition && (
<>
<AutoColumn gap="md">
<RowBetween paddingBottom="20px">
<TYPE.label>
<Trans>Select pair</Trans>
</TYPE.label>
<ButtonText onClick={clearAll}>
<TYPE.blue fontSize="12px">
<Trans>Clear All</Trans>
</TYPE.blue>
</ButtonText>
</RowBetween>
<RowBetween>
<CurrencyDropdown
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onFieldAInput}
hideInput={true}
onMax={() => {
onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '')
}}
onCurrencySelect={handleCurrencyASelect}
showMaxButton={!atMaxAmounts[Field.CURRENCY_A]}
currency={currencies[Field.CURRENCY_A]}
id="add-liquidity-input-tokena"
showCommonBases
/>
<div style={{ width: '12px' }} />
<CurrencyDropdown
value={formattedAmounts[Field.CURRENCY_B]}
hideInput={true}
onUserInput={onFieldBInput}
onCurrencySelect={handleCurrencyBSelect}
onMax={() => {
onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '')
}}
showMaxButton={!atMaxAmounts[Field.CURRENCY_B]}
currency={currencies[Field.CURRENCY_B]}
id="add-liquidity-input-tokenb"
showCommonBases
<ResponsiveTwoColumns wide={!hasExistingPosition}>
<AutoColumn gap="lg">
{!hasExistingPosition && (
<>
<AutoColumn gap="md">
<RowBetween paddingBottom="20px">
<TYPE.label>
<Trans>Select Pair</Trans>
</TYPE.label>
</RowBetween>
<RowBetween>
<CurrencyDropdown
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onFieldAInput}
hideInput={true}
onMax={() => {
onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '')
}}
onCurrencySelect={handleCurrencyASelect}
showMaxButton={!atMaxAmounts[Field.CURRENCY_A]}
currency={currencies[Field.CURRENCY_A]}
id="add-liquidity-input-tokena"
showCommonBases
/>
<div style={{ width: '12px' }} />
<CurrencyDropdown
value={formattedAmounts[Field.CURRENCY_B]}
hideInput={true}
onUserInput={onFieldBInput}
onCurrencySelect={handleCurrencyBSelect}
onMax={() => {
onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '')
}}
showMaxButton={!atMaxAmounts[Field.CURRENCY_B]}
currency={currencies[Field.CURRENCY_B]}
id="add-liquidity-input-tokenb"
showCommonBases
/>
</RowBetween>
<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 ? (
<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>
{noLiquidity && (
<BlueCard
style={{
display: 'flex',
flexDirection: 'column',
flexDirection: 'row',
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
fontSize={14}
style={{ marginBottom: 8, fontWeight: 500, opacity: 0.8 }}
style={{ marginBottom: 8, fontWeight: 500 }}
textAlign="center"
color={theme.primaryText1}
>
You are the first liquidity provider for this Uniswap V3 pool.
</TYPE.body>
<TYPE.body fontWeight={500} textAlign="center" fontSize={14} style={{ opacity: 0.8 }}>
The transaction cost will be much higher as it includes the gas to create the pool.
<Trans>
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.
</Trans>
</TYPE.body>
</BlueCard>
</AutoColumn>
</DynamicSection>
)}
)}
</AutoColumn>{' '}
</>
)}
<DynamicSection
gap="md"
disabled={!feeAmount || invalidPool || (noLiquidity && !startPriceTypedValue)}
>
<RowBetween>
<TYPE.label>
<Trans>Set Price Range</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>
<TYPE.main fontSize={14} fontWeight={400} style={{ marginBottom: '.5rem', lineHeight: '125%' }}>
<Trans>
Your liquidity will only earn fees when the market price of the pair is within your range.{' '}
<ExternalLink
href={
'https://docs.uniswap.org/protocol/concepts/introduction/liquidity-user-guide#4-set-price-range'
}
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}
{hasExistingPosition && existingPosition && (
<PositionPreview
position={existingPosition}
title={<Trans>Selected Range</Trans>}
inRange={!outOfRange}
ticksAtLimit={ticksAtLimit}
/>
)}
</AutoColumn>
<div>
<DynamicSection
disabled={tickLower === undefined || tickUpper === undefined || invalidPool || invalidRange}
>
<AutoColumn gap="md">
<TYPE.label>
{hasExistingPosition ? <Trans>Add more liquidity</Trans> : <Trans>Deposit Amounts</Trans>}
</TYPE.label>
<CurrencyInputPanel
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onFieldAInput}
onMax={() => {
onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '')
}}
showMaxButton={!atMaxAmounts[Field.CURRENCY_A]}
currency={currencies[Field.CURRENCY_A]}
id="add-liquidity-input-tokena"
fiatValue={usdcValues[Field.CURRENCY_A]}
showCommonBases
locked={depositADisabled}
/>
{price && baseCurrency && quoteCurrency && !noLiquidity && (
<LightCard style={{ padding: '12px' }}>
<AutoColumn gap="4px">
<TYPE.main fontWeight={500} textAlign="center" fontSize={12}>
<Trans>Current Price</Trans>
</TYPE.main>
<TYPE.body fontWeight={500} textAlign="center" fontSize={20}>
<HoverInlineText
maxCharacters={20}
text={invertPrice ? price.invert().toSignificant(6) : price.toSignificant(6)}
/>{' '}
</TYPE.body>
<TYPE.main fontWeight={500} textAlign="center" fontSize={12}>
<Trans>
<CurrencyInputPanel
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onFieldBInput}
onMax={() => {
onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '')
}}
showMaxButton={!atMaxAmounts[Field.CURRENCY_B]}
fiatValue={usdcValues[Field.CURRENCY_B]}
currency={currencies[Field.CURRENCY_B]}
id="add-liquidity-input-tokenb"
showCommonBases
locked={depositBDisabled}
/>
</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}
</Trans>
</TYPE.main>
</AutoColumn>
</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>
</>
)}
</TYPE.body>
</Trans>
</AutoRow>
)}
<DynamicSection
disabled={tickLower === undefined || tickUpper === undefined || invalidPool || invalidRange}
>
<AutoColumn gap="md">
<TYPE.label>{hasExistingPosition ? 'Add more liquidity' : t`Deposit Amounts`}</TYPE.label>
<CurrencyInputPanel
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onFieldAInput}
onMax={() => {
onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '')
}}
showMaxButton={!atMaxAmounts[Field.CURRENCY_A]}
currency={currencies[Field.CURRENCY_A]}
id="add-liquidity-input-tokena"
fiatValue={usdcValues[Field.CURRENCY_A]}
showCommonBases
locked={depositADisabled}
/>
<LiquidityChartRangeInput
currencyA={baseCurrency ?? undefined}
currencyB={quoteCurrency ?? undefined}
feeAmount={feeAmount}
ticksAtLimit={ticksAtLimit}
price={price ? parseFloat((invertPrice ? price.invert() : price).toSignificant(8)) : undefined}
priceLower={priceLower}
priceUpper={priceUpper}
onLeftRangeInput={onLeftRangeInput}
onRightRangeInput={onRightRangeInput}
interactive={!hasExistingPosition}
/>
<CurrencyInputPanel
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onFieldBInput}
onMax={() => {
onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '')
}}
showMaxButton={!atMaxAmounts[Field.CURRENCY_B]}
fiatValue={usdcValues[Field.CURRENCY_B]}
currency={currencies[Field.CURRENCY_B]}
id="add-liquidity-input-tokenb"
showCommonBases
locked={depositBDisabled}
/>
</AutoColumn>
</DynamicSection>
<div>
{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>
{noLiquidity && (
<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>
) : (
<Trans>Approve {currencies[Field.CURRENCY_B]?.symbol}</Trans>
'-'
)}
</ButtonPrimary>
)}
</RowBetween>
</TYPE.main>
</RowBetween>
</AutoColumn>
)}
{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]}
</DynamicSection>
<DynamicSection
gap="md"
disabled={!feeAmount || invalidPool || (noLiquidity && !startPriceTypedValue)}
>
<Text fontWeight={500}>{errorMessage ? errorMessage : <Trans>Add</Trans>}</Text>
</ButtonError>
</AutoColumn>
)}
</div>
</AutoColumn>
<StackedContainer>
<StackedItem style={{ opacity: showCapitalEfficiencyWarning ? '0.05' : 1 }}>
<AutoColumn gap="md">
{!noLiquidity && (
<PresetsButtons
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>
</AppBody>
</PageWrapper>
{addIsUnsupported && (
<UnsupportedCurrencyFooter
show={addIsUnsupported}
......
......@@ -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