Commit b2f0236e authored by Noah Zinsmeister's avatar Noah Zinsmeister Committed by GitHub

Add liquidity callback (#830)

* give add liquidity the reducer treatment

rename setDefaultsFromURL to setDefaultsFromURLSearch

* fix tests and crash

* rework DOM structure to make flow more natural

* allow slippage + deadline setting in add liquidity

* disable token selection in mint

clear input between pairs
parent 4b570593
describe('Add Liquidity', () => {
it('loads the two correct tokens', () => {
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'MKR')
cy.get('#add-liquidity-input-token1 .token-symbol-container').should('contain.text', 'ETH')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
})
it('does not crash if ETH is duplicated', () => {
cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'ETH')
cy.get('#add-liquidity-input-token1 .token-symbol-container').should('not.contain.text', 'ETH')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'ETH')
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'ETH')
})
it('token not in storage is loaded', () => {
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'SKL')
cy.get('#add-liquidity-input-token1 .token-symbol-container').should('contain.text', 'MKR')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR')
})
})
This diff is collapsed.
......@@ -15,28 +15,14 @@ import FormattedPriceImpact from './FormattedPriceImpact'
import TokenLogo from '../TokenLogo'
import flatMap from 'lodash.flatmap'
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
trade: Trade
onDismiss: () => void
}
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
const theme = useContext(ThemeContext)
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, slippageTabProps.rawSlippage)
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage)
return (
<AutoColumn gap="md">
<CursorPointer>
<RowBetween onClick={onDismiss} padding={'8px 20px'}>
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}>
Hide Advanced
</Text>
<ChevronUp color={theme.text2} />
</RowBetween>
</CursorPointer>
<SectionBreak />
<>
<AutoColumn style={{ padding: '0 20px' }}>
<RowBetween>
<RowFixed>
......@@ -83,16 +69,36 @@ export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: A
</AutoColumn>
<SectionBreak />
</>
)
}
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
trade?: Trade
onDismiss: () => void
}
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
const theme = useContext(ThemeContext)
return (
<AutoColumn gap="md">
<CursorPointer>
<RowBetween onClick={onDismiss} padding={'8px 20px'}>
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}>
Hide Advanced
</Text>
<ChevronUp color={theme.text2} />
</RowBetween>
</CursorPointer>
<SectionBreak />
{trade && <TradeSummary trade={trade} allowedSlippage={slippageTabProps.rawSlippage} />}
<RowFixed padding={'0 20px'}>
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
Set slippage tolerance
</TYPE.black>
<QuestionHelper text="Your transaction will revert if the execution price changes by more than this amount after you submit your trade." />
</RowFixed>
<SlippageTabs {...slippageTabProps} />
{trade.route.path.length > 2 && (
{trade?.route?.path?.length > 2 && (
<AutoColumn style={{ padding: '0 20px' }}>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
......
import { Percent } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { ChevronDown } from 'react-feather'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { CursorPointer } from '../../theme'
import { warningServerity } from '../../utils/prices'
import { AutoColumn } from '../Column'
import { RowBetween } from '../Row'
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
import { PriceSlippageWarningCard } from './PriceSlippageWarningCard'
import { AdvancedDropwdown, FixedBottom } from './styleds'
import { AdvancedDropdown } from './styleds'
export default function AdvancedSwapDetailsDropdown({
priceImpactWithoutFee,
showAdvanced,
setShowAdvanced,
...rest
}: Omit<AdvancedSwapDetailsProps, 'onDismiss'> & {
showAdvanced: boolean
setShowAdvanced: (showAdvanced: boolean) => void
priceImpactWithoutFee: Percent
}) {
const theme = useContext(ThemeContext)
const severity = warningServerity(priceImpactWithoutFee)
return (
<AdvancedDropwdown>
<AdvancedDropdown>
{showAdvanced ? (
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} />
) : (
......@@ -37,11 +30,6 @@ export default function AdvancedSwapDetailsDropdown({
</RowBetween>
</CursorPointer>
)}
<FixedBottom>
<AutoColumn gap="lg">
{severity > 2 && <PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />}
</AutoColumn>
</FixedBottom>
</AdvancedDropwdown>
</AdvancedDropdown>
)
}
import { Percent } from '@uniswap/sdk'
import React from 'react'
import { ONE_BIPS } from '../../constants'
import { warningServerity } from '../../utils/prices'
import { warningSeverity } from '../../utils/prices'
import { ErrorText } from './styleds'
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
return (
<ErrorText fontWeight={500} fontSize={14} severity={warningServerity(priceImpact)}>
<ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}>
{priceImpact?.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact?.toFixed(2)}%` ?? '-'}
</ErrorText>
)
......
......@@ -28,19 +28,16 @@ export const FixedBottom = styled.div`
margin-bottom: 40px;
`
export const AdvancedDropwdown = styled.div`
position: absolute;
margin-top: -12px;
max-width: 455px;
export const AdvancedDropdown = styled.div`
padding-top: calc(10px + 2rem);
padding-bottom: 10px;
margin-top: -2rem;
width: 100%;
margin-bottom: 100px;
padding: 10px 0;
padding-top: 36px;
max-width: 400px;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
color: ${({ theme }) => theme.text2};
background-color: ${({ theme }) => theme.advancedBG};
color: ${({ theme }) => theme.text2};
z-index: -1;
`
......@@ -57,7 +54,7 @@ export const BottomGrouping = styled.div`
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 }>`
color: ${({ theme, severity }) =>
severity === 3 ? theme.red1 : severity === 2 ? theme.yellow2 : severity === 1 ? theme.green1 : theme.text1};
severity === 3 ? theme.red1 : severity === 2 ? theme.yellow2 : severity === 1 ? theme.text1 : theme.green1};
`
export const InputGroup = styled(AutoColumn)`
......
This diff is collapsed.
......@@ -67,6 +67,10 @@ const BackgroundGradient = styled.div`
}
`
const Marginer = styled.div`
margin-top: 5rem;
`
let Router: React.ComponentType
if (process.env.PUBLIC_URL === '.') {
Router = HashRouter
......@@ -99,6 +103,7 @@ export default function App() {
<Route component={RedirectPathToSwapOnly} />
</Switch>
</Web3ReactManager>
<Marginer />
<Footer />
</BodyWrapper>
<BackgroundGradient />
......
......@@ -2,17 +2,15 @@ import React from 'react'
import styled from 'styled-components'
import NavigationTabs from '../components/NavigationTabs'
export const Body = styled.div`
const Body = styled.div`
position: relative;
max-width: 420px;
width: 100%;
/* min-height: 340px; */
background: ${({ theme }) => theme.bg1};
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 30px;
padding: 1rem;
position: relative;
margin-bottom: 10rem;
`
/**
......
......@@ -30,14 +30,20 @@ import { useSendCallback } from '../../hooks/useSendCallback'
import { useSwapCallback } from '../../hooks/useSwapCallback'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/swap/actions'
import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../state/swap/hooks'
import {
useDefaultsFromURLSearch,
useDerivedSwapInfo,
useSwapActionHandlers,
useSwapState
} from '../../state/swap/hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody'
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
export default function Send({ location: { search } }: RouteComponentProps) {
useDefaultsFromURL(search)
useDefaultsFromURLSearch(search)
// text translation
// const { t } = useTranslation()
......@@ -173,7 +179,7 @@ export default function Send({ location: { search } }: RouteComponentProps) {
const [showInverted, setShowInverted] = useState<boolean>(false)
// warnings on slippage
const severity = !sendingWithSwap ? 0 : warningServerity(priceImpactWithoutFee)
const severity = !sendingWithSwap ? 0 : warningSeverity(priceImpactWithoutFee)
function modalHeader() {
if (!sendingWithSwap) {
......@@ -492,6 +498,9 @@ export default function Send({ location: { search } }: RouteComponentProps) {
)}
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
</BottomGrouping>
</Wrapper>
</AppBody>
{bestTrade && (
<AdvancedSwapDetailsDropdown
trade={bestTrade}
......@@ -499,13 +508,16 @@ export default function Send({ location: { search } }: RouteComponentProps) {
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
priceImpactWithoutFee={priceImpactWithoutFee}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
)}
</Wrapper>
</AppBody>
{priceImpactWithoutFee && severity > 2 && (
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
</AutoColumn>
)}
</>
)
}
......@@ -27,23 +27,30 @@ import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useAppro
import { useSwapCallback } from '../../hooks/useSwapCallback'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/swap/actions'
import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../state/swap/hooks'
import {
useDefaultsFromURLSearch,
useDerivedSwapInfo,
useSwapActionHandlers,
useSwapState
} from '../../state/swap/hooks'
import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody'
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
export default function Swap({ location: { search } }: RouteComponentProps) {
useDefaultsFromURL(search)
// text translation
// const { t } = useTranslation()
useDefaultsFromURLSearch(search)
const { chainId, account } = useActiveWeb3React()
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
// swap state
const { independentField, typedValue } = useSwapState()
const { bestTrade, tokenBalances, parsedAmounts, tokens, error, v1TradeLinkIfBetter } = useDerivedSwapInfo()
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
const isValid = !error
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
......@@ -58,6 +65,11 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
}
const route = bestTrade?.route
const userHasSpecifiedInputOutput =
!!tokens[Field.INPUT] &&
......@@ -69,13 +81,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
// check whether the user has approved the router on the input token
const [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage)
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
}
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
const maxAmountInput: TokenAmount =
!!tokenBalances[Field.INPUT] &&
!!tokens[Field.INPUT] &&
......@@ -88,7 +93,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
: tokenBalances[Field.INPUT]
: undefined
const atMaxAmountInput: boolean =
!!maxAmountInput && !!parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
maxAmountInput && parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
......@@ -130,7 +135,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
const [showInverted, setShowInverted] = useState<boolean>(false)
// warnings on slippage
const priceImpactSeverity = warningServerity(priceImpactWithoutFee)
const priceImpactSeverity = warningSeverity(priceImpactWithoutFee)
function modalHeader() {
return (
......@@ -259,13 +264,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
</AutoColumn>
<BottomGrouping>
{!account ? (
<ButtonLight
onClick={() => {
toggleWalletModal()
}}
>
Connect Wallet
</ButtonLight>
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
......@@ -294,6 +293,9 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
)}
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
</BottomGrouping>
</Wrapper>
</AppBody>
{bestTrade && (
<AdvancedSwapDetailsDropdown
trade={bestTrade}
......@@ -301,13 +303,16 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
priceImpactWithoutFee={priceImpactWithoutFee}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
)}
</Wrapper>
</AppBody>
{priceImpactWithoutFee && priceImpactSeverity > 2 && (
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
</AutoColumn>
)}
</>
)
}
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import { save, load } from 'redux-localstorage-simple'
import application from './application/reducer'
import { updateVersion } from './user/actions'
import user from './user/reducer'
import transactions from './transactions/reducer'
import wallet from './wallet/reducer'
import swap from './swap/reducer'
import transactions from './transactions/reducer'
import { save, load } from 'redux-localstorage-simple'
import mint from './mint/reducer'
import { updateVersion } from './user/actions'
const PERSISTED_KEYS: string[] = ['user', 'transactions']
......@@ -15,7 +18,8 @@ const store = configureStore({
user,
transactions,
wallet,
swap
swap,
mint
},
middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })],
preloadedState: load({ states: PERSISTED_KEYS })
......
import { createAction } from '@reduxjs/toolkit'
import { RouteComponentProps } from 'react-router-dom'
export enum Field {
TOKEN_A = 'TOKEN_A',
TOKEN_B = 'TOKEN_B'
}
export const setDefaultsFromURLMatchParams = createAction<{
chainId: number
params: RouteComponentProps<{ [k: string]: string }>['match']['params']
}>('setDefaultsFromMatch')
export const typeInput = createAction<{ field: Field; typedValue: string; noLiquidity: boolean }>('typeInputMint')
import { useEffect, useCallback, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Token, TokenAmount, Route, JSBI, Price, Percent, Pair } from '@uniswap/sdk'
import { useActiveWeb3React } from '../../hooks'
import { AppDispatch, AppState } from '../index'
import { setDefaultsFromURLMatchParams, Field, typeInput } from './actions'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
import { usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { tryParseAmount } from '../swap/hooks'
const ZERO = JSBI.BigInt(0)
export function useMintState(): AppState['mint'] {
return useSelector<AppState, AppState['mint']>(state => state.mint)
}
export function useDerivedMintInfo(): {
dependentField: Field
tokens: { [field in Field]?: Token }
pair?: Pair | null
tokenBalances: { [field in Field]?: TokenAmount }
parsedAmounts: { [field in Field]?: TokenAmount }
price?: Price
noLiquidity?: boolean
liquidityMinted?: TokenAmount
poolTokenPercentage?: Percent
error?: string
} {
const { account } = useActiveWeb3React()
const {
independentField,
typedValue,
otherTypedValue,
[Field.TOKEN_A]: { address: tokenAAddress },
[Field.TOKEN_B]: { address: tokenBAddress }
} = useMintState()
const dependentField = independentField === Field.TOKEN_A ? Field.TOKEN_B : Field.TOKEN_A
// tokens
const tokenA = useTokenByAddressAndAutomaticallyAdd(tokenAAddress)
const tokenB = useTokenByAddressAndAutomaticallyAdd(tokenBAddress)
const tokens: { [field in Field]?: Token } = useMemo(
() => ({
[Field.TOKEN_A]: tokenA,
[Field.TOKEN_B]: tokenB
}),
[tokenA, tokenB]
)
// pair
const pair = usePair(tokens[Field.TOKEN_A], tokens[Field.TOKEN_B])
const noLiquidity =
pair === null || (!!pair && JSBI.equal(pair.reserve0.raw, ZERO) && JSBI.equal(pair.reserve1.raw, ZERO))
// route
const route = useMemo(
() =>
!noLiquidity && pair && tokens[independentField] ? new Route([pair], tokens[Field.TOKEN_A] as Token) : undefined,
[noLiquidity, pair, tokens, independentField]
)
// balances
const relevantTokenBalances = useTokenBalancesTreatWETHAsETH(account ?? undefined, [
tokens[Field.TOKEN_A],
tokens[Field.TOKEN_B]
])
const tokenBalances: { [field in Field]?: TokenAmount } = {
[Field.TOKEN_A]: relevantTokenBalances?.[tokens[Field.TOKEN_A]?.address ?? ''],
[Field.TOKEN_B]: relevantTokenBalances?.[tokens[Field.TOKEN_B]?.address ?? '']
}
// amounts
const independentAmount = tryParseAmount(typedValue, tokens[independentField])
const dependentAmount = useMemo(() => {
if (noLiquidity && otherTypedValue && tokens[dependentField]) {
return tryParseAmount(otherTypedValue, tokens[dependentField])
} else if (route && independentAmount) {
return dependentField === Field.TOKEN_B
? route.midPrice.quote(independentAmount)
: route.midPrice.invert().quote(independentAmount)
} else {
return
}
}, [noLiquidity, otherTypedValue, tokens, dependentField, independentAmount, route])
const parsedAmounts = {
[Field.TOKEN_A]: independentField === Field.TOKEN_A ? independentAmount : dependentAmount,
[Field.TOKEN_B]: independentField === Field.TOKEN_A ? dependentAmount : independentAmount
}
const price = useMemo(() => {
if (
noLiquidity &&
tokens[Field.TOKEN_A] &&
tokens[Field.TOKEN_B] &&
parsedAmounts[Field.TOKEN_A] &&
parsedAmounts[Field.TOKEN_B]
) {
return new Price(
tokens[Field.TOKEN_A] as Token,
tokens[Field.TOKEN_B] as Token,
(parsedAmounts[Field.TOKEN_A] as TokenAmount).raw,
(parsedAmounts[Field.TOKEN_B] as TokenAmount).raw
)
} else if (route) {
return route.midPrice
} else {
return
}
}, [noLiquidity, tokens, parsedAmounts, route])
// liquidity minted
const totalSupply = useTotalSupply(pair?.liquidityToken)
const liquidityMinted = useMemo(() => {
if (pair && totalSupply && parsedAmounts[Field.TOKEN_A] && parsedAmounts[Field.TOKEN_B]) {
return pair.getLiquidityMinted(
totalSupply,
parsedAmounts[Field.TOKEN_A] as TokenAmount,
parsedAmounts[Field.TOKEN_B] as TokenAmount
)
} else {
return
}
}, [pair, totalSupply, parsedAmounts])
const poolTokenPercentage = useMemo(() => {
if (liquidityMinted && totalSupply) {
return new Percent(liquidityMinted.raw, totalSupply.add(liquidityMinted).raw)
} else {
return
}
}, [liquidityMinted, totalSupply])
let error: string | undefined
if (!account) {
error = 'Connect Wallet'
}
if (!parsedAmounts[Field.TOKEN_A] || !parsedAmounts[Field.TOKEN_B]) {
error = error ?? 'Enter an amount'
}
if (
parsedAmounts[Field.TOKEN_A] &&
tokenBalances?.[Field.TOKEN_A]?.lessThan(parsedAmounts[Field.TOKEN_A] as TokenAmount)
) {
error = 'Insufficient ' + tokens[Field.TOKEN_A]?.symbol + ' balance'
}
if (
parsedAmounts[Field.TOKEN_B] &&
tokenBalances?.[Field.TOKEN_B]?.lessThan(parsedAmounts[Field.TOKEN_B] as TokenAmount)
) {
error = 'Insufficient ' + tokens[Field.TOKEN_B]?.symbol + ' balance'
}
return {
dependentField,
tokens,
pair,
tokenBalances,
parsedAmounts,
price,
noLiquidity,
liquidityMinted,
poolTokenPercentage,
error
}
}
export function useMintActionHandlers(): {
onUserInput: (field: Field, typedValue: string) => void
} {
const dispatch = useDispatch<AppDispatch>()
const { noLiquidity } = useDerivedMintInfo()
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
dispatch(typeInput({ field, typedValue, noLiquidity: noLiquidity === true ? true : false }))
},
[dispatch, noLiquidity]
)
return {
onUserInput
}
}
// updates the mint state to use the appropriate tokens, given the route
export function useDefaultsFromURLMatchParams(params: { [k: string]: string }) {
const { chainId } = useActiveWeb3React()
const dispatch = useDispatch<AppDispatch>()
useEffect(() => {
if (!chainId) return
dispatch(setDefaultsFromURLMatchParams({ chainId, params }))
}, [dispatch, chainId, params])
}
import { ChainId, WETH } from '@uniswap/sdk'
import { createStore, Store } from 'redux'
import { Field, setDefaultsFromURLMatchParams } from './actions'
import reducer, { MintState } from './reducer'
describe('mint reducer', () => {
let store: Store<MintState>
beforeEach(() => {
store = createStore(reducer, {
independentField: Field.TOKEN_A,
typedValue: '',
otherTypedValue: '',
[Field.TOKEN_A]: { address: '' },
[Field.TOKEN_B]: { address: '' }
})
})
describe('setDefaultsFromURLMatchParams', () => {
test('ETH to DAI', () => {
store.dispatch(
setDefaultsFromURLMatchParams({
chainId: ChainId.MAINNET,
params: {
tokens: 'ETH-0x6b175474e89094c44da98b954eedeac495271d0f'
}
})
)
expect(store.getState()).toEqual({
independentField: Field.TOKEN_A,
typedValue: '',
otherTypedValue: '',
[Field.TOKEN_A]: { address: WETH[ChainId.MAINNET].address },
[Field.TOKEN_B]: { address: '0x6b175474e89094c44da98b954eedeac495271d0f' }
})
})
})
})
import { createReducer } from '@reduxjs/toolkit'
import { ChainId, WETH } from '@uniswap/sdk'
import { isAddress } from '../../utils'
import { Field, setDefaultsFromURLMatchParams, typeInput } from './actions'
export interface MintState {
readonly independentField: Field
readonly typedValue: string
readonly otherTypedValue: string // for the case when there's no liquidity
readonly [Field.TOKEN_A]: {
readonly address: string
}
readonly [Field.TOKEN_B]: {
readonly address: string
}
}
const initialState: MintState = {
independentField: Field.TOKEN_A,
typedValue: '',
otherTypedValue: '',
[Field.TOKEN_A]: {
address: ''
},
[Field.TOKEN_B]: {
address: ''
}
}
function parseTokens(chainId: number, tokens: string): string[] {
return (
tokens
// split by '-'
.split('-')
// map to addresses
.map((token): string =>
isAddress(token)
? token
: token.toLowerCase() === 'ETH'.toLowerCase()
? WETH[chainId as ChainId]?.address ?? ''
: ''
)
//remove duplicates
.filter((token, i, array) => array.indexOf(token) === i)
// add two empty elements for cases where the array is length 0
.concat(['', ''])
// only consider the first 2 elements
.slice(0, 2)
)
}
export default createReducer<MintState>(initialState, builder =>
builder
.addCase(setDefaultsFromURLMatchParams, (state, { payload: { chainId, params } }) => {
const tokens = parseTokens(chainId, params?.tokens ?? '')
return {
independentField: Field.TOKEN_A,
typedValue: '',
otherTypedValue: '',
[Field.TOKEN_A]: {
address: tokens[0]
},
[Field.TOKEN_B]: {
address: tokens[1]
}
}
})
.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,
otherTypedValue: state.typedValue
}
}
} else {
return {
...state,
independentField: field,
typedValue,
otherTypedValue: ''
}
}
})
)
......@@ -5,7 +5,7 @@ export enum Field {
OUTPUT = 'OUTPUT'
}
export const setDefaultsFromURL = createAction<{ chainId: number; queryString?: string }>('setDefaultsFromURL')
export const setDefaultsFromURLSearch = createAction<{ chainId: number; queryString?: string }>('setDefaultsFromURL')
export const selectToken = createAction<{ field: Field; address: string }>('selectToken')
export const switchTokens = createAction<void>('switchTokens')
export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInput')
......@@ -7,7 +7,7 @@ import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useTradeExactIn, useTradeExactOut } from '../../hooks/Trades'
import { AppDispatch, AppState } from '../index'
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
import { Field, selectToken, setDefaultsFromURL, switchTokens, typeInput } from './actions'
import { Field, selectToken, setDefaultsFromURLSearch, switchTokens, typeInput } from './actions'
import { useV1TradeLinkIfBetter } from '../../data/V1'
import { V1_TRADE_LINK_THRESHOLD } from '../../constants'
......@@ -52,7 +52,7 @@ export function useSwapActionHandlers(): {
}
// try to parse a user entered amount for a given token
function tryParseAmount(value?: string, token?: Token): TokenAmount | undefined {
export function tryParseAmount(value?: string, token?: Token): TokenAmount | undefined {
if (!value || !token) {
return
}
......@@ -155,11 +155,11 @@ export function useDerivedSwapInfo(): {
// updates the swap state to use the defaults for a given network whenever the query
// string updates
export function useDefaultsFromURL(search?: string) {
export function useDefaultsFromURLSearch(search?: string) {
const { chainId } = useActiveWeb3React()
const dispatch = useDispatch<AppDispatch>()
useEffect(() => {
if (!chainId) return
dispatch(setDefaultsFromURL({ chainId, queryString: search }))
dispatch(setDefaultsFromURLSearch({ chainId, queryString: search }))
}, [dispatch, search, chainId])
}
import { ChainId, WETH } from '@uniswap/sdk'
import { createStore, Store } from 'redux'
import { Field, setDefaultsFromURL } from './actions'
import { Field, setDefaultsFromURLSearch } from './actions'
import reducer, { SwapState } from './reducer'
describe('swap reducer', () => {
......@@ -18,7 +18,7 @@ describe('swap reducer', () => {
describe('setDefaultsFromURL', () => {
test('ETH to DAI', () => {
store.dispatch(
setDefaultsFromURL({
setDefaultsFromURLSearch({
chainId: ChainId.MAINNET,
queryString:
'?inputCurrency=ETH&outputCurrency=0x6b175474e89094c44da98b954eedeac495271d0f&exactAmount=20.5&exactField=outPUT'
......@@ -35,7 +35,7 @@ describe('swap reducer', () => {
test('does not duplicate eth for invalid output token', () => {
store.dispatch(
setDefaultsFromURL({
setDefaultsFromURLSearch({
chainId: ChainId.MAINNET,
queryString: '?outputCurrency=invalid'
})
......@@ -51,7 +51,7 @@ describe('swap reducer', () => {
test('output ETH only', () => {
store.dispatch(
setDefaultsFromURL({
setDefaultsFromURLSearch({
chainId: ChainId.MAINNET,
queryString: '?outputCurrency=eth&exactAmount=20.5'
})
......
......@@ -2,7 +2,7 @@ import { parse } from 'qs'
import { createReducer } from '@reduxjs/toolkit'
import { ChainId, WETH } from '@uniswap/sdk'
import { isAddress } from '../../utils'
import { Field, selectToken, setDefaultsFromURL, switchTokens, typeInput } from './actions'
import { Field, selectToken, setDefaultsFromURLSearch, switchTokens, typeInput } from './actions'
export interface SwapState {
readonly independentField: Field
......@@ -47,7 +47,7 @@ function parseIndependentFieldURLParameter(urlParam: any): Field {
export default createReducer<SwapState>(initialState, builder =>
builder
.addCase(setDefaultsFromURL, (state, { payload: { queryString, chainId } }) => {
.addCase(setDefaultsFromURLSearch, (_, { payload: { queryString, chainId } }) => {
if (queryString && queryString.length > 1) {
const parsedQs = parse(queryString, { parseArrays: false, ignoreQueryPrefix: true })
......
......@@ -51,7 +51,7 @@ export function computeSlippageAdjustedAmounts(
}
}
export function warningServerity(priceImpact: Percent): 0 | 1 | 2 | 3 {
export function warningSeverity(priceImpact: Percent): 0 | 1 | 2 | 3 {
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_HIGH)) return 3
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_MEDIUM)) return 2
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_LOW)) return 1
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment