From cd3c48462d868f28a3ff6a327bb8c4afb5aea117 Mon Sep 17 00:00:00 2001 From: Noah Zinsmeister Date: Wed, 5 May 2021 11:18:54 -0400 Subject: [PATCH] separate v2 + v3 mint reducers reset v3 mint state on migrate mount/unmount --- src/components/NavigationTabs/index.tsx | 7 +- src/pages/AddLiquidity/PoolPriceBar.tsx | 2 +- src/pages/AddLiquidity/Review.tsx | 2 +- src/pages/AddLiquidity/index.tsx | 15 +- src/pages/AddLiquidityV2/index.tsx | 5 +- src/pages/MigrateV2/MigrateV2Pair.tsx | 21 +- src/state/index.ts | 2 + src/state/mint/actions.ts | 14 - src/state/mint/hooks.ts | 440 +++++----------------- src/state/mint/reducer.ts | 27 +- src/state/mint/v2.ts | 169 --------- src/state/mint/v3/actions.ts | 19 + src/state/mint/v3/hooks.ts | 462 ++++++++++++++++++++++++ src/state/mint/v3/reducer.ts | 74 ++++ src/state/mint/{ => v3}/utils.ts | 0 15 files changed, 684 insertions(+), 575 deletions(-) delete mode 100644 src/state/mint/v2.ts create mode 100644 src/state/mint/v3/actions.ts create mode 100644 src/state/mint/v3/hooks.ts create mode 100644 src/state/mint/v3/reducer.ts rename src/state/mint/{ => v3}/utils.ts (100%) diff --git a/src/components/NavigationTabs/index.tsx b/src/components/NavigationTabs/index.tsx index 86b595a24d..485b473306 100644 --- a/src/components/NavigationTabs/index.tsx +++ b/src/components/NavigationTabs/index.tsx @@ -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({ { - adding && dispatch(resetMintState()) + if (adding) { + // not 100% sure both of these are needed + dispatch(resetMintState()) + dispatch(resetMintV3State()) + } }} > diff --git a/src/pages/AddLiquidity/PoolPriceBar.tsx b/src/pages/AddLiquidity/PoolPriceBar.tsx index 025f7c8d85..a86a72b39b 100644 --- a/src/pages/AddLiquidity/PoolPriceBar.tsx +++ b/src/pages/AddLiquidity/PoolPriceBar.tsx @@ -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({ diff --git a/src/pages/AddLiquidity/Review.tsx b/src/pages/AddLiquidity/Review.tsx index 1f68113b7c..6026bf1a0c 100644 --- a/src/pages/AddLiquidity/Review.tsx +++ b/src/pages/AddLiquidity/Review.tsx @@ -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' diff --git a/src/pages/AddLiquidity/index.tsx b/src/pages/AddLiquidity/index.tsx index f912da3ff4..a35c86695f 100644 --- a/src/pages/AddLiquidity/index.tsx +++ b/src/pages/AddLiquidity/index.tsx @@ -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 diff --git a/src/pages/AddLiquidityV2/index.tsx b/src/pages/AddLiquidityV2/index.tsx index 9bc2ed09ca..11e396d3d3 100644 --- a/src/pages/AddLiquidityV2/index.tsx +++ b/src/pages/AddLiquidityV2/index.tsx @@ -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) diff --git a/src/pages/MigrateV2/MigrateV2Pair.tsx b/src/pages/MigrateV2/MigrateV2Pair.tsx index f108f4ae86..f8c5aaaf6c 100644 --- a/src/pages/MigrateV2/MigrateV2Pair.tsx +++ b/src/pages/MigrateV2/MigrateV2Pair.tsx @@ -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() + useEffect(() => { + dispatch(resetMintState()) + return () => { + dispatch(resetMintState()) + } + }, [dispatch]) + const { chainId, account } = useActiveWeb3React() // get pair contract diff --git a/src/state/index.ts b/src/state/index.ts index e1e44ae461..2e833b5d2f 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -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, diff --git a/src/state/mint/actions.ts b/src/state/mint/actions.ts index 7502b82ac9..4fcd912c51 100644 --- a/src/state/mint/actions.ts +++ b/src/state/mint/actions.ts @@ -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('mint/resetMintState') diff --git a/src/state/mint/hooks.ts b/src/state/mint/hooks.ts index 65c52b4c51..ff28db72c6 100644 --- a/src/state/mint/hooks.ts +++ b/src/state/mint/hooks.ts @@ -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((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() @@ -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(() => { - // 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 + if (noLiquidity) { + if (otherTypedValue && currencies[dependentField]) { + return tryParseAmount(otherTypedValue, currencies[dependentField]) } - - 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 + } 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 } - - return undefined - }, [ - independentAmount, - chainId, - outOfRange, - dependentField, - currencyB, - currencyA, - tickLower, - tickUpper, - poolForPosition, - invalidRange, - ]) + }, [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 - ) { + 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 } -} diff --git a/src/state/mint/reducer.ts b/src/state/mint/reducer.ts index 928373c435..3b952aa4bc 100644 --- a/src/state/mint/reducer.ts +++ b/src/state/mint/reducer.ts @@ -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(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 diff --git a/src/state/mint/v2.ts b/src/state/mint/v2.ts deleted file mode 100644 index 76ccc7f206..0000000000 --- a/src/state/mint/v2.ts +++ /dev/null @@ -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, - } -} diff --git a/src/state/mint/v3/actions.ts b/src/state/mint/v3/actions.ts new file mode 100644 index 0000000000..840cfa7c05 --- /dev/null +++ b/src/state/mint/v3/actions.ts @@ -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('mintV3/resetMintState') diff --git a/src/state/mint/v3/hooks.ts b/src/state/mint/v3/hooks.ts new file mode 100644 index 0000000000..450fd1138e --- /dev/null +++ b/src/state/mint/v3/hooks.ts @@ -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((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() + + 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 } +} diff --git a/src/state/mint/v3/reducer.ts b/src/state/mint/v3/reducer.ts new file mode 100644 index 0000000000..286a14a043 --- /dev/null +++ b/src/state/mint/v3/reducer.ts @@ -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(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, + } + } + }) +) diff --git a/src/state/mint/utils.ts b/src/state/mint/v3/utils.ts similarity index 100% rename from src/state/mint/utils.ts rename to src/state/mint/v3/utils.ts