fix: full range lp shortcut (#6136)

* fix: fix full range LP

* make a dedicated hook for syncing query parameters

* sync full range button to url

* add comment explaining lint rule disable
This commit is contained in:
Jordan Frankfurt 2023-03-13 12:36:45 -05:00 committed by GitHub
parent b5f665bc6e
commit 10eda002f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 63 deletions

@ -1,8 +1,6 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { sendEvent } from 'components/analytics'
import { ButtonOutlined } from 'components/Button' import { ButtonOutlined } from 'components/Button'
import { AutoRow } from 'components/Row' import { AutoRow } from 'components/Row'
import React from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
@ -14,18 +12,14 @@ const Button = styled(ButtonOutlined).attrs(() => ({
flex: 1; flex: 1;
` `
export default function PresetsButtons({ setFullRange }: { setFullRange: () => void }) { interface PresetsButtonsProps {
onSetFullRange: () => void
}
export default function PresetsButtons({ onSetFullRange }: PresetsButtonsProps) {
return ( return (
<AutoRow gap="4px" width="auto"> <AutoRow gap="4px" width="auto">
<Button <Button onClick={onSetFullRange}>
onClick={() => {
setFullRange()
sendEvent({
category: 'Liquidity',
action: 'Full Range Clicked',
})
}}
>
<ThemedText.DeprecatedBody fontSize={12}> <ThemedText.DeprecatedBody fontSize={12}>
<Trans>Full Range</Trans> <Trans>Full Range</Trans>
</ThemedText.DeprecatedBody> </ThemedText.DeprecatedBody>

@ -8,10 +8,10 @@ import { FeeAmount, NonfungiblePositionManager } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { sendEvent } from 'components/analytics' import { sendEvent } from 'components/analytics'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter' import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import useParsedQueryString from 'hooks/useParsedQueryString' import usePrevious from 'hooks/usePrevious'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { AlertTriangle } from 'react-feather' import { AlertTriangle } from 'react-feather'
import { useNavigate, useParams } from 'react-router-dom' import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
import { import {
useRangeHopCallbacks, useRangeHopCallbacks,
@ -92,7 +92,6 @@ export default function AddLiquidity() {
const expertMode = useIsExpertMode() const expertMode = useIsExpertMode()
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
const positionManager = useV3NFTPositionManagerContract() const positionManager = useV3NFTPositionManagerContract()
const parsedQs = useParsedQueryString()
// check for existing position if tokenId in url // check for existing position if tokenId in url
const { position: existingPositionDetails, loading: positionLoading } = useV3PositionFromTokenId( const { position: existingPositionDetails, loading: positionLoading } = useV3PositionFromTokenId(
@ -114,8 +113,7 @@ export default function AddLiquidity() {
baseCurrency && currencyB && baseCurrency.wrapped.equals(currencyB.wrapped) ? undefined : currencyB baseCurrency && currencyB && baseCurrency.wrapped.equals(currencyB.wrapped) ? undefined : currencyB
// mint state // mint state
const { independentField, typedValue, startPriceTypedValue, rightRangeTypedValue, leftRangeTypedValue } = const { independentField, typedValue, startPriceTypedValue } = useV3MintState()
useV3MintState()
const { const {
pool, pool,
@ -123,6 +121,7 @@ export default function AddLiquidity() {
dependentField, dependentField,
price, price,
pricesAtTicks, pricesAtTicks,
pricesAtLimit,
parsedAmounts, parsedAmounts,
currencyBalances, currencyBalances,
position, position,
@ -153,26 +152,6 @@ export default function AddLiquidity() {
const [showConfirm, setShowConfirm] = useState<boolean>(false) const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
useEffect(() => {
if (
parsedQs.minPrice &&
typeof parsedQs.minPrice === 'string' &&
parsedQs.minPrice !== leftRangeTypedValue &&
!isNaN(parsedQs.minPrice as any)
) {
onLeftRangeInput(parsedQs.minPrice)
}
if (
parsedQs.maxPrice &&
typeof parsedQs.maxPrice === 'string' &&
parsedQs.maxPrice !== rightRangeTypedValue &&
!isNaN(parsedQs.maxPrice as any)
) {
onRightRangeInput(parsedQs.maxPrice)
}
}, [parsedQs, rightRangeTypedValue, leftRangeTypedValue, onRightRangeInput, onLeftRangeInput])
// txn values // txn values
const deadline = useTransactionDeadline() // custom from users settings const deadline = useTransactionDeadline() // custom from users settings
@ -427,6 +406,58 @@ export default function AddLiquidity() {
!depositBDisabled ? currencies[Field.CURRENCY_B]?.symbol : '' !depositBDisabled ? currencies[Field.CURRENCY_B]?.symbol : ''
}` }`
const [searchParams, setSearchParams] = useSearchParams()
const handleSetFullRange = useCallback(() => {
getSetFullRange()
const minPrice = pricesAtLimit[Bound.LOWER]
if (minPrice) searchParams.set('minPrice', minPrice.toSignificant(5))
const maxPrice = pricesAtLimit[Bound.UPPER]
if (maxPrice) searchParams.set('maxPrice', maxPrice.toSignificant(5))
setSearchParams(searchParams)
sendEvent({
category: 'Liquidity',
action: 'Full Range Clicked',
})
}, [getSetFullRange, pricesAtLimit, searchParams, setSearchParams])
// START: sync values with query string
const oldSearchParams = usePrevious(searchParams)
// use query string as an input to onInput handlers
useEffect(() => {
const minPrice = searchParams.get('minPrice')
const oldMinPrice = oldSearchParams?.get('minPrice')
if (
minPrice &&
typeof minPrice === 'string' &&
!isNaN(minPrice as any) &&
(!oldMinPrice || oldMinPrice !== minPrice)
) {
onLeftRangeInput(minPrice)
}
// disable eslint rule because this hook only cares about the url->input state data flow
// input state -> url updates are handled in the input handlers
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams])
useEffect(() => {
const maxPrice = searchParams.get('maxPrice')
const oldMaxPrice = oldSearchParams?.get('maxPrice')
if (
maxPrice &&
typeof maxPrice === 'string' &&
!isNaN(maxPrice as any) &&
(!oldMaxPrice || oldMaxPrice !== maxPrice)
) {
onRightRangeInput(maxPrice)
}
// disable eslint rule because this hook only cares about the url->input state data flow
// input state -> url updates are handled in the input handlers
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams])
// END: sync values with query string
const Buttons = () => const Buttons = () =>
addIsUnsupported ? ( addIsUnsupported ? (
<ButtonPrimary disabled={true} $borderRadius="12px" padding="12px"> <ButtonPrimary disabled={true} $borderRadius="12px" padding="12px">
@ -825,13 +856,7 @@ export default function AddLiquidity() {
feeAmount={feeAmount} feeAmount={feeAmount}
ticksAtLimit={ticksAtLimit} ticksAtLimit={ticksAtLimit}
/> />
{!noLiquidity && ( {!noLiquidity && <PresetsButtons onSetFullRange={handleSetFullRange} />}
<PresetsButtons
setFullRange={() => {
getSetFullRange()
}}
/>
)}
</AutoColumn> </AutoColumn>
</StackedItem> </StackedItem>
</StackedContainer> </StackedContainer>

@ -16,10 +16,9 @@ import { usePool } from 'hooks/usePools'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { ReactNode, useCallback, useMemo } from 'react' import { ReactNode, useCallback, useMemo } from 'react'
import { useLocation, useNavigate } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { useAppDispatch, useAppSelector } from 'state/hooks' import { useAppDispatch, useAppSelector } from 'state/hooks'
import { getTickToPrice } from 'utils/getTickToPrice' import { getTickToPrice } from 'utils/getTickToPrice'
import { replaceURLParam } from 'utils/routes'
import { BIG_INT_ZERO } from '../../../constants/misc' import { BIG_INT_ZERO } from '../../../constants/misc'
import { PoolState } from '../../../hooks/usePools' import { PoolState } from '../../../hooks/usePools'
@ -48,7 +47,6 @@ export function useV3MintActionHandlers(noLiquidity: boolean | undefined): {
onStartPriceInput: (typedValue: string) => void onStartPriceInput: (typedValue: string) => void
} { } {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const navigate = useNavigate()
const onFieldAInput = useCallback( const onFieldAInput = useCallback(
(typedValue: string) => { (typedValue: string) => {
@ -64,22 +62,30 @@ export function useV3MintActionHandlers(noLiquidity: boolean | undefined): {
[dispatch, noLiquidity] [dispatch, noLiquidity]
) )
const { search } = useLocation() const [searchParams, setSearchParams] = useSearchParams()
const onLeftRangeInput = useCallback( const onLeftRangeInput = useCallback(
(typedValue: string) => { (typedValue: string) => {
dispatch(typeLeftRangeInput({ typedValue })) dispatch(typeLeftRangeInput({ typedValue }))
navigate({ search: replaceURLParam(search, 'minPrice', typedValue) }, { replace: true }) const paramMinPrice = searchParams.get('minPrice')
if (!paramMinPrice || (paramMinPrice && paramMinPrice !== typedValue)) {
searchParams.set('minPrice', typedValue)
setSearchParams(searchParams)
}
}, },
[dispatch, navigate, search] [dispatch, searchParams, setSearchParams]
) )
const onRightRangeInput = useCallback( const onRightRangeInput = useCallback(
(typedValue: string) => { (typedValue: string) => {
dispatch(typeRightRangeInput({ typedValue })) dispatch(typeRightRangeInput({ typedValue }))
navigate({ search: replaceURLParam(search, 'maxPrice', typedValue) }, { replace: true }) const paramMaxPrice = searchParams.get('maxPrice')
if (!paramMaxPrice || (paramMaxPrice && paramMaxPrice !== typedValue)) {
searchParams.set('maxPrice', typedValue)
setSearchParams(searchParams)
}
}, },
[dispatch, navigate, search] [dispatch, searchParams, setSearchParams]
) )
const onStartPriceInput = useCallback( const onStartPriceInput = useCallback(
@ -113,6 +119,9 @@ export function useV3DerivedMintInfo(
pricesAtTicks: { pricesAtTicks: {
[bound in Bound]?: Price<Token, Token> | undefined [bound in Bound]?: Price<Token, Token> | undefined
} }
pricesAtLimit: {
[bound in Bound]?: Price<Token, Token> | undefined
}
currencies: { [field in Field]?: Currency } currencies: { [field in Field]?: Currency }
currencyBalances: { [field in Field]?: CurrencyAmount<Currency> } currencyBalances: { [field in Field]?: CurrencyAmount<Currency> }
dependentField: Field dependentField: Field
@ -226,9 +235,7 @@ export function useV3DerivedMintInfo(
const poolForPosition: Pool | undefined = pool ?? mockPool const poolForPosition: Pool | undefined = pool ?? mockPool
// lower and upper limits in the tick space for `feeAmoun<Trans> // lower and upper limits in the tick space for `feeAmoun<Trans>
const tickSpaceLimits: { const tickSpaceLimits = useMemo(
[bound in Bound]: number | undefined
} = useMemo(
() => ({ () => ({
[Bound.LOWER]: feeAmount ? nearestUsableTick(TickMath.MIN_TICK, TICK_SPACINGS[feeAmount]) : undefined, [Bound.LOWER]: feeAmount ? nearestUsableTick(TickMath.MIN_TICK, TICK_SPACINGS[feeAmount]) : undefined,
[Bound.UPPER]: feeAmount ? nearestUsableTick(TickMath.MAX_TICK, TICK_SPACINGS[feeAmount]) : undefined, [Bound.UPPER]: feeAmount ? nearestUsableTick(TickMath.MAX_TICK, TICK_SPACINGS[feeAmount]) : undefined,
@ -238,9 +245,7 @@ export function useV3DerivedMintInfo(
// parse typed range values and determine closest ticks // parse typed range values and determine closest ticks
// lower should always be a smaller tick // lower should always be a smaller tick
const ticks: { const ticks = useMemo(() => {
[key: string]: number | undefined
} = useMemo(() => {
return { return {
[Bound.LOWER]: [Bound.LOWER]:
typeof existingPosition?.tickLower === 'number' typeof existingPosition?.tickLower === 'number'
@ -286,6 +291,13 @@ export function useV3DerivedMintInfo(
// mark invalid range // mark invalid range
const invalidRange = Boolean(typeof tickLower === 'number' && typeof tickUpper === 'number' && tickLower >= tickUpper) const invalidRange = Boolean(typeof tickLower === 'number' && typeof tickUpper === 'number' && tickLower >= tickUpper)
const pricesAtLimit = useMemo(() => {
return {
[Bound.LOWER]: getTickToPrice(token0, token1, tickSpaceLimits.LOWER),
[Bound.UPPER]: getTickToPrice(token0, token1, tickSpaceLimits.UPPER),
}
}, [token0, token1, tickSpaceLimits.LOWER, tickSpaceLimits.UPPER])
// always returns the price with 0 as base token // always returns the price with 0 as base token
const pricesAtTicks = useMemo(() => { const pricesAtTicks = useMemo(() => {
return { return {
@ -472,6 +484,7 @@ export function useV3DerivedMintInfo(
ticks, ticks,
price, price,
pricesAtTicks, pricesAtTicks,
pricesAtLimit,
position, position,
noLiquidity, noLiquidity,
errorMessage, errorMessage,

@ -1,5 +0,0 @@
export const replaceURLParam = (search: string, param: string, newValue: string) => {
const searchParams = new URLSearchParams(search)
searchParams.set(param, newValue)
return searchParams.toString()
}