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', () => { describe('Add Liquidity', () => {
it('loads the two correct tokens', () => { it('loads the two correct tokens', () => {
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab') cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'MKR') cy.get('#add-liquidity-input-tokena .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-tokenb .token-symbol-container').should('contain.text', 'ETH')
}) })
it('does not crash if ETH is duplicated', () => { it('does not crash if ETH is duplicated', () => {
cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab') cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'ETH') cy.get('#add-liquidity-input-tokena .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-tokenb .token-symbol-container').should('not.contain.text', 'ETH')
}) })
it('token not in storage is loaded', () => { it('token not in storage is loaded', () => {
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'SKL') cy.get('#add-liquidity-input-tokena .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-tokenb .token-symbol-container').should('contain.text', 'MKR')
}) })
}) })
import React, { useState, useEffect, useRef, useCallback, useContext } from 'react' import React, { useState, useRef, useContext } from 'react'
import styled, { ThemeContext } from 'styled-components' import styled, { ThemeContext } from 'styled-components'
import QuestionHelper from '../QuestionHelper' import QuestionHelper from '../QuestionHelper'
import { Text } from 'rebass'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row' import { RowBetween, RowFixed } from '../Row'
import { darken } from 'polished' import { darken } from 'polished'
import { useDebounce } from '../../hooks'
const WARNING_TYPE = Object.freeze({ enum SlippageError {
none: 'none', InvalidInput = 'InvalidInput',
emptyInput: 'emptyInput', RiskyLow = 'RiskyLow',
invalidEntryBound: 'invalidEntryBound', RiskyHigh = 'RiskyHigh'
riskyEntryHigh: 'riskyEntryHigh', }
riskyEntryLow: 'riskyEntryLow'
}) enum DeadlineError {
InvalidInput = 'InvalidInput'
}
const FancyButton = styled.button` const FancyButton = styled.button`
color: ${({ theme }) => theme.text1}; color: ${({ theme }) => theme.text1};
...@@ -46,7 +46,7 @@ const Option = styled(FancyButton)<{ active: boolean }>` ...@@ -46,7 +46,7 @@ const Option = styled(FancyButton)<{ active: boolean }>`
color: ${({ active, theme }) => (active ? theme.white : theme.text1)}; color: ${({ active, theme }) => (active ? theme.white : theme.text1)};
` `
const Input = styled.input<{ active?: boolean }>` const Input = styled.input`
background: ${({ theme }) => theme.bg1}; background: ${({ theme }) => theme.bg1};
flex-grow: 1; flex-grow: 1;
font-size: 12px; font-size: 12px;
...@@ -56,15 +56,8 @@ const Input = styled.input<{ active?: boolean }>` ...@@ -56,15 +56,8 @@ const Input = styled.input<{ active?: boolean }>`
&::-webkit-inner-spin-button { &::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
} }
color: ${({ active, theme, color }) => (color === 'red' ? theme.red1 : active ? 'initial' : theme.text1)}; color: ${({ theme, color }) => (color === 'red' ? theme.red1 : theme.text1)};
cursor: ${({ active }) => (active ? 'initial' : 'inherit')}; text-align: right;
text-align: ${({ active }) => (active ? 'right' : 'left')};
`
const BottomError = styled(Text)<{ show?: boolean }>`
font-size: 14px;
font-weight: 400;
padding-top: ${({ show }) => (show ? '12px' : '')};
` `
const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>` const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>`
...@@ -89,12 +82,6 @@ const SlippageSelector = styled.div` ...@@ -89,12 +82,6 @@ const SlippageSelector = styled.div`
padding: 0 20px; padding: 0 20px;
` `
const Percent = styled.div`
color: ${({ color, theme }) => (color === 'faded' ? theme.bg1 : color === 'red' ? theme.red1 : 'inherit')};
font-size: 0, 8rem;
flex-grow: 0;
`
export interface SlippageTabsProps { export interface SlippageTabsProps {
rawSlippage: number rawSlippage: number
setRawSlippage: (rawSlippage: number) => void setRawSlippage: (rawSlippage: number) => void
...@@ -102,235 +89,139 @@ export interface SlippageTabsProps { ...@@ -102,235 +89,139 @@ export interface SlippageTabsProps {
setDeadline: (deadline: number) => void setDeadline: (deadline: number) => void
} }
export default function SlippageTabs({ setRawSlippage, rawSlippage, deadline, setDeadline }: SlippageTabsProps) { export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, setDeadline }: SlippageTabsProps) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const [activeIndex, setActiveIndex] = useState(2)
const [warningType, setWarningType] = useState(WARNING_TYPE.none)
const inputRef = useRef<HTMLInputElement>() const inputRef = useRef<HTMLInputElement>()
const [userInput, setUserInput] = useState('') const [slippageInput, setSlippageInput] = useState('')
const debouncedInput = useDebounce(userInput, 150) const [deadlineInput, setDeadlineInput] = useState('')
const [initialSlippage] = useState(rawSlippage)
const [deadlineInput, setDeadlineInput] = useState(deadline / 60)
const updateSlippage = useCallback(
newSlippage => {
// round to 2 decimals to prevent ethers error
const numParsed = newSlippage * 100
// set both slippage values in parents
setRawSlippage(numParsed)
},
[setRawSlippage]
)
const checkBounds = useCallback( const slippageInputIsValid =
slippageValue => { slippageInput === '' || (rawSlippage / 100).toFixed(2) === Number.parseFloat(slippageInput).toFixed(2)
setWarningType(WARNING_TYPE.none) const deadlineInputIsValid = deadlineInput === '' || (deadline / 60).toString() === deadlineInput
if (slippageValue === '' || slippageValue === '.') { let slippageError: SlippageError
return setWarningType(WARNING_TYPE.emptyInput) if (slippageInput !== '' && !slippageInputIsValid) {
slippageError = SlippageError.InvalidInput
} else if (slippageInputIsValid && rawSlippage < 50) {
slippageError = SlippageError.RiskyLow
} else if (slippageInputIsValid && rawSlippage > 500) {
slippageError = SlippageError.RiskyHigh
} }
// check bounds and set errors let deadlineError: DeadlineError
if (Number(slippageValue) < 0 || Number(slippageValue) > 50) { if (deadlineInput !== '' && !deadlineInputIsValid) {
return setWarningType(WARNING_TYPE.invalidEntryBound) deadlineError = DeadlineError.InvalidInput
}
if (Number(slippageValue) >= 0 && Number(slippageValue) < 0.1) {
setWarningType(WARNING_TYPE.riskyEntryLow)
}
if (Number(slippageValue) > 5) {
setWarningType(WARNING_TYPE.riskyEntryHigh)
} }
//update the actual slippage value in parent
updateSlippage(Number(slippageValue))
},
[updateSlippage]
)
function parseCustomDeadline(e) { function parseCustomSlippage(event) {
const val = e.target.value setSlippageInput(event.target.value)
const acceptableValues = [/^$/, /^\d+$/]
if (acceptableValues.some(re => re.test(val))) {
setDeadlineInput(val)
setDeadline(val * 60)
}
}
const setFromCustom = () => {
setActiveIndex(4)
inputRef.current.focus()
// if there's a value, evaluate the bounds
checkBounds(debouncedInput)
}
// used for slippage presets let valueAsIntFromRoundedFloat: number
const setFromFixed = useCallback( try {
(index, slippage) => { valueAsIntFromRoundedFloat = Number.parseInt((Number.parseFloat(event.target.value) * 100).toString())
// update slippage in parent, reset errors and input state } catch {}
updateSlippage(slippage)
setWarningType(WARNING_TYPE.none)
setActiveIndex(index)
},
[updateSlippage]
)
useEffect(() => { if (
switch (initialSlippage) { typeof valueAsIntFromRoundedFloat === 'number' &&
case 10: !Number.isNaN(valueAsIntFromRoundedFloat) &&
setFromFixed(1, 0.1) valueAsIntFromRoundedFloat < 5000
break ) {
case 50: setRawSlippage(valueAsIntFromRoundedFloat)
setFromFixed(2, 0.5)
break
case 100:
setFromFixed(3, 1)
break
default:
// restrict to 2 decimal places
const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
// if its within accepted decimal limit, update the input state
if (acceptableValues.some(val => val.test('' + initialSlippage / 100))) {
setUserInput('' + initialSlippage / 100)
setActiveIndex(4)
} }
} }
}, [initialSlippage, setFromFixed])
// check that the theyve entered number and correct decimal function parseCustomDeadline(event) {
const parseInput = e => { setDeadlineInput(event.target.value)
const input = e.target.value
// restrict to 2 decimal places let valueAsInt: number
const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/] try {
// if its within accepted decimal limit, update the input state valueAsInt = Number.parseInt(event.target.value) * 60
if (acceptableValues.some(a => a.test(input))) { } catch {}
setUserInput(input)
}
}
useEffect(() => { if (typeof valueAsInt === 'number' && !Number.isNaN(valueAsInt) && valueAsInt > 0) {
if (activeIndex === 4) { setDeadline(valueAsInt)
checkBounds(debouncedInput) }
} }
})
return ( return (
<> <>
<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>
<SlippageSelector> <SlippageSelector>
<RowBetween> <RowBetween>
<Option <Option
onClick={() => { onClick={() => {
setFromFixed(1, 0.1) setSlippageInput('')
setRawSlippage(10)
}} }}
active={activeIndex === 1} active={rawSlippage === 10}
> >
0.1% 0.1%
</Option> </Option>
<Option <Option
onClick={() => { onClick={() => {
setFromFixed(2, 0.5) setSlippageInput('')
setRawSlippage(50)
}} }}
active={activeIndex === 2} active={rawSlippage === 50}
> >
0.5% 0.5%
</Option> </Option>
<Option <Option
onClick={() => { onClick={() => {
setFromFixed(3, 1) setSlippageInput('')
setRawSlippage(100)
}} }}
active={activeIndex === 3} active={rawSlippage === 100}
> >
1% 1%
</Option> </Option>
<OptionCustom <OptionCustom active={![10, 50, 100].includes(rawSlippage)} warning={!slippageInputIsValid} tabIndex={-1}>
active={activeIndex === 4}
warning={
warningType !== WARNING_TYPE.none &&
warningType !== WARNING_TYPE.emptyInput &&
warningType !== WARNING_TYPE.riskyEntryLow
}
onClick={() => {
setFromCustom()
}}
>
<RowBetween> <RowBetween>
{!(warningType === WARNING_TYPE.none || warningType === WARNING_TYPE.emptyInput) && ( {!!slippageInput &&
<span (slippageError === SlippageError.RiskyLow || slippageError === SlippageError.RiskyHigh) ? (
role="img" <span role="img" aria-label="warning" style={{ color: '#F3841E' }}>
aria-label="warning"
style={{
color:
warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: warningType === WARNING_TYPE.riskyEntryLow
? '#F3841E'
: ''
}}
>
⚠️ ⚠️
</span> </span>
)} ) : null}
<Input <Input
tabIndex={-1}
ref={inputRef} ref={inputRef}
active={activeIndex === 4} placeholder={(rawSlippage / 100).toFixed(2)}
placeholder={ value={slippageInput}
activeIndex === 4 onBlur={() => {
? !!userInput parseCustomSlippage({ target: { value: (rawSlippage / 100).toFixed(2) } })
? '' }}
: '0' onChange={parseCustomSlippage}
: activeIndex !== 4 && userInput !== '' color={!slippageInputIsValid ? 'red' : ''}
? userInput
: 'Custom'
}
value={activeIndex === 4 ? userInput : ''}
onChange={parseInput}
color={
warningType === WARNING_TYPE.emptyInput
? ''
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: ''
}
/> />
<Percent
color={
activeIndex !== 4
? 'faded'
: warningType === WARNING_TYPE.riskyEntryHigh || warningType === WARNING_TYPE.invalidEntryBound
? 'red'
: ''
}
>
% %
</Percent>
</RowBetween> </RowBetween>
</OptionCustom> </OptionCustom>
</RowBetween> </RowBetween>
<RowBetween> {!!slippageError && (
<BottomError <RowBetween
show={activeIndex === 4} style={{
color={ fontSize: '14px',
warningType === WARNING_TYPE.emptyInput paddingTop: '7px',
? '#565A69' color: slippageError === SlippageError.InvalidInput ? 'red' : '#F3841E'
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow }}
? 'red'
: warningType === WARNING_TYPE.riskyEntryLow
? '#F3841E'
: ''
}
> >
{warningType === WARNING_TYPE.emptyInput && 'Enter a slippage percentage'} {slippageError === SlippageError.InvalidInput
{warningType === WARNING_TYPE.invalidEntryBound && 'Please select a value no greater than 50%'} ? 'Enter a valid slippage percentage'
{warningType === WARNING_TYPE.riskyEntryHigh && 'Your transaction may be frontrun'} : slippageError === SlippageError.RiskyLow
{warningType === WARNING_TYPE.riskyEntryLow && 'Your transaction may fail'} ? 'Your transaction may fail'
</BottomError> : 'Your transaction may be frontrun'}
</RowBetween> </RowBetween>
)}
</SlippageSelector> </SlippageSelector>
<AutoColumn gap="sm"> <AutoColumn gap="sm">
<RowFixed padding={'0 20px'}> <RowFixed padding={'0 20px'}>
<TYPE.black fontSize={14} color={theme.text2}> <TYPE.black fontSize={14} color={theme.text2}>
...@@ -339,10 +230,13 @@ export default function SlippageTabs({ setRawSlippage, rawSlippage, deadline, se ...@@ -339,10 +230,13 @@ export default function SlippageTabs({ setRawSlippage, rawSlippage, deadline, se
<QuestionHelper text="Deadline in minutes. If your transaction takes longer than this it will revert." /> <QuestionHelper text="Deadline in minutes. If your transaction takes longer than this it will revert." />
</RowFixed> </RowFixed>
<RowFixed padding={'0 20px'}> <RowFixed padding={'0 20px'}>
<OptionCustom style={{ width: '80px' }}> <OptionCustom style={{ width: '80px' }} tabIndex={-1}>
<Input <Input
tabIndex={-1} color={!!deadlineError ? 'red' : undefined}
placeholder={'' + deadlineInput} onBlur={() => {
parseCustomDeadline({ target: { value: (deadline / 60).toString() } })
}}
placeholder={(deadline / 60).toString()}
value={deadlineInput} value={deadlineInput}
onChange={parseCustomDeadline} onChange={parseCustomDeadline}
/> />
......
...@@ -15,28 +15,14 @@ import FormattedPriceImpact from './FormattedPriceImpact' ...@@ -15,28 +15,14 @@ import FormattedPriceImpact from './FormattedPriceImpact'
import TokenLogo from '../TokenLogo' import TokenLogo from '../TokenLogo'
import flatMap from 'lodash.flatmap' import flatMap from 'lodash.flatmap'
export interface AdvancedSwapDetailsProps extends SlippageTabsProps { function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
trade: Trade
onDismiss: () => void
}
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, slippageTabProps.rawSlippage) const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage)
return ( 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' }}> <AutoColumn style={{ padding: '0 20px' }}>
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
...@@ -83,16 +69,36 @@ export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: A ...@@ -83,16 +69,36 @@ export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: A
</AutoColumn> </AutoColumn>
<SectionBreak /> <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} /> <SlippageTabs {...slippageTabProps} />
{trade.route.path.length > 2 && ( {trade?.route?.path?.length > 2 && (
<AutoColumn style={{ padding: '0 20px' }}> <AutoColumn style={{ padding: '0 20px' }}>
<RowFixed> <RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}> <TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
......
import { Percent } from '@uniswap/sdk'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { ChevronDown } from 'react-feather' import { ChevronDown } from 'react-feather'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { CursorPointer } from '../../theme' import { CursorPointer } from '../../theme'
import { warningServerity } from '../../utils/prices'
import { AutoColumn } from '../Column'
import { RowBetween } from '../Row' import { RowBetween } from '../Row'
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails' import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
import { PriceSlippageWarningCard } from './PriceSlippageWarningCard' import { AdvancedDropdown } from './styleds'
import { AdvancedDropwdown, FixedBottom } from './styleds'
export default function AdvancedSwapDetailsDropdown({ export default function AdvancedSwapDetailsDropdown({
priceImpactWithoutFee,
showAdvanced, showAdvanced,
setShowAdvanced, setShowAdvanced,
...rest ...rest
}: Omit<AdvancedSwapDetailsProps, 'onDismiss'> & { }: Omit<AdvancedSwapDetailsProps, 'onDismiss'> & {
showAdvanced: boolean showAdvanced: boolean
setShowAdvanced: (showAdvanced: boolean) => void setShowAdvanced: (showAdvanced: boolean) => void
priceImpactWithoutFee: Percent
}) { }) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const severity = warningServerity(priceImpactWithoutFee)
return ( return (
<AdvancedDropwdown> <AdvancedDropdown>
{showAdvanced ? ( {showAdvanced ? (
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} /> <AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} />
) : ( ) : (
...@@ -37,11 +30,6 @@ export default function AdvancedSwapDetailsDropdown({ ...@@ -37,11 +30,6 @@ export default function AdvancedSwapDetailsDropdown({
</RowBetween> </RowBetween>
</CursorPointer> </CursorPointer>
)} )}
<FixedBottom> </AdvancedDropdown>
<AutoColumn gap="lg">
{severity > 2 && <PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />}
</AutoColumn>
</FixedBottom>
</AdvancedDropwdown>
) )
} }
import { Percent } from '@uniswap/sdk' import { Percent } from '@uniswap/sdk'
import React from 'react' import React from 'react'
import { ONE_BIPS } from '../../constants' import { ONE_BIPS } from '../../constants'
import { warningServerity } from '../../utils/prices' import { warningSeverity } from '../../utils/prices'
import { ErrorText } from './styleds' import { ErrorText } from './styleds'
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) { export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
return ( 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)}%` ?? '-'} {priceImpact?.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact?.toFixed(2)}%` ?? '-'}
</ErrorText> </ErrorText>
) )
......
...@@ -28,19 +28,16 @@ export const FixedBottom = styled.div` ...@@ -28,19 +28,16 @@ export const FixedBottom = styled.div`
margin-bottom: 40px; margin-bottom: 40px;
` `
export const AdvancedDropwdown = styled.div` export const AdvancedDropdown = styled.div`
position: absolute; padding-top: calc(10px + 2rem);
margin-top: -12px; padding-bottom: 10px;
max-width: 455px; margin-top: -2rem;
width: 100%; width: 100%;
margin-bottom: 100px; max-width: 400px;
padding: 10px 0;
padding-top: 36px;
border-bottom-left-radius: 20px; border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px; border-bottom-right-radius: 20px;
color: ${({ theme }) => theme.text2}; color: ${({ theme }) => theme.text2};
background-color: ${({ theme }) => theme.advancedBG}; background-color: ${({ theme }) => theme.advancedBG};
color: ${({ theme }) => theme.text2};
z-index: -1; z-index: -1;
` `
...@@ -57,7 +54,7 @@ export const BottomGrouping = styled.div` ...@@ -57,7 +54,7 @@ export const BottomGrouping = styled.div`
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 }>` export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 }>`
color: ${({ theme, severity }) => 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)` export const InputGroup = styled(AutoColumn)`
......
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { MaxUint256 } from '@ethersproject/constants' import { TokenAmount, WETH } from '@uniswap/sdk'
import { Contract } from '@ethersproject/contracts' import React, { useContext, useState } from 'react'
import { parseEther, parseUnits } from '@ethersproject/units'
import { JSBI, Percent, Price, Route, Token, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useEffect, useReducer, useState } from 'react'
import { Plus } from 'react-feather' import { Plus } from 'react-feather'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router-dom' import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
import styled, { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { ButtonLight, ButtonPrimary } from '../../components/Button' import { ButtonLight, ButtonPrimary, ButtonError } from '../../components/Button'
import { BlueCard, GreyCard, LightCard } from '../../components/Card' import { BlueCard, GreyCard, LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column' import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal' import ConfirmationModal from '../../components/ConfirmationModal'
...@@ -17,422 +14,143 @@ import CurrencyInputPanel from '../../components/CurrencyInputPanel' ...@@ -17,422 +14,143 @@ import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import DoubleLogo from '../../components/DoubleLogo' import DoubleLogo from '../../components/DoubleLogo'
import PositionCard from '../../components/PositionCard' import PositionCard from '../../components/PositionCard'
import Row, { AutoRow, RowBetween, RowFixed, RowFlat } from '../../components/Row' import Row, { AutoRow, RowBetween, RowFixed, RowFlat } from '../../components/Row'
import SearchModal from '../../components/SearchModal'
import TokenLogo from '../../components/TokenLogo' import TokenLogo from '../../components/TokenLogo'
import { ROUTER_ADDRESS } from '../../constants' import { ROUTER_ADDRESS, MIN_ETH, ONE_BIPS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { useTokenAllowance } from '../../data/Allowances' import { useActiveWeb3React } from '../../hooks'
import { usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { useTokenContract, useActiveWeb3React } from '../../hooks'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens' import { useTransactionAdder } from '../../state/transactions/hooks'
import { useHasPendingApproval, useTransactionAdder } from '../../state/transactions/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract, isAddress } from '../../utils' import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
import AppBody from '../AppBody' import AppBody from '../AppBody'
import { Dots, Wrapper } from '../Pool/styleds' import { Dots, Wrapper } from '../Pool/styleds'
import {
// denominated in bips useDefaultsFromURLMatchParams,
const ALLOWED_SLIPPAGE = 50 useMintState,
useDerivedMintInfo,
// denominated in seconds useMintActionHandlers
const DEADLINE_FROM_NOW = 60 * 20 } from '../../state/mint/hooks'
import { Field } from '../../state/mint/actions'
const FixedBottom = styled.div` import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
position: absolute; import { useWalletModalToggle } from '../../state/application/hooks'
margin-top: 2rem; import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
width: 100%;
`
enum Field {
INPUT = 'INPUT',
OUTPUT = 'OUTPUT'
}
interface AddState {
independentField: Field
typedValue: string
[Field.INPUT]: {
address: string | undefined
}
[Field.OUTPUT]: {
address: string | undefined
}
}
function initializeAddState(inputAddress?: string, outputAddress?: string): AddState {
const validatedInput = isAddress(inputAddress)
const validatedOutput = isAddress(outputAddress)
return {
independentField: Field.INPUT,
typedValue: '',
[Field.INPUT]: {
address: validatedInput || ''
},
[Field.OUTPUT]: {
address: validatedOutput && validatedOutput !== validatedInput ? validatedOutput : ''
}
}
}
enum AddAction {
SELECT_TOKEN,
SWITCH_TOKENS,
TYPE
}
interface Payload {
[AddAction.SELECT_TOKEN]: {
field: Field
address: string
}
[AddAction.SWITCH_TOKENS]: undefined
[AddAction.TYPE]: {
field: Field
typedValue: string
}
}
function reducer(
state: AddState,
action: {
type: AddAction
payload: Payload[AddAction]
}
): AddState {
switch (action.type) {
case AddAction.SELECT_TOKEN: {
const { field, address } = action.payload as Payload[AddAction.SELECT_TOKEN]
const otherField = field === Field.INPUT ? Field.OUTPUT : Field.INPUT
if (address === state[otherField].address) {
// the case where we have to swap the order
return {
...state,
independentField: state.independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT,
[field]: { address },
[otherField]: { address: state[field].address }
}
} else {
// the normal case
return {
...state,
[field]: { address }
}
}
}
case AddAction.TYPE: {
const { field, typedValue } = action.payload as Payload[AddAction.TYPE]
return {
...state,
independentField: field,
typedValue
}
}
default: {
throw Error
}
}
}
export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) { export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
const [token0, token1] = params.tokens.split('-') useDefaultsFromURLMatchParams(params)
const { account, chainId, library } = useActiveWeb3React() const { account, chainId, library } = useActiveWeb3React()
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
// modal states // toggle wallet when disconnected
const [showSearch, setShowSearch] = useState<boolean>(false) const toggleWalletModal = useWalletModalToggle()
// mint state
const { independentField, typedValue, otherTypedValue } = useMintState()
const {
dependentField,
tokens,
pair,
tokenBalances,
parsedAmounts,
price,
noLiquidity,
liquidityMinted,
poolTokenPercentage,
error
} = useDerivedMintInfo()
const { onUserInput } = useMintActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false) const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicke confirm const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
// input state
const [state, dispatch] = useReducer(reducer, initializeAddState(token0, token1))
const { independentField, typedValue, ...fieldData } = state
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
const inputToken = useTokenByAddressAndAutomaticallyAdd(fieldData[Field.INPUT].address)
const outputToken = useTokenByAddressAndAutomaticallyAdd(fieldData[Field.OUTPUT].address)
// get basic SDK entities
const tokens: { [field in Field]: Token } = {
[Field.INPUT]: inputToken,
[Field.OUTPUT]: outputToken
}
// token contracts for approvals and direct sends
const tokenContractInput: Contract = useTokenContract(tokens[Field.INPUT]?.address)
const tokenContractOutput: Contract = useTokenContract(tokens[Field.OUTPUT]?.address)
// exchange data
const pair = usePair(tokens[Field.INPUT], tokens[Field.OUTPUT])
const route: Route = pair ? new Route([pair], tokens[independentField]) : undefined
const totalSupply: TokenAmount = useTotalSupply(pair?.liquidityToken)
const noLiquidity = // used to detect new exchange
pair === null ||
(!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0)))
// get user-pecific and token-specific lookup data // txn values
const userBalances: { [field in Field]: TokenAmount } = { const [txHash, setTxHash] = useState<string>('')
[Field.INPUT]: useTokenBalanceTreatingWETHasETH(account, tokens[Field.INPUT]), const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
[Field.OUTPUT]: useTokenBalanceTreatingWETHasETH(account, tokens[Field.OUTPUT]) const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
}
// track non relational amounts if first person to add liquidity
const [nonrelationalAmounts, setNonrelationalAmounts] = useState({
[Field.INPUT]: null,
[Field.OUTPUT]: null
})
useEffect(() => {
if (typedValue !== '.' && tokens[independentField] && noLiquidity) {
const newNonRelationalAmounts = nonrelationalAmounts
if (typedValue === '') {
if (independentField === Field.OUTPUT) {
newNonRelationalAmounts[Field.OUTPUT] = null
} else {
newNonRelationalAmounts[Field.INPUT] = null
}
} else {
try {
const typedValueParsed = parseUnits(typedValue, tokens[independentField].decimals).toString()
if (independentField === Field.OUTPUT) {
newNonRelationalAmounts[Field.OUTPUT] = new TokenAmount(tokens[independentField], typedValueParsed)
} else {
newNonRelationalAmounts[Field.INPUT] = new TokenAmount(tokens[independentField], typedValueParsed)
}
} catch (error) {
console.log(error)
}
}
setNonrelationalAmounts(newNonRelationalAmounts)
}
}, [independentField, nonrelationalAmounts, tokens, typedValue, noLiquidity])
// caclulate the token amounts based on the input
const parsedAmounts: { [field: number]: TokenAmount } = {}
if (noLiquidity) {
parsedAmounts[independentField] = nonrelationalAmounts[independentField]
parsedAmounts[dependentField] = nonrelationalAmounts[dependentField]
}
if (typedValue !== '' && typedValue !== '.' && tokens[independentField]) {
try {
const typedValueParsed = parseUnits(typedValue, tokens[independentField].decimals).toString()
if (typedValueParsed !== '0')
parsedAmounts[independentField] = new TokenAmount(tokens[independentField], typedValueParsed)
} catch (error) {
console.error(error)
}
}
if (
route &&
!noLiquidity &&
parsedAmounts[independentField] &&
JSBI.greaterThan(parsedAmounts[independentField].raw, JSBI.BigInt(0))
) {
parsedAmounts[dependentField] = route.midPrice.quote(parsedAmounts[independentField])
}
// get formatted amounts // get formatted amounts
const formattedAmounts = { const formattedAmounts = {
[independentField]: typedValue, [independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField]?.toSignificant(6) : '' [dependentField]: noLiquidity ? otherTypedValue : parsedAmounts[dependentField]?.toSignificant(6) ?? ''
} }
// check whether the user has approved the router on both tokens
const inputApproval: TokenAmount = useTokenAllowance(tokens[Field.INPUT], account, ROUTER_ADDRESS)
const outputApproval: TokenAmount = useTokenAllowance(tokens[Field.OUTPUT], account, ROUTER_ADDRESS)
const inputApproved =
tokens[Field.INPUT]?.equals(WETH[chainId]) ||
(!!inputApproval &&
!!parsedAmounts[Field.INPUT] &&
JSBI.greaterThanOrEqual(inputApproval.raw, parsedAmounts[Field.INPUT].raw))
const outputApproved =
tokens[Field.OUTPUT]?.equals(WETH[chainId]) ||
(!!outputApproval &&
!!parsedAmounts[Field.OUTPUT] &&
JSBI.greaterThanOrEqual(outputApproval.raw, parsedAmounts[Field.OUTPUT].raw))
// check on pending approvals for token amounts
const pendingApprovalInput = useHasPendingApproval(tokens[Field.INPUT]?.address)
const pendingApprovalOutput = useHasPendingApproval(tokens[Field.OUTPUT]?.address)
// used for displaying approximate starting price in UI
const derivedPrice =
parsedAmounts[Field.INPUT] &&
parsedAmounts[Field.OUTPUT] &&
nonrelationalAmounts[Field.INPUT] &&
nonrelationalAmounts[Field.OUTPUT] &&
typedValue !== ''
? new Price(
parsedAmounts[Field.INPUT].token,
parsedAmounts[Field.OUTPUT].token,
parsedAmounts[Field.INPUT].raw,
parsedAmounts[Field.OUTPUT].raw
)
: null
// check for estimated liquidity minted
const liquidityMinted: TokenAmount =
!!pair &&
!!totalSupply &&
!!parsedAmounts[Field.INPUT] &&
!!parsedAmounts[Field.OUTPUT] &&
!JSBI.equal(parsedAmounts[Field.INPUT].raw, JSBI.BigInt(0)) &&
!JSBI.equal(parsedAmounts[Field.OUTPUT].raw, JSBI.BigInt(0))
? pair.getLiquidityMinted(totalSupply, parsedAmounts[Field.INPUT], parsedAmounts[Field.OUTPUT])
: undefined
const poolTokenPercentage: Percent =
!!liquidityMinted && !!totalSupply
? new Percent(liquidityMinted.raw, totalSupply.add(liquidityMinted).raw)
: undefined
const onTokenSelection = useCallback((field: Field, address: string) => {
dispatch({
type: AddAction.SELECT_TOKEN,
payload: { field, address }
})
}, [])
const onUserInput = useCallback((field: Field, typedValue: string) => {
dispatch({ type: AddAction.TYPE, payload: { field, typedValue } })
}, [])
const onMax = useCallback((typedValue: string, field) => {
dispatch({
type: AddAction.TYPE,
payload: {
field: field,
typedValue
}
})
}, [])
const MIN_ETHER: TokenAmount = new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01')))
// get the max amounts user can add // get the max amounts user can add
const [maxAmountInput, maxAmountOutput]: TokenAmount[] = [Field.INPUT, Field.OUTPUT].map(index => { const maxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce((accumulator, field) => {
const field = Field[index] return {
return !!userBalances[Field[field]] && ...accumulator,
JSBI.greaterThan( [field]:
userBalances[Field[field]].raw, !!tokenBalances[field] &&
tokens[Field[field]]?.equals(WETH[chainId]) ? MIN_ETHER.raw : JSBI.BigInt(0) !!tokens[field] &&
!!WETH[chainId] &&
tokenBalances[field].greaterThan(
new TokenAmount(tokens[field], tokens[field].equals(WETH[chainId]) ? MIN_ETH : '0')
) )
? tokens[Field[field]]?.equals(WETH[chainId]) ? tokens[field].equals(WETH[chainId])
? userBalances[Field[field]].subtract(MIN_ETHER) ? tokenBalances[field].subtract(new TokenAmount(WETH[chainId], MIN_ETH))
: userBalances[Field[field]] : tokenBalances[field]
: undefined
})
const [atMaxAmountInput, atMaxAmountOutput]: boolean[] = [Field.INPUT, Field.OUTPUT].map(index => {
const field = Field[index]
const maxAmount = index === Field.INPUT ? maxAmountInput : maxAmountOutput
return !!maxAmount && !!parsedAmounts[Field[field]]
? JSBI.equal(maxAmount.raw, parsedAmounts[Field[field]].raw)
: undefined : undefined
})
// errors
const [generalError, setGeneralError] = useState('')
const [inputError, setInputError] = useState('')
const [outputError, setOutputError] = useState('')
const [isValid, setIsValid] = useState(false)
useEffect(() => {
// reset errors
setGeneralError(null)
setInputError(null)
setOutputError(null)
setIsValid(true)
if (!account) {
setGeneralError('Connect Wallet')
setIsValid(false)
} }
}, {})
if (noLiquidity && parsedAmounts[Field.INPUT] && JSBI.equal(parsedAmounts[Field.INPUT].raw, JSBI.BigInt(0))) { const atMaxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce(
setGeneralError('Enter an amount') (accumulator, field) => {
setIsValid(false) return {
...accumulator,
[field]: maxAmounts[field] && parsedAmounts[field] ? maxAmounts[field].equalTo(parsedAmounts[field]) : undefined
} }
},
{}
)
if (noLiquidity && parsedAmounts[Field.OUTPUT] && JSBI.equal(parsedAmounts[Field.OUTPUT].raw, JSBI.BigInt(0))) { // check whether the user has approved the router on the tokens
setGeneralError('Enter an amount') const [approvalA, approveACallback] = useApproveCallback(parsedAmounts[Field.TOKEN_A], ROUTER_ADDRESS)
setIsValid(false) const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.TOKEN_B], ROUTER_ADDRESS)
}
if (!parsedAmounts[Field.INPUT]) {
setGeneralError('Enter an amount')
setIsValid(false)
}
if (!parsedAmounts[Field.OUTPUT]) {
setGeneralError('Enter an amount')
setIsValid(false)
}
if (
parsedAmounts?.[Field.INPUT] &&
userBalances?.[Field.INPUT] &&
JSBI.greaterThan(parsedAmounts?.[Field.INPUT]?.raw, userBalances?.[Field.INPUT]?.raw)
) {
setInputError('Insufficient ' + tokens[Field.INPUT]?.symbol + ' balance')
setIsValid(false)
}
if (
parsedAmounts?.[Field.OUTPUT] &&
userBalances?.[Field.OUTPUT] &&
JSBI.greaterThan(parsedAmounts?.[Field.OUTPUT]?.raw, userBalances?.[Field.OUTPUT]?.raw)
) {
setOutputError('Insufficient ' + tokens[Field.OUTPUT]?.symbol + ' balance')
setIsValid(false)
}
}, [noLiquidity, parsedAmounts, tokens, userBalances, account])
// state for txn
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
const [txHash, setTxHash] = useState<string>('')
async function onAdd() { async function onAdd() {
setAttemptingTxn(true) setAttemptingTxn(true)
const router = getRouterContract(chainId, library, account)
const minInput = calculateSlippageAmount(parsedAmounts[Field.INPUT], ALLOWED_SLIPPAGE)[0] const router = getRouterContract(chainId, library, account)
const minOutput = calculateSlippageAmount(parsedAmounts[Field.OUTPUT], ALLOWED_SLIPPAGE)[0]
const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW const amountsMin = {
[Field.TOKEN_A]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_A], noLiquidity ? 0 : allowedSlippage)[0],
[Field.TOKEN_B]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_B], noLiquidity ? 0 : allowedSlippage)[0]
}
let method, estimate, args, value const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
// one of the tokens is ETH let estimate, method: Function, args: Array<string | string[] | number>, value: BigNumber | null
if (tokens[Field.INPUT].equals(WETH[chainId]) || tokens[Field.OUTPUT].equals(WETH[chainId])) { if (tokens[Field.TOKEN_A].equals(WETH[chainId]) || tokens[Field.TOKEN_B].equals(WETH[chainId])) {
method = router.addLiquidityETH const tokenBIsETH = tokens[Field.TOKEN_B].equals(WETH[chainId])
estimate = router.estimateGas.addLiquidityETH estimate = router.estimateGas.addLiquidityETH
method = router.addLiquidityETH
const outputIsETH = tokens[Field.OUTPUT].equals(WETH[chainId])
args = [ args = [
tokens[outputIsETH ? Field.INPUT : Field.OUTPUT].address, // token tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address, // token
parsedAmounts[outputIsETH ? Field.INPUT : Field.OUTPUT].raw.toString(), // token desired parsedAmounts[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].raw.toString(), // token desired
outputIsETH ? minInput.toString() : minOutput.toString(), // token min amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(), // token min
outputIsETH ? minOutput.toString() : minInput.toString(), // eth min amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(), // eth min
account, account,
deadline deadlineFromNow
] ]
value = BigNumber.from(parsedAmounts[outputIsETH ? Field.OUTPUT : Field.INPUT].raw.toString()) value = BigNumber.from(parsedAmounts[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].raw.toString())
} else { } else {
method = router.addLiquidity
estimate = router.estimateGas.addLiquidity estimate = router.estimateGas.addLiquidity
method = router.addLiquidity
args = [ args = [
tokens[Field.INPUT].address, tokens[Field.TOKEN_A].address,
tokens[Field.OUTPUT].address, tokens[Field.TOKEN_B].address,
parsedAmounts[Field.INPUT].raw.toString(), parsedAmounts[Field.TOKEN_A].raw.toString(),
parsedAmounts[Field.OUTPUT].raw.toString(), parsedAmounts[Field.TOKEN_B].raw.toString(),
noLiquidity ? parsedAmounts[Field.INPUT].raw.toString() : minInput.toString(), amountsMin[Field.TOKEN_A].toString(),
noLiquidity ? parsedAmounts[Field.OUTPUT].raw.toString() : minOutput.toString(), amountsMin[Field.TOKEN_B].toString(),
account, account,
deadline deadlineFromNow
] ]
value = null value = null
} }
...@@ -443,24 +161,26 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< ...@@ -443,24 +161,26 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
...(value ? { value } : {}), ...(value ? { value } : {}),
gasLimit: calculateGasMargin(estimatedGasLimit) gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => { }).then(response => {
ReactGA.event({
category: 'Liquidity',
action: 'Add',
label: [tokens[Field.INPUT]?.symbol, tokens[Field.OUTPUT]?.symbol].join('/')
})
setTxHash(response.hash)
addTransaction(response, { addTransaction(response, {
summary: summary:
'Add ' + 'Add ' +
parsedAmounts[Field.INPUT]?.toSignificant(3) + parsedAmounts[Field.TOKEN_A]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.INPUT]?.symbol + tokens[Field.TOKEN_A]?.symbol +
' and ' + ' and ' +
parsedAmounts[Field.OUTPUT]?.toSignificant(3) + parsedAmounts[Field.TOKEN_B]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.OUTPUT]?.symbol tokens[Field.TOKEN_B]?.symbol
}) })
setTxHash(response.hash)
setPendingConfirmation(false) setPendingConfirmation(false)
ReactGA.event({
category: 'Liquidity',
action: 'Add',
label: [tokens[Field.TOKEN_A]?.symbol, tokens[Field.TOKEN_B]?.symbol].join('/')
})
}) })
) )
.catch((e: Error) => { .catch((e: Error) => {
...@@ -468,64 +188,20 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< ...@@ -468,64 +188,20 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
setPendingConfirmation(true) setPendingConfirmation(true)
setAttemptingTxn(false) setAttemptingTxn(false)
setShowConfirm(false) setShowConfirm(false)
}) setShowAdvanced(false)
}
async function approveAmount(field) {
let useUserBalance = false
const tokenContract = field === Field.INPUT ? tokenContractInput : tokenContractOutput
const estimatedGas = await tokenContract.estimateGas.approve(ROUTER_ADDRESS, MaxUint256).catch(() => {
// general fallback for tokens who restrict approval amounts
useUserBalance = true
return tokenContract.estimateGas.approve(ROUTER_ADDRESS, userBalances[field])
})
tokenContract
.approve(ROUTER_ADDRESS, useUserBalance ? userBalances[field] : MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas)
})
.then(response => {
addTransaction(response, {
summary: 'Approve ' + tokens[field]?.symbol,
approvalOfToken: tokens[field].address
})
}) })
} }
const modalHeader = () => { const modalHeader = () => {
return noLiquidity ? ( return noLiquidity ? (
<AutoColumn gap="12px"> <AutoColumn gap="20px">
<LightCard margin={'30px 0'} borderRadius="20px"> <LightCard mt="20px" borderRadius="20px">
<ColumnCenter> <RowFlat>
<RowFixed> <Text fontSize="48px" fontWeight={500} lineHeight="42px" marginRight={10}>
<Text fontSize={36} fontWeight={500} marginRight={20}> {tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol}
{tokens[Field.INPUT]?.symbol + '-' + tokens[Field.OUTPUT]?.symbol} </Text>
</Text>{' '} <DoubleLogo a0={tokens[Field.TOKEN_A]?.address} a1={tokens[Field.TOKEN_B]?.address} size={30} />
<DoubleLogo a0={tokens[Field.INPUT]?.address} a1={tokens[Field.OUTPUT]?.address} size={36} /> </RowFlat>
</RowFixed>
</ColumnCenter>
</LightCard>
<TYPE.body>Starting pool prices</TYPE.body>
<LightCard borderRadius="20px">
<TYPE.mediumHeader>
{parsedAmounts[0] &&
parsedAmounts[1] &&
JSBI.greaterThan(parsedAmounts[0].raw, JSBI.BigInt(0)) &&
JSBI.greaterThan(parsedAmounts[1].raw, JSBI.BigInt(0)) &&
derivedPrice?.invert().toSignificant(6)}{' '}
{tokens[Field.INPUT]?.symbol + '/' + tokens[Field.OUTPUT]?.symbol}
</TYPE.mediumHeader>
</LightCard>
<LightCard borderRadius="20px">
<TYPE.mediumHeader>
{parsedAmounts[0] &&
parsedAmounts[1] &&
JSBI.greaterThan(parsedAmounts[0].raw, JSBI.BigInt(0)) &&
JSBI.greaterThan(parsedAmounts[1].raw, JSBI.BigInt(0)) &&
derivedPrice?.toSignificant(6)}{' '}
{tokens[Field.OUTPUT]?.symbol + '/' + tokens[Field.INPUT]?.symbol}
</TYPE.mediumHeader>
</LightCard> </LightCard>
</AutoColumn> </AutoColumn>
) : ( ) : (
...@@ -534,17 +210,16 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< ...@@ -534,17 +210,16 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
<Text fontSize="48px" fontWeight={500} lineHeight="42px" marginRight={10}> <Text fontSize="48px" fontWeight={500} lineHeight="42px" marginRight={10}>
{liquidityMinted?.toSignificant(6)} {liquidityMinted?.toSignificant(6)}
</Text> </Text>
<DoubleLogo a0={tokens[Field.INPUT]?.address} a1={tokens[Field.OUTPUT]?.address} size={30} /> <DoubleLogo a0={tokens[Field.TOKEN_A]?.address} a1={tokens[Field.TOKEN_B]?.address} size={30} />
</RowFlat> </RowFlat>
<Row> <Row>
<Text fontSize="24px"> <Text fontSize="24px">
{tokens[Field.INPUT]?.symbol + '/' + tokens[Field.OUTPUT]?.symbol + ' Pool Tokens'} {tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol + ' Pool Tokens'}
</Text> </Text>
</Row> </Row>
<TYPE.italic fontSize={12} textAlign="left" padding={'8px 0 0 0 '}> <TYPE.italic fontSize={12} textAlign="left" padding={'8px 0 0 0 '}>
{`Output is estimated. You will receive at least ${liquidityMinted?.toSignificant(6)} UNI ${ {`Output is estimated. If the price changes by more than ${allowedSlippage /
tokens[Field.INPUT]?.symbol 100}% your transaction will revert.`}
}/${tokens[Field.OUTPUT]?.symbol} or the transaction will revert.`}
</TYPE.italic> </TYPE.italic>
</AutoColumn> </AutoColumn>
) )
...@@ -554,93 +229,70 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< ...@@ -554,93 +229,70 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
return ( return (
<> <>
<RowBetween> <RowBetween>
<TYPE.body>{tokens[Field.INPUT]?.symbol} Deposited</TYPE.body> <TYPE.body>{tokens[Field.TOKEN_A]?.symbol} Deposited</TYPE.body>
<RowFixed> <RowFixed>
<TokenLogo address={tokens[Field.INPUT]?.address} style={{ marginRight: '8px' }} /> <TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '8px' }} />
<TYPE.body>{!!parsedAmounts[Field.INPUT] && parsedAmounts[Field.INPUT].toSignificant(6)}</TYPE.body> <TYPE.body>{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}</TYPE.body>
</RowFixed> </RowFixed>
</RowBetween> </RowBetween>
<RowBetween> <RowBetween>
<TYPE.body>{tokens[Field.OUTPUT]?.symbol} Deposited</TYPE.body> <TYPE.body>{tokens[Field.TOKEN_B]?.symbol} Deposited</TYPE.body>
<RowFixed> <RowFixed>
<TokenLogo address={tokens[Field.OUTPUT]?.address} style={{ marginRight: '8px' }} /> <TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '8px' }} />
<TYPE.body>{!!parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.OUTPUT].toSignificant(6)}</TYPE.body> <TYPE.body>{parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}</TYPE.body>
</RowFixed> </RowFixed>
</RowBetween> </RowBetween>
{route && !JSBI.equal(route?.midPrice?.raw?.denominator, JSBI.BigInt(0)) && (
<RowBetween> <RowBetween>
<TYPE.body>Rate</TYPE.body> <TYPE.body>Rates</TYPE.body>
<TYPE.body> <TYPE.body>
{`1 ${tokens[Field.INPUT]?.symbol} = ${route?.midPrice && {`1 ${tokens[Field.TOKEN_A]?.symbol} = ${price?.toSignificant(4)} ${tokens[Field.TOKEN_B]?.symbol}`}
route?.midPrice?.raw?.denominator && </TYPE.body>
route?.midPrice?.adjusted?.toSignificant(4)} ${tokens[Field.OUTPUT]?.symbol}`} </RowBetween>
<RowBetween style={{ justifyContent: 'flex-end' }}>
<TYPE.body>
{`1 ${tokens[Field.TOKEN_B]?.symbol} = ${price?.invert().toSignificant(4)} ${
tokens[Field.TOKEN_A]?.symbol
}`}
</TYPE.body> </TYPE.body>
</RowBetween> </RowBetween>
)}
<RowBetween> <RowBetween>
<TYPE.body>Minted Pool Share:</TYPE.body> <TYPE.body>Share of Pool:</TYPE.body>
<TYPE.body>{noLiquidity ? '100%' : poolTokenPercentage?.toSignificant(6) + '%'}</TYPE.body> <TYPE.body>{noLiquidity ? '100' : poolTokenPercentage?.toSignificant(4)}%</TYPE.body>
</RowBetween> </RowBetween>
<ButtonPrimary style={{ margin: '20px 0 0 0' }} onClick={onAdd}> <ButtonPrimary style={{ margin: '20px 0 0 0' }} onClick={onAdd}>
<Text fontWeight={500} fontSize={20}> <Text fontWeight={500} fontSize={20}>
{noLiquidity ? 'Supply & Create Pool' : 'Confirm Supply'} {noLiquidity ? 'Create Pool & Supply' : 'Confirm Supply'}
</Text> </Text>
</ButtonPrimary> </ButtonPrimary>
</> </>
) )
} }
const displayPriceInput = noLiquidity
? parsedAmounts[0] &&
parsedAmounts[1] &&
derivedPrice &&
JSBI.greaterThan(parsedAmounts[0].raw, JSBI.BigInt(0)) &&
JSBI.greaterThan(parsedAmounts[1].raw, JSBI.BigInt(0))
? derivedPrice?.toSignificant(6)
: '-'
: pair && route && tokens[Field.INPUT]
? route?.input.equals(tokens[Field.INPUT])
? route.midPrice.toSignificant(6)
: route.midPrice.invert().toSignificant(6)
: '-'
const displayPriceOutput = noLiquidity
? parsedAmounts[0] &&
parsedAmounts[1] &&
derivedPrice &&
JSBI.greaterThan(parsedAmounts[0].raw, JSBI.BigInt(0)) &&
JSBI.greaterThan(parsedAmounts[1].raw, JSBI.BigInt(0))
? derivedPrice?.invert().toSignificant(6)
: '-'
: pair && route && tokens[Field.OUTPUT]
? route?.input.equals(tokens[Field.OUTPUT])
? route.midPrice.toSignificant(6)
: route.midPrice.invert().toSignificant(6)
: '-'
const PriceBar = () => { const PriceBar = () => {
return ( return (
<AutoColumn gap="md" justify="space-between"> <AutoColumn gap="md" justify="space-between">
<AutoRow justify="space-between"> <AutoRow justify="space-between">
<AutoColumn justify="center"> <AutoColumn justify="center">
<TYPE.black>{displayPriceInput}</TYPE.black> <TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}> <Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{tokens[Field.OUTPUT]?.symbol} per {tokens[Field.INPUT]?.symbol} {tokens[Field.TOKEN_B]?.symbol} per {tokens[Field.TOKEN_A]?.symbol}
</Text> </Text>
</AutoColumn> </AutoColumn>
<AutoColumn justify="center"> <AutoColumn justify="center">
<TYPE.black>{displayPriceOutput}</TYPE.black> <TYPE.black>{price?.invert().toSignificant(6) ?? '0'}</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}> <Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{tokens[Field.INPUT]?.symbol} per {tokens[Field.OUTPUT]?.symbol} {tokens[Field.TOKEN_A]?.symbol} per {tokens[Field.TOKEN_B]?.symbol}
</Text> </Text>
</AutoColumn> </AutoColumn>
<AutoColumn justify="center"> <AutoColumn justify="center">
<TYPE.black> <TYPE.black>
{noLiquidity && derivedPrice ? '100' : poolTokenPercentage?.toSignificant(4) ?? '0'} {noLiquidity && price
{'%'} ? '100'
: (poolTokenPercentage?.lessThan(ONE_BIPS) ? '<0.01' : poolTokenPercentage?.toFixed(2)) ?? '0'}
%
</TYPE.black> </TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}> <Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
Pool Share Share of Pool
</Text> </Text>
</AutoColumn> </AutoColumn>
</AutoRow> </AutoRow>
...@@ -648,11 +300,12 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< ...@@ -648,11 +300,12 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
) )
} }
const pendingText = `Supplying ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${ const pendingText = `Supplying ${parsedAmounts[Field.TOKEN_A]?.toSignificant(6)} ${
tokens[Field.INPUT]?.symbol tokens[Field.TOKEN_A]?.symbol
} ${'and'} ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}` } and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}`
return ( return (
<>
<AppBody> <AppBody>
<Wrapper> <Wrapper>
<ConfirmationModal <ConfirmationModal
...@@ -670,12 +323,6 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< ...@@ -670,12 +323,6 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
pendingText={pendingText} pendingText={pendingText}
title={noLiquidity ? 'You are creating a pool' : 'You will receive'} title={noLiquidity ? 'You are creating a pool' : 'You will receive'}
/> />
<SearchModal
isOpen={showSearch}
onDismiss={() => {
setShowSearch(false)
}}
/>
<AutoColumn gap="20px"> <AutoColumn gap="20px">
{noLiquidity && ( {noLiquidity && (
<ColumnCenter> <ColumnCenter>
...@@ -695,36 +342,36 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< ...@@ -695,36 +342,36 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
</ColumnCenter> </ColumnCenter>
)} )}
<CurrencyInputPanel <CurrencyInputPanel
field={Field.INPUT} disableTokenSelect={true}
value={formattedAmounts[Field.INPUT]} field={Field.TOKEN_A}
value={formattedAmounts[Field.TOKEN_A]}
onUserInput={onUserInput} onUserInput={onUserInput}
onMax={() => { onMax={() => {
maxAmountInput && onMax(maxAmountInput.toExact(), Field.INPUT) maxAmounts[Field.TOKEN_A] && onUserInput(Field.TOKEN_A, maxAmounts[Field.TOKEN_A].toExact())
}} }}
showMaxButton={!atMaxAmountInput} showMaxButton={!atMaxAmounts[Field.TOKEN_A]}
token={tokens[Field.INPUT]} token={tokens[Field.TOKEN_A]}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
pair={pair} pair={pair}
label="Input" label="Input"
id="add-liquidity-input-token0" id="add-liquidity-input-tokena"
/> />
<ColumnCenter> <ColumnCenter>
<Plus size="16" color={theme.text2} /> <Plus size="16" color={theme.text2} />
</ColumnCenter> </ColumnCenter>
<CurrencyInputPanel <CurrencyInputPanel
field={Field.OUTPUT} disableTokenSelect={true}
value={formattedAmounts[Field.OUTPUT]} field={Field.TOKEN_B}
value={formattedAmounts[Field.TOKEN_B]}
onUserInput={onUserInput} onUserInput={onUserInput}
onMax={() => { onMax={() => {
maxAmountOutput && onMax(maxAmountOutput?.toExact(), Field.OUTPUT) maxAmounts[Field.TOKEN_B] && onUserInput(Field.TOKEN_B, maxAmounts[Field.TOKEN_B].toExact())
}} }}
showMaxButton={!atMaxAmountOutput} showMaxButton={!atMaxAmounts[Field.TOKEN_B]}
token={tokens[Field.OUTPUT]} token={tokens[Field.TOKEN_B]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
pair={pair} pair={pair}
id="add-liquidity-input-token1" id="add-liquidity-input-tokenb"
/> />
{tokens[Field.OUTPUT] && tokens[Field.INPUT] && ( {tokens[Field.TOKEN_A] && tokens[Field.TOKEN_B] && (
<> <>
<GreyCard padding="0px" borderRadius={'20px'}> <GreyCard padding="0px" borderRadius={'20px'}>
<RowBetween padding="1rem"> <RowBetween padding="1rem">
...@@ -738,61 +385,58 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< ...@@ -738,61 +385,58 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
</GreyCard> </GreyCard>
</> </>
)} )}
{isValid ? (
!inputApproved ? ( {!account ? (
<ButtonLight <ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
onClick={() => { ) : approvalA === ApprovalState.NOT_APPROVED || approvalA === ApprovalState.PENDING ? (
approveAmount(Field.INPUT) <ButtonLight onClick={approveACallback} disabled={approvalA === ApprovalState.PENDING}>
}} {approvalA === ApprovalState.PENDING ? (
disabled={pendingApprovalInput} <Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
>
{pendingApprovalInput ? (
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
) : ( ) : (
'Approve ' + tokens[Field.INPUT]?.symbol 'Approve ' + tokens[Field.TOKEN_A]?.symbol
)} )}
</ButtonLight> </ButtonLight>
) : !outputApproved ? ( ) : approvalB === ApprovalState.NOT_APPROVED || approvalB === ApprovalState.PENDING ? (
<ButtonLight <ButtonLight onClick={approveBCallback} disabled={approvalB === ApprovalState.PENDING}>
onClick={() => { {approvalB === ApprovalState.PENDING ? (
approveAmount(Field.OUTPUT) <Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
}}
disabled={pendingApprovalOutput}
>
{pendingApprovalOutput ? (
<Dots>Approving {tokens[Field.OUTPUT]?.symbol}</Dots>
) : ( ) : (
'Approve ' + tokens[Field.OUTPUT]?.symbol 'Approve ' + tokens[Field.TOKEN_B]?.symbol
)} )}
</ButtonLight> </ButtonLight>
) : ( ) : (
<ButtonPrimary <ButtonError
onClick={() => { onClick={() => {
setShowConfirm(true) setShowConfirm(true)
}} }}
disabled={!isValid}
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
> >
<Text fontSize={20} fontWeight={500}> <Text fontSize={20} fontWeight={500}>
Supply {error ?? 'Supply'}
</Text>
</ButtonPrimary>
)
) : (
<ButtonPrimary disabled={true}>
<Text fontSize={20} fontWeight={500}>
{generalError ? generalError : inputError ? inputError : outputError ? outputError : 'Supply'}
</Text> </Text>
</ButtonPrimary> </ButtonError>
)} )}
</AutoColumn> </AutoColumn>
</Wrapper>
</AppBody>
{isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B] ? (
<AdvancedSwapDetailsDropdown
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
) : null}
{!noLiquidity && ( {pair && !noLiquidity ? (
<FixedBottom> <AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<AutoColumn>
<PositionCard pair={pair} minimal={true} /> <PositionCard pair={pair} minimal={true} />
</AutoColumn> </AutoColumn>
</FixedBottom> ) : null}
)} </>
</Wrapper>
</AppBody>
) )
} }
...@@ -67,6 +67,10 @@ const BackgroundGradient = styled.div` ...@@ -67,6 +67,10 @@ const BackgroundGradient = styled.div`
} }
` `
const Marginer = styled.div`
margin-top: 5rem;
`
let Router: React.ComponentType let Router: React.ComponentType
if (process.env.PUBLIC_URL === '.') { if (process.env.PUBLIC_URL === '.') {
Router = HashRouter Router = HashRouter
...@@ -99,6 +103,7 @@ export default function App() { ...@@ -99,6 +103,7 @@ export default function App() {
<Route component={RedirectPathToSwapOnly} /> <Route component={RedirectPathToSwapOnly} />
</Switch> </Switch>
</Web3ReactManager> </Web3ReactManager>
<Marginer />
<Footer /> <Footer />
</BodyWrapper> </BodyWrapper>
<BackgroundGradient /> <BackgroundGradient />
......
...@@ -2,17 +2,15 @@ import React from 'react' ...@@ -2,17 +2,15 @@ import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import NavigationTabs from '../components/NavigationTabs' import NavigationTabs from '../components/NavigationTabs'
export const Body = styled.div` const Body = styled.div`
position: relative;
max-width: 420px; max-width: 420px;
width: 100%; width: 100%;
/* min-height: 340px; */
background: ${({ theme }) => theme.bg1}; 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), 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); 0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 30px; border-radius: 30px;
padding: 1rem; padding: 1rem;
position: relative;
margin-bottom: 10rem;
` `
/** /**
......
...@@ -30,14 +30,20 @@ import { useSendCallback } from '../../hooks/useSendCallback' ...@@ -30,14 +30,20 @@ import { useSendCallback } from '../../hooks/useSendCallback'
import { useSwapCallback } from '../../hooks/useSwapCallback' import { useSwapCallback } from '../../hooks/useSwapCallback'
import { useWalletModalToggle } from '../../state/application/hooks' import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/swap/actions' 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 { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { CursorPointer, TYPE } from '../../theme' import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices' import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody' import AppBody from '../AppBody'
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
export default function Send({ location: { search } }: RouteComponentProps) { export default function Send({ location: { search } }: RouteComponentProps) {
useDefaultsFromURL(search) useDefaultsFromURLSearch(search)
// text translation // text translation
// const { t } = useTranslation() // const { t } = useTranslation()
...@@ -173,7 +179,7 @@ export default function Send({ location: { search } }: RouteComponentProps) { ...@@ -173,7 +179,7 @@ export default function Send({ location: { search } }: RouteComponentProps) {
const [showInverted, setShowInverted] = useState<boolean>(false) const [showInverted, setShowInverted] = useState<boolean>(false)
// warnings on slippage // warnings on slippage
const severity = !sendingWithSwap ? 0 : warningServerity(priceImpactWithoutFee) const severity = !sendingWithSwap ? 0 : warningSeverity(priceImpactWithoutFee)
function modalHeader() { function modalHeader() {
if (!sendingWithSwap) { if (!sendingWithSwap) {
...@@ -492,6 +498,9 @@ export default function Send({ location: { search } }: RouteComponentProps) { ...@@ -492,6 +498,9 @@ export default function Send({ location: { search } }: RouteComponentProps) {
)} )}
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} /> <V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
</BottomGrouping> </BottomGrouping>
</Wrapper>
</AppBody>
{bestTrade && ( {bestTrade && (
<AdvancedSwapDetailsDropdown <AdvancedSwapDetailsDropdown
trade={bestTrade} trade={bestTrade}
...@@ -499,13 +508,16 @@ export default function Send({ location: { search } }: RouteComponentProps) { ...@@ -499,13 +508,16 @@ export default function Send({ location: { search } }: RouteComponentProps) {
deadline={deadline} deadline={deadline}
showAdvanced={showAdvanced} showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced} setShowAdvanced={setShowAdvanced}
priceImpactWithoutFee={priceImpactWithoutFee}
setDeadline={setDeadline} setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage} 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 ...@@ -27,23 +27,30 @@ import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useAppro
import { useSwapCallback } from '../../hooks/useSwapCallback' import { useSwapCallback } from '../../hooks/useSwapCallback'
import { useWalletModalToggle } from '../../state/application/hooks' import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/swap/actions' 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 { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices' import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody' import AppBody from '../AppBody'
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
export default function Swap({ location: { search } }: RouteComponentProps) { export default function Swap({ location: { search } }: RouteComponentProps) {
useDefaultsFromURL(search) useDefaultsFromURLSearch(search)
// text translation
// const { t } = useTranslation()
const { chainId, account } = useActiveWeb3React() const { chainId, account } = useActiveWeb3React()
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
// toggle wallet when disconnected // toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle() const toggleWalletModal = useWalletModalToggle()
// swap state
const { independentField, typedValue } = useSwapState() const { independentField, typedValue } = useSwapState()
const { bestTrade, tokenBalances, parsedAmounts, tokens, error, v1TradeLinkIfBetter } = useDerivedSwapInfo() const { bestTrade, tokenBalances, parsedAmounts, tokens, error, v1TradeLinkIfBetter } = useDerivedSwapInfo()
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
const isValid = !error const isValid = !error
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
...@@ -58,6 +65,11 @@ export default function Swap({ location: { search } }: RouteComponentProps) { ...@@ -58,6 +65,11 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW) const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE) const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
}
const route = bestTrade?.route const route = bestTrade?.route
const userHasSpecifiedInputOutput = const userHasSpecifiedInputOutput =
!!tokens[Field.INPUT] && !!tokens[Field.INPUT] &&
...@@ -69,13 +81,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) { ...@@ -69,13 +81,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
// check whether the user has approved the router on the input token // check whether the user has approved the router on the input token
const [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage) 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 = const maxAmountInput: TokenAmount =
!!tokenBalances[Field.INPUT] && !!tokenBalances[Field.INPUT] &&
!!tokens[Field.INPUT] && !!tokens[Field.INPUT] &&
...@@ -88,7 +93,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) { ...@@ -88,7 +93,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
: tokenBalances[Field.INPUT] : tokenBalances[Field.INPUT]
: undefined : undefined
const atMaxAmountInput: boolean = 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) const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
...@@ -130,7 +135,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) { ...@@ -130,7 +135,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
const [showInverted, setShowInverted] = useState<boolean>(false) const [showInverted, setShowInverted] = useState<boolean>(false)
// warnings on slippage // warnings on slippage
const priceImpactSeverity = warningServerity(priceImpactWithoutFee) const priceImpactSeverity = warningSeverity(priceImpactWithoutFee)
function modalHeader() { function modalHeader() {
return ( return (
...@@ -259,13 +264,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) { ...@@ -259,13 +264,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
</AutoColumn> </AutoColumn>
<BottomGrouping> <BottomGrouping>
{!account ? ( {!account ? (
<ButtonLight <ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
onClick={() => {
toggleWalletModal()
}}
>
Connect Wallet
</ButtonLight>
) : noRoute && userHasSpecifiedInputOutput ? ( ) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}> <GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main> <TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
...@@ -294,6 +293,9 @@ export default function Swap({ location: { search } }: RouteComponentProps) { ...@@ -294,6 +293,9 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
)} )}
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} /> <V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
</BottomGrouping> </BottomGrouping>
</Wrapper>
</AppBody>
{bestTrade && ( {bestTrade && (
<AdvancedSwapDetailsDropdown <AdvancedSwapDetailsDropdown
trade={bestTrade} trade={bestTrade}
...@@ -301,13 +303,16 @@ export default function Swap({ location: { search } }: RouteComponentProps) { ...@@ -301,13 +303,16 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
deadline={deadline} deadline={deadline}
showAdvanced={showAdvanced} showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced} setShowAdvanced={setShowAdvanced}
priceImpactWithoutFee={priceImpactWithoutFee}
setDeadline={setDeadline} setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage} setRawSlippage={setAllowedSlippage}
/> />
)} )}
</Wrapper>
</AppBody> {priceImpactWithoutFee && priceImpactSeverity > 2 && (
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
</AutoColumn>
)}
</> </>
) )
} }
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit' import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import { save, load } from 'redux-localstorage-simple'
import application from './application/reducer' import application from './application/reducer'
import { updateVersion } from './user/actions'
import user from './user/reducer' import user from './user/reducer'
import transactions from './transactions/reducer'
import wallet from './wallet/reducer' import wallet from './wallet/reducer'
import swap from './swap/reducer' import swap from './swap/reducer'
import transactions from './transactions/reducer' import mint from './mint/reducer'
import { save, load } from 'redux-localstorage-simple'
import { updateVersion } from './user/actions'
const PERSISTED_KEYS: string[] = ['user', 'transactions'] const PERSISTED_KEYS: string[] = ['user', 'transactions']
...@@ -15,7 +18,8 @@ const store = configureStore({ ...@@ -15,7 +18,8 @@ const store = configureStore({
user, user,
transactions, transactions,
wallet, wallet,
swap swap,
mint
}, },
middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })], middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })],
preloadedState: load({ 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 { ...@@ -5,7 +5,7 @@ export enum Field {
OUTPUT = 'OUTPUT' 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 selectToken = createAction<{ field: Field; address: string }>('selectToken')
export const switchTokens = createAction<void>('switchTokens') export const switchTokens = createAction<void>('switchTokens')
export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInput') export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInput')
...@@ -7,7 +7,7 @@ import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens' ...@@ -7,7 +7,7 @@ import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useTradeExactIn, useTradeExactOut } from '../../hooks/Trades' import { useTradeExactIn, useTradeExactOut } from '../../hooks/Trades'
import { AppDispatch, AppState } from '../index' import { AppDispatch, AppState } from '../index'
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks' 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 { useV1TradeLinkIfBetter } from '../../data/V1'
import { V1_TRADE_LINK_THRESHOLD } from '../../constants' import { V1_TRADE_LINK_THRESHOLD } from '../../constants'
...@@ -52,7 +52,7 @@ export function useSwapActionHandlers(): { ...@@ -52,7 +52,7 @@ export function useSwapActionHandlers(): {
} }
// try to parse a user entered amount for a given token // 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) { if (!value || !token) {
return return
} }
...@@ -155,11 +155,11 @@ export function useDerivedSwapInfo(): { ...@@ -155,11 +155,11 @@ export function useDerivedSwapInfo(): {
// updates the swap state to use the defaults for a given network whenever the query // updates the swap state to use the defaults for a given network whenever the query
// string updates // string updates
export function useDefaultsFromURL(search?: string) { export function useDefaultsFromURLSearch(search?: string) {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const dispatch = useDispatch<AppDispatch>() const dispatch = useDispatch<AppDispatch>()
useEffect(() => { useEffect(() => {
if (!chainId) return if (!chainId) return
dispatch(setDefaultsFromURL({ chainId, queryString: search })) dispatch(setDefaultsFromURLSearch({ chainId, queryString: search }))
}, [dispatch, search, chainId]) }, [dispatch, search, chainId])
} }
import { ChainId, WETH } from '@uniswap/sdk' import { ChainId, WETH } from '@uniswap/sdk'
import { createStore, Store } from 'redux' import { createStore, Store } from 'redux'
import { Field, setDefaultsFromURL } from './actions' import { Field, setDefaultsFromURLSearch } from './actions'
import reducer, { SwapState } from './reducer' import reducer, { SwapState } from './reducer'
describe('swap reducer', () => { describe('swap reducer', () => {
...@@ -18,7 +18,7 @@ describe('swap reducer', () => { ...@@ -18,7 +18,7 @@ describe('swap reducer', () => {
describe('setDefaultsFromURL', () => { describe('setDefaultsFromURL', () => {
test('ETH to DAI', () => { test('ETH to DAI', () => {
store.dispatch( store.dispatch(
setDefaultsFromURL({ setDefaultsFromURLSearch({
chainId: ChainId.MAINNET, chainId: ChainId.MAINNET,
queryString: queryString:
'?inputCurrency=ETH&outputCurrency=0x6b175474e89094c44da98b954eedeac495271d0f&exactAmount=20.5&exactField=outPUT' '?inputCurrency=ETH&outputCurrency=0x6b175474e89094c44da98b954eedeac495271d0f&exactAmount=20.5&exactField=outPUT'
...@@ -35,7 +35,7 @@ describe('swap reducer', () => { ...@@ -35,7 +35,7 @@ describe('swap reducer', () => {
test('does not duplicate eth for invalid output token', () => { test('does not duplicate eth for invalid output token', () => {
store.dispatch( store.dispatch(
setDefaultsFromURL({ setDefaultsFromURLSearch({
chainId: ChainId.MAINNET, chainId: ChainId.MAINNET,
queryString: '?outputCurrency=invalid' queryString: '?outputCurrency=invalid'
}) })
...@@ -51,7 +51,7 @@ describe('swap reducer', () => { ...@@ -51,7 +51,7 @@ describe('swap reducer', () => {
test('output ETH only', () => { test('output ETH only', () => {
store.dispatch( store.dispatch(
setDefaultsFromURL({ setDefaultsFromURLSearch({
chainId: ChainId.MAINNET, chainId: ChainId.MAINNET,
queryString: '?outputCurrency=eth&exactAmount=20.5' queryString: '?outputCurrency=eth&exactAmount=20.5'
}) })
......
...@@ -2,7 +2,7 @@ import { parse } from 'qs' ...@@ -2,7 +2,7 @@ import { parse } from 'qs'
import { createReducer } from '@reduxjs/toolkit' import { createReducer } from '@reduxjs/toolkit'
import { ChainId, WETH } from '@uniswap/sdk' import { ChainId, WETH } from '@uniswap/sdk'
import { isAddress } from '../../utils' import { isAddress } from '../../utils'
import { Field, selectToken, setDefaultsFromURL, switchTokens, typeInput } from './actions' import { Field, selectToken, setDefaultsFromURLSearch, switchTokens, typeInput } from './actions'
export interface SwapState { export interface SwapState {
readonly independentField: Field readonly independentField: Field
...@@ -47,7 +47,7 @@ function parseIndependentFieldURLParameter(urlParam: any): Field { ...@@ -47,7 +47,7 @@ function parseIndependentFieldURLParameter(urlParam: any): Field {
export default createReducer<SwapState>(initialState, builder => export default createReducer<SwapState>(initialState, builder =>
builder builder
.addCase(setDefaultsFromURL, (state, { payload: { queryString, chainId } }) => { .addCase(setDefaultsFromURLSearch, (_, { payload: { queryString, chainId } }) => {
if (queryString && queryString.length > 1) { if (queryString && queryString.length > 1) {
const parsedQs = parse(queryString, { parseArrays: false, ignoreQueryPrefix: true }) const parsedQs = parse(queryString, { parseArrays: false, ignoreQueryPrefix: true })
......
...@@ -51,7 +51,7 @@ export function computeSlippageAdjustedAmounts( ...@@ -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_HIGH)) return 3
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_MEDIUM)) return 2 if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_MEDIUM)) return 2
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_LOW)) return 1 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