separate v2 + v3 mint reducers

reset v3 mint state on migrate mount/unmount
This commit is contained in:
Noah Zinsmeister 2021-05-05 11:18:54 -04:00
parent 40f0e619cc
commit cd3c48462d
No known key found for this signature in database
GPG Key ID: 83022DD49188C9F2
15 changed files with 684 additions and 575 deletions

@ -10,6 +10,7 @@ import Settings from '../Settings'
import { useDispatch } from 'react-redux'
import { AppDispatch } from 'state'
import { resetMintState } from 'state/mint/actions'
import { resetMintState as resetMintV3State } from 'state/mint/v3/actions'
import { TYPE } from 'theme'
import useTheme from 'hooks/useTheme'
@ -105,7 +106,11 @@ export function AddRemoveTabs({
<HistoryLink
to={'/pool' + (!!positionID ? `/${positionID.toString()}` : '')}
onClick={() => {
adding && dispatch(resetMintState())
if (adding) {
// not 100% sure both of these are needed
dispatch(resetMintState())
dispatch(resetMintV3State())
}
}}
>
<StyledArrowLeft stroke={theme.text2} />

@ -5,7 +5,7 @@ import { ThemeContext } from 'styled-components'
import { AutoColumn } from '../../components/Column'
import { AutoRow } from '../../components/Row'
import { ONE_BIPS } from '../../constants'
import { Field } from '../../state/mint/actions'
import { Field } from '../../state/mint/v3/actions'
import { TYPE } from '../../theme'
export function PoolPriceBar({

@ -1,5 +1,5 @@
import React from 'react'
import { Field } from '../../state/mint/actions'
import { Field } from '../../state/mint/v3/actions'
import { AutoColumn } from 'components/Column'
import Card from 'components/Card'
import styled from 'styled-components'

@ -21,7 +21,7 @@ import { useCurrency } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
import useTransactionDeadline from '../../hooks/useTransactionDeadline'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field, Bound } from '../../state/mint/actions'
import { Field, Bound } from '../../state/mint/v3/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { useIsExpertMode, useUserSlippageTolerance } from '../../state/user/hooks'
@ -33,7 +33,12 @@ import { currencyId } from '../../utils/currencyId'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import { DynamicSection, CurrencyDropdown, StyledInput, Wrapper, ScrollablePage } from './styled'
import { useTranslation } from 'react-i18next'
import { useMintState, useMintActionHandlers, useDerivedMintInfo, useRangeHopCallbacks } from 'state/mint/hooks'
import {
useV3MintState,
useV3MintActionHandlers,
useRangeHopCallbacks,
useV3DerivedMintInfo,
} from 'state/mint/v3/hooks'
import { FeeAmount, NonfungiblePositionManager } from '@uniswap/v3-sdk'
import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES } from 'constants/v3'
import { useV3PositionFromTokenId } from 'hooks/useV3Positions'
@ -98,7 +103,7 @@ export default function AddLiquidity({
}, [currencyA, currencyB])
// mint state
const { independentField, typedValue, startPriceTypedValue } = useMintState()
const { independentField, typedValue, startPriceTypedValue } = useV3MintState()
const {
ticks,
@ -117,7 +122,7 @@ export default function AddLiquidity({
depositADisabled,
depositBDisabled,
invertPrice,
} = useDerivedMintInfo(
} = useV3DerivedMintInfo(
currencyA ?? undefined,
currencyB ?? undefined,
feeAmount,
@ -131,7 +136,7 @@ export default function AddLiquidity({
onLeftRangeInput,
onRightRangeInput,
onStartPriceInput,
} = useMintActionHandlers(noLiquidity)
} = useV3MintActionHandlers(noLiquidity)
const isValid = !errorMessage && !invalidRange

@ -27,7 +27,7 @@ import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported'
import useTransactionDeadline from '../../hooks/useTransactionDeadline'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/mint/actions'
import { useMintActionHandlers, useMintState } from '../../state/mint/hooks'
import { useDerivedMintInfo, useMintActionHandlers, useMintState } from '../../state/mint/hooks'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { useIsExpertMode, useUserSlippageTolerance } from '../../state/user/hooks'
@ -41,7 +41,6 @@ import { ConfirmAddModalBottom } from './ConfirmAddModalBottom'
import { currencyId } from '../../utils/currencyId'
import { PoolPriceBar } from './PoolPriceBar'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import { useV2DerivedMintInfo } from 'state/mint/v2'
export default function AddLiquidity({
match: {
@ -79,7 +78,7 @@ export default function AddLiquidity({
liquidityMinted,
poolTokenPercentage,
error,
} = useV2DerivedMintInfo(currencyA ?? undefined, currencyB ?? undefined)
} = useDerivedMintInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onFieldAInput, onFieldBInput } = useMintActionHandlers(noLiquidity)

@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react'
import React, { useCallback, useMemo, useState, useEffect } from 'react'
import { Fraction, Price, Token, TokenAmount, WETH9 } from '@uniswap/sdk-core'
import { FACTORY_ADDRESS, JSBI } from '@uniswap/v2-sdk'
import { Redirect, RouteComponentProps } from 'react-router'
@ -30,8 +30,8 @@ import { useUserSlippageTolerance } from 'state/user/hooks'
import ReactGA from 'react-ga'
import { TransactionResponse } from '@ethersproject/providers'
import { useIsTransactionPending, useTransactionAdder } from 'state/transactions/hooks'
import { useDerivedMintInfo, useMintActionHandlers, useRangeHopCallbacks } from 'state/mint/hooks'
import { Bound } from 'state/mint/actions'
import { useV3DerivedMintInfo, useRangeHopCallbacks, useV3MintActionHandlers } from 'state/mint/v3/hooks'
import { Bound, resetMintState } from 'state/mint/v3/actions'
import { useTranslation } from 'react-i18next'
import { AlertCircle, AlertTriangle, ArrowDown } from 'react-feather'
import FeeSelector from 'components/FeeSelector'
@ -44,6 +44,8 @@ import useTheme from 'hooks/useTheme'
import { unwrappedToken } from 'utils/wrappedCurrency'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import Badge, { BadgeVariant } from 'components/Badge'
import { useDispatch } from 'react-redux'
import { AppDispatch } from 'state'
const ZERO = JSBI.BigInt(0)
@ -156,7 +158,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 } = useDerivedMintInfo(
const { ticks, pricesAtTicks, invertPrice, invalidRange, outOfRange } = useV3DerivedMintInfo(
token0,
token1,
feeAmount,
@ -175,7 +177,7 @@ function V2PairMigration({
tickUpper
)
const { onLeftRangeInput, onRightRangeInput } = useMintActionHandlers(noLiquidity)
const { onLeftRangeInput, onRightRangeInput } = useV3MintActionHandlers(noLiquidity)
// the v3 tick is either the pool's tickCurrent, or the tick closest to the v2 spot price
const tick = pool?.tickCurrent ?? priceToClosestTick(v2SpotPrice)
@ -585,6 +587,15 @@ export default function MigrateV2Pair({
params: { address },
},
}: RouteComponentProps<{ address: string }>) {
// reset mint state on component mount, and as a cleanup (on unmount)
const dispatch = useDispatch<AppDispatch>()
useEffect(() => {
dispatch(resetMintState())
return () => {
dispatch(resetMintState())
}
}, [dispatch])
const { chainId, account } = useActiveWeb3React()
// get pair contract

@ -7,6 +7,7 @@ import user from './user/reducer'
import transactions from './transactions/reducer'
import swap from './swap/reducer'
import mint from './mint/reducer'
import mintV3 from './mint/v3/reducer'
import lists from './lists/reducer'
import burn from './burn/reducer'
import burnV3 from './burn/v3/reducer'
@ -21,6 +22,7 @@ const store = configureStore({
transactions,
swap,
mint,
mintV3,
burn,
burnV3,
multicall,

@ -5,19 +5,5 @@ export enum Field {
CURRENCY_B = 'CURRENCY_B',
}
export enum Bound {
LOWER = 'LOWER',
UPPER = 'UPPER',
}
// save for % inputs
export enum RangeType {
PERCENT = 'PERCENT',
RATE = 'RATE',
}
export const typeInput = createAction<{ field: Field; typedValue: string; noLiquidity: boolean }>('mint/typeInputMint')
export const typeStartPriceInput = createAction<{ typedValue: string }>('mint/typeStartPriceInput')
export const typeLeftRangeInput = createAction<{ typedValue: string }>('mint/typeLeftRangeInput')
export const typeRightRangeInput = createAction<{ typedValue: string }>('mint/typeRightRangeInput')
export const resetMintState = createAction<void>('mint/resetMintState')

@ -1,27 +1,19 @@
import { BIG_INT_ZERO } from './../../constants/index'
import { getTickToPrice } from 'utils/getTickToPrice'
import JSBI from 'jsbi'
import { PoolState } from '../../hooks/usePools'
import {
Pool,
FeeAmount,
Position,
priceToClosestTick,
TickMath,
tickToPrice,
TICK_SPACINGS,
} from '@uniswap/v3-sdk/dist/'
import { Currency, CurrencyAmount, ETHER, Price, Rounding } from '@uniswap/sdk-core'
import { useCallback, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { AppDispatch, AppState } from '../index'
import { Field, typeInput } from './actions'
import { Pair } from '@uniswap/v2-sdk'
import { Currency, CurrencyAmount, ETHER, Percent, Price, TokenAmount } from '@uniswap/sdk-core'
import JSBI from 'jsbi'
import { PairState, useV2Pair } from '../../hooks/useV2Pairs'
import { useTotalSupply } from '../../hooks/useTotalSupply'
import { useActiveWeb3React } from '../../hooks'
import { wrappedCurrency, wrappedCurrencyAmount } from '../../utils/wrappedCurrency'
import { AppDispatch, AppState } from '../index'
import { tryParseAmount } from '../swap/hooks'
import { useCurrencyBalances } from '../wallet/hooks'
import { Field, Bound, typeInput, typeStartPriceInput, typeLeftRangeInput, typeRightRangeInput } from './actions'
import { tryParseTick } from './utils'
import { usePool } from 'hooks/usePools'
const ZERO = JSBI.BigInt(0)
export function useMintState(): AppState['mint'] {
return useSelector<AppState, AppState['mint']>((state) => state.mint)
@ -32,9 +24,6 @@ export function useMintActionHandlers(
): {
onFieldAInput: (typedValue: string) => void
onFieldBInput: (typedValue: string) => void
onLeftRangeInput: (typedValue: string) => void
onRightRangeInput: (typedValue: string) => void
onStartPriceInput: (typedValue: string) => void
} {
const dispatch = useDispatch<AppDispatch>()
@ -52,101 +41,49 @@ export function useMintActionHandlers(
[dispatch, noLiquidity]
)
const onLeftRangeInput = useCallback(
(typedValue: string) => {
dispatch(typeLeftRangeInput({ typedValue }))
},
[dispatch]
)
const onRightRangeInput = useCallback(
(typedValue: string) => {
dispatch(typeRightRangeInput({ typedValue }))
},
[dispatch]
)
const onStartPriceInput = useCallback(
(typedValue: string) => {
dispatch(typeStartPriceInput({ typedValue }))
},
[dispatch]
)
return {
onFieldAInput,
onFieldBInput,
onLeftRangeInput,
onRightRangeInput,
onStartPriceInput,
}
}
export function useDerivedMintInfo(
currencyA?: Currency,
currencyB?: Currency,
feeAmount?: FeeAmount,
baseCurrency?: Currency,
// override for existing position
existingPosition?: Position
currencyA: Currency | undefined,
currencyB: Currency | undefined
): {
pool?: Pool | null
poolState: PoolState
ticks: { [bound in Bound]?: number | undefined }
price?: Price
pricesAtTicks: {
[bound in Bound]?: Price | undefined
}
currencies: { [field in Field]?: Currency }
currencyBalances: { [field in Field]?: CurrencyAmount }
dependentField: Field
currencies: { [field in Field]?: Currency }
pair?: Pair | null
pairState: PairState
currencyBalances: { [field in Field]?: CurrencyAmount }
parsedAmounts: { [field in Field]?: CurrencyAmount }
position: Position | undefined
price?: Price
noLiquidity?: boolean
errorMessage?: string
invalidPool: boolean
outOfRange: boolean
invalidRange: boolean
depositADisabled: boolean
depositBDisabled: boolean
invertPrice: boolean
liquidityMinted?: TokenAmount
poolTokenPercentage?: Percent
error?: string
} {
const { account, chainId } = useActiveWeb3React()
const {
independentField,
typedValue,
leftRangeTypedValue,
rightRangeTypedValue,
startPriceTypedValue,
} = useMintState()
const { independentField, typedValue, otherTypedValue } = useMintState()
const dependentField = independentField === Field.CURRENCY_A ? Field.CURRENCY_B : Field.CURRENCY_A
// currencies
// tokens
const currencies: { [field in Field]?: Currency } = useMemo(
() => ({
[Field.CURRENCY_A]: currencyA,
[Field.CURRENCY_B]: currencyB,
[Field.CURRENCY_A]: currencyA ?? undefined,
[Field.CURRENCY_B]: currencyB ?? undefined,
}),
[currencyA, currencyB]
)
// formatted with tokens
const [tokenA, tokenB, baseToken] = useMemo(
() => [
wrappedCurrency(currencyA, chainId),
wrappedCurrency(currencyB, chainId),
wrappedCurrency(baseCurrency, chainId),
],
[chainId, currencyA, currencyB, baseCurrency]
)
// pair
const [pairState, pair] = useV2Pair(currencies[Field.CURRENCY_A], currencies[Field.CURRENCY_B])
const totalSupply = useTotalSupply(pair?.liquidityToken)
const [token0, token1] = useMemo(
() =>
tokenA && tokenB ? (tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]) : [undefined, undefined],
[tokenA, tokenB]
)
const noLiquidity: boolean =
pairState === PairState.NOT_EXISTS || Boolean(totalSupply && JSBI.equal(totalSupply.raw, ZERO))
// balances
const balances = useCurrencyBalances(account ?? undefined, [
@ -158,139 +95,31 @@ export function useDerivedMintInfo(
[Field.CURRENCY_B]: balances[1],
}
// pool
const [poolState, pool] = usePool(currencies[Field.CURRENCY_A], currencies[Field.CURRENCY_B], feeAmount)
const noLiquidity = poolState === PoolState.NOT_EXISTS
// note to parse inputs in reverse
const invertPrice = Boolean(baseToken && token0 && !baseToken.equals(token0))
// always returns the price with 0 as base token
const price = useMemo(() => {
// if no liquidity use typed value
if (noLiquidity) {
const parsedQuoteAmount = tryParseAmount(startPriceTypedValue, invertPrice ? token0 : token1)
if (parsedQuoteAmount && token0 && token1) {
const baseAmount = tryParseAmount('1', invertPrice ? token1 : token0)
const price =
baseAmount && parsedQuoteAmount
? new Price(baseAmount.currency, parsedQuoteAmount.currency, baseAmount.raw, parsedQuoteAmount.raw)
: undefined
return (invertPrice ? price?.invert() : price) ?? undefined
}
return undefined
} else {
// get the amount of quote currency
return pool && token0 ? pool.priceOf(token0) : undefined
}
}, [noLiquidity, startPriceTypedValue, invertPrice, token1, token0, pool])
// used for ratio calculation when pool not initialized
const mockPool = useMemo(() => {
if (tokenA && tokenB && feeAmount && price) {
const currentTick = priceToClosestTick(price)
const currentSqrt = TickMath.getSqrtRatioAtTick(currentTick)
return new Pool(tokenA, tokenB, feeAmount, currentSqrt, JSBI.BigInt(0), currentTick, [])
} else {
return undefined
}
}, [feeAmount, price, tokenA, tokenB])
// if pool exists use it, if not use the mock pool
const poolForPosition: Pool | undefined = pool ?? mockPool
// parse typed range values and determine closest ticks
// lower should always be a smaller tick
const ticks: {
[key: string]: number | undefined
} = useMemo(() => {
return {
[Bound.LOWER]:
typeof existingPosition?.tickLower === 'number'
? existingPosition.tickLower
: invertPrice
? tryParseTick(token1, token0, feeAmount, rightRangeTypedValue)
: tryParseTick(token0, token1, feeAmount, leftRangeTypedValue),
[Bound.UPPER]:
typeof existingPosition?.tickUpper === 'number'
? existingPosition.tickUpper
: invertPrice
? tryParseTick(token1, token0, feeAmount, leftRangeTypedValue)
: tryParseTick(token0, token1, feeAmount, rightRangeTypedValue),
}
}, [existingPosition, feeAmount, invertPrice, leftRangeTypedValue, rightRangeTypedValue, token0, token1])
const { [Bound.LOWER]: tickLower, [Bound.UPPER]: tickUpper } = ticks || {}
// mark invalid range
const invalidRange = Boolean(typeof tickLower === 'number' && typeof tickUpper === 'number' && tickLower >= tickUpper)
// always returns the price with 0 as base token
const pricesAtTicks = useMemo(() => {
return {
[Bound.LOWER]: getTickToPrice(token0, token1, ticks[Bound.LOWER]),
[Bound.UPPER]: getTickToPrice(token0, token1, ticks[Bound.UPPER]),
}
}, [token0, token1, ticks])
const { [Bound.LOWER]: lowerPrice, [Bound.UPPER]: upperPrice } = pricesAtTicks
// liquidity range warning
const outOfRange = Boolean(
!invalidRange && price && lowerPrice && upperPrice && (price.lessThan(lowerPrice) || price.greaterThan(upperPrice))
)
// amounts
const independentAmount: CurrencyAmount | undefined = tryParseAmount(typedValue, currencies[independentField])
const dependentAmount: CurrencyAmount | undefined = useMemo(() => {
if (noLiquidity) {
if (otherTypedValue && currencies[dependentField]) {
return tryParseAmount(otherTypedValue, currencies[dependentField])
}
return undefined
} else if (independentAmount) {
// we wrap the currencies just to get the price in terms of the other token
const wrappedIndependentAmount = wrappedCurrencyAmount(independentAmount, chainId)
const [tokenA, tokenB] = [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)]
if (tokenA && tokenB && wrappedIndependentAmount && pair) {
const dependentCurrency = dependentField === Field.CURRENCY_B ? currencyB : currencyA
if (
independentAmount &&
wrappedIndependentAmount &&
typeof tickLower === 'number' &&
typeof tickUpper === 'number' &&
poolForPosition
) {
// if price is out of range or invalid range - return 0 (single deposit will be independent)
if (outOfRange || invalidRange) {
return undefined
}
const position: Position | undefined = wrappedIndependentAmount.token.equals(poolForPosition.token0)
? Position.fromAmount0({
pool: poolForPosition,
tickLower,
tickUpper,
amount0: independentAmount.raw,
})
: Position.fromAmount1({
pool: poolForPosition,
tickLower,
tickUpper,
amount1: independentAmount.raw,
})
const dependentTokenAmount = wrappedIndependentAmount.token.equals(poolForPosition.token0)
? position.amount1
: position.amount0
const dependentTokenAmount =
dependentField === Field.CURRENCY_B
? pair.priceOf(tokenA).quote(wrappedIndependentAmount)
: pair.priceOf(tokenB).quote(wrappedIndependentAmount)
return dependentCurrency === ETHER ? CurrencyAmount.ether(dependentTokenAmount.raw) : dependentTokenAmount
}
return undefined
}, [
independentAmount,
chainId,
outOfRange,
dependentField,
currencyB,
currencyA,
tickLower,
tickUpper,
poolForPosition,
invalidRange,
])
} else {
return undefined
}
}, [noLiquidity, otherTypedValue, currencies, dependentField, independentAmount, currencyA, chainId, currencyB, pair])
const parsedAmounts: { [field in Field]: CurrencyAmount | undefined } = useMemo(() => {
return {
@ -299,164 +128,75 @@ export function useDerivedMintInfo(
}
}, [dependentAmount, independentAmount, independentField])
// single deposit only if price is out of range
const deposit0Disabled = Boolean(
typeof tickUpper === 'number' && poolForPosition && poolForPosition.tickCurrent >= tickUpper
)
const deposit1Disabled = Boolean(
typeof tickLower === 'number' && poolForPosition && poolForPosition.tickCurrent <= tickLower
)
// sorted for token order
const depositADisabled =
invalidRange ||
Boolean(
(deposit0Disabled && poolForPosition && tokenA && poolForPosition.token0.equals(tokenA)) ||
(deposit1Disabled && poolForPosition && tokenA && poolForPosition.token1.equals(tokenA))
)
const depositBDisabled =
invalidRange ||
Boolean(
(deposit0Disabled && poolForPosition && tokenB && poolForPosition.token0.equals(tokenB)) ||
(deposit1Disabled && poolForPosition && tokenB && poolForPosition.token1.equals(tokenB))
)
// create position entity based on users selection
const position: Position | undefined = useMemo(() => {
if (
!poolForPosition ||
!tokenA ||
!tokenB ||
typeof tickLower !== 'number' ||
typeof tickUpper !== 'number' ||
invalidRange
) {
return undefined
const price = useMemo(() => {
if (noLiquidity) {
const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts
if (currencyAAmount && currencyBAmount) {
return new Price(currencyAAmount.currency, currencyBAmount.currency, currencyAAmount.raw, currencyBAmount.raw)
}
return undefined
} else {
const wrappedCurrencyA = wrappedCurrency(currencyA, chainId)
return pair && wrappedCurrencyA ? pair.priceOf(wrappedCurrencyA) : undefined
}
}, [chainId, currencyA, noLiquidity, pair, parsedAmounts])
// mark as 0 if disbaled because out of range
const amount0 = !deposit0Disabled
? parsedAmounts?.[tokenA.equals(poolForPosition.token0) ? Field.CURRENCY_A : Field.CURRENCY_B]?.raw
: BIG_INT_ZERO
const amount1 = !deposit1Disabled
? parsedAmounts?.[tokenA.equals(poolForPosition.token0) ? Field.CURRENCY_B : Field.CURRENCY_A]?.raw
: BIG_INT_ZERO
if (amount0 !== undefined && amount1 !== undefined) {
return Position.fromAmounts({
pool: poolForPosition,
tickLower,
tickUpper,
amount0,
amount1,
})
// liquidity minted
const liquidityMinted = useMemo(() => {
const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts
const [tokenAmountA, tokenAmountB] = [
wrappedCurrencyAmount(currencyAAmount, chainId),
wrappedCurrencyAmount(currencyBAmount, chainId),
]
if (pair && totalSupply && tokenAmountA && tokenAmountB) {
return pair.getLiquidityMinted(totalSupply, tokenAmountA, tokenAmountB)
} else {
return undefined
}
}, [
parsedAmounts,
poolForPosition,
tokenA,
tokenB,
deposit0Disabled,
deposit1Disabled,
invalidRange,
tickLower,
tickUpper,
])
}, [parsedAmounts, chainId, pair, totalSupply])
let errorMessage: string | undefined
const poolTokenPercentage = useMemo(() => {
if (liquidityMinted && totalSupply) {
return new Percent(liquidityMinted.raw, totalSupply.add(liquidityMinted).raw)
} else {
return undefined
}
}, [liquidityMinted, totalSupply])
let error: string | undefined
if (!account) {
errorMessage = 'Connect Wallet'
error = 'Connect Wallet'
}
if (poolState === PoolState.INVALID) {
errorMessage = errorMessage ?? 'Invalid pair'
if (pairState === PairState.INVALID) {
error = error ?? 'Invalid pair'
}
if (
(!parsedAmounts[Field.CURRENCY_A] && !depositADisabled) ||
(!parsedAmounts[Field.CURRENCY_B] && !depositBDisabled)
) {
errorMessage = errorMessage ?? 'Enter an amount'
if (!parsedAmounts[Field.CURRENCY_A] || !parsedAmounts[Field.CURRENCY_B]) {
error = error ?? 'Enter an amount'
}
const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts
if (currencyAAmount && currencyBalances?.[Field.CURRENCY_A]?.lessThan(currencyAAmount)) {
errorMessage = 'Insufficient ' + currencies[Field.CURRENCY_A]?.symbol + ' balance'
error = 'Insufficient ' + currencies[Field.CURRENCY_A]?.symbol + ' balance'
}
if (currencyBAmount && currencyBalances?.[Field.CURRENCY_B]?.lessThan(currencyBAmount)) {
errorMessage = 'Insufficient ' + currencies[Field.CURRENCY_B]?.symbol + ' balance'
error = 'Insufficient ' + currencies[Field.CURRENCY_B]?.symbol + ' balance'
}
const invalidPool = poolState === PoolState.INVALID
return {
dependentField,
currencies,
pool,
poolState,
pair,
pairState,
currencyBalances,
parsedAmounts,
ticks,
price,
pricesAtTicks,
position,
noLiquidity,
errorMessage,
invalidPool,
invalidRange,
outOfRange,
depositADisabled,
depositBDisabled,
invertPrice,
liquidityMinted,
poolTokenPercentage,
error,
}
}
export function useRangeHopCallbacks(
baseCurrency: Currency | undefined,
quoteCurrency: Currency | undefined,
feeAmount: FeeAmount | undefined,
tickLower: number | undefined,
tickUpper: number | undefined
) {
const { chainId } = useActiveWeb3React()
const baseToken = useMemo(() => wrappedCurrency(baseCurrency, chainId), [baseCurrency, chainId])
const quoteToken = useMemo(() => wrappedCurrency(quoteCurrency, chainId), [quoteCurrency, chainId])
const getDecrementLower = useCallback(() => {
if (baseToken && quoteToken && typeof tickLower === 'number' && feeAmount) {
const newPrice = tickToPrice(baseToken, quoteToken, tickLower - TICK_SPACINGS[feeAmount])
return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP)
}
return ''
}, [baseToken, quoteToken, tickLower, feeAmount])
const getIncrementLower = useCallback(() => {
if (baseToken && quoteToken && typeof tickLower === 'number' && feeAmount) {
const newPrice = tickToPrice(baseToken, quoteToken, tickLower + TICK_SPACINGS[feeAmount])
return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP)
}
return ''
}, [baseToken, quoteToken, tickLower, feeAmount])
const getDecrementUpper = useCallback(() => {
if (baseToken && quoteToken && typeof tickUpper === 'number' && feeAmount) {
const newPrice = tickToPrice(baseToken, quoteToken, tickUpper - TICK_SPACINGS[feeAmount])
return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP)
}
return ''
}, [baseToken, quoteToken, tickUpper, feeAmount])
const getIncrementUpper = useCallback(() => {
if (baseToken && quoteToken && typeof tickUpper === 'number' && feeAmount) {
const newPrice = tickToPrice(baseToken, quoteToken, tickUpper + TICK_SPACINGS[feeAmount])
return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP)
}
return ''
}, [baseToken, quoteToken, tickUpper, feeAmount])
return { getDecrementLower, getIncrementLower, getDecrementUpper, getIncrementUpper }
}

@ -1,12 +1,5 @@
import { createReducer } from '@reduxjs/toolkit'
import {
Field,
resetMintState,
typeInput,
typeStartPriceInput,
typeLeftRangeInput,
typeRightRangeInput,
} from './actions'
import { Field, resetMintState, typeInput } from './actions'
export interface MintState {
readonly independentField: Field
@ -29,24 +22,6 @@ export const initialState: MintState = {
export default createReducer<MintState>(initialState, (builder) =>
builder
.addCase(resetMintState, () => initialState)
.addCase(typeStartPriceInput, (state, { payload: { typedValue } }) => {
return {
...state,
startPriceTypedValue: typedValue,
}
})
.addCase(typeLeftRangeInput, (state, { payload: { typedValue } }) => {
return {
...state,
leftRangeTypedValue: typedValue,
}
})
.addCase(typeRightRangeInput, (state, { payload: { typedValue } }) => {
return {
...state,
rightRangeTypedValue: typedValue,
}
})
.addCase(typeInput, (state, { payload: { field, typedValue, noLiquidity } }) => {
if (noLiquidity) {
// they're typing into the field they've last typed in

@ -1,169 +0,0 @@
import { useMemo } from 'react'
import { Pair } from '@uniswap/v2-sdk'
import { Currency, CurrencyAmount, ETHER, Percent, Price, TokenAmount } from '@uniswap/sdk-core'
import JSBI from 'jsbi'
import { PairState, useV2Pair } from '../../hooks/useV2Pairs'
import { useTotalSupply } from '../../hooks/useTotalSupply'
import { useActiveWeb3React } from '../../hooks'
import { wrappedCurrency, wrappedCurrencyAmount } from '../../utils/wrappedCurrency'
import { tryParseAmount } from '../swap/hooks'
import { useCurrencyBalances } from '../wallet/hooks'
import { Field } from './actions'
import { useMintState } from './hooks'
const ZERO = JSBI.BigInt(0)
export function useV2DerivedMintInfo(
currencyA: Currency | undefined,
currencyB: Currency | undefined
): {
dependentField: Field
currencies: { [field in Field]?: Currency }
pair?: Pair | null
pairState: PairState
currencyBalances: { [field in Field]?: CurrencyAmount }
parsedAmounts: { [field in Field]?: CurrencyAmount }
price?: Price
noLiquidity?: boolean
liquidityMinted?: TokenAmount
poolTokenPercentage?: Percent
error?: string
} {
const { account, chainId } = useActiveWeb3React()
const { independentField, typedValue, otherTypedValue } = useMintState()
const dependentField = independentField === Field.CURRENCY_A ? Field.CURRENCY_B : Field.CURRENCY_A
// tokens
const currencies: { [field in Field]?: Currency } = useMemo(
() => ({
[Field.CURRENCY_A]: currencyA ?? undefined,
[Field.CURRENCY_B]: currencyB ?? undefined,
}),
[currencyA, currencyB]
)
// pair
const [pairState, pair] = useV2Pair(currencies[Field.CURRENCY_A], currencies[Field.CURRENCY_B])
const totalSupply = useTotalSupply(pair?.liquidityToken)
const noLiquidity: boolean =
pairState === PairState.NOT_EXISTS || Boolean(totalSupply && JSBI.equal(totalSupply.raw, ZERO))
// balances
const balances = useCurrencyBalances(account ?? undefined, [
currencies[Field.CURRENCY_A],
currencies[Field.CURRENCY_B],
])
const currencyBalances: { [field in Field]?: CurrencyAmount } = {
[Field.CURRENCY_A]: balances[0],
[Field.CURRENCY_B]: balances[1],
}
// amounts
const independentAmount: CurrencyAmount | undefined = tryParseAmount(typedValue, currencies[independentField])
const dependentAmount: CurrencyAmount | undefined = useMemo(() => {
if (noLiquidity) {
if (otherTypedValue && currencies[dependentField]) {
return tryParseAmount(otherTypedValue, currencies[dependentField])
}
return undefined
} else if (independentAmount) {
// we wrap the currencies just to get the price in terms of the other token
const wrappedIndependentAmount = wrappedCurrencyAmount(independentAmount, chainId)
const [tokenA, tokenB] = [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)]
if (tokenA && tokenB && wrappedIndependentAmount && pair) {
const dependentCurrency = dependentField === Field.CURRENCY_B ? currencyB : currencyA
const dependentTokenAmount =
dependentField === Field.CURRENCY_B
? pair.priceOf(tokenA).quote(wrappedIndependentAmount)
: pair.priceOf(tokenB).quote(wrappedIndependentAmount)
return dependentCurrency === ETHER ? CurrencyAmount.ether(dependentTokenAmount.raw) : dependentTokenAmount
}
return undefined
} else {
return undefined
}
}, [noLiquidity, otherTypedValue, currencies, dependentField, independentAmount, currencyA, chainId, currencyB, pair])
const parsedAmounts: { [field in Field]: CurrencyAmount | undefined } = useMemo(() => {
return {
[Field.CURRENCY_A]: independentField === Field.CURRENCY_A ? independentAmount : dependentAmount,
[Field.CURRENCY_B]: independentField === Field.CURRENCY_A ? dependentAmount : independentAmount,
}
}, [dependentAmount, independentAmount, independentField])
const price = useMemo(() => {
if (noLiquidity) {
const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts
if (currencyAAmount && currencyBAmount) {
return new Price(currencyAAmount.currency, currencyBAmount.currency, currencyAAmount.raw, currencyBAmount.raw)
}
return undefined
} else {
const wrappedCurrencyA = wrappedCurrency(currencyA, chainId)
return pair && wrappedCurrencyA ? pair.priceOf(wrappedCurrencyA) : undefined
}
}, [chainId, currencyA, noLiquidity, pair, parsedAmounts])
// liquidity minted
const liquidityMinted = useMemo(() => {
const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts
const [tokenAmountA, tokenAmountB] = [
wrappedCurrencyAmount(currencyAAmount, chainId),
wrappedCurrencyAmount(currencyBAmount, chainId),
]
if (pair && totalSupply && tokenAmountA && tokenAmountB) {
return pair.getLiquidityMinted(totalSupply, tokenAmountA, tokenAmountB)
} else {
return undefined
}
}, [parsedAmounts, chainId, pair, totalSupply])
const poolTokenPercentage = useMemo(() => {
if (liquidityMinted && totalSupply) {
return new Percent(liquidityMinted.raw, totalSupply.add(liquidityMinted).raw)
} else {
return undefined
}
}, [liquidityMinted, totalSupply])
let error: string | undefined
if (!account) {
error = 'Connect Wallet'
}
if (pairState === PairState.INVALID) {
error = error ?? 'Invalid pair'
}
if (!parsedAmounts[Field.CURRENCY_A] || !parsedAmounts[Field.CURRENCY_B]) {
error = error ?? 'Enter an amount'
}
const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts
if (currencyAAmount && currencyBalances?.[Field.CURRENCY_A]?.lessThan(currencyAAmount)) {
error = 'Insufficient ' + currencies[Field.CURRENCY_A]?.symbol + ' balance'
}
if (currencyBAmount && currencyBalances?.[Field.CURRENCY_B]?.lessThan(currencyBAmount)) {
error = 'Insufficient ' + currencies[Field.CURRENCY_B]?.symbol + ' balance'
}
return {
dependentField,
currencies,
pair,
pairState,
currencyBalances,
parsedAmounts,
price,
noLiquidity,
liquidityMinted,
poolTokenPercentage,
error,
}
}

@ -0,0 +1,19 @@
import { createAction } from '@reduxjs/toolkit'
export enum Field {
CURRENCY_A = 'CURRENCY_A',
CURRENCY_B = 'CURRENCY_B',
}
export enum Bound {
LOWER = 'LOWER',
UPPER = 'UPPER',
}
export const typeInput = createAction<{ field: Field; typedValue: string; noLiquidity: boolean }>(
'mintV3/typeInputMint'
)
export const typeStartPriceInput = createAction<{ typedValue: string }>('mintV3/typeStartPriceInput')
export const typeLeftRangeInput = createAction<{ typedValue: string }>('mintV3/typeLeftRangeInput')
export const typeRightRangeInput = createAction<{ typedValue: string }>('mintV3/typeRightRangeInput')
export const resetMintState = createAction<void>('mintV3/resetMintState')

462
src/state/mint/v3/hooks.ts Normal file

@ -0,0 +1,462 @@
import { BIG_INT_ZERO } from '../../../constants/index'
import { getTickToPrice } from 'utils/getTickToPrice'
import JSBI from 'jsbi'
import { PoolState } from '../../../hooks/usePools'
import {
Pool,
FeeAmount,
Position,
priceToClosestTick,
TickMath,
tickToPrice,
TICK_SPACINGS,
} from '@uniswap/v3-sdk/dist/'
import { Currency, CurrencyAmount, ETHER, Price, Rounding } from '@uniswap/sdk-core'
import { useCallback, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useActiveWeb3React } from '../../../hooks'
import { wrappedCurrency, wrappedCurrencyAmount } from '../../../utils/wrappedCurrency'
import { AppDispatch, AppState } from '../../index'
import { tryParseAmount } from '../../swap/hooks'
import { useCurrencyBalances } from '../../wallet/hooks'
import { Field, Bound, typeInput, typeStartPriceInput, typeLeftRangeInput, typeRightRangeInput } from './actions'
import { tryParseTick } from './utils'
import { usePool } from 'hooks/usePools'
export function useV3MintState(): AppState['mintV3'] {
return useSelector<AppState, AppState['mintV3']>((state) => state.mintV3)
}
export function useV3MintActionHandlers(
noLiquidity: boolean | undefined
): {
onFieldAInput: (typedValue: string) => void
onFieldBInput: (typedValue: string) => void
onLeftRangeInput: (typedValue: string) => void
onRightRangeInput: (typedValue: string) => void
onStartPriceInput: (typedValue: string) => void
} {
const dispatch = useDispatch<AppDispatch>()
const onFieldAInput = useCallback(
(typedValue: string) => {
dispatch(typeInput({ field: Field.CURRENCY_A, typedValue, noLiquidity: noLiquidity === true }))
},
[dispatch, noLiquidity]
)
const onFieldBInput = useCallback(
(typedValue: string) => {
dispatch(typeInput({ field: Field.CURRENCY_B, typedValue, noLiquidity: noLiquidity === true }))
},
[dispatch, noLiquidity]
)
const onLeftRangeInput = useCallback(
(typedValue: string) => {
dispatch(typeLeftRangeInput({ typedValue }))
},
[dispatch]
)
const onRightRangeInput = useCallback(
(typedValue: string) => {
dispatch(typeRightRangeInput({ typedValue }))
},
[dispatch]
)
const onStartPriceInput = useCallback(
(typedValue: string) => {
dispatch(typeStartPriceInput({ typedValue }))
},
[dispatch]
)
return {
onFieldAInput,
onFieldBInput,
onLeftRangeInput,
onRightRangeInput,
onStartPriceInput,
}
}
export function useV3DerivedMintInfo(
currencyA?: Currency,
currencyB?: Currency,
feeAmount?: FeeAmount,
baseCurrency?: Currency,
// override for existing position
existingPosition?: Position
): {
pool?: Pool | null
poolState: PoolState
ticks: { [bound in Bound]?: number | undefined }
price?: Price
pricesAtTicks: {
[bound in Bound]?: Price | undefined
}
currencies: { [field in Field]?: Currency }
currencyBalances: { [field in Field]?: CurrencyAmount }
dependentField: Field
parsedAmounts: { [field in Field]?: CurrencyAmount }
position: Position | undefined
noLiquidity?: boolean
errorMessage?: string
invalidPool: boolean
outOfRange: boolean
invalidRange: boolean
depositADisabled: boolean
depositBDisabled: boolean
invertPrice: boolean
} {
const { account, chainId } = useActiveWeb3React()
const {
independentField,
typedValue,
leftRangeTypedValue,
rightRangeTypedValue,
startPriceTypedValue,
} = useV3MintState()
const dependentField = independentField === Field.CURRENCY_A ? Field.CURRENCY_B : Field.CURRENCY_A
// currencies
const currencies: { [field in Field]?: Currency } = useMemo(
() => ({
[Field.CURRENCY_A]: currencyA,
[Field.CURRENCY_B]: currencyB,
}),
[currencyA, currencyB]
)
// formatted with tokens
const [tokenA, tokenB, baseToken] = useMemo(
() => [
wrappedCurrency(currencyA, chainId),
wrappedCurrency(currencyB, chainId),
wrappedCurrency(baseCurrency, chainId),
],
[chainId, currencyA, currencyB, baseCurrency]
)
const [token0, token1] = useMemo(
() =>
tokenA && tokenB ? (tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]) : [undefined, undefined],
[tokenA, tokenB]
)
// balances
const balances = useCurrencyBalances(account ?? undefined, [
currencies[Field.CURRENCY_A],
currencies[Field.CURRENCY_B],
])
const currencyBalances: { [field in Field]?: CurrencyAmount } = {
[Field.CURRENCY_A]: balances[0],
[Field.CURRENCY_B]: balances[1],
}
// pool
const [poolState, pool] = usePool(currencies[Field.CURRENCY_A], currencies[Field.CURRENCY_B], feeAmount)
const noLiquidity = poolState === PoolState.NOT_EXISTS
// note to parse inputs in reverse
const invertPrice = Boolean(baseToken && token0 && !baseToken.equals(token0))
// always returns the price with 0 as base token
const price = useMemo(() => {
// if no liquidity use typed value
if (noLiquidity) {
const parsedQuoteAmount = tryParseAmount(startPriceTypedValue, invertPrice ? token0 : token1)
if (parsedQuoteAmount && token0 && token1) {
const baseAmount = tryParseAmount('1', invertPrice ? token1 : token0)
const price =
baseAmount && parsedQuoteAmount
? new Price(baseAmount.currency, parsedQuoteAmount.currency, baseAmount.raw, parsedQuoteAmount.raw)
: undefined
return (invertPrice ? price?.invert() : price) ?? undefined
}
return undefined
} else {
// get the amount of quote currency
return pool && token0 ? pool.priceOf(token0) : undefined
}
}, [noLiquidity, startPriceTypedValue, invertPrice, token1, token0, pool])
// used for ratio calculation when pool not initialized
const mockPool = useMemo(() => {
if (tokenA && tokenB && feeAmount && price) {
const currentTick = priceToClosestTick(price)
const currentSqrt = TickMath.getSqrtRatioAtTick(currentTick)
return new Pool(tokenA, tokenB, feeAmount, currentSqrt, JSBI.BigInt(0), currentTick, [])
} else {
return undefined
}
}, [feeAmount, price, tokenA, tokenB])
// if pool exists use it, if not use the mock pool
const poolForPosition: Pool | undefined = pool ?? mockPool
// parse typed range values and determine closest ticks
// lower should always be a smaller tick
const ticks: {
[key: string]: number | undefined
} = useMemo(() => {
return {
[Bound.LOWER]:
typeof existingPosition?.tickLower === 'number'
? existingPosition.tickLower
: invertPrice
? tryParseTick(token1, token0, feeAmount, rightRangeTypedValue)
: tryParseTick(token0, token1, feeAmount, leftRangeTypedValue),
[Bound.UPPER]:
typeof existingPosition?.tickUpper === 'number'
? existingPosition.tickUpper
: invertPrice
? tryParseTick(token1, token0, feeAmount, leftRangeTypedValue)
: tryParseTick(token0, token1, feeAmount, rightRangeTypedValue),
}
}, [existingPosition, feeAmount, invertPrice, leftRangeTypedValue, rightRangeTypedValue, token0, token1])
const { [Bound.LOWER]: tickLower, [Bound.UPPER]: tickUpper } = ticks || {}
// mark invalid range
const invalidRange = Boolean(typeof tickLower === 'number' && typeof tickUpper === 'number' && tickLower >= tickUpper)
// always returns the price with 0 as base token
const pricesAtTicks = useMemo(() => {
return {
[Bound.LOWER]: getTickToPrice(token0, token1, ticks[Bound.LOWER]),
[Bound.UPPER]: getTickToPrice(token0, token1, ticks[Bound.UPPER]),
}
}, [token0, token1, ticks])
const { [Bound.LOWER]: lowerPrice, [Bound.UPPER]: upperPrice } = pricesAtTicks
// liquidity range warning
const outOfRange = Boolean(
!invalidRange && price && lowerPrice && upperPrice && (price.lessThan(lowerPrice) || price.greaterThan(upperPrice))
)
// amounts
const independentAmount: CurrencyAmount | undefined = tryParseAmount(typedValue, currencies[independentField])
const dependentAmount: CurrencyAmount | undefined = useMemo(() => {
// we wrap the currencies just to get the price in terms of the other token
const wrappedIndependentAmount = wrappedCurrencyAmount(independentAmount, chainId)
const dependentCurrency = dependentField === Field.CURRENCY_B ? currencyB : currencyA
if (
independentAmount &&
wrappedIndependentAmount &&
typeof tickLower === 'number' &&
typeof tickUpper === 'number' &&
poolForPosition
) {
// if price is out of range or invalid range - return 0 (single deposit will be independent)
if (outOfRange || invalidRange) {
return undefined
}
const position: Position | undefined = wrappedIndependentAmount.token.equals(poolForPosition.token0)
? Position.fromAmount0({
pool: poolForPosition,
tickLower,
tickUpper,
amount0: independentAmount.raw,
})
: Position.fromAmount1({
pool: poolForPosition,
tickLower,
tickUpper,
amount1: independentAmount.raw,
})
const dependentTokenAmount = wrappedIndependentAmount.token.equals(poolForPosition.token0)
? position.amount1
: position.amount0
return dependentCurrency === ETHER ? CurrencyAmount.ether(dependentTokenAmount.raw) : dependentTokenAmount
}
return undefined
}, [
independentAmount,
chainId,
outOfRange,
dependentField,
currencyB,
currencyA,
tickLower,
tickUpper,
poolForPosition,
invalidRange,
])
const parsedAmounts: { [field in Field]: CurrencyAmount | undefined } = useMemo(() => {
return {
[Field.CURRENCY_A]: independentField === Field.CURRENCY_A ? independentAmount : dependentAmount,
[Field.CURRENCY_B]: independentField === Field.CURRENCY_A ? dependentAmount : independentAmount,
}
}, [dependentAmount, independentAmount, independentField])
// single deposit only if price is out of range
const deposit0Disabled = Boolean(
typeof tickUpper === 'number' && poolForPosition && poolForPosition.tickCurrent >= tickUpper
)
const deposit1Disabled = Boolean(
typeof tickLower === 'number' && poolForPosition && poolForPosition.tickCurrent <= tickLower
)
// sorted for token order
const depositADisabled =
invalidRange ||
Boolean(
(deposit0Disabled && poolForPosition && tokenA && poolForPosition.token0.equals(tokenA)) ||
(deposit1Disabled && poolForPosition && tokenA && poolForPosition.token1.equals(tokenA))
)
const depositBDisabled =
invalidRange ||
Boolean(
(deposit0Disabled && poolForPosition && tokenB && poolForPosition.token0.equals(tokenB)) ||
(deposit1Disabled && poolForPosition && tokenB && poolForPosition.token1.equals(tokenB))
)
// create position entity based on users selection
const position: Position | undefined = useMemo(() => {
if (
!poolForPosition ||
!tokenA ||
!tokenB ||
typeof tickLower !== 'number' ||
typeof tickUpper !== 'number' ||
invalidRange
) {
return undefined
}
// mark as 0 if disbaled because out of range
const amount0 = !deposit0Disabled
? parsedAmounts?.[tokenA.equals(poolForPosition.token0) ? Field.CURRENCY_A : Field.CURRENCY_B]?.raw
: BIG_INT_ZERO
const amount1 = !deposit1Disabled
? parsedAmounts?.[tokenA.equals(poolForPosition.token0) ? Field.CURRENCY_B : Field.CURRENCY_A]?.raw
: BIG_INT_ZERO
if (amount0 !== undefined && amount1 !== undefined) {
return Position.fromAmounts({
pool: poolForPosition,
tickLower,
tickUpper,
amount0,
amount1,
})
} else {
return undefined
}
}, [
parsedAmounts,
poolForPosition,
tokenA,
tokenB,
deposit0Disabled,
deposit1Disabled,
invalidRange,
tickLower,
tickUpper,
])
let errorMessage: string | undefined
if (!account) {
errorMessage = 'Connect Wallet'
}
if (poolState === PoolState.INVALID) {
errorMessage = errorMessage ?? 'Invalid pair'
}
if (
(!parsedAmounts[Field.CURRENCY_A] && !depositADisabled) ||
(!parsedAmounts[Field.CURRENCY_B] && !depositBDisabled)
) {
errorMessage = errorMessage ?? 'Enter an amount'
}
const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts
if (currencyAAmount && currencyBalances?.[Field.CURRENCY_A]?.lessThan(currencyAAmount)) {
errorMessage = 'Insufficient ' + currencies[Field.CURRENCY_A]?.symbol + ' balance'
}
if (currencyBAmount && currencyBalances?.[Field.CURRENCY_B]?.lessThan(currencyBAmount)) {
errorMessage = 'Insufficient ' + currencies[Field.CURRENCY_B]?.symbol + ' balance'
}
const invalidPool = poolState === PoolState.INVALID
return {
dependentField,
currencies,
pool,
poolState,
currencyBalances,
parsedAmounts,
ticks,
price,
pricesAtTicks,
position,
noLiquidity,
errorMessage,
invalidPool,
invalidRange,
outOfRange,
depositADisabled,
depositBDisabled,
invertPrice,
}
}
export function useRangeHopCallbacks(
baseCurrency: Currency | undefined,
quoteCurrency: Currency | undefined,
feeAmount: FeeAmount | undefined,
tickLower: number | undefined,
tickUpper: number | undefined
) {
const { chainId } = useActiveWeb3React()
const baseToken = useMemo(() => wrappedCurrency(baseCurrency, chainId), [baseCurrency, chainId])
const quoteToken = useMemo(() => wrappedCurrency(quoteCurrency, chainId), [quoteCurrency, chainId])
const getDecrementLower = useCallback(() => {
if (baseToken && quoteToken && typeof tickLower === 'number' && feeAmount) {
const newPrice = tickToPrice(baseToken, quoteToken, tickLower - TICK_SPACINGS[feeAmount])
return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP)
}
return ''
}, [baseToken, quoteToken, tickLower, feeAmount])
const getIncrementLower = useCallback(() => {
if (baseToken && quoteToken && typeof tickLower === 'number' && feeAmount) {
const newPrice = tickToPrice(baseToken, quoteToken, tickLower + TICK_SPACINGS[feeAmount])
return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP)
}
return ''
}, [baseToken, quoteToken, tickLower, feeAmount])
const getDecrementUpper = useCallback(() => {
if (baseToken && quoteToken && typeof tickUpper === 'number' && feeAmount) {
const newPrice = tickToPrice(baseToken, quoteToken, tickUpper - TICK_SPACINGS[feeAmount])
return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP)
}
return ''
}, [baseToken, quoteToken, tickUpper, feeAmount])
const getIncrementUpper = useCallback(() => {
if (baseToken && quoteToken && typeof tickUpper === 'number' && feeAmount) {
const newPrice = tickToPrice(baseToken, quoteToken, tickUpper + TICK_SPACINGS[feeAmount])
return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP)
}
return ''
}, [baseToken, quoteToken, tickUpper, feeAmount])
return { getDecrementLower, getIncrementLower, getDecrementUpper, getIncrementUpper }
}

@ -0,0 +1,74 @@
import { createReducer } from '@reduxjs/toolkit'
import {
Field,
resetMintState,
typeInput,
typeStartPriceInput,
typeLeftRangeInput,
typeRightRangeInput,
} from './actions'
export 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
}
export const initialState: MintState = {
independentField: Field.CURRENCY_A,
typedValue: '',
startPriceTypedValue: '',
leftRangeTypedValue: '',
rightRangeTypedValue: '',
}
export default createReducer<MintState>(initialState, (builder) =>
builder
.addCase(resetMintState, () => initialState)
.addCase(typeStartPriceInput, (state, { payload: { typedValue } }) => {
return {
...state,
startPriceTypedValue: typedValue,
}
})
.addCase(typeLeftRangeInput, (state, { payload: { typedValue } }) => {
return {
...state,
leftRangeTypedValue: typedValue,
}
})
.addCase(typeRightRangeInput, (state, { payload: { typedValue } }) => {
return {
...state,
rightRangeTypedValue: typedValue,
}
})
.addCase(typeInput, (state, { payload: { field, typedValue, noLiquidity } }) => {
if (noLiquidity) {
// they're typing into the field they've last typed in
if (field === state.independentField) {
return {
...state,
independentField: field,
typedValue,
}
}
// they're typing into a new field, store the other value
else {
return {
...state,
independentField: field,
typedValue,
}
}
} else {
return {
...state,
independentField: field,
typedValue,
}
}
})
)