separate v2 + v3 mint reducers
reset v3 mint state on migrate mount/unmount
This commit is contained in:
parent
40f0e619cc
commit
cd3c48462d
@ -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,
|
||||
}
|
||||
}
|
19
src/state/mint/v3/actions.ts
Normal file
19
src/state/mint/v3/actions.ts
Normal file
@ -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
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 }
|
||||
}
|
74
src/state/mint/v3/reducer.ts
Normal file
74
src/state/mint/v3/reducer.ts
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
Loading…
Reference in New Issue
Block a user