diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 6dbd13b1e..31bf828ba 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -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}; diff --git a/src/components/FeeSelector/index.tsx b/src/components/FeeSelector/index.tsx index b3089e4ee..685dd3aff 100644 --- a/src/components/FeeSelector/index.tsx +++ b/src/components/FeeSelector/index.tsx @@ -173,7 +173,7 @@ export default function FeeSelector({ onClick={() => handleFeePoolSelectWithEvent(FeeAmount.LOW)} > - + 0.05% fee diff --git a/src/components/InputStepCounter/InputStepCounter.tsx b/src/components/InputStepCounter/InputStepCounter.tsx index dbad34eb9..f9bd2c2b4 100644 --- a/src/components/InputStepCounter/InputStepCounter.tsx +++ b/src/components/InputStepCounter/InputStepCounter.tsx @@ -1,14 +1,13 @@ 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(false) - // format fee amount - const feeAmountFormatted = feeAmount ? formattedFeeAmount(feeAmount * 2) : '' - const handleOnFocus = () => { setUseLocalValue(true) setActive(true) @@ -126,39 +133,45 @@ const StepCounter = ({ return ( - + {title} - { - setLocalValue(val) - }} - /> + + + {!locked && ( + + + + + + )} + + { + setLocalValue(val) + }} + /> + + {!locked && ( + + + + + + )} + + {tokenB} per {tokenA} - {!locked ? ( - - - - -{feeAmountFormatted}% - - - - - +{feeAmountFormatted}% - - - - ) : null} ) } diff --git a/src/components/NavigationTabs/index.tsx b/src/components/NavigationTabs/index.tsx index 874cbcf52..20a6db5d3 100644 --- a/src/components/NavigationTabs/index.tsx +++ b/src/components/NavigationTabs/index.tsx @@ -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 ( - { if (adding) { @@ -119,10 +133,15 @@ export function AddRemoveTabs({ dispatch(resetMintV3State()) } }} + flex={children ? '1' : undefined} > - - + + {creating ? ( Create a pair ) : adding ? ( @@ -131,6 +150,7 @@ export function AddRemoveTabs({ Remove Liquidity )} + {children} diff --git a/src/components/PositionListItem/index.tsx b/src/components/PositionListItem/index.tsx index 370acd35b..44977d66a 100644 --- a/src/components/PositionListItem/index.tsx +++ b/src/components/PositionListItem/index.tsx @@ -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 Min: - {formatPrice(priceLower, 5)} per{' '} - + {formatTickPrice(priceLower, tickAtLimit, Bound.LOWER)} {' '} + per {' '} @@ -254,8 +258,8 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr Max: - {formatPrice(priceUpper, 5)} per{' '} - + {formatTickPrice(priceUpper, tickAtLimit, Bound.UPPER)} {' '} + per diff --git a/src/components/PositionPreview/index.tsx b/src/components/PositionPreview/index.tsx index 2d8ca519e..5bde526fa 100644 --- a/src/components/PositionPreview/index.tsx +++ b/src/components/PositionPreview/index.tsx @@ -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 = ({ Min Price - {`${priceLower.toSignificant(5)}`} + {`${formatTickPrice( + priceLower, + ticksAtLimit, + Bound.LOWER + )}`} {quoteCurrency.symbol} per {baseCurrency.symbol} @@ -138,7 +146,11 @@ export const PositionPreview = ({ Max Price - {`${priceUpper.toSignificant(5)}`} + {`${formatTickPrice( + priceUpper, + ticksAtLimit, + Bound.UPPER + )}`} {quoteCurrency.symbol} per {baseCurrency.symbol} diff --git a/src/components/RangeSelector/PresetsButtons.tsx b/src/components/RangeSelector/PresetsButtons.tsx new file mode 100644 index 000000000..127f60e16 --- /dev/null +++ b/src/components/RangeSelector/PresetsButtons.tsx @@ -0,0 +1,84 @@ +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 +}) => ( + +) + +export default function PresetsButtons({ feeAmount, setRange, setFullRange }: PresetsButtonProps) { + feeAmount = feeAmount ?? FeeAmount.LOW + + return ( + + + + + + + ) +} diff --git a/src/components/RangeSelector/index.tsx b/src/components/RangeSelector/index.tsx index 4eeea1c06..a511b7c95 100644 --- a/src/components/RangeSelector/index.tsx +++ b/src/components/RangeSelector/index.tsx @@ -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 priceUpper?: Price @@ -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 ( - - Min Price} - tokenA={currencyA?.symbol} - tokenB={currencyB?.symbol} - /> - Max Price} - /> - + + + Min Price} + tokenA={currencyA?.symbol} + tokenB={currencyB?.symbol} + /> + Max Price} + /> + + ) } diff --git a/src/components/RateToggle/index.tsx b/src/components/RateToggle/index.tsx index 03690439e..13c7b93c9 100644 --- a/src/components/RateToggle/index.tsx +++ b/src/components/RateToggle/index.tsx @@ -22,10 +22,10 @@ export default function RateToggle({
- {isSorted ? currencyA.symbol : currencyB.symbol} price + {isSorted ? currencyA.symbol : currencyB.symbol} - {isSorted ? currencyB.symbol : currencyA.symbol} price + {isSorted ? currencyB.symbol : currencyA.symbol}
diff --git a/src/hooks/useIsTickAtLimit.ts b/src/hooks/useIsTickAtLimit.ts new file mode 100644 index 000000000..be191a8cd --- /dev/null +++ b/src/hooks/useIsTickAtLimit.ts @@ -0,0 +1,23 @@ +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] + ) +} diff --git a/src/pages/AddLiquidity/Review.tsx b/src/pages/AddLiquidity/Review.tsx index e812edaa5..e4f810110 100644 --- a/src/pages/AddLiquidity/Review.tsx +++ b/src/pages/AddLiquidity/Review.tsx @@ -1,4 +1,4 @@ -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 priceUpper?: Price outOfRange: boolean + ticksAtLimit: { [bound in Bound]?: boolean | undefined } }) { return ( - {position ? : null} + {position ? ( + + ) : null} ) diff --git a/src/pages/AddLiquidity/index.tsx b/src/pages/AddLiquidity/index.tsx index 79765966e..df093eb28 100644 --- a/src/pages/AddLiquidity/index.tsx +++ b/src/pages/AddLiquidity/index.tsx @@ -1,4 +1,4 @@ -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(false) const [attemptingTxn, setAttemptingTxn] = useState(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 ? ( + + + Unsupported Asset + + + ) : !account ? ( + + Connect wallet + + ) : ( + + {(approvalA === ApprovalState.NOT_APPROVED || + approvalA === ApprovalState.PENDING || + approvalB === ApprovalState.NOT_APPROVED || + approvalB === ApprovalState.PENDING) && + isValid && ( + + {showApprovalA && ( + + {approvalA === ApprovalState.PENDING ? ( + + Approving {currencies[Field.CURRENCY_A]?.symbol} + + ) : ( + Approve {currencies[Field.CURRENCY_A]?.symbol} + )} + + )} + {showApprovalB && ( + + {approvalB === ApprovalState.PENDING ? ( + + Approving {currencies[Field.CURRENCY_B]?.symbol} + + ) : ( + Approve {currencies[Field.CURRENCY_B]?.symbol} + )} + + )} + + )} + {mustCreateSeparately && ( + + + Create + + + )} + { + 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]} + > + {errorMessage ? errorMessage : Preview} + + + ) // 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} /> - + + showBackLink={!hasExistingPosition} + > + {!hasExistingPosition && ( + + + + + Clear All + + + + {baseCurrency && quoteCurrency ? ( + { + onLeftRangeInput('') + onRightRangeInput('') + history.push( + `/add/${currencyIdB as string}/${currencyIdA as string}${feeAmount ? '/' + feeAmount : ''}` + ) + }} + /> + ) : null} + + )} + - - {!hasExistingPosition && ( - <> - - - - Select pair - - - - Clear All - - - - - { - onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '') - }} - onCurrencySelect={handleCurrencyASelect} - showMaxButton={!atMaxAmounts[Field.CURRENCY_A]} - currency={currencies[Field.CURRENCY_A]} - id="add-liquidity-input-tokena" - showCommonBases - /> -
- - { - onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '') - }} - showMaxButton={!atMaxAmounts[Field.CURRENCY_B]} - currency={currencies[Field.CURRENCY_B]} - id="add-liquidity-input-tokenb" - showCommonBases + + + {!hasExistingPosition && ( + <> + + + + Select Pair + + + + { + onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '') + }} + onCurrencySelect={handleCurrencyASelect} + showMaxButton={!atMaxAmounts[Field.CURRENCY_A]} + currency={currencies[Field.CURRENCY_A]} + id="add-liquidity-input-tokena" + showCommonBases + /> +
+ + { + onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '') + }} + showMaxButton={!atMaxAmounts[Field.CURRENCY_B]} + currency={currencies[Field.CURRENCY_B]} + id="add-liquidity-input-tokenb" + showCommonBases + /> + + + - - - - {' '} - - )} - {hasExistingPosition && existingPosition ? ( - Selected Range} - inRange={!outOfRange} - /> - ) : ( - <> - {noLiquidity && ( - - - - - Set Starting Price - - {baseCurrency && quoteCurrency ? ( - { - onLeftRangeInput('') - onRightRangeInput('') - history.push( - `/add/${currencyIdB as string}/${currencyIdA as string}${ - feeAmount ? '/' + feeAmount : '' - }` - ) - }} - /> - ) : null} - - - - - - - - Current {baseCurrency?.symbol} Price: - - - {price ? ( - - - {' '} - {quoteCurrency?.symbol} - - - ) : ( - '-' - )} - - + {noLiquidity && ( - +
+ +
- 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. + + 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. +
-
-
- )} + )} + {' '} + + )} - - - - Set Price Range - - - {baseCurrency && quoteCurrency ? ( - { - onLeftRangeInput('') - onRightRangeInput('') - history.push( - `/add/${currencyIdB as string}/${currencyIdA as string}${ - feeAmount ? '/' + feeAmount : '' - }` - ) - }} - /> - ) : null} - - - - Your liquidity will only earn fees when the market price of the pair is within your range.{' '} - - Need help picking a range? - - - - - Selected Range} + inRange={!outOfRange} + ticksAtLimit={ticksAtLimit} + /> + )} + + +
+ + + + {hasExistingPosition ? Add more liquidity : Deposit Amounts} + + + { + 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 && ( - - - - Current Price - - - {' '} - - - + { + 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} + /> + + +
+ + {!hasExistingPosition ? ( + <> + + + + + + + + Set your Price Range + + + + {price && baseCurrency && quoteCurrency && !noLiquidity && ( + + + + Current Price: + + + + + {quoteCurrency?.symbol} per {baseCurrency.symbol} - - - - - )} - - {outOfRange ? ( - - - - - - Your position will not earn fees or be used in trades until the market price moves into - your range. - - - - - ) : null} - - {invalidRange ? ( - - - - - Invalid range selected. The min price must be lower than the max price. - - - - ) : null} - - - )} + + + + )} - - - {hasExistingPosition ? 'Add more liquidity' : t`Deposit Amounts`} - - { - 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} - /> + - { - 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} - /> - - -
- {addIsUnsupported ? ( - - - Unsupported Asset - - - ) : !account ? ( - - Connect wallet - - ) : ( - - {(approvalA === ApprovalState.NOT_APPROVED || - approvalA === ApprovalState.PENDING || - approvalB === ApprovalState.NOT_APPROVED || - approvalB === ApprovalState.PENDING) && - isValid && ( - - {showApprovalA && ( - - {approvalA === ApprovalState.PENDING ? ( - - Approving {currencies[Field.CURRENCY_A]?.symbol} - - ) : ( - Approve {currencies[Field.CURRENCY_A]?.symbol} - )} - - )} - {showApprovalB && ( - - {approvalB === ApprovalState.PENDING ? ( - - Approving {currencies[Field.CURRENCY_B]?.symbol} - + {noLiquidity && ( + + + + Set Starting Price + + {baseCurrency && quoteCurrency ? ( + { + onLeftRangeInput('') + onRightRangeInput('') + history.push( + `/add/${currencyIdB as string}/${currencyIdA as string}${ + feeAmount ? '/' + feeAmount : '' + }` + ) + }} + /> + ) : null} + + + + + + + + Current {baseCurrency?.symbol} Price: + + + {price ? ( + + + {' '} + {quoteCurrency?.symbol} + + ) : ( - Approve {currencies[Field.CURRENCY_B]?.symbol} + '-' )} - - )} - + + + )} - {mustCreateSeparately && ( - - - Create - - - )} - { - 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]} + + + - {errorMessage ? errorMessage : Add} - - - )} -
- + + + + {!noLiquidity && ( + { + const [range1, range2] = getSetRange(numTicks) + onLeftRangeInput(invertPrice ? range2 : range1) + onRightRangeInput(invertPrice ? range1 : range2) + }} + setFullRange={() => { + setShowCapitalEfficiencyWarning(true) + }} + /> + )} + + + + + {showCapitalEfficiencyWarning && ( + + + + + + + Efficiency Comparison + + + + + + On Uniswap V3, setting a range across all prices like V2 is less capital efficient + than a concentrated one. Learn more{' '} + + here + + . + + + + + { + setShowCapitalEfficiencyWarning(false) + + getSetFullRange() + }} + > + + I Understand + + + + + + + )} + + + {outOfRange ? ( + + + + + + Your position will not earn fees or be used in trades until the market price moves into + your range. + + + + + ) : null} + + {invalidRange ? ( + + + + + Invalid range selected. The min price must be lower than the max price. + + + + ) : null} +
+ + + + + + + ) : ( + + )} + - + {addIsUnsupported && ( ` + 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; + `}; +` diff --git a/src/pages/MigrateV2/MigrateV2Pair.tsx b/src/pages/MigrateV2/MigrateV2Pair.tsx index 1d02a577a..b24ebdfbb 100644 --- a/src/pages/MigrateV2/MigrateV2Pair.tsx +++ b/src/pages/MigrateV2/MigrateV2Pair.tsx @@ -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 ? ( diff --git a/src/pages/Pool/PositionPage.tsx b/src/pages/Pool/PositionPage.tsx index ccc330e85..be9287c42 100644 --- a/src/pages/Pool/PositionPage.tsx +++ b/src/pages/Pool/PositionPage.tsx @@ -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, + priceUpper?: Price, + quote?: Token, + base?: Token, + invert?: boolean +): { + priceLower?: Price + priceUpper?: Price + 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 | 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 | 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 | 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 | 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({ Min price - {priceLower?.toSignificant(5)} + + {formatTickPrice(priceLower, tickAtLimit, Bound.LOWER)} + {' '} @@ -801,7 +834,9 @@ export function PositionPage({ Max price - {priceUpper?.toSignificant(5)} + + {formatTickPrice(priceUpper, tickAtLimit, Bound.UPPER)} + {' '} diff --git a/src/state/mint/v3/actions.ts b/src/state/mint/v3/actions.ts index ecdcfa217..65261d806 100644 --- a/src/state/mint/v3/actions.ts +++ b/src/state/mint/v3/actions.ts @@ -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('mintV3/resetMintState') +export const setFullRange = createAction('mintV3/setFullRange') diff --git a/src/state/mint/v3/hooks.ts b/src/state/mint/v3/hooks.ts index 0b9318c25..61e952967 100644 --- a/src/state/mint/v3/hooks.ts +++ b/src/state/mint/v3/hooks.ts @@ -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 } } diff --git a/src/state/mint/v3/reducer.ts b/src/state/mint/v3/reducer.ts index 1bbb49615..c040a994b 100644 --- a/src/state/mint/v3/reducer.ts +++ b/src/state/mint/v3/reducer.ts @@ -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(initialState, (builder) => builder .addCase(resetMintState, () => initialState) + .addCase(setFullRange, (state) => { + return { + ...state, + leftRangeTypedValue: true, + rightRangeTypedValue: true, + } + }) .addCase(typeStartPriceInput, (state, { payload: { typedValue } }) => { return { ...state, diff --git a/src/utils/formatTickPrice.ts b/src/utils/formatTickPrice.ts new file mode 100644 index 000000000..23f17c37f --- /dev/null +++ b/src/utils/formatTickPrice.ts @@ -0,0 +1,20 @@ +import { Bound } from '../state/mint/v3/actions' +import { Price, Token } from '@uniswap/sdk-core' +import { formatPrice } from './formatCurrencyAmount' + +export function formatTickPrice( + price: Price | 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) +}